Spring——面向切面程式設計(AOP)詳解
宣告:本部落格僅僅是一個初學者的學習記錄、心得總結,其中肯定有許多錯誤,不具有參考價值,歡迎大佬指正,謝謝!想和我交流、一起學習、一起進步的朋友可以加我微信Liu__66666666
這是簡單學習一遍之後的記錄,後期還會修改。
一、問題引入
在日常寫專案的時候,肯定少不了要列印日誌。例如,要向資料庫中insert一個使用者,我想在插入前輸出一下相關資訊,怎麼實現呢?最基本的做法是:在insert方法中寫日誌輸出語句。這樣寫完全能實現功能,但是會不會顯得很冗餘?耦合度是不是很高?程式設計的準則是“高內聚,低耦合”,低耦合的意思就是類與類之間的依賴關係儘量少、關聯程度儘量小。
而如果在上述情景中使用面向切面程式設計(AOP),就可以不在insert方法中寫日誌輸出語句卻能實現日誌輸出功能。當然,AOP不止如此。
二、概念引入
1.AOP
在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方 式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個 熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯 的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高 了開發的效率。
2.幾個基本概念
- 切入點:所有要操作的方法定義,要求業務層方法風格統一 - 分離點:將不可分割的元件單獨提取出去定義為單獨的操作功能 - 橫切關注點:將所有與開發無關的程式組成類單獨提取後組織執行 - 織入:將所有切入點、關注點的程式碼組成在一張完整的程式結構中
3.通知(Advice)
AOP是通過通知來實現功能的,有如下五種:
- 前置通知(BeforeAdvice)
後置通知(AfterAdvice)
- 後置返回通知(AfterReturningAdvice)
- 後置異常通知(AfterThrowingAdvice)
環繞通知(AroundAdvice)
三、Pointcut與Execution表示式
pointcut使用execution表示式表示要被切入的方法(即定義切入點)。
execution表示式,功能類似於正則表示式,都是用來匹配篩選,只不過正則表示式用來篩選字串,而execution表示式用來篩選要被切入的方法。
execution表示式的格式為:
execution(<註解>? <修飾符>? <返回值型別> <方法名模式>(<引數模式>) <異常>?)) <and args()>?)
例:execution(@Deprecated public Void aop.MyAspect.hello(int,String) throws Exception))')
package aop;
public class AspectDemo {
@Deprecated
public void hello(int i,String s) throws Exception{
}
}
其實不難發現,這個表示式和我們宣告的方法的各個部分一一對應
註解:(可省略)例如上面程式碼中的
@Deprecated
,就是篩選帶有該註解的方法修飾符(可省略)
- public
- protected
- private
當然一般用萬用字元 *
返回值型別
寫各種返回值,一般用萬用字元 *
方法名模式
包名部分:在上例中,AspectDemo是位於aop包中的,所以可以通過包名.包名.類名的格式來定位到某個類,例如aop.AspectDemo 中aop. 就是包名部分;
當然也可以用萬用字元
- *:匹配任何數量字元,例如service.*.UserService 表示的是service的直接子包
- ..:匹配任何數量字元的重複,如在型別模式中匹配任何數量子包,例如service..代表著匹配service及其包含的所有包;而在方法引數模式中匹配任何數量引數。
- +:匹配指定型別的子型別;僅能作為字尾放在型別模式後邊,例如java.lang.Number+ 表示的是lang包下Numer的子類
類名部分:在上例中aop.AspectDemo中aop.是包名部分,AspectDemo就是類名部分,可以用萬用字元來表示,*用的比較多
引數模式
- 寫法1:直接按照方法的引數列表寫具體型別,上例的方法中引數列表(int i,String s),就可以直接在表示式中寫(int,String)
- 寫法2:使用萬用字元:
- “()”表示方法沒有任何引數;
- “(..)”表示匹配接受任意個引數的方法
- “(..,java.lang.String)”表示匹配接受java.lang.String型別的引數結束,且其前邊可以接受有任意個引數的方法
- “(java.lang.String,..)” 表示匹配接受java.lang.String型別的引數開始,且其後邊可以接受任意個引數的方法
- “(*,java.lang.String)” 表示匹配接受java.lang.String型別的引數結束,且其前邊接受有一個任意型別引數的方法;
異常模式(可省略)
throws Exception1,Exception2.。。。
傳入引數(可省略)
and args(arg-name),一般用於AfterAdvice和Around通知
四、前期準備
- 建立專案,匯入相關jar包,參考Spring——IOC,此外還需匯入aop和aspectj的jar包
- 建立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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
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">
<aop:aspectj-autoproxy/>
</beans>
注意:新增了
xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/beans/spring-aop.xsd
<aop:aspectj-autoproxy/>
不加這個可能會報錯,可坑了
- 建立UserService這個類,內部有insert方法用來註冊使用者
public class UserService {
public void insert(){
System.out.println("UserService正在註冊使用者……");
}
}
- 建立MyAspect類
public class MyAspect {}
五、基於XML配置的AOP
1.BeforeAdvice
(1)在MyAspect類中建立方法beforeAdvice
public void beforeAdvice(){
System.out.println("【AOP】Before Advice正在執行……");
}
(2)在applicationContext.xml中配置
關於pointcut和execution表示式見下文
<!--首先要引入myAspect這個bean,備用-->
<bean id="myAspect" class="aop.MyAspect"/>
<bean id="userService" class="aop.UserService"/>
<aop:config>
<!--配置切面,一個aop:aspect標籤對應一個Aspect類-->
<aop:aspect id="beforeAdvice" ref="myAspect">
<!--配置通知 method對應MyAspect類中定義的方法,pointcut是切入點表示式用於篩選需要被 切入的方法-->
<aop:before method="beforeAdvice" pointcut="execution(* aop..*.*(..)))"/>
</aop:aspect>
</aop:config>
(3)編寫測試類
public class UserServiceTest {
public static void main(String[] args) {
ApplicationContext context=
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.insert();
}
}
(4)輸出結果
【AOP】Before Advice正在執行……
UserService正在註冊使用者……
可以發現,BeforeAdvice就已經實現了
2.AfterAdvice(相當於異常裡面的finally語句)
(1)UserService類同上
(2)在MyAspect中建立方法afterAdvice
public void afterAdvice(){
System.out.println("【AOP】after Advice…… 不管怎樣我都會執行");
}
(3)修改applicationContext.xml
<aop:config>
<aop:aspect id="beforeAdvice" ref="myAspect">
<aop:after method="afterAdvice"
pointcut="execution(* aop.*.insert(..)))" />
</aop:aspect>
</aop:config>
(4)編寫測試類(同上)
(5)輸出結果
UserService正在註冊使用者……
【AOP】after Advice…… 不管怎樣我都會執行
3.AfterReturningAdvice
(1)修改UserService的insert方法,使其有返回值
public class UserService {
public int insert(){
System.out.println("UserService正在註冊使用者……");
return 1;
}
}
(2)在MyAspect中新增afterReturningAdvice方法
public void afterReturningAdvice(int result) {
System.out.println("【AOP】after advice……返回值為"+result);
}
(3)在applicationContext.xml中配置
<aop:config>
<aop:aspect id="beforeAdvice" ref="myAspect">
<aop:after-returning method="afterReturningAdvice"
pointcut="execution(* aop.*.insert(..)))" returning="result"/>
</aop:aspect>
</aop:config>
注意:這裡這個returning="result"與MyAspect類中對應方法的引數名必須保持一致,本例中都為result
(4)編寫測試類(程式碼同1.)
(5)輸出結果
UserService正在註冊使用者……
【AOP】after advice……返回值為1
4.AfterThrowingAdvice
(1)修改UserService使其拋異常
public int insert() throws Exception {
try {
System.out.println("UserService開始註冊使用者……");
int i=1/0;
}catch (Exception e){
throw new Exception("insert方法遇到異常……");
}
return 1;
}
(2)在MyAspect中新增方法 afterThrowingAdvice
//這裡傳入的這個Exception就是捕獲到的異常物件
public void afterThrowingAdvice(Exception e){
System.out.println("【AOP】得到異常資訊:"+e.getMessage());
}
(3)修改applicationContext.xml
<aop:config>
<aop:aspect id="beforeAdvice" ref="myAspect">
<aop:after-throwing method="afterThrowingAdvice"
pointcut="execution(* aop.*.insert(..)))" throwing="e"/>
</aop:aspect>
</aop:config>
注意:這裡的throwing="e”就是跑出的異常物件的名字,要與MyAspect中afterThrowingAdvice方法中傳入的引數Exception e的名字保持一致。
(4)編寫測試類
public static void main(String[] args) throws Exception {
ApplicationContext context=
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.insert();
}
(5)輸出結果
UserService開始註冊使用者……
【AOP】得到異常資訊insert方法遇到異常……
Exception in thread "main" java.lang.Exception: insert方法遇到異常……
at aop.UserService.insert(UserService.java:12)
at aop.UserService$$FastClassBySpringCGLIB$$7e3b8e5e.invoke(<generated>)
...
5.AroundAdvice
(1)修改UserService中的insert方法
public int insert(int arg) throws Exception {
try {
int i = 1 / 0;
} catch (Exception e) {
throw new Exception("insert方法遇到異常……");
}
return 1;
}
(2)在MyAspect中新增方法AroundAdvice
//這裡這個ProceedingJointPoint可以理解為切入點物件,可以通過它獲取切入點(被切入的方法)的引數、返回值、丟擲的異常,並且可以通過pjp.proceed(args);為該切入點設定引數
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println("【AOP】before Advice,獲取到insert方法傳入的引數為:"+args[0]);
Object result;
try {
result=pjp.proceed(args);//這裡是我們手動執行切入點,並傳入引數
System.out.println("【AOP】after Returning Advice,返回值為:"+result);
}catch (Exception e){
//這裡捕獲的就是切入點執行時丟擲的異常
System.out.println("【AOP】after Throwing Advice,錯誤資訊為:"+e.getMessage());
}
System.out.println("【AOP】after advice……不管異常不異常我都執行");
//這個就跟著這樣寫吧。。如果不寫返回值的話會報 null return value does not match...
return 1;
}
(3)修改applicationContext.xml檔案
<aop:config>
<aop:aspect id="beforeAdvice" ref="myAspect">
<aop:around method="aroundAdvice"
pointcut="execution(* aop.*.insert(..)))" />
</aop:aspect>
</aop:config>
(4)編寫測試類
public static void main(String[] args) throws Exception {
ApplicationContext context=
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.insert(2);
}
(5)輸出結果
【AOP】before Advice,獲取到insert方法傳入的引數為:2
【AOP】after Throwing Advice,錯誤資訊為:insert方法遇到異常……
【AOP】after advice……不管異常不異常我都執行
六、基於註解配置的AOP
先把專案狀態恢復到 “四、前期準備”的狀態,然後在applicationContext.xml中新增下面的語句開啟註解和包掃描。
<context:annotation-config/>
<context:component-scan base-package="aop"/>
注意,這個base-package可以配置多個包,以半形(英文)逗號隔開,例如“aop,mvc,dao,service”,當然,為了省事,可以直接配一個頂級包,他會自動遍歷掃描所有的子包及子包的子包等等。
然後為MyAspect類和UserService類加上註解
@Component
@Aspect
public class MyAspect {}
@Service
public class UserService {}
1.BeforeAdvice
(1)在MyAspect類中建立beforeAdvice方法,並寫好註解
@Before(value = "execution(* aop..*.*(..)))")
public void beforeAdvice(){
System.out.println("【AOP】Before Advice正在執行……");
}
不需要配任何bean,是不是很爽
(2)編寫測試類
public static void main(String[] args) {
ApplicationContext context=
new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class);
userService.insert();
}
(3)輸出結果
【AOP】Before Advice正在執行……
UserService正在註冊使用者……
2.AfterService(我就不寫測試了)
(1)在MyAspect類中建立afterAdvice方法,並寫好註解
@After(value = "execution(* aop..*.*(..)))")
public void afterAdvice(){
System.out.println("【AOP】after Advice正在執行……");
}
3.AfterReturningAdvice
(1)修改UserService中的insert方法
public int insert(){
System.out.println("UserService正在註冊使用者……");
return 1;
}
(2)在MyAspect類中建立afterReturningAdvice方法,並寫好註解
@AfterReturning(value = "execution(* aop..*.*(..))&& args(result))")
public void afterReturningAdvice(int result){
System.out.println("【AOP】after Returning Advice正在執行……返回值為:"+result);
}
(3)不寫測試了
4.AfterThrowingAdvice
(1)修改UserService中的insert方法
public int insert() throws Exception {
try {
int i=1/0;
}catch (Exception e){
throw new Exception("【UserService】的insert遇到了錯誤……");
}
return 1;
}
(2)在MyAspect類中建立afterThrowingAdvice方法,並寫好註解
@AfterThrowing(value = "execution(* aop..*.*(..)))",throwing = "e")
public void afterThrowingAdvice(Exception e){
System.out.println("【AOP】after Throwing Advice正在執行……錯誤資訊為:"+e.getMessage());
}
(3)不測試了。。
5.AroundAdvice
(1)把MyAspect中之前寫的方法註釋掉,不然會影響觀察結果
(2)修改insert方法
public int insert(int arg) throws Exception {
try {
int i=1/0;
}catch (Exception e){
throw new Exception("【UserService】的insert遇到了錯誤……");
}
return 1;
}
(3)在MyAspect類中建立aroundAdvice方法,並寫好註解
@Around(value = "execution(* aop..*.*(..)))")
public int aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
System.out.println("【AOP】before Advice,獲取到insert方法傳入的引數為:"+args[0]);
Object result;
try {
result=pjp.proceed(args);
System.out.println("【AOP】after Returning Advice,返回值為:"+result);
}catch (Exception e){
System.out.println("【AOP】after Throwing Advice,錯誤資訊為:"+e.getMessage());
}
System.out.println("【AOP】after advice……不管異常不異常我都執行");
return 1;
}