背景
系統需要上報每次的請求資訊,並上報資料給監控平臺。
問題
獲取介面返回物件
系統介面是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
中。