问题起源

最近在工作中实现一个链路追踪零碎,应用到了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-bytebuddy.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...