簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

Guo_1_9發表於2019-03-03

一個簡易版的Spring框架

功能

  • 支援singleton型別的bean,包括初始化、屬性注入、以及依賴bean注入。
  • 可從xml中讀取配置。
  • 可以使用Aspectj的方式進行AOP編寫,支援介面和類代理。

說明

如果你有幸能看到

  • 1、本文簡易Spring框架參考Github,程式碼註釋參考Github.
  • 2、所有權歸原作者,在這裡自己只是臨摹,參考註釋還原過程。不懂的可以看作者的視訊。
  • 3、大家一起努力,一起學習,有興趣的也可以看下我的Github。上傳了Spring原始碼。大佬可以看看。
  • 4、看本文之前希望你有一份Spring原始碼。對照著找你想要的介面和類。加深印象。
  • 5、本文只為自己以後複習用,如果不對還請諒解。

tiny-spring是為了學習Spring的而開發的,可以認為是一個Spring的精簡版。Spring的程式碼很多,層次複雜,閱讀起來費勁。我嘗試從使用功能的角度出發,參考Spring的實現,一步一步構建,最終完成一個精簡版的Spring。 有人把程式設計師與畫家做比較,畫家有門基本功叫臨摹,tiny-spring可以算是一個程式的臨摹版本-從自己的需求出發,進行程式設計,同時對著名專案進行參考。

第二部分:AOP及實現

AOP分為配置(Pointcut,Advice),織入(Weave)兩部分工作,當然還有一部分是將AOP整合到整個容器的生命週期中。

7.step7-使用JDK動態代理實現AOP織入

織入(weave)相對簡單,我們先從它開始。Spring AOP的織入點是AopProxy,它包含一個方法Object getProxy()來獲取代理後的物件。

在Spring AOP中,我覺得最重要的兩個角色,就是我們熟悉的MethodInterceptor和MethodInvocation(這兩個角色都是AOP聯盟的標準),它們分別對應AOP中兩個基本角色:Advice和Joinpoint。Advice定義了在切點指定的邏輯,而Joinpoint則代表切點

切點通知器相關介面

簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

public interface MethodInterceptor extends Interceptor {

    Object invoke(MethodInvocation invocation) throws Throwable;
}
複製程式碼

Spring的AOP只支援方法級別的呼叫,所以其實在AopProxy裡,我們只需要將MethodInterceptor放入物件的方法呼叫即可。

使用jdk動態代理

簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

public class JdkDynamicAopProxy extends AbstractAopProxy implements InvocationHandler {

    public JdkDynamicAopProxy(AdvisedSupport advised ) {
        super(advised);
    }

    @Override
    public Object getProxy() {
        return Proxy.newProxyInstance(getClass().getClassLoader(),advised.getTargetSource().getInterfaces(),this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //提取攔截的方法
        MethodInterceptor methodInterceptor = (MethodInterceptor) advised.getMethodInterceptor();
        //比較傳入的方法和物件的方法是否一致,如果一致則呼叫傳入的方法,
        if (advised.getMethodMatcher() != null
                && advised.getMethodMatcher().matches(method, advised.getTargetSource().getTarget().getClass())) {
            //這裡應該是先呼叫攔截的方法,然後呼叫原始物件的方法。但是一般括號裡的東西不是優先嗎?括號裡面好像就只有賦值操作而已。
            return methodInterceptor.invoke(new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(),method, args));
        } else {
            return method.invoke(advised.getTargetSource().getTarget(), args);
        }
    }

複製程式碼

我們稱被代理物件為TargetSource,而AdvisedSupport就是儲存TargetSource和MethodInterceptor的後設資料物件。這一步我們先實現一個基於JDK動態代理的JdkDynamicAopProxy,它可以對介面進行代理。於是我們就有了基本的織入功能。

@Test
public void testInterceptor() throws Exception {
  // --------- helloWorldService without AOP
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext("tinyioc.xml");
  HelloWorldService helloWorldService = (HelloWorldService) applicationContext.getBean("helloWorldService");
  helloWorldService.helloWorld();

  // --------- helloWorldService with AOP
  // 1. 設定被代理物件(Joinpoint)
  AdvisedSupport advisedSupport = new AdvisedSupport();
  TargetSource targetSource = new TargetSource(helloWorldService, HelloWorldServiceImpl.class,
      HelloWorldService.class);
  advisedSupport.setTargetSource(targetSource);

  // 2. 設定攔截器(Advice)
  TimerInterceptor timerInterceptor = new TimerInterceptor();
  advisedSupport.setMethodInterceptor(timerInterceptor);

  // 3. 建立代理(Proxy)
  JdkDynamicAopProxy jdkDynamicAopProxy = new JdkDynamicAopProxy(advisedSupport);
  HelloWorldService helloWorldServiceProxy = (HelloWorldService) jdkDynamicAopProxy.getProxy();

  // 4. 基於AOP的呼叫
  helloWorldServiceProxy.helloWorld();

}
複製程式碼

8.step8-使用AspectJ管理切面

git checkout step-8-invite-pointcut-and-aspectj
複製程式碼

完成了織入之後,我們要考慮另外一個問題:對什麼類以及什麼方法進行AOP?對於“在哪切”這一問題的定義,我們又叫做“Pointcut”。Spring中關於Pointcut包含兩個角色:ClassFilterMethodMatcher,分別是對類和方法做匹配。Pointcut有很多種定義方法,例如類名匹配、正則匹配等,但是應用比較廣泛的應該是和AspectJ表示式的方式。

AspectJ是一個“對Java的AOP增強”。它最早是其實是一門語言,我們跟寫Java程式碼一樣寫它,然後靜態編譯之後,就有了AOP的功能。下面是一段AspectJ程式碼:

aspect PointObserving {
    private Vector Point.observers = new Vector();

    public static void addObserver(Point p, Screen s) {
        p.observers.add(s);
    }
    public static void removeObserver(Point p, Screen s) {
        p.observers.remove(s);
    }
    ...
}
複製程式碼

這種方式無疑太重了,為了AOP,還要適應一種語言?所以現在使用也不多,但是它的Pointcut表示式被Spring借鑑了過來。於是我們實現了一個AspectJExpressionPointcut

    @Test
    public void testMethodInterceptor() throws Exception {
        String expression = "execution(* us.codecraft.tinyioc.*.*(..))";
        AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
        aspectJExpressionPointcut.setExpression(expression);
        boolean matches = aspectJExpressionPointcut.getMethodMatcher().matches(HelloWorldServiceImpl.class.getDeclaredMethod("helloWorld"),HelloWorldServiceImpl.class);
        Assert.assertTrue(matches);
    }
複製程式碼
public class AspectJExpressionPointcutTest {

    @Test
    public void testClassFilter() throws Exception {
        String expression = "execution(* us.codecraft.tinyioc.*.*(..))";
        AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
        aspectJExpressionPointcut.setExpression(expression);
        boolean matches = aspectJExpressionPointcut.getClassFilter().matches(HelloWorldService.class);
        Assert.assertTrue(matches);
    }

    @Test
    public void testMethodInterceptor() throws Exception {
        String expression = "execution(* us.codecraft.tinyioc.*.*(..))";
        AspectJExpressionPointcut aspectJExpressionPointcut = new AspectJExpressionPointcut();
        aspectJExpressionPointcut.setExpression(expression);
        boolean matches = aspectJExpressionPointcut.getMethodMatcher().matches(HelloWorldServiceImpl.class.getDeclaredMethod("helloWorld"),HelloWorldServiceImpl.class);
        Assert.assertTrue(matches);
    }
複製程式碼

9.step9-將AOP融入Bean的建立過程

git checkout step-9-auto-create-aop-proxy
複製程式碼

萬事俱備,只欠東風!現在我們有了Pointcut和Weave技術,一個AOP已經算是完成了,但是它還沒有結合到Spring中去。怎麼進行結合呢?Spring給了一個巧妙的答案:使用BeanPostProcessor

BeanPostProcessor是BeanFactory提供的,在Bean初始化過程中進行擴充套件的介面。只要你的Bean實現了BeanPostProcessor介面,那麼Spring在初始化時,會優先找到它們,並且在Bean的初始化過程中,呼叫這個介面,從而實現對BeanFactory核心無侵入的擴充套件。

那麼我們的AOP是怎麼實現的呢?我們知道,在AOP的xml配置中,我們會寫這樣一句話:

<aop:aspectj-autoproxy/>
複製程式碼

它其實相當於:

<bean id="autoProxyCreator" class="org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator"></bean>
複製程式碼

AspectJAwareAdvisorAutoProxyCreator就是AspectJ方式實現織入的核心。它其實是一個BeanPostProcessor。在這裡它會掃描所有Pointcut,並對bean做織入。

為了簡化xml配置,我在tiny-spring中直接使用Bean的方式,而不是用aop字首進行配置:

    <bean id="autoProxyCreator" class="us.codecraft.tinyioc.aop.AspectJAwareAdvisorAutoProxyCreator"></bean>

    <bean id="timeInterceptor" class="us.codecraft.tinyioc.aop.TimerInterceptor"></bean>

    <bean id="aspectjAspect" class="us.codecraft.tinyioc.aop.AspectJExpressionPointcutAdvisor">
        <property name="advice" ref="timeInterceptor"></property>
        <property name="expression" value="execution(* us.codecraft.tinyioc.*.*(..))"></property>
    </bean>
複製程式碼

TimerInterceptor實現了MethodInterceptor(實際上Spring中還有Advice這樣一個角色,為了簡單,就直接用MethodInterceptor了)。

至此,一個AOP基本完工。

10.step10-使用CGLib進行類的織入

git checkout step-10-invite-cglib-and-aopproxy-factory
複製程式碼

前面的JDK動態代理只能對介面進行代理,對於類則無能為力。這裡我們需要一些位元組碼操作技術。這方面大概有幾種選擇:ASMCGLibjavassist,後兩者是對ASM的封裝。Spring中使用了CGLib。

在這一步,我們還要定義一個工廠類ProxyFactory,用於根據TargetSource型別自動建立代理,這樣就需要在呼叫者程式碼中去進行判斷。

另外我們實現了Cglib2AopProxy,使用方式和JdkDynamicAopProxy是完全相同的。

public class Cglib2AopProxy extends AbstractAopProxy {

	public Cglib2AopProxy(AdvisedSupport advised) {
		super(advised);
	}

	//通過cglib類庫建立了一個代理類的例項
	@Override
	public Object getProxy() {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(advised.getTargetSource().getTargetClass());
		enhancer.setInterfaces(advised.getTargetSource().getInterfaces());
		//設定代理類的通知方法,相當於設定攔截器方法
		enhancer.setCallback(new DynamicAdvisedInterceptor(advised));
		Object enhanced = enhancer.create();
		return enhanced;
	}

	//方法攔截器
	private static class DynamicAdvisedInterceptor implements MethodInterceptor {

		private AdvisedSupport advised;

		private org.aopalliance.intercept.MethodInterceptor delegateMethodInterceptor;

		private DynamicAdvisedInterceptor(AdvisedSupport advised) {
			this.advised = advised;
			this.delegateMethodInterceptor = advised.getMethodInterceptor();
		}

		//呼叫代理類的方法(代理類與原始類是父子關係,還有一種是兄弟關係,呼叫實質是呼叫原始類的方法)
		@Override
		public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
			if (advised.getMethodMatcher() == null
					|| advised.getMethodMatcher().matches(method, advised.getTargetSource().getTargetClass())) {
				//這裡也應該是先呼叫攔截方法,然後呼叫原始物件的方法
				return delegateMethodInterceptor.invoke(new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, args, proxy));
			}
			return new CglibMethodInvocation(advised.getTargetSource().getTarget(), method, args, proxy).proceed();
		}
	}

	private static class CglibMethodInvocation extends ReflectiveMethodInvocation {

		private final MethodProxy methodProxy;

		public CglibMethodInvocation(Object target, Method method, Object[] args, MethodProxy methodProxy) {
			super(target, method, args);
			this.methodProxy = methodProxy;
		}

		@Override
		public Object proceed() throws Throwable {
			return this.methodProxy.invoke(this.target, this.arguments);
		}
	}

複製程式碼

有一個細節是CGLib建立的代理是沒有注入屬性的, Spring的解決方式是:CGLib僅作代理,任何屬性都儲存在TargetSource中,使用MethodInterceptor=>TargetSource的方式進行呼叫。

至此,AOP部分完工。

1、通過AspectJ表示式處理AOP的主要類和關係

簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

2、使用cglib動態代理

簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

3、兩種動態代理

簡易版的Spring框架之AOP簡單實現(對我來說不簡單啊)

一些總結:

  • 一層一層的封裝,合理運用組合、繼承類或介面來賦予、增強類的相應功能

  • 介面的運用

    • 通過暴露介面方法來進行非侵入式嵌入(例如:暴露BeanPostProcessor介面,實現該介面的類會優先於普通bean的例項化並可在bean例項化前對bean做一些初始化操作,例:aop織入)
    • BeanFactoryAware介面暴露了獲取beanFactory的能力,繼承該介面的類擁有操作beanFactory的能力,也就能具體的操作bean了。
  • 模板方法模式以及hook方法的應用:   例如: 在AbstractBeanFactory中規範了bean的載入,例項化,初始化,獲取的過程。AutowireCapableBeanFactory裡實現了hook方法(applyPropertyValues方法),該方法在AbstractBeanFactory#initializeBean方法中呼叫,AbstractBeanFactory中有預設的hook方法空實現。

  • 工廠方法模式的應用:例如:BeanFactory#getBean,由子類決定怎樣去獲取bean並在獲取時進行相關操作。工廠方法把例項化推遲到子類。

  • 外觀(門面)模式的運用:ClassPathXmlApplicationContext對 Resouce 、 BeanFactory、BeanDefinition 進行了功能的封裝,解決 根據地址獲取資源通過 IoC 容器註冊bean定義並例項化,初始化bean的問題,並提供簡單運用他們的方法。

  • 代理模式的運用

    • 通過jdk的動態代理:jdk的動態代理是基於介面的,必須實現了某一個或多個任意介面才可以被代理,並且只有這些介面中的方法會被代理。
    • 通過cglib動態代理:cglib是針對類來實現代理的,他的原理是對指定的目標類生成一個子類,並覆蓋其中的方法實現增強,但因為採用的是繼承,所以不能對final修飾的類進行代理。
  • 單例模式的運用

    • tiny-spring預設是單例bean,在AbstractApplicationContext#refresh裡註冊bean定義,初始化後,預設用單例形式例項化bean:preInstantiateSingletons方法裡獲取beanDefinition的name後通過getBean(name)方法例項化bean,下次再getBean(name)時會先檢查該name的beanDefinition裡的bean是否已經例項化,如果已經例項化了,則返回那個bean的引用而不是再例項化一個新的bean返回
    • 標準單例模式中一般的實現方式是:第一次通過getInstance(雙重檢查)例項化該類的物件並儲存,下次再getInstance時返回該物件。
  • 策略模式:   這裡有個想法,看ClassPathXMLApplicationContext構造方法可以知道是預設用自動裝配的策略,在這裡可以另外自己寫個類繼承AbstractBeanFactory,重寫applyPropertyValues方法實現裝配策略,在初始化的時候就可以選擇不同的裝配策略了。

相關文章