Java之因此能够"一次编译,处处运行",一是由于JVM针对各类平台和操做系统都进行了定制,对开发者屏蔽了底层细节。二是由于不管在任何平台都会编译生成固定格式的字节码(.class)文件供JVM使用,不一样平台上的JVM虚拟机均可以载入和执行同一种和平台无关的字节码。因而可知字节码对于Java生态的重要性。之因此被称为字节码,是由于字节码文件由16进制值组成,JVM以字节为单位进行读取。在Java中通常使用javac命令编译源代码为字节码文件,一个.java文件从编译到运行的示例以下图所示。java
.java文件经过javac编译后将获得.class文件,如编写一个简单的ByteCodeDemo类,以下图的左侧部分。编译后生成ByteCodeDemo.class文件,打开后是一堆16进制数,按字节为单位分割展现以下图右侧部分展现。框架
JVM规范要求每个字节码文件都须要由10个部分按固定顺序组成,总体结果以下图所示。ide
ASM是一个Java字节码操控框架。它能够被用来动态生成类或者加强既有类的功能。ASM能够直接产生二进制的class文件,也能够在类被加载到Java虚拟机以前改变类行为。ASM的应用场景有AOP(cglib基于ASM)、热部署、修改其它jar包中的类等。ASM修改字节码流程以下图所示。spa
用于读取已经编译好的.class文件;操作系统
用于从新构建编译后的类,如修改类名、属性及方法等;插件
ASM内部采用访问者模式根据字节码从上到下依次处理,对于字节码文件中不一样的区域有不一样的Visitor。code
当访问不一样区域时会回调相应方法,该方法会返回一个对应的字节码操做对象。经过修改这个对象就能够修改class文件相应结构对应的内容。最后将这个对象的字节码内容覆盖原先的.class文件就能够实现字节码的切入。cdn
代码示例以下,本来Base的process方法只输出一行"process"。对象
目标:咱们指望经过字节码加强后,在方法执行前输出"start",在方法执行后输出"end"。blog
public class Base {
public void process() {
System.out.println("process");
}
}
复制代码
要实如今process方法先后插入输出代码,须要有如下步骤:
基本实现代码参考以下,SimpleGenerator类定义了ClassReader和ClassWriter对象。其中ClassReader负责读取字节码,而后交给ClassVisitor类处理,处理完成后由ClassWriter完成字节码文件替换。
public class SimpleGenerator {
public static void main(String[] args) throws Exception {
//读取类文件
ClassReader classReader = new ClassReader("com.example.aop.asm.Base");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
//处理,经过classVisitor修改类
ClassVisitor classVisitor = new SimpleClassVisitor(classWriter);
classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);
byte[] data = classWriter.toByteArray();
//保存新的字节码文件
File file = new File("target/classes/com/example/aop/asm/Base.class");
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(data);
outputStream.close();
System.out.println("generator new class file success.");
}
}
复制代码
SimpleClassVisitor类继承ClassVisitor类。ClassVisitor中各方法按如下顺序调用,这个顺序在ClassVisitor的java doc中也有说明。
[
visitSource
] [visitOuterClass
] (visitAnnotation
|visitTypeAnnotation
|visitAttribute
)* (visitInnerClass
|visitField
|visitMethod
)*visitEnd
.
其中visitMethod在访问方法时调用,咱们能够经过扩展MethodVisitor方法来实现加强逻辑。相似地,MethodVisitor也须要按顺序调用其方法。
(
visitParameter
)* [visitAnnotationDefault
] (visitAnnotation
|visitAnnotableParameterCount
|visitParameterAnnotation
visitTypeAnnotation
|visitAttribute
)* [visitCode
(visitFrame
|visitXInsn
|visitLabel
|visitInsnAnnotation
|visitTryCatchBlock
|visitTryCatchAnnotation
|visitLocalVariable
|visitLocalVariableAnnotation
|visitLineNumber
)*visitMaxs
]visitEnd
.
public class SimpleClassVisitor extends ClassVisitor {
public SimpleClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
// visitMethod在访问类方法时回调,能够获知当前访问到方法信息
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature,
exceptions);
// 不须要加强Base类的构造方法
if (mv != null && !name.equals("<init>")) {
// 经过定制MethodVisitor,替换原来的MethodVisitor对象来实现方法改写
mv = new SimpleMethodVisitor(mv);
}
return mv;
}
/** * 定制方法visitor */
class SimpleMethodVisitor extends MethodVisitor {
public SimpleMethodVisitor(MethodVisitor methodVisitor) {
super(Opcodes.ASM5, methodVisitor);
}
// visitCode方法在ASM开始访问方法的Code区时回调
@Override
public void visitCode() {
super.visitCode();
// System.out.println("start")对应的字节码,在visitCode访问方法以后添加
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("start");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
// 每当ASM访问到无参数指令时,都会调用visitInsn方法
@Override
public void visitInsn(int opcode) {
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)) {
// System.out.println("end")对应的字节码,在visitInsn方法返回前添加
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("end");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
}
复制代码
运行SimpleGenerator类的main方法便可完成对Base类的字节码加强。加强后的class文件经过反编译后结果以下图所示。能够看到对应的代码已经改变,在其先后增长了"start"和"end"的输出。
能够经过ASM ByteCode Outline 插件很方便地实现源码到字节码的映射。
欢迎关注个人公众号~