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是直接修改字节码, 性能很高, 但开发成本太高, 需要根据个人情况选择使用. 近来流行的流量银行也是基于这种思想设计实现的, 后续也会慢慢介绍到