品Spring:實現bean定義時採用的「先進生產力」

前景回顧

當咱們把寫好的業務代碼交給Spring以後,Spring都會作些什麼呢?java

仔細想象一下,再稍微抽象一下,Spring所作的幾乎所有都是:git

「bean的實例化,bean的依賴裝配,bean的初始化,bean的方法調用,bean的銷燬回收」。github

那問題來了,Spring爲何可以準確無誤的完成這波對bean的操做呢?答案很簡單,就是:spring

「Spring掌握了有關bean的足夠多的信息」。編程

這就是本系列文章第一篇「帝國的基石」的核心思想。Spring經過bean定義的概念收集到了bean的所有信息。設計模式

這件事也說明,當咱們擁有了一個事物的大量有效信息以後,就能夠作出一些很是有價值的操做。如大數據分析,用戶畫像等。數組

緊接着就是第二個問題,Spring應該採用什麼樣的方式來收集bean的信息呢?緩存

這就是本系列文章第二篇「bean定義上梁山」主要講的內容。框架

首先是統一了編程模型,只要是圍繞Spring的開發,包括框架自身的開發,最後大都轉化爲bean定義的註冊。ide

爲了知足不一樣的場景,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)

相關文章
相關標籤/搜索