只发博客园,盗版必究
先说背景
平时我们的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-lang3
的StringUtils
工具类,但是方法不全,导致类加载器加载到该类后又找不到方法。
为了验证猜想,在报错的类构造方法中打印了下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-Class
是org.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工程的完整性,这里笔者选择了方法一并且验证通过,遇到同样问题的同学可以根据自己的实际情况选择解决方案
标签:WEB,SpringBoot,webapps,jar,NoSuchMethodError,dassets,91,apache,加载 From: https://www.cnblogs.com/changxy-codest/p/18332875