springboot 记录api请求日志

Springboot 记录api接口调用记录

在对第三方系统提供接口调用时,通常需要对接口调用情况进行记录以便问题追踪和排查。本文记录一下利用 HandlerInterceptor 实现对接口调用情况的记录。

主要思路

  1. 如何同时获取到请求和响应并进行记录?
    基本思路:使用 Interception 进行请求拦截,对请求体和响应体进行解析,提取感兴趣的内容转换为数据模型;
    存在问题:请求体 HttpServletRequest 和响应体 HttpServletResponse 中的数据流默认只能读取一次,在Interceptor中读取过之后会导致后续程序无法获取到正常数据。

  2. 如何重复读取HttpServletRequest 和 HttpServletResponse 中的数据流而不影响后续程序?
    基本思路:创建继承自 HttpServletRequestWrapper 的请求包装类 BodyCachingHttpServletRequestWrapper,创建继承自 HttpServletResponseWrapper 的响应包装类BodyCachingHttpServletResponseWrapper,包装类可以实现数据流的重复读。
    并在Filter中将原始的 ServletRequest 替换为 BodyCachingHttpServletRequestWrapper 包装类,将 ServletResponse 替换为 BodyCachingHttpServletResponseWrapper 包装类,以使 Interceptor 获取到的请求和响应均是包装类,达到读取流之后不影响后续程序。

  3. 使用自定义注解,实现对接口调用记录 的可配置。

  4. 获取api接口定义的基础地址,例如 /say/{message},以应用于后续接口统计。

API请求拦截主要实现(主要思路第1点)

编写实现了 HandlerInterceptor 接口的拦截器 ApiLogInterceptor ,在preHandle中记录请求信息,并把信息设置到了request上,可供Controller层使用;在afterCompletion 中记录响应,获取到完整的 ApiLogRecord 信息。

将 ApiLogInterceptor 配置进拦截器,以使拦截器生效。

/**
 * API接口请求日志拦截器
 * <bold>需要在WebMvcConfigurationSupport的子类中进行注册才能使当前拦截器生效,本项目注册见RequestInterceptorConf.</bold>
 *
 * @author: Zemel
 * @date: 2020-08-06 17:25
 */
public class ApiLogInterceptor implements HandlerInterceptor {

    private final Logger logger = LoggerFactory.getLogger(ApiLogInterceptor.class);

    private static final String ATTR_REQUEST_ID = "Request-Id";
    private static final String ATTR_REQUEST_LOG = "request-log";
    private static final String DIRECTION_RECEIVE = "RECEIVE";


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        try {
            logRequest(request, handler);
        } catch (Exception e) {
            logger.error("logRequest error", e);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // do nothing
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        try {
            logResponse(request, response, handler);
        } catch (Exception e) {
            logger.error("logResponse error", e);
        }
    }

    private void onLogRecord(ApiLogRecord apiLogRecord) {
        logger.info("log api request: \n {}", apiLogRecord);
    }

    private void logRequest(HttpServletRequest request, Object handler) throws IOException {
        if (!(handler instanceof HandlerMethod)) {
            // 不是controller配置的接口,例如swagger,不进行记录
            return;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;

        // controller配置的api接口地址
        ApiInfo apiInfo = ApiInfoUtil.resolve(handlerMethod);
        ApiLog apiLog = getApiLogAnnotation(handlerMethod);
        if (apiInfo == null ||
                (apiLog != null && !apiLog.log())) {
            // 不记录request
            return;
        }

        ApiLogRecord apiLogRecord = new ApiLogRecord();
        apiLogRecord.setDirection(DIRECTION_RECEIVE);
        apiLogRecord.setApiPath(apiInfo.getApiPath());
        apiLogRecord.setRequestAddr(request.getRemoteAddr());

        String url = request.getRequestURI();
        String queryParams = request.getQueryString();

        if (queryParams != null) {
            try {
                url = url + "?" + URLDecoder.decode(queryParams, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                logger.error("拼接queryString error", e);
            }
        }

        apiLogRecord.setRequestUrl(url);
        apiLogRecord.setRequestMethod(request.getMethod());
        apiLogRecord.setRequestTime(new Date());

        JSONObject headerJson = new JSONObject();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            headerJson.put(headerName, request.getHeader(headerName));
        }
        apiLogRecord.setRequestHeaders(headerJson);

        Map<String, String[]> requestParameterMap = request.getParameterMap();
        JSONObject bodyJson = new JSONObject();
        for (Map.Entry<String, String[]> entry : requestParameterMap.entrySet()) {
            bodyJson.put(entry.getKey(), entry.getValue());
        }
        apiLogRecord.setRequestParams(bodyJson);

        if ((request instanceof BodyCachingHttpServletRequestWrapper)
                && (apiLog == null || apiLog.requestBody())) {
            BodyCachingHttpServletRequestWrapper requestWrapper = (BodyCachingHttpServletRequestWrapper) request;
            apiLogRecord.setRequestBody(new String(requestWrapper.getBody()));
        }

        String requestId = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()) + UUID.randomUUID().toString().substring(0, 3);
        apiLogRecord.setId(requestId);

        request.setAttribute(ATTR_REQUEST_ID, requestId);
        request.setAttribute(ATTR_REQUEST_LOG, apiLogRecord);

    }

    private void logResponse(HttpServletRequest request, HttpServletResponse response, Object handler) {
        Object requestId = request.getAttribute(ATTR_REQUEST_ID);
        if (requestId == null) {
            // 没有requestId标记,什么都不记录
            return;
        }
        ApiLogRecord apiLogRecord = (ApiLogRecord) request.getAttribute(ATTR_REQUEST_LOG);
        apiLogRecord.setResponseTime(new Date());
        Long timeConsumed = apiLogRecord.getResponseTime().getTime() - apiLogRecord.getRequestTime().getTime();
        apiLogRecord.setTimeConsumed(timeConsumed);
        apiLogRecord.setResponseStatus(response.getStatus());

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        ApiLog apiLog = getApiLogAnnotation(handlerMethod);
        if ((response instanceof BodyCachingHttpServletResponseWrapper)
                && (apiLog == null || apiLog.responseBody())) {
            BodyCachingHttpServletResponseWrapper responseWrapper = (BodyCachingHttpServletResponseWrapper) response;
            apiLogRecord.setResponseBody(new String(responseWrapper.getBody()));
        }
        this.onLogRecord(apiLogRecord);
    }


    /**
     * 获取接口上的ApiLogConf注解,方法上的注解优先级高于类上的注解
     */
    private static ApiLog getApiLogAnnotation(HandlerMethod handlerMethod) {
        ApiLog apiLog = null;
        if (handlerMethod.getBeanType().isAnnotationPresent(ApiLog.class)) {
            // 因为ApiLog
            apiLog = AnnotationUtils.getAnnotation(handlerMethod.getBeanType(), ApiLog.class);
        }
        // 接口参数解析
        if (handlerMethod.getMethod().isAnnotationPresent(ApiLog.class)) {
            apiLog = AnnotationUtils.getAnnotation(handlerMethod.getMethod(), ApiLog.class);
        }
        return apiLog;
    }

}
/**
 * 请求拦截器配置
 *
 * @author: Zemel
 * @date: 2020-08-06 17:29
 */
@Configuration
public class RequestInterceptorConf extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        // 添加requestLog拦截器
        registry.addInterceptor(new ApiLogInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

}

使拦截器中的请求和响应流可重复读(主要思路第2点)

/**
 * HttpServletRequestWrapper 包装类,用于缓存请求体以实现请求体的多次读取
 *
 * @author: Zemel
 * @date: 2020-08-10 15:11
 * @see com.dimpt.rest.config.BodyCachingHttpServletResponseWrapper
 * @see com.dimpt.rest.config.BodyCachingWrapperFilter
 */
public class BodyCachingHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private ServletInputStreamWrapper inputStreamWrapper;

    private byte[] body;

    public BodyCachingHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        this.body = StreamUtils.copyToByteArray(request.getInputStream());
        this.inputStreamWrapper = new ServletInputStreamWrapper(new ByteArrayInputStream(this.body));
        resetInputStream();
    }

    private void resetInputStream() {
        this.inputStreamWrapper.setInputStream(new ByteArrayInputStream(this.body != null ? this.body : new byte[0]));
    }

    public byte[] getBody() {
        return body;
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        return this.inputStreamWrapper;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.inputStreamWrapper));
    }


    private static class ServletInputStreamWrapper extends ServletInputStream {

        private InputStream inputStream;

        ServletInputStreamWrapper(InputStream inputStream) {
            this.inputStream = inputStream;
        }

        void setInputStream(InputStream inputStream) {
            this.inputStream = inputStream;
        }

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

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

        @Override
        public void setReadListener(ReadListener readListener) {
            // 只用来缓存,不需要设置读监听器
        }

        @Override
        public int read() throws IOException {
            return this.inputStream.read();
        }
    }

}
/**
 * HttpServletResponse包装类,用于缓存响应体以实现响应体的多次读取
 *
 * @author: Zemel
 * @date: 2020-08-10 15:14
 * @see com.dimpt.rest.config.BodyCachingHttpServletRequestWrapper
 * @see com.dimpt.rest.config.BodyCachingWrapperFilter
 */
public class BodyCachingHttpServletResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    private HttpServletResponse response;

    public BodyCachingHttpServletResponseWrapper(HttpServletResponse response) {
        super(response);
        this.response = response;
    }

    public byte[] getBody() {
        return byteArrayOutputStream.toByteArray();
    }

    @Override
    public ServletOutputStream getOutputStream() {
        return new ServletOutputStreamWrapper(this.byteArrayOutputStream, this.response);
    }

    @Override
    public PrintWriter getWriter() throws IOException {
        return new PrintWriter(new OutputStreamWriter(this.byteArrayOutputStream, this.response.getCharacterEncoding()));
    }


    private static class ServletOutputStreamWrapper extends ServletOutputStream {

        private ByteArrayOutputStream outputStream;
        private HttpServletResponse response;

        public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream, HttpServletResponse response) {
            this.outputStream = outputStream;
            this.response = response;
        }

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

        @Override
        public void setWriteListener(WriteListener listener) {
            // 只用来缓存,不需要设置读监听器
        }

        @Override
        public void write(int b) throws IOException {
            this.outputStream.write(b);
        }

        @Override
        public void flush() throws IOException {
            if (!this.response.isCommitted()) {
                byte[] body = this.outputStream.toByteArray();
                ServletOutputStream servletOutputStream = this.response.getOutputStream();
                servletOutputStream.write(body);
                servletOutputStream.flush();
            }
        }
    }

}
/**
 * 在Filter中将原始 HttpServletRequest 和 HttpServletResponse 替换为对应的缓存包装类,以在Interceptor 中对 body 进行读取
 *
 * @author: Zemel
 * @date: 2020-08-10 15:26
 * @see com.dimpt.rest.config.BodyCachingHttpServletRequestWrapper
 * @see com.dimpt.rest.config.BodyCachingHttpServletResponseWrapper
 */
@Component
@WebFilter(filterName = "BodyCachingWrapperFilter", urlPatterns = "/**")
public class BodyCachingWrapperFilter implements Filter {

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

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        BodyCachingHttpServletRequestWrapper requestWrapper = new BodyCachingHttpServletRequestWrapper((HttpServletRequest) request);
        BodyCachingHttpServletResponseWrapper responseWrapper = new BodyCachingHttpServletResponseWrapper((HttpServletResponse) response);

        // 这里用wrapper类代替,以达到可重复读的目的
        chain.doFilter(requestWrapper, responseWrapper);
    }

    @Override
    public void destroy() {

    }
}

自定义注解使ApiLog可配置(主要思路第3点)

在ApiLogInterceptor中通过getApiLogAnnotation() 方法获取。

/**
 * API接口请求记录配置项
 * <p>获取注解请使用:AnnotationUtils.getAnnotation(BeanOrMethod, ApiLog.class), 否则aliasFor会不生效</p>
 * 拦截器需要在
 *
 * @author Zemel
 * @date: 2021-08-10
 * @see com.dimpt.rest.anno.ApiLogInterceptor
 * @see com.dimpt.rest.config.RequestInterceptorConf
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface ApiLog {

    @AliasFor("log")
    boolean value() default true;

    /**
     * 是否开启记录
     *
     * @return true if necessary
     */
    @AliasFor("value")
    boolean log() default true;

    /**
     * 是否记录请求体,只有当log()为true时生效
     *
     * @return true if necessary
     */
    boolean requestBody() default true;

    /**
     * 是否记录响应体,只有当log()为true时生效
     *
     * @return true if necessary
     */
    boolean responseBody() default true;
}

获取API接口基本信息(主要思路第4点)

  1. 定义记录api基本信息的实体类 ApiInfo,包含apiPath、httpMethod、beanName、beanMethod几个字段。
  2. 编写ApiInfoUtil根据 RequestMappingHandlerMapping 获取所有接口的 ApiInfo 信息。工具类在获取RequestMappingHandlerMapping 实例时用到了 SpringContextUtil 工具类。
public class ApiInfo {

    private String apiPath;
    private String httpMethod;
    private String beanName;
    private String beanMethod;
    // setter 和 getter 省略
}
public class ApiInfoUtil {

    private static Map<RequestMappingInfo, HandlerMethod> listRequestHandlerMapping() {
        RequestMappingHandlerMapping mapping = SpringContextUtil.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        return mapping.getHandlerMethods();
    }

    public static List<ApiInfo> listApiInfo() {
        //获取url与类和方法的对应信息
        Map<RequestMappingInfo, HandlerMethod> map = listRequestHandlerMapping();
        List<ApiInfo> apiInfoList = new ArrayList<>();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : map.entrySet()) {
            RequestMappingInfo info = entry.getKey();
            HandlerMethod handlerMethod = entry.getValue();
            ApiInfo apiInfo = resolve(info, handlerMethod);
            if (apiInfo != null) {
                apiInfoList.add(apiInfo);
            }
        }
        return apiInfoList;
    }

    public static ApiInfo resolve(HandlerMethod handlerMethod) {
        RequestMappingInfo requestMappingInfo = null;
        Map<RequestMappingInfo, HandlerMethod> requestMappingInfoHandlerMethodMap = listRequestHandlerMapping();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : requestMappingInfoHandlerMethodMap.entrySet()) {
            if (entry.getValue().getMethod().toString().equals(handlerMethod.getMethod().toString())) {
                requestMappingInfo = entry.getKey();
                break;
            }
        }
        return resolve(requestMappingInfo, handlerMethod);
    }

    private static ApiInfo resolve(RequestMappingInfo requestMappingInfo, HandlerMethod handlerMethod) {
        if (requestMappingInfo == null || handlerMethod == null) {
            return null;
        }
        Set<String> patterns = requestMappingInfo.getPatternsCondition().getPatterns();
        Set<RequestMethod> requestMethodSet = requestMappingInfo.getMethodsCondition().getMethods();
        ApiInfo apiInfo = null;
        if (patterns.iterator().hasNext() && requestMethodSet.iterator().hasNext()) {
            String apiPath = patterns.iterator().next();
            String httpMethod = requestMethodSet.iterator().next().toString();
            apiInfo = new ApiInfo();
            apiInfo.setApiPath(apiPath);
            apiInfo.setHttpMethod(httpMethod);
            apiInfo.setBeanName(handlerMethod.getBeanType().getName());
            apiInfo.setBeanMethod(handlerMethod.getMethod().getName());
        }
        return apiInfo;
    }
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if(SpringContextUtil.applicationContext == null) {
            SpringContextUtil.applicationContext = applicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    public static Object getBean(String name){
        return getApplicationContext().getBean(name);
    }

    public static <T> T getBean(Class<T> clazz){
        return getApplicationContext().getBean(clazz);
    }

    public static <T> T getBean(String name,Class<T> clazz){
        return getApplicationContext().getBean(name, clazz);
    }

}
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值