前言
前面兩篇 如何實現 AOP(上)、如何實現 AOP(中) 做了一些 AOP 的核心基礎知識簡要介紹,本文進入到了實戰環節了,去實現一個基於 XML 配置的簡易版 AOP,雖然是簡易版的但是麻雀雖小五臟俱全,一些核心的功能都會實現,通過實現這個簡易版的 AOP,相信你會對 AOP 有深入的理解,不止知其然,還能知其所以然。AOP 的頂層介面規範和底層依賴基礎元件都是由一個叫 AOP Alliance 的組織制定的,我們經常聽到的 AspectJ、ASM、CGLIB 就是其中被管理的一些專案,需要明確的一點是,在 Spring 中只是使用了 AspectJ 的核心概念和核心類,並不是像 AspectJ 那樣在編譯期實現的 AOP,而是在執行期。話不多說,下面開始進入主題。
解析 XML 中的 pointcut 定義及方法解析
假設有一個 OrderService 類(P.S. 這裡的 @Component 是我自定義的註解,詳見 這篇),其中有一個下單的方法 placeOrder(),我們想實現的效果是想給這個 placeOrder() 方法加上 資料庫事務,即執行方法之前開啟事務,執行過程中發生異常回滾事務,正常執行完成提交事務。OrderService 類的程式碼如下:
/**
* @author mghio
* @since 2021-06-06
*/
@Component(value = "orderService")
public class OrderService {
public void placeOrder() {
System.out.println("place order");
}
}
很明顯,這裡的 pointcut 就是 placeOrder() 方法,在 XML 配置檔案中的配置如下:
<aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>
我們需要一個類去表達這個概念,pointcut 要實現的功能是給定一個類的方法,判斷是否匹配配置檔案中給定的表示式。總的來看 pointcut 由方法匹配器和匹配表示式兩部分組成,方法匹配器可以有各種不同的實現,所以是一個介面,pointcut 同樣也可以基於多種不同技術實現,故也是一個介面,預設是基於 AspectJ 實現的,類圖結構如下:
實現類 AspectJExpressionPointcut 是基於 AspectJ 實現的,方法的匹配過程是委託給 AspectJ 中的 PointcutExpression 來判斷給定的方法是否匹配表示式,該類的核心實現如下:
/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJExpressionPointcut implements Pointcut, MethodMatcher {
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<>();
static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
}
private String expression;
private ClassLoader pointcutClassLoader;
private PointcutExpression pointcutExpression;
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
@Override
public String getExpression() {
return expression;
}
@Override
public boolean matches(Method method) {
checkReadyToMatch();
ShadowMatch shadowMatch = getShadowMatch(method);
return shadowMatch.alwaysMatches();
}
private void checkReadyToMatch() {
if (Objects.isNull(getExpression())) {
throw new IllegalArgumentException("Must set property 'expression' before attempting to match");
}
if (Objects.isNull(this.pointcutExpression)) {
this.pointcutClassLoader = ClassUtils.getDefaultClassLoader();
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
}
}
private PointcutExpression buildPointcutExpression(ClassLoader classLoader) {
PointcutParser pointcutParser = PointcutParser
.getPointcutParserSupportingSpecifiedPrimitivesAndUsingSpecifiedClassLoaderForResolution(SUPPORTED_PRIMITIVES, classLoader);
return pointcutParser.parsePointcutExpression(replaceBooleanOperators(getExpression()));
}
private String replaceBooleanOperators(String pcExpr) {
String result = StringUtils.replace(pcExpr, " and ", " && ");
result = StringUtils.replace(result, " or ", " || ");
result = StringUtils.replace(result, " not ", " ! ");
return result;
}
private ShadowMatch getShadowMatch(Method method) {
ShadowMatch shadowMatch;
try {
shadowMatch = this.pointcutExpression.matchesMethodExecution(method);
} catch (Exception e) {
throw new RuntimeException("not implemented yet");
}
return shadowMatch;
}
// omit other setter、getter ...
}
到這裡就完成了給定一個類的方法,判斷是否匹配配置檔案中給定的表示式的功能。再來看如下的一個完整的 AOP 配置:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.e3.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/beans/spring-context.xsd">
<context:scann-package base-package="cn.mghio.service.version5,cn.mghio.dao.version5" />
<bean id="tx" class="cn.mghio.tx.TransactionManager"/>
<aop:config>
<aop:aspect ref="tx">
<aop:pointcut id="placeOrder" expression="execution(* cn.mghio.service.version5.*.placeOrder(..))"/>
<aop:before pointcut-ref="placeOrder" method="start"/>
<aop:after-returning pointcut-ref="placeOrder" method="commit"/>
<aop:after-throwing pointcut-ref="placeOrder" method="rollback"/>
</aop:aspect>
</aop:config>
</beans>
在實現各種 XXXAdvice 之前需要定位到這個 Method,比如以上配置檔案中的 start、commit、rollback 等方法,為了達到這個目標我們還需要實現的功能就是根據一個 Bean 名稱(比如這裡的 tx)定位到指定的 Method,然後通過反射呼叫這個定位到的方法。實際上也比較簡單,這個類命名為 MethodLocatingFactory,根據其功能可以定義出目標 Bean 的名稱 targetBeanName、需要定位的方法名稱 methodName 以及定位完成後得到的方法 method 這三個屬性,整體類圖結構如下所示:
根據名稱和型別定位到方法主要是在 setBeanFactory() 方法中完成的,前提是對應的目標 Bean 名稱和方法名稱要設定完成,方法定位的類 MethodLocatingFactory 類的程式碼如下所示:
/**
* @author mghio
* @since 2021-06-06
*/
public class MethodLocatingFactory implements FactoryBean<Method>, BeanFactoryAware {
private String targetBeanName;
private String methodName;
private Method method;
public void setTargetBeanName(String targetBeanName) {
this.targetBeanName = targetBeanName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!StringUtils.hasText(this.targetBeanName)) {
throw new IllegalArgumentException("Property 'targetBeanName' is required");
}
if (!StringUtils.hasText(this.methodName)) {
throw new IllegalArgumentException("Property 'methodName' is required");
}
Class<?> beanClass = beanFactory.getType(this.targetBeanName);
if (Objects.isNull(beanClass)) {
throw new IllegalArgumentException("Can't determine type of bean with name '" + this.targetBeanName);
}
this.method = BeanUtils.resolveSignature(this.methodName, beanClass);
if (Objects.isNull(this.method)) {
throw new IllegalArgumentException("Unable to locate method [" + this.methodName + "] on bean ["
+ this.targetBeanName + "]");
}
}
@Override
public Method getObject() {
return this.method;
}
@Override
public Class<?> getObjectType() {
return Method.class;
}
}
實現各種不同型別的 Advice
各種不同型別的 Advice(BeforeAdvice、AfterAdvice 等)目標都是需要在指定物件的指定方法執行前後按指定次序執行一些操作(稱之為 攔截器),比如以上示例中的一種執行次序為:BeforeAdvice -> placeOrder -> AfterAdvice。這裡的一個關鍵問題就是如何去實現按照指定次序的鏈式呼叫?,這裡先賣個關子,這個問題先放一放等下再介紹具體實現,先來看看要如何定義各種不同型別的 Advice,我們的 Advice 定義都是擴充套件自 AOP Alliance 定義的 MethodInterceptor 介面,Advice 部分的核心類圖如下:
其實到這裡如果有了前面兩篇文章(如何實現 AOP(上)、如何實現 AOP(中))的基礎了,實現起來就相對比較簡單了,就是在方法執行之前、之後以及發生異常時呼叫一些特定的方法即可,AbstractAspectJAdvice 類定義了一下公共的屬性和方法,核心實現原始碼如下:
/**
* @author mghio
* @since 2021-06-06
*/
public abstract class AbstractAspectJAdvice implements Advice {
protected Method adviceMethod;
protected AspectJExpressionPointcut pc;
protected AopInstanceFactory adviceObjectFactory;
public AbstractAspectJAdvice(Method adviceMethod, AspectJExpressionPointcut pc, AopInstanceFactory adviceObjectFactory) {
this.adviceMethod = adviceMethod;
this.pc = pc;
this.adviceObjectFactory = adviceObjectFactory;
}
@Override
public Pointcut getPointcut() {
return pc;
}
protected void invokeAdviceMethod() throws Throwable {
adviceMethod.invoke(adviceObjectFactory.getAspectInstance());
}
public Object getAdviceInstance() throws Exception {
return adviceObjectFactory.getAspectInstance();
}
// omit getter ...
}
有了這個公共抽象父類之後其它幾個 Advice 的實現就很簡單了,AspectJBeforeAdvice 就是在執行攔截方法之前呼叫,核心原始碼如下:
/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJBeforeAdvice extends AbstractAspectJAdvice {
// omit constructor ...
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
this.invokeAdviceMethod();
return mi.proceed();
}
}
同理,AspectJAfterReturningAdvice 就是在方法正常執行結束後呼叫,核心原始碼如下:
/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice {
// omit constructor ...
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object result = mi.proceed();
this.invokeAdviceMethod();
return result;
}
}
剩下的 AspectJAfterThrowingAdvice 想必你已經猜到了,沒錯,就是在方法執行過程中發生異常時呼叫,對應 Java 的異常機制也就是在 try{...}catch{...} 的 catch 中呼叫,核心原始碼如下:
/**
* @author mghio
* @since 2021-06-06
*/
public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice {
// omit constructor ...
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
} catch (Throwable t) {
this.invokeAdviceMethod();
throw t;
}
}
}
我們支援的三種不同的 Advice 已經定義好了,接下來就是如何組裝呼叫的問題了,同時也處理了如何去實現按照指定次序的鏈式呼叫?的問題,這裡的方法呼叫我們也是擴充套件 AOP Alliance 定義的規範,即方法呼叫 MethodInvocation 介面。
由於這裡的方法呼叫是基於反射完成的,將該類命名為 ReflectiveMethodInvocation,要使用反射來呼叫方法,很顯然需要知道目標物件 targetObject、targetMethod 以及方法引數列表 arguments 等引數,當然還有我們的攔截器列表(也就是上文定義的 Advice)interceptors,因為這個是一個類似自呼叫的過程,為了判斷是否已經執行完成所有攔截器,還需要記錄當前呼叫攔截器的下標位置 currentInterceptorIndex,當 currentInterceptorIndex 等於 interceptors.size() - 1 時表示所有攔截器都已呼叫完成,再呼叫我們的實際方法即可。核心的類圖如下:
其中類 ReflectiveMethodInvocation 的核心原始碼實現如下,強烈建議大家將 proceed() 方法結合上問定義的幾個 Advice 類一起看:
/**
* @author mghio
* @since 2021-04-05
*/
public class ReflectiveMethodInvocation implements MethodInvocation {
protected final Object targetObject;
protected final Method targetMethod;
protected Object[] arguments;
protected final List<MethodInterceptor> interceptors;
private int currentInterceptorIndex = -1;
public ReflectiveMethodInvocation(Object targetObject, Method targetMethod,
Object[] arguments, List<MethodInterceptor> interceptors) {
this.targetObject = targetObject;
this.targetMethod = targetMethod;
this.arguments = arguments;
this.interceptors = interceptors;
}
@Override
public Object proceed() throws Throwable {
// all interceptors have been called.
if (this.currentInterceptorIndex == interceptors.size() - 1) {
return invokeJoinpoint();
}
this.currentInterceptorIndex++;
MethodInterceptor methodInterceptor = this.interceptors.get(this.currentInterceptorIndex);
return methodInterceptor.invoke(this);
}
private Object invokeJoinpoint() throws Throwable {
return this.targetMethod.invoke(this.targetObject, this.arguments);
}
// omit other method ...
}
至此,各種不同型別的 Advice 的核心實現已經介紹完畢,本來打算在這邊介紹完 AOP 剩下部分的實現的,但是鑑於文章長度太長,還是放到下一次再開一篇來介紹吧。
總結
本文主要介紹了 AOP 在 XML 配置的 pointcut 解析實現、方法匹配定位以及各種不同型別的 Advice 的實現,特別是 Advice 的實現部分,建議自己動手實現一版,這樣印象會更加深刻,另原始碼已上傳至 GitHub,可自行下載參考,有任何問題請留言交流討論。