1. 程式人生 > >Spring AOP 實現

Spring AOP 實現

AOP(Aspect Orient Programming),我們一般稱為面向切面程式設計,作為面向物件的一種補充,用於處理系統中分佈於各個模組的橫切關注點,比如事務、日誌、快取、分散式鎖等等。AOP實現的關鍵在於AOP框架自動建立的AOP代理,AOP代理主要分為靜態代理和動態代理,靜態代理的代表為AspectJ;而動態代理則以Spring AOP為代表。Spring的主要動態代理有CGLib和JDK自動代理。

使用AspectJ的編譯時增強實現AOP

AspectJ是靜態代理的增強,所謂的靜態代理就是AOP框架會在編譯階段生成AOP代理類,因此也稱為編譯時增強。

編譯成位元組碼.class比原來的.java會

多了一些程式碼,這就是AspectJ的靜態代理,它會在編譯階段將Aspect織入Java位元組碼中, 執行的時候就是經過增強之後的AOP物件。

使用Spring AOP

與AspectJ的靜態代理不同,Spring AOP使用的動態代理,所謂的動態代理就是說AOP框架不會去修改位元組碼,而是在記憶體中臨時為方法生成一個AOP物件,這個AOP物件包含了目標物件的全部方法,並且在特定的切點做了增強處理,並回調原物件的方法。

Spring AOP中的動態代理主要有兩種方式,JDK動態代理和CGLIB動態代理。JDK動態代理通過反射來接收被代理的類,並且要求被代理的類必須實現一個介面。JDK動態代理的核心是InvocationHandler

介面和Proxy類。

如果目標類沒有實現介面,那麼Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個程式碼生成的類庫,可以在執行時動態的生成某個類的子類,注意,CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記為final,那麼它是無法使用CGLIB做動態代理的。

現在我們做一個測試:

首先定義一個介面:

package cn.chinotan.service;

/**
 * @program: test
 * @description: 動物
 * @author: xingcheng
 **/
public interface Animal {

    /**
     * 跑
     * @param where 在什麼地方跑
     * @return
     */
    String run (String where);
    
}

其實現類:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地點是:" + where;
    }
}

其中@Action為自定義的註解,用來指定aop代理的切入點

package cn.chinotan.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @program: test
 * @description: 動作
 * @author: xingcheng
 **/
@Target(ElementType.METHOD)
public @interface Action {
    
}

定義Aspect:

package cn.chinotan.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * @program: test
 * @description: 動作aop實現
 * @author: xingcheng
 **/
@Aspect
@Component
public class ActionAspect {

    @Pointcut("@annotation(cn.chinotan.aop.Action)")
    void actionPointCut() {
    }

    @Before("actionPointCut()")
    void beforeAction() {
        System.out.println("熱身運動");
    }
}

其中有幾種aop的通知註解:

  1. @Before: 前置通知, 在方法執行之前執行
  2. @After: 後置通知, 在方法執行之後執行
  3. @AfterRunning:返回通知, 在方法成功執行返回結果之後執行
  4. @AfterThrowing: 異常通知, 在方法丟擲異常之後
  5. @Around: 環繞通知,圍繞著方法執行

@Pointcut是切入點的註解:

這裡使用了@annotation 可以在使用了自定義註解的配置方法上實現切入

也可以使用execution(* *(..))的形式:

    宣告切入點
    第一個*表示 方法  返回值(例如public int)
    第二個* 表示方法的全限定名(即包名+類名)
    perform表示目標方法引數括號兩個.表示任意型別引數
    方法表示式以“*”號開始,表明了我們不關心方法返回值的型別。然後,我們指定了全限定類名和方法名。對於方法引數列表,
    我們使用兩個點號(..)表明切點要選擇任意的perform()方法,無論該方法的入參是什麼
    execution表示執行的時候觸發

在啟動的application.yml配置檔案中加入

spring.aop.proxy-target-class: false

這個是控制aop的具體實現方式,為true 的話使用cglib,為false的話使用java的Proxy,預設是false

之後執行controller:

package cn.chinotan.controller;

import cn.chinotan.service.Animal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @program: test
 * @description: test類
 * @author: xingcheng
 **/
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    Animal animal;

    @GetMapping("/aopRun")
    public String aopRun() {
        animal.run("狗窩");
        System.out.println("dog代理為:" + animal.getClass());
        
        return "ok";
    }
    
}

列印日誌:

可以看到型別是com.sun.proxy.$Proxy71,也就是前面提到的Proxy類,因此這裡Spring AOP使用了JDK的動態代理。

再來看看不實現介面的情況,修改Dog類:

配置proxy-target-class: false依舊
 

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 **/
@Service
public class Dog {

    @Action
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地點是:" + where;
    }
}

列印日誌:

可以看到類被CGLIB增強了,也就是動態代理。這裡的CGLIB代理就是Spring AOP的代理,這個類也就是所謂的AOP代理,AOP代理類在切點動態地織入了增強處理。

可以看到:

    AspectJ在編譯時就增強了目標物件,Spring AOP的動態代理則是在每次執行時動態的增強,生成AOP代理物件,區別在於生成AOP代理物件的時機不同,相對來說AspectJ的靜態代理方式具有更好的效能,但是AspectJ需要特定的編譯器進行處理,而Spring AOP則無需特定的編譯器處理。

    java動態代理是利用反射機制生成一個實現代理介面的匿名類,在呼叫具體方法前呼叫InvokeHandler來處理。而cglib動態代理是利用asm開源包,對代理物件類的class檔案載入進來,通過修改其位元組碼生成子類來處理。如果目標物件實現了介面,預設情況下會採用JDK的動態代理實現AOP,如果目標物件實現了介面,可以強制使用CGLIB實現AOP,如果目標物件沒有實現了介面,必須採用CGLIB庫,spring會自動在JDK動態代理和CGLIB之間轉換

誤區注意:

在平時開發中,我們通常在Service中定義了一個方法並且切入之後,從Controller裡面呼叫該方法可以實現切入,但是當在同一個Service中實現另一方法並呼叫改方法時卻無法切入

類似於:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地點是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窩");
        System.out.println("狗在吃" + food);
    }
}


package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地點是:" + where;
    }

    @Override
    public void runToEat(String food) {
        run("狗窩");
        System.out.println("狗在吃" + food);
    }
}

我們在執行runToEat方法時,呼叫了自己類中的另一個方法,結果為:

可以看到run()的切面方法並沒有執行,以上結果的出現與Spring AOP的實現原理息息相關,由於Spring AOP採用了動態代理實現AOP,在Spring容器中的bean(也就是目標物件)會被代理物件代替,代理物件里加入了我們需要的增強邏輯,當呼叫代理物件的方法時,目標物件的方法就會被攔截,

通過呼叫代理物件的action方法,在其內部會經過切面增強,然後方法被髮射到目標物件,在目標物件上執行原有邏輯,如果在原有邏輯中巢狀呼叫了work方法,則此時work方法並沒有被進行切面增強,因為此時它已經在目標物件內部。而解決方案很好地說明了,將巢狀方法發射到代理物件,這樣就完成了切面增強。可以看下原始碼:

在程式碼3處,如果配置了exposeProxy開關,則會將代理物件暴露在當前執行緒中,以供其它需要的地方使用,通過使用靜態的全域性ThreadLocal變數就解決了問題。

spring提供了一個這樣的類:

可以看到他可以獲取到當前的aop代理,但是在獲取之前,得開啟exposeProxy開關

@EnableAspectJAutoProxy(proxyTargetClass = false, exposeProxy = true)

這樣就可以進行代理了,列印日誌為:

既然這樣可以,那是不是直接applicationContext.getBean()也可以呢?實驗過後得到的結果是可行,而且配置中的expose-proxy也不用設定成true,那試一下:

package cn.chinotan.service.impl;

import cn.chinotan.aop.Action;
import cn.chinotan.service.Animal;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;

/**
 * @program: test
 * @description: 狗
 * @author: xingcheng
 * @create: 2018-10-27 16:00
 **/
@Service
public class Dog implements Animal {
    
    @Autowired
    ApplicationContext applicationContext;

    @Action
    @Override
    public String run(String where) {
        System.out.println("狗往" + where + "跑");
        return "地點是:" + where;
    }

    @Override
    public void runToEat(String food) {
//        Dog dog = (Dog) AopContext.currentProxy();
        Dog dog = (Dog) applicationContext.getBean("dog");
        dog.run("狗窩");
        System.out.println("狗在吃" + food);
    }
}

列印日誌為:

可見同樣可以