美文网首页
记录一个奇怪的 IllegalAccessError

记录一个奇怪的 IllegalAccessError

作者: 加牛不辣 | 来源:发表于2020-02-22 19:25 被阅读0次

奇怪的 IllegalAccessError

我们有一个用于为 Java 服务服务添加监控的 Java Agent,简单来说就是可以在 JVM 启动时指定一个外部的 Java Agent Jar 文件,这个 Agent 会通过 Java Instrumentation API 修改加载的 Class 字节码从而可以添加所需的监控逻辑。这里不得不提到一个十分好用的字节码操作库 Byte Buddy

说回问题,我们最近在添加 Spring-Boot Tomcat 线程池监控时遇到奇怪的 IllegalAccessError,简化的代码如下:

public class TomcatThreadStatInterceptor {

  @Advice.OnMethodExit
  public static void onExit() {
    List<ObjectName> threadPools = getThreadPoolsObjectName();

    for (ObjectName threadPool : threadPools) {
      registryThreadStatus(threadPool, objectName.getKeyProperty("name"), mBeanServer);
    }
  }

  public static void registryThreadStatus(ObjectName threadPool, String name, MBeanServer mBeanServer) {
    registryGauge("maxThreads", threadPool, mBeanServer);
    registryGauge("currentThreadCount", threadPool, mBeanServer);
    registryGauge("currentThreadsBusy", threadPool, mBeanServer);
    registryGauge("keepAliveCount", threadPool, mBeanServer);
  }

  public static void registryGauge(String attributeName, ObjectName threadPool, MBeanServer mBeanServer) {
    Probe probe = (Probe) AgentContext.getMetrics().probe();
    String name = createMetricName(attributeName);
    probe.registerGauge(name, new TagsGauge<Number>(name) {
      @Override
      public Number getValue() {
        try {
          return (Number) mBeanServer.getAttribute(threadPool, attributeName);
        } catch (Exception e) { }
        return null;
      }
    });
  }

  // ... ...
}

在测试的时候发现当执行到第 22 行的时候会报错:
java.lang.IllegalAccessError: tried to access class TomcatThreadStatInterceptor$1 from class TomcatThreadStatInterceptor

我们知道 22 行 new TagsGauge<Number>(name) { … } 会创建一个匿名内部类,也就是报错中提到的 class TomcatThreadStatInterceptor$1。但奇怪的是这个匿名内部类和 TomcatThreadStatInterceptor 在同一个包下,默认访问权限下应该是能访问到才对。

既然是访问权限的错,那么就尝试将 22 行的匿名内部类改成 Public 权限的普通内部类:

public class TomcatThreadStatInterceptor {

  // ...

  public static void registryThreadStatus(ObjectName threadPool, String name, MBeanServer mBeanServer) {
    registryGauge("maxThreads", threadPool, mBeanServer);
    registryGauge("currentThreadCount", threadPool, mBeanServer);
    registryGauge("currentThreadsBusy", threadPool, mBeanServer);
    registryGauge("keepAliveCount", threadPool, mBeanServer);
  }

  public static void registryGauge(String attributeName, ObjectName threadPool, MBeanServer mBeanServer) {
    Probe probe = (Probe) AgentContext.getMetrics().probe();
    String name = createMetricName(attributeName);
    probe.registerGauge(name, new TomcatTagsGauge(name, attributeName, threadPool, mBeanServer));
  }

  public static class TomcatTagsGauge extends TagsGauge<Number> {
    private String attributeName;
    private ObjectName threadPool;
    private MBeanServer mBeanServer;
    
    public TomcatTagsGauge(String name, String attributeName, ObjectName threadPool, MBeanServer mBeanServer) {
      super(name);
      this.attributeName = attributeName;
      this.threadPool = threadPool;
      this.mBeanServer = mBeanServer;
    }

    @Override
    public Number getValue() {
      try {
          return (Number) mBeanServer.getAttribute(tpName, attributeName);
      } catch (Exception e) { }
      return null;
    }
  }

  // ... ...
}

结果可以正常的执行,而如果将内部类的 Public 关键字去掉,就会遇到和之前匿名内部类一样的报错。那么究竟是为什么,在同一个包下的内部类无法访问呢?我查到了这篇博客:JVM 对于不同 classLoader 加载的对象之间 default 或 protected 字段的访问限制

然后确认了一下确实如此:

  1. TomcatThreadStatInterceptororg.springframework.boot.loader.LaunchedURLClassLoader 加载
  2. TomcatThreadStatInterceptor$TomcatTagsGaugesun.misc.Launcher$AppClassLoader 加载

为什么会是不同的 Class Loader 呢?这要从 Spring Boot 的 Class Loader 说起。

为什么 Spring Boot 有自己的 Class Loader

我们都知道 JVM 有三个 ClassLoader:

  1. Bootstrap Class Loader 由 C++ 编写,用于加载运行 JVM 所需要的 Java 类。
  2. Extension Class Loader 在 JVM 启动后加载一些扩展的类,用的较少。
  3. System Class Loader 用于加载应用服务所依赖和使用的 Java 类。

那么为什么 Spring Boot 有自己的 Class Loader 呢?可以参考 Spring Boot 官方文档:The Executable Jar Format

Spring Boot 提出了一种可执行的独立 Jar 文件,可以将服务自身的和所依赖的 class 文件打包成一个独立的 Jar 文件,只需要 java -jar example.jar 即可启动服务,便于部署和运行。

为了支持只需要一个独立的 Jar 文件,就需要将服务所依赖的其他 Java 库也打包到这个 Jar 文件中。因为当前 Java 官方并没有统一的嵌套 Jar 的规范,因此 Spring Boot 自己定制并实现了一个嵌套 Jar 的格式,自然也就需要自己的 Class Loader 进行加载。

我们可以用 vim 直接打开一个打包好的 Jar 文件,可以看到 Spring Boot 文档里描述的,所有文件分布在四个不同的目录:

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 |
 +-org/springframework/boot/loader
 |  +- <spring boot loader classes>
 |
 +-BOOT-INF
    +-classes
    |  +- <user classes>
    |
    +-lib
       +- <dependency jars>
  1. META-INF 目录放的是 MANIFEST.MF 文件,描述这个 Jar 包的信息。
  2. 根目录放的是 Spring Boot 框架自身的启动所需的文件
  3. BOOT-INF/classes 放的是应用的 class 文件
  4. BOOT-INF/lib 放的是应用的 class 文件所依赖的其他库

当我们运行 java -jar example.jarSystem Class Loader 会加载 Jar 包根目录里的 Spring Boot 框架的类,然后 Spring Boot JarLauncher 就会通过自己的 LaunchedURLClassLoader 去加载 BOOT-INF 目录下的文件,从而实现应用服务的加载和启动。

这和 IllegalAccessError 有什么关系

从上文我们知道,当使用 Spring Boot 时,所有的应用类和所依赖的其他库类都是由 Spring Boot 的 LaunchedURLClassLoader 进行加载。而我们为了给服务加上监控而引入的 Java Agent 则是由 System Class Loader 进行加载的。可以通过这个图看不同 Class Loader 所加载的类:

Bootstrap ClassLoader
 +- JVM classes
 |
 ▲
Extension ClassLoader
 +- Ext JVM classes
 |
 ▲
System ClassLoader
 +- Java Agent classes
 +- Spring Boot Loader classes
 |
 ▲
Spring Boot LaunchedURLClassLoader
 +- user's classes
 +- dependency classes

通过 Java Agent 修改字节码的方式,可以业务无感知的在代码里插入监控逻辑,但在给 Spring Boot 项目插入监控逻辑的过程中经常会遇到 ClassNotFoundException 异常。举个简单的 HTTP 监控的例子:

public class TomcatInvokeInterceptor {

  @Advice.OnMethodEnter
  public static Object[] onEnter() {
    return new Object[] {System.nanoTime()};
  }

  @Advice.OnMethodExit
  public static void onExit(@Advice.AllArguments Object[] args, @Advice.Enter Object[] enterRets) {
    doExit(args, enterRets);
  }

  public static void doExit(Object[] args, Object[] enterRets) {
    Request request = (Request) args[0];
    Response response = (Response) args[1];
    String api = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
    int status = response.getStatus();
    long start = (long) enterRets[0];
    long duration = System.nanoTime() - start;
    // 上报数据
  }
}

当执行到第 14 行要将 Object 对象强转成 Request 时 JVM 需要加载 Request 类,这时 JVM 会选择由当前类也就是 TomcatInvokeInterceptor 类的 Class Loader 进行加载。

这里就会出现问题,Request 类的字节码在 BOOT-INF/lib 目录下,只会被 LaunchedURLClassLoader 找到;而 TomcatInvokeInterceptor 是 Java Agent 的类,是由 System Class Loader 进行加载。那么当 System Class Loader 尝试去加载 Request 时就会找不到而抛出 ClassNotFoundException异常。

这里比较好的解决方式是让 TomcatInvokeInterceptor 也由 LaunchedURLClassLoader 进行加载,那么就能完全避免这个问题。

而这次遇到的 IllegalAccessError 也是同样的原因造成的,虽然 TomcatThreadStatInterceptor 已经是由 LaunchedURLClassLoader 进行加载,但在访问匿名内部类时因为其在 LaunchedURLClassLoader 中并没有被加载从而访问到上层 Class Loader 所加载的匿名内部类。

又因为匿名内部类 default 访问权限,不同 Class Loader 加载之间无法访问从而导致 IllegalAccessError 错误。

显而易见,有两个解决方式:

  1. 将匿名内部类改成 public 的内部类或者普通类
  2. LaunchedURLClassLoader 也加载这些 $1$2 匿名内部类

相关文章

网友评论

      本文标题:记录一个奇怪的 IllegalAccessError

      本文链接:https://www.haomeiwen.com/subject/ucqrqhtx.html