关于java:小知识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<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对象。

获取环境

因为不同的环境(如testprod),要上报数据到不同的中央,所以在初始化时,须要对环境做判断,这里能够通过初始化(@PostConstruct)办法里,取applicationContext.getEnvironment().getActiveProfiles()来判断,然而通过测试发现:

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

【腾讯云】云产品限时秒杀,爆款1核2G云服务器,首年50元

阿里云限时活动-2核2G-5M带宽-60G SSD-1000G月流量 ,特惠价99元/年(原价1234.2元/年,可以直接买3年),速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据