本文是Spring AOP官方文档部分内容的简记。
Spring IoC容器不依赖于AOP,但是作为IoC容器的补充,AOP提供了非常强大的中间件解决方案。
AOP在Spring框架中的用途:
@Aspect
实现。切面=切点+通知。通知类型:
Spring AOP由纯Java实现,不需要控制类加载器,适用于Servlet容器或应用服务器。
Spring AOP的实现和大多数其它的AOP框架不同,Spring的目标不是提供完整的AOP实现,而是在AOP实现和IoC容器提供一个集成方案,以此解决企业级y应用中的一些常见问题。因此,Spring AOP的功能总是和IoC容器结合在一起,切面通过常规的bean definition语法配置。
Spring AOP默认使用标准的JDK动态代理实现。Spring AOP也能使用CGLIB代理,这在代理类而不是接口的时候是必要的。当业务对象没有实现任何接口的时候,CGLIB默认被使用。
Spring利用AspectJ中的切点解析和匹配库来解析AspectJ中的注解,但是AOP运行时仍旧是纯Spring AOP,它并不依赖于AspectJ编译器或织入器。
可以通过Java配置使能@AspectJ
注解支持(XML配置不在此记录)
@Configuration
@EnableAspectJAutoProxy
public class AppConfig{
// TODO
}
任何定义在上下文中并被@Aspect
注解的bean都可以被Spring探测到并用于配置Spring AOP。例如:
package com.example;
import org.aspectj.lang.annotation.Aspect
@Aspect
public class NotVeryUsefulAspect {
// TODO
}
可以通过xml配置将切面类注册成切面bean,也可以通过组件扫描自动探测它们。自动扫描需要配合Spring 原型注解(例如
@Component
)使用。
切点的声明包括两部分:1,由名称和任意多个参数构成的签名;2,切点表达式。在@AspectJ
注解风格的AOP中,切点签名由一个常规的方法定义表示,切点表达式由@Pointcut
注解表示。注意,作为切点签名的方法的返回类型只能是void.
示例代码:
@Pointcut("execution(* transfer(...))") //切点表达式
private void anyOldTransfer() {} // 切点签名
上述切点匹配任何名为transfer的方法调用。关于切点语言,参考Aspect Programming Guide. Spring AOP支持的切点标识符(AspectJ pointcut Designators)如下:
Spring AOP还支持一个名为bean的PCD。这个PCD允许限制匹配连接点到给定名字的Spring bean或者bean集合(通配符)
切点表达式能通过“&&”, “||”, “!“ 进行联合。也可以通过名字引用切点表达式。例如:
@Pointcut("execution(* *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.example.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
package com.example.someapp;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class SystemArchitecture {
//定义一个web层的连接点,方法在某个包或其任意的子包下
@Pointcut("within(com.example.someapp.web..*)")
public void inWebLayer() {}
//定义一个服务层的连接点,。。。。。。。
@Pointcut("within(com.example.someapp.service..*)")
public void inServiceLayer() {}
//定义一个数据访问层连接点,。。。。。。。。
@Pointcut("within(com.example.someapp.dao..*)")
public void inDaoLayer() {}
//业务服务是定义在服务接口中的任意方法的执行。
@Pointcut("execution(* com.example.someapp..service.*.*(..))")
public void businessService() {}
//数据访问层中任意方法的调用
@Pointcut("execution(* com.example.someapp.dao.*.*(..))")
public void dataAccessOperation() {}
}
通知总是和切点表达式相关,它运行在和切点匹配的方法调用之前、之后或者环绕这个方法。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class BeforeExample {
@Before("com.example.SystemArchitecture.dataAccessOperation")
public void doAccessCheck() {
// TODO
}
}
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
@Aspect
public class BeforeExample {
@AfterReturning("com.example.SystemArchitecture.dataAccessOperation")
public void doAccessCheck() {
// TODO
}
}
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) {
// TODO
}
}
环绕通知由@Around
注解声明,通知方法的第一个参数必须是ProceedingJoinPoint
类型。在方法内部,调用ProceedingJoinPoint
对象的proceed()
方法执行要调用的方法。proceed()
方法还能传入一个Object[]
参数。
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;
}
}
可以在通知签名中声明自己需要的参数。
任何通知方法都能将当前连接点声明为它的d一个参数(org.aspectj.lang.JoinPoint
类型),JointPoint
接口提供了很多有用的方法,例如:getArgs()
获取方法参数,getThis()
获取代理对象,getTarget()
获取目标对象等。
为了能使参数值可被通知体访问,可以使用args
绑定形式。如果参数名被用于代替参数表达式的类型名,那么当通知被调用时,参数的值被作为参数值传递给通知。例如:
@Before("com.example.myapp.SystemArchitecture.dataAccessOperation() && args(account, ..)")
public void validateAccount(Account account) {
// ...
}
切点表达式的args(account, ..)
部分有两个目的:1,限制匹配到那些至少需要一个参数的方法,而且参数必须是Account
类型的实例;2,使得实际的Account
对象在通知体中也能访问。
假设有以下泛型类型:
public interface Sample<T> {
void sampleGenericMethod(T param);
void sampleGenericCollectionMethod(Collection param);
}
可以限制拦截的方法类型为确定的参数类型,只需要将通知参数写成实际需要的类型即可。
@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
// Advice implemention
}
绑定到通知调用的参数依赖于切点表达式中匹配的名字。参数名字无法通过java反射技术获取,Spring AOP通过以下策略获取参数名称:
如果参数名称已经有用户显示指定,那么就使用指定的参数名,通知和切点注解都有一个可选的属性”argNames“用于指定注解方法的参数名,例如:
@Before(value="com.example.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames="bean, auditable")
public void audit(Object bean, Auditable auditable) {
AuditCode code = auditable.value();
// ...use code, bean and jp....
}
查看debug信息,尝试从局部变量表中确定参数名。
如果代码编译时没有必须的debug信息,Spring AOP将会尝试绑定变量到参数
如果上述所有策略失效,抛出IllegalArgumentException
异常
@Around("execution(List find*(..)) && " + "com.example.myapp.SystemArchitecture.inDataAccessLayer() && " + "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp, String accountHolderNamePattern) throws Throwable {
String newPattern = preProcess(accountHolderNamePattern);
return pjp.proceed(new Object[] {newPattern});
}
引入使得切面能声明被通知的对象实现了给定的接口,并代表那些对象实现接口。
引入通过@DeclareParents
注解实现。给定一个接口UsageTracked
和这个接口的实现DefaultUsageTracked
,下面的例子表明所有实现了service接口的类都实现了UsageTracked
接口:
@Aspect
public class UsageTracking {
@DeclareParents(value="com.example.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.example.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
默认情况下,应用上下文中的每个切面都是单例的。这是AOP的一个高级主题,暂时略过。