Spring AOP簡介

Spring AOP簡介

1. AOP 概述
1.1.1 What's AOP?
AOP(Aspect Orient Programming):面向切面編程
AOP是一種設計思想,是軟件設計領域中的面向切面編程,它是面向對象編程(OOP)的一種補充和完善。它以經過預編譯方式和運行期動態代理方式,實如今不修改源代碼的狀況下給程序動態統一添加額外功能的一種技術。如圖-1所示:imagejava

AOP與OOP(Object Orient Programming)字面意思相近,但其實二者徹底是面向不一樣領域的設計思想。實際項目中咱們一般將面向對象理解爲一個靜態過程(例如一個系統有多少個模塊,一個模塊有哪些對象,對象有哪些屬性),面向切面的運行期代理方式,理解爲一個動態過程,能夠在對象運行時動態織入一些擴展功能或控制對象執行。
1.1.2 AOP 應用場景分析?
實際項目中一般會將系統分爲兩大部分,一部分是核心業務,一部分是非核業務。在編程實現時咱們首先要完成的是核心業務的實現,非核心業務通常是經過特定方式切入到系統中,這種特定方式通常就是藉助AOP進行實現。mysql

AOP就是要基於OCP(開閉原則),在不改變原有系統核心業務代碼的基礎上動態添加一些擴展功能並能夠"控制"對象的執行。例如AOP應用於項目中的日誌處理,事務處理,權限處理,緩存處理等等。如圖-2所示:
image算法

思考:現有一業務,在沒有AOP編程時,如何基於OCP原則實現功能擴展?
實現對象功能擴展如圖所示:imagespring

1.1.3 AOP 應用原理分析(先了解)?
Spring AOP底層基於代理機制實現功能擴展:sql

  1. 假如目標對象(被代理對象)實現接口,則底層能夠採用JDK動態代理機制爲目標對象建立代理對象(目標類和代理類會實現共同接口)。
  2. 假如目標對象(被代理對象)沒有實現接口,則底層能夠採用CGLIB代理機制爲目標對象建立代理對象(默認建立的代理類會繼承目標對象類型)。
    Spring AOP 原理分析,如圖-3所示:

image

說明:Spring boot2.x版本中AOP如今默認使用的CGLIB代理,假如須要使用JDK動態代理能夠在配置文件(applicatiion.properties)中進行以下配置:數據庫

spring.aop.proxy-target-class=false

1.2 AOP 相關術語分析編程

  • 切面(aspect): 橫切面對象,通常爲一個具體類對象(能夠藉助@Aspect聲明)。
  • 通知(Advice):在切面的某個特定鏈接點上執行的動做(擴展功能),例如around,before,after等。
  • 鏈接點(joinpoint):程序執行過程當中某個特定的點,通常指被攔截到的的方法。
  • 切入點(pointcut):對多個鏈接點(Joinpoint)一種定義,通常能夠理解爲多個鏈接點的集合。

鏈接點與切入點定義如圖-4所示:image緩存

說明:咱們能夠簡單的將機場的一個安檢口理解爲鏈接點,多個安檢口爲切入點,安全檢查過程當作是通知。總之,概念很晦澀難懂,多作例子,作完就會清晰。先能夠按白話去理解。安全

2 Spring AOP快速實踐

2.1 業務描述
基於項目中的核心業務,添加簡單的日誌操做,藉助SLF4J日誌API輸出目標方法的執行時長。(前提,不能修改目標方法代碼-遵循OCP原則)架構

2.2 項目建立及配置
建立maven項目或在已有項目基礎上添加AOP啓動依賴:

<dependency>

 <groupId>org.springframework.boot</groupId>

 <artifactId>spring-boot-starter-aop</artifactId>

</dependency>

說明:基於此依賴spring能夠整合AspectJ框架快速完成AOP的基本實現。AspectJ 是一個面向切面的框架,他定義了AOP的一些語法,有一個專門的字節碼生成器來生成遵照java規範的class文件。

2.3 擴展業務分析及實現
2.3.1 建立日誌切面類對象
將此日誌切面類做爲核心業務加強(一個橫切面對象)類,用於輸出業務執行時長,其關鍵代碼以下:

package com.cy.pj.common.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

/**
 * @Aspect 註解,描述的類爲spring容器中一個切面對象類型(此類型中封裝切入點與通知方法)
 * 1)切入點:要執行擴展業務的方法的集合
 * 2)通知方法:封裝了在切入點方法上要執行的擴展業務方法
 */
@Order(1)//@Order註解用於描述切面的優先級,數字越小優先級越高,默認優先級比較低
@Aspect
@Slf4j
@Component
public class SysLogAspect {
//    private static final Logger log=LoggerFactory.getLogger(SysLogAspect.class); //這句代碼等效於@Slf4j註解
    /**
     * @Pointcut 註解用於描述切入點(在哪些點上執行擴展業務)
     * bean(bean對象名字  默認爲類名首字母小寫):爲一種切入點表達式(這個表達式中定義了哪一個或哪些bean對象的方法要進行功能擴展).
     *     例如,bean(sysUserServiceImpl)表達式表示名字爲sysUserServiceImpl的bean對象中全部方法的集合爲切入點,
      *  也就是說這個sysUserServiceImpl對象中的任意方法執行時都要進行功能擴展.
     */
    @Pointcut("bean(sysUserServiceImpl)")  
    public void doLogPointCut() {}//此方法內部不須要寫具體實現,方法的方法名也是任意的
    /**
     * @Around 註解描述的方法爲一個通知方法(便是一個服務增益方法),此方法內部能夠作服務增益(擴展業務),@Around註解
     *  內部要指定切入點表達式,在此切入點表達式對應的切入點方法上作功能擴展
     * @param jp  表示鏈接點,鏈接點是動態肯定的,用於封裝正在執行的切入點方法(目標方法)信息.
     * @return 目標方法的執行結果
     * @throws Throwable  通知方法中執行過程出現的異常
     */
    @Around("doLogPointCut()")
    public Object around(ProceedingJoinPoint jp) throws Throwable {
        try {
            //1.記錄方法開始執行時間
            long start = System.currentTimeMillis();
            log.info("start:{}",start);
            //2.執行目標方法
            Object result=jp.proceed();//最終(中間還能夠調用本類其它通知或其它切面的通知)會調用目標方法
            //3.記錄方法結束執行時間
            long after = System.currentTimeMillis();
            log.info("after:{}",after);
            String targetClassMethod=getTargetClassMethod(jp);
            log.info("{}目標方法的執行耗時:{}",targetClassMethod,(after-start));
            //4.返回目標方法的執行結果
            return result;//目標方法的執行結果
        }catch(Throwable e) {
            log.error("目標方法執行時出現了異常:{}",e.getMessage());
            throw e;
        }
    }
    /**獲取目標方法的全限定名(目標類全名(包名+類名)+方法名)*/
    private String getTargetClassMethod(ProceedingJoinPoint jp) {
        //1.獲取目標對象的類型
         Class<?> targetCls = jp.getTarget().getClass();
        //2.獲取目標對象的類全名(包名+類名)
        String targetClsName = targetCls.getName();
        //3.火球目標對象的方法名
        //3.1 獲取方法簽名(方法簽名對象中封裝了方法相關信息)
        MethodSignature ms = (MethodSignature)jp.getSignature();
        //3.2 基於方法簽名獲取方法名
        String methodName=ms.getName();
        //4.構建方法的全限定名並返回
        return targetClsName+"."+methodName;
        
    }
    

}

說明:

  • @Aspect 註解用於標識或者描述AOP中的切面類型,基於切面類型構建的對象用於爲目標對象進行功能擴展或控制目標對象的執行。
  • @Pointcut註解用於描述切面中的方法,並定義切面中的切入點(基於特定表達式的方式進行描述),在本案例中切入點表達式用的是bean表達式,這個表達式以bean開頭,bean括號中的內容爲一個spring管理的某個bean對象的名字。
  • @Around註解用於描述切面中方法,這樣的方法會被認爲是一個環繞通知(核心業務方法執行以前和以後要執行的一個動做),@Aournd註解內部value屬性的值爲一個切入點表達式或者是切入點表達式的一個引用(這個引用爲一個@PointCut註解描述的方法的方法名)。
  • ProceedingJoinPoint類爲一個鏈接點類型,此類型的對象用於封裝要執行的目標方法相關的一些信息。只能用於@Around註解描述的方法參數。

2.3.1 業務切面測試實現
啓動項目測試或者進行單元測試,其中Spring Boot項目中的單元測試代碼以下:

@SpringBootTest

public class AopTests {

 @Autowired

 private SysUserService userService;

 @Test

 public void testSysUserService() {

 PageObject<SysUserDeptVo> po\=

 userService.findPageObjects("admin",1);

 System.out.println("rowCount:"+po.getRowCount());

 }

}

對於測試類中的userService對象而言,它有可能指向JDK代理,也有可能指向CGLIB代理,具體是什麼類型的代理對象,要看application.yml配置文件中的配置。

aop:
      proxy-target-class: false #false表示系統底層會基於JDK方式爲目標對象建立代理對象。默認爲true,表示系統底層會基於CGLIB方式爲目標對象建立代理對象

2.3.2 應用總結分析
在業務應用,AOP相關對象分析,如圖-5所示:
image

2.4 擴展業務織入加強分析

2.4.1 基於JDK代理方式實現
假如目標對象有實現接口,則能夠基於JDK爲目標對象建立代理對象,而後爲目標對象進行功能擴展,如圖-6所示:image

2.4.2 基於CGLIB代理方式實現
假如目標對象沒有實現接口(固然實現了接口也是能夠的),能夠基於CGLIB代理方式爲目標對象織入功能擴展,如圖-7所示:
image
說明:目標對象實現了接口也能夠基於CGLIB爲目標對象建立代理對象。

3 Spring AOP編程加強

3.1切面通知應用加強
3.1.1 通知類型
在基於Spring AOP編程的過程當中,基於AspectJ框架標準,spring中定義了五種類型的通知(通知描述的是一種擴展業務),它們分別是:

  • @Before。
  • @AfterReturning。
  • @AfterThrowing。
  • @After。
  • @Around.重點掌握(優先級最高)
    說明:在切面類中使用什麼通知,由業務決定,並非說,在切面中要把全部通知都寫上。

    3.1.2 通知執行順序
    假如五種類型的通知所有寫到一個切面對象中,其執行順序及過程,如圖-8所示:
    image

    說明:實際項目中可能不會在切面中定義全部的通知,具體定義哪些通知要結合業務進行實現。

    3.1.3 通知實踐過程分析
    代碼實踐分析以下:

package com.cy.pj.common.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class SysTimeAspect {

    @Pointcut("bean(sysUserServiceImpl)")
    public void doTime() {
        //不寫具體內容
    }

    @Before("doTime()")
    public void doBefore(JoinPoint jp){
        System.out.println("@Before");
    }

    @After("doTime()")
    public void doAfter(){
        System.out.println("@After");
    }

    /**核心業務正常結束時執行* 說明:假若有after,先執行after,再執行returning*/
    @AfterReturning("doTime()")
    public void doAfterReturning(){
        System.out.println("@AfterReturning");
    }

    /**核心業務出現異常時執行說明:假若有after,先執行after,再執行Throwing*/
    @AfterThrowing("doTime()")
    public void doAfterThrowing(){
        System.out.println("@AfterThrowing");
    }


    
    @Around("doTime()")
    public Object doAround(ProceedingJoinPoint jp)
            throws Throwable{
        System.out.println("@Around.before");
        try{
            Object obj=jp.proceed(); //執行目標方法
            System.out.println("@Around.after");
            return obj;
        }catch(Throwable e){
            //e.printStackTrace();
            System.out.println(e.getMessage());
            System.out.println("@Around.throwing");
            throw e;
        }

    }



}

說明:對於@AfterThrowing通知只有在出現異常時纔會執行,因此當作一些異常監控時可在此方法中進行代碼實現。

課堂練習:定義一個異常監控切面,對目標頁面方法進行異常監控,並以日誌信息的形式輸出異常

package com.cy.pj.common.aspect;

import lombok.extern.slf4j.Slf4j;

@Slf4j

@Aspect

@Component

public class SysExceptionAspect {

 @AfterThrowing(pointcut="bean(\*ServiceImpl)",throwing = "e")

 public void doHandleException(JoinPoint jp,Throwable e) {

 MethodSignature ms\=(MethodSignature)jp.getSignature();

 log.error("{}'exception msg is

{}",ms.getName(),e.getMessage());

 }

}

說明:AfterThrowing中throwing屬性的值,須要與它描述的方法的異常參數名相同。

3.2 切入點表達式加強
Spring中經過切入點表達式定義具體切入點,其經常使用AOP切入點表達式定義及說明:

表-1 Spring AOP 中切入點表達式說明
| 指示符 | 做用 |
|bean| 用於匹配指定bean對象的全部方法|
| within | 用於匹配指定包下全部類內的全部方法|
|execution | 用於按指定語法規則匹配到具體方法 |
| @annotation | 用於匹配指定註解修飾的方法 |

3.2.4 bean表達式(重點)
bean表達式通常應用於類級別,實現粗粒度的切入點定義,案例分析:

  • bean("userServiceImpl")指定一個userServiceImpl類中全部方法。
  • bean("*ServiceImpl")指定全部後綴爲ServiceImpl的類中全部方法。

說明:bean表達式內部的對象是由spring容器管理的一個bean對象,表達式內部的名字應該是spring容器中某個bean的name。

3.2.5 within表達式(瞭解)
within表達式應用於類級別,實現粗粒度的切入點表達式定義,案例分析:

  • within("aop.service.UserServiceImpl")指定當前包中這個類內部的全部方法。
  • within("aop.service.*") 指定當前目錄下的全部類的全部方法。
  • within("aop.service..*") 指定當前目錄以及子目錄中類的全部方法。

within表達式應用場景分析:

1)對全部業務bean都要進行功能加強,可是bean名字又沒有規則。

2)按業務模塊(不一樣包下的業務)對bean對象進行業務功能加強。

3.2.6 execution表達式(瞭解)
execution表達式應用於方法級別,實現細粒度的切入點表達式定義,案例分析:
語法:execution(返回值類型 包名.類名.方法名(參數列表))。

  • execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
  • execution(void aop.service.PersonServiceImpl.addUser(String)) 方法參數必須爲String的addUser方法。
  • execution(* aop.service..*.*(..)) 萬能配置。

    3.2.7 @annotation表達式(重點)

@annotaion表達式應用於方法級別,實現細粒度的切入點表達式定義,案例分析

  • @annotation(anno.RequiredLog) 匹配有此註解描述的方法。
  • @annotation(anno.RequiredCache) 匹配有此註解描述的方法。

其中:RequiredLog爲咱們本身定義的註解,當咱們使用@RequiredLog註解修飾業務層方法時,系統底層會在執行此方法時進行日誌擴展操做。
課堂練習:定義一Cache相關切面,使用註解表達式定義切入點,並使用此註解對須要使用cache的業務方法進行描述,代碼分析以下:
第一步:定義註解RequiredCache

package com.cy.pj.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 *  自定義註解,一個特殊的類,全部註解都默認繼承Annotation接口
 * @Retention註解用於定義註解什麼時候生效
 * @Target註解用於定義註解能夠描述對象
 * @Documented 將註解中的文檔註釋在提取時也要生存API文檔
 * @author Administrator
 *
 */
@Retention(RetentionPolicy.RUNTIME)//@Retention註解用於定義註解什麼時候生效
@Target(ElementType.METHOD) //@Target註解用於定義註解能夠描述對象
@Documented //將註解中的文檔註釋在提取也要生存API文檔
public @interface RequiredCache {
    
    String key() default "";

}

第二步:定義SysCacheAspect切面對象

package com.cy.pj.common.aspect;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class SysCacheAspect {
    //假設這個容器就是cache(固然這個cache還須要進行更好的設計)
//    private Map<Object,Object> cache=new HashMap<>();//此map爲線程不安全的hashMap
    private Map<Object,Object> cache=new ConcurrentHashMap<>();  //此map爲線程安全的hashMap<>();//此map爲線程不安全的hashMap
    
    
    /**
     * 基於@annotation(註解類全名 )表達式定義切入點(這種切入點一般理解爲細粒度的切入點)
     */
    @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)") //自定義註解
    public void doCache() {}
    
    @Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)") //自定義註解
    public void doClearCache() {}
    //FAQ分析  假如我如今須要一個清楚chache的通知方法,咱們該寫在哪一個通知方法中?寫在@AfterReturning通知方法中

    
    @AfterReturning("doClearCache()")
    public void doAfterReturing() {
        cache.clear();
    }
    
    
    
    
    @Around("doCache()")
    public Object around(ProceedingJoinPoint jp)throws Throwable{
        //1.從cache中獲取數據,假如cache中有咱們須要的數據集則直接返回,不須要在查詢數據,這樣能夠確保更好的性能
        System.out.println("Get data from cahce");
        Object result = cache.get("dept");//這個key的名字是本身隨意指定的(未來能夠寫得更加靈活)
        //FAQ分析? 如何將cache中的key定義的更加靈活(在描述切入點的方法註解中直指定)
        //FAQ分析?如何獲取切入點方法上註解中的key?(向獲取目標方法,而後基於目標方法獲取方法上的註解,再經過註解提取key的值)
        if(result!=null)return result;
        //2.假如cache中沒有,則從數據庫中去查詢
        result = jp.proceed();
        System.out.println("Put data to cache");
        //3.將查詢的結果存儲到cache中,便於下次查詢使用
        cache.put("dept", result);
        return result;
        
        
    }

}

第三步:使用@RequiredCache註解對特定業務目標對象中的查詢方法進行描述。

@RequiredCache

 @Override

 public List<Map<String, Object\>> findObjects() {

 ….

 return list;

 }

3.3 切面優先級設置實現
切面的優先級須要藉助@Order註解進行描述,數字越小優先級越高,默認優先級比較低。例如:
定義日誌切面並指定優先級。

@Order(1)
@Aspect
@Component

public class SysLogAspect {

 …

}

定義緩存切面並指定優先級:

@Order(2)

@Aspect

@Component

public class SysCacheAspect {

 …

}

說明:當多個切面做用於同一個目標對象方法時,這些切面會構建成一個切面鏈,相似過濾器鏈、攔截器鏈,其執行分析如圖-9所示:

image

3.4 關鍵對象與術語總結
Spring 基於AspectJ框架實現AOP設計的關鍵對象概覽,如圖-10所示:
image

3.5用戶行爲日誌記錄實現(實踐)
以AOP方式記錄項目中的用戶行爲信息,並將其存儲到數據庫

4.Spring AOP事務處理

4.1 Spring 中事務簡介
4.1.1事務定義(什麼是事務?)
事務(Transaction)是一個業務,是一個不可分割的邏輯工做單元,基於事務能夠更好的保證業務的正確性。
4.1.2 事務特性
事務具有ACID特性,分別是:

  • 原子性(Atomicity):一個事務中的多個操做要麼都成功要麼都失敗。
  • 一致性(Consistency): 例如存錢操做,存以前和存以後的總錢數應該是一致的。
  • 隔離性(Isolation):事務與事務應該是相互隔離的。
  • 持久性(Durability):事務一旦提交,數據要持久保存。

說明:目前市場上在事務一致性方面,一般會作必定的優化,比方說只要最終一致就能夠了,這樣的事務咱們一般會稱之爲柔性事務(只要最終一致就能夠了).

4.2 Spring 中事務管理
4.2.1 Spring 中事務方式概述
Spring框架中提供了一種聲明式事務的處理方式,此方式基於AOP代理,能夠將具體業務邏輯與事務處理進行解耦。也就是讓咱們的業務代碼邏輯不受污染或少許污染,就能夠實現事務控制。

在SpringBoot項目中,其內部提供了事務的自動配置,當咱們在項目中添加了指定依賴spring-boot-starter-jdbc時,框架會自動爲咱們的項目注入事務管理器對象,最經常使用的爲DataSourceTransactionManager對象。
4.2.2 Spring 中事務管理實現
本小節重點講解實際項目中最經常使用的註解方式的事務管理,以註解@Transactional配置方式爲例,進行實踐分析。

基於@Transactional 註解進行聲明式事務管理的實現步驟分爲兩步:

  1. 啓用聲明式事務管理,在項目啓動類上添加@EnableTransactionManagement,新版本中也可不添加(例如新版Spring Boot項目)。
  2. 將@Transactional註解添加到合適的業務類或方法上,並設置合適的屬性信息。

其代碼示例以下:

@Transactional(timeout = 30,

 readOnly = false,

 isolation = Isolation.READ\_COMMITTED,

 rollbackFor = Throwable.class,

 propagation = Propagation.REQUIRED)

 @Service

 public class implements SysUserService {

 @Transactional(readOnly = true)

 @Override

 public PageObject<SysUserDeptVo> findPageObjects(

 String username, Integer pageCurrent) {

 …

 }

}

其中,代碼中的@Transactional註解用於描述類或方法,告訴spring框架咱們要在此類的方法執行時進行事務控制,其具體說明以下:。

  • 當@Transactional註解應用在類上時表示類中全部方法啓動事務管理,而且通常用於事務共性的定義。
  • 當@Transactional描述方法時表示此方法要進行事務管理,假如類和方法上都有@Transactional註解,則方法上的註解通常用於事務特性的定義。

@Transactional 經常使用屬性應用說明:

  • timeout:事務的超時時間,默認值爲-1,表示沒有超時顯示。若是配置了具體時間,則超過該時間限制但事務尚未完成,則自動回滾事務。這個時間的記錄方式是在事務開啓之後到sql語句執行以前。
  • read-only:指定事務是否爲只讀事務,默認值爲 false;爲了忽略那些不須要事務的方法,好比讀取數據,能夠設置read-only爲true。對添加,修改,刪除業務read-only的值應該爲false。
  • rollback-for:用於指定可以觸發事務回滾的異常類型,若是有多個異常類型須要指定,各種型之間能夠經過逗號分隔。
  • no-rollback- for:        拋出no-rollback-for 指定的異常類型,不回滾事務。
  • isolation事務的隔離級別,默認值採用 DEFAULT。當多個事務併發執行時,可能會出現髒讀,不可重複讀,幻讀等現象時,但假如不但願出現這些現象可考慮修改事務的隔離級別(但隔離級別越高併發就會越小,性能就會越差)

Spring 中事務控制過程分析,如圖-11所示:
image

Spring事務管理是基於接口代理(JDK)或動態字節碼(CGLIB)技術,而後經過AOP實施事務加強的。當咱們執行添加了事務特性的目標方式時,系統會經過目標對象的代理對象調用DataSourceTransactionManager對象,在事務開始的時,執行doBegin方法,事務結束時執行doCommit或doRollback方法。

4.2.3Spring 中事務傳播特性

事務傳播(Propagation)特性指"不一樣業務(service)對象"中的事務方法之間相互調用時,事務的傳播方式,如圖-12所示:
image

其中,經常使用事務傳播方式以下:

  • @Transactional(propagation=Propagation.REQUIRED) 。

若是沒有事務建立新事務, 若是當前有事務參與當前事務, Spring 默認的事務傳播行爲是PROPAGATION_REQUIRED,它適合於絕大多數的狀況。假設 ServiveX#methodX() 都工做在事務環境下(即都被 Spring 事務加強了),假設程序中存在以下的調用鏈:

Service1#method1()->Service2#method2()->Service3#method3(),那麼這 3 個服務類的 3 個方法經過 Spring 的事務傳播機制都工做在同一個事務中。如圖-13所示:
image
代碼示例以下:

@Transactional(propagation = Propagation.REQUIRED)

 @Override

 public List<Node> findZtreeMenuNodes() {

 return sysMenuDao.findZtreeMenuNodes();

 }

當有一個業務對象調用如上方法時,此方法始終工做在一個已經存在的事務方法,或者是由調用者建立的一個事務方法中。

@Transactional(propagation=Propagation.REQUIRES_NEW)。
必須是新事務, 若是有當前事務, 掛起當前事務而且開啓新事務,如圖-14所示:
image

代碼示例以下:

@Transactional(propagation = Propagation.REQUIRES\_NEW)

 @Override

 public void saveObject(SysLog entity) {

 sysLogDao.insertObject(entity);

 }

當有一個業務對象調用如上業務方法時,此業務方法會始終運行在一個新的事務中。

4.3 Spring 中事務管理小結
pring 聲明式事務是 Spring 最核心,最經常使用的功能。因爲 Spring 經過 IOC 和 AOP的功能很是透明地實現了聲明式事務的功能,對於通常的開發者基本上無須瞭解 Spring聲明式事務的內部細節,僅須要懂得如何配置就能夠了。但對於中高端開發者還須要瞭解其內部機制。

5. Spring AOP 異步操做實現

5.1異步場景分析
在開發系統的過程當中,一般會考慮到系統的性能問題,提高系統性能的一個重要思想就是「串行」改「並行」。提及「並行」天然離不開「異步」,今天咱們就來聊聊如何使用Spring的@Async的異步註解。

5.2 Spring 業務的異步實現
5.2.1啓動異步配置
在基於註解方式的配置中,藉助@EnableAsync註解進行異步啓動聲明,Spring Boot版的項目中,將@EnableAsync註解應用到啓動類上,代碼示例以下:

/**
 * 
    @EnableAsync 註解表示進行異步啓動聲明(啓動異步任務) spring容器啓動時會建立線程池
 *
 */
@EnableAsync//啓動異步任務
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

5.2.2 Spring中@Async註解應用
在須要異步執行的業務方法上,使用@Async方法進行異步聲明。

//@Async註解描述的方法爲一個異步切入點方法(此方法會運行在spring框架提供的一個線程中)
    @Async  //@Async 描述的方法表示要運行在一個獨立線程中(這個線程由spring內置的線程池來提供)
    @Transactional(propagation = Propagation.REQUIRES_NEW) //讓saveObject方法始終運行在一個本身獨立的事務中
    @Override
    public void saveObject(SysLog entity) {
        System.out.println("SysLogServiceImpl.saveObject.thread->"+Thread.currentThread().getName());
        //模擬耗時操做(假設寫日誌是耗時的)
        try {Thread.sleep(5000);} catch (Exception e) {} 
        sysLogDao.insertObject(entity);
    }

假如須要獲取業務層異步方法的執行結果,可參考以下代碼設計進行實現:

@Transactional(propagation = Propagation.REQUIRES\_NEW)

 @Async

 @Override

 public Future<Integer> saveObject(SysLog entity) {

 System.out.println("SysLogServiceImpl.save:"+

Thread.currentThread().getName());

 int rows\=sysLogDao.insertObject(entity);

 //try{Thread.sleep(5000);}catch(Exception e) {}

 return new AsyncResult<Integer>(rows);

 }

其中,AsyncResult對象能夠對異步方法的執行結果進行封裝,假如外界須要異步方法結果時,能夠經過Future對象的get方法獲取結果。

當咱們須要本身對spring框架提供的鏈接池進行一些簡易配置,能夠參考以下代碼:

當咱們須要本身對spring框架提供的鏈接池進行一些簡易配置,能夠參考以下代碼:

spring:
  task:
    execution:
      pool:
        core-size: 3  #定義核心線程數,當池中線程沒有達到這個值時,每來一個任務都會建立一個新的線程.
        queue-capacity: 1 #當來了新的任務,核心線程都在忙,假如隊列尚未滿,則將任務先扔到隊列
        max-size: 256 #當核心線程數都在忙,任務隊列也滿了,再來新的任務在建立新的線程,最多能夠建立多少個由這個參數決定
        keep-alive: 60000 #當併發訪問高峯期事後,有些線程可能會空閒下來,超出必定的時間,線程要被釋放,這個時間的指定由這個參數決定
    thread-name-prefix: cgb-db-thread- #爲池中的線程起個名字的前綴

說明:對於@Async註解默認會基於ThreadPoolTaskExecutor對象獲取工做線程,而後調用由@Async描述的方法,讓方法運行於一個工做線程,以實現異步操做。可是假如系統中的默認拒絕處理策略,任務執行過程的異常處理不能知足咱們自身業務需求的話,我能夠對異步線程池進行自定義.(SpringBoot中默認的異步配置能夠參考自動配置對象TaskExecutionAutoConfiguration).

5.2.3Spring 自定義異步池的實現(拓展)
爲了讓Spring中的異步池更好的服務於咱們的業務,同時也儘可能避免OOM(Out Of Memory:內存溢出 ),能夠自定義線程池優化設計以下:關鍵代碼以下:

package com.cy.pj.common.config

@Slf4j

@Setter

@Configuration

@ConfigurationProperties("async-thread-pool")

public class SpringAsyncConfig implements AsyncConfigurer{

 /\*\*核心線程數\*/

 private int corePoolSize\=20;

 /\*\*最大線程數\*/

 private int maximumPoolSize=1000;

 /\*\*線程空閒時間\*/

 private int keepAliveTime\=30;

 /\*\*阻塞隊列容量\*/

 private int queueCapacity\=200;

 /\*\*構建線程工廠\*/

 private ThreadFactory threadFactory\=new ThreadFactory() {

 //CAS算法

 private AtomicInteger at\=new AtomicInteger(1000);

 @Override

 public Thread newThread(Runnable r) {

 return new Thread(r,

"db-async-thread-"+at.getAndIncrement());

 }

 }; 

 @Override

 public Executor getAsyncExecutor() {

 ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

 executor.setCorePoolSize(corePoolSize);

 executor.setMaxPoolSize(maximumPoolSize);

 executor.setKeepAliveSeconds(keepAliveTime);

 executor.setQueueCapacity(queueCapacity);

 executor.setRejectedExecutionHandler((Runnable r,

 ThreadPoolExecutor exe) -> {

 log.warn("當前任務線程池隊列已滿.");

 });

 executor.initialize();

 return executor;

 }

 @Override

 public AsyncUncaughtExceptionHandler

getAsyncUncaughtExceptionHandler() {

 return new AsyncUncaughtExceptionHandler() {

 @Override

 public void handleUncaughtException(Throwable ex ,

 Method method , Object... params) {

 log.error("線程池執行任務發生未知異常.", ex);

 }

 };

 }}

其中:@ConfigurationProperties("async-thread-pool")的含義是讀取application.yml配置文件中以"async-thread-pool"名爲前綴的配置信息,並經過所描述類的set方法賦值給對應的屬性,在application.yml中鏈接器池的關鍵配置以下:

async-thread-pool:

 corePoolSize: 20

 maxPoolSize: 1000

 keepAliveSeconds: 30

 queueCapacity: 1000

後續在業務類中,假如咱們使用@Async註解描述業務方法,默認會使用ThreadPoolTaskExecutor池對象中的線程執行異步任務

6 Spring AOP中Cache操做實現(拓展)

6.1緩存場景分析
在業務方法中咱們可能調用數據層方法獲取數據庫中數據,假如訪問數據的頻率比較高,爲了提升的查詢效率,下降數據庫的訪問壓力,能夠在業務層對數據進行緩存.
6.2Spring 中業務緩存應用實現
6.2.1啓動緩存配置
在項目(SpringBoot項目)的啓動類上添加@EnableCaching註解,以啓動緩存配置。關鍵代碼以下:

/\*\*

\* 異步的自動配置生效).

 \* @EnableCaching 註解表示啓動緩存配置

 \*/

@EnableCaching

@SpringBootApplication

public class Application {

 public static void main(String\[\] args) {

 SpringApplication.run(Application.class, args);

 }

}

6.2.2業務方法上應用緩存配置
在須要進行緩存的業務方法上經過@Cacheable註解對方法進行相關描述.表示方法的返回值要存儲到Cache中,假如在更新操做時須要將cache中的數據移除,能夠在更新方法上使用@CacheEvict註解對方法進行描述。例如:
第一步:在相關模塊查詢相關業務方法中,使用緩存,關鍵代碼以下:

/**@Cacheable 註解描述的方法爲一個緩存切入點方法,此方法在執行以前首先會從緩存中取數據,緩存中沒有數據,則從數據庫查詢*/
@Cacheable(value = "menuCache")

@Transactional(readOnly = true)

public List<Map<String,Object>> findObjects() {

....

}

其中,value屬性的值表示要使用的緩存對象,名字本身指定,其中底層爲一個map對象,當向cache中添加數據時,key默認爲方法實際參數的組合。

第二步:在相關模塊更新時,清除指定緩存數據,關鍵代碼以下:

/**@CacheEvict 註解描述的方法也是一個緩存切入點方法,此方法在執行以前首先會從緩存中取數據,緩存中沒有數據,則從數據庫查詢
     * 1) value 屬性用於指定緩存對象的名稱
     * 2) allEntries 用於指定是否清除緩存全部數據,true表示清除緩存全部數據
     * 3) beforeInvocation用於指定清楚緩存的動做在目標方法執行以前仍是以後執行.這裏的false表示以後
    */
    
  @CacheEvict(value="menuCache",allEntries\=true)

 @Override

 public int saveObject(SysDept entity) {...}

其中,allEntries表示清除全部。

說明:spring中的緩存應用原理,如圖-15所示:
image

6.2.3Spring中自定義緩存的實現(拓展)
在Spring中默認cache底層實現是一個Map對象,假如此map對象不能知足咱們實際須要,在實際項目中咱們能夠將數據存儲到第三方緩存系統中.

Spring AOP原生方式實現(拓展)

7.1概述
Spring 整合AspectJ框架實現AOP只是Spring框架中AOP的一種實現方式,此方式相對比較簡單,實現方便。但此方式底層仍是要轉換爲Spring原生AOP的實現,Spring AOP原生方式實現的核心有三大部分構成,分別是:

  • JDK代理。
  • CGLIB代理。
  • org.aopalliance包下的攔截體系。

7.1案例架構分析
本小節以Spring中一種原生AOP架構的基本實現爲例進行原理分析和說明,其簡易架構如圖-16所示:image
其中DefaultAdvisorAutoProxyCreator這個類功能更爲強大,這個類的奇妙之處是他實現BeanPostProcessor接口,當ApplicationContext讀取全部的Bean配置信息後,這個類將掃描上下文,尋找全部的Advisor對象(一個Advisor由切入點和通知組成),將這些Advisor應用到全部符合切入點的Bean中。
7.2案例業務實現
7.2.1業務描述
建立SpringBoot項目,並基於Spring原生AOP的實現爲特定業務對象添加簡易日誌實現。
7.2.2核心業務接口定義及實現
定義RequiredLog註解,用於描述目標業務對象

package com.cy.spring.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

public @interface RequiredLog {

}

定義搜索業務接口,用於定義搜索業務規範

package com.cy.spring.aop;

public interface SearchService {

 Object search(String key);

}

定義搜索業務接口實現,並使用requiredLog註解描述

package com.cy.spring.aop;

import org.springframework.stereotype.Service;

import com.cy.spring.annotation.RequiredLog;

@Service

public class DefaultSearchService implements SearchService {

 @RequiredLog

 @Override

 public Object search(String key) {

 System.out.println("search by "+key);

 return null;

 }

}

7.2.3日誌Advice對象定義
定義LogAdvice對象,基於此對象爲目標業務對象作日誌加強。

package com.cy.spring.advisor;

import org.aopalliance.intercept.MethodInterceptor;

import org.aopalliance.intercept.MethodInvocation;

public class LogAdvice implements MethodInterceptor {

 @Override

 public Object invoke(MethodInvocation invocation)

 throws Throwable {

 System.out.println("start:"+System.currentTimeMillis());

 Object result=invocation.proceed();

 System.out.println("after:"+System.currentTimeMillis());

 return result;

 }

}

其中,MethodInterceptor對象繼承Advice對象,基於此對象方法能夠對目標方法進行攔截。

7.2.4日誌Advisor對象定義及實現
建立日誌Advisor對象,在對象內部定義要切入擴展功能的點以及要應用的通知(Advice)對象。

package com.cy.spring.advisor;

import java.lang.reflect.Method;

import org.springframework.stereotype.Component;

import com.cy.spring.annotation.RequiredLog;

@Component

public class LogAdvisor extends StaticMethodMatcherPointcutAdvisor {

 private static final long serialVersionUID = 7022316764822635205L;

 public LogMethodMatcher() {

 //在特定切入點上要執行的通知

 setAdvice(new LogAdvice());

 }

 //Pointcut

 //方法返回值爲true時,則能夠爲目標方法對象建立代理對象

 @Override

 public boolean matches(Method method,Class<?> targetClass) {

 try {

 Method targetMethod=

 targetClass.getMethod(method.getName(),

 method.getParameterTypes());

 return targetMethod.isAnnotationPresent(RequiredLog.class);

 }catch(Exception e) {

 return false;

 }

 }

}

其中,StaticMethodMatcherPointcutAdvisor類爲Spring框架中定義的一種Advisor,咱們本身寫的Advisor能夠直接繼承此類進行資源整合。
7.2.5日誌業務單元測試實現
基於Spring boot項目進行單元測試:

package com.cy;

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.test.context.junit4.SpringRunner;

import com.cy.spring.aop.SearchService;

@SpringBootTest

public class CgbSbootAop01ApplicationTests {

 @Autowired

 private SearchService searchService;

 @Test

 public void testSearch() {

 //System.out.println(searchService);

 searchService.search("tedu");

 }

}

說明:在spring 框架中,不少功能都是原生AOP進行了功能的擴展和實現。

8 Spirng AOP總結

8.1重難點分析

  • AOP 是什麼,解決了什麼問題,實現原理,應用場景。
    AOP(Aspect Orient Programming)是一種設計思想,是軟件設計領域中的面向切面編程,
    解決的問題: 它是面向對象編程(OOP)的一種補充和完善。它以經過預編譯方式和運行期動態代理方式,實如今不修改源代碼的狀況下給程序動態統一添加額外功能的一種技術。
    AOP實現原理:Spring AOP底層基於代理機制實現功能擴展:

(1). 假如目標對象(被代理對象)實現接口,則底層能夠採用JDK動態代理機制爲目標對象建立代理對象(目標類和代理類會實現共同接口)。
(2). 假如目標對象(被代理對象)沒有實現接口,則底層能夠採用CGLIB代理機制爲目標對象建立代理對象(默認建立的代理類會繼承目標對象類型)

應用場景:AOP就是要基於OCP(開閉原則),在不改變原有系統核心業務代碼的基礎上動態添加一些擴展功能並能夠"控制"對象的執行。例如AOP應用於項目中的日誌處理,事務處理,權限處理,緩存處理等等

  • AOP 編程基本步驟及實現過程(以基於AspectJ框架實現爲例)。
  • AOP 編程中的核心對象及應用關係。
  • AOP 思想在Spring中的實現原理分析。
  • AOP 編程中基於註解方式的配置實現。(@Aspect,@PointCut,@Around,...)
  • AOP 編程中基於註解方式的事務控制。(@Transactional)
  • AOP 編程中異步操做的實現?(@EnableAsync,@Async)
  • AOP 編程中的緩存應用?(@EnableCaching,@Cacheable,@CacheEvict)

8.1FAQ分析

  • 什麼是OCP原則(開閉原則)?
  • 什麼是DIP原則 (依賴倒置)?
  • 什麼是單一職責原則(SRP)?
  • Spring 中AOP的有哪些配置方式?(XML,註解)
  • Spring 中AOP 的通知有哪些基本類型?(5種)
    @Before。
    @AfterReturning。
    @AfterThrowing。
    @After。
    @Around.重點掌握(優先級最高)
  • Spring 中AOP是如何爲Bean對象建立代理對象的?(JDK,CGLIB)
  • Spring 中AOP切面的執行順序如何指定?(@Order)
  • Spring 單體架構項目中事務的控制要經過Connection對象實現,?
  • Spring 如何保證一個線程一個Connection對象?藉助ThreadLocal實現.?
  • 多個事務併發執行時可能會出現什麼問題?(髒讀,不可重複讀,幻影讀,又叫幻讀)
    what's 髒讀(dirty read)?
    在一個事務中,讀取到另外一個事務未提交更新的數據,即讀取到了髒 數據。提示:須要將數據庫的事務隔離級別設置爲最低,纔可以看到髒讀現象。
    what's 不可重複讀(unrepeatable read)?
    對同一記錄的兩次讀取結果不一致,由於在兩次查詢期間,有另外一事務對該記錄作了修改(是針對修改操做),這個過程叫作不可重複讀。
    what's 幻讀(虛讀)(phantom read)?
    對同一張表的兩次查詢結果不一致,由於在兩次查詢期間,有另外一事務進行了插入或者是刪除操做(是針對插入或刪除操做);這個過程叫作幻讀。
    注意:mysql默認的是不容許出現髒讀和不可重複讀
  • 如何理解數據庫中的的悲觀鎖和樂觀鎖?
  • 你瞭解事務的隔離級別嗎?知道具體的應用場景嗎?
  • 事務隔離級別分四個等級,在相同數據環境下,對數據執行相同的操做,設置不一樣的隔離級別,可能致使不一樣的結果。不一樣事務隔離級別可以解決的數據併發問題的能力也是不一樣的。

一、READ UNCOMMITTED(讀未提交數據)

安全性最差,可能出現任何事務併發問題(好比髒讀、不能夠重複讀、幻讀等)

但性能最好(不使用!!)

二、READ COMMITTED(讀已提交數據)(Oracle默認)

安全性較差

性能較好

能夠防止髒讀,但不能防止不可重複讀,也不能防止幻讀;

三、REPEATABLE READ(可重複讀)(MySQL默認)

安全性較高

性能較差

能夠防止髒讀不可重複讀,但不能防止幻讀問題;

四、SERIALIZABLE(串行化)

安全性最高,不會出現任何併發問題,由於它對同一數據的訪問是串行的,非併發訪問;

性能最差;(不使用!!)

MySQL的默認隔離級別爲REPEATABLE READ,便可以防止髒讀和不可重複讀

相關文章
相關標籤/搜索