Question origin
Recently, I implemented a link tracking system in my work, which uses JavaAgent technology to track and monitor the commonly used components of the application without intruding through bytecode injection technology. I package the written JavaAgent program and apply it to the target program. When using IDEA or Eclipse to launch the application, there is no problem. Since our project is a springboot project, we usually use the spring-boot-maven-plugin to package it. In this case, we use java-javaAgent: xxx.jar-jar xxx.jar to launch the application. When an exception is thrown Java. Lang. NoClassDefFoundError. What’s going on here? To understand this problem, you need to have a deep understanding of Java’s classloading mechanism.
Problem of repetition
Before I get to that, let’s replay the situation with a simple example.
- Create a Spring Boot application: agent-springboot-a, simply operate the Jedis client
- Create a JavaAgent application: Agent-ByteBuddy that uses the ByteButty framework to manipulate bytecode and intercept Jedis’ set method. The code address: https://gitee.com/yanghuijava…
After the two projects have been individually packaged, launch the application by commanding the following command
java -javaagent:D:\workspace1\agent-tutorial\agent-bytebuddy\target\agent-bytebu ddy.jar -jar Agent - springboot - a - 0.0.1 - the SNAPSHOT. The jar
Access the address through the browser
http://localhost:8080/user/login?name=admin&password=123456
The application will show an exception:java.lang.NoClassDefFoundError
The analysis reason
We know that the Java language itself provides me with three types of loaders:
- Bootstrap ClassLoader: It is responsible for loading the Java core class libraries /jre/lib/rt.jar
- Extension ClassLoader: The Extension ClassLoader is responsible for loading the JAR under /jre/lib/ext/
- App ClassLoader: Loads classes under the system variable CLASSPATH
If necessary, we can also customize our own classLoader, and their relationship is as follows:
Java’s class loading mechanism follows the principle of parental delegation. When a class loader wants to load a class, it will first look up whether it has loaded it, and return if it has. If not, I didn’t go to load, but is delegated to the parent class to load, has been traced back to the Bootstrap this, in turn back until the load is less than the current class loader, if still cannot load at this moment, it throws the Java. Lang. ClassNotFoundException is unusual, The simple description is (as shown in the figure above) :
- Class loading is top-down
- Class lookups are bottom-up
Why is this parental delegation mechanism needed? Imagine if without this mechanism, each class loader loaded is loaded first, so if the user to write a class with Java class the same core class libraries (package name and class name), then we write this class will be priority to load, while the Java core library classes can not be loaded, thus to tamper with the core of the Java class library.
When a class is loaded, how exactly does the JVM choose the class loader to load? Each loaded Class object records its ClassLoader reference. The JVM first uses the Class loader as the Class loader for the current Class when loading a Class. How do you understand that? For example, there is A class A that contains code: B B = new B(); Then class B will use A’s class loader as the starting class loader.
Knowing the above two classloading mechanisms, let’s now look at how exceptions occur. First of all, JavaAgent always uses the App ClassLoader to load. When we use Eclipse or IDEA to launch the application, we also use the App ClassLoader to load. This way the ClassLoader is launched in the same way as the JavaAgent, so it works fine. However, we usespring-boot-maven-plugin
Plugins package applications through java-javaAgent: XXXX. Jar jar – XXXX. Jar to start the application, the application of this is no longer this App, but instead USES springboot custom LaunchedClassLoader to load, Take a look at the Spring Boot packaged directory structure:
The class relationship of the ClassLoader is as follows:
Can see springboot way packaging applications, the main method of entrance class has not been our application to write their own, but the org. Springframework.. The boot loader. JarLauncher class, This class will use the launchedClassLoader to load our application class, and the application class that our JavaAgent is using will not be loaded because there are no application classes in the classpath, so the App ClassLoader will not be able to load. You can’t access classes loaded by the child ClassLoader (LaunchedClassLoader), so you throw an exception to the NoClassDefFoundError.
The solution
Let’s go back to the Agent-ByteBuddy project’s Maven configuration:
. < the dependency > < groupId > redis. Clients < / groupId > < artifactId > jedis < / artifactId > < version > 3.3.0 < / version > <scope>provided</scope> </dependency> ......
Jedis’ Maven scope provides that you do not package Jedis’ related jars into agent-bytebuddy.jar. If the Jedis class is not loaded by the AppClassLoader, the Jedis class will be loaded by the AppClassLoader. If the Jedis class is not loaded by the AppClassLoader, the Jedis class will be loaded by the AppClassLoader. Yes, the answer is yes;
provided
In this way, we need to customize the classLoader, the implementation way is a little complicated, we will explain step by step 1, in the project Agent-ByteBuddy, create a new class AgentClassLoader
public class AgentClassloader extends URLClassLoader { public AgentClassloader(URL[] urls, ClassLoader parent) { super(urls,parent); }}
2. Create a new plug-in interface
public interface IMethodInterceptor {
Object before(Object thisObj);
void after(Object params);
}
Create an Agent-Plugin, and write a class named JedisMethodInterceptor to implement the 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(" Time: "+ (end-start) +" milliseconds "); }}
Use maven command: MVN clean package, to use later, my JAR location is: D: \ called workspace1 \ agent – tutorial \ agent – plugin \ target \ agent – the plugin – 0.0.1 – the SNAPSHOT. The jar
4. Back in the Agent-ByteBuddy project, create a new Interceptor class with the following code:
public static class Interceptor { private IMethodInterceptor methodInterceptor; public Interceptor(ClassLoader classLoader){ try{ AgentClassloader myClassLoader = new AgentClassloader(new URL[] { new URL (file: D: \ \ "called workspace1 \ \ agent - tutorial \ \ agent - plugin \ \ target \ \ agent - the plugin - 0.0.1 - the SNAPSHOT. Jar")}, this); 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); }}}
The implementation of the Interceptor constructor is the key. We use our own AgentClassLoader to load the method Interceptor. Note that the AgentClassLoader constructor passes in a classLoader object. Take a look at this code first:
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);
}
We use ByteBuddy to do the bytecode injection to implement the interception. This classLoader is passed by ByteBuddy. Here we need to do the interceptionredis.clients.jedis.Jedis
This class intercepts, so this classLoader is the classLoader that loads the Jedis class, and we use the SpringBoot project, which is hereLaunchedClassLoader
, I use the following diagram to describe the class loader relationship:
The pOM.xml file of the agent-bytebuddy project will specify the agent’s premain method entry:
. <manifestEntries> <Premain-Class>com.yanghui.agent.agentBytebuddy.plugin.AgentBootUsePlugin</Premain-Class> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> ......
After packaging, run agent-springboot-a project again, and find no error report, perfect solution. Readers familiar with the open source project Skywalking may find that this is the core principle of Skywalking’s implementation of the plug-in mechanism. As this is just a demonstration project, it is relatively simple. Later I will introduce a manual way to teach you how to implement a link tracking system, and then I will teach you how to implement a general plug-in loader and a micro-kernel architecture.
Demo code address: https://gitee.com/yanghuijava…