背景

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

问题

获取接口返回对象

零碎接口是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计划。
计划如下:

@ControllerAdvicepublic 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;    }@ControllerAdvicepublic 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中。