字节码增强技术
ASM
ASM是对java字节码操纵框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。asm字节码增强技术主要是用来反射的时候提升性能的,如果单纯用jdk的反射调用,性能是非常低下的,而使用字节码增强技术后反射调用的时间已经基本可以与直接调用相当了。
spring 的Aop中使用的cglib的底层原理便是asm。
ASM采用的是责任链模式:
ASM Core API可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:
- ClassReader:用于读取已经编译好的.class文件。
- ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
- 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的AnnotationVisitor等。为了实现AOP,重点要使用的是MethodVisitor。
使用的场景有很多,我们这里只讲一个动态替换class文件的方法。
我们这里的使用场景:热部署
用我们公司的项目来说:项目为多个策略配置类替换,使用自定义类加载器加载的每个策略,然后反射生成一个个单例配置策略对象放在map中。策略数量有限。
因为线上的配置类会在线更换,还需要重新动态修改方法逻辑等,自己定义AOP功能,操作且要求项目不能随意重启。所以只能热部署的方式来加载要修改的class,自己定义了一个类加载器他继承了双亲委派机制,因为还有个父类要用apploader加载,因为一个类加载器多次,报类加载异常。所以,我想到了使用热部署的方式,更换策略对象。
因为我们线上使用的是一个单例的策略类,把当前类替换即可(ps:这里也有另一种方式:java agent)
1,更新class类名这样类加载不会报错,实现热部署。
2,实现不依托spring实现的 自定义框架AOP .
1,引用jar 包
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm-commons -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.1</version>
</dependency>
2,使用ASM
(1):重命名class
原class
public class HelloAsm {
private int aa;
private static String b = "123";
private static final String c = "abc";
private static HashMap hashMap = new HashMap();
public void test2() {
int aa = this.aa;
hashMap.put("22", "11");
System.out.println("Hello ASM :" + c);
}
public void test() {
System.out.println(">>>>>>>>> 我是测试 ");
}
新class
public class GoodAsm {
private int aa;
private static String b = "123";
private static final String c = "abc";
private static HashMap hashMap = new HashMap();
public GoodChild1() {
}
public void test2() {
int var1 = this.aa;
hashMap.put("22", "11");
System.out.println("Hello ASM :abc");
}
public void test() {
System.out.println(">>>>>>>>> 我是测试 ");
}
}
public static void main(String[] args) throws Exception {
String old_name = "com/my/asm/HelloWorld";
String old_name_path = "F:\\davinqi\\target\\classes\\com\\my\\asm\\HelloWorld.class";
String new_name = "com/my/asm/GoodChild1";
InputStream fileInputStream = new FileInputStream( new File(old_name_path ));
// 读class对象
ClassReader cr = new ClassReader(fileInputStream);
// 构建Class写对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// ClassVisitor
Remapper remapper = new SimpleRemapper(old_name , new_name );
ClassVisitor cv = new ClassRemapper(cw, remapper);、
cr.accept(cv, ClassReader.SKIP_DEBUG);
// 生成新Class
byte[] bytes2 = cw.toByteArray();
FileOutputStream fos = new FileOutputStream("F:\\davinqi\\src\\main\\java\\com\\my\\asm\\GoodChild1.class");
fos.write(bytes2);
fos.close();
}
执行完成后在指定路径下生成的class文件,我们就可用使用此类加载到jvm 反射生成实例对象。
F:\davinqi\src\main\java\com\my\asm\GoodAsm.class:
(2):重命名方法名
TODO
(3):方法修改增强
(4):字段修改
图片以及部分描述引用:
https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
本文只是简单记录下,如果有错误希望大家能指正。