深入理解响应包装器

Jan 9 · 13 min read · Hardcore

最初的问题来源于,当服务端发展到一定规模,我希望能够将各式各样的业务请求用一种统一的格式约束起来,一方面是请求总会携带一些通用信息代码,例如表示请求成功与否,没成功的话原因是什么;另一方面也方便提供 API 接口,让前端统一解析处理。

{
  "code": "SUCCESS",
  "timestamp": 1704729600000,
  "data":
    {
      "id": 123,
      "name": "Criheacy",
      "role": "admin"
    }
}

例如这里,我的 Controller 层的 Handler 方法只返回了{"id":123,"name":"Criheacy","role":"admin"} 这样一个对象,至于 codetimestamp 作为每个接口通用的字段会由框架自动拼上去。

注意:这篇文章中会涉及到大量的代码片段,需要有一定 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 接触的,不妨先从这里下手。

这个方法是在什么时候执行的呢?通过逐级排查调用链,它们的调用关系如图所示:

structure

这其中,所有实现了 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);
        }
    }
}
CC BY-NC-SA 4.0 2022 - PRESENT © Criheacy

发表一条评论吧!

这条评论不会展示在页面上,只有作者能够收到