第5章 Spring AOP
面向切面编程(AOP)是面向对象编程(OOP)的补充。AOP通常被称为实施横切关注点的工具。术语横切关注点是指应用程序中无法从应用程序的其余部分分解并且可能导致代码重复和紧密耦合的逻辑。通过使用AOP模块化各个逻辑部分(横切关注点),可以将它们应用于应用程序的多个部分,而无需复制代码或创建硬性依赖关系。
5.1 AOP概念
AOP的核心概念:
- 连接点
- 通知
- 切入点
- 切面
- 织入
- 目标对象
- 引入
5.2 AOP的类型
AOP分为静态AOP和动态AOP。他们之间的区别在于织入过程发生的地点以及如何实现这一过程。
在大多数情况下,Spring AOP是理想的选择,如果需要一项在Spring中未实现的AOP功能,使用AspectJ。许多基于AOP的几角方案(比如事务管理)都已经由Spring提供,因此在开发之前检查一下框架功能。
5.3 Spring中的AOP
5.3.1 AOP Alliance
AOP Alliance是许多开源AOP项目代表共同努力的结果,它为AOP实现定义了一组标准接口。只要适用,Spring就应该使用AOP Alliance接口而不是定义自己的接口。
5.3.2 AOP中的Hello World示例
MethodInterceptor
接口是一个标准的AOP Alliance接口,用于实现方法调用连接点的环绕通知。MethodInterceptor对象标识正在被通知的方法调用,通过使用此对象,可以控制方法调用何时进行。因为是环绕通知,所以能够在调用方法之前、之后、返回前执行相关操作。
使用ProxyFactory
类创建目标对象的代理。一旦设置目标并将一些通知添加到ProxyFactory,就可以通过调用getProxy()
生成代理。
代理创建的编程方法:
public class Agent {
public void speak() {
System.out.println("Agent...speak...");
}
}
// ----------------------------------------------------------//
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class AgentDecorator implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("James ");
Object retVal = invocation.proceed();
System.out.println("!");
return retVal;
}
}
// ----------------------------------------------------------//
import org.springframework.aop.framework.ProxyFactory;
public class AgentAOPDemo {
public static void main(String[] args) {
Agent target = new Agent();
ProxyFactory pf = new ProxyFactory();
pf.addAdvice(new AgentDecorator());
pf.setTarget(target);
Agent proxy = (Agent) pf.getProxy();
target.speak();
System.out.println("");
proxy.speak();
}
}
5.4 Spring AOP架构
Spring AOP的核心架构基于代理。
Spring有两个代理实现:JDK动态代理和CGLIB代理。默认情况下,当被通知的目标对象实现一个接口时,Spring将使用JDK动态代理来创建目标的代理实例。但是,当被通知目标对象没有实现接口时,使用CGLIB来创建代理实例。
5.4.1 Spring中的连接点
Spring AOP中最明显的简化只支持一种连接点类型:方法调用。如果需要在除方法调用外的连接点通知一些代码,可以一起使用Spring和AspectJ。
5.4.2 Spring中的切面
在Spring AOP中,切面由实现了Advisor
接口的类的实例表示。Advisor有两个子接口:PointcutAdvisor
和IntroductionAdvisor
。
所有的Advisor实现都实现了PointcutAdvisor
接口,这些实现使用切入点来控制应用于连接点的通知。在Spring中,引言被视为特殊类型的通知,通过使用IntroductionAdvisor
接口,可以控制将引言引用于哪些类。
5.4.3 关于ProxyFactory类
ProxyFactory
类控制Spring AOP中的织入和代理创建过程。在创建代理之前,必须指定被通知对象或目标对象。
在内部,ProxyFactory
将代理创建过程委托给DefaultAopProxyFactory
的一个实例,该实例又转而委托ObjenesisCglibAopProxy
或JdkDynamicAopProxy
5.4.4 在Spring中创建通知
Spring支持六种通知:
- 前置通知:
方法调用之前执行;
如果前置通知抛出异常,拦截器链(以及目标方法)被终止,异常将传回拦截器链;
- 后置返回通知
方法调用并且返回一个值后执行;
如果目标方法抛出异常,不会执行此通知,异常传回调用堆栈;
- 后置通知
方法调用正常完成后执行;
即使目标方法抛出异常,也会执行此通知;
- 环绕通知
允许在方法调用之前、之后执行;
如果需要,可以选择绕过目标方法;
- 异常通知
在方法调用返回后、抛出异常时执行;
- 引入通知
可以指定由引入通知引入的方法的实现;
5.4.8 创建后置返回通知
后置返回通知可以读取传递给方法的参数,但不能阻止方法执行,也无法修改返回值,可以抛出异常。
后置返回通知的一个很好用法是在方法可能返回无效值时执行一些额外的错误检查。
5.4.9 创建环绕通知
环绕通知功能类似于前置通知和后置通知功能的组合,但存在一个很大的区别:可以修改返回值。不仅如此,还可以组织方法执行,这意味着可以用新代码替换整个方法的实现。
5.4.10 创建异常通知
后置异常通知允许对整个Exception
层次结构进行重新分类,并为应用程序构建集中式异常日志记录。
实现后置异常通知有固定的写法,必须实现ThrowsAdvice
接口,方法最好返回void(此方法不能返回任何有意义的值)。方法名称必须为afterThrowing
(参见ThrowsAdviceInterceptor.AFTER_THROWING
),方法可以有1个或4个参数(参数为4个时,顺序固定),最后一个参数为接收的异常(尽量细化),最好定义了两个接收相同异常的方法(参数个数不同),当不能直接匹配到抛出的异常时,匹配抛出异常额父类,直至匹配到。如果使用了try-catch
捕获异常,异常通知方法会在catch语句之前执行。
public class SimpleThrowsAdvice implements ThrowsAdvice {
public static void main(String[] args) {
ErrorBean errorBean = new ErrorBean();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(errorBean);
pf.addAdvice(new SimpleThrowsAdvice());
ErrorBean proxy = (ErrorBean) pf.getProxy();
try {
proxy.errorProneMethod();
} catch (Exception e) {
}
try {
proxy.otherErrorProneMethod();
} catch (Exception e) {
}
}
public void afterThrowing(Exception ex) {
System.out.println("afterThrowing...1...");
System.out.println(ex.getClass().getName());
}
public void afterThrowing(Method method, Object args, Object target, Exception ex) {
System.out.println("afterThrowing...2...");
System.out.println(ex.getClass().getName());
}
}
5.4.11 选择通知类型
应该根据需要选择最具体的通知类型。
5.5 在Spring中使用顾问和切入点
5.5.1 Pointcut
接口
Spring中的切入点通过实现Pointcut
接口来创建。
Spring支持两种类型的MethodMatcher
——静态和动态MethodMatcher。即静态切入点和动态切入点。
5.5.2 可用的切入点实现
5.5.3 使用DefaultPointcutAdvisor
Advisor
是Spring中某个切面的表示,它是通知(Advice)和切入点(Pointcut)的结合体,规定了应该通知哪些方法以及如何通知。
5.5.4 使用StaticMethodMatcherPointcut
创建静态切入点
public class SimpleStaticPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, Class<?> targetClass) {
return Objects.equals("sing", method.getName());
}
@Override
public ClassFilter getClassFilter() {
return cls -> (cls == GoodGuitarist.class);
}
}
// -------------------------------------- //
public class SimpleAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println(">> Invoking " + invocation.getMethod().getName());
Object retVal = invocation.proceed();
System.out.println(">> Done");
return retVal;
}
}
// -------------------------------------- //
public class StaticPointcutDemo {
public static void main(String[] args) {
GoodGuitarist goodGuitarist = new GoodGuitarist();
GreatGuitarist greatGuitarist = new GreatGuitarist();
SimpleStaticPointcut pointcut = new SimpleStaticPointcut();
SimpleAdvice advice = new SimpleAdvice();
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
ProxyFactory pf = new ProxyFactory();
pf.addAdvisor(advisor);
pf.setTarget(goodGuitarist);
Singer proxy1 = (Singer) pf.getProxy();
pf = new ProxyFactory();
pf.addAdvisor(advisor);
pf.setTarget(greatGuitarist);
Singer proxy2 = (Singer) pf.getProxy();
proxy1.sing();
proxy2.sing();
}
}
5.5.5 使用DynamicMethodMatcherPointcut
创建动态切入点
public class SimpleDynamicPointcut extends DynamicMethodMatcherPointcut {
/**
* 对类型进行检查
*
* @return
*/
@Override
public ClassFilter getClassFilter() {
return cls -> cls == SampleBean.class;
}
/**
* 静态检查
*
* @param method
* @param targetClass
* @return
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
System.out.println("Static check for " + method.getName());
return Objects.equals("foo", method.getName());
}
/**
* 动态检查
*
* @param method
* @param targetClass
* @param args
* @return
*/
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
System.out.println("Dynamic check for " + method.getName());
int x = ((Integer) args[0]).intValue();
return x != 100;
}
}
在执行动态检查前,会先执行静态检查,如果静态检查不通过,不执行动态检查,静态检查的结果被缓存起来以获得更好的性能。
对于DynamicMethodMatcherPointcut
动态检查来说,推荐的做法是,在getClassFilter()
方法执行类检查,在matches(Method method, Class<?> targetClass)
方法中执行方法检查,在matches(Method method, Class<?> targetClass, Object... args)
方法中执行参数检查。
需要注意的一点是,foo()
方法进行了两次静态检查:一次在初始阶段,当所有方法在第一次被调用时(SampleBean proxy = (SampleBean) pf.getProxy();
),另一次在第一次被调用时(proxy.foo(1);
)。
5.5.6 使用简单名称匹配
NameMatchMethodPointcut pc = new NameMatchMethodPointcut();
pc.addMethodName("sing");
pc.addMethodName("rest");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
5.5.7 用正则表达式创建切入点
JdkRegexpMethodPointcut pc = new JdkRegexpMethodPointcut();
pc.setPattern(".*sing.*");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
5.5.8 使用AspectJ切入点表达式创建切入点
可以使用AspectJ的切入点表达式语言进行切入点声明。当使用aop名称空间在XML配置中声明切入点时,Spring默认使用AspectJ的切入点语言。当使用Spring的@AspectJ
注解支持AOP时,也需要使用AspectJ切入点语言。Spring提供AspectJExpressionPointcut
通过AspectJ的表达式语言定义切入点。
要在Spring中使用AspectJ切入点表达式,需要添加依赖:
<!--Aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* sing*(..))");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
5.5.9 创建注解匹配切入点
Spring提供了AnnotationMatchingPointcut
类类定义使用注解的切入点。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface MyAdviceRequired {
}
// -------------------------------------------------- //
public class AnnotationPointcutDemo {
public static void main(String[] args) {
Guitarist guitarist = new Guitarist();
AnnotationMatchingPointcut pc = AnnotationMatchingPointcut.forMethodAnnotation(MyAdviceRequired.class);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pc, new SimpleAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(guitarist);
pf.addAdvisor(advisor);
Guitarist proxy = (Guitarist) pf.getProxy();
proxy.sing();
proxy.sing("bbb");
proxy.rest();
}
}
5.5.10 便捷的Advisor
实现
对于许多Pointcut
实现来说,Spring还提供了一个便捷的Advisor
实现来充当切入点。例如,可以简单地使用NameMatchMethodPointcutAdvisor
,而不是像之前地示例那样配合使用NameMatchMethodPointcut
和DefaultPointcutAdvisor
。
public class NamePointcutUsingAdvisor {
public static void main(String[] args) {
GrammyGuitarist grammyGuitarist = new GrammyGuitarist();
NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(new SimpleAdvice());
advisor.addMethodName("sing");
advisor.addMethodName("rest");
ProxyFactory pf = new ProxyFactory();
pf.setTarget(grammyGuitarist);
pf.addAdvisor(advisor);
GrammyGuitarist proxy = (GrammyGuitarist)pf.getProxy();
proxy.sing();
proxy.sing("ccc");
proxy.rest();
proxy.talk();
}
}
5.6 了解代理
在Spring中有两种类型的代理:使用JDK Proxy
类创建的JDK代理以及使用CGLIB Enhancer
类创建的基于CGLIB地代理。
5.6.1 使用JDK动态代理
JDK代理是Spring中最基本的代理类型。JDK代理只能生成接口的代理,不能生成类的代理。想要代理的任何对象都必须至少实现一个接口,并且生成的代理是实现该接口的对象。
通过ProxyFactory.setInterfaces(Class<?>... interfaces)
指定要代理的接口列表。
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(Singer.class);
pf.setTarget(guitarist);
pf.addAdvisor(advisor);
Singer proxy = (Singer) pf.getProxy();
5.6.2 使用CGLIB代理
如果使用JDK代理,那么在每次调用invoke()
方法时,有关如何处理特定方法调用的决策都会在运行时做出。而是用CGLIB时,CGLIB会为每个代理动态生成新类的字节码,并尽可能重用已生成的类。在这种情况下,所生成的代理类型将是目标对象类的子类。
5.6.4 选择要使用的代理
当需要代理类时,CGLIB代理是默认也是唯一的选择。如果想要代理接口时使用CGLIB,必须使用ProxyFactory.setOptimize(true)
方法将optimize标志设为true。
ProxyFactory pf = new ProxyFactory();
pf.setInterfaces(new Class[]{Singer.class});
pf.setOptimize(true);
5.7 切入点的高级使用
Spring提供的六个基本Pointcut
实现:
AnnotationMatchingPointcut
AspectJExpressionPointcut
DynamicMethodMatcherPointcut
JdkRegexpMethodPointcut
NameMatchMethodPointcut
StaticMethodMatcherPointcut
Spring提供了两个额外的Pointcut
实现:
ComposablePointcut
ControlFlowPointcut
5.7.1 使用控制流切入点
下例中,运行在test方法中的foo方法会接受通知:
public class ControlFlowDemo {
public static void main(String[] args) {
ControlFlowDemo ex = new ControlFlowDemo();
ex.run();
}
private void run() {
TestBean testBean = new TestBean();
ControlFlowPointcut pc = new ControlFlowPointcut(ControlFlowDemo.class, "test");
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pc, new SimpleBeforeAdvice());
ProxyFactory pf = new ProxyFactory();
pf.setTarget(testBean);
pf.addAdvisor(advisor);
TestBean proxy = (TestBean)pf.getProxy();
System.out.println(".....");
proxy.foo();
System.out.println("======");
test(proxy);
}
private void test(TestBean proxy) {
proxy.foo();
}
}
5.7.2 使用组合切入点
使用ComposablePointcut
将两个切入点组合成一个切入点。
ComposablePointcut
支持两种方法:union()
和intersection()
。默认情况下,ComposablePointcut
是通过一个匹配所有类的ClassFilter
以及一个匹配所有方法的MethodMatcher
来创建的。
可以将union()
和intersection()
想象成SQL查询中的WHERE子句,其中union()类似于“or”,intersection()类似于“and”。
5.7.3 组合和切入点接口
构建ComposablePointcut
的另一种方法是使用
org.springframework.aop.support.Pointcuts
此类提供了三个静态方法:
Pointcut union(Pointcut pc1, Pointcut pc2)
Pointcut intersection(Pointcut pc1, Pointcut pc2)
boolean matches(Pointcut pointcut, Method method, Class<?> targetClass, Object... args)
快速检查切入点是否与所提供的方法、类和方法参数相匹配。
5.7.4 切入点小结
两种模式来组合Pointcut
和Advice
。
- 使用
DefaultPointcutAdvisor(Pointcut pointcut, Advice advice)
将Pointcut
和Advice
一起添加到代理中; - 直接使用
PointcutAdvisor
;
5.8 引入(Introduction)入门
引入(Introduction)是Spring中可用的AOP功能集的重要组成部分。通过使用引入,可以动态的向现有对象引入新功能。在Spring中,可以将任何接口的实现引入现有对象。当一个功能是横切的并且使用传统通知不易实现时,就需要动态添加该功能。
5.8.1 引入的基础知识
Spring将引入作为一种特殊类型的环绕通知。引入仅适用于类级别,因此不能在引入时使用切入点。
引入将新的接口实现添加到类中,而切入点定义了通知适用于哪些方法。
可以通过实现IntroductionInterceptor
接口来创建引入。Spring提供了一个名为DelegatingIntroductionInterceptor
的默认实现。如果要使用DelegatingIntroductionInterceptor
创建引入,可以创建一个既继承了DelegatingIntroductionInterceptor
,又实现了想要引入的接口的类。DelegatingIntroductionInterceptor
实现简单地将所有引入方法的调用委托给相应的方法。
就像使用切入点通知时需要使用PointcutAdvisor
一样,需要使用IntroductionAdvisor
向代理添加引入。IntroductionAdvisor
的默认实现是DefaultIntroductionAdvisor
。
不允许使用ProxyFactory.addAdvise()
方法添加应用,应该使用ProxyFactory.addAdvisor()
。
使用标准通知(Pointcut)时,是基于类型的生命周期(per-class life cycle);使用引入(Introduction)时,是基于实例的生命周期(per-instance life cycle)。
5.8.2 使用引入进行对象修改检测
使用引入实现对象修改检测(object modification detection)技术:
@Data
public class Contact {
private String name;
private String phoneNumber;
private String email;
}
// ------------------------------------------------- //
public interface IsModified {
boolean isModified();
}
// ------------------------------------------------- //
public class IsModifiedMixin extends DelegatingIntroductionInterceptor implements IsModified {
private boolean isModified = false;
private Map<Method, Method> methodCache = new HashMap<>();
@Override
public boolean isModified() {
return isModified;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (!isModified) {
if ((invocation.getMethod().getName().startsWith("set")) && (invocation.getArguments().length == 1)) {
Method getter = getGetter(invocation.getMethod());
if (getter != null) {
Object newVal = invocation.getArguments()[0];
Object oldVal = getter.invoke(invocation.getThis(), null);
if (newVal == null && oldVal == null) {
isModified = false;
} else if (newVal == null && oldVal != null) {
isModified = true;
} else if (newVal != null && oldVal == null) {
isModified = true;
} else {
isModified = !newVal.equals(oldVal);
}
}
}
}
return super.invoke(invocation);
}
private Method getGetter(Method setter) {
Method getter = methodCache.get(setter);
if (getter != null) {
return getter;
}
String getterName = setter.getName().replaceFirst("set", "get");
try {
getter = setter.getDeclaringClass().getMethod(getterName, null);
synchronized (methodCache) {
methodCache.put(setter, getter);
}
return getter;
} catch (NoSuchMethodException e) {
return null;
}
}
}
// ------------------------------------------------- //
public class IsModifiedAdvisor extends DefaultIntroductionAdvisor {
public IsModifiedAdvisor() {
super(new IsModifiedMixin());
}
}
// ------------------------------------------------- //
public class IntroductionDemo {
public static void main(String[] args) {
Contact target = new Contact();
target.setName("John Mayer");
IsModifiedAdvisor advisor = new IsModifiedAdvisor();
ProxyFactory pf = new ProxyFactory();
pf.setTarget(target);
pf.addAdvisor(advisor);
pf.setOptimize(true);
// 引入将新的接口实现添加到类中
Contact proxy = (Contact) pf.getProxy();
IsModified proxyInterface = (IsModified) proxy;
System.out.println("Is Contact?: " + (proxy instanceof Contact));
System.out.println("Is IsModified?: " + (proxy instanceof IsModified));
System.out.println("Has been modified?: " + proxyInterface.isModified());
proxy.setName("John Mayer");
System.out.println("Has been modified?: " + proxyInterface.isModified());
proxy.setName("Eric Clapton");
System.out.println("Has been modified?: " + proxyInterface.isModified());
}
}
5.9 AOP的框架服务
对AOP配置使用声明式方法优于手动编程机制。
5.9.1 以声明的方式配置AOP
使用Spring AOP的声明式配置时,存在三个选项:
- 使用ProxyFactoryBean
- 使用Spring aop名称空间
后台使用ProxyFactoryBean
- 使用@AspectJ样式注解
语法基于AspectJ,使用时需要引入一些AspectJ库,但在引导ApplicationContext时,Spring仍然使用代理机制(即为目标创建代理对象)
5.9.2 使用ProxyFactoryBean
ProxyFactoryBean
类是FactoryBean
的一个实现,它允许指定一个bean作为目标,并且为该bean提供一组通知(Advice)和顾问(Advisor)(这些通知和顾问最终被合并到一个AOP代理中。ProxyFactoryBean
用于将拦截器逻辑应用于现有的目标bean,做法是当调用该bean上的方法时,在方法调用之前和之后执行拦截器。因为可以同时使用顾问和通知,所以不仅可以以声明的方式配置通知,还可以配置切入点。
5.9.3 使用aop名称空间
演示了使用XML配置AOP
5.10 使用@AspectJ
样式注解
通过使用@AspectJ
注解实现与aop名称空间中相同的切面。
@EnableAspectJAutoProxy
等价于<aop:aspectj-autoproxy />
在Spring Boot中使用AOP,需要添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
添加依赖后不再需要@EnableAspectJAutoProxy
。
5.11 AspectJ集成
Spring AOP仅支持与执行公共非静态方法相匹配的切入点。
在某些情况下,需要使用更全面的功能集来查看AOP实现。此时,我们偏爱使用AspectJ,因为使用Spring配置AspectJ切面,AspectJ成为Spring AOP的完美补充。
5.11.1 关于AspectJ
AspectJ是全功能的AOP实现,它使用织入过程(编译时或加载时织入)将各个切面引入到代码中。在AspectJ中,切面和切入点都是使用Java类似语法构建的。