在Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了AOP的基礎含義,同時以此發散了一些AOP相關知識點; 本節將在此基礎上進一步解讀AOP的含義以及AOP的使用方式。@pdai
我們在Spring基礎 - Spring簡單例子引入Spring的核心中向你展示了AOP的基礎含義,同時以此發散了一些AOP相關知識點。
本節將在此基礎上進一步解讀AOP的含義以及AOP的使用方式;後續的文章還將深入AOP的實現原理:
AOP的本質也是為了解耦,它是一種設計思想; 在理解時也應該簡化理解。
AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計
AOP最早是AOP聯盟的組織提出的,指定的一套規範,spring將AOP的思想引入框架之中,通過預編譯方式和執行期間動態代理實現程式的統一維護的一種技術,
/**
* @author pdai
*/
public class UserServiceImpl implements IUserService {
/**
* find user list.
*
* @return user list
*/
@Override
public List<User> findUserList() {
System.out.println("execute method: findUserList");
return Collections.singletonList(new User("pdai", 18));
}
/**
* add user
*/
@Override
public void addUser() {
System.out.println("execute method: addUser");
// do something
}
}
我們將記錄紀錄檔功能解耦為紀錄檔切面,它的目標是解耦。進而引出AOP的理念:就是將分散在各個業務邏輯程式碼中相同的程式碼通過橫向切割的方式抽取到一個獨立的模組中!
OOP物件導向程式設計,針對業務處理過程的實體及其屬性和行為進行抽象封裝,以獲得更加清晰高效的邏輯單元劃分。而AOP則是針對業務處理過程中的切面進行提取,它所面對的是處理過程的某個步驟或階段,以獲得邏輯過程的中各部分之間低耦合的隔離效果。這兩種設計思想在目標上有著本質的差異。
首先讓我們從一些重要的AOP概念和術語開始。這些術語不是Spring特有的。
連線點(Jointpoint):表示需要在程式中插入橫切關注點的擴充套件點,連線點可能是類初始化、方法執行、方法呼叫、欄位呼叫或處理異常等等,Spring只支援方法執行連線點,在AOP中表示為在哪裡幹;
切入點(Pointcut): 選擇一組相關連線點的模式,即可以認為連線點的集合,Spring支援perl5正規表示式和AspectJ切入點模式,Spring預設使用AspectJ語法,在AOP中表示為在哪裡乾的集合;
通知(Advice):在連線點上執行的行為,通知提供了在AOP中需要在切入點所選擇的連線點處進行擴充套件現有行為的手段;包括前置通知(before advice)、後置通知(after advice)、環繞通知(around advice),在Spring中通過代理模式實現AOP,並通過攔截器模式以環繞連線點的攔截器鏈織入通知;在AOP中表示為幹什麼;
方面/切面(Aspect):橫切關注點的模組化,比如上邊提到的紀錄檔元件。可以認為是通知、引入和切入點的組合;在Spring中可以使用Schema和@AspectJ方式進行組織實現;在AOP中表示為在哪乾和幹什麼集合;
引入(inter-type declaration):也稱為內部型別宣告,為已有的類新增額外新的欄位或方法,Spring允許引入新的介面(必須對應一個實現)到所有被代理物件(目標物件), 在AOP中表示為幹什麼(引入什麼);
目標物件(Target Object):需要被織入橫切關注點的物件,即該物件是切入點選擇的物件,需要被通知的物件,從而也可稱為被通知物件;由於Spring AOP 通過代理模式實現,從而這個物件永遠是被代理物件,在AOP中表示為對誰幹;
織入(Weaving):把切面連線到其它的應用程式型別或者物件上,並建立一個被通知的物件。這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。在AOP中表示為怎麼實現的;
AOP代理(AOP Proxy):AOP框架使用代理模式建立的物件,從而實現在連線點處插入通知(即應用切面),就是通過代理來對目標物件應用切面。在Spring中,AOP代理可以用JDK動態代理或CGLIB代理實現,而通過攔截器模型應用切面。在AOP中表示為怎麼實現的一種典型方式;
通知型別:
前置通知(Before advice):在某連線點之前執行的通知,但這個通知不能阻止連線點之前的執行流程(除非它丟擲一個異常)。
後置通知(After returning advice):在某連線點正常完成後執行的通知:例如,一個方法沒有丟擲任何異常,正常返回。
異常通知(After throwing advice):在方法丟擲異常退出時執行的通知。
最終通知(After (finally) advice):當某連線點退出的時候執行的通知(不論是正常返回還是異常退出)。
環繞通知(Around Advice):包圍一個連線點的通知,如方法呼叫。這是最強大的一種通知型別。環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它自己的返回值或丟擲異常來結束執行。
環繞通知是最常用的通知型別。和AspectJ一樣,Spring提供所有型別的通知,我們推薦你使用盡可能簡單的通知型別來實現需要的功能。例如,如果你只是需要一個方法的返回值來更新快取,最好使用後置通知而不是環繞通知,儘管環繞通知也能完成同樣的事情。用最合適的通知型別可以使得程式設計模型變得簡單,並且能夠避免很多潛在的錯誤。比如,你不需要在JoinPoint上呼叫用於環繞通知的proceed()方法,就不會有呼叫的問題。
我們把這些術語串聯到一起,方便理解
AspectJ是一個java實現的AOP框架,它能夠對java程式碼進行AOP編譯(一般在編譯期進行),讓java程式碼具有AspectJ的AOP功能(當然需要特殊的編譯器)
可以這樣說AspectJ是目前實現AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程式完全相容,幾乎是無縫關聯,因此對於有java程式設計基礎的工程師,上手和使用都非常容易。
我們看下@Aspect以及增強的幾個註解,為什麼不是Spring包,而是來源於aspectJ呢?
瞭解AspectJ應用到java程式碼的過程(這個過程稱為織入),對於織入這個概念,可以簡單理解為aspect(切面)應用到目標函數(類)的過程。
對於這個過程,一般分為動態織入和靜態織入:
Spring AOP 支援對XML模式和基於@AspectJ註解的兩種設定方式。
Spring提供了使用"aop"名稱空間來定義一個切面,我們來看個例子(例子程式碼):
package tech.pdai.springframework.service;
/**
* @author pdai
*/
public class AopDemoServiceImpl {
public void doMethod1() {
System.out.println("AopDemoServiceImpl.doMethod1()");
}
public String doMethod2() {
System.out.println("AopDemoServiceImpl.doMethod2()");
return "hello world";
}
public String doMethod3() throws Exception {
System.out.println("AopDemoServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
package tech.pdai.springframework.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @author pdai
*/
public class LogAspect {
/**
* 環繞通知.
*
* @param pjp pjp
* @return obj
* @throws Throwable exception
*/
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----------------------");
System.out.println("環繞通知: 進入方法");
Object o = pjp.proceed();
System.out.println("環繞通知: 退出方法");
return o;
}
/**
* 前置通知.
*/
public void doBefore() {
System.out.println("前置通知");
}
/**
* 後置通知.
*
* @param result return val
*/
public void doAfterReturning(String result) {
System.out.println("後置通知, 返回值: " + result);
}
/**
* 異常通知.
*
* @param e exception
*/
public void doAfterThrowing(Exception e) {
System.out.println("異常通知, 異常: " + e.getMessage());
}
/**
* 最終通知.
*/
public void doAfter() {
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.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
">
<context:component-scan base-package="tech.pdai.springframework" />
<aop:aspectj-autoproxy/>
<!-- 目標類 -->
<bean id="demoService" class="tech.pdai.springframework.service.AopDemoServiceImpl">
<!-- configure properties of bean here as normal -->
</bean>
<!-- 切面 -->
<bean id="logAspect" class="tech.pdai.springframework.aspect.LogAspect">
<!-- configure properties of aspect here as normal -->
</bean>
<aop:config>
<!-- 設定切面 -->
<aop:aspect ref="logAspect">
<!-- 設定切入點 -->
<aop:pointcut id="pointCutMethod" expression="execution(* tech.pdai.springframework.service.*.*(..))"/>
<!-- 環繞通知 -->
<aop:around method="doAround" pointcut-ref="pointCutMethod"/>
<!-- 前置通知 -->
<aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
<!-- 後置通知;returning屬性:用於設定後置通知的第二個引數的名稱,型別是Object -->
<aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
<!-- 異常通知:如果沒有異常,將不會執行增強;throwing屬性:用於設定通知第二個引數的的名稱、型別-->
<aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
<!-- 最終通知 -->
<aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
</aop:aspect>
</aop:config>
<!-- more bean definitions for data access objects go here -->
</beans>
/**
* main interfaces.
*
* @param args args
*/
public static void main(String[] args) {
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("aspects.xml");
// retrieve configured instance
AopDemoServiceImpl service = context.getBean("demoService", AopDemoServiceImpl.class);
// use configured instance
service.doMethod1();
service.doMethod2();
try {
service.doMethod3();
} catch (Exception e) {
// e.printStackTrace();
}
}
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod1()
環繞通知: 退出方法
最終通知
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod2()
環繞通知: 退出方法
最終通知
後置通知, 返回值: hello world
-----------------------
環繞通知: 進入方法
前置通知
AopDemoServiceImpl.doMethod3()
最終通知
異常通知, 異常: some exception
基於XML的宣告式AspectJ存在一些不足,需要在Spring組態檔設定大量的程式碼資訊,為了解決這個問題,Spring 使用了@AspectJ框架為AOP的實現提供了一套註解。
註解名稱 | 解釋 |
---|---|
@Aspect | 用來定義一個切面。 |
@pointcut | 用於定義切入點表示式。在使用時還需要定義一個包含名字和任意引數的方法簽名來表示切入點名稱,這個方法簽名就是一個返回值為void,且方法體為空的普通方法。 |
@Before | 用於定義前置通知,相當於BeforeAdvice。在使用時,通常需要指定一個value屬性值,該屬性值用於指定一個切入點表示式(可以是已有的切入點,也可以直接定義切入點表示式)。 |
@AfterReturning | 用於定義後置通知,相當於AfterReturningAdvice。在使用時可以指定pointcut / value和returning屬性,其中pointcut / value這兩個屬性的作用一樣,都用於指定切入點表示式。 |
@Around | 用於定義環繞通知,相當於MethodInterceptor。在使用時需要指定一個value屬性,該屬性用於指定該通知被植入的切入點。 |
@After-Throwing | 用於定義異常通知來處理程式中未處理的異常,相當於ThrowAdvice。在使用時可指定pointcut / value和throwing屬性。其中pointcut/value用於指定切入點表示式,而throwing屬性值用於指定-一個形參名來表示Advice方法中可定義與此同名的形參,該形參可用於存取目標方法丟擲的異常。 |
@After | 用於定義最終final 通知,不管是否異常,該通知都會執行。使用時需要指定一個value屬性,該屬性用於指定該通知被植入的切入點。 |
@DeclareParents | 用於定義引介通知,相當於IntroductionInterceptor (不要求掌握)。 |
Spring AOP的實現方式是動態織入,動態織入的方式是在執行時動態將要增強的程式碼織入到目標類中,這樣往往是通過動態代理技術完成的;如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP採用的就是基於執行時增強的代理技術。所以我們看下如下的兩個例子(例子程式碼 中05模組):
- 基於JDK代理例子
- 基於Cglib代理例子
/**
* Jdk Proxy Service.
*
* @author pdai
*/
public interface IJdkProxyService {
void doMethod1();
String doMethod2();
String doMethod3() throws Exception;
}
/**
* @author pdai
*/
@Service
public class JdkProxyDemoServiceImpl implements IJdkProxyService {
@Override
public void doMethod1() {
System.out.println("JdkProxyServiceImpl.doMethod1()");
}
@Override
public String doMethod2() {
System.out.println("JdkProxyServiceImpl.doMethod2()");
return "hello world";
}
@Override
public String doMethod3() throws Exception {
System.out.println("JdkProxyServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
package tech.pdai.springframework.aspect;
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.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
/**
* @author pdai
*/
@EnableAspectJAutoProxy
@Component
@Aspect
public class LogAspect {
/**
* define point cut.
*/
@Pointcut("execution(* tech.pdai.springframework.service.*.*(..))")
private void pointCutMethod() {
}
/**
* 環繞通知.
*
* @param pjp pjp
* @return obj
* @throws Throwable exception
*/
@Around("pointCutMethod()")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----------------------");
System.out.println("環繞通知: 進入方法");
Object o = pjp.proceed();
System.out.println("環繞通知: 退出方法");
return o;
}
/**
* 前置通知.
*/
@Before("pointCutMethod()")
public void doBefore() {
System.out.println("前置通知");
}
/**
* 後置通知.
*
* @param result return val
*/
@AfterReturning(pointcut = "pointCutMethod()", returning = "result")
public void doAfterReturning(String result) {
System.out.println("後置通知, 返回值: " + result);
}
/**
* 異常通知.
*
* @param e exception
*/
@AfterThrowing(pointcut = "pointCutMethod()", throwing = "e")
public void doAfterThrowing(Exception e) {
System.out.println("異常通知, 異常: " + e.getMessage());
}
/**
* 最終通知.
*/
@After("pointCutMethod()")
public void doAfter() {
System.out.println("最終通知");
}
}
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod1()
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod2()
後置通知, 返回值: hello world
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
JdkProxyServiceImpl.doMethod3()
異常通知, 異常: some exception
最終通知
/**
* Cglib proxy.
*
* @author pdai
*/
@Service
public class CglibProxyDemoServiceImpl {
public void doMethod1() {
System.out.println("CglibProxyDemoServiceImpl.doMethod1()");
}
public String doMethod2() {
System.out.println("CglibProxyDemoServiceImpl.doMethod2()");
return "hello world";
}
public String doMethod3() throws Exception {
System.out.println("CglibProxyDemoServiceImpl.doMethod3()");
throw new Exception("some exception");
}
}
和上面相同
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod1()
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod2()
後置通知, 返回值: hello world
最終通知
環繞通知: 退出方法
-----------------------
環繞通知: 進入方法
前置通知
CglibProxyDemoServiceImpl.doMethod3()
異常通知, 異常: some exception
最終通知
這裡總結下實際開發中會遇到的一些問題:
Spring AOP 使用者可能會經常使用 execution切入點指示符。執行表示式的格式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
*
,它代表了匹配任意的返回型別。*
萬用字元作為所有或者部分命名模式。對應到我們上面的例子:
下面給出一些通用切入點表示式的例子。
// 任意公共方法的執行:
execution(public * *(..))
// 任何一個名字以「set」開始的方法的執行:
execution(* set*(..))
// AccountService介面定義的任意方法的執行:
execution(* com.xyz.service.AccountService.*(..))
// 在service包中定義的任意方法的執行:
execution(* com.xyz.service.*.*(..))
// 在service包或其子包中定義的任意方法的執行:
execution(* com.xyz.service..*.*(..))
// 在service包中的任意連線點(在Spring AOP中只是方法執行):
within(com.xyz.service.*)
// 在service包或其子包中的任意連線點(在Spring AOP中只是方法執行):
within(com.xyz.service..*)
// 實現了AccountService介面的代理物件的任意連線點 (在Spring AOP中只是方法執行):
this(com.xyz.service.AccountService)// 'this'在繫結表單中更加常用
// 實現AccountService介面的目標物件的任意連線點 (在Spring AOP中只是方法執行):
target(com.xyz.service.AccountService) // 'target'在繫結表單中更加常用
// 任何一個只接受一個引數,並且執行時所傳入的引數是Serializable 介面的連線點(在Spring AOP中只是方法執行)
args(java.io.Serializable) // 'args'在繫結表單中更加常用; 請注意在例子中給出的切入點不同於 execution(* *(java.io.Serializable)): args版本只有在動態執行時候傳入引數是Serializable時才匹配,而execution版本在方法簽名中宣告只有一個 Serializable型別的引數時候匹配。
// 目標物件中有一個 @Transactional 註解的任意連線點 (在Spring AOP中只是方法執行)
@target(org.springframework.transaction.annotation.Transactional)// '@target'在繫結表單中更加常用
// 任何一個目標物件宣告的型別有一個 @Transactional 註解的連線點 (在Spring AOP中只是方法執行):
@within(org.springframework.transaction.annotation.Transactional) // '@within'在繫結表單中更加常用
// 任何一個執行的方法有一個 @Transactional 註解的連線點 (在Spring AOP中只是方法執行)
@annotation(org.springframework.transaction.annotation.Transactional) // '@annotation'在繫結表單中更加常用
// 任何一個只接受一個引數,並且執行時所傳入的引數型別具有@Classified 註解的連線點(在Spring AOP中只是方法執行)
@args(com.xyz.security.Classified) // '@args'在繫結表單中更加常用
// 任何一個在名為'tradeService'的Spring bean之上的連線點 (在Spring AOP中只是方法執行)
bean(tradeService)
// 任何一個在名字匹配萬用字元表示式'*Service'的Spring bean之上的連線點 (在Spring AOP中只是方法執行)
bean(*Service)
此外Spring 支援如下三個邏輯運運算元來組合切入點表示式
&&:要求連線點同時匹配兩個切入點表示式
||:要求連線點匹配任意個切入點表示式
!::要求連線點不匹配指定的切入點表示式
如果有多個通知想要在同一連線點執行會發生什麼?Spring AOP遵循跟AspectJ一樣的優先規則來確定通知執行的順序。 在「進入」連線點的情況下,最高優先順序的通知會先執行(所以給定的兩個前置通知中,優先順序高的那個會先執行)。 在「退出」連線點的情況下,最高優先順序的通知會最後執行。(所以給定的兩個後置通知中, 優先順序高的那個會第二個執行)。
當定義在不同的切面裡的兩個通知都需要在一個相同的連線點中執行, 那麼除非你指定,否則執行的順序是未知的。你可以通過指定優先順序來控制執行順序。 在標準的Spring方法中可以在切面類中實現org.springframework.core.Ordered 介面或者用Order註解做到這一點。在兩個切面中, Ordered.getValue()方法返回值(或者註解值)較低的那個有更高的優先順序。
當定義在相同的切面裡的兩個通知都需要在一個相同的連線點中執行, 執行的順序是未知的(因為這裡沒有方法通過反射javac編譯的類來獲取宣告順序)。 考慮在每個切面類中按連線點壓縮這些通知方法到一個通知方法,或者重構通知的片段到各自的切面類中 - 它能在切面級別進行排序。
AspectJ可以做Spring AOP幹不了的事情,它是AOP程式設計的完全解決方案,Spring AOP則致力於解決企業級開發中最普遍的AOP(方法織入)。
下表總結了 Spring AOP 和 AspectJ 之間的關鍵區別:
Spring AOP | AspectJ |
---|---|
在純 Java 中實現 | 使用 Java 程式語言的擴充套件實現 |
不需要單獨的編譯過程 | 除非設定 LTW,否則需要 AspectJ 編譯器 (ajc) |
只能使用執行時織入 | 執行時織入不可用。支援編譯時、編譯後和載入時織入 |
功能不強-僅支援方法級編織 | 更強大 - 可以編織欄位、方法、建構函式、靜態初始值設定項、最終類/方法等......。 |
只能在由 Spring 容器管理的 bean 上實現 | 可以在所有域物件上實現 |
僅支援方法執行切入點 | 支援所有切入點 |
代理是由目標物件建立的, 並且切面應用在這些代理上 | 在執行應用程式之前 (在執行時) 前, 各方面直接在程式碼中進行織入 |
比 AspectJ 慢多了 | 更好的效能 |
易於學習和應用 | 相對於 Spring AOP 來說更復雜 |
以下Spring官方的回答:(總結來說就是 Spring AOP更易用,AspectJ更強大)。
當使用AspectJ時,你可以選擇使用AspectJ語言(也稱為「程式碼風格」)或@AspectJ註解風格。 如果切面在你的設計中扮演一個很大的角色,並且你能在Eclipse等IDE中使用AspectJ Development Tools (AJDT), 那麼首選AspectJ語言 :- 因為該語言專門被設計用來編寫切面,所以會更清晰、更簡單。如果你沒有使用 Eclipse等IDE,或者在你的應用中只有很少的切面並沒有作為一個主要的角色,你或許應該考慮使用@AspectJ風格 並在你的IDE中附加一個普通的Java編輯器,並且在你的構建指令碼中增加切面織入(連結)的段落。
http://shouce.jb51.net/spring/aop.html
https://www.cnblogs.com/linhp/p/5881788.html
https://www.cnblogs.com/bj-xiaodao/p/10777914.html
首先, 從Spring框架的整體架構和組成對整體框架有個認知。
其次,通過案例引出Spring的核心(IoC和AOP),同時對IoC和AOP進行案例使用分析。
基於Spring框架和IOC,AOP的基礎,為構建上層web應用,需要進一步學習SpringMVC。
Spring進階 - IoC,AOP以及SpringMVC的原始碼分析
ConcurrentHashMap<String, Object>
;並且BeanDefinition介面中包含了這個類的Class資訊以及是否是單例等。那麼如何從BeanDefinition中範例化Bean物件呢,這是本文主要研究的內容?