背景
系统需要上报每次的请求信息,并上报数据给监控平台。
问题
获取接口返回对象
系统接口是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
但是以上有个问题,开发的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 {
private static final ThreadLocal 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, HandlerInterceptor, Ordered {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
String[] activeProfiles =
SpringContextUtil.getApplicationContext() == null ? null : SpringContextUtil.getActiveProfile();
……
}
}
数据上报
刚开始数据尚博啊是放在拦截器的PostHandler
方法里:
public class XXXMetricInterceptor implements ApplicationListener, HandlerInterceptor, Ordered {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
…… // 数据上报
}
}
测试发现,当接口发送异常时,并不会进入到postHandle
,之后改为在afterCompletion
方法里:
public class XXXMetricInterceptor implements ApplicationListener, 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
中。