天天看點

Spring AOP架構使用ProxyBeanFactoryaop标簽@AspectJ注解

在Spring AspectJ LTW中分析如果在類加載期織入切面,本篇中學習如何在運作時動态的注入切面,這種方式在實際項目中使用比較多,在Spring AOP架構中提供了三種方式在程式運作時注入切面:

  • ProxyBeanFactory
  • aop标簽(Scheme風格)
  • @Aspect(元注解風格)

下面通過幾個簡單的例子示範如何通過這幾種方式來織入切面。

ProxyBeanFactory

這個例子非常簡單,給目标方法的調用前後各加一行日志。

編寫一個接口,這不是必須的,是否需要接口完全由項目的應用場景來定,看看需要做相應的可擴充性設計,Spring架構也不限制切面目标類必須要實作接口,這裡為了友善示範,定義了一個接口:

public interface IHelloWorldService {
	boolean sayHello(String param);
}
           

編寫一個切面目标,代碼非常簡單,實作上述接口,上面說過了實作接口不是必須的,隻會為了友善後面的示範:

public class HelloWorldService implements IHelloWorldService {
	public boolean sayHello(String param) {
		System.out.println(param + " say:Hello World!");
		return true;
	}
}
           

編寫一個通知類,需要直接或間接的實作Advisor或Advice接口,我寫的這個實作了MethodInterceptor接口:

public class LogInterceptor implements MethodInterceptor {

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
		Method method = arg0.getMethod();
		System.out.println(method.getName() + ":begin");
		Object retVal = arg0.proceed();
		System.out.println(method.getName() + ":end");
		return retVal;
	}

}
           

也可以通過實作MethodBeforeAdvice和AfterReturningAdvice接口實作同樣的功能下面的LogAdvice和上面的LogInterceptor在作用上基本上是一樣的:

public class LogAdvice implements MethodBeforeAdvice, AfterReturningAdvice {

	@Override
	public void afterReturning(Object returnValue, Method method,
			Object[] args, Object target) throws Throwable {
		System.out.println(method.getName() + ":end");
	}

	@Override
	public void before(Method method, Object[] args, Object target)
			throws Throwable {

		System.out.println(method.getName() + ":begin");
	}

}
           

配置通知bean和目标bean:

<bean id="logInterceptor" class="spring.aop.dw.proxyfb.LogInterceptor"></bean>
<bean id="helloWorldService" class="spring.aop.dw.proxyfb.HelloWorldService"></bean>
           

配置一個ProxyFactoryBean:

<bean class="org.springframework.aop.framework.ProxyFactoryBean"
	id="helloWorld">
	<property name="target" ref="helloWorldService"></property>
	<property name="interceptorNames">
		<value>logInterceptor</value>
	</property>
</bean>
           

target是目标bean,可以用targetName代理,interceptorNames是攔截器也即通知的bean名稱,可以使用通配符*字尾,同時也可以配置多個,上面這段配置也可以寫成下面這個樣子:

<bean class="org.springframework.aop.framework.ProxyFactoryBean"
	id="helloWorld">
	<property name="targetName" value="helloWorldService"></property>
	<property name="interceptorNames">
		<list>
			<value>log*</value>
		</list>
	</property>
</bean>
           

AOP的核心就是為目标bean生成一個AOP代理來替代目标bean,當目标bean實作了接口時(前提是架構能檢查到),架構會生成一個JDK動态代理,如果沒有實作接口則會生成一個CGLIB代理。ProxyFactoryBean還有個interfaces屬性用來設定目标bean所實作的接口,預設情況下架構會自動檢查目标bean所實作的接口,是以這裡即使不配置interfaces屬性,架構也能檢查出來目标bean實作了IHelloService接口會生成一個JDK動态代理,這裡還有一個autodetectInterfaces屬性,這個屬性的作用是是否自動檢查是否實作了接口,預設是true檢查,如果把它設定成了false,并且沒有設定interfaces屬性,那麼即使helloWorldService bean實作了接口,架構也會生成CGLIB代理,下面這段配置架構就會使用CGLIB代理技術:

<bean class="org.springframework.aop.framework.ProxyFactoryBean"
	id="helloWorld1">
	<property name="targetName" value="helloWorldService"></property>
	<property name="interceptorNames">
		<list>
			<value>logInterceptor</value>
		</list>
	</property>
	<property name="autodetectInterfaces" value="false"></property>
</bean>
           

測試一下:

IHelloWorldService helloWorld = (IHelloWorldService) context
		.getBean("helloWorld");
System.out.println(helloWorld.getClass());
helloWorld.sayHello("cyy");
           

輸出下面日志說明通知生效,而且擷取到的bean并不是目标bean的類型而是一個代理類型:

class $Proxy4
sayHello:begin
cyy say:Hello World!
sayHello:end
           

從使用上來講,通過ProxyFactoryBean來實作AOP并不是很友善,每個目标必須要配置一個ProxyFactoryBean,當被通知的目标bean數量非常多的時候配置量會非常大。下面來看看另外兩種方式,這兩種方式使用上就友善多了。

aop标簽

Spring提供了一個aop名稱空間,可以使用該名稱空間下的标簽實作aop。在介紹這種方式之前,首先需要了解一下切點表達式,正是因為切點表達式的存在,使得AOP的配置大大簡化。

切點表達式

切入點包含切入點訓示符和類型比對表達式

Spring AOP支援下面幾種切點訓示符:

execution:用于比對方法執行的連接配接點;

within:用于比對指定類型内的方法執行;

this:用于比對目前AOP代理對象類型的執行方法;注意是AOP代理對象的類型比對,這樣就可能包括引入接口也類型比對;

target:用于比對目前目标對象類型的執行方法;注意是目标對象的類型比對,這樣就不包括引入接口也類型比對;

args:用于比對目前執行的方法傳入的參數為指定類型的執行方法;

在切點表達式文法中有三個通配符:

* :比對任何數量字元;

.. :(兩個點)比對任何數量字元的重複,如在類型模式中比對任何數量子包;而在方法參數模式中比對任何數量參數。 

+ :比對指定類型的子類型;僅能作為字尾放在類型模式後邊。

java.lang.Integer:比對Integer類型

java.*.Integer:比對java包下任何一級包下的Integer類,比對java.lang.Integer,但是不比對java.lang.ll.Integer

java..*:比對java包下所有類型

java.lang.*ger:比對任何java.lang包下的以ger結尾的類型

java.lang.AbstractStringBuilder+:比對任意java.lang.AbstractStringBuilder的子類型,如比對java.lang.StringBuilder,也比對java.lang.StringBuffer

切點表達式可以通過&&、||、!來組合切點表達式,下面是幾個切點表達式的例子(從開濤大神的部落格AOP 之 6.5 AspectJ切入點文法詳解 ——跟我學spring3摘抄):

execution

表達式 描述
public * *(..) 任何公共方法的執行
* cn.javass..IPointcutService.*() cn.javass包及所有子包下IPointcutService接口中的任何無參方法
* cn.javass..*.*(..) cn.javass包及所有子包下任何類的任何方法
* cn.javass..IPointcutService.*(*) cn.javass包及所有子包下IPointcutService接口的任何隻有一個參數方法
* (!cn.javass..IPointcutService+).*(..) 非“cn.javass包及所有子包下IPointcutService接口及子類型”的任何方法
* cn.javass..IPointcutService+.*() cn.javass包及所有子包下IPointcutService接口及子類型的的任何無參方法
* cn.javass..IPointcut*.test*(java.util.Date)

cn.javass包及所有子包下IPointcut字首類型的的以test開頭的隻有一個參數類型為java.util.Date的方法,注意該比對是根據方法簽名的參數類型進行比對的,而不是根據執行時傳入的參數類型決定的

如定義方法:public void test(Object obj);即使執行時傳入java.util.Date,也不會比對的;

* cn.javass..IPointcut*.test*(..)  throws

 IllegalArgumentException, ArrayIndexOutOfBoundsException

cn.javass包及所有子包下IPointcut字首類型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException異常

* (cn.javass..IPointcutService+

&& java.io.Serializable+).*(..)

任何實作了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的類型的任何方法
@java.lang.Deprecated * *(..) 任何持有@java.lang.Deprecated注解的方法
@java.lang.Deprecated @cn.javass..Secure  * *(..) 任何持有@java.lang.Deprecated和@cn.javass..Secure注解的方法
@(java.lang.Deprecated || cn.javass..Secure) * *(..) 任何持有@java.lang.Deprecated或@ cn.javass..Secure注解的方法
(@cn.javass..Secure  *)  *(..) 任何傳回值類型持有@cn.javass..Secure的方法
*  (@cn.javass..Secure *).*(..) 任何定義方法的類型持有@cn.javass..Secure的方法
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*))

任何簽名帶有兩個參數的方法,且這個兩個參數都被@ Secure标記了,

如public void test(@Secure String str1,

 @Secure String str1);

* *(

@cn.javass..Secure (@cn.javass..Secure *) ,

@ cn.javass..Secure (@cn.javass..Secure *))

任何帶有兩個參數的方法,且這兩個參數都被@ cn.javass..Secure标記了;且這兩個參數的類型上都持有@ cn.javass..Secure;

* *(

java.util.Map<cn.javass..Model, cn.javass..Model>

, ..)

任何帶有一個java.util.Map參數的方法,且該參數類型是以< cn.javass..Model, cn.javass..Model >為泛型參數;注意隻比對第一個參數為java.util.Map,不包括子類型;

如public void test(HashMap<Model, Model> map, String str);将不比對,必須使用“* *(

java.util.HashMap<cn.javass..Model,cn.javass..Model>

, ..)”進行比對;

而public void test(Map map, int i);也将不比對,因為泛型參數不比對

* *(java.util.Collection<@cn.javass..Secure *>)

任何帶有一個參數(類型為java.util.Collection)的方法,且該參數類型是有一個泛型參數,該泛型參數類型上持有@cn.javass..Secure注解;

如public void test(Collection<Model> collection);Model類型上持有@cn.javass..Secure

* *((@ cn.javass..Secure *))或

* *(@ cn.javass..Secure *)

任何帶有一個參數的方法,且該參數類型持有@ cn.javass..Secure;

如public void test(Model model);且Model類上持有@Secure注解

within

表達式 描述
within(cn.javass..*) cn.javass包及子包下的任何方法執行
within(cn.javass..IPointcutService+) cn.javass包或所有子包下IPointcutService類型及子類型的任何方法
within(@cn.javass..Secure *)

持有cn.javass..Secure注解的任何類型的任何方法

必須是在目标對象上聲明這個注解,在接口上聲明的對它不起作用

this

表達式 描述
this(cn.javass.spring.chapter6.service.IPointcutService) 目前AOP對象實作了 IPointcutService接口的任何方法
this(cn.javass.spring.chapter6.service.IIntroductionService)

目前AOP對象實作了 IIntroductionService接口的任何方法

也可能是引入接口

target

表達式 描述
target(cn.javass.spring.chapter6.service.IPointcutService) 目前目标對象(非AOP對象)實作了 IPointcutService接口的任何方法
target(cn.javass.spring.chapter6.service.IIntroductionService)

目前目标對象(非AOP對象) 實作了IIntroductionService 接口的任何方法

不可能是引入接口

args

表達式 描述
args (java.io.Serializable,..) 任何一個以接受“傳入參數類型為 java.io.Serializable” 開頭,且其後可跟任意個任意類型的參數的方法執行,args指定的參數類型是在運作時動态比對的

下面通過一個簡單的例子來示範如果通過aop标簽來實作AOP,這個例子實作的功能是:攔截spring.aop.dw.namespace及其子包下所有方法,在方法調用前後各添加一行日志:

aop标簽中提供了兩個标簽來定義切面,分别是aop:advisor和aop:aspect,下面分别看一下如何使用者兩個标簽:

使用aop:advisor定義切面

<bean id="logAdvisor" class="spring.aop.dw.namespace.LogAdvisor"></bean>
<aop:config>
	<aop:pointcut id="pointcut"
		expression="execution(* spring.aop.dw.namespace..*.*(..))" />
	<aop:advisor advice-ref="logAdvisor" pointcut-ref="pointcut" />
</aop:config>
           

pointcut-ref屬性設定切點,執行spring.aop.dw.namespace及其子包下所有方法對應的切點表達式是execution(* spring.aop.dw.namespace..*.*(..))

advice-ref屬性設定通知,通知bean的類型是LogAdvisor,它必須要是實作Advice接口,下面是這個類的代碼:

public class LogAdvisor implements MethodBeforeAdvice, AfterReturningAdvice {

	@Override
	public void afterReturning(Object returnValue, Method method,
			Object[] args, Object target) throws Throwable {
		System.out.println(method.getName() + ":end");
	}

	@Override
	public void before(Method method, Object[] args, Object target)
			throws Throwable {

		System.out.println(method.getName() + ":begin");
	}

}
           

before和afterReturning方法分别對應前通知和後通知,方法中傳遞了參數和傳回值等。也可以通過實作MethodInterceptor接口來實作:

public class LogAdvisor implements MethodInterceptor {

	@Override
	public Object invoke(MethodInvocation arg0) throws Throwable {
		Method method = arg0.getMethod();
		System.out.println(method.getName() + ":begin");
		Object retVal = arg0.proceed();
		System.out.println(method.getName() + ":end");
		return retVal;
	}

}
           

這樣切面就定義好了,測試bean和代碼就不列了,和ProxyFactoryBean的時一樣的。

使用aop:aspect定義切面

<bean id="logAspect" class="spring.aop.dw.namespace.LogAspect"></bean>
<aop:config>
	<aop:pointcut id="pointcut"
		expression="execution(* spring.aop.dw.namespace..*.*(..))" />
	<aop:aspect ref="logAspect">
		<aop:before method="before" pointcut-ref="pointcut" />
		<aop:after method="after" pointcut-ref="pointcut" />
	</aop:aspect>
</aop:config>
           

切點定義和使用aop:advisor時并沒有差別,不同的是這種方式需要定義一個切面類,這個類不需要實作任何架構中的接口,類中的方法名沒有任何限制,前提是method上面配置中的method屬性和類中的方法能比對上,aop:before屬性設定前通知,aop:after設定後通知,除此之外還可以通過aop:around定義環繞通知,aop:after-throwing定義異常通知:

public class LogAspect {
	public void before() {
		System.out.println("before");
	}

	public void after() {
		System.out.println("after");
	}
}
           

before方法對應前通知,after方法對應後通知,一般情況下在列印資訊的時候需要把方法和參數資訊列印出來,我們可以通過在切面類的方法中加入JoinPoint參數(如果是環繞通知則是它的子類ProceedingJoinPoint)來實作,這個參數必須定義在方法的第一個參數:

public class LogAspect {
	public void before(JoinPoint jp) {
		Object arg = getArg(jp);
		System.out.println(jp.getTarget() + ":" + jp.getSignature().getName()
				+ ":before:" + arg);
	}

	public void after(JoinPoint jp) {
		Object arg = getArg(jp);
		System.out.println(jp.getTarget() + ":" + jp.getSignature().getName()
				+ ":after:" + arg);
	}

	private Object getArg(JoinPoint jp) {
		Object[] args = jp.getArgs();
		Object arg = null;
		if (args != null && args.length > 0) {
			arg = args[0];
		}
		return arg;
	}

}
           

通過對比發現使用aop:aspect定義切面無需切面實作架構中的接口,侵入性更小,而非侵入性是Spring架構的重要特質之一。

@AspectJ注解

@AspectJ注解方式和和aop:aspect步驟比較類似,隻是把切點和通知定義放到了類的元注解中,切點表達式和Scheme風格的切點表達完全是一樣的

首先定義個切面類,無需實作任何特殊的接口,通過注解定義切面和前、後通知:

@Aspect
public class LogAspect {
	@Pointcut(value = "execution(* spring.aop.dw.annotation..*.*(..))")
	public void pointcut() {

	}

	@Before(value = "pointcut()")
	public void before(JoinPoint jp) {
		Object arg = getArg(jp);
		System.out.println(jp.getTarget() + ":" + jp.getSignature().getName()
				+ ":before:" + arg);
	}

	@After(value = "pointcut()")
	public void after(JoinPoint jp) {
		Object arg = getArg(jp);
		System.out.println(jp.getTarget() + ":" + jp.getSignature().getName()
				+ ":after:" + arg);
	}

	private Object getArg(JoinPoint jp) {
		Object[] args = jp.getArgs();
		Object arg = null;
		if (args != null && args.length > 0) {
			arg = args[0];
		}
		return arg;
	}

}
           

@Pointcut注解定義切點,上面例子的切點的連接配接點是執行spring.aop.dw.annotation及其子包下所有方法

@Before定義前通知,value屬性引用切點,注意要有括号。和aop:aspect方式一樣,JoinPoint參數是可選的

@After定義後通知,value屬性引用切點,和aop:aspect方式一樣,JoinPoint參數是可選的

其它的注解及其隻用可以參考AspectJ的使用文檔

Spring架構預設是不支援@AspectJ注解的,需要在配置中添加<aop:aspectj-autoproxy />打開對@AspectJ注解的支援,此外在配置中還需要定義切面bean:

<aop:aspectj-autoproxy />
<bean id="helloWorldService" class="spring.aop.dw.annotation.HelloWorldService"></bean>
<bean id="aspect" class="spring.aop.dw.annotation.LogAspect" />
           

這樣就通過@AspectJ注解完成了一個切面的定義,測試代碼就不列了。

通過比較這幾種AOP的實作方式,由于切點表達式的原因,ProxyFactoryBean的易用性遠遠落後另外兩種方式,從非侵入性的角度考慮@AspectJ注解和aop:aspect方式優于aop:advisor。

下一篇學習這幾種方式的實作原理。