【小知识】spring拦截器获取到接口信息并上报

背景

系统需要上报每次的请求信息,并上报数据给监控平台。

问题

获取接口返回对象

系统接口是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那样构建新的requestresponse
这里的解决方案是,通过ControllerAdvice来获取存储对象,即ControllerAdvice里的beforeBodyWrite方法,在执行时,将参数里的body暂时存储起来,这里的存储,采用了ThreadLocal方案。
方案如下:

@ControllerAdvice
public classXXXMetricInterceptor implements HandlerInterceptor, Ordered, ResponseBodyAdvice {

    private static final ThreadLocal resultBodyThreadLocal = new ThreadLocal();

    @Override
    public boolean supports(MethodParameter returnType, Class> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class> 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 {

    private static final ThreadLocal resultBodyThreadLocal = new ThreadLocal();

    @Override
    public boolean supports(MethodParameter returnType, Class> converterType) {
        return enable;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
            Class> 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对象。

获取环境

由于不同的环境(如testprod),要上报数据到不同的地方,所以在初始化时,需要对环境做判断,这里可以通过初始化(@PostConstruct)方法里,取applicationContext.getEnvironment().getActiveProfiles()来判断,但是经过测试发现:

  1. 如果没有对XXXMetricInterceptor做继承扩展的话(XXXMetricInterceptor放在公共包里,以jar的方式被引入),getActiveProfiles方法能取到值。
  2. 如果在实际项目中对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中。

你可能感兴趣的:(javaspring)