介紹 概念 面向切麵編程AOP與面向對象編程OOP有所不同,AOP不是對OOP的替換,而是對OOP的一種補充,AOP增強了OOP。 假設我們有幾個業務代碼,都調用了某個方法,按照OOP的思想,我們就會將此方法封裝在一個類中,之後通過 調用 我們可以看作我們的業務代碼被其他代碼入侵或者是業務代碼被其他 ...
介紹
概念
面向切麵編程AOP與面向對象編程OOP有所不同,AOP不是對OOP的替換,而是對OOP的一種補充,AOP增強了OOP。
假設我們有幾個業務代碼,都調用了某個方法,按照OOP的思想,我們就會將此方法封裝在一個類中,之後通過對象.方法名
調用
我們可以看作我們的業務代碼被其他代碼入侵或者是業務代碼被其他與業務不相關的代碼入侵了
這個時候,如果我們使用AOP進行編寫代碼,我們的業務代碼就可以不需要寫其他與業務相關的代碼,這樣就可以保證業務代碼的純潔性
AOP運行流程
通過配置文件,給各個業務方法標識切入點(PointCut),即切入點方法。
之後當程式運行到切入點方法的時候,就會發出一個通知(Advice),去通知執行某個切麵方法(Aspect)
專業術語
項 | 描述 |
---|---|
Aspect | 一個模塊具有一組提供橫切需求的 APIs。例如,一個日誌模塊為了記錄日誌將被 AOP 方面調用。應用程式可以擁有任意數量的方面,這取決於需求。 |
Join point | 在你的應用程式中它代表一個點,你可以在插件 AOP 方面。你也能說,它是在實際的應用程式中,其中一個操作將使用 Spring AOP 框架。 |
Advice | 這是實際行動之前或之後執行的方法。這是在程式執行期間通過 Spring AOP 框架實際被調用的代碼。 |
Pointcut | 這是一組一個或多個連接點,通知應該被執行。你可以使用表達式或模式指定切入點正如我們將在 AOP 的例子中看到的。 |
Introduction | 引用允許你添加新方法或屬性到現有的類中。 |
Target object | 被一個或者多個方面所通知的對象,這個對象永遠是一個被代理對象。也稱為被通知對象。 |
Weaving | Weaving 把方面連接到其它的應用程式類型或者對象上,並創建一個被通知的對象。這些可以在編譯時,類載入時和運行時完成。 |
Advice通知
通知 | 類型 |
---|---|
前置通知(Before Advice) | 在切入點方法執行之前,執行通知 |
環繞通知(Around Advice) | 在切入點方法執行的整個過程都可以執行通知 |
後置通知(After Returning Advice) | 在切入點方法執行之後,只有在方法成功執行時,才能執行通知。 |
最終通知(After Finally Advices) | 在一個方法執行之後,不管是方法是否成功執行 ,執行通知 |
異常通知(After Throwing Advice) | 在一個方法執行之後,只有在方法退出拋出異常時,才能執行通知。 |
PS:其實,這些通知就是相當於你可以在業務方法的執行前(前置通知)、執行中(環繞通知)、執行成功之後(後置通知)、發生異常(異常通知)、不管方法是發生異常還是執行成功(最終通知),執行某些與業務功能無關的功能代碼。
這樣就可以降低業務功能代碼的入侵和污染
下麵使用兩種不同的方式來實現一個方法日誌列印的簡單例子
後置通知例子
下麵的通知是基於xml配置的
1.添加依賴
除了之前的spring的jar包,還需要兩個jar包,aopalliance.jar
和aspectjweaver.jar
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>RELEASE</version>
</dependency>
<!-- aop需要的jar -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>RELEASE</version>
</dependency>
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>RELEASE</version>
</dependency>
2.業務代碼
我編寫了一個TeacherDao類,裡面只有add和delete方法
package com.wan;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:34
* @description
*/
public class TeacherDao {
public void add(Teacher teacher) {
System.out.println("往資料庫中插入一條數據");
}
public void delete(Teacher teacher) {
System.out.println("從資料庫中刪除一條數據");
}
}
3.編寫MyLogging.java
前面說過了通知具有五種類型,我們根據需要,選擇合適的通知類型,讓某個類實現通知對應的介面,這裡其實就是相當於編寫切麵方法
通知類型 | 介面 | 介面方法 | 介面方法參數說明 |
---|---|---|---|
前置通知 | org.springframework.aop.MethodBeforeAdvice | before(Method method, Object[] args, Object target) | method是方法,args是方法的參數,target是目標對象 |
環繞通知 | org.aopalliance.intercept.MethodInterceptor | invoke(MethodInvocation invocation) | invocation對象中包含有method,方法參數和目標對象 |
後置通知 | org.springframework.aop.AfterReturningAdvice | afterReturning(Object returnValue, Method method, Object[] args, Object target) | returnValue是方法的返回值,其他的參數和前置通知一樣 |
最終通知 | org.springframework.aop.AfterAdvice | 無 | 無 |
異常通知 | org.springframework.aop.ThrowsAdvice | 無 | 無 |
我們日誌輸出,選擇後置通知,也就是方法執行完成之後調用
MyLogging.java
package com.wan;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
public class MyLogging implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
String methodName = method.getName();//方法名
int size = args.length;//參數個數
System.out.println("調用了"+target+"的"+methodName+"方法,該方法的參數個數有"+size+"個");
}
}
4.xml配置
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="mylog" class="com.wan.MyLogging"/>
<bean id="teacherdao" class="com.wan.TeacherDao"/>
<aop:config>
<aop:pointcut id="mypointcut" expression="execution(public void add(com.wan.Teacher))"/>
<aop:advisor advice-ref="mylog" pointcut-ref="mypointcut"/>
</aop:config>
</beans>
這裡和之前一樣,也需要引用aop
命名空間,IDEA可以智能幫我們導入,輸入<aop:
,之後就會彈出提示
各元素和屬性說明:
子元素/屬性 | 含義 |
---|---|
aop:pointcut | 切入點,當執行當切入點方法的時候,就會根據通知(Advice)的類型,從而執行非業務功能的代碼 |
id | 切入點的唯一表示,下麵pointcut-ref屬性需要引用此id |
expression | 表達式,只要是符合此表達式的方法,都會被當作切入點 |
aop:advisor | 通知 |
pointcut-ref | 引用切入點的id |
advice-ref | 引用切入點介面類的bean的id |
補充,關於expression的例子:
例子 | 說明 |
---|---|
public boolean addTeacher(com.wan.Teacher) | 所有返回類型為boolean,參數類型為com.wan.Teacher,方法名為addTeacher的方法 |
public void com.wan.TeacherDao.add(com.wan.Teacher) | 方法存在TeacherDao類中,返回類型為空,參數類型為Teacher,方法名為add的方法 |
public * addTeacher(com.wan.Teacher) | 所有返回類型為任意類型,參數類型為com.wan.Teacher,方法名為addTeacher的方法 |
public boolean *(com.wan.Teacher) | 所有返回類型為任意類型,參數類型為Teacher,方法名任意的方法 |
public boolean addTeacher(..) | 所有返回類型為任意類型,參數類型和個數不限,方法名為addTeacher的方法 |
* com.wan.*.*(..) | 在com.wan包下麵的所有方法(不包括子包) |
* com.wan..*.*(..) | 在com.wan包下麵的所有方法(包括子包) |
表達式要寫在execution()的括弧裡面,多個條件可以使用or連接
5.測試
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
TeacherDao teacherdao = (TeacherDao) context.getBean("teacherdao");
teacherdao.add(new Teacher());
其他類型通知Advice使用
前置通知
前置通知和後置通知一樣,也是實現對應的介面,然後重寫before方法,這裡就不過多說明瞭
異常通知
異常通知有點特殊,因為此介面是不需要重寫方法的,但是,我們想要實現異常通知,得按照它定義的規則來
afterThrowing([Method method,Object[] args,Object target],Throwable ex)
- 方法名必須是afterThrowing
- 參數列表中的最後一個參數必須存在,可以是Throwable或者Throwable的子類
- 方法列表的前三個參數要麼都存在,要麼一個都不存在
環繞通知
此通知是spring的最強擴展,因為環繞通知可以攔截方法,對方法的傳入參數的數值、返回值進行更改,或者是決定方法是否執行,也可以對目標進行異常處理。
如果對破解有所瞭解的話,環繞通知還可以被稱為hook,像Android的Xposed框架就是通過hook原理,來達到自由更改系統目的。
實現MethodInterceptor介面,重寫其的invoke方法
invoke方法可以獲得像之前的前置通知的三個參數,method
,target
,args
,也可以獲得返回值returnValue
package com.wan;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
public class MyLogging implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invoke.getThis();
Method method = invoke.getMethod();
Object[] args = invoke.getArguments();
//執行方法,獲得返回值
Object returnValue = invoke.proceed();
}
}
基於註解配置使用
上面說的幾個例子都是基於xml配置文件,我們可以使用註解,從而達到簡化的目的
註解 | 說明 |
---|---|
@Aspect | 標註切入點 |
@Before | 標註前置通知 |
@Around | 標註環繞通知 |
@AfterReturning | 標註後置通知 |
@After | 標註最終通知 |
@AfterThrowing | 標註異常通知 |
步驟
1. 導入相關jar(之前導入的那兩個jar包)
2. 使用註解,標註類和方法
3. xml中開啟配置
<?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: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/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="teacherdao" class="com.wan.TeacherDao"/>
<aop:aspectj-autoproxy/>
<bean class="com.wan.MyLogging"/>
</beans>
之後的測試代碼和之前的一樣
前置通知
package com.wan;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
@Aspect
public class MyLogging {
@Before("execution(public void add(com.wan.Teacher))")
public void sayHello() {
System.out.println("這是前置通知");
}
}
註解使用挺簡單的,大概看一下示例代碼就能知道怎麼使用了
獲得三個參數target、args、method
AOP中有個JoinPoint的介面,此介面可以獲得target
、args
、method
這三個參數
方法名 | 說明 |
---|---|
getTarget() | 獲得目標對象 |
getSignature() | 獲得目標方法的Signature對象,由此對象的getName可以獲得方法名 |
getArgs() | 獲得參數列表 |
package com.wan;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
@Aspect
public class MyLogging {
@AfterReturning(pointcut="execution(public boolean add(com.wan.Teacher))",returning="returnValue")
public void test(JoinPoint jp,Object returnValue) {
//上面的註解的returning屬性把方法的返回值賦值給了參數returnValue
}
}
環繞通知
環繞通知有個特殊的介面ProceedingJoinPoint
,此介面是JoinPoint
的子介面,比JoinPoint介面多了一個proceed方法,用於執行目的對象的方法獲得返回值
package com.wan;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
@Aspect
public class MyLogging {
@Around("execution(public boolean add(com.wan.Teacher))")
public void test(ProceedingJoinPoint jp) {
Object returnValue = jp.proceed();
}
}
異常通知
package com.wan;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
/**
* @author StarsOne
* @date Create in 2019/9/25 0025 16:53
* @description
*/
@Aspect
public class MyLogging {
@AfterThorwing(pointcut="execution(public boolean add(com.wan.Teacher))",throwing="e")
public void test(JoinPoint jp,NullPointException e) {
//上面的註解的throwing屬性把異常賦值給了參數e
//參數中指定了異常為空指針異常,所有,發生異常為空指針異常時候,異常通知才會調用此方法
}
}
PS:除以上兩種方式可以實現AOP,還有一種使用Schema進行配置,我看了一下步驟,覺得比上面兩種還要繁瑣,在這裡就補充了