Spring Web MVC

Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于 Servlet API 之上。

DispatcherServlet

Spring MVC 和许多其他 Web 框架一样,是围绕前端控制器模式设计的。中心 Servlet —— DispatcherServlet 为请求处理提供共享算法,而实际工作由可配置的委托组件执行。该模型是灵活的,支持不同的工作流程。

DispatcherServlet 像任何 Servlet 一样,需要根据 Servlet 规范使用 Java 配置或 web.xml 声明和映射。反过来, DispatcherServlet 使用 Spring 配置来发现请求映射、视图解析、异常处理等等所需的委托组件。

下面是通过 Java 配置注册、初始化 DispatcherServlet 的例子。该类会被 Servlet 容器自动发现:

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletCxt) {
        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();
        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");

除了直接使用 ServletContext API,你还可以继承 AbstractAnnotationConfigDispatcherServletInitializer 并覆盖特定的方法(参见上下文层次结构中的示例)。

下面是通过 web.xml 配置注册、初始化 DispatcherServlet 的例子:

<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

Spring Boot 遵循一个不同的初始化顺序。相比于挂钩到 Servlet 容器的生命周期中,Spring Boot 使用 Spring 配置来引导自身和内嵌的 Servlet 容器。Filter 和 Servlet 在 Spring 配置中声明并被发现和注册到 Servlet 容器。更多的细节请参阅 Spring Boot 文档。

1. 上下文层级

DispatcherServlet 需要一个 WebApplicationContext 来配置,拓展自 ApplicationContextWebApplicationContext 拥有与它关联的 ServletContextServlet 的链接。它还绑定了 ServletContext,这样应用程序就可以在需要的时候使用 RequestContextUtils 的静态方法访问 WebApplicationContext

大多数应用程序只需要一个 WebApplicationContext。也可以一个根WebApplicationContext 被多个 DispatcherServlet (或者 Servlet)实例共享,然后各自拥有自己的子 WebApplicationContext 配置。

WebApplicationContext 包含需要共享给多个 Servlet 实例的数据源和业务服务基础 Bean。这些 Bean 可以在 Servlet 特定的范围被继承或覆盖。子 WebApplicationContext 通常包含如下 Bean:

如下是配置 WebApplicationContext 层级的例子:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[] { RootConfig.class };
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { App1Config.class };
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };

如果不需要应用上下文分层,应用可能通过 getRootConfigClasses() 方法返回所有配置,而 getServletConfigClasses() 方法返回 null。

等价的 web.xml 配置:

<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>
</web-app>

如果不需要应用上下文分层,应用可以只配置根上下文,让 contextConfigLocation Serlvet 参数为空。

2. 特定的 Bean 类型

DispatcherServlet 委托特定的 Bean 来处理请求和渲染适当的响应。特定的 Bean 指的是 Spring 管理的实现 Spring MVC 协议的对象实例。它们通常带有内置的协议,你可以通过拓展或替换来自定义属性。

下表列举了 DispatcherHandler 关联的特定 Bean:

Bean 类型 HandlerAdapter 帮助 DispatcherServlet 调用请求映射的处理器,但不管处理器是否真的被调用。比如,调用被注解的控制器需要解析多个注解。因此, HandlerAdapter 的主要目的是让 DispatcherServlet 不再关注这些细节 HandlerExceptionResolver 异常处理解析器,可能映射到处理器、HTML 错误视图等 ViewResolver 解析基于字符串的视图名到真正的 View LocaleResolver, LocaleContextResolver 解析 Locale,客户端正在使用的语言环境和时区,以便提供国际化的视图 ThemeResovler 解析你的 Web 应用可以使用的主题,比如,提供个性化的布局 MultipartResolver 在 Multipart 解析库的帮助下解析 Multi-part 请求,比如,浏览器表单文件上传 FlashMapManager 存储和检索“输入”和“输出” FlashMap,用来从一个请求传递属性到另一个请求,通常通过重定向

3. Web MVC 配置

应用程序可以声明上述列举的基础 Bean 去处理请求。DispatcherServlet 会在 WebApplicationContext 中检查每一个特定的 Bean。如果没有匹配的 Bean 类型,它会使用 DispatcherServlet.properties 定义的默认类型:

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
    org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
    org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
    org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
    org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager

在大多数情况下,MVC 配置是最好的起点。它在 Java 或 XML 中声明所需的 bean,并提供更高级别的配置回调 API 来定制它。

Spring Boot 依赖于 MVC Java 配置去配置 Spring MVC,同时提供了许多额外的便捷选项。

4. Servlet 配置

在 Servlet 3.0+ 的环境中,你可以选择以编程方式配置 Servlet 容器作为替代方案,或者结合 web.xml 文件。下面是注册 DispatcherServlet 的例子:

import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");

WebApplicationInitializer 是 Spring MVC 提供的接口,确保你的实现被检测并自动用于初始化任何 Servlet 3 容器。使用它的一个抽象实现类 AbstractDispatcherServletInitializer,可以通过覆盖方法的方式指定 Servlet 映射和 DispatcherServlet 配置。

推荐使用 Java 风格的 Spring 配置:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { MyWebConfig.class };
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };

如果你使用 XML 风格的 Spring 配置,你需要直接继承 AbstractDispatcherServletInitializer

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };

AbstractDispatcherServletInitializer 同样提供了一个便捷的方式增加 Filter,并把它们自动映射到 DispatcherServlet

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
    // ...
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };

AbstractDispatcherServletInitializerisAsyncSupported 方法提供了一个为 DispatcherServlet 和映射的 Filter 开启异步支持的方式,默认为 true

最后,如果你需要进一步定制 DispatcherServlet 本身,你可以覆盖 createDispatcherServlet 方法。

5. 流程

DispatcherServlet 按如下顺序处理请求:

WebApplicationContext 被搜寻并绑定为请求的属性,供控制器和其他元素使用。绑定的默认的 keyDispatcherServlet.WEb_APPLICATON_CONTEXT_ATTRIBUTELocale Resolver 被绑定到请求,在处理请求时能够解析语言环境并使用(渲染视图,准备数据等)。如果你不需要解析语言环境,你不需要它。 Theme Resolver 被绑定到请求,让元素如视图决定使用哪个主题。如果你不使用主题,你可以忽略它。
  • 如果你指定了一个 Multipart File Resolver,当请求包含 Multipart,请求会被包装到 MultipartHttpServletRequest 中从而进一步处理其它元素。
  • 搜索合适的处理器。如果发现了这个处理器,这个处理器相关联的执行链(前置处理器,后置处理器和控制器)会为模型准备或渲染被执行。或者代替被注解的控制器,响应会(在 HandlerAdapter)被渲染而不是返回一个视图。
  • 如果返回了一个 model,那么视图会被渲染。如果没有返回 model,(可能是前置处理器或后置处理器处于安全原因拦截了请求),将不会有视图被渲染,因为请求已经完成了。
  • WebApplicationContext 中声明的 HandlerExceptionResolver bean 用来解析处理请求时抛出的异常。这些异常解析器允许自定义定位异常的逻辑。

    Spring DispatcherServlet 也支持返回 Servlet API 指定的最后修改日期。为特定的请求检测最后修改日期的过程非常直接:DispatcherServlet 查看合适的处理器映射,并测试这个处理器是否实现了 LastModified 接口。如果是,那么 LastModified 接口的 long getLastModified(reqesut) 方法的值会被返回到客户端。

    你可以通过在声明 Servlet 时,往 web.xml 文件中添加 Servlet 初始化参数(init-param元素)来自定义你的 DispatcherServlet 实例。下表列出了所支持的参数。

    contextConfigLocation 传给上下文实例(由 contextClass 指定)的字符串,用来指定上下文配置的位置。字符串由多个字符串组成(用 , 分隔)来支持多个上下文。如果多个上下文中有 bean 被定义了两次,那么后定义的 bean 优先级更高 namespace WebApplicationContext 的命名空间,默认是[servlet-name]-servlet

    6. 拦截

    所有的 HandlerMapping 实现均支持处理器拦截器,当你想对某些请求进行特殊处理时这很有用。比如,检查用户标识。
    拦截器必须实现 org.springframework.web.servlet 包的 HandlerInterceptor 接口,它包含三个方法,这足以灵活地配置所有种类的前置、后置处理:

    preHandle(..):在实际的处理器之前执行 postHandle(..):在实际的处理器之后执行 afterCompletion(..):在整个请求完成之后执行

    preHandle(..) 方法返回 boolean 类型,你可以使用该方法中断或者继续执行链的处理。当返回值为 true 时,处理器执行链会继续;当返回值为 false 时,DispatcherServlet 假设该拦截器已经妥善处理请求(且渲染了合适的视图),不会再执行其它的拦截器和执行链的处理器。

    注意:使用 postHandle 方法去处理使用 @ResponseBodyResponseEntity 的方法是无意义的,因为响应的写入和提交在 postHandle 之前的 HandlerAdapter 内已经完成。这意味着对响应的任何修改比如增加额外的 header 已经太晚。如果 有这样的需求,你需要实现 ResponseBodyAdvice。可以使用控制器通知(Controller Advice) bean 声明,也可以直接在 RequestMappingHandlerAdapter 配置。

    7. 异常

    如果在请求映射发生异常,或者在请求处理器如 @Controller 中抛出异常,DispatcherServlet 会委托 HandlerExceptionResolver beans 链解析异常并提供替代的处理,通常是一个 Error 响应。

    下表列举了可用的 HandlerExceptionResolver 实现:

    HandlerExceptionResolver DefaultHandlerExceptionResolver 提升到 Spring MVC 去解析异常,并映射到 HTTP 状态码。也可参考 ResponseEntityExceptionHandler 和 REST API 异常 ResponseStatusExceptionResolver 解析 @ResponseStatus 注解标注的异常,并映射到注解指定的 HTTP 状态码 ExceptionHandlerExceptionResolver 使用在 @Controller@ControllerAdvice 注解标注类中 @ExceptionHandler 注解标注的方法解析异常

    你可以简单的在 Spring 配置中声明多个 HandlerExceptionResolver beans 来定制你的异常解析器链,可以设置它们 order 属性,值越高,越晚执行。

    根据 HandlerExceptionResolver 的约定,它可以返回:

  • 指向 error 视图的 ModelAndView
  • ModelAndView,如果异常在解析器中被处理
  • null,如果异常仍未解析,让子解析器去尝试;如果异常到最后都没有被解析,它可以抛到 Servlet 容器

    MVC 配置自动声明内建的解析器链处理 Spring MVC 异常、@ResponseStatus 标注的异常,支持 @ExceptionHandler 方法。你可以自定义或替换它们。

    容器 error 页面

    如果一个异常通过 HandlerExceptionResolver 链仍未解决并任由它抛出,或者响应码被设置为错误码(如:4xx5xx),Servlet 容器可能会渲染一个默认的 HTML error 页面。你可以在 web.xml 声明 error 页面映射来定制容器默认的 error 页面:

    <error-page>
      <location>/error</location>
    </error-page>
    

    当进行如上配置之后,如果有未解决的异常抛出或响应码被设置为错误码时,Servlet 容器会跳转到配置的 URL (比如:/error)。这会被 DispatcherServlet 处理,可以映射到一个 @Controller 返回一个携带 model 的 error 视图,也可以渲染一个 JSON 响应。如下是一个示例:

    @RestController
    public class ErrorController {
      @RequestMapping(path = "/error")
      public Map<String, Object> handle(HttpServletRequest request) {
          Map<String, Object> map = new HashMap<String, Object>();
          map.put("status", request.getAttribute("javax.servlet.error.status_code"));
          map.put("reason", request.getAttribute("javax.servlet.error.message"));
          return map;
    

    Servlet API 并没有提供通过 Java 配置 error 页面映射的方式。你可以同时使用 WebApplicationInitializer 和最小化的 web.xml 协作来实现。

    8. 视图解析

    Spring MVC 定义了 ViewResolverView 接口,让你可以不用绑定特定的视图技术就能在浏览器渲染 models。ViewResolver 提供视图名与实际视图的映射。View 处理数据的准备工作,然后再移交给特定的视图技术。

    下表展示了更多关于 ViewResolver 层级的细节:

    ViewResolver Description AbstractCachingViewResolver AbstractCachingViewResolver 的子类缓存它们解析过的视图实例。缓存技术改善了某些视图技术的性能。可以通过设置缓存属性为 false 来关闭缓存。此外,如果你必须要在运行时刷新某一视图(比如当 FreeMarker 模板被修改),你可以使用 removeFromCache(String viewName, Locale loc) 方法 XmlViewResolver ViewResolver 的实现,接收 XML 格式(和 Spring 的 XML bean 工厂的 DTD 相同)的配置文件,默认的配置文件为 /WEB-INF/views.xml ResourceBundleViewResolver ViewResolver 的实现,它使用 ResourceBundle 中的 bean 定义,由捆绑基名称(bundle base name)指定。对于每个它应该解析的视图,它使用 [viewname].(class) 属性的值作为视图类, [viewname].url 属性的值作为视图 url UrlBasedViewResolver ViewResolver 接口的简单实现,它将逻辑视图名称直接解析为 URL,而没有显式映射定义。如果逻辑名称以直截了当的方式与视图资源的名称匹配,而不需要任意映射,则这是合适的 InternalResourceViewResolver UrlBasedViewResolver 的便捷子类,支持 InternalResourceView(实际上是 Servlet 和 JSP)和子类(如 JstlViewTilesView)。你可以使用 setViewClass(..) 为所有视图的生成指定视图类 FreeMarkerViewResolver UrlBasedViewResolver 的便捷子类,支持 FreeMarkerView 和它们的定制子类 ContentNegotiatingViewResolver ViewResolver 接口的实现,基于请求文件名或 Accept header 解析视图

    通过声明多个解析器 bean 构建解析器链,必要时可设定 order 属性指定顺序。order 值越大,越晚执行。

    根据 ViewResolver 的约定,找不到视图时会返回 null。无论是 JSP 还是 InternalResourceViewResolver 的情况下,找出 JSP 是否存在的唯一方法是通过 RequestDispatcher 执行调度。因此,必须始终将 InternalResourceViewResolver 配置为视图解析器链中的最后一个。

    配置视图解析就像在 Spring 配置中添加视图解析器 bean 一样简单。MVC Config 为视图解析器和无逻辑视图控制器提供了专用的配置 API,这对于没有控制器逻辑的 HTML 模板呈现非常有用。

    视图名包含特殊的 redirect: 前缀可以执行重定向。UrlBasedViewResolver(和子类)将其识别为需要重定向的指令。视图名的剩余部分是重定向 URL。

    网络效果和控制器直接返回 RedirectView 相同。但现在控制器本身可以简单地按视图逻辑名称操作。像 redirect:/myapp/some/resource 之类的逻辑视图名称将相对于当前 Servlet 上下文重定向,而像 redirect:http://myhost.com/some/arbitrary/path 这样的名称将重定向到绝对的 URL。

    注意,如果控制器方法标注了 @ResponseStatus,注解值比 RedirectView 设置的响应码优先级更高。

    视图名还可以包含特殊的 forward: 前缀,最终由 UrlBasedViewResolver 和子类解析。这将创建一个 InternalResourceView,它会执行 RequestDispatcher.forward()。因此,这个前缀对 InternalResourceViewResolverInternalResourceView(用于 JSP)无用,但对于其它视图技术,仍希望通过 Servlet/JSP 引擎来处理资源的转发,这是有用的。注意,你也可以构建解析器链来实现。

    ContentNegotiatingViewResolver 本身不解析视图,而是委托其它视图解析器,选择与客户端请求的表示形式类似的视图。表示形式可以从 Accept header 或查询参数中确定,如 /path?format=pdf

    ContentNegotiatingViewResolver 通过比较请求的媒体类型(也叫 Content-Type)和它的视图解析器相关联的视图的媒体类型,选择一个合适的视图来处理请求。列表中第一个兼容 Content-Type 的视图将表示形式返回给客户端。如果视图解析器链没有兼容的视图,将查阅通过 DefaultViews 属性指定的视图列表。后一种选项适用于单视图,无论逻辑视图名称如何,都可以呈现当前资源的适当表示。Accept header 可能会包含通配符,比如 text/*,这种情况下 Content-Typetext/xml 的视图是兼容的匹配项。

    9. 区域设置

    Spring 的大多数体系结构都支持国际化,像 Spring web MVC 框架一样。DispatcherServlet 让你可以自动地使用客户端的区域设置解析信息。这是通过 LocaleResolver 对象完成的。

    当请求进入时,DispatcherServlet 将查询区域解析器,如果找到,会尝试用它来设置区域。使用 RequestContext.getLocale() 方法,你总是可以拿到区域解析器解析的区域设置。

    除了自动区域解析,你还可以将拦截器附加到处理器映射以便在特定的情况下修改区域设置,比如,基于请求的一个参数。

    区域解析器和拦截器在 org.springframework.web.servlet.i18n 包中定义,并以正常的方式配置到应用上下文。下面是 Spring 包含的区域解析器选择。

    除了获取客户端的区域设置,了解它们的时区通常也很有用。LocaleContextResolver 接口提供了一个 LocaleResolver 的扩展,允许解析器提供更丰富的 LocaleContext,其中可能包含时区信息。

    如果可用,用户的时区可以通过 RequestContext.getTimeZone() 方法来获取。时区信息将会被 Spring 的 ConversionService 注册的日期/时间转换器和格式化器自动使用。

    Header 解析器

    此区域解析器将检查客户端(比如,Web 浏览器)发送的请求的 accept-language header。通过此 header 字段包含客户端操作系统的区域设置。注意,此解析器不支持时区信息。

    Cookie 解析器

    此区域解析器将检查客户端上可能存在的 Cookie,以查看区域设置和时区是否被指定。如果是,则使用指定的细节。使用此区域解析器的属性,你可以指定 cookie 的名称和最大年龄。下面是定义 CookieLocaleResolver 的示例:

    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
      <property name="cookieName" value="clientlanguage"/>
      <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
      <property name="cookieMaxAge" value="100000"/>
    </bean>
    

    会话解析器

    SessionLocaleResolver 允许你从从可能与用户请求关联的会话中检索区域设置和时区。与 CookieLocaleResolver 相反,此策略在 Servlet 容器的 HttpSession 中存储本地选定的区域设置。因此,这些设置只是每个会话的临时值,所以每次会话终止时都将丢失。

    注意:这与外部会话管理机制(如 Spring Session 项目)没有直接联系。SessionLocaleResolver 将简单地根据当前的 HttpServletRequest 来评估和修改相应的 HttpSession 属性。

    区域设置拦截器

    通过将 LocaleChangeInterceptor 添加到某一处理器映射,可以启用区域设置修改。它会检测请求中的参数并修改区域设置。它会调用同样存在于上下文中的 LocaleResolversetLocale() 方法。下面的示例展示了所有对于 *.view 资源的调用(包含名为 siteLanguage 的参数)现在将更改区域设置。因此,比如,http://www.sf.net/home.view?siteLanguage=nl 这个请求会将站点语言更改为荷兰语。

    <bean id="localeChangeInterceptor"
        class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
      <property name="paramName" value="siteLanguage"/>
    </bean>
    <bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
    <bean id="urlMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
      <property name="interceptors">
              <ref bean="localeChangeInterceptor"/>
          </list>
      </property>
      <property name="mappings">
          <value>/**/*.view=someController</value>
      </property>
    </bean>
    

    10. 主题

    你可以应用 Spring Web MVC 框架主题来设置应用程序的总体外观和感觉,从而增强用户体验。主题是静态资源的集合,通过是样式和图像,它们会影响应用程序的视觉样式。

    若要在你的 Web 应用中使用主题,你必须要设置 org.springframework.ui.context.ThemeSource 接口的实现。WebApplicationContext 接口扩展了 ThemeSource,但将其职责委托给专用实现。默认情况下,委托将是一个 org.springframework.ui.context.support.ResourceBundleThemeSource 的实现,它从类路径的根中加载属性文件。若要使用自定义的 ThemeSource 实现,或者需要配置 ResourceBundleThemeSource 基础名的前缀,你可以在上下文中注册一个名为 themeSource 的 bean。Web 应用上下文会自动检测拥有该名称的 bean 并使用它。

    使用 ResourceBundleThemeSource 时,主题在简单的 properties 文件中定义。properties 文件列出构成主题的资源。下面是一个示例:

    styleSheet=/themes/cool/style.css
    background=/themes/cool/img/coolBg.jpg
    

    属性的键是指从视图代码中引用主题元素的名称。对于 JSP,通常使用 spring:theme 自定义标记,这与 spring:message 标记非常相似。下面的 JSP 片段使用上一示例中定义的主题来自定义外观和感觉:

    <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
          <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
      </head>
      <body style="background=<spring:theme code='background'/>">
      </body>
    </html>
    

    默认情况下,ResourceBundleThemeSource 使用空的基础名前缀。因此,属性文件从类路径的根中加载。因此,你会把 cool.properties 主题定义在类路径的根目录中,例如,在 /WEB-INF/classes 中。ResourceBundleThemeSource 使用标准的 Java 资源捆绑加载机制,允许主题的完全国际化。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties,它引用了带有荷兰文本的特殊背景图像。

    定义主题后,如前一节所述,你可以决定使用哪个主题。DispatcherServlet 将查找名为 themeResolver 的 bean,以找出要使用的 themeResolver 实现。主题解析器的工作方式与 LocaleResolver 的方法非常相似。它检测要用于特定请求的主题,还可以更改请求的主题。以下主题解析程序由 Spring 提供:

    11. Multipart 解析器

    org.springframework.web.multipart 包中的 MultipartResolver 是一种解析 Multipart 请求(包括文件上载)的策略。有一个基于 Commons FileUpload 和另一个基于 Servlet 3.0 Multipart 请求解析的实现。

    要启用 Multipart 处理,你需要在 DispatcherServlet Spring 配置中声明名为 multipartResolverMultipartResolver bean。DispatcherServlet 检测到它并将其应用于传入请求。当接收到 Content-typemultipart/form-dataPOST 请求时,解析器将分析内容并将当前 HttpServletRequest 包装为 MultipartHttpServletRequest,以提供对已解析部分的访问,同时将其作为请求参数。

    Apache FileUpload

    要使用 Apache FileUpload,只需要配置一个名为 multipartResolverCommonsMultipartResolver bean。当然,你还需要在你的类路径有 commons-fileupload 依赖。

    Servlet 3.0

    Servlet 3.0 multipart 解析需要通过 Servlet 容器配置开启:

  • Java 中,在 Servlet 注册器上配置 MultipartConfigElement

  • web.xml 中,在 Servlet 声明中添加 <multipart-config> 部分

  • public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
      // ...
      @Override
      protected void customizeRegistration(ServletRegistration.Dynamic registration) {
          // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
          registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
    

    一旦 Servlet 3.0 配置到位,只需添加一个名为 multipartResolverStandardServletMultipartResolver bean。

    过滤器 Filter

    spring-web 模块提供了很多有用的过滤器。

    1. HTTP PUT 表单

    浏览器只能通过 HTTP GETHTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUTPATCH。Servlet API 需要 ServletRequest.getParameter*() 方法来支持仅用于 HTTP POST 的表单字段访问。

    spring-web 模块提供了 HttpPutFormContentFilter,它拦截内容类型为 application/x-www-form-urlencodedHTTP PUTPATCH 请求,从请求正文读取表单数据,并包装 ServletRequest 以使表单数据可通过 ServletRequest.getParameter*() 系列方法获取。

    2. Forwarded Header

    当请求通过诸如负载平衡器这样的代理时,主机、端口和方案可能会更改对需要创建资源链接的应用程序提出挑战,因为从客户视角链接应反映原始请求的主机、端口和方案。

    RFC 7239 定义了用于提供原始请求信息的代理的 Forwarded HTTP header。还有一些其他非标准标头,如 X-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto

    ForwardedHeaderFilter 检测、提取和使用 Forwarded header 中,或 X-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto 中的信息。它包装请求,以覆盖其主机、端口和方案,并且“隐藏” forwarded headers 以供后续处理。

    请注意,使用 forwarded headers 时有安全注意事项,如 RFC 7239 第 8 节所述。在应用程序级别,很难确定转发的标头是否可以信任。这就是为什么应该正确配置网络上游,以便从外部筛选出不受信任的转发头。

    没有代理且不需要使用 forwarded headers 的应用程序可以配置 ForwardedHeaderFilter 以删除和忽略此类 header。

    3. Shallow ETag

    有一个 ShallowEtagHeaderFilter。它被称为 Shallow 是因为它对内容一无所知。相反,它依赖于缓冲写入响应的实际内容,并计算最终的 ETag 值。

    4. CORS

    Spring MVC 通过控制器上的注解为 CORS 配置提供了细粒度的支持。不管怎样,当使用 Spring Security,这依赖于内置的 CorsFilter(必须在 Spring Security 的过滤链前端)是可选的。

    基于注解的 Controller

    Spring MVC 提供了一种基于注解的编程模型,其中 @Controller@RestController 组件使用注解来表达请求映射、请求输入、异常处理等。带注解的控制器具有灵活的方法签名,并且不必扩展基类,也不必实现特定的接口。

    @Controller
    public class HelloController {
        @GetMapping("/hello")
        public String handle(Model model) {
            model.addAttribute("message", "Hello World!");
            return "index";
    

    在这个特定的例子中,方法接受一个 model,并以字符串形式返回视图名称,但有许多其他选项存在,并将在下一章中进一步解释。

    1. 声明

    你可以使用 Servlet 的 WebApplicationContext 中的标准 Spring bean 定义来定义控制器 bean。@Controller stereotype 允许自动检测,与 Spring 一般支持一起对类路径中的 @Component 类进行检测,并自动注册它们的 bean 定义。它还充当注解类的 stereotype,表示其作为 web 组件的角色。

    若要启用对此类 @Controller bean 的自动检测,可以将组件扫描添加到 Java 配置中:

    @Configuration
    @ComponentScan("org.example.web")
    public class WebConfig {
        // ...
    

    等价的 XML 配置:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:p="http://www.springframework.org/schema/p"
        xmlns:context="http://www.springframework.org/schema/context"
        xsi:schemaLocation="
            http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd">
        <context:component-scan base-package="org.example.web"/>
        <!-- ... -->
    </beans>
    

    @RestController 是一个由 @Controller@ResponseBody 表示的组合注解,它指示每个方法继承类型级别 @ResponseBody 注解的控制器,因此直接写入响应正文而不是通过视图使用 HTML 模板进行解析和呈现。

    AOP 代理

    在某些情况下,控制器可能需要在运行时使用 AOP 代理进行修饰。一个示例是,如果你选择直接在控制器上 @Transactional 注解。在这种情况下,针对控制器,我们建议使用基于类的代理。这通常是带有控制器的默认选项。但是,如果控制器必须实现不是 Spring 上下文回调 (如 InitializingBean*Aware 等) 的接口,则可能需要显式配置基于类的代理。例如,把 <tx:annotation-driven/> 修改为 <tx:annotation-driven proxy-target-class="true"/>

    2. 请求映射

    @RequestMapping 注解用于将请求映射到控制器方法。它具有多种属性,可根据 URL、HTTP 方法、请求参数、header 和媒体类型进行匹配。可以在类级别使用它来表示共享映射或在方法级别上缩小到特定的端点映射。

    @RequestMapping 的 HTTP 方法特定的快捷方式变体:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping
  • 以上是提供的开箱即用的自定义注解,因为可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用默认情况下与所有 HTTP 方法匹配的 @RequestMapping。同样,在类级别上仍需要 @RequestMapping 来表示共享映射。

    下面是一个具有类型和方法级别映射的示例:

    @RestController
    @RequestMapping("/persons")
    class PersonController {
        @GetMapping("/{id}")
        public Person getPerson(@PathVariable Long id) {
            // ...
        @PostMapping
        @ResponseStatus(HttpStatus.CREATED)
        public void add(@RequestBody Person person) {
            // ...
    

    URI 模式

    你可以使用 glob 模式和通配符:

    ? 匹配一个字符 * 在一个路径段内匹配零或多个字符 ** 匹配多个路径段

    还可以使用 @PathVariable 声明 URI 变量并访问它们的值:

    @GetMapping("/owners/{ownerId}/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    

    URI 变量可以在类和方法级别声明:

    @Controller
    @RequestMapping("/owners/{ownerId}")
    public class OwnerController {
        @GetMapping("/pets/{petId}")
        public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
            // ...
    

    URI 变量将自动转换为适当的类型或引发 TypeMismatchException。默认情况下支持简单类型 —— intlongDate,你可以注册对任何其他数据类型的支持。请参见类型转换和 DataBinder

    URI 变量可以显式命名。例如 @PathVariable("customId"),但如果名称相同且代码使用调试信息或 Java 8 中的参数编译器标志进行编译,则可以将该详细信息保留出来。

    语法 {varName:regex} 使用具有语法 {varName:regex} 的正则表达式声明一个 URI 变量,例如给定的 URL /spring-web-3.0.5 .jar,下面的方法提取名称、版本和文件扩展名:

    @GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
    public void handle(@PathVariable String version, @PathVariable String ext) {
        // ...
    

    URI 路径模式还可以具有嵌入 ${…​} 在启动时通过 PropertyPlaceHolderConfigurer 对本地、系统、环境和其他属性源解析的占位符。这可以用来举例说明基于某些外部配置的基础 URL 的参数化。

    Spring MVC 使用 PathMatcher 约定和 spring-core 提供的 AntPathMatcher 实现进行 URI 路径匹配。

    当多个模式匹配一个 URL 时,必须将它们进行比较以找到最佳匹配。这通过 AntPathMatcher.getPatternComparator(String path) 查找更具体的模式。

    把每个单通配符计为 1,每个双通配符计为 2,再加上 URI 变量的个数,分数越低的模式越不具体(A pattern is less specific if it has a lower count of URI variables and single wildcards counted as 1 and double wildcards counted as 2)。分数相等则选择较长的模式。分数和长度都相等,选择的 URI 变量个数比通配符多的模式。

    默认的映射模式 /** 被排除在分数之外,并且总是排在最后。而且,诸如 /public/** 这样的前缀模式被认为比没有双通配符的其他模式更不具体。

    关于完整的详细信息,请参阅 AntPathMatcher 中的 AntPatternComparator,并记住 PathMatcher 的实现可以定制。

    默认 Spring MVC 将 .* 作为后缀匹配,所以映射到 /person 的控制器也隐式映射到 /person.*。然后使用文件扩展(代替 Accept header)来解释所请求的内容类型以用于响应。比如:/person.pdf/person.xml 等。

    当浏览器用于发送难以解释一致的 Accept 报文头时,使用这样的文件扩展是必要的。目前已不再是必要的,使用 Accept 报文头应该是首选。

    随着时间的推移,文件扩展名的使用在很多方面都有问题。当使用 URI 变量、路径参数、URI 编码时,它可能会导致歧义,而且还会使基于 URL 的授权和安全性的推论变得更加困难。

    要完全禁用文件扩展的使用,必须同时设置这两个:

    useSuffixPatternMatching(false),见 PathMatchConfigurer favorPathExtension(false),见 ContentNeogiationConfigurer

    基于 URL 的内容协商仍然是有用的,例如在浏览器中输入 URL 时。为了实现这一点,我们建议使用基于查询参数的策略,以避免大多数附带文件扩展的问题。或者,如果必须使用文件扩展,可以考虑将它们限制为通过 ContentNeogiationConfigurermediaTypes 属性显式地注册扩展的列表。

    后缀匹配与 RFD

    反射文件下载(RFD)攻击类似于 XSS,因为它依赖于请求输入,例如,查询参数、URI 变量,反映在响应中。然而,RFD 攻击并不是将 JavaScript 插入到 HTML 中,而是依赖于浏览器切换来执行下载,并将响应作为可执行脚本在稍后双击时处理。

    在Spring MVC中,@ResponsebodyResponseEntity 方法处于危险之中,因为它们可以呈现不同的内容类型,客户可以通过 URL 路径扩展请求。禁用后缀模式匹配和使用路径扩展进行内容协商降低了风险,但不足以防止 RFD 攻击。

    为了防止 RFD 攻击,在渲染响应体之前,Spring MVC 添加了一个 Content-Disposition:inline;filename=f.txt 报文头去推荐一个固定和安全的下载文件。只有当 URL 路径包含一个文件扩展名时,才会执行此操作,且该文件扩展名既没有放行的,也没有显式地注册为内容协商的目的。然而,当 URL 直接被输入到浏览器中时,它可能会有副作用。

    许多常见的路径扩展在缺省情况下是放行的。使用自定义 HttpMessageConverter 实现的应用程序可以显式地为内容协商注册文件扩展名,以避免为这些扩展添加 Content-Disposition 报文头。参照内容类型。

    检查 CVE-2015-5211 与 RFD 相关的其他建议。

    可消费的媒体类型

    你可以基于请求的 Content-Type 缩小请求映射范围:

    @PostMapping(path = "/pets", consumes = "application/json")
    public void addPet(@RequestBody Pet pet) {
        // ...
    

    consumes 属性支持否定表达式。比如:!text/plain 表示除 text/plain 之外的内容类型。

    你可以在类级别上声明共享的 consumes 属性。不同于大多数请求映射属性,方法级别上的 consumes 属性会覆盖而不是继承类级别上的声明。

    MediaType 提供了常用的媒体类型。比如:APPLICATION_JSON_VALUEAPPLICATION_JSON_UTF8_VALUE 等。

    可产出的媒体类型

    你可以基于 Accept 请求报文头和控制器方法 produces 属性指定的内容类型列表缩小请求映射范围:

    @GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
    @ResponseBody
    public Pet getPet(@PathVariable String petId) {
        // ...
    

    媒体类型可以指定字符集。支持否定表达式。比如:!text/plain 表示除 text/plain 之外的任意内容类型。

    你可以在类级别上声明共享的 produces 属性。不同于大多数请求 映射属性,方法级别上的 produces 属性会覆盖而不是继承类级别上的声明。

    MediaType 提供了常用的媒体类型。比如:APPLICATION_JSON_VALUEAPPLICATION_JSON_UTF8_VALUE 等。

    参数,报文头

    你可以基于请求参数条件缩小请求映射范围。你可以测试某一请求参数是否存在 myParam,是否不存在 !myParam,或者是否为指定的值 myParam=myValue

    @GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
    public void findPet(@PathVariable String petId) {
        // ...
    

    同样适用于请求头条件:

    @GetMapping(path = "/pets", headers = "myHeader=myValue")
    public void findPet(@PathVariable String petId) {
        // ...
    

    你可以用请求头条件匹配 Content-TypeAccept,但是使用 consumesproduces 会更好。

    HTTP HEAD,OPTIONS

    @GetMapping@RequestMapping(method=HttpMethod.GET)),透明地支持 HTTP HEAD 请求映射目的。控制器方法不需要改变。应用于 javax.servlet.http.HttpServlet 的响应包装器,确保 Content-Length 报文头设置为写入的字节数,而不是实际写入响应。

    @GetMapping@RequestMapping(method=HttpMethod.GET)),被隐式映射到并且支持 HTTP HEAD,HTTP HEAD 会像 HTTP GET 一样被处理,代替写入报文体,只有字节数被计算,并设置 Content-Length 报文头。

    默认的情况下,HTTP OPTIONS 请求通过显式在 @RequestMapping 方法上声明 HTTP 方法来匹配 URL 模式,并让请求得以被处理。

    如果 @RequestMapping 没有声明 HTTP 方法,Allow 报文头将被设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应该总是声明支持的 HTTP 方法,比如使用:@GetMapping@PostMapping 等。

    @RequestMapping 方法可以显式地映射到 HTTP HEAD 和 HTTP OPTIONS,但在普通情况下这是不必要的。

    自定义注解

    Spring MVC 支持为请求映射使用组合的注释。这些注释本身就是用 @RequestMapping 进行元注释的注释,并由一个更窄的、更具体的目的来重新声明 @RequestMapping 属性的子集(或全部)。

    @GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping 都是组合注释的例子。它们是开箱即用的,因为可以认为大多数控制器方法都应该映射到特定的 HTTP 方法,而使用 @RequestMapping 来默认匹配所有的 HTTP 方法。如果你需要一个组合注释的示例,请查看它们是如何声明的。

    Spring MVC 还支持定制请求映射属性和自定义请求匹配逻辑。这是一个更高级的选项,它需要子类化 RequestMappingHandlerMapping 并覆盖 getCustomMethodCondition 方法,在那里你可以检查自定义属性并返回你自己的请求条件。

    3. 处理器方法

    @RequestMapping 处理程序方法具有灵活的签名,可以从一系列支持的控制器方法参数和返回值中进行选择。

    下表展示了支持的控制器方法参数。任何参数都不支持响应式类型。

    JDK 8 的 java.util.Optional 作为方法参数是支持的,与具有 required 属性的注解(如:@RequestParam@RequestHeader 等)结合使用,等同于 required=false

    控制器方法参数 @RequestParam 用来访问 Servlet 请求参数。参数值被转换为声明的方法参数类型。
    注意:@RequestParam 是可选的,比如:设置它的属性。查看本表最后的“Any other argument” @RequestHeader 用来访问请求报文头。报文头值被转换为声明的方法参数类型 @CookieValue 用来访问 Cookie。Cookie 值被转换为声明的方法参数类型 @RequestBody 用来访问 HTTP 请求报文体。报文体内容使用 HttpMessageConverters 被转换为声明的方法参数类型 HttpEntity<B> 用来访问请求报文头和报文体。报文体使用 HttpMessageConverters 被转换 @RequestPart 用来访问 multipart/form-data 请求的一部分 java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap 用来访问用于 HTTP 控制器的模型,作为视图渲染的一部分暴露给模板 RedirectAttributes 指定在重定向时使用的属性 —— 用来附加临时存储到重定向之后请求的查询字符串,flash 属性 @ModelAttribute 用于访问模型(如果不存在就实例化)中存在的应用数据绑定和校验的属性。
    注意:它是可选的,比如:设置它的属性。查看本表最后的“Any other argument” Errors, BindingResult 用于访问命令对象(如 @ModelAttribute 参数)校验和数据绑定产生的错误,或 @RequestBody@RequestPart 参数校验产生的错误;ErrorsBindingResult 参数必须紧跟在被校验的方法参数之后 SessionStatus + class-level @SessionAttributes 用于标记表单处理完成以清除通过类级别 @SessionAttributes 注解声明的会话属性 UriComponentsBuilder 用于准备一个相对于当前请求的主机、端口、方案、上下文路径和 Servlet 映射的文字部分的 URL,还需要考虑 ForwardedX-Forwarded-* 报文头 @SessionAttribute 用于访问任意会话属性;与在会话中存储的模型属性相反,这是由类级别的 @SessionAttributes 声明导致的。更多细节请查看 @SessionAttribute @RequestAttribute 用于访问请求属性。更多细节请查看 @RequestAttribute Any other argument 如果方法参数与以上都不符合,默认它会作为 @RequestParam 解析如果它是简单类型(由 BeanUtils#isSimpleProperty 确定),其它情况作为 @ModelAttribute 解析 HttpEntity<B>, ResponseEntity<B> 返回值指定完整的响应,包括通过 HttpMessageConverters 转换并写入响应的 HTTP 报文头和报文体 HttpHeaders 用于返回只有报文头没有报文体的响应 String 用于 ViewResolver 解析的视图名,并与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。处理器方法还可以通过声明模型参数以编程方式丰富模型 View 实例,与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起用来渲染。处理器方法还可以通过声明模型参数以编程方式丰富模型 java.util.Map, org.springframework.ui.Model 将被添加到隐式模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定 @ModelAttribute 将被添加的模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    注意:@ModelAttribute 是可选的,查看本表最后的“Any other return value” ModelAndView object 待使用的视图和模型,以及可选的响应状态 void 返回类型(或 null 返回值)的方法,如果它同时包含 ServletResponse,或者 OutputStream 属性,或者 @ResponseStatus 注解,则认为它已经处理完了响应。如果控制器做了明确的 ETaglastModified 时间戳检查(更多细节查看 @Controller 缓存),也认为它已经处理完了响应。
    如果以上情况都不符合,void 返回类型也可能表示 REST 控制器的无响应体,或 HTML 控制器的默认视图选择 DeferredResult<V> 从任意线程异步地生成任意上述返回值。比如,可能是某个事件或回调的结果。参见异步请求和 DeferredResult Callable<V> 在 Spring MVC 托管的线程中异步地生成任意上述返回值。参见异步请求和 Callable ListenableFuture<V>, java.util.concurrent.CompletionStage<V>, java.util.concurrent.CompletableFuture<V> DeferredResult 的便捷替代方案,比如,当基础服务返回这中间的某一个时 ResponseBodyEmitter, SseEmitter 在 HttpMessageConverter 的帮助下异步地发出一个对象流以写入响应;也支持作为 ResponseEntity 的报文体。参见异步请求和 HTTP 流 StreamingResponseBody 异步地写入响应 OutputStream;也支持作为 ResponseEntity 的报文体。参见异步请求和 HTTP 流 Reactive types — Reactor, RxJava, or others via ReactiveAdapterRegistry DeferredResult 的替代方案,把 multi-value 流 (如FluxObservable) 收集到一个列表。
    用于流媒体场景(比如,text/event-streamapplication/json+stream)代替的,SseEmitterResponseBodyEmitter 被使用,其中 ServletOutputStream 阻塞 I/O 是在 Spring MVC 托管的线程上执行的,并且在每个写入的完成情况下使用背压。
    参见异步请求和响应式类型 Any other return value 如果返回值与以上都不符合,默认它被视为视图名如果它是 Stringvoid (默认视图名通过 RequestToViewNameTranslator 应用选择);或者作为模型属性添加到模型中,除非它是一个简单类型(由 BeanUtils#isSimpleProperty 确定),在这种情况下,它仍未得到解析

    一些带注解的控制器方法参数,表示基于字符串的请求输入(比如,@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue),如果将参数声明为字符串以外的其他内容,则可能需要进行类型转换。

    对于这种情况,类型转换是根据配置的转换器自动应用的。默认支持简单类型,例如 intlongDate 等。类型转换可以通过 WebDataBinder、或通过向 FormattingConversionService 注册 Formatter 来定制,参见 Spring 字段格式化。

    RFC 3986 讨论路径段中的键值对。在 Spring MVC 中,我们将其称为基于 Tim Berners-Lee 的“old post”的“matrix variables”,但是它们也可以被称为 URI 路径参数。

    矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔。如:/cars;color=red,green;year=2012。也可以通过重复的变量名来指定多个值。如:color=red;color=green;color=blue

    如果一个 URL 被期望包含矩阵变量,那么控制器方法的请求映射必须使用一个 URI 变量来掩盖该变量内容,并确保请求能够成功地独立于矩阵变量的顺序和存在。下面是一个例子:

    // GET /pets/42;q=11;r=22
    @GetMapping("/pets/{petId}")
    public void findPet(@PathVariable String petId, @MatrixVariable int q) {
        // petId == 42
        // q == 11
    

    考虑到所有的路径段都可能包含矩阵变量,有时你可能需要消除该矩阵变量所期望的路径变量的歧义。例如:

    // GET /owners/42;q=11/pets/21;q=22
    @GetMapping("/owners/{ownerId}/pets/{petId}")
    public void findPet(
            @MatrixVariable(name="q", pathVar="ownerId") int q1,
            @MatrixVariable(name="q", pathVar="petId") int q2) {
        // q1 == 11
        // q2 == 22
    

    矩阵变量可以定义为可选的和指定的默认值:

    // GET /pets/42
    @GetMapping("/pets/{petId}")
    public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
        // q == 1
    

    使用 MultiValueMap 获取所有矩阵变量:

    // GET /owners/42;q=11;r=12/pets/21;q=22;s=23
    @GetMapping("/owners/{ownerId}/pets/{petId}")
    public void findPet(
            @MatrixVariable MultiValueMap<String, String> matrixVars,
            @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
        // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
        // petMatrixVars: ["q" : 22, "s" : 23]
    

    注意,你需要启用矩阵变量的使用。在 MVC Java 配置中,你需要设置一个带有 removeSemicolonContent=falseUrlPathHelper。在 MVC XML 命名空间中,使用 <mvc:annotation-driven enable-matrix-variables="true"/>

    @RequestParam

    使用 @RequestParam 注解将 Servlet 请求参数(比如,查询参数或表单数据)绑定到控制器中的方法参数。

    下面的代码片段显示了用法:

    @Controller
    @RequestMapping("/pets")
    public class EditPetForm {
        // ...
        @GetMapping
        public String setupForm(@RequestParam("petId") int petId, Model model) {
            Pet pet = this.clinic.loadPet(petId);
            model.addAttribute("pet", pet);
            return "petForm";
        // ...
    

    默认情况下,使用 @RequestParam 注解的方法参数是必需的。但是你可以将 @RequestParamrequired 标志设置为 false,或者使用 java.util.Optional 包装器来指定一个方法参数是可选的。

    如果目标方法参数类型不是字符串,则自动应用类型转换。

    @RequestParam 注解被声明为 Map<String, String>MultiValueMap<String, String> 参数时,该映射会被所有查询参数填充。

    注意,使用 @RequestParam 是可选的,例如设置它的属性。默认情况下,任何参数都是一个简单的值类型,由 BeanUtils#isSimpleProperty 决定,并不是由任何其他参数解析器解析,就像使用 @RequestParam 注解一样。

    @RequestHeader

    使用 @RequestHeader 注解将请求报文头绑定到控制器中的方法参数。

    给定携带报文头的请求:

    Host                    localhost:8080
    Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
    Accept-Language         fr,en-gb;q=0.7,en;q=0.3
    Accept-Encoding         gzip,deflate
    Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Keep-Alive              300
    

    下面获取 Accept-EncodingKeep-Alive 报文头的值:

    @GetMapping("/demo")
    public void handle(
            @RequestHeader("Accept-Encoding") String encoding,
            @RequestHeader("Keep-Alive") long keepAlive) {
        //...
    

    如果目标方法参数类型不是字符串,则自动应用类型转换。

    @RequestHeader 注解被声明为 Map<String, String>MultiValueMap<String, String>HttpHeaders 参数时,该映射会被所有报文头值填充。

    内置的支持可用于将逗号分隔的字符串转换为字符串或类型转换系统已知的其他类型的数组或集合。例如,带有 @RequestHeader("Accept") 的方法参数可能是字符串类型,但也可能是 String[]List<String>

    @CookieValue

    使用 @CookieValue 注解将 HTTP Cookie 的值绑定到控制器中的方法参数。

    给定携带如下 Cookie 的请求:

    JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
    

    下面代理演示如何获取 Cookie 值:

    @GetMapping("/demo")
    public void handle(@CookieValue("JSESSIONID") String cookie) {
        //...
    

    如果目标方法参数类型不是字符串,则自动应用类型转换。

    @ModelAttribute

    在方法参数上使用 @ModelAttribute 来访问模型中的属性,或者如果不存在,就会实例化。模型属性还覆盖了来自 HTTP Servlet 请求参数的值,这些参数的名称与字段名匹配。这被称为数据绑定,它可以避免你不得不处理解析和转换单个查询参数和表单字段。例如:

    @PostMapping("/owners/{ownerId}/pets/{petId}/edit")
    public String processSubmit(@ModelAttribute Pet pet) { }
    

    上述 Pet 实例按如下所示解析:

  • 如果已经通过 Model 添加了模型,从模型解析
  • 通过 @SessionAttributes 从 HTTP 会话解析
  • 通过 被 Converter 处理后的 URI 路径变量解析
  • 调用默认构造函数
  • 使用 Servlet 请求参数调用主要的带参构造函数;参数名是通过 JavaBeans @ConstructorProperties 或字节码中的运行时保留参数名来确定的
  • 虽然使用一个模型来使用属性填充模型是很常见的,但是另一种方法是依赖于 Converter<String, T> 与 URI 路径变量约定相结合。在下面的例子中,模型属性名称 account 与 URI 路径变量 account 匹配,Account 通过传递 account 数字字符串给已注册的 Converter<String, Account> 而被加载:

    @PutMapping("/accounts/{account}")
    public String save(@ModelAttribute("account") Account account) {
        // ...
    

    获得模型属性实例后,应用数据绑定。WebDataBinder 类将 Servlet 请求参数名称(查询参数和表单字段)匹配到目标对象上的字段名。在需要的地方应用类型转换之后填充匹配字段。有关数据绑定(和校验)的更多信息,请参见校验。有关定制数据绑定的更多信息,请参见 DataBinder

    数据绑定可能导致错误。默认情况下,会引起 BindException,但是要检查控制器方法中的错误,请在 @ModelAttribute 旁边立即添加一个 BindingResult 参数,如下所示:

    @PostMapping("/owners/{ownerId}/pets/{petId}/edit")
    public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
        if (result.hasErrors()) {
            return "petForm";
        // ...
    

    在某些情况下,你可能希望访问一个没有数据绑定的模型属性。对于这种情况,你可以将模型注入控制器并直接访问它或设置 @ModelAttribute(binding=false),如下所示:

    @ModelAttribute
    public AccountForm setUpForm() {
        return new AccountForm();
    @ModelAttribute
    public Account findAccount(@PathVariable String accountId) {
        return accountRepository.findOne(accountId);
    @PostMapping("update")
    public String update(@Valid AccountUpdateForm form, BindingResult result,
            @ModelAttribute(binding=false) Account account) {
        // ...
    

    通过添加 javax.validation.Valid 注解或 Spring 的 @Validated 注解(参见 Bean 校验和 Spring 校验),校验会在数据绑定之后自动被应用。例如:

    @PostMapping("/owners/{ownerId}/pets/{petId}/edit")
    public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
        if (result.hasErrors()) {
            return "petForm";
        // ...
    

    注意,@ModelAttribute 的使用是可选的,例如设置它的属性。默认情况下,任何不是简单值类型的参数,如 BeanUtils#isSimpleProperty 所确定的,并不是由任何其他参数解析器解析的,都被视为带有 @ModelAttribute 的注释。

    @SessionAttributes

    @SessionAttributes 用于在请求之间的 WebSession 中存储模型属性。它是一个类型级别的注释,声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该被透明地存储在会话中,以便以后访问。

    @Controller
    @SessionAttributes("pet")
    public class EditPetForm {
        // ...
    

    当在模型中添加名为 pet 的模型属性时,第一个请求会自动被提升到 WebSession 中并保存。直到另一个控制器方法使用 SessionStatus 方法参数来清除存储之前,它仍然存在:

    @Controller
    @SessionAttributes("pet")
    public class EditPetForm {
        // ...
        @PostMapping("/pets/{id}")
        public String handle(Pet pet, BindingResult errors, SessionStatus status) {
            if (errors.hasErrors) {
                // ...
                status.setComplete();
                // ...
    

    @SessionAttribute

    如果你需要访问在全局范围内管理的已存在的会话属性,例如在控制器之外(例如通过一个过滤器),并且可能在方法参数上使用 @SessionAttribute 注解:

    @GetMapping("/")
    public String handle(@SessionAttribute User user) {
        // ...
    

    对于需要添加或删除会话属性的用例,请考虑将 org.springframework.web.context.request.WebRequestjavax.servlet.http.HttpSession 注入到控制器方法中。

    作为控制器工作流的一部分,在会话中临时存储模型属性,可以考虑使用 @SessionAttributes 中描述的 SessionAttributes

    @RequestAttribute

    @SessionAttribute 类似,@RequestAttribute 注解可用于访问前面创建的预先存在的请求属性,例如:通过 Servlet Filter 或者 HandlerInterceptor

    @GetMapping("/")
    public String handle(@RequestAttribute Client client) {
        // ...
    

    重定向属性

    默认情况下,所有模型属性都被认为是在重定向 URL 中作为 URI 模板变量公开的。在剩下的属性中,原始类型或原始类型的集合/数组被自动添加为查询参数。

    如果为重定向准备了一个模型实例,那么将原始类型属性添加为查询参数可能是理想的结果。但是,在带注解的控制器中,模型可能包含添加用于渲染目的的额外属性(例如下拉字段值)。为了避免在URL中出现这样的属性,@RequestMapping 方法可以声明类型 RedirectAttributes 的参数,并使用它指定可用于 RedirectView 的确切属性。如果方法确实重定向,则使用 RedirectAttributes 的内容。否则将使用模型的内容。

    RequestMappingHandlerAdapter 提供了一个名为 ignoreDefaultModelOnRedirect 的标志,该标志可以用来指示当控制器方法重定向时,不应该使用默认模型的内容。相反,控制器方法应该声明类型 RedirectAttributes 的属性,或者如果它不这样做,则不应该将属性传递给 RedirectView。MVC 命名空间和 MVC Java 配置都将此标志设置为 false,以保持向后兼容性。但是,对于新的应用程序,我们建议将其设置为 true

    注意,在扩展重定向 URL 时自动提供了来自当前请求的 URI 模板变量,并且不需要通过模型或 RedirectAttributes 显式添加。例如:

    @PostMapping("/files/{path}")
    public String upload(...) {
        // ...
        return "redirect:files/{path}";
    

    Flash 属性

    Flash 属性提供了一种方法,可以将用于存储的属性存储在另一个请求中。这是当重定向时最需要的 —— 例如,Post/Redirect/Get 模式。Flash 属性在重定向之前(通常在会话中)被临时保存,以便在重定向后立即对请求可用,并立即删除。

    Spring MVC 有两个主要的抽象支持 flash 属性。FlashMap 用于保存 flash 属性,而 FlashMapManager 用于存储、检索和管理 FlashMap 实例。

    Flash 属性支持总是开启的,不需要显式启用,即使不使用,也不会导致 HTTP 会话创建。在每个请求上都有一个“输入”的 FlashMap,其属性从先前的请求(如果有)和“输出” FlashMap 中传递,并将其属性保存为后续请求。两个 FlashMap 实例都可以从 Spring MVC 中的任何地方通过请求上下文中的静态方法访问。

    带注解的控制器通常不需要直接使用 FlashMap。相反,@RequestMapping 方法可以接受类型 RedirectAttributes 的参数,并使用它为重定向场景添加 flash 属性。通过 RedirectAttributes 添加的 Flash 属性会自动传播到“输出” FlashMap。同样,在重定向之后,“输入” FlashMap 中的属性会自动添加到服务于目标 URL 的控制器模型中。

    将请求匹配到 flash 属性

    flash 属性的概念存在于许多其他 Web 框架中,并且已经被证明有时会暴露在并发问题中。这是因为根据定义,flash 属性将存储到下一个请求。但是,“下一个”请求可能不是预期的接收方,而是另一个异步请求(例如轮询或资源请求),在这种情况下,flash 属性被移除得太早。

    为了减少此类问题的可能性,RedirectView 会自动地用目标重定向 URL 的路径和查询参数自动“戳记” FlashMap 实例。在查找“输入”的 FlashMap 时,默认的 FlashMapManager 会将该信息与传入的请求相匹配。

    这并没有完全消除并发性问题的可能性,但是却大大减少了在重定向 URL 中已经可用的信息。因此,建议使用 flash 属性主要用于重定向场景。

    Multipart

    在启用了 MultipartResolver 之后,使用 multipart/form-data 的 POST 请求的内容将被解析并作为常规请求参数访问。在下面的示例中,我们访问一个常规表单字段和一个上传的文件:

    @Controller
    public class FileUploadController {
        @PostMapping("/form")
        public String handleFormUpload(@RequestParam("name") String name,
                @RequestParam("file") MultipartFile file) {
            if (!file.isEmpty()) {
                byte[] bytes = file.getBytes();
                // store the bytes somewhere
                return "redirect:uploadSuccess";
            return "redirect:uploadFailure";
    

    在使用 Servlet 3.0 multipart 解析时,你还可以使用 javax.servlet.http.Part 作为方法参数而不是 Spring 的 MultipartFile

    Multipart 内容也可以作为数据绑定的一部分用于命令对象。例如,上面的表单字段和文件可以是表单对象上的字段:

    class MyForm {
        private String name;
        private MultipartFile file;
        // ...
    @Controller
    public class FileUploadController {
        @PostMapping("/form")
        public String handleFormUpload(MyForm form, BindingResult errors) {
            if (!form.getFile().isEmpty()) {
                byte[] bytes = form.getFile().getBytes();
                // store the bytes somewhere
                return "redirect:uploadSuccess";
            return "redirect:uploadFailure";
    

    还可以在 RESTful 服务场景中从非浏览器客户端提交 Multipart 请求。例如,一个 JSON 格式文件:

    POST /someUrl
    Content-Type: multipart/mixed
    --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
    Content-Disposition: form-data; name="meta-data"
    Content-Type: application/json; charset=UTF-8
    Content-Transfer-Encoding: 8bit
        "name": "value"
    --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
    Content-Disposition: form-data; name="file-data"; filename="file.properties"
    Content-Type: text/xml
    Content-Transfer-Encoding: 8bit
    ... File Data ...
    

    你可以使用 @RequestParam 作为字符串访问 meta-data 部分,但你可能希望它从 JSON(类似于 @RequestBody)中反序列化。使用 @RequestPart 注解访问通过 HttpMessageConverter 转换后的 multipart:

    @PostMapping("/")
    public String handle(@RequestPart("meta-data") MetaData metadata,
            @RequestPart("file-data") MultipartFile file) {
        // ...
    

    @RequestPart 可以与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,它导致应用标准 Bean 校验。默认情况下,校验错误导致 MethodArgumentNotValidException 被转换为 400BAD_REQUEST)响应。或者校验错误可以通过一个 ErrorsBindingResult 参数在控制器中本地处理:

    @PostMapping("/")
    public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
            BindingResult result) {
        // ...
    

    @RequestBody

    使用 @RequestBody 注解将请求报文体通过 HttpMessageReader 读取和反序列化为对象。下面是一个带有 @RequestBody 参数的示例:

    @PostMapping("/accounts")
    public void handle(@RequestBody Account account) {
        // ...
    

    你可以使用 MVC 配置的 Message Converters 选项来配置或自定义消息转换。

    @RequestBody 可以与 javax.validation.Valid 或 Spring 的 @Validated 注解结合使用,它导致应用标准 Bean 校验。默认情况下,校验错误导致 MethodArgumentNotValidException 被转换为 400BAD_REQUEST)响应。或者校验错误可以通过一个 ErrorsBindingResult 参数在控制器中本地处理:

    @PostMapping("/accounts")
    public void handle(@Valid @RequestBody Account account, BindingResult result) {
        // ...
    

    HttpEntity

    HttpEntity 或多或少与使用 @RequestBody 相同,但基于一个容器对象,它公开请求报文头和报文体。下面是一个例子:

    @PostMapping("/accounts")
    public void handle(HttpEntity<Account> entity) {
        // ...
    

    @ResponseBody

    在方法上使用 @ResponseBody 注解,通过 HttpMessageConverter 将返回序列化到响应报文体。例如:

    @GetMapping("/accounts/{id}")
    @ResponseBody
    public Account handle() {
        // ...
    

    @ResponseBody 也支持在类级别上使用,在这种情况下,它被所有控制器方法继承。这是 @RestController 的作用,它只是一个标记为 @Controller@ResponseBody 的元注释。

    @ResponseBody 支持响应式类型。有关其他细节,请参见异常请求和响应式类型。

    你可以使用 MVC 配置的 Message Converters 选项来配置或自定义消息转换。

    @ResponseBody 方法可以与 JSON 序列化视图相结合。有关详细信息,请参见 Jackson JSON。

    ResponseEntity

    ResponseEntity 或多或少与使用 @ResponseBody 相同,但基于一个容器对象,它指定请求报文头和报文体。下面是一个例子:

    @PostMapping("/something")
    public ResponseEntity<String> handle() {
        // ...
        URI location = ...
        return new ResponseEntity.created(location).build();
    

    Jackson JSON

    Jackson 序列化视图

    Spring MVC 为 Jackson 的序列化视图提供了内置的支持,它只允许呈现对象中所有字段的子集。要使用 @ResponseBodyResponseEntity 控制器方法,可以使用 Jackson 的 @JsonView 注释激活序列化视图类:

    @RestController
    public class UserController {
        @GetMapping("/user")
        @JsonView(User.WithoutPasswordView.class)
        public User getUser() {
            return new User("eric", "7!jd#h23");
    public class User {
        public interface WithoutPasswordView {};
        public interface WithPasswordView extends WithoutPasswordView {};
        private String username;
        private String password;
        public User() {
        public User(String username, String password) {
            this.username = username;
            this.password = password;
        @JsonView(WithoutPasswordView.class)
        public String getUsername() {
            return this.username;
        @JsonView(WithPasswordView.class)
        public String getPassword() {
            return this.password;
    

    @JsonView 允许一个视图类数组,但你只能指定一个控制器方法。如果需要激活多个视图,请使用复合接口。

    对于依赖于视图解析的控制器,只需将序列化视图类添加到模型中:

    @Controller
    public class UserController extends AbstractController {
        @GetMapping("/user")
        public String getUser(Model model) {
            model.addAttribute("user", new User("eric", "7!jd#h23"));
            model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
            return "userView";
    

    Jackson JSONP

    为了使 JSONP 支持 @ResponseBodyResponseEntity 方法,请声明一个如下所示继承 AbstractJsonpResponseBodyAdvice@ControllerAdvice bean,构造函数参数表示 JSONP 查询参数名:

    @ControllerAdvice
    public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {
        public JsonpAdvice() {
            super("callback");
    

    对于依赖于视图解析的控制器,当请求有一个名为 JSONPcallback 的查询参数时,JSONP 将自动启用。这些名称可以通过 jsonpParameterNames 属性定制。

    @RequestMapping 方法参数上,用来创建或访问模型中的对象,并通过 WebDataBinder 将其绑定到请求。
  • 作为 @Controller 或 @ControllerAdvice类中的方法级注解,帮助在任何@RequestMapping` 方法调用之前初始化模型。
  • @RequestMapping 方法上标记其返回值是一个模型属性。
  • 本节讨论 @ModelAttribute 方法,也就是上面列表中的第 2 个。控制器可以有任意数量的 @ModelAttribute 方法。在相同的控制器中,所有这些方法都在 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器中共享。有关更多细节,请参见控制器通知部分。

    @ModelAttribute 方法具有灵活的方法签名。它们支持许多与 @RequestMapping 方法相同的参数,除了 @ModelAttribute 本身,以及与请求主体相关的任何东西。

    @ModelAttribute 方法的例子:

    @ModelAttribute
    public void populateModel(@RequestParam String number, Model model) {
        model.addAttribute(accountRepository.findAccount(number));
        // add more ...
    

    只添加一个属性:

    @ModelAttribute
    public Account addAccount(@RequestParam String number) {
        return accountRepository.findAccount(number);
    

    当没有显式指定名称时,将根据在 Javadoc 中所解释的对象类型选择一个默认名称。您总是可以通过使用重载的 addAttribute 方法或通过 @ModelAttributename 属性(为返回值)来分配一个显式名称。

    @ModelAttribute 还可以作为 @RequestMapping 方法的方法级注解,在这种方法中,@RequestMapping 方法的返回值被解释为模型属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非返回值是一个字符串会被解释为视图名。@ModelAttribute 还可以帮助定制模型属性名称:

    @GetMapping("/accounts/{id}")
    @ModelAttribute("myAccount")
    public Account handle() {
        // ...
        return account;
    

    数据绑定器 DataBinder

    @Controller@ControllerAdvice 类可以使用 @InitBinder 方法来初始化 WebDataBinder 的实例,而反过来则用于:

  • 将请求参数(即表单数据或查询)绑定到模型对象。
  • 将基于字符串的请求值(如请求参数、路径变量、报文头、cookie 等)转换为控制器方法参数的目标类型。
  • 在渲染 HTML 表单时将模型对象值格式化为字符串值。
  • @InitBinder 方法可以注册控制器特定的 java.bean.PropertyEditor,或 Spring Converter 和 Formatter 组件。此外,MVC 配置可以用于在全局共享的 FormattingConversionService 中注册 Converter 和 Formatter 类型。

    @InitBinder 方法支持许多与 @RequestMapping 方法相同的参数,除了@ModelAttribute(命令对象)参数。通常,它们是用 WebDataBinder 参数声明的,用于注册,以及 void 返回值。下面是一个例子:

    @Controller
    public class FormController {
        @InitBinder
        public void initBinder(WebDataBinder binder) {
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
            dateFormat.setLenient(false);
            binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
        // ...
    

    或者,当您通过共享的 FormattingConversionService 使用基于 Formatter 的设置时,您可以重用相同的方法,并注册控制器特定的 Formatter:

    @Controller
    public class FormController {
        @InitBinder
        protected void initBinder(WebDataBinder binder) {
            binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
        // ...
    

    异常 Exceptions

    @Controller@ControllerAdvice 类可以使用 @ExceptionHandler 方法来处理控制器方法的异常。例如:

    @Controller
    public class SimpleController {
        // ...
        @ExceptionHandler
        public ResponseEntity<String> handle(IOException ex) {
            // ...
    

    注解可以列出要匹配的异常类型。或者简单地将目标异常声明为如上所示的方法参数。当多个异常方法匹配时,根异常匹配通常更倾向于引发异常匹配。更正式的是,ExceptionDepthComparator 用于根据抛出的异常类型对异常进行排序。

    在一个多 @ControllerAdvice 的安排中,请将您的主根异常映射声明为 @ControllerAdvice,并以相应的顺序排列优先级。虽然根异常匹配更倾向于引发异常,但这主要是给定控制器或 @ControllerAdvice 的方法之一。这意味着,高优先级 @ControllerAdvice 的引发异常匹配比低优先级的 @ControllerAdvice 的任意匹配优先级更高。

    Spring MVC 中对 @ExceptionHandler 方法的支持建立在 DispatcherServlet 级别,HandlerExceptionResolver 机制之上。

    @ExceptionHandler 方法支持如下参数:

    java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap 用于访问错误响应的模型,总是为空 RedirectAttributes 指定在重定向时使用的属性 —— 用来附加临时存储到重定向之后请求的查询字符串,flash 属性 @SessionAttribute 用于访问任意会话属性;与在会话中存储的模型属性相反,这是由类级别的 @SessionAttributes 声明导致的。更多细节请查看 @SessionAttribute @RequestAttribute 用于访问请求属性。更多细节请查看 @RequestAttribute HttpEntity<B>, ResponseEntity<B> 返回值指定完整的响应,包括通过 HttpMessageConverters 转换并写入响应的 HTTP 报文头和报文体 String 用于 ViewResolver 解析的视图名,并与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。处理器方法还可以通过声明模型参数以编程方式丰富模型 View 实例,与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起用来渲染。处理器方法还可以通过声明模型参数以编程方式丰富模型 java.util.Map, org.springframework.ui.Model 将被添加到隐式模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定 @ModelAttribute 将被添加的模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    注意:@ModelAttribute 是可选的,查看本表最后的“Any other return value” ModelAndView object 待使用的视图和模型,以及可选的响应状态 void 返回类型(或 null 返回值)的方法,如果它同时包含 ServletResponse,或者 OutputStream 属性,或者 @ResponseStatus 注解,则认为它已经处理完了响应。如果控制器做了明确的 ETaglastModified 时间戳检查(更多细节查看 @Controller 缓存),也认为它已经处理完了响应。
    如果以上情况都不符合,void 返回类型也可能表示 REST 控制器的无响应体,或 HTML 控制器的默认视图选择 Any other return value 如果返回值与以上都不符合,默认它会被视为模型属性增加到模型中,除非它是一个简单类型(由 BeanUtils#isSimpleProperty 确定),在这种情况下,它仍未得到解析

    REST API 异常

    REST服务的一个常见需求是在响应的主体中包含错误细节。Spring 框架不会自动这样做,因为响应主体中错误细节的表示是特定于应用程序的。然而,@RestController 可以使用 @ExceptionHandler 方法使用 ResponseEntity 返回值来设置响应的状态和主体。这些方法也可以在 @ControllerAdvice 类中声明,以在全球应用它们。

    在响应主体中使用错误细节实现全局异常处理的应用程序应该考虑扩展 ResponseEntityExceptionHandler,该处理程序提供了对 Spring MVC 抛出的异常的处理,以及自定义响应主体的钩子。为了使用这个,创建一个 ResponseEntityExceptionHandler 的子类,用 @ControllerAdvice 注释,重写必要的方法,并将其声明为 Spring bean。

    控制器通知 Controller Advice

    典型的 @ExceptionHandler@InitBinder@ModelAttribute 方法在 @Controller 类(或类层次结构)中应用。如果您想要这样的方法在全局范围内应用,在控制器中,您可以在标记为 @ControllerAdvice@RestControllerAdvice 的类中声明它们。

    @ControllerAdvice 被标记为 @Component,这意味着此类类可以通过组件扫描注册为 Spring bean。@RestControllerAdvice 也是一个带有 @ControllerAdvice@ResponseBody 的元注释,它本质上是通过消息转换(对比视图解析或模板渲染)将 @ExceptionHandler 方法呈现给响应主体。

    在启动时,@RequestMapping@ExceptionHandler 方法的基础设施类检测类型 @ControllerAdvice 的 Spring bean,然后在运行时应用它们的方法。全局 @ExceptionHandler 方法(来自 @ControllerAdvice)是在本地(来自 @Controller之后应用的。相比之下,全局的 @ModelAttribute@InitBinder 方法在本地应用之前就已经应用了。

    默认的 @ControllerAdvice 方法适用于所有的请求,即所有的控制器,但是您可以通过注解的属性将其缩小到控制器的子集:

    // Target all Controllers annotated with @RestController
    @ControllerAdvice(annotations = RestController.class)
    public class ExampleAdvice1 {}
    // Target all Controllers within specific packages
    @ControllerAdvice("org.example.controllers")
    public class ExampleAdvice2 {}
    // Target all Controllers assignable to specific classes
    @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
    public class ExampleAdvice3 {}
    

    请记住上面的选择器是在运行时评估的,如果广泛使用可能会对性能造成负面影响。有关更多细节,请参见 @ControllerAdvice Javadoc。

    URI 链接 URI Links

    这一节描述了 Spring 框架中准备 URI 的各种选项。

    UriComponents

    UriComponents 可以与 java.net.URI 相媲美。不过,它附带一个专用的 UriComponentsBuilder 并支持 URI 模板变量:

    String uriTemplate = "http://example.com/hotels/{hotel}";
    UriComponents uriComponents = UriComponentsBuilder.fromUriString(uriTemplate)  // 使用 URI 模板的静态工厂方法
            .queryParam("q", "{q}")  // 添加或替换 URI 组件
            .build(); // 构建 UriComponents
    URI uri = uriComponents.expand("Westin", "123").encode().toUri();  // 展开 URI 变量、编码,获取 URI
    

    上述可以走捷径用单链完成:

    String uriTemplate = "http://example.com/hotels/{hotel}";
    URI uri = UriComponentsBuilder.fromUriString(uriTemplate)
            .queryParam("q", "{q}")
            .buildAndExpand("Westin", "123")
            .encode()
            .toUri();
    

    UriBuilder

    UriComponentsBuilderUriBuilder 的一个实现。UriBuilderFactoryUriBuilder 一起提供了一个可插入的机制,用于从 URI 模板构建 URI,以及共享基本 URI、编码策略等公共属性的方法。

    RestTemplateWebClient 都可以配置 UriBuilderFactory,以便自定义 URI 模板创建 URI 的方式。默认的实现依赖于内部的 UriComponentsBuilder,并提供了配置公共基础 URI 的选项,另一种编码模式策略,等等。

    配置 RestTemplate 的示例:

    String baseUrl = "http://example.com";
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setUriTemplateHandler(factory);
    

    配置 WebClient 的示例:

    String baseUrl = "http://example.com";
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
    // Configure the UriBuilderFactory..
    WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
    // Or use shortcut on builder..
    WebClient client = WebClient.builder().baseUrl(baseUrl).build();
    // Or use create shortcut...
    WebClient client = WebClient.create(baseUrl);
    

    您也可以直接使用 DefaultUriBuilderFactory,同样你也可能会使用 UriComponentsBuilder。主要的区别是 DefaultUriBuilderFactory 是有状态的,可以重新用于准备许多 URL,共享公共配置,例如基本 URL,而 UriComponentsBuilder 是无状态的,用于单个 URI。

    使用 DefaultUriBuilderFactory 的示例:

    String baseUrl = "http://example.com";
    DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
    URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
            .queryParam("q", "{q}")
            .build("Westin", "123"); // encoding strategy applied..
    

    URI 编码 URI Encoding

    UriComponents 中编码 URI 的默认方法如下:

  • URI变量扩展。
  • 每个 URI 组件(路径、查询等)都是单独编码的。
  • 编码规则如下:在 URI 组件中,将百分比编码应用到所有非法字符,包括 non-US-ASCII 字符,以及在 RFC 3986 中定义的 URI 组件内的所有其他字符。

    UriComponents 中的编码可与 java.net.URI 的多参数构造函数相媲美,如它类级别上的 Javadoc 中“Escaped octets, quotation, encoding, and decoding”部分所述。

    上述默认编码策略并没有对所有具有保留意义的字符进行编码,但是只有在给定的 URI 组件中是非法的。如果这不是您所期望的,您可以使用下面描述的替代策略。

    当使用 DefaultUriBuilderFactory —— 插入到 WebClientRestTemplate 或直接使用时,您可以切换到另一种编码策略,如下所示:

    String baseUrl = "http://example.com";
    DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
    factory.setEncodingMode(EncodingMode.VALUES_ONLY);
    // ...
    

    这种替代编码策略在扩展之前应用 UriUtils.encode(String, Charset) 到每个 URI 变量值,有效地编码所有 non-US-ASCII 字符,以及在 URI 中具有保留意义的所有字符,这确保了扩展的 URI 变量不会对 URI 的结构或含义产生任何影响。

    Servlet 请求相对性 Servlet request relative

    你可以使用 ServletUriComponentsBuilder 创建相对于当前请求的 URI:

    HttpServletRequest request = ...
    // Re-uses host, scheme, port, path and query string...
    ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
            .replaceQueryParam("accountId", "{id}").build()
            .expand("123")
            .encode();
    

    你可以创建相对于上下文路径的 URI:

    // Re-uses host, port and context path...
    ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
            .path("/accounts").build()
    

    你可以创建相对于 Servlet 的 URI(比如,/main/*):

    // Re-uses host, port, context path, and Servlet prefix...
    ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
            .path("/accounts").build()
    

    ServletUriComponentsBuilder 检测并使用来自 ForwardedX-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto 报文头的信息,因此产生的链接反映了原始请求。您需要确保您的应用程序是在一个可信代理的后面,它过滤掉来自外部的这些头。还可以考虑使用 ForwardedHeaderFilter,它可以在每个请求中处理这样的头,并提供一个选项来删除和忽略这些头。

    链接到控制器 Links to controllers

    Spring MVC 提供了一种机制来准备与控制器方法的链接。例如:

    @Controller
    @RequestMapping("/hotels/{hotel}")
    public class BookingController {
        @GetMapping("/bookings/{booking}")
        public String getBooking(@PathVariable Long booking) {
            // ...
    

    你可以引用该方法的名称来准备链接:

    UriComponents uriComponents = MvcUriComponentsBuilder
        .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
    URI uri = uriComponents.encode().toUri();
    

    在上面的例子中,我们提供了实际的方法参数值,在这种情况下,long 类型的值 21,用作路径变量并插入到 URL 中。此外,我们提供了值 42,以填充任何剩余的 URI 变量,比如从类型级别的请求映射继承的 hotel 变量。如果该方法有更多的参数,则可以为 URL 不需要的参数提供 null。一般来说,只有 @PathVariable@RequestParam 参数与构造 URL 相关。

    还有其他使用 MvcUriComponentsBuilder 的方法。例如,您可以使用类似于通过代理进行模拟测试的技术,以避免以名称引用控制器方法(该示例假定为 MvcUriComponentsBuilder.on 的静态导入):

    UriComponents uriComponents = MvcUriComponentsBuilder
        .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
    URI uri = uriComponents.encode().toUri();
    

    上面的例子使用了 MvcUriComponentsBuilder 中的静态方法。在内部,他们依赖 ServletUriComponentsBuilder 来为当前请求的 scheme、主机、端口、上下文路径和 servlet 路径准备一个基本 URL。这在大多数情况下都很有效,但有时可能不够。例如,您可能在请求的上下文之外(例如,准备链接的批处理过程),或者您可能需要插入一个路径前缀(例如,从请求路径中删除的 locale 前缀,需要重新插入到链接中)。

    对于这种情况,您可以使用静态 fromXxx 重载方法,该方法接受 UriComponentsBuilder 以使用基本 URL。或者您可以创建一个带有基本 URL 的 MvcUriComponentsBuilder 实例,然后使用基于实例的 withXxx 方法。例如:

    UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
    MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
    builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
    URI uri = uriComponents.encode().toUri();
    

    MvcUriComponentsBuilder 检测并使用来自 ForwardedX-Forwarded-HostX-Forwarded-PortX-Forwarded-Proto 报文头的信息,因此产生的链接反映了原始请求。您需要确保您的应用程序是在一个可信代理的后面,它过滤掉来自外部的这些头。还可以考虑使用 ForwardedHeaderFilter,它可以在每个请求中处理这样的头,并提供一个选项来删除和忽略这些头。

    链接到视图 Links in views

    您还可以从 JSP、Thymeleaf、FreeMarker 等视图中构建指向注解控制器的链接。这可以通过在 MvcUriComponentsBuilder 中使用 fromMappingName 方法来完成,它引用了名称的映射。

    每个 @RequestMapping 都根据类的大写字母和完整的方法名指定一个默认名称。例如,类 FooController 中的方法 getFoo 被指定为 FC#getFoo。这个策略可以通过创建 HandlerMethodMappingNamingStrategy 的实例来替换或定制,并将其插入到 RequestMappingHandlerMapping 中。默认策略实现还查看 @RequestMapping 中的 name 属性,并使用该属性。这意味着如果默认的映射名称与另一个(例如重载的方法)冲突,您可以在 @RequestMapping 上显式地分配一个名称。

    指定的请求映射名称在启动时记录在 TRACE 级别。
    Spring JSP 标记库提供了一个名为 mvcUrl 的函数,该函数可用于根据此机制为控制器方法准备链接。

    例如给定:

    @RequestMapping("/people/{id}/addresses")
    public class PersonAddressController {
        @RequestMapping("/{country}")
        public HttpEntity getAddress(@PathVariable String country) { ... }
    

    您可以从 JSP 中准备一个链接,如下所示:

    <%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
    <a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
    

    上面的示例依赖于 Spring 标记库中声明的 mvcUrl JSP函数(即 META-INF/spring.tld)。对于更高级的情况(如前一节中解释的自定义基 URL),很容易定义您自己的函数,或者使用自定义标记文件,以便使用一个具有自定义基 URL 的 MvcUriComponentsBuilder 的特定实例。

    异常请求 Async Requests

    Spring MVC 与 Servlet 3.0 异步请求处理有广泛的集成:

  • 控制器方法中的 DeferredResultCallable 返回值为单个异步返回值提供了基本的支持。
  • 控制器可以流化多个值,包括SSE和原始数据。
  • 控制器可以使用响应式客户端,并返回响应式类型以进行响应处理。
  • DeferredResult

    一旦在 Servlet 容器中启用了异步请求处理特性,控制器方法就可以将任何受支持的控制器方法的返回值包装为 DeferredResult

    @ResponseBody
    public DeferredResult<String> quotes() {
        DeferredResult<String> deferredResult = new DeferredResult<String>();
        // Save the deferredResult somewhere..
        return deferredResult;
    // From some other thread...
    deferredResult.setResult(data);
    

    控制器可以从不同的线程异步地生成返回值,例如响应外部事件(JMS 消息)、调度任务或其他。

    Callable

    控制器还可以使用 java.util.concurrent.Callable 来包装任何受支持的返回值。

    @PostMapping
    public Callable<String> processUpload(final MultipartFile file) {
        return new Callable<String>() {
            public String call() throws Exception {
                // ...
                return "someView";
    

    然后,通过配置的 TaskExecutor 执行给定的任务,获得返回值。

    过程 Processing

    下面是Servlet异步请求处理的简要概述:

  • 可以通过调用 request.startAsync()ServletRequest 放入异步模式。这样做的主要效果是,Servlet 和任何过滤器都可以退出,但是响应将保持开放,以便以后处理完成。
  • startasync() 的调用返回 AsyncContext,可用于进一步控制异步处理。例如,它提供了方法调度,类似于 Servlet API 的转发,但它允许应用程序在 Servlet 容器线程上恢复请求处理。 ServletRequest 提供了对当前 DispatcherType 的访问,它可以用来区分处理初始请求、异步调度、转发和其他 dispatcher 类型。

    DeferredResult 处理:

  • 控制器返回一个 DeferredResult,并将其保存在一些内存中队列或列表中,以便访问它。
  • Spring MVC 调用 request.startAsync()
  • 同时,DispatcherServlet 和所有配置的过滤器都退出了请求处理线程,但是响应仍然是打开的。
  • 该应用程序从一些线程和 Spring MVC 中设置 DeferredResult,然后将请求发送回 Servlet 容器。
  • DispatcherServlet 再次被调用,并以异步产生的返回值恢复处理。

    Callable 处理:

  • 控制器返回一个 Callable
  • Spring MVC 调用 request.startAsync(),并将 Callable 提交给一个 TaskExecutor,以便在单独的线程中进行处理。
  • 与此同时,DispatcherServlet 和所有过滤器都退出了 Servlet 容器线程,但是响应仍然是打开的。
  • 最终,Callable 生成一个结果,Spring MVC 将请求发送回 Servlet 容器以完成处理。
  • DispatcherServlet 再次被调用,并以 Callable 异步生成的返回值恢复处理。

    想了解更多背景和内容,您还可以阅读在 Spring MVC 3.2 中引入异步请求处理支持的博客文章。

    异常处理 Exception handling

    当使用 DeferredResult 时,您可以选择是否调用 setResultsetErrorResult(带异常)。在这两种情况下,Spring MVC 将请求发送回 Servlet 容器以完成处理。这时会视为控制器方法返回了给定值,或者它产生了给定的异常。然后,异常会通过常规的异常处理机制,例如调用 @ExceptionHandler 方法。

    当使用 Callable 时,类似的处理逻辑如下。主要区别在于,结果是从 Callable 或它引发的异常中返回的。

    拦截器 Interception

    HandlerInterceptor 也可以 AsyncHandlerInterceptor,以便在启动异步处理而不是 postHandleafterCompletion 的初始请求上接收 afterConcurrentHandlingStarted 回调。

    HandlerInterceptor 还可以注册 CallableProcessingInterceptorDeferredResultProcessingInterceptor,以便更深入地集成异步请求的生命周期,以处理超时事件。有关更多细节,请参见 AsyncHandlerInterceptor

    DeferredResult 提供 onTimeout(Runnable)onCompletion(Runnable) 回调。有关更多详细信息,请参见 DeferredResult 的 Javadoc。可以用 Callable 替换 WebAsyncTask,它公开了超时和完成回调的其他方法。

    对比 WebFlux Compared to WebFlux

    Servlet API 最初是为单次通过 Filter-Servlet 链而构建的。在 Servlet 3.0 中添加的异步请求处理允许应用程序退出 Filter-Servlet 链,但保留对进一步处理的响应。Spring MVC 异步支持是围绕该机制构建的。当一个控制器返回一个 DeferredResult 时,将退出 Filter-Servlet 链,并释放 Servlet 容器线程。稍后,当 DeferredResult 被设置,会发起异步调度(对相同的 URL)恢复处理,控制器会再次被映射,但会使用 DeferredResult 值而不是再次执行它。

    相比之下,Spring WebFlux 既不是构建在 Servlet API 上的,也不需要这样的异步请求处理特性,因为它设计就是异步的。异步处理被构建到所有框架约定中,并且从本质上支持请求处理的 through :: stages。

    从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步和响应式类型作为控制器方法中的返回值。Spring MVC 甚至支持流媒体,包括响应式背压。然而,不同于 WebFlux,单独写响应仍然阻塞(并在一个单独的线程上执行),而 WebFlux 依赖于非阻塞 I/O,并且不需要额外的线程。

    另一个基本的区别是 Spring MVC 不支持控制器方法参数中的异步或响应式类型,例如 @RequestBody@RequestPart 和其他,也不支持异步和响应式类型作为模型属性。Spring WebFlux 确实支持这一切。

    HTTP 流 HTTP Streaming

    DeferredResultCallable 可被用于单个异步返回值。如果你想生成多个异步返回值并将它们写入到响应该怎么做?

    对象 Objects

    ResponseBodyEmitter 返回值可用于生成对象的流,其中每个对象使用 HttpMessageConverter 序列化并写入响应。例如:

    @GetMapping("/events")
    public ResponseBodyEmitter handle() {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter();
        // Save the emitter somewhere..
        return emitter;
    // In some other thread
    emitter.send("Hello once");
    // and again later on
    emitter.send("Hello again");
    // and done at some point
    emitter.complete();
    

    ResponseBodyEmitter 也可以用作 ResponseEntity 的报文体,让你可以自定义响应的状态和报文头。

    当一个 emitter 抛出 IOException(比如,如果远程客户端断开)时,应用不负责清理链接,并且不应该执行 emitter.completeemitter.completeWithError。相反,Servlet 容器会自动启动一个 AsyncListener 错误通知,在这个通知中,Spring MVC 完成一个 completeWithError 调用,这反过来又为应用程序执行了一个最终的异步分派,在这个过程中,Spring MVC 调用了配置的异常解析器并完成该请求。

    SseEmitterResponseBodyEmitter 的子类,提供了服务器发送事件(Server-Sent Events
    )的支持,从服务器发送的事件按照 W3C SSE 规范进行格式化。要从控制器生成 SSE 流只需要返回 SseEmitter

    @GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter handle() {
        SseEmitter emitter = new SseEmitter();
        // Save the emitter somewhere..
        return emitter;
    // In some other thread
    emitter.send("Hello once");
    // and again later on
    emitter.send("Hello again");
    // and done at some point
    emitter.complete();
    

    虽然 SSE 是推流到浏览器的主要选项,但请注意 Internet Explorer 不支持服务器发送的事件。考虑使用 Spring 的 WebSocket 消息,使用 SockJS 回调传输(包括SSE),来支持大部分浏览器。

    还可以参阅上一节中关于异常处理的说明。

    原始数据 Raw data

    有时,绕过消息转换直接推流到响应 OutputStream(比如,文件下载)是有用的。使用 StreamingResponseBody 返回值类型可以做到:

    @GetMapping("/download")
    public StreamingResponseBody handle() {
        return new StreamingResponseBody() {
            @Override
            public void writeTo(OutputStream outputStream) throws IOException {
                // write...
    

    StreamingResponseBody 也可以用作 ResponseEntity 的报文体,让你可以自定义响应的状态和报文头。

    响应式类型

    Spring MVC 支持在控制器中使用响应式客户端库。这包括 spring-webflux 的 WebClient 和 Spring Data 响应式数据仓库等其他的 WebClient。在这种情况下,能够从控制器方法返回响应式类型是很方便的。

    响应式返回值的处理方式如下:

  • 单值适用并且类似于使用 DeferredResult。示例包括 MonoReactor)或 SingleRxJava)。
  • 多值流,具有流媒体类型,如 application/stream+jsontext/event-stream,适用并且类似于使用 ResponseBodyEmitterSseEmitter。例子包括FluxReactor)或 ObservableRxJava)。应用程序还可以返回 Flux<ServerSentEvent>Observable<ServerSentEvent>
  • 多值流,具有任何其他媒体类型(例如,application/json),适用并且类似于使用 DeferredResult<List<?>>
  • Spring MVC 通过 spring-core 提供的 ReactiveAdapterRegistry 支持 ReactorRxJava,使其能够适应多个响应式库。

    当通过响应式类型推流到响应时,Spring MVC 支持响应式背压,但是仍然需要使用阻塞 I/O 来执行实际的写操作。这是通过在一个单独的线程上配置的 MVC TaskExecutor 完成的,以避免阻塞上游源(例如,从 WebClient 返回的 Flux)。默认情况下,将使用一个不适合生产的 SyncTaskExecutorSPR-16203 将在 Spring Framework 5.1 中提供更好的默认值。同时,请通过 MVC 配置配置执行器。

    断开 Disconnects

    当远程客户端离开时,Servlet API 不提供任何通知。因此,当推流到响应时,无论是通过 SseEmitter<<mvc-ann-async-reactive-types,reactive types>,都要定期发送数据,因为如果客户机断开连接,写入将会失败。发送者可以采用一个空的(只包含注释)SSE 事件的形式,或者任意其它数据另一方将其视为心跳并忽略。

    也可以考虑使用 web 消息传递解决方案,比如用带有内建心跳机制的,基于 WebSocket
    STOMP 或带 SockJS 的 WebSocket

    必须在 Servlet 容器级别启用异步请求处理特性。MVC 配置还为异步请求提供了几个选项。

    Servlet 容器

    过滤器和 Servlet 声明有一个 asyncSupported,需要将其设置为 true,以便启用异步请求处理。此外,还应该声明过滤器映射来处理异步的 javax.servlet.DispatchType

    在 Java 配置中,当您使用 AbstractAnnotationConfigDispatcherServletInitializer 初始化 Servlet 容器时,这是自动完成的。

    web.xml 配置中,添加 <async-supported>true</async-supported>DispatcherServlet 和过滤器声明,并添加 <dispatcher>ASYNC</dispatcher> 到过滤器映射。

    Spring MVC

    MVC配置暴露了与异步请求处理相关的选项:

  • Java 配置——在 WebMvcConfigurer 上使用 configureAsyncSupport 回调。

  • XML 命名空间——使用 <mvc:annotation-driven> 下的 <async-support> 元素。

  • 您可以配置以下内容:

  • 异步请求的默认超时值,如果没有设置,则取决于底层的 Servlet 容器(例如,Tomcat 上为 10 秒)。

  • 当使用 Reactive 类型推流时,AsyncTaskExecutor 用于阻塞写入,也用于执行从控制器方法返回的 Callable。强烈建议配置这个属性,如果您使用响应式类型推流或有控制器方法返回 Callable,默认情况下它是 SimpleAsyncTaskExecutor

  • DeferredResultProcessingInterceptorCallableProcessingInterceptor

  • 注意,默认的超时值也可以在 DeferredResultResponseBodyEmitterSseEmitter 上设置。对于 Callable,使用 WebAsyncTask 来提供超时值。

    出于安全原因,浏览器禁止 AJAX 调用当前源之外的资源。例如,您可以在一个选项卡中使用您的银行帐户,在另一个选项卡中使用 evil.com。来自 evil.com 的脚本不能用您的凭证向您的银行 API 发出 AJAX 请求,例如从您的帐户中提取资金!

    跨源资源共享(CORS,Cross-Origin Resource Sharing)是由大多数浏览器实现的 W3C 规范,允许您指定哪些类型的跨域请求是被授权的,而不是使用基于 IFRAME 或 JSONP 的不安全且功能较差的解决方案。

    CORS 规范区分了 preflightsimpleactual 请求。要了解 CORS 的工作原理,您可以阅读本文,也可以阅读其他的文章,或者参考规范以了解更多细节。

    Spring MVC HandlerMapping 提供了对 CORS 的内置支持。在成功地将请求映射到处理器之后,HandlerMapping 将检查给定请求和处理器的 CORS 配置,并采取进一步的操作。当 simpleactual CORS 请求被拦截、验证和要求 CORS 响应头集时,就可以直接处理 preflight 请求。

    为了启用跨源请求(例如,源报文头是存在的,并且与请求的主机不同),您需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝 preflight 请求。在 simpleactual CORS 请求的响应中没有添加 CORS 头,因此浏览器拒绝它们。

    每个 HandlerMapping 都可以通过基于 URL 模式的 CorsConfiguration 映射单独配置。在大多数情况下,应用程序将使用 MVC Java config 或 XML 命名空间来声明这样的映射,这将导致一个单一的全局映射传递给所有的 HadlerMappping

    HandlerMapping 级别上的全局 CORS 配置可以与更细粒度的、处理器级别的 CORS 配置相结合。例如,带注解的控制器可以使用类或方法级的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。

    组合全局和本地配置的规则通常是合并——比如所有全局的和本地的源。对于那些只接受一个值的属性,比如 allowCredentialsmaxAge,本地覆盖了全局值。有关更多细节,请参见 CorsConfiguration#combine(CorsConfiguration)

    要了解更多的源代码或进行高级定制,请检查:

  • CorsConfiguration
  • CorsProcessorDefaultCorsProcessor
  • AbstractHandlerMapping
  • @CrossOrigin

    @CrossOrigin 注解允许对被注解的控制器方法进行跨源请求:

    @RestController
    @RequestMapping("/account")
    public class AccountController {
        @CrossOrigin
        @GetMapping("/{id}")
        public Account retrieve(@PathVariable Long id) {
            // ...
        @DeleteMapping("/{id}")
        public void remove(@PathVariable Long id) {
            // ...
    

    默认情况下 @CrossOrigin 允许:

  • 所有报文头。
  • 控制器方法映射到的所有 HTTP 方式。
  • 默认情况下,allowedCredentials 不被启用,因为它建立了一个信任级别,它公开了敏感的用户特定信息,如 cookie 和 CSRF 令牌,并且应该在适当的地方使用。
  • maxAge 被设置为 30 分钟。

    @CrossOrigin 在类级别上也得到支持,并继承给所有方法:

    @CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
    @RestController
    @RequestMapping("/account")
    public class AccountController {
        @GetMapping("/{id}")
        public Account retrieve(@PathVariable Long id) {
            // ...
        @DeleteMapping("/{id}")
        public void remove(@PathVariable Long id) {
            // ...
    

    可以在类和方法级别使用 CrossOrigin

    @CrossOrigin(maxAge = 3600)
    @RestController
    @RequestMapping("/account")
    public class AccountController {
        @CrossOrigin("http://domain2.com")
        @GetMapping("/{id}")
        public Account retrieve(@PathVariable Long id) {
            // ...
        @DeleteMapping("/{id}")
        public void remove(@PathVariable Long id) {
            // ...
    

    除了细粒度的控制器方法级别配置之外,您还可能需要定义一些全局 CORS 配置。您可以在任何 HandlerMapping 上单独设置基于 url 的 CorsConfiguration 映射。然而,大多数应用程序将使用 MVC Java config 或 MVC XNM 命名空间来实现这一点。

    默认的全局配置允许如下:

  • 所有报文头。
  • GET、HEAD 和 POST 方法。
  • 默认情况下,allowedCredentials 不被启用,因为它建立了一个信任级别,它公开了敏感的用户特定信息,如 cookie 和 CSRF 令牌,并且应该在适当的地方使用。
  • maxAge 被设置为 30 分钟。

    Java 配置

    要在 MVC Java 配置启用 CORS,使用 CorsRegistry 回调:

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/api/**")
                .allowedOrigins("http://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600);
            // Add more mappings...
    

    XML 配置

    要在 XML 命名空间开启 CORS,使用 <mvc:cors> 元素:

    <mvc:cors>
        <mvc:mapping path="/api/**"
            allowed-origins="http://domain1.com, http://domain2.com"
            allowed-methods="GET, PUT"
            allowed-headers="header1, header2, header3"
            exposed-headers="header1, header2" allow-credentials="true"
            max-age="123" />
        <mvc:mapping path="/resources/**"
            allowed-origins="http://domain1.com" />
    </mvc:cors>
    

    CORS 过滤器

    你可以通过内置的 CorsFilter 应用 CORS 支持:

    如果你尝试与 Spring Security 一起使用 CorsFilter,请记住,Spring Security 已经内置了对 CORS 的支持。

    要配置该过滤器,传递 CorsConfigurationSource 给它的构造函数:

    CorsConfiguration config = new CorsConfiguration();
    // Possibly...
    // config.applyPermitDefaultValues()
    config.setAllowCredentials(true);
    config.addAllowedOrigin("http://domain1.com");
    config.addAllowedHeader("");
    config.addAllowedMethod("");
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    CorsFilter filter = new CorsFilter(source);
    

    Web 安全

    Spring Security 项目为保护 web 应用程序免遭恶意攻击提供了支持。查看 Spring Security 参考文档,包括:

    Spring MVC Security
    Spring MVC Test Support
    CSRF protection
    Security Response Headers

    HDIV 是另一个与 Spring MVC 集成的 web 安全框架。

    HTTP 缓存

    好的 HTTP 缓存策略可以显著提高 web 应用程序的性能和客户端的体验。Cache-Control HTTP 响应头主要负责这个,以及 Last-ModifiedETag 等条件报文头。

    Cache-Control HTTP 响应头建议私有缓存(例如浏览器)和公共缓存(例如代理)如何缓存 HTTP 响应以供进一步重用。

    ETag(实体标记)是 HTTP/1.1 兼容的 web 服务器返回的 HTTP 响应头,用于确定给定 URL 中的内容变化。它可以被认为是 Last-Modified 报文头的更复杂的继承。当一个服务器返回一个带有 ETag 报文头的表示时,客户端可以在随后的 GET 中使用这个报文头,在 If-None-Match 报文头中。如果内容没有改变,服务器返回 304Not Modified

    本节描述在 Spring Web MVC 应用程序中配置 HTTP 缓存的不同选择。

    Cache-Control

    Spring Web MVC 支持许多用例和配置应用程序的 Cache-Control 头的方法。尽管 RFC 7234 Section 5.2.2 完全描述了报文头及其可能的指令,但是有几种方法可以解决最常见的情况。

    Spring Web MVC 在几个 API 中使用配置约定:setCachePeriod(int seconds)

    -1 值不会生成 Cache-Control 响应头。 0 值将使用 Cache-Control: no-store 指令阻止缓存。 n > 0 值将使用 Cache-Control: max-age=n 指令缓存给定的 n 秒响应。

    CacheControl 构建器类简单地描述了可用的 Cache-Control 指令,并使构建自己的 HTTP 缓存策略变得更加容易。一旦构建,一个 CacheControl 实例就可以在几个 Spring Web MVC API 中作为参数被接收。

    // Cache for an hour - "Cache-Control: max-age=3600"
    CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
    // Prevent caching - "Cache-Control: no-store"
    CacheControl ccNoStore = CacheControl.noStore();
    // Cache for ten days in public and private caches,
    // public caches should not transform the response
    // "Cache-Control: max-age=864000, public, no-transform"
    CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
                                        .noTransform().cachePublic();
    

    静态资源应该使用适当的 Cache-Control 和条件性的报文头来实现最佳性能。配置 ResourceHttpRequestHandler 用于服务静态资源,不仅可以通过读取文件的元数据来编写 Last-Modified 头,而且如果配置适当,还可以使用 Cache-Control 头。

    您可以在 ResourceHttpRequestHandler 上设置 cachePeriod 属性,或者使用 CacheControl 实例,它支持更具体的指令:

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/resources/**")
                    .addResourceLocations("/public-resources/")
                    .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
    

    在 XML 中:

    <mvc:resources mapping="/resources/**" location="/public-resources/">
        <mvc:cache-control max-age="3600" cache-public="true"/>
    </mvc:resources>
    

    @Controller 缓存

    控制器可以支持 Cache-ControlETag 和/或 If-Modified-Since HTTP 请求;如果要在响应中设置 Cache-Control 头,这确实是推荐的。这涉及计算给定请求的 lastModified long 和/或 Etag 值,将其与 If-Modified-Since 请求头值进行比较,并可能返回响应状态代码 304Not Modified)。

    正如在 HttpEntity 中描述的那样,控制器可以使用 HttpEntity 类型与请求/响应进行交互。返回 ResponseEntity 的控制器可以在响应中包含 HTTP 缓存信息:

    @GetMapping("/book/{id}")
    public ResponseEntity<Book> showBook(@PathVariable Long id) {
        Book book = findBook(id);
        String version = book.getVersion();
        return ResponseEntity
                    .ok()
                    .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
                    .eTag(version) // lastModified is also available
                    .body(book);
    

    这样做不仅会在响应中包含 ETagCache-Control 头,它还会将响应转换为空响应体的 HTTP 304 Not Modified 响应,如果客户机发送的条件头与控制器所设置的缓存信息相匹配。

    @RequestMapping 方法也可能希望支持相同的行为。这可以如下实现:

    @RequestMapping
    public String myHandleMethod(WebRequest webRequest, Model model) {
        long lastModified = // 1. application-specific calculation
        if (request.checkNotModified(lastModified)) {
            // 2. shortcut exit - no further processing necessary
            return null;
        // 3. or otherwise further request processing, actually preparing content
        model.addAttribute(...);
        return "myViewName";
    

    这里有两个关键元素:调用 request.checkNotModified(lastModified) 和返回 null。前者在返回 true 之前设置适当的响应状态和头。后者与前者结合,导致 Spring MVC 不再处理请求。

    注意这里有三个变体:

  • request.checkNotModified(lastModified) 将 lastModified 与 If-Modified-Since 或 If-Unmodified-Since 请求头进行比较
  • request.checkNotModified(eTag) 将 eTag 与 If-None-Match 请求头进行比较
  • request.checkNotModified(eTag, lastModified) 两者都做,这意味着两个条件都应该是有效的
  • 当接收条件性的 GET/HEAD 请求时,checkNotModified 将检查资源是否被修改,如果是这样,它将导致 HTTP 304 Not Modified 响应。如果是条件性的 POST/PUT/DELETE 请求,checkNotModified 将检查该资源是否已被修改,如果它已经被修改,它将导致 HTTP 409 Precondition Failed 响应,以防止并发修改。

    Etag 过滤器

    ETags 的支持由 Servlet 过滤器 ShallowEtagHeaderFilter 提供。它是一个普通的 Servlet 过滤器,因此可以与任何 web 框架结合使用。ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并生成一个 MD5 散列来作为 ETag 头发送,从而创建所谓的浅层 ETags。下一次客户端发送相同资源的请求时,它将使用该散列作为 If-None-Match 值。过滤器检测到这一点,让请求像往常一样处理,最后比较两个哈希。如果它们相等,则返回 304

    请注意,此策略节省了网络带宽,而不是 CPU,因为必须为每个请求计算完整的响应。上面描述的控制器级别的其他策略可以避免计算。

    这个过滤器有一个 writeWeakETag 参数,它配置过滤器来编写弱的 ETags,比如这个:W/"02a2d595e6ed9a0b24f027f2b63b134d6",这是在 RFC 7232 Section 2.3 中定义的。

    在 Spring MVC 中使用视图技术是可插拨的,无论您决定使用 Thymeleaf、Groovy Markup Template、JSP 或其他,主要都是配置更改的问题。本章涵盖了与 Spring MVC 集成的视图技术。我们假设您已经熟悉了视图解析。

    Thymeleaf

    Thymeleaf 是一个现代的服务器端 Java 模板引擎,它强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对 UI 模板的独立工作非常有帮助,例如由设计者,而不需要运行的服务器。如果您想要替换 JSP,Thymeleaf 提供了一组最广泛的特性,这将使这种转换更加容易。Thymeleaf 是积极开发和维护的。要获得更完整的介绍,请参见 Thymeleaf 项目主页。

    Thymeleaf 与 Spring MVC 的集成是由 Thymeleaf 项目管理的。该配置涉及一些 bean 声明,如 ServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver。参见 Thymeleaf+Spring 了解更多细节。

    FreeMarker

    Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件等各种文本输出。Spring 框架有一个内置的集成,可以使用 FreeMarker 模板使用 Spring MVC。

    视图配置 View config

    要把 FreeMarker 配置为视图技术:

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            registry.freemarker();
        // Configure FreeMarker...
        @Bean
        public FreeMarkerConfigurer freeMarkerConfigurer() {
            FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
            configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
            return configurer;
    

    要在 XML 做同样的配置:

    <mvc:annotation-driven/>
    <mvc:view-resolvers>
        <mvc:freemarker/>
    </mvc:view-resolvers>
    <!-- Configure FreeMarker... -->
    <mvc:freemarker-configurer>
        <mvc:template-loader-path location="/WEB-INF/freemarker"/>
    </mvc:freemarker-configurer>
    

    你也可以声明 FreeMarkerConfigurer bean 来掌控所有特性:

    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
    </bean>
    

    您的模板需要存储在上面所示的 FreeMarkerConfigurer 指定的目录中。如果您的控制器返回视图名 welcome,那么解析器将查找 /WEB-INF/freemarker/welcome.ftl 模板。

    FreeMarker 配置

    FreeMarker 的 SettingsSharedVariables 可以通过在 FreeMarkerConfigurer bean 上设置适当的 bean 属性,直接传递到 Spring 的 FreeMarker 配置对象。freemarkerSettings 属性需要一个 java.util.Properties 对象以及 freemarkerVariables 属性需要一个 java.util.Map

    <bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
        <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
        <property name="freemarkerVariables">
                <entry key="xml_escape" value-ref="fmXmlEscape"/>
        </property>
    </bean>
    <bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
    

    有关适用于配置对象的设置和变量的详细信息,请参见 FreeMarker 文档。

    Form handling

    Groovy Markup

    Groovy Markup Template Engine 的主要目的是生成类似 XML 的标记(XML、XHTML、HTML5 等),但它可以用于生成任何基于文本的内容。Spring 框架有一个内置的集成,可以结合 Groovy Markup 来使用 Spring MVC。

    Groovy Markup Template Engine 需要 Groovy 2.3.1+。

    要配置 Groovy Markup Template Engine:

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            registry.groovy();
        // Configure the Groovy Markup Template Engine...
        @Bean
        public GroovyMarkupConfigurer groovyMarkupConfigurer() {
            GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
            configurer.setResourceLoaderPath("/WEB-INF/");
            return configurer;
    

    要在 XML 进行同样的配置:

    <mvc:annotation-driven/>
    <mvc:view-resolvers>
        <mvc:groovy/>
    </mvc:view-resolvers>
    <!-- Configure the Groovy Markup Template Engine... -->
    <mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
    

    与传统的模板引擎不同,Groovy Markup 依赖于使用生成器语法的 DSL。下面是一个 HTML 页面的模板示例:

    yieldUnescaped '<!DOCTYPE html>'
    html(lang:'en') {
        head {
            meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
            title('My page')
        body {
            p('This is an example of HTML contents')
    

    脚本视图 Script Views

    Spring 框架有一个内置的集成,可以使用 Spring MVC 和任何可以在 JSR-223 Java 脚本引擎上运行的模板库。下面是我们在不同的脚本引擎上测试过的模板库列表:

    Handlebars | Nashorn
    Mustache | Nashorn
    React | Nashorn
    EJS | Nashorn
    ERB | JRuby
    String templates | Jython
    Kotlin Script templating | Kotlin

    集成任何其他脚本引擎的基本规则是,它必须实现 ScriptEngineInvocable 接口。

    您需要在类路径上有脚本引擎:

  • Nashorn JavaScript 引擎在 Java 8+ 提供。强烈推荐使用最新的更新版本。
  • 应该将 JRuby 添加为 Ruby 支持的依赖项。
  • 应该将 Jython 添加为 Python 支持的依赖项。
  • org.jetbrains.kotlin:kotlin-script-util 依赖和 META-INF/services/javax.script.ScriptEngineFactory 文件包含 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 行应该被添加,以支持 Kotlin 脚本,请参见本示例了解更多细节。

    您需要使用脚本模板库。使用 Javascript 的一种方法是通过 WebJars。

    声明一个 ScriptTemplateConfigurer bean,以指定要使用的脚本引擎、加载脚本文件、调用什么函数来呈现模板,等等。下面是一个带有 Mustache 模板的例子,还有一个来自 Nashorn JavaScript 引擎:

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            registry.scriptTemplate();
        @Bean
        public ScriptTemplateConfigurer configurer() {
            ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
            configurer.setEngineName("nashorn");
            configurer.setScripts("mustache.js");
            configurer.setRenderObject("Mustache");
            configurer.setRenderFunction("render");
            return configurer;
    

    在 XML 里:

    <mvc:annotation-driven/>
    <mvc:view-resolvers>
        <mvc:script-template/>
    </mvc:view-resolvers>
    <mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
        <mvc:script location="mustache.js"/>
    </mvc:script-template-configurer>
    

    控制器看起来没什么区别:

    @Controller
    public class SampleController {
        @GetMapping("/sample")
        public String test(Model model) {
            model.addObject("title", "Sample title");
            model.addObject("body", "Sample body");
            return "template";
    

    Mustache 模板像这样:

    <title>{{title}}</title> </head> <p>{{body}}</p> </body> </html>

    渲染函数调用的参数如下:

    String template:模板内容。

    Map model:视图模型。

    RenderingContext renderingContext:提供对应用程序上下文、语言环境、模板加载程序和 url 的访问的 RenderingContext (从 5.0 开始)

    Mustache.render() 是与此签名相兼容的,因此您可以直接调用它。

    如果您的模板技术需要一些定制,您可以提供一个实现自定义呈现函数的脚本。例如,Handlerbars 需要在使用模板之前编译它,并且需要一个 polyfill 来模拟服务器端脚本引擎中不可用的一些浏览器工具。

    @Configuration
    @EnableWebMvc
    public class WebConfig implements WebMvcConfigurer {
        @Override
        public void configureViewResolvers(ViewResolverRegistry registry) {
            registry.scriptTemplate();
        @Bean
        public ScriptTemplateConfigurer configurer() {
            ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
            configurer.setEngineName("nashorn");
            configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
            configurer.setRenderFunction("render");
            configurer.setSharedEngine(false);
            return configurer;
    

    在使用非线程安全的脚本引擎时,需要将 sharedEngine 属性设置为 false,这些脚本引擎使用的模板库不是为并发设计的,比如 Nashorn 上运行的 Handlebars 或 React。由于这个 Bug,需要使用 Java 8u60 或更高版本。

    polyfill.js 仅定义了 Handlebars 需要正确运行的窗口对象:

    var window = {};
    

    这个基本的 render.js 实现在使用模板之前编译它。生产就绪的实现还应该存储和重用缓存的模板/预编译模板。这可以在脚本方面完成,也可以在您需要的任何定制(例如管理模板引擎配置)上完成。

    function render(template, model) {
        var compiledTemplate = Handlebars.compile(template);
        return compiledTemplate(model);
    

    查看 Spring 框架单元测试、javaresources,了解更多配置示例。

    JSP & JSTL

    Spring 框架有一个内置的集成,可以结合 JSP 和 JSTL 使用 Spring MVC。

    视角解析器 View resolvers

    在使用 JSP 进行开发时,可以声明一个 InternalResourceViewResolverResourceBundleViewResolver bean。

    ResourceBundleViewResolver 依赖一个属性文件来定义映射到类和 URL 的视图名称。使用 ResourceBundleViewResolver,您可以使用一个解析器混合不同类型的视图。这是一个例子:

    <!-- the ResourceBundleViewResolver -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
        <property name="basename" value="views"/>
    </bean>
    # And a sample properties file is uses (views.properties in WEB-INF/classes):
    welcome.(class)=org.springframework.web.servlet.view.JstlView
    welcome.url=/WEB-INF/jsp/welcome.jsp
    productList.(class)=org.springframework.web.servlet.view.JstlView
    productList.url=/WEB-INF/jsp/productlist.jsp
    

    InternalResourceBundleViewResolver 也可以用于 JSP。作为一种最佳实践,我们强烈建议将 JSP 文件放置在 WEB-INF 目录下的目录中,这样客户就无法直接访问。

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    

    JSP 与 JSTL

    在使用 Java Standard Tag Library 时,必须使用一个特殊的视图类 JstlView,因为 JSTL 需要一些准备工作,比如 I18N 特性将会起作用。

    Spring 的 JSP 标签库

    Spring 提供了对请求参数的数据绑定,如前面章节所述。为了促进 JSP 页面的开发,结合这些数据绑定特性,Spring 提供了一些标签,使事情变得更加容易。所有 Spring 标记都有 html 转义功能,以启用或禁用字符转义。

    spring-webmvc.jar 中包含 spring.tld 标记库描述符(TLD)。对于单个标记的全面引用,浏览 API 引用或查看标记库描述。

    Spring 的表单标签库

    在 2.0 版本中,Spring 提供了一套完整的数据绑定标签,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都支持相应的 HTML 标记对应的属性集,使这些标记更加熟悉和直观。标签生成的 HTML 是 HTML 4.01/XHTML 1.0 兼容的。

    与其他表单/输入标记库不同,Spring 的表单标记库与 Spring Web MVC 集成,使标签能够访问控制器处理的命令对象和引用数据。正如您将在以下示例中看到的,表单标记使 JSP 更易于开发、读取和维护。

    让我们看一下表单标签,看看每个标签是如何使用的。我们已经包含了生成的 HTML 片段,其中某些标签需要进一步的注释。

    这个表单标记库绑定在 spring-webmvc.jar 中。库描述符称为 spring-form.tld

    要使用此库中的标记,请将以下指令添加到 JSP 页面的顶部:

    <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    

    其中 form 是您想要从这个库中使用的标记名称前缀。

    form 标签

    此标记渲染 HTML form 标记,并公开绑定路径到内部标记以进行绑定。它将命令对象放在 PageContext 中,这样就可以通过内部标记访问命令对象。该库中的所有其他标记都是表单标记的嵌套标记。

    假设我们有一个名为 User 的域对象。它是一个具有诸如 firstNamelastName 等属性的 JavaBean。我们将使用它作为表单控制器的表单支持对象,该对象返回 form.jsp。下面是一个例子 form.jsp 看起来像这样:

    <form:form>
        <table>
                <td>First Name:</td>
                <td><form:input path="firstName"/></td>
                <td>Last Name:</td>
                <td><form:input path="lastName"/></td>
                <td colspan="2">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form:form>
    

    firstNamelastName 值是从页面控制器放置在 PageContext 中的命令对象中检索出来的。请继续阅读,以查看更复杂的示例,说明如何将内部标记与表单标记一起使用。

    生成的 HTML 看起来像一个标准表单:

    <form method="POST">
        <table>
                <td>First Name:</td>
                <td><input name="firstName" type="text" value="Harry"/></td>
                <td>Last Name:</td>
                <td><input name="lastName" type="text" value="Potter"/></td>
                <td colspan="2">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form>
    

    前面的 JSP 假定表单支持对象的变量名是 command。如果将表单备份对象放入另一个名称(绝对是最佳实践)的模型中,则可以将表单绑定到命名变量,如下所示:

    <form:form modelAttribute="user">
        <table>
                <td>First Name:</td>
                <td><form:input path="firstName"/></td>
                <td>Last Name:</td>
                <td><form:input path="lastName"/></td>
                <td colspan="2">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form:form>
    

    input 标签

    此标记在默认情况下使用绑定值和 type='text' 渲染 HTML input 标签。对于这个标记的示例,请参见表单标记。从 Spring 3.1 开始,您可以使用其他类型,比如 emailteldate 和其他类型。

    checkbox 标签

    这个标签渲染一个类型为 checkbox 的 HTML input 标签。

    让我们假设我们的 User 有一些偏好,如时事通讯订阅和兴趣爱好列表。下面是 Preferences 类的一个示例:

    public class Preferences {
        private boolean receiveNewsletter;
        private String[] interests;
        private String favouriteWord;
        public boolean isReceiveNewsletter() {
            return receiveNewsletter;
        public void setReceiveNewsletter(boolean receiveNewsletter) {
            this.receiveNewsletter = receiveNewsletter;
        public String[] getInterests() {
            return interests;
        public void setInterests(String[] interests) {
            this.interests = interests;
        public String getFavouriteWord() {
            return favouriteWord;
        public void setFavouriteWord(String favouriteWord) {
            this.favouriteWord = favouriteWord;
    

    form.jsp 看起来像这样:

    <form:form>
        <table>
                <td>Subscribe to newsletter?:</td>
                <%-- Approach 1: Property is of type java.lang.Boolean --%>
                <td><form:checkbox path="preferences.receiveNewsletter"/></td>
                <td>Interests:</td>
                <%-- Approach 2: Property is of an array or of type java.util.Collection --%>
                    Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
                    Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
                    Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
                <td>Favourite Word:</td>
                <%-- Approach 3: Property is of type java.lang.Object --%>
                    Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
        </table>
    </form:form>
    

    checkbox 标签有 3 种方法可以满足您的复选框需要。

  • 方法一——当绑定值为 java.lang.Boolean 类型。如果绑定值为 true,则将 inputcheckbox)标记为 checked。值属性对应于 setValue(Object) 值属性的解析值。
  • 方法二——当绑定值属于类型数组或 java.util.Collection。如果已配置的 setValue(Object) 值存在于绑定 Collection 中,则将 inputcheckbox)标记为 checked
  • 方法3——对于任何其他绑定值类型,如果配置的 setValue(Object) 等于绑定值,则将 inputcheckbox)标记为 checked
  • 注意,无论采用哪种方法,都会生成相同的 HTML 结构。下面是一些复选框的 HTML 片段:

    <td>Interests:</td> Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/> <input type="hidden" value="1" name="_preferences.interests"/> Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/> <input type="hidden" value="1" name="_preferences.interests"/> Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/> <input type="hidden" value="1" name="_preferences.interests"/>

    您可能不希望看到的是每个复选框之后的附加隐藏字段。当没有选中 HTML 页面中的复选框时,当表单提交时,它的值将不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要在 HTML 中使用一个解决方案来实现 Spring 表单数据绑定的工作。复选框标记遵循现有的 Spring 约定,其中包括为每个复选框添加一个下划线(_)的隐藏参数。通过这样做,您可以有效地告诉 Spring:“复选框在窗体中是可见的,我希望我的对象能够在表单数据中绑定到复选框的状态,无论它是什么”。

    checkboxes 标签

    这个标签使用类型 checkbox 渲染多个HTML input 标签。

    基于前面的 checkbox 标签部分构建示例。有时,您不希望在 JSP 页面中列出所有可能的爱好。您更愿意在可用选项的运行时提供一个列表,并将其传递给标记。这就是 checkboxes 标记的目的。您传入一个数组、一个列表或一个包含 items 属性中可用选项的 Map。通常,绑定属性是一个集合,因此它可以保存用户选择的多个值。下面是使用此标记的 JSP 示例:

    <form:form>
        <table>
                <td>Interests:</td>
                    <%-- Property is of an array or of type java.util.Collection --%>
                    <form:checkboxes path="preferences.interests" items="${interestList}"/>
        </table>
    </form:form>
    

    这个例子假设 interestList 是一个列表,它是一个模型属性,包含要从中选择的值的字符串。在使用 Map 的情况下,将使用 Map entry key 作为值,而 Map entry 的值将用作显示的 label。您还可以使用自定义对象,在其中您可以使用 itemValue 提供属性名称,使用 itemLabel 提供 label。

    radiobutton 标签

    这个标签用类型 radio 渲染一个 HTML input 标签。

    典型的使用模式将涉及绑定到同一属性的多个标记实例,但具有不同的值。

    <td>Sex:</td> Male: <form:radiobutton path="sex" value="M"/> <br/> Female: <form:radiobutton path="sex" value="F"/>

    radiobuttons 标签

    这个标签用类型 radio 渲染多个 HTML input 标签。

    就像上面的 checkboxes 标签一样,您可能希望将可用选项作为运行时变量传递。对于这个用法,您将使用 radiobuttons 标签。您传入一个数组、一个列表或一个包含 items 属性中可用选项的 Map。在使用 Map 的情况下,将使用 Map entry key 作为值,而 Map entry 的值将用作显示的 label。您还可以使用自定义对象,在其中您可以使用 itemValue 提供属性名称,使用 itemLabel 提供 label。

    <td>Sex:</td> <td><form:radiobuttons path="sex" items="${sexOptions}"/></td>

    password 标签

    这个标签使用绑定值渲染一个带有类型 password 的 HTML input 标签。

    <td>Password:</td> <form:password path="password"/>

    请注意,默认情况下,没有显示密码值。如果您想要显示密码值,那么将 showPassword 属性的值设置为 true,就像这样。

    <td>Password:</td> <form:password path="password" value="^76525bvHGq" showPassword="true"/>

    select 标签

    这个标记渲染一个 HTML select 元素。它支持对所选选项的数据绑定以及嵌套选项和选项标记的使用。

    让我们假设用户有一个技能列表。

    <td>Skills:</td> <td><form:select path="skills" items="${skills}"/></td>

    如果 User 的技能在 Herbology 中,Skills 行的 HTML 来源将是:

    <td>Skills:</td> <select name="skills" multiple="true"> <option value="Potions">Potions</option> <option value="Herbology" selected="selected">Herbology</option> <option value="Quidditch">Quidditch</option> </select>

    option 标签

    此标记渲染一个HTML option。它根据绑定值设置 selected

    <td>House:</td> <form:select path="house"> <form:option value="Gryffindor"/> <form:option value="Hufflepuff"/> <form:option value="Ravenclaw"/> <form:option value="Slytherin"/> </form:select>

    如果 User 的房子在 Gryffindor,那么 House 行的 HTML 来源如下:

    <td>House:</td> <select name="house"> <option value="Gryffindor" selected="selected">Gryffindor</option> <option value="Hufflepuff">Hufflepuff</option> <option value="Ravenclaw">Ravenclaw</option> <option value="Slytherin">Slytherin</option> </select>

    options 标签

    此标记渲染 HTML option 标记的列表。它根据绑定值设置 selected 属性。

    <td>Country:</td> <form:select path="country"> <form:option value="-" label="--Please Select"/> <form:options items="${countryList}" itemValue="code" itemLabel="name"/> </form:select>

    如果 User 居住在英国,Country 行的 HTML 来源如下:

    <td>Country:</td> <select name="country"> <option value="-">--Please Select</option> <option value="AT">Austria</option> <option value="UK" selected="selected">United Kingdom</option> <option value="US">United States</option> </select>

    如示例所示,选项标签与选项标签的组合使用生成了相同的标准 HTML,但允许您显式地在 JSP 中指定一个值,该值只用于显示(在其所属的地方),如示例中的默认字符串:“-- Please Select”。

    items 属性通常包含一个条目对象的集合或数组。itemValueitemLabel 仅仅是指那些项目对象的 bean 属性,如果指定的话;否则,条目对象本身将被字符串化。或者,您可以指定项目的映射,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果还指定 itemValue 和/或 itemLabel,则项目值属性将应用于 map 键,项目标签属性将应用于 map 值。

    textarea 标签

    这个标记渲染一个HTML textarea

    <td>Notes:</td> <td><form:textarea path="notes" rows="3" cols="20"/></td> <td><form:errors path="notes"/></td>

    hidden 标签

    这个标签使用绑定值渲染一个 hidden 的 HTML input 标签。要提交一个未绑定的隐藏值,请使用带有 hidden 类型的 HTML input 标记。

    <form:hidden path="house"/>
    

    如果我们选择将 house 值作为隐藏的值提交,那么 HTML 看起来像这样:

    <input name="house" type="hidden" value="Gryffindor"/>
    

    errors 标签

    此标记在 HTML span 标记中渲染字段错误。它提供了对控制器中创建的错误的访问,或者由与控制器关联的任何验证器创建的错误。

    假设我们希望在提交表单时显示 firstNamelastName 字段的所有错误消息。我们有一个名为 UserValidatorUser 类实例的验证器。

    public class UserValidator implements Validator {
        public boolean supports(Class candidate) {
            return User.class.isAssignableFrom(candidate);
        public void validate(Object obj, Errors errors) {
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
    

    form.jsp 看起来像这样:

    <form:form>
        <table>
                <td>First Name:</td>
                <td><form:input path="firstName"/></td>
                <%-- Show errors for firstName field --%>
                <td><form:errors path="firstName"/></td>
                <td>Last Name:</td>
                <td><form:input path="lastName"/></td>
                <%-- Show errors for lastName field --%>
                <td><form:errors path="lastName"/></td>
                <td colspan="3">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form:form>
    

    如果我们在 firstNamelastName 字段中提交一个空值的表单,这就是 HTML 的样子:

    <form method="POST">
        <table>
                <td>First Name:</td>
                <td><input name="firstName" type="text" value=""/></td>
                <%-- Associated errors to firstName field displayed --%>
                <td><span name="firstName.errors">Field is required.</span></td>
                <td>Last Name:</td>
                <td><input name="lastName" type="text" value=""/></td>
                <%-- Associated errors to lastName field displayed --%>
                <td><span name="lastName.errors">Field is required.</span></td>
                <td colspan="3">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form>
    

    如果我们想要显示给定页面的整个错误列表,该怎么办?下面的示例显示了 errors 标签也支持一些基本的通配符功能。

    path="*"——显示所有错误。

    path="lastName"——显示与 lastName 字段相关的所有错误。

    如果路径被省略——只显示对象错误。

    下面的示例将显示在页面顶部的错误列表,字段特定的错误显示在字段旁边:

    <form:form>
        <form:errors path="*" cssClass="errorBox"/>
        <table>
                <td>First Name:</td>
                <td><form:input path="firstName"/></td>
                <td><form:errors path="firstName"/></td>
                <td>Last Name:</td>
                <td><form:input path="lastName"/></td>
                <td><form:errors path="lastName"/></td>
                <td colspan="3">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form:form>
    

    HTML 会看起来像这样:

    <form method="POST">
        <span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
        <table>
                <td>First Name:</td>
                <td><input name="firstName" type="text" value=""/></td>
                <td><span name="firstName.errors">Field is required.</span></td>
                <td>Last Name:</td>
                <td><input name="lastName" type="text" value=""/></td>
                <td><span name="lastName.errors">Field is required.</span></td>
                <td colspan="3">
                    <input type="submit" value="Save Changes"/>
        </table>
    </form>
    

    spring-form.tld 标签库描述符(TLD)被包含在 spring-webmvc.jar 中。对于单个标记的全面引用,浏览 API 引用或查看标记库描述。

    HTTP 方式转换

    REST 的一个关键原则是使用统一接口。这意味着所有资源(URL)都可以使用相同的四个 HTTP 方法进行操作:GETPUTPOSTDELETE。对于每种方法,HTTP 规范定义了确切的语义。例如,GET 应该始终是一个安全的操作,这意味着它没有副作用,PUTDELETE 应该是幂等的,这意味着您可以一次又一次地重复这些操作,但是最终结果应该是相同的。虽然 HTTP 定义了这四种方法,但 HTML 只支持两种方法:GETPOST。幸运的是,有两种可能的解决方案:您可以使用 JavaScript 来执行 PUTDELETE,或者简单地使用 real 方法作为附加参数(以 HTML 表单中的隐藏 input 字段建模)。后一个技巧是 Spring 的 HiddenHttpMethodFilter 所做的。这个过滤器是一个普通的 Servlet 过滤器,因此它可以与任何 web 框架(不仅仅是 Spring MVC)结合使用。只需将此过滤器添加到您的 web.xml,这样带有隐藏 _method 参数的 POST 将被转换为相应的 HTTP 方法请求。

    为了支持 HTTP 方法转换,Spring MVC 表单标记被更新以支持设置 HTTP 方法。例如,下面的片段取自最新的 Petclinic 示例:

    <form:form method="delete">
        <p class="submit"><input type="submit" value="Delete Pet"/></p>
    </form:form>
    

    这将实际执行一个 HTTP