共计 5588 个字符,预计需要花费 14 分钟才能阅读完成。
背景
零碎须要上报每次的申请信息,并上报数据给监控平台。
问题
获取接口返回对象
零碎接口是 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
计划。
计划如下:
@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
对象。
获取环境
因为不同的环境(如 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
中。