记一次解决SpringBoot项目由于依赖加载顺序问题导致启动报NoSuchMethodError的问题

只发博客园,盗版必究

先说背景

平时我们的Spring Boot项目都是打成Executable Jar启动应用,最近接了个技术需求,需要打成War包,将多个项目放在同一个Tomcat中运行。

原本Jar包启动一切正常,但是打成WAR放Tomcat启动后报错了,异常栈如下:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'initDdrwServiceImpl': Invocation of init method failed; nested exception is java.lang.NoSuchMethodError: org.apache.commons.lang3.StringUtils.containsAny(Ljava/lang/CharSequence;[Ljava/lang/CharSequence;)Z
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:138)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:422)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1698)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:579)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:501)

问题分析

分析一波,应该是某个依赖包里面集成了commons-lang3StringUtils工具类,但是方法不全,导致类加载器加载到该类后又找不到方法。
为了验证猜想,在报错的类构造方法中打印了下StringUtils类是从哪里加载的,代码如下:

System.out.println("StringUtils: " + StringUtils.class.getResource("").getPath());

// 输出:file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hive-exec-3.1.2.jar!/org/apache/commons/lang3/

果然,没有正确从commons-lang3加载依赖包中的工具类,而是从hive-exec依赖包中加载的。

知道了是依赖包加载的问题,我们通过打包以后的MANIFEST.MF配置文件看下,当前的Main-Classorg.springframework.boot.loader.WarLauncher,具体看了下源码(涉及到SpringBoot的启动原理,大家可以自行搜索相关文章)ExecutableArchiveLauncher中初始化了依赖资源的搜索,核心代码如下:

@Override
protected List<Archive> getClassPathArchives() throws Exception {
	List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
	postProcessClassPathArchives(archives);
	return archives;
}

回到SpringBoot的Application类的main方法中,打印下URLClassLoader的资源路径,代码如下:

URLClassLoader classLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();
List<URL> classLoaderUrls = Arrays.asList(classLoader.getURLs());
for(URL url : classLoader.getURLs()) {
System.out.println(url);
}

得到的结果如下:

file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/classes/
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/dropwizard-metrics-hadoop-metrics2-reporter-0.1.2.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/jaxb-impl-2.2.3-1.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hadoop-hdfs-3.3.4.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/avatica-core-1.17.0.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/ribbon-core-2.2.5.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/lucene-queries-8.5.1.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/dialect-8.6.0.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/ridl-4.1.2.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hbase-replication-2.0.0-alpha4.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/javax.el-3.0.0.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hbase-procedure-2.0.0-alpha4.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/simpleclient_tracer_otel_agent-0.12.0.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/db2jcc4-10.1.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/expiringmap-0.5.8.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hadoop-yarn-server-resourcemanager-3.3.4.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hive-llap-server-3.1.2.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/tinypinyin-2.0.3.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/transmittable-thread-local-2.12.0.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hadoop-yarn-server-common-3.3.4.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/hbase-client-2.0.2.jar
file:/Users/mac/Downloads/apache-tomcat-9.0.91/webapps/api-dassets/WEB-INF/lib/jackson-dataformat-cbor-2.9.5.jar
...以下省略...

得出两个结论:
1、资源加载是有顺序的,其中classes优先级最高,这个很重要为后面解决问题提供了思路
2、hive-exec包在commons-lang3包之前

解决思路

这里提供3种解决方案

方案一、将依赖包中的class文件在打包时放进target/classes目录下

这样就可以首先优先到正确的class文件了,maven配置如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>unpack-some-artifact</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.apache.commons</groupId>
                        <artifactId>commons-lang3</artifactId>
                        <type>jar</type>
                        <overWrite>true</overWrite>
                        <outputDirectory>${project.build.directory}/classes</outputDirectory>
                        <includes>**/*.class</includes>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
    </executions>
</plugin>

方案二、自定义Launcher

SpringBoot项目打包后Executable Jar是通过JarLauncher启动,WAR包是通过WarLauncher启动,大家可以查阅下spring-boot-loader工程看下源码。
大家可以参考JarLauncher或者WarLauncher实现自己的Launcher,Spring预留了postProcessClassPathArchives方法大家可以自由发挥。
在Maven配置文件中指定<layout>ZIP</layout>打包,Main-Class会变为org.springframework.boot.loader.PropertiesLauncher,指定loader.main参数为自己实现的ClassLoader类。

{@code loader.main}: the main method to delegate execution to once the class loader

方案三、在Application类入口处对ClassLoader中的URL重新排序

// 获取当前的ClassLoader
URLClassLoader classLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();
// 获取资源列表
URL[] urls = classLoader.getURLs();

// todo urls排序

// 重置ClassLoader
URLClassLoader orderedClassLoader = new URLClassLoader(urls, classLoader.getParent());
Thread.currentThread().setContextClassLoader(orderedClassLoader);

为了保持SpringBoot工程的完整性,这里笔者选择了方法一并且验证通过,遇到同样问题的同学可以根据自己的实际情况选择解决方案

posted @ 2024-07-30 17:57  codest  阅读(108)  评论(0编辑  收藏  举报