在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。
下一篇學習這幾種方式的實作原理。