美文网首页springboot运营相关
SpringBoot项目记录请求日志

SpringBoot项目记录请求日志

作者: 求心丶 | 来源:发表于2021-04-04 13:41 被阅读0次

    在开发过程中,为了调试及后期维护过程快速排错都会记录请求的入参以及返回值,比较常用的方式是借助日志生成器通过硬编码的方式记录日志,代码不够简洁、优雅。因此,可以借助AOP来实现日志记录,无需在代码中打印日志,并且能够满足不同的日志场景下的定制需求。

    日志注解
    package com.cube.share.log.annotation;
    
    import org.springframework.core.annotation.AliasFor;
    
    import java.lang.annotation.*;
    
    /**
     * @author cube.li
     * @date 2021/4/3 21:42
     * @description 日志注解
     */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface ApiLog {
    
        /**
         * 标题
         */
        String title() default "";
    
        @AliasFor("title")
        String name() default "";
    
        /**
         * 日志打印时排除的类型(例如 File),对入参,出参都有效
         */
        Class<?>[] excludes() default {};
    
        LogLevel level() default LogLevel.DEBUG;
    
        LogType type() default LogType.BOTH;
    
        /**
         * 是否开启
         */
        boolean enable() default true;
    
        /**
         * 是否打印方法信息
         */
        boolean printMethodInfo() default true;
    
        /**
         * 是否打印请求信息
         */
        boolean printRequestInfo() default true;
    
        /**
         * 是否打印耗时
         */
        boolean timeConsumption() default true;
    
        /**
         * 日志级别
         */
        enum LogLevel {
            DEBUG,
            INFO,
            WARN,
            ERROR
        }
    
        /**
         * 记日志类型
         */
        enum LogType {
            /**
             * 入参
             */
            PARAM,
            /**
             * 返回值
             */
            RETURN,
            /**
             * 入参+返回值
             */
            BOTH
        }
    }
    
    

    在需要打印日志的方法上增加该注解,该注解内定义了若干参数,可以根据实际场景给这些参数赋值。

    切面和切点

    package com.cube.share.log.annotation.aspect;
    
    import com.cube.share.log.annotation.ApiLog;
    import com.cube.share.log.util.ApiLogHelper;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.lang.NonNull;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import java.lang.reflect.Method;
    
    /**
     * @author cube.li
     * @date 2021/4/3 21:58
     * @description 日志切面
     */
    @Aspect
    @Component
    @Slf4j
    @ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
    public class ApiLogAop {
    
        @Resource
        private ApiLogHelper logHelper;
    
        @Pointcut("@annotation(com.cube.share.log.annotation.ApiLog)")
        public void pointCut() {
        }
    
        @Around("pointCut()")
        public Object log(@NonNull ProceedingJoinPoint point) throws Throwable {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            //获取此方法上的注解
            ApiLog apiLog = method.getAnnotation(ApiLog.class);
    
            if (!apiLog.enable()) {
                return point.proceed();
            }
    
            switch (apiLog.level()) {
                case DEBUG:
                    if (!log.isDebugEnabled()) {
                        return point.proceed();
                    }
                    break;
                case INFO:
                    if (!log.isInfoEnabled()) {
                        return point.proceed();
                    }
                    break;
                case WARN:
                    if (!log.isWarnEnabled()) {
                        return point.proceed();
                    }
                    break;
                case ERROR:
                    if (!log.isErrorEnabled()) {
                        return point.proceed();
                    }
                    break;
                default:
                    break;
            }
    
            //记录时间
            long start = System.currentTimeMillis(), end;
    
            logHelper.logBeforeProceed(apiLog, method, point.getArgs());
    
            Object result = point.proceed();
            end = System.currentTimeMillis();
    
            logHelper.logAfterProceed(apiLog, result, end - start);
            return result;
        }
    }
    

    定义日志切面,并且将日志注解作为切入点,只要请求方法上具有该日志注解,即可自动进行日志记录。

    打印日志的工具类
    package com.cube.share.log.util;
    
    import com.cube.share.base.utils.IpUtil;
    import com.cube.share.log.annotation.ApiLog;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.ArrayUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.lang.NonNull;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Component;
    
    import javax.annotation.Resource;
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Method;
    import java.text.MessageFormat;
    
    /**
     * @author cube.li
     * @date 2021/4/3 22:05
     * @description 日志打印
     */
    @Component
    @Slf4j
    public class ApiLogHelper {
    
        @Resource
        private HttpServletRequest request;
    
        private static final String SEPARATOR = " | ";
    
        private static final String REQUEST_INFO = "###请求信息###: URI:{0},Content-Type:{1},请求IP:{2}";
    
        private static final String METHOD_INFO = "###方法信息###: 方法名称:{0}";
    
        /**
         * 记录在执行proceed()方法之前的日志,包括:
         * 方法信息
         * 请求信息
         * 参数信息
         *
         * @param apiLog 注解
         * @param method 方法
         * @param args   方法参数
         */
        public void logBeforeProceed(ApiLog apiLog, Method method, Object[] args) {
            StringBuilder content = new StringBuilder("######日志######\n");
            content.append("Title:").append(StringUtils.isEmpty(apiLog.title()) ? apiLog.name() : apiLog.title()).append("\n");
            if (apiLog.printRequestInfo()) {
                content.append(MessageFormat.format(REQUEST_INFO, request.getRequestURI(),
                        request.getContentType(),
                        IpUtil.getIpAddress(request)))
                        .append("\n");
            }
            if (apiLog.printMethodInfo()) {
                content.append(MessageFormat.format(METHOD_INFO,
                        method.getDeclaringClass().getSimpleName() + SEPARATOR + method.getName()))
                        .append("\n");
            }
            if (apiLog.type() == ApiLog.LogType.RETURN) {
                content.append("参数打印未启用!\n");
            } else {
                //排除类型
                Class<?>[] excludes = apiLog.excludes();
                content.append(getParamContent(args, excludes));
            }
            print(content.toString(), apiLog.level());
        }
    
        private StringBuilder getParamContent(Object[] args, Class<?>[] excludes) {
            StringBuilder paramContent = new StringBuilder("###参数信息###: ");
            for (Object arg : args) {
                if (arg == null) {
                    continue;
                }
                if (exclude(arg.getClass(), excludes)) {
                    paramContent.append("#排除的参数类型:").append(arg.getClass()).append(SEPARATOR);
                } else {
                    paramContent.append("#参数类型:").append(arg.getClass())
                            .append(" ").append("参数值:")
                            .append(arg.toString())
                            .append(SEPARATOR);
                }
            }
            return paramContent;
        }
    
        /**
         * 判断指定类型是否需要排除
         *
         * @param target   指定类型
         * @param excludes 需要排除的类型集合
         * @return 排除:true
         */
        private boolean exclude(@Nullable Class<?> target, @NonNull Class<?>[] excludes) {
            if (ArrayUtils.isEmpty(excludes) || target == null) {
                return false;
            }
    
            for (Class<?> clazz : excludes) {
                if (clazz.equals(target)) {
                    return true;
                }
            }
            return false;
        }
    
        /**
         * 记录在执行proceed()方法之后的日志,包括:
         * 返回值信息
         * 执行耗时
         *
         * @param apiLog          注解
         * @param result          返回结果
         * @param timeConsumption 耗时
         */
        public void logAfterProceed(ApiLog apiLog, Object result, long timeConsumption) {
            StringBuilder content = new StringBuilder("###返回值信息###: ");
            if (apiLog.type() == ApiLog.LogType.PARAM) {
                content.append("未启用返回值打印");
            } else {
                content.append(getReturnContent(result, apiLog.excludes()));
            }
    
            if (apiLog.timeConsumption()) {
                content.append("执行耗时:").append(timeConsumption).append("MS");
            } else {
                content.append("未启用方法耗时打印");
            }
            print(content.toString(), apiLog.level());
        }
    
        private StringBuilder getReturnContent(@Nullable Object result, @NonNull Class<?>[] excludes) {
            StringBuilder content = new StringBuilder();
            try {
                if (result == null) {
                    content.append("null");
                    return content;
                }
                Class<?> clazz = result.getClass();
                if (exclude(clazz, excludes)) {
                    content.append("被排除的类型:").append(clazz.getSimpleName());
                } else {
                    content.append("返回值类型:").append(clazz.getSimpleName())
                            .append(SEPARATOR).append("返回值:").append(new ObjectMapper().writeValueAsString(result));
                }
                content.append("\n");
            } catch (JsonProcessingException e) {
                log.error("Java对象转Json字符串失败!");
            }
            return content;
        }
    
    
        /**
         * 打印日志
         *
         * @param content 日志内容
         * @param level   日志级别
         */
        public void print(String content, ApiLog.LogLevel level) {
    
            switch (level) {
                case DEBUG:
                    log.debug(content);
                    break;
                case INFO:
                    log.info(content);
                    break;
                case WARN:
                    log.warn(content);
                    break;
                case ERROR:
                    log.error(content);
                    break;
                default:
                    break;
            }
        }
    }
    
    测试
    package com.cube.share.log.controller;
    
    import com.cube.share.base.templates.ApiResult;
    import com.cube.share.log.annotation.ApiLog;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    
    /**
     * @author cube.li
     * @date 2021/4/4 10:49
     * @description
     */
    @RestController
    @RequestMapping("/log")
    @Slf4j
    public class LogController {
    
        @GetMapping("/info")
        @ApiLog(level = ApiLog.LogLevel.INFO)
        public ApiResult info(@RequestParam("name") String name, @RequestParam("id") Integer id) {
            return ApiResult.success();
        }
    
        @PostMapping("/upload")
        @ApiLog(excludes = Integer.class)
        public ApiResult upload(MultipartFile file, @RequestParam("fileName") String fileName, @RequestParam("id") Integer id) {
            return ApiResult.success();
        }
    
        @ApiLog(level = ApiLog.LogLevel.ERROR, title = "标题")
        @RequestMapping("/error")
        public void error() {
        }
    }
    

    调用info方法,日志打印如下:

    2021-04-04 12:54:08.390  INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper     : ######日志######
    Title:
    ###请求信息###: URI:/log/info,Content-Type:null,请求IP:127.0.0.1
    ###方法信息###: 方法名称:LogController | info
    ###参数信息###: #参数类型:class java.lang.String 参数值:li | #参数类型:class java.lang.Integer 参数值:4 | 
    2021-04-04 12:54:08.395  INFO 11580 --- [nio-9876-exec-3] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: 返回值类型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
    执行耗时:1MS
    

    调用upload方法,日志打印如下:

    2021-04-04 12:54:42.860 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper     : ######日志######
    Title:
    ###请求信息###: URI:/log/upload,Content-Type:multipart/form-data; boundary=--------------------------112997737393373574958179,请求IP:127.0.0.1
    ###方法信息###: 方法名称:LogController | upload
    ###参数信息###: #参数类型:class java.lang.String 参数值:文件名 | #排除的参数类型:class java.lang.Integer | 
    2021-04-04 12:54:42.861 DEBUG 11580 --- [nio-9876-exec-4] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: 返回值类型:ApiResult | 返回值:{"code":200,"msg":null,"data":null}
    执行耗时:1MS
    

    由于注解中设置了排除Integer类型,因此参数id并未被打印

    调用error方法,日志打印如下:

    021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper     : ######日志######
    Title:标题
    ###请求信息###: URI:/log/error,Content-Type:null,请求IP:127.0.0.1
    ###方法信息###: 方法名称:LogController | error
    ###参数信息###: 
    2021-04-04 12:54:59.242 ERROR 11580 --- [nio-9876-exec-6] com.cube.share.log.util.ApiLogHelper     : ###返回值信息###: null执行耗时:0MS
    
    @ConditionalOnProperty注解

    这里顺带提一下@ConditionalOnProperty注解,这个注解定义如下:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Documented
    @Conditional(OnPropertyCondition.class)
    public @interface ConditionalOnProperty {
    
        // 数组,获取对应property名称的值,与name不可同时使用
        String[] value() default {};
    
        // 配置属性名称的前缀
        String prefix() default "";
    
        // 数组,配置属性完整名称或部分名称
        // 可与prefix组合使用,组成完整的配置属性名称,与value不可同时使用
        String[] name() default {};
    
        // 可与name组合使用,比较获取到的属性值与havingValue给定的值是否相同,相同才加载配置
        String havingValue() default "";
    
        // 缺少该配置属性时是否可以加载。如果为true,没有该配置属性时也会正常加载;反之则不会生效
        boolean matchIfMissing() default false;
    }
    

    可以使用@ConditionalOnProperty来配置一个Bean或者切面是否生效,例如,在本文中,如果想要使ApiLogAop不生效,则可以在ApiLogAop切面上加上@ConditionalOnProperty(prefix = "api-log", name = "enable", matchIfMissing = true, havingValue = "true")
    表示,如果在配置了api-log.enable属性且其值为true,则启用ApiLogAop切面,如果没有配置则默认为true即启用。
    在配置文件中增加如下配置:

    api-log:
      enable: false
    

    调用如上几个方法发现并没有打印日志,即ApiLogAop未生效。
    完整代码:https://gitee.com/li-cube/share/tree/master/log

    相关文章

      网友评论

        本文标题:SpringBoot项目记录请求日志

        本文链接:https://www.haomeiwen.com/subject/tnqlkltx.html