SpringBoot教程(二十四) | SpringBoot集成AOP实现日志记录

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?

  1. 降低耦合度:通过将公共功能从业务逻辑中分离出来,AOP可以显著降低系统各模块之间的耦合度,提高系统的可维护性和可扩展性。
  2. 提高代码复用性:公共功能的独立化使得这些功能可以在多个地方重复使用,而无需在每个业务逻辑中都重复编写相同的代码。
  3. 便于集中管理:AOP允许开发者将系统中的公共功能集中管理,便于统一维护和升级。
  4. 提高开发效率:通过使用AOP,开发者可以更加专注于业务逻辑的实现,而无需花费过多时间在公共功能的编写和维护上。

3. AOP一般用来干什么?

  1. 日志记录:在方法调用前后记录日志信息,帮助开发者进行性能分析和故障排查。
  2. 安全控制:在方法调用前进行权限检查,确保只有具有相应权限的用户才能执行该方法。
  3. 事务管理:在方法调用前后管理事务的开启、提交和回滚,确保数据的一致性和完整性。
  4. 异常处理:在方法调用过程中捕获并处理异常,提供友好的错误消息给用户。
  5. 性能监控:对方法调用的性能进行监控和分析,帮助开发者优化系统性能。
  6. 缓存优化:通过缓存方法调用的结果来提高系统性能,减少不必要的计算和资源消耗。

4. AOP 的核心概念

名词概念理解
通知(Advice)拦截到连接点之后所要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类我们要实现的功能,如日志记录,性能统计,安全控制,事务处理,异常处理等等,说明什么时候要干什么
连接点(Joint Point)被拦截到的点,如被拦截的方法、对类成员的访问以及异常处理程序块的执行等等,自身还能嵌套其他的 Joint PointSpring 允许你用通知的地方,方法有关的前前后后(包括抛出异常)
切入点(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 表达式

thistarget表达式用于匹配代理对象或目标对象。它们通常用于基于对象类型的过滤,而不是方法签名。

// 匹配代理对象实现了MyInterface接口的所有连接点
this(com.example.MyInterface)// 匹配目标对象实现了MyInterface接口的所有连接点
target(com.example.MyInterface)

注意:这些表达式在Spring AOP中可能不直接支持,因为Spring AOP是基于代理的,并且thistarget的区分在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请求日志

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/407867.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CSS3页面布局-三栏-中栏流动布局

三栏-中栏流动布局 用负外边距实现 实现三栏布局且中栏内容区不固定的核心问题就是处理右栏的定位&#xff0c; 并在中栏内容区大小改变时控制右栏与布局的关系。 控制两个外包装容器的外边距&#xff0c;一个包围三栏&#xff0c;一个包围左栏和中栏。 <!DOCTYPE html&…

vllm 部署GLM4模型进行 Zero-Shot 文本分类实验,让大模型给出分类原因,准确率可提高6%

文章目录 简介数据集实验设置数据集转换模型推理评估 简介 本文记录了使用 vllm 部署 GLM4-9B-Chat 模型进行 Zero-Shot 文本分类的实验过程与结果。通过对 AG_News 数据集的测试&#xff0c;研究发现大模型在直接进行分类时的准确率为 77%。然而&#xff0c;让模型给出分类原…

【软件测试面试题】WEB功能测试(持续更新)

Hi&#xff0c;大家好&#xff0c;我是小码哥。最近很多朋友都在说今年的互联网行情不好&#xff0c;面试很难&#xff0c;不知道怎么复习&#xff0c;我最近总结了一份在软件测试面试中比较常见的WEB功能测试面试面试题合集&#xff0c;希望对大家有帮助。 建议点赞收藏再阅读…

AI学习记录 - 怎么理解 torch 的 nn.Conv2d

有用就点个赞 怎么理解 nn.Conv2d 参数 conv_layer nn.Conv2d(in_channels1, out_channels 10 // 2, kernel_size3, stride2, padding0, biasFalse) in_channels in_channels 可以设置成1&#xff0c;2&#xff0c;3&#xff0c;4等等都可以&#xff0c;一般来说做图像识别…

微服务案例搭建

目录 一、案例搭建 1.数据库表 2.服务模块 二、具体代码实现如下&#xff1a; (1) 首先是大体框架为&#xff1a; &#xff08;2&#xff09;父模块中的pom文件配置 &#xff08;3&#xff09;shop_common模块&#xff0c;这个模块里面只需要配置pom.xml&#xff0c;与实体…

MySQL如何判断一个字段里面是否包含汉字

SQL查询中&#xff0c;length() 和 char_length() 都是用来获取字符串长度的函数 在单字节字符集下&#xff08;如ASCII&#xff09;&#xff1a;每个字符通常占用1个字节&#xff0c;因此length()和char_length()在这类字符集中给出的结果是一样 在多字节字符集下&#xff0…

matplotlib绘制子图以及局部放大效果

需求&#xff1a;绘制1*2的子图&#xff0c;子图1显示两个三角函数&#xff0c;子图2显示三个对数函数&#xff0c;子图2中对指定的区域进行放大。 绘图细节&#xff1a; 每个子图中每个函数的数据存放到一个列表中&#xff0c;然后将每个子图的数据统一存到一个列表中&#…

Go 使用Redis安装、实例和基本操作

Go使用Redis&#xff1a;详解go-redis/v9库 引言 Redis作为一个高性能的键值对数据库&#xff0c;广泛应用于缓存、消息队列、实时数据分析等场景。在Go语言中&#xff0c;go-redis/v9库提供了丰富的接口和高效的数据交互能力&#xff0c;使得在Go项目中集成Redis变得简单而高…

接口限流经典算法

文章目录 限流基于计数器的限流基于滑动窗口的限流桶漏斗算法令牌桶算法 限流 为了保证系统的安全性和稳定性&#xff0c;防止恶意流量和突发大量流量短时间内大量请求接口&#xff0c;造成服务器崩溃&#xff0c;接口的限流是有必要的。 以下是四种经典的限流算法。 基于计数…

Python测试框架Pytest的使用

pytest基础功能 pytset功能及使用示例1.assert断言2.参数化3.运行参数4.生成测试报告5.获取帮助6.控制用例的执行7.多进程运行用例8.通过标记表达式执行用例9.重新运行失败的用例10.setup和teardown函数 pytset功能及使用示例 1.assert断言 借助python的运算符号和关键字实现不…

UE5打包iOS运行查看Crash日志

1、查看Crash 1、通过xCode打开设备 2、选择APP打开最近的日志 3、选择崩溃时间点对应的日志 4、选择对应的工程打开 5、就能看到对应的Crash日志 2、为了防止Crash写代码需要注意 1、UObject在RemoveFromRoot之前先判断是否Root if (SelectedImage && Selecte…

Frog4Shell — FritzFrog 僵尸网络将一日攻击纳入其武器库

FritzFrog 的背景 Akamai 通过我们的全球传感器网络持续监控威胁,包括我们之前发现的威胁。其中包括FritzFrog 僵尸网络(最初于 2020 年发现),这是一个基于 Golang 的复杂点对点僵尸网络,经过编译可同时支持基于 AMD 和 ARM 的机器。该恶意软件得到积极维护,多年来通过增…

百日筑基第六十天-学习一下Tomcat

百日筑基第六十天-学习一下Tomcat 一、Tomcat 顶层架构 Tomcat 中最顶层的容器是 Server&#xff0c;代表着整个服务器&#xff0c;从上图中可以看出&#xff0c;一个 Server可以包含至少一个 Service&#xff0c;用于具体提供服务。Service 主要包含两个部分&#xff1a;Conn…

AI周报(8.18-8.24)

AI应用-XGO-Rider: 全球首款轮腿式桌面 AI 机器人 中国的 Luwu 智能打造的XGO-Rider 是全球首款轮腿式桌面 AI 机器人。这个小巧紧凑的机器人将轮式机器人的灵活性与腿式机器人的障碍处理能力相结合&#xff0c;可以全方位移动&#xff0c;轻松适应各种地形。 XGO-Rider 主要设…

服务商模式实现JSAPI小程序微信支付(javaphp)

官方文档 https://pay.weixin.qq.com/wiki/doc/apiv3_partner/open/pay/chapter2_1.shtml 使用wechatpay-php实现JSAPI支付&#xff08;服务商和普通商户&#xff09;文章浏览阅读1.3k次&#xff0c;点赞3次&#xff0c;收藏7次。之前我使用的sdk是“wechatpay-guzzle-middle…

python实用教程(二):安装配置Pycharm及使用(Win10)

上一篇&#xff1a;python实用教程&#xff08;一&#xff09;&#xff1a;安装配置anaconda&#xff08;Win10&#xff09;-CSDN博客 1、简介及下载 PyCharm是一款功能强大的 Python 编辑器&#xff0c;具有跨平台性。是Jetbrains家族中的一个明星产品。 下载地址&#xff…

redis实战——go-redis的使用与redis基础数据类型的使用场景(二)

一.go-redis操作hash 常用命令&#xff1a; redisClient.HSet("map", "name", "jack") // 批量设置 redisClient.HMSet("map", map[string]interface{}{"a": "b", "c": "d", "e"…

计算机毕业设计选题推荐-游戏比赛网上售票系统-Java/Python项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

棚子影院CMS程序PHP源码

01, 棚子影视是我现在最常用的一个看视频的网站&#xff0c;支持观看电影、国漫&#xff01;动漫&#xff0c;电视剧、综艺、记录片、香港剧等等。同时棚子影视支持手机&#xff0c;PC端在线观看&#xff0c;不用下载任何播放器&#xff0c;直接电脑或者手机打开网址就可以在线…

vue3 RouterLink路由跳转后RouterView组件未加载,页面未显示,且控制台无任何报错

在使用 vue3 开发项目过程中&#xff0c;组件之间使用 router-link 跳转&#xff0c;但是当我开发的组件跳转到其他组件时&#xff0c;其他组件的页面未加载&#xff0c;再跳转回自己的组件时&#xff0c;自己的组件也加载不出来了&#xff0c;浏览器刷新后页面可以加载出来。但…