Spring AOP面向切面程式設計詳解(基於XML方式 註解方式 注入Aspectj方式)
前言
AOP即面向切面程式設計,是一種程式設計思想,OOP的延續。在程式開發中主要用來解決一些系統層面上的問題,比如日誌,事務,許可權等等。在閱讀本文前希望您已經對Spring有一定的瞭解
注:在能對程式碼進行添加註解方式實現AOP的話,並不推薦使用XML方式。換言之在XML方式配置更適用於不能對程式碼添加註解的情況下(註解配置方式推薦值>XML配置方式推薦值)
AOP相關術語
1.通知(Advice):在切面的某個特定的連線點上執行的動作,即當程式到達一個執行點後會執行相對應的一段程式碼,也稱為增強處理。通知共有如下5種類型[前置通知 後置通知 返回通知 環繞通知 丟擲異常後通知]
2.連線點(JoinPoint):程式執行的某個特定位置,例如類初始化前,類初始化後,方法執行前,方法執行後,方法丟擲異常時等,Spring只支援方法級別的連線點,即方法執行前,方法執行後,方法丟擲異常時
3.切入點(Pointcut):切入點是一個篩選連線點的過程,因為在你的工程中可能有很多連線點,你只是想讓其中幾個,在呼叫這幾個方法之前、之後或者丟擲異常時乾點什麼,那麼就用切入點來定義這幾個方法,讓切點來篩選連線點,選中那幾個你想要的方法
4.切面(Aspect):切面通常是指一個類,是通知和切入點的結合。到這裡會發現連線點就是為了讓你好理解切點產生的。通俗來說切面的配置可以理解為:什麼時候在什麼地方做什麼事。切入點說明了在哪裡幹(指定到方法),通知說明了什麼時候幹什麼
5.引入(Introduction ):引入允許我們向現有的類新增新方法或屬性
6.織入(Weaving):把切面應用到目標物件來建立新的代理物件的過程,織入一般發生在如下幾個時機:
(1)編譯時:當一個類檔案被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器
(2)類載入時:使用特殊的ClassLoader在目標類被載入到程式之前增強類的位元組程式碼
(3)執行時:切面在執行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術
基於XML方式配置AOP
正常通知
1.編寫業務類
public class HelloWorldBusiness {
public String sayHelloWorld(String language) {
String result = "Hello World " + language;
System.out.println("真正的業務方法執行啦~~~");
return result;
}
}
2.編寫切面類
public class HelloWorldBusinessAspect {
public void beforeSayHelloWorld(String language) {
System.out.println("執行方法前執行,引數為:" + language);
}
public void afterSayHelloWorld(String language) {
System.out.println("執行方法後執行,引數為:" + language);
}
public void afterReturningSayHelloWorld(String language, String result) {
System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
}
public void afterThrowingHelloWorld(String language, Throwable e) {
System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
}
}
3.編寫配置檔案
<?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 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 配置業務Bean -->
<bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />
<!-- 配置切面Bean -->
<bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />
<!-- 配置一個切面 -->
<aop:config>
<aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect">
<!-- 配置一個切點 -->
<aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..)) and args(language)" />
<!-- 配置前置通知 -->
<aop:before pointcut-ref="sayHelloWorldPoint" method="beforeSayHelloWorld" arg-names="language"/>
<!-- 配置前置通知 -->
<aop:after pointcut-ref="sayHelloWorldPoint" method="afterSayHelloWorld" arg-names="language"/>
<!-- 配置後置返回通知 -->
<aop:after-returning pointcut-ref="sayHelloWorldPoint" method="afterReturningSayHelloWorld" arg-names="language,result" returning="result" />
<!-- 異常通知 -->
<aop:after-throwing pointcut-ref="sayHelloWorldPoint" method="afterThrowingHelloWorld" arg-names="language,e" throwing="e" />
</aop:aspect>
</aop:config>
</beans>
4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為
執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA
環繞通知
1.編寫業務類
public class HelloWorldBusiness {
public String sayHelloWorld(String language) {
String result = "Hello World " + language;
System.out.println("真正的業務方法執行啦~~~");
return result;
}
}
2.編寫切面類
public class HelloWorldBusinessAspect {
public void aroundSayHelloWorld(ProceedingJoinPoint joinPoint) {
String language = (String) joinPoint.getArgs()[0];
try {
System.out.println("執行方法前執行,引數為:" + language);
String result = (String) joinPoint.proceed();
System.out.println("執行方法後執行,引數為:" + language + " 方法返回值為:" + result);
} catch (Throwable e) {
System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
}
}
}
3.編寫配置檔案
<?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 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 配置業務Bean -->
<bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />
<!-- 配置切面Bean -->
<bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />
<!-- 配置一個切面 -->
<aop:config>
<aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect">
<!-- 配置一個切點 -->
<aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))" />
<!-- 配置環繞通知 -->
<aop:around pointcut-ref="sayHelloWorldPoint" method="aroundSayHelloWorld" />
</aop:aspect>
</aop:config>
</beans>
4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為
執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA 方法返回值為:Hello World JAVA
使用MethodInterceptor實現AOP
1.編寫業務類
public class HelloWorldBusiness {
public String sayHelloWorld(String language) {
String result = "Hello World " + language;
System.out.println("真正的業務方法執行啦~~~");
return result;
}
}
2.編寫攔截器類 實現MethodInterceptor方法
public class HelloWorldBusinessAspect implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 獲取被增強物件引數列表
String language = (String) invocation.getArguments()[0];
// 獲取被增強物件的方法
Method method = invocation.getMethod();
// 繼續執行業務方法
System.out.println("執行" + method.getName() + "方法前執行,引數為: " + language);
Object result = invocation.proceed();
System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
return result;
}
}
3.編寫配置檔案
<?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 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 配置業務Bean -->
<bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />
<!-- 配置切面Bean -->
<bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />
<aop:config>
<!-- 配置一個切點 -->
<aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))" />
<!-- 配置通知類 -->
<aop:advisor advice-ref="helloWorldBusinessAspect" pointcut-ref="sayHelloWorldPoint" />
</aop:config>
</beans>
4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為
執行sayHelloWorld方法前執行,引數為: JAVA
真正的業務方法執行啦~~~
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA
注意事項
1.在Spring的配置檔案中,所有的切面和通知都必須定義在<aop:config>元素內部。(一個application context可以包含多個<aop:config>)。一個<aop:config>可以包含pointcut,advisor和aspect元素(注意這三個元素必須按照這個順序進行宣告)
2.當我們使用<aop:config/>方式進行配置時,可能與Spring的自動代理方式相互衝突(<aop:aspectj-autoproxy/>),因此,建議要麼全部使用<aop:config/>配置方式,要麼全部使用自動代理方式,不要把兩者混合使用
基於註解方式配置AOP
正常通知
1.編寫業務類
@Component
public class HelloWorldBusiness {
public String sayHelloWorld(String language) {
String result = "Hello World " + language;
System.out.println("真正的業務方法執行啦~~~");
return result;
}
}
2.編寫切面類
@Aspect
@Component
public class HelloWorldBusinessAspect {
@Pointcut("execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..)) && args(language)")
public void sysHelloWorldPointCut(String language) {
}
@Before("sysHelloWorldPointCut(language)")
public void beforeSayHelloWorld(String language) {
System.out.println("執行方法前執行,引數為:" + language);
}
@After("sysHelloWorldPointCut(language)")
public void afterSayHelloWorld(String language) {
System.out.println("執行方法後執行,引數為:" + language);
}
@AfterReturning(pointcut = "sysHelloWorldPointCut(language)", returning = "result")
public void afterReturningSayHelloWorld(String language, String result) {
System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
}
@AfterThrowing(pointcut = "sysHelloWorldPointCut(language)", throwing = "e")
public void afterThrowingHelloWorld(String language, Throwable e) {
System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
}
}
3.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {
}
4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為
執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA
環繞通知
1.編寫業務類
@Component
public class HelloWorldBusiness {
public String sayHelloWorld(String language) {
String result = "Hello World " + language;
System.out.println("真正的業務方法執行啦~~~");
return result;
}
}
2.編寫切面類
@Aspect
@Component
public class HelloWorldBusinessAspect {
@Pointcut("execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))")
public void sysHelloWorldPointCut() {
}
@Around("sysHelloWorldPointCut()")
public void aroundSayHelloWorld(ProceedingJoinPoint joinPoint) {
String language = (String) joinPoint.getArgs()[0];
try {
System.out.println("執行方法前執行,引數為:" + language);
String result = (String) joinPoint.proceed();
System.out.println("執行方法後執行,引數為:" + language + " 方法返回值為:" + result);
} catch (Throwable e) {
System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
}
}
}
3.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {
}
4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為
執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA 方法返回值為:Hello World JAVA
純AspectJ方式配置AOP
雖然Spring AOP能夠滿足許多應用的切面要求,但是與AspectJ相比,Spring AOP是一個功能比較弱的AOP解決方案,AspectJ提供了Spring AOP所不能支援的許多型別的切點,例如構造器切點等
1.本地安裝好AspectJ環境(具體安裝自行百度)
2.在IDEA中配置AspectJ環境(具體可百度)
3.編寫Aspect Demo使用AspectJ實現AOP功能
3.1.建立學生類
public class Student {
public Student() {
System.out.println("構造方法執行了");
}
public void doHomeWork(){
System.out.println("學生開始做功課啦~~~");
}
public static void main(String[] args) {
Student student = new Student();
student.doHomeWork();
}
}
3.2.建立AspectJ切面
public aspect StudentAspect {
// 建立構造器切點
pointcut constructPointCut():call(roberto.growth.process.aop.Student.new());
before():constructPointCut(){
System.out.println("建立學生物件前呼叫");
}
after():constructPointCut(){
System.out.println("建立學生物件後呼叫");
}
// 建立學生做功課方法切點
pointcut doHomeWorkPointCut():execution(public * roberto.growth.process.aop.Student.doHomeWork(..));
before():doHomeWorkPointCut(){
System.out.println("學生做功課前");
}
after():doHomeWorkPointCut(){
System.out.println("學生做功課後");
}
}
3.3.執行學生類的main方法,檢視輸出結果
建立學生物件前呼叫
構造方法執行了
建立學生物件後呼叫
學生做功課前
學生開始做功課啦~~~
學生做功課後
注意:由於AspectJ是在編譯時期進行織入,所以在執行main方法前最好先手動編譯一下
切入點相關
切入點表示式
本篇文章不介紹切入點表示式語法,與需要的讀者可以參考切入點表示式可參考: 切入點表示式
切入點執行順序
在配置切面和通知的時候,可以指定order引數來區分切入執行先後順序,order的值越小說明越先被執行
XML方式:<aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect" order="0">
Aspect類添加註解:org.springframework.core.annotation.Order
Aspect類實現介面:org.springframework.core.Ordered實現Ordered介面的getOrder()方法即可
基於註解方式引入新功能
Demo:為程式設計師新增歌手的屬性,讓程式設計師成為斜槓青年
1.編寫程式設計師類
public interface Programmer {
public void coding();
}
@Component
public class ProgrammerImpl implements Programmer{
@Override
public void coding() {
System.out.println("寫最好的程式碼");
}
}
2.編寫歌手類
public interface Singer {
void sing();
}
public class DefaultSinger implements Singer{
@Override
public void sing() {
System.out.println("唱最動聽的歌");
}
}
3.編寫歌手引入切面 為程式設計師新增歌手特性
@Aspect
@Component
public class SingerAspect {
@DeclareParents(value = "roberto.growth.process.aop.Programmer+", defaultImpl = DefaultSinger.class)
public static Singer singer;
}
4.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {
}
5.測試輸出結果如下:(程式設計師即可以寫程式碼也可以唱歌)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
public class StudentTest {
@Autowired
private Programmer programmer;
@Test
public void testSayHelloWorld() {
System.out.println("這個程式設計師是個歌手嗎:" + (programmer instanceof Singer));
programmer.coding();
((Singer) programmer).sing();
}
}
控制檯輸出:
這個程式設計師是個歌手嗎:true
寫最好的程式碼
唱最動聽的歌
基於XML方式引入新功能
Demo:為程式設計師新增歌手的屬性,讓程式設計師成為斜槓青年
1.編寫程式設計師類
public interface Programmer {
public void coding();
}
public class ProgrammerImpl implements Programmer{
@Override
public void coding() {
System.out.println("寫最好的程式碼");
}
}
2.編寫歌手類
public interface Singer {
void sing();
}
public class DefaultSinger implements Singer{
@Override
public void sing() {
System.out.println("唱最動聽的歌");
}
}
3.編寫配置檔案引入切面 為程式設計師新增歌手特性
<?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 http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<!-- 配置程式設計師類 -->
<bean id="programmer" class="roberto.growth.process.aop.ProgrammerImpl" />
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="(roberto.growth.process.aop.Programmer+)" implement-interface="roberto.growth.process.aop.Singer" default-impl="roberto.growth.process.aop.DefaultSinger" />
</aop:aspect>
</aop:config>
</beans>
4.測試輸出結果如下:(程式設計師即可以寫程式碼也可以唱歌)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class StudentTest {
@Autowired
private Programmer programmer;
@Test
public void testSayHelloWorld() {
System.out.println("這個程式設計師是個歌手嗎:" + (programmer instanceof Singer));
programmer.coding();
((Singer) programmer).sing();
}
}
控制檯輸出:
這個程式設計師是個歌手嗎:true
寫最好的程式碼
唱最動聽的歌