Spring原始碼分析(六)SpringAOP例項及標籤的解析

清幽之地發表於2019-03-03

一、前言

Spring的IOC和AOP稱之為Spring框架的兩個核心。AOP是什麼?AOP原理是什麼?本章節開始,我們就來看看SpringAOP到底是怎麼玩轉起來的?

二、AOP是什麼?

1、定義

Aspect Oriented Programming,面向切面程式設計,是一種程式設計範例,旨在通過分離橫切關注點來增加模組性,它通過在不修改程式碼本身的情況下向現有程式碼新增其他行為來實現。動態的將程式碼切入到類的指定方法或指定位置上的程式設計思想,就是面向切面程式設計。

2、使用

在系統中,肯定存在一些公共邏輯模組。比如日誌的記錄,事務的管理,請求的校驗等。如果把這種邏輯模組的程式碼收到寫到業務模組中,程式碼重複度就非常之高。這還不是唯一的問題,關鍵如果公共邏輯模組的程式碼要修改,必須要全部修改。這個根本不符合碼農的科學發展觀。AOP,可以幫助我們解決這些問題。

3、實現

AOP本身並不能解決這些問題,AOP就是一種思想,而解決問題依靠的是AOP具體的實現,也就是我們本章節所說的Spring AOP。不過,值得注意的是,在Spring2.0之後,開始整合aspectj。所以,我們所說的Spring AOP,其實就是Spring加Aspectj這種方式。

4、概念性知識

要熟悉Spring AOP,裡面有些概念一定要先搞搞清楚才行。

  • Aspect 切面,將橫切關注點設計為獨立可重用的物件,這些物件稱為切面。實際上就是一些功能增強的類或者物件的代表,比如:日誌管理、事務管理、異常控制等。

  • Joinpoint 連線點,切面在應用程式執行時加入物件的業務流程中的特定點,稱為連線點。它用來定義在目標程式的哪裡通過AOP加入新的邏輯。通俗講,就是對應的具體的被代理的方法 ,比如saveUser()。Joinpoint跟我們具體的被代理的方法一一對應

  • Pointcut 切點,匹配連線點的斷言。通知和一個切入點表示式關聯,並在滿足這個切入點的連線點上執行。它是joinpoint的集合。

  • Advice 通知/增強,在切面的某個特定的連線點上執行的動作。可以理解為它是一段程式程式碼,在代理類上的上面或者下面增加一些程式碼來實現增強。比如事務管理AOP,通知/增強對應的就是開啟事務、關閉事務這些具體程式碼上的操作。

  • Advisor Advice和Pointcut組成的獨立的單元,用來定義只有一個通知和一個切入點的切面。再通俗點來說,它是將Advice注入到程式中的Pointcut位置。Spring中的事務管理使用的就是advisor。

  • Introduction 引入,通過引入,可以在一個物件中加入新的方法和屬性,而不用修改它的程式。這種方式很少用,基本也不太推薦用。自己定義的通知必須要實現MethodInterceptor。

三、例項

瞭解到上面的知識後,我們通過XML的配置方式具體來看一下Spring AOP的應用。

首先,定義一個切面的類。

public class UserAspect {

	public void beforeAdvice() {
		System.out.println("前置通知");
	}
	public void afterAdvice() {
		System.out.println("後置通知");
	}
	public void afterReturnAdvice() {
		System.out.println("返回通知");
	}
	public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {	
		System.out.println("環繞通知之前");
		Object result = joinPoint.proceed();
		System.out.println("環繞通知之後");
		return result;
	}
}
複製程式碼

其次,在Spring配置檔案中先將這個類註冊成Bean。再通過AOP的標籤關聯到一起。

<bean id="userAspect" class="com.viewscenes.netsupervisor.aspect.UserAspect"></bean>
	
<aop:config>
	<aop:aspect id="userAspect" ref="userAspect">
		<aop:pointcut id="userPointcut" expression="(execution(* 
                              com.viewscenes.netsupervisor.service..*.*(..)))" />
		<aop:before method="beforeAdvice" pointcut-ref="userPointcut"/>
		<aop:after method="afterAdvice"  pointcut-ref="userPointcut"/>
		<aop:after-returning method="afterReturnAdvice" pointcut-ref="userPointcut"/>
		<aop:around method="aroundAdvice" pointcut-ref="userPointcut"/> 
	</aop:aspect>
</aop:config>
複製程式碼

最後,我們通過呼叫UserService中的方法來測試一下。

前置通知
環繞通知之前
----------根據ID刪除使用者資訊------------
環繞通知之後
返回通知
後置通知
複製程式碼

四、XML標籤的解析

不知諸位可否還有印象,Spring是怎麼解析配置檔案中的標籤的呢?如果不記得,可以到Spring原始碼分析(一)Spring的初始化和XML解析回顧一下。

這裡,我們直接來到ConfigBeanDefinitionParser.parse()方法。它位於org.springframework.aop.config包。大概可以分為兩個步驟,註冊入口類和解析子節點。

1、註冊入口類

parse方法的開始就註冊了一個類,AspectJAwareAdvisorAutoProxyCreator。這個類相當重要,它是AOP的入口類。註冊的過程就是把它封裝成BeanDefinition物件,新增到beanDefinitionNames容器中。這個容器,我們已經很熟悉了,就是迴圈它來進行例項化和依賴注入。

//cls就是AspectJAwareAdvisorAutoProxyCreator.class
private static BeanDefinition registerOrEscalateApcAsRequired(Class<?> cls, 
								BeanDefinitionRegistry registry, Object source) {
	RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
	beanDefinition.setSource(source);
	beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
	beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
	
	//註冊beanDefinition 將beanName加入到beanDefinitionNames容器中
	registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
	return beanDefinition;
}
複製程式碼

2、解析子節點

接下來是解析配置檔案標籤的地方,獲取<aop:config>下的子標籤。它的子標籤只有三類:<aop:pointcut>、<aop:advisor>、<aop:aspect>。下面的原始碼也正對應這三種型別。

List<Element> childElts = DomUtils.getChildElements(element);
for (Element elt: childElts) {
	String localName = parserContext.getDelegate().getLocalName(elt);
	if (POINTCUT.equals(localName)) {
		parsePointcut(elt, parserContext);
	}
	else if (ADVISOR.equals(localName)) {
		parseAdvisor(elt, parserContext);
	}
	else if (ASPECT.equals(localName)) {
		parseAspect(elt, parserContext);
	}
}
複製程式碼

pointcut的解析

pointcut解析其實很簡單,把id和expression拿到,封裝成BeanDefinition物件,它的類是AspectJExpressionPointcut,把表示式放入beanDefinition物件的propertyValues屬性,最後同樣是註冊到beanDefinitionNames容器中。

private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserContext parserContext) {
	String id = pointcutElement.getAttribute(ID);
	String expression = pointcutElement.getAttribute(EXPRESSION);
	AbstractBeanDefinition pointcutDefinition = null;
	try {
		pointcutDefinition = createPointcutDefinition(expression);
		String pointcutBeanName = id;
		if (StringUtils.hasText(pointcutBeanName)) {
			//註冊到beanDefinitionNames容器,id為beanName
			parserContext.getRegistry().registerBeanDefinition(pointcutBeanName, pointcutDefinition);
		}
	}
	return pointcutDefinition;
}

protected AbstractBeanDefinition createPointcutDefinition(String expression) {
	RootBeanDefinition beanDefinition = new RootBeanDefinition(AspectJExpressionPointcut.class);
	beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
	beanDefinition.setSynthetic(true);
	beanDefinition.getPropertyValues().add(EXPRESSION, expression);
	return beanDefinition;
}
複製程式碼

aspect的解析

aspect是一個切面。切面裡面包含切入點和通知。引入型別先略過不表。

  • advice

獲取aspect節點下的所有子節點,先過濾advice節點。然後解析生成AspectJPointcutAdvisor類的BeanDefinition物件。

//獲取aspect節點的子節點 
NodeList nodeList = aspectElement.getChildNodes();
boolean adviceFoundAlready = false;
for (int i = 0; i < nodeList.getLength(); i++) {
	Node node = nodeList.item(i);
	//判斷是不是advice節點。
	if (isAdviceNode(node, parserContext)) {
		if (!adviceFoundAlready) {
			adviceFoundAlready = true;
			//aspectName就切面的ref,Bean的名字
			beanReferences.add(new RuntimeBeanReference(aspectName));
		}
		//解析advice 生成AspectJPointcutAdvisor類的BeanDefinition物件。
		AbstractBeanDefinition advisorDefinition = parseAdvice(
				aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences);
		beanDefinitions.add(advisorDefinition);
	}
}

private boolean isAdviceNode(Node aNode, ParserContext parserContext) {
	String name = parserContext.getDelegate().getLocalName(aNode);
	return (BEFORE.equals(name) || AFTER.equals(name) || 
		AFTER_RETURNING_ELEMENT.equals(name) ||
		AFTER_THROWING_ELEMENT.equals(name) || AROUND.equals(name));
}
複製程式碼

parseAdvice方法註冊很多類,最後串聯到一塊來,一個一個來看。

首先,建立了方法工廠bean。註冊了MethodLocatingFactoryBean類,往propertyValues中新增了兩個屬性,targetBeanName切面的Bean、methodName通知的方法名。

RootBeanDefinition methodDefinition = new RootBeanDefinition(MethodLocatingFactoryBean.class);
//aspectName切面類的Bean  methodName方法名稱 比如before
methodDefinition.getPropertyValues().add("targetBeanName", aspectName);
methodDefinition.getPropertyValues().add("methodName", adviceElement.getAttribute("method"));
methodDefinition.setSynthetic(true);
複製程式碼

然後,建立例項工廠的定義。註冊了SimpleBeanFactoryAwareAspectInstanceFactory類,這個類實現了BeanFactoryAware介面。這樣的話,在例項化的時候會呼叫到setBeanFactory方法,可以拿到BeanFactory。有個getAspectInstance方法,根據切面名字就可以拿到切面類的例項。

//註冊SimpleBeanFactoryAwareAspectInstanceFactory例項的BeanDefinition
RootBeanDefinition aspectFactoryDef =
		new RootBeanDefinition(SimpleBeanFactoryAwareAspectInstanceFactory.class);
aspectFactoryDef.getPropertyValues().add("aspectBeanName", aspectName);


//類的屬性和方法
public class SimpleBeanFactoryAwareAspectInstanceFactory implements 
								AspectInstanceFactory, BeanFactoryAware {
	
	private String aspectBeanName;
	private BeanFactory beanFactory;
	
	public void setAspectBeanName(String aspectBeanName) {
		this.aspectBeanName = aspectBeanName;
	}

	public void setBeanFactory(BeanFactory beanFactory) {
		this.beanFactory = beanFactory;
		if (!StringUtils.hasText(this.aspectBeanName)) {
			throw new IllegalArgumentException("'aspectBeanName' is required");
		}
	}
	public Object getAspectInstance() {
		return this.beanFactory.getBean(this.aspectBeanName);
	}
}
複製程式碼

其次,註冊切入點。它把上面這兩個BeanDefinition當做引數傳了過去,最後放入新建的BeanDefinition物件中。這個新建的BeanDefinition物件,是根據advice型別而建立的,當然了,也是五個型別,對應五個類的例項。下面還有三個步驟:設定propertyValues、解析advcie裡的pointcut屬性、設定bean的引數列表。

private AbstractBeanDefinition createAdviceDefinition(
	Element adviceElement, ParserContext parserContext, String aspectName, int order,
			RootBeanDefinition methodDef, RootBeanDefinition aspectFactoryDef,
			List<BeanDefinition> beanDefinitions, List<BeanReference> beanReferences) {

	//getAdviceClass 根據advice的型別建立不同型別的BeanDefinition
	//BEFORE前置通知 					AspectJMethodBeforeAdvice.class
	//AFTER後置通知 					AspectJAfterAdvice.class
	//AFTER_RETURNING_ELEMENT返回後通知 AspectJAfterReturningAdvice.class
	//AFTER_THROWING_ELEMENT異常通知  	AspectJAfterThrowingAdvice.class
	//AROUND環繞通知					AspectJAroundAdvice.class

	RootBeanDefinition adviceDefinition = new RootBeanDefinition(
							   getAdviceClass(adviceElement, parserContext));
	adviceDefinition.setSource(parserContext.extractSource(adviceElement));
	//1、設定propertyValues
	adviceDefinition.getPropertyValues().add(ASPECT_NAME_PROPERTY, aspectName);
	adviceDefinition.getPropertyValues().add(DECLARATION_ORDER_PROPERTY, order);

	//2、解析advcie裡的pointcut屬性
	//pointcut分為兩種。一種是pointcut-ref引用型別,一種是pointcut表示式型別
	//如果是引用型別,返回字串  
	//如果是表示式型別,則建立AspectJExpressionPointcut型別的Bean,將表示式放入propertyValues屬性。
	Object pointcut = parsePointcutProperty(adviceElement, parserContext);
	if (pointcut instanceof BeanDefinition) {
		cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcut);
		beanDefinitions.add((BeanDefinition) pointcut);
	}
	else if (pointcut instanceof String) {
		RuntimeBeanReference pointcutRef = new RuntimeBeanReference((String) pointcut);
		cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef);
		beanReferences.add(pointcutRef);
	}

	//3、設定bean的引數列表。adviceDefinition物件有一個建構函式引數值,放入了三個屬性
	ConstructorArgumentValues cav = adviceDefinition.getConstructorArgumentValues();
	cav.addIndexedArgumentValue(METHOD_INDEX, methodDef);
	cav.addIndexedArgumentValue(POINTCUT_INDEX, pointcutRef);
	cav.addIndexedArgumentValue(ASPECT_INSTANCE_FACTORY_INDEX, aspectFactoryDef);
        
	return adviceDefinition;
}
複製程式碼

最後,配置advisor。建立AspectJPointcutAdvisor類例項的BeanDefinition物件,還是那個建構函式引數值,把上一步返回的adviceDefinition當做引數放入genericArgumentValues。

RootBeanDefinition advisorDefinition = new RootBeanDefinition(AspectJPointcutAdvisor.class);
advisorDefinition.setSource(parserContext.extractSource(adviceElement));
//建構函式引數值 adviceDef就是上一步返回的adviceDefinition
advisorDefinition.getConstructorArgumentValues().addGenericArgumentValue(adviceDef);
複製程式碼

最後的最後,註冊advisorDefinition到容器中並返回。

parserContext.getReaderContext().registerWithGeneratedName(advisorDefinition);
return advisorDefinition;
複製程式碼

一定要記得,這一系列操作都是在迴圈體裡完成的。所以,有幾個通知的型別,就會生成幾個advisorDefinition物件。處理完,新增到迴圈體開頭定義的List中。

  • pointcut

剛才在解析advice已經解析了pointcut,這裡又有一個呢?advice裡的pointcut是獨立使用的,只能作用於當前的advice。但是在aspect裡面也可以單獨定義pointcut,可以作用於所有的advice。解析過程是一樣的,不再贅述。

advisor的解析

advisor可以理解為是隻有一個通知和一個切入點的切面。它的解析也比較簡單。建立一個DefaultBeanFactoryPointcutAdvisor類例項的BeanDefinition的物件,把通知的BeanName和Order放入propertyValues,再把這個BeanDefinition物件註冊到容器中。然後解析pointcut,過程一樣。

相關文章