ASM介绍

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)

Java字节码指令大全

The Java® Virtual Machine Specification

class.jpg
  • 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)

stack-frame.png

JVM && Dalvik

Stack versus Registers

基于指令集的不同

  • jvm面向堆栈架构

  • dalvik面向寄存器架构

    Java字节码被转换成Dalvik虚拟机所使用的替代指令集

访问者模式

针对相同的节点结构实现不同的处理

visitor.jpg

Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。

ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。

Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。

ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。

ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。

类似语法树的结构都可以使用访问者模式来访问元素,提供相应的接口回调执行特定的处理

使用

ASM官方文档

编程框架

  • 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方法(参数可任意设置,同样会被忽略)

stack-frame.png

ClassLoader

运行时产生新的class文件,并通过ClassLoder.defineClass生产新的增强类

new-bank.png

APT

编译前APT处理器的加载

android的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描述文件

annotation-process.png
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文件,然后把对应的字节码织入到被增强代码的合适位置

weaver.png

weave流程[todo]

发散

AspectJ

AOP框架,通过特定切入点语言定义切入点模型,内部编织器使用bcel字节码操作框架和asm框架(?为什么存在多个字节码操作框架)

aspectj.png
语法

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库动态处理字节码,内部通过继承方式(子类化)拦截目标类的请求,对目标类功能进行增强

cglib.png

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方式脚本化动态的生成配置代码从而减少手工操作带来的重复性和易错性

基于ASM的组件化框架

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.

asm-plugin.png

实战

【Android】函数插桩(Gradle + ASM)

Gradle插件

Android Gradle Api

Android Gradle Javadoc

相关的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'
}


Packaging a plugin

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 project

    You 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 the buildSrc 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.

design gradle plugin

这是gradle官方提供的编写gradle插件的指导

  • 继承Plugin类,添加相关的任务

  • 编写Extension扩展,提供外部进行参数配置

  • 继承DefaultTask,读取外部配置的参数,实现具体的任务

  • 配置插件实现类信息

    src/main/resources/META-INF/gradle-plugins/org.samples.greeting.properties
    implementation-class=org.gradle.GreetingPlugin
    

Transform插件

Transform说明

Transform 是用来处理构建的中间物

因此transform插件可以用于拦截编译过程中class输出阶段,通过对class的拦截,进行asm的字节码编辑

老版官网编译过程

compile.jpg

新版编译过程

newcomile.png
Transform运行结果
android.png
transform.png

添加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不能在运行时候重载一个类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345