I want to share an issue that started to happen after upgrading from Java 8, Tomcat 8, and Spring Boot 2 to Java 17, Tomcat 10, and Spring Boot 3.
The issue is that now we can no longer load some font files from the classpath.
We have a fonts
folder inside src/main/resources
. When we compile the project, the folder is copied to WEB-INF/classes
, which is the correct folder for Tomcat.
Looking at the Tomcat Migration Guides I could not see anything specific to the resources change that would explain this.
The deployment is done like the old days: we compile the project to a .war
file and then save it to the Tomcat webapps
folder. So, inside that folder, we have the WEB-INF/classes
folder, which contains all the app resources that will be loaded to the classpath, including the fonts
folder.
To load the resources, we use Spring's ResouceLoader
. Example:
String fontPath = resourceLoader.getResource(“classpath:fonts/font.ttf”).getFile().getPath();
The ResouceLoader is provided by Spring through dependency injection:
private final ResourceLoader resourceLoader;
So, that was working fine before the update. So, I have looked at Tomcat 10 docs to see if the way we place the resources has changed, and it seems it remains the same; that is, files under WEB-INF/classes
should be available. Per the documentation:
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…
I created one sample application to demonstrate the issue: tomcat10-test
Inside it, we have the class AppStartupRunner that demonstrates the issue.
On line 16 we implement the ApplicationRunner
interface, so this code will run during startup.
On line 28, we declare a Runnable
, that tries to load the resource:
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);
}
};
On line 40 we run it using a Thread
:
new Thread(runnable).start(); // this works
On line 44 we run it using ExecutorService
:
executorService.submit(runnable).get(); // this works
On line 51 we run it using ForkJoinPool
:
THREAD_POOL.submit(runnable).get(); // Throws FileNotFoundException exception when running with Tomcat
If I run this code directly from my IDE, it works.
If I generate the .war
file, through the command:
./gradlew clean build -x test
and then deploy the file to Tomcat 10, it does not work.
So to summarize:
- It was working with Java 8, Tomcat 8, and Spring Boot 2.
- It no longer works after updating to Java 17, Tomcat 10, and Spring Boot 3.
- The error only happens when trying to load a resource from
Thread
insideForkJoinPool
and when running with Tomcat.
So, I do not know what changed that now Tomcat is no longer able to load resources from classpath
when running a Thread from a ForkJoinPool
.
I have tried to use some different approaches to load the resources:
Use the Spring's ResourceUtils
class and also use getClass().getClassLoader()
, but I get the same issue.
The expected result is to be able to load a classpath
resource when an application is deployed to Tomcat 10 and the Thread is running inside a ForkJoinPool
.
If you try without explicitly declaring the runnable variable. Since the font loading logic is self-contained in lambda function, we can simply pass it directly to the threads like this: