使用SpringBoot AOP记录操作日志和异常日志

使用SpringBoot AOP记录操作日志和异常日志

平时我们在做项目时经常需要对一些重要功能操作记录日志,方便以后跟踪是谁在操作此功能;我们在操作某些功

能时也有可能会发生异常,但是每次发生异常要定位原因我们都要到服务器去查询日志才能找到,而且也不能对发

生的异常进行统计,从而改进我们的项目,要是能做个功能专门来记录操作日志和异常日志那就好了。

当然我们肯定有方法来做这件事情,而且也不会很难,我们可以在需要的方法中增加记录日志的代码,和在每个方

法中增加记录异常的代码,最终把记录的日志存到数据库中。听起来好像很容易,但是我们做起来会发现,做这项

工作很繁琐,而且都是在做一些重复性工作,还增加大量冗余代码,这种方式记录日志肯定是不可行的。

我们以前学过Spring 三大特性,IOC(控制反转),DI(依赖注入),AOP(面向切面),那其中AOP的主要功能

就是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来。今天我们就来用

springBoot Aop 来做日志记录,好了,废话说了一大堆还是上货吧。

1、创建日志记录表、异常日志表,表结构如下

操作日志表OperationLog

CREATE TABLE `OperationLog` (
  `oper_id` varchar(64) DEFAULT NULL COMMENT '主键id',
  `oper_modul` varchar(64) DEFAULT NULL COMMENT '功能模块',
  `oper_type` varchar(64) DEFAULT NULL COMMENT '操作类型',
  `oper_desc` varchar(500) DEFAULT NULL COMMENT '操作描述',
  `oper_requ_param` text COMMENT '请求参数',
  `oper_resp_param` text COMMENT '返回参数',
  `oper_user_id` varchar(64) DEFAULT NULL COMMENT '操作员ID',
  `oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员名称',
  `oper_method` varchar(255) DEFAULT NULL COMMENT '操作方法',
  `oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',
  `oper_ip` varchar(64) DEFAULT NULL COMMENT '请求ID',
  `oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `oper_ver` varchar(64) DEFAULT NULL COMMENT '操作版本号',
  PRIMARY KEY (`oper_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

使用SpringBoot AOP记录操作日志和异常日志_第1张图片

异常日志表ExceptionLog

CREATE TABLE `ExceptionLog` (
  `exp_id` varchar(64) NOT NULL DEFAULT "" COMMENT '主键id',
  `exp_requ_param` text COMMENT '请求参数',
  `exp_name` varchar(255) COMMENT '异常名称',
  `exp_message` text COMMENT '异常信息',
  `oper_user_id` varchar(64) DEFAULT NULL COMMENT '操作员ID',
  `oper_user_name` varchar(64) DEFAULT NULL COMMENT '操作员名称',
  `oper_method` varchar(255) DEFAULT NULL COMMENT '操作方法',
  `oper_uri` varchar(255) DEFAULT NULL COMMENT '请求URI',
  `oper_ip` varchar(64) DEFAULT NULL COMMENT '请求ID',
  `oper_create_time` datetime DEFAULT NULL COMMENT '操作时间',
  `oper_ver` varchar(64) DEFAULT NULL COMMENT '操作版本号',
  PRIMARY KEY (`exp_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

使用SpringBoot AOP记录操作日志和异常日志_第2张图片

2、添加Maven依赖


<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.5.6version>
        <relativePath/>
    parent>

    <groupId>com.aop.loggroupId>
    <artifactId>spring-boot-aop-logartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>spring-boot-aop-logname>
    <description>spring-boot-aop-logdescription>

    <properties>
        <java.version>1.8java.version>
    properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-aopartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>

        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.3.1.tmpversion>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.58version>
        dependency>

        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.22version>
        dependency>

    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

3、创建实体类

ExceptionLog实体

package com.aop.log.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:04
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ExceptionLog")
public class ExceptionLog {

    // 主键id
    @TableId(value = "exp_id", type = IdType.ASSIGN_UUID)
    private String expId;
    // 请求参数
    private String expRequParam;
    // 异常名称
    private String expName;
    // 异常信息
    private String expMessage;
    // 操作员ID
    private String operUserId;
    // 操作员名称
    private String operUserName;
    // 操作方法
    private String operMethod;
    // 请求URI
    private String operUri;
    // 请求ID
    private String operIp;
    // 操作时间
    private Date operCreateTime;
    // 操作版本号
    private String operVer;
}

OperationLog实体

package com.aop.log.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

/**
 * @author zhangshixing
 * @date 2021年11月05日 9:52
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("OperationLog")
public class OperationLog implements Serializable {

    // 主键id
    @TableId(value = "oper_id", type = IdType.ASSIGN_UUID)
    private String operId;
    // 功能模块
    private String operModul;
    // 操作类型
    private String operType;
    // 操作描述
    private String operDesc;
    // 请求参数
    private String operRequParam;
    // 返回参数
    private String operRespParam;
    // 操作员ID
    private String operUserId;
    // 操作员名称
    private String operUserName;
    // 操作方法
    private String operMethod;
    // 请求URI
    private String operUri;
    // 请求ID
    private String operIp;
    // 操作时间
    private Date operCreateTime;
    // 操作版本号
    private String operVer;
}

OrderInfo订单实体类

package com.aop.log.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:33
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderInfo {

    /**
     * 这里只是为了测试所以只添加了两个参数
     */
    // 订单编号
    private String orderid;
    // 订单金额
    private int price;

}

RespBean公共返回对象

package com.aop.log.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:31
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {

    private long code;
    private String message;
    private Object obj;

    public static RespBean success(String message) {

        return new RespBean(200, message, null);
    }

    public static RespBean success(String message, Object obj) {

        return new RespBean(200, message, obj);
    }

    public static RespBean error(String message) {

        return new RespBean(500, message, null);
    }

    public static RespBean error(String message, Object obj) {

        return new RespBean(500, message, obj);
    }
}

4、创建Mapper

OperationLogMapper

package com.aop.log.mapper;

import com.aop.log.entity.OperationLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperationLogMapper extends BaseMapper<OperationLog> {

}

ExceptionLogMapper

package com.aop.log.mapper;

import com.aop.log.entity.ExceptionLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ExceptionLogMapper extends BaseMapper<ExceptionLog> {

}

5、创建service

OperationLogService

package com.aop.log.service;

import com.aop.log.entity.OperationLog;
import com.baomidou.mybatisplus.extension.service.IService;

public interface OperationLogService extends IService<OperationLog> {
}

ExceptionLogService

package com.aop.log.service;

import com.aop.log.entity.ExceptionLog;
import com.baomidou.mybatisplus.extension.service.IService;

public interface ExceptionLogService extends IService<ExceptionLog> {

}

OperationLogServiceImpl

package com.aop.log.service.impl;

import com.aop.log.entity.OperationLog;
import com.aop.log.mapper.OperationLogMapper;
import com.aop.log.service.OperationLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:10
 */
@Service
public class OperationLogServiceImpl extends ServiceImpl<OperationLogMapper, OperationLog>
        implements OperationLogService {
}

ExceptionLogServiceImpl

package com.aop.log.service.impl;

import com.aop.log.entity.ExceptionLog;
import com.aop.log.mapper.ExceptionLogMapper;
import com.aop.log.service.ExceptionLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:10
 */
@Service
public class ExceptionLogServiceImpl extends ServiceImpl<ExceptionLogMapper, ExceptionLog>
        implements ExceptionLogService {
}

6、创建操作日志注解类OperLog

package com.aop.log.annotation;

import java.lang.annotation.*;

/**
 * @author zhangshixing
 * @date 2021年11月05日 9:43
 * 自定义操作日志注解
 */
//注解放置的目标位置,METHOD是可注解在方法级别上
@Target(ElementType.METHOD)
//注解在哪个阶段执行
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperLog {
    // 操作模块
    String operModul() default "";

    // 操作类型
    String operType() default "";

    // 操作说明
    String operDesc() default "";
}
package com.aop.log.annotation;

public class OprLogConst {

    public static final String ADD = "add";
    public static final String INSERET = "insert";
    public static final String UPDATE = "update";
    public static final String DELETE = "delete";
}

7、创建切面类记录操作日志

package com.aop.log.aop;

import com.alibaba.fastjson.JSON;
import com.aop.log.annotation.OperLog;
import com.aop.log.entity.ExceptionLog;
import com.aop.log.entity.OperationLog;
import com.aop.log.service.ExceptionLogService;
import com.aop.log.service.OperationLogService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author zhangshixing
 * @date 2021年11月05日 9:46
 * 切面处理类,操作日志异常日志记录处理
 */
@Aspect
@Component
public class OperLogAspect {

    /**
     * 操作版本号
     * 项目启动时从命令行传入,例如:java -jar xxx.war --version=201902
     */
    @Value("${version}")
    private String operVer;

    @Autowired
    private OperationLogService operationLogService;

    @Autowired
    private ExceptionLogService exceptionLogService;

    /**
     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
     */
    @Pointcut("@annotation(com.aop.log.annotation.OperLog)")
    public void operLogPoinCut() {
    }

    /**
     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
     */
    @Pointcut("execution(* com.aop.log.controller..*.*(..))")
    public void operExceptionLogPoinCut() {
    }

    /**
     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
     *
     * @param joinPoint 切入点
     * @param keys      返回结果
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "keys")
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
        // 创建OperationLog对象
        OperationLog operlog = new OperationLog();
        try {
            // 主键ID
            String uuid = UUID.randomUUID().toString();
            System.out.println("saveOperLog:" + uuid);
            operlog.setOperId(uuid);
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取操作
            OperLog opLog = method.getAnnotation(OperLog.class);
            if (opLog != null) {
                String operModul = opLog.operModul();
                String operType = opLog.operType();
                String operDesc = opLog.operDesc();
                // 操作模块
                operlog.setOperModul(operModul);
                // 操作类型
                operlog.setOperType(operType);
                // 操作描述
                operlog.setOperDesc(operDesc);
            }
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;
            // 请求方法
            operlog.setOperMethod(methodName);
            // 请求的参数
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 将参数所在的数组转换成json
            String params = JSON.toJSONString(rtnMap);
            // 请求参数
            operlog.setOperRequParam(params);
            // 返回结果
            operlog.setOperRespParam(JSON.toJSONString(keys));
            // 请求用户ID
            // operlog.setOperUserId(UserShiroUtil.getCurrentUserLoginName());
            // 这里写一个固定的用户ID
            operlog.setOperUserId("100293784");
            // 请求用户名称
            // operlog.setOperUserName(UserShiroUtil.getCurrentUserName());
            // // 这里写一个固定的用户名
            operlog.setOperUserName("yiyiyi");
            // 请求IP
            // operlog.setOperIp(IPUtil.getRemortIP(request));
            // 这里写一个固定的IP
            operlog.setOperIp("127.0.0.1");
            // 请求URI
            operlog.setOperUri(request.getRequestURI());
            // 创建时间
            operlog.setOperCreateTime(new Date());
            // 操作版本
            operlog.setOperVer(operVer);
            // 将数据插入到数据库
            operationLogService.save(operlog);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
     *
     * @param joinPoint 切入点
     * @param e         异常信息
     */
    @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);
        ExceptionLog excepLog = new ExceptionLog();
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            String uuid = UUID.randomUUID().toString();
            System.out.println("saveExceptionLog:" + uuid);
            excepLog.setExpId(uuid);
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;
            // 请求的参数
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 将参数所在的数组转换成json
            String params = JSON.toJSONString(rtnMap);
            // 请求参数
            excepLog.setExpRequParam(params);
            // 请求方法名
            excepLog.setOperMethod(methodName);
            // 异常名称
            excepLog.setExpName(e.getClass().getName());
            // 异常信息
            excepLog.setExpMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));
            // 操作员ID
            // excepLog.setOperUserName(UserShiroUtil.getCurrentUserName());
            excepLog.setOperUserId("100293784");
            // 操作员名称
            excepLog.setOperUserName("yiyiyi");
            // 操作URI
            excepLog.setOperUri(request.getRequestURI());
            // 操作员IP
            // excepLog.setOperIp(IPUtil.getRemortIP(request));
            excepLog.setOperIp("127.0.0.1");
            // 操作版本号
            excepLog.setOperVer(operVer);
            // 发生异常时间
            excepLog.setOperCreateTime(new Date());
            exceptionLogService.save(excepLog);
        } catch (Exception e2) {
            e2.printStackTrace();
        }

    }

    /**
     * 转换request 请求参数
     *
     * @param paramMap request获取的参数数组
     */
    public Map<String, String> converMap(Map<String, String[]> paramMap) {
        Map<String, String> rtnMap = new HashMap<String, String>();
        for (String key : paramMap.keySet()) {
            rtnMap.put(key, paramMap.get(key)[0]);
        }
        return rtnMap;
    }

    /**
     * 转换异常信息为字符串
     *
     * @param exceptionName    异常名称
     * @param exceptionMessage 异常信息
     * @param elements         堆栈信息
     */
    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        for (StackTraceElement stet : elements) {
            strbuff.append(stet + "\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        return message;
    }
}

8、在Controller层方法添加@OperLog注解

package com.aop.log.controller;

import com.aop.log.annotation.OperLog;
import com.aop.log.annotation.OprLogConst;
import com.aop.log.entity.OrderInfo;
import com.aop.log.entity.RespBean;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author zhangshixing
 * @date 2021年11月05日 10:25
 */
@Controller
public class OrderController {
    /**
     * 新增订单信息
     */
    @RequestMapping(value = "addOrderInfo")
    @ResponseBody
    @OperLog(operModul = "销售管理-订单新增", operType = OprLogConst.ADD, operDesc = "订单新增功能")
    public RespBean addOrderInfo(OrderInfo orderInfo) {
        if (orderInfo.getPrice() < 0) {
            System.out.println(1 / 0);
            return RespBean.error("提交失败!!!");
        } else if (orderInfo.getPrice() < 30) {
            return RespBean.success("提交成功!!!");
        } else {
            return RespBean.error("提交失败!!!");
        }
    }
}

9、启动类和配置文件

version = 2.1.1
server.port = 9000
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/log?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username = root
spring.datasource.password = root
package com.aop.log;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootAopLogApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootAopLogApplication.class, args);
	}

}

10、操作日志、异常日志查询功能

10.1 记录操作日志

使用SpringBoot AOP记录操作日志和异常日志_第3张图片

在这里插入图片描述

使用SpringBoot AOP记录操作日志和异常日志_第4张图片

在这里插入图片描述

10.2 记录异常日志

使用SpringBoot AOP记录操作日志和异常日志_第5张图片

在这里插入图片描述

11、execution 表达式

学习Spring中的aop组装切面时遇到的execution表达式,下面是execution表达式的详解。

切入点表达式:

整个表达式可以分为五个部分:

1、execution(): 表达式主体。

2、第一个*号:方法返回类型, *号表示所有的类型。

3、包名:表示需要拦截的包名。

4、第二个*号:表示类名,*号表示所有的类。

5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面( )里面表示方法的参数,两个句点表示任

何参数。

表达式语法:

execution(* 包名.*.*(..))

规范写法:execution(* com.baizhi.service.UserServiceImpl.*(..))

这个表达式是重点 ,是最通用的,表示执行接口下的所有接口方法。

execution表达式举例:

书写接口实现方法:UserServiceImpl

方法类型:

add(); query(); add(String name); add(User user); add(String name,Integer age);

execution(* service.UserServiceImpl.add(..))        //执行add()方法
execution(* service.UserServiceImpl.add(String))    //执行add(String name)方法
execution(* service.UserServiceImpl.add(com.baizhi.entity.User))   //执行add(User user)方法
execution(* service.UserServiceImpl.add(String , Integer))    //执行add(String name,Interger age)方法

execution表达式的一般用法:

execution(* service.UserServiceImpl.*(java.util.List))
返回值:任意
包:com.baizhi.service
类:UserServiceImpl
方法:任意
参数:必须是List集合
execution(* service.UserServiceImpl.add*(..))  重点
返回值:任意
包:com.baizhi.service
类:UserServiceImpl
方法:以add关键字开头的方法
参数:任意
execution(* service.UserServiceImpl.*.*(..))  重点
返回值:任意
包:com.baizhi.service
类:当前包下的所有类
方法:所有类中的所有方法
参数:任意
execution(* service..*.*(..))  重点
返回值:任意
包:service包以及它下面所有子包
类:所有包中的所有类
方法:所有类中的所有方法
参数:任意
execution(* *(..))   重点,不建议这样写,栈溢出

注意:要尽可能精准的切入。

你可能感兴趣的:(spring,boot,spring,boot)