premain | JVM级别的AOP

2022-06-27 15:06:22 浏览数 (1)

JDK5版本中提供了Instrumentation功能, 它的最大作用就是可以动态改变和操作字节码.在JDK6版本中又进行了功能扩展和完善. 而premain是JDK的Instrumentation中的一个子功能.

一. premain简介

premain是程序在运行main()之前执行的逻辑, 可以在这部分逻辑中做字节码的修改等操作.

1.1

premain函数

premain函数有两种写法:

代码语言:javascript复制
// 方式1
public static void premain(String agentArgs, Instrumentation inst);
// 方式2
public static void premain(String agentArgs);

1.2

参数简析

agentArgs是premain执行时传入的参数, 它是与main()参数是不同的, 且传参方式也是不同的, 后文会讲到传参方式. inst是JVM内置参数, 可以获得执行时所有类的字节码信息以及类定义的转换等操作.

1.3

方法优先级

其中[方式1]的优先级比[方式2]高, 会优先执行; 两种方式同时存在时, [方式2]会被忽略执行.

1.4

MANIFEST.MF

premain在运行时是需要打成jar, 通过Java执行参数引入执行的, 需要对jar的元数据进行定义. 元数据文件目录(maven工程):

代码语言:javascript复制
/src/main/resources/META-INF/MANIFEST.MF

文件内容:

代码语言:javascript复制
Manifest-Version: 1.0
Premain-Class: com.instrument.premain.MyPremain
Can-Redefine-Classes: true
Can-Retransform-Classes: true

1.5

jar包生成

添加POM插件, 打jar包, 本文中生成的jar名是premain-1.0-SNAPSHOT.jar 执行命令: mvn package

代码语言:javascript复制
<build>
    <plugins>
        <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.3.1</version>
            <configuration>
                <archive>
                        <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

1.6

运行命令

premain运行时的基本格式:

代码语言:javascript复制
java -javaagent:jar位置[=传入premain的参数]

例如:

代码语言:javascript复制
java -javaagent:/xx/premain-1.0-SNAPSHOT.jar=agentArgs –cp MAIN.jar main args1 args2

二. Hello World

了解了上述基本流程, 我们看一个示例, 利用premain打印出指定方法的运行时间.

2.1

Premain实现

接收参数, 并将参数传入TimingTransformer类, 处理方法运行时间,并添加转换器.

代码语言:javascript复制
package com.instrument.premain;

import java.lang.instrument.Instrumentation;

public class MyPremain {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Hi, I'm premain agent! 2:"   agentArgs);
        inst.addTransformer(new TimingTransformer(agentArgs));
    }
}

2.2

TimingTransformer

添加方法运行时间, 之前我们说过用Javassist修改过源码, 这次试用更底层的apache bcel进行字节码替换, 如示例中的addTimer()方法, 这要求大家对字节码命令有很深的了解.

代码语言:javascript复制
package com.instrument.premain;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.InstructionConstants;
import org.apache.bcel.generic.InstructionFactory;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.PUSH;
import org.apache.bcel.generic.Type;

public class TimingTransformer implements ClassFileTransformer {

    private String methodName;

    public TimingTransformer(String methodName) {
        this.methodName = methodName;
        System.out.println("method in timing:"   methodName);
    }

    public byte[] transform(ClassLoader loader, String className, Class cBR,
            java.security.ProtectionDomain pD, byte[] classfileBuffer) {
        try {
            ClassParser cp = new ClassParser(new java.io.ByteArrayInputStream(
                    classfileBuffer), className   ".java");
            JavaClass jclas = cp.parse();
            ClassGen cgen = new ClassGen(jclas);
            Method[] methods = jclas.getMethods();
            int index;
            for (index = 0; index < methods.length; index  ) {
                Method method = methods[index];
                if (method.getName().equals(methodName)) {
                    System.out.println("className:"   className   ",method:"   method.getName());
                    addTimer(cgen, method);
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
                    cgen.getJavaClass().dump(bos);
                    return bos.toByteArray();
                }
            }
        } catch (IOException e) {
            System.err.println(e);
            System.exit(0);
        }
        return null; // No transformation required
    }

    private static void addTimer(ClassGen cgen, Method method) {

// set up the construction tools
        InstructionFactory ifact = new InstructionFactory(cgen);
        InstructionList ilist = new InstructionList();
        ConstantPoolGen pgen = cgen.getConstantPool();
        String cname = cgen.getClassName();
        MethodGen wrapgen = new MethodGen(method, cname, pgen);
        wrapgen.setInstructionList(ilist);

// rename a copy of the original method
        MethodGen methgen = new MethodGen(method, cname, pgen);
        cgen.removeMethod(method);
        String iname = methgen.getName()   "_timing";
        methgen.setName(iname);
        cgen.addMethod(methgen.getMethod());
        Type result = methgen.getReturnType();

// compute the size of the calling parameters
        Type[] parameters = methgen.getArgumentTypes();
        int stackIndex = methgen.isStatic() ? 0 : 1;
        for (int i = 0; i < parameters.length; i  ) {
            stackIndex  = parameters[i].getSize();
        }

// save time prior to invocation
        ilist.append(ifact.createInvoke("java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.createStore(Type.LONG, stackIndex));

// call the wrapped method
        int offset = 0;
        short invoke = Constants.INVOKESTATIC;
        if (!methgen.isStatic()) {
            ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
            offset = 1;
            invoke = Constants.INVOKEVIRTUAL;
        }
        for (int i = 0; i < parameters.length; i  ) {
            Type type = parameters[i];
            ilist.append(InstructionFactory.createLoad(type, offset));
            offset  = type.getSize();
        }
        ilist.append(ifact.createInvoke(cname, iname, result, parameters, invoke));

// store result for return later
        if (result != Type.VOID) {
            ilist.append(InstructionFactory.createStore(result, stackIndex   2));
        }

// print time required for method call
        ilist.append(ifact.createFieldAccess("java.lang.System", "out", new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
        ilist.append(InstructionConstants.DUP);
        ilist.append(InstructionConstants.DUP);
        String text = "Call to method "   methgen.getName()   " took ";
        ilist.append(new PUSH(pgen, text));
        ilist.append(ifact.createInvoke("java.io.PrintStream", "print", Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));
        ilist.append(ifact.createInvoke("java.lang.System", "currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));
        ilist.append(InstructionFactory.createLoad(Type.LONG, stackIndex));
        ilist.append(InstructionConstants.LSUB);
        ilist.append(ifact.createInvoke("java.io.PrintStream", "print", Type.VOID, new Type[]{Type.LONG}, Constants.INVOKEVIRTUAL));
        ilist.append(new PUSH(pgen, " ms."));
        ilist.append(ifact.createInvoke("java.io.PrintStream", "println", Type.VOID, new Type[]{Type.STRING}, Constants.INVOKEVIRTUAL));

// return result from wrapped method call
        if (result != Type.VOID) {
            ilist.append(InstructionFactory.createLoad(result, stackIndex   2));
        }
        ilist.append(InstructionFactory.createReturn(result));

// finalize the constructed method
        wrapgen.stripAttributes(true);
        wrapgen.setMaxStack();
        wrapgen.setMaxLocals();
        cgen.addMethod(wrapgen.getMethod());
        ilist.dispose();
    }

}

2.3

pom依赖

在IDEA中运行时, premain和目标代码程序中都需要引入该依赖

代码语言:javascript复制
<dependency>
    <groupId>org.apache.bcel</groupId>
    <artifactId>bcel</artifactId>
    <version>6.5.0</version>
</dependency>

2.4

目标代码

代码语言:javascript复制
public class TestABC {

    public static void main(String[] args) {
        System.out.println("TestABC main:"  String.join(",",args));
        new TestABC().abc();
    }
    public void abc(){
        System.out.println("abc in method");
    }
}

2.4

添加VM参数

代码语言:javascript复制
-javaagent:/xxx/instrument/premain/target/premain-1.0-SNAPSHOT.jar=abc

2.5

运行结果

代码语言:javascript复制
method in timing:abc
className:TestABC,method:abc
TestABC main:1,2,3
abc in method
Call to method abc_timing took 0 ms.

小结

通过apache bcel修改字节码, 配合Instrumentation实现了无侵入的时间统计,apache bcel是直接修改字节码, 性能很高, 但开发成本太高, 需要根据个人情况选择使用. 近来流行的流量银行也是基于这种思想设计实现的, 后续也会慢慢介绍到

0 人点赞