转载

spring boot AOP笔记

面向切面编程(AOP)是通过另一种思考方式来对面向对象编程(OOP)的补充。在抽象的结构中,OOP模块的基本单元是类,而AOP的基本单元是面。AOP的面能够跨越多个类型和对象来达成模块化。

下面是根据我的理解画的图:

spring boot AOP笔记

这是一个简单的MVC结构,不同的模块之间根据类来分离。但是AOP的切面却可以跨越多个模块。图中的示例表示UserAOP跨越了整个Controller模块。当然他也可以同时跨越Model模块。这取决于AOP的实际业务需求。

AOP提供了一个不同的编程思路,不过springIoc并没有依赖AOP,对于Ioc来说,AOP可以提供支持但不是必须。

AOP结构

  • 切面(Aspect):跨越多个类别的关注点的模块化。事务管理是企业Java应用程序中对切面应用最多的例子。在SpringAOP中,切面是使用常规类(基于模式的方法)或使用@Aspect注释(@AspectJ样式)注释的常规类来实现的 。
  • 连接点(JoinPoint):程序执行期间的一个点。在springAOP中表示AOP程序执行的点。
  • 通知(Advice):连接点在特定状态下执行的操作。其中包括了“around”、“before”和“after”等不同类型的通知。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。
  • 切入点(Pointcut):和连接点匹配的断言。切入点确定了AOP程序的入口,并在这之后和连接点相连并执行对应的连接点方法。切入点表达式如何和连接点匹配是AOP的核心:Spring默认使用AspectJ切入点表达式语言。
  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。
  • 目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。
  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。
  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

其中切入点可以和连接点合并,直接在通知中表示:

@Before("execution(com.dust.controller. . (..))")

public void before() {/* 方法体

/}

该写法等价于:

@Pointcut("execution(com.dust.controller.

.

(..))")

public void point(){}

@Befor("point()")

public void before(){/

方法体 */}

通知

在AOP中,连接点与切入点的关联关系以及相应的判断规则是AOP的核心。切入点确定了AOP入口,但是具体要执行哪部分则由连接点决定。连接点是方法执行的入口。

  • @Before:前置通知,在连接点前执行。这个通知不能阻碍通知之前的程序执行。
@Before("execution(* com.dust.controller.*.*(..))")
    public void beforController() {
        //在Controller被调用前
    }
复制代码
  • @AfterReturning:后置通知,在连接点之后执行,但是可以获取到连接点的返回值。

这里可以知道,对AOP来说,最小的单元是方法。AOP只能在方法和方法之间切入,而不能切入方法本身

@AfterReturning("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //在Controller执行完之后
    }
复制代码

对方法返回值的获取:

@AfterReturning(
        pointcut = "execution(* com.dust.controller.*.*(..))",
        returning = "retVal")
    public void afterController(Object retVal) {
        //对方法返回值的获取
        System.out.println(retVal.toString());
    }
复制代码

其中returning中的参数名称必须要和advice方法的参数名称相同,当方法执行返回时,返回值将作为相应的参数值传递给advice方法。如果方法没有返回值,则该参数为null。

  • @AfterThrowing:异常通知,当方法抛出异常的时候执行。
@AfterThrowing("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //在Controller抛出异常后执行
    }
复制代码

也可以设置到抛出给定异常时才执行advice。

@AfterThrowing(
        pointcut = "execution(* com.dust.controller.*.*(..))",
        throwing = "ex")
    public void afterController(NullPointerException ex) {
        //抛出空指针异常时执行advice
    }
复制代码
  • @After:最终通知,在连接点之后执行。不论返回点是正常还是异常。通常用来释放资源。通常在这里需要进行正常和异常的返回条件。
@After("execution(* com.dust.controller.*.*(..))")
    public void afterController() {
        //最终执行通知
    }
复制代码
  • @Around:环绕通知。包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。他可以确定方法何时、如何甚至是否执行。环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型,在通知体内,调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个Object[]对象-该数组中的值将被作为方法执行时的参数。

通知参数

所有通知方法都可以声明一个类行为org.aspectj.lang.JoinPoint的参数。

环绕通知声明的参数为ProceedingJoinPoint,因为需要执行ProceedingJoinPoint的proceed()方法。而其他的通知则不需要。

JoinPoint提供了很多有用的方法:

  • getArgs()(返回方法参数):返回该方法的参数集合,是一组Object[]
  • getThis()(返回代理对象):获取代理对象本身
  • getTarget()(返回目标):获取连接点所在的目标对象
  • getSignature()(返回正在被通知的方法相关信息):获取连接点的方法签名对象
  • toString()(打印出正在被通知的方法的有用信息)

传入参数

通常通知方法获取方法参数除了上述通过JoinPoint获取外还可以通过pointcut获取

@Pointcut("execution(* com.example.springdemo.controller.*.*(..)) && args(address, text, ..)")
    public void inController(String address, String text) {}

    @Before("inController(address, text)")
    public void beforController(String address, String text) {
        System.out.println("在执行控制器之前,获取参数{address:" + address + ",text:" + text + "}");
    }
复制代码

args(address, text, ..)切入点表达式匹配切入点方法的参数,而后传入给advice方法。 这是要求所有和该切入点匹配的连接点都需要接收参数,还可以单单在连接点接收参数:

@Pointcut("execution(* com.example.springdemo.controller.*.*(..))")
    public void inController() {}

    @Before("inController() && args(address, text, ..)")
    public void beforController(String address, String text) {
        System.out.println("在执行控制器之前,获取参数{address:" + address + ",text:" + text + "}");
    }
复制代码

其他参数:代理对象(this),目标对象(target)和注释(@within, @target, @annotation, @args)都可以以类似的方式绑定。

例子:使用AOP来重试事务

由于并发问题:死锁。可能导致业务执行失败。下次执行又有可能执行成功,因此对于这种操作不希望将重试交给用户来执行,这个可以交由系统来执行,这样对于用户来说他还是一次就执行成功了。

由于要尝试执行多次process(),因此使用@Around环绕通知

@Aspect
@Configuration
public class AOPRedo {

    //默认最大重试次数
    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    @Around("execution(* com.example.springdemo.controller.RestAPIController.*(..))")
    public Object apiAroundController(ProceedingJoinPoint pjp) {
        int num = 0;
        Throwable throwable;
        do {
            num++;
            try {
                return pjp.proceed();
            } catch (Throwable th) {
                System.out.println("尝试捕获");
                throwable = th;
            }
        } while (num <= maxRetries);
        return null;
    }

}
复制代码

其中重试次数可以交给配置文件,通过@PropertySource来导入配置信息。

@RestController
@RequestMapping("api")
public class RestAPIController {

    @Autowired
    EmailService emailService;

    private int count = 0;

    @RequestMapping("email")
    public String email(String address, String info) throws NullPointerException {
        if (count++ < 1) {
            throw new NullPointerException();
        }
        return emailService.setEmail(address,info) + "执行次数:" + count;
    }

}
复制代码

执行结果

spring boot AOP笔记
原文  https://juejin.im/post/5ca5eef851882543ea4b88ee
正文到此结束
Loading...