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请求只接收usernamedetailUserParam请求两个字段都接收returnSimpleUser请求只返回username,不返回passwordreturnDetailUser请求两个字段都返回
@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 中的元素
浙公网安备 33010602011771号