在Spring中定义一个切面是比较繁琐的,需要实现专门的接口,并要进行一些较为复杂的配置,SpringAOP的配置是批评最多的地方。Spring听到这些声音下决心解决这些问题,并取得了很好地突破。现在SpringAOP可以使用@AspectJ注解很简单的定义一个切面,而不需要实现任何接口,对于没有使用java5.0的项目,可以通过基于Schema的配置定义切面。
Spring对AOP进行了重要的增强,主要体现在以下几个方面:
- 新增了基于Schema的配置支持,为AOP专门提供了aop命名空间。
- 新增了对AspectJ切点表达式语言的支持。
- 可以无缝集成AspectJ。
这里所说的SpringAOP包括基于XML配置的AOP和基于@AspectJ注解的AOP,这两种方法虽然在配置切面变现方式不同,但底层都是采用了动态代理技术(JDK和CGLib)。
Spring可以集成AspectJ,但AspectJ本身并不属于SpringAOP的范畴。
编程时,除了编写代码外,我们还会在程序中使用javadoc标签,对类或者成员变量进行注释,如@param、@return等这些javadoc标签。
java中可以自定义这些标签,并通过java语言的反射机制,获取类中标注的注解,完成特定的功能。
注解是代码的附属信息,它遵循一个基本的原则:注解不能直接干扰程序代码的运行,无论增加或者删除注解,代码都能够正常运行,java语言解释器会忽略这些注解,而由第三方工具对注解进行处理。
@Retention(RetentionPolicy.RUNTIME)//声明注解的保留期限
@Target(ElementType.METHOD)//声明可以使用该注解的目标类型
public @interface NeedTest {//定义注解
boolean value() default true;//声明注解成员
}
java语法规定使用@interface修饰符定义注解类,一个注解可以拥有多个成员,成员成名和接口方法声明类似,这里仅定义一个成员,成员声明有以下几点限制:
- 成员以无入参、无抛出异常的的方式声明。如boolean value(int a)或者boolean value() throws Exception等方式是非法的。
- 可以通过default为成员指定默认值。如String level() default “hh”、int high() default 2是合法的,当然也可以不指定默认值。
- 成员的类型是受限的,合法的类型包括原始类型和及其封装类、String、Class、enums、注解类型,以及上述类型的数组类型,如ForumService value、List foo()是非法的。
Retention和Target是java预定义注解,称为元注解,它们被java编译器使用,会对注解类的行为产生影响。
@Retention(RetentionPolicy.RUNTIME)表示这个注解可以在运行期被jvm读取。注解的保留期限类型在java.lang.annotation.Retention类定义:
- SOURCE:注解信息仅保留在目标类代码的源码文件中,但对应的字节码文件将不保留。
- CLASS:注解信息将进入目标类代码的字节码文件中,但类加载器加载字节码文件时不会将注解加载到JVM中,即运行期不能获取注解信息。
- RUNTIME:注解信息在目标类加载到JVM后依然保留,在运行期可以通过反射机制读取类中的注解信息。
@Target(ElementType.METHOD)表示注解只能应用到目标类的方法上,注解的应用目标在java.lang.annotation.ElementType类中定义:
- TYPE:类、接口、注解类、Enum声明处,相应的注解称为类型注解。
- FIELD:类成员变量或者常量声明处,相应的注解称为域值注解。
- METHOD:方法声明处,方法注解
- PARAMETER:参数声明处,参数注解
- CONSTRUCTOR:构造函数声明处,构造函数注解。
- LOCAL_VARIABLE:局部变量声明处,局部变量注解。
- ANNOTATION_TYPE:注解类声明处,注解类注解
- PACKAGE:包声明处,包注解
如果注解只有一个成员,则成员名必须取名为value(),在使用时可以忽略成员名和赋值号(=),如NeedTest(true)。当注解类有多个成员时,如果仅对value()成员进行赋值,则也可以不使用赋值号,如果同时对多个成员进行赋值,则必须使用赋值号。
注解类可以没有成员,,没有成员的注解称为标识注解,解释程序以标识注解存在与否进行相应的处理。此外,所有的注解类都隐式继承于java.lang.annotation.Annotation,但是注解不允许显式继承其它的类。
public class ForumService {
@NeedTest(true)
public void deleteForum() {
System.out.println("删除论坛模块...");
}
@NeedTest(value=true)
public void removeForum() {
System.out.println("删除论坛模块...");
}
}
如果注解和目标类不在同一个包中,则需要通过import引用注解类。如果成员是数组类型可以通过{}进行
赋值。如value={“a”,"b"}
注解不会直接影响程序运行,但是第三方程序或者功能可以利用代码中的注解完成特殊的任务,间接控制程序的运行。
public class MyTest{
public static void main(String[] arg) throws Exception {
//访问注解
Class clazz=ForumService.class;
Method method[]=clazz.getMethods();
System.out.println(method.length);
for(Method m:method) {
NeedTest need=m.getAnnotation(NeedTest.class);
if(need!=null) {
if(need.value()) {
System.out.println("需要测试");
}else {
System.out.println("bu 需要测试");
}
}
}
}
}
结果:
11
需要测试
需要测试
aspectj
aspectjrt
1.5.4
aspectj
aspectjweaver
1.5.3
public interface WaitersInteface {
public void greetTo(String name);
public void serveTo(String name);
}
public class Waiter implements WaitersInteface{
@Override
public void greetTo(String name) {
System.out.println("greet to "+name+"....");
}
@Override
public void serveTo(String name) {
System.out.println("serving to "+name+"....");
}
}
@Aspect
public class GreetAspectJ{
@Before("execution(* greetTo(..))")
public void beforeGreet() {
System.out.println("how are you!~");
}
}
结果:
how are you!~
greet to cui....
serving to wang....
@AspectJ5.0的切点表达式由关键字和操作参数组成,如切点表达式execution(* greetTo(…)),execution为关键字,而 * greetTo(…)为操作参数。在这里execution代表目标类执行某一方法,而(* greetTo(…))描述目标方法匹配的字符串,二者联合表示目标类的greetTo()方法的连接点。
Spring支持9个@AspectJ切点表达式函数,它们用不同的方式描述目标类的连接点,根据描述对象的不同,可以大致分为四种类型。
有些函数入参可以使用通配符,@AspectJ支持三种通配符:
* 匹配任意字符,但它只能匹配上下文中的一个元素。
.. 匹配任意字符,可以匹配上下文中多个元素,但在表示类时,必须和*联合使用,而在表示入参时则
单独使用。
+ 表示按类型匹配指定类的所有类,必须跟在类名后面,如com.spring.Waiter+。继承或拓展指定类的
所有类,同时还包括指定类本身。
@AspectJ按照其是否支持通配符和其支持的程度,可以分为3类:
1.支持所有通配符execution()和within()
2.仅支持+的通配符:args(),this()和target()
3.不支持通配符:@args()、@within()、@target和@annotation
这七个函数除了可以指定类名外,也可以指定变量名,并将目标类型中的变量绑定到增强方法中
Spring支持切点运算符 && 、|| 、!对应and or not
如果not位于切点表达式的开头,则必须在开头添加一个空格,否则将产生解析错误。
- @Before前置增强,相当于BeforeAdvice。Before注解类拥有两个成员。
value成员用于定义切点。
argNames:由于无法通过java反射机制获取方法入参名,所以如果在java编译时未启用调试信息,或者需要在运行期解析切点就必须通过成员指定注解所标注增强方法的参数名(注意二者参数必须完全相同,多个参数用逗号分隔)- @AfterReturning
后置增强相当于AfterReturnAdvice。@AfterReturning注解有四个成员:
value:该成员用于定义切点。
pointcut:表示切点信息,如果显示指定pointcut值,那么它将覆盖value的设置值,可以将pointcut成员看成value的同义词。
returning:将目标对象方法的返回值绑定给增强方法。
argNames:如前所述。- @Around
环绕增强,相当于MethodInterceptor。Around注解类拥有两个成员。
value:该成员用于定义切点。
argNames:如前所述。- AfterThrowing
异常抛出增强,相当于ThrowsAdvice。
value:该成员用于定义切点。
pointcut:表示切点信息,如果显示指定pointcut值,那么它将覆盖value的设置值,可以将pointcut成员看成value的同义词。
throwing:抛出异常绑定到增强方法中。
argNames:如前所述。
切点函数是AspectJ表达式语言的核心,也是使用@AspectJ进行切面定义的难点。
假设有如下类的结构:
@annotation
@annotation表示标注了某个注解的所有方法。
@Aspect
@EnableAspectJAutoProxy
public class AnnotationAspectJ{
@AfterReturning("@annotation(com.spring.NeedTest)")
public void needTest() {
System.out.println("needTestFun!");
}
}
execution
execution是最常用的切点函数。语法如下:
execution(<修饰符模式>? <返回类型模式> <方法名模式> (<参数模式>) <异常模式>?)
除了返回类型模式、方法名模式和参数模式以外,其他类型都是可选的。
通过方法签名定义切点
execution(public * (…)):匹配所有目标类的public方法。第一个代表返回类型,第二个*代表方法名,而…代表入参的方法。
通过类定义切点
execution(* com.smart.Waiter.(…)) 第一个代表返回任意类型,第二个代表com.smart.Waiter这个类路径下的所有方法
execution( com.smart.Waiter+.(…)) 匹配Waiter类以及所有实现类的方法。
在类名模式串中,“.”代表包下的所有类,而“…*”表示包、子孙包的所有类。
通过方法入参定义切点
execution(* joke(String,)) 匹配目标类中的joke方法中第一个参数是String类型,第二个参数可以是任意类型。
execution( joke(Object+)):匹配返回值为任意类型,目标类为joke的方法,方法中拥有一个入参,且入参是Object类或该类的子类。
args()和@args()
args()函数的入参是类名,而@args()函数的入参必须是注解类的类名,虽然args()允许在类名后,使用"+"通配符,但该通配符在此处没有意义,添加和不添加效果都一样。
- args()该函数接收一个类名,表示目标类方法入参对象时指定的类包括子类时,切点匹配如下:
args(com.spring.Waiter)表示运行时入参是Waiter类型对象的方法。这和execution(* (com.spring.Waiter))的区别在于后者表示类的方法签名而言,前者针对运行时的入参类型而言。args(com.spring.Waiter)实际上等价于args(com.spring.Waiter+)就是包括Waiter的本类及其子类。而execution( *(com.spring.Waiter))只包括这一种一类型对象。- @args():该函数接收一个注解类的类名,当方法运行时入参对象标注了指定的注解时,匹配切点。
within()
通过类匹配模式串声明切点。within函数定义的连接点是针对目标类而言,而非针对运行时的对象类型而言。这一点和execution是相同的。但和execution函数不同的是,within函数连接点最小的范围只能是类。而execution所指定的连接点范围可以大到包、小到方法入参。所以从某种意义上说,execution函数包括了within函数的功能。
语法:within(<类匹配模式>)
within(com.spring.Waiter),表示匹配目标类Waiter下的所有方法。
@within()和@target()
除了@annotation()和@args函数外,还有另外两个用于注解的切点函数,分别是@target()和@within()函数。和@annotation()和@args()函数一样,@within()和@target()也只接受注解类名作为入参。其中@target(M)匹配任意标注了@M类的目标类,@within(M)匹配了标注了@M的类和子孙类。
有一个特别值得注意的地方:
如果标注@M是一个接口,则所有实现该接口的类不匹配@within(M).假设Waiter这个接口标注了@Monitorable注解,但它的实现类Awaiter和Bwaiter以及Cwaiter这些实现类都没有标注@Monnitorbale注解。则@within(com.spring.Monitorable)或者@target(com.spring.Monitorable)都不匹配Awaiter和Bwaiter以及Cwaiter。这是因为@target和@within以及@annotation都是针对目标类而言的,而非针对运行时的引用类型而言。这点的区别在开发中要特别的注意。
target()和this()
target()切点函数通过判断目标类是否按照类型匹配指定类来决定连接点是否匹配,而this函数则通过判断代理类是否按类型匹配指定类来决定是否切点匹配。二者都只接受类名的入参,虽然都可以带通配符+,但对于二者使用和不使用+,效果相同,都可以匹配子类。
tatget(M):表示如果目标类按类型匹配与M,则目标类的所有方法都匹配与M。
target(com.spring.Waiter):表示Waiter和子类Awaiter以及子类Bwaiter都匹配切点。
this():this()函数判断代理对象的类是否按类型匹配与指定类,如果匹配则代理对象的所有连接点匹配切点。
@AspectJ可以使用切点函数定义切点,还可以使用逻辑运算符,对切点进行复合运算,得到复合切点。为了在切面中重用切点,还可以对切点进行命名,以便在其他地方引用定义过的切点。当一个连接点匹配多个切点时,需要考虑织入顺序的问题,另外一个重要的问题是如何在增强中访问连接点上下文信息。
切点复合运算
@Aspect
@EnableAspectJAutoProxy
public class GreetAspectJ{
@After("within(com.spring.*)"
+" && execution(* greetTo(..))")
public void afterGreet() {
System.out.println("OK,compeleted");
}
}
命名切点
前面的所展示的例子中,切点直接声明在增强方法处,这种切点声明方式称为匿名切点,匿名切点只能在声明处使用,如果希望在其它地方重用一个切点,则可以通过@Pointcut注解及切面类方法切点进行命名。
public class TestAspectJ{
//通过注解方法inPackage()对该切点进行命名,
//方法可视域修饰符为private,
//表明该命名切点只能在本切面类中使用
@Pointcut("within(com.spring.*)")
private void inPackage() {}
//通过注解方法greetTo()对该切点进行命名,
//方法可视域修饰符为protected,表明该切点
//可以在当前包中的切面类、子切面类中使用。
@Pointcut("execution(* greetTo(..))")
protected void greetTo() {}
//引用命名切点定义的切点,本切点也是命名切点,
//它对应的可视域为public
@Pointcut("inPackage() and greetTo()")
public void inGreetTo() {}
}
@Aspect
@EnableAspectJAutoProxy
public class GreetAspectJ{
@Before("TestAspectJ.inPackage")
public void beforeGreet() {
System.out.println("OK,compeleted");
}
}
增强织入顺序
一个连接点可以自动匹配多个切点,切点对应的增强在连接点上的织入顺序怎样的呢?分为三种情况:
- 如果增强在同一切面类中声明,则依照增强的在切面类中定义的顺序进行织入。
- 如果增强位于不同的切面类中,且这些切面类都实现了org.springframework.core.Ordered接口,则由接口的方法顺序号执行。(顺序号小的先织入)
- 如果增强位于不同的切面类中,且这些切面类没有实现org.springframework.core.Ordered接口,则织入顺序是不确定的。
基于@AspectJ注解的切面,本质上是将切点、增强类型的信息使用注解进行描述,现在把这两个信息移到Schema配置文件中。使用基于Schema的切面定义以后,切点、增强类型的注解信息,从切面类中剥离出来,原来切面类也就蜕变为真正意义上的POJO。
public interface WaitersInteface {
public void greetTo(String name);
public void serveTo(String name);
}
public class Waiter implements WaitersInteface{
@Override
public void greetTo(String name) {
System.out.println("greet to "+name+"....");
}
@Override
public void serveTo(String name) {
System.out.println("serving to "+name+"....");
}
}
public class AdviceMethod {
public void preGreet() {
System.out.println("--HI--");
}
}
public class MyTest{
public static void main(String[] arg) throws Exception {
String path="classpath:applicationContext.xml";
ApplicationContext aContext=new ClassPathXmlApplicationContext(path);
if(aContext.containsBean("waiter")) {
System.out.println("111");
}
Waiter waiter=(Waiter) aContext.getBean("waiter");
waiter.greetTo("cui");
waiter.serveTo("zz");
}
}
结果:
111
--HI--
greet to cui....
serving to zz....
使用aop:aspect元素标签定义切面,其内部可以定义多个增强,在aop:config元素中可以定义多个切面。由于&&在XML中使用不便,所以一般用and操作符代替。
aop:config拥有一个proxy-target-class属性,当设置为true时,表示其中声明的切面均使用CGLib动态代理技术,当设置为false时,使用java动态代理技术。一个配置文件可以同时定义多个aop:config,不同的aop:config可以采取不同的代理技术。
**********后置增强**********
public class AdviceMethod {
public void preGreet() {
System.out.println("--HI--");
}
public void afterReturn(int retVal) {
System.out.println("--afterReturn--");
}
}
如果增强方法不希望接收返回值,将配置的returning属性和增强方法的入参去掉即可。
public class MyTest{
public static void main(String[] arg) throws Exception {
String path="classpath:applicationContext.xml";
ApplicationContext aContext=new ClassPathXmlApplicationContext(path);
if(aContext.containsBean("waiter")) {
System.out.println("111");
}
Waiter waiter=(Waiter) aContext.getBean("waiter");
waiter.greetTo("cui");
waiter.serveTo("zz");
}
}
结果:
111
--HI--
greet to cui....
--afterReturn--
serving to zz....
--afterReturn--
**********环绕增强**********
public class AdviceMethod {
public void preGreet() {
System.out.println("--HI--");
}
public void afterReturn(int retVal) {
System.out.println("--afterReturn--");
}
//环绕增强方法,pjp可以访问到环绕增强的连接点
public void aroundMethod(ProceedingJoinPoint pjp) {
System.out.println("--around--");
}
}
**********抛出增强**********
public class AdviceMethod {
public void preGreet() {
System.out.println("--HI--");
}
public void afterReturn(int retVal) {
System.out.println("--afterReturn--");
}
//环绕增强方法,pjp可以访问到环绕增强的连接点
public void aroundMethod() {
System.out.println("--around--");
}
public void afterThrowMethod(IllegalArgumentException iea) {
System.out.println("--afterThrowMethod--");
}
}
**********Final增强**********
public class AdviceMethod {
public void preGreet() {
System.out.println("--HI--");
}
public void afterReturn(int retVal) {
System.out.println("--afterReturn--");
}
//环绕增强方法,pjp可以访问到环绕增强的连接点
public void aroundMethod() {
System.out.println("--around--");
}
public void afterThrowMethod(IllegalArgumentException iea) {
System.out.println("--afterThrowMethod--");
}
public void afterMethod(IllegalArgumentException iea) {
System.out.println("--afterMethod--");
}
}
**********引介增强**********
使用配置引介增强,引介增强和其他增强都不一样,它没有method、pointcut
和point-ref属性。
public class TestBeforeAdvice implements MethodBeforeAdvice{
@Override
public void before(Method method, Object[] args, Object target)
throws Throwable {
System.out.println("##before##");
}
}
public class MyTest{
public static void main(String[] arg) throws Exception {
String path="classpath:applicationContext.xml";
ApplicationContext aContext=new ClassPathXmlApplicationContext(path);
if(aContext.containsBean("waiter")) {
System.out.println("111");
}
Waiter waiter=(Waiter) aContext.getBean("waiter");
waiter.serveTo("zz");
waiter.greetTo("cui");
}
}
结果:
111
serving to zz....
##before##
greet1 to cui....
我们已经掌握了四种切面定义切面的方式:
- 基于@AspectJ注解的方式
- 基于aop:aspectJ的方式
- 基于aop:advisor的方式
- 基于Advisor类的方式
作为开发者可能会觉得Spring在一个问题上提供了太多的选择,这样是不是让选择陷入困境,Spring是否在做费力不讨好的事情呢?其实,开发人员完全可以根据自己的项目进行选择:
- 如果项目采用java5.0,则可以有限使用@AspectJ注解方式
- 如果项目只能使用低版本的JDK,那么可以考虑使用aop:aspect
- 如果正在升级一个低版本的S平AOP则考虑使用aop:advisor进行复用已存在Advice类
- 如果项目只能使用低版本的Spring,那么只能使用Advisor
到目前为止,我们所接触到的AOP切面织入都是在运行期通过JDK或者CGLib的动态代理的方式实现的。我们知道除了运行期织入切面的方式之外,可以在类加载期通过,字节码编辑技术将切面织入目标类当中,这种织入方式称为LTW(load time weaving)