Implementation of dynamic aspect based on javassist and javaagent

1、 Background introduction

1. Requirement description

The requirement is to add a section of business code before and after a method of a class, or directly replace the business logic of the whole method during program operation, that is, business method customization. Note that it is dynamic change during operation to achieve no intrusion, rather than writing dead pointcuts or logic in the code in advance.

When we get this requirement, we first think of using spring AOP technology, but this method needs to be annotated on the method in advance to intercept, but we don't know which methods to intercept before the service is started. Or intercept all methods directly, but there will be some performance problems more or less. Each time a method is called, it will enter the aspect. It is necessary to judge whether the method needs to be customized. The judgment rules and customized code are generally stored in the cache. At this time, cache queries will be involved, and the performance will certainly be reduced. In view of the above considerations, Java Dynamic bytecode technology is selected for implementation.

2. Dynamic bytecode Technology

Java code can only be executed in the JVM after being compiled into bytecode. Once the bytecode is loaded into the virtual machine, it can be interpreted and executed. Bytecode file (. Class) is an ordinary binary file, which is generated by the java compiler. As long as it is a file, it can be changed. If we parse the original bytecode file with specific rules, modify it or simply redefine it, it can change the code behavior. The advantage of dynamic bytecode technology is to modify it after Java bytecode is generated Line modification to enhance its function, which is equivalent to modifying the binary file of the application.

There are many technologies in Java ecology that can dynamically process bytecode. Two are more popular, one is ASM and the other is javassist.

ASM: it directly operates bytecode instructions with high execution efficiency, but it involves JVM operations and instructions. It requires users to master Java bytecode file format and instructions, which has high requirements for users.

Javassist: it provides a more advanced API with relatively poor execution efficiency, but does not need to master the knowledge of bytecode instructions. It is simple, fast and has low requirements for users.

Considering the simplicity and ease of use, javassist tool is selected to implement it.

3. Technical design

① First, we need a function to scan service classes and methods, so that we can select a method to cut in.

When calling the client service scanning pointcut interface, you need to scan the package name, class name, method name and method parameter list in the service.

② Maintain rules and configure the entry location and business code.

The position can be pre -, post -, or replacement. Customized code classes need to implement the execute method of the icustomizehandler interface to fix the structure.

When cutting into the method, you only need to create the instance object of the handler and then execute the execute method. This method is relatively simple, but it also has some limitations.

In the execute method, if you want to reference other objects in the spring container, you need to obtain them through the ApplicationContext context. Dependency injection cannot be used. If you want to use dependency injection, you also need to process the properties of the class.

③ Maintain the relationship between pointcuts and rules, because one pointcut can maintain multiple rules.

After maintaining the rules and relationships, you need to apply the rules, that is, call the client customized interface to dynamically apply the rules.

4. Preparatory work

① The entry point, customized code, and relationship have been maintained. The customized test service includes org test. demo. app. service. impl. The selectorder method of demoserviceimpl class adds a piece of code before and after the method to print something.

② The code of orderserviceimpl, and then confirm the customization effect by observing the print content of the console.

After introducing the background and preparatory work, let's take a look at how to realize the ability of dynamic section step by step. Next, some necessary knowledge is briefly introduced, and then some core logic in the implementation process is introduced.

2、 Knowledge preparation: javassist

1、Javassist

Javassist is an open source class library for analyzing, editing and creating Java bytecode. Its main advantage is that it is simple and fast. Directly use the form of java coding without understanding the virtual machine instructions, you can dynamically change the class structure or dynamically generate classes.

The most important classes in javassist are classpool, ctclass, ctmethod and ctfield.

Classpool: a container of ctclass objects based on hashtable implementation. The key is the class name and the value is the ctclass object representing the class.

Ctclass: ctclass represents a class. A ctclass (compile time class) object can process a class file. These ctclass objects can be obtained from classpool.

Ctmethods: represents the methods in the class.

Ctfields: represents the fields in the class.

2. Classpool usage

① Get classpool object

② Get class

③ Create a new class

④ Add class search path

Through classpool The classpool obtained by getdefault () uses the JVM's class search path. If the program runs on a web server such as JBoss or tomcat, classpool may not be able to find the user's class because the web server uses multiple class loaders as the system class loader. In this case, classpool must add an additional class search path to search the user's class.

⑤ Avoid memory overflow

If the number of ctclass objects becomes very large (which rarely happens because javassist tries to reduce memory consumption in various ways), classpool can lead to huge memory consumption. To avoid this problem, you can explicitly delete unnecessary ctclass objects from classpool. Or you can use new classpool objects every time.

3. Use of ctclass

Through the ctclass object, you can get a lot of information about the class and modify the class.

① Get class properties

② Type judgment

③ Add class properties

④ Compile class

4. Ctmethod usage

① Get method properties

② Method operation

③ Method internal reference variable

5. Practical application

① Create a new class

② Create proxy method

6. Reference materials

Javassist has rich APIs to operate classes. For other features and uses, please refer to the following articles

Javassist user's Guide (I)

Javassist User Guide (2)

Javassist User Guide (3)

3、 Knowledge preparation: javaagent

For Java programmers, they may have little contact with Java introduction and Java agent. In fact, many of our daily application tools are based on their implementation, such as common hot deployment (jrebel, spring loaded), IDE debug, various online diagnostic tools (btrace, Arthas), etc.

1、Instrumentation

Using Java lang.instrument. Instrumentation, Enables developers to build an application independent agent (agent) is used to monitor and assist programs running on the JVM, and even replace and modify the definitions of some classes. With this function, developers can realize more flexible runtime virtual machine monitoring and Java class operation. This feature actually provides an AOP implementation mode supported at the virtual machine level, so that developers do not need to upgrade the JDK And changes, you can realize some functions of AOP. The greatest function of instrumentation is to dynamically change and operate class definitions.

Some main methods of instrumentation are as follows:

2、Javaagent

Java agent is a special Java program (jar file), which is the client of instrumentation. Unlike ordinary Java programs started through the main method, agent is not a program that can be started separately, but must be attached to a Java application (JVM), run in the same process with it, and interact with the virtual machine through the instrumentation API.

Java agent and instrumentation are inseparable, and they also need to be used together. Because the JVM will inject the instance of instrumentation into the startup method of Java agent as a parameter. Therefore, if we want to use the instrumentation function and get the instrumentation instance, we must use Java agent.

Java agent has two startup opportunities. One is to start the agent program through the - javaagent parameter when the program is started, and the other is to dynamically start the agent program through the attach API in Java tool API during program running.

① Static loading at JVM startup

For the agent loaded during VM startup, instrumentation will pass in the agent program through the premain method, which will be called before the main method of the program is executed. At this time, most Java classes are not loaded ("most" because the agent class itself and its dependent classes are unavoidable and will be loaded first), which is a manipulation of class loading (addtransformer). However, this method has great limitations. Instrumentation is limited to before the main function is executed. At this time, many classes have not been loaded. If you want to inject instrumentation into them, you can't do it.

For example, when the idea starts the debug mode, it starts the debug agent in the form of - Java agent.

② Dynamic loading after JVM startup

For the agent dynamically loaded after VM startup, instrumentation will pass in the agent program through the agentmain method, and agentmain will not be called until the main function starts running.

For example, when Arthas is enabled to diagnose online problems, the agent is dynamically loaded to the target VM through the attach API.

3、MANIFEST. MF

If you want to run the written proxy class, you need to run it in manifest. Exe before you type the jar package Specify the agent entry in MF.

①、MANIFEST. MF

Most jar files contain a meta - inf directory that stores configuration data for packages and extensions, such as security and version information. There will be a manifest MF file, which contains the version, creator and class search path of the jar package. If it is an executable jar package, it will contain the main class attribute, indicating the main method entry.

4、Attach API

Java agent can be loaded after the JVM is started, which is implemented through the attach API. Of course, the attach API is not just for dynamically loading agents. The attach API is actually a tool for cross JVM process communication. It can send certain instructions from one JVM process to another.

Loading agent is only one of the various instructions sent by the attach API, such as jstack printing thread stack, JPS listing Java processes, jmap doing memory dump and other functions, which belong to the instructions that can be sent by the attach API.

The attach API is not a standard API for Java, but a set of extended APIs provided by Sun company to "attach" the agent tool to the target JVM. With it, developers can easily monitor a JVM and run an additional agent.

① Introducing the attach API

When using the attach API, you need to introduce tools jar

When packaging and running, you need to use tools Jar package

② attach agent

5. Reference materials

For more detailed knowledge, please refer to the following articles

Agent implementation based on Java instrument

Talk about Java introduction and related applications

Talk about jar files and manifest MF

4、 Knowledge preparation: JVM class loader

1. Introduction to class loader

Class loader is used to load Java classes into Java virtual machine. Generally speaking, Java virtual machine uses Java classes as follows: Java source program (. Java file) is converted into Java byte code after being compiled by java compiler (. Class file). The class loader is responsible for reading Java byte code and converting it into an instance of java.lang.class class. Each such instance is used to represent a Java class.

Basically all class loaders are Java An instance of the lang.classloader class. java. The basic responsibility of lang. classloader class is to find or generate the corresponding byte code according to the name of a specified class, and then define a Java class from these byte codes, that is, Java An instance of lang.class class.

Class loaders in Java can be roughly divided into two categories: one is provided by the system, and the other is written by Java application developers. Developers can inherit Java Lang. classloader class implements a custom class loader to meet some special needs.

The class loaders provided by the system mainly include the following three:

2. Class loading process - parental delegation model

① Class loader structure

All class loaders except the boot class loader have a parent class loader. The parent class loader of the application class loader is the extension class loader, and the parent class loader of the extension class loader is the boot class loader. In general, the parent class loader of a developer defined class loader is the application class loader.

② Parental delegation model

When the class loader tries to find the bytecode of a class and define it, it will first proxy it to its parent class loader. The parent class loader tries to load the class first. If the parent class loader does not, continue to find the parent class loader, and so on. If the boot class loader does not find it, it will find it from itself. This class loading process is the parental delegation model.

First of all, it should be understood that the Java virtual machine determines whether two Java classes are the same. It depends not only on whether the full name of the class is the same, but also on whether the class loader that loads the class is the same (which can be obtained through class. Getclassloader()). Two classes are equal only if they come from the same class file and are loaded by the same class loader. Classes loaded by different class loaders are incompatible.

The parental delegation model is to ensure the type safety of the Java core library. All Java applications need to reference at least Java Lang. object class, that is, at runtime, Java Lang. object needs to be loaded into the Java virtual machine. If the loading process is completed by the Java application's own class loader, there are likely to be multiple versions of Java Lang. object classes, which are incompatible. Through the parental delegation model, the class loading of the Java core library is uniformly completed by the guided class loader, which ensures that the classes of the same version of the Java core library used by Java applications are compatible with each other.

After successfully loading a class, the class loader will The instance of lang.class class is cached. The next time you request to load this class, the class loader will directly use the cached class instance instead of trying to load it again.

3. Thread context class loader

The thread context class loader can be accessed through Java The method getcontextclassloader () in lang. thread is obtained. You can set the context class loader of the thread through setContextClassLoader (classloader CL). If it is not set through the setContextClassLoader (classloader CL) method, the thread will inherit the context class loader of its parent thread. The context class loader of the initial thread running a Java application is the application class loader. Code running in a thread can load classes and resources through such loaders.

4. Springboot class loader

Since I developed it using springboot (2.0. X) and deployed it on the server in the form of jar, it is necessary to understand the class loading mechanism related to spring boot. Many problems encountered are caused by the class loading mechanism of spring boot.

The executable jar package of springboot, also known as fat jar, is a jar package containing all third-party dependencies. All dependencies except Java virtual machine are embedded in the jar package. It is an all in one jar package. The direct difference between the package generated by the common plug-in Maven jar plugin and the package generated by the spring boot Maven plugin is that the fat jar mainly adds two parts. The first part is the Lib directory, which stores the jar package files that Maven depends on, and the second part is the classes related to the spring boot class loader.

Structure packaged with spring boot Maven plugin

MANIFEST. Contents of MF

From the generated manifest In the MF file, you can see two key information: main class and start class. Note that the startup entry of the program is not the main of the startup class defined in springboot, but jarlauncher #main.

In order to start the springboot program without decompression, in jarlauncher, the jar file under / boot-inf / lib / and / boot-inf / classes / will be read to construct a URL array, and this array will be used to construct the springboot custom class loader launchedurlclassloader, which inherits Java net. Urlclassloader, whose parent class loader is the application class loader.

After the launchedurlclassloader is created, the main function in the startup class we wrote will be started through reflection, and the current thread context class loader will be set to launchedurlclassloader.

5. Javaagent class loader

The code of javaagent is always loaded by the application classloader, which has nothing to do with the real loader of the application code. For example, the code currently running in undertow is loaded by launchedurlclassloader. If - javaagent is added to the startup parameter, the javaagent is still loaded in application classloader.

6. Reference materials

For other in-depth and detailed information, please refer to the following articles:

Explore Java class loader in depth

Deep understanding of Java classloader and its application in javaagent

Java class loading mechanism

Truly understand thread context class loader

Thoroughly analyze the executable principle of springboot jar

Analysis of spring boot application startup principle

5、 Using javassist to scan class methods

First, let's look at how to scan the class and method information under the specified package in the service. Because the source code is not open, only part of the core code logic is posted.

1. Read resources

To read the resource file during the program running, you can inject resourceloader to read it, and metadatareaderfactory can be used to read metadata information from the resource.

Read class metadata information

2. Parsing method information using javassist

6、 Dynamic compilation source code

In the initial rules, we have maintained the source code of a business processing class, but we need to compile it into bytecode before it can be used, so it involves how to dynamically compile the source code.

1、Java Compile API

Javacompiler: represents the java compiler, and the run method performs the compilation operation, Another way of compiling is to generate the compilation task (CompilationTask) first, then to invoke the call method of CompilationTask to perform the compilation task.

Javafileobject: represents a java source file object

Javafilemanager: Java source file management class, which manages a series of javafileobjects

Diagnostic: represents a diagnostic message

Diagnosticlistener: a diagnostic information listener triggered by the compilation process

Dynamically compile related APIs in tools Jar package, so you need to introduce tools. Jar into POM jar

2. Dynamic compilation

The following code first parses the package name and class name from the source code, writes the source file to disk, and then compiles the source code using java compiler. Note that when compiling the same class name again, the name cannot be the same, otherwise the compilation fails, because the JVM has loaded this instance, and a random number can be added to the class name to avoid duplication.

Start the service in the idea. There is no problem with this code. It can be compiled normally. You can see that the class file has also been compiled.

However, once it is run in a jar package, it cannot be compiled normally, and the following errors will occur: the package XXX does not exist, the symbol cannot be found, etc.

In fact, this error is also well understood, javac Run (null, null, javafile. Getabsolutepath()) can be regarded as compiling the source file directly with javac command. If classpath is not specified, other classes referenced in the code cannot be found.

So why is it possible in idea but not in jar package? This is actually because of the particularity of springboot jar. Springboot jar is all in one. Classes and lib are in the jar package. Classes in idea are under the target package and can be accessed directly.

3. Compiling Based on classpath

If so, we can add the files under / boot-inf / classes / and / boot-inf / lib / to the classpath path at compile time.

First, the contents of the jar package cannot be accessed directly. The second method is to unzip the jar package, then splice the paths, and then compile.

① Decompress package

② Splice classpath

③ Compile

The javac command only needs to specify the classpath through the - CP parameter, so that it can be compiled successfully.

4. Elegant dynamic compilation

① Arthas memory compilation

The above method needs to decompress the jar package to get the classpath, otherwise it cannot be compiled, which is not elegant and can only be regarded as an alternative. By referring to the source code of Arthas, it is found that there is a memory compilation module, which can easily realize the ability of dynamic compilation.

By studying its source code, it is found that the bottom layer still uses the API related to Java compiler to complete the compilation. The difference is the way it obtains the reference classes in the source code.

First, inherit forwardingjavafilemanager to implement custom lookup of javafileobjects. Then you can see that it will use the customized packageinternalsfinder to find classes. You can see that it will still find relevant classes from the jar package. More people can read its source code by themselves.

② Use

Firstly, the dependence of Arthas memory compiler is introduced into POM.

Mode of use

5. Reference materials

Arthas Github

Java class runtime dynamic compilation technology

7、 Code entry method

The source code has been compiled into bytecode. Next, let's see how to cut into the method to be intercepted.

1. Load bytecode and define class instance

First, the bytecode needs to be loaded into the JVM to create a class instance before this class can be used.

When toclass does not pass parameters, it actually uses the current context class loader to load bytecode, or it can pass in the class loader itself.

The current context class loader may be different for different containers. I use the undertow container here, and the context class loader is launchedurlclassloader; When using the Tomcat container, the runtime context class loader is Tomcat embedded webappclassloader, and its parent class loader is launchedurlclassloader. When running in idea, the context class loader is appclassloader, that is, the application class loader.

There is a hole to note. When calling during program startup, the context class loader here is launchedurlclassloader; However, when calling during runtime, if the Tomcat container is used, the context class loader here is Tomcat embedded webappclassloader, which is a proxy class loader.

At this time, if you use this class loader to define a class instance, it can be defined successfully, but when you use it later, you will find an error: NoClassDefFoundError.

This is because in the actual request, the context class loader is the launchedurlclassloader, which is the parent class loader of tomcatembeddedwebappclassloader. The class definition is defined in the child class loader. It can't be found in the parent class loader.

Therefore, when calling toclass, you need to pass in the launchedurlclassloader class loader, and you cannot use the subclass loader.

2. Building blocks of code

The simplest way is to directly create an instance object of handler, and then cut into the method for use.

Generated effects:

3. Insert code block

8、 Dynamically create agents and implement class overloading

The method body has been modified. The rest is how to make the JVM overload this class to dynamically change the source code.

1、Javassist HotSwapAgent

Javassist provides an agent for hotswapagent. You can use its redefine method to redefine classes, but this tool is basically unavailable.

First, let's look at javassist util. How hotswapagent overloads classes.

In the redefine method, firstly, the startagent method will be called to dynamically load the agent, and then the class will be redefined through instrumentation, that is, Java lang.instrument. Instrumentation。

In the startagent method, first judge that if the instrumentation already exists, the dynamic agent will not be loaded. If not, first create the agent jar package dynamically, then use virtualmachine attach to the current virtual machine, and then load the agent.

In creating the agent package, first create the mainfest file, and specify premain class and agent class as javassist util. Hotswapagent, and then javassist util. HotSwapAgent. The bytecode file of Java is written to javassist util. HotSwapAgent. Class, and finally called agent jar 。

Hotswapagent premain and agentmain agents are used to obtain an instance of instrumentation, which is passed in by the virtual machine when the agent is loaded.

Agent generated by default Jar is a temporary directory under the user directory, agent The jar directory structure is as follows.

The above is the process of dynamically creating and loading agents. If this function can be used directly, it will be perfect. Unfortunately, it can't.

2. Run in idea mode

First, let's look at the use of javassist. XML in idea util. For the problem of hotswapagent, before we start, let's take a look at several ways to start services in idea.

User local is the same as none: This is the default option. In this way, all dependent jar packages will be spliced and specified through the - classpath parameter. The command line parameters will be very long. If the length of the command line parameter exceeds the OS limit, an error will be reported: command line is too long.

Jar manifest: after splicing all dependent jar packages, create a temporary jar file and write it to meta-inf / manifest.xml In the class path parameter of MF file, and then specify the jar package through the - classpath parameter. The purpose is to shorten the command line.

Classpath file: after splicing all dependent jar packages, write them to a temporary file, and then use COM. Com intellij. rt.execution. Commandlinewrapper to start.

One difference between the three methods is that the online text class loader when they are started is different:

user-local:sun. misc. Launcher$ AppClassLoader@18b4aac2 , application class loader

JAR manifest:sun. misc. Launcher$ AppClassLoader@18b4aac2 , application class loader

classpath file:java. net. urlclassloader@7cbd213e ,urlclassloader

As mentioned earlier, the agent uses the application class loader when loading. Therefore, when starting with user local and jar manifest, the agent can be loaded correctly, and their class loaders are the same. If you use classpath to start, you will report NoClassDefFoundError. This is because the classes of the jar package in javassist are loaded by the urlclassloader class loader, and the application class loader cannot load the Lib class.

3. Run in jar package mode

When we run as a jar package, we will actually report the same error, because the jar package runtime context class loader is org springframework. boot. loader. Launchedurlclassloader@20ad9418 , whose parent class is the application class loader.

To sum up, according to the parent delegation model, the child class loader can load the classes in the parent class loader; However, the parent class loader cannot load classes in the child class loader.

4. Custom agent loading

Because javassist util. The hotswapagent uses the application class loader when loading, so when the agent enters the agentmain method to set the instrumentation variable, it is actually in the application class loader.

The program starts with the hotswapagent loaded by its subclass loader, so there are actually two different instances of hotswapagent class. Although the class name is the same, the class loader used is different. Therefore, the instrumentation instance object is still not available during the program running.

Here I use a simple and crude method to solve this problem:

① Override javassist util. Hotswapagent method (I rewritten it to separate startagent and agentmain)

② Add static setinstrumentation method

③ In the agentmain method, get the context class loader (launchedurlclassloader) of the program runtime

④ Find the javassist. XML file loaded in the program through the launchedurlclassloader util. Hotswapagent class instance

⑤ Call the setinstrumentation method through reflection to set the instrumentation passed in by the JVM.

⑥ After that, we can call hotswapclient. Exe while the program is running Redefine to overload the class.

9、 Results validation and limitations

1. Result verification

You can see that customized code logic has been successfully added before and after the method to be intercepted, or you can dynamically update the code again and re apply the rules. So far, the function of dynamic section has been basically realized.

2. Limitations

① When customizing code, because an object is created and then inserted into the method body in the form of method call, the structure of customized code must be fixed.

② In customized code, spring container objects cannot be directly injected using @ Autowired or other methods. At present, this situation is not handled.

③ Due to the limitations of instrumentation itself, we can only change the method body, not the definition of the method, and cannot add methods and fields to the class. Otherwise, overloading fails.

10、 Attachment: using Arthas to diagnose Java problems

In the process of developing this function, I have a brief understanding of the source code principle of Arthas and how to use Arthas to diagnose some online problems. Here are only some official documents, which are easy to get started.

Arthas is an open source tool for Java developed by Alibaba. It is mainly used to diagnose Java problems! For details, please refer to the official documents.

① Idea installation plug-in: use the cloud toolkit plug-in to diagnose the remote server with one click of Arthas

② Getting started: Arthas quick start

③ Command list: Arthas command list

④ Attach failed: attach error

⑤ GitHub source code: Arthas GitHub

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>