springboot运营相关

SpringBoot项目记录请求日志

2021-04-04  本文已影响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

上一篇下一篇

猜你喜欢

热点阅读