findResource("") returning null when module-info.java is present, why is that?
Asked Answered
I

4

51

I'm debugging why in the presence of module-info.java in my Spring Boot application, spring-orm throws an exception during start up time. This is the exception:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is java.lang.NoClassDefFoundError: javax/transaction/UserTransaction
    at [email protected]/org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1699) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:573) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:495) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:317) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:315) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1089) ~[spring-context-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:859) ~[spring-context-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550) ~[spring-context-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.4.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) [spring-boot-2.0.4.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:398) [spring-boot-2.0.4.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.SpringApplication.run(SpringApplication.java:330) [spring-boot-2.0.4.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) [spring-boot-2.0.4.RELEASE.jar:na]
    at [email protected]/org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.4.RELEASE.jar:na]
    at tech.flexpoint.dashmanserver/tech.flexpoint.dashmanserver.DashmanServerApplication.main(DashmanServerApplication.java:13) [classes/:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at [email protected]/org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.0.4.RELEASE.jar:na]
Caused by: java.lang.NoClassDefFoundError: javax/transaction/UserTransaction
    at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
    at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3119) ~[na:na]
    at java.base/java.lang.Class.privateGetPublicMethods(Class.java:3144) ~[na:na]
    at java.base/java.lang.Class.getMethods(Class.java:1863) ~[na:na]
    at [email protected]/org.hibernate.service.internal.AbstractServiceRegistryImpl.applyInjections(AbstractServiceRegistryImpl.java:288) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.service.internal.AbstractServiceRegistryImpl.injectDependencies(AbstractServiceRegistryImpl.java:279) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.service.internal.AbstractServiceRegistryImpl.initializeService(AbstractServiceRegistryImpl.java:239) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.service.internal.AbstractServiceRegistryImpl.getService(AbstractServiceRegistryImpl.java:210) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.service.internal.SessionFactoryServiceRegistryImpl.getService(SessionFactoryServiceRegistryImpl.java:80) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.internal.SessionFactoryImpl.canAccessTransactionManager(SessionFactoryImpl.java:942) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.internal.SessionFactoryImpl.buildCurrentSessionContext(SessionFactoryImpl.java:953) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.internal.SessionFactoryImpl.<init>(SessionFactoryImpl.java:319) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.boot.internal.SessionFactoryBuilderImpl.build(SessionFactoryBuilderImpl.java:462) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:892) ~[hibernate-core-5.2.17.Final.jar:na]
    at [email protected]/org.springframework.orm.jpa.vendor.SpringHibernateJpaPersistenceProvider.createContainerEntityManagerFactory(SpringHibernateJpaPersistenceProvider.java:57) ~[spring-orm-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.createNativeEntityManagerFactory(LocalContainerEntityManagerFactoryBean.java:365) ~[spring-orm-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.buildNativeEntityManagerFactory(AbstractEntityManagerFactoryBean.java:390) ~[spring-orm-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.afterPropertiesSet(AbstractEntityManagerFactoryBean.java:377) ~[spring-orm-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean.afterPropertiesSet(LocalContainerEntityManagerFactoryBean.java:341) ~[spring-orm-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1758) ~[spring-beans-5.0.8.RELEASE.jar:na]
    at [email protected]/org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1695) ~[spring-beans-5.0.8.RELEASE.jar:na]
    ... 21 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.transaction.UserTransaction
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582) ~[na:na]
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:190) ~[na:na]
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:499) ~[na:na]
    ... 42 common frames omitted

I tracked down the problem to to URLClassLoader.findResource("") returning null if module-info.java is present but "file:/C:/Users/pupeno/Documents/Dashman/code/dashmanserver/target/classes/" if it's not.

I created the minimum possible example that throws the same exception. To run it, you need to:

  1. Clone and install a recent copy of Moditect from here: https://github.com/moditect/moditect due to this bug fix not being out yet: https://github.com/moditect/moditect/issues/51
  2. Clone the demo repo from: https://github.com/dashmantech/demo
  3. Set up a local PostgreSQL database with credentials demo/confi/application.properties
  4. Run mvn clean package first, so that ModiTec creates all the modules
  5. Open the project in a recent copy of IntelliJ
  6. Click play for the "Run Demo" profile (the .idea directory is included with the appropriate run profile, with arguments, etc).

I need findResource("") to return "file:/C:/Users/pupeno/Documents/Dashman/code/dashmanserver/target/classes/" so that spring-orm can work.

findResource("") looks like this:

public URL findResource(final String name) {
    /*
     * The same restriction to finding classes applies to resources
     */
    URL url = AccessController.doPrivileged(
        new PrivilegedAction<>() {
            public URL run() {
                return ucp.findResource(name, true);
            }
        }, acc);

    return url != null ? URLClassPath.checkURL(url) : null;
}

So I can see there's some access going on that's ok without using the module system but it's prevented by Java's module system when a module-infe.java is present. My problem is that I don't see how to make it work, what should export or open for it to work?

The way Spring Boot is causing the call of that method is through RestartClassLoader, a subclass of URLClassLoader, specifically, line 124 which calls super.findResource(name) in:

@Override
public URL findResource(String name) {
    final ClassLoaderFile file = this.updatedFiles.getFile(name);
    if (file == null) {
        return super.findResource(name);
    }
    if (file.getKind() == Kind.DELETED) {
        return null;
    }
    return AccessController
            .doPrivileged((PrivilegedAction<URL>) () -> createFileUrl(name, file));
}

The specific RestartClassLoader instance being used is a member of ClassPathResource and it's defined this way:

this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());

in the constructor, line 85.

Lastly, getDefaultClassLoader() looks like this:

/**
 * Return the default ClassLoader to use: typically the thread context
 * ClassLoader, if available; the ClassLoader that loaded the ClassUtils
 * class will be used as fallback.
 * <p>Call this method if you intend to use the thread context ClassLoader
 * in a scenario where you clearly prefer a non-null ClassLoader reference:
 * for example, for class path resource loading (but not necessarily for
 * {@code Class.forName}, which accepts a {@code null} ClassLoader
 * reference as well).
 * @return the default ClassLoader (only {@code null} if even the system
 * ClassLoader isn't accessible)
 * @see Thread#getContextClassLoader()
 * @see ClassLoader#getSystemClassLoader()
 */
@Nullable
public static ClassLoader getDefaultClassLoader() {
    ClassLoader cl = null;
    try {
        cl = Thread.currentThread().getContextClassLoader();
    }
    catch (Throwable ex) {
        // Cannot access thread context ClassLoader - falling back...
    }
    if (cl == null) {
        // No thread context class loader -> use class loader of this class.
        cl = ClassUtils.class.getClassLoader();
        if (cl == null) {
            // getClassLoader() returning null indicates the bootstrap ClassLoader
            try {
                cl = ClassLoader.getSystemClassLoader();
            }
            catch (Throwable ex) {
                // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
            }
        }
    }
    return cl;
}

My module-info.java contains:

module tech.flexpoint.dashman {
    exports tech.flexpoint.dashman to com.fasterxml.jackson.databind;
    exports tech.flexpoint.dashman.controllers.configurator to javafx.fxml;

    opens tech.flexpoint.dashman to javafx.graphics, jna;
    opens tech.flexpoint.dashman.controllers.common to javafx.fxml;
    opens tech.flexpoint.dashman.controllers.configurator to javafx.fxml;
    opens tech.flexpoint.dashman.models to org.hibernate.validator, tech.flexpoint.dashmancommon, javafx.base;

    opens common;
    opens configurator;
    opens displayer;
    opens winscreensaver;

    requires appdirs;
    requires org.bouncycastle.provider;
    requires com.fasterxml.jackson.core;
    requires com.fasterxml.jackson.databind;
    requires com.fasterxml.jackson.datatype.jdk8;
    requires io.sentry;
    requires jackson.annotations;
    requires java.desktop;
    requires java.sql;
    requires java.validation;
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.graphics;
    requires javafx.media;
    requires javafx.web;
    requires jna;
    requires jna.platform;
    requires org.apache.commons.lang3;
    requires org.kordamp.ikonli.javafx;
    requires org.kordamp.ikonli.fontawesome5;
    requires spring.core;
    requires spring.retry;
    requires spring.web;
    requires tech.flexpoint.dashmancommon;
}

In IntelliJ I have these plugins enabled:

  • Lombok Plugin
  • .ginore
  • PowerShell
  • VisualVM Launcher
  • ANSI Highlighter
  • Batch Scripts Support
  • Bytecode Viewer
  • CMD Support
  • Copyright
  • Coverage
  • CSS Support
  • Database Tools and SQL
  • Git Integration
  • GitHub
  • Gradle
  • Groovy
  • Heroku integration
  • HTML Tools
  • HTTP Client
  • l18n for Java
  • IDE Settings Sync
  • Java Bytecode Decompiler
  • Java EE: EJB, JPA, Servlets
  • Java Stream debugger
  • JavaFX
  • JUnit
  • Lines Sorter
  • Markdown support
  • Maven Integration
  • Maven Integration Extension
  • Persistence Frameworks Support
  • Properties Support
  • Smali Support
  • Spring AOP/@AspectJ
  • Spring Batch
  • Spring Boot
  • Spring Data
  • Spring Integration Patterns
  • Spring OSGi
  • Spring Security
  • Spring Support
  • Spring Web Services
  • Spring WebSocket
  • Terminal
  • YAML
Informal answered 21/8, 2018 at 8:50 Comment(0)
E
12

One thing I notice is that your application (assuming that it's packaged in tech.flexpoint.dashman) does not seem to be opened up to Spring in any way, which will surely result in failed class loading/illegal access.

I would expect to see something like this in module-info.java (depending on your Spring dependencies):

opens tech.flexpoint.dashman to spring.core, spring.beans, spring.context;

The exception is a NoClassDefFoundError, which is thrown at runtime when the class definition of a class that was known at compile time cannot be resolved, in this case the interface javax.transaction.UserTransaction, which is part of the Java Transaction API (JTA).

As others have pointed out, JTA is not bundled with the JDK, and needs to be added as a compile dependency. However, the class that needs to load the UserTransaction class definition comes from the spring-boot-autoconfigure artifact, which is responsible for its own dependencies ([email protected] πŸ‘’ [email protected] πŸ‘’ [email protected]), so you should not need to add JTA as a dependency.

However, because you want to package your own app as a Java 9 module, it needs to explicitly state its dependencies. spring-boot-autoconfigure is not yet a modularized Java 9 library, and does not do this for you (i.e. transitively). The automatic module name for JTA is java.transaction, so you need to add the requirement in module-info.java:

requires java.transaction;

I got your example running and did indeed get NoClassDefFoundErrors when running from IntelliJ IDEA. The stacktrace pointed back to a ClassNotFoundException, which indicates classpath problems. Since IDEA calculates the classpath when launching the application from there, I wanted to see if I could reproduce the error when using the spring-boot-maven-plugin to run the application.

I copied the IDEA run configuration to the spring-boot-maven-plugin configuration, as shown below:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
      <mainClass>tech.flexpoint.demo.DemoApplication</mainClass>
      <jvmArguments>--show-module-resolution --add-opens=java.base/java.lang=spring.core --add-opens=java.base/java.io=tomcat.embed.core --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED</jvmArguments>
      <workingDirectory>${project.basedir}</workingDirectory>
  </configuration>
</plugin>

Then I invoked mvn spring-boot:run and voila, the application booted successfully without errors. I can only conclude that this is an issue with the classpath calculated by IntelliJ.

Etana answered 26/8, 2018 at 6:37 Comment(10)
The application is tech.flexpoint.dashmanserver and it was already open to spring.core, spring.beans but not spring.context. Adding it didn't change anything sadly. I added the exception I get to the beginning of the question. – Informal
I'm using moditect to turn each and every one of my dependencies into modules. – Informal
@pupeno Could very well be an issue with ModiTect, then. It’s still in beta. – Etana
@pupeno I can't think of any other explanation than that this is due to a mistake in one of the module info definitions, maybe most likely for one of the dependencies. Have you tried without moditect, just explicitly stating all of the requirements in your own module-info.java? – Etana
@pupeno Seems to be an IntelliJ classpath issue - see updated answer. – Etana
How does it work without the module-info.java. It is definitely going to be a conflicting module in the list not the Intellij issue – Fermentation
@RinsadAhmed IntelliJ probably uses the information from module-info.java in the classpath generation. The spring-boot-maven-plugin also generates a classpath for executing the app, and clearly does a better job. Please let me know if you get the error to occur using the spring-boot plugin, but if you can't you should reconsider your downvote. – Etana
@pupeno Any luck getting it to run this way? – Etana
My apologies for the delay, life got busy. Indeed, running it from the command line like you did worked. This is good news because that means my project modularization is more finished than I expected, but bad news because I can't debug the application when running it that way. – Informal
@pupeno You can configure the plugin to enable debugging and then create a Remote Debug Run configuration in IDEA. – Etana
T
3

Assuming you have declared the dependency:

<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>javax.transaction-api</artifactId>
    <version>1.3</version>
</dependency>

Include the following in module-info.java:

requires java.transaction;

Version 1.3 declares the automatic module name, while version 1.2 doesn't.
The latter requires javax.transaction.api;. Source

Trifoliate answered 26/8, 2018 at 10:32 Comment(3)
When I add requires java.transaction; I get the message: java.transaction is deprecated and marked for removal. – Informal
@pupeno You can safely ignore such message. Here, two modules share the same name: the one in the JDK that's marked for removal, and the other provided at runtime that you want to depend on. – Trifoliate
Ok. I added requires java.transaction; It didn't seem to make any difference to the issue sadly. – Informal
F
1

As you have mentioned in your original problem, the code works without module-info.java but not with the module-info.java. I can see that you have done all this hard work in explaining the problem, creating a minimal project and so on to drill down to the problem.

Looking at your problem it is obvious that one of the modules is causing the URLClassLoader.findResource("") returning null. It might be one of the modules down the list is overriding this class method or have an ambiguous implementation.

Why don't you start with an empty module-info.java for the minimal example and keep adding 1 module at a time till we see the error? I believe this will help us find the culprit.

Fermentation answered 31/8, 2018 at 2:20 Comment(0)
F
0

this (or similar) issue already had been filed for spring-boot on GitHub (but with Java 9).

I'd have moditect under suspicion, while there are also issues filed for moditect on GitHub and I've also found your issue there; updating ASM to 6.2.1 fixes at least one other breaking change:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.2.1</version>
</dependency>
Fauteuil answered 1/9, 2018 at 4:17 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.