SpringAOP實現
說完了代理模式,就可以研究一下 Spring AOP 了。AOP 不是新的技術,而是對現有技術的更好的使用的方式,其實就是代理模式的典型應用。這一節新建 Spring-09-AOP 項目學習 Spring AOP。
1. AOP簡介
1.1 什么是AOP
AOP 即 Aspect Oriented Programming,意為面向切面編程,通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP 是 OOP 的延續,是軟件開發中的一個熱點,也是 Spring 框架中的一個重要內容,是函數式編程的一種衍生范型。利用 AOP 可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
舉個例子,如下圖,加減乘除四個業務,雖然它們的業務邏輯不同,但他們都有一樣的驗證參數和日志功能,就可以橫向地把這些功能提取出來(抽取橫切關注點),以組合的方式添加到不同的業務邏輯上(代理模式!增強方法!),這樣避免了修改原有的業務邏輯,使類的功能更明確,程序的粒度更細!

1.2 AOP術語
AOP 的特性術語,不同的翻譯還會不一樣,得在過程中理解
- 橫切關注點:跨越程序多個模塊的方法或功能。即與業務邏輯無關,但我們也要關注的部分,就是橫切關注點。如日志、安全、緩存等。
- 切面( Aspect ):橫切關注點被模塊化的特殊對象;即切面應是一個類。
- 通知( Advice):切面要完成的增強處理,通知描述了切面何時執行以及如何執行增強處理;即通知應是切面中的方法。
- 目標( Target ):被通知對象。
- 代理( Proxy ):向目標對象應用通知之后創建的對象。
- 切入點( PointCut ):切面通知執行的地點,即可以插入增強處理的連接點。
- 連接點( JoinPoint ):應用執行過程中能夠插入切面的一個點,這個點可以是方法的調用、異常的拋出。
- 織入( Weaving ):將增強處理添加到目標對象中,並創建一個被增強的對象,這個過程就是織入。
舉個例子,現在要在程序中多處添加日志,則日志就是橫切關注點,把日志模塊化為 Log 類,就是切面,日志要輸出的內容就是通知,目標就是要添加日志的接口或方法,代理就是動態生成的日志代理,切入點即執行輸出日志的地點,連接點就是動態代理中的 invoke 方法前后,可以插入切面( Before/After )。
1.3 Spring中的通知
Spring中有五種通知 Advice
- @Before 前置通知:方法執行之前
- @After 后置通知:方法執行之后
- @Around 環繞通知:方法執行前后都有通知
- @AfterReturnning 返回通知:方法成功執行之后通知
- @AfterThrowing 異常通知:拋出異常之后通知
就像代理模式中 Before 和 After 時的增強,不過功能更多了。
2. 使用Spring AOP
在 Spring 中使用 AOP 要先導入 AspectJ 的包
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
<!--駭人鯨!-->
<scope>runtime</scope>
</dependency>
創建 UserService 接口和 UserServiceImpl 實現類,還是增刪改查四個功能
public interface UserService {
void add();
void delete();
void update();
void select();
}
public class UserServiceImpl implements UserService{
public void add() {
System.out.println("增加用戶!");
}
public void delete() {
System.out.println("刪除用戶!");
}
public void update() {
System.out.println("修改用戶!");
}
public void select() {
System.out.println("查詢用戶!");
}
}
2.1 使用Spring的接口
現在要給業務添加日志,抽取為切面,就要創建 Log 類了,不過這里根據前置日志和后置日志,又分為 BeforeLog 類和 AfterLog 類
BeforeLog 類,實現了 MethodBeforeAdvice 接口,即前置通知
public class BeforeLog implements MethodBeforeAdvice {
// method: 要執行的目標對象的方法 | the method being invoked
// args: 方法的參數 | the arguments to the method
// target: 目標對象 | the target of the method invocation
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName()+" 的 "+method.getName()+" 方法被執行了!");
}
}
AfterLog 類,實現了 AfterReturningAdvice 接口,即返回通知
public class AfterLog implements AfterReturningAdvice {
// 多了一個返回值的參數
// returnValue: 方法調用的返回值 | the value returned by the method, if any
// 其他參數都是一樣的!
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("執行了 "+method.getName()+" 方法,返回值為 "+returnValue);
}
}
然后就可以用 Spring 把切面切入進去了!
創建 applicationContext.xml 配置文件,引入 AOP 的約束
<?xml version="1.0" encoding="UTF-8"?>
<beans ...
// AOP 約束
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="...
// AOP 約束
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
然后注冊一下 bean
<bean id="userService" class="com.qiyuan.service.UserServiceImpl"/>
<bean id="beforeLog" class="com.qiyuan.log.BeforeLog"/>
<bean id="afterLog" class="com.qiyuan.log.AfterLog"/>
開啟 Spring AOP 配置,設置切入點和通知,這里用到了 execution 表達式表明切入點的位置,而通知執行的位置是由實現的接口決定的
<!--開啟 AOP 配置-->
<aop:config>
<!--聲明一個切入點,expression 是一個 execution 表達式,表明切入點在哪-->
<!-- execution 表達式是什么幾把玩意后面再說!先用着!這里表明切入點是 UserServiceImpl 下所有方法-->
<aop:pointcut id="logpointcut" expression="execution(* com.qiyuan.service.UserServiceImpl.*(..))"/>
<!--執行環繞增加,增強方法-->
<!-- Spring根據實現的接口 決定通知的連接點-->
<!-- beforelog 實現的是 前置通知 接口,所以連接點在切入點方法執行前-->
<aop:advisor advice-ref="beforeLog" pointcut-ref="logpointcut"/>
<!-- afterlog 實現的是 返回通知 接口,所以連接點在切入點方法執行成功后-->
<aop:advisor advice-ref="afterLog" pointcut-ref="logpointcut"/>
</aop:config>
設置完后,IDEA 也會顯示通知會在哪個方法執行

現在就可以看一下效果了,用測試方法測試一下
public class MyTest {
@Test
public void Test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 因為動態代理代理的是接口!所以獲取對象要用接口接收
UserService userService = context.getBean("userService", UserService.class);
userService.add();
userService.delete();
}
}
// 執行結果
/*
com.qiyuan.service.UserServiceImpl 的 add 方法被執行了!
增加用戶!
執行了 add 方法,返回值為 null
com.qiyuan.service.UserServiceImpl 的 delete 方法被執行了!
刪除用戶!
執行了 delete 方法,返回值為 null
*/
現在回頭看看,UserService 接口及其實現類只用在乎它的本職工作,Log 類也只用專注於輸出日志,他們看似毫無關聯,但我們通過切面編程的方式,把 Log 切入到了 UserService 中去!這,就是 Spring AOP。
注意:在獲取對象時,實現了接口的對象要用接口去獲取 bean,如
UserService userService = context.getBean("userService", UserService.class);
這里如果改成
UserService userService = context.getBean("userService", UserServiceImpl.class);
會出現錯誤為
org.springframework.beans.factory.BeanNotOfRequiredTypeException:
Bean named 'userService' is expected to be of type 'com.qiyuan.service.UserServiceImpl' but was actually of type 'com.sun.proxy.$Proxy6'
這是因為動態代理中,代理的是一個接口( UserService ),這里返回的是由 Spring 創建的一個代理類,它也實現了代理的接口( UserService ),所以可以用接口接收;但一個接口的實現類不能接受其另一個實現類(如 UserServiceImpl1 不能接受 UserServiceImpl2 ),就是這么簡單的道理。
2.2 使用自定義類(切面)
除了在 XML 中配置實現了 Spring 提供的接口的類的方法,使用自定義的類也能使用 Spring AOP。
首先創建一個 MyAOP 類(就是切面),類中有需要切入的方法(通知)
// 這就是一個切面!
public class MyAOP {
public void Before(){
System.out.println("=====方法執行前=====");
}
public void After(){
System.out.println("=====方法執行后=====");
}
}
在 applicationContext.xml 中進行 AOP 配置
<!--自定義的切面-->
<bean id="myaop" class="com.qiyuan.aop.MyAOP"/>
<aop:config>
<!--引用自定義切面-->
<aop:aspect id="myaop" ref="myaop">
<!--設置切入點,和之前相同!-->
<aop:pointcut id="pointcut" expression="execution(* com.qiyuan.service.UserServiceImpl.*(..))"/>
<!--在切入點的方法執行前,執行切面中的 Before 方法-->
<aop:before method="Before" pointcut-ref="pointcut"/>
<!--在切入點的方法執行后,執行切面中的 After 方法-->
<aop:after method="After" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
這種方式比第一種更簡潔明了,創建了一個切面的 bean,和之前一樣設置切入點,然后配置在什么時候( aop:before、aop:after、aop:around... )執行切面( pointcut-ref )中的什么方法( method= )。不過功能也沒有第一種強大,獲取不到類的名字,方法的名字等。
運行測試方法看看效果
// 和之前一樣的測試方法
// 執行結果
/*
=====方法執行前=====
增加用戶!
=====方法執行后=====
=====方法執行前=====
刪除用戶!
=====方法執行后=====
*/
2.3 小結
兩種方法的共同點
- 都要聲明切入點 pointcut,表明在什么地方進行切入
兩種方法的不同點
- 使用 Spring 的接口,切面實現了什么接口就在什么地方進行通知(執行方法),如實現了 MethodBeforeAdvice 接口,Spring 就會把通知切入到切入點的方法執行前。
- 使用自定義類(切面),要手動設置將切面中的方法(通知)設置到什么時候執行,如用 aop:before 在切入點方法執行前執行 method="Before" 方法。
2.4 使用注解實現
使用注解和使用自定義類差不多,就是用注解換掉了 XML 中的配置。
創建 AnnotationAspect 類,使用 @Aspect 聲明這個類是一個切面
// 聲明這個類是一個切面!
@Aspect
public class AnnotationAspect {
}
將這個類聲明為切面,相當於 XML 中的
<aop:aspect id="myaop" ref="myaop">
注意:這里給 Maven 害慘了,之前導入的 Maven 依賴中設置了作用域
<!--駭人鯨!-->
<scope>runtime</scope>
這個配置導致 @Aspect 注解怎么都找不到!把它去掉就好了!修改完的依賴為
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
給這個類添加方法,即切面對應的通知,和之前一樣
// 聲明這個類是一個切面!
@Aspect
public class AnnotationAspect {
public void Before(){
System.out.println("=====方法執行前=====");
}
public void After(){
System.out.println("=====方法執行后=====");
}
}
這時候還沒完哦,使用 @Before 和 @After 給這兩個方法(通知)設置切入點
@Aspect
public class AnnotationAspect {
// 和 XML 中一樣,通過 execution 表達式設置切入點
@Before("execution(* com.qiyuan.service.UserServiceImpl.*(..))")
public void Before(){
System.out.println("=====方法執行前=====");
}
@After("execution(* com.qiyuan.service.UserServiceImpl.*(..))")
public void After(){
System.out.println("=====方法執行后=====");
}
}
這里就相當於 XML 中的
<!--設置切入點,和之前相同!-->
<aop:pointcut id="pointcut" expression="execution(* com.qiyuan.service.UserServiceImpl.*(..))"/>
<!--在切入點的方法執行前,執行切面中的 Before 方法-->
<aop:before method="Before" pointcut-ref="pointcut"/>
<!--在切入點的方法執行后,執行切面中的 After 方法-->
<aop:after method="After" pointcut-ref="pointcut"/>
這時候還沒完!還要再 XML 中把這個切面注冊一下,並且開啟注解支持
<!--使用注解-->
<bean id="annoAspect" class="com.qiyuan.aop.AnnotationAspect"/>
<!--開啟注解支持-->
<aop:aspectj-autoproxy/>
這樣才是配置好了,運行測試方法,結果和第二種方式一樣,就不貼出來了。
這里還有個環繞方法 @Around 沒試,就先這樣吧。
3. 總結
理解了動態代理,AOP 學起來就比較輕松了。
AOP 的作用在於:不影響現有業務的情況下,添加一些程序中普遍會用到的功能,如日志。
使用 AOP 的方式有三種,Spring 接口、自定義切面和注解。注解還用的不是很明白,不過先這樣吧~🥴。