Spring AOP

发布于:2024-04-29 ⋅ 阅读:(22) ⋅ 点赞:(0)

AOP:Aspect Oriented Programming(面向切面编程):核心思想是将重复的逻辑剥离出来,在不修改原始逻辑的基础上对原始功能进行增强。

优点:无侵入、减少重复代码、提高开发效率、维护方便。

一、AOP 概念

连接点(JoinPoint):可以被 AOP 控制的方法执行。

通知(Advice):重复逻辑代码。

切入点(PointCut):匹配连接点的条件。

切面(Aspect):通知+切点。

通知类型

  • @Around:此注解标注的通知方法在目标方法前、后都执行。
  • @Before :此注解标注的通知方法在目标方法前执行。
  • @After :此注解标注的通知方法在目标方法后执行,无论是否有异常。
  • @AfterReturning:此注解标注的通知方法在目标方法后被执行,有异常不会执行。
  • @AfterThrowing:此注解标注的通知方法发生异常后执行。

注意:@Around 需要自己调用 ProceedingJoinPoint.proceed()来让目标方法执行,其他通知不需要考虑目标方法执行。

1.1、通知顺序

当有多个切面的切点都匹配目标时,多个通知方法都会被执行。

  • 默认按照切面类的名称字母排序:

    • 目标前的通知方法,字母排名靠前的先执行。
    • 目标后的通知方法,字母排名靠前的后执行。
  • @Order (数字)加在切面类上来控制顺序

    • 目标前的通知方法:数字小的先执行。
    • 目标后的通知方法,数字小的后执行。
1.2、切点表达式

切点表达式用来匹配哪些目标方法需要应用通知

  • execution(返回值类型 包名.类名.方法名(参数类型))

    • *:可以通配任意返回值类型、包名、类名、方法名、或者任意类型的一个参数
    • …:可以通配任意 层级的包、或者任意类型、任意个数的参数
  • @annotation() 根据注解匹配

  • 切点表达式–execution

主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为

  • execution(访问修饰符? 返回值 包名.类名?.方法名(方法参数)throws 异常?)
    其中带?的表示可以省略的部分
    • 访问修饰符:可省略(仅能匹配public、protected,private不能增强)
    • 包名.类名:可省略
    • throws异常:可省略
    @Pointcut("execution(public * com.duan.controller.*.*(..))")
    public void loggingPointcut(){
        // 暂不用处理
    }
  • 切点表达式–@annotation

切点表达式也支持匹配目标方法是否有注解。

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.duan.anno.Log)")
    public void loggingPointcut() {
    }
1.3、@PoinCut

该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。

/**
     * com.duan.controller 包中公共方法的切入点
     */
    @Pointcut("execution(public * com.duan.controller.*.*(..))")
    public void loggingPointcut(){
        // 暂不用处理
    }

    @Around("loggingPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        
    }
1.4、连接点

连接点简单理解就是目标方法,在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如方法名、方法参数类型等等。

  • 对于@Around 通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类

二、案例

获得业务中的增删改方法的操作日志,一般都保存到数据库中,现在我们是测试,就先不保存到数据库,直接打印出来,包含请求的接口地址、操作时间、执行方法全类名、执行方法名、方法参数、返回值、方法执行时长。

1、新建一个SpringBoot项目aop,所使用的依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.duan</groupId>
    <artifactId>aop</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.3</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.4.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.4.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.4.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.50</version>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>20.0</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <!--mybatisPlus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>

        <!--mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
    </dependencies>
</project>

2、自定义一个注解 @Log

package com.duan.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author db
 * @version 1.0
 * @description Log
 * @since 2024/4/11
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}

3、新建一个切面类LoggingAspect,这里经常使用的是环绕通知@Around,代码如下:

package com.duan.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;

/**
 * @author db
 * @version 1.0
 * @description LoggingAspect
 * @since 2024/1/3
 */
@Aspect
@Component
@Slf4j
public class LoggingAspect {
    /**
     * com.duan.controller 包中公共方法的切入点
     */
    @Pointcut("execution(public * com.duan.controller.*.*(..))")
    public void loggingPointcut(){
        // 暂不用处理
    }

    @Around("loggingPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        // 获取类名
        String className = pjp.getTarget().getClass().getTypeName();
        // 获取方法名
        String methodName = pjp.getSignature().getName();
        // 获取参数名
        String[] parameterNames = ((MethodSignature) pjp.getSignature()).getParameterNames();

        Object result = null;
        // 获取参数值
        Object[] args = pjp.getArgs();

        // 获取请求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 获取请求的url
        String url = request.getRequestURL().toString();

        // 请求参数,以参数名和值为键值对
        Map<String, Object> paramMap = new HashMap<>();
        IntStream.range(0, parameterNames.length).forEach(i->paramMap.put(parameterNames[i], args[i]));

        // header参数
        Enumeration<String> headerNames = request.getHeaderNames();
        Map<String, Object> headerMap = new HashMap<>();
        while (headerNames.hasMoreElements()){
            String headerName = headerNames.nextElement();
            String headerValue = request.getHeader(headerName);
            headerMap.put(headerName, headerValue);
        }

        // 打印请求参数,记录起始时间
        long start = System.currentTimeMillis();
        log.info("请求| 请求接口:{} | 类名:{} | 方法:{} | header参数:{} | 参数:{} | 请求时间:{}", url, className, methodName, headerMap, paramMap, LocalDateTime.now());

        try {
            result = pjp.proceed();
            System.out.println(result.toString());
        } catch (Exception e) {
            log.error("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "failed");
            throw e;
        }

        // 获取执行完的时间 打印返回报文
        log.info("返回| 处理时间:{} 毫秒 | 返回结果 :{}", (System.currentTimeMillis() - start), "success");
        return result;
    }

}

4、在用户管理中的增加用户方法上使用@Log

package com.duan.controller;

import com.duan.anno.Log;
import com.duan.pojo.Result;
import com.duan.pojo.User;
import com.duan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author db
 * @version 1.0
 * @description UserController
 * @since 2024/4/15
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/addUser")
    @Log
    public Result addUser(@RequestBody User user){
        userService.AddUser(user);
        return Result.success();
    }
}

5、使用postman进行测试


代码地址:https://gitee.com/duan138/practice-code/tree/dev/aop

三、总结

在日常开发中AOP是我们经常用到的知识点,它不光可以记录日志,在不改变源码的前提下,动态的给它增加功能。或者大量的方法里面都有相同的方法,就可以用AOP进行代码提取,简化代码冗余。所以这部分内容还是要熟练掌握。

下篇文章来学习项目中常用的注解。


改变你能改变的,接受你不能改变的,关注公众号:程序员康康,一起成长,共同进步。