SpringBoot教程(二十四) | SpringBoot集成AOP实现日志记录
- (一)AOP 概要
- 1. 什么是 AOP ?
- 2. 为什么要用 AOP?
- 3. AOP一般用来干什么?
- 4. AOP 的核心概念
- (二)Spring AOP
- 1. 简述
- 2. 相关注解
- 3. 执行顺序 (细节)
- 4. @Pointcut 切入点的不同表达式 示例
- (三) AOP如何添加日志记录
- 1. 引入AOP依赖
- 2. 自定义注解
- 3. 切面类 (仅供 讲解示例 使用)
- 4. 测试一(切入点用execution 表达式)
- 5. 测试二(切入点用自定义注解方式)
- `6. 项目上实际用切面类 (正式用这个哦!!!)`
- (四)AOP如何修改入参、返参
(一)AOP 概要
1. 什么是 AOP ?
AOP是Aspect Oriented Programming的缩写,意为面向切面编程。
这是一种通过预编译方式和运行期间动态代理实现程序功能统一维护的技术。
相比传统的面向对象编程(OOP),AOP更关注于将系统中的公共功能
(如日志记录、安全控制、事务处理、异常处理等)从业务逻辑中分离出来
,形成独立的模块,以便在不影响业务逻辑代码的情况下,对这些公共功能进行集中管理和维护。
2. 为什么要用 AOP?
- 降低耦合度:通过将公共功能从业务逻辑中分离出来,AOP可以显著降低系统各模块之间的耦合度,提高系统的可维护性和可扩展性。
- 提高代码复用性:公共功能的独立化使得这些功能可以在多个地方重复使用,而无需在每个业务逻辑中都重复编写相同的代码。
- 便于集中管理:AOP允许开发者将系统中的公共功能集中管理,便于统一维护和升级。
- 提高开发效率:通过使用AOP,开发者可以更加专注于业务逻辑的实现,而无需花费过多时间在公共功能的编写和维护上。
3. AOP一般用来干什么?
- 日志记录:在方法调用前后记录日志信息,帮助开发者进行性能分析和故障排查。
- 安全控制:在方法调用前进行权限检查,确保只有具有相应权限的用户才能执行该方法。
- 事务管理:在方法调用前后管理事务的开启、提交和回滚,确保数据的一致性和完整性。
- 异常处理:在方法调用过程中捕获并处理异常,提供友好的错误消息给用户。
- 性能监控:对方法调用的性能进行监控和分析,帮助开发者优化系统性能。
- 缓存优化:通过缓存方法调用的结果来提高系统性能,减少不必要的计算和资源消耗。
4. AOP 的核心概念
名词 | 概念 | 理解 |
---|---|---|
通知(Advice) | 拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类 | 我们要实现的功能,如日志记录,性能统计,安全控制,事务处理,异常处理等等,说明什么时候要干什么 |
连接点(Joint Point) | 被拦截到的点,如被拦截的方法、对类成员的访问以及异常处理程序块的执行等等,自身还能嵌套其他的 Joint Point | Spring 允许你用通知的地方,方法有关的前前后后(包括抛出异常) |
切入点(Pointcut) | 对连接点进行拦截的定义 | 指定通知到哪个方法,说明在哪干 |
切面(Aspect) | 切面类的定义,里面包含了切入点(Pointcut)和通知(Advice)的定义 | 切面就是通知和切入点的结合 |
目标对象(Target Object) | 切入点选择的对象,也就是需要被通知的对象;由于 Spring AOP 通过代理模式实现,所以该对象永远是被代理对象 | 业务逻辑本身 |
织入(Weaving) | 把切面应用到目标对象从而创建出 AOP 代理对象的过程。织入可以在编译期、类装载期、运行期进行,而 Spring 采用在运行期完成 | 切点定义了哪些连接点会得到通知 |
引入(Introduction ) | 可以在运行期为类动态添加方法和字段,Spring 允许引入新的接口到所有目标对象 | 引入就是在一个接口/类的基础上引入新的接口增强功能 |
AOP 代理(AOP Proxy ) | Spring AOP 可以使用 JDK 动态代理或者 CGLIB 代理,前者基于接口,后者基于类 | 通过代理来对目标对象应用切面 |
(二)Spring AOP
1. 简述
AOP 是 Spring 框架中的一个核心内容。在 Spring 中,AOP 代理可以用 JDK 动态代理或者 CGLIB 代理 CglibAopProxy 实现。Spring 中 AOP 代理由 Spring 的 IOC 容器负责生成和管理,其依赖关系也由 IOC 容器负责管理。
2. 相关注解
注解 | 说明 |
---|---|
@Aspect | 将一个 java 类定义为切面类 |
@Pointcut | 定义一个切入点,可以是一个规则表达式,比如下例中某个 package 下的所有函数, 也可以是一个注解等 |
@Before | 在切入点开始处切入内容 |
@After | 在切入点结尾处切入内容 |
@AfterReturning | 在切入点 return 内容之后处理逻辑 |
@Around | 在切入点前后切入内容,并自己控制何时执行切入点自身的内容 |
@AfterThrowing | 用来处理当切入内容部分抛出异常之后的处理逻辑 |
@Order(100) | AOP 切面执行顺序, @Before 数值越小越先执行,@After 和 @AfterReturning 数值越大越先执行 |
其中 @Before、@After、@AfterReturning、@Around、@AfterThrowing 都属于通知(Advice)。
3. 执行顺序 (细节)
- 正常情况下: @Around -> @Before -> 目标方法 -> @AfterReturning -> @After -> @Around
- 异常情况下: @Around -> @Before -> 目标方法 -> @AfterThrowing-> @After
为什么会存在@Around在前又在后?
原因是被Object result = proceedingJoinPoint.proceed()
这一段代码所影响的;
这段代码上面输出是在@Before之前,下面的输出是在@After之后
4. @Pointcut 切入点的不同表达式 示例
当然,以下是一些不同类型的切入点表达式(Pointcut Expressions)的示例,这些示例通常用于AOP(面向切面编程)框架中,如Spring AOP。
1. execution 表达式
这是最常用的切入点表达式,用于匹配方法执行的连接点。
// 匹配com.example.service包及其子包中所有类的所有方法
execution(* com.example.service..*.*(..))// 匹配com.example.service.UserService类中所有的public方法
execution(public * com.example.service.UserService.*(..))// 匹配所有返回类型为String,且方法名以find开头的public方法
execution(public String com.example..*.find*(..))
2. within 表达式
用于匹配连接点所在的Java类或包。
// 匹配com.example.service包及其子包中所有类的所有方法
within(com.example.service..*)// 精确匹配com.example.service.UserService类中的所有方法
within(com.example.service.UserService)
注意:within
表达式通常用于类型匹配,而不是方法签名匹配。
3. this 和 target 表达式
this
和target
表达式用于匹配代理对象或目标对象。它们通常用于基于对象类型的过滤,而不是方法签名。
// 匹配代理对象实现了MyInterface接口的所有连接点
this(com.example.MyInterface)// 匹配目标对象实现了MyInterface接口的所有连接点
target(com.example.MyInterface)
注意:这些表达式在Spring AOP中可能不直接支持,因为Spring AOP是基于代理的,并且this
和target
的区分在JDK动态代理和CGLIB代理中可能有所不同。但在AspectJ等更强大的AOP框架中,这些表达式是支持的。
4. args 表达式
args
表达式用于匹配方法参数。
// 匹配所有第一个参数为String类型的方法
args(String, ..)// 匹配所有参数中包含至少一个String类型的方法
args(.., String, ..)// 精确匹配第一个参数为特定类型的方法
args(com.example.MyType, ..)
注意:args
表达式中的参数类型是按顺序匹配的,但可以使用..
来匹配任意数量的额外参数。
5. @annotation、@within、@target 和 @args 表达式
这些表达式基于注解来匹配连接点。
// 匹配所有被@Transactional注解标注的方法
@annotation(org.springframework.transaction.annotation.Transactional)// 匹配所有在类级别被@Transactional注解标注的类中的方法
@within(org.springframework.transaction.annotation.Transactional)// 匹配所有目标对象(不是代理对象)被@Service注解标注的类中的方法
@target(org.springframework.stereotype.Service)// 匹配所有至少有一个参数被@Valid注解标注的方法
@args(javax.validation.Valid, ..)
这些表达式提供了强大的灵活性,允许开发者基于注解来定义切面的应用范围。
请注意,具体的语法和支持程度可能会根据你所使用的AOP框架(如Spring AOP、AspectJ等)而有所不同。上述示例主要基于Spring AOP和AspectJ的通用语法。
(三) AOP如何添加日志记录
1. 引入AOP依赖
在Spring Boot中引入AOP就跟引入其他模块一样,非常简单,只需要在pom.xml中加入如下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 自定义注解
由于面向切面的切入点(Pointcut)支持多种写法,
我这边也用了注解形式的写法,因此就自定义了以下这个注解,供后面测试使用
package com.example.reactboot.aop;import java.lang.annotation.*;/*** 自定义注解类*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档public @interface Aoplog {String value() default "";
}
3. 切面类 (仅供 讲解示例 使用)
为了把@Before、@After、@AfterReturning、@AfterThrowing、@Around都讲解一下,该处的切面类我在这边把日志记录的逻辑挪到了@Before、@After、@Around中
实际项目中,其实只需要在@Around里面去实现日志记录即可(因为在这里才能记录方法执行时间、入参、返参的修改)下面目录6.项目上实际用切面类
有提供
package com.example.reactboot.aop;import com.google.gson.Gson;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;/*** 系统日志:切面处理类** @Aspect:声明该类为一个注解类;* @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为某个 package 下的方法,也可以是自定义注解等;* <p>* 切点定义好后,就是围绕这个切点做文章了:* @Before: 在切点之前,织入相关代码;* @After: 在切点之后,织入相关代码;* @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;* @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;* @Around: 在切入点前后织入代码,并且可以自由的控制何时执行切点;*/
@Aspect
@Component
@Order(1)
public class WebLogAspect {private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class);/*** execution 表达式* 可以基于方法的返回类型、包名、类名、方法名以及参数类型等信息来精确地匹配方法.* 以下这个例子:匹配了 com.example.reactboot 包及其子包中所有类的所有方法。*///@Pointcut("execution(* com.example.reactboot.*.*(..))")@Pointcut("execution(public * com.example.reactboot.controller..*.*(..))")public void webLog() {}/*** @annotation 表达式* 用于匹配被指定注解标注的方法。* 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的方法。*/@Pointcut("@annotation(com.example.reactboot.aop.Aoplog)")public void aopLog() {}/*** 在切点之前织入* @param joinPoint* @throws Throwable** 以下这个例子:使用了execution 的内容*/@Before("webLog()")public void doBefore(JoinPoint joinPoint) throws Throwable {// 开始打印请求日志ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 打印请求相关参数logger.info("========== Start ==========");// 打印请求 urllogger.info("请求URL: {}", request.getRequestURL().toString());// 打印 Http methodlogger.info("请求方法: {}", request.getMethod());// 打印调用 controller 的全路径以及执行方法logger.info("全路径以及执行方法 Class Method: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());// 打印请求的 IPlogger.info("请求IP: {}", request.getRemoteAddr());// 打印请求入参logger.info("入参Request Args: {}", new Gson().toJson(joinPoint.getArgs()));}/*** 在切点之后织入** @throws Throwable*/@After("webLog()")public void doAfter() throws Throwable {logger.info("========== End ==========");// 每个请求之间空一行logger.info("");}/*** 切点返回内容后** @throws Throwable*/@AfterReturning("webLog()")public void afterReturning() {logger.info("===@AfterReturning========= 切点返回内容后执行 ==========");}/*** 切点抛出异常后** @throws Throwable*/@AfterThrowing("webLog()")public void afterThrowing() {logger.info("===@AfterThrowing========= 切点抛出异常后执行 ==========");}/*** 环绕* 环绕执行,就是在调用目标方法之前和调用之后,都会执行一定的逻辑* @param proceedingJoinPoint* @return* @throws Throwable*/@Around("webLog()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long startTime = System.currentTimeMillis();Object result = proceedingJoinPoint.proceed();// 打印出参logger.info("出参Response Args : {}", result);// 执行耗时logger.info("执行耗时Time-Consuming : {} ms", System.currentTimeMillis() - startTime);return result;}
}
4. 测试一(切入点用execution 表达式)
以上的切面处理类,使用的 webLog 方法,用的为execution 表达式
控制层 代码
package com.example.reactboot.controller;import com.example.reactboot.aop.Aoplog;
import com.example.reactboot.aop.WebLogAspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 单纯的@Controller,请求的资源面向的就是页面* 而 @RestController,请求的资源面向的是对象或者字符串*/
@RestController
public class HelloController {private final static Logger logger = LoggerFactory.getLogger(HelloController.class);@RequestMapping("/index")public String sayHello(){logger.info("我是index接口");return "index";}
}
请求该接口后,控制台显示打印如下操作
是有切面日志输出的
5. 测试二(切入点用自定义注解方式)
先把以上切面类(WebLogAspect )里面的webLog()全部换成aopLog() 再进行测试
控制层 代码
我新写了一个 xiaoming 的接口,在它上面加上了@Aoplog注解
package com.example.reactboot.controller;import com.example.reactboot.aop.Aoplog;
import com.example.reactboot.aop.WebLogAspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 单纯的@Controller,请求的资源面向的就是页面* 而 @RestController,请求的资源面向的是对象或者字符串*/
@RestController
public class HelloController {private final static Logger logger = LoggerFactory.getLogger(HelloController.class);@RequestMapping("/index")public String sayHello(){logger.info("我是index接口");return "index";}@Aoplog(value = "xiaoming")@RequestMapping("/xiaoming")public String xiaoming(){logger.info("我是xiaoming接口");return "xiaoming";}
}
两个接口都请求后,控制台显示打印如下操作
只有加了注解的才会有切面日志输出
6. 项目上实际用切面类 (正式用这个哦!!!)
正式项目中,直接在@Around中完成日志记录的操作
实体类
package com.example.reactboot.aop;import java.io.Serializable;
import java.util.Date;/*** 操作日志* @author * @since */
public class JgCompanyOperateLog implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/private String unid;/*** 接口名称*/private String interfaceName;/*** 菜单名称*/private String menu;/*** 访问内容*/private String content;/*** 操作结果 0:失败,1:成功*/private Boolean sucessFlag;/*** 访问时间*/private Date operateTime;/*** 类名*/private String className;/*** 方法名*/private String methodName;/*** 参数*/private String params;/*** 请求地址URL*/private String requestUrl;/*** 请求ip*/private String requestIp;/*** 请求耗时*/private Integer timeConsum;public String getClassName() {return className;}public void setClassName(String className) {this.className = className;}public String getMethodName() {return methodName;}public void setMethodName(String methodName) {this.methodName = methodName;}public String getParams() {return params;}public void setParams(String params) {this.params = params;}public String getRequestUrl() {return requestUrl;}public void setRequestUrl(String requestUrl) {this.requestUrl = requestUrl;}public String getRequestIp() {return requestIp;}public void setRequestIp(String requestIp) {this.requestIp = requestIp;}public Integer getTimeConsum() {return timeConsum;}public void setTimeConsum(Integer timeConsum) {this.timeConsum = timeConsum;}public String getUnid() {return unid;}public void setUnid(String unid) {this.unid = unid;}public String getInterfaceName() {return interfaceName;}public void setInterfaceName(String interfaceName) {this.interfaceName = interfaceName;}public String getMenu() {return menu;}public void setMenu(String menu) {this.menu = menu;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}public Boolean getSucessFlag() {return sucessFlag;}public void setSucessFlag(Boolean sucessFlag) {this.sucessFlag = sucessFlag;}public Date getOperateTime() {return operateTime;}public void setOperateTime(Date operateTime) {this.operateTime = operateTime;}@Overridepublic String toString() {return "JgCompanyOperateLog{" +"unid=" + unid +", interfaceName=" + interfaceName +", menu=" + menu +", content=" + content +", sucessFlag=" + sucessFlag +", operateTime=" + operateTime +"}";}
}
切面类
package com.example.reactboot.aop;import com.fasterxml.jackson.databind.ObjectMapper;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
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.util.Date;
import java.util.UUID;/*** 系统日志:切面处理类** @Aspect:声明该类为一个注解类;* @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为某个 package 下的方法,也可以是自定义注解等;* <p>* 切点定义好后,就是围绕这个切点做文章了:* @Before: 在切点之前,织入相关代码;* @After: 在切点之后,织入相关代码;* @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;* @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;* @Around: 在切入点前后织入代码,并且可以自由的控制何时执行切点;*/
@Aspect
@Component
@Order(1)
public class WebLogAspect {private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);/*** execution 表达式* 可以基于方法的返回类型、包名、类名、方法名以及参数类型等信息来精确地匹配方法.* 以下这个例子:匹配了 com.example.reactboot 包及其子包中所有类的所有方法。*///@Pointcut("execution(* com.example.reactboot.*.*(..))")@Pointcut("execution(public * com.example.reactboot.controller..*.*(..))")public void webLog() {}/*** @annotation 表达式* 用于匹配被指定注解标注的方法。* 以下这个例子:匹配了所有被 com.example.reactboot.aop.Aoplog 注解标注的方法。*/@Pointcut("@annotation(com.example.reactboot.aop.Aoplog)")public void aopLog() {}/*** 切点之前** @param joinPoint* @throws Throwable*/@Before("webLog()")public void before(JoinPoint joinPoint) {logger.info("============ 切点之前(@Before)==========");}/*** 切点之后** @throws Throwable*/@After("webLog()")public void after() {logger.info("============ 切点后执行(@After) ==========");}/*** 切点返回内容后** @throws Throwable*/@AfterReturning("webLog()")public void afterReturning() {logger.info("============ 切点返回内容后执行(@AfterReturning) ==========");}/*** 切点抛出异常后** @throws Throwable*/@AfterThrowing("webLog()")public void afterThrowing() {logger.info("============ 切点抛出异常后执行(@AfterThrowing) ==========");}/*** 环绕** @param joinPoint* @return* @throws Throwable*/@Around("webLog()")public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {logger.info("============ 环绕(@Around) ==========");return logAround(joinPoint);}private static final ObjectMapper MAPPER = new ObjectMapper();private Object logAround(ProceedingJoinPoint point) throws Throwable {long beginTime = System.currentTimeMillis();Object result = null;Exception exception = null;try {result = point.proceed();} catch (Exception exp) {exception = exp;}//目标方法完成时间long time = System.currentTimeMillis() - beginTime;saveLog(point, result, exception, time);if (exception != null) {throw exception;}return result;}/**** @param joinPoint* @param result 目标方法返回结果* @param exception 目标方法返回异常* @param time 目标方法完成时间*/private void saveLog(ProceedingJoinPoint joinPoint, Object result, Exception exception, long time) {JgCompanyOperateLog dto = new JgCompanyOperateLog();dto.setUnid(String.valueOf(UUID.randomUUID()));dto.setTimeConsum(Math.toIntExact(time));ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();logger.info("============ 执行point.proceed之后的操作(@Around) ==========");// 请求地址URLlogger.info("URL:{}" ,request.getRequestURL().toString());// 请求方法logger.info("HTTP Method : {}" , request.getMethod());// 请求IPlogger.info("IP:{}" , request.getRemoteAddr());dto.setRequestUrl(request.getRequestURL().toString());dto.setMethodName(request.getMethod());dto.setRequestIp(request.getRemoteAddr());try {MethodSignature signature = (MethodSignature) joinPoint.getSignature();//请求的 类名、方法名String className = joinPoint.getTarget().getClass().getName();String signName = signature.getDeclaringTypeName();if (!signName.equalsIgnoreCase(className)) {signName += "|" + className;}logger.info("类名 : {}" , className);logger.info("方法名 : {}" , signName);logger.info("接口名称 : {}" , signature.getName());logger.info("访问时间 : {}" , new Date());dto.setClassName(className);dto.setMethodName(signName);dto.setOperateTime(new Date());String methodName = signature.getName();dto.setInterfaceName(methodName);//请求的参数Object[] args = joinPoint.getArgs();if (args != null && args.length > 0) {dto.setParams(serial(args));}} catch (Exception e) {dto.setContent(e.toString());}logger.info("存储的日志对象 : {}",dto.toString());//进行数据库保存//jgCompanyOperateLogFeign.saveJgCompanyOperateLog(dto);}private static String serial(Object obj) {try {//用于序列化Java对象为JSON格式字符串return MAPPER.writeValueAsString(obj);} catch (Exception ex) {return obj.toString();}}}
(四)AOP如何修改入参、返参
主要是用到环绕@Around
/*** 环绕* 环绕执行,就是在调用目标方法之前和调用之后,都会执行一定的逻辑* @param proceedingJoinPoint* @return* @throws Throwable*/@Around("webLog()")public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {long startTime = System.currentTimeMillis();logger.info("@Around中的{}", startTime);//获取目标方法的入参Object[] args = proceedingJoinPoint.getArgs(); for (int i = 0; i < args.length; i++) {logger.info("argsName: "+args[i]); //输出目标方法的参数if(i==0){args[i]="无语123";}}// 这个proceed就需要把入参设置进去Object result = proceedingJoinPoint.proceed(args);//根据原方法返回值的类型,进行修改if (result instanceof String) {result = "我把你的值给改了,哈哈哈哈"; }if (result instanceof UserBean) {UserBean entity = (UserBean) result;entity.setAddress("我把你的值给改了,哈哈哈哈");result = entity; }// 打印出参logger.info("@Around中出参Response Args : {}", result);// 执行耗时logger.info("@Around中执行耗时Time-Consuming : {} ms", System.currentTimeMillis() - startTime);return result;}
接口请求为http://localhost:9021/loginIn?name=项目&age=22
返回结果为
可以看到 name 入参被改了,同时返回值的address被加上了值
参考文章
【1】Spring Boot AOP 切面统一打印请求与响应日志
【2】Spring Boot 2.X(八):Spring AOP 实现简单的日志切面
【3】在IDEA 、springboot中使用切面aop实现日志信息的记录到数据库
【4】springboot项目使用切面记录用户操作日志
【5】Spring Boot中使用AOP统一处理Web请求日志