news 2026/5/1 9:41:31

Spring Boot中AOP日志序列化问题解决方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot中AOP日志序列化问题解决方案
spring boot中, package com.weiyu.model; import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * 文件数据 */ @Data @AllArgsConstructor @NoArgsConstructor public class FileData { private String fileName; @JsonIgnore // 防止序列化到日志,是一个双向忽略的注解,就是接收和发送参数都为null private byte[] fileContent; } package com.weiyu.aop; import com.alibaba.fastjson.JSONObject; import com.weiyu.mapper.PerformanceLogMapper; import com.weiyu.model.PerformanceLog; 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.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.util.Arrays; /** * 时间AOP类(时间切面类) */ @Component // 将当前类 TimeAspect 交给 Spring IOC 容器管理 @Aspect // 当前类 TimeAspect 为AOP类(切面类) @Slf4j public class TimeAspect { @Autowired private PerformanceLogMapper performanceLogMapper; /** * 切面:计算执行耗时 * @param joinPoint 固定参数 ProceedingJoinPoint joinPoint(API调用) * @return 原始方法运行的返回值 Object * @throws Throwable 异常 */ // @Before 前置通知,在目标方法前被执行 // @After 后置通知,在目标方法后被执行 // @Around 环绕通知,在目标方法前、后都被执行 // @Around("execution(* com.weiyu.service.*.*(..))") // 切入点表达式,com.weiyu.service 这个包下的所有类和接口的所有方法都会切入执行 // @Around("execution(* com.weiyu.service.impl.PrintServiceImpl.*(..))") // 切入点表达式,com.weiyu.service.impl.PrintServiceImpl 这个类下的所有方法都会切入执行 /* 切入点表达式: 使用 || 组合多个条件 com.weiyu.service.impl.PrintServiceImpl 这个类下的所有方法 或者 com.weiyu.service.impl.ReportServiceImpl 这个类下的所有以query开头的方法 或者 com.weiyu.service.impl.JJDServiceImpl 这个类下的所有以query开头的、返回值类型为PageBean的、参数类型为JJDQueryDTO的方法 com.weiyu.mapper 这个包下的所有类和接口 都会切入执行 通配符: *: 匹配任意字符(除包分隔符 .) ..:匹配当前包及其子包,或任意数量的参数 */ @Around("execution(* com.weiyu.service.impl.PrintServiceImpl.*(..)) || " + "execution(* com.weiyu.service.impl.ReportServiceImpl.query*(..)) || " + "execution(com.weiyu.pojo.PageBean com.weiyu.service.impl.JJDServiceImpl.query*(com.weiyu.pojo.JJDQueryDTO))") public Object calculateExecuteTime(ProceedingJoinPoint joinPoint) throws Throwable { // 记录方法执行的开始时间 long beginTime = System.currentTimeMillis(); // 调用原始方法运行 Object result = joinPoint.proceed(); // 记录方法执行的结束时间 long endTime = System.currentTimeMillis(); // 计算执行耗时,单位ms long executeTime = endTime - beginTime; // 输出到日志,包含返回类型、类名、方法名、执行耗时 log.info("{} 执行时间:{} ms", joinPoint.getSignature(), executeTime); // 返回原始方法运行的返回值 return result; } /** * 切面:计算sql执行耗时,定位慢sql * @param joinPoint 固定参数 ProceedingJoinPoint joinPoint(API调用) * @return 原始方法运行的返回值 Object * @throws Throwable 异常 */ @Around("execution(* com.weiyu.mapper.*.*(..))") // 切入点表达式,com.weiyu.mapper 这个包下的所有类和接口都会切入执行 public Object sqlExecutionTimeDuration(ProceedingJoinPoint joinPoint) throws Throwable { // 记录方法执行的开始时间 long beginTime = System.currentTimeMillis(); // 调用原始方法运行 Object result = joinPoint.proceed(); // 记录方法执行的结束时间 long endTime = System.currentTimeMillis(); // 计算执行耗时,单位ms long executionTimeDuration = endTime - beginTime; // 获取方法签名 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); // 获取包名 String packageName = methodSignature.getDeclaringType().getPackageName(); // 获取类名(简单类名,不带包名) String className = methodSignature.getDeclaringType().getSimpleName(); // 获取方法名 String methodName = methodSignature.getName(); // 获取方法参数类型 Object[] methodParamTypeArgs = methodSignature.getParameterTypes(); String methodParamTypes = Arrays.toString(methodParamTypeArgs); // 获取方法参数名 String[] methodParamNameArgs = methodSignature.getParameterNames(); String methodParamNames = Arrays.toString(methodParamNameArgs); // 获取方法参数值 Object[] methodParamValueArgs = joinPoint.getArgs(); String methodParamValues = Arrays.toString(methodParamValueArgs); // 获取方法返回类型(简单类名,不带包名) String methodReturnType = methodSignature.getReturnType().getSimpleName(); // 执行耗时超过3000ms if (executionTimeDuration > 3000 || "FileData".equals(methodReturnType)) { // 方法返回值 String methodReturnValue = JSONObject.toJSONString(result); // 输出到日志,包含执行耗时、包名、类名、方法名、方法参数类型、方法参数名、方法参数值、方法返回类型、方法返回值 log.info("慢sql>>>执行sql时间:{} ms,包名:{},类名:{},方法名:{},方法参数类型:{},方法参数名:{},方法参数值:{},方法返回类型:{},方法返回值:{}", executionTimeDuration, packageName, className, methodName, methodParamTypes, methodParamNames, methodParamValues, methodReturnType, methodReturnValue); // 性能日志持久化 PerformanceLog performanceLog = new PerformanceLog(); performanceLog.setPerformanceType(1); performanceLog.setOperationTime(LocalDateTime.now()); performanceLog.setPackageName(packageName); performanceLog.setClassName(className); performanceLog.setMethodName(methodName); performanceLog.setMethodParamTypes(methodParamTypes); performanceLog.setMethodParamNames(methodParamNames); performanceLog.setMethodParamValues(methodParamValues); performanceLog.setMethodReturnType(methodReturnType); performanceLog.setMethodReturnValue(methodReturnValue); performanceLog.setExecutionTimeDuration(executionTimeDuration); // 将性能日志存储到数据库(异步执行) performanceLogMapper.insert(performanceLog); } else { log.info("{} 执行sql时间:{} ms", joinPoint.getSignature(), executionTimeDuration); } // 返回原始方法运行的返回值 return result; } } 为什么这样还会输出: 2026-01-17T12:44:24.310+08:00 INFO 12652 --- [http-nio-8080-exec-1] com.weiyu.aop.TimeAspect : 慢sql>>>执行sql时间:145 ms,包名:com.weiyu.mapper,类名:WorkInstructionMapper,方法名:selectFileData,方法参数类型:[class java.lang.String],方法参数名:[fileNo],方法参数值:[4-5-6],方法返回类型:FileData,方法返回值:{"fileContent":"JVBERi0xLjcNCiW1tbW1DQoxIDAgb2JqDQo8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFIvTGFuZyh6aCkgL1N0cnVjdFRyZWVSb290IDI3MyAwIFIvTWFya0luZm88PC9NYXJrZWQgdHJ1ZT4+L01ldGFkYXRhIDE3MTQgMCBSL1ZpZXdlclByZWZlcmVuY2VzIDE3MTUgMCBSPj4NCmVuZG9iag0KMiAwIG9iag0KPDwvVHlwZS9QYWdlcy9Db3VudCA2Ni9LaWRzWyAzIDAgUiAxNiAwIFIgMjAgMCBSIDI0IDAgUiAyOCAwIFIgMzIgMCBSIDM2IDAgUiA0MCAwIFIgNDMgMCBSIDQ3IDAgUiA1MSAwIFIgNTUgMCBSIDU4IDAgUiA2MiAwIFIgNjYgMCBSIDcwIDAgUiA3NCAwIFIgNzggMCBSIDgyIDAgUiA4NiAwIFIgOTAgMCBSIDkyIDAgUiA5NiAwIFIgMTAwIDAgUiAxMDQgMCBSIDEwOCAwIFIgMTEyIDAgUiAx...", "fileName":"操作流程-物资管理.pdf"}

看到你的问题了。问题在于你使用了@JsonIgnore注解来防止fileContent字段被序列化,但是在你自定义的 AOP 切面中,仍然打印出了这个字段的内容。

让我解释一下原因和解决方案:

问题原因

  1. 注解作用域问题

    • @JsonIgnore是 Jackson 的注解,它只在 Jackson 的序列化/反序列化过程中起作用

    • 你在 AOP 切面中使用的是 FastJSON 的JSONObject.toJSONString(result),FastJSON 不识别 Jackson 的注解

  2. 序列化时机

    • 当方法返回FileData对象时,AOP 切面会先执行

    • 切面中的JSONObject.toJSONString(result)会使用 FastJSON 序列化整个对象

    • FastJSON 默认会序列化所有字段,包括byte[] fileContent

解决方案

你有几个选项可以选择:

方案1:修改 AOP 切面,排除大字段

java

// 在 sqlExecutionTimeDuration 方法中,修改序列化逻辑 if (executionTimeDuration > 3000 || "FileData".equals(methodReturnType)) { // 特殊处理 FileData 对象,避免序列化 fileContent String methodReturnValue; if (result instanceof FileData) { FileData fileData = (FileData) result; // 创建一个简化的对象,排除 fileContent Map<String, Object> simplified = new HashMap<>(); simplified.put("fileName", fileData.getFileName()); simplified.put("fileContentSize", fileData.getFileContent() != null ? fileData.getFileContent().length : 0); methodReturnValue = JSONObject.toJSONString(simplified); } else { methodReturnValue = JSONObject.toJSONString(result); } // 后续日志记录... }

方案2:为 FastJSON 添加自定义配置

创建一个自定义的 FastJSON 配置,排除特定字段:

java

@Component public class FastJsonConfig { @PostConstruct public void init() { // 为 FileData 类添加自定义序列化器 ParserConfig.getGlobalInstance().putDeserializer(FileData.class, new ObjectDeserializer() { @Override public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) { return null; } @Override public int getFastMatchToken() { return 0; } }); } }

方案3:使用 Jackson 代替 FastJSON(推荐)

如果你项目中主要使用 Jackson,可以在切面中也使用 Jackson:

java

import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return mapper; } } // 在 TimeAspect 中 @Autowired private ObjectMapper objectMapper; // 修改序列化部分 String methodReturnValue = objectMapper.writeValueAsString(result);

方案4:自定义 FastJSON 的序列化过滤器

java

// 创建过滤器 SerializeFilter filter = new PropertyPreFilter() { @Override public boolean apply(JSONSerializer serializer, Object object, String name) { // 如果是 FileData 对象的 fileContent 字段,则跳过 if (object instanceof FileData && "fileContent".equals(name)) { return false; } return true; } }; // 使用过滤器 String methodReturnValue = JSONObject.toJSONString(result, filter);

方案5:最简单的方法 - 直接跳过 FileData 的返回值

java

// 修改条件判断 if (executionTimeDuration > 3000) { // 如果执行时间超过阈值,才记录日志 // 移除 || "FileData".equals(methodReturnType) 这个条件 // ... } // 或者特殊处理 FileData String methodReturnValue; if (result instanceof FileData) { methodReturnValue = "FileData object with fileContent omitted"; } else { methodReturnValue = JSONObject.toJSONString(result); }

建议

推荐方案1或方案3

  • 方案1简单直接,只修改切面逻辑

  • 方案3更加标准化,统一使用 Jackson 作为序列化工具

这样修改后,你的日志就不会再输出fileContent的二进制内容了,只会记录文件信息(如文件名、文件大小等),既保护了敏感数据,又减少了日志体积。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 21:42:10

【2026最新 架构环境安装篇二】Docker安装MySQL8详细教程

#拉取MySQL镜像 docker pull mysql:8.0#创建本地目录&#xff08;用于挂载数据、配置&#xff09; mkdir -p ~/docker/mysql/data ~/docker/mysql/conf#启动容器&#xff08;挂载目录配置&#xff09; docker run -d \ --name mysql8 \ -p 3306:3306 \ -v ~/docker/mysql/data:…

作者头像 李华
网站建设 2026/5/1 3:07:29

先画个重点:这套PMSM双闭环方案里,外环MPC负责速度控制,内环无差拍处理电流跟踪。咱们直接上硬货,看看怎么用Simulink把这套算法落地

基于扰动观测器的永磁同步电机&#xff08;PMSM&#xff09;模型预测控制&#xff08;MPC&#xff09;仿真&#xff0c;速度外环基于模型预测控制、电流内环基于无差拍控制搭建&#xff0c;控制效果理想&#xff0c;模块程序设计通俗易通&#xff0c;送参考文献&#xff0c;方便…

作者头像 李华
网站建设 2026/5/1 3:07:12

golang 项目依赖备份

依赖存放路径&#xff1a;C:\Users\CHHC\go\pkg\mod清空存放路径下的文件根据go.mod 和 go.sum 下载依赖go mod download打包文件

作者头像 李华
网站建设 2026/4/30 21:31:55

学术“变形记”:用书匠策AI把本科论文从“青铜”炼成“王者”

对于本科生来说&#xff0c;论文写作就像一场“升级打怪”的冒险&#xff1a;选题时像在迷雾森林里找出口&#xff0c;文献综述时仿佛在知识海洋里捞针&#xff0c;写作时又像在搭建一座没有图纸的积木城堡。但别怕&#xff01;现在有一款名为书匠策AI的学术“魔法棒”&#xf…

作者头像 李华
网站建设 2026/5/1 3:06:35

《P1850 [NOIP 2016 提高组] 换教室》

题目背景NOIP2016 提高组 D1T3题目描述对于刚上大学的牛牛来说&#xff0c;他面临的第一个问题是如何根据实际情况申请合适的课程。在可以选择的课程中&#xff0c;有 2n 节课程安排在 n 个时间段上。在第 i&#xff08;1≤i≤n&#xff09;个时间段上&#xff0c;两节内容相同…

作者头像 李华
网站建设 2026/5/1 3:06:37

芯片制造企业如何利用百度富文本编辑器实现PDF跨平台编辑?

今天早上刚到工位&#xff0c;就收到一位网友的微信私聊——原来是某初中学校外包项目的对接人&#xff0c;想咨询Word文档一键导入功能的实现方案。其实我的微信号早在技术社区公开过&#xff0c;但仍有不少开发者表示"大海捞针"&#xff0c;这找技术资源的难度堪比…

作者头像 李华