文章目录
- 前言
- 正文
- 一、项目结构介绍
- 二、核心类
- 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;
}
@PostConstruct
private 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));
}
// 获取module
String module = getModule(cLog, signature, args, logRecordMap);
// 获取actionType
String actionType = getActionType(cLog, signature, args, logRecordMap);
// 获取actionId
List<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);
// 处理自定义的el
if (map.containsKey(el)) {
return map.get(el);
}
// 处理自定义方法el
el = parseCustomerMethodEl(el);
// 执行el
return 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);
// 处理自定义的el
if (map.containsKey(el)) {
return map.get(el);
}
// 处理自定义方法el
el = parseCustomerMethodEl(el);
// 执行el
return 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);
// 处理自定义el
if (map.containsKey(el)) {
return Collections.singletonList(map.get(el));
}
// 执行el
return 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> {
@Override
protected 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;
}
@Override
protected 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 {
@Resource
private 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