奇怪的 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 字段的访问限制
然后确认了一下确实如此:
-
TomcatThreadStatInterceptor
由org.springframework.boot.loader.LaunchedURLClassLoader
加载 -
TomcatThreadStatInterceptor$TomcatTagsGauge
由sun.misc.Launcher$AppClassLoader
加载
为什么会是不同的 Class Loader 呢?这要从 Spring Boot 的 Class Loader 说起。
为什么 Spring Boot 有自己的 Class Loader
我们都知道 JVM 有三个 ClassLoader:
-
Bootstrap Class Loader
由 C++ 编写,用于加载运行 JVM 所需要的 Java 类。 -
Extension Class Loader
在 JVM 启动后加载一些扩展的类,用的较少。 -
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>
-
META-INF
目录放的是MANIFEST.MF
文件,描述这个 Jar 包的信息。 - 根目录放的是 Spring Boot 框架自身的启动所需的文件
-
BOOT-INF/classes
放的是应用的 class 文件 -
BOOT-INF/lib
放的是应用的 class 文件所依赖的其他库
当我们运行 java -jar example.jar
后 System 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
错误。
显而易见,有两个解决方式:
- 将匿名内部类改成
public
的内部类或者普通类 - 让
LaunchedURLClassLoader
也加载这些$1
、$2
匿名内部类
网友评论