spring的AOP和事務

動態代理

代理模式:給一個對象提供一個代理,並由代理對象來控制真實對象的訪問(調用者並不知道真實對象是什麼)。
代理模式分靜態代理和動態代理。這裏只討論動態代理,通俗的講,動態代理就是在不修改代碼的基礎對被代理對象進行方法的增強

基於接口的動態代理

JDK自帶的動態代理就是基於接口的動態代理,被代理對象至少要實現一個接口,否則就無法使用代理。底層還是基於Java的反射來創建代理對象的。
JDK動態代理主要涉及兩個類,java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler 。下面來看個例子。

被代理類的接口IProduct

public interface IProduct {
    void saleProduct(Float money);
}

被代理類Product實現上面這個接口

public class Product implements IProduct{
    @Override
    public void saleProduct(Float money) {
        System.out.println("賣出一個產品,收到金額:" + money);
    }
}

編寫一個客戶端類獲取一個Product的動態代理的對象,來控制被代理對象的訪問。 InvocationHandler的實現主要通過內部類實現(可以直接使用lambda表達式更簡潔)。

public class Client {
    public static void main(String[] args){
        Product product = new Product();
        // 基於接口的動態代理(jdk自帶的)
        // proxyProduct就是Proxy.newProxyInstance生成的代理對象,需要強轉爲被代理對象的接口類。
        // 參數:  
        //    ClassLoader:代理對象的類加載器。使用和被代理對象相同的類加載器,直接調用getClass().getClassLoader()獲取。
        //    Class<?>[]:被代理對象的接口的字節碼。直接調用getClass().getInterfaces()獲取就行
        //    InvocationHandler:進行代碼的增強。直接使用內部類或者new一個InvocationHandler的實現類(在實現類中進行代碼增強)
        IProduct proxyProduct = (IProduct) Proxy.newProxyInstance(product.getClass().getClassLoader()
                , product.getClass().getInterfaces(), new InvocationHandler() {
                    /**
                     * 進行代理增強的方法
                     * 參數:
                     *     proxy:當前的代理對象
                     *     method:被代理對象當前執行的方法
                     *     args:被代理對象當前執行的方法的參數
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        Object res = null;
                        // 獲取方法執行的參數
                        Float money = (Float)args[0];
                        // 判斷當前方法是否是銷售方法
                        if ("saleProduct".equals(method.getName())){
                            // 返回值與被代理對象方法的相同
                            res = method.invoke(product, money * 0.8f);
                        }
                        return res;
                    }
                });
        // 調用這個代理對象,執行被代理對象的方法。
        proxyProduct.saleProduct(1000f);
    }
}

基於子類的動態代理

CGLIB動態代理是一個基於ASM字節碼操作框架的代碼生成庫,它被廣泛用於基於代理的AOP框架提供方法攔截。
本質上說,對於需要被代理的類,它只是動態的生成一個子類以覆蓋非final的方法(所以它不能代理final類或final方法),同時綁定鉤子回調自定義的攔截器。它比使用Java反射的JDK動態代理方法更快。
CGLIB動態代理的核心是net.sf.cglib.proxy.Enhancer類(用於創建被代理對象子類),net.sf.cglib.proxy.MethodInterceptor是最常用的回調類型(它是net.sf.cglib.proxy.Callback的一個子接口)。
使用前需要引入CGLIB的jar依賴。

<!--cglib動態代理的依賴-->
<dependency>
     <groupId>cglib</groupId>
     <artifactId>cglib</artifactId>
     <version>3.2.12</version>
</dependency>

下面來看一個示例(被代理對象還是和上面的相同,這裏實現一個客戶端類)。

public class Client {
    public static void main(String[] args){
        Product product = new Product();
        /**
         * 基於子類的動態代理(cglib)
         * Enhancer的create方法,通過Enhancer來創建代理對象,MethodInterceptor來增強方法
         * 參數:
         *      Class:被代理對象的字節碼
         *      Callback:用於增強方法。一般使用這個接口的子接口MethodInterceptor。
         */
        Product proxyProduct =  (Product) Enhancer.create(product.getClass(), new MethodInterceptor() {
            /**
             * 執行被代理對象的任何方法都會經過此方法
             * @param proxy 當前代理對象
             * @param method 被代理對象要執行的方法
             * @param args1 要執行的方法的參數
             * 以上三個參數與基於接口的動態代理(jdk)的參數一致
             * @param methodProxy 當前執行方法的代理對象 一般使用這個對象來調用原始對象的方法,因爲它性能更高
             * @return 與被代理對象要執行的方法的返回值一致
             * @throws Throwable
             */
            @Override
            public Object intercept(Object proxy, Method method, Object[] args1, MethodProxy methodProxy) throws Throwable {
                Object res = null;
                // 獲取方法執行的參數
                Float money = (Float)args1[0];
                // 判斷當前方法是否是銷售方法
                if ("saleProduct".equals(method.getName())){
                    // 修改原始方法的參數
                    args1[0] = money * 0.8f;
                    // 與JDK動態代理的一個區別,使用MethodProxy對象來調用原始對象的方法
                    res = methodProxy.invoke(product, args1);
                }
                return res;
            }
        });
        // 通過代理對象執行被代理對象的方法
        proxyProduct.saleProduct(1000f);
    }
}

spring AOP

spring AOP是基於動態代理實現的,spring默認使用JDK動態代理實現AOP,也可以強制使用CGLIB。AOP是IoC的一種補充,IoC不依賴與AOP,但是AOP依賴與IoC。

AOP的一些概念

  • JoinPoint(連接點):指被攔截到的點。在spring AOP中,就是指那些被攔截的方法。
  • PointCut(切入點):是指篩選出的連接點(因爲所有的切入點都是連接點,但是有的連接點並不需要進行攔截增強方法)
  • Advice(通知/增強):需要完成的工作叫做通知,就是寫的業務邏輯代碼中的公共代碼,比如事務、日誌等。通知的類型有五種。
    • Before(前置通知):在連接點(攔截的方法)執行前的通知。
    • AfterReturning(後置通知):在連接點正常完成返回後的通知。
    • AfterThrowing(異常通知):如果方法執行過程中拋出了異常,則執行此通知。
    • After(最終通知):無論方法執行是否成功,最終都要執行這個通知。
    • Around(環繞通知):圍繞連接點的通知,它是最常用的通知,因爲它可以包括以上四種通知。
  • Aspect(切面):其實就是通知和切點的結合,通知和切點共同定義了切面的全部內容,它是幹什麼的,什麼時候在哪執行。
  • Introduction(引入):在不改變一個現有類代碼的情況下,爲該類添加屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行爲和狀態。
  • Target(目標):被通知的對象。也就是需要加入額外代碼的對象,也就是真正的業務邏輯被組織織入切面。
  • Weaving(織入):切面在指定的連接點被織入到目標對象中,在目標對象的生命週期裏有多個點(編譯期,類加載期,運行期)可以進行織入,spring是在運行期織入的。

@Aspectj支持

Spring 使用 AspectJ 提供的 library 解釋與 AspectJ 5 相同的註釋,用於切入點解析和匹配。但是,AOP 運行時仍然是純粹的 Spring AOP,並且不依賴於 AspectJ 編譯器或編織器。(也就是spring只是利用Aspectj來對切入點進行解析和匹配,真正的AOP的執行還是使用Spring AOP)。

在springboot中使用spring AOP非常的方便,只需引入一個spring支持AspectJ的starter依賴即可。

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

在spring中也很簡單,在引用spring AOP的基礎上再引入一個aspectjweaver依賴就行。

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.2</version>
</dependency>

在spring中,要使用 Java @Configuration啓用 @AspectJ 支持,還需添加@EnableAspectJAutoProxy註解(在springboot中不用)。

下面來看一個簡單的示例(基於springboot)。
先寫個Service類(包括接口和實現類)。

public interface AccountService {
    Account findById(Integer aid);
}

@Service
public class AccountServiceImpl implements AccountService {
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
    @Override
    public Account findById(Integer aid) {
        log.info("正在執行方法");
        return accountMapper.findById(aid);
    }
}

一個配置類,對service中的方法進行切面。

@Aspect // 聲明切面, 這個bean將被spring自動檢測,並用於配置Spring AOP
@Component // 將這個類註冊爲一個bean,交給spring來管理(所以spring AOP依賴於IoC)
public class Logger {

    private static org.slf4j.Logger log = LoggerFactory.getLogger(Logger.class);

    /**
     * 通用化切入點表達式,其他切入點可以直接引用這個
     */
    @Pointcut("execution(* tkmybatis.service.impl.*.*(..))")
    public void pointCut(){
    }

    /**
     * 前置通知,在切入點方法執行之前執行
     */
    @Before("pointCut()")
    public void printLogBefore(){
        log.info("我在方法執行前");
    }

    /**
     * 後置通知,方法正常返回後執行
     */
    @AfterReturning("pointCut()")
    public void printLogAfterReturning(){
        log.info("我在方法執行正常返回後");
    }

    /**
     * 異常通知,方法發生異常時執行
     */
    @AfterThrowing("pointCut()")
    public void printLogAfterThrow(){
        log.info("我在方法執行拋出異常後");
    }

    /**
     * 最終通知,無論方法是否執行正常,它都會在最後執行
     */
    @After("pointCut()")
    public void printLogAfter(){
        log.info("我在方法執行最後");
    }

    @Around("pointCut()")
    public Object printAround(ProceedingJoinPoint pjp){
        Object res;
        try{
            Object[] args = pjp.getArgs();
            log.info("我在方法執行前");
            res = pjp.proceed(args);
            log.info("我在方法執行正常返回後2");
            return res;
        }catch (Throwable t){
            log.info("我在方法執行拋出異常後2");
            throw new RuntimeException(t);
        }finally {
            log.info("我在方法執行最後");
        }
    }
}

對於切入點表達式execution(* tkmybatis.service.impl.*.*(..)) ,這裏做一下解釋。
execution表達式本來有三個部分(可見類型 返回值 連接點的全限定名),這裏的可見類型可以省略。

    • 代表返回值可以爲任何值。
  • tkmybatis.service.impl代表service實現類所在的包(進一步還可以直接用* .來替代,表示所有包)。
  • .* 代表這個包下的所有類。下一個.* 代表這個類下的所有方法(還可以用方法前綴來表示,比如find*)。
  • 括號裏的 … 代表參數可以爲任意參數,可爲多個或無。

更多高級用法詳見Spring官方文檔 Spring AOP

Spring 事務管理

Spring支持聲明式的事務管理和編程式的事務管理。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上註解就可以獲得完全的事務支持。Spring推薦使用聲明式的事務管理。
而聲明式的事務管理就是基於Spring AOP的,其本質就是對要執行的方法進行攔截,在方法執行前加入一個事務,方法執行完成後根據情況判斷是提交事務還是回滾事務(拋出了異常)。

事務的隔離級別

通過org.springframework.transaction.annotation.Isolation 這個枚舉類來指定。

public enum Isolation {
    // 這是默認值,表示使用底層數據庫的默認隔離級別。對於MySQL的InnoDB存儲引擎,它是REPEATABLE_READ,其它一般的都是READ_COMMITTED
    DEFAULT(-1),
    // 跟沒有一樣,幾乎不使用。
    READ_UNCOMMITTED(1),
    // 只能讀取另一個事務已提交的事務,能防止髒讀。也是一般數據庫的默認隔離級別。
    READ_COMMITTED(2),
    // 可重複讀(在一個事務內多次查詢的結果相同,其它事務不可修改該查詢條件範圍內的數據,相當於加了個讀鎖)
    REPEATABLE_READ(4),
    // 所有的事務依次逐個執行,相當於串行化了,效率太低,一般也不使用。
    SERIALIZABLE(8);
}

事務的傳播行爲

如果在開始當前事務之前,一個事務上下文已經存在,此時有若干選項可以指定一個事務性方法的執行行爲。org.springframework.transaction.annotation.Propagation枚舉類中定義了7表示傳播行爲的枚舉值。

public enum Propagation {

    // 如果當前存在事務,則加入該事務;如果當前沒有事務,則創建一個新的事務。
    REQUIRED(0),
    // 如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行。
    SUPPORTS(1),
    // 如果當前存在事務,則加入該事務;如果當前沒有事務,則拋出異常。
    MANDATORY(2),
    // 創建一個新的事務,如果當前存在事務,則把當前事務掛起。
    REQUIRES_NEW(3),
    // 以非事務方式運行,如果當前存在事務,則把當前事務掛起。
    NOT_SUPPORTED(4),
    // 以非事務方式運行,如果當前存在事務,則拋出異常。
    NEVER(5),
    // 如果當前存在事務,則創建一個事務作爲當前事務的嵌套事務來運行;如果當前沒有事務,則該取值等價於REQUIRED。
    NESTED(6);
}

聲明式事務的使用

聲明式事務可以直接使用@Transactional註解(這裏只討論這個)來配置,當然也可以使用XML配置文件。
先來看一下@Transactional註解都有啥

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 別名爲transactionManager,其實這兩是同一個。就是事務的名字。
    @AliasFor("transactionManager")
	String value() default "";
    // 事務的傳播行爲,默認值爲REQUIRED
    Propagation propagation() default Propagation.REQUIRED;
    // 事務的隔離級別,默認爲默認值(也就是使用底層數據庫的隔離級別)
    Isolation isolation() default Isolation.DEFAULT;
    // 超時時間,默認爲 -1,也就是沒有超時時間。
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    // 是否只讀,默認爲false。
    boolean readOnly() default false;
    // 會觸發事務回滾的異常的字節碼數組
    Class<? extends Throwable>[] rollbackFor() default {};
    // 不會觸發事務回滾的異常的字節碼數組
    Class<? extends Throwable>[] noRollbackFor() default {};
}

這裏直接在實現類上使用,這個註解還可以在接口(一般不在接口上使用)和方法上使用(可見性必須爲public,否則會被忽略)。
如果是在spring中使用,需要在配置類中加上@EnableTransactionManagement註解(springboot則不需要)。proxy-target-class屬性可以控制動態代理的類型,如果值爲false或忽略此屬性,則使用JDK的動態代理,可以設置爲true以強制使用CGLIB來進行動態代理(如果被代理的類沒有實現接口,將會自動使用CGLIB進行動態代理)。

@Service
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) // 這裏觸發回滾的異常設置爲Exception
public class AccountServiceImpl implements AccountService {
}

參考:
CGLIB動態代理介紹
Spring AOP
Spring AOP的使用
Spring 事務管理
Spring 事務

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章