原文地址:犀利豆的部落格
上一講我們講解了Spring 的 IoC 實現。大家可以去我的部落格檢視點選連結,這一講我們繼續說說 Spring 的另外一個重要特性 AOP。之前在看過的大部分教程,對於Spring Aop的實現講解的都不太透徹,大部分文章介紹了Spring Aop的底層技術使用了動態代理,至於Spring Aop的具體實現都語焉不詳。這類文章看以後以後,我腦子裡浮現的就是這樣一個畫面:
我的想法就是,帶領大家,首先梳理 Spring Aop的實現,然後遮蔽細節,自己實現一個Aop框架。加深對Spring Aop的理解。在瞭解上圖1-4步驟的同時,補充 4 到 5 步驟之間的其他細節。
讀完這篇文章你將會了解:
- Aop是什麼?
- 為什麼要使用Aop?
- Spirng 實現Aop的思路是什麼
- 自己根據Spring 思想實現一個 Aop框架
Aop 是什麼?
面向切面的程式設計(aspect-oriented programming,AOP)。通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。
為什麼需要使用Aop?
面向切面程式設計,實際上就是通過預編譯或者動態代理技術在不修改原始碼的情況下給原來的程式統一新增功能的一種技術。我們看幾個關鍵詞,第一個是“動態代理技術”,這個就是Spring Aop實現底層技術。第二個“不修改原始碼”,這個就是Aop最關鍵的地方,也就是我們平時所說的非入侵性。。第三個“新增功能”,不改變原有的原始碼,為程式新增功能。
舉個例子:如果某天你需要統計若干方法的執行時間,如果不是用Aop技術,你要做的就是為每一個方法開始的時候獲取一個開始時間,在方法結束的時候獲取結束時間。二者之差就是方法的執行時間。如果對每一個需要統計的方法都做如上的操作,那程式碼簡直就是災難。如果我們使用Aop技術,在不修改程式碼的情況下,新增一個統計方法執行時間的切面。程式碼就變得十分優雅。具體這個切面怎麼實現?看完下面的文章你一定就會知道。
Spring Aop 是怎麼實現的?
所謂:
計算機程式 = 資料結構 + 演算法
在閱讀過Spring原始碼之後,你就會對這個說法理解更深入了。
Spring Aop實現的程式碼非常非常的繞。也就是說 Spring 為了靈活做了非常深層次的抽象。同時 Spring為了相容 @AspectJ
的Aop協議,使用了很多 Adapter (介面卡)模式又進一步的增加了程式碼的複雜程度。
Spring 的 Aop 實現主要以下幾個步驟:
- 初始化 Aop 容器。
- 讀取配置檔案。
- 將配置檔案裝換為 Aop 能夠識別的資料結構 --
Advisor
。這裡展開講一講這個advisor。Advisor物件中包又含了兩個重要的資料結構,一個是Advice
,一個是Pointcut
。Advice
的作用就是描述一個切面的行為,pointcut
描述的是切面的位置。兩個資料結的組合就是”在哪裡,幹什麼“。這樣Advisor
就包含了”在哪裡幹什麼“的資訊,就能夠全面的描述切面了。 - Spring 將這個 Advisor 轉換成自己能夠識別的資料結構 --
AdvicedSupport
。Spirng 動態的將這些方法攔截器織入到對應的方法。 - 生成動態代理代理。
- 提供呼叫,在使用的時候,呼叫方呼叫的就是代理方法。也就是已經織入了增強方法的方法。
自己實現一個 Aop 框架。
同樣,我也是參考了Aop的設計。只實現了基於方法的攔截器。去除了很多的實現細節。
使用上一講的 IoC 框架管理物件。使用 Cglib 作為動態代理的基礎類。使用 maven 管理 jar 包和 module。所以上一講的 IoC 框架會作為一個 modules 引入專案。
下面我們就來實現我們的Aop 框架吧。
首先來看看程式碼的基本結構。
程式碼結構比上一講的 IoC 複雜不少。我們首先對包每個包都幹了什麼做一個簡單介紹。
invocation
描述的就是一個方法的呼叫。注意這裡指的是“方法的呼叫”,而不是呼叫這個動作。interceptor
大家最熟悉的攔截器,攔截器攔截的目標就是invcation
包裡面的呼叫。advisor
這個包裡的物件,都是用來描述切面的資料結構。adapter
這個包裡面是一些介面卡方法。對於"介面卡"不瞭解的同學可以去看看"設計模式"裡面的"適配模式"。他的作用就是將advice
包裡的物件適配為interceptor
。bean
描述我們 json 配置檔案的物件。core
我們框架的核心邏輯。
這個時候巨集觀的看我們大概梳理出了一條路線, adaper
將 advisor
適配為 interceptor
去攔截 invoction
。
下面我們從這個鏈條的最末端講起:
invcation
首先 MethodInvocation
作為所有方法呼叫的介面。要描述一個方法的呼叫包含三個方法,獲取方法本身getMethod
,獲取方法的引數getArguments
,還有執行方法本身proceed()
。
public interface MethodInvocation {
Method getMethod();
Object[] getArguments();
Object proceed() throws Throwable;
}
複製程式碼
ProxyMethodInvocation
看名字就知道,是代理方法的呼叫,增加了一個獲取代理的方法。
public interface ProxyMethodInvocation extends MethodInvocation {
Object getProxy();
}
複製程式碼
interceptor
AopMethodInterceptor
是 Aop 容器所有攔截器都要實現的介面:
public interface AopMethodInterceptor {
Object invoke(MethodInvocation mi) throws Throwable;
}
複製程式碼
同時我們實現了兩種攔截器BeforeMethodAdviceInterceptor
和AfterRunningAdviceInterceptor
,顧名思義前者就是在方法執行以前攔截,後者就在方法執行結束以後攔截:
public class BeforeMethodAdviceInterceptor implements AopMethodInterceptor {
private BeforeMethodAdvice advice;
public BeforeMethodAdviceInterceptor(BeforeMethodAdvice advice) {
this.advice = advice;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
advice.before(mi.getMethod(),mi.getArguments(),mi);
return mi.proceed();
}
}
複製程式碼
public class AfterRunningAdviceInterceptor implements AopMethodInterceptor {
private AfterRunningAdvice advice;
public AfterRunningAdviceInterceptor(AfterRunningAdvice advice) {
this.advice = advice;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
Object returnVal = mi.proceed();
advice.after(returnVal,mi.getMethod(),mi.getArguments(),mi);
return returnVal;
}
}
複製程式碼
看了上面的程式碼我們發現,實際上 mi.proceed()
才是執行原有的方法。而advice
我們上文就說過,是描述增強的方法”幹什麼“的資料結構,所以對於這個before攔截器,我們就把advice對應的增強方法放在了真正執行的方法前面。而對於after攔截器而言,就放在了真正執行的方法後面。
這個時候我們過頭來看最關鍵的 ReflectioveMethodeInvocation
public class ReflectioveMethodeInvocation implements ProxyMethodInvocation {
public ReflectioveMethodeInvocation(Object proxy, Object target, Method method, Object[] arguments, List<AopMethodInterceptor> interceptorList) {
this.proxy = proxy;
this.target = target;
this.method = method;
this.arguments = arguments;
this.interceptorList = interceptorList;
}
protected final Object proxy;
protected final Object target;
protected final Method method;
protected Object[] arguments = new Object[0];
//儲存所有的攔截器
protected final List<AopMethodInterceptor> interceptorList;
private int currentInterceptorIndex = -1;
@Override
public Object getProxy() {
return proxy;
}
@Override
public Method getMethod() {
return method;
}
@Override
public Object[] getArguments() {
return arguments;
}
@Override
public Object proceed() throws Throwable {
//執行完所有的攔截器後,執行目標方法
if(currentInterceptorIndex == this.interceptorList.size() - 1) {
return invokeOriginal();
}
//迭代的執行攔截器。回顧上面的講解,我們實現的攔擊都會執行 im.proceed() 實際上又會呼叫這個方法。實現了一個遞迴的呼叫,直到執行完所有的攔截器。
AopMethodInterceptor interceptor = interceptorList.get(++currentInterceptorIndex);
return interceptor.invoke(this);
}
protected Object invokeOriginal() throws Throwable{
return ReflectionUtils.invokeMethodUseReflection(target,method,arguments);
}
}
複製程式碼
在實際的運用中,我們的方法很可能被多個方法的攔截器所增強。所以我們,使用了一個list來儲存所有的攔截器。所以我們需要遞迴的去增加攔截器。當處理完了所有的攔截器之後,才會真正呼叫呼叫被增強的方法。我們可以認為,前文所述的動態的織入程式碼就發生在這裡。
public class CglibMethodInvocation extends ReflectioveMethodeInvocation {
private MethodProxy methodProxy;
public CglibMethodInvocation(Object proxy, Object target, Method method, Object[] arguments, List<AopMethodInterceptor> interceptorList, MethodProxy methodProxy) {
super(proxy, target, method, arguments, interceptorList);
this.methodProxy = methodProxy;
}
@Override
protected Object invokeOriginal() throws Throwable {
return methodProxy.invoke(target,arguments);
}
}
複製程式碼
CglibMethodInvocation
只是重寫了 invokeOriginal
方法。使用代理類來呼叫被增強的方法。
advisor
這個包裡面都是一些描述切面的資料結構,我們講解兩個重要的。
@Data
public class Advisor {
//幹什麼
private Advice advice;
//在哪裡
private Pointcut pointcut;
}
複製程式碼
如上文所說,advisor 描述了在哪裡,幹什麼。
@Data
public class AdvisedSupport extends Advisor {
//目標物件
private TargetSource targetSource;
//攔截器列表
private List<AopMethodInterceptor> list = new LinkedList<>();
public void addAopMethodInterceptor(AopMethodInterceptor interceptor){
list.add(interceptor);
}
public void addAopMethodInterceptors(List<AopMethodInterceptor> interceptors){
list.addAll(interceptors);
}
}
複製程式碼
這個AdvisedSupport
就是 我們Aop框架能夠理解的資料結構,這個時候問題就變成了--對於哪個目標,增加哪些攔截器。
core
有了上面的準備,我們就開始講解核心邏輯了。
@Data
public class CglibAopProxy implements AopProxy{
private AdvisedSupport advised;
private Object[] constructorArgs;
private Class<?>[] constructorArgTypes;
public CglibAopProxy(AdvisedSupport config){
this.advised = config;
}
@Override
public Object getProxy() {
return getProxy(null);
}
@Override
public Object getProxy(ClassLoader classLoader) {
Class<?> rootClass = advised.getTargetSource().getTagetClass();
if(classLoader == null){
classLoader = ClassUtils.getDefultClassLoader();
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(rootClass.getSuperclass());
//增加攔截器的核心方法
Callback callbacks = getCallBack(advised);
enhancer.setCallback(callbacks);
enhancer.setClassLoader(classLoader);
if(constructorArgs != null && constructorArgs.length > 0){
return enhancer.create(constructorArgTypes,constructorArgs);
}
return enhancer.create();
}
private Callback getCallBack(AdvisedSupport advised) {
return new DynamicAdvisedIcnterceptor(advised.getList(),advised.getTargetSource());
}
}
複製程式碼
CglibAopProxy
就是我們代理物件生成的核心方法。使用 cglib 生成代理類。我們可以與之前ioc框架的程式碼。比較發現區別就在於:
Callback callbacks = getCallBack(advised);
enhancer.setCallback(callbacks);
複製程式碼
callback與之前不同了,而是寫了一個getCallback()
的方法,我們就來看看 getCallback 裡面的 DynamicAdvisedIcnterceptor
到底幹了啥。
篇幅問題,這裡不會介紹 cglib 的使用,對於callback的作用,不理解的同學需要自行學習。
public class DynamicAdvisedInterceptor implements MethodInterceptor{
protected final List<AopMethodInterceptor> interceptorList;
protected final TargetSource targetSource;
public DynamicAdvisedInterceptor(List<AopMethodInterceptor> interceptorList, TargetSource targetSource) {
this.interceptorList = interceptorList;
this.targetSource = targetSource;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
MethodInvocation invocation = new CglibMethodInvocation(obj,targetSource.getTagetObject(),method, args,interceptorList,proxy);
return invocation.proceed();
}
}
複製程式碼
這裡需要注意,DynamicAdvisedInterceptor
這個類實現的 MethodInterceptor 是 gclib的介面,並非我們之前的 AopMethodInterceptor。
我們近距離觀察 intercept 這個方法我們看到:
MethodInvocation invocation = new CglibMethodInvocation(obj,targetSource.getTagetObject(),method, args,interceptorList,proxy);
複製程式碼
通過這行程式碼,我們的整個邏輯終於連起來了。也就是這個動態的攔截器,把我們通過 CglibMethodInvocation
織入了增強程式碼的方法,委託給了 cglib 來生成代理物件。
至此我們的 Aop 的核心功能就實現了。
AopBeanFactoryImpl
public class AopBeanFactoryImpl extends BeanFactoryImpl{
private static final ConcurrentHashMap<String,AopBeanDefinition> aopBeanDefinitionMap = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String,Object> aopBeanMap = new ConcurrentHashMap<>();
@Override
public Object getBean(String name) throws Exception {
Object aopBean = aopBeanMap.get(name);
if(aopBean != null){
return aopBean;
}
if(aopBeanDefinitionMap.containsKey(name)){
AopBeanDefinition aopBeanDefinition = aopBeanDefinitionMap.get(name);
AdvisedSupport advisedSupport = getAdvisedSupport(aopBeanDefinition);
aopBean = new CglibAopProxy(advisedSupport).getProxy();
aopBeanMap.put(name,aopBean);
return aopBean;
}
return super.getBean(name);
}
protected void registerBean(String name, AopBeanDefinition aopBeanDefinition){
aopBeanDefinitionMap.put(name,aopBeanDefinition);
}
private AdvisedSupport getAdvisedSupport(AopBeanDefinition aopBeanDefinition) throws Exception {
AdvisedSupport advisedSupport = new AdvisedSupport();
List<String> interceptorNames = aopBeanDefinition.getInterceptorNames();
if(interceptorNames != null && !interceptorNames.isEmpty()){
for (String interceptorName : interceptorNames) {
Advice advice = (Advice) getBean(interceptorName);
Advisor advisor = new Advisor();
advisor.setAdvice(advice);
if(advice instanceof BeforeMethodAdvice){
AopMethodInterceptor interceptor = BeforeMethodAdviceAdapter.getInstants().getInterceptor(advisor);
advisedSupport.addAopMethodInterceptor(interceptor);
}
if(advice instanceof AfterRunningAdvice){
AopMethodInterceptor interceptor = AfterRunningAdviceAdapter.getInstants().getInterceptor(advisor);
advisedSupport.addAopMethodInterceptor(interceptor);
}
}
}
TargetSource targetSource = new TargetSource();
Object object = getBean(aopBeanDefinition.getTarget());
targetSource.setTagetClass(object.getClass());
targetSource.setTagetObject(object);
advisedSupport.setTargetSource(targetSource);
return advisedSupport;
}
}
複製程式碼
AopBeanFactoryImpl
是我們產生代理物件的工廠類,繼承了上一講我們實現的 IoC 容器的BeanFactoryImpl。重寫了 getBean方法,如果是一個切面代理類,我們使用Aop框架生成代理類,如果是普通的物件,我們就用原來的IoC容器進行依賴注入。
getAdvisedSupport
就是獲取 Aop 框架認識的資料結構。
剩下沒有講到的類都比較簡單,大家看原始碼就行。與核心邏輯無關。
寫個方法測試一下
我們需要統計一個方法的執行時間。面對這個需求我們怎麼做?
public class StartTimeBeforeMethod implements BeforeMethodAdvice{
@Override
public void before(Method method, Object[] args, Object target) {
long startTime = System.currentTimeMillis();
System.out.println("開始計時");
ThreadLocalUtils.set(startTime);
}
}
複製程式碼
public class EndTimeAfterMethod implements AfterRunningAdvice {
@Override
public Object after(Object returnVal, Method method, Object[] args, Object target) {
long endTime = System.currentTimeMillis();
long startTime = ThreadLocalUtils.get();
ThreadLocalUtils.remove();
System.out.println("方法耗時:" + (endTime - startTime) + "ms");
return returnVal;
}
}
複製程式碼
方法開始前,記錄時間,儲存到 ThredLocal裡面,方法結束記錄時間,列印時間差。完成統計。
目標類:
public class TestService {
public void testMethod() throws InterruptedException {
System.out.println("this is a test method");
Thread.sleep(1000);
}
}
複製程式碼
配置檔案:
[
{
"name":"beforeMethod",
"className":"com.xilidou.framework.aop.test.StartTimeBeforeMethod"
},
{
"name":"afterMethod",
"className":"com.xilidou.framework.aop.test.EndTimeAfterMethod"
},
{
"name":"testService",
"className":"com.xilidou.framework.aop.test.TestService"
},
{
"name":"testServiceProxy",
"className":"com.xilidou.framework.aop.core.ProxyFactoryBean",
"target":"testService",
"interceptorNames":[
"beforeMethod",
"afterMethod"
]
}
]
複製程式碼
測試類:
public class MainTest {
public static void main(String[] args) throws Exception {
AopApplictionContext aopApplictionContext = new AopApplictionContext("application.json");
aopApplictionContext.init();
TestService testService = (TestService) aopApplictionContext.getBean("testServiceProxy");
testService.testMethod();
}
}
複製程式碼
最終我們的執行結果:
開始計時
this is a test method
方法耗時:1015ms
Process finished with exit code 0
複製程式碼
至此 Aop 框架完成。
後記
Spring 的兩大核心特性 IoC 與 Aop 兩大特性就講解完了,希望大家通過我寫的兩篇文章能夠深入理解兩個特性。
Spring的原始碼實在是複雜,閱讀起來常常給人極大的挫敗感,但是隻要能夠堅持,並採用一些行之有效的方法。還是能夠理解Spring的程式碼。並且從中汲取營養。
下一篇文章,我會給大家講講閱讀開原始碼的一些方法和我自己的體會,敬請期待。
最後
github:github.com/diaozxin007…