SpringBoot 实现全局统一请求日志处理


相关工具类

  1. 内容类型判断工具类

    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * 内容类型工具类
     * @author TheEnd
     */
    public class ContentTypeUtil {
    
        public static boolean isBinaryContent(HttpServletRequest request) {
            String contentType = request.getContentType();
            return isBinaryContent(contentType);
        }
        public static boolean isBinaryContent(HttpServletResponse response) {
            String contentType = response.getContentType();
            return isBinaryContent(contentType);
        }
        public static boolean isBinaryContent(String contentType) {
            if (contentType == null || contentType.isEmpty()) {
                return false;
            }
            return contentType.startsWith("image/")
                    || contentType.startsWith("multipart/form-data")
                    || contentType.startsWith("audio/")
                    || contentType.startsWith("video/")
                    || contentType.startsWith("application/octet-stream")
                    || contentType.startsWith("application/vnd.ms-excel")
                    || contentType.startsWith("application/vnd.ms-powerpoint")
                    || contentType.startsWith("application/msword")
                    || contentType.startsWith("application/vnd.openxmlformats-officedocument")
                    || contentType.startsWith("application/zip")
                    || contentType.startsWith("application/x-");
        }
    
    }
  2. 客户端IP获取工具类

    import javax.servlet.http.HttpServletRequest;
    import java.util.*;
    
    /**
     * 用户获取客户端IP的工具类
     * @author TheEnd
     */
    public class IpUtil {
    
        /**
         * 获取客户端IP
         * @param request 请求对象
         * @return 客户端IP
         */
        public static String getIp(HttpServletRequest request) {
            if (request == null) {
                return "";
            }
            ArrayList<String> headerNames = Collections.list(request.getHeaderNames());
            List<String> ipHeaderNames = Arrays.asList("X-Forwarded-For", "Proxy-Client-IP", "X-Real-IP");
            for (String ipHeaderName : ipHeaderNames) {
                String ip = getHeaderValueIgnoreCase(ipHeaderName, headerNames, request);
                if (ip != null && !ip.isEmpty()) {
                    return ip;
                }
            }
    
            return request.getRemoteAddr();
        }
    
        private static String getHeaderValueIgnoreCase(String headerName, List<String> requestHeaderNames, HttpServletRequest request) {
            for (String requestHeaderName : requestHeaderNames) {
                if (requestHeaderName.equalsIgnoreCase(headerName)) {
                    return request.getHeader(requestHeaderName);
                }
            }
            return "";
        }
    
    
    }

获取已登录用户信息封装

此信息属于业务信息, 需要按照业务实际情况进行编写, 所以此处只提供接口和一个空实现

获取已登录用户信息接口

import org.springframework.lang.NonNull;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 已登录用户信息获取工具
 * @author TheEnd
 */
public interface LoginUserInfoHelper {

    /**
     * 用户ID
     * @param request 请求对象
     * @param response 响应对象
     * @return 用户ID
     */
    String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response);

    /**
     * 用户名
     * @param request 请求对象
     * @param response 响应对象
     * @return 用户名
     */
    String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response);

}
默认空实现, 如不需要记录登录人信息, 使用此类即可
import org.springframework.lang.NonNull;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 返回空字符串的用户信息获取器
 * @author The
 */
public class EmptyLoginUserInfoHelper implements LoginUserInfoHelper {
    @Override
    public String userId(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) {
        return "";
    }

    @Override
    public String username(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response) {
        return "";
    }
}
可以根据实际业务需求自定义实现类.

编写日志类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

/**
 * 请求日志
 * @author TheEnd
 */
@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class RequestLog implements Serializable {

    /**
     * 请求方法
     */
    private String method;

    /**
     * 请求路径
     */
    private String url;

    /**
     * 客户端Ip
     */
    private String ip;

    /**
     * 携带Cookie
     */
    private String cookie;

    /**
     * 请求参数
     */
    private String requestParams;

    /**
     * 请求体
     */
    private String requestBody;

    /**
     * 响应HTTP状态码
     */
    private Integer responseCode;

    /**
     * 响应内容
     */
    private String responseBody;

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 用户名
     */
    private String username;

    /**
     * 模块名称
     */
    private String modelName;

    /**
     * 接口名称
     */
    private String interfaceName;

    /**
     * 备注
     */
    private String remarks;

    /**
     * 请求时间
     */
    private Date requestTime;

    /**
     * 响应时间
     */
    private Date responseTime;

    /**
     * 使用毫秒
     */
    private Integer useTime;

}

编写相关注解

  1. 日志额外信息注解

    用于通过注解给日志记录增加额外信息

    可以标注在类上和方法上, 标注在类上时类中所有的方法的日志都有此信息. 类和方法上都有标注时,优先生效方法上的.

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 接口额外信息
     * @author TheEnd
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface LogExtra {
    
        /**
         * 模块名
         * @return 模块名
         */
        String modelName() default "";
    
        /**
         * 接口名
         * @return 接口名
         */
        String interfaceName() default "";
    
        /**
         * 备注
         * @return 备注
         */
        String remarks() default "";
    
    }
  2. 不记录日志注解

    标注了此注解的类和方法不会进行日志记录
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 表名此接口不需要记录日志
     * @author TheEnd
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface NotLog {
    }

编写序列化器

提供了序列化器接口LogSerializer和默认实现, 可以根据业务需求自定义序列化器, 创建一个类并实现LogSerializer接口即可.
  1. 序列化器接口

    import org.springframework.lang.NonNull;
    
    /**
     * 日志序列化器
     * @author TheEnd
     */
    public interface LogSerializer {
    
        /**
         * 序列化
         * @param log 日志对象
         * @return 序列化数据
         */
        byte[] serializer(@NonNull RequestLog log);
    
    }
  2. JDK ToString 序列化

    import org.springframework.lang.NonNull;
    
    /**
     * 将日志对象通过ToString进行序列化的序列化器
     * @author TheEnd
     */
    public class ToStringLogSerializer implements LogSerializer {
        @Override
        public byte[] serializer(@NonNull RequestLog log) {
            return log.toString().getBytes();
        }
    }
  3. Jackson ToJson 序列化

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import org.springframework.lang.NonNull;
    
    /**
     * 将日志对象转为JSON的序列化器
     * @author TheEnd
     */
    public class JsonLogSerializer implements LogSerializer {
    
        private static final ObjectMapper MAPPER = new ObjectMapper();
    
        @Override
        public byte[] serializer(@NonNull RequestLog log) {
            try {
                return MAPPER.writeValueAsBytes(log);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
    }

编写日志打印器

日志打印器用于将日志输出至指定位置, 提供打印器接口和默认实现, 可以根据业务需求, 自定义打印器.

可以将日志输出至文件,控制台,消息中间件,数据库,网络流等

  1. 打印器接口

    import org.springframework.lang.NonNull;
    
    /**
     * 日志打印器
     * @author TheEnd
     */
    public interface LogPrinter {
    
        /**
         * 打印日志
         * @param log 日志对象
         */
        void print(@NonNull RequestLog log);
    
    }
  2. Logback 打印器

    通过Logback打印
    import lombok.Data;
    import lombok.RequiredArgsConstructor;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.lang.NonNull;
    
    /**
     * 将日志写到文件的持久化器
     * @author TheEnd
     */
    @Data
    @RequiredArgsConstructor
    public class LogbackLogPrinter implements LogPrinter {
    
        private final Logger log = LoggerFactory.getLogger(LogbackLogPrinter.class);
    
        /**
         * 序列化器
         */
        private final LogSerializer serializer;
    
        @Override
        public void print(@NonNull RequestLog requestLog) {
            byte[] bytes = serializer.serializer(requestLog);
            log.info(new String(bytes));
        }
    }

编写日志拦截器类

import java.lang.annotation.Annotation;
import java.util.Date;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 日志序列化拦截器
 * @author TheEnd
 */
@Slf4j
@ControllerAdvice
@RequiredArgsConstructor
public class LogPersistenceInterceptor implements HandlerInterceptor, ResponseBodyAdvice<Object> {

    /**
     * 请示时间Key
     */
    private static final String API_START_TIME = "ApiLogSerializableInterceptor:preHandle:start:millis";
    /**
     * 请求体Key
     */
    private static final String API_REQUEST_BODY = "ApiLogSerializableInterceptor:preHandle:request:body";
    /**
     * 处理器key
     */
    private static final String API_HANDLER = "ApiLogSerializableInterceptor:preHandle:handler";

    /**
     * JSON 工具对象
     */
    private final ObjectMapper logMapper = new ObjectMapper();

    /**
     * 日志打印器
     */
    private final List<LogPrinter> logPrinters;
    /**
     * 当前登录用户信息获取器
     */
    private final LoginUserInfoHelper loginUserInfoHelper;

    @Override
    public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception {
        request.setAttribute(API_HANDLER, handler);
        if (!isRecord(request, response, handler)) {
            return true;
        }
        Date startTime = new Date();
        request.setAttribute(API_START_TIME, startTime);
        String body = readBody(request);
        request.setAttribute(API_REQUEST_BODY, body);
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType, @NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) {


        HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();
        HttpServletResponse res = ((ServletServerHttpResponse) response).getServletResponse();
        Object handler = req.getAttribute(API_HANDLER);
        if (isRecord(req, res, handler)) {
            return body;
        }

        String result = "";
        if (!isBinaryContent(res) && body instanceof Serializable) {
            try {
                result = logMapper.writeValueAsString(body);
            } catch (JsonProcessingException e) {
                log.error("HTTP响应体JSON转换失败", e);
            }
        }
        RequestLog requestLog = buildLog(req, res, handler, result);
        for (LogPrinter printer : logPrinters) {
            printer.print(requestLog);
        }
        return body;
    }

    private String readBody(HttpServletRequest request) {
        if (request.getContentLength() <= 0) {
            return "";
        }
        BufferedReader reader = null;
        try {
            reader = request.getReader();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        Stream<String> lines = reader.lines();
        return lines.collect(Collectors.joining(System.lineSeparator()));
    }

    private boolean isRecord(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return false;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (method.isAnnotationPresent(NotLog.class)) {
            return false;
        }
        Class<?> declaringClass = method.getDeclaringClass();
        if (declaringClass.isAnnotationPresent(NotLog.class)) {
            return false;
        }
        if (isBinaryContent(request)) {
            return false;
        }
        return true;
    }

    private RequestLog buildLog(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, @NonNull String result) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        LogExtra methodExtra = method.getAnnotation(LogExtra.class);

        Class<?> declaringClass = method.getDeclaringClass();
        LogExtra classExtra = declaringClass.getAnnotation(LogExtra.class);

        LogExtra merge = mergeLogExtra(methodExtra, classExtra);

        RequestLog requestLog = new RequestLog();
        requestLog.setMethod(request.getMethod());
        requestLog.setUrl(request.getRequestURI());
        requestLog.setIp(IpUtil.getIp(request));
        requestLog.setCookie(cookieSerializable(request));
        requestLog.setRequestParams(parameterSerializable(request));
        requestLog.setRequestBody((String) request.getAttribute(API_REQUEST_BODY));
        requestLog.setResponseCode(response.getStatus());
        requestLog.setResponseBody(result);
        requestLog.setUserId(loginUserInfoHelper.userId(request, response));
        requestLog.setUsername(loginUserInfoHelper.username(request, response));
        requestLog.setModelName(merge.modelName());
        requestLog.setInterfaceName(merge.interfaceName());
        requestLog.setRemarks(merge.remarks());
        Date startTime = (Date) request.getAttribute(API_START_TIME);
        requestLog.setRequestTime(startTime);
        Date responseTime = new Date();
        requestLog.setResponseTime(responseTime);
        requestLog.setUseTime(Math.toIntExact(responseTime.getTime() - startTime.getTime()));
        return requestLog;
    }

    private String cookieSerializable(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        try {
            return logMapper.writeValueAsString(cookies);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private String parameterSerializable(HttpServletRequest request) {
        Map<String, String[]> parameterMap = request.getParameterMap();
        try {
            return logMapper.writeValueAsString(parameterMap);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    private LogExtra mergeLogExtra(LogExtra methodExtra, LogExtra classExtra) {
        return new LogExtra(){
            @Override
            public String modelName() {
                if (methodExtra != null && methodExtra.modelName() != null && !methodExtra.modelName().isEmpty()) {
                    return methodExtra.modelName();
                }
                if (classExtra != null && classExtra.modelName() != null && !classExtra.modelName().isEmpty()) {
                    return classExtra.modelName();
                }
                return "";
            }

            @Override
            public String interfaceName() {
                if (methodExtra != null && methodExtra.interfaceName() != null && !methodExtra.interfaceName().isEmpty()) {
                    return methodExtra.interfaceName();
                }
                if (classExtra != null && classExtra.interfaceName() != null && !classExtra.interfaceName().isEmpty()) {
                    return classExtra.interfaceName();
                }
                return "";
            }

            @Override
            public String remarks() {
                if (methodExtra != null && methodExtra.remarks() != null && !methodExtra.remarks().isEmpty()) {
                    return methodExtra.remarks();
                }
                if (classExtra != null && classExtra.remarks() != null && !classExtra.remarks().isEmpty()) {
                    return classExtra.remarks();
                }
                return "";
            }

            @Override
            public Class<? extends Annotation> annotationType() {
                return LogExtra.class;
            }
        };
    }
}

配置拦截器

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.Collections;
import java.util.List;

/**
 * @author TheEnd
 */
@Configuration
public class MvcConfigurer implements WebMvcConfigurer {

    /**
     * 日志持久化拦截器
     */
    private LogPersistenceInterceptor logPersistenceInterceptor;
    @Autowired
    public MvcConfigurer setLogPersistenceInterceptor(LogPersistenceInterceptor logPersistenceInterceptor) {
        this.logPersistenceInterceptor = logPersistenceInterceptor;
        return this;
    }

    /**
     * 日志打印器
     * @return 日志打印器集合
     */
    @Bean
    public List<LogPrinter> logPrinters() {
        // 根据业务实际情况创建并实例化日志打印器, 并注册至IOC容器.
        LogbackLogPrinter printer = new LogbackLogPrinter(new JsonLogSerializer());
        return Collections.singletonList(printer);
    }

    /**
     * 用户信息获取器
     * @return 用户信息获取器
     */
    @Bean
    public LoginUserInfoHelper loginUserInfoHelper() {
        // 根据业务实际情况创建并实例化用户信息获取器, 并注册至IOC容器.
        return new EmptyLoginUserInfoHelper();
    }

    /**
     * 添加拦截器
     * @param registry 拦截器注册
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logPersistenceInterceptor);
    }
}

解决请求体无法重复读取

Filter中,替换原始的请求对象,实现请求体可以重读读取
  1. 创建RequestWrapper对象, 用于包装原始Request对象

    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.BufferedReader;
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.StringReader;
    
    /**
     * 请求包装器
     * 用于解决请求体无法重复读取问题
     * @author TheEnd
     */
    public class RequestWrapper extends HttpServletRequestWrapper {
        public RequestWrapper(HttpServletRequest request) {
            super(request);
        }
    
        private volatile String requestBody;
    
        @Override
        public BufferedReader getReader() throws IOException {
            return new BufferedReader(new StringReader(getRequestBody()));
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new ServletInputStreamWrapper(requestBody.getBytes());
        }
    
        private static class ServletInputStreamWrapper extends ServletInputStream {
    
            private final ByteArrayInputStream inputStream;
    
            public ServletInputStreamWrapper(byte[] body) {
                inputStream = new ByteArrayInputStream(body);
            }
    
            @Override
            public int read() throws IOException {
                return inputStream.read();
            }
    
            @Override
            public boolean isFinished() {
                return inputStream.available() == 0;
            }
    
            @Override
            public boolean isReady() {
                return true;
            }
    
            @Override
            public void setReadListener(ReadListener listener) {
    
            }
        }
    
        private String getRequestBody() throws IOException {
            if (requestBody == null) {
                synchronized (this) {
                    if (requestBody == null) {
                        StringBuilder requestBodyBuilder = new StringBuilder();
                        BufferedReader reader = super.getReader();
                        String line;
                        while ((line = reader.readLine()) != null) {
                            requestBodyBuilder.append(line);
                        }
                        requestBody = requestBodyBuilder.toString();
                    }
                }
            }
            return requestBody;
        }
    }
  2. 编写过滤器并替换原始Request对象
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 用于替换请求对象的过滤器
 * 用于解决请求体无法重复读取问题
 * @author TheEnd
 */
public class HttpServletRequestWrapperFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if (!(servletRequest instanceof HttpServletRequest)) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (ContentTypeUtil.isBinaryContent(request) || request.getContentLength() < 1) {
            filterChain.doFilter(request, servletResponse);
            return;
        }
        filterChain.doFilter(new RequestWrapper(request), servletResponse);
    }
}

评论已关闭