gradle

ASM介绍

2020-08-17  本文已影响0人  jxcyly1985

ASM介绍

[TOC]

背景和痛点

你是否经历过下面那些让你万马奔腾的场景

这个时候你是否作为一个工程师,能否这些事情自己写个程序做了,改改程序的配置,就自动搞定。那下面介绍的ASM字节码操作框架以及扩展的APT工具和面向AOP的框架可以提供一种思路。

ASM是一个字节码操作框架

特点

作用

ASM对比

java.lang.ref.proxy

BCEL && SERP && javassist 框架对比

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"

指令可以基本分为以下几类:

Java字节码指令大全

The Java® Virtual Machine Specification

class.jpg

Java栈(JVM)

stack-frame.png

JVM && Dalvik

Stack versus Registers

基于指令集的不同

访问者模式

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

visitor.jpg

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

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

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

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

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

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

使用

ASM官方文档

编程框架

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
适用场景

编译后处理[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是下面(不限于)的这些类型

*:匹配任何数量字符;
..:匹配任何数量字符的重复,如在类型模式中匹配任何数量子包;而在方法参数模式中匹配任何数量参数。
+:匹配指定类型的子类型;仅能作为后缀放在类型模式后边。
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

运行时加载

前面所述的方案时,有两个问题

那是否有直接在运行时加载新的class类方式,JVM的提供的JVMTI接口

运行时加载

批评

场景

通常来说,一个切面是分散的,缠绕的代码,从而难以理解和维护。

切面分散的原因是由于(类似日志)函数分布在很多不相关的使用了切面函数的函数中,以及可能不相关的系统中,不同的源语言中。这就意味着改变日志需要修改所有相关的模块。

切面不仅仅缠绕在系统所表达的主线功能中,同样切面之间也互相缠绕。这意味着修改一个关注点需要理解所有缠绕的关注点,同样需要一些方法推测会修改带来的影响。

从之前评判的角度来看,大量使用动态代码方式处理核心关键业务是噩梦, 而日志埋点、性能监控、动态权限控制、甚至是代码调试这些相对独立的非关键业务对提高代码重用性,维护性上有较好的效果。

软件质量

运行时质量 开发时质量
正确 易理解
性能 扩展性
稳定可靠 重用性
容错 维护性
安全 可测试
易用

ASM的横向关注点分离方式通过可编程的的方式提升了实现的灵活性,实现了模块之间的解耦,提升了开发时的质量,从而也间接提升了软件的运行时质量。

AOP

配置

基于ASM的组件化框架

Android

Android AOP编程的四种策略探讨:Aspectj,cglib+dexmaker,Javassist,epic+dexposed

插件

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.

design gradle plugin

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

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

上一篇下一篇

猜你喜欢

热点阅读