Java Agent to teach you how to implement a method time statistics

There were two introductory posts, 200303- How to elegantly count block time in Java, which ended with a discussion on how to count method time according to Java Agent

The blog post “200316-IDEA + Maven Zero Base Build Java Agent Project” describes in detail the whole process of building a Java Agent development test project

This blog post will enter the actual Java Agent, hand in hand to teach you how to implement a statistical method time Java Agent

1. Basic posture points

Although the above two sections teach you how to implement a Hello World version of Agent by hand, in fact, Java Agent is still confused, so we have to first make up the basic knowledge

First of all, let’s take a look at the interface definition of agent Instrumentation in the two methods

/** * Register a Transformer and any class loads from then on will be blocked by Transformer. * Transformer can modify the class's bytecode [] directly */
void addTransformer(ClassFileTransformer transformer);

/** * retriggers class loading for classes already loaded by the JVM. The Transformer registered above is used. * Retransformation can change the method body, but cannot change the method signature, add and delete method/class member attributes */
void retransformClasses(Class
       ... classes) throws 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);

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

The first two methods are important because after the addTransformer method is configured, subsequent class loads are blocked by Transformer. For classes that have already been loaded, you can perform retransformClasses to retrigger the Transformer interception. Class-loaded bytecodes that have been modified will not be restored unless retransformed.

From the above description, it can be known that

  • Can be achieved byTransformerModify the class
  • When the class is loaded, Transformer is triggered to intercept it

2. Implement

We need to count the method time, so we want to record a time before the execution of the method, and count the time difference after the execution, which is the time

Modifying the bytecode directly is a bit cumbersome, so we use the artifact JavaAssist to modify the bytecode

Implement custom ClassFileTransformer, the code is as follows

public class CostTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) {
        // Here we limit the time statistics to target packets only
        if(! className.startsWith("com/git/hui/java/")) {
            return classfileBuffer;
        }

        CtClass cl = null;
        try {
            ClassPool classPool = ClassPool.getDefault();
            cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));

            for (CtMethod method : cl.getDeclaredMethods()) {
                // All methods, count time; Note that local variables need to be declared by 'addLocalVariable'
                method.addLocalVariable("start", CtClass.longType);
                method.insertBefore("start = System.currentTimeMillis();");
                String methodName = method.getLongName();
                method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" +
                        ".currentTimeMillis() - start));");
            }

            byte[] transformed = cl.toBytecode();
            return transformed;
        } catch (Exception e) {
            e.printStackTrace();
        }
        returnclassfileBuffer; }}Copy the code

Then change the Agent slightly

/**
 * Created by @author yihui in 16:39 20/3/15.
 */
public class SimpleAgent {

    /** * JVM starts as an argument. To run this method ** manifest requires the premain-class attribute to be configured@param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
        customLogic(inst);
    }

    /** * The agent-class attribute is required to run the manifest method@param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
        customLogic(inst);
    }

    /** * The statistical method takes **@param inst
     */
    private static void customLogic(Instrumentation inst) {
        inst.addTransformer(new CostTransformer(), true); }}Copy the code

After this agent is finished, the packaging process is the same as the above process, and then the testing part is entered

Create a DemoClz with two methods

public class DemoClz {

    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }

    public int count(int i) {
        System.out.println("cnt: " + i);
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 1; }}Copy the code

Then the corresponding main method is as follows

public class BaseMain {
    public static void main(String[] args) throws InterruptedException {
        DemoClz demoClz = new DemoClz();
        int cnt = 0;
        for (int i = 0; i < 20; i++) {
            if (++cnt % 2= =0) {
                i = demoClz.print(i);
            } else{ i = demoClz.count(i); }}}}Copy the code

Select the JVM parameter to specify the agent mode to run (the operation is the same as above). The output is as follows

Although there is no method time statistics in our application, the final output perfectly prints the call time of each method to achieve non-invasive time statistics

Up to this point, the Java Agent literacy + actual combat (development of a method time-consuming statistics) have been completed, whether it is declared that we can sum up, not, the following introduce a problem encountered in the implementation of the above demo process

3. Exception in thread “main” java.lang.VerifyError: Expecting a stack map frame

In the agent example that demonstrates the method’s time consuming, instead of using the original test case, a new DemoClz is created to do so. So why do you choose to do this? What if you just used the test case in section 2?

public class BaseMain {
    public int print(int i) {
        System.out.println("i: " + i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return i + 2;
    }

    public void run(a) {
        int i = 1;
        while (true) { i = print(i); }}public static void main(String[] args) {
        BaseMain main = new BaseMain();
        main.run();
}
Copy the code

If the agent is still specified by JVM parameters, the above code will be found to throw an exception and cannot run normally

It pointed out that there were bytecode errors in the run method. We counted the time-consuming Agent, mainly because a line of code was added respectively before and after the method started. We directly added it in the run method, which was equivalent to the following code

The above prompt clearly tells us that the last line of statements will never be reached, and the compilation will be an exception; So the problem comes, as a Java Agent provider, I do not know whether the user has written such an infinite loop method, if the application has such an infinite loop task exists, a mount my agent, resulting in the application can not get up, the pan who count ????

Add a -noverify parameter to the JVM parameter (note that this parameter may vary from JDK version to JDK version. -xx: -usesplitverifier)

In the IDEA development environment, the following configuration is required

It’s running again. It’s working

4. Summary

This is a practical project, first clear method parameters Instrumentation its interface definition, through it to achieve Java bytecode modification

We implement method time statistics by implementing custom ClassFileTransformer, modifying bytecode with JavAssist, and injecting time statistics code for the first and last lines of each method

Finally, we leave a small question, in the above implementation, when the method throws an exception, will we inject the last line of statistical time output as scheduled, if not, how to modify, welcome you to comment on the solution

(The specific solution can be obtained in the source code, as well as the supporting test case, for support, favoritessand attention toybutton)

II. The other

Related 0.

Related blog

  • 200303- How to elegantly count code block time in Java
  • 200316-IDEA + Maven Zero base Build Java Agent project

Related to the source code

  • Github.com/liuyueyi/ja…

1. A gray Blog:liuyueyi.github.io/hexblog

A gray personal blog, recording all the study and work in the blog, welcome everyone to go to stroll

2. Statement

As far as the letter is not as good as, has been the content, purely one’s own words, because of the limited personal ability, it is hard to avoid omissions and mistakes, such as finding bugs or better suggestions, welcome criticism and correction, not grudging gratitude

  • Micro Blog address: Small Gray Blog
  • QQ: a gray /3302797840

3. Scan attention

A gray blog