前言
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 ResponseEntity<Object> 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
中有两个方法,并且返回值分别为ResponseEntity<Object>
和Student
。此时客户端收到响应之后,针对响应体的处理变得十分不方便,如果增加更多的方法,并且返回值都不相同,那么客户端将需要根据不同的请求来特定的处理响应体。因此为了方便客户端处理响应数据,服务器端专门创建了一个返回结果类ReturnResult
,并且规定服务器端的所有handler方法执行后往response中写入的响应体都必须为ReturnResult
。在这种情况下,使用ResponseBodyAdvice
可以在不修改已有业务代码的情况下轻松实现上述需求。假设自定义的返回结果类ReturnResult
如下所示。
@Data
public class ReturnResult<T> {
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 ResponseBodyAdvice<Object> {
@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;
ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class);
assertThat(response.getBody() != null, is(true));
ReturnResult<Student> returnResult = MAPPER.readValue(
MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {});
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;
ResponseEntity<ReturnResult> response = restTemplate.getForEntity(url, ReturnResult.class);
assertThat(response.getBody() != null, is(true));
ReturnResult<Student> returnResult = MAPPER.readValue(
MAPPER.writeValueAsString(response.getBody()), new TypeReference<ReturnResult<Student>>() {});
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()
方法的返回值类型为ResponseEntity<Object>
,但是实际往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()
方法的返回值类型为ResponseEntity<Object>
,但是实际往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 = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
contentType, converterType, request, response);
}
}
return body;
}
private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
// 获取ResponseBodyAdvice集合
List<Object> availableAdvice = getAdvice(adviceType);
if (CollectionUtils.isEmpty(availableAdvice)) {
return Collections.emptyList();
}
List<A> 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) {
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) {
// 获取返回值处理器,在这里就会完成RequestResponseBodyMethodProcessor和HttpEntityMethodProcessor的初始化,初始化的同时就会完成ResponseBodyAdvice接口的加载
List<HandlerMethodReturnValueHandler> 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
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
}
Set<Method> 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 List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> 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(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
super(converters, manager, requestResponseBodyAdvice);
}
AbstractMessageConverterMethodProcessor#AbstractMessageConverterMethodProcessor()
protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> 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(List<HttpMessageConverter<?>> converters,
@Nullable List<Object> 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的加载。
总结
如果需要使用ResponseBodyAdvice
来对handler方法的返回值做处理,则需要创建一个类并实现ResponseBodyAdvice
接口,同时该类还需要被@ControllerAdvice
注解修饰,这样的一个ResponseBodyAdvice
接口会被RequestMappingHandlerAdapter
加载,以及在初始化RequestResponseBodyMethodProcessor
和HttpEntityMethodProcessor
这两个返回值处理器时被加载,最终通过这两个返回值处理器将返回值写入response前,加载好的ResponseBodyAdvice
接口的beforeBodyWrite()
方法会被返回值处理器调用,完成对返回值的定制化处理。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。