乐趣区

关于java:聊聊java的类加载机制

问题起源

最近在工作中实现一个链路追踪零碎,应用到了 javaagent 技术,通过字节码注入技术无侵入的对利用罕用的组件进行链路追踪和监控。我把写好的 javaagent 程序打好包利用到目标程序,当应用 idea 或者 eclipse 启动应用程序时,没有什么问题;因为咱们的我的项目是 springboot 我的项目,个别会应用 spring-boot-maven-plugin 插件进行打包,这时通过 java -javaagent:xxxx.jar -jar xxx.jar 来启动利用,这时会抛出一个异样java.lang.NoClassDefFoundError。这是怎么回事呢?要弄清楚这个问题所在,就必须深刻了解 java 的类加载机制了。

问题复现

讲述这个问题之前,先来用一个简略的例子来复现下这个场景。

  • 创立一个 springboot 利用:agent-springboot-a,简略操作 jedis 客户端
  • 创立一个 javaagent 利用:agent-bytebuddy,应用 bytebutty 框架来操作字节码,拦挡 jedis 的 set 办法。
    代码地址:https://gitee.com/yanghuijava…

将两个我的项目各自打好包后,通过命令以下命令启动利用

java -javaagent:D:\workspace1\agent-tutorial\agent-bytebuddy\target\agent-bytebu
ddy.jar -jar agent-springboot-a-0.0.1-SNAPSHOT.jar

通过浏览器拜访地址

http://localhost:8080/user/login?name=admin&password=123456

这时利用会呈现一个异样:java.lang.NoClassDefFoundError

剖析起因

咱们晓得 java 语言自身给我提供了三品种加载器:

  • Bootstrap ClassLoader:负责加载 java 外围类库 /jre/lib/rt.jar
  • Extension ClassLoader:负责加载 /jre/lib/ext/ 下的 jar 包
  • App ClassLoader:加载零碎变量 CLASSPATH 下的类

如果有必要咱们也能够自定义本人的 classLoader,它们的关系如下:

java 的类加载机制是遵循双亲委派准则的,当某个类加载器要去加载某个 class 时,首先会查找本人有没有加载过,如果有,间接返回;如果没有,本人并不会去加载,而是委托给父类去加载,始终追溯到 Bootstrap ClassLoader,加载不到就顺次回退直到以后类加载器,这时如果还是加载不到,则抛出 java.lang.ClassNotFoundException 异样,简略形容就是(如上图所示):

  • 类的加载是自上而下
  • 类的查找是自下而上

为什么要有须要这种双亲委派机制呢?试想一下,如果没有这个机制,每个类加载器加载了都是本人先去加载,那么如果用户编写一个类跟 java 外围类库的类截然不同(包名和类名一样),这时咱们编写的这个类就会被优先加载,而 java 外围类库的类就没法加载了,这样就篡改了 java 的外围类库。

类加载时,jvm 到底是如何抉择类加载器来加载呢?是这样,每一个加载好的 Class 对象都会记录它的 classLoader 援用,jvm 在加载类时首先应用的是类的类加载器,当作以后类的类加载器。怎么了解呢?举个列子,有一个 A 类,外面存在代码:B b = new B(); 那么 B 类会应用 A 的类加载器作为起始类加载器。

理解下面两个类加载机制,当初咱们来看看异样是怎么产生的?首先 javaagent 永远都是应用 App ClassLoader 来加载,当咱们应用 eclipse 或者 idea 来启动应用程序时,应用的也是 App ClassLoader 来加载的,这种形式启动的 classLoader 和 javaagent 的是一样的,所以会运行的很好。然而,咱们应用 spring-boot-maven-plugin 插件打包利用,通过 java -javaagent:xxxx.jar -jar xxxx.jar 来启动利用时,利用的 classLoader 曾经不再是 App ClassLoader 了,而是应用 springboot 自定义的 LaunchedClassLoader 来加载,来看一下 springboot 打包后目录构造:

这时 classLoader 的类关系如下:

能够看到 springboot 形式打包的利用,main 办法的入口类曾经不是咱们利用本人写的了,而是 org.springframework.boot.loader.JarLauncher 类,这个类会应用 LaunchedClassLoader 来加载咱们利用类,而咱们 javaagent 应用到的利用的类,这时就没法加载了,因为这时 classpath 下没有任何利用的类,App ClassLoader 是没法加载的,也无法访问子 classLoader(LaunchedClassLoader)加载的类,故而抛出 NoClassDefFoundError 的异样。

解决办法

一、第一种形式 (不举荐)
咱们回到 agent-bytebuddy 我的项目的 maven 配置:

......
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
    <scope>provided</scope>
</dependency>
......

jedis 的 maven scope 是 provided,也就是说咱们应用 maven 打包时不会把 jedis 的相干 jar 打入 agent-bytebuddy.jar 中;有下面的剖析咱们晓得,既然是因为 appClassloader 加载不到 jedis 类而引发的异样,那么如果咱们把 jedis 的相干类打进 agent-bytebuddy.jar 中,这时 appClassloader 不就能够加载到么?是的,答案是必定的;咱们去掉 <scope>provided</scope>,从新打包,再次运行agent-springboot-a 我的项目,就没有异样产生了;这时 jedis 的 classLoader 是 APPClassLoader,而非 springboot 的 LaunchedClassLoader。不过仔细的你可能会发现,这时我的项目启动居然没有了日志输入。其实这正是双亲委派机制造成,具体为什么,你能够本人先想想,如果想不明确,能够给我留言。我再给你解答。晓得为什么不举荐这种形式了吧,因为把相干 jar 打进 agent jar 包外面,会对咱们的利用造成影响,而这种影响是不好解决的。

二、第二种形式 (举荐):插件机制
这种形式咱们须要自定义 classLoader,实现形式有点简单,咱们一步步来解说
1、在我的项目agent-bytebuddy,新建一个类AgentClassloader

public class AgentClassloader extends URLClassLoader {public AgentClassloader(URL[] urls, ClassLoader parent) {super(urls,parent);
    }
}

2、新建一个插件接口

public interface IMethodInterceptor {Object before(Object thisObj);
    void after(Object params);
}

3、新建一个我的项目 agent-plugin,写一个类JedisMethodInterceptor 实现 IMethodInterceptor 接口,

public class JedisMethodInterceptor implements IMethodInterceptor {
    @Override
    public Object before(Object thisObj) {Long start = System.currentTimeMillis();
        try{Jedis jedis = (Jedis) thisObj;
            System.out.println(jedis.info());
        }catch (Throwable t){t.printStackTrace();
        }
        return start;
    }

    @Override
    public void after(Object params) {Long end = System.currentTimeMillis();
        Long start = (Long)params;
        System.out.println("耗时:" + (end - start) + "毫秒");
    }
}

应用 maven 命令:mvn clean package打好包, 待前面应用,我的 jar 包地位是:D:\workspace1\agent-tutorial\agent-plugin\target\agent-plugin-0.0.1-SNAPSHOT.jar

4、回到 agent-bytebuddy 我的项目,新建一个 Interceptor 类,代码如下:

public static class Interceptor {

        private IMethodInterceptor methodInterceptor;

        public Interceptor(ClassLoader classLoader){
            try{AgentClassloader myClassLoader = new AgentClassloader(new URL[] {new URL("file:D:\\workspace1\\agent-tutorial\\agent-plugin\\target\\agent-plugin-0.0.1-SNAPSHOT.jar")},
                        classLoader);
                Object plugin = Class.forName("com.yanghui.agent.plugin.JedisMethodInterceptor",
                        true,myClassLoader).newInstance();
                this.methodInterceptor = (IMethodInterceptor)plugin;
            }catch (Throwable t){t.printStackTrace();
            }
        }

        @RuntimeType
        public Object intercept(@This Object obj, @Origin Method method, @AllArguments Object[] allArguments,@SuperCall Callable<?> callable) throws Exception{Object before = this.methodInterceptor.before(obj);
            try{return callable.call();
            }finally {this.methodInterceptor.after(before);
            }
        }
    }

Interceptor 构造方法的实现是要害,咱们应用本人的 AgentClassloader 去加载办法拦截器,留神 AgentClassloader 的构造方法传入了一个 classLoader 对象,这里的 classLoader 是哪里来的呢?请看先这段代码:

public static void premain(String agentArgs, Instrumentation inst) throws Exception {AgentBuilder agentBuilder = new AgentBuilder.Default();
        AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, module) -> {
            builder = builder
                    .method(ElementMatchers.named("set"))
                    .intercept(MethodDelegation.to(new Interceptor(classLoader)));
            return builder;
        };
        agentBuilder.type(ElementMatchers.named("redis.clients.jedis.Jedis")
            )
            .transform(transformer)
            .with(new Listener())
            .installOn(inst);
    }

咱们应用 bytebuddy 来做字节码注入实现拦挡,这个 classLoader 就是 bytebuddy 传过来的,这里咱们要对 redis.clients.jedis.Jedis 这个类进行拦挡,也就是说这个 classLoader 就是加载 Jedis 类的 classLoader,咱们应用的 springboot 我的项目,这里就是LaunchedClassLoader,我用下图来形容这品种加载器的关系:

好了到这里咱们的代码实现曾经实现了,先批改下 agent-bytebuddy 我的项目 pom.xml 文件指定 agent 的 premain 办法入口:

......
<manifestEntries>
   <Premain-Class>com.yanghui.agent.agentBytebuddy.plugin.AgentBootUsePlugin</Premain-Class>
   <Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
......

打包后再次运行 agent-springboot-a 我的项目,发现没有报错了,完满解决。
相熟开源我的项目 skywalking 的读者可能会发现,这就是 skywalking 实现插件机制的外围原理。因为这只是个演示我的项目,所以比拟简陋,后续我会推出手把手教你实现一个链路追踪零碎,到时会教你实现一个通用的插件加载器,一个微内核架构。

演示代码地址:https://gitee.com/yanghuijava…

退出移动版