springboot下日志注解开发

一:需求

基于方法的日志注解,持久化入参回参,异常通知,收集traceId。

二:设计

数据库设计:

CREATE TABLE `s_api_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `biz_category` varchar(50) DEFAULT NULL COMMENT '业务目录,默认类名',
  `biz_type` varchar(50) DEFAULT NULL COMMENT '业务类型,默认方法名',
  `biz_no` varchar(50) DEFAULT NULL COMMENT '业务号,支持el表达式',
  `exe_res` tinyint(1) DEFAULT NULL COMMENT '结果',
  `sub_biz_no` varchar(50) DEFAULT NULL COMMENT '子业务号',
  `op_params` varchar(255) DEFAULT NULL COMMENT '参数',
  `trace_id` varchar(20) DEFAULT NULL COMMENT 'traceId',
  `execution_result` varchar(255) DEFAULT NULL COMMENT '执行结果\n',
  `err_msg` varchar(255) DEFAULT NULL COMMENT '异常内容\n',
  `operator_name` varchar(20) DEFAULT NULL COMMENT '操作人\n',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT 'createTime',
  PRIMARY KEY (`id`),
  KEY `bizCategory_bizType_bizNo` (`biz_category`,`biz_type`,`biz_no`) USING BTREE
)  DEFAULT CHARSET=utf8mb4 COMMENT='接口日志表';

方案设计:

aop处理,环绕增强,同步解析出Dao层数据结构,将数据传入自定义处理器。

三:效果

//主启动类配置自定义处理器和加载aop
@ApiLogRecordConfiguration(ApiLogRecordRabbitmqHandle.class)
    @PostMapping("/xxx")
    //日志注解,字段映射支持el表达式
    @EnableApiLogRecord(bizType = "xxx", bizNo = "#aaa", subBizNo = "#ddd.ttt")
    public ResultHolder> xxx(@RequestHeader(bbb) Long ccc,@RequestBody @Valid ddd eee){
        return null;
    }

四:实现

一:自定义处理器接口及实现案例(推送rabbitmq消息处理)

public interface LogRecordHandle {

    /**
     * 日志处理
     *
     * @param logRecordVO vo
     */
    void logHandle(ApiLogRecordVO logRecordVO);

}
public class ApiLogRecordRabbitmqHandle extends AbstractApiLogRecordHandle implements LogRecordHandle {

    @Resource
    private AmqpTemplate amqpTemplate;

    @Override
    public void logHandle(ApiLogRecordVO logRecordVO) {
        //发送消息
        amqpTemplate.convertAndSend(
            SupplierCommonConstants.API_LOG_RECORD_EXCHANGE,
            null,
            JSON.toJSONString(logRecordVO));
    }
}

数据实体类:

@Data
public class ApiLogRecordVO {

    @ApiModelProperty(value = "主键")
    private Long id;

    @ApiModelProperty(value = "业务目录,默认类名")
    private String bizCategory;

    @ApiModelProperty(value = "业务类型,默认方法名")
    private String bizType;

    @ApiModelProperty(value = "业务号,支持el表达式")
    private String bizNo;

    @ApiModelProperty(value = "子业务号,支持el表达式")
    private String subBizNo;

    @ApiModelProperty(value = "执行结果,1:成功,0:失败")
    private Integer exeRes;

    @ApiModelProperty(value = "参数")
    private String opParams;

    @ApiModelProperty(value = "traceId")
    private String traceId;

    @ApiModelProperty(value = "执行结果")
    private String executionResult;

    @ApiModelProperty(value = "异常内容")
    private String errMsg;

    @ApiModelProperty(value = "操作人")
    private String operatorName;

    @ApiModelProperty(value = "创建时间")
    private LocalDateTime createTime;

    @ApiModelProperty(value = "是否记录结果")
    private boolean recordResult;

    @ApiModelProperty(value = "指定异常通知地址")
    private String webHookUrl;

}

二:webHook处理

@ConfigurationProperties(prefix = "c3.webhook.config")
@Component
@Data
public class ApiLogWebHookConfig {

    /**
     *webHook配置.
     */
    private Map webHookConfigMap = new HashMap<>();


}

企业微信机器人通知实体类:

@Data
public class QwWebHookContent {

    /**
     * 默认值:text.
     */
    private String msgtype;

    /**
     * 默认值:text.
     */
    private Text Text;

    /**
     * 默认值:报警类型,用于匹配url.
     */
    private String type;

    /**
     * toJsonString.
     *
     * @return jsonString.
     */
    public String toJsonString() {
        return JSON.toJSONString(this);
    }

    @Data
    @AllArgsConstructor
    public class Text {

        /**
         * 默认值:报警类型,用于匹配url.
         */
        private String content;

        private String[] mentioned_list;

        private String[] mentioned_mobile_list;

        /**
         * content构造.
         */
        public Text(String content) {
            this.content = content;
        }
    }
}

三:自定义dao层数据转换器

1,接口:

public interface LogConvertInterface {

    void convertHandle(Map paramsMap, ApiLogRecordVO apiLogRecordVO);
}

2,抽象类:

public abstract class AbstractLogConvert implements LogConvertInterface {

    /**
     * 处理
     */
    @Override
    public void convertHandle(Map paramsMap, ApiLogRecordVO apiLogRecordVO) {
    }

    /**
     * 默认无
     */
    public abstract static class None extends AbstractLogConvert {

        /**
         * 构造
         */
        private None() {
        }
    }
}

四:注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Documented
public @interface EnableApiLogRecord {

    /**
     * 异常通知
     *
     * @return webHookUrl
     */
    String webHookUrl() default "";

    /**
     * 业务id,默认方法名,支持el表达式
     *
     * @return bizNo
     */
    String bizNo() default "";

    /**
     * 业务id,默认方法名,支持el表达式
     *
     * @return subBizNo
     */
    String subBizNo() default "";

    /**
     * 业务目录,默认类名,支持el表达式
     *
     * @return bizCategory
     */
    String bizCategory() default "";

    /**
     * 自定义类型转换class
     *
     * @return webHookUrl
     */
    Class logConvert() default AbstractLogConvert.None.class;

    /**
     * 业务id,默认方法名,支持el表达式
     *
     * @return bizType
     */
    String bizType() default "";

    /**
     * 操作人,默认取参数中的username字段,支持el表达式
     *
     * @return username
     */
    String username() default "";

    /**
     * 是否保存结果
     *
     * @return recordResult
     */
    boolean recordResult() default false;


}

配置注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApiLogRecordHandleRegistrar.class)
@EnableConfigurationProperties({ApiLogWebHookConfig.class})
public @interface ApiLogRecordConfiguration {

    /**
     * 日志处理方式
     *
     * @return value
     */
    Class value();

}

五:配置加载bean

public class ApiLogRecordHandleRegistrar implements ImportBeanDefinitionRegistrar {


    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(importingClassMetadata, registry);
        registerAop(importingClassMetadata, registry);
    }

    /**
     * 注册配置
     *
     * @param importingClassMetadata metadata
     * @param registry registry
     */
    private void registerDefaultConfiguration(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map defaultAttrs = importingClassMetadata
            .getAnnotationAttributes(ApiLogRecordConfiguration.class.getName(), false);
        if (defaultAttrs != null && defaultAttrs.containsKey("value")) {
            registerLogHandelConfiguration(registry, "LogRecordHandle",
                (Class) defaultAttrs.get("value"));
        }
    }

    /**
     * 注册handle
     *
     * @param registry registry
     * @param name name
     * @param logHandle logHandle
     */
    private void registerLogHandelConfiguration(BeanDefinitionRegistry registry, String name, Class logHandle) {
        RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(logHandle);
        registry.registerBeanDefinition(name, rootBeanDefinition);
    }


    /**
     * 注册handle
     *
     * @param importingClassMetadata meta
     * @param registry registry
     */
    private void registerAop(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
            .genericBeanDefinition(ApiLogRecordAop.class);
        registry.registerBeanDefinition(ApiLogRecordAop.class.getSimpleName(), builder.getBeanDefinition());
    }

}

六:aop处理

@Aspect
@Order(1)
public class ApiLogRecordAop extends ApplicationObjectSupport {

    @Resource
    private LogRecordHandle logRecordHandle;

    @Resource
    private ApiLogWebHookConfig apiLogWebHookConfig;


    private static final Integer RESULT_STR_MAX = 250;

    private static final Integer RESULT_STR_MIN = 0;

    private static final Integer EXE_SUCCESS = 1;

    private static final Integer EXE_FAIL = 0;

    private static final Integer HASH_MAP_INIT = 16;

    private static final String DEFAULT_STR = "default";


    /**
     * 切点
     */
    @Pointcut("@annotation(com.jubaozan.c3.suppliercommon.logrecord.annotation.EnableApiLogRecord)")
    public void logRecordPointcut() {
    }

    /**
     * 增强
     *
     * @param point point
     * @return Object Object
     */
    @Around("logRecordPointcut()")
    public Object interceptor(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        ApiLogRecordVO apiLogRecordVO = new ApiLogRecordVO();
        //解析注解组装参数
        parseAnnotation(apiLogRecordVO, method, point);
        //执行
        try {
            Object res = point.proceed();
            if (apiLogRecordVO.isRecordResult()) {
                apiLogRecordVO.setExecutionResult(JSON.toJSONString(res).substring(RESULT_STR_MIN, RESULT_STR_MAX));
            }
            return res;
        } catch (Throwable throwable) {
            apiLogRecordVO.setExeRes(EXE_FAIL);
            apiLogRecordVO.setErrMsg(throwable.getMessage());
            throw new RuntimeException(throwable.getMessage(), throwable);
        } finally {
            //执行处理方式
            try {
                logRecordHandle.logHandle(apiLogRecordVO);
                webHookHandle(apiLogRecordVO);
            } catch (Exception e) {
                logger.error("日志处理失败,e:", e);
            }
        }
    }

    /**
     * 解析注解
     *
     * @param apiLogRecordVO apiLogRecordVO
     * @param method pjp
     * @param pjp pjp
     */
    private void parseAnnotation(ApiLogRecordVO apiLogRecordVO, Method method, ProceedingJoinPoint pjp) {
        try {
            apiLogRecordVO.setTraceId(MDC.get("traceId"));
            apiLogRecordVO.setExeRes(EXE_SUCCESS);

            EnableApiLogRecord annotation = method.getAnnotation(EnableApiLogRecord.class);
            Object[] args = pjp.getArgs();
            LocalVariableTableParameterNameDiscoverer localVariableTable = new LocalVariableTableParameterNameDiscoverer();
            String[] paraNameArr = localVariableTable.getParameterNames(method);
            Map paramsMap = new HashMap<>(HASH_MAP_INIT);
            ExpressionParser parser = new SpelExpressionParser();
            StandardEvaluationContext context = new StandardEvaluationContext();
            for (int i = 0; i < Objects.requireNonNull(paraNameArr).length; i++) {
                context.setVariable(paraNameArr[i], args[i]);
                paramsMap.put(paraNameArr[i], args[i]);
            }
            //是否指定自定义转换类
            Class convertClass = annotation.logConvert();
            if (convertClass != AbstractLogConvert.None.class) {
                convertClass.newInstance().convertHandle(paramsMap, apiLogRecordVO);
                return;
            }

            String bizCategory = ObjectUtil.isNotEmpty(annotation.bizCategory()) ? annotation.bizCategory()
                : ((MethodInvocationProceedingJoinPoint) pjp).getSignature().getDeclaringType().getSimpleName();
            if (bizCategory.matches("^#.*.$")) {
                bizCategory = parser.parseExpression(bizCategory).getValue(context, String.class);
            }

            String bizType = ObjectUtil.isNotEmpty(annotation.bizType()) ? annotation.bizType() : method.getName();
            if (bizType.matches("^#.*.$")) {
                bizType = parser.parseExpression(bizType).getValue(context, String.class);
            }

            String bizNo = ObjectUtil.isNotEmpty(annotation.bizNo()) ? annotation.bizNo() : method.getName();
            if (bizNo.matches("^#.*.$")) {
                bizNo = parser.parseExpression(bizNo).getValue(context, String.class);
            }

            String userName = ObjectUtil.isNotEmpty(annotation.username()) ? annotation.username() : "#username";
            if (userName.matches("^#.*.$")) {
                userName = parser.parseExpression(userName).getValue(context, String.class);
            }

            String subBizNo = ObjectUtil.isNotEmpty(annotation.subBizNo()) ? annotation.subBizNo() : method.getName();
            if (subBizNo.matches("^#.*.$")) {
                subBizNo = parser.parseExpression(subBizNo).getValue(context, String.class);
            }
            apiLogRecordVO.setBizCategory(bizCategory);
            apiLogRecordVO.setBizType(bizType);
            apiLogRecordVO.setBizNo(bizNo);
            apiLogRecordVO.setSubBizNo(subBizNo);
            apiLogRecordVO.setOperatorName(userName);
            apiLogRecordVO.setOpParams(JSON.toJSONString(paramsMap));
            apiLogRecordVO.setRecordResult(annotation.recordResult());
            apiLogRecordVO.setWebHookUrl(annotation.webHookUrl());
            Optional.ofNullable(paramsMap.get(KeyConstants.KEY_X_C3_USERNAME)).map(Object::toString).ifPresent(
                apiLogRecordVO::setOperatorName);

        } catch (Exception ex) {
            logger.error("日志数据解析失败", ex);
        }
    }

    /**
     * webHook处理
     *
     * @param apiLogRecordVO apiLogRecordVO
     */
    private void webHookHandle(ApiLogRecordVO apiLogRecordVO) {
        try {
            if (apiLogRecordVO.getExeRes().equals(EXE_FAIL)) {
                String url = apiLogRecordVO.getWebHookUrl();
                if (ObjectUtil.isEmpty(url)) {
                    url = apiLogWebHookConfig.getWebHookConfigMap()
                        .getOrDefault(apiLogRecordVO.getBizType(), apiLogWebHookConfig.getWebHookConfigMap().get(DEFAULT_STR));
                }
                if (ObjectUtil.isNotEmpty(url)) {
                    QwWebHookContent qwHookContent = new QwWebHookContent();
                    qwHookContent.setMsgtype("text");
                    qwHookContent.setType("default");
                    qwHookContent.setText(qwHookContent.new Text("接口异常" + ":" + "\n"
                        + "业务目录: " + apiLogRecordVO.getBizCategory() + "\n"
                        + "业务类型: " + apiLogRecordVO.getBizType() + "\n"
                        + "时间:" + LocalDateTime.now() + "\n"
                        + "traceId: " + TraceUtils.getTraceId() + "\n"
                        + "异常类容: " + apiLogRecordVO.getErrMsg()));
                    HttpUtil.post(url, JSON.toJSONString(qwHookContent));

                }

            }
        } catch (Exception ex) {
            logger.error("企微消息提醒异常,内容{}", ex);
        }
    }


}

五:最后

欢迎提不足之处,和优化点

你可能感兴趣的:(应用,spring,boot,java,spring)