前言:
- 上一章中已经介绍,Spring中定义一个切面是比较麻烦的,需要实现专门的接口,并进行一些较为复杂的配置。经过改进,如今Spring AOP已经焕然一新,用户可以使用@AspectJ注解非常容易的定义一个切面,而不需要实现任何接口。
- 对于jdk5.0以下的项目,则可以通过基于Schema的配置定义切面
1、Spring对AOP的支持
spring2.0以后对AOP功能进行了重要的增强,主要变现在以下几个方面:
- 新增了基于Schema的配置支持,为AOP提供了专门的aop命名空间;
- 新增了对AspectJ切点表达式语言的支持,通过注解在POJO中定义切面;
- 无缝集成AspectJ
2、JDK5.0注解知识
2.1、什么是注解
(1)注解是Javadoc标签和Xdoclet标签的延伸和发展,在java5.0以上版本,我们可以自定义这些标签,并通过java语言的反射机制获取类中标注的注解,完成特定的功能。
(2)注解是代码的附属信息,它遵循一个基本原则:注解不能直接干扰程序代码的运行,无论增加或删除注解,代码都能正常运行。
2.2、一个简单的注解类
(1)声明一个注解
@Retention(RetentionPolicy.RUNTIME)//①声明注解的保留期限
@Target(ElementType.METHOD)//②声明可以使用该注解的目标类型
public @interface TestInterface{//③定义注解
boolean value() default true;//④声明注解成员
}
(2)注解的成员声明有以下几点限制:
- 成员以无入参、无抛出异常的方式声明,如int value(String aa),boolean value() throws Exception等都是非法的;
- 可以通过default为成员指定一个默认值;
- 成员类型是受限的,合法的类型包括原始类型及其封装类、String、Class、枚举类、注解类型、上述类型的数组类型;
- 如果注解只有一个成员,则成员名必须取为value(),这样在使用时,可以忽略成员名和赋值等号(=);
- 如果注解有多个成员,只对value赋值时可以省略value和赋值等号(=),多个成员赋值用逗号隔开;
- 所有的注解都隐式继承于Annotation,但注解不允许显式继承其他接口;
- 如果成员是数组类型,则使用“{xxx,xxx,xxx}”将值赋给成员名。
(3)jdk5.0以后,对于Package、Class、Constructor、Method、Field等反射对象都新增了访问注解信息的方法,该方法支持通过泛型直接返回注解对象。
3、着手使用@AspectJ
3.1、使用前的准备
因为java反射机制无法获取入参名,所以需要引入spring的asm模块,asm是轻量级的字节码处理框架,spring就利用asm处理@AspectJ中所描述的方法入参名。
3.2、一个简单的例子
@Aspect--------------//①通过该注解将类标识为一个切面
public class TestAspectj{
@Before("execution(* greetTo(..))")-----------------------//②定义切点和增强类型
public void beforeGreeting(){---------------------//③增强的横切逻辑
..............................
}
}
注:②处的切点表达式的意思是,在greetTo方法处织入增强,greetTo方法可以有任意入参和返回值。
3.3、通过配置使用@Aspect切面
(1)如同spring使用proxyfactory将切面织入目标对象,AspectJ使用AspectJProxyFactory将切面织入目标对象。
(2)AspectJ的自动代理创建器是
<bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/>
(3)抛开自动代理创建器,使用基于Schema的aop命名空间进行配置更简单:
<!-- 基于@Aspect切面的驱动器 -->
<aop:aspectj-autoproxy/>
<bean id="target" class="目标对象"/>
<bean class="注解指定的切面类"/>
当然,spring内部依旧采用AnnotationAwareAspectJAutoProxyCreator进行自动代理的创建工作,但具体实现细节已经被<aop:aspectj-autoproxy/>隐藏起来了。
<aop:aspectj-autoproxy/>有一个属性proxy-target-class属性:
- 默认为false,表示采用jdk的动态代理织入切面;
- 值为true,表示采用cglib的动态代理织入增强;
- 不过即使proxy-target-class属性设置为false,如果目标类没有声明接口,则spring将自动使用cglib动态代理。
3.4、@Aspect语法基础
(1)切点表达式函数
切点表达式由关键字和操作参数组成,比如execution(* greetTo(..)),exccution就是关键字,* greetTo(..)就是操作参数。
spring支持9种@AspectJ切点表达式函数,根据描述对象的不同,可以分为4种类型:
- 方法切点函数:通过描述目标类方法信息定义连接点;
- 方法入参切点函数:通过描述目标类方法入参的信息定义连接点;
- 目标类切点函数:通过描述目标类的代理类的信息定义连接点;
- 代理类切点函数:通过描述目标类的代理类的信息定义连接点。
4种类型的切点函数如图:
(2)在函数入参中使用通配符
@Aspect支持3种通配符:
- * 匹配任意字符,但只能匹配上下文中的一个元素;
- .. 匹配任意字符,可以匹配上下文中的多个元素,但在表示类时,必须和*联合使用,而在表示入参时,则单独使用;
- + 表示按类型匹配指定类的所有类,必须跟在类名后面,如com.baobaotao.Car+。表示匹配Car类及其子类;
@AspectJ函数按其是否支持通配符及其支持的程度,可以分为以下3类:
- 支持所有通配符:execution(),within();
- 仅支持“+”通配符:args(),this(),target();
- 不支持通配符:@args(),@within(),@target()和@annotation()。
(3)逻辑运算符
切点表达式由切点函数组成,切点函数之间还可以进行逻辑运算,组成复合切点,
spring支持以下的切点运算符:&&,||,!
由于以上在XML中是特殊字符,spring在XML中的切点表达式函数提供and,or,not作为替代。
注:当使用not时,前面得有空格,否则报解析错误。
(4)不同的增强类型
@AspectJ为各种增强类型提供了不同的注解
①@Before 相当于BeforeAdvice,它有2个成员:
- value 该成员用于定义切点;
- argNames 由于无法通过反射机制获取方法入参名,所以如果在Java编译时未启用调试信息或者需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(注意两者名字必须完全相同),多个参数名用逗号分隔。
②@AfterReturning 后置增强,相当于AfterReturningAdvice,AfterReturning,它拥有4个成员:
- value 该成员用于定义切点;
- pointcut 表示切点的信息,如果显示指定point值,它将覆盖value的设置值,可以将pointcut成员看成是value的同义词;
- returning 将目标对象方法的返回值绑定给增强的方法;
- argNames 如前所述。
③@Around 环绕增强,相当于MethodInterceptor,它有2个成员:
- value 该成员用于定义切点;
- argNames 如前所述。
④@AfterThrowing 抛出增强,相当于ThrowsAdvice,AfterThrowing,它有4个成员:
- value 该成员用于定义切点;
- pointcut 表示切点的信息,如果显示指定point值,它将覆盖value的设置值,可以将pointcut成员看成是value的同义词;
- throwing 将抛出的异常绑定到增强方法中;
- argNames 如前所述。
⑤@After Final增强,不管是抛出异常还是正常退出,该增强都会得到执行,该增强没有对应的增强接口,可以把它看成ThrowsAdvice和AfterReturningAdvice的混合物,一般用于释放资源,它有2个成员:
- value 该成员用于定义切点;
- argNames 如前所述。
⑥@DeclareParents 引介增强,相当于IntroductionInterceptor,它有2个成员:
- value 该成员用于定义切点,它表示在哪个目标类上添加引介增强,即不需要使用函数表达式,直接指定全类名;
- defaultImpl 默认的接口实现类。
(5)引介增强的具体用法
引介增强通过切面技术将目标类要实现接口的子类融合到目标类中,目标类持有目标接口的实现类的引用,这样目标类就相当于实现了目标接口。
3.5、切点函数详解
(1)execution()
execution()是最常用的切点函数,其语法如下所示:
execution(<访问修饰符>?<返回类型><方法名>(<方法参数>)<异常>?)
?:表示前面的内容可选,即除了返回类型,方法名和方法参数,其他都是可选的。
具体示例:
(2)args()和@args()
(3)命名切点
在前面的例子中,切点直接声明在增强方法处,这种切点声明方式称为匿名切点,匿名切点只能在声明处使用。如果希望其他地方重用一个切点,我们可以通过@Ponitcut注解以及切面类方法对切点对切点进行命名。
具体示例如下:
(4)增强织入的顺序
一个连接点可以同时匹配多个切点,切点对应的增强在连接点上的织入顺序是如何安排的呢?
这个问题要分3种情况讨论:
- 如果增强在同一个切面类中声明,则依照增强在切面类中定义的顺序进行织入;
- 如果增强位于不同的切面类中,且这些切面类都实现了org.springframework.core.Ordered接口,则顺序号小的先织入;
- 如果增强位于不同的切面类中,且这些切面没有实现Ordered接口,则织入的顺序是不确定的。
(5)访问连接点信息
AspectJ使用org.aspectj.lang.JoinPonit接口表示目标类连接点对象,如果是环绕增强时,使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。任何一个增强方法都可以通过将第一个入参声明为JoinPoint访问到连接点上下文的信息(一定要在第一个位置)。
JoinPonit和ProceedingJoinPoint的方法签名如下:
(6)绑定连接点方法入参
(7)绑定代理对象
(8)绑定返回值
3.6、基于Schema配置切面
(1)基于Schema的配置就是将切面信息移到XML中,一个配置示例如下:
(2)配置命名切点:
在7-15②处通过pointcut属性声明的切点是匿名切点,它不能被其他增强或其他切面引用。spring提供了命名切点的配置方式,如下所示:
(3)各种增强类型的配置
(4)绑定连接点信息
(5)Advisor配置
一个配置示例:
3.7、混合切面类型
(1)配置示例:
(2)切面类型总结:
4、JVM Class文件字节码转换基础知识
在类加载期织入切面。