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<Page<aaa>> 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<String, String> 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<String, Object> paramsMap, ApiLogRecordVO apiLogRecordVO);
}

2,抽象类:

public abstract class AbstractLogConvert implements LogConvertInterface {

    /**
     * 处理
     */
    @Override
    public void convertHandle(Map<String, Object> 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<? extends AbstractLogConvert> 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<? extends LogRecordHandle> 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<String, Object> 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<String, Object> 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<? extends AbstractLogConvert> 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);
        }
    }


}

五:最后

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
你可以使用 @Slf4j 注解和 AOP 统一处理打印日志,具体实现可参考以下代码: 1. 引入依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> ``` 2. 在应用主类上添加 @EnableAspectJAutoProxy 注解启用 AOP: ```java @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass = true) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 3. 定义切面类: ```java @Aspect @Component @Slf4j public class LogAspect { @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)" + "||@annotation(org.springframework.web.bind.annotation.GetMapping)" + "||@annotation(org.springframework.web.bind.annotation.PostMapping)" + "||@annotation(org.springframework.web.bind.annotation.PutMapping)" + "||@annotation(org.springframework.web.bind.annotation.DeleteMapping)") public void webLog() { } @Before("webLog()") public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 记录请求内容 log.info("URL : " + request.getRequestURL().toString()); log.info("HTTP_METHOD : " + request.getMethod()); log.info("IP : " + request.getRemoteAddr()); // 记录调用方法 log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName()); // 记录请求参数 log.info("ARGS : " + Arrays.toString(joinPoint.getArgs())); } @AfterReturning(returning = "ret", pointcut = "webLog()") public void doAfterReturning(Object ret) { // 记录响应内容 log.info("RESPONSE : " + ret); } } ``` 4. 在需要打印日志的接口方法上添加 @RequestMapping 等注解即可。 压缩文件的操作请参考以下代码: ```java public static void zip(Path sourcePath, Path zipPath) throws IOException { try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath)); Stream<Path> paths = Files.walk(sourcePath)) { paths.filter(p -> !Files.isDirectory(p)) .forEach(p -> { ZipEntry entry = new ZipEntry(sourcePath.relativize(p).toString()); try { zos.putNextEntry(entry); zos.write(Files.readAllBytes(p)); zos.closeEntry(); } catch (IOException e) { e.printStackTrace(); } }); } } ``` 对于中文加密的问题,我不是很确定您要表达的意思,请再提供更详细的问题描述。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值