Spring Framework 5 MVC 官方手册译文

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:

mvc-context-hierarchy

如下是配置 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 类型 说明
HandlerMapping 将请求映射到处理器,以及一系列前置和后置的处理拦截器,处理器的具体细节由符合一定标准的 HandlerMapping 的实现决定。
两个主要的实现:支持 @RequestMapping 注解的 RequestMappingHandlerMapping;URI 路径精确配置的 SimpleUrlHandlerMapping
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_ATTRIBUTE
  • Locale 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 实例。下表列出了所支持的参数。

参数 说明
contextClass WebApplicationContext 实现类,为 Servlet 初始化了上下文环境,默认为 XmlWebApplicationContext
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 描述
SimpleMappingExceptionResolver 异常类名和 Error 视图名映射。在浏览器程序渲染 error 页面可用
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>
    
    属性 默认值 描述
    cookieName classname + LOCALE cookie 的名称
    cookieMaxAge Servlet 容器默认值 cookie 在客户端保持持久化的最大时间。如果指定为 -1,cookie 不会被持久化,它只会在客户端关闭浏览器前有效
    cookiePath / 限制 cookie 只在你的站点的某些部分可见。当指定 cookiePath 时,cookie 只能在这个路径和这个路径之下可见
  • 会话解析器

    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">
          <list>
              <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"%>
    <html>
      <head>
          <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 提供:

    描述
    FixedThemeResolver 通过设置 defaultThemeName 属性,选择一个固定的主题
    SessionThemeResolver 主题保留在用户的 HTTP 会话中。每个会话只需要设置一次,但是不能在两个会话之间持久化
    CookieThemeResolver 被选择的主题存储在客户端的 cookie 中

    Spring 还提供了一个 ThemeChangeInterceptor,它允许对每个请求使用一个简单的请求参数进行主题更改。

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

    控制器方法参数 描述
    WebRequest, NativeWebRequest 对请求参数、请求、会话属性的通用访问,不直接使用 Servlet API
    javax.servlet.ServletRequest, javax.servlet.ServletResponse 选择任意特定的请求或响应类型 —— 如:ServletRequestHttpServletRequest,或 Spring 的 MultipartRequestMultipartHttpServletRequest
    javax.servlet.http.HttpSession 强制会话的存在。因此,该参数不可能为 null
    注意:会话访问不是线程安全的。如果多个请求被允许并发地访问会话,考虑设置 RequestMappingHandlerAdaptersynchronizeOnSession 标志为 true
    javax.servlet.http.PushBuilder Servlet 4.0 push builder API,用来编程式 HTTP/2 资源推送。注意,每个 Servlet Spec,如果客户端不支持 HTTP/2 特性,注入的 PushBuilder 实例可以为 null
    java.security.Principal 当前已认证的用户;如果已知,可能是一个特定的 Principal 实现
    HttpMethod 请求的 HTTP 方式
    java.util.Locale 当前请求的区域设置,由可用的最具体的 LocaleResolver 确定,实际上是已配置的 LocaleResolverLocaleContextResolver
    java.util.TimeZone + java.time.ZoneId 当前请求关联的时区,由 LocaleContextResolver 确定
    java.io.InputStream, java.io.Reader 用于访问由 Servlet API 暴露的原始请求体
    java.io.OutputStream, java.io.Writer 用于访问由 Servlet API 暴露的原始响应体
    @PathVariable 用于访问 URI 模板变量
    @MatrixVariable 用于访问 URI 路径段中的键值对
    @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 解析
  • 返回值

    下表显示了受支持的控制器方法返回值。所有返回值都支持响应式类型,请参阅下文了解更多细节。

    控制器方法返回值 描述
    @ResponseBody 返回值通过 HttpMessageConverters 转换并写入响应,查看 @ResponseBody
    HttpEntity<B>, ResponseEntity<B> 返回值指定完整的响应,包括通过 HttpMessageConverters 转换并写入响应的 HTTP 报文头和报文体
    HttpHeaders 用于返回只有报文头没有报文体的响应
    String 用于 ViewResolver 解析的视图名,并与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。处理器方法还可以通过声明模型参数以编程方式丰富模型
    View View 实例,与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起用来渲染。处理器方法还可以通过声明模型参数以编程方式丰富模型
    java.util.Map, org.springframework.ui.Model 将被添加到隐式模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    @ModelAttribute 将被添加的模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    注意:@ModelAttribute 是可选的,查看本表最后的“Any other return value”
    ModelAndView object 待使用的视图和模型,以及可选的响应状态
    void 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 属性定制。

模型 Model

@ModelAttribute 注解可以用来:

  • @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 方法支持如下参数:

    方法参数 描述
    Exception type 用于访问引发的异常
    HandlerMethod 用于访问引发异常的控制器方法
    WebRequest, NativeWebRequest 对请求参数、请求、会话属性的通用访问,不直接使用 Servlet API
    javax.servlet.ServletRequest, javax.servlet.ServletResponse 选择任意特定的请求或响应类型 —— 如:ServletRequestHttpServletRequest,或 Spring 的 MultipartRequestMultipartHttpServletRequest
    javax.servlet.http.HttpSession 强制会话的存在。因此,该参数不可能为 null
    注意:会话访问不是线程安全的。如果多个请求被允许并发地访问会话,考虑设置 RequestMappingHandlerAdaptersynchronizeOnSession 标志为 true
    java.security.Principal 当前已认证的用户;如果已知,可能是一个特定的 Principal 实现
    HttpMethod 请求的 HTTP 方式
    java.util.Locale 当前请求的区域设置,有可用的最具体的 LocaleResolver 确定,实际上是已配置的 LocaleResolverLocaleContextResolver
    java.util.TimeZone + java.time.ZoneId 当前请求关联的时区,由 LocaleContextResolver 确定
    java.io.OutputStream, java.io.Writer 用于访问由 Servlet API 暴露的原始响应体
    java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap 用于访问错误响应的模型,总是为空
    RedirectAttributes 指定在重定向时使用的属性 —— 用来附加临时存储到重定向之后请求的查询字符串,flash 属性
    @SessionAttribute 用于访问任意会话属性;与在会话中存储的模型属性相反,这是由类级别的 @SessionAttributes 声明导致的。更多细节请查看 @SessionAttribute
    @RequestAttribute 用于访问请求属性。更多细节请查看 @RequestAttribute
  • 返回值

    @ExceptionHandler 方法支持如下返回值:

    返回值 描述
    @ResponseBody 返回值通过 HttpMessageConverters 转换并写入响应,查看 @ResponseBody
    HttpEntity<B>, ResponseEntity<B> 返回值指定完整的响应,包括通过 HttpMessageConverters 转换并写入响应的 HTTP 报文头和报文体
    String 用于 ViewResolver 解析的视图名,并与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。处理器方法还可以通过声明模型参数以编程方式丰富模型
    View View 实例,与隐式的模型(通过命令对象和 @ModelAttribute 方法确定)一起用来渲染。处理器方法还可以通过声明模型参数以编程方式丰富模型
    java.util.Map, org.springframework.ui.Model 将被添加到隐式模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    @ModelAttribute 将被添加的模型的属性,视图名由 RequestToViewNameTranslator 隐式地确定
    注意:@ModelAttribute 是可选的,查看本表最后的“Any other return value”
    ModelAndView object 待使用的视图和模型,以及可选的响应状态
    void 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 的默认方法如下:

  1. URI变量扩展。
  2. 每个 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 中引入异步请求处理支持的博客文章。

  1. 异常处理 Exception handling

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

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

  2. 拦截器 Interception

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

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

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

  3. 对比 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 可被用于单个异步返回值。如果你想生成多个异步返回值并将它们写入到响应该怎么做?

  1. 对象 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 调用了配置的异常解析器并完成该请求。

  2. SSE

    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),来支持大部分浏览器。

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

  3. 原始数据 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 配置还为异步请求提供了几个选项。

  1. Servlet 容器

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

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

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

  2. Spring MVC

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

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

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

    您可以配置以下内容:

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

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

    • DeferredResultProcessingInterceptorCallableProcessingInterceptor

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

CORS

介绍

出于安全原因,浏览器禁止 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 分钟。
  1. 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...
        }
    }
    
  2. 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。

  1. 视图配置 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 模板。

  2. 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">
            <map>
                <entry key="xml_escape" value-ref="fmXmlEscape"/>
            </map>
        </property>
    </bean>
    
    <bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
    

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

  3. 表单处理

    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+。

  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/"/>
    
  2. 示例

    与传统的模板引擎不同,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 接口。

  1. 要求

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

    • 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。

  2. 脚本模板

    声明一个 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 模板像这样:

    <html>
        <head>
            <title>{{title}}</title>
        </head>
        <body>
            <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。

  1. 视角解析器 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>
    
  2. JSP 与 JSTL

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

  3. Spring 的 JSP 标签库

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

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

  4. 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>
              <tr>
                  <td>First Name:</td>
                  <td><form:input path="firstName"/></td>
              </tr>
              <tr>
                  <td>Last Name:</td>
                  <td><form:input path="lastName"/></td>
              </tr>
              <tr>
                  <td colspan="2">
                      <input type="submit" value="Save Changes"/>
                  </td>
              </tr>
          </table>
      </form:form>
      

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

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

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

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

      <form:form modelAttribute="user">
          <table>
              <tr>
                  <td>First Name:</td>
                  <td><form:input path="firstName"/></td>
              </tr>
              <tr>
                  <td>Last Name:</td>
                  <td><form:input path="lastName"/></td>
              </tr>
              <tr>
                  <td colspan="2">
                      <input type="submit" value="Save Changes"/>
                  </td>
              </tr>
          </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>
              <tr>
                  <td>Subscribe to newsletter?:</td>
                  <%-- Approach 1: Property is of type java.lang.Boolean --%>
                  <td><form:checkbox path="preferences.receiveNewsletter"/></td>
              </tr>
      
              <tr>
                  <td>Interests:</td>
                  <%-- Approach 2: Property is of an array or of type java.util.Collection --%>
                  <td>
                      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>
              </tr>
      
              <tr>
                  <td>Favourite Word:</td>
                  <%-- Approach 3: Property is of type java.lang.Object --%>
                  <td>
                      Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
                  </td>
              </tr>
          </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 片段:

      <tr>
          <td>Interests:</td>
          <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"/>
          </td>
      </tr>
      

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

    • checkboxes 标签

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

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

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

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

    • radiobutton 标签

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

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

      <tr>
          <td>Sex:</td>
          <td>
              Male: <form:radiobutton path="sex" value="M"/> <br/>
              Female: <form:radiobutton path="sex" value="F"/>
          </td>
      </tr>
      
    • radiobuttons 标签

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

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

      <tr>
          <td>Sex:</td>
          <td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
      </tr>
      
    • password 标签

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

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

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

      <tr>
          <td>Password:</td>
          <td>
              <form:password path="password" value="^76525bvHGq" showPassword="true"/>
          </td>
      </tr>
      
    • select 标签

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

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

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

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

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

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

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

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

      <tr>
          <td>House:</td>
          <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>
          </td>
      </tr>
      
    • options 标签

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

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

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

      <tr>
          <td>Country:</td>
          <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>
          </td>
      </tr>
      

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

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

    • textarea 标签

      这个标记渲染一个HTML textarea

      <tr>
          <td>Notes:</td>
          <td><form:textarea path="notes" rows="3" cols="20"/></td>
          <td><form:errors path="notes"/></td>
      </tr>
      
    • 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>
              <tr>
                  <td>First Name:</td>
                  <td><form:input path="firstName"/></td>
                  <%-- Show errors for firstName field --%>
                  <td><form:errors path="firstName"/></td>
              </tr>
      
              <tr>
                  <td>Last Name:</td>
                  <td><form:input path="lastName"/></td>
                  <%-- Show errors for lastName field --%>
                  <td><form:errors path="lastName"/></td>
              </tr>
              <tr>
                  <td colspan="3">
                      <input type="submit" value="Save Changes"/>
                  </td>
              </tr>
          </table>
      </form:form>
      

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

      <form method="POST">
          <table>
              <tr>
                  <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>
              </tr>
      
              <tr>
                  <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>
              </tr>
              <tr>
                  <td colspan="3">
                      <input type="submit" value="Save Changes"/>
                  </td>
              </tr>
          </table>
      </form>
      

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

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

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

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

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

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

      HTML 会看起来像这样:

      <form method="POST">
          <span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
          <table>
              <tr>
                  <td>First Name:</td>
                  <td><input name="firstName" type="text" value=""/></td>
                  <td><span name="firstName.errors">Field is required.</span></td>
              </tr>
      
              <tr>
                  <td>Last Name:</td>
                  <td><input name="lastName" type="text" value=""/></td>
                  <td><span name="lastName.errors">Field is required.</span></td>
              </tr>
              <tr>
                  <td colspan="3">
                      <input type="submit" value="Save Changes"/>
                  </td>
              </tr>
          </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 POST,使用 real DELETE 方法隐藏在请求参数后面,由 HiddenHttpMethodFilter(在 web.xml 中定义)获取。

      <filter>
          <filter-name>httpMethodFilter</filter-name>
          <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
      </filter>
      
      <filter-mapping>
          <filter-name>httpMethodFilter</filter-name>
          <servlet-name>petclinic</servlet-name>
      </filter-mapping>
      

      相应的 @Controller 方法如下所示:

      @RequestMapping(method = RequestMethod.DELETE)
      public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
          this.clinic.deletePet(petId);
          return "redirect:/owners/" + ownerId;
      }
      
    • HTML5 标签

      从 Spring 3 开始,Spring form 标记库允许输入动态属性,这意味着您可以输入任何 HTML5 特有的属性。

      在 Spring 3.1 中,表单输入标记支持输入 text 以外的类型属性。这将允许渲染新的 HTML5 特定输入类型,如 emaildaterange 等。注意,输入 type='text' 是不需要的,因为 text 是默认类型。

Tiles

在使用 Spring 的 web 应用程序中,将 Tiles(与其他视图技术一样)集成在一起是可能的。下面介绍如何实现这一点。

本节将重点介绍 Spring 在 org.springframework.web.servlet.view.tiles3 包中对 Tiles v3 的支持。

  1. 依赖

    为了能够使用 Tiles,您必须添加对 Tiles 3.0.1 或更高版本的依赖,以及它对您的项目的传递依赖关系。

  2. 配置

    为了能够使用 Tiles,您必须使用包含定义的文件来配置它(关于定义和其他 Tiles 概念的基本信息,请查看 http://tiles.apache.org)。在 Spring 中,这是使用 TilesConfigurer 完成的。请看下面的示例 ApplicationContext 配置:

    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
        <property name="definitions">
            <list>
                <value>/WEB-INF/defs/general.xml</value>
                <value>/WEB-INF/defs/widgets.xml</value>
                <value>/WEB-INF/defs/administrator.xml</value>
                <value>/WEB-INF/defs/customer.xml</value>
                <value>/WEB-INF/defs/templates.xml</value>
            </list>
        </property>
    </bean>
    

    如您所见,有五个包含定义的文件,它们都位于 WEB-INF/defs 目录中。在 WebApplicationContext 初始化时,文件将被加载,定义工厂将被初始化。完成之后,在定义文件中包含的 Tiles 可以作为 Spring web 应用程序中的视图使用。要能够使用视图,就像使用与 Spring 使用的任何其他视图技术一样,必须有一个 ViewResolver。在下面你可以找到两种可能性,UrlBasedViewResolverResourceBundleViewResolver

    您可以通过添加下划线和区域设置来指定区域特定的 Tiles 定义。例如:

    <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
        <property name="definitions">
            <list>
                <value>/WEB-INF/defs/tiles.xml</value>
                <value>/WEB-INF/defs/tiles_fr_FR.xml</value>
            </list>
        </property>
    </bean>
    

    在这个配置中,tiles_fr_FR.xml 将用于 fr_FR 地区的请求,而 tiles.xml 用于默认情况。

    由于下划线用于指示区域设置,因此建议避免在 Tiles 定义的文件名中使用它们。

    • UrlBasedViewResolver

      UrlBasedViewResolver 为它必须解析的每个视图实例化给定的 viewClass

      <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
          <property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
      </bean>
      
    • ResourceBundleViewResolver

      ResourceBundleViewResolver 必须提供包含视图名称和视图类的属性文件,解析器可以使用:

      <bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
          <property name="basename" value="views"/>
      </bean>
      
      ...
      welcomeView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
      welcomeView.url=welcome (this is the name of a Tiles definition)
      
      vetsView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
      vetsView.url=vetsView (again, this is the name of a Tiles definition)
      
      findOwnersForm.(class)=org.springframework.web.servlet.view.JstlView
      findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
      ...
      

      正如您所看到的,在使用 ResourceBundleViewResolver 时,您可以轻松地混合不同的视图技术。

      注意,TilesView 类支持拆箱即用的 JSTL(JSP标准标记库)。

    • SimpleSpringPreparerFactory 和 SpringBeanPreparerFactory

      作为一个高级特性,Spring 还支持两个特殊的 Tiles PreparerFactory 实现。查看 Tiles 文档,了解如何在您的 Tiles 定义文件中使用 ViewPreparer 引用。

      指定 SimpleSpringPreparerFactory 自动装配基于特定 preparer 类的 ViewPreparer 实例,应用 Spring 容器回调及配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围的注释配置被激活,那么 ViewPreparer 类中的注释将被自动检测和应用。注意,这需要在 Tiles 定义文件中准备类,就像默认的 PreparerFactory 一样。

      指定 SpringBeanPreparerFactory 操作指定的 preparer 名称,而不是类,从 DispatcherServlet 的应用程序上下文获取相应的 Spring bean。在本例中,完整的 bean 创建过程将在 Spring 应用程序上下文的控制中,允许使用显式依赖注入配置、作用域 bean 等。注意,您需要为每个 preparer 名称定义一个 Spring bean 定义(在您的 Tiles 定义中使用)。

      <bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
          <property name="definitions">
              <list>
                  <value>/WEB-INF/defs/general.xml</value>
                  <value>/WEB-INF/defs/widgets.xml</value>
                  <value>/WEB-INF/defs/administrator.xml</value>
                  <value>/WEB-INF/defs/customer.xml</value>
                  <value>/WEB-INF/defs/templates.xml</value>
              </list>
          </property>
      
          <!-- resolving preparer names as Spring bean definition names -->
          <property name="preparerFactoryClass"
                  value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>
      
      </bean>
      

RSS、Atom

AbstractAtomFeedViewAbstractRssFeedView 都继承了基类 AbstractFeedView,并被用于提供 Atom 和 RSS 提要视图。它们基于 java.net 的 ROME 项目,并位于 org.springframework.web.servlet.view.feed 包中。

AbstractAtomFeedView 要求您实现 buildFeedEntries() 方法,并可以选择重写 buildFeedMetadata() 方法(默认实现为空),如下所示。

public class SampleContentAtomView extends AbstractAtomFeedView {

    @Override
    protected void buildFeedMetadata(Map<String, Object> model,
            Feed feed, HttpServletRequest request) {
        // implementation omitted
    }

    @Override
    protected List<Entry> buildFeedEntries(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        // implementation omitted
    }

}

类似的需求应用于实现 AbstractRssFeedView,如下所示。

public class SampleContentAtomView extends AbstractRssFeedView {

    @Override
    protected void buildFeedMetadata(Map<String, Object> model,
            Channel feed, HttpServletRequest request) {
        // implementation omitted
    }

    @Override
    protected List<Item> buildFeedItems(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        // implementation omitted
    }

}

buildFeedItems()buildFeedEntires() 方法传入 HTTP 请求,以防您需要访问 Locale。HTTP 响应仅用于设置 cookie 或其他 HTTP 头。在方法返回后,提要将自动被写入响应对象。

关于创建 Atom 视图的示例,请参考 Alef Arendsen 的 Spring 团队博客条目。

PDF、Excel

  1. 介绍

    返回 HTML 页面并不总是用户查看模型输出的最佳方式,Spring 使得从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。文档便视图,并将从服务器上以正确的内容类型从服务器推流到(希望)允许客户端 PC 运行他们的电子表格或 PDF 查看器应用程序以响应。

    为了使用 Excel 视图,您需要将 Apache POI 库添加到您的类路径中,而对于PDF 生成最好是 OpenPDF 库。

    如果可能的话,使用底层文档生成库的最新版本。特别是,我们强烈推荐 OpenPDF(例如 OpenPDF 1.0.5),而不是已经过时的原始 iText 2.1.7,因为它积极维护并修复了不可信 PDF 内容的一个重要漏洞。

  2. 配置

    基于文档视图以几乎相同的方式处理 XSLT 视图,而下面的部分建立在前一个通过展示如何调用 XSLT 示例中使用相同的控制器呈现相同的模型作为 PDF 文档和 Excel 电子表格(也可以在 Open Office 中查看或操作)。

  3. 视图定义

    首先,我们来修改 views.properites(或 xml 等效项)并为两种文档类型添加一个简单的视图定义。现在,整个文件与前面显示的 XSLT 视图类似:

    home.(class)=xslt.HomePage
    home.stylesheetLocation=/WEB-INF/xsl/home.xslt
    home.root=words
    
    xl.(class)=excel.HomePage
    
    pdf.(class)=pdf.HomePage
    

    如果您想从模板电子表格或可输入的 PDF 表单开始添加模型数据,请在视图定义中指定位置作为 url 属性。

  4. 控制器

    我们将使用的控制器代码与前面的 XSLT 示例完全相同,只是更改了视图的名称。当然,您可以很聪明,并根据 URL 参数或其他一些逻辑来选择,Spring 确实非常擅长将视图与控制器分离!

  5. Excel 视图

    正如我们在 XSLT 示例中所做的那样,我们将子类化适当的抽象类,以便在生成输出文档时实现自定义行为。对于 Excel,这涉及到编写 org.springframework.web.servlet.view.document.AbstractExcelView(由 POI 生成的 Excel 文件) 或 org.springframework.web.servlet.view.document.AbstractJExcelView(用于 JExcelApi 生成的 Excel 文件) 的子类,和实现 buildExcelDocument() 方法。

    下面是我们的 POI Excel 视图的完整清单,它显示了一个新电子表格的第一列连续行中模型映射的单词列表:

    package excel;
    
    // imports omitted for brevity
    
    public class HomePage extends AbstractExcelView {
    
        protected void buildExcelDocument(Map model, HSSFWorkbook wb, HttpServletRequest req,
                HttpServletResponse resp) throws Exception {
    
            HSSFSheet sheet;
            HSSFRow sheetRow;
            HSSFCell cell;
    
            // Go to the first sheet
            // getSheetAt: only if wb is created from an existing document
            // sheet = wb.getSheetAt(0);
            sheet = wb.createSheet("Spring");
            sheet.setDefaultColumnWidth((short) 12);
    
            // write a text at A1
            cell = getCell(sheet, 0, 0);
            setText(cell, "Spring-Excel test");
    
            List words = (List) model.get("wordList");
            for (int i=0; i < words.size(); i++) {
                cell = getCell(sheet, 2+i, 0);
                setText(cell, (String) words.get(i));
            }
        }
    
    }
    

    下面是生成相同 Excel 文件的视图,现在使用 JExcelApi:

    package excel;
    
    // imports omitted for brevity
    
    public class HomePage extends AbstractJExcelView {
    
        protected void buildExcelDocument(Map model, WritableWorkbook wb,
                HttpServletRequest request, HttpServletResponse response) throws Exception {
    
            WritableSheet sheet = wb.createSheet("Spring", 0);
    
            sheet.addCell(new Label(0, 0, "Spring-Excel test"));
    
            List words = (List) model.get("wordList");
            for (int i = 0; i < words.size(); i++) {
                sheet.addCell(new Label(2+i, 0, (String) words.get(i)));
            }
        }
    }
    

    注意 API 之间的差异。我们已经发现 JExcelApi 更加直观,而且 JExcelApi 具有更好的图像处理功能。然而,在使用 JExcelApi 时,存在大体积 Excel 文件的内存问题。

    如果您现在对控制器进行修改,那么它将返回 xl 作为视图的名称(return new ModelAndView("xl", map);)并再次运行您的应用程序,您应该会发现,当您请求与以前相同的页面时,会自动创建和下载 Excel 电子表格。

  6. PDF 视图

    PDF版本的单词列表更简单。这一次,类扩展了 org.springframework.web.servlet.view.document.AbstractPdfView 并实现 buildPdfDocument() 方法如下:

    package pdf;
    
    // imports omitted for brevity
    
    public class PDFPage extends AbstractPdfView {
    
        protected void buildPdfDocument(Map model, Document doc, PdfWriter writer,
            HttpServletRequest req, HttpServletResponse resp) throws Exception {
            List words = (List) model.get("wordList");
            for (int i=0; i<words.size(); i++) {
                doc.add( new Paragraph((String) words.get(i)));
            }
        }
    
    }
    

    再次,修改控制器以返回 pdf 视图,并 return new ModelAndView("pdf", map);,并在应用程序中重新加载 URL。这一次,PDF 文档应该出现在模型映射中的每个单词。

Jackson

  1. JSON

    MappingJackson2JsonView 使用 Jackson 库的 ObjectMapper 将响应内容呈现为 JSON。默认情况下,模型映射的全部内容(除了框架特定的类之外)将被编码为 JSON。对于需要过滤映射内容的情况,用户可以通过 RenderedAttributes 属性指定一组特定的模型属性。extractValueFromSingleKeyModel 属性也可以用于直接提取和序列化的单键模型中的值,而不是作为模型属性的映射。

    通过使用 Jackson 提供的注解,可以根据需要定制 JSON 映射。当需要进一步控制时,可以通过 ObjectMapper 属性注入定制的 ObjectMapper,以便为特定类型提供定制的 JSON 序列化/反序列化器。

    当请求有一个名为 JSONP 或 callback 的查询参数时,将支持并自动启用 JSONP。JSONP 查询参数名称可以通过 jsonpParameterNames 属性定制。

  2. XML

    MappingJackson2XmlView 使用 Jackson XML 扩展的 XmlMapper 将响应内容呈现为 XML。如果该模型包含多个条目,则应该使用 modelKey bean 属性显式地设置要序列化的对象。如果模型包含一个条目,它将被自动序列化。

    通过使用 JAXB 或 Jackson 提供的注释,可以定制 XML 映射。当需要进一步的控制时,可以通过 ObjectMapper 属性注入定制的 XmlMapper,以便为特定类型提供定制的 XML 序列化/反序列化程序。

XSLT

XSLT 是 XML 的转换语言,在 web 应用程序中作为一种视图技术非常流行。如果应用程序很自然地处理 XML,或者您的模型可以很容易地转换成 XML,那么 XSLT 是一种不错的视图技术。下一节将展示如何以模型数据的形式生成 XML 文档,并在 Spring Web MVC 应用程序中使用 XSLT 进行转换。

这个示例是一个简单的 Spring 应用程序,它在控制器中创建一个单词列表并将它们添加到模型映射中。该映射与我们的 XSLT 视图的视图名称一起返回。有关 Spring Web MVC 的控制器接口的详细信息,请参见被注解的控制器。XSLT 控制器将把单词列表转换成一个简单的 XML 文档,以便进行转换。

  1. Bean

    配置是简单 Spring 应用程序的标准。MVC 配置必须定义一个 XsltViewResolver bean 和常规的 MVC 注解配置。

    @EnableWebMvc
    @ComponentScan
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Bean
        public XsltViewResolver xsltViewResolver() {
            XsltViewResolver viewResolver = new XsltViewResolver();
            viewResolver.setPrefix("/WEB-INF/xsl/");
            viewResolver.setSuffix(".xslt");
            return viewResolver;
        }
    
    }
    

    我们需要一个封装我们的单词生成逻辑的控制器。

  2. 控制器

    控制器逻辑被封装在 @Controller 类中,处理程序方法被定义为这样…

    @Controller
    public class XsltController {
    
        @RequestMapping("/")
        public String home(Model model) throws Exception {
    
            Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            Element root = document.createElement("wordList");
    
            List<String> words = Arrays.asList("Hello", "Spring", "Framework");
            for (String word : words) {
                Element wordNode = document.createElement("word");
                Text textNode = document.createTextNode(word);
                wordNode.appendChild(textNode);
                root.appendChild(wordNode);
            }
    
            model.addAttribute("wordList", root);
            return "home";
        }
    
    }
    

    到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。注意,您还可以将 XML 文件加载为资源,并使用它而不是自定义 DOM 文档。

    当然,有可用的软件包可以自动地 domify 一个对象图,但是在 Spring 中,您可以完全灵活地以任何方式从模型中创建 DOM。这阻止了 XML 在模型数据结构中扮演太大的角色,当使用工具来管理 domification 过程时,这是一种危险。

    接下来,XsltViewResolver 将解析 home XSLT 模板文件,并将 DOM 文档合并到它以生成我们的视图。

  3. 变换 Transformation

    最后,XsltViewResolver 将解析 home XSLT 模板文件,并将 DOM 文档合并到它以生成我们的视图。如 XsltViewResolver 配置中所示,XSLT 模板在 WEB-INF/xsl 目录中的 war 文件中存在,并以 XSLT 文件扩展名结尾。

    <?xml version="1.0" encoding="utf-8"?>
    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:output method="html" omit-xml-declaration="yes"/>
    
        <xsl:template match="/">
            <html>
                <head><title>Hello!</title></head>
                <body>
                    <h1>My First Words</h1>
                    <ul>
                        <xsl:apply-templates/>
                    </ul>
                </body>
            </html>
        </xsl:template>
    
        <xsl:template match="word">
            <li><xsl:value-of select="."/></li>
        </xsl:template>
    
    </xsl:stylesheet>
    

    这会渲染为:

    <html>
        <head>
            <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <title>Hello!</title>
        </head>
        <body>
            <h1>My First Words</h1>
            <ul>
                <li>Hello</li>
                <li>Spring</li>
                <li>Framework</li>
            </ul>
        </body>
    </html>
    

MVC 配置

MVC Java config 和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,以及配置 API 来定制它。

对于更高级的自定义,在配置 API 中不可用,请参阅高级 Java 配置和高级 XML 配置。

您不需要了解 MVC Java config 和 MVC 命名空间所创建的底层 Bean,但是如果您想了解更多,请参见特殊 Bean 类型和 Web MVC 配置。

开启 MVC 配置

在 Java 配置中使用 @EnableWebMvc 注解:

@Configuration
@EnableWebMvc
public class WebConfig {
}

在 XML 中使用 <mvc:annotation-driven> 元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

上面注册了许多 Spring MVC 基础结构 bean,它们也适应了类路径上可用的依赖项:例如 JSON、XML 等的 payload 转换器。

MVC 配置 API

在 Java 配置中实现 WebMvcConfigurer 接口:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}

在 XML 中请检查 <mvc:annotation-driven/> 的属性和子元素。您可以查看 Spring MVC XML schema 或使用 IDE 的代码完成特性来发现可用的属性和子元素。

类型转换

默认情况下,安装了 NumberDate 类型的格式化程序,包括对 @NumberFormat@DateTimeFormat 注释的支持。如果在类路径上存在 Joda-Time,那么也可以对 Joda-Time 格式库进行完全支持。

在 Java 配置中,注册自定义格式化程序和转换器:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }
}

在 XML 中,同样:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven conversion-service="conversionService"/>

    <bean id="conversionService"
            class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="org.example.MyConverter"/>
            </set>
        </property>
        <property name="formatters">
            <set>
                <bean class="org.example.MyFormatter"/>
                <bean class="org.example.MyAnnotationFormatterFactory"/>
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.example.MyFormatterRegistrar"/>
            </set>
        </property>
    </bean>

</beans>

当使用 FormatterRegistrars 时,查看 FormatterRegistrar SPI 和 FormattingConversionServiceFactoryBean 了解更多信息。

校验器 Validation

默认情况下,如果类路径中存在 Bean Validation(比如 Hibernate Validator),LocalValidatorFactoryBean 会被注册为全局校验器,用于控制器方法参数上的 @ValidValidated

在 Java 配置中,你可以自定义全局 Validator 接口:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public Validator getValidator(); {
        // ...
    }
}

在 XML 中,同样:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven validator="globalValidator"/>

</beans>

注意,你也可以在本地注册 Validator

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

如果你需要在别处注入 LocalValidatorFactoryBean,创建一个 bean 并用 @Primary 标记,以避免在 MVC 配置中声明的那个冲突。

拦截器 Interceptors

在 Java 配置中,注册拦截器以应用到进来的请求:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
        registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
    }
}

在 XML 中,同样:

<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/admin/**"/>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/secure/*"/>
        <bean class="org.example.SecurityInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

内容类型 Content Types

您可以配置 Spring MVC 如何从请求中确定请求的媒体类型。如:Accept 报文头、URL 路径扩展、查询参数等。

默认情况下,URL 路径扩展是先检查的——jsonxmlrssatom 根据类路径依赖关系注册为已知的扩展,并检查 Accept 头。

考虑更改这些默认值只 Accept 头,如果必须使用基于 url 的内容类型解析,请考虑路径扩展上的查询参数策略。参见后缀匹配和后缀匹配与 RFD 了解更多细节。

在Java配置中,定制请求的内容类型解析:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.mediaType("json", MediaType.APPLICATION_JSON);
    }
}

在 XML 中, 同样:

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="mediaTypes">
        <value>
            json=application/json
            xml=application/xml
        </value>
    </property>
</bean>

消息转换器 Message Converters

如果你想要替换 Spring MVC 创建的默认转换器,在 Java 配置中自定义 HttpMessageConverter 可以通过覆盖 configureMessageConverters() 方法。如你想要自定义它们或者在默认的基础上增加附加的转换器,可以覆盖 extendMessageConverters() 方法。

下面是一个示例,该示例使用定制的 ObjectMapper 而不是默认的 ObjectMapper 添加Jackson JSON 和 XML 转换器:

@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
                .indentOutput(true)
                .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
                .modulesToInstall(new ParameterNamesModule());
        converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
        converters.add(new MappingJackson2XmlHttpMessageConverter(builder.xml().build()));
    }
}

在这个示例中,Jackson2ObjectMapperBuilder 用来为开启缩进支持的 MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter 创建通用配置。一个定制的日期格式和 jackson-module-parameter-names 的注册,这增加了对访问参数名的支持(在 Java 8 中加入)。

这个构建器对 Jackson 的默认性质进行了如下自定义:

1. [`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES) 被禁用。
2. [`MapperFeature.DEFAULT_VIEW_INCLUSION`](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION) 被禁用。

如果在类路径中检测到它们,它还会自动注册以下著名模块:

1. [jackson-datatype-jdk7](https://github.com/FasterXML/jackson-datatype-jdk7):用于对 Java 7 类型的支持(如 java.nio.file.Path)。
2. [jackson-datatype-joda](https://github.com/FasterXML/jackson-datatype-joda):用于对 Joda-Time 类型的支持。
3. [jackson-datatype-jsr310](https://github.com/FasterXML/jackson-datatype-jsr310):用于对 Java 8 Date 和 Time API 类型的支持。
4. [jackson-datatype-jdk8](https://github.com/FasterXML/jackson-datatype-jdk8):用于对 Java 8 类型如 Optional 的支持。

开启 Jackson XML 支持的缩进要求除了 jackson-dataformat-xml 之外,还需要使用 woodstox-core-asl 依赖项。

其它有趣的 Jackson 模板也是可用的:

1. [jackson-datatype-money](https://github.com/zalando/jackson-datatype-money):用于对 `javax.money` 类型的支持(非官方模块)。
2. [jackson-datatype-hibernate](https://github.com/FasterXML/jackson-datatype-hibernate):用于对 Hibernate 具体类型和性质的支持(包括懒加载切面)。

也可以在 XML 中做同样的配置:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
        <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
            <property name="objectMapper" ref="xmlMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
      p:indentOutput="true"
      p:simpleDateFormat="yyyy-MM-dd"
      p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>

<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>

视图控制器

这是定义 ParameterizableViewController 的快捷方式,它可以在调用时立即转发到视图。当在视图生成响应之前没有 Java 控制器逻辑执行时,在静态情况下使用它。

/ 请求转发到 Java 中的 home 视图的示例:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }
}

同样在 XML 使用 <mvc:view-controller> 元素:

<mvc:view-controller path="/" view-name="home"/>

视图解析器

MVC 配置简化了视图解析器的注册。

下面是一个 Java 配置示例,它使用 JSP 和 Jackson 作为 JSON 渲染的默认视图配置内容协商视图解决方案:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.jsp();
    }
}

同样的在 XML 中:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:jsp/>
</mvc:view-resolvers>

但是请注意,FreeMarker、Tiles、Groovy Markup 和脚本模板还需要配置底层视图技术。

MVC 命名空间提供了专用的元素。例如 FreeMarker:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:freemarker cache="false"/>
</mvc:view-resolvers>

<mvc:freemarker-configurer>
    <mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>

在 Java 配置中只需添加各自的 Configurer bean:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.freeMarker().cache(false);
    }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/WEB-INF/");
        return configurer;
    }
}

静态资源 Static Resources

此选项提供了一种方便的方法,可以从基于资源的位置列表中提供静态资源。

在下面的示例中,给定一个以 /resources 开头的请求,相对路径用于在 web 应用程序根目录下或在 /static 下的类路径下查找和服务相对于 /public 的静态资源。这些资源将在未来 1 年的期限内服务,以确保浏览器缓存的最大使用和浏览器的 HTTP 请求的减少。Last-Modified 报文头也会被评估,如果是现在的则返回一个 304 状态码。

在Java配置:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCachePeriod(31556926);
    }
}

在 XML 中:

<mvc:resources mapping="/resources/**"
    location="/public, classpath:/static/"
    cache-period="31556926" />

也可参见 HTTP 缓存对于静态资源的支持。

资源处理程序还支持 ResourceResolversResourceTransformers 的链。这可以用来创建一个工具链,用于优化资源。

VersionResourceResolver 可用于基于从内容、固定应用程序版本或其他内容计算的 MD5 散列上的版本化资源 URL。ContentVersionStrategy(MD5 散列)是一个不错的选择,有一些值得注意的异常,比如与模块加载器一起使用的 JavaScript 资源。

例如在Java配置中:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}

在 XML 中,同样:

<mvc:resources mapping="/resources/**" location="/public/">
    <mvc:resource-chain>
        <mvc:resource-cache/>
        <mvc:resolvers>
            <mvc:version-resolver>
                <mvc:content-version-strategy patterns="/**"/>
            </mvc:version-resolver>
        </mvc:resolvers>
    </mvc:resource-chain>
</mvc:resources>

您可以使用 ResourceUrlProvider 重写 URL 并应用解析器和转换器的全链(比如插入版本号)。MVC 配置提供了一个 ResourceUrlProvider bean,这样它就可以被注入到其他 bean 中。您还可以使用 ResourceUrlEncodingFilter 对 Thymeleaf、JSP、FreeMarker 和其他使用 HttpServletResponse#encodeURL 的 URL 标记进行重写。

WebJars 还支持通过 WebJarsResourceResolver 以及自动注册当 org.webjars:webjars-locator 存在于类路径中时。解析器可以重新编写 URL 来包含 jar 的版本,也可以匹配没有版本的传入 URL。比如,/jquery/jquery.min.js/jquery/1.2.0/jquery.min.js

默认 Servlet

这允许将 DispatcherServlet 映射到 / (因此覆盖了容器默认 Servlet 的映射),同时仍然允许由容器的默认 Servlet 处理静态资源请求。它配置一个 DefaultServletHttpRequestHandler,它的URL映射为 /**,并且相对于其他 URL 映射的优先级最低。

这个处理程序将把所有请求转发给默认 Servlet。因此,重要的是它仍然保持在所有其他 URL HandlerMappings 的顺序中。如果您使用,或者您正在设置自己的自定义 HandlerMapping 实例,请确保将其 order 属性设置为低于 DefaultServletHttpRequestHandler 的值,这是 Integer.MAX_VALUE

使用默认设置启用该特性:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

或在 XML 中:

<mvc:default-servlet-handler/>

要重写 / Servlet 映射,需要注意的是,缺省 Servlet 的 RequestDispatcher 必须通过名称检索,而不是通过路径检索。DefaultServletHttpRequestHandler 将尝试在启动时自动检测容器的默认 Servlet,使用大多数主要 Servlet 容器的已知名称列表(包括 Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)。如果默认的 Servlet 已经使用不同的名称进行了配置,或者使用了一个不同的 Servlet 容器,而默认的 Servlet 名称是未知的,那么默认 Servlet 的名称必须被显式地提供,如下例所示:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable("myCustomDefaultServlet");
    }

}

或者在 XML 中:

<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

路径匹配 Path Matching

这允许定制与 URL 匹配和 URL 处理相关的选项。有关单个选项的详细信息,请查看 PathMatchConfigurer API。

示例 Java 配置:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseSuffixPatternMatch(true)
            .setUseTrailingSlashMatch(false)
            .setUseRegisteredSuffixPatternMatch(true)
            .setPathMatcher(antPathMatcher())
            .setUrlPathHelper(urlPathHelper());
    }

    @Bean
    public UrlPathHelper urlPathHelper() {
        //...
    }

    @Bean
    public PathMatcher antPathMatcher() {
        //...
    }

}

在 XML 中,同样:

<mvc:annotation-driven>
    <mvc:path-matching
        suffix-pattern="true"
        trailing-slash="false"
        registered-suffixes-only="true"
        path-helper="pathHelper"
        path-matcher="pathMatcher"/>
</mvc:annotation-driven>

<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>

高级 Java 配置

@EnableWebMvc 引入 DelegatingWebMvcConfiguration,(1)为 Spring MVC 应用程序提供默认的 Spring 配置,(2)检测并代理 WebMvcConfigurer 去定制配置。

要使用高级模式,移除 @EnableWebMvc 并直接继承 DelegatingWebMvcConfiguration 并不是 WebMvcConfigurer

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    // ...

}

你可以保留 WebConfig 中存在的方法,但是你现在也可以覆盖来自基类的 bean 声明,并且你仍然可以在类路径中拥有任意数量的 WebMvcConfigurer

高级 XML 配置

MVC 命名空间并没有高级模式。如果你需要定制 bean 上你不能修改的属性,你可以使用 Spring ApplicationContextBeanPostProcessor 生命周期钩子:

@Component
public class MyPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
        // ...
    }
}

注意,MyPostProcessor 需要在 XML 中显式声明为 bean,或通过 <component scan/> 声明检测。

HTTTP/2

Servlet 4 容器需要支持 HTTP/2,Spring Framework 5 与 Servlet API 4 兼容。从编程模型的角度来看,应用程序不需要做什么特别的事情。不过,还有一些与服务器配置相关的考虑。更多细节请查看 HTTP/2 wiki 页面。

Servlet API 确实公开了一个与 HTTP/2 相关的构造。javax.servlet.http.PushBuilder 可以用于主动地将资源推给客户端,并支持将其作为方法参数传递给 @RequestMapping 方法。

REST 客户端

本节描述客户端访问 REST 端点的选项。

RestTemplate

RestTemplate 是最初的 Spring REST 客户端,它通过提供一个可参数化的方法来执行 HTTP 请求,在 Spring 框架(例如 JdbcTemplateJmsTemplate 等)中遵循类似的方法。

RestTemplate 有一个同步 API,并依赖于阻塞 I/O。对于低并发的客户端场景,这是可以接受的。在服务器环境中,或者在编排远程调用序列时,更喜欢使用 WebClient,它提供了更高效的执行模型,包括无缝支持流。

有关使用 RestTemplate 的更多细节,请参阅 RestTemplate

WebClient

WebClient 是一个可以为 RestTemplate 提供替代的响应式客户端。它公开了一个实用的、流畅的 API,并且依赖于非阻塞 I/O,这使得它能够比 RestTemplate 更有效地支持高并发性(即使用少量的线程)。WebClient 是一种适合于流媒体的场景。

有关使用 WebClient 的更多细节,请参见 WebClient

测试

本节概述 Spring MVC 应用程序的 Spring 测试中可用的选项。

  • Servlet API Mocks

    Servlet API 约定的 Mock 实现,用于单元测试控制器、过滤器和其它 Web 组件。更多细节请查看 Servlet API 模拟对象。

  • TestContext Framework

    支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括在测试方法中高效缓存负载配置,以及支持使用 MockServletContext 加载 WebApplicationContext。有关更多详细信息,请参见 TestContext Framework

  • Spring MVC Test

    一个框架,也称为 MockMvc,用于通过 DispatcherServlet 测试带注释的控制器,即支持注释,并使用 Spring MVC 基础设施,但是没有 HTTP 服务器。有关更多细节,请参见 Spring MVC Test

  • Client-side REST

    spring-test 提供了一个 MockRestServiceServer,它可以用作模拟服务器,用于测试内部使用 RestTemplate 的客户端代码。有关更多详细信息,请参见 Client REST Tests

  • WebTestClient

    WebTestClient 是为测试 WebFlux 应用而构建的,但是它也可以用于端到端集成测试,通过 HTTP 连接到任何服务器。它是一个非阻塞的、响应式的客户端,非常适合测试异步和流媒体场景。

WebSockets

参考文档的这部分内容涵盖了对 Servlet 堆栈、包含原始 WebSocket 交互 的 WebSocket 消息、通过 SockJS WebSocket 仿真、在 WebSocket 上通过 STOMP 作为子协议进行发布。

介绍

WebSocket 协议 RFC 6455 提供了一种标准化的方法,可以在单个 TCP 连接上建立客户机和服务器之间的全双工、双向通信通道。它是一种不同于 HTTP 的 TCP 协议,但它的设计目的是通过使用端口 80 和 443 来处理 HTTP,并允许重用现有的防火墙规则。

WebSocket 交互开始于一个 HTTP 请求,它使用 HTTP Upgrade 头来升级,或者在这个情况下切换到 WebSocket 协议:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

与通常的 200 状态码不同,带有 WebSocket 支持的服务器返回:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

在成功地握手之后,HTTP 升级请求下的 TCP 套接字仍然对客户机和服务器都开放,以继续发送和接收消息。

完整介绍 WebSockets 的工作方式超出了本文的范围。请阅读 RFC 6455, HTML5 的 WebSocket 章节,或者 Web 上的许多介绍和教程之一。

请注意,如果 WebSocket 服务器在 web 服务器(例如 nginx)后面运行,您可能需要配置它来将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序运行在云环境中,请检查与 WebSocket 支持相关的云服务提供商的说明。

HTTP 对比 WebSocket

尽管 WebSocket 被设计为 HTTP 兼容并从 HTTP 请求开始,但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为许多 url。要与应用程序客户端交互,请访问那些 url,请求——响应样式。服务器将请求路由到基于 HTTP URL、方法和头的适当处理程序。

在 WebSockets 中,通常只有一个 URL 用于初始连接,随后所有应用程序消息都在同一个 TCP 连接上运行。这指向一个完全不同的异步、事件驱动的消息传递架构。

WebSocket 也是一种低级的传输协议,它不像 HTTP 那样对消息的内容规定任何语义。这意味着除非客户机和服务器同意消息语义,否则无法路由或处理消息。

WebSocket 客户机和服务器可以通过 HTTP 握手请求的 Sec-WebSocket-Protocol 头来协商使用更高级别的消息传递协议(例如 STOMP),或者在没有这些协议的情况下,他们需要制定自己的约定。

什么时候用它

WebSockets 可以使网页充满活力和交互性。然而,在许多情况下,Ajax 和 HTTP 流和/或长轮询的组合可以提供一个简单而有效的解决方案。

例如,新闻、邮件和社交 feed 需要动态更新,但每隔几分钟就可以做到这一点。另一方面,协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是决定因素。如果消息量相对较低(例如监视网络故障),HTTP 流或轮询可能提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 最好的例子。

请记住,在 Internet 上,在您的控制之外的限制性代理,可能会阻止 WebSocket 交互,因为它们没有配置为传递升级头,或者因为它们关闭了长时间的连接,而这些连接看起来是空闲的?这意味着,对于防火墙内的内部应用程序,WebSocket 的使用比面向公众的应用程序要简单得多。

WebSocket API

Spring 框架提供了一个 WebSocket API,可以用来编写处理 WebSocket 消息的客户端和服务器端应用程序。

WebSocketHandler

创建 WebSocket 服务器就像实现 WebSocketHandler 一样简单,或者更有可能扩展 TextWebSocketHandlerBinaryWebSocketHandler

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class MyHandler extends TextWebSocketHandler {

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // ...
    }

}

有专门的 WebSocket Java-config 和 XML 命名空间支持,用于将上面的 WebSocket 处理程序映射到特定的 URL:

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

等价的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

上面的内容用于 Spring MVC 应用程序,应该包含在 DispatcherServlet 的配置中。然而,Spring 的 WebSocket 支持并不依赖于 Spring MVC。在 WebSocketHttpRequestHandler 的帮助下,将 WebSocketHandler 集成到其他 HTTP 服务环境中相对简单。

WebSocket Handshake

自定义初始 HTTP WebSocket 握手请求的最简单方法是通过 HandshakeInterceptor,它公开了 before 和 after 握手方法。这样的拦截器可以用来阻止握手或向 WebSocketSession 提供任何可用的属性。例如,有一个内置的拦截器,用于将 HTTP 会话属性传递给 WebSocket 会话:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

}

等价的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

一个更高级的选项是扩展 DefaultHandshakeHandler,它执行 WebSocket 握手的步骤,包括验证客户端、协商子协议和其他。如果应用程序需要配置自定义的 RequestUpgradeStrategy,以适应 WebSocket 服务器引擎和尚未支持的版本(还可以在此主题参考“部署”章节了解更多),那么应用程序也可能需要使用此选项。Java-config 和 XML 命名空间都可以配置定制的 HandshakeHandler

Spring提供了一个 WebSocketHandlerDecorator 基类,可以用来修饰 WebSocketHandler 和其他行为。在使用 WebSocket Java-config 或 XML 命名空间时,提供了日志记录和异常处理实现,并在默认情况下添加了这些实现。ExceptionWebSocketHandlerDecorator 捕获所有未捕获的异常,这些异常来自任何 WebSocketHandler 方法,并关闭 WebSocket 会话,其状态为 1011,表示服务器错误。

部署

Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,在这个应用程序中,DispatcherServlet 既提供 HTTP WebSocket 握手,也提供其他 HTTP 请求。通过调用 WebSocketHttpRequestHandler,它也很容易集成到其他 HTTP 处理场景中。这很方便也很容易理解。但是,对于 JSR-356 运行时,需要特别考虑。

Java WebSocket API(JSR-356)提供了两个部署机制。第一个涉及到启动时的 Servlet 容器类路径扫描(Servlet 3 特性);另一个是在 Servlet 容器初始化时使用的注册 API。这两种机制都不能为所有 HTTP 处理(包括 WebSocket 握手和所有其他 HTTP 请求——比如 Spring MVC 的 DispatcherServlet)使用一个“前端控制器”。

这是 JSR-356 的一个重要限制,Spring 的 WebSocket 支持在运行在 JSR-356 运行时中甚至在运行的时候,也能满足特定于服务器的请求。这种策略目前存在于 Tomcat、Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(和 WildFly)中。

已经创建了克服 Java WebSocket API 中上述限制的请求,并可以在 WEBSOCKET_SPEC-211 中进行跟踪。Tomcat、Undertow 和 WebSphere 提供了他们自己的 API 选项,这使得它成为可能,而且 Jetty 也有可能。我们希望更多的服务器也会这么做。

次要考虑的是,使用 JSR-356 支持的 Servlet 容器将执行一个 ServletContainerInitializer (SCI)扫描,它可以在某些情况下显著降低应用程序启动速度。如果在使用 JSR-356 支持的 Servlet 容器版本升级后观察到显著的影响,应该可以通过在 web.xml 中使用 <absolute-ordering /> 元素,选择性地启用或禁用 web 片段(和 SCI 扫描):

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering/>

</web-app>

然后,您可以通过名称选择性地启用 web 片段,比如 Spring 的 SpringServletContainerInitializer,它提供了对 Servlet 3 Java 初始化 API 的支持,如果需要的话:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering>
        <name>spring_web</name>
    </absolute-ordering>

</web-app>

服务器配置

每个底层 WebSocket 引擎都公开了控制运行时特性的配置属性,比如消息缓冲区大小、空闲超时和其他。

对于 Tomcat、WildFly 和 GlassFish,在 WebSocket Java config 中添加 ServletServerContainerFactoryBean

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }

}

或者 XML 命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>

</beans>

对于客户端 WebSocket 配置,应该使用 WebSocketContainerFactoryBean(XML)或 ContainerProvider.getWebSocketContainer()(Java config)。

对于 Jetty,您需要提供一个预配置的 Jetty WebSocketServerFactory,并通过 WebSocket Java config 将其插入 Spring 的 DefaultHandshakeHandler

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(),
            "/echo").setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }

}

或者 WebSocket XML 命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/echo" handler="echoHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>

    <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
        <constructor-arg ref="serverFactory"/>
    </bean>

    <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
        <constructor-arg>
            <bean class="org.eclipse.jetty...WebSocketPolicy">
                <constructor-arg value="SERVER"/>
                <property name="inputBufferSize" value="8092"/>
                <property name="idleTimeout" value="600000"/>
            </bean>
        </constructor-arg>
    </bean>

</beans>

允许来源 Allowed origins

在 Spring Framework 4.1.5 中,WebSocket 和 SockJS 的默认行为是只接受相同的源请求。也可以允许所有或指定的起源列表。这个检查主要是为浏览器客户设计的。没有什么可以阻止其他类型的客户机修改源报头值(参见 RFC 6454:The Web Origin Concept 了解更多细节)。

3种可能的行为是:

  • 只允许相同的源请求(默认):在这种模式下,当启用 SockJS 时,Iframe HTTP 响应头的 X-Frame-Options 被设置为 SAMEORIGIN,而 JSONP 传输是禁用的,因为它不允许检查请求的来源。因此,当启用此模式时,不支持 IE6 和 IE7。
  • 允许指定的起源列表:每个提供的允许来源必须以 http://https:// 开头。在这种模式下,当 SockJS 启用时,IFrame 和 JSONP 的传输都是禁用的。因此,在启用此模式时,不支持 IE6 到 IE9。
  • 允许所有来源:为了启用该模式,您应该提供 * 作为允许的原始值。在这种模式下,所有的传输都是可用的。

WebSocket 和 SockJS 的起源可以配置如下:

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("http://mydomain.com");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

等价的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers allowed-origins="http://mydomain.com">
        <websocket:mapping path="/myHandler" handler="myHandler" />
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

SockJS 回调 SockJS Fallback

在公共 Internet 上,控制外部的限制性代理可能会阻止 WebSocket 交互,因为它们没有被配置为传递 Upgrade 报文头,或者因为它们关闭了长时间运行的连接,而这些连接看起来是空闲的。

这个问题的解决方案是 WebSocket 仿真,即尝试先使用 WebSocket,然后再返回基于 HTTP 的技术,模拟 WebSocket 交互并公开相同的应用程序级 API。

在 Servlet 堆栈上,Spring 框架为 SockJS 协议提供了服务器(以及客户机)支持。

概述

SockJS 的目标是让应用程序使用 WebSocket API,但在运行时需要返回到非 WebSocket 替代品,即不需要更改应用程序代码。

SockJS 包括:

  • SockJS protocol 以可执行的 narrated tests 的形式定义。
  • SockJS JavaScript client ——用于浏览器的客户端库。
  • SockJS 服务器实现,包括 Spring Framework spring-websocket 模块中的一个。
  • 在 4.1 spring-websocket 中还提供了一个 SockJS Java 客户端。

SockJS 是为在浏览器中使用而设计的。它使用多种技术支持多种浏览器版本。对于 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS client 页面。传输分为 3 大类:WebSocket、HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参阅 this blog post

SockJS 客户端首先发送 GET /info 来从服务器获取基本信息。在那之后,它必须决定使用什么运输工具。如果可能使用 WebSocket。如果没有,在大多数浏览器中至少有一个 HTTP 流选项,如果没有使用 HTTP(长)轮询。

所有传输请求都有以下 URL 结构:

http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
  • {server-id} ——用于在集群中路由请求,但不用于其他用途。
  • {session-id} ——关联了属于一个 SockJS 会话的 HTTP 请求。
  • {transport} ——表示传输类型,例如 websocket、xhr-streaming 等。

WebSocket 传输只需要一个 HTTP 请求来完成 WebSocket 握手。此后所有的消息都在那个套接字上交换。

HTTP 传输需要更多的请求。例如,Ajax/XHR 流依赖于一个长期运行的请求,用于服务器到客户端消息,以及客户机到服务器消息的额外 HTTP POST 请求。长轮询类似,除非它在每个服务器对客户机发送之后结束当前请求。

SockJS 添加了最小的消息框架。例如,服务器发送字母 oopen 帧),消息被作为 a["message1","message2"](JSON 编码数组),字母 hheartbeat 帧),如果没有消息流 25 秒(默认值),和字母 cclose 帧)关闭会话。

要了解更多信息,请在浏览器中运行一个示例,并观察 HTTP 请求。SockJS 客户端允许修改传输列表,这样就可以一次查看每个传输。SockJS 客户端还提供了一个调试标志,可以在浏览器控制台中提供有用的消息。在服务器端,为 org.springframework.web.socket 启用跟踪日志记录。更多细节请参考 SockJS 协议的 narrated test

启用 SockJS

很容易通过 Java 配置启用 SockJS:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").withSockJS();
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

等价的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:sockjs/>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

上面的内容用于 Spring MVC 应用程序,应该包含在 DispatcherServlet 的配置中。然而,Spring 的 WebSocket 和 SockJS 支持并不依赖于 Spring MVC。在 SockJsHttpRequestHandler 的帮助下集成到其他 HTTP 服务环境相对简单。

在浏览器端,应用程序可以使用 sockjs-client(版本 1.0.x)来模拟 W3C WebSocket API,并与服务器进行通信,根据所运行的浏览器选择最佳的传输选项。查看 sockjs-client 页面和浏览器支持的传输类型列表。客户端还提供了几个配置选项,例如,指定要包含哪些传输。

IE 8、9

Internet Explorer 8 和 9 将在一段时间内保持通用。它们是拥有 SockJS 的关键原因。本节讨论在这些浏览器中运行的重要考虑。

SockJS 客户端通过微软的 XDomainRequest 支持 IE 8 和 9 的 Ajax/XHR 流。它可以跨域工作,但不支持发送 cookie。cookie 对于 Java 应用程序来说非常重要。然而,由于 SockJS 客户端可以与许多服务器类型(而不仅仅是 Java 类型)一起使用,所以它需要知道 cookie 是否重要。如果是这样,SockJS 客户更喜欢使用 Ajax/XHR 来进行流处理,或者其他方式依赖于基于 iframe 的技术。

SockJS 客户端的第一个 /info 请求是一个信息请求,它可以影响客户机对传输的选择。其中一个细节是服务器应用程序是否依赖于 cookie,例如用于身份验证的目的,或者是使用粘性会话的集群。Spring 的 SockJS 支持包括一个名为 sessionCookieNeeded 的属性。由于大多数 Java 应用程序依赖于 JSESSIONID cookie,因此默认启用它。如果您的应用程序不需要它,您可以关闭这个选项,SockJS 客户机应该在 IE 8 和 9 中选择 xdr 流。

如果您确实使用基于 iframe 的传输,并且在任何情况下,最好知道浏览器可以通过设置 HTTP 响应头 X-Frame-OptionsDENYSAMEORIGINALLOW-FROM <origin> 来阻止在给定页面上使用 iframe。这是用来防止 clickjacking 的。

Spring Security 3.2+ 提供了对每个响应设置 X-Frame-Options 的支持。默认情况下,Spring Security Java 配置将其设置为 DENY。在 3.2 中,Spring Security XML 命名空间不设置默认的头,但是可以配置为这样做,并且在将来它可能默认设置它。

参见 Spring Security文档的 7.1节“默认安全头”,详细说明如何配置 X-Frame-Options 头的设置。你也可以检查或查看 SEC-2501 作为额外的背景。

如果您的应用程序添加了 X-Frame-Options 响应头(应该是这样!),并且依赖于基于 iframe 的传输,那么您将需要将报文头值设置为 SAMEORIGINALLOW-FROM。此外,Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的。默认情况下,iframe 将从 CDN 位置下载 SockJS 客户端。将此选项配置为来自与应用程序相同的源的 URL 是一个好主意。

在 Java 配置中,可以这样做,如下所示。XML 命名空间通过 <websocket:sockjs> 元素提供了类似的选项:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS()
                .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
    }

    // ...

}

在最初的开发过程中,一定要启用 SockJS 客户机 devel 模式,以防止浏览器缓存 SockJS 请求(比如 iframe),否则将被缓存。有关如何启用它的详细信息,请参阅 SockJS client 页面。

心跳 Heartbeats

SockJS 协议要求服务器发送心跳消息以阻止代理终止连接。Spring SockJS 配置有一个名为 heartbeatTime 的属性,可以用来定制频率。默认情况下,在 25 秒后发送心跳,假设没有其他消息被发送到该连接上。这 25 秒 的值与以下 IETF 推荐的公共 Internet 应用程序一致。

当在 WebSocket/SockJS 上使用 STOMP 时,如果 STOMP 客户端和服务器通过协商心跳来交换,那么 SockJS 心跳就会被禁用。

Spring SockJS 支持还允许配置 TaskScheduler 用于调度心跳任务。任务调度器由一个线程池作为支持,默认设置基于可用处理器的数量。应用程序应该考虑根据特定的需求定制设置。

客户端断开 Client disconnects

HTTP 流和 HTTP 长轮询 SockJS 传输需要连接保持比平常更长时间。有关这些技术的概述,请参阅本博客文章。

在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,它允许退出 Servlet 容器线程处理请求,并继续从另一个线程写入响应。

一个特定的问题是 Servlet API 不为已经离开的客户机提供通知,请参阅 SERVLET_SPEC-44。然而,Servlet 容器在随后尝试写入响应的尝试中引发了一个异常。由于 Spring 的 SockJS 服务支持多次发送的心跳(默认为每 25 秒),这意味着在该时间段内或更早的时候,如果消息发送得更频繁,则通常会检测到客户端断开。

由于客户端已断开连接,因此可能会发生网络 IO 故障,这可以用不必要的堆栈跟踪填充日志。Spring 会尽最大努力来识别这些网络故障,这些故障表示客户机断开(特定于每个服务器),并使用 AbstractSockJsSession 中定义的专用日志类别 DISCONNECTED_CLIENT_LOG_CATEGORY 来记录一个最小的消息。如果需要查看堆栈跟踪,请将该日志类别设置为 TRACE

SockJS 和 CORS

如果允许跨源请求(参见允许的起源 Allowed origins),SockJS 协议在 XHR 流和轮询传输中使用 CORS 进行跨域支持。因此,除非检测到 CORS 头的存在,否则将自动添加 CORS 头。因此,如果应用程序已经配置为提供 CORS 支持,例如通过 Servlet 过滤器,Spring 的 SockJsService 将跳过这一部分。

在 Spring 的 SockJsService 中,还可以通过 suppressCors 属性禁用添加这些 CORS 头。

下面是 SockJS 所期望的报文头和值列表:

  • ccess-Control-Allow-Origin ——从 Origin 请求头的值初始化。
  • Access-Control-Allow-Credentials ——始终设置为 true
  • Access-Control-Request-Headers ——从等效请求头中初始化。
  • Access-Control-Allow-Methods ——传输支持的 HTTP 方法(查看 TransportType 枚举)。
  • Access-Control-Max-Age ——设置为 31536000(1 年)。

对于具体实现,请参见 AbstractSockJsService 中的 addCorsHeaders 和源代码中的 TransportType 枚举。

或者,如果 CORS 配置允许它考虑不使用 SockJS 端点前缀的 URL,从而允许 Spring 的 SockJsService 处理它。

SockJsClient

提供了一个 SockJS Java 客户端,以便在不使用浏览器的情况下连接到远程 SockJS 端点。当需要在公共网络上的 2 个服务器之间进行双向通信时,这一点特别有用,即网络代理可能阻止 WebSocket 协议的使用。SockJS Java 客户端对于测试目的也非常有用,例如可以模拟大量并发用户。

SockJS Java 客户端支持 websocket、xhr-streaming 和 xhr-polling 传输。剩下的只有在浏览器中使用才有意义。

WebSocketTransport 可以配置为:

  • StandardWebSocketClient,在 JSR-356 运行时
  • JettyWebSocketClient,使用 Jetty 9+ 原生 WebSocket API
  • Spring 的 WebSocketClient 的任何实现

由定义的 XhrTransport 支持 xhr-streaming 和 xhr-polling,因为从客户端的角度来看,除了用于连接到服务器的 URL 之外,没有其他区别。目前有两种实现方式:

  • RestTemplateXhrTransport 为 HTTP 请求使用 Spring 的 RestTemplate
  • JettyXhrTransport 为 HTTP 请求使用 Jetty 的 HttpClient

下面的示例展示了如何创建 SockJS 客户端并连接到 SockJS 端点:

List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

SockJS 使用 JSON 格式的数组作为消息。默认情况下,Jackson 2 被使用,并且需要在类路径上。或者,您可以配置 SockJsMessageCodec 的自定义实现,并在 SockJsClient 上配置它。
要使用 SockJsClient 来模拟大量并发用户,您需要配置底层 HTTP 客户端(用于 XHR 传输),以允许足够数量的连接和线程。例如使用 Jetty:

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

还可以考虑定制这些服务器端 SockJS 相关属性(详情请参见 Javadoc):

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/sockjs").withSockJS()
            .setStreamBytesLimit(512 * 1024)
            .setHttpMessageCacheSize(1000)
            .setDisconnectDelay(30 * 1000);
    }

    // ...
}

STOMP

WebSocket 协议定义了两种类型的消息,文本和二进制,但是它们的内容没有定义。定义了一个客户端和服务器的机制来协商一个子协议——即。一个更高级别的消息传递协议,可以在 WebSocket 上使用,来定义每个可以发送什么类型的消息,每个消息的格式和内容是什么等等。子协议的使用是可选的,但是客户机和服务器都需要在一些定义消息内容的协议上达成一致。

概述

STOMP 是一种简单的、面向文本的消息传递协议,最初是为脚本语言(如 Ruby、Python 和 Perl)创建的,用于连接企业消息代理。它的目的是解决常用消息传递模式的最小子集。STOMP 可以用于任何可靠的双向流网络协议,如 TCP 和 WebSocket。虽然 STOMP 是一种面向文本的协议,但消息有效负载可以是文本或二进制文件。

STOMP 是一种基于帧的协议,它的帧以 HTTP 为模型。STOMP 帧结构:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用 SENDSUBSCRIBE 命令来发送或订阅消息,并使用 destination 头来描述消息的内容以及谁应该接收消息。这支持一个简单的发布—订阅机制,该机制可用于通过代理向其他连接的客户机发送消息,或向服务器发送消息,请求执行一些工作。

当使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序充当客户的 STOMP 代理。消息被路由到 @Controller 消息处理方法,或者发送到一个简单的内存代理,该代理跟踪订阅并向订阅用户广播消息。您还可以配置 Spring 与专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)进行实际的消息广播。在这种情况下,Spring 维护与代理的 TCP 连接,将消息传递给它,并将消息从它传递到连接的 WebSocket 客户端。因此,Spring web 应用程序可以依赖于统一的基于 HTTP 的安全性、常见的验证和熟悉的编程模型消息处理工作。

下面是一个客户订阅的例子,该客户端订阅服务器可能定期发出的股票报价,例如通过一个计划的任务,通过一个 SimpMessagingTemplate 发送消息给代理:

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

这里有一个客户端发送交易请求的例子,服务器可以通过 @MessageMapping 方法处理,稍后在执行之后,将一个交易确认消息和详细信息广播到客户端:

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

目的地的含义在 STOMP 规范中是故意不透明的,它可以是任何字符串,并且完全由 STOMP 服务器来定义它们支持的目的地的语义和语法。然而,对于目的地是类似路径的字符串,/topic/.. 意味着发布—订阅(一对多)和 /queue/ 意味着点对点(一对一)的消息交换。

STOMP 服务器可以使用消息命令向所有订阅者广播消息。下面是一个服务器发送股票报价给订阅客户的例子:

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

重要的是要知道服务器不能发送未经请求的消息。服务器上的所有消息都必须响应特定的客户机订阅,而服务器消息的 subscription-id 头必须与客户机订阅的 id 头相匹配。

以上概述旨在为 STOMP 协议提供最基本的理解。建议全面审查协议规范。

益处

使用 STOMP 作为子协议可以使 Spring 框架和 Spring 安全性提供更丰富的编程模型替代使用原始的 WebSockets。同样的问题也可以通过 HTTP 和原始的 TCP 以及它如何支持 Spring MVC 和其他 web 框架来提供丰富的功能来实现。以下是一份益处清单:

无需创建自定义消息传递协议和消息格式。

STOMP 客户端可以在 Spring 框架中包括一个 Java 客户端。

消息代理(如 RabbitMQ、ActiveMQ 和其他)可以(可选地)用于管理订阅和广播消息。

应用程序逻辑可以组织在任意数量的 @Controller 和基于 STOMP 目的地头的消息中,并使用一个 WebSocketHandler 处理原始的 WebSocket 消息。

使用 Spring Security 来基于 STOMP 目的地和消息类型来保护消息。

启用 STOMP

spring-messagingspring-websocket 模块中提供了对 WebSocket 上的 STOMP 的支持。一旦您有了这些依赖项,您就可以将一个 STOMP 端点暴露出来,通过 WebSocket 与 SockJS 的回调,如下所示:

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // /portfolio 是指 WebSocket(或 SockJS)客户端需要连接到 WebSocket 握手的端点的HTTP URL
        registry.addEndpoint("/portfolio").withSockJS();  
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 在 @Controller 类中,以 /app 开头的 STOMP 消息被路由到 @MessageMapping 方法
        config.setApplicationDestinationPrefixes("/app"); 
        // 使用内置的消息代理进行订阅和广播;路由消息,其目的地头以 /topic 或 /queue 开头到代理
        config.enableSimpleBroker("/topic", "/queue"); 
    }
}

在 XML 中同样的配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio">
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/topic, /queue"/>
    </websocket:message-broker>

</beans>

对于内置的简单代理,/topic/queue 前缀没有任何特殊含义。它们仅仅是一种区分“发布-订阅”和“点-点”消息(即许多订阅者和一个使用者)之间的约定。当使用外部代理时,请检查代理的 STOMP 页面,以了解它支持哪些类型的 STOMP 目的地和前缀。
要从浏览器连接,对于 SockJS,您可以使用 sockjs-client。对于 STOMP,许多应用程序都使用 jmesnil/stomp-websocket 库(也称为 stomp.js),它是完整的,已经在生产中使用了很多年,但已经不再维护。目前,JSteunou/webstomp-client 是该库最活跃的维护和演进的继承者,下面的示例代码基于此:

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

或者如果通过 WebSocket 连接(没有 SockJS):

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

注意,上面的 stompClient 不需要指定登录和密码头。即使这样做了,在服务器端,它们也会被忽略,甚至被重写。有关身份验证的更多信息,请参见连接到代理和身份验证的部分。

需要更多示例代码请参考:

消息流 Flow of Messages

一旦暴露了 STOMP 端点,Spring 应用程序就成为连接客户端的 STOMP 代理。本节描述服务器端的消息流。

spring-messaging 模块包含了消息应用程序的基础支持,这些应用程序起源于 Spring Integration,随后被提取并集成到 Spring 框架中,用于跨多个 Spring 项目和应用程序场景的更广泛使用。下面是一些可用的消息传递抽象的列表:

Java配置(即 @EnableWebSocketMessageBroker)和 XML 命名空间配置(即 <websocket:message-broker>)都使用上述组件来组装消息工作流。下图显示了在启用简单的内置的消息代理时使用的组件:

上图有 3 个消息通道:

  • clientInboundChannel ——用于传递从 WebSocket 客户端接收到的消息。
  • clientOutboundChannel ——用于发送服务器消息给 WebSocket 客户端。
  • brokerChannel ——用于从服务端、应用程序代码发送消息给消息代理。

下一个图显示了外部代理(例如 RabbitMQ)用于管理订阅和广播消息时使用的组件:

image

上面的图的主要区别是使用代理中继器将消息传递给外部的 STOMP 代理,并将消息从代理传递到订阅客户端。

当从 WebSocket connectin 接收消息时,它们被解码到 STOMP 帧,然后转换成一个 Spring 消息表示,并发送到 clientInboundChannel 进行进一步处理。例如,可以将目标头从 /app 开头的 STOMP 消息路由到带注解的控制器中的 @MessageMapping 方法,而 /topic/queue 消息可以直接路由到消息代理。

一个带注解的 @Controller 处理客户端的 STOMP 消息,可以通过 brokerChannel 向消息代理发送消息,代理将通过 clientOutboundChannel 向匹配的订阅者广播消息。同样的控制器在响应 HTTP 请求时也可以执行相同的操作,因此客户机可以执行 HTTP POST,然后 @PostMapping 方法可以向消息代理发送消息,以便向订阅的客户机广播。

让我们通过一个简单的示例来跟踪流。给定以下服务器设置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio");
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }

}

@Controller
public class GreetingController {

    @MessageMapping("/greeting") {
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }

}
  1. 客户端连接到 http://localhost:8080/portfolio,一旦建立了 WebSocket 连接,STOMP 框架就开始运行。
  2. 客户端发送带有目的地头 /topic/greetingSUBSCRIBE 帧。接收并解码后,消息被发送到 clientInboundChannel,然后路由到消息代理,该代理存储客户端订阅。
  3. 客户端发送 SEND 帧到 /app/greeting/app 前缀有助于将其路由到带注解的控制器。去掉 /app 前缀后,目标的剩余 /greeting 部分映射到 GreetingController 中的 @MessageMapping 方法。
  4. GreetingController 返回的值以返回值和 /topic/greeting 的缺省目标标头(从输入目的地 /app 替换为 /topic),转换为带有 payload 的 Spring 消息。生成的消息被发送到 brokerChannel 并由消息代理处理。
  5. 消息代理找到所有匹配的订阅者,并通过 clientOutboundChannel 每个订阅者发送 MESSAGE 帧,其中消息被编码为 STOMP 帧,并发送到 WebSocket 连接。

下一节将提供关于带注解的方法的更多细节,包括支持的参数类型和返回值。

被注解的控制器

应用程序使用被 @Controller 注解的类处理客户端发送来的消息。这些类可以声明如下描述的 @MessageMapping@SubscribeMapping@ExceptionHandler 方法。

  1. @MessageMapping

@MessageMapping 注解可用于基于目的地路由消息的方法。在方法级和类型级别上都支持它。在类型级别 @MessageMapping 用于在控制器中跨所有方法表示共享映射。

默认情况下,目标映射是 Ant 风格的路径模式。比如 /foo*/foo/**。该模式包括对模板变量的支持。/foo/{id},可以用 @DestinationVariable 方法参数来引用。

应用程序可以选择切换到一个点分隔的目标约定。参考 Dot as Separator

@MessageMapping 方法可以具有以下参数的灵活签名:

方法参数 描述
Message 用于访问完整的消息
MessageHeaders 用于访问消息中的头
MessageHeaderAccessor, SimpMessageHeaderAccessor, StompHeaderAccessor 用于通过具体类型的 accessor 方法访问头
@Payload 用于访问消息的 payload,通过配置的 MessageConverter 转换(比如,从 JSON)。
由于默认情况下没有匹配其他参数,因此不需要此注释。
Payload 参数可以用 @javax.validation.Valid 或 Spring 的 @Validated 注解,以便被自动校验
@Header 用于访问特定头的值,根据需要使用 org.springframework.core.convert.converter.Converter 类型转换器
@Headers 用于访问消息中的所有头。这个参数必须为 java.util.Map 类型
@DestinationVariable 用于访问从消息目的地提取的模板变量。值将根据需要转换为声明的方法参数类型
java.security.Principal 反映在 WebSocket HTTP 握手时登录的用户

@MessageMapping 方法返回一个值时,默认情况下,该值通过配置的 MessageConverter 被序列化 payload,然后作为消息发送到向订阅者广播的 brokerChannel。出站消息的目的地与入站消息的目的地相同,但前缀为 /topic

您可以使用 @SendTo 方法注解来定制目的地,以将 payload 发送到该目的地。@SendTo 也可以在类级别使用,以共享一个默认的目标目的地来发送消息。@SendToUser 是只向与消息关联的用户发送消息的变体。有关详细信息,请参见用户目的地。

@MessageMapping 方法的返回值可以用 ListenableFutureCompletableFutureCompletionStage 来包装,以异步地生成 payload。

作为从 @MessageMapping 方法返回 payload 的替代方法,您还可以使用 SimpMessagingTemplate 发送消息,这也是如何在覆盖下处理返回值的方法。参见发送消息。

  1. @SubscribeMapping

@SubscribeMapping 注解与 @MessageMapping 结合使用,以缩小到订阅消息的映射。在这种情况下,@MessageMapping 注解指定目标,而 @SubscribeMapping 仅表示对订阅消息的兴趣。

@SubscribeMapping 方法通常与任何关于映射和输入参数的 @MessageMapping 方法没有区别。例如,您可以将它与一个方法级别的 @MessageMapping 结合起来,以表达一个共享的目标前缀,您可以使用与任何 @MessageMapping 方法相同的方法参数。

@SubscribeMapping 的关键区别在于,该方法的返回值被序列化为 payload,并发送到 clientOutboundChannel,而不是 brokerChannel,而是直接回复到客户端,而不是通过代理进行广播。这对于实现一次性的、“请求—应答”消息交换非常有用,并且从不占用订阅。这种模式的常见场景是当数据必须加载和呈现时应用程序初始化。

可以用 @SendTo 注解 @SubscribeMapping 方法,在这种情况下,返回值被发送到带有显式指定目标目的地的 brokerChannel

  1. @MessageExceptionHandler

应用程序可以使用 @MessageExceptionHandler 方法来处理 @MessageMapping 方法中的异常。感兴趣的异常可以在注解本身中声明,或者通过一个方法参数来声明,如果您想要访问异常实例:

@Controller
public class MyController {

    // ...

    @MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

@MessageExceptionHandler 方法支持灵活的方法签名,并支持与 @MessageMapping 方法相同的方法参数类型和返回值。

典型的 @MessageExceptionHandler 方法在 @Controller 类(或类层次结构)中应用。如果您想要这样的方法在全局范围内应用,跨控制器,您可以在标记为 @ControllerAdvice 的类中声明它们。这可与 Spring MVC 中的类似支持相媲美。

发送消息

如果您想从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以将消息发送到 brokerChannel。最简单的方法是将 SimpMessagingTemplate 注入,并使用它发送消息。通常情况下,它应该很容易被输入,例如:

@Controller
public class GreetingController {

    private SimpMessagingTemplate template;

    @Autowired
    public GreetingController(SimpMessagingTemplate template) {
        this.template = template;
    }

    @RequestMapping(path="/greetings", method=POST)
    public void greet(String greeting) {
        String text = "[" + getTimestamp() + "]:" + greeting;
        this.template.convertAndSend("/topic/greetings", text);
    }

}

但是,如果同一类型的另一个 bean 存在,它也可以通过它的名称 brokerMessagingTemplate 进行限定。

简单代理 Simple Broker

内置的、简单的消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目的地的连接客户端。代理支持类似路径的目的地,包括订阅 Ant 风格的目标模式。

应用程序还可以使用点分隔的目的地(vs slash)。参见 Dot as Separator

外部代理 External Broker

简单代理很适合入门,但是只支持 STOMP 命令的一个子集(例如,没有 ack、receipt 等),依赖于一个简单的消息发送循环,并且不适合集群。作为另一种选择,应用程序可以升级到使用功能齐全的消息代理。

检查 STOMP 文档来选择你的消息代理(例如 RabbitMQ、ActiveMQ 等),安装代理,并以开启 STOMP 支持的方式运行它。然后,在 Spring 配置中启用 STOMP 代理,而不是简单的代理。

下面是一个示例配置,它支持功能齐全的代理:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue");
        registry.setApplicationDestinationPrefixes("/app");
    }

}

等价的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio" />
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

</beans>

在上面的配置中, STOMP 代理中继器是一个 Spring MessageHandler,它通过将消息转发给外部消息代理来处理消息。为此,它将与代理建立 TCP 连接,将所有消息转发给它,然后通过 WebSocket 会话将从代理接收到的所有消息转发给客户端。从本质上说,它充当了一个“中继器”,在两个方向上转发消息。

请添加 org.projectreactor:reactor-netio.netty:netty-all 依赖于您的项目来管理 TCP 连接。

此外,应用程序组件(例如,HTTP 请求处理方法、业务服务等)也可以向代理中继发送消息,如发送消息中所描述的,以便将消息广播到订阅的 WebSocket 客户端。

实际上,代理中继器支持健壮和可伸缩的消息广播。

连接到代理

一个 STOMP 代理中继器维护一个与代理的单个“系统” TCP 连接。此连接仅用于从服务器端应用程序发出的消息,而不是用于接收消息。您可以为该连接配置 STOMP 凭据,即 STOMP 帧登录和密码头。这在 XML 命名空间和 Java 配置中都暴露为 systemLogin/systemPasscode 属性,默认值为 guest/guest

STOMP 代理中继器还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接。您可以为客户端创建的所有 TCP 连接配置 STOMP 凭据。这在 XML 命名空间和 Java 配置中都暴露为 cilentLogin/clientPasscode 属性,默认值为 guest/guest

STOMP代理中继总是在每一个 CONNECT 帧上设置登录和密码头,并代表客户端将其转发给代理。因此 WebSocket 客户机不需要设置这些头;他们将被忽略。正如身份验证部分解释的那样,WebSocket 客户机应该依靠 HTTP 身份验证来保护 WebSocket 端点并建立客户端标识。

在“系统” TCP 连接上,STOMP 代理中继器也发送和接收来自消息代理的心跳。您可以配置发送和接收心跳的间隔(默认为 10 秒)。如果与代理的连接丢失,代理中继器将继续尝试每 5 秒重新连接一次,直到成功。

任何 Spring bean 都可以实现 ApplicationListener,以便在丢失和重新建立代理的“系统”连接时接收通知。例如,当没有活动的“系统”连接时,股票报价服务广播股票报价可以停止发送消息。

默认情况下,STOMP 代理中继器总是连接,并在连接丢失时重新连接到同一个主机和端口。如果您希望提供多个地址,在每次尝试连接时,您可以配置一个地址供应商,而不是一个固定的主机和端口。例如:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
        registry.setApplicationDestinationPrefixes("/app");
    }

    private ReactorNettyTcpClient<byte[]> createTcpClient() {

        Consumer<ClientOptions.Builder<?>> builderConsumer = builder -> {
            builder.connectAddress(()-> {
                // Select address to connect to ...
            });
        };

        return new ReactorNettyTcpClient<>(builderConsumer, new StompReactorNettyCodec());
    }
}

STOMP 代理中继还可以配置一个虚拟主机属性。该属性的值将被设置为每个 CONNECT 帧的主机头,并可能在云环境中有用,在云环境中,建立 TCP 连接的实际主机与提供基于云的 STOMP 服务的主机不同。

点作为分隔符 Dot as Separator

当消息被路由到 @MessageMapping 方法时,它们将与 AntPathMatcher 匹配,并且默认模式将使用斜杠 / 作为分隔符。这在 web 应用程序中是一个很好的约定,类似于 HTTP URL。但是,如果您更习惯于消息传递约定,您可以切换到使用点 . 作为分隔符。

在 Java 配置中:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    // ...

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setPathMatcher(new AntPathMatcher("."));
        registry.enableStompBrokerRelay("/queue", "/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

在 XML 中:

<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:websocket="http://www.springframework.org/schema/websocket"
        xsi:schemaLocation="
                http://www.springframework.org/schema/beans
                http://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/websocket
                http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
        <websocket:stomp-endpoint path="/stomp"/>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

    
    <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
        <constructor-arg index="0" value="."/>
    </bean>
    

</beans>

之后,控制器可以使用点 . 作为分隔符在 @MessageMapping 方法中:

@Controller
@MessageMapping("foo")
public class FooController {

    @MessageMapping("bar.{baz}")
    public void handleBaz(@DestinationVariable String baz) {
        // ...
    }
}

客户现在可以发送一条消息到 /app/foo.bar.baz123

在上面的示例中,我们没有更改“代理中继”的前缀,因为它们完全依赖于外部消息代理。检查您正在使用的代理的 STOMP 文档页,以了解它支持目标标题的哪些约定。

另一方面,“简单代理”依赖于配置的 PathMatcher,因此,如果您切换该分隔符,它也将应用于代理,并将消息与订阅中的模式匹配。

认证 Authentication

WebSocket 消息会话的每一个 STOMP 都以一个 HTTP 请求开始——这可能是一个请求升级到 WebSockets(即 WebSocket 握手),或者在 SockJS 回调的例子中,是一系列的 SockJS HTTP 传输请求。

Web 应用程序已经有了用于安全 HTTP 请求的身份验证和授权。通常,用户通过 Spring Security 认证,使用一些机制,比如登录页面、HTTP 基本身份验证或其他机制。已验证的用户的安全上下文保存在 HTTP 会话中,并与在同一个基于 cookie 的会话中的后续请求相关联。

因此,对于 WebSocket 握手,或者对于 SockJS HTTP 传输请求,通常会有一个经过身份验证的用户通过 HttpServletRequest#getUserPrincipal() 访问。Spring 会自动将用户与 WebSocket 或 SockJS 会话关联起来,然后通过用户头将所有的 STOMP 消息传送到会话中。

简而言之,一个典型的 web 应用程序没有什么特别需要做的,除了它已经为安全所做的以外。在 HTTP 请求级别上,用户通过一个基于 cookie 的 HTTP 会话来进行身份验证,该 HTTP 会话将与为该用户创建的 WebSocket 或 SockJS 会话相关联,并在用户头上对流经应用程序的每个消息进行标记。

注意,STOMP 协议在 CONNECT 帧上有一个 loginpasscode 头。这些最初的设计是为了并且仍然需要例如在 TCP 上的 STOMP。但是,在默认情况下,对 WebSocket 上的 STOMP 忽略了 STOMP 协议级别的授权头,并假设用户已经在 HTTP 传输层上进行了身份验证,并期望 WebSocket 或 SockJS 会话包含经过身份验证的用户。

Spring Security 提供了 WebSocket 子协议授权,它使用一个 ChannelInterceptor 来基于用户头信息授权消息。另外,Spring 会话提供了一个 WebSocket 集成,以确保在 WebSocket 会话仍然处于活动状态时,用户 HTTP 会话不会过期。

令牌认证 Token Authentication

Spring Security OAuth 支持基于令牌的安全性,包括 JSON Web token (JWT)。这可以作为 Web 应用程序中的身份验证机制,包括对 WebSocket 交互的 STOMP,正如前一节所描述的那样,即通过基于 cookie 的会话维护身份。

与此同时,基于 cookie 的会话并不总是最适合在不希望在所有或移动应用程序中维护服务器端会话的应用程序中使用,在这些应用程序中,使用头进行身份验证是很常见的。

WebSocket 协议 RFC 6455 “没有规定服务器可以在 WebSocket 握手期间对客户进行身份验证的任何特殊方式”。但在实践中,浏览器客户端只能使用标准的身份验证头(即基本的 HTTP 身份验证)或 cookie,不能提供定制的头信息。同样,SockJS JavaScript 客户端不提供使用 SockJS 传输请求发送 HTTP 头的方法,参见 sockjs-client issue 196。相反,它允许发送可以用来发送令牌的查询参数,但这也有其自身的缺点,例如,令牌可能会在服务器日志中不经意地记录到 URL。

上述限制适用于基于浏览器的客户端,不适用于 Spring 基于 java 的 STOMP 客户端,它支持通过 WebSocket 和 SockJS 请求发送消息头。

因此,希望避免使用 cookie 的应用程序在 HTTP 协议级别上可能没有任何好的方法来进行身份验证。与其使用 cookie,他们可能更喜欢在 STOMP 消息协议级别的头文件中进行身份验证,有两个简单的步骤:

使用 STOMP 客户端在连接时传递身份验证头。
使用 ChannelInterceptor 处理身份验证头。

下面是用于注册自定义身份验证拦截器的示例服务器端配置。注意,拦截器只需要对连接消息进行身份验证和设置用户头。Spring 将注意并保存已验证的用户,并将其与同一会话中的后续 STOMP 消息关联:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new ChannelInterceptorAdapter() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ... ; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}

还要注意的是,在使用 Spring Security 对消息的授权时,目前需要确保在 Spring Security 之前命令验证 ChannelInterceptor 配置。最好通过声明自定义拦截器在其自己的 WebSocketMessageBrokerConfigurer@Order(Ordered.HIGHEST_PRECEDENCE + 99) 进行标记。

用户目的地 User Destinations

应用程序可以发送针对特定用户的消息,而 Spring 的 STOMP 支持可以识别以 /user/ 为目的的目的地。例如,客户端可能订阅目标 /user/queue/position-updates。该目的地将由 UserDestinationMessageHandler 处理,并转换为用户会话的惟一目标,例如,/queue/position-updates-user123。这提供了订阅一个通用命名的目的地的便利,同时确保不会与订阅同一目的地的其他用户发生冲突,这样每个用户都可以获得唯一的股票位置更新。

在发送端消息可以发送到一个目的地,例如 /user/{username}/queue/position-updates,这将由 UserDestinationMessageHandler 转换为一个或多个目的地,每个会话与用户关联。这允许应用程序中的任何组件发送针对特定用户的消息,而不需要知道任何比其名称和通用目的地更重要的信息。这也通过注解和消息传递模板来支持。

例如,消息处理方法可以向与通过 @SendToUser 注解处理的消息关联的用户发送消息(也支持在类级别上共享一个公共目标):

@Controller
public class PortfolioController {

    @MessageMapping("/trade")
    @SendToUser("/queue/position-updates")
    public TradeResult executeTrade(Trade trade, Principal principal) {
        // ...
        return tradeResult;
    }
}

如果用户有多个会话,默认情况下所有订阅的会话都是定向的。但是,有时可能只需要将发送消息的会话作为目标。这可以通过将 broadcast 属性设置为 false 来完成,例如:

@Controller
public class MyController {

    @MessageMapping("/action")
    public void handleAction() throws Exception{
        // raise MyBusinessException here
    }

    @MessageExceptionHandler
    @SendToUser(destinations="/queue/errors", broadcast=false)
    public ApplicationError handleException(MyBusinessException exception) {
        // ...
        return appError;
    }
}

虽然用户目的地通常意味着经过身份验证的用户,但并不需要严格要求。与经过身份验证的用户没有关联的 WebSocket 会话可以订阅用户目的地。在这种情况下,@SendToUser 注解将与 broadcast=false 行为完全相同,即只针对发送被处理消息的会话。

还可以通过注入由 Java config 或 XML 命名空间创建的 SimpMessagingTemplate 向用户目的地发送消息,例如(如果需要使用 @Qualifier, bean 名称是 brokerMessagingTemplate):

@Service
public class TradeServiceImpl implements TradeService {

    private final SimpMessagingTemplate messagingTemplate;

    @Autowired
    public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    // ...

    public void afterTradeExecuted(Trade trade) {
        this.messagingTemplate.convertAndSendToUser(
                trade.getUserName(), "/queue/position-updates", trade.getResult());
    }
}

当使用外部消息代理使用用户目的地时,请检查代理文档关于如何管理非活动队列,以便在用户会话结束时删除所有唯一的用户队列。例如,RabbitMQ 在诸如 /exchange/amq.direct/position-updates 等目的地创建自动删除队列。因此,在这种情况下,客户端可以订阅 /user/exchange/amq.direct/position-updates。类似地,ActiveMQ 具有清除非活动目的地的配置选项。

在多应用程序服务器场景中,用户目的地可能仍然没有解决,因为用户连接到不同的服务器。在这种情况下,您可以配置一个目的地来广播未解析的消息,以便其他服务器有机会尝试。这可以通过在 Java config 中 MessageBrokerRegistryuserDestinationBroadcast 属性和 XML 中消息代理元素的用户目的地广播属性来完成。

事件和拦截器

已经发布了几个 ApplicationContext 事件(下面列出),可以通过实现 Spring 的 ApplicationListener 接口来接收这些事件。

  • BrokerAvailabilityEvent ——指示代理何时可用/不可用。当“简单”代理在启动时立即可用,并且在应用程序运行时保持不变,那么 STOMP“代理中继器”可能会失去与功能完整的代理的连接,例如,如果代理重新启动。代理继电器重新连接逻辑,当它返回时将重新建立与代理的“系统”连接,因此当状态从连接到断开连接时,这个事件就会被发布。使用 SimpMessagingTemplate 的组件应该订阅此事件,并且在代理不可用时避免发送消息。在任何情况下,在发送消息时,他们都应该准备好处理 MessageDeliveryException
  • SessionConnectEvent ——当接收到新的 STOMP CONNECT 时发布,指示新的客户端会话的开始。该事件包含表示连接的消息,包括会话 id、用户信息(如果有的话)以及客户端可能发送的任何自定义头。这对于跟踪客户端会话非常有用。订阅此事件的组件可以使用 SimpMessageHeaderAccessorStompMessageHeaderAccessor 包装所包含的消息。
  • SessionConnectedEvent ——在一个 SessionConnectEvent 后不久发布,当代理发送一个STOMP CONNECTED 帧响应 CONNECT。在这一点上,可以认为 STOMP 会话是完全建立的。
  • SessionSubscribeEvent ——当接收到新的 STOMP SUBSCRIBE 时发布。
  • SessionUnsubscribeEvent ——当接收到新的 STOMP UNSUBSCRIBE 时发布。
  • SessionDisconnectEvent ——在 STOMP 会话结束时发布。断开连接可能是从客户端发送的,也可能在 WebSocket 会话关闭时自动生成。在某些情况下,该事件可能会在每次会话中发布一次以上。对于多个断开事件,组件应该具有幂等性。

当使用功能全面的代理时,STOMP “代理中继器”会自动地重新连接“系统”连接,以防代理临时不可用。然而,客户端连接不会自动重新连接。假设心跳被激活,客户通常会注意到代理在 10 秒内没有响应。客户端需要实现他们自己的重新连接逻辑。

此外,应用程序可以通过在各自的消息通道上注册一个 ChannelInterceptor 来直接拦截所有传入和传出消息。例如拦截入站消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.setInterceptors(new MyChannelInterceptor());
    }
}

一个定制的 ChannelInterceptor 可以扩展空的方法基类 ChannelInterceptorAdapter 并使用 StompHeaderAccessorSimpMessageHeaderAccessor 来访问有关消息的信息。

public class MyChannelInterceptor extends ChannelInterceptorAdapter {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

STOMP 客户端

Spring 为 WebSocket 客户端提供了一个 STOMP,并在 TCP 客户端提供了一个 STOMP。

要开始创建、配置 WebSocketStompClient

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在上面的示例中,可以用 SockJsClient 替换标准 WebSocketClient,因为这也是 WebSocketClient 的实现。SockJsClient 可以使用 WebSocket 或基于 HTTP 的传输作为回调。要了解更多细节,请参见 SockJsClient

接下来建立一个连接并为 STOMP 会话提供一个处理程序:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

当会话准备好使用时,将通知处理程序:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        // ...
    }
}

一旦确定了会话,就可以发送任何 payload,并将其序列化为已配置的 MessageConverter

session.send("/topic/foo", "payload");

您还可以订阅目的地。订阅方法要求订阅的消息处理程序,并返回可用于取消订阅的订阅句柄。对于每个接收到的消息,处理程序可以指定目标对象类型,并将 payload 反序列化为:

session.subscribe("/topic/foo", new StompFrameHandler() {

    @Override
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        // ...
    }

});

为了启用 STOMP心跳,配置 WebSocketStompClient 和一个 TaskScheduler,并可选自定义心跳间隔,10 秒钟用于写不活动,这将导致发送心跳的时间;10 秒钟的读不活动,这将关闭连接。

当使用 WebSocketStompClient 进行性能测试,以模拟来自同一台机器的数千个客户机时,考虑关闭心跳,因为每个连接都安排自己的心跳任务,而这并没有针对在同一台机器上运行的大量客户机进行优化。
STOMP 协议还支持客户端必须添加一个 receipt 头的收据,在发送或订阅处理后,服务器会用 RECEIPT 帧对其进行响应。为了支持这一点,StompSession 提供了 setAutoReceipt(boolean),它会在随后的每个发送或订阅中添加一个 receipt 头。或者,您也可以手动添加一个 receipt 头到 StompHeaders。发送和订阅都返回一个可用于注册获取成功和失败回调的 Receiptable 实例。对于这个特性,客户端必须配置一个 TaskScheduler,并且在收据到期之前的时间(默认为 15 秒)。

注意,StompSessionHandler 本身是一个 StompFrameHandler,它允许它处理 ERROR 帧,除了处理消息的异常的 handleException 回调,以及用于传输级错误的 handleTransportError,包括 ConnectionLostException

WebSocket Scope

每个 WebSocket 会话都有一个属性映射。映射被附加到入站客户端消息的头部,并且可以从控制器方法访问,例如:

@Controller
public class MyController {

    @MessageMapping("/action")
    public void handle(SimpMessageHeaderAccessor headerAccessor) {
        Map<String, Object> attrs = headerAccessor.getSessionAttributes();
        // ...
    }
}

还可以在 websocket 范围内声明 spring 管理的 bean。WebSocket-scoped bean 可以被注入到控制器和在 clientInboundChannel 上注册的任何通道拦截器中。这些都是典型的单例,比任何单独的 WebSocket 会话都要长。因此,您需要使用范围代理模式来使用 WebSocket-scoped bean:

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {

    @PostConstruct
    public void init() {
        // Invoked after dependencies injected
    }

    // ...

    @PreDestroy
    public void destroy() {
        // Invoked when the WebSocket session ends
    }
}

@Controller
public class MyController {

    private final MyBean myBean;

    @Autowired
    public MyController(MyBean myBean) {
        this.myBean = myBean;
    }

    @MessageMapping("/action")
    public void handle() {
        // this.myBean from the current WebSocket session
    }
}

与任何自定义范围一样,Spring 在第一次从控制器访问的时候初始化一个新的 MyBean 实例,并将实例存储在 WebSocket 会话属性中。在会话结束之前返回相同的实例。WebSocket-scoped bean 将会有所有 Spring 生命周期方法,如上面的示例所示。

性能

在性能方面没有什么灵丹妙药。许多因素可能会影响它,包括消息的大小、容量、应用程序是否执行需要阻塞的工作,以及诸如网络速度等外部因素。本节的目标是提供关于可用配置选项的概述,以及关于如何进行伸缩的一些想法。

在消息传递应用程序消息中,通过通道来传递由线程池支持的异步执行。配置这样的应用程序需要对通道和消息流有很好的了解。因此建议审查消息流。

最明显的起点是配置支持 clientInboundChannelclientOutboundChannel 的线程池。默认情况下,两者都配置为可用处理器数量的两倍。

如果在带注解的方法中处理消息的方式主要是 CPU,那么 clientInboundChannel 的线程数应该仍然接近处理器的数量。如果他们所做的工作是更多的 IO 限制,并且需要阻塞或等待数据库或其他外部系统,那么线程池的大小就需要增加。

ThreadPoolExecutor 有三个重要属性。这些是核心和最大线程池大小以及队列的容量,用于存储没有可用线程的任务。

混淆的一个常见问题是,配置核心池大小(例如 10)和最大池大小(例如 20)会导致线程池中有 1020 个线程。事实上,如果容量保持在它的默认值 Integer.MAX_VALUE,这时线程池将不会超出核心池大小,因为所有额外的任务都将被排队。

请查看 ThreadPoolExecutor 的 Javadoc,了解这些属性是如何工作的,并了解各种排队策略。

clientOutboundChannel 方面,它主要是向 WebSocket 客户端发送消息。如果客户端在快速网络上,那么线程的数量应该与可用处理器的数量保持接近。如果它们速度很慢,或者在低带宽上,它们将花费更长的时间来消耗消息,并给线程池带来负担。因此,增加线程池的大小是必要的。

尽管 clientInboundChannel 的工作负载是可以预测的——毕竟它是基于应用程序所做的工作——如何配置 clientOutboundChannel 是比较困难的,因为它基于应用程序控制之外的因素。出于这个原因,还有两个与发送消息相关的属性。这些是 sendTimeLimitsendBufferSizeLimit。这些数据用于配置发送的时间长度以及在向客户机发送消息时可以缓冲多少数据。

一般的想法是,在任何给定的时间,只有一个线程可用来发送给客户端。与此同时,所有附加的消息都得到了缓冲,您可以使用这些属性来决定允许发送消息的时间以及在平均时间中可以缓冲多少数据。请查看此配置的 XML 模式的 Javadoc 和文档,以获取重要的附加细节。

这是示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
    }

    // ...

}
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

上面显示的 WebSocket 传输配置也可以用来配置传入的 STOMP 消息的最大允许大小。尽管在理论上,WebSocket 消息的大小几乎是无限的,但实际上 WebSocket 服务器却有限制——例如,在 Tomcat 上有 8K, Jetty 上有 64K。出于这个原因,像 JavaScript webstomp-client 和其他人这样的 STOMP 客户端会在 16K 边界上拆分更大的 STOMP 消息,并将它们作为多个 WebSocket 消息发送,从而要求服务器进行缓冲和重新组装。

Spring 对 WebSocket 支持的 STOMP 可以这样做,因此应用程序可以为 STOMP 消息配置最大大小,而不考虑 WebSocket 服务器特定的消息大小。请记住,如果有必要的话,WebSocket 消息的大小将会自动调整,以确保它们能够以最少的速度传送 16K 的 WebSocket 消息。

这是示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(128 * 1024);
    }

    // ...

}
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker>
        <websocket:transport message-size="131072" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

关于缩放的一个重要问题是使用多个应用程序实例。目前,简单的代理是不可能做到这一点的。但是,当使用像 RabbitMQ 这样的全功能代理时,每个应用程序实例连接到代理,从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的 WebSocket 客户端。

监测 Monitoring

当使用 @EnableWebSocketMessageBroker<websocket:message-broker> 关键基础设施组件自动收集统计数据和计数器时,将对应用程序的内部状态提供重要的了解。配置还声明了一个 WebSocketMessageBrokerStats 类型的 bean,它在一个地方收集所有可用的信息,默认情况下每 30 分钟在 INFO 级别记录它。这个 bean 可以通过 Spring 的 MBeanExporter 导出到 JMX,以便在运行时查看,例如通过 JDK 的 jconsole。下面是可用信息的摘要。

Client WebSocket Sessions
    Current
        指示当前有多少客户端会话,通过 WebSocket 对 HTTP 流进行进一步细分,并轮询 SockJS 会话。
        
    Total
        指示已经建立了多少个会话。
        
    Abnormally Closed
        Connect Failures
            这些会话是建立起来的,但在 60 秒内没有收到任何消息后被关闭。这通常是代理或网络问题的指示。
            
        Send Limit Exceeded
            在超过配置的发送超时或发送缓冲区限制后,会话关闭(见前一节)。
            
        Transport Errors
            会话在传输错误后关闭,例如无法读取或写入 WebSocket 连接或 HTTP 请求/响应。
            
    STOMP Frames
        CONNECT、CONNECTED 和 DISCONNECT 帧的总数,表明有多少客户端连接到 STOMP 级别。请注意,当会话关闭异常或客户端关闭而不发送断开连接时,断开连接计数可能会降低。
        
STOMP Broker Relay
    TCP Connections
        指示为代理建立客户端 WebSocket 会话的 TCP 连接数。这应该等于客户端 WebSocket 会话的数量 +1 额外的共享“系统”连接,用于从应用程序中发送消息。
        
    STOMP Frames
        通过代理客户端转发或接收的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数。注意,无论客户端 WebSocket 会话是如何关闭的,DISCONNECT 帧都被发送到代理。因此,一个较低的 DISCONNECT 帧数表明,代理是主动关闭连接,可能是因为没有及时到达的心跳、无效的输入帧或其他。
        
Client Inbound Channel
    来自线程池的统计数据支持 clientInboundChannel,提供对传入消息处理的健康的洞察。在这里排队的任务表明应用程序处理消息的速度可能太慢。如果有 I/O 绑定任务(例如,缓慢的数据库查询、HTTP 请求到第三方 REST API 等),请考虑增加线程池的大小。
    
Client Outbound Channel
    来自线程池的统计数据支持 clientOutboundChannel,提供对向客户端广播消息的健康的洞察。在这里排队的任务是指示客户端太慢而不能使用消息。解决这一问题的一种方法是增加线程池的大小,以适应预期的并发慢客户机的数量。另一种选择是减少发送超时并发送缓冲区大小限制(参见前一节)。
    
SockJS Task Scheduler
    来自 SockJS 任务调度程序的线程池的统计信息,用于发送心跳。请注意,当心跳在 STOMP 水平上协商时,SockJS 心跳会被禁用。

测试

在 WebSocket 支持下,使用 Spring 的 STOMP 来测试应用程序有两种主要的方法。第一个是编写服务器端测试,验证控制器的功能和它们的带注解的消息处理方法。第二种方法是编写包含运行客户机和服务器的完整端到端测试。

这两种方法并不相互排斥。相反,在总体测试策略中每个都有一个位置。服务器端测试更集中,更容易编写和维护。另一方面,端到端集成测试更完整,测试也更丰富,但它们也更需要编写和维护。

最简单的服务器端测试形式是编写控制器单元测试。但是,这还不够用,因为控制器的大部分工作都依赖于它的注解。纯单元测试不能测试这个。

理想状态下的控制器应该在运行时被调用,就像测试控制器使用 Spring MVC 测试框架处理 HTTP 请求的方法一样。即不运行 Servlet 容器,而是依赖 Spring 框架来调用带注解的控制器。就像这里的 Spring MVC 测试一样,有两种可能的选择,要么使用“基于上下文的”或“独立的”设置:

  1. 在 Spring TestContext 框架的帮助下加载实际的 Spring 配置,将 clientInboundChannel 注入到测试字段中,并使用它来发送由控制器方法处理的消息。
  2. 手动设置调用控制器所需的最小 Spring 框架基础结构(即 SimpAnnotationMethodMessageHandler),并将控制器直接传递给控制器。

这两种设置场景都在股票组合示例应用程序的测试中得到了演示。

第二种方法是创建端到端集成测试。为此,您需要在嵌入式模式下运行 WebSocket 服务器,并将其连接到 WebSocket 客户端,以发送包含 STOMP 帧的 WebSocket 消息。股票组合样例应用程序的测试也演示了使用 Tomcat 作为嵌入式 WebSocket 服务器和一个简单的 STOMP 客户端进行测试的方法。

其它 Web 框架

介绍

本章详细介绍了 Spring 与第三方 web 框架的集成。

Spring 框架的核心价值主张之一是允许选择。从一般意义上说,Spring 并不强制使用或购买任何特定的体系结构、技术或方法(尽管它确实推荐了一些其他的架构)。选择与开发人员及其开发团队最相关的体系结构、技术或方法的这种自由在 web 区域是最明显,在那里 spring 提供了自己的 web 框架(spring MVC),同时提供与许多受欢迎的第三方 web 框架的集成。

通用配置

在深入讨论每个受支持的 web 框架的集成细节之前,让我们先看看不是特定于任何一个 web 框架的 Spring 配置。(此部分同样适用于 Spring 的 web 框架 Spring MVC。)

由(Spring 的)轻量级应用程序模型所支持的一个概念(因为缺少一个更好的词)是分层架构的。请记住,在“经典”分层架构中,web 层只是众多层中的一层;它是服务器端应用程序的入口点之一,它代表服务层中定义的服务对象(facades),以满足业务特定(和表示技术无关的)用例。在 Spring 中,这些服务对象、任何其他特定于业务的对象、数据访问对象等都存在于一个不同的“业务上下文”中,其中不包含 web 或表示层对象(如 Spring MVC 控制器之类的表示对象通常配置在不同的“表示上下文”中)。本节详细介绍如何配置一个包含应用程序中所有“业务 bean”的 Spring 容器(WebApplicationContext)。

关于细节:我们需要做的就是在 web 应用的标准 Java EE servlet web.xml 文件中声明一个 ContextLoaderListener,并添加一个 contextConfigLocation<context-param/> 部分(在同一个文件中),它定义了要加载的 Spring XML 配置文件集。

<listener/> 配置下面找到:

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param/> 配置下面找到:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

如果您没有指定 contextConfigLocation 上下文参数,那么 ContextLoaderListener 将查找一个名为 /WEB-INF/applicationContext.xml 的文件加载。一旦加载了上下文文件,Spring 就会根据 bean 定义创建一个 WebApplicationContext 对象,并将其存储在 web 应用程序的 ServletContext 中。

所有的 Java web 框架都构建在 Servlet API 之上,因此可以使用下面的代码片段来访问由 ContextLoaderListener 创建的“业务上下文” ApplicationContext

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 类是为了方便起见,所以您不必记住 ServletContext 属性的名称。如果一个对象在 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 键下不存在,它的 getWebApplicationContext() 方法将返回 null。使用 getRequiredWebApplicationContext() 方法比在应用程序中获取 NullPointerExceptions 更有好处。当应用程序上下文丢失时,该方法抛出异常。

一旦您有了对 WebApplicationContext 的引用,您就可以通过它们的名称或类型来检索 bean。大多数开发人员通过名称检索 bean,然后将其转换为实现的接口之一。

幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法。它们不仅可以很容易地从 Spring 容器中获取 bean,而且还允许您在它们的控制器上使用依赖项注入。每个 web 框架部分都详细介绍了其特定的集成策略。

JSF

JavaServer Faces (JSF) 是 JCP 的标准组件、事件驱动的 web 用户界面框架。在 Java EE 5 中,它是 Java EE 伞的正式部分。

对于流行的 JSF 运行时以及流行的 JSF 组件库,请查看 Apache MyFaces 项目。MyFaces 项目还提供了常见的 JSF 扩展,如 MyFaces Orchestra:基于 spring 的 JSF 扩展,提供了丰富的会话范围支持。

Spring Web Flow 2.0 通过其新建立的 Spring Faces 模块提供了丰富的 JSF 支持(如本节所述)和以 Spring 为中心的用法(在 Spring MVC dispatcher 中使用 JSF 视图)。查看 Spring Web Flow 网站了解详情!
Spring JSF 集成的关键元素是 JSF ELResolver 机制。

Spring Bean 解析器

SpringBeanFacesELResolver 是 JSF 1.2+ 兼容的 ELResolver 实现,集成了 JSF 1.2 和 JSP 2.1 所使用的标准 Unified EL。就像 SpringBeanVariableResolver 一样,它首先集成了 Spring 的 “业务上下文” WebApplicationContext,然后是底层 JSF 实现的默认解析器。

明智的配置是在您的 JSF faces—context.xml 文件中简单地定义 SpringBeanFacesELResolver

<faces-config>
    <application>
        <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
        ...
    </application>
</faces-config>

FacesContextUtils

当将一个的属性映射到 faces-config.xml 中的 bean 时,定制的 VariableResolver 可以很好地工作,但有时可能需要显式地获取 bean。FacesContextUtils 类使这变得很容易。它类似于 WebApplicationContextUtils,只是它使用 FacesContext 参数而不是 ServletContext 参数。

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());

Apache Struts 2.x

Struts 是由 Craig McClanahan 发明的,它是由 Apache Software Foundation 主办的开源项目。当时,它大大简化了 JSP/Servlet 编程范式,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此就像啤酒一样免费),而且它有一个很大的社区,这使得项目得以发展并在 Java web 开发人员中流行起来。

查看 Struts Spring 插件,用于内置 Struts 的内置 Spring 集成。

Tapestry 5.x

来自 Tapestry 的主页:

Tapestry 是一个“面向组件的框架,用于在 Java 中创建动态、健壮、高度可伸缩的 web 应用程序”。

虽然 Spring 有自己强大的 web 层,但是使用 Tapestry 为 web 用户界面和底层的 Spring 容器构建一个企业 Java 应用程序有许多独特的优势。

有关更多信息,请查看 Tapestry 的 Spring 专用集成模块。

进一步资源

下面是关于本章描述的各种 web 框架的进一步资源的链接。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容