工程结构
-ear
-web1.war
-WEB-INF
-lib
-common.jar // 公共的jar包
-web2.war
-WEB-INF
-lib
-common.jar // 公共的jar包
有1个ear包里有2个war,war都含有共同的common.jar
问题
这样部署到 jboss里没问题。但由于项目需要hotfix–即而当遇到测试测出不能进行下去的bug时候,就需要将fix好的class文件手工替换,假设现在遇到的class在common.jar 里,替换的时候就需要替换2个地方。为什么不重新部署一个,因为那样太耗时了。
解决过程记录
按之前EAR工程在jboss(wildfly)里的结构以及class loading关系
里的方法
把common作为一个jar后在application.xml里加上
<module>
<ejb>common.jar</ejb>
</module>
然后在web1 和web2里将common.jar去掉
[org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Invocation of init method failed; nested exception is java.lang.ClassCastException: com.aci.ws.idealx.resolver.EmployeeValueResolver cannot be cast to org.springframework.web.method.support.HandlerMethodArgumentResolver
Caused by: java.lang.ClassCastException: com.xxx.resolver.EmployeeValueResolver cannot be cast to org.springframework.web.method.support.HandlerMethodArgumentResolver"}
由于web1.war里引用了commn.jar里的EmployeeValueResolver,
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new EmployeeValueResolver());
}
EmployeeValueResolver的代码
public class EmployeeValueResolver
implements HandlerMethodArgumentResolver {...}
HandlerMethodArgumentResolver 是spring-web-5.1.2.release.jar里的类。
而spring-web-5.1.2.release.jar在web工程里也有引用
整个项目结构如下:
-ear
-common.jar //这里EmployeeValueResolver 找到lib下HandlerMethodArgumentResolver
-web1.war
-WEB-INF
-lib
-spring-web-5.1.2.release.jar
-web2.war
-WEB-INF
-lib
-spring-web-5.1.2.release.jar
-3rdparty
spring-web-5.1.3.release.jar
为什么报class cast出错?
由于之前有经验,虽然类命一样,可是类加载器不一样,也会认定为不同的2个class。所以猜测是由于类加载器不一致引起的。
那就进行调用的地方断点跟踪
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
//这里断点
argumentResolvers.add(new EmployeeValueResolver());
}
断点后,打印出
new EmployeeValueResolver().getClass().getInterfaces()[0].getClassLoader()
结果是
ModuleClassLoader for Module
“deployment.epj2ee.ear.3rdparty/spring-web-5.1.3.RELEASE.jar:main”
from Service Module Loader
再打印
HandlerMethodArgumentResolver.class.getClassLoader()
结果是
ModuleClassLoader for Module
“deployment.epj2ee.ear.web1.war:main” from Service Module
Loader
很明显2个类加载器不一致,这就是造成报class cast的原因。
为什么原来放在war的lib目录下没问题呢?
可以肯定是加载器一致。根据wildfly guide 文档里class加载的层次级别从高到低如下
- System Dependencies
- User Dependencies - These are dependencies that are added through jboss-deployment-structure.xml or through the Dependencies: manifest entry.
- Local Resource - Class files packaged up inside the deployment itself, e.g. class files from WEB-INF/classes or WEB-INF/lib of a war.
- Inter deployment dependencies - These are dependencies on other deployments in an ear deployment. This can include classes in an ear’s lib directory, or classes defined in other ejb jars
按上面的理论,原来的 common.jar放在war的lib 下,在第3级别,web1加载器(deployment.epj2ee.ear.web1.war:main) 在 加载EmployeeValueResolver(new EmployeeValueResolver())的时候发现在本地lib里找到,那么类加载器就是web1的,并且去加载HandlerMethodArgumentResolver 也可以从本地找到。
那现在问题是 为什么HandlerMethodArgumentResolver在2个地方都有啊?为什么能就是从web1.war里找到呢?
这得从jboss 的类加载器找出答案
public class ModuleClassLoader extends ConcurrentClassLoader {
protected final Class<?> findClass(String className, boolean exportsOnly, final boolean resolve) throws ClassNotFoundException {
// Check if we have already loaded it..
//private native final Class<?> findLoadedClass0(String name);
//现在通过native去检查是否这个className已经加载了。即使其他的module
//的类加载器已经加载过了,但由于类加载器不同,这里也是返回null
Class<?> loadedClass = findLoadedClass(className);
if (loadedClass != null) {
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
...
final Module module = this.module;
log.trace("Finding class %s from %s", className, module);
//找不到的话就去当前module里找
//也就是说当类找不到的时候,就从本地的module里先找
//那找不到怎么办?找不到了自然就从上面的优先级去找了
final Class<?> clazz = module.loadModuleClass(className, resolve);
if (clazz != null) {
return clazz;
}
log.trace("Class %s not found from %s", className, module);
throw new ClassNotFoundException(getClassNotFoundExceptionMessage(className, module));
}
}
Class<?> loadModuleClass(final String className, final boolean resolve) throws ClassNotFoundException {
//先从当前的路径里去找
for (String s : systemPackages) {
if (className.startsWith(s)) {
//这个moduleClassLoader是当前module的class loader
return moduleClassLoader.loadClass(className, resolve);
}
}
//如果找不到,就从其它地方去找,这里的其它地方就是从上面的加载优先等级找起
final String path = pathOfClass(className);
final Map<String, List<LocalLoader>> paths = getPathsUnchecked();
final List<LocalLoader> loaders = paths.get(path);
if (loaders != null) {
Class<?> clazz;
for (LocalLoader loader : loaders) {
clazz = loader.loadClassLocal(className, resolve);
if (clazz != null) {
return clazz;
}
}
}
final LocalLoader fallbackLoader = this.fallbackLoader;
if (fallbackLoader != null) {
return fallbackLoader.loadClassLocal(className, resolve);
}
return null;
}
现在将common.jar 不再放在第3级别,而是放到第2级别。 根据上面的分析就很容易知道本module找不到class后就只能去其它的module里找。那么类加载器自然就不同了。而都放在war下,那类加载器自始至终都相等。这也是文档里的
问题还没解决,怎么办?
从通过文档里看到有个参数是 ,具体见英文注释解释。就是把local的级别变低,如下
这样又会出错
‘org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration’:
BeanPostProcessor before instantiation of bean failed; nested
exception is java.lang.IllegalArgumentException: Class
‘com.xxx.aop.DesDecryptAspect’ is not an @AspectJ aspect
原因很好解释,会去判断class是否有 org.aspectj.lang.annotation.Aspect
public boolean isAspect() {
return clazz.getAnnotation(Aspect.class) != null;
}
而工程下有多个jar
local-last value=“true” 即有同名的类在不同的路径戏啊,放弃从自己的路径去找,让自己的级别最低。这样肯定找到的是什么莫名其妙的jar里,肯定就出错了。
最终的解决方案
看下面的英文注释,
-
所有的在 deployment下使用的标签都能被sub-deployment使用上,也就是说
resource-root也能用。 -
resource-root的解释-相当于把resource-root里的东西合并到lib里。
那不就相当于在sub-deployment里如果加上resource-root,就相当于把my-library.jar放到了war的lib下,这样classloader就都统一了。
<deployment>
<!-- These add additional classes to the module.
In this case it is the same as including the jar
in the EAR's lib directory -->
<resources>
<resource-root path="my-library.jar" />
</resources>
</deployment>
<sub-deployment name="myapp.war">
<!-- This corresponds to the module for a web deployment -->
<!-- it can use all the same tags as the <deployment> entry above -->
</sub-deployment>
因此最后的解决方案:
<sub-deployment name="web1.war">
<resources>
<resource-root path="common.jar" />
</resources>
</sub-deployment>
总计
这篇啰啰嗦嗦的记录了一堆,而最后解决的方案其实很简单。但我认为更重要的解决问题的过程,遇到问题就去baidu,google也许很容易能找到答案,这样找的答案会失去一个重要的思考的过程。这里的记录其实就是我的一个探索的过程,总结来说就是 通过阅读官方文档,猜测,做实验印证,结合通过源码的阅读来印证的一个解决过程。