参考:如何优雅地记录操作日志?
一、LogRecord注解
主要记录了success日志文案,和bizNo业务唯一标识,使用了spel表达式解析,具体语法写在了注释上
/**
* 操作日志记录注解,字段使用spel表达式解析,表达式规则如下:
*
* #{} 里面的是表达式,外面的是字面量,如"目标值:#{#targetBO.targetValue}"
* '' 表示字面量,如'-'
* #xxx 表示获取参数变量,如#targetBO.targetValue
* #p0 #p1 表示获取第一个参数,第二个参数,以此类推,如#p0.targetValue
* `@xxx.()` 表示调用bean方法,如@targetManageManagementImpl.getTargetAddBizNo(#targetDTO)
* T() 表示访问静态变量或方法,如T(java.lang.Integer).parseInt()
* +-*`/` >< 算术运算,两边需要为数字,如果为string,可以使用T(java.lang.Integer).parseInt()进行转换
* #num[1] 通过数组下标访问元素
* A?:B 三目运算符,A成立返回A,否则返回B,如 #targetBO.targetValue == null ? : '-'
* #creatorName 预设的当前登录人姓名
* #item 指代list参数中的每一项,设置list字段为list参数的名称,可以对list中的每一项生成一条日志,如:list = "specialActivityTargetList", success = "目标值:#{#item.targetValue}"
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {
/**
* 业务执行成功的操作日志文本
*/
String success();
/**
* 业务执行失败的操作日志文本
*/
String fail() default "";
/**
* 操作人
*/
String operator() default "";
/**
* 关联的业务对象标识
*/
String bizNo();
/**
* 操作日志类别
*/
String type();
/**
* 操作类型
*/
String operateType();
/**
* 解析list,每条对应一个操作日志
*/
String list() default "";
}
二、OperateLogAspect切面
使用spel表达式解析@LogRecord注解内容,保存到日志表中。可以解析list数据,保存多条日志,预设了当前登录人姓名参数。为了避免影响主流程,使用异步保存
@Slf4j
@Aspect
@Component
public class OperateLogAspect {
private final static String SYS_OPERATOR = "admin";
@Autowired
private LogRecordExpressionEvaluator expressionEvaluator;
@Autowired
private BeanFactory beanFactory;
@Autowired
private OperateLogService operateLogService;
@Pointcut("@annotation(com.xxx.common.annotation.LogRecord)")
public void pointcut() {
}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
LogRecord logRecord;
String bizNo;
String success;
OperateLogContent operateLogContent = null;
try {
// 获取方法和注解
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
logRecord = method.getAnnotation(LogRecord.class);
log.info("拦截的方法:{},入参bizNo:{},success:{}", method, logRecord.bizNo(), logRecord.success());
// 解析参数名称,放到EvaluationContext中
Object[] args = joinPoint.getArgs();
EvaluationContext context = expressionEvaluator.createEvaluationContext(method, args, AopProxyUtils.ultimateTargetClass(joinPoint.getTarget()), beanFactory);
// 设置creatorName默认参数
context.setVariable("creatorName", UserUtil.getUserInfo().getUserName());
// 解析spel表达式
if (StringUtils.isNotBlank(logRecord.list())) {
// list数据解析
List<Object> list = (List<Object>) context.lookupVariable(logRecord.list());
List<String> bizNoList = Lists.newArrayList();
List<String> successList = Lists.newArrayList();
for (Object o : list) {
context.setVariable("item", o);
bizNoList.add((String) expressionEvaluator.parseExpression(logRecord.bizNo(), context));
successList.add((String) expressionEvaluator.parseExpression(logRecord.success(), context));
}
bizNo = String.join(",", bizNoList);
success = String.join(",", successList);
} else {
bizNo = (String) expressionEvaluator.parseExpression(logRecord.bizNo(), context);
success = (String) expressionEvaluator.parseExpression(logRecord.success(), context);
}
log.info("解析入参bizNo:{},success:{}", bizNo, success);
String loginId = UserUtil.getUserInfo().getLoginId();
if (StringUtils.isNotBlank(logRecord.operator())) {
loginId = logRecord.operator();
} else if (StringUtils.isBlank(loginId)) {
loginId = SYS_OPERATOR;
}
operateLogContent = new OperateLogContent(loginId, logRecord.type(), logRecord.operateType(), bizNo, success, null);
} catch (Exception e) {
log.error("OperateLogAspect.doAround.E errMsg:{}", e.getMessage(), e);
}
try {
Object result = joinPoint.proceed();
if (operateLogContent != null) {
this.saveLog(operateLogContent);
}
return result;
} catch (Throwable e) {
if (operateLogContent != null) {
operateLogContent.setErrMsg(StringUtils.isBlank(operateLogContent.getErrMsg()) ? e.getMessage() : operateLogContent.getErrMsg());
this.saveLog(operateLogContent);
}
throw e;
}
}
@Async
public void saveLog(OperateLogContent operateLogContent) {
if (operateLogContent == null) {
return;
}
try {
List<OperateLogBO> operateLogBOS = Lists.newArrayList();
String[] bizNos = operateLogContent.getBizNo().split(",");
String[] successList = operateLogContent.getSuccess().split(",");
for (int i = 0; i < bizNos.length; i++) {
OperateLogBO operateLogBO = new OperateLogBO();
operateLogBO.setType(operateLogContent.getType());
operateLogBO.setBizNo(bizNos[i]);
if (StringUtils.isBlank(operateLogContent.getErrMsg())) {
operateLogBO.setSuccess(NumberConstants.ONE);
operateLogBO.setContent(successList[i]);
} else {
operateLogBO.setSuccess(NumberConstants.ZERO);
operateLogBO.setContent(operateLogContent.getErrMsg());
}
operateLogBO.setOperateType(operateLogContent.getOperateType());
operateLogBO.setCreatorId(operateLogContent.getOperator());
operateLogBO.setModifierId(operateLogContent.getOperator());
operateLogBOS.add(operateLogBO);
}
operateLogService.batchInsert(operateLogBOS);
} catch (Exception e) {
log.error("OperateLogAspect.saveOperateLog.E operateLogContent:{}, errMsg:{}", JsonUtils.toJson(operateLogContent), e.getMessage(), e);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OperateLogContent implements Serializable {
private static final long serialVersionUID = 1884231306841382985L;
/**
* 操作人
*/
private String operator;
/**
* 操作日志类别
*/
private String type;
/**
* 操作类型
*/
private String operateType;
/**
* 解析的业务编号
*/
private String bizNo;
/**
* 解析的日志文案
*/
private String success;
/**
* 错误信息
*/
private String errMsg;
}
}
三、spel解析器封装
封装spel解析器,主要是缓存方法和表达式,不用重复解析
@Component
public class LogRecordExpressionEvaluator {
private final SpelExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 缓存方法
*/
private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);
/**
* 缓存表达式
*/
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>(64);
/**
* 创建EvaluationContext
*
* @param method
* @param args
* @param targetClass
* @param beanFactory
* @return
*/
public EvaluationContext createEvaluationContext(Method method, Object[] args, Class<?> targetClass, BeanFactory beanFactory) {
Method targetMethod = getTargetMethod(targetClass, method);
MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(null, targetMethod, args, parameterNameDiscoverer);
if (beanFactory != null) {
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
}
return evaluationContext;
}
/**
* 解析表达式
*
* @param expression
* @param evalContext
* @return
*/
public Object parseExpression(String expression, EvaluationContext evalContext) {
Expression expr = expressionCache.get(expression);
if (expr == null) {
expr = parser.parseExpression(expression, ParserContext.TEMPLATE_EXPRESSION);
expressionCache.put(expression, expr);
}
return expr.getValue(evalContext);
}
private Method getTargetMethod(Class<?> targetClass, Method method) {
AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
return targetMethodCache.computeIfAbsent(methodKey, k -> AopUtils.getMostSpecificMethod(method, targetClass));
}
}
四、使用示例
实例是批量保存和单个保存的场景,日志文案比较特殊,使用了一个方法来生成
@LogRecord(list = "targetList", success = "#{@targetOperateLogService.getTargetOperateContent(#item, '导入')}",
bizNo = "2_#{#item.dateField}_#{#item.branchId}_#{#item.metricsId}", type = "targetManage", operateType = "import")
@Override
public void batchSave(List<TargetBO> targetList) {
targetManageDAO.batchSave(TargetConvert.targetBOListToDOList(targetList));
}
@LogRecord(success = "#{@targetOperateLogService.getTargetOperateContent(#targetBO, '添加')}", type = "targetManage",
bizNo = "2_#{#targetBO.dateField}_#{#targetBO.branchId}_#{#targetBO.metricsId}", operateType = "add")
@Override
public int save(TargetBO targetBO) {
targetBO.setIsDelete("N");
targetBO.setDateType(3);
targetBO.setTargetStatus("0");
return targetManageDAO.save(TargetConvert.targetBOToTargetDO(targetBO));
}
public String getTargetOperateContent(BaseTargetBO targetBO, String type) {
// 一些业务处理
... ...
// 获取单位缓存
Map<String, String> fieldUnitMap = systemCacheService.fieldUnitMap();
String unit = fieldUnitMap.get(metricsBO.getMetricsUnit());
return UserUtil.getUserInfo().getUserName() + type + ",目标值:" + targetValue + (unit == null ? "" : unit) + ";挑战目标:" + challengeValue + (unit == null ? "" : unit) + ";权重:" + weight;
}
五、其他
1、使用threadLocal保存用户信息
使用threadLocal保存用户信息,可以随时获取用户信息,不用参数传递
public class UserUtil {
/**
* 拷贝父线程threadLocal
* @see ExecutorConfig
*/
private static final TransmittableThreadLocal<UserInfoDTO> context = new TransmittableThreadLocal<>();
public static void setUserInfo(UserInfoDTO userInfoDTO) {
context.set(userInfoDTO);
}
public static UserInfoDTO getUserInfo() {
UserInfoDTO userInfoDTO = context.get();
if (userInfoDTO != null) {
return userInfoDTO;
} else {
return new UserInfoDTO();
}
}
public static void remove() {
context.remove();
}
}
2、用户信息拦截器
用于在每个请求开始时设置用户信息到threadLocal中
public class UserInfoInterceptor implements HandlerInterceptor {
@Autowired
private IUserServiceManagement userServiceManagement;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
SsoUserInfo ssoUserInfo = SSOUtil.getUserInfo(httpServletRequest);
String loginId = ssoUserInfo.getLoginId();
UserInfoDTO userInfo = userServiceManagement.getUserInfo(loginId, ssoUserInfo.getUserName(), ssoUserInfo.getWorkCode());
UserUtil.setUserInfo(userInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
UserUtil.remove();
}
}
拦截器配置:
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
/**
* 配置拦截器
* @param
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
3、拷贝线程池中的threadLocal
使用线程池时,父子线程threadLocal会丢失,使用TransmittableThreadLocal进行拷贝
参考:https://github.com/alibaba/transmittable-thread-local
@Slf4j
@Configuration
@EnableAsync
public class ExecutorConfig {
@Bean
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//配置核心线程数
executor.setCorePoolSize(5);
//配置最大线程数
executor.setMaxPoolSize(10);
//配置队列大小
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 拷贝父线程threadLocal @see UserUtil
executor.setTaskDecorator(TtlRunnable::get);
//执行初始化
executor.initialize();
return executor;
}
}