Spring

EUNEIR發表於2024-03-13

概述

分析以下程式存在的問題:

image-20230625154909502

Web層:

public class UserAction {
    private UserService userService = new UserServiceImpl();

    public void deleteRequest(){
        userService.deleteUser();
    }
}

Service層:

public interface UserService {
    void deleteUser();
}
public class UserServiceImpl implements UserService {
    private UserDao userDao = new UserDaoImplForMySQL();
    @Override
    public void deleteUser() {
        userDao.deleteById();
    }
}

Dao層:

public interface UserDao {
    void deleteById();
}
public class UserDaoImplForMySQL implements UserDao {
    @Override
    public void deleteById() {
        System.out.println("MySQL delete user data");
    }
}

層與層間以介面銜接,程式可以正常執行

假設此時要改成Oracle資料庫,就需要再提供一個UserDao的實現類UserDaoImplForOracle

public class UserDaoImplForOracle implements UserDao {
    @Override
    public void deleteById() {
        System.out.println("Oracle delete user data");
    }
}

此時需要修改UserServiceImpl

public class UserServiceImpl implements UserService {
    //修改了這行程式碼
    //private UserDao userDao = new UserDaoImplForMySQL();
    private UserDao userDao = new UserDaoImplForOracle();
    @Override
    public void deleteUser() {
        userDao.deleteById();
    }
}

修改了就要重新進行測試,在擴充套件系統的功能時修改了之前執行良好的程式,這就違背了OCP開閉原則

OCP原則是最核心的原則,軟體開發七大原則中的其他六個原則都是為這個原則服務的。

程式還違背了依賴倒置原則 DIP原則,在現有的程式架構中:

image-20230625160307855

當前是上層模組依賴下層模組的,下層程式只要一變動就會影響上層模組:

  • UserAction依賴了具體的UserServiceImpl
  • UserServiceImpl依賴了具體的UserDaoImplForMySQL

這樣是違背了依賴倒置原則,依賴倒置原則的核心:面向介面程式設計

image-20230625160703130

這樣就是完全抽象的,符合依賴倒置原則的。不new具體的實現類物件,就可以解耦合;(暫時忽略空指標異常)

這樣設計確實可以保證耦合度最低,UserServiceImpl不必關心UserDao的具體實現類,把建立物件的權力和物件關係維護的權力交出去了,不在程式中硬編碼實現

物件關係的維護:實現類具體是Oracle還是MySQL

這個過程就被稱為 控制反轉Inversion of Control

反轉的是:

  1. 不在程式中使用硬編碼方式new物件
  2. 不在程式中使用硬編碼方法維護物件之間的關係

這兩者的權力都交出去了,這就是控制反轉的核心思想

控制反轉是一種新型設計模式,沒有被納入GoF 23種設計模式範圍之內

權力交給了Spring框架,Spring框架實現了控制反轉IoC的思想,Spring是一個實現了IoC思想的容器

  • Spring框架可以new物件
  • Spring框架可以維護物件和物件之間的關係

控制反轉的實現方式有多種,其中比較重要的是:依賴注入Dependency Injection

控制反轉IoC是一種思想,依賴注入DI是一種具體的實現方式

依賴注入有常見的兩種注入方式:

  1. set注入
  2. 構造方法注入

也就是為private UserDao userDao 屬性賦值的方法

依賴:A物件和B物件的關係

注入:讓A物件和B物件產生關係的一種手段

Spring的8個模組

image-20230625163706533

Spring是一個開源框架,它由Rod Johnson建立。它是為了解決企業應用開發的複雜性而建立的。

從簡單性、可測試性和松耦合的角度而言,任何Java應用都可以從Spring中受益。

Spring是一個輕量級的控制反轉(IoC)和麵向切面(AOP)的容器框架。

Spring最初的出現是為了解決EJB(企業級JavaBean)臃腫的設計,以及難以測試等問題。

Spring為簡化開發而生,讓程式設計師只需關注核心業務的實現,儘可能的不再關注非業務邏輯程式碼(事務控制,安全日誌等)。

Spring5之前是7大模組,Spring5新增了WebFlux模組

image-20230625172242338
  1. Spring Core模組

這是Spring框架最基礎的部分,它提供了依賴注入(Dependency Injection)特徵來實現容器對Bean的管理。核心容器的主要元件是 BeanFactory,BeanFactory是工廠模式的一個實現,是任何Spring應用的核心。它使用IoC將應用配置和依賴從實際的應用程式碼中分離出來。

  1. Spring Context模組

如果說核心模組中的BeanFactory使Spring成為容器的話,那麼上下文模組就是Spring成為框架的原因。

這個模組擴充套件了BeanFactory,增加了對國際化(I18N)訊息、事件傳播、驗證的支援。另外提供了許多企業服務,例如電子郵件、JNDI訪問、EJB整合、遠端以及時序排程(scheduling)服務。也包括了對模版框架例如Velocity和FreeMarker整合的支援

  1. Spring AOP模組

Spring在它的AOP模組中提供了對面向切面程式設計的豐富支援,Spring AOP 模組為基於 Spring 的應用程式中的物件提供了事務管理服務。透過使用 Spring AOP,不用依賴元件,就可以將宣告性事務管理整合到應用程式中,可以自定義攔截器、切點、日誌等操作。

  1. Spring DAO模組

提供了一個JDBC的抽象層和異常層次結構,消除了煩瑣的JDBC編碼和資料庫廠商特有的錯誤程式碼解析,用於簡化JDBC。

  1. Spring ORM模組

Spring提供了ORM模組。Spring並不試圖實現它自己的ORM解決方案,而是為幾種流行的ORM框架提供了整合方案,包括Hibernate、JDO和iBATIS SQL對映,這些都遵從 Spring 的通用事務和 DAO 異常層次結構。

  1. Spring Web MVC模組

Spring為構建Web應用提供了一個功能全面的MVC框架。雖然Spring可以很容易地與其它MVC框架整合,例如Struts,但Spring的MVC框架使用IoC對控制邏輯和業務物件提供了完全的分離。

  1. Spring WebFlux模組

Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是專門為 Servlet API 和 Servlet 容器構建的。反應式堆疊 Web 框架 Spring WebFlux 是在 5.0 版的後期新增的。它是完全非阻塞的,支援反應式流(Reactive Stream)背壓,並在Netty,Undertow和Servlet 3.1+容器等伺服器上執行。

  1. Spring Web模組

Web 上下文模組建立在應用程式上下文模組之上,為基於 Web 的應用程式提供了上下文,提供了Spring和其它Web框架的整合,比如Struts、WebWork。還提供了一些面向服務支援,例如:實現檔案上傳的multipart請求。

Spring的特點:

  1. 輕量

    • 從大小與開銷兩方面而言Spring都是輕量的。完整的Spring框架可以在一個大小隻有1MB多的JAR檔案裡釋出。並且Spring所需的處理開銷也是微不足道的。

    • Spring是非侵入式的:Spring應用中的物件不依賴於Spring的特定類,Spring的執行不需要依賴其他東西。

      假如某個框架中有一個類,該類的方法上有一個引數HttpServletRequest,這時無法對該類進行單元測試,這個引數在Tomcat伺服器當中,這就是侵入式的設計,離不開Tomcat伺服器的支援

  2. 控制反轉

    • Spring透過一種稱作控制反轉(IoC)的技術促進了松耦合。當應用了IoC,一個物件依賴的其它物件會透過被動的方式傳遞進來,而不是這個物件自己建立或者查詢依賴物件。你可以認為IoC與JNDI相反——不是物件從容器中查詢依賴,而是容器在物件初始化時不等物件請求就主動將依賴傳遞給它。
  3. 面向切面

    • Spring提供了面向切面程式設計的豐富支援,允許透過分離應用的業務邏輯與系統級服務(例如審計(auditing)和事務(transaction)管理)進行內聚性的開發。應用物件只實現它們應該做的——完成業務邏輯——僅此而已。它們並不負責(甚至是意識)其它的系統級關注點,例如日誌或事務支援。
  4. 容器

    • Spring包含並管理應用物件的配置和生命週期,在這個意義上它是一種容器,你可以配置你的每個bean如何被建立——基於一個可配置原型(prototype),你的bean可以建立一個單獨的例項或者每次需要時都生成一個新的例項——以及它們是如何相互關聯的。然而,Spring不應該被混同於傳統的重量級的EJB容器,它們經常是龐大與笨重的,難以使用。
  5. 框架

    • Spring可以將簡單的元件配置、組合成為複雜的應用。在Spring中,應用物件被宣告式地組合,典型地是在一個XML檔案裡。Spring也提供了很多基礎功能(事務管理、持久化框架整合等等),將應用邏輯的開發留給了你。

所有Spring的這些特徵使你能夠編寫更乾淨、更可管理、並且更易於測試的程式碼。它們也為Spring中的各種模組提供了基礎支援。

Spring下載

官網:https://spring.io/ 中文網:http://spring.p2hp.com/projects/spring-framework.html

image-20230625182241266 image-20230625182309462

image-20230625182921481

pom檔案:

<repository>
    <id>repository.spring.milestone</id>
    <name>Spring Milestone Repository</name>
    <url>https://repo.spring.io/milestone</url>
</repository>

...

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

需要關聯內部倉庫,因為Spring6還沒有正式釋出

Spring的jar包

開啟libs目錄,會看到很多jar包:

img

spring-core-5.3.9.jar:位元組碼(這個是支撐程式執行的jar包

spring-core-5.3.9-javadoc.jar:程式碼中的註釋

spring-core-5.3.9-sources.jar:原始碼

spring的jar包:

img

JAR檔案 描述
spring-aop.jar 這個jar 檔案包含在應用中使用Spring 的AOP 特性時所需的類
spring-aspects.jar 提供對AspectJ的支援,以便可以方便的將面向切面的功能整合進IDE中
spring-beans.jar 這個jar 檔案是所有應用都要用到的,它包含訪問配置檔案、建立和管理bean 以及進行Inversion ofControl / Dependency Injection(IoC/DI)操作相關的所有類。如果應用只需基本的IoC/DI 支援,引入spring-core.jar 及spring-beans.jar 檔案就可以了。
spring-context.jar 這個jar 檔案為Spring 核心提供了大量擴充套件。可以找到使用Spring ApplicationContext特性時所需的全部類,JDNI 所需的全部類,instrumentation元件以及校驗Validation 方面的相關類。
spring-context.jar 雖然類路徑掃描非常快,但是Spring內部存在大量的類,新增此依賴,可以透過在編譯時建立候選物件的靜態列表來提高大型應用程式的啟動效能。
spring-context.jar 用來提供Spring上下文的一些擴充套件模組,例如實現郵件服務、檢視解析、快取、定時任務排程等
spring-core.jar Spring 框架基本的核心工具類。Spring 其它元件要都要使用到這個包裡的類,是其它元件的基本核心,當然你也可以在自己的應用系統中使用這些工具類。
spring-expression.jar Spring表示式語言。
spring-instrument.jar Spring3.0對伺服器的代理介面。
spring-jcl.jar Spring的日誌模組。JCL,全稱為"Jakarta Commons Logging",也可稱為"Apache Commons Logging"。
spring-jdbc.jar Spring對JDBC的支援。
spring-jms.jar 這個jar包提供了對JMS 1.0.2/1.1的支援類。JMS是Java訊息服務。屬於JavaEE規範之一。
spring-messaging.jar 為整合messaging api和訊息協議提供支援
spring-orm.jar Spring整合ORM框架的支援,比如整合hibernate,mybatis等。
spring-oxm.jar 為主流O/X Mapping元件提供了統一層抽象和封裝,OXM是Object Xml Mapping。物件和XML之間的相互轉換。
spring-r2dbc.jar Reactive Relational Database Connectivity (關係型資料庫的響應式連線) 的縮寫。這個jar檔案是Spring對r2dbc的支援。
spring-test.jar 對Junit等測試框架的簡單封裝。
spring-tx.jar 為JDBC、Hibernate、JDO、JPA、Beans等提供的一致的宣告式和程式設計式事務管理支援。
spring-web.jar Spring整合MVC框架的支援,比如整合Struts等。
spring-webflux.jar WebFlux是 Spring5 新增的新模組,用於 web 的開發,功能和 SpringMVC 類似的,Webflux 使用當前一種比較流程響應式程式設計出現的框架。
spring-webmvc.jar SpringMVC框架的類庫
spring-websocket.jar Spring整合WebSocket框架時使用

注意:

如果只是想用Spring的IoC功能,僅需要引入:spring-context即可。將這個jar包新增到classpath當中。

如果採用maven只需要引入context的依賴即可。

image-20230625183815279

Spring入門程式

演示Spring是如何建立物件的,不演示維護物件間的關係

<repositories>
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>


    <dependencies>
        <!--引入Spring基礎依賴-->
        <!--如果想使用Spring的jdbc、事務tx 還需要新增其他的依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>

        <!--log4j2的依賴-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.19.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.19.0</version>
        </dependency>
    </dependencies>
image-20230625184336266

想讓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">

    <!--配置bean,spring才能幫我們管理這個物件-->
    <!--
        bean的兩個重要屬性:
            id: bean的唯一標識
            class:類的全限定名
    -->
    <bean id="userBean" class="com.eun.spring.bean.User" />
</beans>

spring.xml一般放在類的根路徑下

    @Test
    public void firstSpringTest(){
        //1. 獲取Spring容器
        // 應用上下文 Spring容器
        //ApplicationContext介面 下很多實現類,其中ClassPathXmlApplicationContext從類路徑下載入Spring上下文物件
        //這行程式碼只要一執行,立刻啟動Spring容器,解析xml配置檔案,將例項化所有bean物件放在容器中
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        //2. 根據bean Id從Spring容器中獲取這個物件
        Object userBean = applicationContext.getBean("userBean");
        System.out.println(userBean);//com.eun.spring.bean.User@6572421
    }
  • bean的id不能重複
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Bean name 'userBean' is already used in this <beans> element
Offending resource: class path resource [spring.xml]
  • 預設情況下透過反射機制呼叫無引數構造方法例項化物件
    • Class.forName()傳入class屬性值,獲取constructor再newInstance
public class User {
    public User() {
        System.out.println("User constructor executed");
    }
}
/**
User constructor executed
com.eun.spring.bean.User@6572421
*/

所以如果只提供一個有參構造,預設情況下會報錯(無參構造不再提供):

Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userBean' defined in class path resource [spring.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.eun.spring.bean.User]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.eun.spring.bean.User.<init>()

無參構造會在建立ClassPathXmlApplicationContext時立刻被呼叫,不是在呼叫getBean()時建立物件

  • 建立好的bean放在Map集合中了
key value
bean的id bean物件
  • 可以有多個spring.xml配置檔案
//可變長引數
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml","spring6.xml");
//                                                                         "xml/beans.xml"
  • 配置檔案中的類也可以是JDK中的類,只要有無引數構造方法
<bean id="nowTime" class="java.util.Date" />
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

        Object nowTime = applicationContext.getBean("nowTime");
        String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS").format(nowTime);
        System.out.println(now); //2023-06-25 19:09:35 910
  • 如果getBean()方法指定的bean的id不存在,不會返回null而是直接報錯:
	org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'nowTime2' available

	@Override
	public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
		BeanDefinition bd = this.beanDefinitionMap.get(beanName);
		if (bd == null) {
			if (logger.isTraceEnabled()) {
				logger.trace("No bean named '" + beanName + "' found in " + this);
			}
			throw new NoSuchBeanDefinitionException(beanName);
		}
		return bd;
	}
  • getBean()方法獲取的是Object型別,如果訪問子類特有的方法還需要向下轉型,有沒有其他的方法解決這個問題?
Date nowTime = applicationContext.getBean("nowTime", Date.class);
  • ClassPathXmlApplicationContext是從類路徑中載入配置檔案,如果沒有在類路徑當中,又應該如何載入配置檔案呢?
ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("d:/spring6.xml");

FileSystemXmlApplicationContext從系統中載入配置檔案

  • ApplicationContext的超級父介面是BeanFactory,是能夠生產Bean物件的一個工廠物件

    BeanFactory是IoC容器的頂級介面

    Spring的IoC容器實際上使用了工廠模式,IoC實現:

    xml解析 + 工廠模式 + 反射機制

        BeanFactory applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("
                                               userBean", User.class);
        System.out.println(user);

啟用log4j2日誌框架

Spring5之後,Spring框架支援整合的日誌框架是Log4j2

  1. 引入Log4j2的依賴
<!--log4j2的依賴-->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>2.19.0</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j2-impl</artifactId>
  <version>2.19.0</version>
</dependency>
  1. 在類的根路徑下提供log4j2.xml配置檔案(檔名固定為:log4j2.xml,檔案必須放到類根路徑下。)
<?xml version="1.0" encoding="UTF-8"?>

<configuration>

    <loggers>
        <!--
            level指定日誌級別,從低到高的優先順序:
                ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
        -->
        <root level="DEBUG">
            <appender-ref ref="spring6log"/>
        </root>
    </loggers>

    <appenders>
        <!--輸出日誌資訊到控制檯-->
        <console name="spring6log" target="SYSTEM_OUT">
            <!--控制日誌輸出的格式-->
            <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
        </console>
    </appenders>
</configuration>

啟用完畢

  • 如果在自己的專案中想使用日誌框架
        //建立某個類的日誌記錄器物件
        //引數:記錄哪個類的日誌
        //只要是SpringTest類中的程式碼執行記錄日誌的話,就輸出相關的日誌資訊
        Logger logger = LoggerFactory.getLogger(SpringTest.class);

        //根據不同的日誌級別輸出資訊
        logger.debug("我是一條除錯資訊");
        logger.info("我是一條日誌訊息");
        logger.error("我是一條錯誤資訊");
/*
2023-06-25 19:30:06 045 [main] INFO com.eun.spring.test.SpringTest - 我是一條日誌訊息
2023-06-25 19:30:06 047 [main] DEBUG com.eun.spring.test.SpringTest - 我是一條除錯資訊
2023-06-25 19:30:06 047 [main] ERROR com.eun.spring.test.SpringTest - 我是一條錯誤資訊
*/

此時的日誌級別是DEBUG,這三條訊息都會輸出,如果將日誌級別設定為ERROR:

2023-06-25 19:31:32 100 [main] ERROR com.eun.spring.test.SpringTest - 我是一條錯誤資訊

只會輸出 >= ERROR級的資訊

Spring對IoC的實現

控制反轉是一種思想,是為了降低程式耦合度,提高程式擴充套件力,達到OCP、DIP原則

控制反轉,反轉的是什麼?

  • 將物件建立的權利交出去,交給第三方容器負責
  • 將物件和物件之間的關係的維護權交出去,交給第三方容器負責

控制反轉的實現:DI

Spring透過依賴注入的方式來完成Bean管理,Bean管理指的是:

  1. Bean物件的建立(Spring容器啟動時建立,在上文中已經演示)
  2. Bean物件中屬性的賦值(Bean物件之間關係的維護,set/constructor注入)

依賴注入:

  • 依賴是物件與物件之間的關聯關係
  • 注入是一種資料傳遞行為,透過注入行為來讓物件和物件之間產生關係

依賴注入常用的兩種方式:

  1. set注入
  2. 構造方法注入

set注入

透過set方法進行依賴注入,需要保證set方法存在

public class UserService {
    //對userDao進行set注入,需要提供set方法
    private UserDao userDao;

    //必須以set開始
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void saveUser(){
        userDao.insert();
    }
}

在spring.xml檔案中進行配置:

    <bean id="userDaoBean" class="com.eun.spring.dao.UserDao" />

    <bean id="userServiceBean" class="com.eun.spring.service.UserService" >
        <!--
            name :  set方法名 去掉set後首字母小寫
            ref  : userDaoBean的id
        -->
        <property name="userDao" ref="userDaoBean" />
    </bean>

property標籤可以讓spring呼叫set方法,ref指定要注入的bean的id

ref可以看作屬性名,獲取到屬性名就可以獲取到該屬性的型別:

        Class<?> clazz = Class.forName(user);
        Class<?> paramType = clazz.getDeclaredField(field).getType();
        Method setAgeMethod = clazz.getDeclaredMethod("set" + field.toUpperCase().charAt(0) 
                                                      + field.substring(1),paramType);
        Object obj = clazz.getConstructor().newInstance();
        setAgeMethod.invoke(obj,20);

構造注入

透過構造方法來給屬性賦值

這種方法是有區別的,set注入是先建立物件後呼叫set方法為屬性賦值;構造注入是建立物件的同時給屬性賦值,也就是注入時機不同

public class CustomerService {
    private UserDao userDao;
    private VipDao vipDao;

    public CustomerService(UserDao userDao, VipDao vipDao) {
        this.userDao = userDao;
        this.vipDao = vipDao;
    }
    public void save(){
        userDao.insert();
        vipDao.insert();
    }
}
    <bean id="userDaoBean" class="com.eun.spring.dao.UserDao" />
    <bean id="vipDaoBean" class="com.eun.spring.dao.VipDao" />
    <bean id="customerServiceBean" class="com.eun.spring.service.CustomerService" >
        <!--
            constructor-arg:構造方法注入
                index: 指定引數下標
                ref: 指定注入Bean的id
        -->
        <constructor-arg index="0" ref="userDaoBean" />
        <constructor-arg index="1" ref="vipDaoBean" />
    </bean>

或者可以根據引數的名字進行注入:

    <bean id="userDaoBean" class="com.eun.spring.dao.UserDao" />
    <bean id="vipDaoBean" class="com.eun.spring.dao.VipDao" />
    <bean id="customerServiceBean" class="com.eun.spring.service.CustomerService" >
        <!--
            constructor-arg:構造方法注入
                name: 指定引數的名字
                ref: 指定注入Bean的id
        -->
        <constructor-arg name="userDao" ref="userDaoBean" />
        <constructor-arg name="vipDao" ref="vipDaoBean" />
    </bean>

反射可以獲取到引數的名字嗎?

或者不指定name或index,Spring會進行型別匹配:

    <bean id="userDaoBean" class="com.eun.spring.dao.UserDao" />
    <bean id="vipDaoBean" class="com.eun.spring.dao.VipDao" />
    <bean id="customerServiceBean" class="com.eun.spring.service.CustomerService" >
        <constructor-arg ref="userDaoBean" />
        <constructor-arg ref="vipDaoBean" />
    </bean>

這種方式是根據型別進行注入的,型別不同時順序不會影響結果

    public void testConstructorDI(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
        applicationContext.getBean(CustomerService.class).save();
    }

set注入專題

注入外部bean

    <!--宣告Bean-->
    <bean id="orderDaoBean" class="com.eun.spring.dao.OrderDao" />
    
    <bean id="orderServiceBean" class="com.eun.spring.service.OrderService" >
        <!--注入外部Bean:使用ref引入-->
        <property name="orderDao" ref="orderDaoBean" />
    </bean>
    @Test
    public void testSetDI(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("set-di.xml");
        OrderService orderServiceBean = applicationContext.getBean("orderServiceBean", 
                                                                   OrderService.class);
        orderServiceBean.generator();//order is inserting
    }

注入內部bean

    <bean id="orderServiceBean" class="com.eun.spring.service.OrderService" >
        <!--注入內部bean-->
        <property name="orderDao">
            <bean class="com.eun.spring.dao.OrderDao" />
        </property>
    </bean>

property標籤中巢狀使用bean標籤就是注入內部bean

注入內部bean不需要指定id屬性

注入簡單型別

上文中的OrderDao是引用資料型別,如果是基本資料型別如何注入?

  • set注入:
public class User {
    private String name;
    private String password;
    private int age;

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

    public void setPassword(String password) {
        this.password = password;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
    <!--注入簡單型別-->
    <bean id="userBean" class="com.eun.spring.bean.User" >
        <!--簡單型別使用value賦值-->
        <property name="userName" value="zhangsan"/>
        <property name="password" value="123" />
        <property name="age" value="20" />
    </bean>

Spring中有一個工具類:BeanUtils

image-20230626085130166

這些都是簡單型別,對這些型別進行測試:

public class SimpleValueType {
    private int age;
    private Integer ageWrapper;
    private boolean flag;
    private Boolean flagWrapper;
    private char c;
    private Character cWrapper;
    private Season season;
    private String username;
    private Class clazz;
}
    <bean id="svt" class="com.eun.spring.bean.SimpleValueType" >
        <property name="age" value="20"/>
        <property name="ageWrapper" value="20"/>
        <property name="username" value="zhangsan"/>
        <property name="season" value="SPRING"/>
        <property name="flag" value="false"/>
        <property name="flagWrapper" value="true"/>
        <property name="c" value="M"/>
        <property name="cWrapper" value="F"/>
        <property name="clazz" value="java.lang.String"/>
    </bean>
SimpleValueType{age=20, ageWrapper=20, flag=false, flagWrapper=true, c=M, cWrapper=F, season=SPRING, username='zhangsan', clazz=class java.lang.String}

測試Date:

    private Date birth;

    public void setBirth(Date birth) {
        this.birth = birth;
    }
    <bean id="svt" class="com.eun.spring.bean.SimpleValueType" >
        <property name="birth" value="1970-10-11"/>
    </bean>

但是這樣就會報錯:

org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birth';
#例項化失敗,對於屬性birth,無法將給定String型別引數轉化為需要的Date型別引數

因為spring將value="1970-10-11" 看作字串,無法轉換為java.util.Date

如果將Date作為簡單型別注入,使用value賦值的日期字串格式是規定的

    public static void main(String[] args) {
        System.out.println(new Date());
        //Mon Jun 26 09:15:38 CST 2023
    }
<property name="birth" value="Mon Jun 26 09:15:38 CST 2023"/>

這樣賦值就可以成功了:

SimpleValueType{age=20, ageWrapper=20, flag=false, flagWrapper=true, c=M, cWrapper=F, season=SPRING, username='zhangsan', clazz=class java.lang.String, birth=Mon Jun 26 23:15:38 CST 2023}

但是這樣賦值太麻煩了,實際開發中不將Date看作簡單型別

    <bean id="date" class="java.util.Date"/>

    <bean id="svt" class="com.eun.spring.bean.SimpleValueType" >
        <property name="birth" ref="date"/>
    </bean>

但是目前只能將new Date()也就是容器啟動時建立的時間賦值給birth屬性,因為此時無法對bean的建立進行操作

經典應用
/**
 * @author LiuYiBo
 * @ClassName MyDataSource
 * @description: 所有資料來源都要實現javax.sql.DataSource規範
 *              資料來源:提供Connection物件
 * @date 2023/06/26
 * @version: 1.0
 */
public class MyDataSource implements DataSource {
    private String driver;
    private String url;
    private String username;
    private String password;

    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = null;
        try {
            Class.forName(driver);
            connection = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return connection;
    }
}
    <bean id="myDataSourceBean" class="com.eun.spring.jdbc.MyDataSource" >
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mybatis_db"/>
        <property name="username" value="root"/>
        <property name="password" value=""/>
    </bean>
    @Test
    public void MyDataSourceTest() throws SQLException {
        ApplicationContext context = new ClassPathXmlApplicationContext("set-di.xml");
        MyDataSource myDataSourceBean = context.getBean("myDataSourceBean", MyDataSource.class);
        DataSource dataSource = context.getBean(DataSource.class);
        Connection connection = myDataSourceBean.getConnection();
        System.out.println(connection);
        //com.mysql.cj.jdbc.ConnectionImpl@2228db21
    }

級聯屬性賦值

public class Clazz {
    private String cname;

    public void setCname(String cname) {
        this.cname = cname;
    }

    @Override
    public String toString() {
        return "Clazz{" +
                "cname='" + cname + '\'' +
                '}';
    }
}
public class Student {
    private String sname;
    private Clazz clazz;

    public void setClazz(Clazz clazz) {
        this.clazz = clazz;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    @Override
    public String toString() {
        return "Student{" +
                "sname='" + sname + '\'' +
                ", clazz=" + clazz +
                '}';
    }
}

Student類有一個clazz屬性

  • 注入外部Bean賦值:
    <bean id="studentBean" class="com.eun.spring.bean.Student" >
        <property name="sname" value="zhangsan"/>
        <property name="clazz" ref="clazzBean"/>
    </bean>
    <bean id="clazzBean" class="com.eun.spring.bean.Clazz" >
        <property name="cname" value="class101"/>
    </bean>

列印Student資訊:

Student{sname='zhangsan', clazz=Clazz{cname='class101'}}
  • 級聯屬性賦值:
    <bean id="studentBean" class="com.eun.spring.bean.Student" >
        <property name="sname" value="zhangsan"/>
        <property name="clazz" ref="clazzBean"/>
        <property name="clazz.cname" value="class101"/>  <!--級聯屬性賦值-->
    </bean>
    <bean id="clazzBean" class="com.eun.spring.bean.Clazz" />

這樣做必須為clazz屬性提供一個get方法,這裡的clazz.cname實際上是getClazz().setCname("")

級聯屬性的兩個順序不能顛倒

注入陣列

  • 陣列中的元素是簡單型別
public class ArrayValueType {
    private String[] hobbies;

    public void setHobbies(String[] hobbies) {
        this.hobbies = hobbies;
    }

    @Override
    public String toString() {
        return "ArrayValueType{" +
                "hobbies=" + Arrays.toString(hobbies) +
                '}';
    }
}

    <bean id="avt" class="com.eun.spring.bean.ArrayValueType" >
        <!--String陣列注入-->
        <property name="hobbies">
            <array>
                <value>smoke</value>
                <value>drink</value>
                <value>fire</value>
            </array>
        </property>
    </bean>
  • 陣列中的元素是引用型別
public class Friend {
    private String name;
    public Friend(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Friend{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class ArrayValueType {
    private Friend[] friends;
    public void setFriends(Friend[] friends) {
        this.friends = friends;
    }
    @Override
    public String toString() {
        return "ArrayValueType{" +
                "friends=" + Arrays.toString(friends) +
                '}';
    }
}
<bean id="f1" class="com.eun.spring.bean.Friend" > <constructor-arg value="f1"/> </bean>
<bean id="f2" class="com.eun.spring.bean.Friend" > <constructor-arg value="f2"/> </bean>
<bean id="f3" class="com.eun.spring.bean.Friend" > <constructor-arg value="f3"/> </bean>

<bean id="avt" class="com.eun.spring.bean.ArrayValueType" >
    <!--String陣列注入-->
    <property name="friends">
        <array>
            <ref bean="f1"/>
            <ref bean="f2"/>
            <ref bean="f3"/>
        </array>
    </property>
</bean>

或者可以注入內部bean:

<property name="friends">
            <array>
                <bean class="com.eun.spring.bean.Friend" >
                    <constructor-arg value="f1"/>
                </bean>
                <bean class="com.eun.spring.bean.Friend" >
                    <constructor-arg value="f2"/>
                </bean>
                <bean class="com.eun.spring.bean.Friend" >
                    <constructor-arg value="f3"/>
                </bean>
            </array>
</property>

注入List和Set

public class Person {
    private List<String> names;
    private Set<String> address;

    public void setNames(List<String> names) {
        this.names = names;
    }

    public void setAddress(Set<String> address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{" +
                "names=" + names +
                ", address=" + address +
                '}';
    }
}
    <bean id="personBean" class="com.eun.spring.bean.Person" >
        <property name="names">
            <list>
                <value>zhangsan</value>
                <value>lisi</value>
                <value>wangwu</value>
            </list>
        </property>

        <property name="address">
            <!--set集合無序不可重複-->
            <set>
                <value>BeiJing</value>
                <value>BeiJing</value>
                <value>TianJin</value>
                <value>ShangHai</value>
            </set>
        </property>
    </bean>
    @Test
    public void testCollectionDI() {
        ApplicationContext context = new ClassPathXmlApplicationContext("collection-di.xml");
        Person personBean = context.getBean("personBean", Person.class);
        System.out.println(personBean);
        //Person{names=[zhangsan, lisi, wangwu], address=[BeiJing, TianJin, ShangHai]}
    }

注入Map集合

private Map<Integer,String> phones;

public void setPhones(Map<Integer, String> phones) {
    this.phones = phones;
}
        <property name="phones">
            <map>
                <entry key="1" value="12345"/>
                <entry key="2" value="23456"/>
                <entry key="3" value="34567"/>
                <!--<entry key-ref= value-ref= />-->
            </map>
        </property>


            <map>
                <entry key="threadScope">
                    <bean class="org.springframework.context.support.SimpleThreadScope" />
                </entry>
            </map>

注入Properties

properties的key和value只能是字串

    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }
    <property name="properties">
        <props>
            <prop key="driver">com.mysql.cj.jdbc.Driver</prop>
            <prop key="url">jdbc:mysql://localhost:3306/mybatis_db</prop>
            <prop key="user">root</prop>
            <!--   <prop key = "" value = "" />   -->
        </props>
    </property>

注入null和空字串

public class Cat {
    private String name;
    private int age;

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

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

注入null:不給這個屬性賦值

    <bean id="catBean" class="com.eun.spring.bean.Cat" >
        <property name="age" value="2"/>
    </bean>

因為String類的預設值就是null

不能寫value="null" ,這是注入了一個字串

或者可以使用null標籤:

    <bean id="catBean" class="com.eun.spring.bean.Cat" >
        <property name="name">
            <null/>
        </property>
        <property name="age" value="2"/>
    </bean>

注入空字串:

    <bean id="catBean" class="com.eun.spring.bean.Cat" >
        <property name="name" value=""/>
        <property name="age" value="2"/>
    </bean>
Cat{name='', age=2}

或者使用value的自閉合標籤:

    <bean id="catBean" class="com.eun.spring.bean.Cat" >
        <property name="name">
            <value/>
        </property>
        <property name="age" value="2"/>
    </bean>

注入特殊符號

XML語法中有5個特殊字元:< > &,這5個特殊符號會當作XML語法的一部分

public class MathBean {
    private String result;
    public void setResult(String result) {
        this.result = result;
    }
    @Override
    public String toString() {
        return "MathBean{" +
                "result='" + result + '\'' +
                '}';
    }
}
image-20230626121523283

<就會報錯:

org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 49 in XML document from class path resource [collection-di.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 49; columnNumber: 42; 與元素型別 "property" 相關聯的 "value" 屬性值不能包含 '<' 字元。

第一種解決方案:使用實體符號代替特殊符號

    <bean id="mathBean" class="com.eun.spring.bean.MathBean" >
        <property name="result" value="2 &lt; 3"/>
    </bean>
特殊字元 跳脫字元
> &gt;
< &lt;
' &apos;
" &quot;
& &amp;

第二種方案:<![CDATA[]]>,CDATA區中的資料不會被XML檔案解析器解析,有特殊符號也當作普通字串處理。

    <bean id="mathBean" class="com.eun.spring.bean.MathBean" >
        <property name="result">
            <value> <![CDATA[2 < 3]]> </value>
        </property>
    </bean>

但是此處必須使用value標籤,不能使用value屬性(因為CDATA只能放在兩個標籤當中)

set注入總結:

property標籤指明使用set注入,必須包含的屬性:name,可以使用的屬性:value(簡單資料型別)、ref(複雜資料型別)

可選巢狀的子標籤:

image-20230807045735997

基於p名稱空間的注入

目的:簡化配置。

  • 第一:在XML頭部資訊中新增p名稱空間的配置資訊:xmlns:p="http://www.springframework.org/schema/p"
  • 第二:p名稱空間注入是基於setter方法的,所以需要對應的屬性提供setter方法。
public class Dog {
    private String name;
    private int age;
    
    //一般將Date看作非簡單型別
    private Date birth;

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

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

    public void setBirth(Date birth) {
        this.birth = birth;
    }
}
<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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="nowTime" class="java.util.Date" />
    
    <bean id="dogBean" class="com.eun.spring.bean.Dog" p:name="popi" p:age="3" p:birth-ref="nowTime"/>
</beans>

基於c名稱空間的注入

c名稱空間注入是簡化構造方法注入的

使用c名稱空間的兩個前提條件:

第一:需要在xml配置檔案頭部新增資訊:xmlns:c="http://www.springframework.org/schema/c"

第二:需要提供構造方法。

public class People {
    private String name;
    private int age;
    private boolean sex;

    //c名稱空間是基於構造方法的
    public People(String name, int age, boolean sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}
image-20230626123646540
<bean id="peopleBean" class="com.eun.spring.bean.People" c:_0="zhangsan" c:_1="30" c:_2="true"/>

util名稱空間

目的:配置複用

使用util名稱空間的前提是:在spring配置檔案頭部新增配置資訊。如下:

image-20230626130039060

假如系統整合多個資料庫連線池,或者要配置多個資料來源:

image-20230626125156243

也可以把這些資訊都放在Properties屬性類集合中:

public class MyDataSource implements DataSource {
/*    private String driver;
    private String url;
    private String username;
    private String password;*/
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return null;
    }

    @Override
    public String toString() {
        return "MyDataSource{" +
                "properties=" + properties +
                '}';
    }
}
public class MyOtherDataSource implements DataSource {
    private Properties properties;

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public Connection getConnection() throws SQLException {
        return null;
    }
}

讓spring管理資料來源:

    <bean id="myDataSource" class="com.eun.spring.jdbc.MyDataSource" >
        <property name="properties">
            <props>
                <prop key="driver">com.mysql.cj.jdbc.Driver"</prop>
                <prop key="url">jdbc:mysql://localhost:3306/mybatis_db</prop>
                <prop key="username">root</prop>
                <prop key="password"></prop>
            </props>
        </property>
    </bean>

    <bean id="myOtherDataSource" class="com.eun.spring.jdbc.MyOtherDataSource" >
        <property name="properties">
            <props>
                <prop key="driver">com.mysql.cj.jdbc.Driver"</prop>
                <prop key="url">jdbc:mysql://localhost:3306/mybatis_db</prop>
                <prop key="username">root</prop>
                <prop key="password"></prop>
            </props>
        </property>
    </bean>

配置資訊都是相同的,可以使用util名稱空間簡化:

<?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:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util  http://www.springframework.org/schema/util/spring-util.xsd">
    
    <!--引入util名稱空間-->
    <util:properties id="prop" >
        <prop key="driver">com.mysql.cj.jdbc.Driver"</prop>
        <prop key="url">jdbc:mysql://localhost:3306/mybatis_db</prop>
    </util:properties>
    
    <bean id="myDataSource" class="com.eun.spring.jdbc.MyDataSource" >
        <property name="properties" ref="prop"/>
    </bean>

    <bean id="myOtherDataSource" class="com.eun.spring.jdbc.MyOtherDataSource" >
        <property name="properties" ref="prop"/>
    </bean>
</beans>
引入外部屬性配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       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
                          http://www.springframework.org/schema/util   http://www.springframework.org/schema/util/spring-util.xsd
                          http://www.springframework.org/schema/context   http://www.springframework.org/schema/context/spring-context.xsd">

    <!--
        引入外部properties檔案
        第一步:引入context名稱空間
        第二步:使用context:property-placeholder 的 location 指明配置檔案路徑
        第三步:需要使用的地方使用${key}取值
    -->
    <context:property-placeholder location="jdbc.properties"/>

    <util:properties id="prop">
        <prop key="driver">${jdbc.driver}</prop>
        <prop key="url">${jdbc.url}</prop>
        <prop key="username">${jdbc.username}</prop>
        <prop key="password">${jdbc.password}</prop>
    </util:properties>
    
    <bean id="dataSource" class="com.eun.spring.jdbc.MyDataSource" >
        <property name="properties" ref="prop" />
    </bean>
</beans>

-----------------------------------------------------------------------------------------------------
    <context:property-placeholder location="jdbc.properties" />


    <bean id="myDataSource" class="com.eun.spring.jdbc.MyDataSource">
        <property name="properties" >
            <props>
                <prop key="driver">${jdbc.driver}</prop>
                <prop key="username">${jdbc.username}</prop>
                <prop key="password">${jdbc.password}</prop>
            </props>
        </property>
    </bean>

這樣做是沒有問題的,但是如果在屬性配置檔案中將使用者名稱寫為username就會出現問題:

image-20230626141034801
2023-06-26 14:10:45 136 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dataSource'
MyDataSource{properties={password=, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/, username=bike1}}

使用者名稱竟然是Windows系統的使用者名稱,因為Spring中使用${}載入內容時預設先載入Windows系統的環境變數

其實只使用util名稱空間也可以達到相同的效果:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/util  http://www.springframework.org/schema/util/spring-util.xsd">
    
    <util:properties id="props" location="jdbc.properties" />
    
    <bean id="myDataSource" class="com.eun.spring.jdbc.MyDataSource" >
        <property name="properties" ref="props" />
    </bean>
    <bean id="myOtherDataSource" class="com.eun.spring.jdbc.MyOtherDataSource" >
        <property name="properties" ref="props" />
    </bean>
</beans>

使用util的id和location屬性

基於XML的自動裝配

Spring可以完成自動化的注入,自動化注入又被稱為 自動裝配,可以根據名字進行自動裝配,也可以根據型別進行自動裝配

自動裝配也是基於set方法的

  1. 根據名字進行自動裝配:被注入物件的id是注入類中屬性的set方法去掉方法名後首字母小寫的字串

以OrderService、OrderDao為例:

    <bean id="orderService" class="com.eun.spring.service.OrderService" >
        <property name="orderDao" ref="orderDao" />
    </bean>
    <bean id="orderDao" class="com.eun.spring.dao.OrderDao" />

這是之前的寫法,沒有問題

自動裝配:

    <!--根據名字進行自動裝配-->
    <bean id="orderService" class="com.eun.spring.service.OrderService" autowire="byName" />
    <!--被注入bean的id要和注入類中屬性的set方法去掉方法名後首字母小寫的字串相等-->
    <bean id="orderDao" class="com.eun.spring.dao.OrderDao" />
  1. 根據型別進行自動裝配

以CustomerDao為例:

public class CustomerService {
    private UserDao userDao;
    private VipDao vipDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    public void setVipDao(VipDao vipDao) {
        this.vipDao = vipDao;
    }
    
    public void save(){
        userDao.insert();
        vipDao.insert();
    }
}
    <bean id="userDao" class="com.eun.spring.dao.UserDao" />
    <bean id="vipDao" class="com.eun.spring.dao.VipDao" />
    <bean id="customerService" class="com.eun.spring.service.CustomerService" autowire="byType"/>

但是依據型別進行自動裝配時有效的配置檔案中必須有且僅有一個符合的型別,否則會報錯:

NoUniqueBeanDefinitionException: No qualifying bean of type 'com.eun.spring.dao.UserDao' available: expected single matching bean but found 2:

Bean的作用域

singleton

預設情況下,Spring的IoC容器建立的Bean物件是單例的,測試:

    @Test
    public void testBeanScope(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
        SpringBean springBean = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean); //com.eun.spring.bean.SpringBean@38234a38

        SpringBean springBean2 = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean2); //com.eun.spring.bean.SpringBean@38234a38

        SpringBean springBean3 = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean3); //com.eun.spring.bean.SpringBean@38234a38
    }
public class SpringBean {
    private static final Logger logger = LoggerFactory.getLogger(SpringBean.class);
    public SpringBean() {
        logger.info("SpringBean的無參構造方法執行了");
    }
}

如果將三個建立物件的程式碼註釋,觀察構造方法執行:

    public void testBeanScope(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
       //2023-06-26 15:09:59 076 [main] INFO com.eun.spring.bean.SpringBean - SpringBean的無參構造方法執行了
        
/*        SpringBean springBean = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean); //com.eun.spring.bean.SpringBean@38234a38

        SpringBean springBean2 = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean2); //com.eun.spring.bean.SpringBean@38234a38

        SpringBean springBean3 = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println(springBean3); //com.eun.spring.bean.SpringBean@38234a38*/
    }

說明預設情況下在初始化Spring上下文時例項化bean物件,每次getBean方法執行時獲得到的都是相同的物件

如果希望呼叫getBean()方法獲取的是不同的物件,也就是不希望在初始化Spring上下文時例項化bean物件,可以使用scope屬性設定bean的作用域:

<bean id="springBean" class="com.eun.spring.bean.SpringBean" scope="prototype"/>
    @Test
    public void testBeanScope(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
        
        SpringBean springBean = applicationContext.getBean("springBean", SpringBean.class);
        //SpringBean的無參構造方法執行了
        System.out.println(springBean); //com.eun.spring.bean.SpringBean@59f63e24

        SpringBean springBean2 = applicationContext.getBean("springBean", SpringBean.class);
        //SpringBean的無參構造方法執行了
        System.out.println(springBean2); //com.eun.spring.bean.SpringBean@61f05988

        SpringBean springBean3 = applicationContext.getBean("springBean", SpringBean.class);
        //SpringBean的無參構造方法執行了
        System.out.println(springBean3); //com.eun.spring.bean.SpringBean@7ca33c24
    }

當bean的scope屬性被設定為prototype時:

  • bean是多例的
  • spring上下文初始化時,並不會初始化這些prototype的bean
  • 每一次呼叫getBean方法就會例項化
scope

在web專案中,scope還有其他選項:

  • request:一次請求中一個bean
  • session:一次會話中一個bean
image-20230626152634085

scope屬性的值一共有8個:

  • singleton:預設的,單例。
  • prototype:原型。每呼叫一次getBean()方法則獲取一個新的Bean物件。或每次注入的時候都是新物件。
  • request:一個請求對應一個Bean。僅限於在WEB應用中使用
  • session:一個會話對應一個Bean。僅限於在WEB應用中使用
  • global session:portlet應用中專用的。如果在Servlet的WEB應用中使用global session的話,和session一個效果。(portlet和servlet都是規範。servlet執行在servlet容器中,例如Tomcat。portlet執行在portlet容器中。)
  • application:一個應用對應一個Bean。僅限於在WEB應用中使用。
  • websocket:一個websocket生命週期對應一個Bean。僅限於在WEB應用中使用。
  • 自定義scope:很少使用。

自定義scope:定義一個執行緒級別的Scope,在同一個執行緒中,獲取的Bean都是同一個,跨執行緒則是不同的物件

  1. 自定義scope需要實現scope介面,spring內建了執行緒範圍的類:org.springframework.context.support.SimpleThreadScope,可以直接使用

  2. 將自定義的Scope註冊到Spring容器中

    <!--使用自定義作用域配置器指定scope範圍-->
    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer" >
        <property name="scopes">
            <map> <!--map集合可以指定多個範圍-->
                <entry key="threadScope"> <!--作用域的名字-->
                    <!--注入內部bean-->
                    <bean class="org.springframework.context.support.SimpleThreadScope" />
                </entry>
            </map>
        </property>
    </bean>
image-20230626154400452

SimpleThreadScope物件放在了CustomScopeConfigurer的scopes集合中

  1. 使用自定義scope
<bean id="springBean" class="com.eun.spring.bean.SpringBean" scope="threadScope" />
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
        SpringBean bean_1InThread_1 = applicationContext.getBean("springBean", SpringBean.class);
        SpringBean bean_2InThread_1 = applicationContext.getBean("springBean", SpringBean.class);
        System.out.println("bean_1InThread_1 = " + bean_1InThread_1);
		//bean_1InThread_1 = com.eun.spring.bean.SpringBean@37ddb69a
        System.out.println("bean_2InThread_1 = " + bean_2InThread_1);
		//bean_2InThread_1 = com.eun.spring.bean.SpringBean@37ddb69a
        
		new Thread(()->{
            SpringBean bean_1InThread_2 = applicationContext.getBean("springBean", SpringBean.class);
            SpringBean bean_2InThread_2 = applicationContext.getBean("springBean", SpringBean.class);
            
            System.out.println("bean_1InThread_2 = " + bean_1InThread_2);
            //bean_1InThread_2 = com.eun.spring.bean.SpringBean@449b3217
            System.out.println("bean_2InThread_2 = " + bean_2InThread_2);
            //bean_2InThread_2 = com.eun.spring.bean.SpringBean@449b3217
        }).start();

GoF 工廠模式

設計模式:一種可以被重複利用的解決方案。

《Design Patterns: Elements of Reusable Object-Oriented Software》(即《設計模式》一書),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。這幾位作者常被稱為"四人組(Gang of Four)"

不過除了GoF23種設計模式之外,還有其它的設計模式,比如:JavaEE的設計模式(DAO模式、MVC模式等)

GoF23種設計模式可分為三大類:

  • 建立型(5個):解決物件建立問題。
    • 單例模式
    • 工廠方法模式
    • 抽象工廠模式
    • 建造者模式
    • 原型模式
  • 結構型(7個):一些類或物件組合在一起的經典結構。
    • 代理模式
    • 裝飾模式
    • 介面卡模式
    • 組合模式
    • 享元模式
    • 外觀模式
    • 橋接模式
  • 行為型(11個):解決類或物件之間的互動問題。
    • 策略模式
    • 模板方法模式
    • 責任鏈模式
    • 觀察者模式
    • 迭代子模式
    • 命令模式
    • 備忘錄模式
    • 狀態模式
    • 訪問者模式
    • 中介者模式
    • 直譯器模式

工廠模式

工廠模式是解決物件建立問題的,所以工廠模式屬於建立型設計模式。

  • 第一種:簡單工廠模式(Simple Factory):不屬於23種設計模式之一。簡單工廠模式又叫做:靜態 工廠方法模式。簡單工廠模式是工廠方法模式的一種特殊實現。
  • 第二種:工廠方法模式(Factory Method):是23種設計模式之一。
  • 第三種:抽象工廠模式(Abstract Factory):是23種設計模式之一。

簡單工廠模式

角色:

  1. 抽象產品
  2. 具體產品
  3. 工廠類
public class WeaponFactory {
    public static Weapon getWeapon(WeaponEnum weaponEnum){
        Weapon weapon = null;
        switch (weaponEnum){
            case DAGGER -> weapon = new Dagger();
            case FIGHTER -> weapon = new Fighter();
            case TANK -> weapon = new Tank();
            default -> throw new RuntimeException("no such weapon");
        }
        return weapon;
    }
}
classDiagram Weapon <|-- Tank: 繼承 Weapon <|-- Fighter: 繼承 Weapon <|-- Dagger: 繼承 class Weapon{ <<abstract>> +abstract attack() void } class Tank{ +attack() void } class Fighter{ +attack() void } class Dagger{ +attack() void } class WeaponFactory{ +createWeapon(WeaponEnum enum) Weapon } Weapon <.. WeaponFactory : 依賴

對於客戶端程式來說,不需要關心具體產品的生產細節,只需要向工廠索要即可

簡單工廠模式初步達成了 “職責分離 ” :工廠類負責生產,客戶端負責消費,生產者和消費者分離。

        Weapon dagger = WeaponFactory.getWeapon(WeaponEnum.DAGGER);
        Weapon tank = WeaponFactory.getWeapon(WeaponEnum.TANK);
        Weapon fighter = WeaponFactory.getWeapon(WeaponEnum.FIGHTER);

簡單工廠模式的缺點:

  • 缺點1:工廠類集中了所有產品的創造邏輯,形成一個無所不知的全能類,有人把它叫做上帝類。顯然工廠類非常關鍵,不能出問題,一旦出問題,整個系統癱瘓。
  • 缺點2:不符合OCP開閉原則,在進行系統擴充套件時,需要修改工廠類。

Spring中的BeanFactory就使用了簡單工廠模式。

工廠方法模式

對簡單工廠模式進行改進:之前多個產品對應一個工廠,修改一處就牽一髮而動全身,現在將每個產品都單獨對應一個工廠,一個工廠對應生產一種產品,工廠就不是全能類了

工廠方法模式既保留了簡單工廠模式的優點,同 時又解決了簡單工廠模式的缺點。

工廠方法模式的角色包括:

  • **抽象工廠角色 ** WeaponFactory
  • 具體工廠角色 DaggerFactory
  • 抽象產品角色 Weapon
  • 具體產品角色 Dagger
classDiagram class Weapon { <<abstract>> + abstract attack() void } class WeaponFactory { <<abstract>> + abstract getWeapon() Weapon } class DaggerFactory { + getWeapon() Weapon } class Gun { + attack() void } class Dagger { + attack() void } class GunFactory { + getWeapon() Weapon } WeaponFactory <|-- DaggerFactory Weapon <|-- Gun Weapon <|-- Dagger WeaponFactory <|--GunFactory WeaponFactory ..>Weapon
public abstract class WeaponFactory {
    public abstract Weapon getWeapon(); // 例項方法
}

這樣在擴充套件時符合OCP,新增Fighter類時只需要1. 定義Fitghter整合Weapon 2. 定義FighterFactory繼承WeaponFactory

工廠方法模式的優點:

  • 一個呼叫者想建立一個物件,只要知道其名稱就可以了。

    	Weapon dagger = new DaggerFactory().getWeapon(); 
    
  • 擴充套件性高,如果想增加一個產品,只要擴充套件一個工廠類就可以。

  • 遮蔽產品的具體實現,呼叫者只關心產品的介面。

工廠方法模式的缺點:

  • 每次增加一個產品時,都需要增加一個具體類和物件實現工廠,使得系統中類的個數成倍增加,在一定程度上增加了系統的複雜度,同時也增加了系統具體類的依賴。這並不是什麼好事。

抽象工廠模式

Bean的獲取方式

Spring為Bean提供了多種獲取方式,通常有四種:

  1. 構造方法例項化
  2. 簡單工廠模式獲取
  3. factory-bean獲取
  4. FactoryBean介面獲取

構造方法例項化

預設情況下就是呼叫Bean的構造方法進行例項化

簡單工廠模式例項化

public class Star {
    private static final Logger logger = LoggerFactory.getLogger(Star.class);
    public Star() {
        logger.info("Star constructor executed");
    }   
}

工廠模式,就需要提供一個工廠類:

public class StarFactory {
    public static Star getStar(){
        //在靜態方法中new物件返回
        Star star = new Star();
        return star;
    }
}

在spring配置檔案中配置工廠類:

    <!--告知Spring框架,透過哪個類的哪個方法獲取Bean-->
    <!-- factory-method指定工廠類中的靜態方法 -->
    <bean id="starBean" class="com.eun.bean.StarFactory" factory-method="getStar"/>

factory-bean 工廠方法模式例項化

public class Gun {
    public Gun() {
        System.out.println("Gun constructor execute");
    }
}
/**
	工廠方法模式的方法是例項方法
*/
public class GunFactory {
    public Gun getGun(){
        return new Gun();
    }
}

如果想呼叫這個例項方法,spring框架就要先建立出GunFactory物件,建立這個物件就是將這個物件交給spring框架進行管理

    <!-- 工廠方法模式,透過factory-bean屬性 + factory-method屬性共同完成 -->
    <!-- 告訴spring框架,呼叫哪個物件的哪個方法來獲取bean-->
    <bean id="gunFactory" class="com.eun.bean.GunFactory" />
    <bean id="gun" factory-bean="gunFactory" factory-method="getGun"/> <!--不需要指定class-->

FactoryBean介面

如果工廠類直接實現FactoryBean介面後,factory-bean就不需要指定了,factory-method也不需要指定了

factory-bean會自動指向實現FactoryBean介面的類,factory-method會自動指向getObject()方法

定義工廠類,實現FactoryBean介面:

public class PersonFactory implements FactoryBean<Person> {
    @Override
    public Person getObject() throws Exception {
        return new Person();
    }

    @Override
    public Class<?> getObjectType() {
        return Person.class;
    }
}

預設是單例的,如果想多例就重寫預設方法isSingleton

PersonFactory是一個特殊的Bean:工廠Bean,透過工廠Bean這個特殊的Bean可以獲取一個普通的Bean

後三種方式與第一種方式不同的是,可以經過工廠的加工再將Bean物件交給Spring容器,給了程式設計師操作的機會

<!--透過特殊的工廠Bean返回普通Bean-->
<bean id="personBean" class="com.eun.bean.PersonFactory" />

主要目的是透過factory-bean對普通bean進行加工處理

BeanFactory和FactoryBean有何區別

BeanFactory

Spring IoC容器的頂級物件,BeanFactory被翻譯為“Bean工廠”,在Spring的IoC容器中,“Bean工廠”負責建立Bean物件。

BeanFactory是工廠。

FactoryBean

FactoryBean:它是一個Bean,是一個能夠輔助Spring例項化其它Bean物件的一個Bean。

在Spring中,Bean可以分為兩類:

  • 第一類:普通Bean
  • 第二類:工廠Bean(記住:工廠Bean也是一種Bean,只不過這種Bean比較特殊,它可以輔助Spring例項化其它Bean物件。)

注入自定義Date

java.util.Date在Spring中被當作簡單型別,簡單型別在注入的時候可以直接使用value屬性或value標籤,但是我們之前也測試過了,對於Date型別,如果使用value進行注入,對字串的格式有要求:Mon Oct 10 14:30:26 CST 2022,其他的格式是不會被識別的

在這種情況下,我們就可以透過FactoryBean介面介入Date物件的建立過程,獲得指定時間的Date物件

對Student型別的birth屬性進行注入:

public class Student {
    private Date birth;

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @Override
    public String toString() {
        return "Student{" +
                "birth=" + birth +
                '}';
    }
}

要求:根據指定的時間獲取Date物件,比如:1999-10-11,得到對應的Date物件並注入

這就不能將Date作為簡單型別進行注入了,只能將Date作為引用型別,並且根據傳入的引數返回指定時間的Date物件

可以藉助FactoryBean介入Date物件的建立過程:

public class DateFactoryBean implements FactoryBean<Date> {
    private String birth;

    public DateFactoryBean(String birth) {
        this.birth = birth;
    }

    @Override
    public Date getObject() throws Exception {
        return new SimpleDateFormat("yyyy-MM-dd").parse(birth);
    }

    @Override
    public Class<?> getObjectType() {
        return null;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:p="http://www.springframework.org/schema/p"
       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="dateBean" class="com.eun.bean.DateFactoryBean" c:_0="1980-10-11"/>
    
    <bean id="studentBean" class="com.eun.bean.Student" p:birth-ref="dateBean"/>
</beans>

在建立Date物件時,透過構造方法注入為其傳遞字串時間,獲得指定的Date物件,再將Date物件注入給Student物件

DateFactory中也可以使用set注入

工廠Bean可以協助Spring建立普通Bean物件

如果此處使用自動裝配,不管是byName還是byType都是null:

    <bean id="birth" class="com.eun.bean.DateFactoryBean" c:time="1970-1-1"/>
    <bean id="studentBean" class="com.eun.bean.Student" autowire="byName"/>

Bean的生命週期

Spring就是一個管理Bean物件的容器,負責物件的建立、物件的銷燬等

生命週期:從建立到銷燬的整個過程

  • 為什麼要學習生命週期?

其實生命週期的本質是:在哪個時間節點上呼叫了哪個類的哪個方法。

我們需要充分的瞭解在這個生命線上,都有哪些特殊的時間節點。

只有我們知道了特殊的時間節點都在哪,到時我們才可以確定程式碼寫到哪。

我們可能需要在某個特殊的時間點上執行一段特定的程式碼,這段程式碼就可以放到這個節點上。當生命線走到這裡的時候,自然會被呼叫。

5步生命週期

  • 第一步:例項化Bean,呼叫無引數構造方法
  • 第二步:Bean屬性賦值,呼叫set方法
  • 第三步:初始化Bean,呼叫Bean的init方法(需要自己寫)
  • 第四步:使用Bean
  • 第五步:銷燬Bean,呼叫Bean的destroy方法(需要自己寫)
image-20230626210818026

AbstractAutowireCapableBeanFactory類的doCreateBean()方法

public class User {
    private String name;
    public User() {
        System.out.println("第一步:無引數構造方法執行");
    }

    public void setName(String name) {
        System.out.println("第二步:給物件的屬性賦值");
        this.name = name;
    }
    public void initBean(){
        System.out.println("第三步:初始化Bean");
    }
    public void destroyBean(){
        System.out.println("第五步:銷燬Bean");
    }
}

配置initBean()destroyBean()

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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="user" class="com.eun.bean.User" 
          init-method="initBean" destroy-method="destroyBean" p:name="zhangsan"/>
</beans>

使用bean:

    @Test
    public void testBeanLifeCycle(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User user = applicationContext.getBean("user", User.class);
        System.out.println("第四步:使用bean " + user);

        //必須手動關閉Spring容器才會銷燬Bean
        ((ClassPathXmlApplicationContext) applicationContext).close();
    }

/*
第一步:無引數構造方法執行
第二步:給物件的屬性賦值
第三步:初始化Bean
第四步:使用bean User{name='zhangsan'}
2023-06-26 21:23:49 957 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@6cd24612, started on Mon Jun 26 21:23:49 CST 2023
第五步:銷燬Bean
*/

close()ClassPathXmlApplicationContext 的方法,在ApplicationContext類中沒有

prototype的bean銷燬?

7步生命週期

在初始化Bean的前後還有兩個階段,被稱為 “初始化前” 和 “初始化後”,如果想在這兩個階段操作需要定義一個Bean後處理器

  • 第一步:例項化Bean,呼叫無引數構造方法
  • 第二步:Bean屬性賦值,呼叫set方法
  • 第三步:呼叫Bean後處理器的before()方法
  • 第四步:初始化Bean,呼叫Bean的init方法(需要自己寫)
  • 第五步:呼叫Bean後處理器的after()方法
  • 第六步:使用Bean
  • 第七步:銷燬Bean,呼叫Bean的destroy方法(需要自己寫)

定義日誌Bean後處理器,繼承BeanPostProcessor介面,並重寫before()after()方法,在初始化前後都記錄日誌:

public class LogBeanPostProcessor implements BeanPostProcessor {
    private static final Logger logger = LoggerFactory.getLogger(LogBeanPostProcessor.class);

    /**
     * 
     * @param bean 剛建立的bean物件
     * @param beanName bean物件的名字
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        logger.info("Bean後處理器的before方法");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }

    /**
     *
     * @param bean 剛建立的bean物件
     * @param beanName bean物件的名字
     * @return
     * @throws BeansException
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        logger.info("Bean後處理器的after方法");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }
}

配置Bean後處理器:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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後處理器-->
    <bean class="com.eun.bean.LogBeanPostProcessor" />

    <bean id="user" class="com.eun.bean.User"
          init-method="initBean" destroy-method="destroyBean" p:name="zhangsan"/>
</beans>

Bean後處理器將作用於當前的配置檔案中所有的Bean

image-20230627083937783
第一步:無引數構造方法執行
第二步:給物件的屬性賦值
第三步:Bean後處理器的before方法
第四步:初始化Bean
第五步:Bean後處理器的after方法
第六步:使用bean User{name='zhangsan'}
2023-06-27 08:45:26 125 [main] DEBUG org.springframework.context.support.ClassPathXmlApplicationContext - Closing org.springframework.context.support.ClassPathXmlApplicationContext@6cd24612, started on Tue Jun 27 08:45:25 CST 2023
第五步:銷燬Bean

10步生命週期

image-20230627084053440

新增的三步:

點位1:在bean後處理器before方法之前,檢查是否實現Aware相關介面

Aware相關的介面包括:BeanNameAwareBeanClassLoaderAwareBeanFactoryAware

  • 當Bean實現了BeanNameAware,Spring會將Bean的名字傳遞給Bean。

    Snipaste_2023-06-27_08-50-00
  • 當Bean實現了BeanClassLoaderAware,Spring會將載入該Bean的類載入器傳遞給Bean。

    image-20230627085014532
  • 當Bean實現了BeanFactoryAware,Spring會將Bean工廠物件傳遞給Bean。

    image-20230627085026611

點位2:在bean後處理器before方法之後,檢查Bean是否實現了InitializingBean介面

image-20230627085605551

點位3: 銷燬bean之前,檢查Bean是否實現了DisposableBean介面

image-20230627085819484

介面中的destroy()方法是在我們定義的destroy-method執行之前執行的

  • InitializingBean的方法早於init-method的執行。
  • DisposableBean的方法早於destroy-method的執行。

不同作用域的管理方式

Spring會根據Bean的作用域選擇管理方式

  • singleton:Spring精確的知道該Bean何時被建立,何時初始化完成,何時被銷燬,Spring進行完整生命週期管理
  • prototype:Spring只負責建立,建立之後就將Bean交給客戶端程式碼管理,Spring不再追蹤其生命週期

只負責建立:建立階段的幾個生命週期也是會執行的

將上文中示例改為prototype

第一步:無引數構造方法執行
第二步:給物件的屬性賦值
第三步:檢查是否實現BeanNameAware介面 user
第三步:檢查是否實現BeanClassLoaderAware介面 jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
第三步:檢查是否實現BeanFactoryAware介面 org.springframework.beans.factory.support.DefaultListableBeanFactory@31dadd46: defining beans [com.eun.bean.LogBeanPostProcessor#0,user]; root of factory hierarchy
第四步:Bean後處理器的before方法
第五步:檢查是否實現InitializingBean介面
第六步:初始化Bean
第七步:Bean後處理器的after方法
第八步:使用bean User{name='zhangsan'}

最終銷燬前、銷燬時這兩個階段沒有執行

自己new的物件交給Spring管理

    @Test
    public void testRegisterBean(){
        //自己new物件
        Student student = new Student();
        System.out.println(student);

        //註冊
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        factory.registerSingleton("studentBean",student);
        
        //從Spring容器中獲取
        Student studentBean = factory.getBean("studentBean", Student.class);
        System.out.println(studentBean);
    }

Bean的迴圈依賴

A物件中有B屬性,B物件中有A屬性,這就是迴圈依賴;比如:丈夫類Husband,妻子類Wife。Husband中有Wife的引用。Wife中有Husband的引用。

image-20230627091723531
public class Wife {
    private String name;
    private Husband husband;
}
public class Husband {
    private String name;
    private Wife wife;
}

在生成toString方法時注意:

在Wife中生成toString,輸出時會呼叫Husband的toString方法,Husband中的toString方法還會呼叫Wife中的toString方法;所以在Wife中輸出時要呼叫husband.getName()

singleton下的set注入產生的迴圈依賴

新增set方法並測試:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:p="http://www.springframework.org/schema/p"
       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="husbandBean" class="com.eun.spring.bean.Husband" p:wife-ref="wifeBean" p:name="zhangsan"/>
    <bean id="wifeBean" class="com.eun.spring.bean.Wife" p:husband-ref="husbandBean" p:name="lisi"/>

</beans>
    @Test
    public void testSingletonSetter(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Wife wife = applicationContext.getBean("wifeBean", Wife.class);
        Husband husband = applicationContext.getBean("husbandBean", Husband.class);
        System.out.println(wife); //Wife{name='lisi', husband=zhangsan}
        System.out.println(husband); //Husband{name='zhangsan', wife=lisi}
    }

這樣做是沒有問題的,因為singleton下基於set方法的注入是事先建立物件,再透過set方法進行賦值,例項化物件和物件屬性賦值的操作分離了

singleton是事先建立好所有的bean,將這些bean儲存在Spring容器中

在這種模式下,Spring對Bean的建立分為兩個階段:

第一階段:在Spring容器載入的時候,例項化Bean,只要其中任意一個Bean例項化之後,立刻曝光(未進行屬性賦值)

第二階段:Bean曝光之後再進行屬性的賦值

第二階段結束才完成了Spring容器的初始化

核心解決方案:例項化和物件的屬性賦值分為兩個階段來完成

Bean建立完畢後,對屬性進行賦值,husband的p:wife-ref屬性賦值wifeBean,這個賦值操作是可以成功的,因為在上一個階段中wifeBean建立成功後就進行曝光了。

注意:只有在scope是singleton的情況下才會進行提前曝光的操作,因為這個物件是要事先建立好的

prototype下的set注入產生的迴圈依賴

    <bean id="husbandBean" class="com.eun.spring.bean.Husband" 
          p:wife-ref="wifeBean" p:name="zhangsan" scope="prototype"/>
    <bean id="wifeBean" class="com.eun.spring.bean.Wife" 
          p:husband-ref="husbandBean" p:name="lisi" scope="prototype"/>
    @Test
    public void testSingletonSetter(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        Wife wife = applicationContext.getBean("wifeBean", Wife.class);
        //Husband husband = applicationContext.getBean("husbandBean", Husband.class);

    }

如果是prototype,在初始化容器時並不會對其進行建立和賦值的操作,建立這個Bean時會進行初始化、屬性賦值、初始化,建立好直接交給客戶端程式使用;在建立wife的時候,根據配置資訊會對wifeBean的husband屬性賦值,賦值內容為husbandBean對應的bean,而husbandBean在建立時需要對其wife屬性進行賦值,根據配置資訊會將wifeBean賦值給它(這裡並不會將第一步的wifeBean賦值給它,因為沒有儲存在map集合中),但是對於prototype作用域的bean來說,建立的bean不會被加入Spring儲存Bean的map集合中,也就是每次使用到這個bean,都會建立出新的物件使用,所以對於當前要給husbandBean的wife屬性賦值,一定會建立一個新的Wife物件,而建立這個Wife物件根據配置資訊會對他的husband屬性賦值,也就是:

  • 第四行程式碼要獲取一個wife物件,根據配置檔案需要對Wife類的husband屬性進行賦值,會建立一個新的Husband物件
  • 建立Husband物件要對wife屬性進行賦值,根據配置檔案新建一個Wife物件
  • 新建Wife物件時,Spring某種機制檢測到這和第一步重複了,報錯:當前的Bean正在建立中
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'wifeBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'husbandBean' while setting bean property 'husband'; 
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'husbandBean' defined in class path resource [spring.xml]: Cannot resolve reference to bean 'wifeBean' while setting bean property 'wife'; 
nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'wifeBean': Requested bean is currently in creation: Is there an unresolvable circular reference? #當前的Bean正在建立中

原因在於:

  1. prototype的Bean建立和賦值是不可分隔的,沒有提前曝光,必須將這兩個操作都完成才能返回這個物件給客戶端
  2. 第二步中,建立Husband物件時要對Wife屬性進行賦值,檢測到Wife是prototype就會建立一個新的物件

prototype + setter下的迴圈依賴就會出現異常BeanCurrentlyInCreationException 當前的Bean正在建立中

只要其中一個是Singleton就沒有問題,對於當前的Wife物件的建立來說:

  • WifeBean是Singleton:Spring容器最開始就建立WifeBean,建立完畢進行屬性的賦值,在給husband屬性賦值時建立Husband物件時就會將單例的Wife賦值給Husband,Husband建立完畢賦值給WifeBean的husband屬性,WifeBean初始化完成

  • HusbandBean是Singleton:Spring容器最開始就會建立Husband,建立完畢對Wife屬性賦值,新建Wife物件,將單例的Husband賦值給Wife物件,Wife物件建立完畢賦值給Husband,Husband完成初始化存貯在map集合中,在程式中建立Wife物件時直接將單例的Husband賦值給husband屬性,完成Wife的建立

    但是這樣做,Wife物件中的Husband屬性都是同一個

構造注入

  • singleton模式:
    <bean id="husbandBean" class="com.eun.spring.bean.Husband" 
          c:name="zhangsan" c:wife-ref="wifeBean" scope="singleton"/>
    <bean id="wifeBean" class="com.eun.spring.bean.Wife" 
          c:name="lisi" c:husband-ref="husbandBean" scope="singleton"/>

構造注入在建立物件時就需要進行賦值,也就是沒有提前曝光的操作了,對於husbandBean和wifeBean來說,都是在Spring容器啟動時建立並初始化,假設先建立husbandBean物件,需要對構造方法進行wife-ref注入,這樣會使用wifeBean物件,這個物件是單例的,對這個物件的建立需要使用husband物件,而第一步的husband物件還未建立完畢,此時再建立husband物件也是出錯:當前Bean正在建立中

  • prototype模式:也無法解決這個問題

構造注入的迴圈依賴是無法解決的

迴圈依賴的原始碼

為什麼set + singleton模式下的迴圈依賴可以解決?

根本的原因在於:這種方式可以做到將“例項化Bean”和“給Bean屬性賦值”這兩個動作分開去完成。

例項化Bean的時候:呼叫無引數構造方法來完成。此時可以先不給屬性賦值,可以提前將該Bean物件“曝光”給外界。

給Bean屬性賦值的時候:呼叫setter方法來完成。

兩個步驟是完全可以分離開去完成的,並且這兩步不要求在同一個時間點上完成。

也就是說,Bean都是單例的,我們可以先把所有的單例Bean例項化出來,放到一個集合當中(我們可以稱之為快取),所有的單例Bean全部例項化完成之後,以後再的呼叫setter方法給屬性賦值。這樣就解決了迴圈依賴的問題。

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
			throws BeanCreationException {

		// Instantiate the bean.
		BeanWrapper instanceWrapper = null;
		if (mbd.isSingleton()) {
			instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
		}
		if (instanceWrapper == null) {
			instanceWrapper = createBeanInstance(beanName, mbd, args);
		}
       	/*建立Bean*/
		Object bean = instanceWrapper.getWrappedInstance();
		Class<?> beanType = instanceWrapper.getWrappedClass();
		if (beanType != NullBean.class) {
			mbd.resolvedTargetType = beanType;
		}

		// Allow post-processors to modify the merged bean definition.
		synchronized (mbd.postProcessingLock) {
			if (!mbd.postProcessed) {
				try {
					applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
				}
				catch (Throwable ex) {
					throw new BeanCreationException(mbd.getResourceDescription(), beanName,
							"Post-processing of merged bean definition failed", ex);
				}
				mbd.postProcessed = true;
			}
		}

    	//急切快取bean可以解決迴圈引用
		// Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
				isSingletonCurrentlyInCreation(beanName));
		if (earlySingletonExposure) {
			if (logger.isTraceEnabled()) {
				logger.trace("Eagerly caching bean '" + beanName +
						"' to allow for resolving potential circular references");
			}
            /*加入快取,提前曝光*/
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

		// Initialize the bean instance.
		Object exposedObject = bean;
		try {
            /*填充bean:給Bena的屬性賦值*/
			populateBean(beanName, mbd, instanceWrapper);
			exposedObject = initializeBean(beanName, exposedObject, mbd);
		}
		catch (Throwable ex) {
			if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
				throw (BeanCreationException) ex;
			}
			else {
				throw new BeanCreationException(
						mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
			}
		}
  1. 建立Bean,未進行賦值:
image-20230627134542554
  1. 將Bean加入快取,提前曝光
image-20230627134838421

進入這個方法,進入了DefaultSingletonBeanRegistry類,該類有三個重要的屬性:

image-20230627135029294
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); //一級快取

/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); //二級快取

/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);  //三級快取

這三個快取都是Map集合,Map集合的key儲存的都是Bean的id

  • 一級快取:儲存完整的單例Bean物件,這個快取中的Bean物件屬性已經完成賦值了

  • 二級快取:儲存早期單例Bean物件,這個快取中的單例Bean物件的屬性沒有賦值

  • 三級快取:單例工廠集合物件,儲存的是工廠物件,每一個單例Bean都會對應一個工廠物件

    ​ 這個集合中儲存的是建立該物件時對應的單例工廠物件

在該方法中:

image-20230627135817871

最終在獲取這個bean物件時:

	@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
        //嘗試從一級快取中獲取
		Object singletonObject = this.singletonObjects.get(beanName);
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			singletonObject = this.earlySingletonObjects.get(beanName);
			if (singletonObject == null && allowEarlyReference) {
                //兩層判斷防抖
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					singletonObject = this.singletonObjects.get(beanName);
                    //判斷能否從一級快取中獲取到
					if (singletonObject == null) {
						singletonObject = this.earlySingletonObjects.get(beanName);
                        //判斷能否從二級快取中獲取到                      
						if (singletonObject == null) {
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
                            //如果工廠存在
							if (singletonFactory != null) {
								singletonObject = singletonFactory.getObject();
                                //將Bean物件放入二級快取
								this.earlySingletonObjects.put(beanName, singletonObject);
                                //將工廠從三級快取中移除
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

myspring

準備工作:

使用者提供bean類:

public class User {
    private String name;
    private int age;

    public User() {
    }
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    public void setAge(int age) {
        this.age = age;
    }
}
public class UserDao {
    public void insert(){
        System.out.println("mysql database saving data");
    }
}
public class UserService {
    private UserDao userDao;

    public UserService() {
    }
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
    public void save(){
        userDao.insert();
    }
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

使用者提供配置檔案:

<?xml version="1.0" encoding="UTF-8" ?>
<beans>
    <bean id="user" class="com.eun.myspring.bean.User" >
        <property name = "name" value="zhangsan"/>
        <property name = "age" value="30"/>
    </bean>

    <bean id="userDao" class="com.eun.myspring.bean.UserDao" />

    <bean id="userService" class="com.eun.myspring.bean.UserService" >
        <property name="userDao" ref="userDao"/>
    </bean>
</beans>

myspring:

public class ClassPathXMLApplicationContext implements ApplicationContext {
    private final Map<String,Object> earlySingletonObjects = new HashMap<>();
    private final Map<String, Object> singletonObjects = new HashMap<>();
	private static final Logger logger = LoggerFactory.getLogger(ClassPathXMLApplicationContext.class);
    
    /**
     * 解析myspring配置檔案,初始化所有的bean物件
     * @param configLocation
     */
    public ClassPathXMLApplicationContext(String configLocation) {
        try {
            InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(configLocation);
            Document document = new SAXReader().read(is);
            initEarlySingletonObjects(document);
            initSingletonObjects(document);
            System.out.println("singletonObjects = " + singletonObjects);
            System.out.println("earlySingletonObjects = " + earlySingletonObjects);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 提前曝光
     * @param document
     */
    private void initEarlySingletonObjects(Document document) {
        List<Node> nodes = document.selectNodes("//bean");
        nodes.stream().map(node -> (Element) node).forEach(element -> {
            try {
                String idValue = element.attributeValue(Constant.ID);
                String classValue = element.attributeValue(Constant.CLASS);
                Class<?> clazz = Class.forName(classValue);
                Object bean = clazz.getConstructor().newInstance();
                earlySingletonObjects.put(idValue,bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
    
    private void initSingletonObjects(Document document){
        document.selectNodes("//bean").stream().map(node -> (Element) node).forEach(element -> {
            try {
                String id = element.attributeValue(Constant.ID);
                List<Element> properties = element.elements();
                Object bean = earlySingletonObjects.get(id);
                Class<?> clazz = Class.forName(element.attributeValue(Constant.CLASS));
                if (properties.size() != 0){
                    properties.stream().forEach(propertyElement -> {
                        String fieldName = propertyElement.attributeValue(Constant.NAME);
                        String simpleValue = propertyElement.attributeValue(Constant.VALUE);
                        String refValue = propertyElement.attributeValue(Constant.REF);
                        if (simpleValue != null){
                            setSimpleValue(bean, clazz, fieldName, simpleValue);
                        }else {
                            setRefValue(bean, clazz, fieldName, refValue);
                        }
                    });
                }
                earlySingletonObjects.remove(id);
                singletonObjects.put(id,bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }
    public static String getSetMethodName(String fieldName){
        return Constant.SET + fieldName.toUpperCase().charAt(0) + fieldName.substring(1);
    }
    
    private void setRefValue(Object bean, Class<?> clazz, String fieldName, String refValue){
        try {
            Object refObj = earlySingletonObjects.get(refValue);
            if (refObj == null){
                refObj = singletonObjects.get(refValue);
            }
            Class<?> fieldType = clazz.getDeclaredField(fieldName).getType();
            String setMethodName = getSetMethodName(fieldName);
            Method setMethod = clazz.getDeclaredMethod(setMethodName,fieldType);
            setMethod.invoke(bean,refObj);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void setSimpleValue(Object bean, Class<?> clazz, String fieldName, String simpleValue) {
        try {
            Class<?> fieldType = clazz.getDeclaredField(fieldName).getType();
            String setMethodName = getSetMethodName(fieldName);
            Method setMethod = clazz.getDeclaredMethod(setMethodName,fieldType);
            setMethod.invoke(bean,getPropertyVal(fieldType.getSimpleName(), simpleValue));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private Object getPropertyVal(String simpleName, String valueStr) {
    Object propertyVal = null;
        switch (simpleName) {
            case "byte": case "Byte":
                propertyVal = Byte.valueOf(valueStr);
                break;
            case "short": case "Short":
                propertyVal = Short.valueOf(valueStr);
                break;
            case "int": case "Integer":
                propertyVal = Integer.valueOf(valueStr);
                break;
            case "long": case "Long":
                propertyVal = Long.valueOf(valueStr);
                break;
            case "float": case "Float":
                propertyVal = Float.valueOf(valueStr);
                break;
            case "double": case "Double":
                propertyVal = Double.valueOf(valueStr);
                break;
            case "boolean": case "Boolean":
                propertyVal = Boolean.valueOf(valueStr);
                break;
            case "char": case "Character":
                propertyVal = valueStr.charAt(0);
                break;
            case "String":
                propertyVal = valueStr;
                break;
        }
        return propertyVal;
    }


    @Override
    public Object getBean(String beanName) {
        Object bean = singletonObjects.get(beanName);
        if (bean == null) {
            throw new RuntimeException("bean doesn't exists");
        }
        return bean;
    }

    @Override
    public <T> T getBean(String beanName, Class<T> beanType) {
        Object bean = getBean(beanName);
        return (T) bean;
    }
}

Spring IoC註解式開發

註解是為了簡化XML的配置,Spring6倡導全註解開發

需求:給定包名,掃描包下所有的.java檔案,只要有Component註解就建立物件

  • 獲取包下所有檔案需要使用File類的list方法,生成File物件需要將包名轉換為絕對路徑,如何轉換?
        String packageName = "com.eun.myspring.bean";
        String packagePath = packageName.replaceAll("\\.", "/");
        URL resource = ClassLoader.getSystemClassLoader().getResource(packagePath);

        //獲取絕對路徑
        String path = resource.getPath();
        File f = new File(path);
        String[] files = f.list();
        Arrays.stream(files).map(clazzPath -> packageName + "." + clazzPath.split("\\.")[0])
            .forEach(clazzPath -> {
            System.out.println(clazzPath);
			/**
				com.eun.myspring.bean.User
                com.eun.myspring.bean.UserDao
                com.eun.myspring.bean.UserService
			*/
        });

宣告Bean的註解

  • Component 元件
  • Controller 控制器
  • Service 業務
  • Repository DAO

原始碼如下:

image-20230627183350339 image-20230627183423461 image-20230627183504545 image-20230627183537488

Controller、Service、Repository都是Component註解的別名,為了增強程式的可讀性

註解的使用

  1. 加入aop依賴(包含在spring-context中)

    image-20230627184107776
  2. 配置檔案上新增context名稱空間

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:context="http://www.springframework.org/schema/context"
           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
                               
                              http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
    </beans>
    
  3. 配置檔案中指定掃描的包

    類似於上文中的練習,至少告知包名才能進掃描

        <!--指定要掃描的包-->
        <context:component-scan base-package="com.eun.spring.bean"/>
    
  4. Bean上使用註解

    @Repository("studentBean")
    public class Student {
    
    }
    
    Student studentBean = applicationContext.getBean("studentBean", Student.class); 
    

上面的原始碼可以看到這些註解都是有預設值的:

image-20230627190356037

可以省略為 Component ,預設的名稱是類名首字母的小寫

@Repository
public class Student {
    
}
Student student = applicationContext.getBean("student",Student.class);
//com.eun.spring.bean.Student@b78a709
多個包的掃描問題
image-20230627190743822
  • 第一種解決辦法:在配置檔案中指定多個包,用 ,隔開
    <!--指定要掃描的包-->
    <context:component-scan base-package="com.eun.spring.bean,com.eun.spring.dao"/>
  • 第二種解決辦法:指定多個包的共同父包(犧牲一定效率)
    <!--指定要掃描的包-->
    <context:component-scan base-package="com.eun.spring"/>

選擇化例項bean

假設在某個包下有很多Bean,有的Bean上標註了Component,有的標註了Controller,有的標註了Service,有的標註了Repository,現在由於某種特殊業務的需要,只允許其中所有的Controller參與Bean管理,其他的都不例項化。這應該怎麼辦呢?

@Component
public class A {
    public A() {
        System.out.println("A的無引數構造方法執行");
    }
}

@Controller
class B {
    public B() {
        System.out.println("B的無引數構造方法執行");
    }
}

@Service
class C {
    public C() {
        System.out.println("C的無引數構造方法執行");
    }
}

@Repository
class D {
    public D() {
        System.out.println("D的無引數構造方法執行");
    }
}

@Controller
class E {
    public E() {
        System.out.println("E的無引數構造方法執行");
    }
}

@Controller
class F {
    public F() {
        System.out.println("F的無引數構造方法執行");
    }
}
  • 第一種解決方案:use-default-filters="false" 讓指定包下所有宣告bean的註解全部失效
<!--讓指定包下宣告Bean的註解全部失效-->
<context:component-scan base-package="com.eun.spring.bean" use-default-filters="false">
    <!--使用include指定可以生效的註解-->
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>

此時只有@Repository@Service可以生效,對應C、D類:

2023-06-27 19:31:36 104 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'c'
C的無引數構造方法執行
2023-06-27 19:31:36 107 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'd'
D的無引數構造方法執行
  • 第二種解決方案:use-default-filters="true" 讓指定包下宣告Bean的註解全部生效
<!--讓指定包下宣告Bean的註解全部生效-->
<context:component-scan base-package="com.eun.spring.bean" use-default-filters="true">
    <!--使用exclude指定不包含的註解-->
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>

此時A、B、E、F是有效的

注意:Component註解不能排除掉,排除Component所有宣告註解都會失效

注入註解

宣告Bean後進行例項化,需要對屬性進行注入:

  • @Value 注入簡單型別
  • @Autowired
  • @Qualifier
  • @Resource

簡單型別 @Value

@Component
public class MyDataSource implements DataSource {
    @Value("com.mysql.cj.jdbc.Driver")
    private String driver;
    @Value("jdbc:mysql://localhost:3306/mybatis_db")
    private String url;
    @Value("root")
    private String username;
    @Value("root")
    private String password;
}
    @Test
    public void testDIByAnnotation(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        MyDataSource myDataSource = applicationContext.getBean("myDataSource", MyDataSource.class);
        System.out.println(myDataSource);
        /**
2023-06-27 19:43:31 795 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'myDataSource'
MyDataSource{driver='com.mysql.cj.jdbc.Driver', url='jdbc:mysql://localhost:3306/mybatis_db', username='root', password='root'}
         */
    }

使用@value註解屬性可以不寫set方法,靠反射注入(預設就是反射注入)

@Value也可以寫在set方法上:

public class MyDataSource implements DataSource {
    
    private String driver;
    
    private String url;
    
    private String username;
    
    private String password;
    
    @Value("com.mysql.cj.jdbc.Driver")
    public void setDriver(String driver) {
        System.out.println("setDriver method");
        this.driver = driver;
    }
    @Value("jdbc:mysql://localhost:3306/mybatis_db")
    public void setUrl(String url) {
        System.out.println("setUrl method");
        this.url = url;
    }
    @Value("root")
    public void setUsername(String username) {
        System.out.println("setUsername method");
        this.username = username;
    }
    @Value("root")
    public void setPassword(String password) {
        System.out.println("setPassword method");
        this.password = password;
    }
}
setDriver method
setPassword method
setUrl method
setUsername method
MyDataSource{driver='com.mysql.cj.jdbc.Driver', url='jdbc:mysql://localhost:3306/mybatis_db', username='root', password='root'}

使用@Value註解set方法是依靠set方法注入

@Value 註解也可以用在構造方法上:

public class MyDataSource implements DataSource {

    private String driver;

    private String url;

    private String username;

    private String password;

    public MyDataSource(@Value("com.mysql.cj.jdbc.Driver")String driver,
                        @Value("jdbc:mysql://localhost:3306/mybatis_db")String url,
                        @Value("root") String username,
                        @Value("root") String password) {
        this.driver = driver;
        this.url = url;
        this.username = username;
        this.password = password;
    }

使用@Value註解構造方法是依靠構造方法注入

  • 使用在屬性上:呼叫無參構造建立物件,使用反射注入
  • 使用在set方法上:呼叫無參構造建立物件,使用set方法注入
  • 使用在有參構造上:使用構造注入

非簡單型別 @Autowired @Qualifier

@Autowired可以注入非簡單型別,翻譯為:自動裝配

image-20230627195614936

單獨使用@Autowired註解,是基於型別進行自動裝配 byType,如果想使用基於名稱by Name的自動裝配需要配合@Qualifier

image-20230627200227328
<?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">
    <context:component-scan base-package="org.eun" />
</beans>
@Repository
public class OrderDaoImplForMySQL implements OrderDao {
    @Override
    public void insert() {
        System.out.println("MySQL database saving order info");
    }
}
@Service
public class OrderService {
    //根據型別進行自動裝配
    @Autowired
    private OrderDao orderDao;
    public void generate(){
        orderDao.insert();
    }
}

測試:

    @Test
    public void testDIByAnnotation(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-choose.xml");
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
        orderService.generate(); //MySQL database saving order info
    }

這時是沒有問題的,但是如果介面下有兩個實現類就不能根據型別進行裝配了:

image-20230627200802291 image-20230627201008734
exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.eun.dao.OrderDao' available: expected single matching bean but found 2: orderDaoImplForMySQL,orderDaoImplForOracle

根據型別進行自動裝配時,當前有效的配置檔案中只能有且僅有一個匹配的型別

想解決這個問題,只能根據名字進行裝配

@Service
public class OrderService {
    //根據型別進行自動裝配
    @Autowired
    @Qualifier("orderDaoImplForOracle")
    private OrderDao orderDao;
    public void generate(){
        orderDao.insert();
    }
}

使用@Autowired沒有提供set方法,所以set方法也不是必須的

使用@Autowired註解set方法:

public class OrderService {

    private OrderDao orderDao;

    @Autowired
    public void setOrderDao(OrderDao orderDao) {
        System.out.println("setOrderDao executed");
        this.orderDao = orderDao;
    }

    public void generate(){
        orderDao.insert();
    }
}
/**
setOrderDao executed
MySQL database saving order info */

說明@Autowired可以使用set方法進行自動裝配

使用@Autowired註解構造方法:

@Service
public class OrderService {

    private OrderDao orderDao;

    @Autowired
    public OrderService(OrderDao orderDao) {
        System.out.println("constructor");
        this.orderDao = orderDao;
    }

    public void setOrderDao(OrderDao orderDao) {
        System.out.println("setOrderDao executed");
        this.orderDao = orderDao;
    }

    public void generate(){
        orderDao.insert();
    }
}
/**
constructor
MySQL database saving order info
*/

說明@Autowired可以使用構造方法進行自動裝配

使用@Autowired註解構造方法的引數:

@Service
public class OrderService {

    private OrderDao orderDao;
    
    public OrderService(@Autowired OrderDao orderDao) {
        System.out.println("constructor");
        this.orderDao = orderDao;
    }

    public void setOrderDao(OrderDao orderDao) {
        System.out.println("setOrderDao executed");
        this.orderDao = orderDao;
    }

    public void generate(){
        orderDao.insert();
    }
}
/**
constructor
MySQL database saving order info
*/

說明@Autowired可以使用構造方法的引數進行自動裝配

省略Autowired

@Service
public class OrderService {

    private OrderDao orderDao;

    public OrderService(OrderDao orderDao) {
        System.out.println("constructor");
        this.orderDao = orderDao;
    }

    public void generate(){
        orderDao.insert();
    }
}
/**
constructor
MySQL database saving order info
*/

要求:構造方法必須有且僅有一個,並且構造方法的引數和類中的非簡單型別相同,這時就可以省略(最好不要省略)

  • 使用在屬性上:(預設)無參構造建立物件,反射注入

    但是使用反射是不推薦的

    image-20230809214220733

    如果沒有無參構造,會使用有參構造建立物件(並可能進行反射賦值)

  • 使用在set方法上:(預設)無參構造建立物件,set注入

    如果使用在set方法上,並且沒有無參構造,會使用有參構造建立物件(此時已賦值),並且執行set方法

  • 使用在構造方法/引數上:構造注入

  • autowired省略:只能有一個構造方法,並且引數列表和例項變數型別相同

單獨使用Autowired是根據型別進行自動裝配,如果介面下有多個實現類就無法完成裝配,必須聯合Qualifier使用

image-20231202081219185

非簡單型別 @Resource

@Resource註解也可以完成非簡單型別注入。那它和@Autowired註解有什麼區別?

  • @Resource註解是JDK擴充套件包中的,也就是說屬於JDK的一部分。所以該註解是標準註解,更加具有通用性。(JSR-250標準中制定的註解型別。JSR是Java規範提案。);@Autowired註解是Spring框架自己的。
  • @Resource註解預設根據名稱裝配byName,未指定name時,使用屬性名作為name。透過預設的name找不到的話會自動啟動透過型別byType裝配。
  • @Autowired註解預設根據型別裝配byType,如果想根據名稱裝配,需要配合@Qualifier註解一起用。
  • @Resource註解用在屬性上、setter方法上。
  • @Autowired註解用在屬性上、setter方法上、構造方法上、構造方法引數上。

@Resource註解屬於JDK擴充套件包,所以不在JDK當中,需要額外引入以下依賴:【如果是JDK8的話不需要額外引入依賴。高於JDK11或低於JDK8需要引入以下依賴。

<dependency>
  <groupId>jakarta.annotation</groupId>
  <artifactId>jakarta.annotation-api</artifactId>
  <version>2.1.1</version>
</dependency>

Spring6不支援JavaEE,支援JakartaEE9;Spring5及以下版本使用javax

image-20230627203535441

屬性上:

@Service
public class OrderService {

    //@Resource 預設name找不到 使用型別裝配
    @Resource(name = "orderDaoImplForMySQL")
    private OrderDao orderDao;

    public void generate(){
        orderDao.insert();
    }
}

如果不指定name,會使用屬性名orderDao作為name,但是預設的name找不到對應的bean,所以自動使用型別裝配,如果此時有兩個OrderDao的實現類就會報錯

NoUniqueBeanDefinitionException: No qualifying bean of type 'org.eun.dao.OrderDao' available: expected single matching bean but found 2: orderDaoImplForMySQL,orderDaoImplForOracle

set方法上:

@Service
public class OrderService {

    private OrderDao orderDao;

    @Resource(name = "orderDaoImplForMySQL")
    public void setOrderDao(OrderDao orderDao) {
        this.orderDao = orderDao;
    }

    public void generate(){
        orderDao.insert();
    }
}

全註解式開發

<?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">
    <context:component-scan base-package="org.eun" />
</beans>

現在的配置檔案中只有元件掃描的內容,可以不寫這個配置檔案了,使用配置類代替:

@Configuration
@ComponentScan("org.eun")
public class SpringConfig {

}
  • @Configuration :指明是配置類
  • @ComponentScan("org.eun") 元件掃描範圍

在獲取bean例項的時候:

@Test
public void testDIByAnnotation(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
	OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

Jdbc Template

Jdbc Template是Spring提供的一個JDBC模板類,封裝了JDBC的程式碼

        <!--新增的依賴:mysql驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!--新增的依賴:spring jdbc,這個依賴中有JdbcTemplate-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0-M2</version>
        </dependency>

透過JdbcTemplate物件進行CRUD,Spring容器管理JdbcTemplate物件:

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" />
    </bean>

需要指定資料來源,可以使用Druid、c3p0、dbcp等資料庫連線池,也可以使用自定義資料庫連線池

public class MyDataSource implements DataSource {

    private Properties properties;
    @Override
    public Connection getConnection() throws SQLException {
        Connection connection = null;
        try {
            Class.forName(properties.getProperty("driver"));
            connection = DriverManager.getConnection(properties.getProperty("url"), 
                    properties.getProperty("username"), 
                    properties.getProperty("password"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return connection;
    }

    public MyDataSource(Properties properties) {
        this.properties = properties;
    }
}
<?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:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       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/util http://www.springframework.org/schema/util/spring-util.xsd
                          http://www.springframework.org/schema/context   http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="jdbc.properties"/>

    <bean id="myDataSource" class="com.eun.bean.MyDataSource" p:properties-ref="prop"/>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" 
          p:dataSource-ref="myDataSource"/>
</beans>
    @Test
    public void test() throws SQLException {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        System.out.println(jdbcTemplate);
        //org.springframework.jdbc.core.JdbcTemplate@178213b
    }

環境配置成功

另一種配置方法:

    <context:component-scan base-package="com.eun.bean" />
    <util:properties id="props" location="jdbc.properties" />

引入外部配置檔案,開啟componentScan,使用註解注入:

image-20230810002249752

手動建立JdbcTemplate物件,納入Spring容器的管理中:

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = new JdbcTemplate(applicationContext
                                                     .getBean("myDataSource", MyDataSource.class));
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        factory.registerSingleton("jdbcTemplate",jdbcTemplate);

        JdbcTemplate template = factory.getBean("jdbcTemplate", JdbcTemplate.class);
        System.out.println(template);

insert

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "insert into t_user(real_name,age) values(?,?)";

        int updateCount = jdbcTemplate.update(sql,"王五",20); //可變長引數 給 ? 傳值
        System.out.println(updateCount);

update

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "update t_user set real_name = ? where id = ?";
        int updateCount = jdbcTemplate.update(sql,"張三丰",1);
        System.out.println(updateCount);

delete

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "delete from t_user where id = ?";
        int updateCount = jdbcTemplate.update(sql,1);
        System.out.println(updateCount);

selectOne

        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select * from t_user where id = ?";
        User u = jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper<>(User.class),2);
        System.out.println(u);

queryForObject方法三個引數:

  • 第一個引數:sql語句
  • 第二個引數:Bean屬性值和資料庫記錄行的對映物件。在構造方法中指定對映的物件型別。
  • 第三個引數:可變長引數,給sql語句的佔位符問號傳值。

資料庫中的欄位名和Java Bean都要符合各自的命名規範

selectAll

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select * from t_user";
        List<User> users = jdbcTemplate.query(sql,new BeanPropertyRowMapper<>(User.class));
        System.out.println(users);
        //[User{id = 2, realName = 李四, age = 33}, User{id = 3, realName = 王五, age = 20}]

查一個值

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select count(*) from t_user";
        Integer total = jdbcTemplate.queryForObject(sql, int.class);
        System.out.println(total);

批次新增

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "insert into t_user(id,real_name,age) values(?,?,?)";
        Object[] objs1 = {null, "小花", 20};
        Object[] objs2 = {null, "小明", 21};
        Object[] objs3 = {null, "小剛", 22};
        List<Object[]> list = new ArrayList<>();
        list.add(objs1);
        list.add(objs2);
        list.add(objs3);
		//batchUpdate
        int[] count = jdbcTemplate.batchUpdate(sql, list);
        System.out.println(Arrays.toString(count));//[1, 1, 1]

批次更新

        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        // 批次修改
        String sql = "update t_user set real_name = ?, age = ? where id = ?";
        Object[] objs1 = {"小花11", 10, 2};
        Object[] objs2 = {"小明22", 12, 3};
        Object[] objs3 = {"小剛33", 9, 4};
        List<Object[]> list = new ArrayList<>();
        list.add(objs1);
        list.add(objs2);
        list.add(objs3);

        int[] count = jdbcTemplate.batchUpdate(sql, list);
        System.out.println(Arrays.toString(count));//[1, 1, 1]

批次刪除

    JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
    // 批次刪除
    String sql = "delete from t_user where id = ?";
    Object[] objs1 = {2};
    Object[] objs2 = {3};
    Object[] objs3 = {4};
    List<Object[]> list = new ArrayList<>();
    list.add(objs1);
    list.add(objs2);
    list.add(objs3);
    int[] count = jdbcTemplate.batchUpdate(sql, list);
    System.out.println(Arrays.toString(count));

回撥函式

    @Test
    public void testCallback(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        JdbcTemplate jdbcTemplate = applicationContext.getBean("jdbcTemplate", JdbcTemplate.class);
        String sql = "select id,real_name,age from t_user where id = ?";
        //當execute方法執行的時候,回撥函式中doInPreparedStatement會被呼叫
        jdbcTemplate.execute(sql, new PreparedStatementCallback<User>() {
            @Override
            public User doInPreparedStatement(PreparedStatement ps) throws Exception{
                ps.setInt(1,2);
                ResultSet rs = ps.executeQuery();
                User user = null;
                if (rs.next()) {
                    user = new User();
                    user.setId(rs.getInt("id"));
                    user.setRealName(rs.getString("real_name"));
                    user.setAge(rs.getInt("age"));
                }
                return user;
            }
        });
    }

使用druid連線池

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>druid</artifactId>
  <version>1.1.8</version>
</dependency>

將druid資料來源配置到配置檔案中:

    <context:property-placeholder location="jdbc.properties"/>

    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <!--根據原始碼得知set方法名稱-->
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" 
          p:dataSource-ref="druidDataSource"/>

GoF代理模式

  1. 當一個物件需要保護時
  2. 給某個物件進行功能增強時
  3. A和B物件無法直接互動時

可以考慮使用代理模式,代理模式屬於結構性設計模式

代理模式的作用是:為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個客戶不想或者不能直接引用一個物件,此時可以透過一個稱之為“代理”的第三者來實現間接引用。代理物件可以在客戶端和目標物件之間起到中介的作用,並且可以透過代理物件去掉客戶不應該看到的內容和服務或者新增客戶需要的額外服務。 透過引入一個新的物件來實現對真實物件的操作或者將新的物件作為真實物件的一個替身,這種實現機制即為代理模式,透過引入代理物件來間接訪問一個物件,這就是代理模式的模式動機。客戶端程式無法察覺使用代理物件或者目標物件。

代理模式在程式碼實現上有兩種方式:

  • 靜態代理
  • 動態代理
public interface OrderService {
    /**
     * 生成訂單資訊
     */
    void generate();

    /**
     * 修改訂單資訊
     */
    void modify();

    /**
     * 檢視訂單資訊
     */
    void detail();
}

業務需求:統計所有業務類中業務方法的耗時

  • 解決方案一:硬編碼新增統計耗時的程式
image-20230628153729366

缺點:

  1. 違背OCP
  2. 程式碼沒有複用
  3. 業務類很多時,太麻煩,去掉這個程式碼也是很麻煩

靜態代理

代理是一種模式,提供了目標物件的間接訪問方式,透過代理物件可以訪問目標物件,以便於在實現目標的基礎上增加額外的功能操作,例如前攔截、後攔截等

圖片.png

靜態代理:編寫一個代理類,實現和目標物件相同的介面,並在內部維護一個目標物件的引用。透過構造器注入目標物件,在代理物件中呼叫目標物件的同名方法,新增前後攔截等業務功能

圖片.png
  • 解決方案二:靜態代理

加入代理類:OrderServiceProxy

image-20230628154803529
public class OrderServiceProxy implements OrderService{
    OrderService target;

    public OrderServiceProxy(OrderService target) {
        this.target = target;
    }

    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        
        target.generate();
        
        long end = System.currentTimeMillis();
        System.out.println("耗時:" + (end - begin));
    }
//....
}

呼叫:

        OrderService target = new OrderServiceImpl();
        OrderService orderServiceProxy = new OrderServiceProxy(target);

        orderServiceProxy.generate();
        orderServiceProxy.detail();
        orderServiceProxy.modify();

關聯的耦合度比繼承低,但是會導致“類爆炸”問題,一百個介面就要寫一百個代理類

可以使用位元組碼生成技術生成代理類物件,也就是動態代理

動態代理

程式執行階段在記憶體中動態生成代理類,被稱為動態代理,目的是為了減少代理類的數量,解決程式碼複用

動態代理技術:

  • JDK動態代理:只能代理介面
  • CGLIB:Code Generation Library,開源、高質量、高效能的Code生成類庫,可以在執行期間擴充套件Java類和介面,既可以代理介面,又可以代理類,透過繼承的方式實現,效能比JDK動態代理好,底層有一個位元組碼處理框架ASM
  • Javassist:開源的分析、編輯、建立Java位元組碼的類庫,為JBoss應用伺服器實現動態AOP

JDK動態代理

    OrderService target = new OrderServiceImpl();
    OrderService orderServiceProxy = (OrderService) Proxy.
            /*
            * newProxyInstance:新建代理物件,透過呼叫這個方法在記憶體中建立代理物件
            *   這個方法
            *       1.在記憶體中動態生成一個代理類的位元組碼Class物件
            *       2. 根據Class物件建立了代理物件例項並返回
            *   引數一:類載入器 記憶體中生成的位元組碼也是class檔案,要執行也要載入到記憶體中
            *                載入類就需要類載入器相同,並且JDK要求代理類的類載入器必須和目標類的類載入器相同
            *   引數二:代理類要實現的介面:代理類要和目標類實現同一組介面,代理類實現了這組介面就可以向下轉型
            *   引數三:呼叫處理器 :當代理物件呼叫代理方法的時候,註冊在呼叫處理器中的invoke方法被呼叫
            * */
            newProxyInstance(target.getClass().getClassLoader(),
                            target.getClass().getInterfaces(),
                    /**
                     * 引數一:代理物件
                     * 引數二:目標物件的目標方法
                     * 引數三:呼叫代理方法時傳遞的引數
                     */
                    (proxy,method,argvs) -> {
                                System.out.println("advance");
                                return method.invoke(target,argvs);
                            });
    orderServiceProxy.detail();
    orderServiceProxy.modify();
    orderServiceProxy.generate();
public class ProxyUtil {
    public static <T> T newProxyInstance(T t, InvocationHandler handler) {
        return (T) Proxy.newProxyInstance(t.getClass().getClassLoader(),
                t.getClass().getInterfaces(),
                handler);
    }
}

CGLIB動態代理

CGLIB既可以代理介面,又可以代理類,底層使用繼承方式實現,所以被代理的類不能用final修飾

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>
public class UserService {
    //目標方法
    public boolean login(String username,String password){
        System.out.println("系統正在退出");
        return "admin".equals(username) && "123".equals(password);
    }
    //目標方法
    public void logout(){
        System.out.println("系統正在退出");
    }
}

CGLIB可以在記憶體中直接生成代理:

        //建立位元組碼增強器物件,依靠它生成代理類
        Enhancer enhancer = new Enhancer();

        //告知CGLIB父類 目標類是誰
        enhancer.setSuperclass(UserService.class);

        //設定回撥  MethodInterceptor 等同於 InvocationHandler
        enhancer.setCallback(new MethodInterceptor() {
            /**
             *  函式式介面
             * @param target 目標物件
             * @param method
             * @param objects 方法實參
             * @param methodProxy 方法
             * @return
             * @throws Throwable
             */
            @Override
            public Object intercept(Object target, Method method, 
                                    Object[] objects, MethodProxy methodProxy) throws Throwable {
                long begin = System.currentTimeMillis();
                Object retVal = methodProxy.invokeSuper(target, objects);
                long end = System.currentTimeMillis();
                System.out.println("duration : " + (end - begin) + "ms");
                return retVal;
            }
        });

        //建立代理物件
        //1. 在記憶體中生成UserService類的子類 也就是代理類的位元組碼
        //2. 建立代理物件
        // 父類是UserService,子類也是UserService
        UserService userServiceProxy = (UserService) enhancer.create();

        boolean success = userServiceProxy.login("admin", "123");
        System.out.println(success ? "成功" : "失敗");
        userServiceProxy.logout();

需要新增兩個執行時引數:

- --add-opens java.base/java.lang=ALL-UNNAMED
- --add-opens java.base/sun.net.util=ALL-UNNAMED

CGLIB生成的代理物件的名字:

com.eun.service.UserService$$EnhancerByCGLIB$$51285760@63d4e2ba

面向切面AOP

IoC使元件松耦合,AOP能夠讓我們捕捉程式中經常使用的功能,將其轉化為元件

AOP:Aspect Orientend Programming 面向切面程式設計;AOP是對OOP的補充延申;AOP底層就是動態代理實現的

切面:程式中和業務邏輯無關的通用程式碼,比如事務控制

Spring的AOP使用的動態代理是:JDK動態代理 + CGLIB動態代理技術;Spring在這兩種動態代理中靈活切換,如果是代理介面,預設使用JDK動態代理,如果代理某個類,該類沒有實現介面,就會使用CGLIB;也可以透過某些配置讓Spring只使用CGLIB

AOP

一般一個系統當中都會有一些系統服務,例如:日誌、事務管理、安全等。這些系統服務被稱為:交叉業務

這些交叉業務幾乎是通用的,不管你是做銀行賬戶轉賬,還是刪除使用者資料。日誌、事務管理、安全,這些都是需要做的。

如果在每一個業務處理過程當中,都摻雜這些交叉業務程式碼進去的話,存在兩方面問題:

  • 第一:交叉業務程式碼在多個業務流程中反覆出現,顯然這個交叉業務程式碼沒有得到複用。並且修改這些交叉業務程式碼的話,需要修改多處。
  • 第二:程式設計師無法專注核心業務程式碼的編寫,在編寫核心業務程式碼的同時還需要處理這些交叉業務。

解決辦法:切面程式設計,使用動態代理將與業務邏輯無關的切面程式碼單獨提取出來放在呼叫處理器InvocationHandler中,只要在呼叫處理器中寫一次就可以了,單獨提取出來形成的東西就是切面;切面一旦形成在業務層就不需要寫與業務邏輯無關的程式碼了

image-20230628205827277

總結:將與核心業務無關的程式碼獨立的抽取出來,形成一個獨立的元件(切面),然後以橫向交叉的方式應用到業務流程當中的過程被稱為AOP

之前的動態代理就是AOP的體現。

AOP的優點:

  • 第一:程式碼複用性增強。
  • 第二:程式碼易維護。
  • 第三:使開發者更關注業務邏輯。

七大術語

public class UserService{
    public void do1(){
        System.out.println("do 1");
    }
    public void do2(){
        System.out.println("do 2");
    }
    public void do3(){
        System.out.println("do 3");
    }
    public void do4(){
        System.out.println("do 4");
    }
    public void do5(){
        System.out.println("do 5");
    }
    // 核心業務方法
    public void service(){
        do1();
        do2();
        do3();
        do5();
    }
}
  • 連線點 Joinpoint

    • 在程式的整個執行流程中,可以織入切面的位置。方法的執行前後,異常丟擲之後等位置。

          public void service(){
              try{
                  //JoinPoint 連線點
                  do1();
                  //JoinPoint
                  do2();
                  //JoinPoint
                  do3();
                  //JoinPoint
                  do5();
                  //JoinPoint
              } catch (Exception e){
                  //JoinPoint
              }
          }
      

      連線點描述的是位置

  • 切點 Pointcut

    • 程式執行流程中,真正織入切面的方法。(一個切點對應多個連線點)

      image-20230628211008487

      切點描述的是方法

  • 通知 Advice

    • 通知又叫增強,就是具體你要織入的程式碼

    • 通知包括:

      • 前置通知:程式碼織入在切點的前一個連線點位置

      • 後置通知:程式碼織入在切點的後一個連線點位置

      • 環繞通知:程式碼織入在切點的前後兩個連線點位置

      • 異常通知:程式碼織入在catch語句塊中的連線點位置

      • 最終通知:程式碼織入在finally語句塊中的連線點位置

    • 通知就是具體的事務、日誌、安全程式碼

  • 切面 Aspect

    • 切點 + 通知就是切面

      image-20230628211558915

  • 織入 Weaving

    • 把通知應用到目標物件上的過程。
  • 代理物件 Proxy

    • 一個目標物件被織入通知後產生的新物件。
  • 目標物件 Target

    • 被織入通知的物件。

image-20230628211719768

切點表示式

切點:某一個方法

切點表示式:匹配某些方法的表示式,用來定義通知 織入 哪些 切點

切點表示式的語法格式:

execution([訪問控制許可權修飾符] 返回值型別 [全限定類名]方法名(形式引數列表)[異常])

訪問許可權修飾符:

  • 可選項
  • 預設就是包括4個許可權
  • public 就是隻匹配公開的方法

返回值型別:

  • 必填
  • * 代表任意

全限定類名:

  • 可選項
  • .. 代表當前包以及子包下所有的類 例如 com..
  • 預設代表所有的類

方法名:

  • 必填
  • * 代表所有方法,例如set*表示所有set方法

形式引數列表:

  • 必填
  • () 表示沒有引數的方法
  • (..) 引數型別和個數隨意
  • (*) 只有一個引數的方法
  • (*,String) 第一個型別隨意,第二個型別是String

異常:

  • 可選項
  • 省略時表示任意異常型別
execution(public * com.eun.mall.service..delete*(..)) 
//com.eun.mall.service包下所有類中方法名以delete開始,返回值型別、個數隨意,返回值型別隨意的公開方法
execution(* com.eun.mall..*(..))
//com.eun.mall下所有類中的所有方法
execution(* *(..))
//所有方法

Spring AOP

Spring使用AOP包括以下三種方式:

  • 第一種方式:Spring框架結合Aspectj框架實現AOP,基於註解方式

  • 第二種方式:Spring框架結合Aspectj框架實現AOP,基於XML方式

  • 第三種方式:Spring框架自己實現的AOP,基於XML配置方式

實際開發中,都是Spring框架結合Aspectj框架基於註解實現AOP

Aspectj:Eclipse組織的一個支援AOP的框架。AspectJ框架是獨立於Spring框架之外的一個框架,Spring框架使用了AspectJ框架

<!--spring aspects依賴-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>6.0.0-M2</version>
</dependency>

image-20230629092228932

<?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"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="com.eun.service" />

    <aop:aspectj-autoproxy />
</beans>

目標類:

@Service
public class UserService {
    //目標方法
    public void login(){
        System.out.println("系統正在進行身份認證");
    }
}

對於沒有實現介面的類,會使用CGLIB動態代理

切面類:

@Aspect //切面類需要使用這個註解進行標註
@Component
public class LogAspect {
    //切面 = 通知 + 切點
    //通知以方法的形式出現
    //Before註解標註的是前置通知,value是切點表示式
    //@Before("execution(public void com.eun.service.UserService.login(..))")
    @Before("execution(* com.eun.service.UserService.*(..))")
    public void beforeAdvice(){
        System.out.println("這是一段前置通知");
    }
}

切面 = 通知 + 切點

    <context:component-scan base-package="com.eun.service" />

    <!--
        開啟aspectj的自動代理
        spring容器在掃描類的時候會檢視是否有@Aspect註解,如果有就根據該類中的資訊生成代理物件
        proxy-target-class="true"  : 必須使用CGLIB
    -->
    <aop:aspectj-autoproxy proxy-target-class="true"/>

測試程式:

    @Test
    public void testBeforeAdvice(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        UserService userService = applicationContext.getBean("userService", UserService.class);
        userService.login();
        /**
         * 這是一段前置通知
         * 系統正在進行身份認證
         */
    }

通知是動態織入的,說明getBean()獲取的是Proxy例項

通知型別

  • 前置通知:@Before 目標方法執行之前的通知

        @Before("execution(* com.eun.service..*(..))")
        public void beforeAdvice(){
            System.out.println("這是一段前置通知");
        }
    
  • 後置通知:@AfterReturning 目標方法執行之後的通知

        @AfterReturning("execution(* com.eun.service..*(..))")
        public void afterReturning(){
            System.out.println("這是一段後置通知");
        }
    
  • 環繞通知:@Around 目標方法之前新增通知,同時目標方法執行之後新增通知。

        @Around("execution(* com.eun.service..*(..))")
        public void around(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("前環繞");
            //執行目標
            joinPoint.proceed();
            
            System.out.println("後環繞");
        }
        /*
        *   前環繞
            這是一段前置通知
            生成訂單
            這是一段後置通知
            後環繞
        * */
    

    環繞通知是最大的範圍,在前置之前,後置(最終)之後

  • 異常通知:@AfterThrowing 發生異常之後執行的通知

    後置通知 後環繞都沒有了

        @AfterThrowing("execution(* com.eun.service..*(..))")
        public void afterThrowing(){
            System.out.println("異常通知");
        }
    
        public void generate(){
            if (1 == 1){
                throw new RuntimeException();
            }
            System.out.println("生成訂單");
        }
    	/**
        前環繞
        這是一段前置通知
        異常通知
        最終通知
    	*/
    
        public void generate(){
            System.out.println("生成訂單");
            if (1 == 1){
                throw new RuntimeException();
            }
        }
    	/**
        前環繞
        這是一段前置通知
        生成訂單
        異常通知
        最終通知
    	*/
    
  • 最終通知:@After 放在finally語句塊中的通知

        @After("execution(* com.eun.service..*(..))")
        public void finalAfter(){
            System.out.println("最終通知");
        }
        /**
        前環繞
        這是一段前置通知
        生成訂單
        這是一段後置通知
        最終通知
        後環繞
        */
    
前環繞 Around
	  前置通知 Before
	  method
	  後置通知 AfterReturning
	  
	  最終通知 After
後環繞 Around

業務方法在執行過程:

image-20230812003358942
#沒有異常                            #業務方法出現異常
前環繞								  前環繞
前置通知                             前置通知
生成訂單                             生成訂單
後置通知                             異常通知
最終通知							 最終通知
後環繞                               NullPointerException

出現了異常就沒有執行後置通知和後環繞,說明前後置通知、環繞通知在代理物件中的形式應該是

public final void generate() {
    try {
        this.h.invoke(this, m3, null);
        return;
    }
    catch (Error | RuntimeException throwable) {
        throw throwable;
    }
    catch (Throwable throwable) {
        throw new UndeclaredThrowableException(throwable);
    }
}
//而在invoke方法當中:
    @Test
    public void aTest(){
        try{
            System.out.println("前環繞");
            System.out.println("前置通知");
            System.out.println("生成訂單");
            String s = null;
            s.toString();
            System.out.println("後置通知");
        }catch (Exception e){
            System.out.println("異常通知");
            throw e;
        } finally {
            System.out.println("最終通知");
        }
        System.out.println("後環繞");
    }

因為方法最終執行失敗,並且異常資訊是在最終通知之後顯示的,而最終通知是方法結束時執行,所以此處一定是將異常上拋了

代理物件的代理方法中發現異常才會執行失敗

如果在環繞通知中新增try-catch:

    public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {

       System.out.println("前環繞");
        try {
            joinPoint.proceed();
        } catch (Throwable e) {
            //throw e;
            e.printStackTrace();
        }
        System.out.println("後環繞");
    }

選擇阻止上拋,方法會列印出異常資訊但是執行成功,輸出結果:

前環繞
前置通知
生成訂單
異常通知
最終通知
NullPointerException #e.printStackTrace()
後環繞

說明新增的try-catch一定是包含了原有的try-catch(因為異常並沒有拋給代理方法,並且內層try-catch觸發了異常通知和最終通知),後環繞可以正常的執行

對應的invoke:

    @Test
    public void aTest() {
        try {

            try {
                System.out.println("前環繞");
                System.out.println("前置通知");
                System.out.println("生成訂單");
                String s = null;
                s.toString();
                System.out.println("後置通知");
            } catch (Exception e) {
                System.out.println("異常通知");
                throw e;
            } finally {
                System.out.println("最終通知");
            }

        } catch (Exception e) {
            e.printStackTrace();
            //throw e;
        }
        System.out.println("後環繞");
    }

基於註解的切面順序

image-20230629120052499

如果還有其他的交叉業務,怎麼控制切面的執行順序

@Aspect
@Component
@Order(1)
public class SecurityAspect {
    @Before("execution(* com.eun.service..*(..))")
    public void beforeAdvice(){
        System.out.println("安全前置通知");
    }
}
@Aspect 
@Component
@Order(2)
public class LogAspect {
    @Before("execution(* com.eun.service..*(..))")
    public void beforeAdvice(){
        System.out.println("日誌前置通知");
    }

指定這兩個切面的順序:安全通知先執行,日誌通知後執行

@Order(number) number數字越小優先順序越高

通用切點

image-20230629120800776

這三個通知要織入給相同的方法,切點表示式都是一樣的,但是現在沒有程式碼複用

解決:@PointCut 通用切點

    @Pointcut("execution(* com.eun.service..*(..))")
    public void commonPointCut(){
        
    }

    @Before("commonPointCut()")
    public void beforeAdvice(){
        System.out.println("日誌前置通知");
    }

    @AfterReturning("commonPointCut()")
    public void afterReturning(){
        System.out.println("這是一段後置通知");
    }

    @Around("commonPointCut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("前環繞");
        joinPoint.proceed();
        System.out.println("後環繞");
    }

但是這是在LogAspect中定義的,如果想在SecurityAspect中使用:

@Aspect
@Component
@Order(1)
public class SecurityAspect {
    @Before("com.eun.service.LogAspect.commonPointCut()")
    public void beforeAdvice(){
        System.out.println("安全前置通知");
    }
}

連線點

在環繞通知中使用過連線點:

image-20230629123311844

其實在其他的通知中都有連線點引數:

    @Before("commonPointCut()")
    public void beforeAdvice(JoinPoint point){
        System.out.println("日誌前置通知");
        Signature signature = point.getSignature();
        System.out.print(Modifier.toString(signature.getModifiers()) + " ");
        System.out.print(signature.getName() + " (");
        System.out.println(signature.getDeclaringTypeName() + ")");
        //public generate (com.eun.service.OrderService)
    }

getSignature()作用是獲取目標方法簽名

AOP全註解開發

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    <context:component-scan base-package="com.eun.service" />
    
    <aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>

不寫配置檔案

@Configuration
@ComponentScan("com.eun.service")
/*啟用CGLIB自動代理*/
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringConfig {
}

測試:

@Test
public void testBeforeAdvice(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
    OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    orderService.generate();
}

AOP XML開發

第一步:編寫目標類

// 目標類
public class VipService {
    public void add(){
        System.out.println("儲存vip資訊。");
    }
}

第二步:編寫切面類,並且編寫通知

// 負責計時的切面類
public class TimerAspect {
    
    public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long begin = System.currentTimeMillis();
        //執行目標
        proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("耗時"+(end - begin)+"毫秒");
    }
}

第三步:編寫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"
       xmlns:context="http://www.springframework.org/schema/context"
       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/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--納入spring bean管理-->
    <bean id="vipService" class="com.powernode.spring6.service.VipService"/>
    <bean id="timerAspect" class="com.powernode.spring6.service.TimerAspect"/>

    <!--aop配置-->
    <aop:config>
        <!--切點表示式-->
        <aop:pointcut id="p" expression="execution(* com.powernode.spring6.service.VipService.*(..))"/>
        <!--切面-->
        <aop:aspect ref="timerAspect">
            <!--切面=通知 + 切點-->
            <aop:around method="time" pointcut-ref="p"/>
        </aop:aspect>
    </aop:config>
</beans>

程式設計式事務解決方案

專案中的事務控制是在所難免的。在一個業務流程當中,可能需要多條DML語句共同完成,為了保證資料的安全,這多條DML語句要麼同時成功,要麼同時失敗。這就需要新增事務控制的程式碼。例如以下虛擬碼:

class 業務類1{
    public void 業務方法1(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法2(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
}
class 業務類2{
    public void 業務方法1(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
    public void 業務方法2(){
        try{
            // 開啟事務
            startTransaction();
            
            // 執行核心業務邏輯
            step1();
            step2();
            step3();
            ....
            
            // 提交事務
            commitTransaction();
        }catch(Exception e){
            // 回滾事務
            rollbackTransaction();
        }
    }
}

可以看到,這些業務類中的每一個業務方法都是需要控制事務的,而控制事務的程式碼又是固定的格式,都是:

try{
    // 開啟事務
    startTransaction();

    // 執行核心業務邏輯
    //......

    // 提交事務
    commitTransaction();
}catch(Exception e){
    // 回滾事務
    rollbackTransaction();
}

這個控制事務的程式碼就是和業務邏輯沒有關係的“交叉業務”。以上虛擬碼當中可以看到這些交叉業務的程式碼沒有得到複用,並且如果這些交叉業務程式碼需要修改,那必然需要修改多處,難維護,怎麼解決?可以採用AOP思想解決。可以把以上控制事務的程式碼作為環繞通知,切入到目標類的方法當中。

目標類:

@Service
public class AccountService {

    public void transfer(){

        System.out.println("銀行賬戶正在完成轉賬操作");
        throw new RuntimeException();
    }

    public void withdraw(){
        System.out.println("銀行賬戶正在取款");
    }
}
@Service
public class OrderService {

    public void generate(){
        System.out.println("生成訂單");
    }

    public void cancel(){
        System.out.println("訂單已取消");
    }
}

切面:

@Aspect
@Component
public class TransactionAspect {
    @Around("execution(* com.eun.service..*(..))")
    public void aroundAdvice(ProceedingJoinPoint point){
        try {
            System.out.println("transaction begin");
            point.proceed();
            System.out.println("commit");
        } catch (Throwable e) {
            System.err.println("error");
            System.out.println("rollback");
        } finally {
            System.out.println("============");
        }
    }
}

測試:

transaction begin
銀行賬戶正在完成轉賬操作
error
rollback
============
transaction begin
銀行賬戶正在取款
commit
============

安全日誌解決方案

凡是在系統中進行修改操作的,刪除操作的,新增操作的,都要把使用者記錄下來。因為這幾個操作是屬於危險行為。例如有業務類和業務方法:

@Service
public class UserService {
    public void getUser(){
        System.out.println("獲取使用者資訊");
    }
    public void saveUser(){
        System.out.println("儲存使用者");
    }
    public void deleteUser(){
        System.out.println("刪除使用者");
    }
    public void modifyUser(){
        System.out.println("修改使用者");
    }
}
@Service
public class VipService {
    public void getVip(){
        System.out.println("獲取會員資訊");
    }
    public void saveVip(){
        System.out.println("儲存會員");
    }
    public void deleteVip(){
        System.out.println("刪除會員");
    }
    public void modifyVip(){
        System.out.println("修改會員");
    }
}

注意:只需要對save delete modify 方法進行記錄日誌的操作

也就是說要將通知織入到這三簇方法上

@Aspect
@Component
public class SecurityAspect {
    @Pointcut("execution(* com.eun.biz..*delete*(..))")
    public void deleteMethod(){}

    @Pointcut("execution(* com.eun.biz..*save*(..))")
    public void saveMethod(){}

    @Pointcut("execution(* com.eun.biz..*modify*(..))")
    public void modifyMethod(){}

    @Before("deleteMethod()||saveMethod()||modifyMethod()")
    public void beforeAdvice(JoinPoint joinPoint){
        System.out.println("某使用者正在操作:" + joinPoint.getSignature().getName() + "方法");
    }
}

Spring對事務的支援

上文中的事務控制是透過程式碼進行控制,是程式設計式的,Spring推出了宣告式事務解決方案

Spring-tx底層也是基於AOP進行事務控制的

  • 什麼是事務

    • 在一個業務流程當中,通常需要多條DML(insert delete update)語句共同聯合才能完成,這多條DML語句必須同時成功,或者同時失敗,這樣才能保證資料的安全。
    • 多條DML要麼同時成功,要麼同時失敗,這叫做事務。
    • 事務:Transaction(tx)
  • 事務的四個處理過程:

    • 第一步:開啟事務 (start transaction)
    • 第二步:執行核心業務程式碼
    • 第三步:提交事務(如果核心業務處理過程中沒有出現異常)(commit transaction)
    • 第四步:回滾事務(如果核心業務處理過程中出現異常)(rollback transaction)
  • 事務的四個特性:

    • A 原子性:事務是最小的工作單元,不可再分。
    • C 一致性:事務要求要麼同時成功,要麼同時失敗。事務前和事務後的總量不變。
    • I 隔離性:事務和事務之間因為有隔離性,才可以保證互不干擾。
    • D 永續性:永續性是事務結束的標誌。
    <!--倉庫-->
    <repositories>
        <!--spring里程碑版本的倉庫-->
        <repository>
            <id>repository.spring.milestone</id>
            <name>Spring Milestone Repository</name>
            <url>https://repo.spring.io/milestone</url>
        </repository>
    </repositories>

    <!--依賴-->
    <dependencies>
        <!--spring context-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--spring jdbc-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--mysql驅動-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
      <!--德魯伊連線池-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.13</version>
        </dependency>
      <!--@Resource註解-->
        <dependency>
            <groupId>jakarta.annotation</groupId>
            <artifactId>jakarta.annotation-api</artifactId>
            <version>2.1.1</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

</project>

事務場景:銀行轉賬

image-20230629144525517
<?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"
       xmlns:p="http://www.springframework.org/schema/p"
       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">
    
    <context:component-scan base-package="com.eun" />
    <context:property-placeholder location="jdbc.properties" />
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
          p:driverClassName="${jdbc.driver}" p:url="${jdbc.url}" 
          p:username="${jdbc.username}" p:password="${jdbc.password}" />
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" 
          p:dataSource-ref="dataSource"/>
    
</beans>
@Service("accountService")
public class AccountServiceImpl implements AccountService {
    @Resource
    private AccountDao accountDao;

    @Override
    public void transfer(String fromActno, String toActno, double money) {
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money){
            throw new MoneyNotEnoughException("money not enough");
        }
        Account toAct = accountDao.selectByActno(toActno);

        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);

        int count = accountDao.update(fromAct);
        count += accountDao.update(toAct);

        if (count != 2){
            throw new UnknownException("unknown exception");
        }

        System.out.println("轉賬成功");
    }

    public void setAccountDao(AccountDao accountDao) {
        this.accountDao = accountDao;
    }
}
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
    @Resource
    private JdbcTemplate jdbcTemplate;
    @Override
    public Account selectByActno(String actno) {
        String sql = "select * from t_act where actno = ?";
        Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
        return account;
    }

    @Override
    public int update(Account account) {
        String sql = "update t_act set balance = ? where actno = ?";
        int update = jdbcTemplate.update(sql, account.getBalance(), account.getActno());
        return update;
    }
}

image-20230629152312157

此時轉出賬戶的錢就會丟失,解決:

image-20230629153854710

通知:

image-20230629154118588

需要獲取到事務物件,使用Spring對事務的封裝

Spring實現事務的兩種方式:

  1. 程式設計式事務
    • 透過編寫程式碼實現事務的管理
  2. 宣告式事務
    • 基於註解方式
    • 基於XML方式

宣告式事務

  • 基於註解方式
  • 基於XML註解方式

Spring對事務的管理底層實現方式是基於AOP實現的,採用AOP的方式進行了封裝,所以Spring專門針對事務開發了一套API,API核心介面:

image-20230629160032914

PlatformTransactionManager:spring事務管理器核心介面,Spring6中有兩個實現:

  • DataSourceTransactionManager : 支援JdbcTemplate、Mybatis、Hibernate等事務管理,給其他ORM框架整合的機會
  • JtaTransactionManager :支援分散式事務管理

如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager來管理事務

配置事務管理器:

 <!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" 
          p:dataSource-ref="dataSource"/>

事務管理本質上就是:

conn.setAutoCommit(false);

所以事務管理器需要獲取到connection物件,資料來源提供connection物件

<?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:tx="http://www.springframework.org/schema/tx"  
       
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           
                            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" p:dataSource-ref="dataSource"/>
    <!--開啟事務註解驅動器 使用註解方式控制事務-->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

宣告式事務的註解實現

image-20230629160941672

此時事務成功開啟,在程式執行的過程中遇到異常就進行回滾:

image-20230629161912151

可以看到,第一條sql語句結束就發生了異常,直接進行了rollback

image-20230629162059247

如果在轉賬成功之後發生了異常:

image-20230629162152752

也是會進行回滾的,因為核心業務都被包含在代理物件的try之內,在任何時候發生異常都會被catch捕捉,進而回滾並上拋


image-20230629153421835

但是會織入具體的實現類:

image-20230629153455390
@Transactional的屬性
image-20230629162407211

其中:

事務的傳播行為
image-20230629162431726

propagation是事務的傳播行為:

在service類中有a()方法和b()方法,a()方法上有事務,b()方法上也有事務,當a()方法執行過程中呼叫了b()方法,事務是如何傳遞的?合併到一個事務裡?還是開啟一個新的事務?這就是事務傳播行為。

image-20230629163108686
事務傳播簡介

有如下程式碼:

@Service
public class AService{
    @Autowired
    private BService bService;
    
    public void order(){
        yy();
        bService.xx();
        zz();
    }
}

此時bService中的事務傳播到了aService中,可能生成的SQL語句如下:

BEGIN:
	update yy;
	-------事務分界線-------
		begin:
			update xx;
        commit;
    ----------------------
    update zz;
COMMIT;

MYSQL是不支援的

但是這裡很明顯是存在問題的,第二個begin執行時,對同一個Connection呼叫begin(setAutoCommit)等方法,會開啟一個新的事務,也就會隱式的將第一個事務直接提交,從而導致AService的部分事務失效

所以 B事務 傳播到 A事務 中,B事務需要進行一些調整,無外乎以下幾種情況:

  • 當AService中有事務:

第一種情況:融入A事務(幹掉B的事務),也就是保證兩個事務同時成功或失敗

形成的sql如下:

BEGIN:
	update yy;
	-------事務分界線-------
			update xx;
    ----------------------
COMMIT;

融入外界事務:兩個事務使用同一個Connection連線

第二種情況:掛起A事務,讓B事務獨立於A事務執行:不希望B的異常導致A回滾

需要兩個Connection連線,A事務執行到B事務的程式碼時停止執行,從資料來源中再獲取一個Connection物件,執行B事務,B事務執行完畢觸發之前的連線繼續執行,兩個連線各自做各自的事情,互不干涉;但是兩個連線有同步的機制:B事務執行完後喚醒A事務

ThreadLocal<Thread,Connection>
//A執行到B的程式碼,將當前的Connection物件從ThreadLocal中拿出來,儲存在某個地方
//再從DataSource中獲取一個Connection,將這個Connection繫結當前執行緒,使用這個Connection繼續執行B事務的程式碼
//執行完畢將原來的Connection替換ThreadLocal,繼續執行A的程式碼
//實際上就是執行緒繫結的切換,實現了掛起和恢復的過程

第三種情況:巢狀事務

MySQL其實不支援巢狀事務,但是mybatis可以透過儲存點模擬,透過設定儲存點,將內部的事務轉化為透過儲存點回滾至儲存點,實現類似兩個事務的操作

begin:
	update ...;
    SAVEPOINT a;
    	update ...;
    	update ...;
    	-- 以上程式碼出錯會回滾至儲存點
   	ROLLBACK to a;
   	-- 後續的事務不會受到影響
   	update ..
commit;

內部SAVEPOINT a後的程式碼如果有問題則直接回滾至儲存點

整個事務的提交不受內部 偽事務 的影響

七種傳播行為:

  • REQUIRED:B方法必須執行在事務中,如果外界存在事務,B方法融入外界事務,否則會啟動一個新的事務 (原子性的查詢)

  • SUPPORTS:如果外界有事務,就融入外界事務,如果沒有事務就以非事務的方式執行(只讀或查詢)

  • MANDATORY(強制的):B必須在事務中執行,如果外界無事務就拋異常

  • REQUIRES_NEW:當前方法必須執行在它自己的事務中,一個新的事務被啟動,掛起外界事務

  • NOT_SUPPORTED:當前方法不能執行在事務中;如果存在事務,掛起外界事務,非事務的方式執行當前方法(兩個Connection的切換和同步

  • NEVER:B必須在無事務的環境中執行,如果外界有事務就拋異常

  • NESTED:巢狀的,如果當前存在事務,B方法會在巢狀的事務中執行,巢狀事務可以獨立於當前事務進行單獨的提交或者回滾,如果當前事務不存在,行為與REQUIRED相同


測試REQUIRE:被呼叫方法必須執行在事務中;如果外界存在事務,B方法融入外界事務,否則會啟動一個新的事務

image-20230629191109750 image-20230629191120682

在Service中呼叫Service2的save方法

image-20230629191830448 image-20230629192208965

在service的save方法中儲存act-003,呼叫service2的save方法,其中儲存act-004

測試思路:如果service2的save方法出現異常,事務回滾是否會影響service的save方法中act-004的儲存?

    @Test
    public void testSpringTx(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        AccountService accountService = 
            applicationContext.getBean("accountService", AccountService.class);
        Account account = new Account("act-003", 100000.0);
        accountService.save(account);
    }

結果:

image-20230629192345982

說明REQUIRE會將被呼叫的方法和上下文方法的事務合併

如果在上下文方法中呼叫service2的save方法之後輸出內容:

image-20230813031057685

後面的內容並不會被輸出,所以invoke中的內容應該是:

public void save(Account act){
    Connection conn;
    try{
        accountDao.insert(act_1);

        try{
            accountDao.insert(act_2);
        }catch(Exception e){
            conn.commit();
            throw e;
        }
        
		System.err.println("here---------------");
    }catch(Exception e){
		conn.commit();
        throw e;
    }
}

問題是:在第九行是否進行了commit?

可以在輸出語句之前進行測試:

image-20230813031640425

結果:

2023-08-12T19:15:52.505756Z	   24 Query	SET autocommit=0
2023-08-12T19:15:52.517993Z	   24 Query	insert into t_act values('act-003',0.0)
2023-08-12T19:15:52.520608Z	   24 Query	insert into t_act values('act-004',1000.0)
2023-08-12T19:15:52.521097Z	   24 Query	rollback
2023-08-12T19:15:52.521675Z	   24 Query	SET autocommit=1

說明在b事務的執行過程中一旦出現異常會立刻提交

思考1:

如果刪除上下文方法的@Transactional(propagation = Propagation.*REQUIRED*),只保留被調方法的事務控制註解:

image-20230629192622349

上下文方法沒有開啟事務,被調方法在單獨的事務中執行

思考2:

在service的上下文方法中捕捉是否能避免事務回滾?

image-20230629193434102
public void save(){
    Connection conn;
    try{
        accountDao.insert(act_1);
		
        //自行新增的try
        try{
            
           //service2的save方法
           try{
            	accountDao.insert(act_2);
        	}catch(Exception e){
            	conn.commit();
            	throw e;
        	}
            
        }catch(Exception e){
			e.printStackTrace();
        }
        
		System.err.println("here---------------");
    }catch(Exception e){
		conn.commit();
        throw e;
    }
}

在service2的save方法中一旦出現異常就會立刻提交,但是外層的try會將異常捕捉,也就是在service中的save裡呼叫save方法之後的內容還可以正常執行

如果在上文中21行後進行資料庫操作,在內層try中的異常已經被18行捕捉,這次操作是否可以被commit?

image-20230813034223330

是會被觸發的,報錯資訊:

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

事務回滾因為已經被標記為只回滾


測試REQUIRE_NEW:開啟新事務,將原事務掛起,不存在巢狀關係

沒有異常時:

//呼叫service1的save方法
2023-06-29T11:39:00.303882Z	  188 Query	SET autocommit=1
2023-06-29T11:39:00.331996Z	  188 Query	SET autocommit=0
2023-06-29T11:39:00.348431Z	  188 Query	insert into t_act values('act-003',100000.0)

//呼叫service2的save方法,暫停當前的事務,開啟新的事務
2023-06-29T11:39:00.365806Z	  189 Query	SET autocommit=1
2023-06-29T11:39:00.367289Z	  189 Query	SET autocommit=0
2023-06-29T11:39:00.371201Z	  189 Query	insert into t_act values('act-004',10000.0)
2023-06-29T11:39:00.371948Z	  189 Query	commit
2023-06-29T11:39:00.372975Z	  189 Query	SET autocommit=1

//提交、回滾、關閉後新事務執行完畢,執行原先的事務
2023-06-29T11:39:00.373614Z	  188 Query	commit
2023-06-29T11:39:00.374360Z	  188 Query	SET autocommit=1
2023-08-13 03:51:25 394 Creating new transaction with name 
2023-08-13 03:51:25 436 INFO com.alibaba.druid.pool.DruidDataSource - {dataSource-1} inited
2023-08-13 03:51:25 732 Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@d2387c8] for JDBC transaction
2023-08-13 03:51:25 734 Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@d2387c8] to manual commit
2023-08-13 03:51:25 735 Executing prepared SQL update
2023-08-13 03:51:25 736 Executing prepared SQL statement [insert into t_act values(?,?)]
2023-08-13 03:51:25 748 Suspending current transaction, creating new transaction with name [com.eun.spring.service.impl.AccountServiceImpl2.save]
2023-08-13 03:51:25 765 Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@78dc4696] for JDBC transaction
2023-08-13 03:51:25 766 Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@78dc4696] to manual commit
2023-08-13 03:51:25 766 Executing prepared SQL update
2023-08-13 03:51:25 766 Executing prepared SQL statement [insert into t_act values(?,?)]
2023-08-13 03:51:25 767 Initiating transaction commit
2023-08-13 03:51:25 767 Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@78dc4696]
2023-08-13 03:51:25 779 Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@78dc4696] after transaction
2023-08-13 03:51:25 779 Resuming suspended transaction after completion of inner transaction
here---------------
2023-08-13 03:51:25 780 Initiating transaction commit
2023-08-13 03:51:25 780 Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@d2387c8]
2023-08-13 03:51:25 781 Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@d2387c8] after transaction

如果出現異常:

image-20230813035953332 image-20230813040005203

在service2的save方法中出現異常,理應不應該影響service的save提交,但是兩個事務都會回滾:

2023-08-12T20:00:55.020219Z	   40 Query	SET autocommit=0
2023-08-12T20:00:55.032610Z	   40 Query	insert into t_act values('act-003',0.0)

2023-08-12T20:00:55.049942Z	   41 Query	SET autocommit=0
2023-08-12T20:00:55.050511Z	   41 Query	insert into t_act values('act-004',1000.0)
2023-08-12T20:00:55.051224Z	   41 Query	rollback
2023-08-12T20:00:55.051686Z	   41 Query	SET autocommit=1

2023-08-12T20:00:55.052561Z	   40 Query	rollback
2023-08-12T20:00:55.062901Z	   40 Query	SET autocommit=1
Creating new transaction with name [com.eun.spring.service.impl.AccountServiceImpl.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
{dataSource-1} inited
Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@3003697] for JDBC transaction
Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3003697] to manual commit
Executing prepared SQL update
Executing prepared SQL statement [insert into t_act values(?,?)]
Suspending current transaction, creating new transaction with name [com.eun.spring.service.impl.AccountServiceImpl2.save]
Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@7dc51783] for JDBC transaction
Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7dc51783] to manual commit
Executing prepared SQL update
Executing prepared SQL statement [insert into t_act values(?,?)]
Initiating transaction rollback
Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@7dc51783]
Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7dc51783] after transaction
Resuming suspended transaction after completion of inner transaction
Initiating transaction rollback
Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@3003697]
Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3003697] after transaction

因為service2的save方法的異常上拋到了service的save方法中,如果在service的save方法中捕捉就不會影響本方法的提交:

image-20230813042521435
2023-08-12T20:25:39.852814Z	   42 Query	SET autocommit=0
2023-08-12T20:25:39.864387Z	   42 Query	insert into t_act values('act-003',0.0)

2023-08-12T20:25:39.882583Z	   43 Query	SET autocommit=0
2023-08-12T20:25:39.883186Z	   43 Query	insert into t_act values('act-004',1000.0)
2023-08-12T20:25:39.883930Z	   43 Query	rollback
2023-08-12T20:25:39.884430Z	   43 Query	SET autocommit=1

2023-08-12T20:25:39.886346Z	   42 Query	commit
2023-08-12T20:25:39.897324Z	   42 Query	SET autocommit=1
事務的隔離級別 Isolation
image-20230629162502566

防止事務A和事務B的互相干擾,兩個事務訪問同一張表類似於多執行緒併發訪問臨界資源

資料庫讀取資料的三大問題:

  • 髒讀:讀取到沒有提交到資料庫的資料(快取中的資料)
  • 不可重複讀:同一個事務中,第一次和第二次讀取的資料不同
  • 幻讀:讀到的資料是假的
public enum Isolation {
	DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),

    //讀未提交 最低階別
	READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),

    //二級:讀已提交(Oracle的預設級別)
	READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),

    //可重複讀(MySQL的預設級別)
	REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),

    //序列化 最高階別
	SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
}

事務隔離級別包含四個:

  • READ_UNCOMMITTED:讀未提交
    • 存在髒讀(dirty read)問題,髒讀:能夠讀取到其他事務未提交的資料
  • READ_COMMITTED:讀已提交
    • 解決了髒讀問題,其他事務提交之後才能讀到,存在不可重複讀的問題(其他事務多次提交就會不可重複)
  • REPEATABLE_READ :可重複讀
    • 解決了不可重複讀,達到可重複讀效果,只要當前事務不結束,讀取到的資料都是一樣的,但是存在幻讀問題
  • SERIALIZABLE:序列化
    • 解決了幻讀問題,事務排隊執行,不支援併發
隔離級別 髒讀 不可重複讀 幻讀
讀未提交
讀提交
可重複讀
序列化

讀未提交問題:

Iso1的事務查詢,Iso2的事務提交;需要保證Iso2的事務提交在在Iso1執行完畢之後執行:

Iso2 -> 事務開啟
Iso2 -> insert(Account);

Iso1 -> 查詢

Iso2 -> 提交

這樣才能確保讀到未提交的資料:

@Service("iso2")
public class IsolationService2 {
    @Resource
    private AccountDao accountDao;

    @Transactional
    public void save(Account account) throws Exception {
        accountDao.insert(account);
        Thread.sleep(1000 * 20); // 保證iso1讀到的是未提交的資料
    }
}
@Service("iso1")
public class IsolationService1 {
    @Resource
    private AccountDao accountDao;

    //iso1 查詢
    @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    public void getByActno(String actno){
        Account account = accountDao.selectByActno(actno);
        System.out.println("查詢到的賬戶資訊:" + account);
    }
}
    @Test //先執行
    public void isolation1Test() throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService2 iso2 = applicationContext.getBean("iso2", IsolationService2.class);

        iso2.save(new Account("act-999",0.0));
    }
    
    @Test
    public void isolation2Test() throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        IsolationService1 iso1 = applicationContext.getBean("iso1", IsolationService1.class);

        iso1.getByActno("act-999"); //查詢到的賬戶資訊:Account{actno = act-999, balance = 0.0}
    }

如果改為讀提交:

image-20230813050630873

就無法讀取到iso1的資料了,但是在以上方法中account會被賦值null嗎?

org.springframework.dao.EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

JdbcTemplate會直接報錯:希望查到一條記錄,但是沒有查到

事務超時
image-20230629162532568

timeout 事務的超時時間,預設值-1

@Transactional(timeout = 10)

意思是:如果超過10s該事務的所有DML語句沒有執行完畢的話,最終結果進行回滾

注意:事務的超時時間指的是:在當前事務中,最後一條DML語句執行之前的時間。如果最後一條DML語句後面有很多業務邏輯,這些業務程式碼的執行時機不計入超時時間。

image-20230813051314446

在insert執行之前sleep20s,超過了timeout = 10,會進行回滾

超時會報錯:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Sun Aug 13 05:14:56 CST 2023
image-20230813051402848

在最後一條DML語句之後sleep,不計入超時時間,不會回滾

如果想把service中某一個方法的所有時間都納入超時時間,可以在方法最後加一個無關緊要的DML語句

只讀事務
@Transactional(readOnly = true)

該事務執行的過程中只允許select語句的執行,其他語句不能執行(Connection is read-only)

可以啟動spring的最佳化策略,提高select語句的執行效率

設定某些異常回滾事務
@Transactional(rollbackFor = RuntimeException.class)

只要發生RuntimeException及其子類異常都進行回滾,發生IO異常就不會回滾

Spring預設只回滾RuntimeException,如果想回滾全部異常需要手動指定Exception.class

設定某些異常不回滾事務
@Transactional(noRollbackFor = RuntimeException.class)

事務的全註解開發

當前的配置檔案:

    <context:component-scan base-package="com.eun.spring" />
    <aop:aspectj-autoproxy />
    <context:property-placeholder location="jdbc.properties" />
	
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />
    </bean>

    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate" >
        <property name="dataSource" ref="dataSource" />
    </bean>
    <!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource" />
     </bean>
    <!--開啟事務註解驅動器 使用註解方式控制事務-->
    <tx:annotation-driven transaction-manager="transactionManager" />
@Configuration
@ComponentScan("com.eun.spring")
//開啟註解驅動器
@EnableTransactionManagement()
public class SpringConfig {
    //spring掃描到bean註解後會呼叫這個方法,將返回值納入IoC容器管理
    //返回的物件就是一個bean
    @Bean("dataSource")
    public DruidDataSource getDruidDataSource() throws IOException {
        DruidDataSource druidDataSource = new DruidDataSource();
/*      Properties properties = new Properties();
        properties.load(ClassLoader.getSystemResourceAsStream("jdbc.properties"));
        druidDataSource.configFromPropety(properties);*/
        druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        druidDataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
        druidDataSource.setUsername("root");
        druidDataSource.setPassword("");
        return druidDataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate getJdbcTemplate(DataSource dataSource){
        //可以手動呼叫getDruidDataSource()方法,但是一般會使用引數,自動會注入
        return new JdbcTemplate(dataSource);
    }
    @Bean("transactionManager")
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    } 

}

事務的XML實現

  1. 配置事務管理器

  2. 配置通知

    因為不需要配置事務註解驅動器,就需要手動配置通知

  3. 配置切面

還需要新增aspectj依賴

    <!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource" />
     </bean>
    
    <!--配置通知-->
        <tx:advice id="txAdvice" >
            <!--配置通知的屬性-->
            <tx:attributes>
                <tx:method name="transfer" propagation="REQUIRED" isolation="READ_COMMITTED"/>
            </tx:attributes>
        </tx:advice>
    <!--配置切面-->
    <aop:config >
        <!--切點-->
        <aop:pointcut id="txPointCut" expression="execution(* com.eun.spring..*(..))"/>
        <!--切面-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut" />
    </aop:config>

Spring6整合JUnit5

之前我們使用的都是JUnit4.13.2,並沒有使用Spring對Junit的支援;Spring底層有一套API支援單元測試

spring對JUnit4的支援

    <dependencies>
        <!--spring context依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0-M2</version>
        </dependency>
        <!--spring對junit的支援相關依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.0</version>
        </dependency>
        <!--junit4依賴-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

之前的方式:

public class SpringJunit4Test {
    @Test
    public void testUser(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        User bean = applicationContext.getBean(User.class);
        System.out.println(bean);
    }
}

使用註解:

//junit框架的api
@RunWith(SpringJUnit4ClassRunner.class)
//context包下,載入配置檔案
@ContextConfiguration("classpath:spring.xml")

就會以註解方式載入Spring容器,不需要每次new .... ,並且可以將User作為屬性注入:

//junit框架的api
@RunWith(SpringJUnit4ClassRunner.class)
//context包下,載入配置檔案
@ContextConfiguration("classpath:spring.xml")
public class SpringJunit4Test {

    @Autowired
    private User user;

    @Test
    public void testUser(){
        System.out.println(user);
    }
}

但是注意要保持版本一致

spring對JUnit5的支援

    <dependencies>
        <!--spring context依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>6.0.0</version>
        </dependency>
        <!--spring對junit的支援相關依賴-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>6.0.0</version>
        </dependency>
        <!--junit5依賴-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test {
    @Autowired
    private User user;
    @Test
    public void testUser(){
        System.out.println(user);
    }
}

Spring整合Mybatis3.5

  1. 準備資料庫表 t_act

  2. 建立模組,引入依賴

    • spring-context
    • spring-jdbc(關聯引入事務)
    • mysql
    • mybatis
    • mybatis-spring:mybatis提供的是與spring框架整合的依賴
    • Druid
    • junit
  3. 基於三層架構建包

  4. pojo類

  5. mapper介面

  6. mapper配置檔案

  7. service介面及其實現類

  8. jdbc.properties配置檔案

  9. mybatis-config.xml配置檔案

    可以沒有,大部分配置檔案可以轉移到spring的配置檔案中;但是mybatis系統級的配置還需要這個檔案

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
        </settings>
    
    </configuration>
    

    環境、掃描mapper相關的配置都轉移到spring中了

  10. spring.xml

    元件掃描、外部屬性檔案、資料來源

    SqlSessionFactoryBean配置:注入mybatis核心配置檔案路徑、指定別名包、注入資料來源

    Mapper掃描配置器:掃描mapper下的介面

    DataSourceTransactionManager、啟動事務註解

    <?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" xmlns:tx="http://www.springframework.org/schema/tx"
           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 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
        <context:component-scan base-package="com.eun.bank" />
        <context:property-placeholder location="jdbc.properties" />
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
            <property name="driverClassName" value="${jdbc.driver}" />
            <property name="url" value="${jdbc.url}" />
            <property name="username" value="${jdbc.username}" />
            <property name="password" value="${jdbc.password}" />
        </bean>
    
        <!--配置SqlSessionFactoryBean-->
        <bean class="org.mybatis.spring.SqlSessionFactoryBean" >
            <!--注入資料來源-->
            <property name="dataSource" ref="dataSource" />
            <!--指定mybatis核心配置檔案-->
            <property name="configLocation" value="mybatis-config.xml" />
            <!--指定別名-->
            <property name="typeAliasesPackage" value="com.eun.bank.pojo" />
        </bean>
    
        <!--Mapper掃描配置器-->
        <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer" >
            <property name="basePackage" value="com.eun.bank.mapper" />
        </bean>
    
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
            <property name="dataSource" ref="dataSource" />
        </bean>
        
        <!--事務註解掃描驅動器-->
        <tx:annotation-driven transaction-manager="transactionManager" />
    </beans>
    
  11. 測試

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"
       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">
    <context:component-scan base-package="com.eun.bank" />
</beans>

在主配置檔案中直接使用import匯入就可以了:

<import resource="common.xml" />

Spring的八大設計模式

簡單工廠模式

BeanFactory的getBean方法,透過唯一標識獲取Bean,是典型的簡單工廠模式(靜態工廠模式)

工廠方法模式

FactoryBean是工廠方法模式,在配置檔案中透過factory-method指定工廠方法,該方法是一個例項方法

單例模式

spring解決迴圈依賴的getSingleton方法

裝飾器模式

JavaSE的IO流就是非常典型的裝飾器模式。Spring中配置DataSource的時候,這些dataSource可能是各種不同型別的,比如不同的資料庫:Oracle、SQL Server、MySQL等,也可能是不同的資料來源:比如apache提供的org.apache.commons.dhcp.BasicDataSource,spring提供的org.springframework.jndi.JndiObjectFactoryBean

裝飾器模式可以在儘可能少的修改原有類程式碼的情況下動態的切換不同的資料來源;Spring根據每次請求的不同,將dataSource設定成不同的資料來源,以達到切換不同資料來源的目的。

Spring中類名帶有:DecoratorWrapper單詞的類,都是裝飾器模式

觀察者模式

定義物件間的一對多的關係,當一個物件的狀態發生改變時,所有依賴於它的物件都得到通知並自動更新。spring中觀察者模式一般用在listener的實現。

Spring中的事件程式設計模式就是觀察者模式的實現,在spring中定義一個ApplicationListener介面,用來監聽Application的事件,Application其實就是ApplicationContext,ApplicationContext中內建了幾個事件,比較容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent

策略模式

策略模式是行為型模式,呼叫不同的方法,適應行為的變化,強調父類的呼叫子類的特性

getHandler是HandlerMappering介面的唯一的方法,用於根據請求找到匹配的處理器

比如我們自己寫了AccountDao介面,這個介面下有不同的實現類:AccountDaoForMySQL,AccountDaoForOracle。對於Service來說不需要關心具體底層的實現,只需要面向AccountDAO介面呼叫不同的實現類,底層可以靈活切換,這就是策略模式

模板方法模式

Spring中的JdbcTemplate類就是一個模板類,就是模板方法設計模式的具體體現,在模板類的模板方法execute中編寫核心演算法,具體的實現步驟在l

AOT

  1. JIT:Just In Time 動態編譯、實時編譯,一邊執行一遍進行編譯;目前預設使用JIT;程式執行時,進行JIT動態編譯,吞吐量高,可以動態生成程式碼;但是啟動很慢,編譯時需要佔用執行時資源

  2. AOT:Ahead Of Time 執行前編譯、提前編譯,AOT可以將原始碼直接轉換為機器碼,執行時直接啟動,啟動速度快、記憶體佔用低;執行時不能進行最佳化,安裝時間長

JIT在程式執行過程中將位元組碼檔案轉換為機器碼並部署到環境;AOT在程式執行之前將位元組碼轉換為機器碼

.java -> .class -> (jaotc編譯工具) -> .so(程式函式庫)

相關文章