SpringBoot 自定义注解实现操作日志记录

文章目录

  • 前言
  • 正文
    • 一、项目结构介绍
    • 二、核心类
    • 2.1 核心注解
      • 2.1.1 CLog 日志注解
      • 2.1.2 ProcessorBean 处理器bean
    • 2.2 切面类
    • 2.3 自定义线程池
    • 2.4 工具类
      • 2.4.1 管理者工具类
    • 2.5 测试
      • 2.5.1 订单创建处理器
      • 2.5.2 订单管理者
      • 2.5.3 订单控制器
      • 2.5.4 测试报文
      • 2.5.5 测试结果
  • 附录
    • 1、其他相关文章

前言

关于操作日志记录,在一个项目中是必要的。
本文基于 java8 和 SpringBoot 2.7 来实现此功能。

之前写过一个简单的接口报文日志打印的,和本文的起始思路相同,都是使用切面。但是本文功能更为强大,也更复杂。文章见本文附录《SpringBoot自定义starter之接口日志输出》。

本文代码仓库:https://gitee.com/fengsoshuai/custom-log2.git

正文

本文知识点如下:
自定义注解,SpringBoot使用切面,全局异常处理器,ThreadLocal的使用,MDC传递日志ID,登录拦截器,日志拦截器,自定义线程池,SPEL表达式解析,模版方法设计模式等。

一、项目结构介绍

在这里插入图片描述
其中 org.feng.clog 是核心代码区域。org.feng.test 是用于测试功能写的。

二、核心类

在这里插入图片描述

在项目启动时,会把AbstractProcessorTemplate 的子类放入Spring容器。同时会执行注册处理器的方法,其定义如下:

package org.feng.clog;import lombok.extern.slf4j.Slf4j;
import org.feng.clog.annotation.ProcessorBean;
import org.feng.clog.utils.SpringBeanUtils;import javax.annotation.PostConstruct;/*** 处理器模板** @author feng*/
@Slf4j
public abstract class AbstractProcessorTemplate<T, R> implements Processor<T, R> {protected void init(ProcessorContext<T> context) {}protected void after(ProcessorContext<T> context, R result) {}public R start(ProcessorContext<T> context) {init(context);// 直接调用handle会导致aop失效// R result = handle(context);AbstractProcessorTemplate<T, R> template = SpringBeanUtils.getByClass(this.getClass());R result = template.handle(context);after(context, result);return result;}@PostConstructprivate void registerProcessor() {if (this.getClass().isAnnotationPresent(ProcessorBean.class)) {ProcessorBean processorBean = this.getClass().getDeclaredAnnotation(ProcessorBean.class);log.info("ProcessorBean Register, action is {}, processor is {}", processorBean.action(), this.getClass().getName());ProcessorFactory.register(processorBean.action(), this);}}
}

2.1 核心注解

2.1.1 CLog 日志注解

package org.feng.clog.annotation;import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;import java.lang.annotation.*;/*** 日志注解</br>* <pre>* <ul>使用示例:* <li>@CLog(template = "这是简单模版,无参数",actionType = ActionTypeEnum.UPDATE,actionIdEl = "{#userReq.id}",moduleEl = "1")</li>* <li>@CLog(template = "带参数模版,学生名称:{#userReq.name},班级名称:{#userReq.classReq.name}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "带参数计算模版,{#userReq.classReq.number > 20?'大班':'小班'}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "复杂模版,{#userReq.classReq.number > 20?'大班':('这是名称:').concat(#userReq.name).concat(',这是年龄:').concat(#userReq.age)}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "自定义表达式处理,{SfObjectUtil.isEmpty(#userReq.id)?'id为0或者为空':'id不为0或者为空'}",actionTypeStr = "这是操作",actionIdEl = "{#userReq.id}")</li>* <li>@CLog(template = "自定义处理,{logDesc}",actionTypeStr = "这是操作",actionIdEl = "{id}")</li>* </ul>* </pre>** @author feng*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CLog {/*** 日志模版*/String template();/*** 模块*/ModuleEnum module() default ModuleEnum.DEFAULT;/*** 所属模块名*/String moduleStr() default "";/*** 所属模块名</br>* 变量/表达式获取*/String moduleEl() default "";/*** 操作类型*/ActionTypeEnum actionType() default ActionTypeEnum.DEFAULT;/*** 操作类型,优先级高于枚举;不为空时强制读取此值*/String actionTypeStr() default "";/*** 操作类型</br>* 变量/表达式获取*/String actionTypeEl() default "";/*** 业务操作唯一值</br>* 变量/表达式获取*/String actionIdEl() default "";/*** 业务操作唯一值,多值*/String actionIds() default "";/*** 扩展字段*/String ext() default "";
}

2.1.2 ProcessorBean 处理器bean

package org.feng.clog.annotation;import org.feng.clog.enums.ActionTypeEnum;import java.lang.annotation.*;/*** 处理器bean** @author feng*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ProcessorBean {ActionTypeEnum action();
}

2.2 切面类

package org.feng.clog.aspect;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.feng.clog.LogId;
import org.feng.clog.LogRecordContext;
import org.feng.clog.annotation.CLog;
import org.feng.clog.config.LogCustomerConfig;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;
import org.feng.clog.utils.SpELParserUtils;
import org.feng.clog.utils.StringUtil;
import org.feng.clog.utils.UserUtil;
import org.feng.clog.vo.UserVo;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** 日志切面** @author feng*/
@Aspect
@Component
@Slf4j
public class LogAspect {private static final Pattern BRACES_PATTERN = Pattern.compile("\\{.*?}");@Resource(name = "logThreadPoolTaskExecutor")private Executor  executor;@Pointcut("@annotation(org.feng.clog.annotation.CLog)")private void pointCut() {}@AfterReturning(value = "pointCut()")public void after(JoinPoint joinPoint) {try {addLog(joinPoint);} finally {LogRecordContext.clean();}}public void addLog(JoinPoint joinPoint) {String logId = LogId.get();UserVo userVo = UserUtil.get();Map<String, String> logRecordMap = LogRecordContext.get();executor.execute(() -> {try {// 传递logId到异步线程LogId.put(logId);// 获取方法+入参MethodSignature signature = (MethodSignature) joinPoint.getSignature();Object[] args = joinPoint.getArgs();// 获取注解CLog cLog = signature.getMethod().getDeclaredAnnotation(CLog.class);// 获取模版中的参数(如果存在参数),并拼接List<String> templateParameters = getTemplateParameters(cLog.template());buildTemplateData(templateParameters, signature, args, logRecordMap);String template = cLog.template();for (String templateParameter : templateParameters) {template = template.replace(templateParameter, logRecordMap.get(templateParameter));}// 获取moduleString module = getModule(cLog, signature, args, logRecordMap);// 获取actionTypeString actionType = getActionType(cLog, signature, args, logRecordMap);// 获取actionIdList<String> actionIds = getActionId(cLog, signature, args, logRecordMap);// 获取扩展字段JSONObject ext = getExt(cLog, signature, args, logRecordMap);if (StringUtil.isNotBlank(template)) {for (String actionId : actionIds) {log.info("记录日志,user={}, template={}, module={}, actionType={}, actionId={}, ext={}", userVo, template, module, actionType, actionId, ext);// todo 日志落库}} else {log.info("设置日志数据失败:不满足注解条件");}} catch (Exception e) {log.warn("设置日志异常:", e);}});}private List<String> getTemplateParameters(String template) {List<String> parameters = new ArrayList<>();Matcher matcher = BRACES_PATTERN.matcher(template);while (matcher.find()) {parameters.add(matcher.group());}return parameters;}private void buildTemplateData(List<String> parameters, MethodSignature signature, Object[] args, Map<String, String> map) {for (String el : parameters) {// 如果EL表达式为空,则直接下一个if (!StringUtil.isNotBlank(el)) {continue;}String spEl = el;// 兼容自定义数据spEl = getEl(spEl);if (map.containsKey(spEl)) {map.put("{" + spEl + "}", map.get(spEl));continue;}// 自定义类处理spEl = parseCustomerMethodEl(spEl);// El执行if (spEl.contains("#")) {String value = SpELParserUtils.parse(signature.getMethod(), args, spEl, String.class);map.put(el, value);} else {map.put(el, "");}}}private String getModule(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了module枚举时,优先获取枚举对应的描述if (!ModuleEnum.DEFAULT.equals(cLog.module())) {return cLog.module().getDesc();}// 设置了moduleStr时if (StringUtil.isNotBlank(cLog.moduleStr())) {return cLog.moduleStr();}// 设置了moduleEl时if (StringUtil.isNotBlank(cLog.moduleEl())) {try {String el = cLog.moduleEl();el = getEl(el);// 处理自定义的elif (map.containsKey(el)) {return map.get(el);}// 处理自定义方法elel = parseCustomerMethodEl(el);// 执行elreturn SpELParserUtils.parse(signature.getMethod(), args, el, String.class);} catch (Exception e) {log.error("日志切面获取module错误", e);}}return null;}private String getActionType(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了actionType枚举时,优先获取枚举对应的描述if (!ActionTypeEnum.DEFAULT.equals(cLog.actionType())) {return cLog.actionType().getDesc();}// 设置了actionTypeStr时if (StringUtil.isNotBlank(cLog.actionTypeStr())) {return cLog.actionTypeStr();}// 设置了actionTypeEl时if (StringUtil.isNotBlank(cLog.actionTypeEl())) {String el = cLog.actionTypeEl();el = getEl(el);// 处理自定义的elif (map.containsKey(el)) {return map.get(el);}// 处理自定义方法elel = parseCustomerMethodEl(el);// 执行elreturn SpELParserUtils.parse(signature.getMethod(), args, el, String.class);}return null;}private List<String> getActionId(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 设置了actionIdEl时if (StringUtil.isNotBlank(cLog.actionIdEl())) {if (map.containsKey(cLog.actionIdEl())) {return Collections.singletonList(map.get(cLog.actionIdEl()));}String el = cLog.actionIdEl();el = getEl(el);// 处理自定义elif (map.containsKey(el)) {return Collections.singletonList(map.get(el));}// 执行elreturn Collections.singletonList(SpELParserUtils.parse(signature.getMethod(), args, el, String.class));}// 设置了actionIds时if (StringUtil.isNotBlank(cLog.actionIds())) {String el = getEl(cLog.actionIds());if (map.containsKey(el)) {return Arrays.asList(map.get(el).split(","));}}return Collections.singletonList(System.currentTimeMillis() * 10 + new Random().nextInt(10000) + "");}private JSONObject getExt(CLog cLog, MethodSignature signature, Object[] args, Map<String, String> map) {// 如果EL表达式为空,则直接结束if (!StringUtil.isNotBlank(cLog.ext())) {return null;}String spEl = cLog.ext();//兼容自定义数据spEl = getEl(spEl);if (map.containsKey(spEl)) {String value = map.get(spEl);if (StringUtil.isNotBlank(value)) {try {return JSONObject.parseObject(value);} catch (Exception e) {log.info("JSON转换失败:{},{}", value, e.getMessage());return null;}}return null;}// 自定义类处理spEl = parseCustomerMethodEl(spEl);// El执行if (spEl.contains("#")) {String value = SpELParserUtils.parse(signature.getMethod(), args, spEl, String.class);if (StringUtil.isNotBlank(value)) {try {return JSONObject.parseObject(value);} catch (Exception e) {log.info("JSON转换失败:{},{}", value, e.getMessage());return null;}}return null;}return null;}private String parseCustomerMethodEl(String el) {for (String key : LogCustomerConfig.getCustomerMethod().keySet()) {if (el.contains(key)) {String className = key.split("\\.")[0];el = el.replace(className, "T(" + LogCustomerConfig.getCustomerMethod().get(key) + ")");}}return el;}private String getEl(String str) {str = str.replaceAll("\\{", "");str = str.replaceAll("}", "");return str;}}

2.3 自定义线程池

package org.feng.clog.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;/*** 线程池配置** @author feng*/
@Configuration
@EnableAsync
public class ThreadPoolConfig {@Bean(name = "logThreadPoolTaskExecutor")public Executor initLogCpuExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(Runtime.getRuntime().availableProcessors() + 1);executor.setMaxPoolSize(150);executor.setQueueCapacity(50);executor.setKeepAliveSeconds(60);executor.setThreadNamePrefix("log-thread-pool-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());executor.initialize();executor.setTaskDecorator(runnable -> runnable);return executor;}
}

2.4 工具类

2.4.1 管理者工具类

package org.feng.clog.utils;import org.feng.clog.AbstractProcessorTemplate;
import org.feng.clog.ProcessorContext;
import org.feng.clog.ProcessorFactory;/*** 管理工具** @author feng*/
public class ManagerUtil {public static <R, T> R handle(ProcessorContext<T> context) {AbstractProcessorTemplate<T, R> processor = ProcessorFactory.getProcessor(context.getAction());if (processor == null) {throw new RuntimeException("未找到 " + context.getAction() + "对应的处理器");}return processor.start(context);}
}

2.5 测试

2.5.1 订单创建处理器

package org.feng.test;import lombok.extern.slf4j.Slf4j;
import org.feng.clog.AbstractProcessorTemplate;
import org.feng.clog.LogRecordContext;
import org.feng.clog.ProcessorContext;
import org.feng.clog.annotation.CLog;
import org.feng.clog.annotation.ProcessorBean;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.enums.ModuleEnum;
import org.feng.clog.utils.StringUtil;
import org.springframework.stereotype.Service;/*** 创建订单处理器** @author feng*/
@Slf4j
@Service
@ProcessorBean(action = ActionTypeEnum.ORDER_CREATE)
public class OrderCreateProcessor extends AbstractProcessorTemplate<OrderCreateReq, Boolean> {@Overrideprotected void init(ProcessorContext<OrderCreateReq> context) {preHandleReq(context.getData());}@Override@CLog(template = "测试日志记录,{testK1}", module = ModuleEnum.ORDER, actionType = ActionTypeEnum.ORDER_CREATE,actionIdEl = "{#context.data.orderNum}", ext = "{JacksonUtil.toJSONString(#context.data)}")public Boolean handle(ProcessorContext<OrderCreateReq> context) {LogRecordContext.put("testK1", "3wewd2");OrderCreateReq orderCreateReq = context.getData();log.info("处理--创建订单{}", orderCreateReq.getOrderNum());return true;}@Overrideprotected void after(ProcessorContext<OrderCreateReq> context, Boolean result) {// todo 后置操作}private void preHandleReq(OrderCreateReq req) {// todo 参数校验// 例如校验参数if (StringUtil.isBlank(req.getOrderNum())) {throw new IllegalArgumentException("订单号不能为空");}}
}

2.5.2 订单管理者

package org.feng.test;import org.feng.clog.ProcessorContext;
import org.feng.clog.enums.ActionTypeEnum;
import org.feng.clog.utils.ManagerUtil;
import org.springframework.stereotype.Component;/*** 订单管理** @author feng*/
@Component
public class OrderManager {/*** 创建订单*/public Boolean createOrder(OrderCreateReq req) {ProcessorContext<OrderCreateReq> processorContext = new ProcessorContext<>();processorContext.setAction(ActionTypeEnum.ORDER_CREATE);processorContext.setData(req);return ManagerUtil.handle(processorContext);}
}

2.5.3 订单控制器

package org.feng.test;import org.feng.clog.utils.ResultUtil;
import org.feng.clog.vo.ResultVo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;/*** 控制器** @author feng*/
@RestController
@RequestMapping("order")
public class OrderController {@Resourceprivate OrderManager orderManager;// @WithoutLogin@PostMapping("/test1")public ResultVo<String> test1(@RequestBody OrderCreateReq req) {// 创建Boolean started = orderManager.createOrder(req);return ResultUtil.success("success " + started);}
}

2.5.4 测试报文

{"orderNum": "1001","type": 1,"senderName": "","likes": ["1", "2", "3"]
}

2.5.5 测试结果

控制台日志输出:

2024-02-28 11:48:40.102  INFO  92309 --- [log-thread-pool-1] org.feng.clog.aspect.LogAspect.lambda$addLog$0(LogAspect.java:95) : [logId=d3b0dc267ce64dfa8a987e8eb6aad4ba] 记录日志,user=UserVo(id=1001, username=feng123, phone=18143431243, email=null), template=测试日志记录,3wewd2, module=订单, actionType=订单创建, actionId=1001, ext={"senderName":"","orderNum":"1001","type":1,"likes":["1","2","3"]}

可以看到,日志中记录了logId,以及日志注解对应的信息。

附录

1、其他相关文章

  • SpringBoot自定义starter之接口日志输出
  • SpringBoot使用线程池之ThreadPoolTaskExecutor和ThreadPoolExecutor

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

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

相关文章

Linux学习之system V

目录 一&#xff0c;system V共享内存 快速认识接口 shmget(shared memory get) shmat(shared memory attach) shmdt(shared memory delete) shmctl (shared memory control) 编写代码 综上那么共享内存与管道通信有什么区别&#xff1f; system v消息队列 system v信号…

运维管理制度优化:确保IT系统稳定运行的关键策略

1、总则 第一条&#xff1a;为保障公司信息系统软硬件设备的良好运行&#xff0c;使员工的运维工作制度化、流程化、规范化&#xff0c;特制订本制度。 第二条&#xff1a;运维工作总体目标&#xff1a;立足根本促发展&#xff0c;开拓运维新局面。在企业发展壮大时期&#x…

助你打通SwiftUI任督二脉

序言 开年的第一篇文章&#xff0c;今天分享的是SwiftUI&#xff0c;SwiftUI出来好几年&#xff0c;之前一直没学习&#xff0c;所以现在才开始&#xff1b;如果大家还留在 ​​iOS​​ 开发&#xff0c;这们语言也是一个趋势&#xff1b; 目前待业中.... 不得不说已逝的2023年…

多个版本的Python如何不冲突?

转载文章&#xff0c;防止忘记或删除 转载于&#xff1a;电脑中存在多个版本的Python如何不冲突&#xff1f; - 知乎 (zhihu.com) 如何安装多版本的Python并与之共存&#xff1f; 如果你的工作涉及到Python多版本之间开发或测试&#xff0c;那么请收藏本文&#xff0c; 如果你…

从Spring Boot应用上下文获取Bean定义及理解其来源

前言 在Spring框架中&#xff0c;Bean是组成应用程序的核心单元。特别是在Spring Boot项目中&#xff0c;通过使用SpringApplication.run()方法启动应用后&#xff0c;我们可以获得一个ConfigurableApplicationContext实例&#xff0c;这个实例代表了整个应用程序的运行时环境…

开源世界的学术问题

自由软件基金会是1983年成立的&#xff0c;到现在是41年。正好很有意思的是&#xff0c;在去年还有一篇文章&#xff08;CSDN 的翻译&#xff09;&#xff0c;专门在质疑说成立 40 年的自由软件基金会是不是已经快不行了&#xff0c;所以我们会用这个标题叫做兴衰发展历程来介绍…

2024!深入了解 大语言模型(LLM)微调方法(总结)

来源: AINLPer公众号&#xff08;每日干货分享&#xff01;&#xff01;&#xff09; 编辑: ShuYini 校稿: ShuYini 时间: 2024-2-28 引言 众所周知&#xff0c;大语言模型(LLM)正在飞速发展&#xff0c;各行业都有了自己的大模型。其中&#xff0c;大模型微调技术在此过程中起…

[vue2] 使用provide和inject时,无法获取到实时更新的数据

一、场景 当vue文件中存在多级的父子组件传值&#xff08;即&#xff1a;祖先向下传递数据&#xff09;、多个子组件或孙子级组件都要使用顶级或父级的数据时&#xff0c;使用provide 和 inject 组合无疑是很方便的一种做法了&#xff0c;但如此只是注入的初始值&#xff0c;并…

如何使用便签快速分类工作待办事项

在日常工作和生活中&#xff0c;我们经常需要处理各种各样的待办事项。而有效地分类这些任务&#xff0c;可以帮助我们更好地管理时间和提高工作效率。使用便签是一种简单而实用的方法&#xff0c;下面将介绍如何利用好用便签来快速分类工作待办事项。 首先&#xff0c;你可以…

【前端入门】设计模式+单多页+React

设计模式是一种解决特定问题的经验总结&#xff0c;它提供了经过验证的解决方案&#xff0c;可以在软件开发过程中使用。设计模式可以帮助前端开发人员更有效地组织和管理代码&#xff0c;并提供一种共享的语言和框架&#xff0c;以便与其他开发人员进行交流。 以下是一些常见…

航海雷达练习软件SPx Radar Trainer

产品简介 航海雷达练习软件SPx Radar Trainer是一款基于计算机的模拟训练软件&#xff0c;为小型船舶、初步接触航海操作的学员提供了虚拟练习环境&#xff0c;让学员可以在计算机上熟悉雷达视频图像与实际船舶运行环境的结合、以便于更好的驾驶船舶避撞。 产品特色 【模拟小…

python脚本实现全景站点矩阵转欧拉角

效果 脚本 import re import numpy as np import math import csv from settings import * # 以下是一个示例代码,可以输入3*3旋转矩阵,然后输出旋转角度:# ,输入3*3旋转矩阵# 计算x,y,z旋转角def rotation_matrix_to_euler_angles(R):