开启AspectJ
配置类方式:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
xml配置文件方式:
<aop:aspectj-autoproxy/>
定义切面(Aspect)
注解方式:
@Aspect
public class NotVeryUsefulAspect {
}
使用@Aspect
注解,即将一个类定义为切面类。
xml方式:
<bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
<!-- configure properties of the aspect here -->
</bean>
定义切点(Pointcut)
@Pointcut("execution(* transfer(..))")// the pointcut expression
private void anyOldTransfer() {}// the pointcut signature
使用@Pointcut注解定义一个切点,在注解里,使用AspectJ表达式表明连接点的位置。
切点方法的返回值必须为void
连结切点表达式
可以使用逻辑连接符&&
||
!
连结切点表达式
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
anyPublicOperation
匹配任意的public方法inTrading
匹配所有com.xyz.someapp.trading包下的方法tradingOperation
则匹配所有com.xyz.someapp.trading包下的public方法
以下是一些切点的示例(摘自Spring官方文档):
package com.xyz.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
/**
* A join point is in the web layer if the method is defined
* in a type in the com.xyz.someapp.web package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.web..*)")
public void inWebLayer() {}
/**
* A join point is in the service layer if the method is defined
* in a type in the com.xyz.someapp.service package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.service..*)")
public void inServiceLayer() {}
/**
* A join point is in the data access layer if the method is defined
* in a type in the com.xyz.someapp.dao package or any sub-package
* under that.
*/
@Pointcut("within(com.xyz.someapp.dao..*)")
public void inDataAccessLayer() {}
/**
* A business service is the execution of any method defined on a service
* interface. This definition assumes that interfaces are placed in the
* "service" package, and that implementation types are in sub-packages.
*
* If you group service interfaces by functional area (for example,
* in packages com.xyz.someapp.abc.service and com.xyz.someapp.def.service) then
* the pointcut expression "execution(* com.xyz.someapp..service.*.*(..))"
* could be used instead.
*
* Alternatively, you can write the expression using the 'bean'
* PCD, like so "bean(*Service)". (This assumes that you have
* named your Spring service beans in a consistent fashion.)
*/
@Pointcut("execution(* com.xyz.someapp..service.*.*(..))")
public void businessService() {}
/**
* A data access operation is the execution of any method defined on a
* dao interface. This definition assumes that interfaces are placed in the
* "dao" package, and that implementation types are in sub-packages.
*/
@Pointcut("execution(* com.xyz.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
使用xml配置事务管理的AOP示例:
<aop:config>
<aop:advisor
pointcut="com.xyz.someapp.SystemArchitecture.businessService()"
advice-ref="tx-advice"/>
</aop:config>
<tx:advice id="tx-advice">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
execution
execution
是我们最常用的切点表达式,他的语法如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
示例:
匹配所有的public方法
execution(public * *(..))
匹配所有以set开头的方法
execution(* set*(..))
匹配
AccountService
里的所有方法execution(* com.xyz.service.AccountService.*(..))
匹配service包下的所有方法
execution(* com.xyz.service.*.*(..))
匹配service包和它的子包下的所有方法
execution(* com.xyz.service..*.*(..))
匹配service包下的所有连接点(在spring AOP中指的是方法)
within(com.xyz.service.*)
匹配service包和其子包下的所有连接点
within(com.xyz.service..*)
匹配实现了
AccountService
接口的所有代理类的所有连接点this(com.xyz.service.AccountService)
匹配所有实现了
AccountService
接口的目标对象的连接点target(com.xyz.service.AccountService)
匹配所有只有一个参数,且运行时参数类型是Serializable的连接点
args(java.io.Serializable)
args表达式和execution( (java.io.Serializable))的不同是,args中的参数类型是运行时参数类型,而execution的参数类型是方法声明中的类型。
匹配目标对象有@Transactional注解的所有连接点
@target(org.springframework.transaction.annotation.Transactional)
匹配方法上有
@Transactional
注解的所有连接点@annotation(org.springframework.transaction.annotation.Transactional)
匹配目标对象的类型声明上有
@Transactional
注解的所有连接点@within(org.springframework.transaction.annotation.Transactional)
匹配只有一个参数,且参数的运行时类型有一个@Classified注解的所有连接点
@args(com.xyz.security.Classified)
匹配名为
tradeService
的Spring bean上的所有连接点bean(tradeService)
匹配所有名字以
Service
结尾的Spring bean上的所有连接点bean(*Service)
定义通知(Advice)
Before Advice
使用@Before注解在切面中定义一个before advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
// @Before("execution(* com.xyz.myapp.dao.*.*(..))") 或者使用ececution表达式
public void doAccessCheck() {
// ...
}
}
After Returning Advice
after returning advice的定义和before advice一样。但如果你想要在advice方法中使用切点方法的返回值,可以使用以下的方式:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class AfterReturningExample {
@AfterReturning(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
returning="retVal")
public void doAccessCheck(Object retVal) {
// ...
}
}
returns属性中使用的名称必须与advice方法中的参数名称相对应。 当方法执行返回时,返回值作为相应的参数值传递给advice方法。 返回子句还将匹配仅限于那些返回指定类型值的方法执行(在本例中为Object,它匹配任何返回值)。
After Throwing Advice
after throwing advice是当切点方法抛出异常时执行通知方法。使用@AfterThrowing注解定义。
除了普通的定义方法之外。通常,我们希望通知方法仅在指定的异常类型被抛出时执行,且有时候(例如打印异常日志)我们希望在通知方法里访问到抛出的异常。我们可以使用注解的参数来限制匹配到的抛出异常类型,并将抛出的异常绑定到通知方法的参数上。以下示例说明了如何执行此操作:
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;
@Aspect
public class AfterThrowingExample {
@AfterThrowing(
pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
throwing="ex")
public void doRecoveryActions(DataAccessException ex) {
// ...
}
}
After(Finally) Advice
After advice是当前切点方法退出前最终要执行通知方法,类似于finally块的作用。使用@After注解定义,通常用于释放资源等操作。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;
@Aspect
public class AfterFinallyExample {
@After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
public void doReleaseLock() {
// ...
}
}
Around Advice
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
@Aspect
public class AroundExample {
@Around("com.xyz.myapp.SystemArchitecture.businessService()")
public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
// start stopwatch
Object retVal = pjp.proceed();
// stop stopwatch
return retVal;
}
}
访问当前JoinPoint
任何通知方法都可以使用JoinPoint作为第一个参数,around advice需要使用它的子类,ProceedingJoinPoint。
JoinPoint接口有一些使用的方法:
- getArgs():返回被通知的方法的参数
- getThis():返回代理对象
- getTarget:返回目标对象
- getSignature():返回被通知方法的声明信息
- toString():打印被通知方法的描述
通知参数
传递参数到通知
我们已经知道如何绑定返回值和异常到通知中。现在我们要想在通知方法里使用参数值,可以使用args参数。
@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
// ...
}
args(account,..)首先会限制切点匹配到至少有一个参数,且第一个参数的类型为Acount的方法。其次可以将被通知方法的参数的值传递到通知方法中去。
另一种方式是声明一个切点,然后在通知中引用切点:
@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}
@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
// ...
}
使用注解传递参数
使用自定义注解,通过向注解中传递参数来达到传递参数到通知方法中。
- 先定义一个注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
AuditCode value();
}
然后在通知方法中,配置匹配该注解:
@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
AuditCode code = auditable.value();
// ...
}
该通知匹配含有@Auditable注解的方法,并将注解作为通知方法的参数传递进去。可以在注解中配置想要的值,继而获取注解的属性值。
范型参数
Spring AOP可以处理类和方法参数中的范型。如果有以下接口:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection<T> param);
}
我们可以通过通知方法中的参数的类型来限制通知匹配到的该接口的范型的类型:
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implementation
}
上面的通知只能匹配到Sample<MyType>
这个类型里的方法。
如果是集合参数,比如Collection<MyType> param
,则不能用这种方法。如果是集合参数,我们需要将参数类型定义为Collection<?>
并手动判断里面元素的类型。
确定参数名
通知调用中的参数绑定依赖于切点表达式和通知方法声明中的参数名。Java反射不能获取到参数名,所以Spring AOP使用以下策略来决定参数名:
在切点表达式中,使用argNames属性来指定被通知方法的参数名。这些参数名在运行时可以获取到:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code and bean
}
如果第一个参数是JoinPoint
, ProceedingJoinPoint
, 或 JoinPoint.StaticPart
中的一个,则不需要在argNames中指定它的参数名:
@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ... use code, bean, and jp
}
通知顺序
如果多个通知匹配到了同一个连接点,会发生什么呢?
Spring AOP遵循和AspectJ同样的优先规则。如果两个before通知,则权重高的先运行。如果两个after通知,则权重高的后运行。
当两个通知定义在不同的切面上,可以使用Order
注解或者让切面类实现org.springframework.core.Ordered
接口,来定义通知的顺序。切面返回的Ordered.getValue()值越低,则权重越高。
当在同一个切面类里定义的两个通知方法匹配到了同一个连接点,则没有办法确定通知方法执行的顺序。但是你可以将这两个通知方法合并为同一个方法,或者将他们定义在不同的切面类里。
引入(Introductions)
引入允许切面为被通知对象实现一个给定的接口,并提供这个接口的实现。
可以使用@DeclareParents注解创建一个引入,该注解让匹配到的类型有一个新的父类。例如,有一个名为UsageTracked的接口,它的一个实现类是DefaultUsageTracked,下面的切面表明,所有的service接口的实现类同时实现了UsageTracked接口:
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
被注解的域的类型决定了被实现的接口,在上面例子中,静态域mixin的类型为UsageTracked,表明实现的接口是UsageTracked。注解的value属性表明哪些类型实现UsageTracked接口。