首页 > 解决方案 > 使用 IntelliJ 从 PropertiesLoader 加载的 Spring Boot 模块时出现 NoClassDefFoundError

问题描述

我有一个应用程序,它有一个 Spring Boot 核心,可以在运行时添加可选模块(使用 PropertiesLoader)。

-Dloader.main=com.mycompany.App
-Dloader.path="C:\dir\some-module.jar"

这些模块被打包为胖罐子,因此它们的所有依赖项都与它们捆绑在一起。我为此使用了阴影插件。

其中一个附加模块依赖于 JDBC,当我解压缩它的 JAR 时,我可以看到存在 Hikari 的东西。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

当我从 JAR 运行应用程序时,一切正常。但是,当我从 IntellIJ 的类路径运行时,其中一个 Hikari 类无法加载。

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

当我在这里设置断点时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 
}

我可以看到LaunchedURLClassLoader创建正确(即它具有对模块 JAR 的引用)并且当我评估时

classLoader.loadClass("com.zaxxer.hikari.HikariDataSource")

在调试器中,我可以看到可以从这里很好地加载该类。

但是,当DataSourceJmxConfiguration$Hikari加载时,它会使用父类加载器加载,因为 Spring Boot Auto-Configure 捆绑到核心 Spring Boot 应用程序中。当 Spring 尝试调用getDeclaredMethods时,它会爆炸,因为HikariDataSource需要由子类加载器加载,而父类不知道它的任何子类。

HikariDataSource肯定在类路径上,因为DataSourceJmxConfiguration$Hikari条件是它在那里

@Configuration
@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnSingleCandidate(DataSource.class)
static class Hikari {
    //...
}

我无法确定直接从 JAR 运行有什么不同,以及为什么这对 IntelliJ 所做的工作有效。我可以看到初始类加载器是以相同的方式创建的。

不幸的是,这个问题不适合在此处发布可重现的示例(我可以,但您必须自己构建整个项目结构)。相反,我在 GitHub 上创建了一个 MCVE——它只有 5 个文件,其中 3 个是 POM。

这些类的加载方式有什么区别,我该如何解决?

我可以继续作为“JAR 应用程序”运行,它可以工作,但作为“应用程序”运行更方便,因为那时我可以进行热替换,并且不需要为每次更改构建新的 JAR。我也想知道我自己的理解。

标签: javaspringspring-bootintellij-ideaclassloader

解决方案


我检查了 Intellij 运行的 java 命令(它写在运行窗口的第一行,你可以复制它并检查它),当你将它作为应用程序运行时,Intellij 以编程方式在 java 命令中添加 -classpath 选项. 在类路径中, jdbc-1.0-SNAPSHOT.jar 不存在(似乎包含所需类的任何 jar 都不存在)。所以我添加了这个配置你的配置(“测试(失败)”之一)

-Dloader.main=com.mycompany.App
-Dloader.path=C:\Java\jdbc-autoconfigure-failure\jdbc\target\shaded\jdbc-1.0-SNAPSHOT.jar
-classpath $Classpath$

它奏效了。因为似乎选项 -classpath $Classpath$ 使用项目 -classpath VM 选项重新加载类路径并加载 HikariCP-3.2.0.jar。您可以使用该解决方案,也可以在依赖项选项卡的“核心”模块设置中添加 jdbc-1.0-SNAPSHOT.jar。

编辑:这是应该用于作为应用程序运行的配置。首先在工作目录的 lib 文件夹中添加 spring-boot 所需的所有库,您可以在 core-1.0-SNAPSHOT.jar 中的 BOOT-INF\lib 中找到 spring-boot 所需的所有 jar。在此示例中,我将所有需要的 jar 复制到 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"

编辑:发现问题

-Dloader.main=com.mycompany.App
-Dloader.path=\"C:\Java\jdbc-autoconfigure-failure\jdbc\target\shaded\jdbc-1.0-SNAPSHOT.jar\"

推荐阅读