1. 程式人生 > 其它 >藍橋杯[省賽][B組]-乘積最大

藍橋杯[省賽][B組]-乘積最大

目錄

Spring框架技術

SSM(Spring+SpringMVC+Mybatis)階段的學習,也算是成功出了Java新手村。

前面我們已經學習過Mybatis了。

從這裡開始,很多的概念理解起來就稍微有一點難度了,因為你們沒有接觸過企業開發場景,很難體會到那種思想帶來的好處,甚至到後期接觸到的幾乎都是基於雲端計算和大資料理論實現的框架(當下最熱門最前沿的技術)逐漸不再是和計算機基礎相關聯,而是和怎麼高效幹活相關了。

在JavaWeb階段,我們已經學習瞭如何使用Java進行Web應用程式開發,我們現在已經具有搭建Web網站的能力,

但是,我們在開發的過程中,發現存在諸多的不便,我們發現雖然我們思路很清晰,知道如何編寫對應的介面,但是這樣的開發效率,實在是太慢了,並且對於物件建立的管理,存在諸多的不妥之處,因此,我們要去繼續學習更多的框架技術,來簡化和規範我們的Java開發。

Spring就是這樣的一個框架(文件:https://docs.spring.io/spring-framework/docs/5.2.13.RELEASE/spring-framework-reference/),它就是為了簡化開發而生。

它是輕量級的IoCAOP的容器框架,主要是針對JavaBean的生命週期進行管理的輕量級容器,並且它的生態已經發展得極為龐大。

那麼,首先一問,什麼是IoCAOP,什麼又是JavaBean呢?

只是聽起來滿滿的高階感,實際上沒有多高階(很多東西都是這樣,名字聽起來很牛,實際上只是一個很容易理解的東西)

因此,一切的一切,我們還要從JavaBean說起。

什麼是JavaBean

JavaBean就是有一定規範的Java實體類,跟普通類差不多,不同的是類內部提供了一些公共的方法以便外界對該物件內部屬性進行操作,比如set、get操作,實際上,就是我們之前一直在用的:

public class User{
	private String name;
	private int age;
	public String getName(){
		return name;
	}
	public String getAge(){
		return age;
	}
	public void setName(String name){
		this.name = name;
	}
	public void setAge(int age){
		this.age = age;
	}
}

它的所有屬性都是private,所有的屬性都可以通過get/set方法進行訪問,同時還需要有一個無參構造(預設就有)

因此我們之前編寫的很多類,其實都可以是一個JavaBean。

IoC理論基礎

整個程式其實是依靠各個部分相互協作,共同完成一個操作。

比如要展示借閱資訊列表,那麼首先需要使用Servlet進行請求和響應的資料處理,然後請求的資料全部交給對應的Service(業務層)來處理,當Service發現要從資料庫中獲取資料時,再向對應的Mapper發起請求。

它們之間就像連線在一起的齒輪,誰也離不開誰:

就像一個團隊,每個人的分工都很明確,流水線上的一套操作必須環環相扣,這是一種高度耦合的體系。

雖然這樣的體系邏輯非常清晰,整個流程也能夠讓人快速瞭解,但是這樣存在一個很嚴重的問題,我們現在的時代實際上是一個軟體專案高速迭代的時代,我們發現很多App三天兩頭隔三差五地就更新,而且是什麼功能當下最火,就馬不停蹄地進行跟進開發。

因此,就很容易出現,之前寫好的程式碼,實現的功能,需要全部推翻,改成新的功能,那麼我們就不得不去修改某些流水線上的模組,但是這樣一修改,會直接導致整個流水線的引用關係大面積更新。

就像我不想用這個Service實現類了,我想使用其他的實現類用不同的邏輯做這些功能,那麼這個時候,我們只能每個類都去挨個進行修改,當專案特別龐大時,光是改個類名就夠你改一天。

因此,高耦合度帶來的缺點是很明顯的,也是現代軟體開發中很致命的問題。如果要改善這種情況,我們只能將各個模組進行解耦,讓各個模組之間的依賴性不再那麼地強。

也就是說,Service的實現類,不再由我們決定,而是讓程式自己決定,所有的實現類物件,全部交給程式來管理,所有物件之間的關係,也由程式來動態決定,這樣就引入了IoC理論。

IOC是Inversion of Control的縮寫,翻譯為:“控制反轉”,把複雜系統分解成相互合作的物件,這些物件類通過封裝以後,內部實現對外部是透明的,從而降低了解決問題的複雜度,而且可以靈活地被重用和擴充套件。

我們可以將物件交給IoC容器進行管理。

比如當我們需要一個介面的實現時,由它根據配置檔案來決定到底給我們哪一個實現類,這樣,我們就可以不用再關心我們要去使用哪一個實現類了,我們只需要關心,給到我的一定是一個可以正常使用的實現類,能用就完事了,反正介面定義了啥,我只管調,這樣,我們就可以放心地讓一個人去寫檢視層的程式碼,一個人去寫業務層的程式碼,開發效率更高。

高內聚,低耦合,是現代軟體的開發的設計目標,而Spring框架就給我們提供了這樣的一個IoC容器進行物件的管理。

使用IoC容器

首先一定要明確,使用Spring首要目的是為了使得軟體專案進行解耦,而不是為了去簡化程式碼!

Spring並不是一個獨立的框架,它實際上包含了很多的模組:

而我們首先要去學習的就是Core Container,也就是核心容器模組。

Spring是一個非入侵式的框架,就像一個工具庫一樣,因此,我們只需要直接匯入其依賴就可以使用了。

第一個Spring專案

我們建立一個新的Maven專案,並匯入Spring框架的依賴,Spring框架的座標:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.13</version>
</dependency>

接著在resource中建立一個Spring配置檔案,命名為test.xml,直接右鍵點選即可建立:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

最後,在主方法中編寫:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("text");
    
}

這樣,一個最基本的Spring專案就建立完成了,接著我們來看看如何向IoC容器中註冊JavaBean,首先建立一個Student類:

//注意,這裡還用不到值注入,只需要包含成員屬性即可,不用Getter/Setter。
public class Student {
    String name;
    int age;
}

然後在配置檔案中新增這個bean:

<bean name="student" class="com.test.bean.Student"/>

現在,這個物件不需要我們再去生成了,而是由IoC容器來提供:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student);
}

實際上,這裡得到的Student物件是由Spring通過反射機制幫助我們建立的,初學者會非常疑惑,為什麼要這樣來建立物件,我們直接new一個它不香嗎?

為什麼要交給IoC容器管理呢?在後面的學習中,我們再慢慢進行體會。

將JavaBean交給IoC容器管理

通過前面的例子,我們發現只要將我們建立好的JavaBean通過配置檔案編寫,即可將其交給IoC容器進行管理,那麼,我們來看看,一個JavaBean的詳細配置:

<bean name="student" class="com.test.bean.Student"/>

其中name屬性(也可以是id屬性),全域性唯一,不可出現重複的名稱,我們發現,之前其實就是通過Bean的名稱來向IoC容器索要對應的物件,也可以通過其他方式獲取。

我們現在在主方法中連續獲取兩個物件:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
Student student2 = (Student) context.getBean("student");
System.out.println(student);
System.out.println(student2);

我們發現兩次獲取到的實際上是同一個物件,也就是說.

預設情況下,通過IoC容器進行管理的JavaBean是單例模式的,無論怎麼獲取始終為那一個物件,那麼如何進行修改呢?只需要修改其作用域即可,新增scope屬性:

<bean name="student" class="com.test.bean.Student" scope="prototype"/>

通過將其設定為prototype(原型模式)來使得其每次都會建立一個新的物件。

我們接著來觀察一下,這兩種模式下Bean的生命週期,我們給構造方法新增一個輸出:

public class Student {
    String name;
    int age;

    public Student(){
        System.out.println("我被構造了!");
    }
}

接著我們在mian方法中打上斷點來檢視物件分別是在什麼時候被構造的。

我們發現,當Bean的作用域為單例模式,那麼它會在一開始就被建立,而處於原型模式下,只有在獲取時才會被建立,也就是說。

單例模式下,Bean會被IoC容器儲存,只要容器沒有被銷燬,那麼此物件將一直存在,而原型模式才是相當於直接new了一個物件,並不會被儲存。

我們還可以通過配置檔案,告訴建立一個物件需要執行此初始化方法,以及銷燬一個物件的銷燬方法:

public class Student {
    String name;
    int age;

    private void init(){
        System.out.println("我是初始化方法!");
    }

    private void destroy(){
        System.out.println("我是銷燬方法!");
    }

    public Student(){
        System.out.println("我被構造了!");
    }
}
public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    System.out.println(student);
    context.close();  //手動銷燬容器
}

最後在XML檔案中編寫配置:

<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/>

接下來測試一下即可。

我們還可以手動指定Bean的載入順序,若某個Bean需要保證一定在另一個Bean載入之前載入,那麼就可以使用depend-on屬性。

依賴注入DI

現在我們已經瞭解瞭如何註冊和使用一個Bean,那麼,如何向Bean的成員屬性進行賦值呢?

也就是說,IoC在建立物件時,需要將我們預先給定的屬性注入到物件中,非常簡單,我們可以使用property標籤來實現,但是一定注意,此屬性必須存在一個set方法,否則無法賦值:

<bean name="student" class="com.test.bean.Student">
    <property name="name" value="小明"/>
</bean>
public class Student {
    String name;
    int age;

    public void setName(String name) {
        this.name = name;
    }

    public void say(){
        System.out.println("我是:"+name);
    }
}

最後測試是否能夠成功將屬性注入到我們的物件中:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = (Student) context.getBean("student");
    student.say();
}

那麼,如果成員屬性是一個非基本型別非String的物件型別,我們該怎麼注入呢?

public class Card {
}
public class Student {
    String name;
    int age;
    Card card;

    public void setCard(Card card) {
        this.card = card;
    }
  
  	public void setName(String name) {
        this.name = name;
    }

    public void say(){
        System.out.println("我是:"+name+",我都學生證:"+card);
    }
}

我們只需要將對應的型別也註冊為bean即可,然後直接使用ref屬性來進行引用:

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student">
    <property name="name" value="小明"/>
    <property name="card" ref="card"/>
</bean>

那麼,集合如何實現注入呢?我們需要在property內部進行編寫:

<bean name="student" class="com.test.bean.Student">
    <property name="list">
        <list>
            <value type="double">100.0</value>
            <value type="double">95.0</value>
            <value type="double">92.5</value>
        </list>
    </property>
</bean>

現在,我們就可以直接以一個數組的方式將屬性注入,注意如果是List型別的話,我們也可以使用array陣列。同樣的,如果是一個Map型別,我們也可以使用entry來注入:

public class Student {
    String name;
    int age;
    Map<String, Double> map;

    public void setMap(Map<String, Double> map) {
        this.map = map;
    }

    public void say(){
        System.out.println("我的成績:"+ map);
    }
}
<bean name="student" class="com.test.bean.Student">
    <property name="map">
        <map>
            <entry key="語文" value="100.0"/>
            <entry key="數學" value="80.0"/>
            <entry key="英語" value="92.5"/>
        </map>
    </property>
</bean>

我們還可以使用自動裝配來實現屬性值的注入:

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"/>

自動裝配會根據set方法中需要的型別,自動在容器中查詢是否存在對應型別或是對應名稱以及對應構造方法的Bean,比如我們上面指定的為byType,那麼其中的card屬性就會被自動注入型別為Card的Bean

我們已經瞭解瞭如何使用set方法來建立物件,那麼能否不使用預設的無參構造方法,而是指定一個有參構造進行物件的建立呢?

我們可以指定構造方法:

<bean name="student" class="com.test.bean.Student">
        <constructor-arg name="name" value="小明"/>
        <constructor-arg index="1" value="18"/>
    </bean>
public class Student {
    String name;
    int age;

    public Student(String name, int age){
        this.name = name;
        this.age = age;
    }

    public void say(){
        System.out.println("我是:"+name+"今年"+age+"歲了!");
    }
}

通過手動指定構造方法引數,我們就可以直接告訴容器使用哪一個構造方法來建立物件。


面向切面AOP

AOP思想實際上就是:在執行時,動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想就是面向切面的程式設計。

也就是說,我們可以使用AOP來幫助我們在方法執行前或執行之後,做一些額外的操作,實際上,就是代理!

通過AOP我們可以在保證原有業務不變的情況下,新增額外的動作,比如我們的某些方法執行完成之後,需要列印日誌,那麼這個時候,我們就可以使用AOP來幫助我們完成,它可以批量地為這些方法新增動作。

可以說,它相當於將我們原有的方法,在不改變原始碼的基礎上進行了增強處理。

相當於我們的整個業務流程,被直接斬斷,並在斷掉的位置添加了一個額外的操作,再連線起來,也就是在一個切點位置插入內容。

它的原理實際上就是通過動態代理機制實現的,我們在JavaWeb階段已經給大家講解過動態代理了。

不過Spring底層並不是使用的JDK提供的動態代理,而是使用的第三方庫實現,它能夠以父類的形式代理,而不是介面。

使用SpringAOP

Spring是支援AOP程式設計的框架之一(實際上它整合了AspectJ框架的一部分),要使用AOP我們需要先匯入一個依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.13</version>
</dependency>

那麼,如何使用AOP呢?

首先我們要明確,要實現AOP操作,我們需要知道這些內容:

  1. 需要切入的類,類的哪個方法需要被切入
  2. 切入之後需要執行什麼動作
  3. 是在方法執行前切入還是在方法執行後切入
  4. 如何告訴Spring需要進行切入

那麼我們依次來看,首先需要解決的問題是,找到需要切入的類:

public class Student {
    String name;
    int age;

	//分別在test方法執行前後切入
    public int test(String str) {
        System.out.println("我是一個測試方法:"+str);
        return str.length();
    }
}

現在我們希望在test方法執行前後新增我們的額外執行的內容,接著,我們來看看如何為方法執行前和執行後新增切入動作。

比如現在我們想在方法返回之後,再執行我們的動作,首先定義我們要執行的操作:

public class AopTest {

    //執行之後的方法
    public void after(){
        System.out.println("我是執行之後");
    }

    //執行之前的方法
    public void before(){
        System.out.println("我是執行之前");
    }
}

那麼,現在如何告訴Spring我們需要在方法執行之前和之後插入其他邏輯呢?

首先我們將要進行AOP操作的類註冊為Bean:

<bean name="student" class="com.test.bean.Student"/>
<bean name="aopTest" class="com.test.aop.AopTest"/>

一個是Student類,還有一個就是包含我們要切入方法的AopTest類,註冊為Bean後,他們就交給Spring進行管理,這樣Spring才能幫助我們完成AOP操作。

接著,我們需要告訴Spring,我們需要新增切入點,首先將頂部修改為,引入aop相關標籤:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

通過使用aop:config來新增一個新的AOP配置:

<aop:config>
    
</aop:config>

首先第一行,我們需要告訴Spring,我們要切入的是哪一個類的哪個或是哪些方法:

<aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/>

其中,expression屬性的execution填寫格式如下:

修飾符 包名.類名.方法名稱(方法引數)
  • 修飾符:public、protected、private、包括返回值型別、static等等(使用*代表任意修飾符)
  • 包名:如com.test(代表全部,比如com.代表com包下的全部包)
  • 類名:使用*也可以代表包下的所有類
  • 方法名稱:可以使用*代表全部方法
  • 方法引數:填寫對應的引數即可,比如(String, String),也可以使用*來代表任意一個引數,使用..代表所有引數。

也可以使用其他屬性來進行匹配,比如@annotation可以用於表示標記了哪些註解的方法被切入。

接著,我們需要為此方法新增一個執行前動作和一個執行後動作:

<aop:aspect ref="aopTest">
    <aop:before method="before" pointcut-ref="test"/>
    <aop:after-returning method="after" pointcut-ref="test"/>
</aop:aspect>

這樣,我們就完成了全部的配置,現在來實驗一下吧:

public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
    Student student = context.getBean(Student.class);
    student.test("mllumingfei");
}

我們發現,方法執行前後,分別呼叫了我們對應的方法。但是僅僅這樣還是不能滿足一些需求,在某些情況下,我們可以需求方法執行的一些引數,比如方法執行之後返回了什麼,或是方法開始之前傳入了什麼引數等等。

這個時候,我們可以為我們切入的方法新增一個引數,通過此引數就可以快速獲取切點位置的一些資訊:

//執行之前的方法
public void before(JoinPoint point){
    System.out.println("我是執行之前");
    System.out.println(point.getTarget());  //獲取執行方法的物件
    System.out.println(Arrays.toString(point.getArgs()));  //獲取傳入方法的實參
}

通過新增JoinPoint作為形參,Spring會自動給我們一個實現類物件,這樣我們就能獲取方法的一些資訊了。

最後我們再來看環繞方法,環繞方法相當於完全代理了此方法,它完全將此方法包含在中間,需要我們手動呼叫才可以執行此方法,並且我們可以直接獲取更多的引數:

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法開始之前");
    Object value = joinPoint.proceed();
    System.out.println("方法執行完成,結果為:"+value);
    return value;
}

注意,如果代理方法存在返回值,那麼環繞方法也需要有一個返回值,通過proceed方法來執行代理的方法,也可以修改引數之後呼叫proceed(Object[]),使用我們給定的引數再去執行:

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("方法開始之前");
    String arg = joinPoint.getArgs()[0] + "傘兵一號lbw";
    Object value = joinPoint.proceed(new Object[]{arg});
    System.out.println("方法執行完成,結果為:"+value);
    return value;
}

使用介面實現AOP

前面我們介紹瞭如何使用xml配置一個AOP操作,我們來看看如何使用Advice實現AOP。

它與我們之前學習的動態代理更接近一些,比如在方法開始執行之前或是執行之後會去呼叫我們實現的介面,

首先我們需要將一個類實現Advice介面,只有實現此介面,才可以被通知,比如我們這裡使用MethodBeforeAdvice表示是一個在方法執行之前的動作:

public class AopTest implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("通過Advice實現AOP");
    }
}

我們發現,方法中包括了很多的引數,其中args代表的是方法執行前得到的實參列表,還有target表示執行此方法的例項物件。

執行之後,效果和之前是一樣的,但是在這裡我們就可以快速獲取到更多資訊。

<aop:config>
    <aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/>
    <aop:advisor advice-ref="before" pointcut-ref="stu"/>
</aop:config>

除了此介面以外,還有其他的介面,比如AfterReturningAdvice就需要實現一個方法執行之後的操作:

public class AopTest implements MethodBeforeAdvice, AfterReturningAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("我是方法執行之前!");
    }

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("我是方法執行之後!");
    }
}

其實,我們之前學習的操作正好對應了AOP 領域中的特性術語

  • 通知(Advice): AOP 框架中的增強處理,通知描述了切面何時執行以及如何執行增強處理,也就是我們上面編寫的方法實現。
  • 連線點(join point): 連線點表示應用執行過程中能夠插入切面的一個點,這個點可以是方法的呼叫、異常的丟擲,實際上就是我們在方法執行前或是執行後需要做的內容。
  • 切點(PointCut): 可以插入增強處理的連線點,可以是方法執行之前也可以方法執行之後,還可以是丟擲異常之類的。
  • 切面(Aspect): 切面是通知和切點的結合,我們之前在xml中定義的就是切面,包括很多資訊。
  • 引入(Introduction):引入允許我們向現有的類新增新的方法或者屬性。
  • 織入(Weaving): 將增強處理新增到目標物件中,並建立一個被增強的物件,我們之前都是在將我們的增強處理新增到目標物件。

使用註解開發

前面我們已經瞭解了IoC容器和AOP實現,但是我們發現,要使用這些功能,我們就不得不編寫大量的配置,這是非常浪費時間和精力的,並且我們還只是演示了幾個小的例子,如果是像之前一樣去編寫一個完整的Web應用程式,那麼產生的配置可能會非常多。能否有一種更加高效的方法能夠省去配置呢?

當然還是註解了。

註解實現配置檔案

那麼,現在既然不使用XML檔案了,那通過註解的方式就只能以實體類的形式進行配置了,我們在要作為配置的類上新增@Configuration註解,我們這裡建立一個新的類MainConfiguration

@Configuration
public class MainConfiguration {
    //沒有配置任何Bean
}

你可以直接把它等價於:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
		<!-- 沒有配置任何Bean -->
</beans>

那麼我們來看看,如何配置Bean,之前我們是直接在配置檔案中編寫Bean的一些資訊,現在在配置類中,我們只需要編寫一個方法,並返回我們要建立的Bean的物件即可,並在其上方新增@Bean註解:

@Bean
public Card card(){
    return new Card();
}

這樣,等價於:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
		<bean class="com.test.bean.Card"></bean>
</beans>

我們還可以繼續新增@Scope註解來指定作用域,這裡我們就用原型模式:

@Bean
@Scope("prototype")
public Card card(){
    return new Card();
}

採用這種方式,我們就可以更加方便地控制一個Bean物件的建立過程,現在相當於這個物件時由我們建立好了再交給Spring進行後續處理,我們可以在物件建立時做很多額外的操作,包括一些屬性值的配置等。

既然現在我們已經建立好了配置類,接著我們就可以在主方法中載入此配置類,並建立一個基於配置類的容器:

public class Main {
    public static void main(String[] args) {
      	//使用AnnotationConfigApplicationContext來實現註解配置
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); //這裡需要告訴Spring哪個類作為配置類
        Card card = context.getBean(Card.class);  //容器用法和之前一樣
        System.out.println(card);
    }
}

在配置的過程中,我們可以點選IDEA底部的Spring標籤,開啟後可以對當前向容器中註冊的Bean進行集中檢視,並且會標註Bean之間的依賴關係,我們可以發現,Bean的預設名稱實際上就是首字母小寫的方法名稱,我們也可以手動指定:

@Bean("ml")
@Scope("prototype")
public Card card(){
    return new Card();
}

除了像原來一樣在配置檔案中建立Bean以外,我們還可以直接在類上新增@Component註解來將一個類進行註冊(現在最常用的方式)

不過要實現這樣的方式,我們需要新增一個自動掃描,來告訴Spring需要在哪些包中查詢我們提供@Component宣告的Bean。

只需要在配置類上新增一個@ComponentScan註解即可,如果要新增多個包進行掃描,可以使用@ComponentScans來批量新增。這裡我們演示將bean包下的所有類進行掃描:

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {

}

現在刪除類中的Bean定義,我們在Student類的上面新增@Component註解,來表示此型別需要作為Bean交給容器進行管理:

@Component
@Scope("prototype")
public class Student {
    String name;
    int age;
    Card card;
}

同樣的,在類上也可以直接新增@Scope來限定作用域。

效果和剛剛實際上是相同的,我們可以來測試一下:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);
        System.out.println(context.getBean(Student.class));
    }
}

我們可以看到IDEA的Spring板塊中也顯示了我們剛剛通過直接在類上新增@Component宣告的Bean。

@Component同樣效果的還有@Controller@Service@Repository,但是現在暫時不提,講到SpringMVC時再來探討。

現在我們就有兩種方式註冊一個Bean了,那麼如何實現像之前一樣的自動注入呢,比如我們將Card也註冊為Bean,我們希望Spring自動將其注入到Student的屬性中:

@Component
public class Student {
    String name;
    int sid;
    Card card;
}

因此,我們可以將此型別,也通過這種方式註冊為一個Bean:

@Component
@Scope("prototype")
public class Card {
}

現在,我們在需要注入的位置,新增一個@Resource註解來實現自動裝配:

@Component
public class Student {
    String name;
    int sid;
    
    @Resource
    Card card;
}

這樣的好處是,我們完全不需要建立任何的set方法,只需要新增這樣的一個註解就可以了,Spring會跟之前配置檔案的自動注入一樣,在整個容器中進行查詢,並將對應的Bean例項物件注入到此屬性中,當然,如果還是需要通過set方法來注入,可以將註解新增到方法上:

@Component
public class Student {
    String name;
    int sid;
    Card card;

    @Resource
    public void setCard(Card card) {
        System.out.println("通過方法");
        this.card = card;
    }
}

除了使用@Resource以外,我們還可以使用@Autowired(IDEA不推薦將其使用在欄位上,會出現黃標,但是可以放在方法或是構造方法上),它們的效果是一樣的,但是它們存在區別,雖然它們都是自動裝配:

  • @Resource預設ByName如果找不到則ByType,可以新增到set方法、欄位上。
  • @Autowired預設是byType,可以新增在構造方法、set方法、欄位、方法引數上。

並且@Autowired可以配合@Qualifier使用,來指定一個名稱的Bean進行注入:

@Autowired
@Qualifier("sxc")
public void setCard(Card card) {
    System.out.println("通過方法");
    this.card = card;
}

如果Bean是在配置檔案中進行定義的,我們還可以在方法的引數中使用@Autowired來進行自動注入:

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {

    @Bean
    public Student student(@Autowired Card card){
        Student student = new Student();
        student.setCard(card);
        return student;
    }
}

我們還可以通過@PostConstruct註解來新增構造後執行的方法,它等價於之前講解的init-method

@PostConstruct
public void init(){
    System.out.println("我是初始化方法!1");
}

注意它們的順序:Constructor(構造方法) -> @Autowired(依賴注入) -> @PostConstruct

同樣的,如果需要銷燬方法,也可以使用@PreDestroy註解,這裡就不做演示了。

這樣,兩種通過註解進行Bean宣告的方式就講解完畢了,那麼什麼時候該用什麼方式去宣告呢?

  • 如果要註冊為Bean的類是由其他框架提供,我們無法修改其原始碼,那麼我們就使用第一種方式進行配置。
  • 如果要註冊為Bean的類是我們自己編寫的,我們就可以直接在類上添加註解,並在配置中新增掃描。

註解實現AOP操作

瞭解瞭如何使用註解註冊Bean之後,我們接著來看如何通過註解實現AOP操作,首先我們需要在主類新增@EnableAspectJAutoProxy註解,開啟AOP註解支援:

@EnableAspectJAutoProxy
@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {
}

接著我們只需在定義AOP增強操作的類上新增@Aspect註解和@Component將其註冊為Bean即可,就像我們之前在配置檔案中也要將其註冊為Bean:

@Component
@Aspect
public class AopTest {

}

接著,我們直接在裡面編寫方法,並將此方法新增到一個切點中,比如我們希望在Student的test方法執行之前執行我們的方法:

public int test(String str){
    System.out.println("我被呼叫了:"+str);
    return str.length();
}

只需要新增@Before註解即可:

@Before("execution(* com.test.bean.Student.test(..))")
public void before(){
    System.out.println("我是之前執行的內容!");
}

同樣的,我們可以為其新增JoinPoint引數來獲取切入點資訊:

@Before("execution(* com.test.bean.Student.test(..))")
public void before(JoinPoint point){
    System.out.println("引數列表:"+ Arrays.toString(point.getArgs()));
    System.out.println("我是之前執行的內容!");
}

我們也可以使用@AfterReturning註解來指定方法返回後的操作:

@AfterReturning(value = "execution(* com.test.bean.Student.test(..))", returning = "returnVal")
public void after(Object returnVal){
    System.out.println("方法已返回,結果為:"+returnVal);
}

我們還可以指定returning屬性,並將其作為方法某個引數的實參。同樣的,環繞也可以直接通過註解宣告:

@Around("execution(* com.test.bean.Student.test(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
    System.out.println("方法執行之前!");
    Object val = point.proceed();
    System.out.println("方法執行之後!");
    return val;
}

其他註解配置

配置檔案可能不止一個,我們有可能會根據模組劃分,定義多個配置檔案,這個時候,可能會出現很多個配置類,如果我們需要@Import註解來快速將某個類加入到容器中,比如我們現在建立一個新的配置檔案,並將資料庫Bean也搬過去:

public class Test2Configuration {
    @Bean
    public Connection getConnection() throws SQLException {
        System.out.println("建立新的連線!");
        return DriverManager.getConnection("jdbc:mysql://localhost:3306/study",
                "root",
                "root");
    }
}
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import(Test2Configuration.class)
public class TestConfiguration {

    @Resource
    Connection connection;

    @PostConstruct
    public void init(){
        System.out.println(connection);
    }
}

注意另一個配置類並沒有新增任何註解,實際上,相當於匯入的類被強制註冊為了一個Bean,到現在,我們一共瞭解了三種註冊為Bean的方式,利用這種特性,我們還可以將其他的型別也強制註冊為Bean:

@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import({Test2Configuration.class, Date.class})
public class TestConfiguration {

    @Resource
    Connection connection;
    @Resource
    Date date;

    @PostConstruct
    public void init(){
        System.out.println(date+" -> "+connection);
    }
}

可以看到,日期直接作為一個Bean放入到IoC容器中了,並且時間永遠都是被new的那個時間,也就是同一個物件(因為預設是單例模式)。

通過@Import方式最主要為了實現的目標並不是建立Bean,而是為了方便一些框架的Registrar進行Bean定義,在講解到Spring原理時,我們再來詳細討論,目前只做瞭解即可。

到這裡,關於Spring框架的大致內容就聊得差不多了,其餘的內容,我們會在後面繼續講解。


深入Mybatis框架

學習了Spring之後,我們已經瞭解如何將一個類作為Bean交由IoC容器管理,也就是說,現在我們可以通過更方便的方式來使用Mybatis框架,我們可以直接把SqlSessionFactory、Mapper交給Spring進行管理,並且可以通過注入的方式快速地使用它們。

因此,我們要學習一下如何將Mybatis與Spring進行整合,那麼首先,我們需要在之前知識的基礎上繼續深化學習。

瞭解資料來源

在之前,我們如果需要建立一個JDBC的連線,那麼必須使用DriverManager.getConnection()來建立連線,連線建立後,我們才可以進行資料庫操作。

而學習了Mybatis之後,我們就不用再去使用DriverManager為我們提供連線物件,而是直接使用Mybatis為我們提供的SqlSessionFactory工具類來獲取對應的SqlSession通過會話物件去操作資料庫。

那麼,它到底是如何封裝JDBC的呢?我們可以試著來猜想一下,會不會是Mybatis每次都是幫助我們呼叫DriverManager來實現的資料庫連線建立?我們可以看看Mybatis的原始碼:

public SqlSession openSession(boolean autoCommit) {
    return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, autoCommit);
}

在通過SqlSessionFactory呼叫openSession方法之後,它呼叫了內部的一個私有的方法openSessionFromDataSource,我們接著來看,這個方法裡面定義了什麼內容:

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;

    DefaultSqlSession var8;
    try {
      	//獲取當前環境(由配置檔案對映的物件實體)
        Environment environment = this.configuration.getEnvironment();
      	//事務工廠(暫時不提,下一板塊講解)
        TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
      	//配置檔案中:<transactionManager type="JDBC"/>
      	//生成事務(根據我們的配置,會預設生成JdbcTransaction),這裡是關鍵,我們看到這裡用到了environment.getDataSource()方法
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      	//執行器,包括全部的資料庫操作方法定義,本質上是在使用執行器操作資料庫,需要傳入事務物件
        Executor executor = this.configuration.newExecutor(tx, execType);
      	//封裝為SqlSession物件
        var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
    } catch (Exception var12) {
        this.closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
    } finally {
        ErrorContext.instance().reset();
    }
		
    return var8;
}

也就是說,我們的資料來源配置資訊,存放在了Transaction物件中,那麼現在我們只需要知道執行器到底是如何執行SQL語句的,我們就知道到底如何建立Connection物件了,就需要獲取資料庫的連結資訊了,那麼我們來看看,這個DataSource到底是個什麼:

public interface DataSource  extends CommonDataSource, Wrapper {

  Connection getConnection() throws SQLException;

  Connection getConnection(String username, String password)
    throws SQLException;
}

我們發現,它是在javax.sql定義的一個介面,它包括了兩個方法,都是用於獲取連線的。因此,現在我們可以斷定,並不是通過之前DriverManager的方法去獲取連線了,而是使用DataSource的實現類來獲取的,因此,也就正式引入到我們這一節的話題了:

資料庫連結的建立和關閉是極其耗費系統資源的操作,通過DriverManager獲取的資料庫連線,

一個數據庫連線物件均對應一個物理資料庫連線,每次操作都開啟一個物理連線,使用完後立即關閉連線,頻繁的開啟、關閉連線會持續消耗網路資源,造成整個系統性能的低下。

因此,JDBC為我們定義了一個數據源的標準,也就是DataSource介面,告訴資料來源資料庫的連線資訊,並將所有的連線全部交給資料來源進行集中管理,當需要一個Connection物件時,可以向資料來源申請,資料來源會根據內部機制,合理地分配連線物件給我們。

一般比較常用的DataSource實現,都是採用池化技術,就是在一開始就建立好N個連線,這樣之後使用就無需再次進行連線,而是直接使用現成的Connection物件進行資料庫操作。

當然,也可以使用傳統的即用即連的方式獲取Connection物件,Mybatis為我們提供了幾個預設的資料來源實現,我們之前一直在使用的是官方的預設配置,也就是池化資料來源:

<dataSource type="POOLED">
      <property name="driver" value="${driver}"/>
      <property name="url" value="${url}"/>
      <property name="username" value="${username}"/>
      <property name="password" value="${password}"/>
    </dataSource>

一共三個選項:

  • UNPOOLED 不使用連線池的資料來源
  • POOLED 使用連線池的資料來源
  • JNDI 使用JNDI實現的資料來源

解讀Mybatis資料來源實現

那麼我們先來看看,不使用池化的資料來源實現,它叫做UnpooledDataSource,我們來看看原始碼:

public class UnpooledDataSource implements DataSource {
    private ClassLoader driverClassLoader;
    private Properties driverProperties;
    private static Map<String, Driver> registeredDrivers = new ConcurrentHashMap();
    private String driver;
    private String url;
    private String username;
    private String password;
    private Boolean autoCommit;
    private Integer defaultTransactionIsolationLevel;
    private Integer defaultNetworkTimeout;

首先這個類中定義了很多的成員,包括資料庫的連線資訊、資料庫驅動資訊、事務相關資訊等。

我們接著來看,它是如何實現DataSource中提供的介面的:

public Connection getConnection() throws SQLException {
    return this.doGetConnection(this.username, this.password);
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.doGetConnection(username, password);
}

實際上,這兩個方法都指向了內部的一個doGetConnection方法,那麼我們接著來看:

private Connection doGetConnection(String username, String password) throws SQLException {
    Properties props = new Properties();
    if (this.driverProperties != null) {
        props.putAll(this.driverProperties);
    }

    if (username != null) {
        props.setProperty("user", username);
    }

    if (password != null) {
        props.setProperty("password", password);
    }

    return this.doGetConnection(props);
}

首先它將資料庫的連線資訊也給新增到Properties物件中進行存放,並交給下一個doGetConnection來處理,套娃就完事了唄,接著來看下一層原始碼:

private Connection doGetConnection(Properties properties) throws SQLException {
  	//若未初始化驅動,需要先初始化,內部維護了一個Map來記錄初始化資訊,這裡不多介紹了
    this.initializeDriver();
  	//傳統的獲取連線的方式
    Connection connection = DriverManager.getConnection(this.url, properties);
  	//對連線進行額外的一些配置
    this.configureConnection(connection);
    return connection;
}

到這裡,就返回Connection物件了,而此物件正是通過DriverManager來建立的,因此,非池化的資料來源實現依然使用的是傳統的連線建立方式,那我們接著來看池化的資料來源實現,它是PooledDataSource類:

public class PooledDataSource implements DataSource {
    private static final Log log = LogFactory.getLog(PooledDataSource.class);
    private final PoolState state = new PoolState(this);
    private final UnpooledDataSource dataSource;
    protected int poolMaximumActiveConnections = 10;
    protected int poolMaximumIdleConnections = 5;
    protected int poolMaximumCheckoutTime = 20000;
    protected int poolTimeToWait = 20000;
    protected int poolMaximumLocalBadConnectionTolerance = 3;
    protected String poolPingQuery = "NO PING QUERY SET";
    protected boolean poolPingEnabled;
    protected int poolPingConnectionsNotUsedFor;
    private int expectedConnectionTypeCode;

我們發現,在這裡的定義就比非池化的實現複雜得多了,因為它還要考慮併發的問題,並且還要考慮如何合理地存放大量的連結物件,該如何進行合理分配,因此它的玩法非常之高階。

首先注意,它存放了一個UnpooledDataSource,此物件是在構造時就被建立,其實建立Connection還是依靠資料庫驅動建立,我們後面慢慢解析,首先我們來看看它是如何實現介面方法的:

public Connection getConnection() throws SQLException {
    return this.popConnection(this.dataSource.getUsername(), this.dataSource.getPassword()).getProxyConnection();
}

public Connection getConnection(String username, String password) throws SQLException {
    return this.popConnection(username, password).getProxyConnection();
}

可以看到,它呼叫了popConnection()方法來獲取連線物件,然後進行了一個代理,我們可以猜測,有可能整個連線池就是一個類似於棧的集合型別結構實現的。那麼我們接著來看看popConnection方法:

private PooledConnection popConnection(String username, String password) throws SQLException {
    boolean countedWait = false;
  	//返回的是PooledConnection物件,
    PooledConnection conn = null;
    long t = System.currentTimeMillis();
    int localBadConnectionCount = 0;

    while(conn == null) {
        synchronized(this.state) {   //加鎖,因為有可能很多個執行緒都需要獲取連線物件
            PoolState var10000;
          	//PoolState存了兩個List,一個是空閒列表,一個是活躍列表
            if (!this.state.idleConnections.isEmpty()) {   //有空閒連線時,可以直接分配Connection
                conn = (PooledConnection)this.state.idleConnections.remove(0);  //ArrayList中取第一個元素
                if (log.isDebugEnabled()) {
                    log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
                }
              //如果已經沒有多餘的連線可以分配,那麼就檢查一下活躍連線數是否達到最大的分配上限,如果沒有,就new一個
            } else if (this.state.activeConnections.size() < this.poolMaximumActiveConnections) {
              	//注意new了之後並沒有立即往List裡面塞,只是存了一些基本資訊
              	//我們發現,這裡依靠UnpooledDataSource建立了一個Connection物件,並將其封裝到PooledConnection中
                conn = new PooledConnection(this.dataSource.getConnection(), this);
                if (log.isDebugEnabled()) {
                    log.debug("Created connection " + conn.getRealHashCode() + ".");
                }
              //以上條件都不滿足,那麼只能從之前的連線中尋找了,看看有沒有那種卡住的連結(由於網路問題有可能之前的連線一直被卡住,然而正常情況下早就結束並且可以使用了,所以這裡相當於是優化也算是一種撿漏的方式)
            } else {
              	//獲取最早建立的連線
                PooledConnection oldestActiveConnection = (PooledConnection)this.state.activeConnections.get(0);
                long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
              	//判斷是否超過最大的使用時間
                if (longestCheckoutTime > (long)this.poolMaximumCheckoutTime) {
                  	//超時統計資訊(不重要)
                    ++this.state.claimedOverdueConnectionCount;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
                    var10000 = this.state;
                    var10000.accumulatedCheckoutTime += longestCheckoutTime;
                  	//從活躍列表中移除此連結資訊
                    this.state.activeConnections.remove(oldestActiveConnection);
                  	//如果開啟事務,還需要回滾一下
                    if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
                        try {
                            oldestActiveConnection.getRealConnection().rollback();
                        } catch (SQLException var15) {
                            log.debug("Bad connection. Could not roll back");
                        }
                    }
										
                  	//這裡就根據之前的連線物件直接new一個新的連線(注意使用的還是之前的Connection物件,只是被重新封裝了)
                    conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
                    conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
                    conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
                  	//過期
                    oldestActiveConnection.invalidate();
                    if (log.isDebugEnabled()) {
                        log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
                    }
                } else {
                  //確實是沒得用了,只能卡住了(阻塞)
                  //然後記錄一下有幾個執行緒在等待當前的任務搞完
                    try {
                        if (!countedWait) {
                            ++this.state.hadToWaitCount;
                            countedWait = true;
                        }

                        if (log.isDebugEnabled()) {
                            log.debug("Waiting as long as " + this.poolTimeToWait + " milliseconds for connection.");
                        }

                        long wt = System.currentTimeMillis();
                        this.state.wait((long)this.poolTimeToWait);   //要是超過等待時間還是沒等到,只能放棄
                      	//注意這樣的話con就為null了
                        var10000 = this.state;
                        var10000.accumulatedWaitTime += System.currentTimeMillis() - wt;
                    } catch (InterruptedException var16) {
                        break;
                    }
                }
            }
						
          	//經過之前的操作,已經成功分配到連線物件的情況下
            if (conn != null) {
                if (conn.isValid()) {  //是否有效
                    if (!conn.getRealConnection().getAutoCommit()) {  //清理之前遺留的事務操作
                        conn.getRealConnection().rollback();
                    }

                    conn.setConnectionTypeCode(this.assembleConnectionTypeCode(this.dataSource.getUrl(), username, password));
                    conn.setCheckoutTimestamp(System.currentTimeMillis());
                    conn.setLastUsedTimestamp(System.currentTimeMillis());
                  	//新增到活躍表中
                    this.state.activeConnections.add(conn);
                    //統計資訊(不重要)
                    ++this.state.requestCount;
                    var10000 = this.state;
                    var10000.accumulatedRequestTime += System.currentTimeMillis() - t;
                } else {
                  	//無效的連線,直接拋異常
                    if (log.isDebugEnabled()) {
                        log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
                    }

                    ++this.state.badConnectionCount;
                    ++localBadConnectionCount;
                    conn = null;
                    if (localBadConnectionCount > this.poolMaximumIdleConnections + this.poolMaximumLocalBadConnectionTolerance) {
                        if (log.isDebugEnabled()) {
                            log.debug("PooledDataSource: Could not get a good connection to the database.");
                        }

                        throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
                    }
                }
            }
        }
    }
	
  	//最後該幹嘛幹嘛,拿不到連線直接拋異常
    if (conn == null) {
        if (log.isDebugEnabled()) {
            log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
        }

        throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
    } else {
        return conn;
    }
}

經過上面一頓猛如虎的操作之後,我們可以得到以下資訊:

如果最後得到了連線物件(有可能是從空閒列表中得到,有可能是直接建立的新的,還有可能是經過回收策略回收得到的)。

那麼連線(Connection)物件一定會被放在活躍列表中(state.activeConnections)

那麼肯定有一個疑問,現在我們已經知道獲取一個連結會直接進入到活躍列表中,那麼,如果一個連線被關閉,又會發生什麼事情呢,我們來看看此方法返回之後,會呼叫getProxyConnection來獲取一個代理物件,實際上就是PooledConnection類:

class PooledConnection implements InvocationHandler {
  private static final String CLOSE = "close";
    private static final Class<?>[] IFACES = new Class[]{Connection.class};
    private final int hashCode;
  	//會記錄是來自哪一個資料來源建立的的
    private final PooledDataSource dataSource;
  	//連線物件本體
    private final Connection realConnection;
  	//代理的連結物件
    private final Connection proxyConnection;
  ...

它直接代理了構造方法中傳入的Connection物件,也是使用JDK的動態代理實現的,那麼我們來看一下,它是如何進行代理的:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String methodName = method.getName();
  	//如果呼叫的是Connection物件的close方法,
    if ("close".equals(methodName)) {
      	//這裡並不會真的關閉連線(這也是為什麼用代理),而是呼叫之前資料來源的pushConnection方法,將此連線改為為空閒狀態
        this.dataSource.pushConnection(this);
        return null;
    } else {
        try {
            if (!Object.class.equals(method.getDeclaringClass())) {
                this.checkConnection();
              	//任何操作執行之前都會檢查連線是否可用
            }

          	//該幹嘛幹嘛
            return method.invoke(this.realConnection, args);
        } catch (Throwable var6) {
            throw ExceptionUtil.unwrapThrowable(var6);
        }
    }
}

那麼我們最後再來看看pushConnection方法:

protected void pushConnection(PooledConnection conn) throws SQLException {
    synchronized(this.state) {   //老規矩,先來把鎖
      	//先從活躍列表移除此連線
        this.state.activeConnections.remove(conn);
      	//判斷此連結是否可用
        if (conn.isValid()) {
            PoolState var10000;
          	//看看閒置列表容量是否已滿(容量滿了就回不去了)
            if (this.state.idleConnections.size() < this.poolMaximumIdleConnections && conn.getConnectionTypeCode() == this.expectedConnectionTypeCode) {
                var10000 = this.state;
                var10000.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }

              	//把唯一有用的Connection物件拿出來,然後重新建立一個PooledConnection
                PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
              	//放入閒置列表,成功回收
                this.state.idleConnections.add(newConn);
                newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
                newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
                conn.invalidate();
                if (log.isDebugEnabled()) {
                    log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
                }

                this.state.notifyAll();
            } else {
                var10000 = this.state;
                var10000.accumulatedCheckoutTime += conn.getCheckoutTime();
                if (!conn.getRealConnection().getAutoCommit()) {
                    conn.getRealConnection().rollback();
                }

                conn.getRealConnection().close();
                if (log.isDebugEnabled()) {
                    log.debug("Closed connection " + conn.getRealHashCode() + ".");
                }

                conn.invalidate();
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
            }

            ++this.state.badConnectionCount;
        }

    }
}

這樣,我們就已經完全瞭解了Mybatis的池化資料來源的執行流程了。

只不過,無論Connection管理方式如何變換,無論資料來源再高階,我們要知道,它都最終都會使用DriverManager來建立連線物件,而最終使用的也是DriverManager提供的Connection物件。

整合Mybatis框架

通過了解資料來源,我們已經清楚,Mybatis實際上是在使用自己編寫的資料來源(資料來源有很多,之後我們再聊其他的)預設使用的是池化的資料來源,它預先儲存了很多的連線物件。

那麼我們來看一下,如何將Mybatis與Spring更好的結合呢,比如我們現在希望將SqlSessionFactory交給IoC容器進行管理,而不是我們自己建立工具類來管理(我們之前一直都在使用工具類管理和建立會話)

首先匯入依賴:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.7</version>
</dependency>
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>2.0.6</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.3.13</version>
</dependency>

在mybatis-spring依賴中,為我們提供了SqlSessionTemplate類,它其實就是官方封裝的一個工具類,我們可以將其註冊為Bean,這樣我們隨時都可以向IoC容器索要,而不用自己再去編寫一個工具類了,我們可以直接在配置類中建立:

@Configuration
@ComponentScan("com.test")
public class TestConfiguration {
    @Bean
    public SqlSessionTemplate sqlSessionTemplate() throws IOException {
        SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
        return new SqlSessionTemplate(factory);
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/study"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
  	<mappers>
        <mapper class="com.test.mapper.TestMapper"/>
    </mappers>
</configuration>
public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
    SqlSessionTemplate template = context.getBean(SqlSessionTemplate.class);
    TestMapper testMapper = template.getMapper(TestMapper.class);
    System.out.println(testMapper.getStudent());
}
@Mapper
public interface TestMapper {

    @Select("select * from student where sid = 1")
    Student getStudent();
}
@Data
public class Student {
    int sid;
    String name;
    String sex;
}

最後成功得到Student實體類,證明SqlSessionTemplate成功註冊為Bean可以使用了。

雖然這樣已經很方便了,但是還不夠方便,我們依然需要手動去獲取Mapper物件,那麼能否直接得到對應的Mapper物件呢,我們希望讓Spring直接幫助我們管理所有的Mapper,當需要時,可以直接從容器中獲取,我們可以直接在配置類上方添加註解:

@MapperScan("com.test.mapper")

這樣,Spring會自動掃描所有的Mapper,並將其實現註冊為Bean,那麼我們現在就可以直接通過容器獲取了:

public static void main(String[] args) throws InterruptedException {
    ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
    TestMapper mapper = context.getBean(TestMapper.class);
    System.out.println(mapper.getStudent());
}

請一定注意,必須存在SqlSessionTemplate或是SqlSessionFactoryBean的Bean,否則會無法初始化(畢竟要資料庫的連結資訊)

我們接著來看,如果我們希望直接去除Mybatis的配置檔案,那麼改怎麼去實現呢?

我們可以使用SqlSessionFactoryBean類:

@Configuration
@ComponentScan("com.test")
@MapperScan("com.test.mapper")
public class TestConfiguration {
    @Bean
    public DataSource dataSource(){
        return new PooledDataSource("com.mysql.cj.jdbc.Driver",
                "jdbc:mysql://localhost:3306/study", "root", "123456");
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean(@Autowired DataSource dataSource){
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        return bean;
    }
}

首先我們需要建立一個數據源的實現類,因為這是資料庫最基本的資訊,然後再給到SqlSessionFactoryBean例項,這樣,我們相當於直接在一開始通過IoC容器配置了SqlSessionFactory,只需要傳入一個DataSource的實現即可。

刪除配置檔案,重新再來執行,同樣可以正常使用Mapper。

從這裡開始,通過IoC容器,Mybatis已經不再需要使用配置檔案了,之後基於Spring的開發將不會再出現Mybatis的配置檔案。

使用HikariCP連線池

前面我們提到了資料來源還有其他實現,比如C3P0、Druid等,它們都是非常優秀的資料來源實現(可以自行了解),不過我們這裡要介紹的,是之後在SpringBoot中還會遇到的HikariCP連線池。

HikariCP是由日本程式設計師開源的一個數據庫連線池元件,程式碼非常輕量,並且速度非常的快。

根據官方提供的資料,在酷睿i7開啟32個執行緒32個連線的情況下,進行隨機資料庫讀寫操作,HikariCP的速度是現在常用的C3P0資料庫連線池的數百倍。在SpringBoot2.0中,官方也是推薦使用HikariCP。

首先,我們需要匯入依賴:

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.4.5</version>
</dependency>

接著修改一下Bean的定義:

@Bean
public DataSource dataSource() throws SQLException {
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/study");
    dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    return dataSource;
}

最後我們發現,同樣可以得到輸出結果,但是出現了一個報錯:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

此資料來源實際上是採用了SLF4J日誌框架列印日誌資訊,但是現在沒有任何的日誌實現(slf4j只是一個API標準,它規範了多種日誌框架的操作,統一使用SLF4J定義的方法來操作不同的日誌框架)我們這裡就使用JUL作為日誌實現,我們需要匯入另一個依賴:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.25</version>
</dependency>

注意版本一定要和slf4j-api保持一致!

這樣就能得到我們的日誌資訊了:

十二月 07, 2021 8:46:41 下午 com.zaxxer.hikari.HikariDataSource getConnection
資訊: HikariPool-1 - Starting...
十二月 07, 2021 8:46:41 下午 com.zaxxer.hikari.HikariDataSource getConnection
資訊: HikariPool-1 - Start completed.
Student(sid=1, name=小明, sex=男)

在SpringBoot階段,我們還會遇到HikariPool-1 - Starting...HikariPool-1 - Start completed.同款日誌資訊。

當然,Lombok肯定也是支援這個日誌框架快速註解的:

@Slf4j
public class Main {
    public static void main(String[] args) {
        log.info("專案正在啟動...");
        ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
        TestMapper mapper = context.getBean(TestMapper.class);
        System.out.println(mapper.getStudent());
    }
}

Mybatis事務管理

我們前面已經講解了如何讓Mybatis與Spring更好地融合在一起,通過將對應的Bean型別註冊到容器中,就能更加方便的去使用Mapper,那麼現在,我們接著來看Spring的事務控制。

在開始之前,我們還是回顧一下事務機制。首先事務遵循一個ACID原則:

  • 原子性(Atomicity):事務是一個原子操作,由一系列動作組成。事務的原子性確保動作要麼全部完成,要麼完全不起作用。
  • 一致性(Consistency):一旦事務完成(不管成功還是失敗),系統必須確保它所建模的業務處於一致的狀態,而不會是部分完成部分失敗。在現實中的資料不應該被破壞。
  • 隔離性(Isolation):可能有許多事務會同時處理相同的資料,因此每個事務都應該與其他事務隔離開來,防止資料損壞。
  • 永續性(Durability):一旦事務完成,無論發生什麼系統錯誤,它的結果都不應該受到影響,這樣就能從任何系統崩潰中恢復過來。通常情況下,事務的結果被寫到持久化儲存器中。

簡單來說,事務就是要麼完成,要麼就啥都別做!並且不同的事務直接相互隔離,互不干擾。

那麼我們接著來深入瞭解一下事務的隔離機制(在之前資料庫入門階段並沒有提到)我們說了,事務之間是相互隔離互不干擾的,那麼如果出現了下面的情況,會怎麼樣呢:

當兩個事務同時在執行,並且同時在操作同一個資料,這樣很容易出現併發相關的問題,比如一個事務先讀取了某條資料,而另一個事務此時修改了此資料,當前一個事務緊接著再次讀取時,會導致和前一次讀取的資料不一致,這就是一種典型的資料虛讀現象。

因此,為了解決這些問題,事務之間實際上是存在一些隔離級別的:

  • ISOLATION_READ_UNCOMMITTED(讀未提交):其他事務會讀取當前事務尚未更改的提交(相當於讀取的是這個事務暫時快取的內容,並不是資料庫中的內容)
  • ISOLATION_READ_COMMITTED(讀已提交):其他事務會讀取當前事務已經提交的資料(也就是直接讀取資料庫中已經發生更改的內容)
  • ISOLATION_REPEATABLE_READ(可重複讀):其他事務會讀取當前事務已經提交的資料並且其他事務執行過程中不允許再進行資料修改(注意這裡僅僅是不允許修改資料)
  • ISOLATION_SERIALIZABLE(序列化):它完全服從ACID原則,一個事務必須等待其他事務結束之後才能開始執行,相當於挨個執行,效率很低

我們依次來看看,不同的隔離級別會導致什麼問題。首先是讀未提交級別,此級別屬於最低級別,相當於各個事務共享一個快取區域,任何事務的操作都在這裡進行。那麼它會導致以下問題:

也就是說,事務A最後得到的實際上是一個毫無意義的資料(事務B已經回滾了)我們稱此資料為"髒資料",這種現象稱為髒讀

我們接著來看讀已提交級別,事務只能讀取其他事務已經提交的內容,相當於直接從資料中讀取資料,這樣就可以避免髒讀問題了,但是它還是存在以下問題:

這正是我們前面例子中提到的問題,雖然它避免了髒讀問題,但是如果事件B修改並提交了資料,那麼實際上事務A之前讀取到的資料依然不是最新的資料,直接導致兩次讀取的資料不一致,這種現象稱為虛讀也可以稱為不可重複讀

因此,下一個隔離級別可重複讀就能夠解決這樣的問題(MySQL的預設隔離級別),它規定在其他事務執行時,不允許修改資料,這樣,就可以有效地避免不可重複讀的問題,但是這樣就一定安全了嗎?

這裡僅僅是禁止了事務執行過程中的UPDATE操作,但是它並沒有禁止INSERT這類操作,因此,如果事務A執行過程中事務B插入了新的資料,那麼A這時是毫不知情的,比如:

兩個人同時報名一個活動,兩個報名的事務同時在進行,但是他們一開始讀取到的人數都是5,而這時,它們都會認為報名成功後人數應該變成6,而正常情況下應該是7,因此這個時候就發生了資料的幻讀現象。

因此,要解決這種問題,只能使用最後一種隔離級別序列化來實現了,每個事務不能同時進行,直接避免所有併發問題,簡單粗暴,但是效率爆減,並不推薦。

最後總結三種情況:

  • 髒讀:讀取到了被回滾的資料,它毫無意義。
  • 虛讀(不可重複讀):由於其他事務更新資料,兩次讀取的資料不一致。
  • 幻讀:由於其他事務執行插入刪除操作,而又無法感知到表中記錄條數發生變化,當下次再讀取時會莫名其妙多出或缺失資料,就像產生幻覺一樣。

(對於虛讀和幻讀的區分:虛讀是某個資料前後讀取不一致,幻讀是整個表的記錄數量前後讀取不一致)

最後這張圖,請務必記在你的腦海,記在你的心中,記在你的全世界:

Mybatis對於資料庫的事務管理,也有著相應的封裝。

一個事務無非就是建立、提交、回滾、關閉,因此這些操作被Mybatis抽象為一個介面:

public interface Transaction {
    Connection getConnection() throws SQLException;

    void commit() throws SQLException;

    void rollback() throws SQLException;

    void close() throws SQLException;

    Integer getTimeout() throws SQLException;
}

對於此介面的實現,MyBatis的事務管理分為兩種形式:

  1. 使用JDBC的事務管理機制:即利用對應資料庫的驅動生成的Connection物件完成對事務的提交(commit())、回滾(rollback())、關閉(close())等,對應的實現類為JdbcTransaction
  2. 使用MANAGED的事務管理機制:這種機制MyBatis自身不會去實現事務管理,而是讓程式的容器(比如Spring)來實現對事務的管理,對應的實現類為ManagedTransaction

而我們之前一直使用的其實就是JDBC的事務,相當於直接使用Connection物件(之前JavaWeb階段已經講解過了)在進行事務操作,並沒有額外的管理機制,對應的配置為:

<transactionManager type="JDBC"/>

那麼我們來看看JdbcTransaction是不是像我們上面所說的那樣管理事務的,直接上原始碼:

public class JdbcTransaction implements Transaction {
    private static final Log log = LogFactory.getLog(JdbcTransaction.class);
    protected Connection connection;
    protected DataSource dataSource;
    protected TransactionIsolationLevel level;
    protected boolean autoCommit;

    public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
      	//資料來源
        this.dataSource = ds;
      	//事務隔離級別,上面已經提到過了
        this.level = desiredLevel;
      	//是否自動提交
        this.autoCommit = desiredAutoCommit;
    }
  	
  //也可以直接給個Connection物件
   public JdbcTransaction(Connection connection) {
        this.connection = connection;
    }

    public Connection getConnection() throws SQLException {
      	//沒有就通過資料來源新開一個Connection
        if (this.connection == null) {
            this.openConnection();
        }
	
        return this.connection;
    }

    public void commit() throws SQLException {
      	//連線已經建立並且沒開啟自動提交才可以使用
        if (this.connection != null && !this.connection.getAutoCommit()) {
            if (log.isDebugEnabled()) {
                log.debug("Committing JDBC Connection [" + this.connection + "]");
            }
						//實際上使用的是資料庫驅動提供的Connection物件進行事務操作
            this.connection.commit();
        }

    }

相當於JdbcTransaction只是為資料庫驅動提供的Connection物件套了層殼,所有的事務操作實際上是直接呼叫Connection物件。

那麼我們接著來看ManagedTransaction的原始碼:

public class ManagedTransaction implements Transaction {
    ...

    public void commit() throws SQLException {
    }

    public void rollback() throws SQLException {
    }

    ...
}

我們發現,大體內容和JdbcTransaction差不多,但是它並沒有實現任何的事務操作。也就是說,它希望將實現交給其他的管理框架來完成,而Spring就為Mybatis提供了一個非常好的事務管理實現。

使用Spring事務管理

現在我們來學習一下Spring提供的事務管理(Spring事務管理分為程式設計式事務和宣告式事務,但是程式設計式事務過於複雜並且具有高度耦合性,違背了Spring框架的設計初衷,因此這裡只講解宣告式事務)宣告式事務是基於AOP實現的。

使用宣告式事務非常簡單,我們只需要在配置類新增@EnableTransactionManagement註解即可,這樣就可以開啟Spring的事務支援了。

接著,我們只需要把一個事務要做的所有事情封裝到Service層的一個方法中即可,首先需要在配置檔案中註冊一個新的Bean,事務需要執行必須有一個事務管理器:

@Bean
public TransactionManager transactionManager(@Autowired DataSource dataSource){
    return new DataSourceTransactionManager(dataSource);
}

接著編寫Mapper操作:

@Mapper
public interface TestMapper {

    @Insert("insert into student(name, sex) values('測試', '男')")
    void insertStudent();
}

這樣會向資料庫中插入一條新的學生資訊,接著,假設我們這裡有一個業務需要連續插入兩條學生資訊,首先編寫業務層的介面:

public interface TestService {

    void test();
}

接著,我們再來編寫業務層的實現,我們可以直接將其註冊為Bean,交給Spring來進行管理,這樣就可以自動將Mapper注入到類中了,並且可以支援事務:

@Component
public class TestServiceImpl implements TestService{

    @Resource
    TestMapper mapper;

    @Transactional
    @Override
    public void test() {
        mapper.insertStudent();
        if(true) throw new RuntimeException("我是測試異常!");
        mapper.insertStudent();
    }
}

我們只需在方法上新增@Transactional註解,即可表示此方法執行的是一個事務操作,在呼叫此方法時,Spring會通過AOP機制為其進行增強,一旦發現異常,事務會自動回滾。最後我們來呼叫一下此方法:

@Slf4j
public class Main {
    public static void main(String[] args) {
        log.info("專案正在啟動...");
        ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
        TestService service = context.getBean(TestService.class);
        service.test();
    }
}

得到的結果是出現錯誤:

十二月 08, 2021 3:09:29 下午 com.test.Main main
資訊: 專案正在啟動...
十二月 08, 2021 3:09:29 下午 com.zaxxer.hikari.HikariDataSource getConnection
資訊: HikariPool-1 - Starting...
十二月 08, 2021 3:09:29 下午 com.zaxxer.hikari.HikariDataSource getConnection
資訊: HikariPool-1 - Start completed.
Exception in thread "main" java.lang.RuntimeException: 我是測試異常!
	at com.test.service.TestServiceImpl.test(TestServiceImpl.java:22)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
	at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy30.test(Unknown Source)
	at com.test.Main.main(Main.java:17)

我們發現,整個棧追蹤資訊中包含了大量aop包下的相關內容,也就印證了,它確實是通過AOP實現的,那麼我們接著來看一下,資料庫中的資料是否沒有發生變化(出現異常回滾了)

結果顯而易見,確實被回滾了,資料庫中沒有任何的內容。

我們接著來研究一下@Transactional註解的一些引數:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    String[] label() default {};

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    String timeoutString() default "";

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

我們來講解幾個比較關鍵的資訊:

  • transactionManager:指定事務管理器
  • propagation:事務傳播規則,一個事務可以包括N個子事務
  • isolation:事務隔離級別,不多說了
  • timeout:事務超時時間
  • readOnly:是否為只讀事務,不同的資料庫會根據只讀屬性進行優化,比如MySQL一旦宣告事務為只讀,那麼久不允許增刪改操作了。
  • rollbackFor和noRollbackFor:發生指定異常時回滾或是不回滾,預設發生任何異常都回滾

除了事務的傳播規則,其他的內容其實已經給大家講解過了,那麼我們就來看看事務的傳播。事務傳播一共有七種級別:

Spring預設的傳播級別是PROPAGATION_REQUIRED,那麼我們來看看,它是如何傳播的,現在我們的Service類中一共存在兩個事務,而一個事務方法包含了另一個事務方法:

@Transactional
public void test() {
    test2();
    if(true) throw new RuntimeException("我是測試異常!");  //發生異常時,會回滾另一個事務嗎?
}

@Transactional
public void test2() {
    mapper.insertStudent();
}

最後我們得到結果,另一個事務被回滾了,也就是說,相當於另一個事務直接加入到此事務中了,也就是表中所描述的那樣。

如果單獨執行test2()則會開啟一個新的事務,而執行test()則會直接讓內部的test2()加入到當前事務中。

@Transactional
public void test() {
    test2();
}

@Transactional(propagation = Propagation.SUPPORTS)
public void test2() {
    mapper.insertStudent();
   	if(true) throw new RuntimeException("我是測試異常!");
}

現在我們將test2()的傳播級別設定為SUPPORTS,那麼這時如果單獨呼叫test2()方法,並不會以事務的方式執行,當發生異常時,雖然依然存在AOP增強,但是不會進行回滾操作,而現在再呼叫test()方法,才會以事務的方式執行。

我們接著來看MANDATORY,它非常嚴格,如果當前方法並沒有在任何事務中進行,會直接出現異常:

@Transactional
public void test() {
    test2();
}

@Transactional(propagation = Propagation.MANDATORY)
public void test2() {
    mapper.insertStudent();
    if(true) throw new RuntimeException("我是測試異常!");
}

直接執行test2()方法,報錯如下:

Exception in thread "main" org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
	at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:595)
	at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:382)
	at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at com.sun.proxy.$Proxy29.test2(Unknown Source)
	at com.test.Main.main(Main.java:17)

NESTED級別表示如果存在外層事務,則此方法單獨建立一個子事務,回滾只會影響到此子事務,實際上就是利用建立Savepoint,然後回滾到此儲存點實現的。

NEVER級別表示此方法不應該加入到任何事務中,其餘型別適用於同時操作多資料來源情況下的分散式事務管理,這裡暫時不做介紹。

至此,有關Spring的核心內容就講解完畢了。


整合JUnit測試

既然使用了Spring,那麼怎麼整合到JUnit中進行測試呢,首先大家能夠想到的肯定是:

public class TestMain {

    @Test
    public void test(){
        ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
        TestService service = context.getBean(TestService.class);
        service.test();
    }
}

直接編寫一個測試用例即可,但是這樣的話,如果我們有很多個測試用例,那麼我們不可能每次測試都去建立ApplicationContext吧?我們可以使用@Before新增一個測試前動作來提前配置ApplicationContext,但是這樣的話,還是不夠簡便,能不能有更快速高效的方法呢?

Spring為我們提供了一個Test模組,它會自動整合Junit進行測試,我們可以匯入一下依賴:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.12</version>
</dependency>

這裡匯入的是JUnit5和SpringTest模組依賴,然後直接在我們的測試類上新增兩個註解就可以搞定:

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestConfiguration.class)
public class TestMain {

    @Autowired
    TestService service;
    
    @Test
    public void test(){
        service.test();
    }
}

@ExtendWith是由JUnit提供的註解,等同於舊版本的@RunWith註解,然後使用SpringTest模組提供的@ContextConfiguration註解來表示要載入哪一個配置檔案,可以是XML檔案也可以是類,我們這裡就直接使用類進行載入。

配置完成後,我們可以直接使用@Autowired來進行依賴注入,並且直接在測試方法中使用注入的Bean,現在就非常方便了。


探究Spring原理

探究IoC原理

首先我們大致瞭解一下ApplicationContext的載入流程:

我們可以看到,整個過程極為複雜,一句話肯定是無法解釋的,所以我們就從ApplicationContext說起吧。

由於Spring的原始碼極為複雜,因此我們不可能再像瞭解其他框架那樣直接自底向上逐行幹原始碼了,我們可以先從一些最基本的介面定義開始講起,自頂向下逐步瓦解,那麼我們來看看ApplicationContext最頂層介面是什麼,一直往上,我們會發現有一個AbstractApplicationContext類,我們直接右鍵生成一個UML類圖。

我們發現最頂層實際上是一個BeanFactory介面,那麼我們就從這個介面開始研究起。

我們可以看到此介面中定義了很多的行為:

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";

    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    <T> ObjectProvider<T> getBeanProvider(Class<T> var1);

    <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);

    boolean containsBean(String var1);

    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;

    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;

    String[] getAliases(String var1);
}

我們發現,其中最眼熟的就是getBean()方法了,此方法被過載了很多次,可以接受多種引數。

因此,我們可以斷定,一個IoC容器最基本的行為在此介面中已經被定義好了。

也就是說,所有的BeanFactory實現類都應該具備容器管理Bean的基本能力,就像它的名字一樣,它就是一個Bean工廠,工廠就是用來生產Bean例項物件的。

我們可以直接找到此介面的一個抽象實現AbstractBeanFactory類,它實現了getBean()方法:

public Object getBean(String name) throws BeansException {
    return this.doGetBean(name, (Class)null, (Object[])null, false);
}

那麼我們doGetBean()接著來看方法裡面幹了什麼:

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    String beanName = this.transformedBeanName(name);
    Object sharedInstance = this.getSingleton(beanName);
    Object beanInstance;
    if (sharedInstance != null && args == null) {
      ...

因為所有的Bean預設都是單例模式,物件只會存在一個,因此它會先呼叫父類的getSingleton()方法來直接獲取單例物件,如果有的話,就可以直接拿到Bean的例項。

如果沒有會進入else程式碼塊,我們接著來看,首先會進行一個判斷:

if (this.isPrototypeCurrentlyInCreation(beanName)) {
    throw new BeanCurrentlyInCreationException(beanName);
}

這是為了解決迴圈依賴進行的處理,比如A和B都是以原型模式進行建立,而A中需要注入B,B中需要注入A,這時就會出現A還未建立完成,就需要B,而B這時也沒建立完成,因為B需要A,而A等著B,這樣就只能無限迴圈下去了,所以就出現了迴圈依賴的問題(同理,一個物件,多個物件也會出現這種情況)但是在單例模式下,由於每個Bean只會建立一個例項,Spring完全有機會處理好迴圈依賴的問題,只需要一個正確的賦值操作實現迴圈即可。

那麼單例模式下是如何解決迴圈依賴問題的呢?

我們回到getSingleton()方法中,單例模式是可以自動解決迴圈依賴問題的:

@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    Object singletonObject = this.singletonObjects.get(beanName);
  	//先從第一層列表中拿Bean例項,拿到直接返回
    if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
      	//第一層拿不到,並且已經認定為處於迴圈狀態,看看第二層有沒有
        singletonObject = this.earlySingletonObjects.get(beanName);
        if (singletonObject == null && allowEarlyReference) {
            synchronized(this.singletonObjects) {
              	//加鎖再執行一次上述流程
                singletonObject = this.singletonObjects.get(beanName);
                if (singletonObject == null) {
                    singletonObject = this.earlySingletonObjects.get(beanName);
                    if (singletonObject == null) {
                      	//仍然沒有獲取到例項,只能從singletonFactory中獲取了
                        ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                        if (singletonFactory != null) {
                            singletonObject = singletonFactory.getObject();
                          	//丟進earlySingletonObjects中,下次就可以直接在第二層拿到了
                            this.earlySingletonObjects.put(beanName, singletonObject);
                            this.singletonFactories.remove(beanName);
                        }
                    }
                }
            }
        }
    }

    return singletonObject;
}

看起來很複雜,實際上它使用了三層列表的方式來處理迴圈依賴的問題。包括:

  • singletonObjects
  • earlySingletonObjects
  • singletonFactories

當第一層拿不到時,會接著判斷這個Bean是否處於建立狀態isSingletonCurrentlyInCreation(),它會從一個Set集合中查詢,這個集合中儲存了已經建立但還未注入屬性的例項物件,也就是說處於正在建立狀態,如果說發現此Bean處於正在建立狀態(一定是因為某個Bean需要注入這個Bean的例項),就可以斷定它應該是出現了迴圈依賴的情況。

earlySingletonObjects相當於是專門處理迴圈依賴的表,一般包含singletonObjects中的全部例項,如果這個裡面還是沒有,接著往下走,這時會從singletonFactories中獲取(所有的Bean初始化完成之後都會被丟進singletonFactories,也就是隻建立了,但是還沒進行依賴注入的時候)在獲取到後,向earlySingletonObjects中丟入此Bean的例項,並將例項從singletonFactories中移除。

我們最後再來梳理一下流程,還是用我們剛才的例子,A依賴於B,B依賴於A:

  1. 假如A先載入,那麼A首先進入了singletonFactories中,注意這時還沒進行依賴注入,A中的B還是null
    • singletonFactories:A
    • earlySingletonObjects:
    • singletonObjects:
  2. 接著肯定是注入A的依賴B了,但是B還沒初始化,因此現在先把B給載入了,B構造完成後也進了singletonFactories
    • singletonFactories:A,B
    • earlySingletonObjects:
    • singletonObjects:
  3. 開始為B注入依賴,發現B依賴於A,這時又得去獲取A的例項,根據上面的分析,這時候A還在singletonFactories中,那麼它會被丟進earlySingletonObjects,然後從singletonFactories中移除,然後返回A的例項(注意此時A中的B依賴還是null)
    • singletonFactories:B
    • earlySingletonObjects:A
    • singletonObjects:
  4. 這時B已經完成依賴注入了,因此可以直接丟進singletonObjects中
    • singletonFactories:
    • earlySingletonObjects:A
    • singletonObjects:B
  5. 然後再將B注入到A中,完成A的依賴注入,A也被丟進singletonObjects中,至此迴圈依賴完成,A和B完成例項建立
    • singletonFactories:
    • earlySingletonObjects:
    • singletonObjects:A,B

經過整體過程梳理,關於Spring如何解決單例模式的迴圈依賴理解起來就非常簡單了。

現在讓我們回到之前的地方,原型模式下如果出現迴圈依賴會直接丟擲異常,如果不存在會接著向下:

//BeanFactory存在父子關係
BeanFactory parentBeanFactory = this.getParentBeanFactory();
//如果存在父BeanFactory,同時當前BeanFactory沒有這個Bean的定義
if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
  	//這裡是因為Bean可能有別名,找最原始的那個名稱
    String nameToLookup = this.originalBeanName(name);
    if (parentBeanFactory instanceof AbstractBeanFactory) {
      	//向父BeanFactory遞迴查詢
        return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly);
    }

    if (args != null) {
      	//根據引數查詢
        return parentBeanFactory.getBean(nameToLookup, args);
    }

    if (requiredType != null) {
      	//根據型別查詢
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    }

  	//各種找
    return parentBeanFactory.getBean(nameToLookup);
}

也就是說,BeanFactory會先看當前是否存在Bean的定義,如果沒有會直接用父BeanFactory各種找。

這裡出現了一個新的介面BeanDefinition,既然工廠需要生產商品,那麼肯定需要拿到商品的原材料以及製作配方,我們的Bean也是這樣,Bean工廠需要拿到Bean的資訊才可以去生成這個Bean的例項物件,而BeanDefinition就是用於存放Bean的資訊的,所有的Bean資訊正是從XML配置檔案讀取或是註解掃描後得到的。

我們接著來看,如果此BeanFactory不存在父BeanFactory或是包含了Bean的定義,那麼會接著往下走,這時只能自己建立Bean了,首先會拿到一個RootBeanDefinition物件:

try {
    if (requiredType != null) {
        beanCreation.tag("beanType", requiredType::toString);
    }

    RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);

下面的內容就非常複雜了,但是我們可以知道,它一定是根據對應的型別(單例、原型)進行了對應的處理,最後自行建立一個新的物件返回。一個Bean的載入流程為:

首先拿到BeanDefinition定義,選擇對應的構造方法,通過反射進行例項化,然後進行屬性填充(依賴注入),完成之後再呼叫初始化方法(init-method),最後如果存在AOP,則會生成一個代理物件,最後返回的才是我們真正得到的Bean物件。

最後讓我們回到ApplicationContext中,實際上,它就是一個強化版的BeanFactory,在最基本的Bean管理基礎上,還添加了:

  • 國際化(MessageSource)
  • 訪問資源,如URL和檔案(ResourceLoader)
  • 載入多個(有繼承關係)上下文
  • 訊息傳送、響應機制(ApplicationEventPublisher)
  • AOP機制

我們發現,無論是還是的構造方法中都會呼叫refresh()方法來重新整理應用程式上下文:

public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
    this();
    this.register(componentClasses);
    this.refresh();
}

此方法在講解完AOP原理之後,再進行講解。綜上,有關IoC容器的大部分原理就講解完畢了。

探究AOP原理

前面我們提到了PostProcessor,它其實是Spring提供的一種後置處理機制,它可以讓我們能夠插手Bean、BeanFactory、BeanDefinition的建立過程,相當於進行一個最終的處理,而最後得到的結果(比如Bean例項、Bean定義等)就是經過後置處理器返回的結果,它是整個載入過程的最後一步。

而AOP機制正是通過它來實現的,我們首先來認識一下第一個介面BeanPostProcessor,它相當於Bean初始化的一個後置動作,我們可以直接實現此介面:

//注意它後置處理器也要進行註冊
@Component
public class TestBeanProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println(beanName);  //列印bean的名稱
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }
}

我們發現,此介面中包括兩個方法,一個是postProcessAfterInitialization用於在Bean初始化之後進行處理,還有一個postProcessBeforeInitialization用於在Bean初始化之前進行處理,注意這裡的初始化不是建立物件,而是呼叫類的初始化方法,比如:

@Component
public class TestBeanProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("我是之後:"+beanName);
        return bean;   //這裡返回的Bean相當於最終的結果了,我們依然能夠插手修改,這裡返回之後是什麼就是什麼了
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("我是之前:"+beanName);
        return bean;   //這裡返回的Bean會交給下一個階段,也就是初始化方法
    }
}
@Component
public class TestServiceImpl implements TestService{

    public TestServiceImpl(){
        System.out.println("我是構造方法");
    }

    @PostConstruct
    public void init(){
        System.out.println("我是初始化方法");
    }

    TestMapper mapper;

    @Autowired
    public void setMapper(TestMapper mapper) {
        System.out.println("我是依賴注入");
        this.mapper = mapper;
    }
  	
  	...

而TestServiceImpl的載入順序為:

我是構造方法
我是依賴注入
我是之前:testServiceImpl
我是初始化方法
我是之後:testServiceImpl

現在我們再來總結一下一個Bean的載入流程:

[Bean定義]首先掃描Bean,載入Bean定義 -> [依賴注入]根據Bean定義通過反射建立Bean例項 -> [依賴注入]進行依賴注入(順便解決迴圈依賴問題)-> [初始化Bean]BeanPostProcessor的初始化之前方法 -> [初始化Bean]Bean初始化方法 -> [初始化Bean]BeanPostProcessor的初始化之前後方法 -> [完成]最終得到的Bean載入完成的例項

利用這種機制,理解Aop的實現過程就非常簡單了,AOP實際上也是通過這種機制實現的,它的實現類是AnnotationAwareAspectJAutoProxyCreator,而它就是在最後對Bean進行了代理,因此最後我們得到的結果實際上就是一個動態代理的物件(有關詳細實現過程,這裡就不進行列舉了,感興趣的可以繼續深入)

那麼肯定有人有疑問了,這個類沒有被註冊啊,那按理說它不應該參與到Bean的初始化流程中的,為什麼它直接就被載入了呢?

還記得@EnableAspectJAutoProxy嗎?我們來看看它是如何定義就知道了:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
    boolean proxyTargetClass() default false;

    boolean exposeProxy() default false;
}

我們發現它使用了@Import來註冊AspectJAutoProxyRegistrar,那麼這個類又是什麼呢,我們接著來看:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    AspectJAutoProxyRegistrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      	//註冊AnnotationAwareAspectJAutoProxyCreator到容器中
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }

            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }

    }
}

它實現了介面,這個介面也是Spring提供的一種Bean載入機制,它支援直接向容器中新增Bean定義,容器也會載入這個Bean:

  • ImportBeanDefinitionRegistrar類只能通過其他類@Import的方式來載入,通常是啟動類或配置類。
  • 使用@Import,如果括號中的類是ImportBeanDefinitionRegistrar的實現類,則會呼叫介面中方法(一般用於註冊Bean)
  • 實現該介面的類擁有註冊bean的能力。

我們可以看到此介面提供了一個BeanDefinitionRegistry正是用於註冊Bean的定義的。

因此,當我們打上了@EnableAspectJAutoProxy註解之後,首先會通過@Import載入AspectJAutoProxyRegistrar,然後呼叫其registerBeanDefinitions方法,然後使用工具類註冊AnnotationAwareAspectJAutoProxyCreator到容器中,這樣在每個Bean建立之後,如果需要使用AOP,那麼就會通過AOP的後置處理器進行處理,最後返回一個代理物件。

我們也可以嘗試編寫一個自己的ImportBeanDefinitionRegistrar實現,首先編寫一個測試Bean:

public class TestBean {
    
    @PostConstruct
    void init(){
        System.out.println("我被初始化了!");
    }
}
public class TestBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinition definition = BeanDefinitionBuilder.rootBeanDefinition(Student.class).getBeanDefinition();
        registry.registerBeanDefinition("lbwnb", definition);
    }
}

觀察控制檯輸出,成功載入Bean例項。

BeanPostProcessor差不多的還有BeanFactoryPostProcessor,它和前者一樣,也是用於我們自己處理後置動作的,不過這裡是用於處理BeanFactory載入的後置動作,BeanDefinitionRegistryPostProcessor直接繼承自BeanFactoryPostProcessor,並且還添加了新的動作postProcessBeanDefinitionRegistry,你可以在這裡動態新增Bean定義或是修改已經存在的Bean定義,這裡我們就直接演示BeanDefinitionRegistryPostProcessor的實現:

@Component
public class TestDefinitionProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        System.out.println("我是Bean定義後置處理!");
        BeanDefinition definition = BeanDefinitionBuilder.rootBeanDefinition(TestBean.class).getBeanDefinition();
        registry.registerBeanDefinition("lbwnb", definition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        System.out.println("我是Bean工廠後置處理!");
    }
}

在這裡註冊Bean定義其實和之前那種方法效果一樣。

最後,我們再完善一下Bean載入流程(加粗部分是新增的):

[Bean定義]首先掃描Bean,載入Bean定義 -> [Bean定義]Bean定義和Bean工廠後置處理 -> [依賴注入]根據Bean定義通過反射建立Bean例項 -> [依賴注入]進行依賴注入(順便解決迴圈依賴問題)-> [初始化Bean]BeanPostProcessor的初始化之前方法 -> [初始化Bean]Bean初始化方法 -> [初始化Bean]BeanPostProcessor的初始化之前後方法 -> [完成]最終得到的Bean載入完成的例項

最後我們再來研究一下ApplicationContext中的refresh()方法:

public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
        this.prepareRefresh();
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
      	//初始化Bean工廠
        this.prepareBeanFactory(beanFactory);

        try {
            this.postProcessBeanFactory(beanFactory);
            StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
          	//呼叫所有的Bean工廠、Bean註冊後置處理器
            this.invokeBeanFactoryPostProcessors(beanFactory);
          	//註冊Bean後置處理器(包括Spring內部的)
            this.registerBeanPostProcessors(beanFactory);
            beanPostProcess.end();
          	//國際化支援
            this.initMessageSource();
          	//監聽和事件廣播
            this.initApplicationEventMulticaster();
            this.onRefresh();
            this.registerListeners();
          	//例項化所有的Bean
            this.finishBeanFactoryInitialization(beanFactory);
            this.finishRefresh();
        } catch (BeansException var10) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
            }

            this.destroyBeans();
            this.cancelRefresh(var10);
            throw var10;
        } finally {
            this.resetCommonCaches();
            contextRefresh.end();
        }

    }
}

我們可以給這些部分分別打上斷點來觀察一下此方法的整體載入流程。

Mybatis整合原理

通過之前的瞭解,我們再來看Mybatis的@MapperScan是如何實現的,現在理解起來就非常簡單了。

我們可以直接開啟檢視:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MapperScannerRegistrar.class})
@Repeatable(MapperScans.class)
public @interface MapperScan {
    String[] value() default {};

    String[] basePackages() default {};
  	...

我們發現,和Aop一樣,它也是通過Registrar機制,通過@Import來進行Bean的註冊,我們來看看MapperScannerRegistrar是個什麼東西,關鍵程式碼如下:

void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
    builder.addPropertyValue("processPropertyPlaceHolders", true);
    Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
    if (!Annotation.class.equals(annotationClass)) {
        builder.addPropertyValue("annotationClass", annotationClass);
    }

    Class<?> markerInterface = annoAttrs.getClass("markerInterface");
    if (!Class.class.equals(markerInterface)) {
        builder.addPropertyValue("markerInterface", markerInterface);
    }

    Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
    if (!BeanNameGenerator.class.equals(generatorClass)) {
        builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
    }

    Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
    if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) {
        builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
    }

    String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
    if (StringUtils.hasText(sqlSessionTemplateRef)) {
        builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
    }

    String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
    if (StringUtils.hasText(sqlSessionFactoryRef)) {
        builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
    }

    List<String> basePackages = new ArrayList();
    basePackages.addAll((Collection)Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));
    basePackages.addAll((Collection)Arrays.stream(annoAttrs.getStringArray("basePackages")).filter(StringUtils::hasText).collect(Collectors.toList()));
    basePackages.addAll((Collection)Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName).collect(Collectors.toList()));
    if (basePackages.isEmpty()) {
        basePackages.add(getDefaultBasePackage(annoMeta));
    }

    String lazyInitialization = annoAttrs.getString("lazyInitialization");
    if (StringUtils.hasText(lazyInitialization)) {
        builder.addPropertyValue("lazyInitialization", lazyInitialization);
    }

    String defaultScope = annoAttrs.getString("defaultScope");
    if (!"".equals(defaultScope)) {
        builder.addPropertyValue("defaultScope", defaultScope);
    }

    builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));
    registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}

雖然很長很多,但是這些程式碼都是在新增一些Bean定義的屬性,而最關鍵的則是最上方的MapperScannerConfigurer,Mybatis將其Bean資訊註冊到了容器中,那麼這個類又是幹嘛的呢?

public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    private String basePackage;

它實現了BeanDefinitionRegistryPostProcessor,也就是說它為Bean資訊載入提供了後置處理,我們接著來看看它在Bean資訊後置處理中做了什麼:

public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    if (this.processPropertyPlaceHolders) {
        this.processPropertyPlaceHolders();
    }

  	//初始化類路徑Mapper掃描器,它相當於是一個工具類,可以快速掃描出整個包下的類定義資訊
  	//ClassPathMapperScanner是Mybatis自己實現的一個掃描器,修改了一些掃描規則
    ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
    scanner.setAddToConfig(this.addToConfig);
    scanner.setAnnotationClass(this.annotationClass);
    scanner.setMarkerInterface(this.markerInterface);
    scanner.setSqlSessionFactory(this.sqlSessionFactory);
    scanner.setSqlSessionTemplate(this.sqlSessionTemplate);
    scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName);
    scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName);
    scanner.setResourceLoader(this.applicationContext);
    scanner.setBeanNameGenerator(this.nameGenerator);
    scanner.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(this.lazyInitialization)) {
        scanner.setLazyInitialization(Boolean.valueOf(this.lazyInitialization));
    }

    if (StringUtils.hasText(this.defaultScope)) {
        scanner.setDefaultScope(this.defaultScope);
    }

  	//新增過濾器,這裡會配置為所有的介面都能被掃描(因此即使你不新增@Mapper註解都能夠被掃描並載入)
    scanner.registerFilters();
  	//開始掃描
    scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n"));
}

開始掃描後,會呼叫doScan()方法,我們接著來看(這是ClassPathMapperScanner中的掃描方法):

public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
  	//首先從包中掃描所有的Bean定義
    if (beanDefinitions.isEmpty()) {
        LOGGER.warn(() -> {
            return "No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.";
        });
    } else {
      	//處理所有的Bean定義,實際上就是生成對應Mapper的代理物件,並註冊到容器中
        this.processBeanDefinitions(beanDefinitions);
    }

  	//最後返回所有的Bean定義集合
    return beanDefinitions;
}

通過斷點我們發現,最後處理得到的Bean定義發現此Bean是一個MapperFactoryBean,它不同於普通的Bean,FactoryBean相當於為普通的Bean添加了一層外殼,它並不是依靠Spring直接通過反射建立,而是使用介面中的方法:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

通過getObject()方法,就可以獲取到Bean的例項了。

注意這裡一定要區分FactoryBean和BeanFactory的概念:

  • BeanFactory是個Factory,也就是 IOC 容器或物件工廠,所有的 Bean 都是由 BeanFactory( 也就是 IOC 容器 ) 來進行管理。
  • FactoryBean是一個能生產或者修飾生成物件的工廠Bean(本質上也是一個Bean),可以在BeanFactory(IOC容器)中被管理,所以它並不是一個簡單的Bean。當使用容器中factory bean的時候,該容器不會返回factory bean本身,而是返回其生成的物件。要想獲取FactoryBean的實現類本身,得在getBean(String BeanName)中的BeanName之前加上&,寫成getBean(String &BeanName)。

我們也可以自己編寫一個實現:

@Component("test")
public class TestFb implements FactoryBean<Student> {
    @Override
    public Student getObject() throws Exception {
        System.out.println("獲取了學生");
        return new Student();
    }

    @Override
    public Class<?> getObjectType() {
        return Student.class;
    }
}
public static void main(String[] args) {
    log.info("專案正在啟動...");
    ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class);
    System.out.println(context.getBean("&test"));   //得到FactoryBean本身(得加個&搞得像C語言指標一樣)
    System.out.println(context.getBean("test"));   //得到FactoryBean呼叫getObject()之後的結果
}

因此,實際上我們的Mapper最終就以FactoryBean的形式,被註冊到容器中進行載入了:

public T getObject() throws Exception {
    return this.getSqlSession().getMapper(this.mapperInterface);
}

這樣,整個Mybatis的@MapperScan的原理就全部解釋完畢了。

在瞭解完了Spring的底層原理之後,我們其實已經完全可以根據這些實現原理來手寫一個Spring框架了。