文章目录
- 前言
- 正文
- 一、项目结构介绍
- 二、核心类
- 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