1. 简介

ASM是assemble英文的简称,中文名为汇编,官方地址https://asm.ow2.io/,下面是官方的一段英文简介:

ASM is an all purpose Java bytecode manipulation and analysis framework. It can be used to modify existing classes or to dynamically generate classes, directly in binary form. ASM provides some common bytecode transformations and analysis algorithms from which custom complex transformations and code analysis tools can be built. ASM offers similar functionality as other Java bytecode frameworks, but is focused on performance. Because it was designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but can of course be used in a static way too, e.g. in compilers).

ASM is used in many projects, including:

  • the OpenJDK, to generate the lambda call sites, and also in the Nashorn compiler,
  • the Groovy compiler and the Kotlin compiler,
  • Cobertura and Jacoco, to instrument classes in order to measure code coverage,
  • CGLIB, to dynamically generate proxy classes (which are used in other projects such as Mockito and EasyMock),
  • Gradle, to generate some classes at runtime.

翻译如下:

ASM是一个通用的Java字节码操作和分析框架。它可用于修改现有的类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,从中可以构建定制的复杂转换和代码分析工具。ASM提供了与其他Java字节码框架类似的功能,但侧重于性能。因为它被设计和实现得尽可能的小和快,所以它非常适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。

主要用途:

  • OpenJDK,用来生成lambda调用站点,还有在Nashorn编译器中,
  • Groovy编译器和Kotlin编译器,
  • Cobertura和Jacoco,用来测量代码覆盖率,
  • CGLIB为了动态生成代理类(在其他项目中使用,如mock和EasyMock),
  • Gradle在运行时生成一些类。

2. 框架使用

官方提供了使用手册,地址:https://asm.ow2.io/asm4-guide.pdf,引入依赖

<dependency>
<groupId>asm</groupId>
<artifactId>asm-all</artifactId>
<version>3.3.1</version>
</dependency>

下面结合例子分析

2.1 ClassReader--解析一个类文件

创建一个T1类

package com.wzj.asm;

/**
* 光标必须位于类体内,View-Show ByteCode
*/ public class T1 {
int i = 0;
public void m() {
int j=1;
} }

在idea中的安装插件ByteCode插件



然后通过idea中的View菜单->show bytecode看到字节码文件





下面的ClassPrinter类用来实现解析T1.Class这个类

package com.wzj.asm;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor; import java.io.IOException; import static org.objectweb.asm.Opcodes.ASM4; /**
* @Author: wzj
* @Date: 2020/8/5 21:29
* @Desc: 解析一个类
*/
public class ClassPrinter extends ClassVisitor {
public ClassPrinter() {
super(ASM4);
} @Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(name + " extends " + superName + "{" );
} @Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
System.out.println(" " + name);
return null;
} @Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println(" " + name + "()");
return null;
} @Override
public void visitEnd() { System.out.println("}");
} public static void main(String[] args) throws IOException {
ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader(
ClassPrinter.class.getClassLoader().getResourceAsStream("com/wzj/asm/T1.class")); cr.accept(cp, 0);
}
}

visit方法访问类的类名、父类等信息,visitField方法访问类的属性信息,visitMethod方法访问类的方法信息,最后打印出该类的信息

2.2 ClassWriter--生成一个类文件

package com.wzj.asm;

import org.objectweb.asm.ClassWriter;

import java.io.File;
import java.io.FileOutputStream; import static org.objectweb.asm.Opcodes.*; /**
* @Author: wzj
* @Date: 2020/8/5 21:26
* @Desc: 生成一个类
*/
public class ClassWriterTest {
public static void main(String[] args) throws Exception {
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
null);
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, -1).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, 0).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, 1).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray(); MyClassLoader myClassLoader = new MyClassLoader();
Class c = myClassLoader.defineClass("pkg.Comparable", b);
System.out.println(c.getMethods()[0].getName()); String path = (String)System.getProperties().get("user.dir");
File f = new File(path + "/com/wzj/asm/");
f.mkdirs();
FileOutputStream fos = new FileOutputStream(new File(path + "/com/wzj/asm/Comparable.class"));
fos.write(b);
}
}

自定义一个类加载器

package com.wzj.asm;

/**
* @Author: wzj
* @Date: 2020/8/5 21:26
* @Desc: 自定义类加载器
*/
public class MyClassLoader extends ClassLoader{
public Class defineClass(String name, byte[] b) {
return defineClass(name, b, 0, b.length);
}
}

生成的类文件如下

package pkg;

public interface Comparable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1; int compareTo(Object var1);
}

2.3 利用ClassVisitor对原始类方法增强功能

依然使用之前代理系列的例子Apple类

package com.wzj.asm;

import com.wzj.proxy.v8.Sellalbe;

import java.util.Random;

/**
* @Author: wzj
* @Date: 2020/8/3 10:29
* @Desc: 待销苹果
*/
public class Apple implements Sellalbe { @Override
public void secKill() {
System.out.println("苹果正在秒杀中...");
try {
Thread.sleep(new Random().nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

ClassTransformedTest类,内部类ClassVisitor实现父类的方法visitMethod,并在方法中判断目标方法为secKill时,对该方法增加TimeProxy.before()方法,代码如下

package com.wzj.asm;

import org.objectweb.asm.*;

import java.io.File;
import java.io.FileOutputStream; import static org.objectweb.asm.Opcodes.ASM4;
import static org.objectweb.asm.Opcodes.INVOKESTATIC; /**
* @Author: wzj
* @Date: 2020/9/1 21:10
* @Desc: 类方法增强
*/
public class ClassTransformedTest {
public static void main(String[] args) throws Exception {
ClassReader cr = new ClassReader(
ClassPrinter.class.getClassLoader().getResourceAsStream("com/wzj/asm/Apple.class")); ClassWriter cw = new ClassWriter(0);
ClassVisitor cv = new ClassVisitor(ASM4, cw) {
//增强secKill方法, 在方法里加入时间代理
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(ASM4, mv) {
@Override
public void visitCode() {
if(name.equals("secKill")) {
visitMethodInsn(INVOKESTATIC, "com/wzj/asm/TimeProxy","before", "()V", false);
super.visitCode();
}
}
};
}
}; cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); MyClassLoader cl = new MyClassLoader();
Class c2 = cl.defineClass("com.wzj.asm.Apple", b2);
c2.getConstructor().newInstance(); String path = (String)System.getProperties().get("user.dir");
File f = new File(path + "/com/wzj/asm/"); if(!f.exists()) {
f.mkdirs();
} FileOutputStream fos = new FileOutputStream(new File(path + "/com/wzj/asm/Apple.class"));
fos.write(b2);
fos.flush();
fos.close(); }
}

最终生成的增强类如下

package com.wzj.asm;

import com.wzj.proxy.v8.Sellalbe;
import java.util.Random; public class Apple implements Sellalbe {
public Apple() {
} public void secKill() {
TimeProxy.before();
System.out.println("苹果正在秒杀中..."); try {
Thread.sleep((long)(new Random()).nextInt(3000));
} catch (InterruptedException var2) {
var2.printStackTrace();
} }
}

3. 框架剖析

ASM的核心类是ClassVisitor、MethodVisitor,通过访问者模式,对字节码文件类的信息、方法的信息进行增加或者修改,因为对于一个java的class类,它的结构是完全固定的,包括大致10几项,分别为Magic(魔数)、Version(版本)、Constant Pool(常量池)、Access_flag(访问标识)、This Class(本实例指针)、Super Class(父类实例指针)、Interfaces(接口)、Fields(字段)、Methods(方法)、Class attributes(类属性)等。

在 ASM 中,ClassReader 类,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept 方法,这个方法接受一个实现了 ClassVisitor 接口的对象实例作为参数,然后依次调用 ClassVisitor 接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用, ClassReader 知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。

ClassVisitor 会产生一些子过程,比如 visitMethod 会返回一个实现 MethordVisitor 接口的实例, visitField 会返回一个实现 FieldVisitor 接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 ClassReader 来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 ClassReader 类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。

各个 ClassVisitor 通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。官方给出了如下图说明通过责任链修改类,分别代表简单责任链与复杂责任链下各种的应用

ClassAdaptor 类实现了 ClassVisitor 接口所定义的所有函数,当新建一个 ClassAdaptor 对象的时候,需要传入一个实现了 ClassVisitor 接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor 类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。官方也给出了一个适配器模式的图

每个 ClassAdaptor 类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。

【趣味设计模式系列】之【代理模式4--ASM框架解析】的更多相关文章

  1. 设计模式系列之代理模式(Proxy Pattern)——对象的间接访问

    说明:设计模式系列文章是读刘伟所著<设计模式的艺术之道(软件开发人员内功修炼之道)>一书的阅读笔记.个人感觉这本书讲的不错,有兴趣推荐读一读.详细内容也可以看看此书作者的博客https:/ ...

  2. C#设计模式系列:代理模式(Proxy)

    代理模式提供了一个中介控制对某个对象的访问.现实生活中,我们可能会用支票在市场交易中用来代替现金,支票就是账户中资金的代理. 1.代理模式简介 1.1>.定义 代理模式(Proxy)定义:代理模 ...

  3. C#设计模式系列:代理模式(Proxy Pattren)

    一.引言 在软件开发过程中,有些对象有时候会由于网络或者其他的障碍,以至于不能够或者不能直接访问到这些对象,如果直接访问对象给系统带来不必要的复杂性,这时候可以在客户端和目标对象之间增加一层中间层,让 ...

  4. 【趣味设计模式系列】之【代理模式3--Cglib动态代理源码解析】

    1. 图解 上图主要描述了Cglib动态代理的主要执行过程,下面做详细分析,以下源码使用的Cglib版本为3.2.12. 2. Enhancer源码分析 public Object create() ...

  5. 设计模式总结篇系列:代理模式(Proxy)

    时代在发展,我们发现,现在不少明星都开始进行微访谈之类的,有越来越多的参与捐赠等.新的一天开始了,首先看下新的一天的日程安排: interface Schedule{ public void weiT ...

  6. Java设计模式之《代理模式》及应用场景

    原创作品,可以转载,但是请标注出处地址:http://www.cnblogs.com/V1haoge/p/6525527.html 代理模式算是我接触较早的模式,代理就是中介,中间人.法律上也有代理, ...

  7. Java设计模式系列-抽象工厂模式

    原创文章,转载请标注出处:https://www.cnblogs.com/V1haoge/p/10755412.html 一.概述 抽象工厂模式是对工厂方法模式的再升级,但是二者面对的场景稍显差别. ...

  8. Java设计模式系列-工厂方法模式

    原创文章,转载请标注出处:<Java设计模式系列-工厂方法模式> 一.概述 工厂,就是生产产品的地方. 在Java设计模式中使用工厂的概念,那就是生成对象的地方了. 本来直接就能创建的对象 ...

  9. Java设计模式系列-装饰器模式

    原创文章,转载请标注出处:<Java设计模式系列-装饰器模式> 一.概述 装饰器模式作用是针对目标方法进行增强,提供新的功能或者额外的功能. 不同于适配器模式和桥接模式,装饰器模式涉及的是 ...

  10. java设计模式之Proxy(代理模式)

    java设计模式之Proxy(代理模式) 2008-03-25 20:30 227人阅读 评论(0) 收藏 举报 设计模式javaauthorizationpermissionsstringclass ...

随机推荐

  1. Spring的事务抽象

    Spring提供了一致的事务管理抽象,该抽象能实现为不同的事务API提供一致的编程模型.无视我们使用jdbc.hibernate.mybatis哪种方式来操作数据,无视事务是jta事务还是jdbc事务 ...

  2. Ubuntu环境下使用Jupyter Notebook查找桌面.csv文档的方法

    这个问题困扰了我很久,最后在一个老师发来的完成结果里找到了答案.(奇怪的是教材里没有.老师也不讲.尤其是百度也没有啊啊啊啊) 好了进入正题.教材里的原话是这样的 这行代码实现的环境应该是在window ...

  3. vue 项目运行报错

    'vue-cli-service' 不是内部或外部命令,也不是可运行的程序 或批处理文件. 运行Vue项目文件的时候报如下错误 需要先用淘宝镜像来运行:cnpm install 然后运行成功后 就可以 ...

  4. 常见排序算法原理及JS代码实现

    目录 数组 sort() 方法 冒泡排序 选择排序 插入排序 希尔排序 归并排序 堆排序 快速排序 创建时间:2020-08-07 本文只是将作者学习的过程以及算法理解进行简单的分享,提供多一个角度的 ...

  5. Go语言入门系列(五)之指针和结构体的使用

    Go语言入门系列前面的文章: Go语言入门系列(二)之基础语法总结 Go语言入门系列(三)之数组和切片 Go语言入门系列(四)之map的使用 1. 指针 如果你使用过C或C++,那你肯定对指针这个概念 ...

  6. C#LeetCode刷题之#104-二叉树的最大深度​​​​​​​(Maximum Depth of Binary Tree)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4072 访问. 给定一个二叉树,找出其最大深度. 二叉树的深度为根 ...

  7. C#LeetCode刷题之#121-买卖股票的最佳时机(Best Time to Buy and Sell Stock)

    问题 该文章的最新版本已迁移至个人博客[比特飞],单击链接 https://www.byteflying.com/archives/4014 访问. 给定一个数组,它的第 i 个元素是一支给定股票第  ...

  8. 打码(C语言)常见粗心小错误 (前方高能,一定要点)

    打码(C语言)常见粗心小错误 标签(空格分隔): 博客 自我介绍 本人学院 (http://sdcs.sysu.edu.cn/) 欢迎访问 本人学号 16340213 目录 打码C语言常见粗心小错误 ...

  9. HTML基础-03

    盒子模型 盒子模型(框模型 box model) - 浏览器在渲染页面时,它会将页面中的每一个元素都想象成是一个矩形的盒子. - 想象成盒子以后,对于页面的布局就变成了如何摆放盒子 - 每一个盒子从内 ...

  10. 教你如何使用ES6的Promise对象

    教你如何使用ES6的Promise对象 Promise对象,ES6新增的一个全新特性,这个是 ES6中非常重要的一个对象 Promise的设计初衷 首先,我们先一起了解一下,为什么要设计出这么一个玩意 ...