写在前面的话

相关背景及资源:

曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享

曹工说Spring Boot源码(2)-- Bean Definition到底是什么,咱们对着接口,逐个方法讲解

曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,我们来试一下

曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?

曹工说Spring Boot源码(5)-- 怎么从properties文件读取bean

曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的

曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)

曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中得到了什么(util命名空间)

曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(这次来说说它的奇技淫巧)

曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)

曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

曹工说Spring Boot源码(17)-- Spring从xml文件里到底得到了什么(aop:config完整解析【中】)

曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

曹工说Spring Boot源码(19)-- Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)

曹工说Spring Boot源码(20)-- 码网恢恢,疏而不漏,如何记录Spring RedisTemplate每次操作日志

曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了

曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的

工程代码地址 思维导图地址

工程结构图:

概要

上一篇,我们讲了spring是怎么获取class上的注解,以及注解的元注解的。在注解大行其道的今天,理解这些相对底层一点的知识,是绝对有必要的。另外,在上一讲中,我们提到了,spring其实最终也是利用ASM去读取注解的,其中,还使用了访问者设计模式。

访问者设计模式有效地分离了对数据的访问和和对数据的操作,因为class结构是很固定的,所以,visitor模式就尤其适合。在访问到特定数据时,就回调应用注册的回调方法。ASM基本上就是在visitor这个设计模式的基础上建立起来的。

今天,我们的主题有两个,1是简单地了解下ASM,2是投入实战,看看要怎么去利用ASM + java的Intrumentation机制,来在java启动时,就去修改class,实现简单的aop功能。

本篇覆盖第一个主题,下一个主题留带下一篇(demo已经ok了)。

ASM的核心之读取功能

我们目的是读取以下测试类上的注解和所有的方法的名称。

以下代码demo见:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/asm-demo/src/main/java/com/yn/onlyvisit

  1. 测试类

    package com.yn.onlyvisit;
    
    @CustomAnnotationOnClass
    public class Person {
    private String name; private int age; public String getName() {
    return name;
    } public void setName(String name) {
    this.name = name;
    } public int getAge() {
    return age;
    } public void setAge(int age) {
    this.age = age;
    }
    } @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CustomAnnotationOnClass {
    }
  2. 定义classVisitor,里面实现visitor的回调方法

    package com.yn.onlyvisit;
    
    import org.objectweb.asm.*;
    import org.objectweb.asm.commons.AdviceAdapter;
    import org.objectweb.asm.commons.AnalyzerAdapter;
    import org.objectweb.asm.util.ASMifier;
    import org.objectweb.asm.util.Textifier;
    import org.objectweb.asm.util.TraceMethodVisitor; import java.util.ArrayList;
    import java.util.List; public class MyClassVistor extends ClassVisitor {
    private List<String> methodList = new ArrayList<>();
    private List<String> annotationOnClass = new ArrayList<>();
    public MyClassVistor() {
    super(Opcodes.ASM6);
    } @Override
    public MethodVisitor visitMethod(int access, String name,
    String desc, String signature,
    String[] exceptions) {
    //每访问到一个方法,加入到field中
    System.out.println("visitMethod: " + name);
    methodList.add(name); return super.visitMethod(access, name, desc, signature, exceptions);
    } @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
    // 访问到类上注解,加入field
    annotationOnClass.add(descriptor);
    return super.visitAnnotation(descriptor, visible);
    } @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
    System.out.println("field:" + name);
    return super.visitField(access, name, descriptor, signature, value);
    } public List<String> getMethodList() {
    return methodList;
    } public List<String> getAnnotationOnClass() {
    return annotationOnClass;
    }
    }
  3. 测试代码

    import org.objectweb.asm.ClassReader;
    
    import java.io.IOException;
    import java.util.List; public class TestClassVisit {
    public static void main(String[] args) throws IOException {
    // 使用classreader读取目标类
    ClassReader classReader = new ClassReader("com.yn.onlyvisit.Person");
    // new一个visitor
    MyClassVistor classVisitor = new MyClassVistor();
    // 传入classreader
    classReader.accept(classVisitor,ClassReader.SKIP_DEBUG);
    // 此时,目标类已经读取完毕,我们可以打印看看效果
    List<String> methodList = classVisitor.getMethodList();
    System.out.println(methodList);
    System.out.println(classVisitor.getAnnotationOnClass());
    }
    }

    输出如下:

    field:name

    field:age

    visitMethod:

    visitMethod: getName

    visitMethod: setName

    visitMethod: getAge

    visitMethod: setAge

    [, getName, setName, getAge, setAge]

    [Lcom/yn/onlyvisit/CustomAnnotationOnClass;]

ASM的核心之生成全新class

案例讲解

注意,我们限定的是,生成全新的class,为什么限定这么死,因为还有一种是,在已经存在的类的基础上,修改class。

生成全新class的场景也是常见的,比如cglib底层就使用了asm,代理类是动态生成的,对吧?虽然我还没验证,但基本就是目前要讲的这种场景。

还有就是,fastjson里也用了asm,至于里面是否是生成全新class,留带验证。

asm的官方文档,有下面这样一个例子。

目标类如下,我们的目标,就是生成这样一个类的class:

package pkg;
public interface Comparable extends Mesurable {
int LESS = -1;
int EQUAL = 0;
int GREATER = 1;
int compareTo(Object o);
}

我们只需要如下几行代码,即可完成该目标。

package com.yn.classgenerate;

import org.objectweb.asm.ClassWriter;

import java.io.*;
import java.lang.reflect.Field; import static org.objectweb.asm.Opcodes.*; public class TestClassWriter {
public static void main(String[] args) throws IOException {
ClassWriter cw = new ClassWriter(0);
cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
null);
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "LESS", "I",
null, new Integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd();
cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd();
cw.visitEnd();
byte[] b = cw.toByteArray(); File file = new File("F:\\gitee-ckl\\all-simple-demo-in-work\\asm-demo\\src\\main\\java\\com\\yn\\classgenerate\\Target.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(b);
fos.close(); }
}

执行上述代码,在指定位置,就会生成一个Target.class,反编译之后,如下:

ClassWriter初识

上面那个demo,是否够神奇?为什么这么神奇呢,核心都在ClassWriter这个类。

这个类,大家可以理解为,一个class文件包含了很多东西,对吧?常量池、field集合、method集合、注解、class名、实现的接口集合等等,这个classWriter呢,其中就有很多field,分别来存储这些东西。

注意的是,上图中,有些字段,比如firstField,为什么不是集合呢?按理说,一个class里很多field啊,因为,这里用了链表结构来存储field。我们看这个field上的注释。

/**
* The fields of this class, stored in a linked list of {@link FieldWriter} linked via their
* {@link FieldWriter#fv} field. This field stores the first element of this list.
*/
private FieldWriter firstField;

看到了吧,链表结构。

所以,ClassWriter,大家一定要好好理解,这个ClassWriter,主要的使用方法就是:提供给你一堆方法,你可以调用他们,来给里面的field设置东西,比如,你要设置类名,那你就调用:

cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
null);

要加个field,那就这样:

cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();

ClassWriter为啥要实现ClassVisitor

如小标题所言,ClassWriter是实现了ClassVisitor的。

public class ClassWriter extends ClassVisitor

前面我们说的那些,手动去调用的方法,也是来源于ClassVisitor的。

cw.visit(V1_7, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
"pkg/Comparable", null, "java/lang/Object",
null);

该方法,来源于:

org.objectweb.asm.ClassVisitor#visit
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) {
if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
}

那么,接下来这段话,大家好好理解下:

前面的demo中,我们手动调用了ClassWriter的各种visit方法,去生成class;但是,我们又知道,ClassWriter的那些方法,来自于ClassVisitor,而:当我们向下面这样来编码的时候,ClassVisitor的方法会自动被调用(忘了的,往前翻到:ASM的核心之读取功能),那么,我们可以实现如下的class复制功能了:

package com.yn.classgenerate;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import static org.objectweb.asm.Opcodes.ASM4; public class CopyClassVersion1 {
public static void main(String[] args) throws IOException {
ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");
//1
ClassWriter cw = new ClassWriter(0);
//2
classReader.accept(cw, 0);
byte[] b2 = cw.toByteArray(); File file = new File("F:\\gitee-ckl\\all-simple-demo-in-work\\asm-demo\\src\\main\\java\\com\\yn\\classgenerate\\CopyClass2.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(b2);
fos.close();
}
}

这里的核心,就是要把classWriter,当成ClassVisitor,传递给ClassReader。

  1. 上述代码点1,此时,classWriter内部是空的,没法生成一个class
  2. 传递给classReader后,随着classReader不断去解析com.yn.classgenerate.CopyClass这个类,classWriter的各个visit方法,不断被回调,因此,com.yn.classgenerate.CopyClass的各类field、method等,不断被写入classWriter中,于是,复制就这样完成了。

ClassVisitor那些链式操作

前面那个复制class的操作中,classreader是直接回调classWriter的,我们其实也可以在中间横插一脚。

public class CopyClass {
public static void main(String[] args) throws IOException {
ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");
ClassWriter cw = new ClassWriter(0); // cv forwards all events to cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { };
classReader.accept(cv, 0);
byte[] b2 = cw.toByteArray(); File file = new File("F:\\gitee-ckl\\all-simple-demo-in-work\\asm-demo\\src\\main\\java\\com\\yn\\classgenerate\\CopyClass2.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(b2);
fos.close();
}
}

在上面这个例子中,我们从classReader的下面这句开始看:

 classReader.accept(cv, 0);

那么,可以知道,classReader是去回调cv,那么cv是谁?

ClassVisitor cv = new ClassVisitor(ASM4, cw) { };

cv的构造函数里,传入了cw,cw呢,就是classwriter。

现在的链路是这样的:

classReader --> cv --> cw。

上面这个链路中,classReader肯定会回调cv,但是cv,怎么就确定它会当个二传手呢?

看看ClassVisitor的构造函数:

public ClassVisitor(final int api, final ClassVisitor classVisitor) {
if (api < Opcodes.ASM4 || api > Opcodes.ASM6) {
throw new IllegalArgumentException();
}
this.api = api;
this.cv = classVisitor;
}

其把ClassVisitor保存到了一个域:cv中。这个cv如何被使用呢?我们看看下面的方法:

org.objectweb.asm.ClassVisitor#visit
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) { if (cv != null) {
cv.visit(version, access, name, signature, superName, interfaces);
}
} public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
if (cv != null) {
return cv.visitAnnotation(descriptor, visible);
}
return null;
}

这就有意思了,如果cv不为null,就调用cv去处理,这就是个delegate啊,代理啊。

中间商搞鬼那些事

上面的demo中,cv简直是尽忠职守,自己在中间,丝毫不做什么事,就是一个称职的代理。但不是所有代理都需要这样,甚至是不鼓励这样。

官网中有个demo,如下所示,可以修改class的版本:

public class ChangeVersionAdapter extends ClassVisitor {

    public ChangeVersionAdapter(ClassVisitor classVisitor) {
super(ASM4, classVisitor);
} @Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
cv.visit(V1_8, access, name, signature, superName, interfaces);
} }

测试类:


public class TestChangeClassVersion {
public static void main(String[] args) throws IOException {
ClassReader classReader = new ClassReader("com.yn.classgenerate.CopyClass");
ClassWriter cw = new ClassWriter(0); ClassVisitor cv = new ChangeVersionAdapter(cw) { };
classReader.accept(cv, 0);
byte[] b2 = cw.toByteArray(); File file = new File("F:\\gitee-ckl\\all-simple-demo-in-work\\asm-demo\\src\\main\\java\\com\\yn\\classgenerate\\CopyClass2.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(b2);
fos.close();
}
}

官网还画了个图,贴心:

通过这样,classWriter中,版本号已经被改了,但它还被蒙在鼓里,可怜。

如果要删除字段、删除方法,怎么整

在ClassVisitor中,有几个特殊的方法:

主要就是这几个,你看他们的返回值,不太一样,是xxxVistor,和ClassVisitor有点像?那就对了。

我们看看fieldVisitor:

其结构和方法,都和ClassVisitor类似,也就是说,我们可以返回一个自定义的FieldVistor,然后,ASM框架,就会使用我们返回的这个FieldVisitor去visit我们的field的相关属性,回调fieldVisitor中的相关方法。

那,怎么删除呢?返回null。

这么简单吗,是的。

package com.yn.classgenerate;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor; import static org.objectweb.asm.Opcodes.ASM6;
// 该demo来自官网文档
public class RemoveMethodAdapter extends ClassVisitor {
private String mName;
private String mDesc; public RemoveMethodAdapter(
ClassVisitor cv, String mName, String mDesc) {
super(ASM6, cv);
this.mName = mName;
this.mDesc = mDesc;
} @Override
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
if (name.equals(mName) && desc.equals(mDesc)) {
// 这样就可以了。
// do not delegate to next visitor -> this removes the method
return null;
}
return cv.visitMethod(access, name, desc, signature, exceptions);
}
}

总结

asm的基本操作大概如此,这些比较粗浅,下一讲我们会实现一个有用一点的东西,会结合java的instrument机制来讲。

大家要跟着我的demo一起来实践,https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/asm-demo

这样才能学的劳。

曹工说Spring Boot源码(24)-- Spring注解扫描的瑞士军刀,asm技术实战(上)的更多相关文章

  1. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  2. 精尽Spring Boot源码分析 - 文章导读

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  3. 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享

    写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...

  4. 曹工说Spring Boot源码(25)-- Spring注解扫描的瑞士军刀,ASM + Java Instrumentation,顺便提提Jar包破解

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎

    曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎 写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean De ...

  6. 曹工说Spring Boot源码(27)-- Spring的component-scan,光是include-filter属性的各种配置方式,就够玩半天了.md

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(28)-- Spring的component-scan机制,让你自己来进行简单实现,怎么办

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(30)-- ConfigurationClassPostProcessor 实在太硬核了,为了了解它,我可能debug了快一天

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. URL与URI与URN的区别与联系

    1.什么是URL? 统一资源定位符(或称统一资源定位器/定位地址.URL地址等[1],英语:Uniform Resource Locator,常缩写为URL),有时也被俗称为网页地址(网址).如同在网 ...

  2. MyBatis学习总结之一对一映射

    知识点:JavaType和ofType都是用来指定对象类型的,但是JavaType是用来指定pojo中属性的类型,而ofType指定的是映射到list集合属性中pojo的类型. 本次mybatis的练 ...

  3. 20. API概览 Schemas

    能被机器所理解的概要, 描述了通过api可得到的资源, URL, 表示方式以及支持的操作. API概要在很多使用场景下都是有用的工具, 例如生成参考文档, 或者驱动可以与API交互的动态客户端库. r ...

  4. 第二章 表与指针Pro SQL Server Internal (Dmitri Korotkev)

    聚集索引 聚集索引就是表中数据的物理顺序,它是按照聚集索引分类的.表只能定义一个聚集索引. 如果你要在一个有数据的堆表中创建一个聚集索引,如2-5所示,第一步要做的就是SQL服务器创建另一个根据聚集索 ...

  5. C# InputStream获取后乱码处理

    Post推送过来的数据流获取后部分中文出现乱码,晚上找了好多办法,不如朋友鼎力相助,哈哈哈~不说废话了上代码把 旧代码基本是网上普遍写法,字段不长用起来不会有乱码情况,但是传送字段一旦过长,超过byt ...

  6. session和el表达式

    2015/1/21 ## 回顾昨天案例 ## # 模拟购物车: >> 基本步骤: |-- 显示所有的书籍: |-- 制作书记列表/模仿数据库: |-- 参见昨天示例: |-- 制作查看详情 ...

  7. [面试专题]Web缓存详解

    Web缓存详解 标签(空格分隔): 缓存 缓存之于性能优化 请求更快:通过将内容缓存在本地浏览器或距离最近的缓存服务器(如CDN),在不影响网站交互的前提下可以大大加快网站加载速度. 降低服务器压力: ...

  8. 基于SpringCloud搭建项目-Zuul篇(六)

    本文主要介绍zuul的基本原理和在sprngcloud服务下如何使用 一.简单介绍 Zuul 是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器.提供路由.监控.弹性. ...

  9. codeigniter框架的使用感受和注意事项

    codeigniter是一个轻量级的php的web框架,今年2月22日,正式发布了4.0版本.简称CI框架 先使用了CI的3.15版,基本上是不用安装,把框架文件放到web目录下,让后通过简单的配置, ...

  10. cocos2d-x android 入门

    前一段时间使用传统方式做了一个CS软件,发现 UI 显示的比较慢,突发奇起,开始研究起来 GPU 加速,最后开始学习 cocos2dx. 开发环境以最新的 Cocos2d-x 3.17.1 Andro ...