SpringBoot通用日志解决方案

金融项目中对于业务较为敏感我们通常需要将用户的操作形成一个结构化的数据并进行持久化。

结构化日志需要的字段:

操作员信息、客户端IP地址、请求地址、控制器名称、控制器方法名称、HTTP请求类型、HTTP请求参数。

问题描述:

在获取请求参数时必然会读取request.getInputStream。由于流只允许读一次,后续读取必然会导致异常。

解决方案:

在SpringBoot框架中给我们提供了一个基于Filter的简单通用日志——CommonsRequestLoggingFilter,这个日志仅仅只实现了日志文件的输出远远达不到我们的设计目标。

通过阅读源码我发现了ContentCachingRequestWrapper这个类能够解决HttpServletRequest inputStream只能读取一次的问题,但是这个类有缺陷(前提必须是doFilter之前不能使用request.getInputStream()方法)。

配置Filter让后续的请求可以正常request.getInputStream

package com.bbc.ibank.sys.app.filter;

import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 请求上下文缓存过滤器<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/10/12 2:55 下午<br>
 */
public class ContentCachingRequestFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
        filterChain.doFilter(wrapper, response);
    }
}

拦截器

package com.bbc.ibank.sys.app.interceptor;

import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.netfinworks.vfsso.client.authapi.VfSsoUser;
import com.bbc.ibank.dal.mapper.LogDOMapper;
import com.bbc.ibank.dal.model.LogDO;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Objects;

/**
 * 通用日志拦截器<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/10/12 10:16 上午<br>
 */
public class LogInterceptor implements HandlerInterceptor {

    private static final Log log = LogFactory.get(LogInterceptor.class);

    @Resource(name = "logDOMapper")
    private LogDOMapper logDOMapper;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception e) {
        try {
            // 当前登录操作员信息
            String userInfo = JSONUtil.toJsonStr(VfSsoUser.get());

            // 控制器名称
            String controllerName = null;
            // 方法名称
            String actionName = null;

            if (handler instanceof HandlerMethod) {
                HandlerMethod handlerMethod = (HandlerMethod) handler;
                controllerName = handlerMethod.getBean().getClass().getSimpleName();
                actionName = handlerMethod.getMethod().getName();
            }

            // 客户端IP
            String clientIP = ServletUtil.getClientIP(request);
            // 请求地址
            String requestUrl = request.getRequestURL().toString();
            // 请求方法类型
            String method = request.getMethod();
            // 请求参数
            String params = null;
            if (ServletUtil.isGetMethod(request)) {
                params = JSONUtil.toJsonStr(ServletUtil.getParams(request));
            } else if (ServletUtil.isPostMethod(request)) {
                if (ContentType.FORM_URLENCODED.getValue().equals(request.getContentType())) {
                    params = JSONUtil.toJsonStr(ServletUtil.getParams(request));
                } else if (ContentType.JSON.getValue().equals(request.getContentType())) {
                    ContentCachingRequestWrapper nativeRequest =
                            WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
                    if (Objects.nonNull(nativeRequest)) {
                        params = new String(nativeRequest.getContentAsByteArray(), StandardCharsets.UTF_8.name());
                    }
                }
            }

            LogDO logDO = new LogDO();
            logDO.setController(controllerName);
            logDO.setAction(actionName);
            logDO.setUrl(requestUrl);
            logDO.setMethod(method);
            logDO.setIp(clientIP);
            logDO.setCreateTime(new Date());
            logDO.setParams(params);
            logDO.setUserInfo(userInfo);
            logDOMapper.insertSelective(logDO);

        } catch (Exception exception) {
            log.error("通用日志异常:", e);
        }
    }
}

注册Filter和拦截器

package com.bbc.ibank.sys.app.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import cn.hutool.setting.Setting;
import com.netfinworks.vfsso.client.filter.VfSsoCasFilter;
import com.bbc.ibank.sys.app.annotation.IgnoreLoginCheck;
import com.bbc.ibank.sys.app.constant.AppConst;
import com.bbc.ibank.sys.app.constant.SymbolConst;
import com.bbc.ibank.sys.app.filter.ContentCachingRequestFilter;
import com.bbc.ibank.sys.app.interceptor.*;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 过滤器配置<br>
 * 作者:徐承恩<br>
 * 邮箱:xuchengen@gmail.com<br>
 * 日期:2020/5/11 10:56 上午<br>
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    private static final Log log = LogFactory.get(WebConfig.class);

    @Value(value = "${profile}")
    private String profile;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 执行顺序就是添加的顺序

        // 通用日志拦截器
        registry.addInterceptor(logInterceptor())
                .addPathPatterns(AppConst.INTERCEPTOR_API_BASE_PATH);
    }

    @Bean
    public LogInterceptor logInterceptor() {
        return new LogInterceptor();
    }

    @Bean
    public FilterRegistrationBean<ContentCachingRequestFilter> contentCacheingRequestFilter() {
        FilterRegistrationBean<ContentCachingRequestFilter> registration =
                new FilterRegistrationBean<>(new ContentCachingRequestFilter());
        registration.addUrlPatterns(AppConst.FILTER_API_BASE_PATH);
        registration.setName(AppConst.CONTENT_CACHING_REQUEST_FILTER_NAME);
        registration.setOrder(1);
        return registration;
    }
}