Spring AOP官方文档学习笔记(二)之基于注解的Spring AOP ...

打印 上一主题 下一主题

主题 951|帖子 951|积分 2853

1.@Aspect注解
(1) @Aspect注解用于声明一个切面类,我们可在该类中来自定义切面,早在Spring之前,AspectJ框架中就已经存在了这么一个注解,而Spring为了提供统一的注解风格,因此采用了和AspectJ框架相同的注解方式,这便是@Aspect注解的由来,换句话说,在Spring想做AOP框架之前,AspectJ AOP框架就已经很火了,而直接把AspectJ搬过来又不现实,因此,Spring想了一个折中的方案,即只使用AspectJ框架的声明,写法和定义方式(比如@Aspect注解),而底层由Spring自己实现,这样,就避免了我们程序员从AspectJ AOP切换到Spring AOP后,还要再去学一套新的写法了,也正因为如此,如果想要使用Spring AOP,就必须依赖aspectjweaver.jar包(不然谁来提供写法和定义方式),我们可以通过maven进行导入,如下
  1. <dependency>
  2.   <groupId>org.aspectj</groupId>
  3.   <artifactId>aspectjrt</artifactId>
  4.   <version>1.9.5</version>
  5. </dependency>
  6. <dependency>
  7.   <groupId>org.springframework</groupId>
  8.   <artifactId>spring-aspects</artifactId>
  9.   <version>${spring.framework.version}</version>
  10. </dependency>
复制代码
(2) 同时还需使用@EnableAspectJAutoProxy注解来开启Spring对于AspectJ注解的支持,如下
  1. @Configuration
  2. @EnableAspectJAutoProxy
  3. public class Config {
  4. }
复制代码
如果是基于xml的配置,可通过如下标签进行开启
  1. [/code][b]2.自定义一个切面类[/b]
  2. (1) 在基于注解的配置下,除了使用@Aspect注解外,还需要声明该切面是一个bean,否则,spring在扫描过程中是会忽略掉这个类的,如下
  3. [code]@Aspect
  4. @Component
  5. public class Logger {
  6. }
复制代码
(2) 对上面的例子,基于xml配置的写法如下
  1. @Aspect
  2. public class Logger {
  3. }
  4. <beans ...>
  5.    
  6.     <bean id="logger" ></bean>
  7. </beans>
复制代码
(3) 由@Aspect注解标注的类,称之为切面类,与普通的类一样,都有成员方法与成员变量,不同的是,切面类还可以包含连接点,通知,引介等与AOP有关的东西
(4) 切面不能再被增强,如果想拿一个切面来增强另一个切面,是不可能的,Spring会将切面类从自动代理(auto-proxying)中排除
3.自定义一个切入点
(1) Spring AOP中的切入点目前只可能是bean中的方法,而对于一个普通类中的方法,是不可能成为切入点的,在Spring中,声明一个切入点主要包括两个部分:一个切入点签名以及一个切入点表达式,如下
  1. //如下定义了一个叫做anyExampleAMethod的切入点,这个切入点会匹配cn.example.spring.boke包下的ExampleA类中的任何方法
  2. //其中,(1)就代表的是切入点表达式,(2)就代表的是切入点签名,注意,这个签名的返回值必须是void
  3. @Pointcut("execution(* cn.example.spring.boke.ExampleA.*(..))")      //(1)
  4. public void anyExampleAMethod() {}                                   //(2)
复制代码
(2) Spring AOP的切入点表达式中,支持如下等切入点标识符

  • execution:最为常用,用于匹配某个包,某个类中的方法
  • within:进行类型匹配,用于匹配某个包下所有类的所有方法或某个指定类中的所有方法,如下
  1. //指定了within的类型,这个切入点会匹配cn.example.spring.boke包下ExampleA类中的任何方法
  2. @Pointcut("within(cn.example.spring.boke.ExampleA)")
  3. public void withinDesignator(){}
复制代码

  • this:进行类型匹配,用于匹配生成的代理对象的类型是否为指定类型,如下
  1. //此前我们提到过,Spring AOP中的底层实现分为jdk动态代理和cglib动态代理,jdk动态代理基于接口,要求目标对象必须实现某个接口,而cglib动态代理基于继承,因此不同的实现方式下,导致Spring生成的代理对象的类型可能不同,这就是this标识符的基础
  2. //首先定义一个接口
  3. public interface Parent {
  4.     void register();
  5.     void sendEmail();
  6. }
  7. //让我们的ExampleA类,实现这个接口
  8. @Component
  9. public class ExampleA implements Parent{
  10.     public void register() {
  11.     }
  12.     public void sendEmail() {
  13.     }
  14. }
  15. //设置@EnableAspectJAutoProxy注解中的proxyTargetClass属性值为false,表示使用jdk动态代理,为true,表示使用cglib动态代理,默认值为false,不过我们这里显式的声明出来
  16. @Configuration
  17. @EnableAspectJAutoProxy(proxyTargetClass = false)
  18. @ComponentScan(basePackages = "cn.example.spring.boke")
  19. public class Config {
  20. }
  21. //切面类,在其中声明一个this标识符,并指定类型为ExampleA
  22. @Aspect
  23. @Component
  24. public class Logger {
  25.     /**
  26.      * this标识符,进行类型匹配,用于匹配代理对象的类型是否为指定类型
  27.      */
  28.     @Pointcut(value = "this(cn.example.spring.boke.ExampleA)")
  29.     public void thisDesignator(){}
  30.     @Around(value = "thisDesignator()")
  31.     public void around(ProceedingJoinPoint joinPoint) throws Throwable {
  32.         System.out.println(new Date() + " 开始执行...");
  33.         joinPoint.proceed();
  34.         System.out.println(new Date() + " 结束执行...");
  35.     }
  36. }
  37. //执行如下打印方法,可见通知未被执行,原因就是因为我们使用了jdk动态代理,Spring为我们生成的代理对象继承自jdk中的Proxy类并实现了Parent接口,它不属于ExampleA类型,自然而然切入点匹配失败,我们的通知未被执行
  38. AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
  39. ctx.getBean(Parent.class).register();
  40. ctx.getBean(Parent.class).sendEmail();
  41. //打印一下系统中代理对象的类型是否为ExampleA,结果为false
  42. System.out.println(ctx.getBean(Parent.class) instanceof ExampleA);
  43. //为了能进行匹配,我们可以将@EnableAspectJAutoProxy中的proxyTargetClass属性设置为true,使用cglib动态代理,这时再执行上面的打印方法,通知就会被执行了,原因就是因为使用了cglib动态代理后,Spring为我们生成的代理对象是继承自ExampleA,当然属于ExampleA类型,因此通知会被执行
  44. @EnableAspectJAutoProxy(proxyTargetClass = true)
复制代码

  • target:进行类型匹配,用于匹配目标对象的类型是否为指定类型,跟上面的this类似
  • args:进行方法参数匹配,用于匹配方法的参数类型是否为指定类型,如下
  1. //ExampleA中的register方法的参数为String
  2. @Component
  3. public class ExampleA{
  4.     public void register(String name) {
  5.     }
  6.     public void sendEmail() {
  7.     }
  8. }
  9. @Aspect
  10. @Component
  11. public class Logger {
  12.     /**
  13.      * 指定了args参数的类型为String,因此只会与ExampleA中的register方法匹配
  14.      */
  15.     @Pointcut(value = "args(java.lang.String)")
  16.     public void argsDesignator() {}
  17.     @Around(value = "argsDesignator()")
  18.     public void around(ProceedingJoinPoint joinPoint) throws Throwable {
  19.         System.out.println(new Date() + " 开始执行...");
  20.         joinPoint.proceed();
  21.         System.out.println(new Date() + " 结束执行...");
  22.     }
  23. }
复制代码

  • @target:用于匹配目标对象的类上有没有标注指定注解
  • @args:用于匹配方法的参数的所属类上有没有标注指定注解
  • @within:用于匹配某个类上有没有标注指定注解
  • @annotation:最常用,用于匹配某个方法上有没有标注指定注解
(3) Spring的AOP是基于代理实现的,因此,在目标对象中进行内部调用是不会被拦截的(即this指针会导致AOP失效问题),此外,对于jdk动态代理,只能拦截public方法,而对于cglib动态代理,会拦截public和protected方法(package-visible 方法在配置后也能被拦截)
(4) Spring AOP还提供了一个PCD bean,用于按照bean的名称进行切入,它是Spring AOP独有的,如下
  1. //匹配所有beanName以A结尾的bean
  2. @Pointcut("bean(*A)")
  3. public void pcd() {}
复制代码
4.组合切入点表达式
(1) 可以通过 &&,|| 和 !来组合切入点表达式,如下
  1. //切入所有public方法
  2. @Pointcut("execution(public * *(..))")
  3. public void allPublicMethod() {}
  4. //切入boke包下所有类中的所有方法
  5. @Pointcut("within(cn.example.spring.boke.*)")
  6. public void methodInBokePackage() {}
  7. //使用 && 操作符,将上面两个切入点表达式组合起来,即切入boke包下所有类中的所有public方法
  8. @Pointcut("allPublicMethod() && methodInBokePackage()")
  9. public void allPublicMethodInBokePackage() {}
复制代码
5.常见切入点表达式例子
(1)在实际工作中,我们的切入点表达式的通常形式为:execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?),其中ret-type-pattern表示一个方法的返回类型, 用 * 号可以代表任何类型; name-pattern表示方法名称,用 * 可以进行全部或部分名称匹配; param-pattern表示方法参数,其中用()代表无参方法,用(..)代表任何数量的参数(0个或多个),用()代表1个参数,(, String)代表有两个参数,第一个参数可以是任何类型,而第二个参数只能是String类型; 除此之外,其他带有 ? 的都是选填项,可不填写
(2)常见例子
  1. //匹配任意public方法
  2. execution(public * *(..))
  3. //匹配任意名称以set开头的方法
  4. execution(* set*(..))
  5. //匹配com.xyz.service包下,AccountService类下的任意方法
  6. execution(* com.xyz.service.AccountService.*(..))
  7. //匹配com.xyz.service包下,任意类下的任意方法
  8. execution(* com.xyz.service.*.*(..))
  9. //匹配com.xyz.service包及其子包下,任意类下的任意方法
  10. execution(* com.xyz.service..*.*(..))
  11. //匹配com.xyz.service包下,任意类下的任意方法
  12. within(com.xyz.service.*)
  13. //匹配com.xyz.service包及其子包下,任意类下的任意方法
  14. within(com.xyz.service..*)
  15. //匹配代理对象的类型为AccountService的类下的任意方法
  16. this(com.xyz.service.AccountService)
  17. //匹配目标对象的类型为AccountService的类下的任意方法
  18. target(com.xyz.service.AccountService)
  19. //匹配方法参数只有一个且参数类型为Serializable的方法,注意它与execution(* *(java.io.Serializable))的一点区别:execution这个例子只能匹配参数类型为Serializable的方法,如果说某个方法的参数类型是Serializable的子类,是不会匹配的,而下面args这个例子可以匹配参数类型为Serializable或其子类的方法
  20. args(java.io.Serializable)
  21. //匹配标注了@Transactional注解的目标对象中的任意方法
  22. @target(org.springframework.transaction.annotation.Transactional)
  23. //匹配标注了@Transactional注解的类中的任意方法
  24. @within(org.springframework.transaction.annotation.Transactional)
  25. //匹配标注了@Transactional注解的任意方法
  26. @annotation(org.springframework.transaction.annotation.Transactional)
  27. //匹配方法的参数有且只有一个且该参数的所属类上标注了@Classified注解的任意方法
  28. @args(com.xyz.security.Classified)
  29. //匹配beanName为tradeService的bean中的任意方法
  30. bean(tradeService)
  31. //匹配所有以Service作为beanName结尾的bean中的任意方法
  32. bean(*Service)
复制代码
6.编写良好的pointcuts
(1)Spring将切入点标识符分为3大类,分别为:

  • Kinded:类型标识符,如execution, get, set, call等,它们都是根据类型进行选择,比如execution选择的都是可执行方法这一类型,其中除了execution,其他的都是AspectJ框架提供的
  • Scoping:范围标识符,如within;
  • Contextual:上下文标识符,如this, target和@annotation,它们都是根据方法所处的环境(比如在哪个类中)进行选择
    Spring建议一个良好的切入点表达式应该至少包括前两种类型(kinded和scoping,在这两种标识符中scoping又特别重要,因为它的匹配速度非常快,可以快速的排除掉那些不应该被处理的方法),此外在有根据上下文环境的需求时,可以包括contextual标识符
7.声明一个通知
(1)在前面已经提及过通知,它是增强的逻辑,与切入点相关联,会在切入点执行前或执行后执行,在Spring中总共分为5大类,如下

  • Before Advice:使用@Before注解可定义前置通知,它会在切入点执行之前执行
  1. @Aspect
  2. @Component
  3. public class Logger {
  4.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  5.     public void before() {
  6.         //...
  7.     }
  8. }
复制代码

  • After Returning Advice:使用@AfterReturning注解可定义返回通知,它会在切入点"正常"执行之后执行
  1. //一个普通的bean ExampleA
  2. @Component
  3. public class ExampleA{
  4.     public String doSomething() {
  5.         return "finish";
  6.     }
  7. }
  8. //有时候,我们可能需要访问切入点执行后的返回值,那么我们可以使用@AfterReturning注解中的returning属性来指定返回值的名称,然后再给这个切面方法添加一个形参,这个形参类型即为切入点执行后的返回值类型(或其父类型,但不能完全不一致,否则切面会切入失败),形参名要与刚刚设置过的returning属性值一致,如下例
  9. @Aspect
  10. @Component
  11. public class Logger {
  12.     @AfterReturning(value = "execution(* cn.example.spring.boke.ExampleA.*(..))", returning = "returnVal")
  13.     public void afterReturning(Object returnVal) {
  14.         System.out.println(new Date() + " 开始执行...");
  15.         System.out.println(returnVal);
  16.         System.out.println(new Date() + " 结束执行...");
  17.     }
  18. }
复制代码

  • After Throwing Advice:使用@AfterThrowing注解可定义异常通知,它会在切入点触发异常之后执行
  1. //同样,我们有时候也期望访问切入点执行过程中抛出的异常,与返回通知一致,例子如下
  2. @Aspect
  3. @Component
  4. public class Logger {
  5.     @AfterThrowing(value = "execution(* cn.example.spring.boke.ExampleA.*(..))", throwing = "throwable")
  6.     public void afterReturning(Throwable throwable) {
  7.         System.out.println(new Date() + " 开始执行...");
  8.         System.out.println(throwable);
  9.         System.out.println(new Date() + " 结束执行...");
  10.     }
  11. }
复制代码

  • After (Finally) Advice:使用@After注解可定义后置通知,它会在切入点无论以何种方式执行(正常或异常)后执行,常用于释放资源等目的,类似于try-catch语句中的finally块,它与@AfterReturning的区别是,@AfterReturning只适用于切入点正常返回
  1. @Aspect
  2. @Component
  3. public class Logger {
  4.     @After(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  5.     public void afterReturning() {
  6.         //...
  7.     }
  8. }
复制代码

  • Around advice:使用@After注解可定义环绕通知,它既可以在切入点之前执行通知,又可以在切入点之后执行,甚至可以不用执行切入点,是最为灵活强大的通知
  1. @Aspect
  2. @Component
  3. public class Logger {
  4.     //环绕通知方法可不声明形参,但如果要声明形参,第一个形参的类型必须是ProceedingJoinPoint类型,对ProceedingJoinPoint调用proceed方法后会导致切入点真正的执行,此外,proceed方法还有一个重载方法,我们可以对它传递一个Object[],那么当切入点执行时会以这个数组中的值作为方法参数值来执行
  5.     //我们可以调用一次,多次或根本不调用proceed方法
  6.     @Around(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  7.     public void afterReturning(ProceedingJoinPoint joinPoint) {
  8.         Object returnVal = null;
  9.         try {
  10.             //...
  11.             returnVal = joinPoint.proceed();
  12.             //...
  13.         } catch (Throwable e) {
  14.             //...
  15.             e.printStackTrace();
  16.         }
  17.         return returnVal;
  18.     }
  19. }
复制代码
8.切入点信息获取
(1)有时候,我们期望获取到切入点相关信息,比如它的签名,形参等信息,Spring为我们提供了JoinPoint类型,用于获取相关信息,在前面的环绕通知的例子中,我们就已经使用了JoinPoint的子类型ProceedingJoinPoint,它添加了proceed方法,来显式的调用执行切入点
  1. @Aspect
  2. @Component
  3. public class Logger {
  4.     //除了下面的例子外,使用JoinPoint,还可以获取到切入点的其他一些信息,可参考api文档
  5.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  6.     public void before(JoinPoint joinPoint) {
  7.         System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
  8.         System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
  9.         System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
  10.     }
  11. }
复制代码
9.通知执行顺序
(1)不同切面类中的通知,在默认情况下,按照所在切面类名的字典序排序,若其排序越高则优先级也就越高,如下
  1. @Configuration
  2. @EnableAspectJAutoProxy
  3. @ComponentScan(basePackages = "cn.example.spring.boke")
  4. public class Config { }
  5. @Component
  6. public class ExampleA{
  7.     public void doSomething() {
  8.         System.out.println("doSomething...");
  9.     }
  10. }
  11. //声明两个切面类TimerLogger和OperationLogger
  12. @Aspect
  13. @Component
  14. public class TimerLogger {
  15.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  16.     public void a() {
  17.         System.out.println("timer...");
  18.     }
  19. }
  20. @Aspect
  21. @Component
  22. public class OperationLogger {
  23.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  24.     public void a() {
  25.         System.out.println("operation...");
  26.     }
  27. }
  28. //启动容器,观察打印结果
  29. AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
  30. ctx.getBean(ExampleA.class).doSomething();
  31. //打印结果如下,OperationLogger中的前置通知先执行,TimerLogger中的前置通知后执行,就是因为O的字典序列大于T,因此OperationLogger中的通知的优先级高于TimerLogger中的,而对于前置通知而言,优先级越高的越先执行,对于后置通知,优先级越高的越后执行
  32. operation...
  33. timer...
  34. doSomething...
  35. //我们可以将TimerLogger改为ATimerLogger,这样的话它里面的前置通知就会先执行了
  36. @Aspect
  37. @Component
  38. public class ATimerLogger {
  39.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  40.     public void a() {
  41.         System.out.println("timer...");
  42.     }
  43. }
复制代码
(2)我们可以对切面类实现Ordered接口或添加@Order注解来显示的指定优先级,其中指定的值越小,优先级越高
  1. //此时TimerLogger的优先级高于OperationLogger
  2. @Aspect
  3. @Component
  4. @Order(1)
  5. public class TimerLogger {
  6.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  7.     public void a() {
  8.         System.out.println("timer...");
  9.     }
  10. }
  11. @Aspect
  12. @Component
  13. @Order(2)
  14. public class OperationLogger {
  15.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  16.     public void a() {
  17.         System.out.println("operation...");
  18.     }
  19. }
复制代码
(3)对于同个切面类中的相同类型的通知,其优先级只与通知方法名字典序的排序有关,排序越高,优先级越高,如下
  1. //Logger切面类中定义了两个前置通知为aPrint和bPrint
  2. @Aspect
  3. @Component
  4. public class Logger {
  5.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  6.     public void aPrint() {
  7.         System.out.println("a");
  8.     }
  9.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  10.     public void bPrint() {
  11.         System.out.println("b");
  12.     }
  13. }
  14. //启动容器,可见aPrint先于bPrint,这就是因为a的字典序高于b
  15. a
  16. b
  17. doSomething...
  18. //将aPrint改为caPrint,这时bPrint会先执行,因为此时它的字典序高
  19. @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  20. publicvoid caPrint() {
  21.     System.out.println("a");
  22. }
  23. //此外使用@Order注解,无法改变优先级,因为此时显式指定优先级的策略已经失效了,如下面这个例子还是按照之前默认的优先级进行执行
  24. @Aspect
  25. @Component
  26. public class Logger {
  27.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  28.     @Order(Ordered.LOWEST_PRECEDENCE)
  29.     public void aPrint() {
  30.         System.out.println("a");
  31.     }
  32.     @Before(value = "execution(* cn.example.spring.boke.ExampleA.*(..))")
  33.     @Order(Ordered.HIGHEST_PRECEDENCE)
  34.     public void bPrint() {
  35.         System.out.println("b");
  36.     }
  37. }
复制代码
10.Introductions(引介)
(1)引介能够使指定的对象实现某些接口,并提供对这些接口的实现,以达到向对象中动态添加它所没有方法的目的,例子如下
  1. //我们希望向ExampleA类中增加某些新的方法
  2. @Component
  3. public class ExampleA{ }
  4. //声明一个接口,这个接口里的方法即为我们希望增加的新的方法
  5. public interface Extention {
  6.     void doSomething();
  7. }
  8. //新方法的具体实现
  9. public class ExtentionImpl implements Extention{
  10.     @Override
  11.     public void doSomething() {
  12.         System.out.println("doSomething...");
  13.     }
  14. }
  15. //定义一个切面
  16. @Component
  17. @Aspect
  18. public class MyAspect {
  19.     //使用@DeclarePrents注解,声明被拦截的类有一个新的父类型,其中value指定拦截哪些类,在下面这个例子中指定拦截cn.example.spring.boke包下的所有类,同时指定它们的父类型均为Extention,具体的实现为ExtentionImpl
  20.     @DeclareParents(value = "cn.example.spring.boke.*", defaultImpl = ExtentionImpl.class)
  21.     public Extention extention;
  22. }
  23. //开启AOP
  24. @Configuration
  25. @EnableAspectJAutoProxy
  26. @ComponentScan(basePackages = "cn.example.spring.boke")
  27. public class Config { }
  28. //启动容器,从容器中获取到exampleA并将其强制转换为Extention,这样我们就能使用向ExampleA中新添加的方法了
  29. AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class);
  30. Extention exampleA = (Extention)ctx.getBean("exampleA");
  31. exampleA.doSomething();
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

tsx81429

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表