Search code examples
javatomcatclasspathembedded-resource

Causes of Spring ClasspathResource FileNotFoundException


My web application uses Spring 4.3.5. We need to load a Freemarker template that is stored in one of our JARs' classpath from within the jar itself.

Let me try to generalize my scenario as most as possible (I will provide code later)

Jar structure

  • src
    • /com/acme/package/ComponentUsingResource.java
  • META-INF
    • resources
      • template1.ftl.html
      • template1.ft2.html

The jar is contained in WEB-INF/lib inside the WAR application.

From ComponentUsingResource.java I use the following statement to load a resource: new ClasspathResource("META-INF/templates/template1.ftl.html").getInputStream().

The very same WAR application works on my laptop (JDK 8 121, Tomcat 8.0.43), on our SIT environment (JDK 8 >100, Tomcat 8.0.39), on a Tomcat 8.0.36 installed on my laptop on the go, but it does not work at our customer site (JDK 8 96, Tomcat 8.0.36). Doesn't work because of a FileNotFoundException when loading that ClasspathResource

When the application starts, a chain of @Autowired dependencies goes into the initialization of my resource-based bean

    Arrays.asList("ftt-mail-natixclose-it", "ftt-mail-natixflussi-it", "ftt-mail-processreport-en", "ftt-mail-processreport-it",
                  "ftt-mail-regclosed-en", "ftt-mail-regclosed-it", "ftt-mail-regnotclosed-multi-en", "ftt-mail-regnotclosed-multi-it",
                  "ftt-mail-regnotclosed-single-en", "ftt-mail-regnotclosed-single-it", "ftt-mail-timestamps-en", "ftt-mail-timestamps-it",
                  "ftt-mail-missingdata-multi-en", "ftt-mail-missingdata-multi-it", "ftt-mail-missingdata-single-en",
                  "ftt-mail-missingdata-single-it", "ftt-mail-natixflussi-it")
          .parallelStream().forEach(templateName -> {

              ClassPathResource classpathResource = new ClassPathResource("META-INF/templates/" + templateName + ".ftl.html");
              try (Reader reader = new InputStreamReader(classpathResource.getInputStream(), Charset.forName("utf-8")))
              {
                  freemarker.template.Template tmpl = new freemarker.template.Template(templateName, reader, configuration);
                  cacheTemplate.put(templateName, tmpl);
              }
              catch (IOException e)
              {
                  throw new RuntimeException("Errror processing template " + templateName, e);
              }

          });

I understand that writing into a cache from a ParallelStream without proper Java synchronization is questionable practice, and that will be addressed later.

The problem is that among all these resources, the code can't find a single one: "ftt-mail-regclosed-en.ftl.html". It loads the resources everywhere except at customer's SIT. And since that's a parallel stream, I may guess that this is the only offending resource.

Root exception is: java.io.FileNotFoundException: class path resource [META-INF/templates/ftt-mail-regclosed-en.ftl.html] cannot be opened because it does not exist

I can't provide the full stack trace because I can't technically copy&paste fragments here.

I have triple-checked the contents of the jar file and all expected resources are in place. I asked the customer (remember, the WARs are binarily identical!) to make the same check but instead of unpacking it they have provided me a screenshot of a partial binary dump of the jar file where I can see the file names.

I know that the application has been processed by Black Duck scanner against software vulnerabilities. It often reports about suspicious dependencies, but I (nor the customer) have no evidence or knowledge about Black Duck stripping resources. I am saying this because in our thread one of the techs suspected that BD may have stripped out a resource from the package. Very unlikely to me.

What other steps can I do to investigate this issue? What is causing such a FileNotFoundException in a single environment only?

Please note, it's very important: the very same syntax new ClasspathResource("META-INF/something") is used in other parts of the software from early releases. The previous release, containing such method of loading resources, did work fine. That means it loaded its own classpath resources from other jars embedded in the package


Solution

  • This is a potential answer. I haven't been able to reproduce the issue.

    When you invoke new ClassPathResource using Spring, you are setting no class loader. I supposed that Spring used the System classloader (ClassLoader.getSystemClassLoader()).

    Wrong. Spring uses an internal utility method to get the class loader of the current context, bound to the thread. It could have happened, for reasons unknown to me, that the class loader bound to the bean initialization method (which happened to be a WebApplicationClassLoader sponsored by Apache Tomcat) is not the same as the system class loader.

    The Oracle system class loader, in fact, is unable to load resources from Jar files. At least because I have debugged and found the answer by myself.

    Maybe, and I say maybe, removing parallelism and not saving 2 seconds of one-time startup processing, makes the Lambda expression able to get the web application classloader instance instead of the Oracle JDK.

    This, however, does not explain why this happened only at customer site and not in all of our environments, PROD included.

    Solution 2 is to explicitly pass getClass().getClassLoader() to constructor of ClassPathResource