background

specifications

A requirement is to add a piece of business code before and after a method of a class during program execution, or to directly replace the business logic of the entire method, that is, business method customization. Note that changes are made dynamically at run time, making them invasive-free, rather than dead pointcuts or logic written into the code beforehand.

The first thing that came to mind with this requirement was to use Spring AOP technology, but this approach required intercepting methods with annotations beforehand, and we didn’t know which methods to intercept until the service started. Or intercept all methods directly, but it is more or less have some performance problems, every time a method call, will enter the aspect, needs to determine whether this method would need to be a guest to customize, and judging rules and customization code generally stored in the cache, then also cache the query, the performance would be reduced. In view of the above consideration, Java dynamic bytecode technology is chosen to implement.

Dynamic bytecode technology

Java code is compiled into bytecode before it can be executed in the JVM, and bytecode can be interpreted and executed once it is loaded into the virtual machine. Bytecode files (.class) are plain binary files that are generated by the Java compiler. As long as a file can be changed, if we parse the original bytecode file with specific rules, modify it or redefine it, we can change the code behavior. The advantage of dynamic bytecode technology is that after Java bytecode is generated, it can be modified to enhance its functionality, which is equivalent to modifying the binaries of an application.

There are a number of technologies in the Java ecosystem that can handle bytecode dynamically. Two of the most popular are ASM and Javassist.

  • ASM: Directly operate bytecode instructions, which has high execution efficiency. However, it involves the operations and instructions of JVM, requiring users to master Java bytecode file formats and instructions, which has high requirements on users.
  • Javassist: provides more advanced API, execution efficiency is relatively poor, but do not need to master the knowledge of bytecode instructions, simple, fast, low requirements for users.

For ease of use, the Javassist tool is chosen.

Technical design

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

Invoking the client service scan pointcut interface scans out the package name, class name, method name, and method parameter list in the service.

2. Maintain rules and configure the position and business code to cut into.

Positions can be front, post, or replace. Custom code classes need to implement the execute method of the ICustomizeHandler interface to fix the structure.

To access the method, simply create an instance of this handler and execute the execute method. This approach 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 get them from the ApplicationContext, not dependency injection. If you want to use dependency injection, you also need to handle the class properties.

Maintain the relationship between pointcuts and rules, as a 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.

The preparatory work

1, the breakthrough point, customization code, and has been maintaining good relations, customized test org. The in – service service. Test. The demo app. Service. The impl. DemoServiceImpl selectOrder method in a class, Add a bit of code before and after the method to print something.

2. The code for OrderServiceImpl, and then verify customization by observing the console print.

package org.test.demo.app.service.impl;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.test.demo.app.service.DemoService;
import org.test.demo.domain.entity.Order;
import org.test.demo.domain.repository.OrderRepository;

@Service
public class DemoServiceImpl implements DemoService {

    private static final Logger LOGGER = LoggerFactory.getLogger(DemoServiceImpl.class);

    private final OrderRepository orderRepository;

    public DemoServiceImpl(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public List<Order> selectOrder(String orderNumber, String status) {

        Order params = new Order();
        params.setOrderNumber(orderNumber);
        params.setStatus(status);
        List<Order> orders = orderRepository.select(params);
        LOGGER.info("order size is {}", orders.size());

        returnorders; }}Copy the code

With the background and preparation covered, let’s take a look at implementing the dynamic aspect capability step by step. Next, I will briefly introduce some necessary knowledge, and then introduce some core logic in the implementation process.

Knowledge preparation: Javassist

Javassist

Javassist is an open source library for analyzing, editing, and creating Java bytecode. Its main advantages are simplicity and speed. You can dynamically change the structure of a class, or generate a class on the fly, using Java encoded form directly without needing to understand virtual machine instructions.

The most important classes in Javassist are ClassPool, CtClass, CtMethod, and CtField.

  • ClassPool: a hashtable-based CtClass object container where the key is the class name and the value is the CtClass object representing the class.
  • CtClassCtClass represents a class. A CtClass (compile-time class) object can handle a class file. These CtClass objects are available from ClassPool.
  • CtMethods: represents a method in a class.
  • CtFields: represents a field in a class.

ClassPool use

1. Get the ClassPool object

// Get the ClassPool object, using the default ClassPool
ClassPool pool = new ClassPool(true);
// The effect is the same as new ClassPool(true)
ClassPool pool1 = ClassPool.getDefault();
Copy the code

2. Get the class

If CtClass is not found, an exception will be thrown
CtClass ctClass = pool.get("org.test.demo.DemoService");
// Get CtClass by class name. Null is returned if no exception is found
CtClass ctClass1 = pool.getOrNull("org.test.demo.DemoService");
Copy the code

Create a new class

// Copy a class and create a new class
CtClass ctClass2 = pool.getAndRename("org.test.demo.DemoService"."org.test.demo.DemoCopyService");
// Create a new class with the class name
CtClass ctClass3 = pool.makeClass("org.test.demo.NewDemoService");
// Create a new class through the file stream. Note that the file must be a compiled class file, not a source file.
CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("./customize/DemoBeforeHandler.class")));
Copy the code

4. Add class search path

The ClassPool obtained via classpool.getDefault () uses the JVM’s class search path. If the program is running on a Web server such as JBoss or Tomcat, ClassPool may not be able to find the user’s classes because the Web server uses multiple classloaders as system classloaders. In this case, ClassPool must add additional class search paths to find the user’s classes.

// Insert the class search path before the search path
pool.insertClassPath(new ClassClassPath(this.getClass()));
// Add the class search path to the search path
pool.appendClassPath(new ClassClassPath(this.getClass()));
// use a directory as the class search path
pool.insertClassPath("/usr/local/javalib");
Copy the code

5. 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 cause a significant memory consumption. To avoid this problem, you can explicitly remove unnecessary CtClass objects from the ClassPool. Or use a new ClassPool object every time.

// Remove the CtClass object from the ClassPool
ctClass.detach();
// You can also create a new ClassPool at a time instead of classpool.getdefault () to avoid running out of memory
ClassPool pool2 = new ClassPool(true);
Copy the code

CtClass use

CtClass objects allow you to get a lot of information about classes and modify them.

1. Get class attributes

/ / the name of the class
String simpleName = ctClass.getSimpleName();
/ / class name
String name = ctClass.getName();
/ / package name
String packageName = ctClass.getPackageName();
/ / interface
CtClass[] interfaces = ctClass.getInterfaces();
/ / a derived class
CtClass superclass = ctClass.getSuperclass();
// Get the bytecode file, which can be bytecode level manipulation through the ClassFile object
ClassFile classFile = ctClass.getClassFile();
// Gets a method with arguments, the second argument being an array of arguments, of type CtClass
CtMethod ctMethod = ctClass.getDeclaredMethod("selectOrder".new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
// Get the field
CtField ctField = ctClass.getField("orderRepository");
Copy the code

2. Type judgment

// Determine the array type
ctClass.isArray();
// Determine the native type
ctClass.isPrimitive();
// Determine the interface type
ctClass.isInterface();
// Determine the enumeration type
ctClass.isEnum();
// Determine the annotation type
ctClass.isAnn
Copy the code

3. Add class attributes

// Add an interfacectClass.addInterface(...) ;// Add the constructorctClass.addConstructor(...) ;// Add a fieldctClass.addField(...) ;// Add methodctClass.addMethod(...) ;Copy the code

4. Compile classes

// Compile into a bytecode file, load the class using the current thread context class loader, and throw an exception if the class already exists or fails to compile
Class clazz = ctClass.toClass();
// Edit to a bytecode file, returning an array of bytes
byte[] bytes = ctClass.toBytecode();
Copy the code

CtMethod use

1. Get method properties

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
/ / the method name
String methodName = ctMethod.getName();
// Return type
CtClass returnType = ctMethod.getReturnType();
/ / the method parameters, method parameter List format is obtained by this way: com. Test. TestService. SelectOrder (Java. Lang. String, Java. Util. List, com. Test. The Order)
ctMethod.getLongName();
// method signature format :(Ljava/lang/String; Ljava/util/List; Lcom/test/Order;) Ljava/lang/Integer;
ctMethod.getSignature();

// Get the method parameter name
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// The first argument to a non-static member function is this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
    argKeys.add(attr.variableName(i));
}
Copy the code

2. Method operation

// Insert the code block before the method body
ctMethod.insertBefore("");
// Insert code block after method body
ctMethod.insertAfter("");
// Insert a code block after a line of bytecode
ctMethod.insertAt(10."");
// Add parameters
ctMethod.addParameter(CtClass);
// Set the method name
ctMethod.setName("newName");
// Set the method body
ctMethod.setBody("");
Copy the code

3. Methods reference variables internally

The practical application

Create a new class

public static void main(String[] args) throws Exception {
    ClassPool pool = new ClassPool(true);

    // Create an implementation class for IHello
    CtClass newClass = pool.makeClass("org.test.HelloImpl");
    // Add an interface
    newClass.addInterface(pool.get(IHello.class.getName()));
    // Return type Void
    CtClass returnType = pool.get(void.class.getName());
    / / parameters
    CtClass[] parameters = new CtClass[]{ pool.get(String.class.getName()) };
    // Define the method
    CtMethod method = new CtMethod(returnType, "sayHello", parameters, newClass);
    // Method body code block, must be wrapped with {} code
    String source = "{" +
                        "System.out.println(\"hello \" + $1);"
                  + "}"
                    ;
    // Set the method body
    method.setBody(source);
    // Add method
    newClass.addMethod(method);
    // Compile and convert to Class bytecode objects
    Class helloClass = newClass.toClass();

    IHello hello = (IHello) helloClass.newInstance();
    hello.sayHello("javassist");
}
Copy the code

2. Create proxy methods

public static void main(String[] args) throws Exception {
    ClassPool pool = new ClassPool(true);

    CtClass targetClass = pool.get("com.lyyzoo.test.bytecode.javassist.service.HelloServiceImpl");

    CtMethod method = targetClass.getDeclaredMethod("sayHello");

    // The copy method generates a new proxy method
    CtMethod agentMethod = CtNewMethod.copy(method, method.getName()+"$agent", targetClass, null);
    agentMethod.setModifiers(Modifier.PRIVATE);
    // Add method
    targetClass.addMethod(agentMethod);
    // Build a new method body and use the proxy method
    String source = "{"
            + "System.out.println(\"before handle >  ...\" + $type);"
            + method.getName() + "$agent($$);"
            + "System.out.println(\"after handle ... \ ");"
            + "}"
            ;
    // Set the method body
    method.setBody(source);
    // Duplicate Class definition.... is not redefined if the class is already loaded
    targetClass.toClass();

    / / using javassist. Util. HotSwapAgent redefine classes. -xxaltJVM = dcEVm-javaAgent :E:\hotswap-agent-1.3.0.jar
    //HotSwapAgent.redefine(HelloServiceImpl.class, targetClass);

    IHello hello = new HelloServiceImpl();
    hello.sayHello("javassist");
}
Copy the code

Data reference

Javassist has a rich API for manipulating classes. See the following article for additional features and usage.

Javassist User Guide (1)

Javassist User Guide (2)

Javassist User Guide (3)

Preparation: Javaagent

For Java programmers, techniques like Java Intrumentation, Java Agent, and so on May be rare. In fact, many of the tools we use on a daily basis are based on their implementation, such as the common hot deployment (JRebel, spring-loaded), IDE Debug, various on-line diagnostic tools (BTrace, Arthas), and so on.

Instrumentation

Using Java. Lang. Instrument, Instrumentation, allowing developers to build a independent of the applications of Agent (Agent), is used to monitor and assist the run on the JVM program, even can replace and modify the definition of some class. With this capability, developers can implement more flexible runtime virtual machine monitoring and Java class manipulation, which in effect provides a virtual-machine-supported implementation of AOP that allows developers to implement some of THE functionality of AOP without making any upgrades or changes to the JDK. The best use of Instrumentation is to define dynamic changes and operations for classes.

Some of the main methods of Instrumentation are as follows:

public interface Instrumentation {
    /** * Register a Transformer and any class loads from then on will be blocked by Transformer. The transform method of ClassFileTransformer can modify the bytecode of a class directly, but only the body of the method. It cannot change the method signature, add or delete the method/class member attributes */
    void addTransformer(ClassFileTransformer transformer);

    /** * retriggers class loading for classes already loaded by the JVM and redecorates the classes using ClassFileTransformer registered above. * /
    void retransformClasses(Class
       ... classes) throws UnmodifiableClassException;

    /** * redefines the class to pass processing results (bytecode) directly to the JVM instead of using Transformer modifications. * Calling this method can also only modify the method body, but cannot change the method signature, add or delete method/class member attributes */
    void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;

    /** * gets the size of an object */
    long getObjectSize(Object objectToSize);

    /** * Add a JAR to the classpath of the Bootstrap classloader */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /** * Add a JAR to the system ClassLoader classpath */
    void appendToSystemClassLoaderSearch(JarFile jarfile);

    /** * gets all class objects currently loaded by the JVM */
    Class[] getAllLoadedClasses();
}
Copy the code

Javaagent

The Java Agent is a special Java program (Jar file) that is the client of Instrumentation. Different from ordinary Java programs started by main method, Agent is not a separate program can be started, and must be attached to a Java application program (JVM), and it runs in the same process, through Instrumentation API and virtual machine interaction.

Java Agent and Instrumentation are inseparable, and both need to be used together. Because the JVM will inject the Instrumentation instance as a parameter into the Java Agent startup method. So if we want to use Instrumentation and get Instrumentation instances, we have to go through the Java Agent.

The Java Agent has two startup opportunities, one is to start the agent with the -JavaAgent parameter when the program is started, and the other is to dynamically start the agent with the Attach API in the Java Tool API while the program is running.

1. The JVM is statically loaded at startup

For agents loaded at VM startup, Instrumentation is passed into the agent through the premain method, which is called before the main method is executed. Most Of the Java classes are not loaded at this point (” most “because the Agent class itself and its dependent classes will inevitably be loaded first), which is a good opportunity to manipulate addTransformer. However, this approach has great limitations, Instrumentation is only before the execution of the main function, at this time many classes have not been loaded, if you want to inject Instrumentation can not be done.

/** * agentArgs is a program argument derived from premain and passed in via -javaAgent. This parameter is a string. If the program has more than one parameter, the program must parse the string itself. * inst is a Java. Lang. Instrument. The instance of Instrumentation and automatic passed by the JVM. * /
public static void premain(String agentArgs, Instrumentation inst) {}/** * Premain with Instrumentation has a higher priority than premain without Instrumentation. * If a premain with Instrumentation parameter exists, the premain without this parameter is ignored. * /
public static void premain(String agentArgs) {}Copy the code

For example, when IDEA starts the debug mode, the debug agent is started as -JavaAgent.

2. Dynamic loading after JVM startup

/** * agentArgs is a program parameter obtained by the agentMain function and passed in when attach is attached. This parameter is a string. If the program has more than one parameter, the program must parse the string itself. * inst is a Java. Lang. Instrument. The instance of Instrumentation and automatic passed by the JVM. * /
public static void agentmain(String agentArgs, Instrumentation inst) {}/** * AgentMain with Instrumentation has a higher priority than AgentMain without Instrumentation. * If an AgentMain with Instrumentation parameter exists, agentMain without this parameter is ignored. * /
public static void agentmain(String agentArgs) {}Copy the code

This approach can be used for example by attaching the API to dynamically load agents to the target VM when Arthas is enabled to diagnose online problems.

MANIFEST.MF

To run a written proxy class, you need to specify the proxy entry in the manifest.mf file before you jar it.

1, the MANIFEST. MF

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

For example, the manifest.mf file in the Jar package packaged by the MVN clean package command shows the Jar version, creator, SpringBoot version, program entry, class search path, and other information.

2. Parameters related to agent

  • Premain-ClassThis property specifies the proxy class,The class that contains the premain method.
  • Agent-Class: The JVM loads proxies dynamically. This property specifies the proxy class,The class that contains the AgentMain method.
  • Boot-class-path: Sets the list of paths searched by the Boot classloader, separated by one or more Spaces.
  • Can-Redefine-Classes: Boolean value (true or false).Whether the classes required by this proxy can be redefined.
  • Can-Retransform-Classes: Boolean value (true or false).Whether classes required by this proxy can be reconverted.
  • Can-Set-Native-Method-Prefix: Boolean value (true or false).Whether the native method prefix required by this proxy can be set.

Attach API

The Java Agent can be loaded after the JVM has started, via the Attach API. Of course, the Attach API is not just for dynamically loading agents. The Attach API is actually a cross-JVM process communication tool that can send instructions from one JVM process to another.

Loading agent is only one of the various instructions sent by the Attach API. Functions such as jStack printing thread stack, JPS listing Java processes, jMAP doing memory dump, etc., all belong to the instructions sent by the Attach API.

The Attach API is not a standard Java API, but an extended set of APIS provided by Sun for “attaching” proxy programs to target JVMS. With it, developers can easily monitor a JVM and run an additional agent.

1. Introduce the Attach API

When using the Attach API, you need to import tools.jar

<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
Copy the code

When packaging runs, you need to package tools.jar into it

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <includeSystemScope>true</includeSystemScope>
            </configuration>
        </plugin>
    </plugins>
</build>
Copy the code

2, attach the agent

// Related classes such as VirtualMachine are located in tools.jar in the JDK
VirtualMachine vm = VirtualMachine.attach("1234");  // 1234 indicates the target JVM process PID
try {
    vm.loadAgent("... /agent.jar");    // Specify the jar package path for the agent and send it to the target process
} finally {
    vm.detach();
}
Copy the code

Data reference

For more detailed knowledge, please refer to the following articles

Agent implementation based on Java Instrument

Talk about Java Intrumentation and related applications

Talk about JAR files and manifest.mf

Knowledge preparation: JVM class loaders

Introduction to class loaders

The Class loader is used to load Java classes into the Java virtual machine. In general, Java virtual machines use Java classes as follows: Java source programs (.java files) are converted to Java bytecode (.class files) after being compiled by the Java compiler. The classloader is responsible for reading the Java bytecode and converting it into an instance of the java.lang.Class Class. Each such instance is used to represent a Java class.

Basically all classloaders are an instance of the java.lang.ClassLoader class. The basic responsibility of the java.lang.ClassLoader Class is to find or generate bytecode for a given Class name, and then define a Java Class, an instance of the java.lang.ClassLoader Class, from that bytecode.

Class loaders in Java can be roughly divided into two classes, one is provided by the system, and the other is written by Java application developers. Developers can implement custom class loaders by inheriting java.lang.ClassLoader classes to meet some special needs.

The system provides the following three class loaders:

  • Bootstrap ClassLoader: responsible for the$JAVA_HOME/libor-XbootclasspathThe file (identified by filename, such as rt.jar) below the specified path is loaded into the virtual machine memory. It is used to load the Java core library, is implemented in native code, does not inherit from java.lang.ClassLoader, the boot ClassLoader cannot be directly referenced by Java code.
  • Extension ClassLoader: Responsible for loading$JAVA_HOME/lib/extFiles in a directory, orjava.ext.dirsThe class library of the path specified by the system variable, which is used to load Java extension libraries.
  • Application ClassLoader: is generally the default loader of the system, which is based on the Java application classpath (CLASSPATH) to load the Java class. General Java application classes are completed by it to load, can be throughClassLoader.getSystemClassLoader()To get it.

Class loading process – Parent delegate model

1. Class loader structure

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

2. Parental delegation model

When the class loader tries to find the bytecode of a class and define it, it will first delegate to its parent class loader, which will try to load the class first. If the parent class loader does not, it will continue to look for the parent class loader, and so on. If the boot class loader cannot find it, it will look from itself. This class loading process is the parent delegate model.

First of all, the Java virtual machine determines whether two Java classes are the same not only by having the same full name of the class, but also by having the same classloader that loads the class (available through class.getClassLoader()). Two classes are equal only if they come from the same Class file and are loaded by the same classloader. Classes loaded by different class loaders are incompatible.

The parental delegation model is designed to keep Java core libraries type safe. All Java applications need to reference at least the Java.lang. Object class, meaning that the java.lang.Object class needs to be loaded into the Java virtual machine at runtime. If the loading process is done by the Java application’s own classloader, it is likely that there are multiple versions of the Java.lang. Object class that are incompatible with each other. Through the parental delegation model, the class loading work of Java core library is uniformly completed by the bootstrap class loader, which ensures that Java applications are using the same version of Java core library classes and are compatible with each other.

When a Class is successfully loaded, the Class loader caches the resulting instance of the Java.lang. Class Class. The next time the class is loaded, the class loader uses the cached instance of the class without attempting to load it again.

Thread context class loader

Thread context classloaders are available through the method getContextClassLoader() in java.lang.Thread, and can be set by setContextClassLoader(ClassLoader CL) for a Thread. If not set by the setContextClassLoader(ClassLoader CL) method, the thread inherits its parent thread’s context ClassLoader. The context class loader for the initial thread on which a Java application runs is the application class loader. Code running in a thread can load classes and resources through this type of loader.

SpringBoot class loader

Since I developed using SpringBoot (2.0.x) and deployed it as a JAR on the server, it is necessary to understand the class loading mechanism related to SpringBoot. Many of the problems encountered are caused by the class loading mechanism of SpringBoot.

The executable JAR package of SpringBoot, also known as the FAT JAR, is an all-in-one JAR package containing all third-party dependencies. The JAR package contains all dependencies except the Java VIRTUAL machine. Ordinary plugin maven – jar – generated by the plugin package and spring – the boot – maven – direct difference between package is generated by the plugin, fat increased the two main parts in the jar, the first part is the lib directory, deposit is maven dependent jar package files, The second part is the Spring Boot classloader-related classes.

Using the spring-boot-Maven-plugin package structure:

├ ─ the BOOT - INF │ ├ ─ classes │ │ │ application. Yml │ │ │ the bootstrap, yml │ │ │ │ │ ├ ─ org │ │ │ └ ─ sunny │ │ │ └ ─ demo │ │ │ │ Class │ │ ├ ─ ├ ─ class-imp. Class │ │ ├ ─ imp. Class │ │ ├ ─ imp. Class │ │ ├ ─ imp. │ ├ ─ meta-inf │ │ MANIFEST. The MF │ │ spring - autoconfigure - metadata. The properties │ │ │ └ ─ maven │ └ ─ org. Sunny │ └ ─ sunny - demo │ Pom. The properties │ pom. XML │ └ ─ org └ ─ springframework └ ─ boot └ ─ loader │ JarLauncher. Class │ LaunchedURLClassLoader. Class │ . │ ├─ Archive │ archive. Class │ ExplodedArchive. Class │....... │ ├─ Data │ RandomAccessData. Class │....... │ ├─ Jar │ ├.class │ jarfile.class │..... │ └ ─ util SystemPropertyUtils. ClassCopy the code

Manifest.mf contents:

From the generated manifest.mf file, you can see two key pieces of information main-class and start-class. The startup entry of the program is not main as defined in SpringBoot, but JarLauncher#main.

In order to start the SpringBoot program without decompressing it, JarLauncher will read jar files in/boot-INF /lib/ and construct an array of URLS in/boot-INF /classes/. This array is used to construct SpringBoot’s custom LaunchedURLClassLoader, which inherits from java.net.URLClassLoader and whose parent is the application classloader.

Once the LaunchedURLClassLoader is created, reflection launches the main function in the launcher class we wrote and sets the current thread context classloader to LaunchedURLClassLoader.

Javaagent class loader

Javaagent code is always loaded by the Application ClassLoader, regardless of the actual loader of the Application code. For example, the code currently running in Undertow is loaded by the LaunchedURLClassLoader. If the startup parameter is -JavaAgent, the JavaAgent is still loaded by the Application ClassLoader.

Data reference

For further details, please refer to the following articles:

Delve into Java class loaders

In-depth understanding of Java ClassLoader and its application in JavaAgent

Java class loading mechanism

Really understand thread context classloaders

Thoroughly dialysis SpringBoot JAR executable principles

Spring Boot Application startup principle analysis

Use 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 posted part of the core code logic.

Read the resource

To read Resource files while the program is running, inject ResourceLoader to read them, and MetadataReaderFactory can be used to read metadata information from the Resource.

public class DefaultApiScanService implements ResourceLoaderAware.InitializingBean {

    private ResourceLoader resourceLoader;
    private ResourcePatternResolver resolver;
    private MetadataReaderFactory metadataReader;

    @Override
    public void setResourceLoader(@NotNull ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void afterPropertiesSet(a) throws Exception {
        Assert.notNull(this.resourceLoader, "resourceLoader should not be null");
        this.resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        this.metadataReader = newMethodsMetadataReaderFactory(resourceLoader); }}Copy the code

Read class metadata information:

String packagePattern = "org.test.demo.app.service.impl";
// Read the resource file
Resource[] resources = resolver.getResources("classpath*:" + packagePattern + "/**/*.class");
for (Resource resource : resources) {
    MetadataReader reader = metadataReader.getMetadataReader(resource);
    // Read the class metadata information
    ClassMetadata classMetadata = reader.getClassMetadata();
}
Copy the code

Use Javassist to parse method information

// Create a new ClassPool to avoid memory overflow
ClassPool classPool = new ClassPool(true);
// Add the current class loading path to the ClassPath of the ClassPool to avoid missing classes
classPool.insertClassPath(new ClassClassPath(this.getClass()));
// Use ClassPool to load classes
CtClass ctClass = classPool.get(classMetadata.getClassName());
// Remove interfaces, annotations, enumerations, native, arrays, and proxy classes that are not parsed
if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isEnum() || ctClass.isPrimitive() || ctClass.isArray() || ctClass.getSimpleName().contains("$")) {
    return;
}
// Get all declared methods
CtMethod[] methods = ctClass.getDeclaredMethods();
for (CtMethod method : methods) {
    // Proxy methods are not resolved
    if (method.getName().contains("$")) {
        continue;
    }
    / / package name
    String packageName = ctClass.getPackageName();
    / / the name of the class
    String className = ctClass.getSimpleName();
    / / the method name
    String methodName = method.getName();
    // Parameter: method.getLongName() Com. Test. TestService. SelectOrder (Java. Lang. String, Java. Util. List, com. Test. The Order), so the interception in the brackets
    String methodSignature = StringUtils.defaultIfBlank(StringUtils.substringBetween(method.getLongName(), "(".")"), null);
}
Copy the code

Dynamically compiled source code

We have maintained the source code for a business processing class in our initial rules, but it needs to be compiled into bytecode before it can be used, so it is a matter of compiling the source code dynamically.

Java Compile API

  • JavaCompilerAnother way to CompilationTask is to call the call method to CompilationTask.
  • JavaFileObject: represents a Java source file object
  • JavaFileManager: Java source file management class, which manages a series of JavafileObjects
  • Diagnostic: Indicates diagnostic information
  • DiagnosticListener: Diagnostic information listener, which is triggered by the compilation process

The API for dynamic compilation is in the Tools. jar package, so you need to include tools.jar in the POM

<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
Copy the code

Dynamic compilation

The following code parses the package and class names from the source code, writes the source file to disk, and compiles the source using JavaCompiler. Note that when compiling the same class name again, the name must not be the same, otherwise the compilation will fail, because the JVM has already loaded the instance, you can add a random number to the class name to avoid duplication.

private void createAndCompileJavaFile(String sourceCode) throws Exception {
    // Parse package names from source code
    String packageName = StringUtils.trim(StringUtils.substringBetween(sourceCode, "package".";"));
    // Parse the class name from the source
    String className = StringUtils.trim(StringUtils.substringBetween(sourceCode, "class"."implements"));
    / / class name
    String classFullName = packageName + "." + className;

    // Write the source code to a Java file
    File javaFile = new File(CUSTOMIZE_SRC_DIR + StringUtils.replace(classFullName, ".", File.separator) + ".java");
    FileUtils.writeByteArrayToFile(javaFile, sourceCode.getBytes());

    // Compile Java files using JavaCompiler
    JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
    // Compile, in fact, the bottom layer is to call the javac command to perform the compilation
    int result = javac.run(null.null.null, javaFile.getAbsolutePath());

    if(result ! =0) {
        System.out.println("compile failure.");
    } else {
        System.out.println("compile success."); }}Copy the code

Start the service in IDEA. This code compiled without any problems, and you can see that the class file compiled.

However, once the jar package is executed, it will not compile normally, and the following errors will occur: package XXX does not exist, symbols can not be found, etc.

Javac.run (null, null, null, javafile.getabsolutePath ()) can be treated as if the javac command is used to compile the source file. If classpath is not specified, The other classes referenced in the code must not be found.

Why is it possible to run IDEA but not JAR packages? This is actually because of the particularity of the Springboot JAR. The Springboot JAR is an all-in-one, where classes and lib are included in the JAR package, and the classes in IDEA are under the target package, which can be directly accessed.

Compile based on classpath

If so, we can add/boot-INF /classes/ and/boot-INF /lib/ to the compile-time classpath.

First, the contents of the JAR package cannot be accessed directly. The less desirable method is to unzip the JAR package and then assemble the path before compiling.

1. Decompress the package

File file = new File("app.jar");
/ / get JarFile
JarFile jarFile = new JarFile(file);

// Decompress the JAR package
for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
    JarEntry je = e.nextElement();
    String outFileName = CUSTOMIZE_LIB_DIR + je.getName();
    File f = new File(outFileName);

    if(je.isDirectory()){
        if (!f.exists()) {
            f.mkdirs();
        }
    } else{
        File pf = f.getParentFile();
        if(! pf.exists()){ pf.mkdirs(); }try (InputStream in = jarFile.getInputStream(je);
             OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
            byte[] buffer = new byte[2048];
            int b = 0;
            while ((b = in.read(buffer)) > 0) {
                out.write(buffer, 0, b); } out.flush(); }}}Copy the code

2. Splice classpath

String bootLib = StringUtils.join(CUSTOMIZE_LIB_DIR, "BOOT-INF", File.separator, "lib");
String bootLibPath = StringUtils.join(bootLib, File.separator);
String bootClasses = StringUtils.join(CUSTOMIZE_LIB_DIR, "BOOT-INF", File.separator, "classes");

File libDir = new File(bootLib);
File[] libs = libDir.listFiles();
/ / stitching classpath
StringBuilder classpath = new StringBuilder(StringUtils.join(bootClasses, File.pathSeparator));
for (File lib : libs) {
    classpath.append(bootLibPath).append(lib.getName()).append(File.pathSeparator);
}
return classpath.toString();
Copy the code

3, compile,

The javac command simply specifies the classpath with the -cp argument, and it will compile successfully.

// Compile Java files using JavaCompiler
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
// Compile, in fact, the bottom layer is to call the javac command to perform the compilation
int result = javac.run(null.null.null."-cp", classpath, javaFile.getAbsolutePath());
Copy the code

Elegant dynamic compilation

Arthas memory compilation

The above method requires unpacking the JAR package to get the classpath, otherwise it won’t compile, is inelegant, and is only an alternative. Arthas’s source code has an in-memory compilation module that can easily implement dynamic compilation.

By studying its source code, the underlying compiler still uses javacompiler-related apis, but the difference is the way it gets the referenced classes in the source code.

First inheritance custom search JavaFileObject ForwardingJavaFileManager implementation. Then you can see that it uses the custom PackageInternalsFinder to find the class, and you can see that it still looks for the relevant class from the jar package. More people can read their own source code.

2, use,

The arthas-MemoryCompiler dependency was first introduced into the POM.

<dependency>
    <groupId>com.taobao.arthas</groupId>
    <artifactId>arthas-memorycompiler</artifactId>
    <version>3.1.1</version>
</dependency>
Copy the code

Usage:

Arthas dynamic compilation with Arthas
DynamicCompiler dynamicCompiler = new DynamicCompiler(Thread.currentThread().getContextClassLoader());
dynamicCompiler.addSource(className, sourceCode);
Map<String, byte[]> byteCodes = dynamicCompiler.buildByteCodes();

File outputDir = new File(CUSTOMIZE_CLZ_DIR);

for (Map.Entry<String, byte[]> entry : byteCodes.entrySet()) {
    File byteCodeFile = new File(outputDir, StringUtils.replace(entry.getKey(), ".", File.separator) + ".class");
    FileUtils.writeByteArrayToFile(byteCodeFile, entry.getValue());
}
Copy the code

Data reference

Arthas Github

Java class runtime dynamic compilation technology

Code entry method

Now that the source code is compiled into bytecode, it’s time to dive into the method you want to intercept.

Load the bytecode and define the Class instance

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

// Read bytecode files
CtClass executeClass = classPool.makeClass(new FileInputStream("..../DemoBeforeHandler.class"));
// The current context class loader
System.out.println("----> current thread context classLoader : " + Thread.currentThread().getContextClassLoader().toString());
// The parent of the current context class loader
System.out.println("----> current thread context classLoader's parent classLoader : " + Thread.currentThread().getContextClassLoader().getParent().toString());
// The application class loader
System.out.println("----> application classLoader : " + ClassLoader.getSystemClassLoader().toString());
// Define a Class instance
Class clazz = executeClass.toClass();
Copy the code

When a toClass passes no arguments, it actually loads the bytecode internally using the current context class loader, or it can pass in the class loader itself.

The current context classloader may be different for different containers. I’m using the undertow container here, and the context classloader is the LaunchedURLClassLoader; When using tomcat container, runtime context class loader is TomcatEmbeddedWebappClassLoader, its parent class loader is LaunchedURLClassLoader. When running in IDEA, the context class loader is the AppClassLoader, or application class loader.

----> current thread context classLoader : org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418
----> current thread context classLoader's parent classLoader : sun.misc.Launcher$AppClassLoader@5c647e05
----> application classLoader : sun.misc.Launcher$AppClassLoader@5c647e05
Copy the code

There is a catch here. When called during program startup, the context classloader is the LaunchedURLClassLoader; But invoked during operation, if you are using tomcat container, the context class loader is TomcatEmbeddedWebappClassLoader here, is a proxy class loader.

If the Class loader is used to define an instance of the Class, it succeeds. If the Class loader is used to define an instance of the Class, an error is reported: NoClassDefFoundError.

This is because the actual request, the context class loader is LaunchedURLClassLoader, is the parent class loader TomcatEmbeddedWebappClassLoader, class definition defined in subclasses loaders, used in the parent class loader certainly could not find.

----> current thread context classLoader : TomcatEmbeddedWebappClassLoader
  context: ROOT
  delegate: true
----------> Parent Classloader:
org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418

----> current thread context classLoader's parent classLoader : org.springframework.boot.loader.LaunchedURLClassLoader@20ad9418
----> application classLoader : sun.misc.Launcher$AppClassLoader@5c647e05
Copy the code

Therefore, the LaunchedURLClassLoader is passed in when toClass is called, not the subclass loader.

final String LAUNCHED_CLASS_LOADER = "org.springframework.boot.loader.LaunchedURLClassLoader";
// Context classloader
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(! LAUNCHED_CLASS_LOADER.equals(contextClassLoader.getClass().getName())) {if (LAUNCHED_CLASS_LOADER.equals(contextClassLoader.getParent().getClass().getName())) {
        contextClassLoader = contextClassLoader.getParent();
    } else{ contextClassLoader = ClassLoader.getSystemClassLoader(); }}// Pass in the classloader
executeClass.toClass(contextClassLoader, null);
Copy the code

Building blocks

The easiest way to do this is to create an instance of handler directly and then use it in a method.

private void builderExecuteBody(CtClass executeClass, boolean returnValue) {
    StringBuilder executeBody = new StringBuilder("{").append("\r\n");
    // effect: org.test.demoHandler DemoHandler = new org.test.demohandler ();
    executeBody
            .append(executeClass.getName()) / / type
            .append("")
            .append(executeClass.getSimpleName()) // Variable name
            .append("=")
            .append("new ").append(executeClass.getName()).append("();")
            .append("\r\n");
    // If there is a return value, use temporary variable storage
    if (returnValue) {
        executeBody.append("Object result = ");
    }
    // effect: demohandler. execute($$);
    executeBody
            .append(executeClass.getSimpleName()).append(".execute($args);")
            .append("\r\n");
    if (returnValue) {
        executeBody.append("return ($r) result;").append("\r\n");
    }
    executeBody.append("}").append("\r\n");
}
Copy the code

Generated effects:

{
org.test.demo.app.service.impl.DemoBeforeHandler DemoBeforeHandler = new org.test.demo.app.service.impl.DemoBeforeHandler();
DemoBeforeHandler.execute($args);
}
Copy the code

Insert code block

String targetClassName = point.getPackageName() + "." + point.getClassName();
/ / the target class
CtClass targetClass = classPool.get(targetClassName);
// Build the CtClass array based on the parameter type
CtClass[] params = buildParams(classPool, point);
// Target method
CtMethod targetMethod = targetClass.getDeclaredMethod(point.getMethodName(), params);

// Pre-rule
String beforeCode = "{... }";

// Replace the rule
String replaceCode = "{... }";

// post-rule
String afterCode = "{... }";

// Replace the method
targetMethod.setBody(replaceCode);

// Insert the code block before the method body
targetMethod.insertBefore(beforeCode);

// Insert code block after method body
targetMethod.insertAfter(afterCode);
Copy the code

Dynamically create agents and implement class overloading

Now that the method body has been modified, all that remains is for the JVM to reload the class and change the source code dynamically.

Javassist HotSwapAgent

Javassist provides a proxy for HotSwapAgent that uses its Re-define method to redefine a class, but the tool is largely unusable.

javassist.util.HotSwapAgent.redefine(Class.forName(targetClassName), targetClass);
Copy the code

First of all, we’ll look at the javassist. Util. HotSwapAgent reloading classes principle.

In define method, the startAgent method will be called to dynamically load the agent, and then redefine the class through instrumentation, Instrumentation is Java. Lang. Instrument. Instrumentation.

In the startAgent method, first determine that if the instrumentation already exists, the dynamic agent will not be loaded. If not, first create the proxy jar package dynamically, attach it to the current VirtualMachine using VirtualMachine, and then load the proxy.

In creating Agent bags, first create MAINFEST file, and specify the Premain Class, Agent Class for javassist. Util. HotSwapAgent, Then the javassist. Util. HotSwapAgent. Java bytecode file is written to javassist. Util. HotSwapAgent. Class, finally into agent. The jar.

HotSwapAgent PreMain and AgentMain, agents are used to obtain instances of Instrumentation, which are passed in by the virtual machine when the agent is loaded.

By default, the generated agent.jar directory is in a temporary directory under the user directory.

This is the process of creating and loading agents on the fly. This feature would be perfect if it could be used directly, but it is not.

IDEA mode running

First of all, let’s take a look at the IDEA of using javassist. Util. HotSwapAgent problem, before you start, let’s look at the IDEA of start the service in several ways.

  • user-local & none: user-local is the same as None, which is the default option. This method concatenates all dependent JAR packages with the -classpath argument, which is long on the command line. If the length of a Command line parameter exceeds the OS limit, an error message is displayed: Command line is too long.
  • JAR manifest: After concatenating all dependent JAR packages, create a temporary JAR file and writeMETA-INF/MANIFEST.MFOf the fileClass-PathThe jar package is then specified with the -classpath argument, which is intended to shorten the command line.
  • classpath file: After concatenating all dependent JAR packages, write them to a temporary file and passcom.intellij.rt.execution.CommandLineWrapperTo start.

One difference between these three methods is that they start with different inline classloaders:

  • 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 loads using the application class loader, so when started using user-local, JAR Manifest, the agent loads correctly and their class loader is the same. NoClassDefFoundError is raised when starting with classpath. This is because the classes of the JAR package in Javassist are loaded by the URLClassLoader class loader, whereas the application class loader cannot load the lib class.

java.lang.NoClassDefFoundError: javassist/NotFoundException
    at java.lang.Class.getDeclaredMethods0(Native Method)
    at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
    at java.lang.Class.getDeclaredMethod(Class.java:2128)
    at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:327)
    at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411)
Caused by: java.lang.ClassNotFoundException: javassist.NotFoundException
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)...5 more
Copy the code

JAR package

When we run into jars, actually will still be the same mistake, because the jar package runtime context class loader is org springframework). The boot loader. 20 ad9418 LaunchedURLClassLoader @, The parent class loader is the application class loader.

To sum up, according to the parent delegate model, the subclass loader can load classes from the parent class loader; But the parent class loader cannot load classes in the subclass loader.

Custom agent loading

Due to javassist. Util. HotSwapAgent in load is used when the application class loader, so the agent when entering agentmain method set instrumentation variable, is actually in the application class loader.

The program starts with HotSwapAgent loaded by its subclass loader, so there are actually two different instances of the HotSwapAgent class with the same name but different classloaders. So you still don’t get instrumentation instances while the program is running.

I’m going to solve this problem in a very simple and crude way:

1) cover the javassist. Util. HotSwapAgent method (I am rewriting, separate startAgent and agentmain) (2) increase the static setInstrumentation method (3) in agentmain approach, Get application runtime context class loader (LaunchedURLClassLoader) (4) find program load through LaunchedURLClassLoader javassist. Util. HotSwapAgent class instance (5) invoked by means of reflection The setInstrumentation method sets the Instrumentation passed in by the JVM. ⑥ After that, we can call hotswapClient.define to reload the class during the program run.

private static final String CLASS_LOADER = "org.springframework.boot.loader.LaunchedURLClassLoader";
private static final String SWAP_CLIENT = "javassist.util.HotSwapClient";
private static final String SWAP_CLIENT_SETTER = "setInstrumentation";

// Add a static setInstrumentation method set by agentMain to Instrumentation
public static void setInstrumentation(Instrumentation instrumentation) {
    HotSwapAgent.instrumentation = instrumentation;
}

public static void agentmain(String agentArgs, Instrumentation inst) throws Throwable {
    if(! inst.isRedefineClassesSupported())throw new RuntimeException("this JVM does not support redefinition of classes");

    instrumentation = inst;

    // Get the LaunchedURLClassLoader
    ClassLoader classLoader = getClassLoader(inst);
    / / get LaunchedURLClassLoader class loader in the javassist. Util. HotSwapClient class instanceClass<? > clientClass = classLoader.loadClass(SWAP_CLIENT);// Set Instrumentation with reflection
    clientClass.getMethod(SWAP_CLIENT_SETTER, Instrumentation.class).invoke(null, inst);
}

private static ClassLoader getClassLoader(Instrumentation inst) {
    // Get all loaded classes
    Class[] loadedClasses = inst.getAllLoadedClasses();
    / / to find LaunchedURLClassLoader
    returnArrays.stream(loadedClasses) .filter(c -> c.getClassLoader() ! =null&& c.getClassLoader().getClass() ! =null)
            .filter(c -> CLASS_LOADER.equals(c.getClassLoader().getClass().getName()))
            .map(Class::getClassLoader)
            .findFirst()
            .orElse(Thread.currentThread().getContextClassLoader());
}
Copy the code

Results validation and limitations

results

You can see that you have successfully added custom code logic before and after the method you want to intercept, or you can dynamically update the code again and reapply the rules. At this point, the function of dynamic section is basically realized.

limitations

The structure of custom code must be fixed because it is created as an object and then inserted into the method body as a method call. (2) In custom code, it is not possible to directly inject Spring container objects using @autowired or other methods. (3) due to the limitations of Instrumentation itself, we can only change the method body, can not change the definition of the method, can not be added to the class method, field, otherwise overload failure.

Bonus: Use Arthas to diagnose Java problems

In the process of developing this feature, I took a quick look at Arthas’s source code and how to use Arthas to diagnose some online problems. Here is a list of the official documentation that makes it easy to get started.

Arthas is an open source tool for Diagnosing Java problems. Please refer to the official documentation for details.

IDEA installation plug-in: Arthas one-click diagnostics for remote servers using the Cloud Toolkit plug-in

Get Started: Arthas Quick Start

Command list: Arthas command list

Attach failed: Attach times ERROR

Arthas Github source code: Arthas Github