Spring AOP 實現
AOP(Aspect Orient Programming),我們一般稱為面向切面程式設計,作為面向物件的一種補充,用於處理系統中分佈於各個模組的橫切關注點,比如事務、日誌、快取、分散式鎖等等。AOP實現的關鍵在於AOP框架自動建立的AOP代理,AOP代理主要分為靜態代理和動態代理,靜態代理的代表為AspectJ;而動態代理則以Spring AOP為代表。Spring的主要動態代理有CGLib和JDK自動代理。
使用AspectJ的編譯時增強實現AOP
AspectJ是靜態代理的增強,所謂的靜態代理就是AOP框架會在編譯階段生成AOP代理類,因此也稱為編譯時增強。
編譯成位元組碼.class比原來的.java會
使用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的通知註解:
- @Before: 前置通知, 在方法執行之前執行
- @After: 後置通知, 在方法執行之後執行
- @AfterRunning:返回通知, 在方法成功執行返回結果之後執行
- @AfterThrowing: 異常通知, 在方法丟擲異常之後
- @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);
}
}
列印日誌為:
可見同樣可以