自定义注解记录操作日志

参考:如何优雅地记录操作日志?

一、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;
    }
}

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值