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:

  1. It was working with Java 8, Tomcat 8, and Spring Boot 2.
  2. It no longer works after updating to Java 17, Tomcat 10, and Spring Boot 3.
  3. The error only happens when trying to load a resource from Thread inside ForkJoinPool 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.

1

There are 1 best solutions below

3
On

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:

@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);
        }
    }