问题描述:
最近团队在使用 Axis2 开发 WebService 服务时,遇到服务发布成功,但调用一直出现如下错误
Axis2 Can’t find Spring’s ApplicationContext问题分析:
查看异常出处:SpringAppContextAwareObjectSupplier.getServiceObject 方法可知
ApplicationContext 由 ApplicationContextHolder.getContext() 调用得到,再看
ApplicationContextHolder 类可知其实现了 ApplicationContextAware 接口只是负责管理注入的 Spring 上下文对象置于静态变量 appCtx 供 Axis2 调用提出一种可行方案 A:
直接在 Spring 配置文件中注入 ApplicationContext 至 ApplicationContextHolder.appCtxA 方案效果:
Axis2 WebService 服务可正常发布,但调用依然是出现同样的错误对 A 方案结果提出疑问:
难道实现 ApplicationContextAware 接口不能实现 Spring 容器初始化完成后的注入?对此自己写了一个类 TestAware 实现 ApplicationContextAware 接口并打印判断是否注入且注入内容是否为 null
很明显测试结构表明这个是注入成功的提出一种可行方案 B:
自己写一个类实现 ApplicationContextAware 接口,并在方法 setApplicationContext 中手动将 ApplicationContext 注入 ApplicationContextHolder.appCtx(同时外层添加异常处理块记录日志)B 方案效果:
启动直接报错
java.lang.LinkageError: loader constraint violation: when resolving method “org.apache.axis2.extensions.spring.receivers.ApplicationContextHolder. setApplicationContext(Lorg/springframework/context/ApplicationContext);” the class loader (instance of org/mortbay/jetty/webapp/WebAppClassLoader) of the current class, foo/bar/baz/TestAware, and the class loader (instance of sun/misc/Launcher$AppClassLoader) for resolved class, org/apache/axis2/extensions/spring/receivers/ApplicationContextHolder, have different Class objects for the type org.apache.axis2.extensions.spring.receivers.ApplicationContextHolder. setApplicationContext(Lorg/springframework/context/ApplicationContext); used in the signature对 B 方案问题分析:
1)ApplicationContextHolder 在链接的时候出现了错误
2)ApplicationContextHolder 由 sun.misc.launcher.AppClassLoader 加载
3)TestAware 由 org.mortbay.jetty.webapp.WebAppClassLoader 加载
4)ApplicationContextHolder.setApplicationContext(ApplicationContext) 的方法签名类型不同导致方法调用失败
很显然这是 Web 容器中多 ClassLoader 引起的问题,因为 Web 容器有自己的加载器,但并不完全遵守“双亲委托”模型
对于 Java ClassLoader 知识此处不表
根据 Java ClassLoader 机制可知 AppClassLoader 先于 WebAppClassLoader 加载运行,且是后者的 Parent ClassLoader(父加载器),所以可以判断 ApplicationContextHolder 引用的 ApplicationContext 的类加载器是 AppClassLoader;TestAware 引用的 ApplicationContext 的类加载器不是 AppClassLoader,即 TestAware 的 ApplicationContext 的类加载器是 WebAppClassLoader解决方案:
知道了问题的原因那么只要使 JVM 中的每个类只加载一次且只能被其中一个加载器加载即可。如果所有的 ClassLoader 都遵守“双亲委托”模型,自然不会出现该问题,那么对于像 WebAppClassLoader 这样 ClassLoader 如何保证这一条件呢?
根据 Jetty WebAppClassLoader 可知其只负责加载 /WEB-INF/classes 下的所有类,并负责加载 /WEB-INF/lib 目录下(不包括/WEB-INF/lib/foo/ 目录)的所有 jar,对于其它的所有类或 jar 以及 WebAppClassLoader 自己都是由 AppClassLoader 加载,那么我们只需要把 Web 工程中的所有的 jar 都放置于 /WEB-INF/lib 目录下即可避免该问题重新梳理:
准备用例:
/**
* 单独生成 jar 放在除 /WEB-INF/lib 的任意位置
*/
public class A{
public void hello(C c){
System.out.println("AAAAA");
}
}
/**
* 单独生成 jar 放在 /WEB-INF/lib 下<br>
* 或者不生成 jar 直接就在 /WEB-INF/classes 下
*/
public class B{
public String webRequest(){
hello(new C());
}
public String hello(C c){
System.out.println("BBBBB");
c.hello();
new A().hello(c);
}
}
/**
* 单独生成 jar 放在 /WEB-INF/lib 下<br>
* 或者不生成 jar 直接就在 /WEB-INF/classes 下
*/
public class C{
public void hello(){
System.out.println("CCCCC");
}
}
知识点:WebAppClassLoader 不遵守“双亲委托”模型,始终在第一时间由自己尝试加载类,只有在自己搜索路径中不存在类时,才会委托给 Parent ClassLoader(父加载器)
运行分析:
WebAppClassLoader 不管什么时候加载 A,A 始终都是由 AppClassLoader 加载;此时 C 也必须由 AppClassLoader 加载。
记作:A-acl C-acl A-acl.hello(C-acl c)
B 如果此时被 WebAppClassLoader 加载,则 A 会被委托 AppClassLoader 搜索或加载到(因为 /WEB-INF/lib 下没有),则 C 又会由 WebAppClassLoader 重新加载(因为其在/WEB-INF/lib or classes 下)
记作:B-wacl C-wacl A-wacl.hello(C-wacl c)
如果 B-wacl 调用 A.hello(C c) 则会出现类链接异常,因为 B-wacl 中链接到的 A 虽然是 A-acl 但 C 链接到的却是 C-wacl ,而 A-acl 中的链接到的 C 是 C-acl,所以虽然 A B 都只加载一次是唯一的,但这 2 个 C 是不同的,故表现出重复加载同一个类的问题,导致 2 个类(在 JVM 中)表现出不一致