一、AOP简介

AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(其余两个:IOC - 控制反转DI - 依赖注入)。


那么AOP为何那么重要呢?

在我们的程序中,经常存在一些系统性的需求,比如 权限校验日志记录统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,那么面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代。


二、AOP体系与概念

简单地去理解,其实AOP要做三类事:

  • 在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。

  • 在什么时候切入,是业务代码执行前还是执行后。

  • 切入后做什么事,比如做权限校验、日志记录等。

AOP的体系图:
请添加图片描述
一些概念:

概念说明
Pointcut切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
Advice处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
Aspect切面,即 PointcutAdvice
Joint point连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
Weaving织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

三、AOP实例

1、创建SpringBoot工程

如何创建详见:IDEA 创建 SpringBoot 项目


2、添加依赖

<!-- springboot-aop包,AOP切面注解,Aspectd等相关注解 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3、AOP相关注解

package com.cw.tsb.app.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }

    @Around("pointCut()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        System.out.println("------------- doAround.");
        Object obj = null;
        try {
            obj = joinPoint.proceed();
        } catch (Throwable t){
            t.printStackTrace();
        }
        return obj;
    }

    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint){
        System.out.println("------------- doAfter.");
    }

    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint){
        System.out.println("------------- doBefore.");
    }

    /**
     * 后置返回
     *      如果第一个参数为JoinPoint,则第二个参数为返回值的信息
     *      如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
     * returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行,
     *      参数为Object类型将匹配任何目标返回值
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, String result){
        System.out.println("doAfterReturning result = " + result);
    }

    @AfterThrowing(value = "pointCut()", throwing = "t")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable t){
        System.out.println("------------- doAfterThrowing throwable = " + t.toString());
    }
}

3.1、@Aspect

该注解要添加在类上,声明这是一个切面类,使用时需要与@Component注解一起用,表明同时将该类交给spring管理。

@Component
@Aspect
public class ControllerAspect {
}

3.2、@Pointcut

用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。

该注解需要添加在方法上,该方法签名必须是 public void 类型,可以将@Pointcut 中的方法看作是一个用来引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此 @Pointcut 中的方法只需要方法签名,而不需要在方法体内编写实际代码

该注解有两个常用的表达式:execution()annotation()


3.2.1、execution()
@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }
}

表达式为:

execution(* com.cw.tsb.app.controller..*.*(..))
  • 第一个 * :表示返回值类型,* 表示所有类型;

  • 包名:标识需要拦截的包名;

  • 包名后的 ..:表示当前包和当前包的所有子包,在本例中指 com.cw.tsb.app.controller 包、子包下所有类;

  • 第二个 * :表示类名,* 表示所有类;

  • 最后的 *(..) :星号表示方法名,* 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。


3.2.2、annotation()

annotation() 方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:

@Aspect
@Component
public class ControllerAspect {
    @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }
}

然后使用该切面的话,就会切入注解是 @PostMapping 的所有方法。这种方式很适合处理 @GetMapping@PostMapping@DeleteMapping不同注解有各种特定处理逻辑的场景。

还有就是如上面案例所示,针对自定义注解来定义切面。

@Aspect
@Component
public class ControllerAspect {
    @Pointcut("@annotation(com.cw.tsb.app.annotation.PermissionsAnnotation)")
    private void permissionCheck() {
    	//该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }
}

3.3、@Around

@Around 注解用于修饰 Around 增强处理,Around增强处理非常强大,表现在:

  • @Around 可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用 ProceedingJoinPoint 参数的 procedd() 方法才会执行目标方法。

  • @Around 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。

Around 增强处理有以下特点:

  • 当定义一个 Around 增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用 ProceedingJoinPointproceed 方法才会执行目标方法:这就是 @Around 增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用 ProceedingJoinPointproceed 方法,则目标方法不会执行。

  • 调用 ProceedingJoinPointproceed 方法时,还可以传入一个 Object[] 对象,该数组中的值将被传入目标方法作为实参 —— 这就是 Around 增强处理方法可以改变目标方法参数值的关键。这就是如果传入的 Object[] 数组长度与目标方法所需要的参数个数不相等,或者 Object[] 数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。

@Around 功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的@Before@AfterReturning 就能解决的问题,就没有必要使用 Around 了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用 Around 。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用 Around 增强处理了。


3.4、@Before

@Before 注解指定的方法在切面切入目标方法之前执行,可以做一些 Log 处理,也可以做一些信息的统计,比如 获取用户的请求 URL 以及 用户的 IP 地址等等,这个在做个人站点的时候都能用得到,都是常用的方法。例如下面代码:

@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }

	/**
     * 在上面定义的切面方法之前执行该方法
     * @param joinPoint jointPoint
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        // 获取签名
        Signature signature = joinPoint.getSignature();
        // 获取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 获取即将执行的方法名
        String funcName = signature.getName();
        log.info("即将执行方法为: {},属于{}包", funcName, declaringTypeName);

        // 也可以用来记录一些信息,比如获取请求的 URL 和 IP
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 获取请求 URL
        String url = request.getRequestURL().toString();
        // 获取请求 IP
        String ip = request.getRemoteAddr();
    }
}

JointPoint 对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs() 获取)等。


3.5、@After

@After 注解和 @Before 注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。

@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }

	/**
     * 在上面定义的切面方法之后执行该方法
     * @param joinPoint jointPoint
     */
    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("==== doAfter 方法进入了====");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("方法{}已经执行完", method);
    }
}

到这里,我们来写个 Controller 测试一下执行结果,新建一个 AopController 如下:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }
}

启动项目,在浏览器中输入:http://localhost:8080/aop/csdn,观察一下控制台的输出信息:

====doBefore 方法进入了====  
即将执行方法为: testAop,属于com.itcodai.mutest.AopController包  
用户请求的 url 为:http://localhost:8080/aop/name,ip地址为:0:0:0:0:0:0:0:1  
==== doAfter 方法进入了====  
方法 testAop 已经执行完

从打印出来的 Log 中可以看出程序执行的逻辑与顺序,可以很直观的掌握 @Before@After 两个注解的实际作用。


3.6、@AfterReturning

@AfterReturning 注解和 @After 有些类似,区别在于 @AfterReturning 注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理,例如:

@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }

	/**
     * 后置返回
     *      如果第一个参数为JoinPoint,则第二个参数为返回值的信息
     *      如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数
     * returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行,
     *      参数为Object类型将匹配任何目标返回值
     */
    @AfterReturning(value = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, String result){
        // 实际项目中可以根据业务做具体的返回值增强
    }
}

需要注意的是,在 @AfterReturning 注解 中,属性 returning 的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning 方法中可以对返回值进行增强,可以根据业务需要做相应的封装。


3.7、@AfterThrowing

当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing 属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。

@Aspect
@Component
public class ControllerAspect {

    @Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")
    public void pointCut() {
        //该方法仅用于扫描controller包下类中的方法,而不做任何特殊的处理。
    }

	@AfterThrowing(value = "pointCut()", throwing = "t")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable t){
        System.out.println("------------- doAfterThrowing throwable = " + t.toString());
        // 处理异常的逻辑
    }
}



Logo

Authing 是一款以开发者为中心的全场景身份云产品,集成了所有主流身份认证协议,为企业和开发者提供完善安全的用户认证和访问管理服务

更多推荐