SpringBoot切面记录Http接口的请求日志

976 阅读2分钟

SpringBoot切面记录Http接口的请求日志

本文知识点:

  1. 切面及注解使用
  2. 全局获取请求参数、参数值
  3. 自定义Jackson序列化器及使用
  4. 获取请求IP地址的方法
  5. StopWatch的使用

1. 引入切面pom依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.7</version>
</dependency>

2. 自定义@Logs注解

package com.fay.common;

import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;

/**
 * 日志注解
 * @author fay
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Logs {

    @AliasFor("msg")
    String value() default "";
    @AliasFor("value")
    String msg() default "";

    String logType() default "query";
    String bizType() default "common";

}

3. 切面实现

package com.fay.common;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.StringJoiner;

/**
 * 日志切面
 * @author fay
 */
@Slf4j
@Aspect
@Component
public class LogsAspect {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Pointcut("@annotation(com.fay.test.common.Logs)")
    public void pointcut() {}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 打印请求信息
        this.writeRequest(point);
        // 请求开始
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = point.proceed();
        stopWatch.stop();
        // 打印结果信息
        this.writeResponse(point, result, stopWatch.getTotalTimeMillis());
        return result;
    }

    private void writeRequest(JoinPoint point) {
        HttpServletRequest request = RequestUtils.getRequest();
        if (Objects.isNull(request)) {
            return;
        }
        String ip = RequestUtils.getIp(request);
        String requestMethod = request.getMethod();
        String requestUri = request.getRequestURI();
        // 类名
        String clazzName = point.getTarget().getClass().getName();
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        // 方法名
        String methodName = method.getName();
        // 请求参数
        String requestParams = this.getRequestParams(point);
        log.info("请求开始:{} -> {} {} 参数:{}", ip, requestMethod, requestUri, requestParams);
        log.info("{}#{}", clazzName, methodName);
    }

    private void writeResponse(JoinPoint point, Object result, long time) {
        String resultStr = this.serialize(result);
        log.info("执行结果:{},执行耗时:{}ms", resultStr, time);
        // 方法Log注解日志
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Logs logs = method.getAnnotation(Logs.class);
        if (Objects.nonNull(logs)) {
            log.info("log:{}, {}, {}", logs.msg(), logs.logType(), logs.bizType());
        }
    }

    /**
     * 获取请求参数名及参数值
     * 获取方法参数名称的另一种方式:
     * <pre>
     *     LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
     *     String[] paramNames = u.getParameterNames(signature.getMethod());
     * </pre>
     *
     * @param point point
     * @return 参数名及参数值
     */
    private String getRequestParams(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        StringJoiner joiner = new StringJoiner("&");
        // 参数名
        String[] paramNames = signature.getParameterNames();
        // 参数值
        Object[] paramArgs = point.getArgs();
        for (int i = 0; i < paramNames.length; i++) {
            Object paramArg = paramArgs[i];
            if (paramArg instanceof ServletRequest || paramArg instanceof ServletResponse || paramArg instanceof MultipartFile) {
                continue;
            }
            String paramArgsStr = this.serialize(paramArg);
            joiner.add(paramNames[i] + "=" + paramArgsStr);
        }
        return joiner.toString();
    }
    /**
     * 序列化对象
     * obj 被序列化的对象
     */
    private String serialize(Object obj) {
        if (obj instanceof ServletRequest || obj instanceof ServletResponse || obj instanceof MultipartFile) {
            return obj.getClass().getSimpleName();
        }
        // 使用自定义序列化器:默认超过500的字符串不序列化,替换为...
        SimpleModule simpleModule = new SimpleModule();
        simpleModule.addSerializer(String.class, new LargeStringSerializer());
        try {
            return objectMapper.registerModule(simpleModule).writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 自定义字符串序列化器
     * maxStrLen 序列化最大字符串长度,默认500,超过的序列化替换为...
     * liveStrLen 序列化保留的字符数,默认0不保留
     */
    private static class LargeStringSerializer extends JsonSerializer<String> {

        private final int maxStrLen;
        private final int liveStrLen;

        public LargeStringSerializer() {
            this(500, 0);
        }

        public LargeStringSerializer (int maxStrLen, int liveStrLen) {
            this.maxStrLen = maxStrLen;
            this.liveStrLen = liveStrLen;
        }

        @Override
        public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            // 超长字符串序列化省略,替换为...
            if (s.length() > maxStrLen && s.length() > liveStrLen) {
                s = s.substring(0, liveStrLen) + "...";
            }
            jsonGenerator.writeString(s);
        }
    }

}

请求工具类

package com.fay.test.common;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Objects;

/**
 * 请求工具类
 * @author fay
 */
public class RequestUtils {
    private RequestUtils() {}

    public static HttpServletRequest getRequest(){
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(requestAttributes)) {
            return null;
        }
        return requestAttributes.getRequest();
    }

    public static String getIp(HttpServletRequest request){
        String ip = request.getHeader("x-forwarded-for");
        String unknown = "unknown";
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

使用示例

@Log(msg = "打印", bizType = "biz")
@GetMapping("/print")
public String print(String id, @RequestParam(required = false) Integer age, @RequestParam("name") String name) {
    String print = testConfigServe.print();
    System.out.println("代码:" + print + ", " + id + ", " + ", " + age + name);
    return print;
}

@Log(msg = "打印Post", bizType = "biz")
@PostMapping("/printPost")
public Map<String, Object> printPost(@RequestBody Map<String, Object> param, @RequestParam(required = false) Integer age, @RequestParam("title") String title) {
    String jsonStr = JSONUtil.toJsonStr(param);
    System.out.println("代码:" + jsonStr + (age + title));
    param.put("title", title);
    return param;
}

启动后请求测试

GET http://localhost:8081/print?id=99&name=Fay
Accept: application/json

###

POST http://localhost:8081/printPost?title=Fay&id=12&age=18
Content-Type: application/json

{
   "id": 99,
   "name": "abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234567890"
}