Spring学习笔记:基于注解的Sprig AOP配置的深入学习
在上一个在学习刘Java的关于Spring的AOP的学习中,算是对这个Spring的AOP的理解有一个深入。首先不是在对于这个AOP只有一个概念的理解,所谓一种实践方式,人家也提供了ApectJ这些工具或者说是规范。Spring AOP对于AOP是一种扩展和支持。有了XML肯定对应的还会有注解的形式的支持,对应各个标签会有对应的各个注解。
1 注解配置概述
如同IOC支持基于注解的两种基本配置方式,一个是常见的XML配置文件方式,另一个是项目中最常用到的注解方式。什么情况来采用什么配置方式,要区分情况:
XML配置方式,看起来更像“POJO”,xml的配置方式对于代码是“无侵入”的,配置文件和代码是分开的。而且由于是在配置文件中集中,因为集中所以要方便管理配置。
基于注解方式,代码和配置封装在一个类中,和代码在一起会更直观,方便理解,以及模块化。
使用注解还有一个好处就是可以在Spring AOP和Aspectj框架之间无缝切换,方便后续功能升级(如果需要超过Spring AOP的功能,虽然大概率不会出现)
Spring团队推荐基于注解方式。
PS:
Spring AOP 框架 AspectJ框架
这里说一下Spring AOP和AspectJ的AOP框架,两个是单独的两个框架,前面可能我的表达意思不对。
但是Spring AOP在表达式什么之类的使用方式是和AspectJ是相同的,但是在实现的基础上也不一样。Spring AOP是基于动态代理的实现的(JDK的反射/cglib),这个AspectJ框架在编译或者加载的时候代码织入来完成。
2 相关依赖
基于注解所需的依赖和基于XML所需的依赖一致,其中spring-context包括Spring IoC和Spring AOP等核心依赖。
而aspectjweaver则是AspectJ框架的依赖,Spring使用该依赖来解析AspectJ的切入点表达式语法,以及AOP的注解支持。
<properties><spring-framework.version>5.2.8.RELEASE</spring-framework.version><aspectjweaver>1.9.6</aspectjweaver>
</properties>
<dependencies><!--spring 核心组件所需依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring-framework.version}</version><scope>compile</scope></dependency><!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver --><!--用于解析AspectJ的切入点表达式语法,以及AOP的注解支持--><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>${aspectjweaver}</version></dependency>
</dependencies>
3 开启AOP注解支持
想使用AOP注解,就需要开启注解支持。开启注解支持可以用XML和Java风格的配置启用注解。
3.1 基于XML的配置
引入AOP相关schema文件,加入AOP的命名空间< aop:aspectj-autoproxy/>标签就可以。Spring将会查找被@AspectJ注解标注的bean。
表明这个bean是一个切面的bean,然后会进行AOP的自动配置,比如里面的通知和切入点表达式解析。
关于代理对象的实现方式选择,对应的属性是proxy-class-target,默认的是false,也就是默认的实现方式是JDK代理方式,如果遇到的是非接口方式,JDK代理没有办法就采用cglib,这样的默认流程。如果选择的是true就是,采用cglib的动态代理方式。
<?xml version="1.0" encoding="UTF-8"?>
<!--加入AOP 相关schema-->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aophttps://www.springframework.org/schema/aop/spring-aop.xsd"><!--开启注解自动配置--><aop:aspectj-autoproxy/>
</beans>
3.2 基于Java的配置
和Ioc的注解支持,可以使用Java配置开启,AOP的注解支持同样可以使用Java注解配置开启,舍弃XML文件使用Java代码的方式。
/*** @author lx*/
@Configuration
@ComponentScan("com.spring.aop.annotation")
//Java配置开启AOP注解支持
@EnableAspectJAutoProxy
public class MainConfiguration {
}
@Configuration和@ComponentScan,是IoC的Java注解配置,重要的是@EnableAspectJAutoProxy注解,该注解开启Spring AOP的注解自动配置。
它的proxyTargetClass属性用于指定使用哪一种代理,默认为false,表示先使用JDK代理,不符合要求再使用Cglib代理,改成true就表示使用CGlib代理。
4 AOP注解配置第一例
新建maven工程,引入依赖通过注解开启AOP注解支持。
5 @Aspect切面类
@Aspect注解对应XML配置中的< aop:aspect >标签
在开启Spring AOP注解自动配置支持,Spring容器将会在扫描包路径下(@ComponentScan配置的路径)扫描具有@Aspect注解的bean。该类被当作切面类并且用于自动配置Spring AOP。
@Aspect注解不会被Spring组件扫描工具自动检测,所以需要在这个切面类上添加一个@Component(对应的@Component注解扩展@Repository,@service和@controller这些注解也可以。),将这些注册到Spring容器。
切面类可以有自己的字段和方法,这些字段和方法可以包含有pointcut切入点,advice通知和introduction声明,可以进行绑定。
在Spring AOP,切面类本身不能做其他切面通知的目标,类上标注@Aspect注解之后,该类的bean将从AOP自动配的的bean中排除。所以切面类的方法不能被代理。
/*** 一个切面类** @author lx*/
@Component
@Aspect
public class AspectMethod1 {/*** 一个切入点,绑定到方法上,该切入点匹配“任何方法”*/@Pointcut("execution(* *(..))")public void pointCut() {}/*** 一个前置通知,绑定到方法上*/@Before("pointCut()")public void before() {System.out.println("----before advice----");}/*** 切面类的方法,无法被增强*/public void aspectMethod1() {System.out.println("aspectMethod1");}
}
//-----------------
/*** 一个切面类** @author lx*/
@Component
@Aspect
public class AspectMethod2 {/*** 一个切入点,绑定到方法上,该切入点匹配“任何方法”*/@Pointcut("execution(* *(..))")public void pointCut() {}/*** 一个前置通知,绑定到方法上*/@Before("pointCut()")public void before() {System.out.println("----before advice----");}/*** 切面类的方法,无法被增强*/public void aspectMethod2() {System.out.println("aspectMethod2");}
}
@Test
public void aspectMethod() {//1 获取容器AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);//2 获取切面类beanAspectMethod1 am1 = ac.getBean("aspectMethod1", AspectMethod1.class);AspectMethod2 am2 = ac.getBean("aspectMethod2", AspectMethod2.class);//2 调用切面类方法,并不会被代理am1.aspectMethod1();am2.aspectMethod2();
}
aspectMethod1
aspectMethod2
返回的结果,没有看到对应匹配的before这些输出,说明切面类的方法没有被加强。
6 @Pointcut切入点
@Pointcut对应着XML配置< aop:pointcut > 标签,都用来定义一个切入点,来控制什么地方,什么情况下执行通知,切入点表达式是一致的。
@Pointcut注解标注一个切面类的方法,该方法名是该切入点的名字。在通知中通过名字引用该切入点,也可以将多个切入点通过名字引用使用&&、||、!组合起来成为一个新的切入点,这是基于XML的配置所不具备的功能。
企业应用程序,模块化把所有切入点定义在一个切面类,该类专门用于存放切入点,其他切面类用于存放通知,切入点在所有切面类中是共享的。
/**1. 切面类,专门用于存放切入点定义*/
@Aspect
@Component
public class AspectPointcut {/*** 任何方法的切入点*/@Pointcut("execution(* *(..))")public void pointcut1() {}/*** AspectPointcutTarget类的target1方法的切入点*/@Pointcut("execution(* *..AspectPointcutTarget.target1(..))")public void pointcut2() {}/*** AspectPointcutTarget类的任何方法的切入点*/@Pointcut("within(*..AspectPointcutTarget)")public void pointcut3() {}/*** 第一个参数是String类型的任何方法的切入点*/@Pointcut("args(String,..)")public void pointcut4() {}/*** 通过切入点名称组合切入点* <p>* AspectPointcutTarget类的第一个参数是String类型的任何方法的切入点*/@Pointcut("pointcut3() && pointcut4()")public void pointcut5() {}//…………
}
7 注解配置通知
advice通知同样可以使用注解声明,并且绑定到一个方法,一共有5种通知注解。
对应的XML的注解:< aop:before>,< aop:after-returning >,< aop:after-throwing>,< aop:after>,< aop:around>这几个标签,
1 befoer :用于配置前置通知before advice,在切入点方法之前执行。
2 afterReturning:用于配置后置通知after-returning advice,在切入定之后执行
3 afterThrowing:用于配置后置通知after-throwing advice,切入点方法和后置通知抛出异常可能会执行
4 after:配置前置通知 after advice,无论切入点方法是否执行,都会在其后执行
5 around:用于配置环绕通知
/*** 切面类*/
@Aspect
@Component
public class AspectAdvice {/*** 切入点,用于筛选通知将在哪些方法上执行*/@Pointcut("execution(* AspectAdviceTarget.target())")public void pt() {}//五种通知/*** 前置通知* 可以通过名称引用定义好的切入点*/@Before("pt()")public void before() {System.out.println("---before---");}/*** 后置通知* 也可以定义自己的切入点*/@AfterReturning("execution(* AspectAdviceTarget.target())")public void afterReturning() {System.out.println("---afterReturning---");}/*** 异常通知*/@AfterThrowing("pt()")public void afterThrowing() {System.out.println("---afterThrowing---");}/*** 最终通知*/@After("pt()")public void after() {System.out.println("---after---");}// /**
// * 环绕通知
// *
// * @param pjp 连接点
// */
// @Around("pt()")
// public Object around(ProceedingJoinPoint pjp) {
// System.out.println("---around---");
// try {
// System.out.println("前置通知");
// //调用目标方法
// Object proceed = pjp.proceed();
// System.out.println("后置通知");
// return proceed;
// } catch (Throwable throwable) {
// System.out.println("异常通知");
// throwable.printStackTrace();
// return null;
// } finally {
// System.out.println("最终通知");
// }
// }
}
//--------------
/*** 目标类** @author lx*/
@Component
public class AspectAdviceTarget {public int target() {System.out.println("target");//抛出一个异常//int i = 1 / 0;return 33;}
}
调用方法
@Test
public void aspectAdvice() {//1 获取容器AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);//2 获取目标类beanAspectAdviceTarget aspectAdviceTarget = ac.getBean("aspectAdviceTarget", AspectAdviceTarget.class);//2 调用目标方法aspectAdviceTarget.target();
}
返回的结果
---before---
target
---afterReturning---
---after---
几个AOP环绕通知都起效果
7.1 参数绑定
在基于XML的配置中,我们可以将各种参数,返回值,异常信息传递给通知方法,在基于注解可以像xml一样使用。
7.1.1 返回值和异常绑定
@AfterReturning注解中有一个returning属性,它的值是后置通知方法中的参数名,用来将切入点方法返回值绑定到参数上。
@AfterThrowing注解中的throwing属性,它的值是异常通知方法中的参数名,用来将前置通知、切入点方法、后置通知执行过程中抛出的异常绑定到该参数上。另外,这里的最后抛出的异常同样遵循后者优先的覆盖原则,详见基于XML配置的文章。
/*** 切面类* @author lx*/
@Component
@Aspect
public class AspectArgument {@Pointcut("within(AspectArgumentTarget)")public void pt() {}/*** 后置通知,获取切入点方法的返回值作为参数** @param date 切入点方法的返回值*/@AfterReturning(value = "pt()", returning = "date")public void afterReturning(Date date) {System.out.println("----afterReturning----");System.out.println("Get the return value : " + date);System.out.println("----afterReturning----");}/*** 异常通知,获取抛出的异常作为参数** @param e 前置通知、切入点方法、后置通知执行过程中抛出的异常*/@AfterThrowing(value = "pt()", throwing = "e")public void afterThrowing(Exception e) {System.out.println("----afterThrowing----");System.out.println("Get the exception : " + Arrays.toString(e.getStackTrace()));System.out.println("----afterThrowing----");}
}
//------------------
@Component
public class AspectArgumentTarget {public Date target() {Date date = new Date();System.out.println("target return: " + date);//抛出一个异常//int i=1/0;return date;}
}
@Test
public void aspectArgument() {//1 获取容器AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MainConfiguration.class);//2 获取目标类beanAspectArgumentTarget aspectArgumentTarget = ac.getBean("aspectArgumentTarget", AspectArgumentTarget.class);//2 调用目标方法Date target = aspectArgumentTarget.target();System.out.println(" returned value: "+target);
}
正常情况下:
target return: Fri Sep 18 00:00:41 CST 2020
----afterReturning----
Get the return value : Fri Sep 18 00:00:41 CST 2020
----afterReturning----returned value: Fri Sep 18 00:00:41 CST 2020
有异常的情况下:
target return: Fri Sep 18 00:01:33 CST 2020
----afterThrowing----
Get the exception : [com.spring.aop.annotation.aspectargument.AspectArgumentTarget.target(AspectArgumentTarget.java:14)//……………………
----afterThrowing----
//最终抛出的异常
java.lang.ArithmeticException: / by zero
at com.spring.aop.annotation.aspectargument.AspectArgumentTarget.target(AspectArgumentTarget.java:14)
//……………………
7.1.2 其他参数绑定
其他参数绑定,需要使用切入点表达式的语法,包括切入点方法参数args、代理对象this、目标对象target,和相关注解(@within, @target, @annotation, 和 @args)都可以绑定到通知方法的参数中。这些内容我们在基于XML的配置部分已经详细讲解了。
在通知方法的第一个参数同样可以默认设置一个JoinPoint,用来获取当前连接点的信息。
绑定参数的时候,会通过参数名字注入参数,如果类型不兼容,那么将不会执行该通知。预定义的切入点,如果要绑定参数,那么切入点绑定的方法同样必须有这些参数,并且参数名字同样要对应。
7.2 通知顺序
同一个连接点方法绑定了多个统一类型的通知,有时需要制定执行顺序,在基于XML配置文件我们可以通过order属性指定通知顺序,注解也可以指定执行顺序。
以下是一个代码样例
/*** order测试* order越小的切面,其内部定义的前置通知越先执行,后置通知越后执行。* 相同的order的切面则无法确定它们内部的通知执行顺序,同一个切面内的相同类型的通知也无法确定执行顺序。*/
@Component
public class AspectOrder {/*** 默认order为Integer.MAX_VALUE*/@Component@Aspectpublic static class AspectOrder1 {@Pointcut("execution(* *..AspectOrderTarget.*())")public void pt() {}@Before("pt()")public void before() {System.out.println("-----Before advice 1-----");}@AfterReturning("pt()")public void afterReturning() {System.out.println("-----afterReturning advice 1-----");}@After("pt()")public void after() {System.out.println("-----after advice 1-----");}}/*** 使用注解*/@Component@Aspect@Order(Integer.MAX_VALUE - 2)public static class AspectOrder2 {@Before("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void before() {System.out.println("-----Before advice 2-----");}@AfterReturning("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void afterReturning() {System.out.println("-----afterReturning advice 2-----");}@After("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void after() {System.out.println("-----after advice 2-----");}}/*** 实现Ordered接口*/@Component@Aspectpublic static class AspectOrder3 implements Ordered {@Before("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void before() {System.out.println("-----Before advice 3-----");}@AfterReturning("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void afterReturning() {System.out.println("-----afterReturning advice 3-----");}@After("com.spring.aop.annotation.aspectorder.AspectOrder.AspectOrder1.pt()")public void after() {System.out.println("-----after advice 3-----");}/*** @return 获取order*/public int getOrder() {return Integer.MAX_VALUE - 1;}}
}//----------------/*** @author lx*/
@Component
public class AspectOrderTarget {public void target() {System.out.println("target");}
}
8 DeclareParents Intrduction
@DeclareParents 对应基于XML配置中的< aop:declare-parents >标签,即Introduction(引介)。
在不修改代码的前提下,Intrdcuction可以在运行期为类动态添加一些额外的方法或者属性。
@DeclareParents绑定到一个切面类的属性上,该属性的类型就是新增的功能接口,实际上Spring创建的目标类的代理类会同时实现这个接口(无论是JDK还是CGlib),因此可以多一些功能。@DeclareParents还有两个属性:
value:需要增强的类扫描路径,该路径下的被Spring管理的bean被增强
defaultImpl:一个实现了新增的功能接口的类,作为新的增强方法的默认实现类
9 总结
基于注解还是基于XML的都是差不多,只是标签换成了注解。(一个项目可以在同一个工程可以同时使用两种方式来完成AOP,但一般不需要这么混着来。)
在Spring程序中很多地方隐式使用AOP代理机制,比如对于@Configuration标注的类,将默认创建一个cglib生成一个代理类,这样能代理他内部@Bean方法,当@Bean方法被调用时,将会返回同一个代理对象(单例)。这是其他注解@Component注解这些所不具备的特性。
还有像Spring的自动事务管理,传播行为@Transaction注解等也是AOP代理机制来实现的。