Reputation: 44240
I have an application which has a Spring Boot core which can have optional modules added at runtime (using PropertiesLoader).
-Dloader.main=com.mycompany.App
-Dloader.path="C:\dir\some-module.jar"
The modules are packaged as fat jars, so they have all of their dependencies bundled with them. I use the shade plugin for this.
One of the additional modules has a dependency on JDBC and when I unzip its JAR, I can see that the Hikari stuff is present.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
When I run the application from the JAR everything works. However, when I run from IntellIJ's classpath, one of the Hikari classes fails to load.
java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.jdbc.DataSourceJmxConfiguration$Hikari
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:64) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:447) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:128) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:117) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:327) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:232) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:705) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:531) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at com.mycompany.App.main(App.java:11) ~[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:567) ~[na:na]
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:48) ~[spring-boot-loader-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:87) ~[spring-boot-loader-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:50) ~[spring-boot-loader-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.loader.PropertiesLauncher.main(PropertiesLauncher.java:593) ~[spring-boot-loader-2.1.3.RELEASE.jar:2.1.3.RELEASE]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:686) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:583) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:568) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:626) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1705) ~[na:na]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:738) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:679) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:647) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1518) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1023) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.addBeanTypeForNonAliasDefinition(BeanTypeRegistry.java:195) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.addBeanTypeForNonAliasDefinition(BeanTypeRegistry.java:159) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.addBeanType(BeanTypeRegistry.java:152) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.updateTypesIfNecessary(BeanTypeRegistry.java:140) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133) ~[na:na]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.updateTypesIfNecessary(BeanTypeRegistry.java:135) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.BeanTypeRegistry.getNamesForType(BeanTypeRegistry.java:97) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.collectBeanNamesForType(OnBeanCondition.java:298) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:289) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanNamesForType(OnBeanCondition.java:278) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchingBeans(OnBeanCondition.java:189) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:138) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47) ~[spring-boot-autoconfigure-2.1.3.RELEASE.jar:2.1.3.RELEASE]
... 24 common frames omitted
Caused by: java.lang.NoClassDefFoundError: com/zaxxer/hikari/HikariDataSource
at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na]
at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3171) ~[na:na]
at java.base/java.lang.Class.getDeclaredMethods(Class.java:2314) ~[na:na]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:668) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
... 46 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.zaxxer.hikari.HikariDataSource
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583) ~[na:na]
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na]
... 50 common frames omitted
When I put a breakpoint here in org.springframework.boot.loader.Launcher
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
this.launch(args, this.getMainClass(), classLoader); //breakpoint
}
I can see that the LaunchedURLClassLoader
is created correctly (i.e. it has a reference to the module JAR) and when I evaluate
classLoader.loadClass("com.zaxxer.hikari.HikariDataSource")
in the debugger, I can see that the class can be loaded fine from here.
When DataSourceJmxConfiguration$Hikari
is loaded, however, it loads with a parent classloader since the Spring Boot Auto-Configure is bundled into the core Spring Boot app. When Spring attempts to call getDeclaredMethods
, it blows up since HikariDataSource
needs to be loaded by the child classloader, and the parent is not aware of any of it's children.
HikariDataSource
is certainly on the classpath since DataSourceJmxConfiguration$Hikari
is conditional upon it being there
@Configuration
@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnSingleCandidate(DataSource.class)
static class Hikari {
//...
}
I wasn't able to determine what was different about running directly from the JAR, and why that works over what IntelliJ is doing. I can see that the the initial classloader is created in what looks to be the same way.
This problem unfortunately does not lend itself to posting a reproducible example here (I could but you would have to construct the entire project structure yourself). Instead, I have created a MCVE on GitHub - it is only 5 files, 3 of which are POMs.
What's the difference with regards to how these classes are loaded, and how can I fix it?
I could continue to run as a 'JAR Application', which works, but it is more convenient to run as an 'Application', since then I can do a hot replace and I do not need to build a new JAR for every change. I would also just like to know for my own understanding.
Upvotes: 4
Views: 1777
Reputation: 470
I have check the java command that Intellij running (it is written in the 1st line in the run window, you can copy it and check it), and when you run it as application, Intellij programmatically add the -classpath options in the java command. And in the classpath, the jdbc-1.0-SNAPSHOT.jar does not exists (neither any jar that seems to contains the class needed). So I added this configuration your configuration (the "Test (failing)" one)
-Dloader.main=com.mycompany.App
-Dloader.path=C:\Java\jdbc-autoconfigure-failure\jdbc\target\shaded\jdbc-1.0-SNAPSHOT.jar
-classpath $Classpath$
and it worked. Because it seem the option -classpath $Classpath$ reload the classpath using the project -classpath VM options and load the HikariCP-3.2.0.jar. You can either use the solution or add the jdbc-1.0-SNAPSHOT.jar in the "core" module setting in dependencies tab.
EDIT: This is the configuration that should be use for running as Application. First add all library needed for spring-boot in lib folder inside working directory, you can find all jars needed for spring-boot in BOOT-INF\lib inside core-1.0-SNAPSHOT.jar. In this example I am copying all jars needed into C:\Java\jdbc-autoconfigure-failure\core\lib
-Dloader.main=com.mycompany.App
-Dloader.path="C:\Java\jdbc-autoconfigure-failure\core\lib,C:\Java\jdbc-autoconfigure-failure\core\target\classes,C:\Java\jdbc-autoconfigure-failure\jdbc\target\shaded\jdbc-1.0-SNAPSHOT.jar"
-classpath "C:\Java\jdbc-autoconfigure-failure\core\lib\spring-boot-loader-2.1.3.RELEASE.jar"
EDIT: Found the issue
-Dloader.main=com.mycompany.App
-Dloader.path=\"C:\Java\jdbc-autoconfigure-failure\jdbc\target\shaded\jdbc-1.0-SNAPSHOT.jar\"
Upvotes: 1