寫在前面的話
相關背景及資源:
曹工說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
-
測試類
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 { }
-
定義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; } }
-
測試代碼
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,此時,classWriter內部是空的,沒法生成一個class
- 傳遞給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
這樣才能學的勞。