ASM介绍
[TOC]
背景和痛点
你是否经历过下面那些让你万马奔腾的场景
当你在工程的代码中按照产品增加了埋点信息,然后产品经理说某个模块A的1000多处埋点信息需要去掉,当你注释去掉之后,第二天产品经理说去错了,模块A需要加回去,然后是需要把模块B的1000多处埋点去掉,当你傻逼逼的整完之后,第三天产品经理说老板说模块B不能去掉,需要加回去 【apt + asm】
-
某些初级工程师小A,写了一个业务对象,今天跟你说构造参数变了,你需要跟着同步一下,你发现你有100多处创建了这个对象。
你改完了100多处,第二天公司一起编译的时候,你的模块编译报错了,你找小A,小A说我又想了还是应该改回去了,但是忘记同步你了,你心中万马奔腾,然后又手动修改了100多处 【dagger2】
测试常常需要你改参数验证场景,你把相关流程参数的Ctrl+F搜了一遍,然后Replace了一遍,发了一个包,测试验证结果不对,你发现有一个地方的参数不能变,改了一下继续再次验证。测试完后,测试又来烦你,开发哥哥把参数再改一个值,需要验证第二个场景,然后验证第三个场景的时,你怒了,麻痹老子一天就陪你玩Ctrl+F了
这个时候你是否作为一个工程师,能否这些事情自己写个程序做了,改改程序的配置,就自动搞定。那下面介绍的ASM字节码操作框架以及扩展的APT工具和面向AOP的框架可以提供一种思路。
ASM是一个字节码操作框架
特点
- 屏蔽了字节码格式的相关细节
- 现代的[1]编程模型
作用
- 动态生成类
- 增强既有类的功能
ASM对比
java.lang.ref.proxy
- 面向接口化的设计
- 反射的性能损失
BCEL && SERP && javassist 框架对比
- ASM框架包体积小,只有33K
- ASM类转换的负载低
- SERP、javassist采用java编码的形式,不需要了解虚拟机指令
- BCEL、ASM需要了解class的组织结构和jvm指令,性能的优势带来使用的学习成本较高
Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)
原理
Class文件结构
public class HelloWorld{
public static void main(String[] args) {
System.out.println("Hello world");
}
}
命令
javap -v xxxxxxx.class
Classfile /D:/doc/attachment/HelloWorld.class
Last modified 2020-7-30; size 425 bytes
MD5 checksum 1bee14068c12eb08a10f158de4fdbf77
Compiled from "HelloWorld.java"
public class HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
}
SourceFile: "HelloWorld.java"
-
常量池:
- 序号 + 类型 + 内容
descriptor 方法描述符 ,使用 (参数)返回值 格式
flags 方法的访问标记
Code 方法内字节码指令描述
指令前的数字表示这条指令到方法开始的偏移量
LineNumberTable是代码行数对应方法开始的指令偏移量
指令可以基本分为以下几类:
- 存储指令 (例如:aload_0, istore)
- 算术与逻辑指令 (例如: ladd, fcmpl)
- 类型转换指令 (例如:i2b, d2i)
- 对象创建与操作指令 (例如:new, putfield)
- 堆栈操作指令 (例如:swap, dup2)
- 控制转移指令 (例如:ifeq, goto)
- 方法调用与返回指令 (例如:invokespecial, areturn)
The Java® Virtual Machine Specification
- Magic: 该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
- Version: 该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
- Constant Pool: 该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
- Access_flag: 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
- This Class: 指向表示该类全限定名称的字符串常量的指针。
- Super Class: 指向表示父类全限定名称的字符串常量的指针。
- Interfaces: 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
- Fields: 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
- Methods: 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
- Class attributes: 该项存放了在该文件中类或接口所定义的属性的基本信息。
Java栈(JVM)
JVM && Dalvik
基于指令集的不同
jvm面向堆栈架构
-
dalvik面向寄存器架构
Java字节码被转换成Dalvik虚拟机所使用的替代指令集
访问者模式
针对相同的节点结构实现不同的处理
Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。
ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
类似语法树的结构都可以使用访问者模式来访问元素,提供相应的接口回调执行特定的处理
使用
编程框架
-
ClassReader
获取字节码数据,分析字节码,生成字节码抽象树
-
ClassVisitor
提供节点的访问处理
A visitor to visit a Java class. The methods of this class must be called in the following order:
visit
[visitSource
] [visitModule
][visitNestHost
][visitPermittedclass
][visitOuterClass
] (visitAnnotation
|visitTypeAnnotation
|visitAttribute
)* (visitNestMember
|visitInnerClass
|visitRecordComponent
|visitField
|visitMethod
)*visitEnd
.[2] ClassWriter
public class NewBankGenerator {
public void generate() {
try {
ClassReader classReader = new ClassReader("com.example.apidemo.asm.Bank");
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
BankVisitor bankVisitor = new BankVisitor(Opcodes.ASM8, classWriter);
classReader.accept(bankVisitor, ClassReader.SKIP_DEBUG);
byte[] classByte = classWriter.toByteArray();
File file = new File("Bank.class");
FileOutputStream fileOutputStream = new FileOutputStream(file);
fileOutputStream.write(classByte, 0, classByte.length);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
class BankVisitor extends ClassVisitor {
public BankVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
System.out.println("visitMethod access " + access + " name " + name + " descriptor " +descriptor + " signature " + signature);
if ("account".equals(name) || "deposit".equals(name) || "withdraw".equals(name)) {
System.out.println("SecurityCheck inject");
methodVisitor.visitTypeInsn(NEW, "com/example/apidemo/asm/SecurityCheck");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "com/example/apidemo/asm/SecurityCheck", "<init>", "()V", false);
methodVisitor.visitVarInsn(ASTORE, 1);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/example/apidemo/asm/SecurityCheck", "check", "()V", false);
methodVisitor.visitMaxs(2, 2);
}
return methodVisitor;
}
}
}
ClassWriter(0)
:表示 ASM 不会自动自动帮你计算栈帧和局部变量表和操作数栈大小。
ClassWriter(ClassWriter.COMPUTE_MAXS)
:表示 ASM 会自动帮你计算局部变量表和操作数栈的大小,但是你还是需要调用visitMaxs
方法,但是可以使用任意参数,因为它们会被忽略。带有这个标识,对于栈帧大小,还是需要你手动计算。
ClassWriter(ClassWriter.COMPUTE_FRAMES)
:表示 ASM 会自动帮你计算所有的内容。你不必去调用visitFrame
,但是你还是需要调用visitMaxs
方法(参数可任意设置,同样会被忽略)
ClassLoader
运行时产生新的class文件,并通过ClassLoder.defineClass生产新的增强类
APT
编译前APT处理器的加载
javac 本身提供了编译时候注解相关的选项,实际通过SPI[ServiceLoader]方式调用
javac -processor com.example.apt_processor.AptProcessor
D:\Users\11123013>javac
用法: javac <options> <source files>
其中, 可能的选项包括:
-g 生成所有调试信息
-g:none 不生成任何调试信息
-g:{lines,vars,source} 只生成某些调试信息
-nowarn 不生成任何警告
-verbose 输出有关编译器正在执行的操作的消息
-deprecation 输出使用已过时的 API 的源位置
-classpath <路径> 指定查找用户类文件和注释处理程序的位置
-cp <路径> 指定查找用户类文件和注释处理程序的位置
-sourcepath <路径> 指定查找输入源文件的位置
-bootclasspath <路径> 覆盖引导类文件的位置
-extdirs <目录> 覆盖所安装扩展的位置
-endorseddirs <目录> 覆盖签名的标准路径的位置
-proc:{none,only} 控制是否执行注释处理和/或编译。
-processor <class1>[,<class2>,<class3>...] 要运行的注释处理程序的名称; 绕过默认的搜索进程
-processorpath <路径> 指定查找注释处理程序的位置
-parameters 生成元数据以用于方法参数的反射
-d <目录> 指定放置生成的类文件的位置
-s <目录> 指定放置生成的源文件的位置
-h <目录> 指定放置生成的本机标头文件的位置
-implicit:{none,class} 指定是否为隐式引用文件生成类文件
-encoding <编码> 指定源文件使用的字符编码
-source <发行版> 提供与指定发行版的源兼容性
-target <发行版> 生成特定 VM 版本的类文件
-profile <配置文件> 请确保使用的 API 在指定的配置文件中可用
-version 版本信息
-help 输出标准选项的提要
-A关键字[=值] 传递给注释处理程序的选项
-X 输出非标准选项的提要
-J<标记> 直接将 <标记> 传递给运行时系统
-Werror 出现警告时终止编译
@<文件名> 从文件读取选项和文件名
google-AutoService 用于生成SPI描述文件
Element & Type
-
element
表示程序元素,如包、类或方法。每个元素表示一个静态的语言级构造(而不是,例如,虚拟机的运行时构造)。
-
Type
类型包括基元类型、声明类型(类和接口类型)、数组类型、类型变量和null类型。还表示通配符类型参数、可执行文件的签名和返回类型,以及与包和关键字void对应的伪类型。
适用场景
- 适用于代码结构重复,调用入口比较固定统一的情况
编译后处理[todo]
经典常用框架
ButterKnife
通过processor进行元素遍历处理,同时使用JavaFileObject生成中间java文件参与共同编译
生成中间java文件在 \app\build\generated\source\apt\debug\com\example\apt_test\MainActivity_ViewBinding.java
[取决于当前的gradle插件的版本]
编织
编织的能力意味着可以针对已经编写好的java文件,解析java文件,或者解析生成的class文件,然后把对应的字节码织入到被增强代码的合适位置
weave流程[todo]
发散
AspectJ
AOP框架,通过特定切入点语言定义切入点模型,内部编织器使用bcel字节码操作框架和asm框架(?为什么存在多个字节码操作框架)
语法
category(<注解?> <修饰符?> <返回值类型> <类型声明?>.<方法名>(参数列表) <异常列表>?
category是下面(不限于)的这些类型
call
execution
initilization
set
get
*:匹配任何数量字符;
..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式
execution(@com.example.apt_annotation.LogTime public void com.example.apidemo.aspectj.AjExample.calc())
匹配LogTime 注解 ,com.example.apidemo.aspectj.AjExample.calc 方法体
execution(@com.example.apt_annotation.LogTime public void com.example.apidemo.aspectj.AjExample.*(..))
匹配LogTime 注解 ,com.example.apidemo.aspectj.AjExample.下所有方法
execution(@com.example.apt_annotation.LogTime * com.example.apidemo.*(..))
匹配LogTime 注解 ,com.example.apidemo 包下所有方法
Spring框架[todo]
CGLib
动态代理框架,cglib内部是使用asm库动态处理字节码,内部通过继承方式(子类化)拦截目标类的请求,对目标类功能进行增强
Java文件生成框架
JavaWriter && JavaPoet && CodeModel
- 适用于静态编译期间预生成.java代码参与整个软件的编译,是静态的能力增强
- asm框架是在运行时动态的改变行为
运行时加载
前面所述的方案时,有两个问题
- 静态编译时操作,行为在编译后就确定了
- 在条件符合时动态创建新的classLoader加载修改后的class类文件[3]
- 调用方式需要调整,对使用者不友好
- 需要实现类似classloader内部缓存管理的细节,比较复杂
那是否有直接在运行时加载新的class类方式,JVM的提供的JVMTI接口
通过PID向目标应用的JVM发送load执行,使其加载对应的Agent.lib,获取Instrumentation对象
通过Instrumentation添加Transfromer方式增加JVM的hook处理,在JVM加载类之前可以进行ClassFileTransformer的transform操作
批评
IDE的难以显示支持,以及代码的调试增加难度
虽然AOP方式是为了改进"模块性和代码结构",但是同样也有针对破坏模块化的争议,阻碍了“程序的独立开发和可理解性”
脆弱的切入点问题,对增强类的修改极其敏感,被增强的类和增强代码之间存在很强的耦合性
场景
通常来说,一个切面是分散的,缠绕的代码,从而难以理解和维护。
切面分散的原因是由于(类似日志)函数分布在很多不相关的使用了切面函数的函数中,以及可能不相关的系统中,不同的源语言中。这就意味着改变日志需要修改所有相关的模块。
切面不仅仅缠绕在系统所表达的主线功能中,同样切面之间也互相缠绕。这意味着修改一个关注点需要理解所有缠绕的关注点,同样需要一些方法推测会修改带来的影响。
从之前评判的角度来看,大量使用动态代码方式处理核心关键业务是噩梦, 而日志埋点、性能监控、动态权限控制、甚至是代码调试这些相对独立的非关键业务对提高代码重用性,维护性上有较好的效果。
软件质量
运行时质量 | 开发时质量 |
---|---|
正确 | 易理解 |
性能 | 扩展性 |
稳定可靠 | 重用性 |
容错 | 维护性 |
安全 | 可测试 |
易用 |
ASM的横向关注点分离方式通过可编程的的方式提升了实现的灵活性,实现了模块之间的解耦,提升了开发时的质量,从而也间接提升了软件的运行时质量。
- 通过模块化抽离拆分的方式把水平关注点进行封装,降低了代码的规模和复杂性,从而提升了代码理解性
- 把非核心逻辑的剥离,提升了核心模块的逻辑稳定性,那就意味着可以进行良好的抽象和实现的分离,从而提升系统的可扩展性
- 细颗粒度的职责清晰单一的关注点封装本身就是良好的重用性和维护性的前提
- 和核心业务逻辑的解耦,方便进行独立的测试
AOP
- 传统的面向对象的编程注重"纵向"的继承方式的表达,相同功能和切面的模块之间的“横向”关系不能很好的表达
- 传统的面向对象的结构性设计模式(修饰者模式)可以一定程度上通过解耦的方式增强既有类的功能,但是重复的改造的工作量随着切面覆盖量成比例增加
- 传统面向的设计模式,最终体现在代码结构上,横向的功能代码依然是分散的
- 通过可编程方式,动态的生成增强代码成为解决AOP的一种方式
配置
- 通过基于APT方式脚本化动态的生成配置代码从而减少手工操作带来的重复性和易错性
Android
Android AOP编程的四种策略探讨:Aspectj,cglib+dexmaker,Javassist,epic+dexposed
插件
-
asm bytecode outline
Displays bytecode for Java classes and ASMified code which will help you in your class generation.
-
asm bytecode viewer
Displays bytecode for Java classes and ASMified code which will help you in your class generation.
实战
Gradle插件
相关的Android Gradle 插件提供的Api文档
如何编写基于Android Gradle的插件
apply plugin: 'groovy'
repositories {
jcenter()
google()
}
dependencies {
implementation gradleApi()//gradle sdk
implementation localGroovy()//groovy sdk
implementation 'com.android.tools.build:gradle:4.0.1'
}
There are several places where you can put the source for the plugin.
-
Build script
You can include the source for the plugin directly in the build script. This has the benefit that the plugin is automatically compiled and included in the classpath of the build script without you having to do anything. However, the plugin is not visible outside the build script, and so you cannot reuse the plugin outside the build script it is defined in.
-
buildSrc
projectYou can put the source for the plugin in the
*rootProjectDir*/buildSrc/src/main/java
directory (or*rootProjectDir*/buildSrc/src/main/groovy
or*rootProjectDir*/buildSrc/src/main/kotlin
depending on which language you prefer). Gradle will take care of compiling and testing the plugin and making it available on the classpath of the build script. The plugin is visible to every build script used by the build. However, it is not visible outside the build, and so you cannot reuse the plugin outside the build it is defined in.See Organizing Gradle Projects for more details about thebuildSrc
project. -
Standalone project
You can create a separate project for your plugin. This project produces and publishes a JAR which you can then use in multiple builds and share with others. Generally, this JAR might include some plugins, or bundle several related task classes into a single library. Or some combination of the two.
这是gradle官方提供的编写gradle插件的指导
继承Plugin类,添加相关的任务
编写Extension扩展,提供外部进行参数配置
继承DefaultTask,读取外部配置的参数,实现具体的任务
-
配置插件实现类信息
src/main/resources/META-INF/gradle-plugins/org.samples.greeting.properties implementation-class=org.gradle.GreetingPlugin
Transform插件
Transform说明
Transform 是用来处理构建的中间物
-
输入,包括JarInput和DirectoryInput
-
输出,返回自定义的内容的输出信息
因此transform插件可以用于拦截编译过程中class输出阶段,通过对class的拦截,进行asm的字节码编辑
老版官网编译过程
新版编译过程
Transform运行结果
添加Transform处理
public class Bank {
int mAccount;
int mCash;
public void account() {
mAccount++;
Log.d("Bank", "mAccount++");
}
public void deposit() {
mCash++;
Log.d("Bank", "mCash++");
}
public void withdraw() {
mCash--;
Log.d("Bank", "mCash++");
}
}
转换后
public class Bank {
int mAccount;
int mCash;
public Bank() {
Log.d("ASM-Bank", "<init>");
}
public void account() {
SecurityCheck var1 = new SecurityCheck();
var1.check();
++this.mAccount;
Log.d("Bank", "mAccount++");
Log.d("ASM-Bank", "account");
}
public void deposit() {
SecurityCheck var1 = new SecurityCheck();
var1.check();
++this.mCash;
Log.d("Bank", "mCash++");
Log.d("ASM-Bank", "deposit");
}
public void withdraw() {
SecurityCheck var1 = new SecurityCheck();
var1.check();
--this.mCash;
Log.d("Bank", "mCash++");
Log.d("ASM-Bank", "withdraw");
}
}
运行结果
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/ASM-Bank: <init>
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo I/System.out: check
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/Bank: mAccount++
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/ASM-Bank: account
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo I/System.out: check
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/Bank: mCash++
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/ASM-Bank: deposit
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo I/System.out: check
2020-08-03 16:34:26.046 27077-27077/com.example.apidemo D/Bank: mCash++
2020-08-03 16:34:26.047 27077-27077/com.example.apidemo D/ASM-Bank: withdraw
总结
各种基于ASM,APT技术和切面的框架,都是提供了一种通过不在业务中直接硬编码的方式,而是通过我们自己定义抽象规则的方式,相当于在你业务之上增加了一个抽象中间层,让你可以利用定义的抽象规则可编程的去操作你的业务代码,减少那些重复的手动工作量和出错的可能性,提升灵活性。
比如业务有个接口改变了,需要增加传入调用类的hashcode做映射,如果接口很多地方调用就会需要修改很多处,如果识别出易变的业务和不易变的业务,把易变的业务抽象成规则,那么基于规则就可以很好的统一处理。
附录
[1] 现代的是指进行相关的关注点分离,应用相关的设计模式进行了业务的协作,而不是传统的过程式,函数式的模型,更加符合面向对象的设计原则,从而相对来说现代的编程模式对外的接口更加简洁,使用更加简单。
[2]来源官方文档ClassVisitor
[3]为什么需要新的ClassLoader,因为同一个ClassLoader对于已经加载的类不能进行覆盖,也就是说JVM不能在运行时候重载一个类