Spring Boot實踐——AOP實現
借鑒:http://www.cnblogs.com/xrq730/p/4919025.html
https://blog.csdn.net/zhaokejin521/article/details/50144753
http://www.importnew.com/24305.html
AOP介紹
一、AOP
AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行為的一個集合。不過OOP允許開發者定義縱向的關系,但並不適合定義橫向的關系,例如日志功能。日志代碼往往橫向地散布在所有對象層次中,而與它對應的對象的核心功能毫無關系對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此,這種散布在各處的無關的代碼被稱為橫切(cross cutting),在OOP設計中,它導致了大量代碼的重復,而不利於各個模塊的重用。
AOP技術恰恰相反,它利用一種稱為"橫切"的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行為封裝到一個可重用模塊,並將其命名為"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻為業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重復代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。
使用"橫切"技術,AOP把軟件系統分為兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關系不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在核心關注點的多處,而各處基本相似,比如權限認證、日志、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
AOP(Aspect Orient Programming),我們一般稱為面向方面(切面)編程,作為面向對象的一種補充,用於處理系統中分布於各個模塊的橫切關注點,比如事務管理、日志、緩存等等。AOP實現的關鍵在於AOP框架自動創建的AOP代理,AOP代理主要分為靜態代理和動態代理,靜態代理的代表為AspectJ;而動態代理則以Spring AOP為代表。
與AspectJ的靜態代理不同,Spring AOP使用的動態代理,所謂的動態代理就是說AOP框架不會去修改字節碼,而是在內存中臨時為方法生成一個AOP對象,這個AOP對象包含了目標對象的全部方法,並且在特定的切點做了增強處理,並回調原對象的方法。
Spring AOP中的動態代理主要有兩種方式,JDK動態代理和CGLIB動態代理。JDK動態代理通過反射來接收被代理的類,並且要求被代理的類必須實現一個接口。JDK動態代理的核心是InvocationHandler
接口和Proxy
類。
如果目標類沒有實現接口,那么Spring AOP會選擇使用CGLIB來動態代理目標類。CGLIB(Code Generation Library),是一個代碼生成的類庫,是利用asm開源包,可以在運行時動態的生成某個類的子類。注意,CGLIB是通過繼承的方式做的動態代理,因此如果某個類被標記為final
,那么它是無法使用CGLIB做動態代理的。
二、AOP核心概念
1、橫切關注點
對哪些方法進行攔截,攔截后怎么處理,這些關注點稱之為橫切關注點
2、切面(aspect)
類是對物體特征的抽象,切面就是對橫切關注點的抽象
3、連接點(joinpoint)
被攔截到的點,因為Spring只支持方法類型的連接點,所以在Spring中連接點指的就是被攔截到的方法,實際上連接點還可以是字段或者構造器
4、切入點(pointcut)
對連接點進行攔截的定義
5、通知(advice)
所謂通知指的就是指攔截到連接點之后要執行的代碼,通知分為前置、后置、異常、最終、環繞通知五類
6、目標對象
代理的目標對象
7、織入(weave)
將切面應用到目標對象並導致代理對象創建的過程
8、引入(introduction)
在不修改代碼的前提下,引入可以在運行期為類動態地添加一些方法或字段
三、Spring對AOP的支持
Spring中AOP代理由Spring的IOC容器負責生成、管理,其依賴關系也由IOC容器負責管理。因此,AOP代理可以直接使用容器中的其它bean實例作為目標,這種關系可由IOC容器的依賴注入提供。Spring創建代理的規則為:
1、默認使用Java動態代理來創建AOP代理,這樣就可以為任何接口實例創建代理了
2、當需要代理的類不是代理接口的時候,Spring會切換為使用CGLIB代理,也可強制使用CGLIB
AOP編程其實是很簡單的事情,縱觀AOP編程,程序員只需要參與三個部分:
1、定義普通業務組件
2、定義切入點,一個切入點可能橫切多個業務組件
3、定義增強處理,增強處理就是在AOP框架為普通業務組件織入的處理動作
所以進行AOP編程的關鍵就是定義切入點和定義增強處理,一旦定義了合適的切入點和增強處理,AOP框架將自動生成AOP代理,即:代理對象的方法=增強處理+被代理對象的方法。
實現方式
Spring除了支持Schema方式配置AOP,還支持注解方式:使用@AspectJ風格的切面聲明。
一、Aspectj介紹
@AspectJ 作為通過 Java 5 注釋注釋的普通的 Java 類,它指的是聲明 aspects 的一種風格。
AspectJ是靜態代理的增強,所謂的靜態代理就是AOP框架會在編譯階段生成AOP代理類,因此也稱為編譯時增強。
AspectJ: 基於字節碼操作(Bytecode Manipulation),通過編織階段(Weaving Phase),對目標Java類型的字節碼進行操作,將需要的Advice邏輯給編織進去,形成新的字節碼。畢竟JVM執行的都是Java源代碼編譯后得到的字節碼,所以AspectJ相當於在這個過程中做了一點手腳,讓Advice能夠參與進來。
而編織階段可以有兩個選擇,分別是加載時編織(也可以成為運行時編織)和編譯時編織
- 加載時編織(Load-Time Weaving):顧名思義,這種編織方式是在JVM加載類的時候完成的。
- 編譯時編織(Compile-Time Weaving):需要使用AspectJ的編譯器來替換JDK的編譯器。
詳情:AOP的兩種實現-Spring AOP以及AspectJ
1、添加spirng aop支持和AspectJ依賴
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>5.0.7.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency>
或
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> </dependencies>
2、啟用對@AspectJ的支持
Spring默認不支持@AspectJ風格的切面聲明,為了支持需要使用如下配置:
<!-- 自動掃描使用了aspectj注解的類 --> <aop:aspectj-autoproxy/>
或者在配置類上加注解
@Configuration @ComponentScan("com.only.mate.springboot.aop") @EnableAspectJAutoProxy//開啟AspectJ注解 public class CustomAopConfigurer { }
3、聲明切面
@Aspect @Component public class CustomLogAspect { }
或者
定一個普通類
public class CustomAuthorityAspect { }
在配置文件中定義一個POJO
<bean id="customAuthorityAspect" class="com.only.mate.springboot.aop.CustomAuthorityAspect" />
然后在該切面中進行切入點及通知定義,接着往下看吧。
4、聲明切入點
@AspectJ風格的命名切入點使用org.aspectj.lang.annotation包下的@Pointcut+方法(方法必須是返回void類型)實現。
@Pointcut(value="切入點表達式", argNames = "參數名列表") public void pointcutName(……) {}
value:指定切入點表達式;
argNames:指定命名切入點方法參數列表參數名字,可以有多個用“,”分隔,這些參數將傳遞給通知方法同名的參數,同時比如切入點表達式“args(param)”將匹配參數類型為命名切入點方法同名參數指定的參數類型。
pointcutName:切入點名字,可以使用該名字進行引用該切入點表達式。
案例:
@Pointcut(value="execution(* com.only.mate.springboot.controller.*.sayAdvisorBefore(..)) && args(param)", argNames = "param") public void pointCut(String param) {}
定義了一個切入點,名字為“pointCut”,該切入點將匹配目標方法的第一個參數類型為通知方法實現中參數名為“param”的參數類型。
5、聲明通知
@AspectJ風格的聲明通知也支持5種通知類型:
A、前置通知:使用org.aspectj.lang.annotation 包下的@Before注解聲明。
@Before(value = "切入點表達式或命名切入點", argNames = "參數列表參數名")
value:指定切入點表達式或命名切入點。
argNames:與Schema方式配置中的同義。
B、后置返回通知:使用org.aspectj.lang.annotation 包下的@AfterReturning注解聲明。
@AfterReturning( value="切入點表達式或命名切入點", pointcut="切入點表達式或命名切入點", argNames="參數列表參數名", returning="返回值對應參數名")
value:指定切入點表達式或命名切入點。
pointcut:同樣是指定切入點表達式或命名切入點,如果指定了將覆蓋value屬性指定的,pointcut具有高優先級。
argNames:與Schema方式配置中的同義。
returning:與Schema方式配置中的同義。
C、后置異常通知:使用org.aspectj.lang.annotation 包下的@AfterThrowing注解聲明。
@AfterThrowing ( value="切入點表達式或命名切入點", pointcut="切入點表達式或命名切入點", argNames="參數列表參數名", throwing="異常對應參數名")
value:指定切入點表達式或命名切入點。
pointcut:同樣是指定切入點表達式或命名切入點,如果指定了將覆蓋value屬性指定的,pointcut具有高優先級。
argNames:與Schema方式配置中的同義。
throwing:與Schema方式配置中的同義。
D、后置最終通知:使用org.aspectj.lang.annotation 包下的@After注解聲明。
@After ( value="切入點表達式或命名切入點", argNames="參數列表參數名")
value:指定切入點表達式或命名切入點。
argNames:與Schema方式配置中的同義。
E、環繞通知:使用org.aspectj.lang.annotation 包下的@Around注解聲明。
@Around ( value="切入點表達式或命名切入點", argNames="參數列表參數名")
value:指定切入點表達式或命名切入點。
argNames:與Schema方式配置中的同義。
二、實踐
1、Schema方式配置AOP
A、定一個切入點
/** * 自定義一個切入點-權限校驗 * @ClassName: CustomAuthorityAspect * @Description: TODO * @author OnlyMate * @Date 2018年9月7日 下午2:24:24 * */ public class CustomAuthorityAspect { private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class); /** * 加密 * @Title: encode * @Description: TODO * @Date 2018年9月7日 下午2:30:05 * @author OnlyMate */ public void encode() { logger.info("CustomAuthorityAspect ==> encode method: encode data"); } /** * 解密 * @Title: decode * @Description: TODO * @Date 2018年9月7日 下午2:30:11 * @author OnlyMate */ public void decode() { logger.info("CustomAuthorityAspect ==> decode method: decode data"); } }
B、通過Schema方式配置AOP
<bean id="customAuthorityAspect" class="com.only.mate.springboot.aop.CustomAuthorityAspect" /> <aop:config proxy-target-class="false"> <!-- AOP實現 --> <aop:aspect id="customAuthority" ref="customAuthorityAspect"> <aop:pointcut id="addAllMethod" expression="execution(* com.only.mate.springboot.controller.*.*(..))" /> <aop:before method="encode" pointcut-ref="addAllMethod" /> <aop:after method="decode" pointcut-ref="addAllMethod" /> </aop:aspect> </aop:config>
前面說過Spring使用動態代理或是CGLIB生成代理是有規則的,高版本的Spring會自動選擇是使用動態代理還是CGLIB生成代理內容,當然我們也可以強制使用CGLIB生成代理,那就是<aop:config>里面有一個"proxy-target-class"屬性,這個屬性值如果被設置為true,那么基於類的代理將起作用,如果proxy-target-class被設置為false或者這個屬性被省略,那么基於接口的代理將起作用
2、使用@AspectJ風格的切面聲明
A、定一個切入點
/** * @Description: 自定義切面 * @ClassName: CustomLogAspect * @author OnlyMate * @Date 2018年9月10日 下午3:51:32 * */ @Aspect @Component public class CustomLogAspect { private Logger logger = LoggerFactory.getLogger(CustomLogAspect.class); /** * @Description: 定義切入點 * @Title: pointCut * @author OnlyMate * @Date 2018年9月10日 下午3:52:17 */ //被注解CustomAopAnnotation表示的方法 //@Pointcut("@annotation(com.only.mate.springboot.annotation.CustomAopAnnotation") @Pointcut("execution(public * com.only.mate.springboot.controller.*.*(..))") public void pointCut(){ } /** * @Description: 定義前置通知 * @Title: before * @author OnlyMate * @Date 2018年9月10日 下午3:52:23 * @param joinPoint * @throws Throwable */ @Before("pointCut()") public void before(JoinPoint joinPoint) throws Throwable { // 接收到請求,記錄請求內容 logger.info("【注解:Before】------------------切面 before"); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 記錄下請求內容 logger.info("【注解:Before】瀏覽器輸入的網址=URL : " + request.getRequestURL().toString()); logger.info("【注解:Before】HTTP_METHOD : " + request.getMethod()); logger.info("【注解:Before】IP : " + request.getRemoteAddr()); logger.info("【注解:Before】執行的業務方法名=CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); logger.info("【注解:Before】業務方法獲得的參數=ARGS : " + Arrays.toString(joinPoint.getArgs())); } /** * @Description: 后置返回通知 * @Title: afterReturning * @author OnlyMate * @Date 2018年9月10日 下午3:52:30 * @param ret * @throws Throwable */ @AfterReturning(returning = "ret", pointcut = "pointCut()") public void afterReturning(Object ret) throws Throwable { // 處理完請求,返回內容 logger.info("【注解:AfterReturning】這個會在切面最后的最后打印,方法的返回值 : " + ret); } /** * @Description: 后置異常通知 * @Title: afterThrowing * @author OnlyMate * @Date 2018年9月10日 下午3:52:37 * @param jp */ @AfterThrowing("pointCut()") public void afterThrowing(JoinPoint jp){ logger.info("【注解:AfterThrowing】方法異常時執行....."); } /** * @Description: 后置最終通知,final增強,不管是拋出異常或者正常退出都會執行 * @Title: after * @author OnlyMate * @Date 2018年9月10日 下午3:52:48 * @param jp */ @After("pointCut()") public void after(JoinPoint jp){ logger.info("【注解:After】方法最后執行....."); } /** * @Description: 環繞通知,環繞增強,相當於MethodInterceptor * @Title: around * @author OnlyMate * @Date 2018年9月10日 下午3:52:56 * @param pjp * @return */ @Around("pointCut()") public Object around(ProceedingJoinPoint pjp) { logger.info("【注解:Around . 環繞前】方法環繞start....."); try { //如果不執行這句,會不執行切面的Before方法及controller的業務方法 Object o = pjp.proceed(); logger.info("【注解:Around. 環繞后】方法環繞proceed,結果是 :" + o); return o; } catch (Throwable e) { e.printStackTrace(); return null; } } }
B、使用@AspectJ風格的切面聲明
/** * 自定義AOP配置類 * @ClassName: CustomAopConfigurer * @Description: TODO * @author OnlyMate * @Date 2018年9月7日 下午3:43:21 * */ @Configuration @ComponentScan("com.only.mate.springboot.aop") @EnableAspectJAutoProxy//開啟AspectJ注解 public class CustomAopConfigurer { }
效果圖
總結
AspectJ在編譯時就增強了目標對象,Spring AOP的動態代理則是在每次運行時動態的增強,生成AOP代理對象,區別在於生成AOP代理對象的時機不同,相對來說AspectJ的靜態代理方式具有更好的性能,但是AspectJ需要特定的編譯器進行處理,而Spring AOP則無需特定的編譯器處理。