代码结构以及资源位置
测试代码
@RestController
@RequestMapping("/json")
public class JsonController {
@GetMapping("/user/1")
public String queryUserInfo() throws Exception {
// 如果使用全路径, 必须使用/开头
String path = JsonController.class.getPackage().getName().replace(".", File.separator);
// 注意第一个字符不能是File.separator, 必须是/开头,
String fileName = "/" + path + File.separator + "UserInfo.json";
URL resource = JsonController.class.getResource(fileName);
// 使用URL转换成uri可以兼容前面有/E:/idea-workspace/learning的windows盘符路径
byte[] bytes = Files.readAllBytes(Paths.get(resource.toURI()));
return new String(bytes, StandardCharsets.UTF_8);
}
@GetMapping("/user/2")
public String queryUserInfo2() throws Exception {
String path = JsonController.class.getResource("UserInfo.json").getPath();
byte[] bytes = Files.readAllBytes(Paths.get(path.substring(1)));
return new String(bytes, StandardCharsets.UTF_8);
}
@GetMapping("/user/3")
public String queryUserInfo3() throws Exception {
String path = JsonController.class.getPackage().getName().replace(".", File.separator);
// classloader不能使用/开始获取文件, 因为文件不能包含/字符
URL resource = JsonController.class.getClassLoader().getResource(path + File.separator + "UserInfo.json");
// 很显然toUri的兼容性好些
byte[] bytes = Files.readAllBytes(Paths.get(resource.toURI()));
return new String(bytes, StandardCharsets.UTF_8);
}
@GetMapping("/user/4")
public String queryUserInfo4() throws Exception {
// 使用classloader获取资源和使用class的使用/开始获取资源本质逻辑一模一样
URL resource = JsonController.class.getClassLoader().getResource("static/UserInfo.json");
// 很显然toUri的兼容性好些
byte[] bytes = Files.readAllBytes(Paths.get(resource.toURI()));
return new String(bytes, StandardCharsets.UTF_8);
}
}
本地调试一切正常, 但是上环境就报500了
{
"timestamp": "2023-02-11T14:14:07.500+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/json/user/4"
}
也可以通过 java -jar springboot-demo-0.0.1-SNAPSHOT.jar
启动也可以复现问题
说明: 查看jar包确认资源文件已经被正确的打到jar中
说明代码存在兼容性
使用org.springframework.core.io.ClassPathResource获取
// 在jar包运行之后仍然不能找到文件
@GetMapping("/user/5")
public String queryUserInfo5() {
try {
// 如果使用全路径, 必须使用/开头
String path = JsonController.class.getPackage().getName().replace(".", File.separator);
// 注意第一个字符不能是File.separator, 必须是/开头,
String fileName = "/" + path + File.separator + "UserInfo.json";
ClassPathResource pathResource = new ClassPathResource(fileName);
byte[] bytes = Files.readAllBytes(Paths.get(pathResource.getURI()));
return new String(bytes, StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
// 必须使用ClassPathResource并且使用其getInputStream()方法才能获取的到数据
@GetMapping("/user/6")
public String queryUserInfo6() {
try {
// 如果使用全路径, 必须使用/开头
String path = JsonController.class.getPackage().getName().replace(".", File.separator);
// 注意第一个字符不能是File.separator, 必须是/开头,
String fileName = "/" + path + File.separator + "UserInfo.json";
ClassPathResource pathResource = new ClassPathResource(fileName);
byte[] buf = new byte[1024];
int len = -1;
StringBuilder sb = new StringBuilder();
try (InputStream is = pathResource.getInputStream()) {
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
}
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
queryUserInfo5方法只是用了ClassPathResource来获取URI, jar运行依然不能获取到资源
ClassPathResource pathResource = new ClassPathResource(fileName);
byte[] bytes = Files.readAllBytes(Paths.get(pathResource.getURI()));
queryUserInfo6使用了ClassPathResource的getInputStream()方法, jar运行依然可以好获取到
ClassPathResource pathResource = new ClassPathResource(fileName);
...
try (InputStream is = pathResource.getInputStream()) {
...
}
}
使用spring的ClassPathResource确实解决了问题, 但是为什么会出现这样的问题?
tomcat项目和springboot项目差异
传统的tomcat项目都是提供war包, 然后war就会解压到webapps或者配置的应用目录下面, 所以我们的资源是在普通的目录里面
但是随着springboot 的出现, 问题开始有点不同了, springboot通过内嵌tomcat容器, 是的我们可以直接运行jar包, 所以此时我们要获取的资源文件是在jar包里面, 显然我们要获取资源就必须解析jar
所以我们的问题就此出现了, 如果仅仅使用传统的resouce api, 是不能获取到jar包中的文件, 就是说不会解析jar。
查看ClassPathResource源码找不同
进入
使用的类加载器是: TomcatEmbeddedWebappClassLoader
调用getResourceAsStream方法在org.apache.catalina.loader.WebappClassLoaderBase中
WebappClassLoaderBase位于tocmat-ebed-core-xxx包中
getResourceAsStream的实现
public InputStream getResourceAsStream(String name) {
...
// (1) Delegate to parent if requested
if (delegateFirst) {
stream = parent.getResourceAsStream(name);
if (stream != null) {
return stream;
}
}
// (2) Search local repositories
String path = nameToPath(name);
WebResource resource = resources.getClassLoaderResource(path);
if (resource.exists()) {
stream = resource.getInputStream();
trackLastModified(path, resource);
}
try {
if (hasExternalRepositories && stream == null) {
URL url = super.findResource(name);
if (url != null) {
stream = url.openStream();
}
}
} catch (IOException e) {
// Ignore
}
if (stream != null) {
return stream;
}
...
}
主要就是两个一个是委派/一个是搜索本地存储(还有个是无条件委派本质和委派一样)
所以不太点就是WebResourceRoot回去搜索本地存储的文件, 然后返回resource
然后这个代码就很明显了, 回去classes下搜索
@Override
public WebResource getClassLoaderResource(String path) {
return getResource("/WEB-INF/classes" + path, true, true);
}
直接使用getResourceAsStream方法也是可以获取
private static String getString(InputStream inputStream) throws IOException {
byte[] buf = new byte[1024];
int len = -1;
StringBuilder sb = new StringBuilder();
try (InputStream is = inputStream) {
while ((len = is.read(buf)) != -1) {
sb.append(new String(buf, 0, len, StandardCharsets.UTF_8));
}
}
return sb.toString();
}
@GetMapping("/user/0")
public String queryUserInfo0() throws Exception {
// 使用classloader获取资源和使用class的使用/开始获取资源本质逻辑一模一样
InputStream inputStream = JsonController.class.getClassLoader().getResourceAsStream("static/UserInfo.json");
return getString(inputStream);
}
测试发现只要使用了InputStream就都可以获取到了
@GetMapping("/user/000")
public String queryUserInfo000() throws Exception {
URL resource = JsonController.class.getResource("UserInfo.json");
return getString(resource.openStream());
}
@GetMapping("/user/00")
public String queryUserInfo00() throws Exception {
URL resource = JsonController.class.getResource("UserInfo.json");
BufferedReader reader =
new BufferedReader(new InputStreamReader(resource.openStream(), StandardCharsets.UTF_8));
String line;
StringBuilder sb = new StringBuilder();
while ((line = reader.readLine()) != null) {
sb.append(line);
}
return sb.toString();
}
@GetMapping("/user/0")
public String queryUserInfo0() throws Exception {
// 使用classloader获取资源和使用class的使用/开始获取资源本质逻辑一模一样
InputStream inputStream = JsonController.class.getClassLoader().getResourceAsStream("static/UserInfo.json");
return getString(inputStream);
}
resource.openStream()
搞半天问题其实出在这行代码上
byte[] bytes = Files.readAllBytes(Paths.get(path.substring(1)));
不具有jar包兼容性, 坑!!!
如果我们在编写获取文件的代码时候, 最好使用jar包本地测试一下, 防止出现不兼容问题