Web 容器 Jetty 多 ClassLoader 重复加载 Class 导致 java.lang.LinkageError


  • 问题描述:
    最近团队在使用 Axis2 开发 WebService 服务时,遇到服务发布成功,但调用一直出现如下错误


    Axis2 Can’t find Spring’s ApplicationContext

  • 问题分析:
    查看异常出处:SpringAppContextAwareObjectSupplier.getServiceObject 方法可知
    ApplicationContext 由 ApplicationContextHolder.getContext() 调用得到,再看
    ApplicationContextHolder 类可知其实现了 ApplicationContextAware 接口只是负责管理注入的 Spring 上下文对象置于静态变量 appCtx 供 Axis2 调用

  • 提出一种可行方案 A:
    直接在 Spring 配置文件中注入 ApplicationContext 至 ApplicationContextHolder.appCtx

  • A 方案效果:
    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 中)表现出不一致

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值