背景
零碎须要上报每次的申请信息,并上报数据给监控平台。
问题
获取接口返回对象
零碎接口是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
那样构建新的request
和response
。
这里的解决方案是,通过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
对象。
获取环境
因为不同的环境(如test
,prod
),要上报数据到不同的中央,所以在初始化时,须要对环境做判断,这里能够通过初始化(@PostConstruct)办法里,取applicationContext.getEnvironment().getActiveProfiles()
来判断,然而通过测试发现:
- 如果没有对
XXXMetricInterceptor
做继承扩大的话(XXXMetricInterceptor
放在公共包里,以jar
的形式被引入),getActiveProfiles
办法能取到值。 - 如果在理论我的项目中对
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
中。