Spring 學習筆記

簡介

Spring 概述

​ Spring 是分層的 Java SE/EE 應用 full-stack 輕量級開源框架,以 IoC(Inverse Of Control)控制反轉 和 AOP(Aspect Oriented Programming)面向切面編程為內核,提供了展現層 Spring MVC 和持久層 Spring JDBC 以及業務層事務管理等技術,還能整合開源的第三方框架和類庫,是使用最多的 Java EE 企業應用開源框架。

​ 我們平常說的 Spring 指的是 Spring Framework,其為 Java 程式提供全面的基礎架構支援,Spring 處理基礎結構,使得我們可以專註於業務本身。是非入侵式框架(導入項目不會破壞原有項目程式碼)

​ Spring 之父:Rod Johnson

Framework Modules 組成

​ Spring Framework 由組成大約 20 個模組的 feature 組成,這些模組分為:

  • Core Container 核心容器

  • Date Access/Integration 數據訪問/整合

  • Web

  • AOP (Aspect Oriented Programming) 面向切面編程

  • Instrumentation 檢測

  • Messaging 消息

  • Test

img

IOC

導論

​ 在面向對象編程中,我們經常處理的問題就是解耦,程式的耦合性越低,表名這個程式的可讀性以及可維護性越高。IoC(Inversion of Control) 控制反轉,就是常用的面向對象編程的設計原則,使用這個原則我們可以降低耦合性。其中依賴注入是控制反轉最常見的實現

什麼是程式耦合

  • 耦合

    程式間的依賴關係

    ​ 包括:類之間的依賴

    ​ 方法之間的依賴

  • 解耦

    降低程式間的依賴關係

    我們在開發中,有些依賴關係是必須的,有些依賴關係可以通過優化程式碼來解除的。

所以實際開發中應做到:編譯期不依賴,運行時才依賴

解耦思路:

  1. 使用反射來創建對象,而避免使用 new 關鍵詞
  2. 通過讀取配置文件來獲取要創建的對象全限定類名

範例:JDBC 連接資料庫

public static void main(String[] args) throws Exception{
    //1.註冊驅動
    /*使用new對象的方式註冊驅動
    DriverManager.registerDriver(new com.mysql.jdbc.Driver());*/
    /*使用反射方式創建對象註冊驅動,此時配置內容只作為一個字元串傳遞
    Class.forName("com.mysql.jdbc.Driver");*/
    
    //而通過讀取配置文件的方式,解決上面將字元串在程式碼中寫死的問題,便於修改配置
    Properties properties = new Properties();
    properties.load(new FileInputStream("src/main/resources/data.properties"));
    //略...
    //2.獲取連接
    Connection conn = DriverManager.getConnection(url,user,password);
    //3.獲取操作資料庫的預處理對象
    PrepareStatement ps = conn.prepareStatement("select * from tb_students");
    //4.執行SQL,獲取結果集
    result = ps.executeQuery();
    //5.遍歷結果集
    while(result.next()){
        int no = result.getInt("no");
        String name = result.getString("name");
		System.out.println(no + "," + name);
    //6.釋放資源
    result.close();
    ps.close();
    conn.close();
}

​ 傳統的 JDBC 獲取連接方式也是為了解耦而使用讀取配置文件的方式配置數據源。

耦合性(Coupling),也叫耦合度,是對模組間關聯程度的度量。耦合的強弱取決於模組間介面的複雜性、調 用模組的方式以及通過介面傳送數據的多少。模組間的耦合度是指模組之間的依賴關係,包括控制關係、調用關係、數據傳遞關係。模組間聯繫越多,其耦合性越強,同時表明其獨立性越差( 降低耦合性,可以提高其獨立 性)。耦合性存在於各個領域,而非軟體設計中獨有的,但是我們只討論軟體工程中的耦合。

在軟體工程中,耦合指的就是就是對象之間的依賴性。對象之間的耦合越高,維護成本越高。因此對象的設計應使類和構件之間的耦合最小。軟體設計中通常用耦合度和內聚度作為衡量模組獨立程度的標準。劃分模組的一個 準則就是高內聚低耦合

它有如下分類:

​ (1) 內容耦合。當一個模組直接修改或操作另一個模組的數據時,或一個模組不通過正常入口而轉入另 一個模組時,這樣的耦合被稱為內容耦合。內容耦合是最高程度的耦合,應該避免使用之。 (2) 公共耦合。兩個或兩個以上的模組共同引用一個全局數據項,這種耦合被稱為公共耦合。在具有大 量公共耦合的結構中,確定究竟是哪個模組給全局變數賦了一個特定的值是十分困難的。 (3) 外部耦合 。一組模組都訪問同一全局簡單變數而不是同一全局數據結構,而且不是通過參數表傳 遞該全局變數的資訊,則稱之為外部耦合。

​ (4) 控制耦合 。一個模組通過介面向另一個模組傳遞一個控制訊號,接受訊號的模組根據訊號值而進 行適當的動作,這種耦合被稱為控制耦合。

​ (5) 標記耦合 。若一個模組 A 通過介面向兩個模組 B 和 C 傳遞一個公共參數,那麼稱模組 B 和 C 之間 存在一個標記耦合。

​ (6) 數據耦合。模組之間通過參數來傳遞數據,那麼被稱為數據耦合。數據耦合是最低的一種耦合形 式,系統中一般都存在這種類型的耦合,因為為了完成一些有意義的功能,往往需要將某些模組的輸出數據作為另
一些模組的輸入數據。

​ (7) 非直接耦合 。兩個模組之間沒有直接關係,它們之間的聯繫完全是通過主模組的控制和調用來實 現的。

總結: 耦合是影響軟體複雜程度和設計品質的一個重要因素,在設計上我們應採用以下原則:如果模組間必須 存在耦合,就盡量使用數據耦合,少用控制耦合,限制公共耦合的範圍,盡量避免使用內容耦合。

內聚與耦合

​ 內聚標誌一個模組內各個元素彼此結合的緊密程度,它是資訊隱蔽和局部化概念的自然擴展。內聚是從 功能角度來度量模組內的聯繫,一個好的內聚模組應當恰好做一件事。它描述的是模組內的功能聯繫。耦合是軟體結構中各模組之間相互連接的一種度量,耦合強弱取決於模組間介面的複雜程度、進入或訪問一個模組的點以及通過介面的數據。 程式講究的是低耦合,高內聚。就是同一個模組內的各個元素之間要高度緊密,但是各個模組之 間的相互依存度卻要不那麼緊密。

​ 內聚和耦合是密切相關的,同其他模組存在高耦合的模組意味著低內聚,而高內聚的模組意味著該模組同其他模組之間是低耦合。在進行軟體設計時,應力爭做到高內聚,低耦合

​ 具體到項目中,帶來了哪些依賴問題呢:

使用工廠模式解耦

​ 先了解一下工廠模式解耦的思想,會給下面 Spring 控制反轉使用帶來啟發。

​ 在實際開發中我們可以把三層的對象都使用配置文件配置起來,當啟動伺服器應用載入的時候,讓一個類中的方法通過讀取配置文件,把這些對象創建出來並存起來。在接下來的使用的時候,可以直接拿過來用。

​ 那麼,這個讀取配置文件,創建和獲取三層對象的類就是工廠(Factory)

範例:

項目結構:

image-20200507222036404

對應程式碼:以表現層 – 業務層 – 持久層 – 工廠 順序

表現層程式碼:

package com.yh.view;

import com.yh.factory.BeanFactory;
import com.yh.service.INameService;

/**
 * 模擬一個表現層用於調用業務層
 * @author YH
 * @create 2020-05-07 16:19
 */
public class Cilent {
    public static void main(String[] args){
        //想調用業務層方法依賴與其實現類對象
//        INameService service = new NameServiceImpl();
        INameService service = (INameService)BeanFactory.getBean("nameService");
        System.out.println("表現層後台程式碼執行調用業務邏輯層:1");
        service.method();
    }
}

業務層程式碼:

package com.yh.service;

/**
 * 業務邏輯層介面
 * @author YH
 * @create 2020-05-07 16:17
 */
public interface INameService {
    void method();
}
package com.yh.service.impl;

import com.yh.dao.INameDao;
import com.yh.dao.impl.NameDaoImpl;
import com.yh.factory.BeanFactory;
import com.yh.service.INameService;

/**
 * 模擬業務邏輯層調用持久層
 * @author YH
 * @create 2020-05-07 16:18
 */
public class NameServiceImpl implements INameService {
    @Override
    public void method() {
        //想調用持久層方法依賴與其實現類對象
//        INameDao nameDao = new NameDaoImpl();
        INameDao nameDao = (INameDao) BeanFactory.getBean("nameDao");
        System.out.println("業務邏輯層實現類執行調用持久層:2");
        nameDao.method();
    }
}

持久層程式碼:

package com.yh.dao;

/**
 * 持久層介面
 * @author YH
 * @create 2020-05-07 16:14
 */
public interface INameDao {
    void method();
}
package com.yh.dao.impl;

import com.yh.dao.INameDao;

/**
 * 模擬持久層
 * @author YH
 * @create 2020-05-07 16:15
 */
public class NameDaoImpl implements INameDao {
    @Override
    public void method() {
        System.out.println("持久層dao執行 3");
    }
}

工廠:

package com.yh.factory;

import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 一個創建Bean對象的工廠
 * Bean:在電腦英語中,有可重用組件的含義
 * JavaBean:用java語言編寫的可重用組件
 * 注意:JavaBean不等於實體類,且包含實體類,即 JavaBean > 實體類
 *
 * 創建service和dao對象
 *
 * 1.需要通過配置文件讀取配置,可用兩種方式: xml 或 properties
 *      配置的內容:唯一標識=全限定類名(key-value)
 * 2.再通過讀取配置文件中配置的內容,反射創建對象
 * @author YH
 * @create 2020-05-07 17:14
 */
public class BeanFactory {
    private static Properties props;

    /**
     * 定義一個Map,作為存儲對象的容器,存放我們要創建的對象
     */
    private static Map<String,Object> beans = null;

    /**
     * 靜態程式碼塊只執行一次,保證了從始至終只生成配置中對應的唯一一個實例
     */
    static {
        try {
            props = new Properties();
            InputStream in = BeanFactory.class.getClassLoader().getResourceAsStream("factory.properties");
            props.load(in);
            //實例化Map容器
            beans = new HashMap<>();
            //取出配置文件中所有的key
            Enumeration<Object> keys = props.keys();
            //遍歷枚舉
            while(keys.hasMoreElements()){
                //取出每個key
                String key = keys.nextElement().toString();
                //根據key從配置中讀取value
                String beanPath = props.getProperty(key);
                //反射創建實例對象
                Object value = BeanFactory.class.forName(beanPath).newInstance();
                //把key和value存入容器中
                beans.put(key,value);
            }

        } catch (Exception e) {
            //讀取配置文件出現異常那麼後面的操作都無意義,所以直接聲明一個錯誤終止程式
            throw new ExceptionInInitializerError("初始化properties時發生錯誤!");
        }
    }

    /**
     * 根據bean的名稱獲取bean對象
     * @param beanName
     * @return
     */
    public static Object getBean(String beanName){
        return beans.get(beanName);
    }

    /**
     * 傳入key的名稱尋找對應的value全類名 並創建對象返回
     * @param beanName
     * @return
     *//*
    public static Object getBean(String beanName){
        Object bean = null;
        try {
            String beanPath = props.getProperty(beanName);
            //每次都會調用默認構造函數創建對象
            bean = (Object) Class.forName(beanPath).newInstance();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return bean;
    }*/
}

小結:通過工廠類初始化載入就將配置文件中所代表的類創建並存儲到 Map 中,需要使用時調用工廠方法即可,避免了 new,即避免了反覆創建對象,也降低了程式的耦合度

控制反轉 IOC

​ 控制反轉(Inversion Of Control)把創建對象的權利交給框架,是框架的重要特徵,並非面向對象編程的專用術語。它包括依賴注入(DI)和依賴查找(DL)

作用:消減電腦程式的耦合(解除我們程式碼中的依賴關係)

以上面小節為例:

​ 我們通過工廠創建對象,將對象存儲在容器中,提供獲取對象的方法。在這個過程中:

​ 獲取對象的方式發生了改變:

​ 以前:獲取對象,採用 new 的方式,是主動的

image-20200508120420272

​ 現在:通過工廠獲取對象,工廠為我們查找或者創建對象,是被動的

image-20200508120555456

使用 Spring 的 IOC 解決程式耦合

  1. 準備 spring 的開發包

    • spring 目錄結構:
      • docs:API 和開發規範
      • libs:jar 包和源碼
      • schema:約束
  2. 以上一節工廠解耦改為使用 spring

第一步:向項目的 pro.xml 文件中加入配置,將 spring 的 jar 包導入工程:

<!--設置打包方式-->
<packaging>jar</packaging>

<dependencies>
    <!--        導入spring-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
</dependencies>

第二步:在資源目錄下創建一個 xml 文件

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

    <!-- bean標籤:用於配置讓spring創建對象,並且存入IOC容器之中
         id屬性:對象的唯一標識
         class屬性:指定要創建對象的全限定類名
    -->
    <bean id="dao" class="yh.dao.impl.NameDaoImpl"></bean>
    <bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
</beans>

第三步:讓 spring 管理資源,在配置文件中配置 service 和 dao

public class Client {
    /**
     * 獲取spring的核心容器 並根據id獲取對象
     * @param args
     */
    public static void main(String[] args){
        //1.獲取核心容器對象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id獲取bean對象
        INameDao dao = (INameDao)ac.getBean("dao");
        INameService service = ac.getBean("service",INameService.class);

        System.out.println(dao);
        System.out.println(service);
    }
}

測試配置是否成功:

image-20200508142735850

Spring 中工廠的類結構:

image-20200508143643321

image-20200508150247755

​ 可以看出 BeanFactory 是 Spring 容器中的頂層介面,ApplicationContext 是其子介面,它們創建對象的時間點的區別:

​ ApplicationContext:只要一讀取配置文件,默認情況下就會創建對象(即時創建),可以推斷:即時創建對象適合使用在單例模式的場景,對象只創建一次

​ BeanFactory:什麼時候使用對象了,才會創建對象(延遲創建),同理:延遲創建對象適合於多例模式的場景,節省性能開銷

ApplicationContext 的三個常用實現類:

  • ClassPathXmlApplicationContext:載入類路徑下的配置文件,要求配置文件必須在類路徑下
  • FileSystemXmlApplication:載入磁碟任意路徑下的配置文件(必須有訪問許可權)
  • AnnotationConfigApplicationContext:用於讀取註解創建容器

Spring 基於 XML 的 IOC 細節

IOC 本質

控制反轉 IoC,是一種設計思想,DI(依賴注入)是實現 IoC 的一種方法。沒有 IoC 的程式中,使用面向對象編程,對象的創建與對象的依賴關係完全硬編碼在程式中,對象的創建由程式自己控制,控制反轉後將對象的創建轉移給第三方,即獲得依賴對象的方式反轉了。

image-20200521135312514

IoC 是 Spring 框架的核心內容,使用多種方式完美實現了 IoC,可以使用 XML 配置,也可以使用註解,新版本的 Spring 也可以零配置實現 IoC。Spring 容器在初始化時先讀取配置文件,根據配置文件或元數據創建與組織對象存入容器中,程式使用時再從 IoC 容器中取出需要的對象。

image-20200521143116286

控制反轉是一種通過描述(XML 或註解)並通過第三方去生產或獲取特定對象的方式,在 Spring 中實現控制反轉的是 IoC 容器,其實現方法是依賴注入(Dependency Injection,DI)。

所謂控制反轉,就是應用本身不負責依賴對象的創建及維護,依賴對象的創建及維護是由外部容器負責的。其中依賴注入是控制反轉最常見的實現。

​ 那我們來先搞清這個依賴對象是什麼,下面是傳統三層架構的程式碼示例:

持久層:

//持久層介面
public interface IUserDao {
    void daoMethod();
}
//持久層介面實現1
public class UserDaoImpl implements IUserDao {
    public void daoMethod() {
        System.out.println("資料庫連接1");
    }
}
//持久層介面實現2
public class UserDaoImpl2 implements IUserDao {
    public void daoMethod() {
        System.out.println("資料庫連接2");
    }
}

持久層即數據訪問層(DAL 層),其功能主要是負責資料庫的訪問,實現對數據表的 CEUD 等操作。

​ 可能會有變更介面實現的需求(如 MySQL 換為 Oracle)

業務邏輯層:

//業務邏輯層介面
public interface IUserService {
    void serviceMethod();
}
//業務邏輯層介面實現
public class UserServiceImpl implements IUserService {
    //業務層需要或許持久層對象,調用其方法
    IUserDao dao = new UserDaoImpl();
    public void serviceMethod() {
        dao.daoMethod();
    }
}

三層架構的核心,其關注點是業務規則的制定、業務流程的實現等與業務需求有關的系統設計。

視圖層(表示層):

@Test
public void test1(){
    //程式入口要獲取業務層對象來調用功能
    IUserService service = new UserServiceImpl();
    service.serviceMethod();
}

表示層主要作用是與用戶進行交互,顯示數據(如列印到控制台的資訊)和接收傳輸用戶的數據,提供用戶操作介面等。

運行結果:image-20200520195731474

​ 這就是傳統三層架構的一個調用流程,可以看出作為三層核心的業務層起的一個承上啟下的作用。表示層與用戶交互,要執行功能那麼就需要先貨到控制層的對象,調用相關功能。即沒有業務層對象就沒法實現操作,則表示層依賴於業務邏輯層,沒它不行;同樣的,業務邏輯層作為一個指揮全局的頭,需要指揮小弟來辦事,所以他先得有個小弟,那麼就獲取一個持久層對象了,同樣是沒有這個小弟沒法辦事,而且加入要辦另外一件事需要另一個小弟,那業務層大哥也要做相應的調整(改程式碼)。此時業務邏輯層依賴於持久層。

真是世間美好與你環環相扣,變強了,頭也就禿了(手動**)

​ 針對變更持久層實現需要修改業務層程式碼的問題做一個優化,使用 set 方法注入方式獲取對象,如下:

業務層實現類:

public class UserServiceImpl implements IUserService {
    /**
     * 對象注入
     */
    private IUserDao dao;
    public void set(IUserDao dao){
        this.dao = dao;
    }

    public void serviceMethod() {
        dao.daoMethod();
    }
}

​ 利用多態的特性可接收任何其實現對象,外部根據不同的需求傳遞不同的實現對象參數,從而避免了二次修改業務層程式碼。

測試程式碼:

@Test
    public void test1(){
        //程式入口要獲取業務層對象來調用功能
        IUserService service = new UserServiceImpl();

//        service.setDaoImpl(new UserDaoImpl());
        service.setDaoImpl(new UserDaoImpl2());

        service.serviceMethod();
    }

傳入不同的實現參數,獲取不同的連接:

image-20200521105738040

image-20200521105701107

​ 對比:

​ 之前,程式主動創建對象,由程式設計師決定使用的功能(更改程式碼)

​ 使用 set 注入後,程式變成被動接受對象,由使用者決定使用的功能(傳遞對應的參數)

這種讓程式設計師不再管理對象創建的思想,使得程式系統的耦合性大大降低,讓程式設計師可以更加專註於業務的實現上,這就是 IoC 的原型。

​ 對,是原型,起關鍵作用的就是 set 方法,它是得以注入的關鍵,下面就使用 Spring IoC 來建立第一個程式:

JavaBean:

public class Hello {
    private String name;

    //注意此set方法
    public void setName(String name){
        this.name = name;
    }
    public void run(){
        System.out.println("Hello!" + name);
    }
}

使用 XML 方式進行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="//www.springframework.org/schema/beans
        //www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置元數據
	bean的配置:
    使用Spring創建對象,對象都用Bean表示
    對比原有的new創建對象方式:
        Heelo hello = new Hello() 即 類型 變數名 = new 類型()
        - id 指定對象變數名 -> 變數名
        - class 指定要創建的對象的類
        - property 指定對象的屬性
             name 指定屬性名
             value 指定屬性值
    -->
    <bean id="hello" class="yh.pojo.Hello">
        <property name="name" value="熊大"/>
    </bean>
</beans>

測試程式碼:

@Test
public void test1(){
    //獲取Spring的上下文對象
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    //我們的對象現在都在spring中管理了,我們要使用,直接去裡面取出來即可(取Bean)
    Hello hello = context.getBean("hello", Hello.class);
    //Hello hello = (Hello)context.getBean("hello");
    hello.run();
}

結果:image-20200521153102644

整個過程中:

​ hello 對象有 Spring 創建

​ hello 對象的屬性也由 Spring 容器設置

這就是控制反轉:

  • 控制:傳統程式的對象是由程式本身控制創建的,使用 Spring 後,對象是由 Spring 創建的。

  • 反轉:程式本身不創建對象,而變成被動地接收對象。

  • 依賴注入:就是利用 set 方法進行注入。

  • IOC 就是一種編程思想,由主動的編程編編程被動的接收。

至此,我們徹底不用去程式中改動了,要實現不同的操作,只需要在 xml 配置文件中進行修改,對象由 Spring 來創建、管理、裝配。

​ 現在我們來修改最開始的那個傳統實例,看看用 IoC 如何實現它:

配置文件:

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

    <bean id="daoImpl" class="yh.dao.impl.UserDaoImpl"/>
    <bean id="daoImpl2" class="yh.dao.impl.UserDaoImpl2"/>

    <bean id="service" class="yh.service.impl.UserServiceImpl">
        <!--
        ref:引用spring容器中創建好的對象
        value:具體的值類型數據
        -->
        <property name="dao" ref="daoImpl"/>
    </bean>
</beans>

​ 由於業務層實現中原本就設置了 set 方法,所以可以直接配置注入屬性的資訊

注意:set 方法命名一定要按照規範,否則無法識別注入

其他地方都不用修改,直接進行測試:

@Test
public void test1(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    IUserService service = context.getBean("service", IUserService.class);
    service.serviceMethod();
}

結果:

image-20200521161949574

如需更改配置,直接修改配置文件中的 dao 屬性值(配置文件不屬於程式程式碼),如下:

<bean id="service" class="yh.service.impl.UserServiceImpl">
    <!--更改引用值-->
        <property name="dao" ref="daoImpl2"/>
    </bean>

image-20200521161914292

​ 增加實現,或換實現原都可以通過元數據完成了。

IOC 中 bean 標籤和管理對象細節

bean 標籤

作用:用於配置對象讓 spring 來創建

​ 默認情況下他調用的是類中的無參構造器,如果沒有無參構造器則不能創建成功

屬性

​ id:給對象在容器中提供一個唯一標識,用於獲取對象

​ class:指定類的全限定類名,用於反射創建對象。默認情況下調用無參構造器

​ scope:指定對象的作用範圍

​ init-method:指定類中的初始化方法名稱

​ destroy-method:指定類中銷毀方法名稱

實例化 Bean 的三種方式

第一種方式:使用構造器

  • 使用默認無參構造函數(bean 對象需要設置 set 方法)
<!--在默認情況下:
    他會根據默認無參構造函數來創建類對象,如果bean中沒有默認無參構造函數,將會創建失敗-->
<bean id="service" class="yh.service.impl.NameServiceImpl"></bean>
  • 使用有參構造器(即用構造器代替 set 方法給屬性注入值)
<bean id="hello" class="yh.pojo.Hello">
    <constructor-arg name="name" value="Spring"/>
</bean>

第二種方式:spring管理實例工廠,使用實例工廠的方法創建對象

<!--先把工廠的創建交給spring來管理,然後使用工廠bean來調用裡面的方法(先創建工廠對象,再用其獲取service對象)
    factory-bean 屬性:用於指定實例工廠bean的id
    factory-method 屬性:用於指定實例工廠中創建對象的方法
-->
    <bean id="instanceFactory" class="yh.factory.InstanceFactory"></bean>
    <bean id="nameService" factory-bean="instanceFactory" factory-method="createNameService"></bean>

第三種方式:spring管理靜態工廠,使用靜態工廠的方法創建對象

<!--使用StaticFactory類中的靜態方法創建對象,並存入spring容器
    id:指定bean的id,用於從容器中獲取
    class:指定靜態工廠的全限定類名
    factory-method 屬性:指定生成對象的工廠靜態方法
-->
    <bean id="nameService" class="yh.factory.StaticFactory" factory-method="createNameService"></bean>

調用類:

package yh.view;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import yh.service.INameService;

/**
 * 模擬一個表現層用於調用業務層
 * @author YH
 * @create 2020-05-07 16:19
 */
public class Client {
    /**
     * 獲取spring的核心容器 並根據id獲取對象
     * @param args
     */
    public static void main(String[] args){
        //1.獲取核心容器對象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id獲取bean對象
        INameService service = ac.getBean("nameService",INameService.class);

        System.out.println(service);
    }
}

測試結果都能獲取到對象:

image-20200508174144220

bean 的作用範圍和生命周期

在配置文件載入的時候,容器中管理的對象(Bean)就已經初始化了,需要哪個對象通過 Spring 上下文對象直接獲取即可(getBean())。

參考官方介紹:

範圍 描述
singleton (默認)為每個 Spring IoC 容器的單個 object 實例定義單個 bean 定義。
prototype 為任意數量的 object 實例定義單個 bean 定義。
request 將單個 bean 定義範圍限定為單個 HTTP 請求的生命周期。也就是說,每個 HTTP 請求都有自己的 bean 實例,該實例是在單個 bean 定義的後面創建的。僅在 web-aware Spring ApplicationContext的 context 中有效。
session 將單個 bean 定義範圍限定為 HTTP Session的生命周期。僅在 web-aware Spring ApplicationContext的 context 中有效。
application 將單個 bean 定義範圍限定為ServletContext的生命周期。僅在 web-aware Spring ApplicationContext的 context 中有效。
websocket 將單個 bean 定義範圍限定為WebSocket的生命周期。僅在 web-aware Spring ApplicationContext的 context 中有效。

bean 對象的作用範圍:

​ 使用 scope 屬性指定對象的作用範圍,參數:

singleton:單例的(默認值)

prototype:多例的

​ request:WEB 項目中 Spring 創建一個 Bean 的對象,將對象存入到 request 域中

​ session:WEB 項目中 Spring 創建一個 Bean 的對象,將對象存入到 session 域中

​ global session:作用於集群環境的會話範圍(全局會話範圍),不是集群它就是 session

global session(全局變數)應用場景:

​ 一個web工程可能有多個伺服器分流,用戶首次發送請求訪問 web 時所連接的伺服器和提交登錄所請求的伺服器可能不一同一個伺服器,但是驗證碼生成首先是從第一次訪問時的伺服器獲取的,並保存在獨有 session 中,提交登錄時肯定需要比較驗證碼正確性,由於可能不在一個伺服器無法驗證,所以就需要 global session 這個全局變數,無論在哪個伺服器都可以驗證

示意圖:

image-20200508200937130

生命周期:

單例對象:scope=”singleton”

​ 一個應用只有一個對象的實例,它的作用範圍就是整個應用

​ 對象出生:當應用載入,創建容器時,對象就被創建了

​ 對象活著:只要容器在,對象一直活著

​ 對象死亡:當應用卸載,容器銷毀時,對象也被銷毀

多例對象:scope=”prototype”

​ 每次訪問時,都會重新創建對象實例

​ 對象出生:當使用對象時,創建新的對象實例

​ 對象活著:對象使用期間一直活著

​ 對象死亡:當對象長時間不用,被java的垃圾回收機制回收了

Spring 的依賴注入

依賴注入的概念

​ 依賴注入:Dependdency Injection。它是 spring 框架核心 IOC 的具體實現

​ 我們的程式在編寫時,通過控制反轉,把對象的創建交給了 spring,但是程式碼中不可能出現沒有依賴的情況。ioc 解耦只是降低他們的依賴關係,但不會消除。例如:我們的業務層仍會調用持久層的方法。

​ 那這種業務層和持久的依賴關係,在使用 spring 之後,就讓 spring 來維護了;

​ 簡單的說,就是坐等框架把持久層對象傳入業務層,而不用我們自己去獲取

構造函數注入

​ 顧名思義,就是使用類中的構造函數,給成員變數賦值

  • 構造函數注入

要求:
類中需要提供一個對應的帶參構造器
涉及的標籤:
constructor-arg
屬性:
index:指定要注入的數據給構造函數中指定索引位置的參數賦值,索引從0開始
type:指定要注入數據的數據類型,該類型也是某個或某些參數的類型
name:指定給構造器中指定名稱的參數賦值
—————以上三個屬性用於指定要給哪個參數賦值—————
value:用於提供基本類型和String類型的數據
ref:用於指定其他的bean類型數據(即在spring的IOC核心容器中出現過的bean對象
– 優勢:
在獲取bean對象時,注入數據時必須的操作,否則對象無法創建成功
– 弊端:
改變了bean對象的實例化方式,調用有參構造器,使我們在創建對象時,不管需不需要這些數據,也必須提供

xml 文件配置:

<!--使用構造函數的方式,給service中的屬性傳值-->
    <bean id="nameService" class="yh.service.impl.NameServiceImpl">
        <constructor-arg name="name" value="雲翯"></constructor-arg>
        <constructor-arg name="age" value="18"></constructor-arg>
        <constructor-arg name="birthday" ref="now"></constructor-arg>
    </bean>
<!--    配置一個日期對象-->
    <bean id="now" class="java.util.Date"></bean>

實現類提供有參構造器:

public class NameServiceImpl implements INameService {
    private String name;
    private Integer age;
    private Date birthday;

    public NameServiceImpl(String name, Integer age, Date birthday) {
        this.name = name;
        this.age = age;
        this.birthday = birthday;
    }

    @Override
    public void method() {
        System.out.println(name + "," + age + "," + birthday);
    }
}

調用類:

public class Client {
    /**
     * 獲取spring的核心容器 並根據id獲取對象
     * @param args
     */
    public static void main(String[] args){
        //1.獲取核心容器對象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id獲取bean對象
        INameService service = ac.getBean("nameService",INameService.class);
        service.method();
    }
}

測試結果:

image-20200509090209538

set 方法注入(常用)

涉及的標籤:property
出現的位置:bean標籤內部
屬性:
name:指定所用的set方法名稱
value:指定基本類型和String類型的數據
ref:指定其他bean類型數據(即spring的IOC核心容器中出現過的bean對象)

  • 優勢:
    創建對象時沒有明確的限制,可以直接使用默認構造函數
  • 弊端:
    因為是先創建對象再通過set賦值,假如某個成員必須有值,而獲取對象時有可能set方法還沒有執行

顧名思義,實現類中需要提供set方法。範例:

xml 配置文件:

<bean id="nameService1" class="yh.service.impl.NameServiceImpl1">
    <property name="name" value="雲翯1"></property>
    <property name="age" value="19"></property>
    <property name="birthday" ref="now"></property>
</bean>
<!--    配置一個日期對象-->
    <bean id="now" class="java.util.Date"></bean>

帶有 set() 方法的實現類:

public class NameServiceImpl1 implements INameService {
    private String name;
    private Integer age;
    private Date birthday;

    public NameServiceImpl1() {}

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

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }

    @Override
    public void method() {
        System.out.println(name + "," + age + "," + birthday);
    }
}

調用類:

public class Client {
    /**
     * 獲取spring的核心容器 並根據id獲取對象
     * @param args
     */
    public static void main(String[] args){
        //1.獲取核心容器對象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id獲取bean對象
        INameService service = ac.getBean("nameService1",INameService.class);
        service.method();
    }
}

測試結果:

image-20200509091958836

  • 注入集合數據(複雜類型注入)

    ​ 顧名思義,就是給集合成員傳值,他用的也是set方法注入的方式,只不過變數的數據類型都是集合。

    用於給 List 結構集合注入的標籤
    list、array、set
    用於給 Map 結構集合注入的標籤
    map、props
    結構相同,標籤可以互用

    範例:

    xml 配置文件:

    <!-- 複雜類型的注入/集合類型的注入-->
        <bean id="nameService2" class="yh.service.impl.NameServiceImpl3">
            <property name="myStrs">
                <array>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </array>
            </property>
            <property name="myList">
                <list>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </list>
            </property>
            <property name="mySet">
                <set>
                    <value>AAA</value>
                    <value>BBB</value>
                    <value>CCC</value>
                </set>
            </property>
            <property name="myMap">
                <map>
                    <entry key="testA" value="aaa"></entry>
                    <entry key="testB" value="bbb"></entry>
                    <entry key="testC" value="ccc"></entry>
                </map>
            </property>
            <property name="myProps">
                <props>
                    <prop key="testA">aaa</prop>
                    <prop key="testB">bbb</prop>
                </props>
            </property>
        </bean>
    

    集合等複雜類型的屬性,同樣使用set方法賦值:

public class NameServiceImpl3 implements INameService {
    private String[] myStrs;
    private List<String> myList;
    private Set<String> mySet;
    private Map<String,String> myMap;
    private Properties myProps;

    public void setMyStrs(String[] myStrs) {
        this.myStrs = myStrs;
    }

    public void setMyList(List<String> myList) {
        this.myList = myList;
    }

    public void setMySet(Set<String> mySet) {
        this.mySet = mySet;
    }

    public void setMyMap(Map<String,String> myMap) {
        this.myMap = myMap;
    }

    public void setMyProps(Properties myProps) {
        this.myProps = myProps;
    }

    @Override
    public void method() {
        System.out.println(myStrs);
        System.out.println(myList);
        System.out.println(mySet);
        System.out.println(myMap);
        System.out.println(myProps);
    }
}

調用類:

public class Client {
    /**
     * 獲取spring的核心容器 並根據id獲取對象
     * @param args
     */
    public static void main(String[] args){
        //1.獲取核心容器對象
        ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
        //2.根據id獲取bean對象
        INameService service = ac.getBean("nameService2",INameService.class);
        service.method();
    }
}

測試結果:

image-20200509101235696

命名空間注入

​ 我們可以使用 p 命名空間和 c 命名空間,進行注入

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:p="//www.springframework.org/schema/p"
       xmlns:c="//www.springframework.org/schema/c"
       xsi:schemaLocation="//www.springframework.org/schema/beans
        //www.springframework.org/schema/beans/spring-beans.xsd">
	<!--p命名空間注入,可以直接注入屬性的值-->
    <bean id="user" class="yh.pojo.User" p:name="無問西東" p:age="18"/>
    
    <!--c命名空間注入,可以直接注入構造器的值-->
    <bean id="user2" class="yh.pojo.User" c:name="無問西東" c:age="18"/>
</beans>

​ p 和 c 命名空間允許 bean 元素通過屬性(而不是嵌套的子元素)來描述注入的屬性值。但是不能直接使用,需要導入 XML 約束:

xmlns:p="//www.springframework.org/schema/p"
xmlns:c="//www.springframework.org/schema/c"

案例:使用 spring Ioc(XML)實現的 CRUD

結構:

image-20200522192401585

Account 類:

public class Account {
    private int id;
    private String name;
    private float money;
    //標準JavaBean,剩餘程式碼略...
}

dao 介面:

public interface IAccountDao {
    /**
     * 查詢所有
     * @return
     */
    List<Account> findAccounts();

    /**
     * 查詢一個
     * @param account
     * @return
     */
    Account findAccountById(Integer account);

    /**
     * 保存
     * @param account
     */
    void saveAccount(Account account);

    /**
     * 更新
     * @param account
     */
    void updateAccount(Account account);

    /**
     * 刪除
     * @param id
     */
    void deleteAccountById(Integer id);
}

dao 介面實現:

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;

    public void setRunner(QueryRunner runner) {
        this.runner = runner;
    }

    public List<Account> findAccounts() {
        try {
            return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public Account findAccountById(Integer account) {
        try {
            return runner.query("select * from account where id=?",new BeanHandler<Account>(Account.class),account);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void saveAccount(Account account) {
        try {
            runner.update("insert into account(name,money) values(?,?)",account.getName(),account.getMoney());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void updateAccount(Account account) {
        try {
            runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteAccountById(Integer id) {
        try {
            runner.update("delete from account where id=?",id);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

service 層:

public interface IAccountService {
    /**
     * 查詢所有
     * @return
     */
    List<Account> findAccounts();

    /**
     * 查詢一個
     * @param account
     * @return
     */
    Account findAccountById(Integer account);

    /**
     * 保存
     * @param account
     */
    void saveAccount(Account account);

    /**
     * 更新
     * @param account
     */
    void updateAccount(Account account);

    /**
     * 刪除
     * @param id
     */
    void deleteAccountById(Integer id);
}

service 介面實現:

public class AccountServiceImpl implements IAccountService {
    private IAccountDao accountDao;

    /**
     * set注入
     * @param accountDao
     */
    public void setAccountDao(IAccountDao accountDao) {
        this.accountDao = accountDao;
    }

    /**
     * 獲取所有賬戶資訊
     * @return
     */
    public List<Account> findAccounts() {
        return accountDao.findAccounts();
    }

    public Account findAccountById(Integer account) {
        return accountDao.findAccountById(account);
    }

    public void saveAccount(Account account) {
        accountDao.saveAccount(account);
    }

    public void updateAccount(Account account) {
        accountDao.updateAccount(account);
    }

    public void deleteAccountById(Integer id) {
        accountDao.deleteAccountById(id);
    }
}

Spring 上下文配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="//www.springframework.org/schema/beans
        //www.springframework.org/schema/beans/spring-beans.xsd">
    <!--配置service-->
    <bean id="accountService" class="yh.service.impl.AccountServiceImpl">
        <property name="accountDao" ref="accountDao"/>
    </bean>
    <!--配置dao-->
    <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
        <property name="runner" ref="dbutils"/>
    </bean>
    <!--配置dbutils 避免多執行緒干擾,設此bean設為多例-->
    <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dateScore"/>
    </bean>
    <!--配置數據源-->
    <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--連接資料庫的基本資訊-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

測試程式碼:

public class Mytest {
    @Test
    public void testFindAll(){
        //獲取容器
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //獲取業務層對象
        IAccountService service = context.getBean("accountService", IAccountService.class);
        //調用方法
        List<Account> accounts = service.findAccounts();
        for (Account account : accounts) {
            System.out.println(account.toString());
        }
    }
    @Test
    public void testFindOne(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        Account account = service.findAccountById(2);
        System.out.println(account.toString());
    }
    @Test
    public void testSave(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.saveAccount(new Account(5,"ddd",999));
    }
    @Test
    public void testUpdate(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.updateAccount(new Account(2,"bbb2",999));
    }
    @Test
    public void testDelete(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        IAccountService service = context.getBean("accountService", IAccountService.class);
        service.deleteAccountById(2);
    }
}

基於註解的 IOC 配置

Java 註解(Annotation)

​ 從 JDK 5.0 開始,java 增加了對元數據(MetaData)的支援,也就是 Annotation(註解)

​ 註解是程式碼里的特殊標記,可以在編譯、類載入、運行時被讀取,並執行相應的處理,通過使用註解,我們可以在不改變原有邏輯的情況下,在源文件中嵌入一些補充資訊。程式碼分析工具、開發工具和部署工具可以通過這些補充資訊進行驗證或進行部署。

​ 註解可以像修飾符一樣被使用,用來修飾包、類、構造器、方法、成員變數、參數、局部變數的聲明,這些資訊被保存在 Annation 的 name=value 對中。

框架 = 註解 + 反射 + 設計模式

  • 自定義註解

    ​ 使用 @interface 關鍵字如:public @interface testAnnotation,自定義註解自動繼承了 java.lang.annotation.Annotation 介面;

    ​ 註解的成員變數在定義時以無參方法的形式來聲明(如:String[] value()),其方法名和返回值定義了該成員變數的名字和類型,此為配置參數,類型只能是八種基本數據類型、String、Class、enum、Annotation 這幾個類型的數組(有多個 value 值);

    ​ 可以在定義註解的成員變數時使用 default 關鍵字,為其指定初始值。如果只有一個參數成員,建議設置參數名為 value

    ​ 如果定義的註解有配置參數,那麼使用時必須指定參數值,除非它有默認值。格式:參數名 = 參數值,如果只有一個參數成員,且名稱為 value,可以省略 value = 參數值,直接寫參數值即可;

    ​ 沒有成員定義的註解稱為標記(如:@Override)包含成員變數的註解稱為元數據註解

    注意:自定義註解必須配上註解的資訊處理流程(使用反射)才有意義。

  • JDK 中的元註解

    元註解:對現有註解進行解釋說明的註解。

    jdk 提供的 4 中元註解:

    • @Retention:用於修飾一個 Annotation 定義,指定其生命周期,包含一個 RetentionPolicy 類型的成員變數,使用時需指定 value 的值:

      RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),編譯器直接丟棄這種策略的注釋;

      RetentionPolicy.CLASS:在 class 文件中有效(即 class 保留),當運行 Java 程式時,JVM 不會保留注釋。這是默認值

      RetentionPolicy.RUNTIME:在運行時有效(即運行時保留),當運行 Java 程式時,JVM 會保留注釋。程式可以通過反射獲取該注釋

    image-20200509135250937

    • @Target:用於指定被修飾的 Annotation 能用於修飾哪些程式元素
    • Documented:表示所修飾的註解在被Javadoc解析時,保留下來
    • Inherited:被其修飾的註解將有繼承性(子類繼承父類的註解)
    • jdk 8 新增:可重複註解 和 類型註解

擴展:元數據,是指對數據進行修飾的數據。如:在String name = "YunHe";"YunHe"為數據,而 String name = 就為元數據

基於註解的 IOC 配置

​ 配置註解與配置 xml 文件要實現的功能是一樣的,都是要降低程式間的耦合,只是配置的形式不一樣 。

與 xml 配置對應,可將註解簡單分為:

用於創建對象的:

相當於:<bean id="" class="">
	@Component:
        作用:用於把當前類對象存入 spring 容器中
        屬性:
            value:用於指定 bean 的 id。默認值為當前類名首字母小寫
	@Controller:一般用在表現層
    @Service:一般用在業務層
	@Repository:一般用在持久層
		以上三個註解作用和屬性與 @Component 一樣(父子關係),是 spring 框架提供明確的三層使用註解,
    使我們的三層對象更加清晰

用於注入數據的:

相當於:<property name="" ref="">  /  <property name="" value="">
	@Autowired
       	作用:自動按照類型注入(自動裝配)。當使用註解注入屬性時,set方法可以省略。自動將spring容器中
    的 bean 注入到類型匹配的帶有此註解的屬性。當有多個類型匹配時,配合@Qualifier指定要注入的bean
	@Qualifier
		作用:在自動按照類型注入的基礎之上,再按照bean的id注入(解決自動注入存在多個同類型的 bean
    所產生的歧義問題),它在給欄位注入時不能獨立使用,必須和 @Autowire 一起使用;但是給方法參數注
    入時,可以獨立使用(指定形參所要接收的bean的id名)。
		屬性:
			value:指定bean的id
	@Resource
		作用:直接按照bean的id注入,它也只能注入其他bean類型
		屬性:
			name:指定bean的id
    @Value
    	作用:注入基本數據類型和 String 類型數據
    	屬性:
    		value:用於指定值

用於改變作用範圍的:

相當於:<bean id="" class="" scope="">
	@Scope
    作用:指定 bean 的作用範圍
    屬性:
    	value:指定範圍的值
    		取值:singleton/prototype/request/session/globalsession

聲明周期相關:

相當於:<bean id="" class="" init-method="" destroy-method="">
	@PostConstruct
	作用:用於指定初始化方法
	
	@PreDestroy
	作用:用於指定銷毀方法

自動按照類型注入示意圖:

image-20200511173755370

注意:spring 識別 bean 的範圍時需通過 xml 配置設置 spring 創建容器時要掃描的包。

使用註解方式修改上例 CRUD 程式

這裡就貼上修改的部分程式碼(改動太小了)

dao 實現類:(兩個註解)

@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
    @Autowired
    private QueryRunner runner;
//...
}

service 實現類:(兩個註解)

@Service("accountService")
public class AccountServiceImpl implements IAccountService {
    @Autowired
    private IAccountDao accountDao;
//...
}

使用註解的 xml 文件配置:

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

    <!--    告知 spring 創建容器時要掃描的包-->
    <context:component-scan base-package="yh"/>

    <bean id="dbutils" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
        <constructor-arg name="ds" ref="dateScore"/>
    </bean>
    <bean id="dateScore" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="com.mysql.jdbc.Driver"/>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?SSl=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
        <property name="user" value="root"/>
        <property name="password" value="root"/>
    </bean>
</beans>

​ 這樣就完成配置了,但是 DBUtils 及 c3p0 的配置能不能也轉換成註解形式呢?答案是當然可以,這就要新引入一個配置類的概念。

​ 在項目中新建一個結構如下:

image-20200523120534361

並新建配置類如下:

/**
 * 該類是一個配置類它的作用和bean.xml是一樣的
 * spring中的新註解:
 *
 * Configuration 註解
 *      作用:指定當前類是一個配置類
 * ComponentScan 註解
 *      作用:指定Spring在創建容器時掃描配置的包
 *      屬性:value,指定要包
 *      效果同xml配置中的<context:component-scan base-package=""/>一樣
 * Bean 註解
 *      作用:用於把當前方法的返回值作為bean對象存入spring的IoC容器中
 *      屬性:
 *          name:用於指定bean的id,當不寫時,默認為方法名
 *      細節:
 *          當我們使用註解配置方法時,如果方法有參數,spring框架會去容器中查找有沒有可用的bean對象
 *          查找的方式和Autowired註解的作用一樣
 * Import 註解
 *      作用:用於導入其他配置的類
 *      屬性:
 *          value:用於指定其他配置類的位元組碼
 *          當我們使用Import的註解之後,使用Import註解的類就是父配置類,而導入的都是子配置類
 * @author YH
 * @create 2020-05-22 21:36
 */
@Configuration
@ComponentScan("yh")
@Import(JDBCConfig.class)
public class SpringConfiguration {
    /**
     * 用於創建一個QueryRunner對象
     * 細節:默認獲取的是單例的,但runner對象我們需要多例的,所以可加上scope
     * @param dataSource
     * @return
     */
    @Bean(name="runner")
    @Scope("prototype")
    public QueryRunner createQueryRunner(DataSource dataSource){
        return new QueryRunner(dataSource);
    }
}

配置jdbc的配置類:

/**
 * 註解方式獲取jdbc連接的配置類
 * PropertySource 註解
 *      作用:指定properties文件的位置
 *      屬性:
 *          value 註解:指定文件的名稱和路徑(properties文件的key)
 *              關鍵字:classpath,便是類路徑下
 * @author YH
 * @create 2020-05-23 9:47
 */
@PropertySource("classpath:data.properties")	//引入外部的properties屬性文件
public class JDBCConfig {
    @Value("${jdbc.driver}")
    private String driver;

    @Value("${jdbc.url}")
    private String url;

    @Value("${jdbc.username}")
    private String username;

    @Value("${jdbc.password}")
    private String password;

    /**
     * 創建數據源
     * @return
     */
    @Bean(name="dataSource")
    public DataSource createDataSource(){
        try {
            ComboPooledDataSource ds = new ComboPooledDataSource();
            ds.setDriverClass(driver);
            ds.setJdbcUrl(url);
            ds.setUser(username);
            ds.setPassword(password);
            return ds;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

properties 配置的資料庫參數:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf8
#不要用username作為key(獲取到了我程式的作者標記了)
jdbc.username=root
jdbc.password=root

​ 如上就是純註解的配置形式,配置類的作用同 bean.xml 一樣,所以相對的,也會有對應 xml 中配置的註解(往往能見名知意)。測試類原來是通過載入 xml 的方式也要變更為 Annotation 的,如下:

@Test
    public void testFindAll(){
//        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        //改為註解工廠
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        IAccountService service = context.getBean("accountService", IAccountService.class);
        List<Account> accounts = service.findAccounts();
        for (Account account : accounts) {
            System.out.println(account.toString());
        }
    }

​ 純註解的形式配置的工作量也不小,所以合理與 xml 搭配使用方能體現效率。

Qualifier 註解

可以使用在類或屬性上以及方法形參前,用於解決有多個同類型 bean 的自動注入問題,通過 @Qualifier() 指定 bean id 來確認哪個 bean 才是我們需要注入的(設置的 value 值需要與目標 bean id 名相同)

@Primary 註解也用於解決自動注入時多個相同類型 bean 的問題,它定義了首選項,除非另有說明,否則將優先使用與 @Primary 關聯的 bean。

Spring 整合 Junit

在上面的測試程式碼中都會有以下兩行程式碼:

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
IAccountService service = context.getBean("accountService", IAccountService.class);

這兩行程式碼的作用是獲取容器,如果不寫會提示空指針,所以不能輕易去掉。

​ 針對問題,我們需要 Spring 自動幫我們創建容器,我們就無序手動創建了,上面的問題也能解決。

​ 首先 Junit 實現底層是集成了 main 方法,它無法知曉我們是否使用了 Spring 框架,自然無可能幫我們創建容器,不過 junit 給我嗎暴露了一個註解,可以讓我們替換掉它的運行器。

​ 所以我們需要依賴 spring 框架,因為它提供了一個運行器,可以讀取配置文件(或註解)來創建容器。我們只需要告訴它配置文件的位置即可。

配置步驟

  1. 添加 junit 必備的 jar 包依賴

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    

    對於 Spring 5,需用 4.12 及以上 Junit jar 包

  2. 使用 @RunWith 註解替換原有運行器並使用@Autowired 給測試類中的變數注入數據

    /**
     * RunWith:替換原有運行器
     * @author YH
     * @create 2020-05-22 21:20
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    public class MyTest {
        //由spring自動注入業務層對象
        @Autowired
        IAccountService service;
        
        @Test
        public void testFindAll(){
            List<Account> accounts = service.findAccounts();
            for (Account account : accounts) {
                System.out.println(account.toString());
            }
        }
    }
    
  3. 使用 @ContextConfiguration 指定 Spring 配置文件的位置

    /**
     * RunWith:替換原有運行器
     * ContextConfiguration
     *   屬性:
     *       location屬性:用於指定配置文件的位置,如果是類路徑下,需要用classpath:表名
     *       classes屬性:用於指定註解的類,當不使用xml配置時,需要用此屬性指定註解類的位置
     * @author YH
     * @create 2020-05-22 21:20
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(classes={SpringConfiguration.class}) //{}表示支援設置多個配置類
    public class MyTest {
        @Autowired
        ApplicationContext context;
        @Autowired
        IAccountService service;
        @Test
        public void testFindAll(){
            List<Account> accounts = service.findAccounts();
            for (Account account : accounts) {
                System.out.println(account.toString());
            }
        }
    }
    
  4. 其中@Autowired 會給測試類中的變數注入數據

為什麼不把測試類配置到 xml 中

​ 首先,測試類配置到 xml 中肯定是可以實現的,但為什麼不這樣做?

原因:

  1. 當我們在 xml 中配置一個 bean ,spring 載入配置文件創建容器時,就會創建對象。

  2. 而測試僅僅起測試作用,在項目中它並不參與程式邏輯,也不會解決需求上的問題,所以創建完了,並沒有使用,那麼存在容器中就會造成資源的浪費。

    所以,基於以上兩點,我們不應該把測試類配置到 xml 中。

AOP

AOP(Aspect Oriented Programming)面向切面編程,通過預編譯的方式和運行期動態代理實現程式功能的統一維護的一種技術。將程式中重複的功能程式碼抽象出來,在需要執行的時候使用動態代理在不修改源碼的基礎上,對我們已有的方法進行增強。

從幾個知識面作為學習 AOP 的突破口

一個轉賬案例

修改上面的 CRUD 案例,首先原案例程式碼中的事務由 connection 對象的 setAutocommit(true) 而被自動控制。此方式控制事務,一次只執行一條 sql 語句,沒有問題,但執行多條 sql 就無法實現功能。原因是 sql 執行一次會獲取一次資料庫連接,統一 sql 語句的執行結果會被快取,後面執行會直接讀取快取;而多條 sql 執行就需要各自或許連接並執行,持久層方法都是獨立事務的,不符合事務的一致性,下面來探討一下。

持久層程式碼:

package yh.dao.impl;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import yh.dao.IAccountDao;
import yh.pojo.Account;

import java.sql.SQLException;
import java.util.List;

/**
 * @author YH
 * @create 2020-07-01 8:59
 */
public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    public void setRunner(QueryRunner runner){
        this.runner = runner;
    }
    @Override
    public Account findName(String name) {
        try {
            List<Account> accounts = runner.query("select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.isEmpty()){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("結果集不唯一,數據有問題");
            }
            return accounts.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void update(Account account) {
        try {
            runner.update("update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

業務層程式碼:

package yh.service.impl;

import yh.dao.impl.AccountDaoImpl;
import yh.pojo.Account;
import yh.service.IAccountService;

/**
 * @author YH
 * @create 2020-07-01 8:53
 */
public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        //根據賬戶資訊獲取賬戶對象
        Account source = accountDao.findName(sourceName);
        Account target = accountDao.findName(targetName);
        //轉出賬戶減錢,轉入賬戶加錢
        source.setMoney(source.getMoney() - money);
        target.setMoney(target.getMoney() + money);
        //提交更新
        accountDao.update(source);
        int i = 1/0;//模擬程式出錯
        accountDao.update(target);
    }
}

理想情況下,程式正常運行,轉賬結果正確

一旦出錯,前面執行後面的執行中斷,即轉出賬戶減錢了,而收款賬戶餘額未增加,且事務無法回滾(因為它們有各自的事務)

下面就是新增在業務層的轉賬方法,每個執行方法都獲取一次連接,都是獨立的事務,一旦中途出現中斷,就無法實現事務的回滾。

image-20200701150622969

解決辦法:

​ 使用 ThreadLocal 對象把 Connection 和當前執行緒綁定,從而使一個執行緒中只有一個能控制事務的對象,原來的事務是在持久層,現需將事務應用在業務層。

image-20200702105954209

持久層程式碼:

public class AccountDaoImpl implements IAccountDao {
    private QueryRunner runner;
    private ConnectionUtils connectionUtils;

    public void setRunner(QueryRunner runner){
        this.runner = runner;
    }
    public void setConnectionUtils (ConnectionUtils connectionUtils){
        this.connectionUtils = connectionUtils;
    }

    @Override
    public Account findName(String name) {
        try {
            //使用與執行緒綁定的連接
            List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from mybatis.account where name=?", new BeanListHandler<Account>(Account.class), name);
            if(accounts == null || accounts.isEmpty()){
                return null;
            }
            if (accounts.size() > 1){
                throw new RuntimeException("結果集不唯一,數據有問題");
            }
            return accounts.get(0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public void update(Account account) {
        try {
            //使用與執行緒綁定的連接
            runner.update(connectionUtils.getThreadConnection(),"update mybatis.account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

業務層程式碼:

public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;
    private TransactionManager transactionManager;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    public void setTransactionManager(TransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        //根據賬戶資訊獲取賬戶對象
        Account source = accountDao.findName(sourceName);
        Account target = accountDao.findName(targetName);
        try {//開啟事務
            transactionManager.beginTransaction();
            //轉出賬戶減錢,轉入賬戶加錢
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            //提交更新
            accountDao.update(source);
//            int i = 1 / 0;
            accountDao.update(target);

            //提交事務
            transactionManager.commit();
        } catch (Exception e){
            transactionManager.rollback();
            e.printStackTrace();
        }finally {
            //釋放執行緒並解綁連接
            transactionManager.release();
        }
    }
}

連接工具類程式碼:

package yh.utils;

import javax.sql.DataSource;
import java.sql.Connection;

/**
 * 連接的工具類
 * 從數據源中獲取連接,並實現和執行緒的綁定
 * @author YH
 * @create 2020-07-02 9:30
 */
public class ConnectionUtils {
    private ThreadLocal<Connection> tl = new ThreadLocal<>();
    private DataSource dataSource;

    public void setDataSource(DataSource dataSource){
        this.dataSource = dataSource;
    }

    /**
     * 獲取當前執行緒上的連接
     * @return
     */
    public Connection getThreadConnection(){
        try {
            //1.先從Threadlocal上獲取連接
            Connection conn = tl.get();
            //2.判斷當前執行緒上是否有連接
            if (conn == null) {
                //3.如果ThreadLocal上沒有連接,那麼從數據源獲取連接並存入ThreadLocal
                conn = dataSource.getConnection();
                tl.set(conn);
            }
            //4.返回當前執行緒連接
            return conn;
        } catch(Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 直接刪除連接,讓執行緒與連接解綁
     */
    public void removeConnection(){
        tl.remove();
    }
}

事務管理工具類的程式碼:

package yh.utils;

import java.sql.SQLException;

/**
 * 事務管理相關的工具類
 * 負責開啟事務、提交事務、回滾事務、釋放連接
 * @author YH
 * @create 2020-07-02 9:57
 */
public class TransactionManager {
    private ConnectionUtils connectionUtils;
    public void setConnectionUtils(ConnectionUtils connectionUtils){
        this.connectionUtils = connectionUtils;
    }

    /**
     * 開啟事務
     */
    public void  beginTransaction(){
        try {
            connectionUtils.getThreadConnection().setAutoCommit(false);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提交事務
     */
    public void commit(){
        try {
            connectionUtils.getThreadConnection().commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 回滾事務
     */
    public void rollback(){
        try {
            connectionUtils.getThreadConnection().rollback();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 釋放資源 並 解綁執行緒和連接
     * 默認情況下執行緒回收到執行緒池其上依舊綁定了已經會受到連接池的連接,
     * 即連接時關閉的,再次啟動執行緒時,能直接獲取到連接,但這個連接顯然
     * 無法使用,顧需在執行緒關閉後讓其與連接解綁
     */
    public void release(){
        try {
            //回收到執行緒池
            connectionUtils.getThreadConnection().close();
            connectionUtils.removeConnection();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

執行緒回收到執行緒池,而執行緒綁定的連接也會回到連接池,如果該執行緒在此運行,那麼此時獲取它的連接是可以獲取到的,但這個連接已經關閉回到連接池中,這樣顯然不行。所以在執行緒關閉前還需要做執行緒解綁操作。

解決事務問題後,發現我只是增加一個功能,就要對原有程式碼進行這麼大的改動,而且業務層和持久層對兩個工具類方法有很強的依賴,顯然這就是問題,有什麼解決辦法呢?

一個動態代理的案例

場景:有生產者(被代理類)與經銷商(代理方)。生產者可以售出產品,經銷商也可以銷售產品,但由經銷商銷售的產品經銷商從中收取百分之20的金額。即要對被代理的工廠增加代理的程式碼,使得代理經銷商能收益。如下:

基於介面的代理

image-20200702155111612

共同實現的介面:

/**
 * 定義一個代理類和被代理類共同要實現的介面
 * 從而實現基於介面的代理
 * @author YH
 * @create 2020-07-02 14:37
 */
public interface IProducer {
    /**
     * 銷售產品
     * @param money
     */
    public void saleProduct(float money);

    /**
     * 產品售後
     * @param money
     */
    public void afterProduct(float money);
}

生產者(被代理對象):

public class Producer implements IProducer {
    public void saleProduct(float money){
        System.out.println("銷售產品,並拿到錢:" + money);
    }
    public void afterProduct(float money){
        System.out.println("產品售後,並拿到錢:" + money);
    }
}

模擬消費(代理對象):

public class Client {
    public static void main(String[] args){
        //被代理對象(被內部類方法,需要聲明為不可變的)
        final Producer producer = new Producer();
        /**
         * 動態代理:
         *  特點:位元組碼隨意調用,隨用隨載入
         *  作用:不修改源碼的基礎上對方法增強
         *  分類:
         *      基於介面的動態代理
         *      基於子類的動態代理
         *  基於介面的動態代理:
         *      涉及的類:Proxy
         *      提供者:官方JDK
         *  如何創建代理對象:
         *      使用Proxy類的newProxyInstance方法
         *  創建代理對象的要求:
         *      被代理類至少實現一個介面,如果沒有則不能使用
         *  newProxyInstance方法的參數:
         *      lassLoader:類載入器。用於載入代理對象位元組碼的,和被代理對象使用相同的類載入器。固定寫法
         *      Class<?>[]:位元組碼數組。傳遞被代理對象實現的介面資訊,使得代理對象和被代理對象具有相同的方法。固定寫法
         *      InvocationHandler:用於提供增強的程式碼。用於說明如何代理(一般寫一些介面的實現類,通常是匿名內部類)
         */
        IProducer proxyProducer = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler(){
                    /**
                     * 執行被代理對象的任何方法都會經過這裡
                     * @param o 代理對象的引用
                     * @param method 當前執行的方法
                     * @param objects 當前執行方法所需的參數
                     * @return 和被代理對象有相同的返回值
                     * @throws Throwable
                     */
                    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                        //提供增強的程式碼
                        //1.獲取方法執行的參數
                        Float money = (Float)objects[0];
                        //2.判斷當前方法是不是銷售
                        if("saleProduct".equals(method.getName())) {
                            return method.invoke(producer, money * 0.8f);
                        }
                        return null;
                    }
                });
        //測試調用被代理類的方法
        proxyProducer.saleProduct(10000f);
    }
}

最終實現了,在經銷商處銷售的商品工廠只能拿到8000。

基於介面的代理方式有一個缺陷就是必須要實現一個介面,無法實現介面要怎麼辦呢,那就是實現動態代理的另一種方式:基於子類的動態代理

這種方式需要有第三方 jar 包: cglib 的支援

增加 pom.xml 文件依賴:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

模擬消費(代理類)

/**
 * 基於子類的動態代理
 * @author YH
 * @create 2020-07-02 16:53
 */
public class Client {
    public static void main(String[] args){
        final Producer producer = new Producer();
        /**
         *  基於介面的動態代理:
         *      涉及的類:Enhancer
         *      提供者:第三方cglib
         *  如何創建代理對象:
         *      使用Enhancer類的create()方法
         *  創建代理對象的要求:
         *      被代理類不能是最終類
         *  create()方法的參數:
         *      class:位元組碼。用於指定被代理對象的位元組碼
         *      Callback:用於提供增強的程式碼。即如何代理,一般用該介面的子類介面的實現類 MethodInterceptor
         */
        Producer cglibProduct = (Producer   ) Enhancer.create(producer.getClass(), new MethodInterceptor() {
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                //提供增強的程式碼
                //1.獲取方法執行的參數
                Float money = (Float)objects[0];
                //2.判斷當前方法是不是銷售
                if("saleProduct".equals(method.getName())) {
                    return method.invoke(producer, money * 0.8f);
                }
                return null;
            }
        });
        //測試調用方法
        cglibProduct.saleProduct(10000f);
    }
}

生產者(被代理類)

public class Producer implements IProducer {
    public void saleProduct(float money){
        System.out.println("銷售產品,並拿到錢:" + money);
    }
    public void afterProduct(float money){
        System.out.println("產品售後,並拿到錢:" + money);
    }
}

結果相同:

image-20200702170930894

結合動態代理修改轉賬案例

image-20200703103354790

持久層程式碼不變

業務層程式碼(被代理對象):

public class AccountServiceImpl implements IAccountService {
    private AccountDaoImpl accountDao;

    public void setAccountDao(AccountDaoImpl accountDao){
        this.accountDao = accountDao;
    }
    @Override
    public void transfer(String sourceName, String targetName, Float money) {
        try {
            //根據賬戶資訊獲取賬戶對象
        	Account source = accountDao.findName(sourceName);
        	Account target = accountDao.findName(targetName);
            //轉出賬戶減錢,轉入賬戶加錢
            source.setMoney(source.getMoney() - money);
            target.setMoney(target.getMoney() + money);
            //提交更新
            accountDao.update(source);
//            int i = 1 / 0;
            accountDao.update(target);
        } catch (Exception e){
            //改為運行時異常,將異常拋給調用者(代理類)來處理,否則調用處後的回滾操作無法執行
            // (當然被代理類中也可以不捕獲異常,代理類捕獲)
            throw new RuntimeException(e);
        }
    }
}

代理工廠:

package yh.factory;

import yh.service.IAccountService;
import yh.utils.TransactionManager;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 用於創建Service的代理對象工廠
 * @author YH
 * @create 2020-07-03 8:55
 */
public class BeanFactory {
    private IAccountService accountService;
    private TransactionManager transactionManager;

    public void setAccountService(IAccountService accountService){
        this.accountService = accountService;
    }
    public void setTransactionManager(TransactionManager transactionManager){
        this.transactionManager = transactionManager;
    }

    public IAccountService getAccountService(){
        return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 獲取AccountService的代理對象
                     * @param proxy
                     * @param method
                     * @param args
                     * @return
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object returnValue = null;
                        try {
                            //1.開啟事務
                            transactionManager.beginTransaction();
                            //2.執行操作
                            returnValue = method.invoke(accountService, args);
                            //3.提交事務
                            transactionManager.commit();
                            //4.返回被代理對象
                            return returnValue;
                        } catch(Exception e){
                            //5.回滾
                            transactionManager.rollback();
                            throw new RuntimeException(e);
                        } finally {
                            //6.釋放資源
                            transactionManager.release();
                        }
                    }
                });
    }
}

被代理對象實現了一個介面,顧使用了基於介面的動態代理方式。

至此,無論業務層中有多少個方法,都會由代理類為其增加事務管理,而不是每個單獨都要設置,在不增加業務類程式碼的情況下實現了功能的增強!

Spring AOP

使用 AOP 就可以通過配置的方式實現上面案例的功能,這也是通過案例引入 AOP 的原因。

  • AOP 相關術語

    • Joinpoint(連接點):

      指那些被攔截到的點。在 Spring 中,這些點指的是方法,因為 Spring 只支援方法類型的連接點

    • Pointcut(切點):

      切點的定義會匹配通知所要織入的一個或多個連接點,即定義攔截規則(通常使用明確的類和方法名稱,可配合正則表達式使用)

    • Advice(通知/增強):

      攔截到 Joinpoint 之後要做的事情(新增的功能)

      通知的類型:前置通知、後置通知、異常通知、最終通知、環繞通知。對應到案例中如下:

      image-20200704070309875

    • Introduction(引入):

      一種特殊的通知。在不修改類程式碼的前提下,Introduction 可以在運行期為類動態地添加一些方法或屬性

    • Target(目標對象):

      代理的目標對象

    • Weaving(織入):

      把增強應用到目標對象並創建新的代理對象的過程。

      Spring 採用動態代理織入(運行期);AspectJ 採用編譯器織入和類裝載期織入。

    • Proxy(代理):

      一個類被 AOP 織入增強後,就會產生一個結果代理類

    • Aspect(切面):

      切點和通知的結合

    小結

    ​ 通知包含了需要應用於多個對象的橫切行為;連接點是程式執行過程中能夠應用通知的所有點;切點定義了通知被應用的具體位置,即哪些連接點(方法),且定義了哪些連接點會得到通知。

  • 注意

    • 開發階段(我們做的)

      • 核心業務程式碼,即開發主線由我們自己完成,熟悉也無需求;
      • 抽取出共用程式碼,製作成 aop 通知,開發階段後最後再做。並在配置文件中聲明切入點與通知間的關係,即切面。
    • 運行階段(Spring 框架做的)

      • Spring 監控奇瑞乳墊方法的執行。一旦監控切入點方法執行,便使用代理機制,動態創建目標對象的代理對象。根據通知類別,在代理對象的對應位置,織入通知對應的功能,完成完整的程式碼邏輯運行。

    Spring 會根據目標類是否實現了介面來決定採用哪種動態代理方式。

    動態代理中用到的 invoke() 方法有攔截功能。

基於 xml 配置的 AOP 示例

image-20200704103124481

添加依賴

<!--用於解析Spring表達式-->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

bean.xml 配置

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

<!--    ioc配置,將Service配置進來-->
    <bean id="accountService" class="yh.service.impl.AccountServiceImpl"/>


<!--    aop配置
    1.把通知bean也交給spring管理
    2.使用aop:config標籤標示開始aop配置
    3.使用aop:aspect變遷配置切面
        id:給切面定義唯一的表示
        ref:指定切面的通知類bean的id
        4.內部標籤中配置通知類型(前置通知為例)
            使用aop:before表示配置前置通知
                method:指定通知列中哪個方法用於通知
                pointcut:指定切入點表達式,表示對業務層中哪些方法進行增強
         切入點表達式寫法:
            關鍵字:execution(表達式)
            標準寫法 execution(public void 全類名.方法名(參數列表))
            其中許可權修飾符可以省略,返回值類型、全類名、方法名、形參列表都可以用通配符代替
            全統配寫法:* *..*.*(..)    多個包用 .. 表示一個包及其子包,形參列表.. 表示無參或多參
            但實際開發中只會對業務層的實現類方法進行統配,寫法:* 業務層包路徑.*.*(..)
-->
    <!-- 配置Logger類-->
    <bean id="logger" class="yh.utils.Logger"/>
    <!--配置aop-->
    <aop:config>
        <!--配置切面-->
        <aop:aspect id="loggerAdvice" ref="logger">
            <!--配置通知類型且定義通知方法和切入點方法的關聯-->
            <aop:before method="printLog" pointcut="execution(public void yh.service.impl.AccountServiceImpl.saveAccount())"/>
        </aop:aspect>
    </aop:config>
</beans>

注意添加 aop 命名空間和約束

業務層介面

public interface IAccountService {
    /**
     * 模擬保存賬戶
     */
    void saveAccount();

    /**
     * 模式更新賬戶
     * @param i
     */
    void updateAccount(int i);

    /**
     * 模擬刪除賬戶
     * @return
     */
    int deleteAccount();
}

業務層實現類

public class AccountServiceImpl implements IAccountService {
    public void saveAccount() {
        System.out.println("save account!");
    }

    public void updateAccount(int i) {
        System.out.println("update account!");
    }

    public int deleteAccount() {
        System.out.println("delete account!");
        return 1;
    }
}

通知類

public class Logger {
    /**
     * 輸出日誌:計劃讓其在切入點之前執行(即前置通知,在匹配的業務層方法前執行)
     */
    public void printLog(){
        System.out.println("輸出日誌...");
    }
}

測試

@Test
public void aopTest(){
    //1.獲取容器
    ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
    //2.獲取bean
    IAccountService as = (IAccountService)context.getBean("accountService");
    //3.執行方法
    as.saveAccount();
    //spring表達式所匹配的連接點方法才會被應用通知
    as.updateAccount(1);
    as.deleteAccount();
}

結果:image-20200704110858678

增加對應的方法,對四種通知進行配置:

<aop:config>
    <!--提取共用的表達式,供通知引用-->
    <aop:pointcut id="ref" expression="execution(* yh.service.impl.*.*(..))"/>
    <!--配置切面-->
    <aop:aspect id="loggerAdvice" ref="logger">
        <!--前置通知:切入點方法執行前通知-->
        <aop:before method="beforeAdvice" pointcut-ref="ref"/>

        <!--後置通知:切入點方法執行後通知-->
        <aop:after-returning method="afterAdvice" pointcut-ref="ref"/>

        <!--異常通知:切入點方法拋出異常時通知-->
        <aop:after-throwing method="exceptionAdvice" pointcut-ref="ref"/>

        <!--最終通知:無論切入點方法是否正常執行,都會執行-->
        <aop:after method="finallyAdvice" pointcut-ref="ref"/>
    </aop:aspect>
</aop:config>

image-20200704113819901

環繞通知配置

使用所編寫的邏輯將被通知的目標方法完全包裝起來(類似前面的動態代理對方法的增強),實現了一個方法中同時編寫各類通知。

bean.xml中配置環繞通知

<!--配置環繞通知-->
<aop:around method="aroundAdvice" pointcut-ref="ref"/>

通知類中定義環繞通知的方法:

/**
 * 環繞通知
 * Spring提供了一個介面:ProceedingJoinPoint,改介面有一個 proceed() 方法,用於明確切入點方法
 * 改介面可作為環繞通知方法的參數使用,由Spring創建
 * 通過環繞通知我們可以手動控制被增強方法在通知中執行的位置
 */
public Object aroundAdvice(ProceedingJoinPoint pjp){
    Object returnValue = null;

    try {
        System.out.println("我是前置通知");

        //得到執行方法所需的參數
        Object[] args = pjp.getArgs();
        //執行切入點(業務類)方法
        returnValue = pjp.proceed(args);

        System.out.println("我是後置通知");
    } catch (Throwable throwable) {
        System.out.println("我是異常通知");
        throwable.printStackTrace();
    } finally {
        System.out.println("我是最終通知");
    }
    return returnValue;
}

類似代理類環繞增強被代理類,但明顯更加簡便明了,大多數事情被 spring 完成了,我們可以在被通知方法執行前後定義想要增加的功能,從而實現各類通知,結果如下:

image-20200704121351204

基於註解的配置

業務類要加上 @Service("accountService") 讓 Spring 容器管理並指定標識 id

通知類

/**
 * 記錄日誌的工具類,定義通知的共用程式碼
 * @author YH
 * @create 2020-07-04 7:27
 * Component註解,指示Spring容器將創建管理當前類對象
 *      value:用於指定 bean 的 id。默認值為當前類名首字母小寫
 *      (三層有各自的註解,但功能一樣,是Component的子類)
 *  Aspect註解:表示當前類是一個切面
 */
@Component("logger")
@Aspect
public class Logger {
    /**
     * 通過註解定義可重用切點表達式,供通註解知引用
     */
    @Pointcut("execution(* yh.service.impl.*.*(..))")
    public void spe(){}

    /**
     * 前置通知
     */
    @Before("spe()")
    public void beforeAdvice(){
        System.out.println("前置通知...");
    }
    /**
     * 後置通知
     */
    @AfterReturning("spe()")
    public void afterAdvice(){
        System.out.println("後置通知...");
    }
    /**
     * 異常通知
     */
    @AfterThrowing("spe()")
    public void exceptionAdvice(){
        System.out.println("異常通知...");
    }
    /**
     * 最終通知
     */
    @After("spe()")
    public void finallyAdvice(){
        System.out.println("最終通知...");
    }

    /**
     * 環繞通知
     * Spring提供了一個介面:ProceedingJoinPoint,改介面有一個 proceed() 方法,用於明確切入點方法
     * 改介面可作為環繞通知方法的參數使用,由Spring創建
     * 通過環繞通知我們可以手動控制被增強方法在通知中執行的位置
     */
    @Around("spe()")
    public Object aroundAdvice(ProceedingJoinPoint pjp){
        Object returnValue = null;

        try {
            System.out.println("我是前置通知");

            //得到執行方法所需的參數
            Object[] args = pjp.getArgs();
            //執行切入點(業務類)方法
            returnValue = pjp.proceed(args);

            System.out.println("我是後置通知");
        } catch (Throwable throwable) {
            System.out.println("我是異常通知");
            throwable.printStackTrace();
        } finally {
            System.out.println("我是最終通知");
        }
        return returnValue;
    }
}

bean.xml

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

<!--    配置Spring創建容器時要掃描的包-->
    <context:component-scan base-package="yh"/>

<!--    開啟註解aop的支援-->
    <aop:aspectj-autoproxy/>
</beans>

使用註解,命名空間和約束都需要設置

純註解獲取 Spring 容器方式與通過 xml 配合不一樣,如下:

先定義一個 java 配置類:

@Configuration
@ComponentScan("yh") //指定掃描的包
@EnableAspectJAutoProxy //開啟基於註解AOP的支援
public class SpringConfiguration {
}
/**
 * 測試純註解配置
 */
@Test
public void annotationAopTest2(){
    ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    IAccountService service = (IAccountService)context.getBean("accountService");
    service.saveAccount();
}

基於註解配置通知時,建議應用於環繞通知。其他通知的順序可能不是想要的結果(如後置通知在最終通知之前執行)

改造轉賬案例

  • 基於 XML 配置

    改動幾乎都在 bean.xml 文件中:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="//www.springframework.org/schema/beans"
           xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="//www.springframework.org/schema/aop"
           xmlns:context="//www.springframework.org/schema/context"
           xsi:schemaLocation="//www.springframework.org/schema/beans
            //www.springframework.org/schema/beans/spring-beans.xsd
            //www.springframework.org/schema/aop
            //www.springframework.org/schema/aop/spring-aop.xsd //www.springframework.org/schema/context //www.springframework.org/schema/context/spring-context.xsd">
    
    <!--配置service-->
        <bean id="accountService" class="yh.service.impl.AccountServiceImpl">
            <property name="accountDao" ref="accountDao"/>
        </bean>
    
    <!--配置dao-->
        <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
            <property name="runner" ref="runner"/>
            <property name="connectionUtils" ref="connectionUtils"/>
        </bean>
    <!--    配置QueryRunner-->
        <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
            <constructor-arg ref="dataSource"/>
        </bean>
    
    <!--配置數據源-->
        <!-- 讀取數據源文件的位置-->
        <context:property-placeholder location="classpath:jdbc.properties"/>
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${driverClassName}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
    
        <!--    配置事務管理工具類-->
        <bean id="transactionManager" class="yh.utils.TransactionManager">
            <property name="connectionUtils" ref="connectionUtils"/>
        </bean>
        <!--    配置連接工具類-->
        <bean id="connectionUtils" class="yh.utils.ConnectionUtils">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
    <!--    AOP配置-->
        <aop:config>
            <!--提取共用的表達式,供通知引用-->
            <aop:pointcut id="pe1" expression="execution(* yh.service.impl.AccountServiceImpl.transfer(..))"/>
            <!--配置切面-->
            <aop:aspect id="tr" ref="transactionManager">
                <!--確定通知類型,定義通知方法和切入點方法的關聯-->
                <aop:before method="beginTransaction" pointcut-ref="pe1"/>
                <aop:after-throwing method="rollback" pointcut-ref="pe1"/>
                <aop:after-returning method="commit" pointcut-ref="pe1"/>
                <aop:after method="release" pointcut-ref="pe1"/>
    
            </aop:aspect>
        </aop:config>
    
    </beans>
    
  • 純註解配置

    基於註解配置中,由於 Spring 原因,最終通知(@After)和後置通知(@AfterReturning)或異常通知(@AfterThrowing)的執行順序無法控制,所以使用環繞通知:

    image-20200705151342804

    持久層、業務層等工具列類只用加上組件註解(@Component 註解之類)以及其成員屬性的注入註解(@Autowired 註解)即可

    SpringConfiguration 配置類:

    @Configuration	//表名此類為配置類
    @EnableAspectJAutoProxy	//開啟Spring AOP支援
    @ComponentScan("yh")	//指定spring創建容器要掃描的包
    @Import(JdbcConfig.class)	//導入子配置類
    public class SpringConfiguration {
        @Bean(name = "runner")	//將方法的返回值創建為bean 並存入Spring容器中
        public QueryRunner createQueryRunner(DataSource dataSource){//形參可自動注入
            return new QueryRunner(dataSource);
        }
    }
    

    JDBC配置類:

    @PropertySource("classpath:jdbc.properties")	//引入外部properties屬性文件
    public class JdbcConfig {
        //@Value是@PropertySource的屬性註解,用於讀取配置文件中的key-value
        @Value("${driverClassName}")
        private String driver;
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        @Bean(name="dataSource")
        public DataSource createDataSource(){
            DruidDataSource ds = new DruidDataSource();
            ds.setDriverClassName(driver);
            ds.setUrl(url);
            ds.setUsername(username);
            ds.setPassword(password);
            return ds;
        }
    }
    

    通知類:

    /**
     * 事務管理相關的工具類
     * 負責開啟事務、提交事務、回滾事務、釋放連接
     * @author YH
     * @create 2020-07-02 9:57
     */
    @Component("txManager")
    @Aspect	//指示此類是切面
    public class TransactionManager {
        @Autowired
        private ConnectionUtils connectionUtils;
    
        @Pointcut("execution(* yh.service.impl.*.*(..))")
        public void spe(){}
    
        /**
         * 開啟事務
         */
        public void  beginTransaction(){
            try {
                connectionUtils.getThreadConnection().setAutoCommit(false);
                System.out.println("開啟事務");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 提交事務
         */
        public void commit(){
            try {
                connectionUtils.getThreadConnection().commit();
                System.out.println("提交事務");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 回滾事務
         */
        public void rollback(){
            try {
                connectionUtils.getThreadConnection().rollback();
                System.out.println("回滾事務");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 釋放資源 並 解綁執行緒和連接
         * 默認情況下執行緒回收到執行緒池其上依舊綁定了已經會受到連接池的連接,
         * 即連接時關閉的,再次啟動執行緒時,能直接獲取到連接,但這個連接顯然
         * 無法使用,顧需在執行緒關閉後讓其與連接解綁
         */
        public void release(){
            try {
                //回收到執行緒池
                connectionUtils.getThreadConnection().close();
                connectionUtils.removeConnection();
                System.out.println("關閉資源");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
        * 環繞通知,配置註解通知建議只使用環繞通知
        */
        @Around("spe()")
        public Object aroundAdvice(ProceedingJoinPoint pjp){
            Object returnValue = null;
    
            try {
                this.beginTransaction();
    
                Object[] args = pjp.getArgs();
                returnValue = pjp.proceed(args);
    
                this.commit();
            } catch (Throwable throwable) {
                this.rollback();
                throwable.printStackTrace();
            } finally {
                this.release();
            }
            return returnValue;
        }
    }
    

    properties屬性文件:

    driverClassName=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&amp;useUnicode=true&amp;characterEncoding=utf8
    jdbc.username=root
    jdbc.password=root
    

    xml 引入外部屬性文件的兩種方式:

     <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
     	<property name="location" value="classpath:jdbc.properties"/>
     </bean>
    
    <context:property-placeholder location="classpath:jdbc.properties"/>
    

Spring 中的 JdbcTemplate

概述

Spring 框架提供了很多的操作模板類

  • 操作關係型數據
    • JdbcTemplate
    • HibernateTemplate
  • 操作 nosql 資料庫
    • RedisTemplate
  • 操作消息隊列
    • JmsTemplate

應用

關鍵依賴

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

<!-- //mvnrepository.com/artifact/org.springframework/spring-tx -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

基本配置

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

<!--    配置JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

<!--    配置數據源-->
    <!--引入外部屬性文件-->
    <context:property-placeholder location="data.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

data.properties

driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

簡單的 CRUD

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import yh.domain.Account;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * JdbcTemplate的簡單用法
 * @author YH
 * @create 2020-07-05 17:25
 */
public class JdbcTemplate1 {
    public static void main(String[] args){
        ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
        JdbcTemplate jt = (JdbcTemplate)context.getBean("jdbcTemplate");
        //保存
          jt.update("insert into mybatis.account(name,money) values(?,?)","zzz",2000);
        //修改
          jt.update("update mybatis.account set money=money+? where name=?",99,"aaa");
        //刪除
          jt.update("delete from mybatis.account where id=?",7);
        //查詢所有
        List<Account> accountList = jt.query("select * from mybatis.account", new AccountRowMapper());
        for(Account a : accountList){
            System.out.println(a);
        }
        //查詢一個
        List<Account> accountList = jt.query("select * from mybatis.account where id=?",
                new AccountRowMapper(),3);
        System.out.println(accountList.isEmpty() ? "沒有結果" : accountList.get(0));

        //查詢返回一行一列,常用於分頁中獲取總記錄數
        Integer total = jt.queryForObject("select count(*) from mybatis.account where id>?",
                Integer.class, 1);
        System.out.println(total);
    }

    /**
     * 處理查詢結果集的封裝
     */
    static class AccountRowMapper implements RowMapper<Account> {
        /**
         * @param resultSet 查詢sql返回的結果集
         * @param i 所查詢表的行數
         */
        @Override
        public Account mapRow(ResultSet resultSet, int i) throws SQLException {
            Account account = new Account();
            account.setId(resultSet.getInt("id"));
            account.setName(resultSet.getString("name"));
            account.setMoney(resultSet.getFloat("money"));

            return account;
        }
    }

在 Dao 中使用

dao 中使用 JdbcTemplate 有兩種方式,普通做法,在 dao 中增加一個 JdbcTemplate 引用屬性,交由 spring 注入,而後進行 update()、query() 調用。但當有多個 dao 時,每個 dao 內都要重複定義程式碼:private JdbcTemplate jdbcTemplate;

第二種方式:使用 Spring 提供的 JdbcDaoSupport 抽象類,其內部封裝了 JdbcTemplate 屬性,只需給予一個 DataSource 給它就可以獲取 JdbcTemplate 對象,讓我們的 dao 繼承它就可以獲取屬性以及注入 DataSource:

image-20200706051500764

持久層介面:

public interface IAccountDao {
    /**
     * 通過Id查賬戶
     * @param id
     * @return
     */
    public Account findAccountById(Integer id);

    /**
     * 通過Id查賬戶
     * @param name
     * @return
     */
    public Account findAccountByName(String name);

    /**
     * 修改賬戶
     * @param account
     */
    public void updateAccount(Account account);
}

持久層實現類:

public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
    /*註:繼承父類所獲得的屬性可進行注入,數據源就是通過此特性注入(見bean.xml)*/

    @Override
    public Account findAccountById(Integer id) {
        JdbcTemplate jt = getJdbcTemplate();
        List<Account> list = jt.query("select * from mybatis.account where id=?",
                new AccountRowMapper(), id);
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public Account findAccountByName(String name) {
        JdbcTemplate jt = getJdbcTemplate();
        List<Account> list = jt.query("select * from mybatis.account where name=?",
                new AccountRowMapper(), name);
        if(list.size() > 1){
            throw new RuntimeException("結果集不唯一,查詢的對象有多個");
        }
        return list.isEmpty() ? null : list.get(0);
    }

    @Override
    public void updateAccount(Account account) {
        JdbcTemplate jt = getJdbcTemplate();
        jt.update("update mybatis.account set name=?,money=? where id=?",
                account.getName(),account.getMoney(),account.getId());
    }

封裝查詢結果集的工具類:

public class AccountRowMapper implements RowMapper<Account> {
    @Override
    public Account mapRow(ResultSet resultSet, int i) throws SQLException {
        Account account = new Account();
        account.setId(resultSet.getInt("id"));
        account.setName(resultSet.getString("name"));
        account.setMoney(resultSet.getFloat("money"));
        return account;
    }
}

bean.xml

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

<!--    配置dao-->
    <bean id="accountDao" class="yh.dao.impl.AccountDaoImpl">
        <!--給所繼承的父類的屬性注入值-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

<!--    配置數據源-->
    <!--引入外部屬性文件-->
    <context:property-placeholder location="data.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${driverClassName}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

data.properties

driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis?ssl=true&useUnicode=true&characterEncoding=utf8
jdbc.username=root
jdbc.password=root

注意:第一種方式可以使用註解或者 xml 配置;但第二種方式只能用 xml 配置

Spring 中的事務控制

JavaEE 體系進行分層開發,事務處理位於業務層,Spring 提供了分層設計業務層的事務處理解決方案。Spring 提供了一組基於 AOP 的事務控制介面 ,可以通過編程或配置方式實現。

  • PlatformTransactionManager 介面提供了三個方法:
//獲取事務狀態資訊
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事務
void commit(TransactionStatus var1) throws TransactionException;
//回滾事務
void rollback(TransactionStatus var1) throws TransactionException;

開發中使用的是它額實現類對象對事務進行管理:

//使用 Spring JDBC 或 iBatis 進行持久化數據時使用 
org.springframework.jdbc.datasource.DataSourceTransactionManager
    
//使用 Hibernate 版本進行持久化數據時使用
org.springframework.orm.hibernate5.HibernateTransactionManager
  • 事務的定義資訊對象 TransactionDefinition:
//獲取事務對象的名稱
String getName();
//獲取事務隔離級別
int getIsolationLevel();
//獲取事務傳播行為
int getPropagationBehavior();
//獲取事務超時時間
int getTimeout();
//獲取事務是否只讀
boolean isReadOnly();

讀寫型事務:增加、刪除、修改開啟事務;

只讀型事務:執行查詢時,也會開啟事務。

  1. 事務隔離級別

    事務隔離級別反應了事務提交並發訪問時的處理態度

    • ISOLATION_DEFAULT:默認級別,歸屬下列某一類
    • ISOLATION_READ_UNCOMMITTED:可以讀取未提交數據
    • ISOLATION_READ_COMMITTED:只能讀取已提交數據,解決臟讀問題(Oracle 默認級別)
    • ISOLATION_REPEATABLE_READ:是否讀取其他事務提交修改後的數據,解決不可重複讀取問題(MySQL默認級別)
    • ISOLATION_SERIALIZABLE:是否讀取其他事務提交添加後的數據,解決幻影讀問題
  2. 事務的傳播行為

    REQUIRED:如果當前沒有事務,就新建一個事務;如果已經存在一個事務,加入到這個事務中(默認值)

    SUPPORTS:支援當前事務,如果當前沒有事務,就以非事務方法執行(沒有事務)

    MANDATORY:使用當前的事務,如果當前沒有事務,就拋出異常

    REQUERS_NEW:新建事務,如果當前在事務中,把當前事務掛起。

    NOT_SUPPORTED:以非事務方式執行操作,如果當前存在事務,就把當前事務掛起

    NEVER:以非事務方式運行,如果當前存在事務,拋出異常

    NESTED:如果當前存在事務,則在嵌套事務內執行。如果當前沒有事務,則執行 REQUIRED 類似的操作

  3. 超時時間

    默認值是 -1,沒有超時限制;如需有,以秒為單位進行設置

  4. 是否是只讀事務

    建議查詢時設置為只讀

  • TransactionStatus 介面

    /**
    * TransactionStatus介面描述了某個時間點上事務對象的狀態資訊,包含有6個具體的操作
    */
    //刷新事務
    void flush();
    //獲取是否存在存儲點
    boolean hasSavepoint();
    //獲取事務是否完成
    boolean isCompleted();
    //獲取事務是否為新的事務
    boolean isNewTransaction();
    //獲取事務是否回滾
    boolean isRollbackOnly();
    //設置事務回滾
    void setRollbackOnly();
    

基於 XML 的聲明式事務控制(配置方式)

image-20200706133549093

環境搭建

  • 必備依賴:spring-jdbc-xxx 和 spring-tx-xxx 等

  • 創建 spring 的配置文件並導入約束

  • 準備資料庫表和實體類

  • 編寫業務層介面和實現類

  • 編寫 Dao 介面和實現類

    以上按照項目需求編寫,關鍵是配置,個人理解是上面所寫的 AOP 事務的更強形式

  • 編寫 bean.xml 配置

    • 各層級配置
    • 事務管理器配置
    <!--配置一個事務管理器-->
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <!--注入數據源-->
            <property name="dataSource" ref="dataSource"/>
        </bean>
    <!--配置事務-->
        <!--配置事務的通知並引用事務管理器(用於管理事務)-->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <!--配置事務的屬性-->
            <tx:attributes>
                <!--指定方法名稱:是核心業務方法
                 read-only:是否只讀事務,默認false
                 isolation:指定隔離級別,默認值是使用的資料庫默認隔離級別
                 propagation:指定事務的傳播行為
                 timeout:指定超時時間,默認值-1表示永不超時
                 rollback-for:指定會進行回滾的異常類型,未指定表示任何異常都回滾
                 no-rollback-for:指定不進行回滾的異常類型,未指定表示任何異常都回滾
                -->
                <tx:method name="*" read-only="false" propagation="REQUIRED"/>
                <tx:method name="find*" read-only="true" propagation="SUPPORTS"/>
            </tx:attributes>
        </tx:advice>
    
    <!--aop切點表達式-->
        <aop:config>
            <!--配置切入點表達式-->
            <aop:pointcut id="pt1" expression="execution(* yh.service.impl.*.*(..))"/>
            <!--配置切入點表達和事務通知的關係-->
            <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"/>
        </aop:config>
    

    對比前面指定通知位置或者使用環繞通知都或多或少需要手動去處理代理邏輯,從而控制控制事務的方法的執行順序。

    而使用 Spring 事務控制器,配置一個事務通知後,我們只需關聯切入點表達式和事務通知即可。

基於註解的配置方式

image-20200706170145152

  • 必備依賴:spring-jdbc-xxx 和 spring-tx-xxx 等

  • 創建 spring 的配置文件並導入約束

  • 準備資料庫表和實體類

  • 創建業務層介面及其實現類,並使用符合語義的註解讓 spring 進行管理

  • 創建持久層介面及其實現類,並使用符合語義的註解讓 spring 進行管理

  • 配置步驟

    • 總 JavaConfig 類

      @Configuration
      @Import(value={jdbcConfig.class,JdbcTemplateConfig.class,TransactionManager.class})
      @ComponentScan("yh")	//創建spring容器時掃描的包
      @EnableTransactionManagement    //開啟基於註解的事務管理功能(與開啟aop支援不要混淆)
      public class SpringConfiguration {
      }
      
    • 創建事務管理器配置類並注入數據源

      public class TransactionManager {
          @Bean(name="txManager")
          public PlatformTransactionManager createTxManager(DataSource dataSource){
              return new DataSourceTransactionManager(dataSource);
          }
      }
      
    • 數據源、JdbcTemplate 的 JavaConfig:

      @PropertySource("classpath:jdbc.properties")	//引入外部的properties屬性文件
      public class jdbcConfig {
          @Value("${driverClassName}")
          private String driver;
      
          @Value("${jdbc.url}")
          private String url;
      
          @Value("${jdbc.username}")
          private String username;
      
          @Value("${jdbc.password}")
          private String password;
      
          @Bean(name="dataSource")
          public DataSource createDataSource(){
              DruidDataSource ds = new DruidDataSource();
              ds.setDriverClassName(driver);
              ds.setUrl(url);
              ds.setUsername(username);
              ds.setPassword(password);
              return ds;
          }
      }
      

      jdbc.properties 屬性文件:

      driverClassName=com.mysql.jdbc.Driver
      jdbc.url=jdbc:mysql://localhost:3306?ssl=true&useUnicode=true&characterEncoding=utf8
      jdbc.username=root
      jdbc.password=root
      
      public class JdbcTemplateConfig {
          @Bean(name="jdbcTemplate")
          public JdbcTemplate ceeateJdbcTemplate(DataSource dataSource){
              return new JdbcTemplate(dataSource);
          }
      }
      
    • 在業務層使用 @Transactional 註解

      @Service("accountService")
      @Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
      public class AccountServiceImpl implements IAccountService {
          /**
           * 獲取dao對象
           */
          @Autowired
          private IAccountDao accountDao;
      
          /**
          * 轉賬方法
          * Transactional註解與<tx:Advice/>標籤含義相同,配置事務通知
       	* 可用在介面、類、方法上,表示其支援事務
       	* 三個位置的優先順序 方法>類>介面
       	*/
          @Override
          @Transactional(readOnly = false,propagation = Propagation.REQUIRED)
          public void transferAccount(String sourceName, String targetName, float money) {
              //獲取賬戶
              Account source = accountDao.findByName(sourceName);
              Account target = accountDao.findByName(targetName);
              //修改賬戶金額
              source.setMoney(source.getMoney()-money);
              target.setMoney(target.getMoney()+money);
              //將修改後的賬戶更新至資料庫
              accountDao.updateAccount(source);
      //        int i = 1/0;//模擬異常
              accountDao.updateAccount(target);
          }
      }
      
  • 測試

    @RunWith(SpringJUnit4ClassRunner.class)	//替換原有運行器
    @ContextConfiguration(classes = SpringConfiguration.class) //指定容器配置來源
    public class MyTest {
        @Autowired
        private IAccountService service;
        @Test
        public void test1(){
            service.transferAccount("aaa","ccc",100);
        }
    }
    

    基於純註解配置以上。

    使用 Spring 事務管理,業務程式碼全稱躺著任由擺布,各層級沒有程式碼侵入問題。