SpringMVC-ResponseBodyAdvice-元一软件

SpringMVC-ResponseBodyAdvice-元一软件
前言ResponseBodyAdvice接口可以在将handler方法的返回值写入response前对返回值进行处理例如将返回值封装成一个与客户端约定好的对象以便于客户端处理响应数据。本篇文章将学习如果使用ResponseBodyAdvice以及其实现原理。SpringBoot版本2.4.1正文一. ResponseBodyAdvice的使用假如已经存在一个Controller如下所示。RestController public class LoginController { private static final String DATE_STRING 20200620; private static final SimpleDateFormat dateFormat new SimpleDateFormat(yyyyMMdd); private final Student student; public LoginController() { student new Student(); student.setName(Lee); student.setAge(20); student.setSex(male); try { student.setDate(dateFormat.parse(DATE_STRING)); } catch (ParseException e) { System.out.println(e.getMessage()); } } RequestMapping(value /api/v1/student/name, method RequestMethod.GET) public ResponseEntityObject getStudentByName(RequestParam(name name) String name) { if (student.getName().equals(name)) { return new ResponseEntity(student, HttpStatus.OK); } else { return new ResponseEntity(String.format(get student failed by name: %s, name), HttpStatus.BAD_REQUEST); } } RequestMapping(value /api/v1/student/age, method RequestMethod.GET) public Student getStudentByAge(RequestParam(name age) int age) { if (student.getAge() age) { return student; } else { return null; } } } Data public class Student { private String name; private int age; private String sex; private Date date; }上述Controller中有两个方法并且返回值分别为ResponseEntityObject和Student。此时客户端收到响应之后针对响应体的处理变得十分不方便如果增加更多的方法并且返回值都不相同那么客户端将需要根据不同的请求来特定的处理响应体。因此为了方便客户端处理响应数据服务器端专门创建了一个返回结果类ReturnResult并且规定服务器端的所有handler方法执行后往response中写入的响应体都必须为ReturnResult。在这种情况下使用ResponseBodyAdvice可以在不修改已有业务代码的情况下轻松实现上述需求。假设自定义的返回结果类ReturnResult如下所示。Data public class ReturnResultT { private int statusCode; private T body; public ReturnResult() {} public ReturnResult(T body) { this.body body; } }ReturnResult的body就是原本需要写入response的响应内容现在整个ReturnResult为需要写入response的响应内容相当于ReturnResult对handler方法的返回值进行了一层封装。现在创建一个ReturnResultAdvice类并实现ResponseBodyAdvice接口如下所示。ControllerAdvice public class ReturnResultAdvice implements ResponseBodyAdviceObject { Override public boolean supports(Nullable MethodParameter returnType, Nullable Class converterType) { return true; } Override public Object beforeBodyWrite(Object body, Nullable MethodParameter returnType, Nullable MediaType selectedContentType, Nullable Class selectedConverterType, Nullable ServerHttpRequest request, Nullable ServerHttpResponse response) { if (body null) { return null; } if (body instanceof ReturnResult) { return body; } return new ReturnResult(body); } }ReturnResultAdvice的beforeBodyWrite()方法会在handler方法返回值写入response前被调用。下面在单元测试中模拟客户端发起请求。ExtendWith(SpringExtension.class) SpringBootTest(webEnvironment SpringBootTest.WebEnvironment.RANDOM_PORT) ActiveProfiles class LoginControllerTest { private static final ObjectMapper MAPPER new ObjectMapper(); private static final SimpleDateFormat DATE_FORMAT new SimpleDateFormat(yyyyMMdd); Autowired private TestRestTemplate restTemplate; Test void givenName_whenGetStudentByNameAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception { String name Lee; String url /api/v1/student/name?name name; ResponseEntityReturnResult response restTemplate.getForEntity(url, ReturnResult.class); assertThat(response.getBody() ! null, is(true)); ReturnResultStudent returnResult MAPPER.readValue( MAPPER.writeValueAsString(response.getBody()), new TypeReferenceReturnResultStudent() {}); Student student returnResult.getBody(); assertThat(student ! null, is(true)); assertThat(student.getName(), is(Lee)); assertThat(student.getAge(), is(20)); assertThat(student.getSex(), is(male)); assertThat(student.getDate(), is(DATE_FORMAT.parse(20200620))); } Test void givenAge_whenGetStudentByAgeAndStudentConvertedToReturnResultByResponseBodyAdvice_thenGetStudentSuccess() throws Exception { int age 20; String url /api/v1/student/age?age age; ResponseEntityReturnResult response restTemplate.getForEntity(url, ReturnResult.class); assertThat(response.getBody() ! null, is(true)); ReturnResultStudent returnResult MAPPER.readValue( MAPPER.writeValueAsString(response.getBody()), new TypeReferenceReturnResultStudent() {}); Student student returnResult.getBody(); assertThat(student ! null, is(true)); assertThat(student.getName(), is(Lee)); assertThat(student.getAge(), is(20)); assertThat(student.getSex(), is(male)); assertThat(student.getDate(), is(DATE_FORMAT.parse(20200620))); } }运行测试程序断言全部通过。最后对整个例子进行两点说明。虽然LoginController的getStudentByName()方法的返回值类型为ResponseEntityObject但是实际往response写的响应体内容为ResponseEntity中的body例子中这个body对应的就是创建ResponseEntity对象时传入的student。因此ReturnResultAdvice处理getStudentByName()方法的返回值时处理的实际上还是Student对象。至于原理将在第二小节进行说明在单元测试程序中MAPPER.readValue(MAPPER.writeValueAsString(...), ...)这样的用法是因为使用restTemplate从response中将响应内容反序列化为带有泛型参数的对象时会将对象中的泛型的内容反序列化为LinkHashMap因此借助ObjectMapper和TypeReference来直接获取到Student对象。小节由ControllerAdvice注解修饰并实现ResponseBodyAdvice接口的类所实现的beforeBodyWrite()方法会在handler方法返回值写入response前被调用并且handler方法返回值会作为入参传入beforeBodyWrite()从而可以在返回值写入response前对返回值进行一些定制操作例如对返回值进行一层封装。二. ResponseBodyAdvice的原理首先说明一下为什么第一小节中LoginController的getStudentByName()方法的返回值类型为ResponseEntityObject但是实际往response写的响应体内容为ResponseEntity中的body。首先所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessor的writeWithMessageConverters()中这个方法的声明如下所示。protected T void writeWithMessageConverters(Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException其中value就是需要写入响应体的值同时也是ResponseBodyAdvice要处理的值。然后如果handler方法的返回值是非ResponseEntity对象且handler方法由ResponseBody注解修饰那么writeWithMessageConverters()的调用发生在RequestResponseBodyMethodProcessor#handleReturnValue()中如果handler方法的返回值是ResponseEntity对象那么writeWithMessageConverters()的调用发生在HttpEntityMethodProcessor#handleReturnValue()中分别看一下在这两个方法中调用writeWithMessageConverters()时传入的参数就可以解释之前的疑问了。RequestResponseBodyMethodProcessor#handleReturnValue()public void handleReturnValue(Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { ... writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); }HttpEntityMethodProcessor#handleReturnValue()public void handleReturnValue(Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { ...... HttpEntity? responseEntity (HttpEntity?) returnValue; ...... writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); ...... }现在开始对ResponseBodyAdvice的原理进行分析。已知所有ResponseBodyAdvice接口的调用是发生在AbstractMessageConverterMethodProcessor的writeWithMessageConverters()中其部分源码如下所示。protected T void writeWithMessageConverters(Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { ...... if (selectedMediaType ! null) { selectedMediaType selectedMediaType.removeQualityValue(); for (HttpMessageConverter? converter : this.messageConverters) { GenericHttpMessageConverter genericConverter (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter?) converter : null); if (genericConverter ! null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { // ResponseBodyAdvice的调用发生在这里 body getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class? extends HttpMessageConverter?) converter.getClass(), inputMessage, outputMessage); if (body ! null) { Object theBody body; LogFormatUtils.traceDebug(logger, traceOn - Writing [ LogFormatUtils.formatValue(theBody, !traceOn) ]); addContentDispositionHeader(inputMessage, outputMessage); if (genericConverter ! null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug(Nothing to write: null body); } } return; } } } ...... }AbstractMessageConverterMethodProcessor的getAdvice()方法会返回其在构造函数中加载好的RequestResponseBodyAdviceChain对象下面看一下RequestResponseBodyAdviceChain的beforeBodyWrite()方法。public Object beforeBodyWrite(Nullable Object body, MethodParameter returnType, MediaType contentType, Class? extends HttpMessageConverter? converterType, ServerHttpRequest request, ServerHttpResponse response) { return processBody(body, returnType, contentType, converterType, request, response); } private T Object processBody(Nullable Object body, MethodParameter returnType, MediaType contentType, Class? extends HttpMessageConverter? converterType, ServerHttpRequest request, ServerHttpResponse response) { // 从加载好的ResponseBodyAdvice中获取适用于当前handler的ResponseBodyAdvice for (ResponseBodyAdvice? advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { if (advice.supports(returnType, converterType)) { // 执行ResponseBodyAdvice的beforeBodyWrite()方法以处理handler方法返回值 body ((ResponseBodyAdviceT) advice).beforeBodyWrite((T) body, returnType, contentType, converterType, request, response); } } return body; } private A ListA getMatchingAdvice(MethodParameter parameter, Class? extends A adviceType) { // 获取ResponseBodyAdvice集合 ListObject availableAdvice getAdvice(adviceType); if (CollectionUtils.isEmpty(availableAdvice)) { return Collections.emptyList(); } ListA result new ArrayList(availableAdvice.size()); for (Object advice : availableAdvice) { // 判断ResponseBodyAdvice是否由ControllerAdvice注解修饰 if (advice instanceof ControllerAdviceBean) { ControllerAdviceBean adviceBean (ControllerAdviceBean) advice; // 判断ResponseBodyAdvice是否适用于当前handler if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { continue; } advice adviceBean.resolveBean(); } if (adviceType.isAssignableFrom(advice.getClass())) { result.add((A) advice); } } return result; }在RequestResponseBodyAdviceChain中beforeBodyWrite()方法调用了processBody()方法processBody()方法会遍历所有加载好并且适用于当前handler的ResponseBodyAdvice并执行至此所有由ControllerAdvice注解修饰的ResponseBodyAdvice接口会在这里执行。小节由ControllerAdvice注解修饰的ResponseBodyAdvice接口会被SpringMVC框架加载到RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor这两个返回值处理器中当这两个返回值处理器将返回值写入response前适用于当前handler的ResponseBodyAdvice接口会被调用从而可以完成对返回值的定制化改造。三. ResponseBodyAdvice的加载由第二小节可知正是因为RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor这两个返回值处理器会将由ControllerAdvice注解修饰的ResponseBodyAdvice接口加载才能够实现将返回值写入response前调用这些ResponseBodyAdvice接口对返回值进行一些操作。那么本小节将对ResponseBodyAdvice接口的加载进行学习。首先给出结论ResponseBodyAdvice的加载发生在RequestMappingHandlerAdapter的afterPropertiesSet()方法中。已知RequestMappingHandlerAdapter实现了InitializingBean接口因此RequestMappingHandlerAdapter实现了afterPropertiesSet()方法。该方法实现如下。public void afterPropertiesSet() { // 加载ControllerAdviceBean相关内容同时就会将由ControllerAdvice注解修饰的ResponseBodyAdvice接口加载 initControllerAdviceCache(); if (this.argumentResolvers null) { ListHandlerMethodArgumentResolver resolvers getDefaultArgumentResolvers(); this.argumentResolvers new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.initBinderArgumentResolvers null) { ListHandlerMethodArgumentResolver resolvers getDefaultInitBinderArgumentResolvers(); this.initBinderArgumentResolvers new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } if (this.returnValueHandlers null) { // 获取返回值处理器在这里就会完成RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor的初始化初始化的同时就会完成ResponseBodyAdvice接口的加载 ListHandlerMethodReturnValueHandler handlers getDefaultReturnValueHandlers(); this.returnValueHandlers new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } }上述实现中initControllerAdviceCache()会加载ControllerAdviceBean相关内容到RequestMappingHandlerAdapter中这其中就包含由ControllerAdvice注解修饰的ResponseBodyAdvice接口。然后在getDefaultReturnValueHandlers()方法中会创建返回值处理器在创建RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor时会使用加载好的ResponseBodyAdvice接口完成这两个返回值处理器的初始化。上述两个方法的部分源码如下所示。initControllerAdviceCache()private void initControllerAdviceCache() { if (getApplicationContext() null) { return; } // 获取由ControllerAdvice注解修饰的bean ListControllerAdviceBean adviceBeans ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); ListObject requestResponseBodyAdviceBeans new ArrayList(); for (ControllerAdviceBean adviceBean : adviceBeans) { Class? beanType adviceBean.getBeanType(); if (beanType null) { throw new IllegalStateException(Unresolvable type for ControllerAdviceBean: adviceBean); } SetMethod attrMethods MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { this.modelAttributeAdviceCache.put(adviceBean, attrMethods); } SetMethod binderMethods MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS); if (!binderMethods.isEmpty()) { this.initBinderAdviceCache.put(adviceBean, binderMethods); } // 如果ControllerAdviceBean实现了ResponseBodyAdvice接口那么这个ControllerAdviceBean需要加载到requestResponseBodyAdvice中 if (RequestBodyAdvice.class.isAssignableFrom(beanType) || ResponseBodyAdvice.class.isAssignableFrom(beanType)) { requestResponseBodyAdviceBeans.add(adviceBean); } } if (!requestResponseBodyAdviceBeans.isEmpty()) { this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans); } ...... }getDefaultReturnValueHandlers()private ListHandlerMethodReturnValueHandler getDefaultReturnValueHandlers() { ListHandlerMethodReturnValueHandler handlers new ArrayList(20); ... // 创建并加载HttpEntityMethodProcessor handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); ... // 创建并加载RequestResponseBodyMethodProcessor handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); ... return handlers; }根据getDefaultReturnValueHandlers()方法可知在创建HttpEntityMethodProcessor或者RequestResponseBodyMethodProcessor时会将RequestMappingHandlerAdapter加载好的ResponseBodyAdvice传入构造函数并且无论是HttpEntityMethodProcessor还是RequestResponseBodyMethodProcessor其构造函数最终都会调用到父类AbstractMessageConverterMethodArgumentResolver的构造函数并在其中初始化一个RequestResponseBodyAdviceChain以完成ResponseBodyAdvice的加载。构造函数源码如下所示。HttpEntityMethodProcessor#HttpEntityMethodProcessor()public HttpEntityMethodProcessor(ListHttpMessageConverter? converters, Nullable ContentNegotiationManager manager, ListObject requestResponseBodyAdvice) { super(converters, manager, requestResponseBodyAdvice); }AbstractMessageConverterMethodProcessor#AbstractMessageConverterMethodProcessor()protected AbstractMessageConverterMethodProcessor(ListHttpMessageConverter? converters, Nullable ContentNegotiationManager manager, Nullable ListObject requestResponseBodyAdvice) { super(converters, requestResponseBodyAdvice); this.contentNegotiationManager (manager ! null ? manager : new ContentNegotiationManager()); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(SAFE_EXTENSIONS); }AbstractMessageConverterMethodArgumentResolver#AbstractMessageConverterMethodArgumentResolver()public AbstractMessageConverterMethodArgumentResolver(ListHttpMessageConverter? converters, Nullable ListObject requestResponseBodyAdvice) { Assert.notEmpty(converters, messageConverters must not be empty); this.messageConverters converters; this.allSupportedMediaTypes getAllSupportedMediaTypes(converters); this.advice new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); }小节RequestMappingHandlerAdapter会在其实现的afterPropertiesSet()方法中加载由ControllerAdvice注解修饰的ResponseBodyAdvice接口然后会创建并加载返回值处理器在创建RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor这两个返回值处理器时会传入加载好的ResponseBodyAdvice从而完成了ResponseBodyAdvice的加载。