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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://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="http://www.springframework.org/schema/p"
xmlns:c="http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://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="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">

<!--    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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       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
        http://www.springframework.org/schema/context
        http://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="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           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 http://www.springframework.org/schema/context https://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>

<!-- https://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://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="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://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 事務管理,業務代碼全稱躺著任由擺布,各層級沒有代碼侵入問題。

posted @ 2020-07-06 20:07  "無問西東"  閱讀(...)  評論(...編輯  收藏
色网站直播