鏈接點 - Joinpoint切點 - Pointcut加強/通知 - Advice切面 - Aspect織入 - Weaving實例總結參考文獻html
在上一章節中咱們初步瞭解 Spring AOP,包括 Spring AOP 的基本概念以及使用,本文將對 AOP 核心概念進行解讀。java
鏈接點是指程序執行過程當中的一些點,好比方法調用,異常處理等。在 Spring AOP 中,僅支持方法級別的鏈接點。以上是官方說明,通俗地講就是可以被攔截的地方,每一個成員方法均可以稱之爲鏈接點。咱們仍是借用租房的案例作分析。web
Rent 接口:spring
public interface Rent {
//租房
public void rent(String address);
//買傢俱
public void buyFurniture();
}
複製代碼
該接口的實現類是 Host,即房東類,具體實現以下:express
@Component
public class Host implements Rent {
@Override
public void rent(String address) {
System.out.println("出租位於"+address+"處的房屋");
}
@Override
public void buyFurniture() {
System.out.println("添置傢俱");
}
}
複製代碼
其中 rent()方法即爲一個鏈接點。segmentfault
接下來咱們看看鏈接點的定義:api
public interface JoinPoint {
String METHOD_EXECUTION = "method-execution";
String METHOD_CALL = "method-call";
String CONSTRUCTOR_EXECUTION = "constructor-execution";
String CONSTRUCTOR_CALL = "constructor-call";
String FIELD_GET = "field-get";
String FIELD_SET = "field-set";
String STATICINITIALIZATION = "staticinitialization";
String PREINITIALIZATION = "preinitialization";
String INITIALIZATION = "initialization";
String EXCEPTION_HANDLER = "exception-handler";
String SYNCHRONIZATION_LOCK = "lock";
String SYNCHRONIZATION_UNLOCK = "unlock";
String ADVICE_EXECUTION = "adviceexecution";
String toString();
String toShortString();
String toLongString();
//獲取代理對象
Object getThis();
/**
返回目標對象。該對象將始終與target切入點指示符匹配的對象相同。除非您特別須要此反射訪問,不然應使用 target切入點指示符到達此對象,以得到更好的靜態類型和性能。
若是沒有目標對象,則返回null。
**/
Object getTarget();
//獲取傳入目標方法的參數對象
Object[] getArgs();
//獲取封裝了署名信息的對象,在該對象中能夠獲取到目標方法名,所屬類的Class等信息
Signature getSignature();
/**
返回與鏈接點對應的源位置。
若是沒有可用的源位置,則返回null。
返回默認構造函數的定義類的SourceLocation。
**/
SourceLocation getSourceLocation();
String getKind();
JoinPoint.StaticPart getStaticPart();
public interface EnclosingStaticPart extends JoinPoint.StaticPart {
}
//該幫助對象僅包含有關鏈接點的靜態信息。它能夠從JoinPoint.getStaticPart()方法中得到,也可使用特殊形式在建議中單獨訪問 thisJoinPointStaticPart。
public interface StaticPart {
Signature getSignature();
SourceLocation getSourceLocation();
String getKind();
int getId();
String toString();
String toShortString();
String toLongString();
}
}
複製代碼
JoinPoint 接口中經常使用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。可是咱們平時使用並不直接使用 JoinPoint 的實現類,中間還有一個接口實現,叫作 ProceedingJoinPoint,其定義以下:app
public interface ProceedingJoinPoint extends JoinPoint {
void set$AroundClosure(AroundClosure var1);
//執行目標方法
Object proceed() throws Throwable;
//傳入的新的參數去執行目標方法
Object proceed(Object[] var1) throws Throwable;
}
複製代碼
ProceedingJoinPoint 對象是 JoinPoint 的子接口,該對象只用在@Around 的切面方法中。在該接口中,proceed 方法是核心,該方法用於執行攔截器邏輯。關於攔截器這裏說一下,之前置通知攔截器爲例,在執行目標方法前,該攔截器首先會執行前置通知邏輯,若是攔截器鏈中還有其餘的攔截器,則繼續調用下一個攔截器邏輯。直到攔截器鏈中沒有其餘的攔截器後,再去調用目標方法。ide
proceed() 方法的具體實如今 MethodInvocationProceedingJoinPoint 類中,其定義以下:函數
public Object proceed() throws Throwable {
return this.methodInvocation.invocableClone().proceed();
}
public Object proceed(Object[] arguments) throws Throwable {
Assert.notNull(arguments, "Argument array passed to proceed cannot be null");
if (arguments.length != this.methodInvocation.getArguments().length) {
throw new IllegalArgumentException("Expecting " + this.methodInvocation.getArguments().length + " arguments to proceed, but was passed " + arguments.length + " arguments");
} else {
this.methodInvocation.setArguments(arguments);
return this.methodInvocation.invocableClone(arguments).proceed();
}
}
複製代碼
查看代碼可知,arguments 參數被傳入到 ProxyMethodInvocation 對象中,並調用自身的 proceed()方法,接着咱們定位到此處進行查看相關代碼:
public MethodInvocation invocableClone(Object... arguments) {
if (this.userAttributes == null) {
this.userAttributes = new HashMap();
}
try {
ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation)this.clone();
clone.arguments = arguments;
return clone;
} catch (CloneNotSupportedException var3) {
throw new IllegalStateException("Should be able to clone object of type [" + this.getClass() + "]: " + var3);
}
}
public Object proceed() throws Throwable {
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return this.invokeJoinpoint();
} else {
Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();
} else {
return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
}
}
}
複製代碼
如上所示,MethodInvocation 的實現類 ReflectiveMethodInvocation 獲取到傳入的參數以後,執行 proceed 方法,獲取到前置通知攔截器邏輯,而後經過反射進行調用。關於 ReflectiveMethodInvocation 類,又繼承自 JoinPoint 接口,因此咱們看一下這些定義之間的繼承關係:
關於鏈接點的相關知識,咱們先了解到這裏。有了這些鏈接點,咱們才能進行一些橫切操做,可是在操做以前,咱們須要定位選擇鏈接點,怎麼選擇的呢?這就是切點 Pointcut 要作的事情了,繼續往下看。
在上述定義的接口中的全部方法均可以認爲是 JoinPoint,可是有時咱們並不但願在全部的方法上都添加 Advice(這個後續會講到),而 Pointcut 的做用就是提供一組規則(使用 AspectJ pointcut expression language 來描述 )來匹配 JoinPoint,給知足規則的 JoinPoint 添加 Advice。
在上一節中咱們基於 XML 和註解實現了 AOP 功能,總結髮現切點的定義也分爲兩種。當基於 XML 文件時,能夠經過在配置文件中進行定義,具體以下:
<!--Spring基於Xml的切面-->
<aop:config>
<!--定義切點函數-->
<aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>
<!-- 定義切面 order 定義優先級,值越小優先級越大-->
<aop:aspect ref="proxy" order="0">
<!--前置通知-->
<aop:before method="seeHouse" pointcut-ref="rentPointCut" />
<!--環繞通知-->
<aop:around method="getMoney" pointcut-ref="rentPointCut" />
<!--後置通知-->
<aop:after method="fare" pointcut-ref="rentPointCut" />
</aop:aspect>
</aop:config>
複製代碼
當基於註解進行配置時,定義切點須要兩個步驟:
代碼定義以下:
//定義一個切入點表達式,用來肯定哪些類須要代理
@Pointcut("execution(* com.msdn.bean.Host.*(..))")
public void rentPointCut(){
}
複製代碼
這裏你們也都看到了,關於切點的定義要麼是經過來定義,又或者使用@Pointcut 來定義,並無其餘的地方出現過,可是經過以前在 Spring IoC 自定義標籤解析一文能夠知道,若是聲明瞭註解,那麼就必定會在程序中的某個地方註冊了對應的解析器。這裏就不從頭找起了,咱們先查看一下 Pointcut 定義:
package org.springframework.aop;
public interface Pointcut {
Pointcut TRUE = TruePointcut.INSTANCE;
/** 返回一個類型過濾器 */
ClassFilter getClassFilter();
/** 返回一個方法匹配器 */
MethodMatcher getMethodMatcher();
}
複製代碼
Pointcut 接口中定義了兩個接口,分別用於返回類型過濾器和方法匹配器。用於對定義的切點函數進行解析,關於切點函數的講解,你們能夠閱讀Spring AOP : AspectJ Pointcut 切點。下面咱們再來看一下類型過濾器和方法匹配器接口的定義:
@FunctionalInterface
public interface ClassFilter {
ClassFilter TRUE = TrueClassFilter.INSTANCE;
boolean matches(Class<?> var1);
}
public interface MethodMatcher {
MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
boolean matches(Method var1, Class<?> var2);
boolean isRuntime();
boolean matches(Method var1, Class<?> var2, Object... var3);
}
複製代碼
上面的兩個接口均定義了 matches 方法,咱們定義的切點函數就是經過 matches 方法進行解析的,而後選擇知足規則的鏈接點。在 Spring 中提供了一個 AspectJ 表達式切點類 AspectJExpressionPointcut,下面咱們來看一下這個類的繼承關係:
如上所示,這個類最終實現了 Pointcut、ClassFilter 和 MethodMatcher 接口,其中就包括 matches 方法的實現,具體代碼以下:
public boolean matches(Class<?> targetClass) {
PointcutExpression pointcutExpression = this.obtainPointcutExpression();
try {
try {
return pointcutExpression.couldMatchJoinPointsInType(targetClass);
} catch (ReflectionWorldException var5) {
logger.debug("PointcutExpression matching rejected target class - trying fallback expression", var5);
PointcutExpression fallbackExpression = this.getFallbackPointcutExpression(targetClass);
if (fallbackExpression != null) {
return fallbackExpression.couldMatchJoinPointsInType(targetClass);
}
}
} catch (Throwable var6) {
logger.debug("PointcutExpression matching rejected target class", var6);
}
return false;
}
複製代碼
經過該方法,對切點函數進行解析,該類也就具有了經過 AspectJ 表達式對鏈接點進行選擇的能力。
經過切點選擇出鏈接點以後,就要進行接下來的處理——通知(Advice)。
通知 Advice 即咱們定義的橫切邏輯,好比咱們能夠定義一個用於監控方法性能的通知,也能夠定義一個事務處理的通知等。若是說切點解決了通知在哪裏調用的問題,那麼如今還須要考慮了一個問題,即通知在什麼時候被調用?是在目標方法執行前被調用,仍是在目標方法執行結束後被調用,還在二者兼備呢?Spring 幫咱們解答了這個問題,Spring 中定義瞭如下幾種通知類型:
上面是五種通知的介紹,下面咱們來看一下通知的源碼,以下:
package org.aopalliance.aop;
public interface Advice {
}
複製代碼
如上,通知接口裏好像什麼都沒定義。不過別慌,咱們再去到它的子類接口中一探究竟。
//前置通知
public interface BeforeAdvice extends Advice {
}
//返回通知
public interface AfterReturningAdvice extends AfterAdvice {
void afterReturning(@Nullable Object var1, Method var2, Object[] var3, @Nullable Object var4) throws Throwable;
}
//後置通知
public interface AfterAdvice extends Advice {
}
//環繞通知
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation var1) throws Throwable;
}
//異常通知
public interface ThrowsAdvice extends AfterAdvice {
}
複製代碼
以上通知的定義很簡單,咱們找一下它們的具體實現類,這裏先看一下前置通知的實現類 AspectJMethodBeforeAdvice。
public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable {
public AspectJMethodBeforeAdvice(Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
super(aspectJBeforeAdviceMethod, pointcut, aif);
}
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null);
}
public boolean isBeforeAdvice() {
return true;
}
public boolean isAfterAdvice() {
return false;
}
}
複製代碼
上面的核心代碼是 before()方法,用於執行咱們定義的前置通知函數。
因爲環繞通知比較重要,因此再來看一下它的具體實現類代碼。
public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {
public AspectJAroundAdvice(Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
super(aspectJAroundAdviceMethod, pointcut, aif);
}
public boolean isBeforeAdvice() {
return false;
}
public boolean isAfterAdvice() {
return false;
}
protected boolean supportsProceedingJoinPoint() {
return true;
}
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
} else {
ProxyMethodInvocation pmi = (ProxyMethodInvocation)mi;
ProceedingJoinPoint pjp = this.lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = this.getJoinPointMatch(pmi);
return this.invokeAdviceMethod(pjp, jpm, (Object)null, (Throwable)null);
}
}
protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) {
return new MethodInvocationProceedingJoinPoint(rmi);
}
}
複製代碼
核心代碼爲 invoke 方法,當執行目標方法時,會首先來到這裏,而後再進入到自定義的環繞方法中。
如今咱們有了切點 Pointcut 和通知 Advice,因爲這兩個模塊目前仍是分離的,咱們須要把它們整合在一塊兒。這樣切點就能夠爲通知進行導航,而後由通知邏輯實施精確打擊。那怎麼整合兩個模塊呢?答案是,切面
。好的,是時候來介紹切面 Aspect 這個概念了。
切面 Aspect 整合了切點和通知兩個模塊,切點解決了 where 問題,通知解決了 when 和 how 問題。切面把二者整合起來,就能夠解決 對什麼方法(where)在什麼時候(when - 前置仍是後置,或者環繞)執行什麼樣的橫切邏輯(how)的三連發問題。在 AOP 中,切面只是一個概念,並無一個具體的接口或類與此對應。
切面類型主要分紅了三種:
通常切面,切點切面,引介/引入切面介紹:
public interface Advisor {
Advice EMPTY_ADVICE = new Advice() {
};
Advice getAdvice();
boolean isPerInstance();
}
複製代碼
咱們重點看一下 PointcutAdvisor ,關於該接口的定義以下:
public interface PointcutAdvisor extends Advisor {
Pointcut getPointcut();
}
複製代碼
Advisor 中有一個 getAdvice 方法,用於返回通知。PointcutAdvisor 在 Advisor 基礎上,新增了 getPointcut 方法,用於返回切點對象。所以 PointcutAdvisor 的實現類便可以返回切點,也能夠返回通知,因此說 PointcutAdvisor 和切面的功能類似。因此說 PointcutAdvisor 和切面的功能類似。不過他們之間仍是有一些差別的,好比看下面的配置:
<!--Spring基於Xml的切面-->
<aop:config>
<!--定義切點函數-->
<aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>
<!-- 定義切面 order 定義優先級,值越小優先級越大-->
<aop:aspect ref="proxy" order="0">
<!--前置通知-->
<aop:before method="seeHouse" pointcut-ref="rentPointCut" />
<!--環繞通知-->
<aop:around method="aroundMethod" pointcut-ref="rentPointCut" />
<!--後置通知-->
<aop:after method="fare" pointcut-ref="rentPointCut" />
</aop:aspect>
</aop:config>
複製代碼
如上,一個切面中配置了一個切點和三個通知,三個通知均引用了同一個切點,即 pointcut-ref="helloPointcut"
。這裏在一個切面中,一個切點對應多個通知,是一對多的關係(能夠配置多個 pointcut,造成多對多的關係)。而在 PointcutAdvisor 的實現類 AspectJPointcutAdvisor 中,切點和通知是一一對應的關係。
public AspectJPointcutAdvisor(AbstractAspectJAdvice advice) {
Assert.notNull(advice, "Advice must not be null");
this.advice = advice;
this.pointcut = advice.buildSafePointcut();
}
複製代碼
上面的通知最終會被轉換成三個 PointcutAdvisor,這裏我把在 AbstractAdvisorAutoProxyCreator 源碼調試的結果貼在下面:
織入是把切面應用到目標對象並建立新的代理對象的過程。切面在指定的鏈接點被織入到目標對象中。在目標對象的生命週期裏有不少個點能夠織入:
Spring AOP 既然是在目標對象運行期織入切面的,那它是經過什麼方式織入的呢?先來講說以何種方式進行織入,首先仍是從 LoadTimeWeaverAwareProcessor 開始,該類是後置處理器 BeanPostProcessor 的一個實現類,咱們都知道 BeanPostProcessor 有兩個核心方法,用於在 bean 初始化以前和以後被調用。具體是在 bean 對象初始化完成後,Spring經過切點對 bean 類中的方法進行匹配。若匹配成功,則會爲該 bean 生成代理對象,並將代理對象返回給容器。容器向後置處理器輸入 bean 對象,獲得 bean 對象的代理,這樣就完成了織入過程。 關於後置處理器的細節,這裏就很少說了,你們如有興趣,能夠參考以前寫的Spring之BeanFactoryPostProcessor和BeanPostProcessor
結合上述分析的內容,咱們對上一章節中的案例進行擴展,修改切面定義。
@Order(0)
@Aspect
@Component
public class JoinPointDemo {
//定義一個切入點表達式,用來肯定哪些類須要代理
@Pointcut("execution(* com.msdn.bean.Host.*(..))")
public void rentPointCut(){
}
/**
* 前置方法,在目標方法執行前執行
* @param joinPoint 封裝了代理方法信息的對象,若用不到則能夠忽略不寫
*/
@Before("rentPointCut()")
public void seeHouse(JoinPoint joinPoint){
Signature oo = joinPoint.getSignature();
System.out.println("前置方法準備執行......");
System.out.println("目標方法名爲:" + joinPoint.getSignature().getName());
System.out.println("目標方法所屬類的簡單類名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目標方法所屬類的類名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目標方法聲明類型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
//獲取傳入目標方法的參數
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "個參數爲:" + args[i]);
}
System.out.println("被代理的對象:" + joinPoint.getTarget());
System.out.println("代理對象本身:" + joinPoint.getThis());
System.out.println("前置方法執行結束......");
}
/**
* 環繞方法,可自定義目標方法執行的時機
* @param point
* @return 該方法須要返回值,返回值視爲目標方法的返回值
*/
@Around("rentPointCut()")
public Object aroundMethod(ProceedingJoinPoint point){
Object result = null;
try {
System.out.println("目標方法執行前...");
//獲取目標方法的參數,判斷執行哪一個proceed方法
Object[] args = point.getArgs();
if (args.length > 0){
//用新的參數值執行目標方法
result = point.proceed(new Object[]{"上海市黃浦區中華路某某公寓19樓2號"});
}else{
//執行目標方法
result = point.proceed();
}
} catch (Throwable e) {
//異常通知
System.out.println("執行目標方法異常後...");
throw new RuntimeException(e);
}
System.out.println("目標方法執行後...");
return result;
}
@After("rentPointCut()")
public void fare(JoinPoint joinPoint){
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/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.msdn.bean,com.msdn.aop" />
<aop:aspectj-autoproxy />
</beans>
複製代碼
測試代碼爲:
@Test
public void aopTest(){
ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");
Object host = context.getBean("host");
Rent o = (Rent) host;
o.rent("新東方");
o.buyFurniture();
}
複製代碼
執行結果爲:
目標方法執行前...
前置方法準備執行......
目標方法名爲:rent
目標方法所屬類的簡單類名:Rent
目標方法所屬類的類名:com.msdn.bean.Rent
目標方法聲明類型:public abstract
第1個參數爲:上海市黃浦區中華路某某公寓19樓2號
被代理的對象:com.msdn.bean.Host@524d6d96
代理對象本身:com.msdn.bean.Host@524d6d96
前置方法執行結束......
出租位於上海市黃浦區中華路某某公寓19樓2號處的房屋
目標方法執行後...
執行後置方法
********************
目標方法執行前...
前置方法準備執行......
目標方法名爲:buyFurniture
目標方法所屬類的簡單類名:Rent
目標方法所屬類的類名:com.msdn.bean.Rent
目標方法聲明類型:public abstract
被代理的對象:com.msdn.bean.Host@524d6d96
代理對象本身:com.msdn.bean.Host@524d6d96
前置方法執行結束......
添置傢俱
目標方法執行後...
執行後置方法
複製代碼
前面三篇文章只是在介紹 Spring AOP 的原理和基本使用,從本文開始準備深刻學習 AOP,其中就先了解 AOP 的核心概念,對於後續源碼的學習很是有必要。若是文中有什麼不對的地方,歡迎指正。