Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 462
pupeno Avatar asked Aug 21 '18 08:08

pupeno


3 Answers

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.

like image 130
Henrik Avatar answered Nov 19 '22 18:11

Henrik


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

like image 3
Atom 12 Avatar answered Nov 19 '22 19:11

Atom 12


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.

like image 1
Rinsad Ahmed Avatar answered Nov 19 '22 19:11

Rinsad Ahmed