当前位置: 首页 > 后端技术 > Java

【Tips】spring拦截器获取接口返回值

时间:2023-04-01 19:14:41 Java

背景系统需要上报每一次请求信息,并将数据上报给监控平台。问题获取接口返回的对象系统接口为RestController,返回结果均为@ResponseBody对象。上报数据时,需要解析返回的结果对象,提取对象中的状态码。从response对象中获取返回结果对象,之前是在filter中通过ContentCachingResponseWrapper方式来获取:publicclassAccessLogFilterextendsOncePerRequestFilterimplementsOrdered{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{longbiginTime=System.currentTimeMillis();HttpServletRequesthttpServletRequest=请求;HttpServletResponsehttpServletResponse=响应;如果(!(httpServletRequestinstanceofContentCachingRequestWrapper)){httpServletRequest=newContentCachingRequestWrapper(request);}if(!(httpServletResponseinstanceofContentCachingResponseWrapper)){httpServletResponse=newContentCachingResponseWrapper(response);}try{...//其他操作filterChain.doFilter(httpServletRequest,httpServletResponse);字符串响应正文=invokeHttpByteResponseData((ContentCachingResponseWrapper)httpServletResponse);……//上报等其他操作}finally{((ContentCachingResponseWrapper)httpServletResponse).copyBodyToResponse();AccessLogEntityHolder.remove();}}publicStringinvokeHttpByteResponseData(ContentCachingResponseWrapperresponse){try{Stringcharset=getResponseCharset(response);返回IOUtils.toString(response.getContentAsByteArray(),字符集);}catch(IOExceptione){thrownewRuntimeException("访问日志解析器解析接口返回数据异常!",e);}}protectedStringgetResponseCharset(ContentCachingResponseWrapperresponse){if(response.getContentType()==null){returnStandardCharsets.UTF_8.name();}booleanisStream=response.getContentType().equalsIgnoreCase(MediaType.APPLICATION_OCTET_STREAM;返回isStream&&StringUtils.isNotBlank(response.getCharacterEncoding())?response.getCharacterEncoding():StandardCharsets.UTF_8.name();现在由于担心和引入的第三个插件包中的filter冲突,改为UsingtheInterceptor方法,spring的Interceptor不能像filter一样构造新的请求和响应。这里的解决方案是通过ControllerAdvice获取存储对象,即ControllerAdvice中的beforeBodyWrite方法,在执行时将body临时存储在参数中。这里的存储采用了ThreadLocal的方案。方法如下:@ControllerAdvicepublicclassXXXMetricInterceptorimplementsHandlerInterceptor,Ordered,ResponseBodyAdvice{privatestaticfinalThreadLocalresultBodyThreadLocal=newThreadLocal();@Overridepublicbooleansupports(MethodParameterreturnType,Class>converterType){returntrue;}@OverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){resultBodyThreadLocal.set(body);}返回身体;}@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{returntrue;}@OverridepublicvoidpostHandle(HttpServletRequest请求,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{}@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{}}但是上面的public组件有Interptor{}}的问题允许其他项目扩展,也就是说在@Configuration中配置配置方法:@Bean@ConditionalOnMissingBean(XXXMetricInterceptor.class)publicXXXMetricInterceptorgetXXXMetricInterceptor(){returnnewXXXMetricInterceptor();}而上面的方案是由于@ControllerAdvice注解包含了@Component,无法做@ConditionalOnMissingBean判定,所以改为将@ControllerAdvice部分独立取出,然后在Interceptor里注入:publicclassXXXMetricInterceptorimplementsHandlerInterceptor,Ordered{@AutowiredprivateXXXResponseBodyStorageresponseBodyStorage;}@ControllerAdvicepublicclassXXXResponseBodyStorageimplementsOrdered,ResponseBodyAdvice{privatestaticfinalThreadLocalresultBodyThreadLocalcal=newThreadLocal();@Overridepublicbooleansupports(MethodParameterreturnType,Class>converterType){returnenable;}@OverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaType>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){resultBodyThreadLocal.set(body);返回身体;}publicObjectget(){返回resultBodyThreadLocal.get();}publicvoidremove(Thread.ThreadLocal.get(););}@OverridepublicintgetOrder(){returnOrdered.LOWEST_PRECEDENCE;}}注意:Spring允许存在多个ControllerAdvice对象,实际项目中已经有专门用于转换结果的ControllerAdvice对象获取环境由于环境不同(比如test,prod),数据需要上报到不同的地方,所以需要在初始化的时候判断环境。在这里,您可以在初始化(@PostConstruct)方法中使用applicationContext.getEnvironment()。getActiveProfiles()来判断,但是经过测试发现,如果没有对XXXMetricInterceptor的继承扩展(XXXMetricInterceptor放在public包中,以jar的形式引入),getActiveProfiles方法是可以获取到值的。如果在实际项目中继承扩展XXXMetricInterceptor,那么getActiveProfiles在@PostConstruct方法中返回空。解决方法是调整初始化的时间点,在spring应用可用的时候重新初始化:.getApplicationContext()==null?null:SpringContextUtil.getActiveProfile();……}}数据上报才刚刚开始。数据上博放在拦截器的PostHandler方法中:publicclassXXXMetricInterceptorimplementsApplicationListener,HandlerInterceptor,Ordered{@OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{...//数据上报}}测试发现接口发送异常时不会进入postHandle,然后改成afterCompletion方法中:publicclassXXXMetricInterceptorimplementsApplicationListener,HandlerInterceptor,Ordered{@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{try{...//数据报告}catch(Exceptione){...}finally{responseBodyStorage.remove();}}}ifinterface发生异常时,会先通过@ExceptionHandler处理,然后进入ControllerAdvice链接,然后进入afterCompletion