乐趣区

关于java:小知识spring拦截器获取到接口信息并上报

背景

零碎须要上报每次的申请信息,并上报数据给监控平台。

问题

获取接口返回对象

零碎接口是 RestController,返回的后果都是@ResponseBody 对象。上报数据时,须要解析返回后果对象,提取对象中的 状态码 。从response 对象中获取返回后果对象,之前是在 filter 中通过 ContentCachingResponseWrapper 形式来获取:

public class AccessLogFilter extends OncePerRequestFilter implements Ordered {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {long biginTime = System.currentTimeMillis();
        HttpServletRequest httpServletRequest = request;
        HttpServletResponse httpServletResponse = response;
        if (!(httpServletRequest instanceof ContentCachingRequestWrapper)) {httpServletRequest = new ContentCachingRequestWrapper(request);
        }
        if (!(httpServletResponse instanceof ContentCachingResponseWrapper)) {httpServletResponse = new ContentCachingResponseWrapper(response);
        }

        try {
            …… // 其余操作
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            String responseBody = invokeHttpByteResponseData((ContentCachingResponseWrapper) httpServletResponse);
            …… // 上报等其余操作
        } finally {((ContentCachingResponseWrapper) httpServletResponse).copyBodyToResponse();
            AccessLogEntityHolder.remove();}
    }   
        
    public String invokeHttpByteResponseData(ContentCachingResponseWrapper response) {
        try {String charset = getResponseCharset(response);
            return IOUtils.toString(response.getContentAsByteArray(), charset);
        } catch (IOException e) {throw new RuntimeException("拜访日志解析器解析接口返回数据异样!", e);
        }
    }


    protected String getResponseCharset(ContentCachingResponseWrapper response) {if (response.getContentType() == null) {return StandardCharsets.UTF_8.name();
        }
        boolean isStream = response.getContentType()
                .equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        return isStream && StringUtils.isNotBlank(response.getCharacterEncoding()) ? response.getCharacterEncoding()
                : StandardCharsets.UTF_8.name();}    
    
}

当初因为放心跟引入的一个第三次插件包里的 filter 有抵触,改为应用 Interceptor 形式。而 spring 的 Interceptor 无奈像 filter 那样构建新的 requestresponse
这里的解决方案是,通过 ControllerAdvice 来获取存储对象,即 ControllerAdvice 里的 beforeBodyWrite 办法,在执行时,将参数里的 body 临时存储起来,这里的存储,采纳了 ThreadLocal 计划。
计划如下:

@ControllerAdvice
public classXXXMetricInterceptor implements HandlerInterceptor, Ordered, ResponseBodyAdvice<Object> {private static final ThreadLocal<Object> resultBodyThreadLocal = new ThreadLocal();

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

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {resultBodyThreadLocal.set(body);
        return body;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {return true;}

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

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {}}      

然而以上有个问题,开发的 Interceptor 是个公共组件,容许其余我的项目扩大,也就是说,到时候配置形式是在 @Configuration 里配置:

@Bean
@ConditionalOnMissingBean(XXXMetricInterceptor.class)
public XXXMetricInterceptor getXXXMetricInterceptor() {return newXXXMetricInterceptor();
}

而以上计划因为 @ControllerAdvice 注解里蕴含了 @Component,无奈做@ConditionalOnMissingBean 判断,所以改为将 @ControllerAdvice 局部独立取出,而后在 Interceptor 里注入:

public class XXXMetricInterceptor implements HandlerInterceptor, Ordered {

    @Autowired
    private XXXResponseBodyStorage responseBodyStorage;
    
}

@ControllerAdvice
public class XXXResponseBodyStorage implements Ordered, ResponseBodyAdvice<Object> {private static final ThreadLocal<Object> resultBodyThreadLocal = new ThreadLocal();

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

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
            ServerHttpResponse response) {resultBodyThreadLocal.set(body);
        return body;
    }

    public Object get() {return resultBodyThreadLocal.get();
    }

    public void remove() {resultBodyThreadLocal.remove();
    }

    @Override
    public int getOrder() {return Ordered.LOWEST_PRECEDENCE;}
}

注:spring容许多个 ControllerAdvice 对象存在,理论我的项目中曾经存在专门对后果做转换的 ControllerAdvice 对象。

获取环境

因为不同的环境(如 testprod),要上报数据到不同的中央,所以在初始化时,须要对环境做判断,这里能够通过初始化(@PostConstruct)办法里,取applicationContext.getEnvironment().getActiveProfiles() 来判断,然而通过测试发现:

  1. 如果没有对 XXXMetricInterceptor 做继承扩大的话(XXXMetricInterceptor放在公共包里,以 jar 的形式被引入 ),getActiveProfiles 办法能取到值。
  2. 如果在理论我的项目中对 XXXMetricInterceptor 做了继承扩大,那么 @PostConstruct 办法里 getActiveProfiles 返回的是空。

解决方案是调整初始化的工夫点,改为在 spring 的 application 可用时再初始化:

public class XXXMetricInterceptor implements ApplicationListener<ApplicationReadyEvent>, HandlerInterceptor, Ordered {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {String[] activeProfiles =
                SpringContextUtil.getApplicationContext() == null ? null : SpringContextUtil.getActiveProfile();
        ……
    }
    
}

数据上报

刚开始数据尚博啊是放在拦截器的 PostHandler 办法里:

public class XXXMetricInterceptor implements ApplicationListener<ApplicationReadyEvent>, HandlerInterceptor, Ordered {

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {…… // 数据上报}
}

测试发现,当接口发送异样时,并不会进入到 postHandle,之后改为在afterCompletion 办法里:

public class XXXMetricInterceptor implements ApplicationListener<ApplicationReadyEvent>, HandlerInterceptor, Ordered {

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        try {…… // 数据上报} catch (Exception e) {……} finally {responseBodyStorage.remove();
        }
    }
    
}  

如果接口产生异样,会先通过 @ExceptionHandler 的解决,之后进入 ControllerAdvice 环节,再之后进入到 afterCompletion 中。

退出移动版