Spring 的AOP 與事務操做 彙總詳解

我的認爲,要了解事務以前應該先了解AOP,由於spring運用aop完成的事務控制php

AOP

簡介

AOP爲Aspect Oriented Programming的縮寫,意爲: 面向切面編程 ,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。主要目標仍是致力於 解耦 ,咱們能夠看到解耦這一理念貫穿於咱們的整個編碼工做中。咱們經過各類設計模式或者設計原則來使得對象之間解耦。經過Spring IOC容器中利用依賴注入使得對象之間的耦合度更低。而AOP的思想解耦得更完全, 經過動態的添加功能來加強實現,而且作到毫無代碼的侵入性 。利用AOP能夠對業務邏輯和非業務邏輯的部分進行隔離,能夠提取非業務邏輯的部分,提升程序的可重用性,同時提升了開發的效率。mysql

爲何使用AOP?

核心業務代碼與切面代碼解耦,切面代碼對核心業務代碼徹底無侵入,遵照單一職責原則,徹底隔離核心業務代碼與切面代碼。web

低耦合帶來可維護性高,修改或者新增一個切面代碼僅需集中在一處進行更改。低耦合也意味着切面代碼可複用性高。spring

Spring IOC容器自然地爲AOP的實現提供了便利,IOC和AOP的結合使得Spring的解耦能力更強。sql

實際應用

使用 @EnableAspectJAutoProxy(啓用面向切面自動代理) 註解配置數據庫

開啓了上述配置以後,全部在容器中,被 @AspectJ 註解的 bean 都會被 Spring 當作是 AOP 配置類,稱爲一個 Aspect(面) 。express

配置 Pointcut (加強的切入點)

表明要攔截哪些方法編程

// 指定的方法
    @Pointcut("execution(* testExecution(..))")
    public void anyTestMethod() {}
  1. execution :匹配方法簽名後端

這個最簡單的方式就是上面的例子," execution(* testExecution(..)) "表示的是匹配名爲 testExecution 的方法,*表明任意返回值,(..)表示零個或多個任意參數。設計模式

  1. within :指定所在類或所在包下面的方法(Spring AOP 獨有)

// service 層
    // ".." 表明包及其子包
    @Pointcut("within(ric.study.demo.aop.svc..*)")
    public void inSvcLayer() {}
  1. @annotation :方法上具備特定的註解

// 指定註解
    @Pointcut("@annotation(ric.study.demo.aop.HaveAop)")
    public void withAnnotation() {}
  1. bean(idOrNameOfBean) :匹配 bean 的名字(Spring AOP 獨有)

// controller 層
    @Pointcut("bean(testController)")
    public void inControllerLayer() {}

若是你是在開發企業級應用,Spring 建議你使用 SystemArchitecture 這種切面配置方式,即將一些公共的 PointCut 配置所有寫在這個一個類裏面維護

package com.xyz.someapp;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class SystemArchitecture {

  @Pointcut("within(com.xyz.someapp.web..*)")
  public void inWebLayer() {}

  @Pointcut("within(com.xyz.someapp.service..*)")
  public void inServiceLayer() {}

  @Pointcut("within(com.xyz.someapp.dao..*)")
  public void inDataAccessLayer() {}

  @Pointcut("execution(* com.xyz.someapp.service.*.*(..))")
  public void businessService() {}

  @Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
  public void dataAccessOperation() {}

}

上面這個 SystemArchitecture 很好理解,該 Aspect 定義了一堆的 Pointcut,隨後在任何須要 Pointcut 的地方均可以直接引用。

配置Advice(對程序進行怎麼樣的加強)

Advice註解

Advice註解一共有五種,分別是:

  1. @Before前置通知

前置通知在切入點運行前執行,不會影響切入點的邏輯

  1. @After後置通知

後置通知在切入點正常運行結束後執行,若是切入點拋出異常,則在拋出異常前執行

  1. @AfterThrowing異常通知

異常通知在切入點拋出異常前執行,若是切入點正常運行(未拋出異常),則不執行

  1. @AfterReturning返回通知

返回通知在切入點正常運行結束後執行,若是切入點拋出異常,則不執行

  1. @Around環繞通知

環繞通知是功能最強大的通知,能夠在切入點執行先後自定義一些操做。環繞通知須要負責決定是繼續處理join point(調用ProceedingJoinPoint的proceed方法)仍是中斷執行

/**
 * 注:實際開發過程中,Advice應遵循單一職責,不該混在一塊兒
 */
@Aspect
@Component
public class GlobalAopAdvice {

    @Before("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // 實際使用過程中 能夠像這樣把Advice 和 Pointcut 合在一塊兒,直接在Advice上面定義切入點
    @Before("execution(* ric.study.demo.dao.*.*(..))")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // 在方法
    @AfterReturning("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ... 實現代碼
    }

    // returnVal 就是相應方法的返回值
    @AfterReturning(
        pointcut="ric.study.demo.aop.SystemArchitecture.dataAccessOperation()",
        returning="returnVal")
    public void doAccessCheck(Object returnVal) {
        //  ... 實現代碼
    }

    // 異常返回的時候
    @AfterThrowing("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ... 實現代碼
    }

    // 注意理解它和 @AfterReturning 之間的區別,這裏會攔截正常返回和異常的狀況
    @After("ric.study.demo.aop.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // 一般就像 finally 塊同樣使用,用來釋放資源。
        // 不管正常返回仍是異常退出,都會被攔截到
    }

    // 這種最靈活,既能作 @Before 的事情,也能夠作 @AfterReturning 的事情
    @Around("ric.study.demo.aop.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
       	//  target 方法執行前... 實現代碼
        Object retVal = pjp.proceed();
        //  target 方法執行後... 實現代碼
        return retVal;
    }
}

實際配置例子

配置一個註解配置的aop須要如下幾點:

  1. 首先aop須要基於ioc,須要它的掃描bean

  2. 開啓註解配置方式須要用到的註解 @EnableAspectJAutoProxy ,固然,要保證這個註解的類被掃描到:

@Configurable
@EnableAspectJAutoProxy
@ComponentScan("com.hyx.aop")
public class Config {}
  1. 一個目標類,該類就是被切入的方法對象,須要被掃描到

public interface AopSvc {

    void doSomething();
}
@Service("aopSvc")
public class AopSvcImpl implements AopSvc {

    @Override
    public void doSomething() {
        System.out.println("目標類作了一些事情");
    }
}
  1. 一個 aspect ,也就是 Pointcut 和 Advice 的集合, Pointcut 定義了它切入哪一個方法, Advice 定義了它何時進行切入,這些類都須要 @Aspect 註解

@Aspect
@Component
public class PointCutConfig {
    @Pointcut("within(com.hyx.aop..*)")
    public void inSvcLayer() {}
}
@Component
@Aspect
public class ServiceLogAspect {

    // 攔截,打印日誌,而且經過JoinPoint 獲取方法參數
    @Before("com.hyx.aop.PointCutConfig.inSvcLayer()")
    public void logBeforeSvc(JoinPoint joinPoint) {
        System.out.println("在service 方法執行前 打印第 1 第二天志");
        System.out.println("攔截的service 方法的方法簽名: " + joinPoint.getSignature());
        System.out.println("攔截的service 方法的方法入參: " + Arrays.toString(joinPoint.getArgs()));
    }

    // 這裏是Advice和Pointcut 合在一塊兒配置的方式,使用該方式無需另寫Pointcut方法
    @Before("within(com.hyx.aop..*)")
    public void logBeforeSvc2() {
        System.out.println("在service的方法執行前 打印第 2 第二天志");
    }
}

註解配置aop

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">



<bean id="amd" class="com.hyx.bean.AmdBean">
        <property name="cpu" value="5800x"></property>
        <!-- 使用ref注入的話,後面寫的是已經在bean上註冊的對應id名稱的對象,就是ref=(另外一個bean的id) -->
        <property name="mainboard" value="5700xt" ></property>
    </bean>

    <bean id="amdService" class="com.hyx.bean.AmdService">
        <property name="amdBean" ref="amd"></property>
    </bean>
    <bean id="serviceLog" class="com.hyx.aop.ServiceLogAspect">
    </bean>
    <aop:config>
        <!--要切入的對象-->
        <aop:pointcut id="saveTrackOrderLogMethod" expression="execution(* com.hyx.*.*.*(..))"/>
        <!-- 切入點 -->
        <aop:aspect id="orderTrackingAspect" ref="serviceLog">
            <!--  <aop:以前before、以後after... method="切面類中的方法" pointcut-ref="上面的切入的對象"/>  -->
            <aop:after-returning method="logBeforeSvc2" pointcut-ref="saveTrackOrderLogMethod"/>
        </aop:aspect>
    </aop:config>


</beans>

什麼是事務?

事務是邏輯上的一組操做,要麼都執行,要麼都不執行。

事務可否生效數據庫引擎是否支持事務是關鍵。好比經常使用的 MySQL 數據庫默認使用支持事務的 innodb 引擎。可是,若是把數據庫引擎變爲 myisam ,那麼程序也就再也不支持事務了!

spring中的事務

spring只提供如PlatformTransactionManager事務管理接口,具體的實現交給瞭如jdbc等平臺,根據鏈接數據庫的工具不一樣,其內部實現也有所不一樣,spring的事務在代碼邏輯中,是使用aop進行實現的,具體的在aop操做中的開啓事務,回滾事務等操做須要調用jdbc等工具的實現,也須要看數據庫引擎是否支持事務操做(mysql的innodb引擎支持操做)

事物的特性(ACID)

  1. 原子性 ( Atomicity ): 一個事務( transaction )中的全部操做,或者所有完成,或者所有不完成,不會結束在中間某個環節。事務在執行過程當中發生錯誤,會被回滾( Rollback )到事務開始前的狀態,就像這個事務歷來沒有執行過同樣。即,事務不可分割、不可約簡。 事務是一個總體,要麼全完成,要麼全不完成

  2. 一致性 ( Consistency ): 在事務開始以前和事務結束之後,數據庫的完整性沒有被破壞。這表示寫入的資料必須徹底符合全部的預設約束、觸發器、級聯回滾等。

  3. 隔離性 ( Isolation ): 數據庫容許多個併發事務同時對其數據進行讀寫和修改的能力,隔離性能夠防止多個事務併發執行時因爲交叉執行而致使數據的不一致。事務隔離分爲不一樣級別,包括未提交讀( Read uncommitted )、提交讀( read committed )、可重複讀( repeatable read )和串行化( Serializable )。

  4. 持久性 ( Durability ): 事務處理結束後,對數據的修改就是永久的,即使系統故障也不會丟失。

在 MySQL 中,恢復機制是經過 回滾日誌(undo log) 實現的,全部事務進行的修改都會先先記錄到這個回滾日誌中,而後再執行相關的操做。若是執行過程當中遇到異常的話,咱們直接利用 回滾日誌 中的信息將數據回滾到修改以前的樣子便可!而且,回滾日誌會先於數據持久化到磁盤上。這樣就保證了即便遇到數據庫忽然宕機等狀況,當用戶再次啓動數據庫的時候,數據庫還可以經過查詢回滾日誌來回滾將以前未完成的事務。

Spring 編程式事務管理

編程式事務管理是侵入性事務管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,對於編程式事務管理,Spring推薦使用TransactionTemplate。

Spring 聲明式事務管理

代碼侵入性最小,實際是經過 AOP 實現(基於@Transactional 的全註解方式使用最多)。

事務的傳播機制

事務的傳播性通常用在 事務嵌套的場景,好比一個事務方法裏面調用了另一個事務方法,那麼兩個方法是各自做爲獨立的方法提交仍是內層的事務合併到外層的事務一塊兒提交 ,這就是須要事務傳播機制的配置來肯定怎麼樣執行。

  • 經常使用的事務傳播機制以下:

  1. PROPAGATION_REQUIRED

Spring默認的傳播機制,能知足絕大部分業務需求,若是外層有事務,則當前事務加入到外層事務,一塊提交,一塊回滾。若是外層沒有事務,新建一個事務執行

  1. PROPAGATION_REQUES_NEW

該事務傳播機制是每次都會新開啓一個事務,同時把外層事務掛起,噹噹前事務執行完畢,恢復上層事務的執行。若是外層沒有事務,執行當前新開啓的事務便可

  1. PROPAGATION_SUPPORT

若是外層有事務,則加入外層事務,若是外層沒有事務,則直接使用非事務方式執行。徹底依賴外層的事務

  1. PROPAGATION_NOT_SUPPORT

該傳播機制不支持事務,若是外層存在事務則掛起,執行完當前代碼,則恢復外層事務,不管是否異常都不會回滾當前的代碼

  1. PROPAGATION_NEVER

該傳播機制不支持外層事務,即若是外層有事務就拋出異常

  1. PROPAGATION_MANDATORY

與NEVER相反,若是外層沒有事務,則拋出異常

  1. PROPAGATION_NESTED

該傳播機制的特色是能夠保存狀態保存點,當前事務回滾到某一個點,從而避免全部的嵌套事務都回滾,即各自回滾各自的,若是子事務沒有把異常吃掉,基本仍是會引發所有回滾的。 傳播規則回答了這樣一個問題:一個新的事務應該被啓動仍是被掛起,或者是一個方法是否應該在事務性上下文中運行。

事務的隔離級別

在多事務併發處理數據時有時會出現下面的問題:

  1. 髒讀(Dirty read)

髒讀發生在一個事務讀取了被另外一個事務改寫但還沒有提交的數據時。若是這些改變在稍後被回滾了,那麼第一個事務讀取的數據就會是無效的。

  1. 不可重複讀(Nonrepeatable read)

不可重複讀發生在一個事務執行相同的查詢兩次或兩次以上,但每次查詢結果都不相同時。這一般是因爲另外一個併發事務在兩次查詢之間更新了數據。

  1. 幻讀(Phantom reads)

幻讀和不可重複讀類似。當一個事務(T1)讀取幾行記錄後,另外一個併發事務(T2)插入了一些記錄時,幻讀就發生了。在後來的查詢中,第一個事務(T1)就會發現一些原來沒有的額外記錄。

在理想狀態下,事務之間將徹底隔離,從而能夠防止這些問題發生。然而,徹底隔離會影響性能,由於隔離常常涉及到鎖定在數據庫中的記錄(甚至有時是鎖表)。徹底隔離要求事務相互等待來完成工做,會阻礙併發。所以,能夠根據業務場景選擇不一樣的隔離級別。

隔離級別 含義
ISOLATION_DEFAULT 使用後端數據庫默認的隔離級別
ISOLATION_READ_UNCOMMITTED 容許讀取還沒有提交的更改。可能致使髒讀、幻讀或不可重複讀。
ISOLATION_READ_COMMITTED (Oracle 默認級別)容許從已經提交的併發事務讀取。可防止髒讀,但幻讀和不可重複讀仍可能會發生。
ISOLATION_REPEATABLE_READ (MYSQL默認級別)對相同字段的屢次讀取的結果是一致的,除非數據被當前事務自己改變。可防止髒讀和不可重複讀,但幻讀仍可能發生。
ISOLATION_SERIALIZABLE 徹底服從ACID的隔離級別,確保不發生髒讀、不可重複讀和幻影讀。這在全部隔離級別中也是最慢的,由於它一般是經過徹底鎖定當前事務所涉及的數據表來完成的。

事務設置只讀

若是一個事務只對數據庫執行讀操做,那麼該數據庫就可能利用那個事務的只讀特性,採起某些優化措施。經過把一個事務聲明爲只讀,能夠給後端數據庫一個機會來應用那些它認爲合適的優化措施。

事務超時回滾

爲了使一個應用程序很好地執行,它的事務不能運行太長時間。所以,聲明式事務的下一個特性就是它的超時。

假設事務的運行時間變得格外的長,因爲事務可能涉及對數據庫的鎖定,因此長時間運行的事務會沒必要要地佔用數據庫資源。這時就能夠聲明一個事務在特定秒數後自動回滾,沒必要等它本身結束。

回滾規則

在默認設置下,事務只在出現運行時異常(runtime exception)時回滾,而在出現受檢查異常(checked exception)時不回滾。 不過,能夠聲明在出現特定受檢查異常時像運行時異常同樣回滾。一樣,也能夠聲明一個事務在出現特定的異常時不回滾,即便特定的異常是運行時異常。

spring中基於xml的事務配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns: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
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
        
        <!--省略了各dao的bean配置代碼-->
            <!-- 配置數據源 -->
   	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--鏈接數據庫的必備信息-->
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mysql:///test?useUnicode=true&serverTimezone=GMT%2B8&characterEncoding=utf-8"></property>
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
    </bean>
<!---->
    <!--spring配置事務-->
    <!--1. 配置事務管理器-->
    <!--2. 配置事務的通知
            導入事務的約束,須要tx的名稱空間與約束,也須要aop的
        3. 配置AOP的切入點表達式
        4. 創建事務通知和切入點表達式對應關係
        5. 配置事務的屬性
                是在aop:advisor內進行配置
    -->

    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!--配置事務的通知-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!--配置事務屬性
                isolation:用於指定事務的隔離級別。默認值是DEFAULT,表示使用數據庫的默認隔離級別。
                propagation:用於指定事務的傳播行爲。默認值是REQUIRED,表示必定會有事務,增刪改的選擇。查詢方法能夠選擇SUPPORTS。
                read-only:用於指定事務是否只讀。只有查詢方法才能設置爲true。默認值是false,表示讀寫。
                timeout:用於指定事務的超時時間,默認值是-1,表示永不超時。若是指定了數值,以秒爲單位。
                rollback-for:用於指定一個異常,當產生該異常時,事務回滾,產生其餘異常時,事務不回滾。沒有默認值。表示任何異常都回滾。
                no-rollback-for:用於指定一個異常,當產生該異常時,事務不回滾,產生其餘異常時事務回滾。沒有默認值。表示任何異常都回滾。
        -->
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED" read-only="false"/>
            <tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
        </tx:attributes>
    </tx:advice>
    <!--配置aop-->
    <aop:config>
        <!--配置切入點表達式-->
        <aop:pointcut id="pt1" expression="execution(* com.itheima.service.*.*(..))"/>
        <!-- 創建切入點表達式和事務通知對應關係 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
    </aop:config>
</beans>

spring中基於註解的配置

  1. 修改xml配置文件

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


<!--省略dao與數據庫連接-->
<!-- 關於事務的註解配置   -->

    <!-- 配置spring建立容器時要掃描的包-->
    <context:component-scan base-package="com.itheima"></context:component-scan>

    <!-- spring中基於註解 的聲明式事務控制配置步驟
        一、配置事務管理器
        二、開啓spring對註解事務的支持
        三、在須要事務支持的地方使用@Transactional註解


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



    <!-- 開啓spring對註解事務的支持-->
    <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
</beans>
  1. 於目標類上添加註解 @Transactional ,其屬性值與xml屬性值一致


若是你以爲文章不錯,文末的贊 ???? 又回來啦,記得給我「點贊」和「在看」哦~

相關文章
相關標籤/搜索