一. AOP 概念
AOP
AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行爲的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌代碼往往橫向地散佈在所有對象層次中,而與它對應的對象的核心功能毫無關係對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此,這種散佈在各處的無關的代碼被稱爲橫切(cross cutting),在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。
AOP技術恰恰相反,它利用一種稱爲"橫切"的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲"Aspect",即切面。所謂"切面",簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。
使用"橫切"技術,AOP把軟件系統分爲兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在覈心關注點的多處,而各處基本相似,比如權限認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。
AOP核心概念
面向切面編程
(1)通知(增強)Advice
通知定義了切面是什麼以及何時使用,應該應用在某個方法被調用之前?之後?還是拋出異常時?等等。
(2)連接點 Join point
連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時,拋出異常時,甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程中,並添加新的行爲。
(3)切點 Pointcut
切點有助於縮小切面所通知的連接點的範圍。如果說通知定義了切面的“什麼”和“何時”的話,那麼切點就定義了“何處”,切點會匹配通知所要織入的一個或多個連接點,一般常用正則表達式定義所匹配的類和方法名稱來指定這些切點。
(4)切面 Aspect
切面是通知和切點的結合。通知和切點定義了切面的全部內容——它是什麼,在何時何處完成其功能。
(5)引入 Introduction
引入允許我們向現有的類添加新方法或屬性,從而無需修改這些現有類的情況下,讓他們具有新的行爲和狀態。
(6)織入 Weaving
在過去我常常把織入與引入的概念混淆,我是這樣來辨別的,“引入”我把它看做是一個定義,也就是一個名詞,而“織入”我把它看做是一個動作,一個動詞,也就是切面在指定的連接點被織入到目標對象中。
總結
通知包含了需要用於多個應用對象的橫切行爲;連接點是程序執行過程中能夠應用通知的所有點;切點定義了通知被應用的具體位置(在哪些連接點)。其中關鍵的概念是切點定義了哪些連接點會得到通知(增強)。創建切點來定義切面所織入的連接點是AOP框架的基本功能。
另外,Spring是基於動態代理的,所以Spring只支持方法連接點,而像AspectJ和JBoss除了方法切點,它們還提供字段和構造器接入點。如果需要方法攔截之外的連接點攔截功能,則可以利用AspectJ來補充SpringAOP的功能。
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代理,即:代理對象的方法=增強處理+被代理對象的方法。
二. 基於AspectJ的AOP簡單實現:
首先,在 applicationContext.xml 核心配置文件中需要用到 AOP 命名空間,和 兩個架包
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.8.RELEASE</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
註釋在類上
@Aspect 聲明該類爲 切面類
@Order(2) 當同一個目標方法有多個切面的時,哪個切面類先執行,取決於在切面類上的註解@order(值小的先執行)
註解的這個方法在AOP裏叫:通知(advice)
@Before 前置通知(在目標方法執行之前執行)
@After 後置通知(在目標方法執行之後執行)無論有沒有異常拋出都會執行
@AfterReturning 返回通知(在目標方法返回結果之後執行)
@AfterThrowing 異常通知(在目標方法跑出異常之後執行)
@Around 環繞通知(圍繞着方法執行)
1. AOP 採用註釋 操作:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 使用註解驅動 自動註冊bean -->
<context:component-scan base-package="cn.jq.springdemo"></context:component-scan>
<!-- 使用註解驅動 , 讓切面類裏的方法上的註釋起作用 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
2. 定義一個操作類:
@Component
public class HelloAop {
public int add(int a, int b) {
return a+b;
}
public int div(int a, int b) {
return a/b;
}
}
3. 自定義兩個橫切關注點:
打印當前時間:
@Component
@Aspect
@Order(1)
public class TimeAscept {
//除法上加入切入點
@Before("execution(public int cn.jq.springdemo.aop.HelloAop.div(int, int))")
public void printTime() {
System.out.println("CurrentTime = " + System.currentTimeMillis());
}
}
日誌記錄:
AspectJ 切入點表達式:
execution(* cn.jq.springdemo.aop.HelloAop.*(..)) : 匹配HelloAop中聲明的所有方法,
第一個 * 代表任意修飾符及任意返回值. 第二個 * 代表任意方法。 .. 代表匹配任意數量的參數。
@Component
@Aspect //切面類
@Order(2) //值越小,優先級越高
public class LoggerAscept {
Logger logger = Logger.getLogger(this.getClass());
//定義切入點表達式
@Pointcut("execution(* cn.jq.springdemo.aop.HelloAop.*(..))")
public void joinPointExpression() {}
@Before("joinPointExpression()")
public void beforeMethod(org.aspectj.lang.JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName(); //獲取方法名
List<Object> args = Arrays.asList(joinPoint.getArgs()); //獲取參數數組
logger.info(methodName+"方法執行之前,參數:" + args);
}
@After("joinPointExpression()")
public void afterMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
logger.info(methodName+"方法執行之後,參數:" + args);
}
@AfterReturning(value="joinPointExpression()", returning="rs")
public void afterReturningMethod(JoinPoint joinPoint, Object rs) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
logger.info(methodName+"方法(無異常)執行之後,參數:" + args + ",結果:"+ rs);
}
@AfterThrowing(value="joinPointExpression()",throwing="ex")
public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
logger.info(methodName+"方法執行時發生異常啦,參數:" + args + ",異常信息:" + ex.getMessage());
}
}
4. 測試類:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
HelloAop helloAop = (HelloAop) context.getBean("helloAop");
helloAop.add(2, 2);
helloAop.div(10, 0);
}
----結果--
2018-08-25 13:35:25,442 INFO [cn.jq.springdemo.aop.LoggerAscept.beforeMethod(LoggerAscept.java:32)] add方法執行之前,參數:[2, 2]
2018-08-25 13:35:25,456 INFO [cn.jq.springdemo.aop.LoggerAscept.afterMethod(LoggerAscept.java:39)] add方法執行之後,參數:[2, 2]
2018-08-25 13:35:25,456 INFO [cn.jq.springdemo.aop.LoggerAscept.afterReturningMethod(LoggerAscept.java:46)] add方法(無異常)執行之後,參數:[2, 2],結果:4
CurrentTime = 1535175325456
2018-08-25 13:35:25,456 INFO [cn.jq.springdemo.aop.LoggerAscept.beforeMethod(LoggerAscept.java:32)] div方法執行之前,參數:[10, 0]
2018-08-25 13:35:25,457 INFO [cn.jq.springdemo.aop.LoggerAscept.afterMethod(LoggerAscept.java:39)] div方法執行之後,參數:[10, 0]
2018-08-25 13:35:25,457 INFO [cn.jq.springdemo.aop.LoggerAscept.afterThrowingMethod(LoggerAscept.java:53)] div方法執行時發生異常啦,參數:[10, 0],異常信息:/ by zero
Exception in thread "main" java.lang.ArithmeticException: / by zero
at cn.jq.springdemo.aop.HelloAop.div(HelloAop.java:12)
at cn.jq.springdemo.aop.HelloAop$$FastClassBySpringCGLIB$$c897cdd6.invoke(<generated>)
此時,AOP 的四種通知 簡單實現。
環繞通知:就是把前面4中通知全給整合在一起。使用它必須要求:
1、必須要帶參數 ProceedingJoinPoint 類型的參數,這個參數可以直接調用原來的目標方法。
2、環繞通知方法必須有返回值,這個反正值就是目標方法的返回值。
@Component
@Aspect //橫切關注點
@Order(2) //值越小,優先級越高
public class LoggerAscept {
Logger logger = Logger.getLogger(this.getClass());
//定義切入點表達式
@Pointcut("execution(* cn.jq.springdemo.aop.HelloAop.*(..))")
public void joinPointExpression() {}
@Around("joinPointExpression()")
public Object aroundMethod(ProceedingJoinPoint pjp) {
Object rs = null;
String methodName = pjp.getSignature().getName();
try {
//前置通知
logger.info(methodName+"方法執行之前");
rs = pjp.proceed(); //在這裏執行目標方法
//返回通知
logger.info(methodName+"方法(無異常)執行之後,結果:"+ rs);
} catch (Throwable e) {
//異常通知
logger.info(methodName+"方法執行時發生異常啦,異常信息:" + e.getMessage());
e.printStackTrace();
}
//後置通知
logger.info(methodName+"方法執行之後, 結果" + rs);
return rs;
}
}
----結果一樣---
基於 xml 配置文件的 AOP 使用
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 使用註解驅動 自動註冊bean -->
<context:component-scan base-package="cn.jq.springdemo"></context:component-scan>
<!-- 使用註解驅動AOP , 讓切面類裏的方法上的註釋起作用 -->
<!-- <aop:aspectj-autoproxy></aop:aspectj-autoproxy> -->
<!-- 使用xml配置AOP -->
<aop:config>
<!-- 配置切面點表達式 -->
<aop:pointcut expression="execution(* cn.jq.springdemo.aop.HelloAop.*(..))" id="pointCutLogger"/>
<aop:pointcut expression="execution(public int cn.jq.springdemo.aop.HelloAop.div(int, int))" id="pointCutTime"/>
<!-- 配置切面和通知 -->
<aop:aspect ref="loggerAscept" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointCutLogger"/>
<aop:after method="afterMethod" pointcut-ref="pointCutLogger"/> <!-- 注意方法名 參數名一致 -->
<aop:after-returning method="afterReturningMethod" pointcut-ref="pointCutLogger" returning="rs"/>
<aop:after-throwing method="afterThrowingMethod" pointcut-ref="pointCutLogger" throwing="ex"/>
</aop:aspect>
<aop:aspect ref="timeAscept" order="1">
<aop:before method="printTime" pointcut-ref="pointCutTime"/>
</aop:aspect>
</aop:config>
</beans>