目錄
3.1 Spring AOP簡介
- Spring的AOP模塊是Spring框架體系中很重要的模塊,提供了面向切面編程的實現。
3.1.1 AOP(面向切面編程)
它也稱面向方面編程,是面向對象編程(OOP)的一種補充,已成爲一種比較成熟的編程方式。
- 在傳統的業務處理代碼中,通常都會進行事務處理、日誌記錄等操作。雖然使用OOP可以通過組合或者繼承的方式來達到代碼的重用,但如果要實現某個功能(如日誌記錄),相同的代碼仍然會分散到各個方法中。這樣,如果想要關閉某個功能,或者對其進行修改,就必須修改所有相關方法。這不但增加了開發人員的工作量,而且提高了代碼的出錯率。
- 爲了解決這一問題,AOP思想隨之產生。AOP採取橫向抽取機制,將分散在各個方法中的重複代碼提取出來,然後在程序編譯或運行時再將這些提取出來的代碼應用到需要執行的地方。這種採用橫向抽取機制的方式,採用傳統的OOP思想顯然是無法辦到的,因爲OOP只能實現父子關係的縱向重用。雖然AOP是一種新的編程思想,但卻不是OOP的替代品,它只是OOP的延伸和補充。
- 在AOP思想中,通過Aspect(切面)可以分別在不同類的方法中加入事務、日誌、權限和異常等功能。
目前流行的AOP框架有兩個
- Spring AOP,Spring AOP使用純Java實現,不需要專門的編譯過程和類加載器,在運行期間通過代理方式向目標類植入增強的代碼。
- AspectJ,AspectJ是一個基於Java語言的AOP框架,從Spring 2.0開始,Spring AOP引入了對AspectJ的支持,AspectJ擴展了Java語言,提供了一個專門的編譯器,在編譯時提供橫向代碼的植入。
3.1.2 AOP術語
在學習使用AOP之前,首先要了解一下AOP的專業術語。這些術語包括Aspect、Joinpoint、Pointcut、Advice、Target Object、Proxy和Weaving,對於這些專業術語的解釋,具體如下:
- Aspect(切面):在實際應用中,切面通常是指封裝的用於橫向插入系統功能(如事務、日誌等)的類,該類要被Spring容器識別爲切面,需要在配置文件中通過< bean>元素指定。,也就是說功能類
- Joinpoint(連接點):在程序執行過程中的某個階段點,它實際上是對象的一個操作,例如方法的調用或異常的拋出。在Spring AOP中,連接點就是指方法的調用。
- Pointcut(切入點):是指切面與程序流程的交叉點,即那些需要處理的連接點。通常在程序中,切入點指的是類或者方法名,如某個通知要應用到所有以add開頭的方法中,那麼所有滿足這一規則的方法都是切入點。,切入點就是需要應用到切面的方法或者類。
- Advice(通知增強處理):AOP框架在特定的切入點執行增強處理,即在定義好的切入點處所要執行的程序代碼。可以將其理解爲切面類中的方法,它是切面的具體實現。
- Target Object(目標對象):是指所有被通知的對象,也稱爲被增強對象。如果AOP框架採用的是動態的AOP實現,那麼該對象就是一個被代理對象。
- Proxy(代理):將通知應用到目標對象之後,被動態創建的對象。
- Weaving(織入):將切面代碼插入目標對象上,從而生成代理對象的過程。
應該可以理解爲將切面代碼插入目標對象,然後生成一個代理對象執行切面程序。
3.2 AspectJ開發
它是一個基於java語言的AOP框架,從Spring 2.0開始,Spring AOP引入了對AspectJ的支持,AspectJ擴展了Java語言,提供了一個專門的編譯器,在編譯時提供橫向代碼的植入,允許直接使用AspectJ進行編程,而Spring自身的AOP API也儘量與AspectJ保持一致
使用AspectJ實現AOP有兩種方式
- 基於XML的聲明式AspectJ
- 基於註解的聲明式AspectJ
3.2.1 基於XML的聲明式AspectJ
基於XML的聲明式AspectJ是指通過XML文件來定義切面,切入點和通知,所有的切面,切入點和通知都必須定義在< aop:config>元素中,Spring配置文件中的< beans>元素可以包含多個< aop: config>元素,一個< aop: config>元素中又可以包含屬性和子元素,其子元素包括< aop: pointcut>、< aop: advisor>和< aop: aspect>。在配置時,這3個子元素必須按照此順序來定義。
在< aop:aspect>元素下,同樣包含屬性和多個子元素,通過使用< aop:aspect>元素及其子元素就可以在XML文件中配置切面,切入點和通知。配置代碼如下
1. 配置切面
配置切面使用的是< aop:aspect>元素,該元素會將一個定義好的Spring Bean轉換爲切面Bean,所以前提是在applicationContext.xml配置文件中先定義一個普通的Spring Bean,定義完成後,通過元素的ref屬性值就可以引用該Bean。
2. 配置切入點
在Spring的配置文件中,切入點是通過< aop:pointcut>元素來定義的。當< aop:pointcut>元素作爲< aop:config>元素的子元素定義時,表示該切入點是全局切入點,可以被多個切面所共享;當< aop:pointcut>元素作爲< aop:aspect>元素的子元素時,表示該切入點只對當前切面有效。在定義< aop:pointcut>元素時,通常會指定id和expression兩個屬性,如圖
< aop:pointcut>元素的屬性及其描述
在上述配置代碼片段中,execution(* com.ssm.jdk.* .* (…))就是定義的切入點表達式,該切入點表達式的意思是匹配com.ssm.jdk包中任意類的任意方法的執行。其中execution是表達式的主體,第1個 * 表示的是返回類型,使用 * 代表所有類型;com.ssm.jdk表示的是需要攔截的包名,後面第2個 * 表示的是類名,使用 * 代表所有的類;第3個 * 表示的是方法名,使用 * 表示所有方法;後面的()表示方法的參數,其中的“…”表示任意參數。需要注意的是,第1個 * 與包名之間有一個空格。
上面示例中定義的切入點表達式只是開發中常用的配置方式。
而Spring AOP中切入點表達式的基本格式如下:
execution(modifiers-pattern?
ret-type-pattern declaring-type-pattern?
name-pattern(param-pattern) throws-pattern?
在上述格式中,各部分說明如下:
- modifiers-pattern:表示定義的目標方法的訪問修飾符,如public、private等。
- ret-type-pattern:表示定義的目標方法的返回值類型,如void、String等。
- declaring-type-pattern:表示定義的目標方法的類路徑,如com.ssm.jdk.UserDaoImpl。
- name-pattern:表示具體需要被代理的目標方法,如add()方法。
- param-pattern:表示需要被代理的目標方法包含的參數,本章示例中目標方法參數都爲空。
- throws- pattern:表示需要被代理的目標方法拋出的異常類型。
帶有問號(?)的部分(如modifiers-pattern、declaring-type-pattern和throws-pattern)表示可選配置項,其他部分屬於必須配置項。
3. 配置通知
在配置代碼中,分別使用< aop:aspect>的子元素配置了5種常用通知,這些子元素不支持再使用子元素,但在使用時可以指定一些屬性,如圖
實例1
- 創建項目,導入所需要的jar包,發佈到類路徑下
- 創建接口USerDao,添加方法
package com.ssm.aspectj;
public interface UserDao {
public void addUser();
public void deleteUser();
}
- 創建UserDao類的實現類,UserDaoImpl
package com.ssm.aspectj;
import org.springframework.stereotype.Repository;
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
// TODO Auto-generated method stub
System.out.println("增加用戶");
}
@Override
public void deleteUser() {
// TODO Auto-generated method stub
System.out.println("刪除用戶");
}
}
我們這裏把這個類作爲目標類,對其中的方法進行增強處理‘
- 創建一個切面編程包,在其中創建切面類MyAspect,並定義不同的通知
package com.ssm.aspectj.xml;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* 切面類,在此類中編寫通知
*/
public class MyAspect {
//前置通知
public void myBefore(JoinPoint joinPoint){
System.out.print("前置通知:模擬執行權限檢查..,");
System.out.print("目標類是:"+joinPoint.getTarget());
System.out.println(",被植入增強處理的目標方法爲:"+joinPoint.getSignature().getName());
}
//後置通知
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("後置通知:模擬記錄日誌..,");
System.out.println("被植入增強處理的目標方法爲:" + joinPoint.getSignature().getName());
}
/**
* 環繞通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可執行目標方法
* 1.必須是Object類型的返回值
* 2.必須接收一個參數,類型爲ProceedingJoinPoint
* 3.必須throws Throwable
*/
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//開始
System.out.println("環繞開始:執行目標方法之前,模擬開啓事務..,");
//執行當前目標方法
Object obj=proceedingJoinPoint.proceed();
//結束
System.out.println("環繞結束:執行目標方法之後,模擬關閉事務..,");
return obj;
}
//異常通知
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("異常通知:出錯了"+e.getMessage());
}
//最終通知
public void myAfter(){
System.out.println("最終通知:模擬方法結束後釋放資源..");
}
}
分別定義了5種不同類型的通知,在通知中使用了JoinPoint接口及其子接口ProceedingJoinPoint作爲參數來獲得目標對象的類名、目標方法名和目標方法參數等。
注意1
環繞通知必須接收一個類型爲ProceedingJoinPoint的參數,返回值也必須是Object類型,且必須拋出異常。異常通知中可以傳入Throwable類型的參數來輸出異常信息。
- 創建項目配置文件
<?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-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 1 目標類 -->
<bean id="userDao" class="com.ssm.aspectj.UserDaoImpl" />
<!-- 2 切面 -->
<bean id="myAspect" class="com.ssm.aspectj.xml.MyAspect" />
<!-- 3 aop編程 -->
<aop:config>
<!-- 1.配置切面 -->
<aop:aspect id="aspect" ref="myAspect">
<!-- 2.配置切入點 -->
<aop:pointcut expression="execution(* com.ssm.aspectj.*.*(..))" id="myPointCut" />
<!-- 3.配置通知 -->
<!-- 前置通知 -->
<aop:before method="myBefore" pointcut-ref="myPointCut" />
<!--後置通知-->
<aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
<!--環繞通知 -->
<aop:around method="myAround" pointcut-ref="myPointCut" />
<!--異常通知 -->
<aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e" />
<!--最終通知 -->
<aop:after method="myAfter" pointcut-ref="myPointCut" />
</aop:aspect>
</aop:config>
</beans>
returning=“returnVal”
注意2
在AOP的配置信息中,使用< aop:after-returning>配置的後置通知和使用< aop:after>配置的最終通知雖然都是在目標方法執行之後執行,但它們是有區別的。後置通知只有在目標方法成功執行後纔會被植入,而最終通知不論目標方法如何結束(包括成功執行和異常中止兩種情況),它都會被植入。另外,如果程序沒有異常,異常通知將不會執行。
- 創建測試類
package com.ssm.aspectj.xml;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.aspectj.UserDao;
public class TestXmlAspectJ {
public static void main(String[] args) {
String xmlPath="com/ssm/aspectj/xml/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlPath);
//從容器中獲得內容
UserDao userDao=(UserDao)applicationContext.getBean("userDao");
//執行方法
userDao.addUser();
}
}
運行結果
在執行方法userDao.addUser();時,切面的程序就會響應,在編譯的時候就會運行起來。
3.2.2 基於註解的聲明式AspectJ
基於XML的聲明式AspectJ,有一個缺點就是需要在Spring文件中配置大量的代碼信息,爲解決這一問題,AspectJ框架爲AOP的實現提供了一套註解,用以取代Spring配置文件中爲實現AOP功能所配置的臃腫的代碼。
AspectJ註解的圖示
實例2
- 對MyAspect.java類進行修改
package com.ssm.aspectj.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 切面類,在此類中編寫通知
*/
@Aspect
@Component
public class MyAspect {
//定義切入點表達式
@Pointcut("execution(* com.ssm.aspectj.*.*(..))")
//使用一個返回值爲void、方法體爲空的方法來命名切入點
public void myPointCut(){}
//前置通知
@Before("myPointCut()")
public void myBefore(JoinPoint joinPoint){
System.out.print("前置通知:模擬執行權限檢查..,");
System.out.print("目標類是:"+joinPoint.getTarget());
System.out.println(",被植入增強處理的目標方法爲:"+joinPoint.getSignature().getName());
}
//後置通知
@AfterReturning(value="myPointCut()")
public void myAfterReturning(JoinPoint joinPoint) {
System.out.print("後置通知:模擬記錄日誌..,");
System.out.println("被植入增強處理的目標方法爲:" + joinPoint.getSignature().getName());
}
/**
* 環繞通知
* ProceedingJoinPoint是JoinPoint的子接口,表示可執行目標方法
* 1.必須是Object類型的返回值
* 2.必須接收一個參數,類型爲ProceedingJoinPoint
* 3.必須throws Throwable
*/
@Around("myPointCut()")
public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
//開始
System.out.println("環繞開始:執行目標方法之前,模擬開啓事務..,");
//執行當前目標方法
Object obj=proceedingJoinPoint.proceed();
//結束
System.out.println("環繞結束:執行目標方法之後,模擬關閉事務..,");
return obj;
}
//異常通知
@AfterThrowing(value="myPointCut()",throwing="e")
public void myAfterThrowing(JoinPoint joinPoint,Throwable e){
System.out.println("異常通知:出錯了"+e.getMessage());
}
//最終通知
@After("myPointCut()")
public void myAfter(){
System.out.println("最終通知:模擬方法結束後釋放資源..");
}
}
首先使用@Aspect註解定義了切面類,由於該類在Spring中是作爲組件使用的,因此還需要添加@Component註解才能生效。然後使用@Pointcut註解來配置切入表達式,並通過定義方法來表示切入點名稱。接下來在每個通知相應的方法上添加了相應的註解,並將切入點名稱“myPointcut”作爲參數傳遞給需要執行增強的通知方法。如果需要其他參數(如異常通知的異常參數),可以根據代碼提示傳遞相應的屬性值。
- 在目標類UserDaoImpl中添加註解
@Repository(“userDao”)
package com.ssm.aspectj;
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoImpl implements UserDao{
@Override
public void addUser() {
// TODO Auto-generated method stub
System.out.println("增加用戶");
}
@Override
public void deleteUser() {
// TODO Auto-generated method stub
System.out.println("刪除用戶");
}
}
- 創建配置文件編輯
<?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"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 指定需要掃描的包,使註解生效 -->
<context:component-scan base-package="com.ssm" />
<!-- 啓動基於註解的聲明式AspectJ支持 -->
<aop:aspectj-autoproxy />
</beans>
首先引入了context約束信息,然後使用元素設置了需要掃描的包,使註解生效。由於此案例中的目標類位於com.ssm.aspectj包中,因此這裏設置base-package的值爲“com.ssm",最後,使用<aop.aspectj-autoproxy/>來啓動Spring對基於註解的聲明式Aspect的支持。
- 創建測試類
package com.ssm.aspectj.annotation;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.ssm.aspectj.UserDao;
public class TestAnnotation {
public static void main(String[] args) {
String xmlPath="com/ssm/aspectj/annotation/applicationContext.xml";
ApplicationContext applicationContext=new ClassPathXmlApplicationContext(xmlPath);
UserDao userDao=(UserDao)applicationContext.getBean("userDao");
userDao.addUser();
}
}
運行結果和上述一致
注意3
如果在同一個連接點有多個通知需要執行(即是同一個連接點配置了多個切面),那麼在同一切面中,目標方法之前的前置通知和環繞通知的執行順序是未知的,目標方法之後的後置通知和環繞通知的執行順序也是未知的。