前景回顧
當我們把寫好的業務代碼交給Spring之后,Spring都會做些什么呢?
仔細想象一下,再稍微抽象一下,Spring所做的幾乎全部都是:
“bean的實例化,bean的依賴裝配,bean的初始化,bean的方法調用,bean的銷毀回收”。
那問題來了,Spring為什么能夠准確無誤的完成這波對bean的操作呢?答案很簡單,就是:
“Spring掌握了有關bean的足夠多的信息”。
這就是本系列文章第一篇“帝國的基石”的核心思想。Spring通過bean定義的概念收集到了bean的全部信息。
這件事也說明,當我們擁有了一個事物的大量有效信息之后,就可以做出一些非常有價值的操作。如大數據分析,用戶畫像等。
緊接着就是第二個問題,Spring應該采用什么樣的方式來收集bean的信息呢?
這就是本系列文章第二篇“bean定義上梁山”主要講的內容。
首先是統一了編程模型,只要是圍繞Spring的開發,包括框架自身的開發,最后大都轉化為bean定義的注冊。
為了滿足不同的場景,Spring提供了兩大類的bean定義注冊方式:
實現指定接口,采用寫代碼的方式來注冊,這是非常靈活的動態注冊,根據不同的條件注冊不同的bean,主要用於第三方組件和Spring的整合。
標上指定注解,采用注解掃描的方式來注冊,這相當於一種靜態的注冊,非常不靈活,但特別簡單易用,主要用於普通業務代碼的開發。
Spring設計的這一切,看起來確實完美,用起來也確實很爽,但實現起來呢,也確實的非常麻煩。
尤其是在全部采用注解和Java配置的時候,那才叫一個繁瑣,看看源碼便知一二。
所以本篇及接下來的幾篇都會寫一些和實現細節相關的內容,俗稱“干貨”,哈哈。
最容易想到的實現方案
一個bean其實就是一個類,所以bean的信息就是類的信息。
那一個類都有哪些信息呢,閉着眼睛都能說出來,共四大類信息:
類型信息,類名,父類,實現的接口,訪問控制/修飾符
字段信息,字段名,字段類型,訪問控制/修飾符
方法信息,方法名,返回類型,參數類型,訪問控制/修飾符
注解信息,類上的注解,字段上的注解,方法上的注解/方法參數上的注解
注:還有內部類/外部類這些信息,也是非常重要的。
看到這里腦海中應該立馬蹦出兩個字,沒錯,就是反射。
但是,Spring並沒有采用反射來獲取這些信息,個人認為可能有以下兩個大的原因:
性能損耗問題:
要想使用反射,JVM必須先加載類,然后生成對應的Class<?>對象,最后緩存起來。
實際的工程可能會注冊較多的bean,但是真正運行時不一定都會用得到。
所以JVM加載過多的類,不僅會耗費較多的時間,還會占用較多的內存,而且加載的類很多可能都不用。
信息完整度問題:
JDK在1.8版本中新增加了一些和反射相關的API,比如和方法參數名稱相關的。此時才能使用反射獲取相對完善的信息。
但Spring很早就提供了對注解的支持,所以當時的反射並不完善,也可能是通過反射獲取到的信息並不能完全符合要求。
總之,Spring沒有選擇反射。
那如何獲取類的這些信息呢?答案應該只剩一種,就是直接從字節碼文件中獲取。
采用先進的生產力
源碼經過編譯變成字節碼,所以源碼中有的信息,在字節碼中肯定都有。只不過換了一種存在的形式。
Java源碼遵循Java語法規范,生成的字節碼遵循JVM中的字節碼規范。
字節碼文件的結構確實有些復雜,應用程序想要直接從字節碼中讀出需要的信息也確實有些困難。
小平同志曾說過,“科學技術是第一生產力”。所以要解決復雜的問題,必須要有比較可靠的技術才行。
對於復雜的字節碼來說,先進的生產力就是ASM了。ASM是一個小巧快速的Java字節碼操作框架。
它既可以讀字節碼文件,也可以寫字節碼文件。Spring框架主要用它來讀取字節碼。
ASM框架是采用訪問者模式設計出來的,如果不熟悉這個設計模式的可以閱讀本公眾號上一篇文章“趣說訪問者模式”。
該模式的核心思想就是,訪問者按照一定的規則順序進行訪問,期間會自動獲取到相關信息,把有用的信息保存下來即可。
下面介紹一下ASM的具體使用方式,可以看看作為了解,說不定以后會用到。哈哈。
ASM定義了ClassVisitor來獲取類型信息,AnnotationVisitor來獲取注解信息,FieldVisitor來獲取字段信息,MethodVisitor來獲取方法信息。
先准備好產生字節碼的素材,其實就是一個類啦,這個類僅作測試使用,不用考慮是否合理,如下:
@Configuration("ddd")
@ComponentScan(basePackages = {"a.b.c", "x.y.z"},
scopedProxy = ScopedProxyMode.DEFAULT,
includeFilters = {@Filter(classes = Integer.class)})
@Ann0(ann1 = @Ann1(name = "ann1Name"))
public class D<@Null T extends Number> extends C<@Valid Long, @NotNull Date> implements A, B {
protected Long lon = Long.MAX_VALUE;
private String str;
@Autowired(required = false)
private Date date;
@Resource(name = "aaa", lookup = "bbb")
private Map<@NotNull String, @Null Object> map;
@Bean(name = {"cc", "dd"}, initMethod = "init")
public String getStr(@NotNull String sssss, @Null int iiiii, double dddd, @Valid long llll) throws Exception {
return sssss;
}
@Override
public double getDouble(double d) {
return d;
}
}
這個類里面包含了較為全面的信息,泛型、父類、實現的接口、字段、方法、注解等。
按照ASM規定的訪問順序,首先訪問類型信息,使用ClassVisitor的visit方法,如下:
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
log("---ClassVisitor-visit---");
log("version", version);
log("access", access);
log("name", name);
log("signature", signature);
log("superName", superName);
log("interfaces", Arrays.toString(interfaces));
}
這個方法會由ASM框架調用,方法參數的值是框架傳進來的,我們要做的只是在方法內部把這些參數值保存下來就行了。
然后可以按照自己的需求去解析和使用,我這里只是簡單輸出一下。如下:
//版本信息,52表示的是JDK1.8
version = 52
//訪問控制信息,表示的是public class
access = 33
//類型的名稱
name = org/cnt/ts/asm/D
//類型的簽名,依次為,本類的泛型、父類、父類的泛型、實現的接口
signature = <T:Ljava/lang/Number;>Lorg/cnt/ts/asm/C<Ljava/lang/Long;Ljava/util/Date;>;Lorg/cnt/ts/asm/A;Lorg/cnt/ts/asm/B;
//父類型的名稱
superName = org/cnt/ts/asm/C
//實現的接口
interfaces = [org/cnt/ts/asm/A, org/cnt/ts/asm/B]
現在我們已經獲取到了這些信息,雖然我們並不知道它是如何在字節碼中存着的,這就是訪問者模式的好處。
類型名稱都是以斜線“/”分割,是因為斜線是路徑分隔符,可以非常方便的拼出完整路徑,從磁盤上讀取.class文件的內容。
還有以大寫“L”開頭后跟一個類型名稱的,這個大寫L表示的是“對象”的意思,后跟的就是對象的類型名稱,說白了就是類、接口、枚舉、注解等這些。
接着訪問的是類型上標的注解,使用ClassVisitor的visitAnnotation方法,如下:
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
log("---ClassVisitor-visitAnnotation---");
log("descriptor", descriptor);
log("visible", visible);
return new _AnnotationVisitor();
}
需要說明的是,這個方法只能訪問到注解的類型信息,注解的屬性信息需要使用AnnotationVisitor去訪問,也就是這個方法的返回類型。
類上標有@Configuration("ddd"),所以輸出結果如下:
//類型描述/名稱
descriptor = Lorg/springframework/context/annotation/Configuration;
//這個是可見性,表明在運行時可以獲取到注解的信息
visible = true
然后使用AnnotationVisitor去訪問顯式設置過的注解屬性信息,使用visit方法訪問基本的信息,如下:
@Override
public void visit(String name, Object value) {
log("---AnnotationVisitor-visit---");
log("name", name);
log("value", value);
}
實際上我們是把ddd設置給了注解的value屬性,所以結果如下:
//屬性名稱,是value
name = value
//屬性值,是ddd
value = ddd
至此,@Configuration注解已經訪問完畢。
然后再訪問@ComponentScan注解,同樣使用ClassVisitor的visitAnnotation方法,和上面的那個一樣。
得到的結果如下:
descriptor = Lorg/springframework/context/annotation/ComponentScan;
visible = true
然后使用AnnotationVisitor去訪問設置過的注解屬性信息,使用visitArray方法訪問數組類型的信息,如下:
@Override
public AnnotationVisitor visitArray(String name) {
log("---AnnotationVisitor-visitArray---");
log("name", name);
return new _AnnotationVisitor();
}
這個方法只能訪問到數組類型屬性的名稱,結果如下:
name = basePackages
屬性的值還是使用基本的visit方法去訪問,因為數組的值是多個,所以visit方法會多次調用,按順序依次獲取數組的每個元素值。
因數組有兩個值,所以方法調用兩次,結果如下:
name = null
value = a.b.c
name = null
value = x.y.z
因為數組的值沒有名稱,所以name總是null。value的值就是數組的元素值,按先后順序保存在一起即可。
然后由於注解的下一個屬性是枚舉類型的,所以使用visitEnum方法來訪問,如下:
@Override
public void visitEnum(String name, String descriptor, String value) {
log("---AnnotationVisitor-visitEnum---");
log("name", name);
log("descriptor", descriptor);
log("value", value);
}
結果如下:
//注解的屬性名稱,是scopedProxy
name = scopedProxy
//枚舉類型,是ScopedProxyMode
descriptor = Lorg/springframework/context/annotation/ScopedProxyMode;
//屬性的值,是我們設置的DEFAULT
value = DEFAULT
然后繼續訪問數組類型的屬性,使用visitArray方法訪問。
得到的結果如下:
name = includeFilters
接下來該獲取數組的元素了,由於這個數組元素的類型也是一個注解,所有使用visitAnnotation方法訪問,如下:
@Override
public AnnotationVisitor visitAnnotation(String name, String descriptor) {
log("---AnnotationVisitor-visitAnnotation---");
log("name", name);
log("descriptor", descriptor);
return new _AnnotationVisitor();
}
得到的結果如下:
name = null
//注解類型名稱
descriptor = Lorg/springframework/context/annotation/ComponentScan$Filter;
可以看到這個注解是@ComponentScan內部的@Filter注解。這個注解本身是作為數組元素的值,所以name是null,因為數組元素是沒有名稱的。
然后再訪問@Filter這個注解的屬性,得到屬性名稱如下:
name = classes
屬性值是一個數組,它只有一個元素,如下:
name = null
value = Ljava/lang/Integer;
注,代碼較多,不再貼了,只給出結果的解析。
下面是map類型的那個字段的結果,如下:
//訪問控制,private
access = 2
//字段名稱
name = map
//字段類型
descriptor = Ljava/util/Map;
//字段類型簽名,包括泛型信息
signature = Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;
value = null
該字段上標了注解,結果如下:
descriptor = Ljavax/annotation/Resource;
visible = true
並且設置了注解的兩個屬性,結果如下:
name = name
value = aaa
name = lookup
value = bbb
由於編譯器會生成默認的無參構造函數,所以會有如下:
//訪問控制,public
access = 1
//對應於構造函數名稱
name = <init>
//方法沒有參數,返回類型是void
descriptor = ()V
signature = null
exceptions = null
這有一個定義的方法結果,如下:
//public
access = 1
//方法名稱
name = getStr
//方法參數四個,分別是,String、int、double、long,返回類型是String
descriptor = (Ljava/lang/String;IDJ)Ljava/lang/String;
signature = null
//拋出Exception異常
exceptions = [java/lang/Exception]
參數里面的大寫字母I表示int,D表示double,J表示long,都是基本數據類,要記住不是包裝類型。
方法的四個參數名稱,依次分別是:
//參數名稱
name = sssss
//參數訪問修飾,0表示沒有修飾
access = 0
name = iiiii
access = 0
name = dddd
access = 0
name = llll
access = 0
由於方法上標有注解,結果如下:
descriptor = Lorg/springframework/context/annotation/Bean;
visible = true
數組類型的屬性名稱,如下:
name = name
屬性值有兩個,如下:
name = null
value = cc
name = null
value = dd
簡單類型的屬性值,如下:
name = initMethod
value = init
由於方法的其中三個參數上也標了注解,結果如下:
//參數位置,第0個參數
parameter = 0
//注解類型名稱,@NotNull
descriptor = Ljavax/validation/constraints/NotNull;
//可見性,運行時可見
visible = true
parameter = 1
descriptor = Ljavax/validation/constraints/Null;
visible = true
parameter = 3
descriptor = Ljavax/validation/Valid;
visible = true
以上這些只是部分的輸出結果。完整示例代碼參見文章末尾,可以自己運行一下仔細研究研究。
結尾總結
在業務開發中直接使用ASM的情況肯定較少,一般在框架開發或組件開發時可能會用到。
ASM的使用並不是特別難,多做測試即可發現規律。
我在測試時發現兩個值得注意的事情:
只能訪問到顯式設置注解屬性的那些值,對於注解的默認屬性值是訪問不到的。
要想獲取到注解的默認值,需要去訪問注解自己的字節碼文件,而不是使用注解的類的字節碼文件。
只能訪問到類型自己定義的信息,從父類型繼承的信息也是訪問不到的。
也就是說,字節碼中只包括在源碼文件中出現的信息,字節碼本身不處理繼承問題。
因此,JVM在加載一個類型時,要加載它的父類型,並處理繼承問題。
完整示例代碼:
https://github.com/coding-new-talking/taste-spring.git
(END)
作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號和知識星球的二維碼,歡迎關注!