background
The system needs to report each request information and report the data to the monitoring platform.
question
Get interface return object
The system interface is RestController
, and the returned results are all @ResponseBody
objects. When reporting data, you need to parse the returned result object and extract 状态码
in the object. Obtain the returned result object from the response
object, which was previously obtained in the filter
method through the ContentCachingResponseWrapper
method:
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();
}
}
Now, because of fear of conflict with the filter
in a third plug-in package introduced, the Interceptor
method is used instead. And spring's Interceptor
can't build new request
and response
filter
这里的解决方案是, ControllerAdvice
来获取存储对象, ControllerAdvice
里的beforeBodyWrite
方法,在执行时,将参数里的body
Stored temporarily, the storage here adopts the ThreadLocal
scheme.
The plan is as follows:
@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 {
}
}
But there is a problem with the above. The developed Interceptor
is a public component that allows other projects to expand. That is to say, the configuration method will be configured in @Configuration
:
@Bean
@ConditionalOnMissingBean(XXXMetricInterceptor.class)
public XXXMetricInterceptor getXXXMetricInterceptor() {
return newXXXMetricInterceptor();
}
The above scheme cannot do @ConditionalOnMissingBean
@ControllerAdvice
because the annotation contains @Component
, so the @ControllerAdvice
is taken out independently, and then Inject in 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;
}
}
Note: spring
allows multiple ControllerAdvice
objects exist, and the actual project already exists a ControllerAdvice
object that converts the result.
get environment
Due to different environments (such as test
, prod
), data needs to be reported to different places, so during initialization, it is necessary to judge the environment, which can be done by initialization (@PostConstruct) In the method, take applicationContext.getEnvironment().getActiveProfiles()
to judge, but after testing, it is found that:
- If there is no right
XXXMetricInterceptor
to do inheritance extension (XXXMetricInterceptor
is placed in the public package, and is introduced in the wayjar
---cfdcbba954c35b9ccff695ea92272e2a---),getActiveProfiles
method value can be obtained. - If an inheritance extension is made to
XXXMetricInterceptor
in the actual project, then the@PostConstruct
methodgetActiveProfiles
returns empty.
The solution is to adjust the time of initialization and change it to reinitialize when spring's application
is available:
public class XXXMetricInterceptor implements ApplicationListener<ApplicationReadyEvent>, HandlerInterceptor, Ordered {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
String[] activeProfiles =
SpringContextUtil.getApplicationContext() == null ? null : SpringContextUtil.getActiveProfile();
……
}
}
data reporting
At the beginning, the data Shangbo was placed in the interceptor PostHandler
method:
public class XXXMetricInterceptor implements ApplicationListener<ApplicationReadyEvent>, HandlerInterceptor, Ordered {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
…… // 数据上报
}
}
The test found that when the interface sends an exception, it will not enter the postHandle
, and then change it to the afterCompletion
method:
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();
}
}
}
If an exception occurs on the interface, it will first go through the processing of @ExceptionHandler
8cef41dff082fed1961abe1873eb062c---, then enter the ControllerAdvice
link, and then enter the afterCompletion
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。