背景
零碎须要上报每次的申请信息,并上报数据给监控平台。
问题
获取接口返回对象
零碎接口是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
中。