深入理解响应包装器
最初的问题来源于,当服务端发展到一定规模,我希望能够将各式各样的业务请求用一种统一的格式约束起来,一方面是请求总会携带一些通用信息代码,例如表示请求成功与否,没成功的话原因是什么;另一方面也方便提供 API 接口,让前端统一解析处理。
{
"code": "SUCCESS",
"timestamp": 1704729600000,
"data":
{
"id": 123,
"name": "Criheacy",
"role": "admin"
}
}
例如这里,我的 Controller 层的 Handler 方法只返回了{"id":123,"name":"Criheacy","role":"admin"}
这样一个对象,至于 code
和 timestamp
作为每个接口通用的字段会由框架自动拼上去。
注意:这篇文章中会涉及到大量的代码片段,需要有一定 Java 语言和 Spring 框架的基础知识。我尝试尽量用易于理解的语言解释,但前提是不能丢掉关键信息,从而在以后查找的时候能快速定位到代码片段,进一步加深理解。本文中使用的 Spring Boot 版本为 3.2.1。
横生枝节
这本来是一个简单的需求,使用 Spring 这样一个 AOP 框架应该很好实现。我很快查到了 HandlerMethodReturnValueHandler
这个接口,它这个绕口令一样的名字应该叫作 「handler of the return value of handler method」,即「用于处理响应方法返回值的处理器」(我已经尝试把两个 handler 翻译成不同的词语了,然而还是很绕)。其中 「handler method」就是在 Controller 层写的各种函数,通过 @RequestMapping
映射到一个具体的 URL 地址,响应该地址下的 API 请求。而这个「处理响应方法的方法」就是在更上一层,在响应方法周围做一些装饰。
实现这个接口需要实现两个方法,分别用来「决定是否需要对返回值进行操作」,以及「具体的操作流程」。结合上面的业务场景,我写了如下的框架:
@Slf4j
public class ResponseModelHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return true;
}
@Override
public void handleReturnValue(Object returnValue,
MethodParameter returnType,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest) {
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
if (response == null) {
return;
}
ResponseModel<?> responseModel = buildResponseModel(returnValue);
response.setStatus(responseModel.getCode().getHttpStatus());
try {
serializeModelToResponse(response, responseModel);
} catch (IOException e) {
return;
}
mavContainer.setRequestHandled(true);
}
}
这个类需要在 @Configuration
类中注册:
@Slf4j
@Configuration
public class ServiceConfig implements WebMvcConfigurer {
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(new ResponseModelHandler());
}
}
然而运行后发现这种方式没用,handleReturnValue
方法并未被执行。通过排查,甚至 supportsReturnType
方法也未被调用过,也就是说 Spring 根本没判断这里的处理是否需要执行。
顺藤摸瓜
我们是通过 WebMvcConfigurer
提供的 addReturnValueHandlers
方法与 Spring 接触的,不妨先从这里下手。
这个方法是在什么时候执行的呢?通过逐级排查调用链,它们的调用关系如图所示:
这其中,所有实现了 WebMvcConfigurer
接口的子类都会被加入 WebMvcConfigurerComposite
(这个类同样也实现了 WebMvcConfigurer
,类似组合模式),而 WebMvcConfigurerComposite
会被 WebMvcConfigurationSupport
的子类 DelegatingWebMvcConfiguration
这个 @Configuration
类调用, WebMvcConfigurationSupport
又会在 RequestMappingHandlerAdapter
初始化的时候获取所有的 returnValueHandler
用于构建这个 Bean。
RequestMappingHandlerAdapter
这个 Bean 在处理请求过程中发挥了重大作用,怎么由 URL 匹配对应的 handler 方法、请求前中后的 interceptor、响应体应该使用哪种格式,都要由它决定:
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(contentNegotiationManager);
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = getAsyncSupportConfigurer();
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
其中 adapter.setCustomReturnValueHandlers(getReturnValueHandlers())
这一句读取了 WebMvcConfigurationSupport
里的 handlers,将其加入到了自己的 customReturnValueHandlers
列表。这个列表什么时候被读取呢?当 RequestMappingHandlerAdapter
完成构建后,Bean 生命周期上的 afterProptertiesSet
方法会被触发:
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
initMessageConverters();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
if (BEAN_VALIDATION_PRESENT) {
List<HandlerMethodArgumentResolver> resolvers = this.argumentResolvers.getResolvers();
this.methodValidator = HandlerMethodValidator.from(
this.webBindingInitializer, this.parameterNameDiscoverer,
methodParamPredicate(resolvers, ModelAttributeMethodProcessor.class),
methodParamPredicate(resolvers, RequestParamMethodArgumentResolver.class));
}
}
其中 returnValueHandlers
的初始化期间,调用了 getDefaultReturnValueHandlers
方法:
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
// Single-purpose return value types
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());
handlers.add(new MapMethodProcessor());
// Custom return value types
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}
// Catch-all
if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ServletModelAttributeMethodProcessor(true));
}
return handlers;
}
getCustomReturnValueHandlers
方法就会获取到之前在 Bean 构建时写入的 customReturnValueHandlers
,将其添加到真正的 handlers 中。只不过,在它之前还排着一堆 handlers,自定义的处理器被安排在所有处理器列表的最后面。还记得 handler 的 ModelAndViewContainer
参数有个叫 requestHandled
的方法吗?会不会是在它之前的 handlers 抢先处理了这次请求,并把 requestHandled
状态设置为 true,导致我们自己的 handler 被跳过了?
我们一个一个排查了这些 handler,终于发现问题出在最后一个 RequestResponseBodyMethodProcessor
上,它的写法是这样的:
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
// other methods are ignored
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
if (returnValue instanceof ProblemDetail detail) {
outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));
if (detail.getInstance() == null) {
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
detail.setInstance(path);
}
}
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
原来在这个处理器的开头就直接一句 mavContainer.setRequestHandled(true)
,它上层的 HandlerMethodReturnValueHandlerComposite
看到这次请求已经被处理,就不再交由后面的 handler 执行,因此我们自定义的拦截器连检查 supportsReturnType
的机会都没有,直接就被屏蔽掉了。
到此为止,问题已经真相大白。用一句话来说就是,Spring 为我们设定好的处理器抢先处理了请求,我们自定义的处理器被排在后面,没轮到我们去处理就结束了。
水落石出
那这么说,Spring Boot 让我们添加自定义 handler 的意义是什么呢?什么情况下请求不会被 RequestResponseBodyMethodProcessor
拦住呢?让我们检查一下它 supportsReturnType
方法的实现:
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
RequestResponseBodyMethodProcessor
的生效范围是对所有添加了 @ResponseBody
注解的才能生效。也就是说,只要方法上带了 @ResponseBody
注解,或者方法所处的 Controller
类上带了这个注解,那么整个方法就会被 RequestResponseBodyMethodProcessor
拦住。你可能会问,我平时写的方法和类也不会主动去加 @ResponseBody
呀,为什么我的请求还是会被拦住?检查一下你的类,你可能使用了 @RestController
,而 @RestController
比起 @Controller
的区别就在于多了一个 @ResponseBody
注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
寻寻觅觅
我在网上搜到了各种答案,既然上面说的整个 handler 的扫描和构建过程都是在 RequestMappingHandlerAdapter
这个 Bean
的初始化阶段完成的,我可以利用 Spring 提供的 InitializingBean#afterPropertiesSet
这个方法来进行一些后置处理,打破 getDefaultReturnValueHandlers
方法预设的规则,将我自己的 Handler 强行插入在 RequestResponseBodyMethodProcessor
的前面:
@Configuration
@RequiredArgsConstructor
public class RequestMappingHandlerAdapterPostConfigurer implements InitializingBean {
private final RequestMappingHandlerAdapter adapter;
private final CustomHandler customHandler;
@Override
public void afterPropertiesSet() {
List<HandlerMethodReturnValueHandler> handlerList = adapter.getReturnValueHandlers();
if (handlerList == null) {
return;
}
List<HandlerMethodReturnValueHandler> newHandlerList = new ArrayList<>();
for (HandlerMethodReturnValueHandler handler : handlerList) {
if (handler instanceof RequestResponseBodyMethodProcessor) {
newHandlerList.add(customHandler);
}
newHandlerList.add(handler);
}
adapter.setReturnValueHandlers(newHandlerList);
}
}
还有其它的许多答案都大同小异,包括但不限于 在外面套一层装饰器、将自己的 handler 类作为子类 等等,但我始终觉得这种「强行插队」的做法不太优雅,Spring Boot 在设计之初这种情况不可能没有考虑到,还得让你手动打破规则来实现自己的业务逻辑,这其中肯定还有别的解决方案。
斩草除根
其实这个默认的处理器 RequestResponseBodyMethodProcessor
就已经提供了自定义的方法。如果你查看它对于 handleReturnValue
的实现,你会发现它最后一句话用使用了 writeWithMessageConverters
:
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
if (returnValue instanceof ProblemDetail detail) {
outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));
if (detail.getInstance() == null) {
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
detail.setInstance(path);
}
}
// Try even with null return value. ResponseBodyAdvice could get involved.
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
writeWithMessageConverters
是一个较长(写得也较为丑陋)的方法,前面的大部分篇幅都是决定消息的 MediaType
类型(即响应头中的 Content-Type
字段)的,再往下你会发现有这样一句话:
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
这里的 getAdvice
会获取到一个 RequestResponseBodyAdviceChain
,看名字就知道这是一个 Advice
链:在 RequestMappingHandlerAdapter
初始化完成之后,initControllerAdviceCache
方法就会扫描所有带有 @ControllerAdvice
注解的 Bean
,这些 ControllerAdvice
可以通过 @Order
注解或者实现 Ordered
接口指定执行顺序,继而被 OrderComparator
中读取并按顺序组装成链缓存到 requestResponseBodyAdvice
中。
在请求到来时, requestResponseBodyAdvice
就会被取出,依次执行所有实现了 ResponseBodyAdvice
的接口的类。ResponseBodyAdvice
接口提供了 beforeBodyWrite
方法,它会将 handler 方法的返回作为 body
参数传入第一个 advice,处理之后的返回值再作为 body
传入第二个 advice,以此类推。
@Nullable
private <T> Object processBody(@Nullable Object body, MethodParameter returnType, MediaType contentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request, ServerHttpResponse response) {
for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
if (advice.supports(returnType, converterType)) {
body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
如此一来,经过深入分析事情就简单多了,ResponseBodyAdvice
刚好能满足我们的需求。这里是一个简易版本的实现,如果要支持更多类型,可以多写几个 ResponseBodyAdvice
,通过在 supports
方法中来控制启用与否。
@ControllerAdvice
public class ResponseModelWrapper implements ResponseBodyAdvice<Object> {
@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) {
ResponseModel<?> responseModel = buildResponseModel(body);
response.setStatusCode(HttpStatusCode.valueOf(responseModel.getCode().getHttpStatus()));
return responseModel;
}
@Nonnull
private static ResponseModel<?> buildResponseModel(Object returnValue) {
if (returnValue instanceof ResponseModel<?>) {
return (ResponseModel<?>) returnValue;
} else {
return new ResponseModel<>(returnValue);
}
}
}