20220705 RequestMappingHandlerAdapter
概述
DispatcherServlet
九大组件中 HandlerAdapter
的实现之一
Spring Boot 启动日志:
2022-06-09 19:14:14.788 DEBUG 13144 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : ControllerAdvice beans: 0 @ModelAttribute, 0 @InitBinder, 1 RequestBodyAdvice, 1 ResponseBodyAdvice
RequestBodyAdvice
和 ResponseBodyAdvice
默认有一个 JsonViewRequestBodyAdvice
和 JsonViewResponseBodyAdvice
是通过自动配置类中的 @Bean
方法添加进来的:
WebMvcConfigurationSupport#requestMappingHandlerAdapter
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
RequestBodyAdvice
允许在读取请求体并将其转换为对象之前自定义请求,还允许在将结果对象作为 @RequestBody
或 HttpEntity
方法参数传递给控制器方法之前对其进行处理。
该约定的实现可以直接使用 RequestMappingHandlerAdapter
注册,或者更可能使用 @ControllerAdvice
进行注解,在这种情况下它们会被自动检测到。
接口定义
public interface RequestBodyAdvice {
boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);
HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
}
起作用的地点:
AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
if (message.hasBody()) {
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
JsonViewRequestBodyAdvice
Javadoc
一个
RequestBodyAdvice
实现,增加了对在 Spring MVC@HttpEntity
或@RequestBody
方法参数上声明的 Jackson 的@JsonView
注解的支持。注解中指定的反序列化视图将被传递给
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
,然后它将使用它来反序列化请求正文。请注意,尽管
@JsonView
允许指定多个类,但仅使用一个类参数支持请求正文通知的使用。考虑使用复合接口。
用来支持方法参数上的 @JsonView
ResponseBodyAdvice
允许在执行 @ResponseBody
或 ResponseEntity
控制器方法之后但在使用 HttpMessageConverter
编写正文之前自定义响应。
实现可以直接使用 RequestMappingHandlerAdapter
和 ExceptionHandlerExceptionResolver
进行注册,或者更可能使用 @ControllerAdvice
进行注解,在这种情况下它们将被两者自动检测到。
接口定义
public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}
起作用的地点:
RequestResponseBodyMethodProcessor#writeWithMessageConverters
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
RequestResponseBodyAdviceChain#processBody
在 HttpMessageConverter#write
写出响应正文之前
JsonViewResponseBodyAdvice
ResponseBodyAdvice
的实现类,用来支持 Jackson 的 @JsonView
注解
@JsonView
用于指示由方法或注解字段定义的属性所属的视图的注解。
需要配合 @ResponseBody
和 @RequestBody
使用
简单使用
User 类有两个属性,用户名 username
和 密码 password
simpleUserParam
请求只接收username
detailUserParam
请求两个字段都接收returnSimpleUser
请求只返回username
,不返回password
returnDetailUser
请求两个字段都返回
@RequestMapping("/simpleUserParam")
public User simpleUserParam(@RequestBody @JsonView(User.UserSimpleView.class) User user) {
return user;
}
@RequestMapping("/detailUserParam")
public User detailUserParam(@RequestBody @JsonView(User.UserDetailView.class) User user) {
return user;
}
@RequestMapping("/returnSimpleUser")
@JsonView(User.UserSimpleView.class)
public User returnSimpleUser() {
return getUser();
}
@RequestMapping("/returnDetailUser")
@JsonView(User.UserDetailView.class)
public User returnDetailUser() {
return getUser();
}
private User getUser() {
return new User("n1", "p1");
}
@RequestMapping("/simpleUser")
@JsonView(User.UserSimpleView.class)
public User simpleUser() {
return getUser();
}
@RequestMapping("/detailUser")
@JsonView(User.UserDetailView.class)
public User detailUser() {
return getUser();
}
private User getUser() {
return new User("n1", "p1");
}
RequestResponseBodyAdviceChain
这个类同时实现了 RequestBodyAdvice
和 ResponseBodyAdvice
,类似于 Composite
组合类,包含字段
private final List<Object> requestBodyAdvice = new ArrayList<>(4);
private final List<Object> responseBodyAdvice = new ArrayList<>(4);
这里的泛型之所以是 Object
,是因为要同时支持 RequestBodyAdvice
和 ResponseBodyAdvice
,而这两个接口没有共同父接口
RequestResponseBodyMethodProcessor
类中包含字段
private final RequestResponseBodyAdviceChain advice;
值来源自 RequestMappingHandlerAdapter
中的字段
private final List<Object> requestResponseBodyAdvice = new ArrayList<>();
自动配置里进行了赋值
// WebMvcConfigurationSupport#requestMappingHandlerAdapter
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
Spring Boot 默认注册情况
默认注册了 1 个实现:JsonViewResponseBodyAdvice
支持 AbstractJackson2HttpMessageConverter
且需要返回类注解 JsonView
@ModelAttribute
与 Model 相关
Javadoc
将方法参数或方法返回值绑定到命名模型属性的注解,公开给 Web 视图。支持带有 @RequestMapping
方法的控制器类。
警告:数据绑定可能会通过暴露不应被外部客户端访问或修改的部分对象图而导致安全问题。因此,数据绑定的设计和使用应考虑安全性。有关详细信息,请参阅参考手册中有关 Spring Web MVC 和 Spring WebFlux 的数据绑定的专门部分。
@ModelAttribute
可用于通过注解 @RequestMapping
方法的相应参数,使用特定属性名称将命令对象公开给 Web 视图。 @ModelAttribute
也可用于通过使用 @RequestMapping
方法在控制器类中注解访问器方法来向 Web 视图公开引用数据。允许此类访问器方法具有 @RequestMapping
方法支持的任何参数,返回要公开的模型属性值。
但请注意,当请求处理导致异常时,Web 视图无法使用引用数据和所有其他模型内容,因为随时可能引发异常,从而使模型的内容不可靠。因此,@ExceptionHandler
方法不提供对 Model 参数的访问。
参考文档
示例代码
@ModelAttribute
public MyBean1 myBean1() {
return new MyBean1(1, "n1", new Date());
}
@RequestMapping("/first")
public String first(MyBean1 myBean1) {
System.out.println(myBean1);
return "first";
}
调用 myBean1 方法:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
RequestMappingHandlerAdapter#invokeHandlerMethod
最终属性会合并到 ModelAndViewContainer
中,
private final ModelMap defaultModel = new BindingAwareModelMap();
private ModelMap redirectModel;
解析方法 first
方法参数时,会选择到 ServletModelAttributeMethodProcessor
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
}
默认情况下,支持所有非简单类型的方法参数和返回值,即使没有使用 ModelAttribute
注解
ModelAttributeMethodProcessor#resolveArgument
解析方法时,同样从 ModelAndViewContainer
的 ModelMap
中获取
使用说明
一个控制器可以有任意数量的 @ModelAttribute
方法。所有这些方法(不能有 @RequestMapping
注解 )都在同一控制器中的 @RequestMapping
方法之前调用。
@ModelAttribute
可以注解方法参数、方法
可以注解在控制器方法参数,控制器方法(作用在返回值上)
@ModelAttribute
@RequestMapping("/myBean1")
public MyBean1 myBean1() {
return new MyBean1(1, "n1", new Date());
}
在 @RequestMapping
方法上使用 @ModelAttribute
报错:
2022-07-06 09:48:00.379 DEBUG 21104 --- [nio-8080-exec-1] o.s.w.servlet.view.InternalResourceView : View name 'myBean1', model {myBean1=MyBean1(id=1, name=n1, date=Wed Jul 06 09:47:58 CST 2022), org.springframework.validation.BindingResult.myBean1=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
2022-07-06 09:48:01.841 DEBUG 21104 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Error rendering view [org.springframework.web.servlet.view.InternalResourceView: name 'myBean1'; URL [myBean1]]
javax.servlet.ServletException: Circular view path [myBean1]: would dispatch back to the current handler URL [/myBean1] again. Check your ViewResolver setup! (Hint: This may be the result of an unspecified view, due to default view name generation.)
是因为处理返回值时,类型变成了 ServletModelAttributeMethodProcessor
,没有 @ModelAttribute
注解时,是 RequestResponseBodyMethodProcessor
两者的区别是 RequestResponseBodyMethodProcessor
直接将响应正文写入输出流,ServletModelAttributeMethodProcessor
只是在 ModelAndViewContainer
中添加元素,之后会应用默认视图名称 /myBean
,而默认配置的视图解析器认为转发陷入循环了,因此报错
解决方法:可以设置视图配置,并添加相应的视图文件 /resources/resources/myBean1.html
spring.mvc.view.suffix=.html
原理是让转发请求的url不再是相同的url,这样处理请求的 HandlerMapping
不再是返回 @RequestMapping
处理器方法,而是处理资源的处理器
@InitBinder
与 DataBinder
相关
@InitBinder
和 @ModelAttribute
相同,是由方法参数解析器 ServletModelAttributeMethodProcessor
(实现 HandlerMethodArgumentResolver
)处理
@RequestBody
由 RequestResponseBodyMethodProcessor
(实现 HandlerMethodArgumentResolver
)处理
因此,@RequestBody
参数里的 Date
类型格式,不能使用 @InitBinder
注册的转换器
@InitBinder
方法可以注册特定于控制器的 java.beans.PropertyEditor
或 Spring Converter
和 Formatter
组件。
注册的转换器可以处理请求参数,也就是 Model 中的元素