我想分享一个从 Java 8、Tomcat 8 和 Spring Boot 2 升级到 Java 17、Tomcat 10 和 Spring Boot 3 后开始发生的问题。
问题是现在我们无法再从类路径加载某些字体文件。
我们在
fonts
中有一个 src/main/resources
文件夹。当我们编译项目时,该文件夹被复制到WEB-INF/classes
,这是Tomcat的正确文件夹。
查看 Tomcat 迁移指南 我看不到任何特定于资源更改的信息来解释这一点。
部署就像以前一样完成:我们将项目编译为
.war
文件,然后将其保存到 Tomcat webapps
文件夹中。因此,在该文件夹中,我们有 WEB-INF/classes
文件夹,其中包含将加载到类路径的所有应用程序资源,包括 fonts
文件夹。
为了加载资源,我们使用Spring的
ResouceLoader
。示例:
String fontPath = resourceLoader.getResource(“classpath:fonts/font.ttf”).getFile().getPath();
ResouceLoader是Spring通过依赖注入提供的:
private final ResourceLoader resourceLoader;
所以,更新之前一切正常。因此,我查看了 Tomcat 10 文档,看看我们放置资源的方式是否发生了变化,看起来还是一样;也就是说,
WEB-INF/classes
下的文件应该可用。根据文档:
WebappX — A class loader is created for each web application that is deployed in a single Tomcat instance. All unpacked classes and resources in the /WEB-INF/classes directory of your web application… are made visible to this web application…
我创建了一个示例应用程序来演示该问题:tomcat10-test
在其中,我们有类 AppStartupRunner 演示了该问题。
在第 16 行,我们实现了
ApplicationRunner
接口,因此此代码将在启动期间运行。
在第 28 行,我们声明了一个 Runnable
,它尝试加载资源:
Runnable runnable = () -> {
try {
String fontPath = resourceLoader.getResource("classpath:fonts/font.ttf").getFile().getPath();
logger.info("fontPath: {}", fontPath);
} catch (IOException e) {
logger.error("error load font thread", e);
throw new RuntimeException(e);
}
};
在第 40 行,我们使用
Thread
: 运行它
new Thread(runnable).start(); // this works
在第 44 行,我们使用
ExecutorService
: 运行它
executorService.submit(runnable).get(); // this works
在第 51 行,我们使用
ForkJoinPool
: 运行它
THREAD_POOL.submit(runnable).get(); // Throws FileNotFoundException exception when running with Tomcat
如果我直接从 IDE 运行此代码,它就可以工作。 如果我通过命令生成
.war
文件:
./gradlew clean build -x test
然后将文件部署到Tomcat 10,不起作用。
总结一下:
Thread
内部 ForkJoinPool
加载资源以及使用 Tomcat 运行时,才会发生该错误。所以,我不知道发生了什么变化,现在 Tomcat 在从
classpath
运行线程时不再能够从 ForkJoinPool
加载资源。
我尝试过使用一些不同的方法来加载资源: 使用 Spring 的
ResourceUtils
类,也使用 getClass().getClassLoader()
,但我遇到了同样的问题。
预期结果是当应用程序部署到 Tomcat 10 并且线程在
classpath
内运行时能够加载 ForkJoinPool
资源。
如果您尝试不显式声明可运行变量。由于字体加载逻辑在 lambda 函数中是独立的,我们可以简单地将其直接传递给线程,如下所示:
@Override
public void run(ApplicationArguments args) {
try {
String fontPath = resourceLoader.getResource("classpath:fonts/font.ttf").getFile().getPath();
logger.info("fontPath: {}", fontPath);
logger.info("---> Load font using thread");
new Thread(() -> loadFont(fontPath)).start();
Thread.sleep(2000);
logger.info("---> Load font using executor service");
executorService.submit(() -> loadFont(fontPath));
logger.info("---> Load font using fork join pool");
THREAD_POOL.submit(() -> loadFont(fontPath));
} catch (InterruptedException | IOException e) {
logger.error("Error loading font", e);
} finally {
executorService.shutdown();
THREAD_POOL.shutdown();
}
}
private void loadFont(String fontPath) {
try {
logger.info("Loading font: {}", fontPath);
} catch (Exception e) {
logger.error("Error loading font", e);
}
}