一、增强类
1 前置增强(MethodBeforeAdvice)
重写before(Method method,Object[] args,Object obj)方法,method为目标类的方法;args为目标类方法参数;obj为目标类实例。
2 后置增强(AfterReturningAdvice)
重写afterReturning(Object returnObj,Method method,Object[] args,Object obj)方法,returnObj为目标方法返回的结果,其余参数合和befor方法一致。
3 环绕增强(MethodInterceptor)
重写invoke(MethodInvocation)方法,MethodInvocation不仅封装了目标方法及其入参数组,还封装了目标方法所在的实力对象,通过MethodInvocation的getArguments方法可以获取目标方法的入参参数,通过proceed方法反射调用目标实例相应的方法。
4 异常抛出增强(ThrowsAdvice)
重写afterThrowing(Method method,Object[] args,Object target,Exception ex)方法,方法参数规定如下:前三个参数(method,args,target)是可选的(要么三个全部提供,要么都不不提供),最后一个参数是Throwable或其子类。
5 引介增强(IntroductionInterceptor)
该接口没有定义任何方法,Spring为该接口提供了DelegatingIntroductionInterceptor实现类,一般情况下通过继承该类,覆盖invoke(MethodInvocation mi)方法来定义自己的引介增强类。
二、切面
介绍增强时,我们注意到增强被织入到目标类的所有方法中,如果我们要有选择地进行织入,就需要使用切点进行目标连接点的定位了。
增强提供了连接点的方位信息:如织入到方法前、方法后,而切点进一步描述织入到哪些类的哪些方法上。
Spring通过PointCut接口描述切点,PointCut由ClassFilter和MethodMatcher构成,它通过ClassFilter定位到类,通过MethodMatcher定位到方法,这样PointCut就拥有了描述某些类的某些具体方法的能力。
ClassFilter只定义了matches(Class clazz),其参数代表一个被检测的类,该方法判别被检测的类时候匹配过滤条件。
MethodMatcher支持静态方法匹配以及动态方法匹配。静态方法匹配仅对方法名进行匹配,且只会判别一次;动态方法匹配会在运行时检查方法入参的值,所以每次调用方法都会进行判断。
切点类型
Spring提供了6中切点:
- 静态方法切点
StaticMethodMatcherPointCut,默认匹配所有类。两个主要的子类是NamedMatchMethodPointCut以及AbstractRegexpMatchMethodPointCut - 动态方法切点
DynamicMethodMatcherPointCut - 注解切点
AnnotationMatchingPointCut,支持在Bean中直接通过注解标签定义切点 - 表达式切点
ExpressionPointCut,为了支持AspectJ切点表达式语法定义的接口 - 流程切点
ControlFlowPointCut,根据程序执行堆栈信息查看目标方法是否由某一个方法直接或者间接调用,以此判断是否为匹配的连接点 - 复合切点
ComposablePointCut,为创建多个切点而提供的方便操作类
切面类型
增强包含横切代码,又包含部分连接点信息,所以我们可以通过增强类生成一个切面。切点只包含类和方法信息,所以要结合增强才能制作出切面。
- 一般切面
Advisor,仅包含一个Advice。即切面仅包含一个增强,由于它代表的连接点是目标类的所有方法,所以一般不直接使用。 - 切点切面
PointAdvisor,具有切点的切面,包含Advice和PointCut两个类。 - 引介切面
IntroductionAdvisor
三、织入切面到目标类的方法
1 通过ProxyFactory代理工厂
Spring中通过使用ProxyFactory将增强织入到目标类中,ProxyFactory内部就是使用JDK代理或CGLib代理技术,将增强织入到目标类中。
Spring定义了AopProxy接口,并提供了两个final类型的实现类:
2 通过Spring配置
在配置文件中定义如下Bean:
3 自动创建代理
Spring提供了自动创建代理机制,让容器为我们自动生成代理。在内部,Spring使用BeanPostProcessor自动完成这项工作。
基于BeanPostProcessor的自动代理创建器的实现类,将根据一些规则在容器实例化Bean时为匹配的Bean生成代理实例。代理创建器可以分为以下三类:
- 基于Bean配置名规则的自动代理器
允许为一组特定配置名的Bean自动创建代理实例,实现类为BeanNameAutoProxyCreator - 基于Advisor匹配机制的自动代理创建器
对容器中所有的Advisor进行扫描,自动将这些切面应用到匹配的Bean中,实现类为DefaultAdvisorAutoProxyCreator - 基于注解的自动代理创建器
为包含注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator
四、基于注解的AOP
在基于注解的方式中,我们利用@AspectJ来描述切点、增强,和之前的PointCut和Advice相比,两者只是表述方式不同。
下面是一个例子:
我们惊奇的发现,这个切面只是一个普通的POJO,特殊的地方在于标注了@AspectJ注解。
如何通过配置使用@AspectJ切面
在上一节中我们介绍的自动代理创建器,其中AnnotationAwareAspectJAutoProxyCreator可以将@AspectJ注解的切面织入到目标Bean中。
如果使用基于Schema的aop命名空间进行配置,那就更加简单了:
首先在配置文件中引入aop的命名空间,如①、②出所示,然后通过aop命名空间的
自动为Spring容器中那些匹配@AspectJ切面的Bean创建代理,完成切面织入(Spring内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行代理的创建工作)。
@AspectJ语法基础
1 增强类型
@Before
前置增强,相当于BeforeAdvice,有两个成员:
- value:定义切点
- argNames:指定注解所标注增强方法的参数名(两者名字必须完全相同),多个参数用逗号分离
@AfterReturning
后置增强,相当于AfterReturningAdvice,有四个成员:
- value:定义切点
- pointcut:表示切点信息,如果显示定义将覆盖value的值
- returning:将目标对象方法的返回值绑定给增强方法
- argNames:如前所述
@Around
环绕增强,相当于MethodInterceptor,有两个成员:
- value:定义切点
- argNames:如前所述
@AfterThrowing
环绕增强,相当于ThrowsAdvice,有四个成员:
- value:定义切点
- pointcut:表示切点信息,如果显示定义将覆盖value的值
- throwing:将目标对象方法的返回值绑定给增强方法
- argNames:如前所述
@After
final增强,不管是异常抛出还是正常退出,该增强都会执行,有两个成员:
- value:定义切点
- argNames:如前所述
@DeclareParents
引介增强,相当于IntroductionInterceptor,有两个成员:
- value:定义切点,表示在哪个目标类上添加引介增强
- defaultImpl:默认的接口实现类
2 切点函数表达式
Spring支持9个@AspectJ切点表达式函数,大致分为四种类型:
- 方法切点函数:通过描述目标类方法信息定义连接点
- 方法入参切点函数:通过描述目标类方法入参信息定义连接点
- 目标类切点函数:通过描述目标类类型信息定义连接点
- 代理类切点函数:通过描述目标类的代理类信息定义连接点
四种类型的切点函数如下进行说明:
@annotation()
@annotation表示标注了某个注解的所有方法。例如:
假设类NaughtyWaiter#greetTo()方法标注了@NeedTest注解,那么该方法将会被织入增强。
execution()
是最常用的切点函数,语法如下:
execution(<修饰符模式>?<返回类型模式><方法模式>(<参数模式>)<异常模式>?)
修饰符模式和异常模式是可选的。
- execution(public * *(..)):匹配目标类所有public方法
- execution(* *To(..)):匹配目标类所有以To为后缀的方法
- execution(* Waiter.*(..)):匹配Waiter接口的所有方法
- execution(* Waiter+.*(..)):匹配Waiter接口和其实现类的所有方法
- execution(* com.package.*(..)):匹配package包下所有类的方法
- execution(* com.package..*(..)):匹配package包及其子包下所有类的方法
- execution(* joke(String,int)):匹配joke(String,int)方法和其入参。如果方法中入参类型是java.lang包下的,可以直接使用类名,否则使用全限定类名
args()和@args()
args()接受一个类名,表示目标类方法入参对象是指定类时(包含子类),切点匹配。例如:
args(com.lzn.Waiter),等价于execution(* *(com.lzn.Waiter+))
表示运行时入参是Waiter类型的方法,则匹配。
@args()接受一个注解类的类名,当方法的运行时入参对象标注了指定的注解时,方法匹配切点。
- 如果在类继承树中注解点②高于入参类型点①,不会匹配
- 如果在类继承树中注解点②低于入参类型点①,匹配所在类和其子类
within()
within()定义的连接点是针对目标类而言的。语法如下所示:
within(<类匹配模式>)
各种切面类型总结
LTW(Load Time Weaving)
AOP切面织入除了通过JDK动态代理以及CGLib代理的方式实现之外,还可以通过在类加载期通过字节码编辑技术将切面植入到目标类中,这种方式叫做LTW。
Spring的LTW仅支持@AspectJ定义的切面,它利用类路径下的META_INF/aop.xml配置文件找到切面定义以及切面所要实施的候选目标类的信息,通过LoadTimeWeaver在ClassLoader加载类文件时,将切面织入到目标类中,工作原理如图所示:
Spring利用特定web容器的ClassLoader,通过LoadTimeWeaver将Spring提供的ClassFileTransformer注册到ClassLoader中。在类加载时期,注册的ClassFileTransformer读取META_INF/aop.xml配置文件,获取切面,对加载到VM中的Bean类进行字节码转换,织入切面。Spring容器初始化Bean实例时,采用的Bean类就是已经织入切面的类。
Spring的LoadTimeWeaver
大多数web应用服务器(Tomcat除外)的ClassLoader都支持直接访问Instrument,无需通过javaagent参数指定代理,拥有这种能力的ClassLoader称为“组件使能”。通过“组件使能”功能,可以非常方便的访问到ClassLoader的Instrument。Spring利用了web应用服务器类加载的这个特性,为他们提供了专门的LoadTimeWeaver。以便向特定的ClassLoader注册ClassFileTransformer,对类进行字节码转换,实现切面织入。
LoadTimeWeaver接口有三个方法:
- void addTransformer(ClassFileTransformer transformer)
添加一个ClassFileTransformer 到加载期织入器中 - ClassLoader getInstrumentableClassLoader()
我们知道JVM拥有Instrument组件,但这是JVM级别的。Spring对ClassLoader 进行扩展,让他具有Instrument组件,以便只对ClassLoader 中的类应用ClassFileTransformer - getThrowawayClassLoader()
返回一个丢弃的ClassLoader ,目的是使Instrument的作用范围仅局限在本ClassLoader 中,而不影响父类的ClassLoader
Spring只需在配置文件中加入一行配置就能启用LoadTimeWeaver
使用LTW织入切面实例
第一步:定义切面
第二步:创建可被增强类
第三步:Spring配置文件
第四步:在src下创建META-INF目录,并在该目录下新增AspectJ的配置文件aop.xml:
// 切面
// 要织入增强的目标类