背景

开发项目的时候,对出入参可以通过idea 的debug模式实现。但是项目一旦发布到线上如果发现有数据存在问题,那么究竟是哪一个环节出现的问题呢。有些情况就会不好分析。或者在系统间互相调用的时候,自己作为被调用方,如何证明调用方的参数正确与否呢。当然是通过日志实现,可以通过如果log日志输出实现。但是更建议写成一个通用的,要么放到基础组件中,也可以放到各自微服务中选择是否启用。

分析

思考一番个人觉得可以采用三种方式实现

  1. 拦截器
  2. 过滤器
  3. aop切面(个人更推荐使用)
    拦截器和过滤器实现方式差不多,因此我就只说其中一个和aop切面实现。

filter实现

首先在项目启动的时候注入过滤器bean

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean timeFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean();
        bean.setFilter(new TimeFilter());
        bean.addUrlPatterns("/remote/*");
        return bean;
    }
}

过滤实现bean

@Component
public class TimeFilter implements Filter {

    private Logger log = LoggerFactory.getLogger(TimeFilter.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        ServletRequest requestWrapper = null;
        if(HttpMethod.GET.toString().equalsIgnoreCase(httpServletRequest.getMethod())){
            log.info("入参参数--get-----" + JSONObject.toJSONString(httpServletRequest.getQueryString()));
        }else if(HttpMethod.POST.toString().equalsIgnoreCase(httpServletRequest.getMethod())){
            requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request);
            byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream());
            String body = new String(bodyBytes, httpServletRequest.getCharacterEncoding());
            log.info("入参参数--post-----" + body);
        }
        long start = System.currentTimeMillis();
        if (null == requestWrapper) {
            chain.doFilter(request, response);
        } else {
            chain.doFilter(requestWrapper, response);
        }
        log.info(request.getRemoteAddr() + ":" + request.getLocalPort() + httpServletRequest.getRequestURI() + " 耗时 : " + (System.currentTimeMillis() - start) + "ms");
    }

    @Override
    public void destroy() {

    }
}

如果用过滤器实现,目前我只想到的在请求中通过流的方式把请求方式拿取出来。但是使用流有一个重要的问题:

流只能读取一次, 流只能读取一次, 流只能读取一次

重要的事情说三遍

因此如果硬要使用, 那必须得处理一下request stream。

public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private final byte[] body;

    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        String bodyString = HttpHelper.getBodyString(request);
        body = bodyString.getBytes(Charset.forName("UTF-8"));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}
public class HttpHelper {

    public static String getBodyString(HttpServletRequest request) throws IOException {
        StringBuilder sb = new StringBuilder();
        InputStream inputStream = null;
        BufferedReader reader = null;
        try {
            inputStream = request.getInputStream();
            reader = new BufferedReader(
                    new InputStreamReader(inputStream, Charset.forName("UTF-8")));

            char[] bodyCharBuffer = new char[1024];
            int len = 0;
            while ((len = reader.read(bodyCharBuffer)) != -1) {
                sb.append(new String(bodyCharBuffer, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}

总结filter实现

这样就可以通过流获取请求参数并且request中还有流。这种实现方法有一个弊端:先把流转成字符串,又把字符串转回流,这样的目的就为了输出请求参数。在我看来有点好钢没有用在刀刃上的感觉。毕竟我们做这个也只是一个辅助性的util,当线上并发大的时候这样来回转换流会拉低不少系统的并发。

切面实现(个人比较推荐)

aop是spring很重要的一个属性,因此在这里可以使用一下。直接上干货有两种实现方式二选一,两种方式大家按需选择

方式一

pom引用

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>
@Aspect
@Component
@Order(1)
@Slf4j
public class AspectConfig {

    @Pointcut("execution(* com.xxx.xxx.xxx.controller..*.*(..))")
    private void webLog(){
    }


    @Before(value="webLog()")
    public void before(JoinPoint joinPoint){
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        log.info("请求日志的打印");
        log.info("请求地址:{}", Optional.ofNullable(request.getRequestURI().toString()).orElse(null));
        log.info("请求方式:{}",request.getMethod());
        log.info("请求类方法:{}",joinPoint.getSignature());
        log.info("请求类方法参数:{}", JSONObject.toJSONString(JSONObject.toJSONString(filterArgs(joinPoint.getArgs()))));
    }

    private List<Object> filterArgs(Object[] objects) {
        return Arrays.stream(objects).filter(obj -> !(obj instanceof MultipartFile)
                && !(obj instanceof HttpServletResponse)
                && !(obj instanceof HttpServletRequest)).collect(Collectors.toList());
    }
}

此种方式在请求之前拦截,但是没法实现整个请求所消耗的时间。在拦截配置中配置需要拦截的包,这种方式适合只拦截参数并且不对请求时长数据做输出的。

方式二

@Aspect
@Component
@Order(1)
@Slf4j
public class AspectConfig {
    @Around("@within(org.springframework.web.bind.annotation.RestController)" +
            "||@within(org.springframework.stereotype.Controller)")
    public Object after(ProceedingJoinPoint joinPoint) throws Throwable{
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        log.info("请求日志的打印");
        log.info("请求地址:{}", Optional.ofNullable(request.getRequestURI().toString()).orElse(null));
        log.info("请求方式:{}",request.getMethod());
        log.info("请求类方法:{}",joinPoint.getSignature());
        log.info("请求类方法参数:{}", JSONObject.toJSONString(filterArgs(joinPoint.getArgs())));
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed(joinPoint.getArgs());
        long end = System.currentTimeMillis();
        log.info("执行耗时:{}", end - start);
        return result;
    }

    private List<Object> filterArgs(Object[] objects) {
        return Arrays.stream(objects).filter(obj -> !(obj instanceof MultipartFile)
                && !(obj instanceof HttpServletResponse)
                && !(obj instanceof HttpServletRequest)).collect(Collectors.toList());
    }
}

这种方式通过对注解切面实现,通用性很强,适合做成项目级别,每个微服务引用基础包后自动启用。这里注意一个点public Object after方法必须要有返回值,不然请求的返回值是null。Object result = joinPoint.proceed(joinPoint.getArgs());这个就是调用请求result即为调用请求后的response。

以上就是我的三种实现在项目中出入参监控的实现方式,如有问题,希望大家评论区留言多多指教!