java源代码到字节代码再到虚拟机的运行过程,每一步都有不同的实现方式,比如,可以不使用javac,直接在运行时动态编译源代码;字节代码不由编译器生成,而是使用工具来动态创建;在字节代码被虚拟机执行前,可以通过修改字节代码的内容来改变程序行为
字节代码的一些知识点
- 字节代码并不是只存在于class文件中,还可以通过网络下载,或者由程序动态生成,字节代码更精确的定义是包含单个java类或接口定义的字节流,通常由byte[]表示
- 在java源代码中引用一个接口或类,可以通过import语句,减少麻烦。但是在java字节码中,始终使用全名,并且把全名中的.替换为/,即类似com/java/xx/sample这样的形式
- java字节码中的常量池的作用:常量池中包含的是java中基本类型和字符串常量值、类和接口的名称及域的名称。这些常量被代码中其他部分引用。相同的常量只会出现一次。可以将常量池看作是一个常量的查找表
- 字节代码中只存在基本类型 int、long、float、double,其他基本类型都由int来表示
动态编译java源代码
通过动态编译java源代码,可以把编译和运行统一起来,都在运行时完成,基本思路就是运行时使用API编译出字节代码,再使用类加载器加载到虚拟机中运行,运行时一般使用java 反射API(其实方法句柄也ok吧?)
- 使用javac工具,代码中运行javac命令,缺点是输入输出都只能以文件形式存在
- java编译器API,编译器API仅包含接口声明,对应的实现由平台实现者提供(类似SPI?).使用者通过工厂方法或服务加载器来查找具体实现。(比javac工具粒度更细,可以控制更多细节)
- Eclipse JDT编译器,是eclipse自己开发的,提供的接口比java编译器API更加丰富,但使用页更复杂
字节代码增强
字节代码增强的含义就是对已有的java字节代码进行修改,改变其运行的行为。增强的行为可以发生在程序运行前或运行时。发生在程序运行前的做法是先对字节代码进行处理,得到修改后的字节码,再有虚拟机运行(编译完成后,由工具对编译后的字节代码进行处理)。程序运行时的做法是在字节代码将要被虚拟机加载之前对字节代码进行修改,通常有增强代理和类加载器实现(一般框架使用第二种方式)
使用ASM
可以实现下列三种(使用ASM即是在程序运行时的字节码增加)
- 读取字节代码
- 生成字节代码
- 修改字节代码
读取字节代码
import org.objectweb.asm.*;
import java.io.IOException;
/**
* 利用asm统计String类中方法个数
* @author: whp
* @create: 2020-01-17 23:54
**/
public class MethodCounter implements ClassVisitor {
private int count = 0;
@Override
public void visit(int i, int i1, String s, String s1, String s2, String[] strings) {
}
@Override
public void visitSource(String s, String s1) {
}
@Override
public void visitOuterClass(String s, String s1, String s2) {
}
@Override
public AnnotationVisitor visitAnnotation(String s, boolean b) {
return null;
}
@Override
public void visitAttribute(Attribute attribute) {
}
@Override
public void visitInnerClass(String s, String s1, String s2, int i) {
}
@Override
public FieldVisitor visitField(int i, String s, String s1, String s2, Object o) {
return null;
}
//访问访问时,该方法会被调用
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
count++;
System.out.println(s);//打印出方法名
return null;
}
@Override
public void visitEnd() {
}
public int getCount(){
return count;
}
public static void main(String[] args) throws IOException {
ClassReader reader = new ClassReader("java.lang.String");
MethodCounter methodCounter = new MethodCounter();
reader.accept(methodCounter,0);
System.out.println(methodCounter.getCount());
}
}
生成字节代码
一般先按照设计思路编写最终的java源代码,在编译字节码,再利用asm中类逆向生成,使用ASM的java代码!
修改字节代码
基本思想类似,也是会利用逆向的思想
增强代理
ASM 的 使用方式 主要分为两种
- 在源代码编译之后,在运行相关的程序来对字节代码进行处理
- 运行时处理,可以使用类加载器来实现,不过比较麻烦,一种更简单的方式是使用 java.lang.instrument包提供的API,其中比较重要的是Instrumentation这个接口
增强代理的的用法
代理程序是一个jar包,该jar包清单文件中定义了启动代理的java类名称,不同的虚拟机实现提供的启动代理的方式不尽相同。主要有以下两种。第一种做法是通过虚拟机的启动参数指定代理程序jar包的路径。所用参数是“-javaagent”,代理jar包的清单文件要包含Premain-Class属性。该属性指定一个java类,该类中包含一个premain方法,虚拟机启动之后,代理类的premain会先被调用,然后才是主java类的main方法被调用 ; 第二种方法是虚拟机运行主程序之后,再启动代理程序,这类的代理程序jar包清单文件要有Agent-Class属性指明代理类的名称。当代理类被加载后,虚拟机会尝试调用类中的agentmain方法
详细参考
基于Java Instrument的Agent实现
注解的使用场景
处理注解的两种方式:
- 一种是编译时,对于保留策略是SOURCE的注解来说,编译时是惟一的处理方式,类似@override,@deprecated ,拓展知识 JSR-269 (可插拔式注解处理机制)
- 对于注解声明为运行时仍然保留,那么可以通过反射API操作