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

RequestBodyAdviceResponseBodyAdvice

默认有一个 JsonViewRequestBodyAdviceJsonViewResponseBodyAdvice

是通过自动配置类中的 @Bean 方法添加进来的:

WebMvcConfigurationSupport#requestMappingHandlerAdapter

if (jackson2Present) {
    adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
    adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}

RequestBodyAdvice

允许在读取请求体并将其转换为对象之前自定义请求,还允许在将结果对象作为 @RequestBodyHttpEntity 方法参数传递给控制器方法之前对其进行处理。

该约定的实现可以直接使用 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

允许在执行 @ResponseBodyResponseEntity 控制器方法之后但在使用 HttpMessageConverter 编写正文之前自定义响应。

实现可以直接使用 RequestMappingHandlerAdapterExceptionHandlerExceptionResolver 进行注册,或者更可能使用 @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

这个类同时实现了 RequestBodyAdviceResponseBodyAdvice ,类似于 Composite 组合类,包含字段

private final List<Object> requestBodyAdvice = new ArrayList<>(4);

private final List<Object> responseBodyAdvice = new ArrayList<>(4);

这里的泛型之所以是 Object ,是因为要同时支持 RequestBodyAdviceResponseBodyAdvice ,而这两个接口没有共同父接口

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

解析方法时,同样从 ModelAndViewContainerModelMap 中获取

使用说明

一个控制器可以有任意数量的 @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)处理

@RequestBodyRequestResponseBodyMethodProcessor (实现 HandlerMethodArgumentResolver )处理

因此,@RequestBody 参数里的 Date 类型格式,不能使用 @InitBinder 注册的转换器

@InitBinder 方法可以注册特定于控制器的 java.beans.PropertyEditor 或 Spring Converter 和 Formatter 组件。

注册的转换器可以处理请求参数,也就是 Model 中的元素

posted @ 2022-11-28 09:48  流星<。)#)))≦  阅读(76)  评论(0编辑  收藏  举报