AOP (面向切面编程) 系列之二

Posted 起名字就是这样费劲的事

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP (面向切面编程) 系列之二相关的知识,希望对你有一定的参考价值。

AOP (面向切面编程) 系列之二

上接  、  
使用 Aspactj 注解的方式实现 AOP ,实现和
 spring xml 配置方式相同的功能。

show code

Talkischeap,just show you the code.
本文使用 AspectJ 注解完成 AOP 切入点、切面方法的定义。

用作切入点的注解

这次新建一个用于方法的注解,不再沿用前文的了,以免混淆。

 
   
   
 
  1. package com.hxx.annotation;

  2. import java.lang.annotation.ElementType;

  3. import java.lang.annotation.Retention;

  4. import java.lang.annotation.RetentionPolicy;

  5. import java.lang.annotation.Target;

  6. @Retention(RetentionPolicy.RUNTIME)

  7. @Target(ElementType.METHOD)

  8. public @interface AnnotationTwo {

  9.    String value();

  10. }

为了简单一点只定义一个 value 属性吧,用起来方便。(仅仅只是为了偷懒)

切入点和切面方法定义

一个类搞定切入点和切面方法的定义和配置。 为了有(zhi)始(xiang)有(tou)终(lan),功能和前文的 SpringAspect 完全相同。

 
   
   
 
  1. package com.hxx.aop;

  2. import com.hxx.annotation.AnnotationTwo;

  3. import org.aspectj.lang.JoinPoint;

  4. import org.aspectj.lang.ProceedingJoinPoint;

  5. import org.aspectj.lang.annotation.After;

  6. import org.aspectj.lang.annotation.AfterReturning;

  7. import org.aspectj.lang.annotation.AfterThrowing;

  8. import org.aspectj.lang.annotation.Around;

  9. import org.aspectj.lang.annotation.Aspect;

  10. import org.aspectj.lang.annotation.Before;

  11. import org.aspectj.lang.annotation.Pointcut;

  12. import org.aspectj.lang.reflect.MethodSignature;

  13. import org.springframework.stereotype.Component;

  14. /**

  15. * <ul>

  16. * <li>功能说明:使用 aspactj 注解方式实现 AOP </li>

  17. * <li>邮箱:houxiangxiang@cibfintech.com</li>

  18. * </ul>

  19. */

  20. @Aspect

  21. @Component

  22. public class AnnotationAspect {

  23.    /**

  24.     * 定义切入点

  25.     */

  26.    @Pointcut("@annotation(com.hxx.annotation.AnnotationTwo)")

  27.    public void doPoint() {

  28.    }

  29.    /**

  30.     * 围绕 切入点 doPoint() 执行的方法

  31.     *

  32.     * @param point

  33.     * @return

  34.     * @throws Throwable

  35.     */

  36.    @Around("doPoint()")

  37.    public Object doAround(ProceedingJoinPoint point) throws Throwable {

  38.        System.out.println("---------------> around start");

  39.        MethodSignature methodSignature = (MethodSignature) point.getSignature();

  40.        AnnotationTwo annotation = methodSignature.getMethod().getAnnotation(AnnotationTwo.class);

  41.        System.out.println("annotation ---------------> " + annotation);

  42.        Object proceed = 0;

  43.        try {

  44.            proceed = point.proceed();

  45.        } catch (Throwable throwable) {

  46.            throw throwable;

  47.        }

  48.        System.out.println("---------------> around end");

  49.        return proceed;

  50.    }

  51.    /**

  52.     * 被注解方法执行前执行的方法

  53.     *

  54.     * @param point

  55.     */

  56.    @Before("doPoint()")

  57.    public void doBefore(JoinPoint point) {

  58.        System.out.println("---------------> before ");

  59.    }

  60.    /**

  61.     * 被注解方法执行前执行的方法

  62.     *

  63.     * @param point

  64.     */

  65.    @After("doPoint()")

  66.    public void doAfter(JoinPoint point) {

  67.        System.out.println("---------------> after");

  68.    }

  69.    /**

  70.     * 后置异常通知:在方法抛出异常之后执行,可以访问到异常信息,且可以指定出现特定异常信息时执行代码

  71.     *

  72.     * @param point

  73.     */

  74.    @AfterThrowing(pointcut = "doPoint()", throwing = "throwable")

  75.    public void afterThrowing(JoinPoint point, Throwable throwable) {

  76.        System.out.println("---------------> afterThrowing");

  77.        System.out.println(throwable.getMessage());

  78.    }

  79.    /**

  80.     * @param point

  81.     * @param returnValue

  82.     */

  83.    @AfterReturning(pointcut = "doPoint()", returning = "returnValue")

  84.    public void afterReturning(JoinPoint point, int returnValue) {

  85.        System.out.println("---------------> afterReturning +++ " + returnValue);

  86.    }

  87. }

都用了 AspectJ 注解,肯定是要引入 aspectj 依赖的。按照惯例,本文只举例 maven 的 pom 文件依赖方式,jar 包版本号一般选择最新版即可。

 
   
   
 
  1. <dependency>

  2.    <groupId>org.aspectj</groupId>

  3.    <artifactId>aspectjrt</artifactId>

  4.    <version>${aspectj.version}</version>

  5. </dependency>

细心的你一定能发现在类上我们用了一个 spring 的注解 @Component ,这里只是为了声明由 spring 自动实例化并管理我们的 AnnotationAspect 。 用到了 spring 注解,就要引入 spring 支持嘛:

 
   
   
 
  1. <dependency>

  2.    <groupId>org.springframework</groupId>

  3.    <artifactId>spring-context-support</artifactId>

  4.    <version>${spring.version}</version>

  5. </dependency>

如果不交给 spring 管理或者自己手动实例化的话,那就不需要使用 @Component 注解,也不需要引入 spring 支持。

使用到的 aspactj 注解

再次重申:注解是 元数据 ,本身不具备任何业务处理能力。
aspactj 注解的背后有一套逻辑处理,我们只需要使用这些注解即可。

    • @Aspect

    声明这是个切面,需要进行 AOP 的处理,类似 xml 配置方式中的 aop:aspect 标签。
    支持一个参数,指明切面的构造方式,默认单例(singleton),一般默认即可。

    • @Pointcut

    定义切入点,类似 xml 配置方式中的 aop:pointcut 标签。
    参数是切入点的表达式(expression),上例中 "@annotation(com.hxx.annotation.AnnotationTwo)"是指代所有被 AnnotationTwo 注解标注的地方。 expression 的写法有很多种,此处继续挖坑,以后填。
    @Pointcut 标注的方法名会作为切入点在下面的注解中使用到,但本身这个注解非必需,且方法体并不会被执行。

    • @Around

    围绕切入点执行的逻辑方法,类似 xml 配置方式中的 aop:around 标签。
    被标注的方法同样可以根据条件拒绝执行切点方法,也可以处理过程中产生的异常(吞掉或者包装)。
    方法名不重要,不会出现在任何显式被调用或者配置的地方(与 xml 配置方式不同)。
    注解参数是切入点表达式或者或者 @Pointcut 标注的方法名(二者等效,都指代一条切入点规则),本例中是后者。

  • @Before
    在切入点执行前执行的内容,类似 xml 配置方式中的 aop:before 标签。
    被标注的方法名同样不重要,注解参数同 @Around

  • @After
    在切入点执行后执行的内容,类似 xml 配置方式中的 aop:fter 标签。
    其余同 @Before

  • @AfterThrowing
    切入点发生异常(且没有被 @Around 吞掉)时执行的方法,类似 xml 配置方式中的 aop:after-throwing 标签。
    如果不需要拿到异常(居然大部分场景都关心异常的吧),则和 @Before 配置没有差别。
    需要拿到异常时,注解参数 pointcut 表示切入点, throwing 表示接收异常的参数名。

  • @AfterReturning
    切入点返回之后执行的逻辑,类似 xml 配置方式中的 aop:after-returning 标签。
    注解参数与 @AfterThrowing 类似, returning 表示接收返回值的参数名。

模拟使用场景

为了方便对比(懒癌上身),在   文章的 ApiDemo 中添加一个方法,模拟模拟新的业务场景。其他配置不变,依旧交给 spring 管理 bean ,支持 spring 注解、支持 aop 。

 
   
   
 
  1. package com.hxx.api;

  2. /**

  3. * <ul>

  4. * <li>功能说明:模拟业务接口</li>

  5. * <li>邮箱:houxiangxiang@cibfintech.com</li>

  6. * </ul>

  7. */

  8. public interface ApiDemo {

  9.    /**

  10.     * 模拟业务方法定义1

  11.     *

  12.     * @param input 输入

  13.     * @return 输出

  14.     */

  15.    int work(int input);

  16.    /**

  17.     * 模拟业务方法定义2

  18.     *

  19.     * @param input 输入

  20.     * @return 输出

  21.     */

  22.    int workTwo(int input);

  23. }

ApiDemo 的实现中添加 workTwo ,并使用我们刚定义好的 AnnotationTwo

 
   
   
 
  1. package com.hxx.api.impl;

  2. import com.hxx.annotation.AnnotationDemo;

  3. import com.hxx.annotation.AnnotationTwo;

  4. import com.hxx.api.ApiDemo;

  5. import com.hxx.enums.UserType;

  6. import org.springframework.stereotype.Service;

  7. @Service("apiDemo")

  8. public class ApiDemoImpl implements ApiDemo {

  9.    @AnnotationDemo(time = 1, count = 2, name = "admin", userType = UserType.SYSTEM_ADMIN)

  10.    public int work(int input) {

  11.        System.out.println("------->> ApiDemo work");

  12.        return 3 / input;

  13.    }

  14.    @AnnotationTwo("houxx")

  15.    public int workTwo(int input) {

  16.        System.out.println("------->> ApiDemo workTwo");

  17.        return 5 / input;

  18.    }

  19. }

结果测试

在前文中的 junit 测试用例中添加两个测试方法:

 
   
   
 
  1. package com.hxx.api;

  2. import org.junit.Test;

  3. import org.junit.runner.RunWith;

  4. import org.springframework.test.context.ContextConfiguration;

  5. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

  6. import javax.annotation.Resource;

  7. @RunWith(SpringJUnit4ClassRunner.class) //使用junit4进行测试

  8. @ContextConfiguration(locations = {"classpath*:applicationContext.xml"}) //加载配置文件

  9. public class ApiDemoTest {

  10.    @Resource(name = "apiDemo")

  11.    private ApiDemo apiDemo;

  12.    @Test

  13.    public void testWork() throws Exception {

  14.        apiDemo.work(1);

  15.    }

  16.    @Test

  17.    public void testWorkException() throws Exception {

  18.        apiDemo.work(0);

  19.    }

  20.    @Test

  21.    public void testWorkTwo() throws Exception {

  22.        apiDemo.workTwo(4);

  23.    }

  24.    @Test

  25.    public void testWorkTwoException() throws Exception {

  26.        apiDemo.workTwo(0);

  27.    }

  28. }

运行 testWorkTwo() 可以得到结果:

 
   
   
 
  1. ---------------> around start

  2. annotation ---------------> @com.hxx.annotation.AnnotationTwo(value=houxx)

  3. ---------------> before

  4. ------->> ApiDemo workTwo

  5. ---------------> around end

  6. ---------------> after

  7. ---------------> afterReturning +++ 1

执行结果完全符合预期,且和 testWork() 的表现完全一致。 我们再运行一下 testWorkTwoException() ,测试一下发生异常的情况:

 
   
   
 
  1. ---------------> around start

  2. annotation ---------------> @com.hxx.annotation.AnnotationTwo(value=houxx)

  3. ---------------> before

  4. ------->> ApiDemo workTwo

  5. ---------------> after

  6. ---------------> afterThrowing

  7. / by zero

  8. java.lang.ArithmeticException: / by zero

  9.    at com.hxx.api.impl.ApiDemoImpl.workTwo(ApiDemoImpl.java:22)

  10.    ……

当出现异常且在 around 中没有吞掉时,afterThrowing 执行了,after 也执行了,但 afterReturning 无法再执行,around 中抛出异常后的语句也无法再执行。这和 testWorkException 的表现也完全一致。 在来看一下 around 中吞掉异常的情况(仅仅注释 doAround 的 throw 语句):

 
   
   
 
  1. ---------------> around start

  2. annotation ---------------> @com.hxx.annotation.AnnotationTwo(value=houxx)

  3. ---------------> before

  4. ------->> ApiDemo workTwo

  5. ---------------> around end

  6. ---------------> after

  7. ---------------> afterReturning +++ 0

由此看到可以在 around 中吞掉异常,给回默认返回值,表现也和 xml 配置方式完全一样。

对比 spring xml 配置方式和 AspctJ 注解方式

首先,二者能达到完全一样的效果。
从使用上来看,spring 重度依赖患者可能更喜欢前者。
  一文中曾经说过:

很多情况下,XML 配置其实就是为了分离代码和配置而使用的。但有些场景,我们更希望与代码紧密结合。

我认为,这就是符合这个场景的实例。从代码习惯上来说,更喜欢切点、切面的定义和配置出现在一起,而不是拆分为两处。
如果没有强制规范的话,喜欢那种方式就可以用那种方式,写得开心就好。不过,非常不建议混搭~

照旧,点击查看原文阅读完整代码(github)。


以上是关于AOP (面向切面编程) 系列之二的主要内容,如果未能解决你的问题,请参考以下文章

C#中使用面向切面编程(AOP)中实践代码整洁

Spring系列Spring AOP面向切面编程

面向切面编程AOP之动态代理

8.Spring系列之AOP

#yyds干货盘点# Spring核心之面向切面编程(AOP)

[Spring实战系列](16)面向切面编程(AOP)概述