java通过修改类的字节码实现aop功能(转载)
在jdk5.0版本中,添加了一个新的接口java.lang.instrument.Instrumentation,这个接口可以用来在类加载的时候 对类的字节码进行修改,改变类的功能的实现。通过这种方法,我们就可以实现aop的功能,这是和proxy动态代理的不同的另一种aop的实现。
这种方法的原理很简单,就是通过修改编译器已经编译好的字节码来修改类,和我们通常修改类源码来实现对类的修改差不多。只不过是在两个不同的层次进行的修改。
一下是使用这个接口的具体方法。
第一步:
需要实现 java.lang.instrument.Instrumentation这个接口,通过在这个接口的实现类中,在加载类的时候修改类的字节码。以下是接口的实现 Transformer.java
public class Transformer implements ClassFileTransformer {
//在程序运行之前就被jvm调用的方法
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new Transformer());
}
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] result =null; //定义新的字节码存储变量
//在此通过修改类的字节码 classfileBuffer,来更改类。
return result; //返回新的字节码
}
}
第二步:
需要将Transformer类的class文件生成jar文件,如t.jar,在jar文件的MANIFEST.MF添加 Premain-Class: Transformer 这一项。在运行程序时,需要在jvm使用如下参数 -javaagent:t.jar 。这样jvm在运行时,就会先找到 Premain-Class: Transformer 这一项指定的 Transformer类的premain方法,运行此方法。也就实现了在以后的类的加载中,对类的字节码进行修改的功能。
就是使用java.lang.instrument.Instrumentation接口的方法。那么如何实现aop功能呢,这就需要在更改字节码的时候,在类的功能上添加aop方面的功能。
现在我们看看如何实现在方法的调用前和返回时打印当前时间的aop功能。
先更改 Transformer的 transform方法
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] result = classfileBuffer ; //定义新的字节码存储变量
//在此通过修改类的字节码 classfileBuffer,来更改类。
if (loader != ClassLoader.getSystemClassLoader()) {
return classfileBuffer;
}
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(true);
ClassAdapter adapter = new PerfClassAdapter(writer, className);
reader.accept(adapter, true);
result = writer.toByteArray();
return result; //返回新的字节码
}
这样就通过 PerfClassAdapter类来修改字节码.
PerfClassAdapter.java
public class PerfClassAdapter extends ClassAdapter {
private String className;
public PerfClassAdapter(ClassVisitor visitor, String theClass) {
super(visitor);
this.className = theClass;
}
public MethodVisitor visitMethod(int arg,
String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(arg,
name,
descriptor,
signature,
exceptions);
MethodAdapter ma = new PerfMethodAdapter(mv, className, name);
return ma;
}
}
PerfMethodAdapter.java
public class PerfMethodAdapter extends MethodAdapter {
private String _className, _methodName;
public PerfMethodAdapter(MethodVisitor visitor,
String className,
String methodName) {
super(visitor);
_className = className;
_methodName = methodName;
}
public void visitCode() {
this.visitLdcInsn(_className);
this.visitLdcInsn(_methodName);
this.visitMethodInsn(INVOKESTATIC,
" AOP_LOG ",
"start",
"(Ljava/lang/String;Ljava/lang/String;)V");
super.visitCode();
}
public void visitInsn(int inst) {
switch (inst) {
case Opcodes.ARETURN:
case Opcodes.DRETURN:
case Opcodes.FRETURN:
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
this.visitLdcInsn(_className);
this.visitLdcInsn(_methodName);
this.visitMethodInsn(INVOKESTATIC,
"AOP_LOG",
"end",
"(Ljava/lang/String;Ljava/lang/String;)V");
break;
default:
break;
}
super.visitInsn(inst);
}
}
AOP_LOG.java
public class AOP_LOG {
public static void start(String className, String methodName) {
System.out.println(new StringBuilder(className)
.append('"t')
.append(methodName)
.append(""tstart"t")
.append(System.currentTimeMillis()));
}
public static void end(String className, String methodName) {
System.out.println(new StringBuilder(className)
.append('"t')
.append(methodName)
.append(""tend"t")
.append(System.currentTimeMillis()));
}
}
这些类结合起来,就实现了在方法的调用前,会调用 AOP_LOG类的start方法,在方法返回后,调用 AOP_LOG类的 end方法。
现在写个测试类来测试一下。
TSMain.java
public class TSMain {
public static void main(String[] args) {
TSMain tsm = new TSMain();
tsm.printClassName();
}
public void printClassName(){
System.out.println("hello "+this.getClass().getName());
}
}
运行命令如下:
=>java -javaagent:t.jar classpath cpdir TSMain # cpdir 需要用到的类的路径
输出结果如下:
TSMain main start 1179374205562
TSMain <init> start 1179374205562
TSMain <init> end 1179374205562
TSMain printClassName start 1179374205562
hello TSMain
TSMain printClassName end 1179374205562
TSMain main end 1179374205562
到这里就完成整个aop的功能实现了。需要补充说明的是,这里修改类的字节码是使用了asm的基础类库,所以需要导入这些asm的jar文件。
这种方法的原理很简单,就是通过修改编译器已经编译好的字节码来修改类,和我们通常修改类源码来实现对类的修改差不多。只不过是在两个不同的层次进行的修改。
一下是使用这个接口的具体方法。
第一步:
需要实现 java.lang.instrument.Instrumentation这个接口,通过在这个接口的实现类中,在加载类的时候修改类的字节码。以下是接口的实现 Transformer.java
public class Transformer implements ClassFileTransformer {
//在程序运行之前就被jvm调用的方法
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new Transformer());
}
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] result =null; //定义新的字节码存储变量
//在此通过修改类的字节码 classfileBuffer,来更改类。
return result; //返回新的字节码
}
}
第二步:
需要将Transformer类的class文件生成jar文件,如t.jar,在jar文件的MANIFEST.MF添加 Premain-Class: Transformer 这一项。在运行程序时,需要在jvm使用如下参数 -javaagent:t.jar 。这样jvm在运行时,就会先找到 Premain-Class: Transformer 这一项指定的 Transformer类的premain方法,运行此方法。也就实现了在以后的类的加载中,对类的字节码进行修改的功能。
就是使用java.lang.instrument.Instrumentation接口的方法。那么如何实现aop功能呢,这就需要在更改字节码的时候,在类的功能上添加aop方面的功能。
现在我们看看如何实现在方法的调用前和返回时打印当前时间的aop功能。
先更改 Transformer的 transform方法
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] result = classfileBuffer ; //定义新的字节码存储变量
//在此通过修改类的字节码 classfileBuffer,来更改类。
if (loader != ClassLoader.getSystemClassLoader()) {
return classfileBuffer;
}
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(true);
ClassAdapter adapter = new PerfClassAdapter(writer, className);
reader.accept(adapter, true);
result = writer.toByteArray();
return result; //返回新的字节码
}
这样就通过 PerfClassAdapter类来修改字节码.
PerfClassAdapter.java
public class PerfClassAdapter extends ClassAdapter {
private String className;
public PerfClassAdapter(ClassVisitor visitor, String theClass) {
super(visitor);
this.className = theClass;
}
public MethodVisitor visitMethod(int arg,
String name,
String descriptor,
String signature,
String[] exceptions) {
MethodVisitor mv = super.visitMethod(arg,
name,
descriptor,
signature,
exceptions);
MethodAdapter ma = new PerfMethodAdapter(mv, className, name);
return ma;
}
}
PerfMethodAdapter.java
public class PerfMethodAdapter extends MethodAdapter {
private String _className, _methodName;
public PerfMethodAdapter(MethodVisitor visitor,
String className,
String methodName) {
super(visitor);
_className = className;
_methodName = methodName;
}
public void visitCode() {
this.visitLdcInsn(_className);
this.visitLdcInsn(_methodName);
this.visitMethodInsn(INVOKESTATIC,
" AOP_LOG ",
"start",
"(Ljava/lang/String;Ljava/lang/String;)V");
super.visitCode();
}
public void visitInsn(int inst) {
switch (inst) {
case Opcodes.ARETURN:
case Opcodes.DRETURN:
case Opcodes.FRETURN:
case Opcodes.IRETURN:
case Opcodes.LRETURN:
case Opcodes.RETURN:
case Opcodes.ATHROW:
this.visitLdcInsn(_className);
this.visitLdcInsn(_methodName);
this.visitMethodInsn(INVOKESTATIC,
"AOP_LOG",
"end",
"(Ljava/lang/String;Ljava/lang/String;)V");
break;
default:
break;
}
super.visitInsn(inst);
}
}
AOP_LOG.java
public class AOP_LOG {
public static void start(String className, String methodName) {
System.out.println(new StringBuilder(className)
.append('"t')
.append(methodName)
.append(""tstart"t")
.append(System.currentTimeMillis()));
}
public static void end(String className, String methodName) {
System.out.println(new StringBuilder(className)
.append('"t')
.append(methodName)
.append(""tend"t")
.append(System.currentTimeMillis()));
}
}
这些类结合起来,就实现了在方法的调用前,会调用 AOP_LOG类的start方法,在方法返回后,调用 AOP_LOG类的 end方法。
现在写个测试类来测试一下。
TSMain.java
public class TSMain {
public static void main(String[] args) {
TSMain tsm = new TSMain();
tsm.printClassName();
}
public void printClassName(){
System.out.println("hello "+this.getClass().getName());
}
}
运行命令如下:
=>java -javaagent:t.jar classpath cpdir TSMain # cpdir 需要用到的类的路径
输出结果如下:
TSMain main start 1179374205562
TSMain <init> start 1179374205562
TSMain <init> end 1179374205562
TSMain printClassName start 1179374205562
hello TSMain
TSMain printClassName end 1179374205562
TSMain main end 1179374205562
到这里就完成整个aop的功能实现了。需要补充说明的是,这里修改类的字节码是使用了asm的基础类库,所以需要导入这些asm的jar文件。