AOP (面向切面編程) 系列之一
上接 java 注解(annotation) ,注解是 元數(shù)據(jù) ,本身不具備任何業(yè)務(wù)處理能力。AOP 編程就是賦予注解實際處理能力的一種常見方式。
AOP 是什么
AOP 是 Aspect Oriented Programming 的簡稱,一般翻譯為面向切面編程 。
提到 AOP 可能還有一些陌生,先說一個人盡皆知的 OOP (Object-oriented programming,面向?qū)ο蟪绦蛟O(shè)計)。把現(xiàn)實事物抽象為程序?qū)ο蟮木幊趟枷氪蟠筇岣哕浖闹赜眯?、靈活性和擴展性,被廣泛地應(yīng)用于軟件開發(fā)領(lǐng)域。
AOP 是指可以通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)在不修改源代碼的情況下給程序動態(tài)統(tǒng)一添加功能的一種技術(shù),目的是調(diào)用者和被調(diào)用者之間的解耦,提高代碼的靈活性和可擴展性。
因此, AOP 取代 OOP 的說法根本就是無稽之談。二者適用的場景和設(shè)計的目標并不相同。
通俗的說 AOP 是在程序編譯過程或者運行過程(動態(tài)代理)中根據(jù)提前設(shè)置的切面統(tǒng)一添加特定的功能,而對業(yè)務(wù)代碼無侵入或低侵入。
常見的 AOP 使用場景
目前常見的應(yīng)用場景主要是日志記錄,性能統(tǒng)計,安全控制,事務(wù)處理,異常處理等等。尤其在編程框架中最為常見。
spring XML 配置方式 AOP
Talk is cheap, just show you the code
本文使用純 spring XML 配置方式使用 AOP ,不使用 AspectJ 注解(注解方式是挖的另一個坑,下一篇填)。
用作切面的注解
借用 java 注解(annotation) 的自定義注解 AnnotationDemo (僅僅只是偷懶罷了)。
注解在上文中已經(jīng)解釋地很詳細了。
package com.hxx.annotation;
import com.hxx.enums.UserType;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AnnotationDemo {
long time() default 0;
int count() default 0;
String name() default "XiaoMing";
UserType userType() default UserType.SYSTEM_ADMIN;
}
切面方法定義
這里是指切面觸發(fā)的特定邏輯(aspect definition)。
實在是不擅長文字描述,直接看代碼吧。
package com.hxx.aop;
import com.hxx.annotation.AnnotationDemo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
/**
* <ul>
* <li>功能說明:使用 spring 的 XML 方式配置 AOP</li>
* <li>作者:tal on 2018/10/17 0017 17:20 </li>
* <li>郵箱:houxiangxiang@cibfintech.com</li>
* </ul>
*/
public class SpringAspect {
/**
* 切點執(zhí)行前執(zhí)行的方法
*
* @param point
*/
public void doBefore(JoinPoint point) {
System.out.println("---------------> before ");
}
public Object doAround(ProceedingJoinPoint point) throws Throwable {
System.out.println("---------------> around start");
MethodSignature methodSignature = (MethodSignature) point.getSignature();
AnnotationDemo annotation = methodSignature.getMethod().getAnnotation(AnnotationDemo.class);
System.out.println("annotation ---------------> " + annotation);
Object proceed = null;
try {
proceed = point.proceed();
} catch (Throwable throwable) {
throw throwable;
}
System.out.println("---------------> around end");
return proceed;
}
/**
* 切點執(zhí)行后執(zhí)行的方法
*
* @param point
*/
public void doAfter(JoinPoint point) {
System.out.println("---------------> after");
}
/**
* 后置異常通知:在切點拋出異常之后執(zhí)行,可以訪問到異常信息,且可以指定出現(xiàn)特定異常信息時執(zhí)行代碼
*
* @param point
*/
public void afterThrowing(JoinPoint point, Throwable throwable) {
System.out.println("---------------> afterThrowing");
System.out.println(throwable.getMessage());
}
/**
*
* @param point
* @param returnValue
*/
public void afterReturning(JoinPoint point, int returnValue) {
System.out.println("---------------> afterReturning +++ " + returnValue);
}
}
說不用 AspectJ 注解就一個都不用。
雖然沒有用 AspectJ 注解,但也用了 aspectj 的幾個類,所以是要引入 aspectj 依賴的。本文只舉例 maven 的 pom 文件依賴方式,jar 包版本號一般選擇最新版即可。
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
除此之外這就是一個普普通通的類了。類里的方法名和參數(shù)名會在 spring xml 配置里用到,叫什么不要緊,一致即可(不一致編譯就會報錯)。
spring XML 配置
使用 spring xml 配置先引入 spring 依賴,照舊只上 maven 依賴,版本自己選。
<!-- spring context 支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- spring aop 支持-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
spring context 入口文件(applicationContext.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: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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.hxx" />
<!-- 支持注解 -->
<context:annotation-config />
<!-- 支持 aop -->
<aop:aspectj-autoproxy />
<import resource="applicationContext-aop.xml" />
</beans>
重頭戲是 aop 的 xml 配置(applicationContext-aop.xml),其內(nèi)容是這樣的:
<?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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="springAspect" class="com.hxx.aop.SpringAspect" />
<aop:config proxy-target-class="true">
<aop:aspect ref="springAspect">
<aop:pointcut id="springPointcut"
expression="@annotation(com.hxx.annotation.AnnotationDemo)" />
<aop:around pointcut-ref="springPointcut" method="doAround" />
<aop:before pointcut-ref="springPointcut" method="doBefore" />
<aop:after pointcut-ref="springPointcut" method="doAfter" />
<aop:after-throwing pointcut-ref="springPointcut" method="afterThrowing"
throwing="throwable" />
<aop:after-returning pointcut-ref="springPointcut" method="afterReturning"
returning="returnValue" />
</aop:aspect>
</aop:config>
</beans>
先來讀一下這個配置文件
<aop:config></aop:config> 是一條 aop 規(guī)則,當然實際使用中可以根據(jù)需求定義多條規(guī)則。
proxy-target-class 指定是否創(chuàng)建基于類的(CGLIB)代理,默認創(chuàng)建 jdk 代理(Java interface-based)。一般選 true 即可(這二者代理的性能差別也先挖個坑,以后再填)。
aop:aspect 標簽定義切面,ref 指定切面的實例;id 設(shè)置切面的標識; order 可以指定切面的順序,用于同一個切入點(連接點)多個切面時指定執(zhí)行順序。
aop:pointcut 標簽定義一個切入點,即切面執(zhí)行的條件。id 設(shè)置標識,指定切入點對應(yīng)要執(zhí)行的方法時會使用。expression 指切點表達式,符合條件則觸發(fā)切入點。@annotation(com.hxx.annotation.AnnotationDemo)的意思是被 AnnotationDemo 注解標注的地方是切入點(expression 的詳細解釋、種類及復(fù)合用法也先挖個坑)。
aop:around 標簽定義切點圍繞的方法,這里可以根據(jù)條件拒絕執(zhí)行切點方法,也可以處理過程中產(chǎn)生的異常(吞掉或者包裝)。pointcut-ref 指定屬于哪個切入點,method 指定要執(zhí)行切面方法名,本例中是 SpringAspect 的 doAround 方法。此時 doAround 方法只能有一個 JoinPoint 類型的參數(shù),否則運行過程中會因參數(shù)不匹配而報錯。
在 around 指定的方法里,調(diào)用 JoinPoint 的 proceed 方法就會執(zhí)行切入點。around 也因圍繞著這個過程而得名。
aop:before 標簽定義切入點執(zhí)行前要執(zhí)行的方法。method 指定要執(zhí)行切面的方法名,即 JoinPoint 的 proceed 方法執(zhí)行前執(zhí)行的方法(如果同時也有 around 配置的話)。
aop:after 標簽定義切入點執(zhí)行后要執(zhí)行的方法。
aop:after-throwing 標簽定義發(fā)生異常時執(zhí)行的方法,此時可以拿到發(fā)生的異常,但無法阻止異常拋出。throwing 指定 在 method 指定的方法里異常的參數(shù)名。出現(xiàn) throwing 配置也要在方法里有同名的 Throwable 類型參數(shù)接收發(fā)生的異常,否則同樣在運行過程中會因參數(shù)不匹配而報錯。
aop:after-returning 標簽定義返回結(jié)果后執(zhí)行的方法,與 aop:after-throwing 類似,returning 指代切面方法中接收返回值的形參。配置和方法必須同時出現(xiàn)。
看完這些就可以看出 around 最為強大,ProceedingJoinPoint 參數(shù)可以控制切入點的執(zhí)行,因此其使用場景也最多。
從 JoinPoint 中可以拿到切入點的方法以及其注解信息,還原出切點處的注解配置。
AOP 使用
定義并配置好了切面、切入點,來看看怎么用。
首先模擬業(yè)務(wù)場景定義一個接口:
package com.hxx.api;
/**
* <ul>
* <li>功能說明:模擬業(yè)務(wù)接口</li>
* <li>作者:tal on 2018/10/18 0018 14:34 </li>
* <li>郵箱:houxiangxiang@cibfintech.com</li>
* </ul>
*/
public interface ApiDemo {
/**
* 模擬業(yè)務(wù)方法定義
*
* @param input 輸入
* @return 輸出
*/
int work(int input);
}
簡單實現(xiàn)一下這個接口,并使用一下配置好的 AOP :
package com.hxx.api.impl;
import com.hxx.annotation.AnnotationDemo;
import com.hxx.api.ApiDemo;
import com.hxx.enums.UserType;
import org.springframework.stereotype.Service;
@Service("apiDemo")
public class ApiDemoImpl implements ApiDemo {
@AnnotationDemo(time = 1, count = 2, name = "admin", userType = UserType.SYSTEM_ADMIN)
public int work(int input) {
System.out.println("------->> ApiDemo work");
return 3 / input;
}
}
使用非常簡單,在方法上添加注解即可,對業(yè)務(wù)完全無侵入。
結(jié)果測試
借助 junit 測試用例進行測試:
package com.hxx.api;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class) //使用junit4進行測試
@ContextConfiguration(locations = {"classpath*:applicationContext.xml"}) //加載配置文件
public class ApiDemoTest {
@Resource(name = "apiDemo")
private ApiDemo apiDemo;
@Test
public void testWork() throws Exception {
apiDemo.work(1);
}
@Test
public void testWorkException() throws Exception {
apiDemo.work(0);
}
}
運行 testWork() 可以得到結(jié)果:
---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before
------->> ApiDemo work
---------------> around end
---------------> after
---------------> afterReturning +++ 3
執(zhí)行結(jié)果完全符合預(yù)期,但沒測試到出現(xiàn)異常的情況,我們再運行一下 testWorkException() :
---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before
------->> ApiDemo work
---------------> after
---------------> afterThrowing
/ by zero
java.lang.ArithmeticException: / by zero
at com.hxx.api.impl.ApiDemoImpl.work(ApiDemoImpl.java:15)
……
當出現(xiàn)異常且在 around 中沒有吞掉時,afterThrowing 執(zhí)行了,after 也執(zhí)行了,但 afterReturning 無法再執(zhí)行,around 中拋出異常后的語句也無法再執(zhí)行。
如果在 around 中吞掉異常,修改 SpringAspect 的 doAround 方法為(僅僅注釋了 throw 語句):
public Object doAround(ProceedingJoinPoint point) throws Throwable {
System.out.println("---------------> around start");
MethodSignature methodSignature = (MethodSignature) point.getSignature();
AnnotationDemo annotation = methodSignature.getMethod().getAnnotation(AnnotationDemo.class);
System.out.println("annotation ---------------> " + annotation);
Object proceed = 0;
try {
proceed = point.proceed();
} catch (Throwable throwable) {
// throw throwable;
}
System.out.println("---------------> around end");
return proceed;
}
重新運行 testWorkException 后得到結(jié)果:
---------------> around start
annotation ---------------> @com.hxx.annotation.AnnotationDemo(name=admin, count=2, time=1, userType=SYSTEM_ADMIN - 系統(tǒng)管理員)
---------------> before
------->> ApiDemo work
---------------> around end
---------------> after
---------------> afterReturning +++ 0
由此看到可以在 around 中吞掉異常,給回默認返回值。