Web on Servlet 堆栈

文档的此部分涵盖对基于 Servlet API 构建并部署到 Servlet 容器的 Servlet 堆栈 Web 应用程序的支持。各个章节包括Spring MVCView TechnologiesCORS SupportWebSocket Support。有关反应式堆栈 Web 应用程序,请参见网上反应堆

1. Spring Web MVC

Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从一开始就已包含在 Spring Framework 中。正式名称“ Spring Web MVC”来自其源模块的名称(spring-webmvc),但更通常称为“ Spring MVC”。

与 Spring Web MVCParallel,Spring Framework 5.0 引入了一个反应式堆栈 Web 框架,其名称“ Spring WebFlux”也基于其源模块(spring-webflux)。本节介绍 Spring Web MVC。 next section涵盖了 Spring WebFlux。

有关基线信息以及与 Servlet 容器和 Java EE 版本范围的兼容性,请参见 Spring Framework Wiki

1.1. DispatcherServlet

与 Spring WebFlux 中的相同

与其他许多 Web 框架一样,Spring MVC 围绕前端控制器模式进行设计,其中中央Servlet DispatcherServlet提供了用于请求处理的共享算法,而实际工作是由可配置的委托组件执行的。该模型非常灵活,并支持多种工作流程。

_和其他Servlet一样,需要根据 Servlet 规范通过使用 Java 配置或在web.xml中进行声明和 Map。反过来,DispatcherServlet使用 Spring 配置发现请求 Map,视图解析,异常处理and more所需的委托组件。

以下 Java 配置示例注册并初始化DispatcherServlet,它由 Servlet 容器自动检测(请参见Servlet Config):

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/*");
    }
}

Note

除了直接使用 ServletContext API 外,您还可以扩展AbstractAnnotationConfigDispatcherServletInitializer并覆盖特定方法(请参见Context Hierarchy下的示例)。

以下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>

Note

Spring Boot 遵循不同的初始化 Sequences。 Spring Boot 并没有陷入 Servlet 容器的生命周期,而是使用 Spring 配置来引导自身和嵌入式 Servlet 容器。在 Spring 配置中检测到FilterServlet声明,并在 Servlet 容器中注册。有关更多详细信息,请参见Spring Boot 文档

1.1.1. 上下文层次

DispatcherServlet期望其自己的配置为WebApplicationContext(纯ApplicationContext的 extensions)。 WebApplicationContext具有到ServletContext和与其关联的Servlet的链接。它还绑定到ServletContext,以便应用程序可以在RequestContextUtils上使用静态方法来查找WebApplicationContext(如果需要访问它们)。

对于许多应用程序来说,只有一个WebApplicationContext很简单并且足够。也可能具有上下文层次结构,其中一个根WebApplicationContext跨多个DispatcherServlet(或其他Servlet)实例共享,每个实例都有其自己的子WebApplicationContext配置。有关上下文层次结构功能的更多信息,请参见ApplicationContext 的其他功能

WebApplicationContext通常包含需要在多个Servlet实例之间共享的基础结构 Bean,例如数据存储库和业务服务。这些 Bean 是有效继承的,可以在 Servlet 特定子WebApplicationContext中重写(即重新声明),该子WebApplicationContext通常包含给定Servlet本地的 Bean。下图显示了这种关系:

mvc 上下文层次结构

以下示例配置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/*" };
    }
}

Tip

如果不需要应用程序上下文层次结构,则应用程序可以通过getServletConfigClasses()getRootConfigClasses()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>

Tip

如果不需要应用程序上下文层次结构,则应用程序可以仅配置“根”上下文,并将contextConfigLocation Servlet 参数保留为空。

1.1.2. 特殊 bean 类

与 Spring WebFlux 中的相同

DispatcherServlet委托特殊 bean 处理请求并呈现适当的响应。所谓“特殊 bean”,是指实现 WebFlux 框架 Contract 的 SpringManagement 的Object实例。这些通常带有内置 Contract,但是您可以自定义它们的属性并扩展或替换它们。

下表列出了DispatcherHandler检测到的特殊 bean:

Bean type Explanation
HandlerMapping 将请求与interceptors列表一起 Map 到处理程序,以进行预处理和后期处理。Map 基于某些条件,具体取决于HandlerMapping实现。


HandlerMapping的两个主要实现是RequestMappingHandlerMapping(支持@RequestMapping带注解的方法)和SimpleUrlHandlerMapping(将 URI 路径模式的显式注册维护到处理程序)。
| HandlerAdapter |帮助DispatcherServlet调用 Map 到请求的处理程序,无论实际如何调用该处理程序。例如,调用带注解的控制器需要解析注解。 HandlerAdapter的主要目的是使DispatcherServlet免受此类细节的影响。
| HandlerExceptionResolver |解决异常的策略,可能将它们 Map 到处理程序,HTML 错误视图或其他目标。参见Exceptions
| ViewResolver |解析从处理程序返回到实际View的基于逻辑String的视图名称,使用该视图名称呈现给响应。参见View ResolutionView Technologies
| LocaleResolverLocaleContextResolver |解析 Client 端正在使用的Locale以及可能的时区,以便能够提供国际化的视图。参见Locale
| ThemeResolver |解决 Web 应用程序可以使用的主题,例如提供个性化的布局。参见Themes
| MultipartResolver |用于借助一些 Multipart 解析库来分析 Multipart 请求(例如,浏览器表单文件上载)的抽象。参见Multipart Resolver
| FlashMapManager |存储和检索“Importing”和“输出” FlashMap,它们可用于将属性从一个请求传递到另一个请求,通常跨重定向。参见Flash Attributes

1.1.3. Web MVC 配置

与 Spring WebFlux 中的相同

应用程序可以声明处理请求所必需的特殊 bean 类中列出的基础结构 bean。 DispatcherServlet为每个特殊 bean 检查WebApplicationContext。如果没有匹配的 Bean 类型,它将使用DispatcherServlet.properties中列出的默认类型。

在大多数情况下,MVC Config是最佳起点。它使用 Java 或 XML 声明所需的 bean,并提供更高级别的配置回调 API 对其进行自定义。

Note

Spring Boot 依靠 MVC Java 配置来配置 Spring MVC,并提供许多额外的方便选项。

1.1.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 容器。名为AbstractDispatcherServletInitializerWebApplicationInitializer的抽象 Base Class 实现通过覆盖方法来指定 servletMap 和DispatcherServlet配置的位置,从而使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实例,并将它们自动 Map 到DispatcherServlet,如以下示例所示:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }
}

每个过滤器都会根据其具体类型添加一个默认名称,并自动 Map 到DispatcherServlet

AbstractDispatcherServletInitializer的受isAsyncSupported保护的方法提供了一个位置,以对DispatcherServlet及其 Map 的所有过滤器启用异步支持。默认情况下,此标志设置为true

最后,如果您需要进一步自定义DispatcherServlet本身,则可以覆盖createDispatcherServlet方法。

1.1.5. Processing

与 Spring WebFlux 中的相同

DispatcherServlet处理请求的方式如下:

  • 搜索WebApplicationContext并将其绑定在请求中,作为控制器和流程中其他元素可以使用的属性。默认情况下,它是在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE键下绑定的。

  • 语言环境解析器绑定到请求,以使流程中的元素解析在处理请求(呈现视图,准备数据等)时要使用的语言环境。如果不需要语言环境解析,则不需要语言环境解析器。

  • 主题解析器绑定到请求,以使诸如视图之类的元素确定要使用的主题。如果不使用主题,则可以忽略它。

  • 如果指定 Multipart 文件解析器,则将检查请求中是否有 Multipart。如果找到 Multipart,则将请求包装在MultipartHttpServletRequest中,以供流程中的其他元素进一步处理。有关 Multipart 处理的更多信息,请参见Multipart Resolver

  • 搜索适当的处理程序。如果找到处理程序,则执行与处理程序(预处理器,后处理器和控制器)关联的执行链,以准备模型或渲染。或者,对于带注解 的控制器,可以呈现响应(在HandlerAdapter内),而不返回视图。

  • 如果返回模型,则呈现视图。如果没有返回任何模型(可能是由于预处理器或后处理器拦截了该请求,可能出于安全原因),则不会呈现任何视图,因为该请求可能已经被满足。

WebApplicationContext中声明的HandlerExceptionResolver bean 用于解决在请求处理期间引发的异常。这些异常解析器允许定制逻辑以解决异常。有关更多详细信息,请参见Exceptions

Spring DispatcherServlet还支持 Servlet API 指定的last-modification-date的返回。确定特定请求的最后修改日期的过程很简单:DispatcherServlet查找适当的处理程序 Map 并测试找到的处理程序是否实现LastModified接口。如果是这样,则LastModified接口的long getLastModified(request)方法的值返回给 Client 端。

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

表 1. DispatcherServlet 初始化参数

Parameter Explanation
contextClass 实现ConfigurableWebApplicationContext的类,将由此 Servlet 实例化并在本地配置。默认情况下,使用XmlWebApplicationContext
contextConfigLocation 传递给上下文实例的字符串(由contextClass指定),以指示可以在哪里找到上下文。该字符串可能包含多个字符串(使用逗号作为分隔符)以支持多个上下文。对于具有两次定义的 bean 的多个上下文位置,以最新位置为准。
namespace WebApplicationContext的命名空间。默认为[servlet-name]-servlet
throwExceptionIfNoHandlerFound 在找不到请求处理程序时是否抛出NoHandlerFoundException。然后可以使用HandlerExceptionResolver捕获异常(例如,通过使用@ExceptionHandler控制器方法),然后将其作为其他任何异常进行处理。


默认情况下,它设置为false,在这种情况下DispatcherServlet将响应状态设置为 404(NOT_FOUND),而不会引发异常。
请注意,如果还配置了默认 servlet 处理,则始终将未解决的请求转发到默认 servlet,并且永远不会引发 404.

1.1.6. Interception

所有HandlerMapping实现都支持处理程序拦截器,这些拦截器在您要将特定功能应用于某些请求时非常有用-例如检查主体。拦截器必须使用三种方法从org.springframework.web.servlet包中实现HandlerInterceptor,这三种方法应提供足够的灵 Active 以执行所有类型的预处理和后处理:

  • preHandle(..):在执行实际处理程序之前

  • postHandle(..):执行处理程序后

  • afterCompletion(..):完成完整的请求后

preHandle(..)方法返回布尔值。您可以使用此方法来中断或 continue 执行链的处理。当此方法返回true时,处理程序执行链 continue。当它返回 false 时,DispatcherServlet假定拦截器本身已经处理了请求(例如,提供了适当的视图),并且不会 continue 执行执行链中的其他拦截器和实际处理程序。

有关如何配置拦截器的示例,请参见 MVC 配置部分中的Interceptors。您还可以通过使用各个HandlerMapping实现上的设置器直接注册它们。

请注意,postHandle@ResponseBodyResponseEntity方法中用处不大,因为在HandlerAdapter内和postHandle之前将响应写入和提交。这意味着对响应进行任何更改为时已晚,例如添加额外的 Headers。对于此类情况,您可以实现ResponseBodyAdvice并将其声明为Controller Advice bean 或直接在RequestMappingHandlerAdapter上对其进行配置。

1.1.7. Exceptions

与 Spring WebFlux 中的相同

如果在请求 Map 期间发生异常或从请求处理程序(例如@Controller)引发异常,则DispatcherServlet委托HandlerExceptionResolver bean 链来解决该异常并提供替代处理,通常是错误响应。

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

表 2. HandlerExceptionResolver 实现

HandlerExceptionResolver Description
SimpleMappingExceptionResolver 异常类名称和错误视图名称之间的 Map。对于在浏览器应用程序中呈现错误页面很有用。
DefaultHandlerExceptionResolver 解决了 Spring MVC 引发的异常,并将它们 Map 到 HTTP 状态代码。另请参见ResponseEntityExceptionHandlerREST APIexception
ResponseStatusExceptionResolver 使用@ResponseStatusComments 解决异常,并根据注解 中的值将其 Map 到 HTTP 状态代码。
ExceptionHandlerExceptionResolver 通过调用@Controller@ControllerAdvice类中的@ExceptionHandler方法来解决异常。参见@ExceptionHandler methods
解析器链

您可以通过在 Spring 配置中声明多个HandlerExceptionResolver bean 并根据需要设置其order属性来形成异常解析器链。 order 属性越高,异常解析器的定位就越晚。

HandlerExceptionResolver的 Contract 规定它可以返回:

  • ModelAndView指向错误视图。

  • 如果在解析程序中处理了异常,则为空ModelAndView

  • null如果仍未解决异常,则供以后的解析器尝试,并且,如果异常仍在末尾,则允许其冒泡到 Servlet 容器。

MVC Config自动为默认的 Spring MVC 异常,@ResponseStatus带注解 的异常以及对@ExceptionHandler方法的支持声明内置解析器。您可以自定义该列表或替换它。

容器错误页面

如果任何HandlerExceptionResolver都无法解决异常,因此该异常可以 continue 传播,或者如果响应状态设置为错误状态(即 4xx,5xx),则 Servlet 容器可以在 HTML 中呈现默认错误页面。要自定义容器的默认错误页面,可以在web.xml中声明一个错误页面 Map。以下示例显示了如何执行此操作:

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

给定前面的示例,当异常冒出气泡或响应具有错误状态时,Servlet 容器在容器内向配置的 URL(例如/error)进行 ERROR 调度。然后由DispatcherServlet处理,可能将其 Map 到@Controller,可以实现该错误以返回带有模型的错误视图名称或呈现 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;
    }
}

Tip

Servlet API 没有提供在 Java 中创建错误页面 Map 的方法。但是,您可以同时使用WebApplicationInitializer和最小的web.xml

1.1.8. 查看分辨率

与 Spring WebFlux 中的相同

Spring MVC 定义了ViewResolverView接口,这些接口使您可以在浏览器中呈现模型,而无需将您绑定到特定的视图技术。 ViewResolver提供了视图名称和实际视图之间的 Map。 View解决了在移交给特定视图技术之前的数据准备问题。

下表提供了有关ViewResolver层次结构的更多详细信息:

表 3. ViewResolver 实现

ViewResolver Description
AbstractCachingViewResolver AbstractCachingViewResolver缓存视图实例所解析的子类。缓存可以提高某些视图技术的性能。您可以通过将cache属性设置为false来关闭缓存。此外,如果必须在运行时刷新某个视图(例如,修改 FreeMarker 模板时),则可以使用removeFromCache(String viewName, Locale loc)方法。
XmlViewResolver ViewResolver的实现,该实现接受用 XML 编写的配置文件,该配置文件的 DTD 与 Spring 的 XML bean 工厂相同。默认配置文件为/WEB-INF/views.xml
ResourceBundleViewResolver ViewResolver的实现使用ResourceBundle中的 bean 定义(由包基本名称指定)。对于应该解析的每个视图,它将属性[viewname].(class)的值用作视图类,并将属性[viewname].url的值用作视图 URL。您可以在View Technologies的章节中找到示例。
UrlBasedViewResolver ViewResolver接口的简单实现会影响逻辑视图名称到 URL 的直接解析,而无需显式 Map 定义。如果您的逻辑名称以直接的方式与视图资源的名称匹配,而不需要任意 Map,则这是适当的。
InternalResourceViewResolver 方便的UrlBasedViewResolver子类,支持InternalResourceView(实际上是 Servlet 和 JSP)以及JstlViewTilesView之类的子类。您可以使用setViewClass(..)为该解析器生成的所有视图指定视图类。有关详细信息,请参见UrlBasedViewResolver javadoc。
FreeMarkerViewResolver UrlBasedViewResolver的便捷子类,支持FreeMarkerView及其自定义子类。
ContentNegotiatingViewResolver ViewResolver接口的实现,该接口根据请求文件名或AcceptHeaders 解析视图。参见Content Negotiation
Handling

与 Spring WebFlux 中的相同

您可以pass 语句多个解析器 bean 以及必要时通过设置order属性以指定 Sequences 来链接视图解析器。请记住,order 属性越高,视图解析器在链中的定位就越晚。

ViewResolver的协定指定它可以返回 null 来指示找不到该视图。但是,对于 JSP 和InternalResourceViewResolver而言,弄清 JSP 是否存在的唯一方法是通过RequestDispatcher进行调度。因此,必须始终将InternalResourceViewResolver配置为在视图解析器的总体 Sequences 中排在最后。

配置视图分辨率就像在 Spring 配置中添加ViewResolver bean 一样简单。 MVC ConfigView Resolvers和添加无逻辑的View Controllers提供了专用的配置 API,这对于不带控制器逻辑的 HTML 模板呈现非常有用。

Redirecting

与 Spring WebFlux 中的相同

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

最终效果与控制器返回RedirectView的效果相同,但是现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如redirect:/myapp/some/resource)相对于当前 Servlet 上下文重定向,而名称(例如redirect:http://myhost.com/some/arbitrary/path)重定向到绝对 URL。

请注意,如果控制器方法用@ResponseStatusComments,则注解 值优先于RedirectView设置的响应状态。

Forwarding

您还可以为视图名称使用特殊的forward:前缀,这些名称最终由UrlBasedViewResolver和子类解析。这将创建一个InternalResourceView,它执行RequestDispatcher.forward()。因此,此前缀对于InternalResourceViewResolverInternalResourceView(对于 JSP)没有用,但是如果您使用另一种视图技术,但仍然希望强制转发由 Servlet/JSP 引擎处理的资源,则该前缀很有用。请注意,您也可以改为链接多个视图解析器。

Content Negotiation

与 Spring WebFlux 中的相同

ContentNegotiatingViewResolver本身不会解析视图,而是委派给其他视图解析器,并选择类似于 Client 端请求的表示形式的视图。可以从AcceptHeaders 或查询参数(例如"/path?format=pdf")确定表示形式。

ContentNegotiatingViewResolver通过将请求媒体类型与与每个ViewResolvers关联的View支持的媒体类型(也称为Content-Type)进行比较,从而选择合适的View来处理请求。列表中具有兼容Content-Type的第一个View将表示形式返回给 Client 端。如果ViewResolver链无法提供兼容的视图,请查阅通过DefaultViews属性指定的视图列表。后一个选项适用于单例Views,该单例Views可以呈现当前资源的适当表示形式,而与逻辑视图名称无关。 AcceptHeaders 可以包含通配符(例如text/*),在这种情况下,Content-Typetext/xmlView是兼容的匹配。

有关配置详细信息,请参见MVC Config下的View Resolvers

1.1.9. Locale

正如 Spring Web MVC 框架所做的那样,Spring 体系结构的大多数部分都支持国际化。 DispatcherServlet使您可以使用 Client 端的语言环境自动解析邮件。这是通过LocaleResolver个对象完成的。

收到请求时,DispatcherServlet查找语言环境解析器,如果找到一个,它将尝试使用它来设置语言环境。通过使用RequestContext.getLocale()方法,您始终可以检索由语言环境解析器解析的语言环境。

除了自动的语言环境解析之外,您还可以在处理程序 Map 上附加一个拦截器(有关处理程序 Map 拦截器的更多信息,请参见Interception)以在特定情况下(例如,基于请求中的参数)更改语言环境。

语言环境解析器和拦截器在org.springframework.web.servlet.i18n包中定义,并以常规方式在您的应用程序上下文中配置。 Spring 包含以下选择的语言环境解析器。

Time Zone

除了获取 Client 的语言环境外,了解其时区通常也很有用。 LocaleContextResolver界面提供了对LocaleResolver的扩展,该扩展使解析器可以提供更丰富的LocaleContext,其中可能包含时区信息。

如果可用,可以使用RequestContext.getTimeZone()方法获取用户的TimeZone。 Spring 的ConversionService注册的任何 Date/Time ConverterFormatter对象都会自动使用时区信息。

Header Resolver

此语言环境解析器检查 Client 端(例如,Web 浏览器)发送的请求中的accept-languageHeaders。通常,此头字段包含 Client 端 os 的语言环境。请注意,此解析器不支持时区信息。

此语言环境解析器检查 Client 端上可能存在的Cookie,以查看是否指定了LocaleTimeZone。如果是这样,它将使用指定的详细信息。通过使用此语言环境解析器的属性,可以指定 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>

下表描述了属性CookieLocaleResolver

表 4. CookieLocaleResolver 属性

Session Resolver

SessionLocaleResolver可让您从可能与用户请求关联的会话中检索LocaleTimeZone。与CookieLocaleResolver相反,此策略将本地选择的语言环境设置存储在 Servlet 容器的HttpSession中。结果,这些设置对于每个会话都是临时的,因此在每个会话终止时会丢失。

请注意,与外部会话 Management 机制(例如 Spring Session 项目)没有直接关系。该SessionLocaleResolver对当前HttpServletRequest评估并修改了相应的HttpSession属性。

Locale Interceptor

您可以通过将LocaleChangeInterceptor添加到HandlerMapping定义之一来启用语言环境更改。它在请求中检测到一个参数,并相应地更改语言环境,从而在调度程序的应用程序上下文中在LocaleResolver上调用setLocale方法。下一个示例显示对包含参数siteLanguage的所有*.view资源的调用现在会更改语言环境。因此,例如,对 URL 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>

1.1.10. Themes

您可以应用 Spring Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源(通常是样式表和图像)的集合,这些资源会影响应用程序的视觉样式。

定义主题

要在 Web 应用程序中使用主题,必须设置org.springframework.ui.context.ThemeSource接口的实现。 WebApplicationContext接口扩展了ThemeSource,但将其职责委托给专用的实现。默认情况下,委托是org.springframework.ui.context.support.ResourceBundleThemeSource实现,该实现从 Classpath 的根目录加载属性文件。要使用自定义ThemeSource实现或配置ResourceBundleThemeSource的基本名称前缀,可以在应用程序上下文中使用保留名称themeSource注册 Bean。 Web 应用程序上下文会自动检测到具有该名称的 bean 并使用它。

当您使用ResourceBundleThemeSource时,将在一个简单的属性文件中定义一个主题。属性文件列出了组成主题的资源,如以下示例所示:

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使用空的基本名称前缀。结果,从 Classpath 的根加载属性文件。因此,您可以将cool.properties主题定义放在 Classpath 的根目录中(例如,在/WEB-INF/classes中)。 ResourceBundleThemeSource使用标准的 Java 资源束加载机制,允许主题的完全国际化。例如,我们可能有一个/WEB-INF/classes/cool_nl.properties,它引用了带有荷兰 Literals 的特殊背景图像。

Resolving Themes

定义主题后,如preceding section中所述,您可以决定使用哪个主题。 DispatcherServlet查找名为themeResolver的 bean,以找出要使用的ThemeResolver实现。主题解析器的工作方式与LocaleResolver大致相同。它可以检测用于特定请求的主题,还可以更改请求的主题。下表描述了 Spring 提供的主题解析器:

表 5. ThemeResolver 实现

Class Description
FixedThemeResolver 选择通过使用defaultThemeName属性设置的固定主题。
SessionThemeResolver 主题在用户的 HTTP 会话中维护。每个会话只需设置一次,但在会话之间不会保留。
CookieThemeResolver 所选主题存储在 Client 端的 cookie 中。

Spring 还提供了一个ThemeChangeInterceptor,该主题可以使用简单的 request 参数在每个请求上更改主题。

1.1.11. Multipart 解析器

与 Spring WebFlux 中的相同

org.springframework.web.multipart包中的MultipartResolver是一种用于解析包括文件上传在内的 Multipart 请求的策略。有一种基于Commons FileUpload的实现,另一种基于 Servlet 3.0Multipart 请求解析。

要启用 Multipart 处理,您需要在DispatcherServlet Spring 配置中声明名称为multipartResolverMultipartResolver bean。 DispatcherServlet检测到它并将其应用于传入的请求。收到 Content Type 为multipart/form-data的 POST 时,解析程序将解析内容并将当前HttpServletRequest包装为MultipartHttpServletRequest以提供对已解析部分的访问权限,除了将其公开为请求参数之外。

Apache Commons FileUpload

要使用 Apache Commons FileUpload,可以配置名称为multipartResolver的类型CommonsMultipartResolver的 bean。您还需要commons-fileupload作为对 Classpath 的依赖。

Servlet 3.0

需要通过 Servlet 容器配置启用 Servlet 3.0Multipart 解析。为此:

  • 在 Java 中,在 Servlet 注册上设置MultipartConfigElement

  • web.xml中,将"<multipart-config>"部分添加到 Servlet 声明中。

以下示例显示了如何在 Servlet 注册上设置MultipartConfigElement

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 配置到位后,您可以添加名称为multipartResolver的类型为StandardServletMultipartResolver的 bean。

1.1.12. Logging

与 Spring WebFlux 中的相同

Spring MVC 中的 DEBUG 级别的日志被设计为紧凑,最少且人性化的。它侧重于高价值的信息,这些信息一遍又一遍地有用,而其他信息则仅在调试特定问题时才有用。

TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,也不应是消防水带),但可用于调试任何问题。此外,某些日志消息在 TRACE 和 DEBUG 上可能显示不同级别的详细信息。

良好的日志记录来自使用日志的经验。如果发现任何不符合既定目标的东西,请告诉我们。

Sensitive Data

与 Spring WebFlux 中的相同

调试和跟踪日志记录可能会记录敏感信息。这就是为什么默认情况下屏蔽请求参数和 Headers,并且必须通过DispatcherServlet上的enableLoggingRequestDetails属性显式启用它们的完整登录的原因。

以下示例显示了如何使用 Java 配置来执行此操作:

public class MyInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return ... ;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return ... ;
    }

    @Override
    protected String[] getServletMappings() {
        return ... ;
    }

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setInitParameter("enableLoggingRequestDetails", "true");
    }

}

1.2. Filters

与 Spring WebFlux 中的相同

spring-web模块提供了一些有用的过滤器:

1.2.1. 表格数据

浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器 Client 端也可以使用 HTTP PUT,PATCH 和 DELETE。 Servlet API 需要ServletRequest.getParameter*()个方法才能仅对 HTTP POST 支持表单字段访问。

spring-web模块提供FormContentFilter来拦截 Content Type 为application/x-www-form-urlencoded的 HTTP PUT,PATCH 和 DELETE 请求,从请求的正文中读取表单数据,并包装ServletRequest以通过ServletRequest.getParameter*()系列方法使表单数据可用。

1.2.2. 转发的标题

与 Spring WebFlux 中的相同

当请求通过代理(例如负载平衡器)进行处理时,主机,端口和方案可能会更改,这使得从 Client 端角度创建指向正确的主机,端口和方案的链接带来了挑战。

RFC 7239定义Forwarded HTTPHeaders,代理可用来提供有关原始请求的信息。还有其他非标准 Headers,包括X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

ForwardedHeaderFilter是一个 Servlet 过滤器,它根据ForwardedHeaders 修改请求的主机,端口和方案,然后删除这些 Headers。

对于转发的 Headers,存在安全方面的考虑,因为应用程序无法知道 Headers 是由代理添加的,还是由恶意 Client 端添加的。这就是为什么应配置信任边界处的代理以删除来自外部的不受信任的ForwardedHeaders 的原因。您还可以使用removeOnly=true配置ForwardedHeaderFilter,在这种情况下,它将删除但不使用 Headers。

1.2.3. 浅 ETag

ShallowEtagHeaderFilter过滤器通过缓存写入响应的内容并从中计算 MD5 哈希值来创建“浅” ETag。Client 端下一次发送时,将执行相同的操作,但还会将计算值与If-None-Match请求 Headers 进行比较,如果两者相等,则返回 304(NOT_MODIFIED)。

此策略可节省网络带宽,但不能节省 CPU,因为必须为每个请求计算完整响应。如前所述,控制器级别的其他策略可以避免计算。参见HTTP Caching

该过滤器具有一个writeWeakETag参数,该参数将过滤器配置为写入弱 ETag,类似于以下代码:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(在RFC 7232 第 2.3 节中定义)。

1.2.4. CORS

与 Spring WebFlux 中的相同

Spring MVC 通过控制器上的注解 为 CORS 配置提供了细粒度的支持。但是,当与 Spring Security 一起使用时,我们建议您依赖内置的CorsFilter,该CorsFilter必须在 Spring Security 的过滤器链之前 Order。

有关更多详细信息,请参见CORSCORS Filter部分。

1.3. 带注解 的控制器

与 Spring WebFlux 中的相同

Spring MVC 提供了基于注解 的编程模型,其中@Controller@RestController组件使用注解 来表达请求 Map,请求 Importing,异常处理等。带注解 的控制器具有灵活的方法签名,无需扩展 Base Class 或实现特定的接口。以下示例显示了由注解 定义的控制器:

@Controller
public class HelloController {

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

在前面的示例中,该方法接受Model并以String的形式返回视图名称,但是还存在许多其他选项,本章稍后将对其进行说明。

Tip

spring.io上的指南和教程使用本节中介绍的基于注解 的编程模型。

1.3.1. Declaration

与 Spring WebFlux 中的相同

您可以使用 Servlet 的WebApplicationContext中的标准 Spring bean 定义来定义控制器 bean。 @Controller原型允许自动检测,与 Spring 对在 Classpath 中检测@Component类并为其自动注册 Bean 定义的常规支持保持一致。它还充当带注解 类的构造型,表明其作为 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是一个composed annotation,它本身会用@Controller@ResponseBody进行元注解,以表示其每个方法都继承了类型级别@ResponseBodyComments 的控制器,因此直接将其写入响应主体(与视图分辨率和 HTML 模板渲染相比)。

AOP Proxies

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

1.3.2. 请求 Map

与 Spring WebFlux 中的相同

您可以使用@RequestMapping注解将请求 Map 到控制器方法。它具有各种属性,可以通过 URL,HTTP 方法,请求参数,Headers 和媒体类型进行匹配。您可以在类级别使用它来表示共享的 Map,也可以在方法级别使用它来缩小到特定的端点 Map。

也有@RequestMapping的 HTTP 方法特定的快捷方式:

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

之所以提供快捷方式是Custom Annotations,是因为可以说,大多数控制器方法应该 Map 到特定的 HTTP 方法,而不是使用@RequestMapping(默认情况下,该方法与所有 HTTP 方法匹配)。同时,在类级别仍需要@RequestMapping来表示共享 Map。

以下示例具有类型和方法级别的 Map:

@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
URI patterns

与 Spring WebFlux 中的相同

您可以使用以下 glob 模式和通配符来 Map 请求:

  • ?匹配一个字符

  • *匹配路径段中的零个或多个字符

  • **匹配零个或多个路径段

您还可以声明 URI 变量并使用@PathVariable访问其值,如以下示例所示:

@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等),您可以注册对任何其他数据类型的支持。参见Type ConversionDataBinder

您可以显式命名 URI 变量(例如@PathVariable("customId")),但是如果名称相同并且您的代码是使用调试信息或 Java 8 上的-parameters编译器标志编译的,则可以省略该详细信息。

语法{varName:regex}声明带有正则表达式的 URI 变量,语法为{varName:regex}。例如,给定 URL "/spring-web-3.0.5 .jar",以下方法将提取名称,版本和文件 extensions:

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

URI 路径模式还可以嵌入${…}占位符,这些占位符在启动时可以通过针对本地,系统,环境和其他属性源使用PropertyPlaceHolderConfigurer进行解析。例如,您可以使用它来基于一些外部配置参数化基本 URL。

Note

Spring MVC 使用PathMatcher协定和spring-coreAntPathMatcher实现实现 URI 路径匹配。

Pattern Comparison

与 Spring WebFlux 中的相同

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

如果模式的 URI 变量数较少,单个通配符计为 1,双通配符计为 2,则模式的特异性较低。给定分数,则选择较长的模式。给定相同的分数和长度,将选择 URI 变量比通配符更多的模式。

默认 Map 模式(/**)被排除在评分之外,并且始终排在最后。另外,前缀模式(例如/public/**)被认为比其他没有双通配符的模式更具体。

有关完整的详细信息,请参见AntPathMatcher中的AntPatternComparator,并请记住您可以自定义PathMatcher实现。请参阅配置部分中的Path Matching

Suffix Match

默认情况下,Spring MVC 执行.*后缀模式匹配,以便 Map 到/person的控制器也隐式 Map 到/person.*。然后,文件 extensions 用于解释请求的 Content Type 以用于响应(即,代替AcceptHeaders)—例如/person.pdf/person.xml等。

当浏览器过去曾经发送难以理解的AcceptHeaders 时,以这种方式使用文件 extensions 是必要的。目前,这已不再是必须的,使用AcceptHeaders 应该是首选。

随着时间的流逝,文件 extensions 的使用已经以各种方式证明是有问题的。当使用 URI 变量,路径参数和 URI 编码进行覆盖时,可能会引起歧义。关于基于 URL 的授权和安全性的推理(请参阅下一部分以了解更多详细信息)也变得更加困难。

要完全禁用文件 extensions,必须设置以下两项:

基于 URL 的内容协商仍然有用(例如,在浏览器中键入 URL 时)。为此,我们建议使用基于查询参数的策略,以避免文件 extensions 附带的大多数问题。另外,如果您必须使用文件 extensions,请考虑通过ContentNegotiationConfigurermediaTypes属性将它们限制为显式注册的 extensions 列表。

后缀匹配和 RFD

反射文件下载(RFD)攻击与 XSS 相似,它依赖于响应中反映的请求 Importing(例如,查询参数和 URI 变量)。但是,RFD 攻击不是将 JavaScript 插入 HTML,而是依靠浏览器切换来执行下载,并在以后双击时将响应视为可执行脚本。

在 Spring MVC 中,@ResponseBodyResponseEntity方法存在风险,因为它们可以呈现不同的 Content Type,Client 端可以通过 URL 路径扩展请求这些 Content Type。禁用后缀模式匹配并使用路径扩展进行内容协商可以降低风险,但不足以防止 RFD 攻击。

为了防止 RFD 攻击,Spring MVC 在呈现响应主体之前添加了Content-Disposition:inline;filename=f.txtHeaders,以建议一个固定且安全的下载文件。仅当 URL 路径包含既未列入白名单也未明确注册用于内容协商的文件 extensions 时,才执行此操作。但是,当直接在浏览器中键入 URL 时,它可能会产生副作用。

默认情况下,许多常见的路径 extensions 都被列入白名单。具有自定义HttpMessageConverter实现的应用程序可以显式注册文件 extensions 以进行内容协商,以避免为这些 extensions 添加Content-DispositionHeaders。参见Content Types

有关 RFD 的其他建议,请参见CVE-2015-5211

消耗媒体类型

与 Spring WebFlux 中的相同

您可以根据请求的Content-Type缩小请求 Map,如下例所示:

@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
    // ...
}
  • (1) 使用consumes属性按 Content Type 缩小 Map 范围。

consumes属性还支持否定表达式-例如,!text/plain表示text/plain以外的任何 Content Type。

您可以在类级别声明共享的consumes属性。但是,与大多数其他请求 Map 属性不同,在类级使用时,方法级consumes属性会覆盖而不是扩展类级声明。

Tip

MediaType为常用的媒体类型(例如APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE)提供常数。

可生产的媒体类型

与 Spring WebFlux 中的相同

您可以根据Accept请求 Headers 和控制器方法生成的 Content Type 列表来缩小请求 Map,如下例所示:

@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
  • (1) 使用produces属性按 Content Type 缩小 Map 范围。

媒体类型可以指定字符集。支持否定的表达式。例如,!text/plain表示除“文本/纯文本”之外的任何 Content Type。

Note

对于 JSONContent Type,即使RFC7159明确声明“未为此注册定义任何字符集参数”,也应指定 UTF-8 字符集,因为某些浏览器要求它正确解释 UTF-8 特殊字符。

您可以在类级别声明共享的produces属性。但是,与大多数其他请求 Map 属性不同,在类级使用时,方法级produces属性会覆盖而不是扩展类级声明。

Tip

MediaType为常用的媒体类型(例如APPLICATION_JSON_UTF8_VALUEAPPLICATION_XML_VALUE)提供常数。

Parameters, headers

与 Spring WebFlux 中的相同

您可以根据请求参数条件来缩小请求 Map。您可以测试是否存在请求参数(myParam),是否存在请求参数(!myParam)或特定值(myParam=myValue)。以下示例显示如何测试特定值:

@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
  • (1) 测试myParam是否等于myValue

您还可以将其与请求 Headers 条件一起使用,如以下示例所示:

@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
  • (1) 测试myHeader是否等于myValue

Tip

您可以将Content-TypeAccept与 Headers 条件进行匹配,但最好使用consumesproduces

HTTP HEAD,选项

与 Spring WebFlux 中的相同

@GetMapping(和@RequestMapping(method=HttpMethod.GET))透明地支持 HTTP HEAD 以进行请求 Map。控制器方法不需要更改。 javax.servlet.http.HttpServlet中应用的响应包装器确保将Content-LengthHeaders 设置为写入的字节数(实际上未写入响应)。

@GetMapping(和@RequestMapping(method=HttpMethod.GET))被隐式 Map 到并支持 HTTP HEAD。像处理 HTTP GET 一样处理 HTTP HEAD 请求,不同的是,不是写入正文,而是计算字节数并设置Content-Length头。

默认情况下,通过将Allow响应 Headers 设置为所有具有匹配 URL 模式的@RequestMapping方法中列出的 HTTP 方法列表来处理 HTTP OPTIONS。

对于没有 HTTP 方法声明的@RequestMappingAllowHeaders 设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明受支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体@GetMapping@PostMapping等)。

您可以将@RequestMapping方法显式 Map 到 HTTP HEAD 和 HTTP OPTIONS,但这在通常情况下不是必需的。

Custom Annotations

与 Spring WebFlux 中的相同

Spring MVC 支持使用composed annotations进行请求 Map。这些注解本身用@RequestMapping进行元注解,并组成它们以更狭窄,更具体的用途重新声明@RequestMapping属性的子集(或全部)。

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping是组合注解 的示例。之所以提供它们,是因为大多数控制器方法应该 Map 到特定的 HTTP 方法,而不是使用@RequestMapping,默认情况下,@RequestMapping匹配所有 HTTP 方法。如果需要组合注解 的示例,请查看如何声明它们。

Spring MVC 还支持带有自定义请求匹配逻辑的自定义请求 Map 属性。这是一个更高级的选项,它需要子类化RequestMappingHandlerMapping并覆盖getCustomMethodCondition方法,您可以在其中检查自定义属性并返回自己的RequestCondition

Explicit Registrations

与 Spring WebFlux 中的相同

您可以以编程方式注册处理程序方法,这些方法可用于动态注册或高级用例,例如同一处理程序在不同 URL 下的不同实例。下面的示例注册一个处理程序方法:

@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }

}
  • (1) 注入目标处理程序和控制器的处理程序 Map。
  • (2) 准备请求 Map 元数据。
  • (3) 获取处理程序方法。
  • (4) 添加注册。

1.3.3. 处理程序方法

与 Spring WebFlux 中的相同

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

Method Arguments

与 Spring WebFlux 中的相同

下表描述了受支持的控制器方法参数。任何参数均不支持 Reactive 类型。

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

控制器方法参数 Description
WebRequest , NativeWebRequest 对请求参数以及请求和会话属性的常规访问,而无需直接使用 Servlet API。
javax.servlet.ServletRequest , javax.servlet.ServletResponse 选择任何特定的请求或响应类型,例如ServletRequestHttpServletRequest或 Spring 的MultipartRequestMultipartHttpServletRequest
javax.servlet.http.HttpSession 强制会话的存在。因此,这样的参数永远不会是null。请注意,会话访问不是线程安全的。如果允许多个请求并发访问会话,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为true
javax.servlet.http.PushBuilder 用于程序化 HTTP/2 资源推送的 Servlet 4.0 推送构建器 API。请注意,根据 Servlet 规范,如果 Client 端不支持 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 模板变量。参见URI patterns
@MatrixVariable 用于访问 URI 路径段中的名称/值对。参见Matrix Variables
@RequestParam 用于访问 Servlet 请求参数,包括 Multipart 文件。参数值将转换为声明的方法参数类型。参见@RequestParamMultipart


请注意,对于简单的参数值,使用@RequestParam是可选的。请参阅此表末尾的“其他任何参数”。
| @RequestHeader |用于访问请求 Headers。Headers 值将转换为声明的方法参数类型。参见@RequestHeader
| @CookieValue |用于访问 cookie。 Cookies 值将转换为声明的方法参数类型。参见@CookieValue
| @RequestBody |用于访问 HTTP 请求正文。通过使用HttpMessageConverter实现,主体内容将转换为声明的方法参数类型。参见@RequestBody
| HttpEntity<B> |用于访问请求 Headers 和正文。主体用HttpMessageConverter转换。参见HttpEntity
| @RequestPart |要访问multipart/form-data请求中的 Component,请用HttpMessageConverter转换 Component 的主体。参见Multipart
| java.util.Maporg.springframework.ui.Modelorg.springframework.ui.ModelMap |用于访问 HTML 控制器中使用的模型,并作为视图渲染的一部分公开给模板。
| RedirectAttributes |指定在重定向(即附加到查询字符串)的情况下使用的属性,以及将 Flash 属性临时存储直到重定向后的请求。参见Redirect AttributesFlash Attributes
| @ModelAttribute |用于访问应用了数据绑定和验证的模型中的现有属性(如果不存在,则进行实例化)。参见@ModelAttribute以及ModelDataBinder
请注意,使用@ModelAttribute是可选的(例如,设置其属性)。请参阅此表末尾的“其他任何参数”。
| ErrorsBindingResult |用于访问命令对象的验证和数据绑定(即@ModelAttribute参数)中的错误或@RequestBody@RequestPart参数的验证中的错误。您必须在经过验证的方法参数后立即声明ErrorsBindingResult参数。
| SessionStatus类级@SessionAttributes |用于标记表单处理完成,将触发清除通过类级@SessionAttributesComments 声明的会话属性。有关更多详细信息,请参见@SessionAttributes
| UriComponentsBuilder |用于准备相对于当前请求的主机,端口,方案,上下文路径以及 servletMap 的 Literals 部分的 URL。参见URI Links
| @SessionAttribute |用于访问任何会话属性,与由于类级别@SessionAttributes声明而存储在会话中的模型属性相反。有关更多详细信息,请参见@SessionAttribute
| @RequestAttribute |用于访问请求属性。有关更多详细信息,请参见@RequestAttribute
|任何其他参数|如果方法参数与该表中的任何较早值都不匹配,并且是简单类型(由BeanUtils#isSimpleProperty确定,则将其解析为@RequestParam。否则,将其解析为@ModelAttribute。 |

Return Values

与 Spring WebFlux 中的相同

下表描述了受支持的控制器方法返回值。所有返回值都支持 Reactive 类型。

控制器方法返回值 Description
@ResponseBody 返回值通过HttpMessageConverter实现进行转换,并写入响应中。参见@ResponseBody
HttpEntity<B> , ResponseEntity<B> 指定完整响应(包括 HTTPHeaders 和正文)的返回值将通过HttpMessageConverter实现进行转换并写入响应中。参见ResponseEntity
HttpHeaders 用于返回不包含标题的响应。
String ViewResolver实现解析的视图名称,并与通过命令对象和@ModelAttribute方法确定的隐式模型一起使用。处理程序方法还可以pass 语句Model参数(请参见Explicit Registrations)以编程方式丰富模型。
View 用于与隐式模型一起渲染的View实例-通过命令对象和@ModelAttribute方法确定。处理程序方法还可以pass 语句Model参数(请参见Explicit Registrations)以编程方式丰富模型。
java.util.Map , org.springframework.ui.Model 要添加到隐式模型的属性,视图名称通过RequestToViewNameTranslator隐式确定。
@ModelAttribute 要添加到模型的属性,视图名称通过RequestToViewNameTranslator隐式确定。


请注意,@ModelAttribute是可选的。请参见此表末尾的“其他任何返回值”。
| ModelAndView object |要使用的视图和模型属性,以及响应状态(可选)。
| void |具有void返回类型(或null返回值)的方法,如果它也具有ServletResponseOutputStream参数或@ResponseStatusComments,则认为已完全处理了响应。如果控制器对ETaglastModified时间戳进行了肯定检查,则也是如此(有关详细信息,请参见Controllers)。
如果以上所有条件都不成立,则void返回类型还可以为 REST 控制器指示“无响应正文”,或者为 HTML 控制器指示默认的视图名称选择。
| DeferredResult<V> |从任何线程异步生成任何上述返回值,例如由于某些事件或回调的结果。参见Asynchronous RequestsDeferredResult
| Callable<V> |在 Spring MVCManagement 的线程中异步产生上述任何返回值。参见Asynchronous RequestsCallable
| ListenableFuture<V>java.util.concurrent.CompletionStage<V>java.util.concurrent.CompletableFuture<V> |替代DeferredResult,以方便使用(例如,当基础服务返回其中之一时)。
| ResponseBodyEmitterSseEmitter |使用HttpMessageConverter实现异步发出对象流以将其写入响应。也支持作为ResponseEntity的主体。参见Asynchronous RequestsHTTP Streaming
| StreamingResponseBody |异步写入响应OutputStream。也支持作为ResponseEntity的主体。参见Asynchronous RequestsHTTP Streaming
|Reactive 类型-反应器,RxJava 或其他通过ReactiveAdapterRegistry |替代DeferredResult的多值流(例如FluxObservable)收集到List
对于流方案(例如text/event-streamapplication/json+stream),将使用SseEmitterResponseBodyEmitter代替,其中ServletOutputStream阻塞 I/O 在 Spring MVCManagement 的线程上执行,并且在每次写入完成时施加反压。
参见Asynchronous RequestsReactive Types
|任何其他返回值|与该表中的任何较早值不匹配且为Stringvoid的任何返回值均被视为视图名称(适用于通过RequestToViewNameTranslator进行默认视图名称选择),前提是该操作不简单类型,由BeanUtils#isSimpleProperty确定。简单类型的值仍然无法解析。

Type Conversion

与 Spring WebFlux 中的相同

如果声明的参数不是String,则表示基于String的请求 Importing 的某些带注解 的控制器方法参数(例如@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue)可能需要类型转换。

在这种情况下,将根据配置的转换器自动应用类型转换。默认情况下,支持简单类型(intlongDate等)。您可以通过WebDataBinder(请参见DataBinder)或通过向FormattingConversionService注册Formatters来定制类型转换。参见Spring 字段格式

Matrix Variables

与 Spring WebFlux 中的相同

RFC 3986讨论路径段中的名称/值对。在 Spring MVC 中,基于 Tim Berners-Lee 的"old post",我们将其称为“矩阵变量”,但它们也可以称为 URI 路径参数。

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

如果期望 URL 包含矩阵变量,则控制器方法的请求 Map 必须使用 URI 变量来屏蔽该变量内容,并确保可以成功地匹配请求,而与矩阵变量的 Sequences 和状态无关。以下示例使用矩阵变量:

// 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=falsePath Matching来设置UrlPathHelper。在 MVC XML 名称空间中,可以设置<mvc:annotation-driven enable-matrix-variables="true"/>

@RequestParam

与 Spring WebFlux 中的相同

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

以下示例显示了如何执行此操作:

@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...

}
  • (1) 使用@RequestParam绑定petId

默认情况下,使用此注解的方法参数是必需的,但是您可以通过将@RequestParam注解的required标志设置为false或通过java.util.Optional包装器声明参数来指定方法参数是可选的。

如果目标方法参数类型不是String,则会自动应用类型转换。参见Type Conversion

将参数类型声明为数组或列表,可以为同一参数名称解析多个参数值。

如果将@RequestParam注解声明为Map<String, String>MultiValueMap<String, String>,而未在注解中指定参数名,则将使用每个给定参数名的请求参数值填充 Map。

请注意,使用@RequestParam是可选的(例如,设置其属性)。默认情况下,任何简单值类型(由BeanUtils#isSimpleProperty确定)且未由其他任何参数解析器解析的参数都将被视为带有@RequestParamComments。

@RequestHeader

与 Spring WebFlux 中的相同

您可以使用@RequestHeader注解将请求 Headers 绑定到控制器中的方法参数。

考虑以下带有 Headers 的请求:

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-AliveHeaders 的值:

@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
  • (1) 获取Accept-EncodingHeaders 的值。
  • (2) 获取Keep-AliveHeaders 的值。

如果目标方法的参数类型不是String,则将自动应用类型转换。参见Type Conversion

Map<String, String>MultiValueMap<String, String>HttpHeaders参数上使用@RequestHeaderComments 时,将使用所有 Headers 值填充 Map。

Tip

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

@CookieValue

与 Spring WebFlux 中的相同

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

考虑带有以下 cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下示例显示如何获取 cookie 值:

@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
  • (1) 获取JSESSIONID cookie 的值。

如果目标方法的参数类型不是String,那么将自动应用类型转换。参见Type Conversion

@ModelAttribute

与 Spring WebFlux 中的相同

您可以在方法参数上使用@ModelAttributeComments 来访问模型中的属性,或者将其实例化(如果不存在)。 model 属性还覆盖了名称与字段名称匹配的 HTTP Servlet 请求参数中的值。这称为数据绑定,它使您不必处理解析和转换单个查询参数和表单字段的工作。以下示例显示了如何执行此操作:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
  • (1) 绑定Pet的实例。

上面的Pet实例解析如下:

  • 从模型(如果已使用Model添加)。

  • 通过使用@SessionAttributes在 HTTP 会话中进行。

  • 来自通过Converter传递的 URI 路径变量(请参见下一个示例)。

  • 从默认构造函数的调用开始。

  • 从带有与 Servlet 请求参数匹配的参数的“主要构造函数”的调用开始。参数名称是通过 JavaBeans @ConstructorProperties或字节码中运行时保留的参数名称确定的。

虽然通常使用Model来为模型填充属性,但另一种替代方法是将Converter<String, T>与 URI 路径变量约定结合使用。在以下示例中,模型属性名称account与 URI 路径变量account匹配,并且通过将String帐号传递给已注册的Converter<String, Account>来加载Account

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

获取模型属性实例后,将应用数据绑定。 WebDataBinder类将 Servlet 请求参数名称(查询参数和表单字段)与目标Object上的字段名称匹配。必要时在应用类型转换后填充匹配字段。有关数据绑定(和验证)的更多信息,请参见Validation。有关自定义数据绑定的更多信息,请参见DataBinder

数据绑定可能会导致错误。默认情况下,引发BindException。但是,要检查 controller 方法中的此类错误,可以在@ModelAttribute旁边紧接着添加BindingResult参数,如以下示例所示:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
  • (1)@ModelAttribute旁边添加一个BindingResult

在某些情况下,您可能希望访问没有数据绑定的模型属性。对于这种情况,您可以将Model注入控制器并直接访问它,或者设置@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) { (1)
    // ...
}
  • (1) 设置@ModelAttribute(binding=false)

通过添加javax.validation.ValidComments 或 Spring 的@ValidatedComments(ee Bean validationSpring validation),可以在数据绑定之后自动应用验证。以下示例显示了如何执行此操作:

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

请注意,使用@ModelAttribute是可选的(例如,设置其属性)。默认情况下,任何不是简单值类型(由BeanUtils#isSimpleProperty确定)且未由任何其他参数解析器解析的参数都将被视为以@ModelAttributeComments。

@SessionAttributes

与 Spring WebFlux 中的相同

@SessionAttributes用于在请求之间的 HTTP Servlet 会话中存储模型属性。它是类型级别的注解,用于声明特定控制器使用的会话属性。这通常列出应透明地存储在会话中以供后续访问请求的模型属性名称或模型属性类型。

下面的示例使用@SessionAttributes注解:

@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
  • (1) 使用@SessionAttributesComments。

在第一个请求上,将名称为pet的模型属性添加到模型时,该属性会自动提升为 HTTP Servlet 会话并保存在该会话中。它会一直保留在那里,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如以下示例所示:

@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
            status.setComplete(); (2)
            // ...
        }
    }
}
  • (1)Pet值存储在 Servlet 会话中。
  • (2) 从 Servlet 会话中清除Pet值。
@SessionAttribute

与 Spring WebFlux 中的相同

如果您需要访问全局存在(例如,在控制器外部(例如,通过过滤器)Management)并且可能存在或可能不存在的预先存在的会话属性,则可以在方法参数上使用@SessionAttributeComments,作为以下示例显示:

@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
  • (1) 使用@SessionAttributeComments。

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

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

@RequestAttribute

与 Spring WebFlux 中的相同

@SessionAttribute相似,您可以使用@RequestAttribute注解来访问先前创建的预先存在的请求属性(例如,由 Servlet FilterHandlerInterceptor创建):

@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
  • (1) 使用@RequestAttributeComments。
Redirect Attributes

默认情况下,所有模型属性均被视为在重定向 URL 中作为 URI 模板变量公开。在其余属性中,那些属于原始类型或原始类型的集合或数组的属性会自动附加为查询参数。

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

RequestMappingHandlerAdapter提供了一个名为ignoreDefaultModelOnRedirect的标志,您可以使用该标志来表示如果控制器方法重定向,则绝不要使用默认Model的内容。相反,控制器方法应声明类型为RedirectAttributes的属性,或者,如果没有声明,则不应将任何属性传递给RedirectView。 MVC 名称空间和 MVC Java 配置都将此标志设置为false,以保持向后兼容性。但是,对于新应用程序,建议将其设置为true

请注意,展开重定向 URL 时,本请求中的 URI 模板变量将自动变为可用,并且您需要通过ModelRedirectAttributes显式添加它们。以下示例显示了如何定义重定向:

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

将数据传递到重定向目标的另一种方法是使用闪存属性。与其他重定向属性不同,Flash 属性保存在 HTTP 会话中(因此不会出现在 URL 中)。有关更多信息,请参见Flash Attributes

Flash Attributes

Flash 属性为一个请求提供了一种存储要在另一个请求中使用的属性的方式。重定向时最常需要此操作,例如 Post-Redirect-Get 模式。 Flash 属性在重定向之前(通常在会话中)被临时保存,以便在重定向之后可供请求使用,并立即被删除。

Spring MVC 有两个主要抽象来支持 Flash 属性。 FlashMap用于保存 Flash 属性,而FlashMapManager用于存储,检索和 ManagementFlashMap实例。

Flash 属性支持始终处于“启用”状态,无需显式启用。但是,如果不使用它,则永远不会导致 HTTP 会话创建。在每个请求上,都有一个具有从上一个请求(如果有)传递的属性的“Importing” FlashMap,和一个具有为后续请求保存的属性的“输出” FlashMap。通过RequestContextUtils中的静态方法,可以从 Spring MVC 中的任何位置访问这两个FlashMap实例。

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

将请求与 Flash 属性匹配

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

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

这不能完全消除并发问题的可能性,但是可以通过重定向 URL 中已经可用的信息大大减少并发问题。因此,我们建议您主要将 Flash 属性用于重定向方案。

Multipart

与 Spring WebFlux 中的相同

MultipartResolver成为enabled之后,将解析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";
    }
}

将参数类型声明为List<MultipartFile>可以为同一参数名称解析多个文件。

如果将@RequestParam注解声明为Map<String, MultipartFile>MultiValueMap<String, MultipartFile>,而未在注解中指定参数名称,则将使用每个给定参数名的 Multipart 文件填充 Map。

Note

使用 Servlet 3.0Multipart 解析时,您还可以将javax.servlet.http.Part而不是 Spring 的MultipartFile声明为方法参数或集合值类型。

您还可以将 Multipart 内容用作绑定到command object的数据的一部分。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如以下示例所示:

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 服务方案中,也可以从非浏览器 Client 端提交 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作为String来访问“元数据”部分,但您可能希望将其从 JSON 反序列化(类似于@RequestBody)。用HttpMessageConverter转换后,使用@RequestPart注解来访问 Multipart:

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

您可以将@RequestPartjavax.validation.Valid结合使用,也可以使用 Spring 的@ValidatedComments,这两种注解 都会导致应用标准 Bean 验证。默认情况下,验证错误会导致MethodArgumentNotValidException,它变成 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内部本地处理验证错误,如以下示例所示:

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

与 Spring WebFlux 中的相同

您可以使用@RequestBody注解将请求正文读取并通过HttpMessageConverter反序列化为Object。以下示例使用@RequestBody参数:

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

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

您可以将@RequestBodyjavax.validation.Valid或 Spring 的@ValidatedComments 结合使用,这两种注解 都会导致应用标准 Bean 验证。默认情况下,验证错误会导致MethodArgumentNotValidException,它变成 400(BAD_REQUEST)响应。或者,您可以通过ErrorsBindingResult参数在控制器内部本地处理验证错误,如以下示例所示:

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

与 Spring WebFlux 中的相同

HttpEntity与使用@RequestBody大致相同,但是它基于一个容器对象,该对象公开了请求 Headers 和正文。以下 Lists 显示了一个示例:

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

与 Spring WebFlux 中的相同

您可以在方法上使用@ResponseBody注解,以使返回值通过HttpMessageConverter序列化到响应主体。以下 Lists 显示了一个示例:

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

@ResponseBody在类级别上也受支持,在这种情况下,它被所有控制器方法继承。这就是@RestController的效果,它不过是用@Controller@ResponseBody标记的元注解。

您可以将@ResponseBody与反应类型一起使用。有关更多详细信息,请参见Asynchronous RequestsReactive Types

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

您可以将@ResponseBody方法与 JSON 序列化视图结合使用。有关详情,请参见Jackson JSON

ResponseEntity

与 Spring WebFlux 中的相同

ResponseEntity类似于@ResponseBody,但具有状态和标题。例如:

@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}

Spring MVC 支持使用单个值reactive type异步生成ResponseEntity和/或主体的单值和多值反应类型。

Jackson JSON

Spring 提供了对 Jackson JSON 库的支持。

Jackson 序列化视图

与 Spring WebFlux 中的相同

Spring MVC 为Jackson 的序列化视图提供了内置支持,该支持仅渲染Object中所有字段的一部分。要将它与@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;
    }
}

Note

@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";
    }
}

1.3.4. Model

与 Spring WebFlux 中的相同

您可以使用@ModelAttribute注解:

  • method argument in @RequestMapping方法中,从模型创建或访问Object并将其通过WebDataBinder绑定到请求。

  • 作为@Controller@ControllerAdvice类中的方法级注解,可在任何@RequestMapping方法调用之前帮助初始化模型。

  • @RequestMapping方法上标记其返回值的是模型属性。

本节讨论@ModelAttribute方法-前面列表中的第二项。控制器可以具有任意数量的@ModelAttribute方法。所有此类方法均在同一控制器中的@RequestMapping方法之前调用。 @ModelAttribute方法也可以通过@ControllerAdvice在控制器之间共享。有关更多详细信息,请参见Controller Advice部分。

@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);
}

Note

如果未明确指定名称,则根据Object类型选择默认名称,如Conventions的 javadoc 中所述。您始终可以通过使用重载的addAttribute方法或通过@ModelAttribute上的name属性(用于返回值)来分配显式名称。

您也可以将@ModelAttribute用作@RequestMapping方法的方法级注解,在这种情况下@RequestMapping方法的返回值将解释为模型属性。通常不需要这样做,因为这是 HTML 控制器中的默认行为,除非返回值是String,否则它将被解释为视图名称。 @ModelAttribute还可以自定义模型属性名称,如以下示例所示:

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

1.3.5. DataBinder

与 Spring WebFlux 中的相同

@Controller@ControllerAdvice类可以具有@InitBinder个方法来初始化WebDataBinder的实例,而这些实例又可以:

  • 将请求参数(即表单或查询数据)绑定到模型对象。

  • 将基于字符串的请求值(例如请求参数,路径变量,Headers,Cookie 等)转换为控制器方法参数的目标类型。

  • 呈现 HTML 表单时,将模型对象的值格式化为String值。

@InitBinder个方法可以注册特定于控制器的java.bean.PropertyEditor或 Spring ConverterFormatter组件。此外,您可以使用MVC config在全局共享的FormattingConversionService中注册ConverterFormatter类型。

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

@Controller
public class FormController {

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

    // ...
}
  • (1) 定义@InitBinder方法。

另外,当您通过共享FormattingConversionService使用基于Formatter的设置时,可以重新使用相同的方法并注册特定于控制器的Formatter实现,如以下示例所示:

@Controller
public class FormController {

    @InitBinder (1)
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
    }

    // ...
}
  • (1) 在自定义格式化程序上定义@InitBinder方法。

1.3.6. Exceptions

与 Spring WebFlux 中的相同

@Controller@ControllerAdvice类可以具有@ExceptionHandler个方法来处理控制器方法中的异常,如以下示例所示:

@Controller
public class SimpleController {

    // ...

    @ExceptionHandler
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

该异常可能与正在传播的顶级异常(即,直接抛出IOException)或顶级包装异常中的直接原因(例如,在IllegalStateException内包装的IOException)相匹配。

对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根源异常匹配通常比原因异常匹配更可取。更具体地说,ExceptionDepthComparator用于根据异常对引发的异常类型的深度进行排序。

另外,Comments 声明可以缩小异常类型以使其匹配,如以下示例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
    // ...
}

您甚至可以使用带有非常通用的参数签名的特定异常类型的列表,如以下示例所示:

@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
    // ...
}

Note

根和原因异常匹配之间的区别可能令人惊讶。

在前面显示的IOException变体中,通常以实际的FileSystemExceptionRemoteException实例作为参数来调用该方法,因为这两个实例都从IOException扩展。但是,如果任何此类匹配异常都在本身是IOException的包装器异常中传播,则传入的异常实例就是该包装器异常。

handle(Exception)变体中的行为甚至更简单。在包装方案中,总是使用包装器异常来调用此方法,在这种情况下,实际匹配的异常将通过ex.getCause()找到。仅当将它们作为顶级异常抛出时,传入的异常才是实际的FileSystemExceptionRemoteException实例。

通常,我们建议您在参数签名中尽可能具体,以减少根类型和原因异常类型之间不匹配的可能性。考虑将多重匹配方法分解为单独的@ExceptionHandler方法,每个方法都通过其签名匹配单个特定的异常类型。

在多@ControllerAdvice的安排中,我们建议在以相应 Sequences 优先的@ControllerAdvice上声明您的主根异常 Map。尽管根源异常匹配是原因的首选,但这是在给定控制器或@ControllerAdvice类的方法之间定义的。这意味着优先级较高的@ControllerAdvice bean 上的原因匹配优于优先级较低的@ControllerAdvice bean 上的任何匹配(例如,根)。

最后但并非最不重要的一点是,@ExceptionHandler方法实现可以选择以放弃原始形式重新处理给定异常实例的方法。在仅对根级别匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下,这很有用。重新抛出的异常会在其余的解决方案链中传播,就像给定的@ExceptionHandler方法最初不会匹配一样。

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

Method Arguments

@ExceptionHandler方法支持以下参数:

Method argument Description
Exception type 用于访问引发的异常。
HandlerMethod 用于访问引发异常的控制器方法。
WebRequest , NativeWebRequest 对请求参数以及请求和会话属性的常规访问,而无需直接使用 Servlet API。
javax.servlet.ServletRequest , javax.servlet.ServletResponse 选择任何特定的请求或响应类型(例如ServletRequestHttpServletRequest或 Spring 的MultipartRequestMultipartHttpServletRequest)。
javax.servlet.http.HttpSession 强制会话的存在。因此,这样的参数永远不会是null


请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为true
| java.security.Principal |当前经过身份验证的用户-可能是特定的Principal实现类(如果已知)。
| HttpMethod |请求的 HTTP 方法。
| java.util.Locale |当前请求的语言环境,由最具体的LocaleResolver可用决定,实际上是配置的LocaleResolverLocaleContextResolver
| java.util.TimeZonejava.time.ZoneId |与当前请求关联的时区,由LocaleContextResolver确定。
| java.io.OutputStreamjava.io.Writer |用于访问原始响应主体,如 Servlet API 所公开。
| java.util.Maporg.springframework.ui.Modelorg.springframework.ui.ModelMap |用于访问模型以进行错误响应。总是空的。
| RedirectAttributes |指定在重定向的情况下使用的属性(要附加到查询字符串中)和 flash 属性,这些属性将临时存储直到重定向后的请求。参见Redirect AttributesFlash Attributes
| @SessionAttribute |用于访问任何会话属性,与由于类级别@SessionAttributes声明而存储在会话中的模型属性相反。有关更多详细信息,请参见@SessionAttribute
| @RequestAttribute |用于访问请求属性。有关更多详细信息,请参见@RequestAttribute

Return Values

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

Return value Description
@ResponseBody 返回值通过HttpMessageConverter个实例进行转换并写入响应中。参见@ResponseBody
HttpEntity<B> , ResponseEntity<B> 返回值指定完整的响应(包括 HTTPHeaders 和正文)通过HttpMessageConverter实例进行转换并写入响应中。参见ResponseEntity
String 要通过ViewResolver实现解析的视图名称,并与隐式模型一起使用-通过命令对象和@ModelAttribute方法确定。处理程序方法还可以pass 语句Model参数(如前所述)以编程方式丰富模型。
View 通过命令对象和@ModelAttribute方法确定的用于与隐式模型一起渲染的View实例。处理程序方法还可以pass 语句Model参数(先前描述)来以编程方式丰富模型。
java.util.Map , org.springframework.ui.Model 通过RequestToViewNameTranslator隐式确定的视图名称将添加到隐式模型的属性。
@ModelAttribute 通过RequestToViewNameTranslator隐式确定的视图名称将添加到模型的属性。


请注意,@ModelAttribute是可选的。请参见此表末尾的“其他任何返回值”。
| ModelAndView object |要使用的视图和模型属性,以及响应状态(可选)。
| void |具有void返回类型(或null返回值)的方法,如果它也具有ServletResponseOutputStream自变量或@ResponseStatusComments,则认为已完全处理了响应。如果控制器对ETaglastModified时间戳进行了肯定检查,则也是如此(有关详细信息,请参见Controllers)。
如果以上所有条件都不成立,则void返回类型还可以为 REST 控制器指示“无响应正文”,或者为 HTML 控制器指示默认视图名称选择。
|其他任何返回值|如果返回值与上述任何一个都不匹配并且不是简单类型(由BeanUtils#isSimpleProperty确定),则默认情况下会将其视为要添加到模型的模型属性。如果是简单类型,则仍然无法解决。

REST API 异常

与 Spring WebFlux 中的相同

REST 服务的常见要求是在响应正文中包含错误详细信息。 Spring 框架不会自动执行此操作,因为响应主体中错误详细信息的表示是特定于应用程序的。但是,@RestController可以使用具有ResponseEntity返回值的@ExceptionHandler方法来设置响应的状态和主体。也可以在@ControllerAdvice类中声明此类方法以将其全局应用。

在响应正文中实现具有错误详细信息的全局异常处理的应用程序应考虑扩展ResponseEntityExceptionHandler,该扩展为 Spring MVC 引发的异常提供处理,并提供用于自定义响应正文的钩子。要使用此功能,请创建ResponseEntityExceptionHandler的子类,并用@ControllerAdvice对其进行注解,覆盖必要的方法,然后将其声明为 Spring bean。

1.3.7. 控制器建议

与 Spring WebFlux 中的相同

通常,@ExceptionHandler@InitBinder@ModelAttribute方法适用于声明它们的@Controller类(或类层次结构)。如果希望此类方法更全局地应用(跨控制器),则可以在标有@ControllerAdvice@RestControllerAdvice的类中声明它们。

@ControllerAdvice标有@Component,这意味着可以通过component scanning将此类注册为 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 {}

前面示例中的 selectors 在运行时进行评估,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参见@ControllerAdvice javadoc。

1.4. URI 链接

与 Spring WebFlux 中的相同

本节描述了 Spring 框架中可用于 URI 的各种选项。

1.4.1. UriComponents

Spring MVC 和 Spring WebFlux

UriComponentsBuilder帮助从具有变量的 URI 模板构建 URI,如以下示例所示:

UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
  • (1) 带有 URI 模板的静态工厂方法。
  • (2) 添加或替换 URI 组件。
  • (3) 请求对 URI 模板和 URI 变量进行编码。
  • (4) 构建UriComponents
  • (5) 展开变量并获得URI

可以将前面的示例合并为一个链,并使用buildAndExpand进行缩短,如以下示例所示:

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

您可以通过直接转到 URI(这意味着编码)来进一步缩短它,如以下示例所示:

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

您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:

URI uri = UriComponentsBuilder
        .fromUriString("http://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");

1.4.2. UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder实现UriBuilder。您可以依次创建UriBuilderUriBuilderFactoryUriBuilderFactoryUriBuilder一起提供了一种可插入的机制,用于基于共享配置(例如基本 URL,编码首选项和其他详细信息)从 URI 模板构建 URI。

您可以使用UriBuilderFactory配置RestTemplateWebClient以自定义 URI 的准备。 DefaultUriBuilderFactoryUriBuilderFactory的默认实现,该实现在内部使用UriComponentsBuilder并公开共享的配置选项。

以下示例显示了如何配置RestTemplate

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

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

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

以下示例配置WebClient

// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

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

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

此外,您也可以直接使用DefaultUriBuilderFactory。它类似于使用UriComponentsBuilder,但不是静态工厂方法,而是一个包含配置和首选项的实际实例,如以下示例所示:

String baseUrl = "http://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");

1.4.3. URI 编码

Spring MVC 和 Spring WebFlux

UriComponentsBuilder公开了两个级别的编码选项:

这两个选项都使用转义的八位字节替换非 ASCII 和非法字符。但是,第一个选项还会替换出现在 URI 变量中的具有保留含义的字符。

Tip

考虑“;”,它在路径上是合法的,但具有保留的含义。第一个选项代替“;” URI 变量中带有“%3B”,但 URI 模板中没有。相比之下,第二个选项永远不会替换“;”,因为它是路径中的合法字符。

在大多数情况下,第一个选项可能会产生预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而选项 2 仅在 URI 变量有意包含保留字符的情况下才有用。

以下示例使用第一个选项:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
            .queryParam("q", "{q}")
            .encode()
            .buildAndExpand("New York", "foo+bar")
            .toUri();

    // Result is "/hotel%20list/New%20York?q=foo%2Bbar"

您可以通过直接转到 URI(这意味着编码)来缩短前面的示例,如以下示例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
            .queryParam("q", "{q}")
            .build("New York", "foo+bar")

您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:

URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
            .build("New York", "foo+bar")

WebClientRestTemplate通过UriBuilderFactory策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置。如下例所示:

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

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();

DefaultUriBuilderFactory实现内部使用UriComponentsBuilder来扩展和编码 URI 模板。作为工厂,它提供了一个位置,可以根据以下一种编码模式来配置编码方法:

  • TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode()(对应于较早列表中的第一个选项)来预编码 URI 模板,并在扩展时严格编码 URI 变量。

  • VALUES_ONLY:不对 URI 模板进行编码,而是在将它们扩展到模板之前通过UriUtils#encodeUriUriVariables对 URI 变量进行严格编码。

  • URI_COMPONENTS:在扩展 URI 变量之后*,使用与较早列表中第二个选项相对应的UriComponents#encode()来编码 URI 组件值。

  • NONE:未应用编码。

出于历史原因和向后兼容性,RestTemplate设置为EncodingMode.URI_COMPONENTSWebClient依赖于DefaultUriBuilderFactory中的默认值,该默认值已从 5.0.x 中的EncodingMode.URI_COMPONENTS更改为 5.1 中的EncodingMode.TEMPLATE_AND_VALUES

1.4.4. 相对 Servlet 请求

您可以使用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()

Note

从 5.1 开始,ServletUriComponentsBuilder会忽略ForwardedX-Forwarded-*Headers 中的信息,这些 Headers 指定了 Client 端起源的地址。考虑使用ForwardedHeaderFilter提取和使用或丢弃此类 Headers。

1.4.5. 链接到控制器

Spring MVC 提供了一种准备到控制器方法的链接的机制。例如,以下 MVC 控制器允许创建链接:

@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

    @GetMapping("/bookings/{booking}")
    public ModelAndView getBooking(@PathVariable Long booking) {
        // ...
    }
}

您可以通过按名称引用方法来准备链接,如以下示例所示:

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

URI uri = uriComponents.encode().toUri();

在前面的示例中,我们提供了实际的方法参数值(在本例中为 long 值:21),用作路径变量并插入到 URL 中。此外,我们提供值42来填充任何剩余的 URI 变量,例如从类型级请求 Map 继承的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();

Note

当控制器方法签名可用于fromMethodCall链接创建时,它们的设计受到限制。除了需要适当的参数签名外,返回类型还存在技术限制(即,为链接生成器调用生成运行时代理),因此返回类型不得为final。特别是,视图名称的通用String返回类型在这里不起作用。您应该改用ModelAndView或什至是普通Object(返回值为String)。

较早的示例在MvcUriComponentsBuilder中使用静态方法。在内部,他们依靠ServletUriComponentsBuilder从当前请求的方案,主机,端口,上下文路径和 servlet 路径准备基本 URL。在大多数情况下,此方法效果很好。但是,有时可能不足。例如,您可能不在请求的上下文之内(例如,准备链接的批处理过程),或者您可能需要插入路径前缀(例如,从请求路径中删除并需要重新设置的语言环境前缀)。插入链接)。

在这种情况下,您可以使用接受fromXxx的静态fromXxx重载方法来使用基本 URL。或者,您可以使用基本 URL 创建MvcUriComponentsBuilder的实例,然后使用基于实例的withXxx方法。例如,以下 Lists 使用withMethodCall

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();

Note

从 5.1 开始,MvcUriComponentsBuilder会忽略ForwardedX-Forwarded-*Headers 中的信息,这些 Headers 指定了 Client 端起源的地址。考虑使用ForwardedHeaderFilter提取和使用或丢弃此类 Headers。

1.4.6. 视图中的链接

在 Thymeleaf,FreeMarker 或 JSP 之类的视图中,可以通过引用每个请求 Map 的隐式或显式分配的名称来构建到带注解 的控制器的链接。

考虑以下示例:

@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函数(即 META-INF/spring.tld),但是可以轻松定义自己的函数或为其他模板技术准备类似的函数。

这是这样的。在启动时,每个@RequestMapping都会通过HandlerMethodMappingNamingStrategy分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如ThingController中的getThing方法变为“ TC#getThing”)。如果存在名称冲突,则可以使用@RequestMapping(name="..")分配一个明确的名称或实现自己的HandlerMethodMappingNamingStrategy

1.5. 异步请求

与 WebFlux 相比

Spring MVC 与 Servlet 3.0 异步请求processing进行了广泛的集成:

1.5.1. DeferredResult

与 WebFlux 相比

一旦异步请求处理功能在 Servlet 容器中为enabled,控制器方法就可以使用DeferredResult包装任何受支持的控制器方法返回值,如以下示例所示:

@GetMapping("/quotes")
@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 消息),计划任务或其他事件。

1.5.2. Callable

与 WebFlux 相比

控制器可以使用java.util.concurrent.Callable包装任何受支持的返回值,如以下示例所示:

@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}

然后可以通过configured TaskExecutor运行给定的任务来获取返回值。

1.5.3. Processing

与 WebFlux 相比

这是 Servlet 异步请求处理的非常简洁的概述:

  • 可以通过调用request.startAsync()ServletRequest置于异步模式。这样做的主要效果是 Servlet(以及所有过滤器)可以退出,但是响应保持打开状态,以便以后完成处理。

  • request.startAsync()的调用返回AsyncContext,您可以将其用于对异步处理的进一步控制。例如,它提供了dispatch方法,与 Servlet API 的转发类似,不同之处在于,它允许应用程序恢复 Servlet 容器线程上的请求处理。

  • ServletRequest提供对当前DispatcherType的访问,您可以使用它们区分处理初始请求,异步调度,转发和其他调度程序类型。

DeferredResult处理如下:

  • 控制器返回一个DeferredResult并将其保存在一些可以访问它的内存队列或列表中。

  • Spring MVC 调用request.startAsync()

  • 同时,DispatcherServlet和所有已配置的过滤器退出请求处理线程,但响应保持打开状态。

  • 应用程序从某个线程设置DeferredResult,Spring MVC 将请求分派回 Servlet 容器。

  • 再次调用DispatcherServlet,并使用异步产生的返回值恢复处理。

Callable处理如下:

  • 控制器返回Callable

  • Spring MVC 调用request.startAsync()并将Callable提交给TaskExecutor以便在单独的线程中进行处理。

  • 同时,DispatcherServlet和所有过滤器退出 Servlet 容器线程,但响应保持打开状态。

  • 最终Callable产生一个结果,Spring MVC 将请求分派回 Servlet 容器以完成处理。

  • 再次调用DispatcherServlet,并使用Callable异步生成的返回值恢复处理。

有关进一步的背景知识,您还可以阅读博客文章,它介绍了 Spring MVC 3.2 中的异步请求处理支持。

Exception Handling

使用DeferredResult时,可以选择是呼叫setResult还是setErrorResult,但有 exception。在这两种情况下,Spring MVC 都将请求分派回 Servlet 容器以完成处理。然后将其视为控制器方法返回了给定值,或者好像它产生了给定的异常。然后,该异常将通过常规的异常处理机制(例如,调用@ExceptionHandler方法)进行处理。

当您使用Callable时,会发生类似的处理逻辑,主要区别是从Callable返回结果或引发异常。

Interception

HandlerInterceptor实例的类型可以为AsyncHandlerInterceptor,以在启动异步处理的初始请求(而不是postHandleafterCompletion)上接收afterConcurrentHandlingStarted回调。

HandlerInterceptor实现也可以注册CallableProcessingInterceptorDeferredResultProcessingInterceptor,以与异步请求的生命周期更深入地集成(例如,处理超时事件)。有关更多详细信息,请参见AsyncHandlerInterceptor

DeferredResult提供onTimeout(Runnable)onCompletion(Runnable)回调。有关更多详细信息,请参见DeferredResult 的 JavadocCallable可以替代WebAsyncTask,后者提供了超时和完成回调的其他方法。

与 WebFlux 相比

Servlet API 最初是为通过 Filter-Servlet 链进行一次传递而构建的。 Servlet 3.0 中添加了异步请求处理,使应用程序可以退出 Filter-Servlet 链,但保留响应以进行进一步处理。 Spring MVC 异步支持围绕该机制构建。当控制器返回DeferredResult时,退出 Filter-Servlet 链,并释放 Servlet 容器线程。稍后,在设置DeferredResult时,将进行ASYNC调度(到相同的 URL),在此期间再次 Map 控制器,但不是调用它,而是使用DeferredResult值(就像控制器返回了它)来恢复处理。 。

相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这种异步请求处理功能,因为它在设计上是异步的。异步处理已内置在所有框架协定中,并在请求处理的所有阶段得到内在支持。

从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步和Reactive Types作为控制器方法中的返回值。 Spring MVC 甚至支持流式传输,包括 Reactive 背压。但是,与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O,并且每次写入都不需要额外的线程,因此对响应的单个写入仍然处于阻塞状态(并在单独的线程上执行)。

另一个根本的区别是,Spring MVC 在控制器方法参数中不支持异步或 Reactive 类型(例如@RequestBody@RequestPart等),也没有对异步和 Reactive 类型作为模型属性的任何明确支持。 Spring WebFlux 确实支持所有这些。

1.5.4. HTTP 流

与 Spring WebFlux 中的相同

您可以将DeferredResultCallable用作单个异步返回值。如果要产生多个异步值并将那些值写入响应,该怎么办?本节介绍如何执行此操作。

Objects

您可以使用ResponseBodyEmitter返回值生成对象流,其中每个对象都使用HttpMessageConverter序列化并写入响应,如以下示例所示:

@GetMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();

您还可以将ResponseBodyEmitter用作ResponseEntity的正文,以自定义响应的状态和标题。

emitter抛出IOException时(例如,如果远程 Client 端离开了),应用程序不负责清理连接,并且不应调用emitter.completeemitter.completeWithError。取而代之的是,Servlet 容器自动启动AsyncListener错误通知,Spring MVC 在其中发出completeWithError调用。该调用依次对应用程序执行最后的ASYNC分派,在此期间 Spring MVC 调用配置的异常解析器并完成请求。

SSE

SseEmitter(ResponseBodyEmitter的子类)提供对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 messagingSockJS fallback传输(包括 SSE)一起使用,这些传输针对广泛的浏览器。

另请参阅previous section以获取有关异常处理的说明。

Raw Data

有时,绕过消息转换并直接流式传输到响应OutputStream很有用(例如,用于文件下载)。您可以使用StreamingResponseBody返回值类型来执行此操作,如以下示例所示:

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

您可以将StreamingResponseBody用作ResponseEntity中的正文,以自定义响应的状态和标题。

1.5.5. 反应类型

与 Spring WebFlux 中的相同

Spring MVC 支持在控制器中使用反应式 Client 端库(另请参阅 WebFlux 部分中的Reactive Libraries)。这包括spring-webflux中的WebClient以及其他内容,例如 Spring Data 反应数据存储库。在这种情况下,能够从控制器方法返回反应类型是很方便的。

Reactive 返回值的处理方式如下:

  • 与使用DeferredResult相似,单值承诺也适用于此。示例包括Mono(Reactor)或Single(RxJava)。

  • 类似于使用ResponseBodyEmitterSseEmitter,适用于具有流媒体类型(例如application/stream+jsontext/event-stream)的多值流。示例包括Flux(Reactor)或Observable(RxJava)。应用程序还可以返回Flux<ServerSentEvent>Observable<ServerSentEvent>

  • 类似于使用DeferredResult<List<?>>,可以适应具有任何其他媒体类型(例如application/json)的多值流。

Tip

Spring MVC 通过spring-coreReactiveAdapterRegistry支持 Reactor 和 RxJava,这使其可以适应多个反应式库。

为了流式传输到响应,支持了反作用背压,但响应的写仍处于阻塞状态,并通过configured TaskExecutor在单独的线程上执行,以避免阻塞上游源(例如WebClient返回的Flux)。默认情况下,SimpleAsyncTaskExecutor用于阻止写入,但是在负载下不适合。如果您打算使用 Reactive 类型进行流式传输,则应使用MVC configuration来配置任务 Actuator。

1.5.6. Disconnects

与 Spring WebFlux 中的相同

当远程 Client 端离开时,Servlet API 不提供任何通知。因此,在通过SseEmitter或\ <<mvc-ann-async-reactive-types,reactive types>流式传输到响应时,定期发送数据很重要,因为如果 Client 端断开连接,写入将失败。发送可以采取空(仅 Comment)SSE 事件或另一端必须将其解释为心跳和忽略的任何其他数据的形式。

或者,考虑使用具有内置心跳机制的 Web 消息传递解决方案(例如通过 WebSocket 进行 STOMP或带有SockJS的 WebSocket)。

1.5.7. Configuration

与 WebFlux 相比

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

Servlet Container

过滤器和 Servlet 声明具有asyncSupported标志,需要将其设置为true才能启用异步请求处理。另外,应声明过滤器 Map 以处理ASYNC javax.servlet.DispatchType

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

web.xml配置中,可以将<async-supported>true</async-supported>添加到DispatcherServletFilter声明中,并添加<dispatcher>ASYNC</dispatcher>来过滤 Map。

Spring MVC

MVC 配置公开了以下与异步请求处理相关的选项:

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

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

您可以配置以下内容:

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

  • AsyncTaskExecutor用于在与Reactive Types流式传输时阻止写操作,并用于执行从控制器方法返回的Callable实例。如果您使用反应式类型进行流式处理或具有返回Callable的控制器方法,我们强烈建议配置此属性,因为默认情况下,它是SimpleAsyncTaskExecutor

  • DeferredResultProcessingInterceptor个实施和CallableProcessingInterceptor个实施。

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

1.6. CORS

与 Spring WebFlux 中的相同

Spring MVC 使您可以处理 CORS(跨域资源共享)。本节介绍如何执行此操作。

1.6.1. Introduction

与 Spring WebFlux 中的相同

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

跨域资源共享(CORS)是由most browsers实现的W3C specification,它使您可以指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的变通办法。

1.6.2. Processing

与 Spring WebFlux 中的相同

CORS 规范区分飞行前,简单和实际要求。要了解 CORS 的工作原理,您可以阅读this article,或者阅读规范以获得更多详细信息。

Spring MVC HandlerMapping实现为 CORS 提供内置支持。成功将请求 Map 到处理程序后,HandlerMapping个实现检查给定请求和处理程序的 CORS 配置并采取进一步的措施。飞行前请求直接处理,而简单和实际的 CORS 请求被拦截,验证并设置了必需的 CORS 响应 Headers。

为了启用跨域请求(即OriginHeaders 存在并且与请求的主机不同),您需要具有一些显式声明的 CORS 配置。如果找不到匹配的 CORS 配置,则飞行前请求将被拒绝。没有将 CORSHeaders 添加到简单和实际 CORS 请求的响应中,因此,浏览器拒绝了它们。

每个HandlerMapping可以分别是configured,并具有基于 URL 模式的CorsConfigurationMap。在大多数情况下,应用程序使用 MVC Java 配置或 XML 名称空间声明此类 Map,这导致将单个全局 Map 传递给所有HandlerMappping实例。

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

整体配置和局部配置的组合规则通常是相加的,例如,所有全局和所有本地来源。对于仅接受单个值的那些属性(例如allowCredentialsmaxAge),局部变量将覆盖全局值。有关更多详细信息,请参见CorsConfiguration#combine(CorsConfiguration)

Tip

要从源中了解更多信息或进行高级自定义,请查看后面的代码:

  • CorsConfiguration

  • CorsProcessor , DefaultCorsProcessor

  • AbstractHandlerMapping

1.6.3. @CrossOrigin

与 Spring WebFlux 中的相同

@CrossOriginComments 启用带注解 的控制器方法上的跨域请求,如以下示例所示:

@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

默认情况下,@CrossOrigin允许:

  • All origins.

  • All headers.

  • 控制器方法 Map 到的所有 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) {
        // ...
    }
}

1.6.4. 全局配置

与 Spring WebFlux 中的相同

除了细粒度的控制器方法级别配置之外,您可能还想定义一些全局 CORS 配置。您可以在任何HandlerMapping上分别设置基于 URL 的CorsConfigurationMap。但是,大多数应用程序使用 MVC Java 配置或 MVC XNM 命名空间来执行此操作。

默认情况下,全局配置启用以下功能:

  • All origins.

  • All headers.

  • GETHEADPOST方法。

默认情况下未启用allowedCredentials,因为它构建了一个信任级别,该级别公开了敏感的用户特定信息(例如 cookie 和 CSRF 令牌),仅应在适当的地方使用。

maxAge设置为 30 分钟。

Java Configuration

与 Spring WebFlux 中的相同

要在 MVC Java 配置中启用 CORS,可以使用CorsRegistry回调,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("http://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
XML Configuration

要在 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>

1.6.5. CORS 过滤器

与 Spring WebFlux 中的相同

您可以通过内置的CorsFilter来应用 CORS 支持。

Note

如果您尝试将CorsFilter与 Spring Security 一起使用,请记住 Spring Security 的 CORS 具有built-in support

要配置过滤器,请将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);

1.7. 网络安全

与 Spring WebFlux 中的相同

Spring Security项目为保护 Web 应用程序免受恶意攻击提供支持。请参阅 Spring Security 参考文档,包括:

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

1.8. HTTP 缓存

与 Spring WebFlux 中的相同

HTTP 缓存可以显着提高 Web 应用程序的性能。 HTTP 缓存围绕Cache-Control响应 Headers 以及随后的条件请求 Headers(例如Last-ModifiedETag)展开。 Cache-Control为私有(例如浏览器)和公共(例如代理)缓存提供有关如何缓存和重用响应的建议。 ETagHeaders 用于发出条件请求,如果内容未更改,则可能导致没有主体的 304(NOT_MODIFIED)。 ETag可以看作是Last-ModifiedHeaders 的更复杂的后继者。

本节描述了 Spring Web MVC 中与 HTTP 缓存相关的选项。

1.8.1. CacheControl

与 Spring WebFlux 中的相同

CacheControl支持配置与Cache-ControlHeaders 相关的设置,并且在许多地方都作为参数接受:

RFC 7234描述了Cache-Control响应 Headers 的所有可能的指令,而CacheControl类型采用了面向用例的方法,该方法着重于常见方案:

// 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();

WebContentGenerator还接受一个更简单的cachePeriod属性(以秒为单位定义),该属性的工作方式如下:

  • -1的值不会生成Cache-Control响应头。

  • 0值可防止使用'Cache-Control: no-store'指令进行缓存。

  • n > 0值使用'Cache-Control: max-age=n'指令将给定响应缓存n秒。

1.8.2. Controllers

与 Spring WebFlux 中的相同

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为需要先计算资源的lastModifiedETag值,然后才能将其与条件请求 Headers 进行比较。控制器可以将ETagHeaders 和Cache-Control设置添加到ResponseEntity,如以下示例所示:

@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);
}

如果与条件请求 Headers 的比较表明内容未更改,则前面的示例发送带有空主体的 304(NOT_MODIFIED)响应。否则,ETagCache-ControlHeaders 将添加到响应中。

您还可以在控制器中针对条件请求 Headers 进行检查,如以下示例所示:

@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

    long eTag = ... (1)

    if (request.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
  • (1) 特定于应用程序的计算。
  • (2) 响应已设置为 304(NOT_MODIFIED)-没有进一步处理。
  • (3) continue 进行请求处理。

可以使用三种变体来检查针对eTag值和lastModified值或两者的条件请求。对于有条件的GETHEAD请求,可以将响应设置为 304(NOT_MODIFIED)。对于条件POSTPUTDELETE,您可以改为将响应设置为 409(PRECONDITION_FAILED),以防止并发修改。

1.8.3. 静态资源

与 Spring WebFlux 中的相同

您应该为静态资源提供Cache-Control和条件响应 Headers,以实现最佳性能。请参阅有关配置Static Resources的部分。

1.8.4. ETag 过滤器

您可以使用ShallowEtagHeaderFilter添加根据响应内容计算的“浅” eTag值,从而节省带宽,但不节省 CPU 时间。参见Shallow ETag

1.9. 查看技术

与 Spring WebFlux 中的相同

Spring MVC 中视图技术的使用是可插入的,无论您决定使用 Thymeleaf,Groovy 标记模板,JSP 还是其他技术,主要取决于配置更改。本章介绍与 Spring MVC 集成的视图技术。我们假设您已经熟悉View Resolution

1.9.1. Thymeleaf

与 Spring WebFlux 中的相同

Thymeleaf 是一种现代的服务器端 Java 模板引擎,它强调可以通过双击在浏览器中预览的自然 HTML 模板,这对于独立处理 UI 模板(例如,由设计人员)而无需使用非常有用。正在运行的服务器。如果要替换 JSP,Thymeleaf 提供了最广泛的功能集之一,以使这种过渡更加容易。 Thymeleaf 是积极开发和维护的。有关更完整的介绍,请参见Thymeleaf项目主页。

Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目 Management。该配置涉及一些 Bean 声明,例如ServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver。有关更多详细信息,请参见Thymeleaf+Spring

1.9.2. FreeMarker

与 Spring WebFlux 中的相同

Apache FreeMarker是一个模板引擎,用于生成从 HTML 到电子邮件等的任何类型的文本输出。 Spring 框架具有内置的集成,可以将 Spring MVC 与 FreeMarker 模板一起使用。

View Configuration

与 Spring WebFlux 中的相同

以下示例显示了如何将 FreeMarker 配置为一种视图技术:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freemarker();
    }

    // Configure FreeMarker...

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

以下示例显示了如何在 XML 中进行配置:

<mvc:annotation-driven/>

<mvc:view-resolvers>
    <mvc:freemarker/>
</mvc:view-resolvers>

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

另外,您也可以声明FreeMarkerConfigurer bean,以完全控制所有属性,如以下示例所示:

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

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

FreeMarker Configuration

与 Spring WebFlux 中的相同

您可以通过在FreeMarkerConfigurer bean 上设置适当的 bean 属性,将 FreeMarker 的“设置”和“ SharedVariables”直接传递给 FreeMarker Configuration对象(由 SpringManagement)。 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"/>

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

Form Handling

Spring 提供了一个供 JSP 使用的标签库,其中包含<spring:bind/>元素。该元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中Validator的验证失败的结果。 Spring 还支持 FreeMarker 中的相同功能,并带有用于生成表单 Importing 元素本身的附加便利宏。

绑定宏

两种语言都在spring-webmvc.jar文件中维护了一组标准宏,因此它们始终可用于经过适当配置的应用程序。

Spring 库中定义的某些宏被视为内部(私有)宏,但是在宏定义中不存在这种范围,使所有宏对调用代码和用户模板可见。以下各节仅关注您需要从模板内直接调用的宏。如果您希望直接查看宏代码,则该文件名为spring.ftl,位于org.springframework.web.servlet.view.freemarker包中。

Simple Binding

在充当 Spring MVC 控制器的表单视图的 HTML 表单(vm 或 ftl 模板)中,您可以使用类似于下一个示例的代码绑定到字段值,并以类似于 JSP 的方式显示每个 Importing 字段的错误消息。当量。以下示例显示了先前配置的personForm视图:

<!-- freemarker macros have to be imported into a namespace. We strongly
recommend sticking to 'spring' -->
<#import "/spring.ftl" as spring/>
<html>
    ...
    <form action="" method="POST">
        Name:
        <@spring.bind "myModelObject.name"/>
        <input type="text"
            name="${spring.status.expression}"
            value="${spring.status.value?html}"/><br>
        <#list spring.status.errorMessages as error> <b>${error}</b> <br> </#list>
        <br>
        ...
        <input type="submit" value="submit"/>
    </form>
    ...
</html>

<@spring.bind>需要一个'path'参数,该参数由命令对象的名称(除非您在FormController属性中进行了更改,否则为'command'),后跟句点和命令对象上的字段名称希望绑定。您还可以使用嵌套字段,例如command.address.streetbind宏假定web.xml中的 ServletContext 参数defaultHtmlEscape指定的默认 HTML 转义行为。

名为<@spring.bindEscaped>的宏的可选形式带有第二个参数,并明确指定在状态错误消息或值中应使用 HTML 转义。您可以根据需要将其设置为truefalse。附加的表单处理宏可简化 HTML 转义的使用,并且应尽可能使用这些宏。下一节将对它们进行说明。

Input macros

两种语言的附加便利宏均简化了绑定和表单生成(包括验证错误显示)。从来没有必要使用这些宏来生成表单 Importing 字段,并且您可以将它们与简单的 HTML 混合或匹配,或者直接调用我们前面强调的 spring 绑定宏。

下表可用的宏显示了每个 FTL 定义和参数列表:

表 6.宏定义表

macro FTL definition
message(根据 code 参数从资源包中输出字符串) <@spring.message code/>
messageText(根据 code 参数从资源包中输出一个字符串,回退到默认参数的值) \ <@spring.messageText code, text/>
url(使用应用程序的上下文根作为相对 URL 的前缀) <@spring.url relativeUrl/>
formInput(用于收集用户 Importing 的标准 Importing 字段) \ <@spring.formInput path, attributes, fieldType/>
formHiddenInput(用于提交非用户 Importing 的隐藏 Importing 字段) \ <@spring.formHiddenInput path, attributes/>
formPasswordInput(用于收集密码的标准 Importing 字段.请注意,此类型的字段中不会填充任何值.) \ <@spring.formPasswordInput path, attributes/>
formTextarea(大文本字段,用于收集长而自由格式的文本 Importing) \ <@spring.formTextarea path, attributes/>
formSingleSelect(选项的下拉框,用于选择一个必需的值) \ <@spring.formSingleSelect path, options, attributes/>
formMultiSelect(选项列表框,允许用户选择 0 个或多个值) \ <@spring.formMultiSelect path, options, attributes/>
formRadioButtons(一组单选按钮,可从可用选项中进行单个选择) \ <@spring.formRadioButtons path, options separator, attributes/>
formCheckboxes(一组允许选择 0 个或多个值的复选框) \ <@spring.formCheckboxes path, options, separator, attributes/>
formCheckbox(单个复选框) \ <@spring.formCheckbox path, attributes/>
showErrors(简化显示绑定字段的验证错误) \ <@spring.showErrors separator, classOrStyle/>
  • 在 FTL(FreeMarker)中,实际上并不需要formHiddenInputformPasswordInput,因为您可以使用普通的formInput宏,将hiddenpassword指定为fieldType参数的值。

以上任何宏的参数都具有一致的含义:

  • path:要绑定的字段的名称(即“ command.name”)

  • options:可以在 Importing 字段中选择的所有可用值中的Map。Map 的键表示从表单回发并绑定到命令对象的值。针对键存储的 Map 对象是在表单上显示给用户的标签,并且可能与表单回发的相应值不同。通常,这种 Map 由控制器作为参考数据提供。您可以使用任何Map实现,具体取决于所需的行为。对于严格排序的 Map,可以将SortedMap(例如TreeMap)与合适的Comparator一起使用,对于要按插入 Sequences 返回值的任意 Map,请使用LinkedHashMapcommons-collectionsLinkedMap

  • separator:当多个选项可用作离散元素(单选按钮或复选框)时,用于分隔列表中的每个字符的字符序列(例如<br>)。

  • attributes:HTML 标记本身中将包含一个附加的任意标记字符串或文本。该字符串实际上是由宏回显的。例如,在textarea字段中,您可以提供属性(例如'rows =“ 5” cols =“ 60”'),也可以传递样式信息,例如'style =“ border:1px solid silver”'。

  • classOrStyle:对于showErrors宏,包装每个错误的span元素使用的 CSS 类的名称。如果未提供任何信息(或该值为空),则错误将被包装在<b></b>标记中。

以下各节概述了宏的示例(某些在 FTL 中,有些在 VTL 中)。如果两种语言之间存在用法差异,请在注解 中进行说明。

Input Fields

formInput宏带有path参数(command.name)和一个附加的attributes参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一起,对 path 参数执行隐式 Spring 绑定。绑定将保持有效,直到发生新的绑定为止,因此showErrors宏无需再次传递 path 参数。它在最后创建绑定的字段上进行操作。

showErrors宏带有分隔符参数(用于分隔给定字段上的多个错误的字符),并且还接受第二个参数-这次是类名或样式属性。注意,FreeMarker 可以为 attributes 参数指定默认值。下面的示例演示如何使用formInputshowWErrors宏:

<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>

下一个示例显示表单片段的输出,生成名称字段,并在提交表单后在该字段中没有值的情况下显示验证错误。验证通过 Spring 的 Validation 框架进行。

生成的 HTML 类似于以下示例:

Name:
<input type="text" name="name" value="">
<br>
    <b>required</b>
<br>
<br>

formTextarea宏的工作方式与formInput宏相同,并且接受相同的参数列表。通常,第二个参数(属性)用于传递样式信息或textarearowscols属性。

Selection Fields

您可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择 Importing:

  • formSingleSelect

  • formMultiSelect

  • formRadioButtons

  • formCheckboxes

四个宏中的每个宏都接受一个Map选项,其中包含表单字段的值和与该值相对应的标签。值和标签可以相同。

下一个示例是 FTL 中的单选按钮。表单支持对象为此字段指定默认值“伦敦”,因此无需验证。呈现表单时,将在模型中以“ cityMap”为名称提供可供选择的整个城市列表作为参考数据。以下 Lists 显示了示例:

...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>

上面的 Lists 呈现了一行单选按钮,其中一个用于cityMap中的每个值,并使用一个分隔符""。没有提供其他属性(缺少该宏的最后一个参数)。 cityMap对 Map 中的每个键值对使用相同的String。Map 键是表单实际作为 POSTed 请求参数提交的键。Map 值是用户看到的标签。在前面的示例中,给定三个知名城市的列表以及表单支持对象中的默认值,HTML 类似于以下内容:

Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>

如果您的应用程序希望通过内部代码处理城市(例如),则可以使用适当的键创建代码 Map,如以下示例所示:

protected Map<String, String> referenceData(HttpServletRequest request) throws Exception {
    Map<String, String> cityMap = new LinkedHashMap<>();
    cityMap.put("LDN", "London");
    cityMap.put("PRS", "Paris");
    cityMap.put("NYC", "New York");

    Map<String, String> model = new HashMap<>();
    model.put("cityMap", cityMap);
    return model;
}

现在,该代码会生成输出,其中无线电值是相关代码,但是用户仍然可以看到更友好的城市名称,如下所示:

Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>
HTML Escaping

前面描述的表单宏的默认用法导致 HTML 元素符合 HTML 4.01,并且使用web.xml文件中定义的 HTML 转义的默认值,如 Spring 的绑定支持所使用。要使元素符合 XHTML 或覆盖默认的 HTML 转义值,可以在模板(或模型中对模板可见的位置)中指定两个变量。在模板中指定它们的优点是,可以在稍后的模板处理中将它们更改为不同的值,以为表单中的不同字段提供不同的行为。

要为您的标记切换到 XHTML 兼容性,请为名为xhtmlCompliant的模型或上下文变量指定值true,如以下示例所示:

<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>

处理完此指令后,Spring 宏生成的任何元素现在都符合 XHTML。

以类似的方式,您可以指定每个字段的 HTML 转义,如以下示例所示:

<#-- until this point, default HTML escaping is used -->

<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>

<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->

1.9.3. Groovy 标记

Groovy 标记模板引擎主要用于生成类似 XML 的标记(XML,XHTML,HTML5 等),但是您可以使用它来生成任何基于文本的内容。 Spring 框架具有内置的集成,可以将 Spring MVC 与 Groovy 标记一起使用。

Note

Groovy 标记模板引擎需要 Groovy 2.3.1.

Configuration

以下示例显示了如何配置 Groovy 标记模板引擎:

@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/"/>
Example

与传统的模板引擎不同,Groovy 标记依赖于使用构建器语法的 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')
    }
}

1.9.4. 脚本视图

与 Spring WebFlux 中的相同

Spring 框架具有内置的集成,可以将 Spring MVC 与可以在JSR-223 Java 脚本引擎之上运行的任何模板库一起使用。我们已经在不同的脚本引擎上测试了以下模板库:

Tip

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

Requirements

与 Spring WebFlux 中的相同

您需要在 Classpath 上具有脚本引擎,其细节因脚本引擎而异:

  • Java 8 随附了Nashorn JavaScript 引擎。强烈建议使用可用的最新更新版本。

  • 应该添加JRuby作为 Ruby 支持的依赖项。

  • 应该添加Jython作为对 Python 支持的依赖。

  • 为了支持 Kotlin 脚本,应添加org.jetbrains.kotlin:kotlin-script-util依赖项和包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory行的META-INF/services/javax.script.ScriptEngineFactory文件。有关更多详细信息,请参见this example

您需要具有脚本模板库。一种针对 Javascript 的方法是通过WebJars

Script Templates

与 Spring WebFlux 中的相同

您可以声明一个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>

对于 Java 和 XML 配置,该控制器看起来没有什么不同,如以下示例所示:

@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>

使用以下参数调用 render 函数:

  • 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;
    }
}

Note

当您将非线程安全脚本引擎与不是为并发设计的模板库一起使用时,需要将sharedEngine属性设置为false,例如 Nashorn 上运行的 Handlebars 或 React。在这种情况下,由于this bug,因此需要 Java 8u60 或更高版本。

polyfill.js仅定义 Handlebars 正常运行所需的window对象,如下所示:

var window = {};

这个基本的render.js实现在使用模板之前先对其进行编译。生产就绪的实现还应该存储任何重用的缓存模板或预编译的模板。您可以在脚本方面进行操作(并处理所需的任何自定义,例如 Management 模板引擎配置)。以下示例显示了如何执行此操作:

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

查看 Spring Framework 单元测试Javaresources,以获取更多配置示例。

1.9.5. JSP 和 JSTL

Spring 框架具有内置的集成,可以将 Spring MVC 与 JSP 和 JSTL 一起使用。

View Resolvers

使用 JSP 开发时,可以声明InternalResourceViewResolverResourceBundleViewResolver bean。

ResourceBundleViewResolver依靠属性文件来定义 Map 到类和 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'目录下的目录中,以便 Client 端无法直接访问。

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

使用 Java 标准标记库时,必须使用特殊的视图类JstlView,因为 JSTL 需要一些准备工作,然后 I18N 功能才能正常工作。

Spring 的 JSP 标签库

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

spring.tld标签库 Descriptors(TLD)包含在spring-webmvc.jar中。有关单个标签的全面参考,请浏览API reference或查看标签库说明。

Spring 的表单标签库

从 2.0 版开始,Spring 使用 JSP 和 Spring Web MVC 时,提供了一组全面的数据绑定感知标记,用于处理表单元素。每个标签都支持与其对应的 HTML 标签对等物的属性集,从而使标签熟悉且使用直观。标记生成的 HTML 符合 HTML 4.01/XHTML 1.0.

与其他表单/Importing 标签库不同,Spring 的表单标签库与 Spring Web MVC 集成在一起,使标签可以访问命令对象和控制器处理的参考数据。正如我们在以下示例中所示,表单标签使 JSP 易于开发,读取和维护。

我们浏览一下表单标签,并查看有关如何使用每个标签的示例。我们包含了生成的 HTML 代码段,其中某些标记需要进一步的注解。

Configuration

表单标签库 Binding 在spring-webmvc.jar中。库 Descriptors 称为spring-form.tld

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

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

其中form是要用于此库中标签的标签名称前缀。

表单标签

此标记呈现 HTML'form'元素,并向内部标记公开绑定路径以进行绑定。它将命令对象放在PageContext中,以便内部标签可以访问该命令对象。该库中的所有其他标签都是form标签的嵌套标签。

假设我们有一个名为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>

页面控制器从放置在PageContext中的命令对象中检索firstNamelastName值。continue 阅读以查看有关如何将内部标签与form标签一起使用的更复杂的示例。

下面的 Lists 显示了生成的 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>
Importing 标签

默认情况下,此标记呈现具有绑定值和type='text'的 HTML input元素。有关此标记的示例,请参见表单标签。您还可以使用特定于 HTML5 的类型,例如emailteldate等。

复选框标签

此标签呈现type设置为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标记有三种方法,应该可以满足您所有复选框的需求。

  • 方法一:当绑定值为java.lang.Boolean时,如果绑定值为true,则input(checkbox)被标记为checkedvalue属性对应于setValue(Object) value 属性的解析值。

  • 方法二:当界限值是arrayjava.util.Collection类型时,如果界限Collection中存在已配置的setValue(Object)值,则input(checkbox)被标记为checked

  • 方法三:对于任何其他绑定值类型,如果已配置的setValue(Object)等于绑定值,则将input(checkbox)标记为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 表单数据绑定生效。 checkbox标签遵循现有的 Spring 约定,其中为每个复选框包括一个带有下划线(_)前缀的隐藏参数。通过这样做,您可以有效地告诉 Spring“复选框在表单中可见,并且我希望与表单数据绑定的对象能够反映复选框的状态,无论如何。”

复选框标签

此标签呈现type设置为checkbox的多个 HTML input标签。

本节以上一个checkbox标签节的示例为基础。有时,您宁愿不必在 JSP 页面中列出所有可能的爱好。您宁愿在运行时提供可用选项的列表,然后将其传递给标记。这就是checkboxes标记的目的。您可以传入items属性中包含可用选项的ArrayListMap。通常,bound 属性是一个集合,因此它可以容纳用户选择的多个值。以下示例显示了使用此标记的 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>

本示例假定interestListList,可以用作模型属性,其中包含要从中选择的值的字符串。如果使用Map,则将 Map 条目键用作值,并将 Map 条目的值用作要显示的标签。您也可以使用自定义对象,在其中可以使用itemValue提供值的属性名称,并通过itemLabel提供标签。

单选按钮标记

此标记呈现type设置为radio的 HTML input元素。

典型的用法模式涉及绑定到相同属性但值不同的多个标记实例,如以下示例所示:

<tr>
    <td>Sex:</td>
    <td>
        Male: <form:radiobutton path="sex" value="M"/> 
Female: <form:radiobutton path="sex" value="F"/> </td> </tr>
单选按钮标记

此标记呈现type设置为radio的多个 HTML input元素。

checkboxes tag一样,您可能希望将可用选项作为运行时变量传递。为此,您可以使用radiobuttons标签。您传入ArrayListMap,它们包含items属性中的可用选项。如果使用Map,则将 Map 条目键用作值,并将 Map 条目的值用作要显示的标签。您还可以使用一个自定义对象,在其中您可以使用itemValue提供值的属性名称,并通过itemLabel提供标签,如以下示例所示:

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

此标记呈现具有设置为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>
选择标签

此标记呈现 HTML“ select”元素。它支持将数据绑定到所选选项以及使用嵌套的optionoptions标签。

假设User拥有技能列表。相应的 HTML 可能如下所示:

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

如果User's技能是草药学,则“技能”行的 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>
选项标签

此标记呈现 HTML option元素。它基于绑定值设置selected。以下 HTML 显示了其典型输出:

<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's房屋位于格兰芬多,则“房屋”行的 HTML 源代码如下:

<tr>
    <td>House:</td>
    <td>
        <select name="house">
            <option value="Gryffindor" selected="selected">Gryffindor</option> (1)
            <option value="Hufflepuff">Hufflepuff</option>
            <option value="Ravenclaw">Ravenclaw</option>
            <option value="Slytherin">Slytherin</option>
        </select>
    </td>
</tr>
  • (1) 请注意添加了selected属性。
选项标签

此标记呈现 HTML option元素的列表。它根据绑定值设置selected属性。以下 HTML 显示了其典型输出:

<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居住在英国,则“国家/locale”行的 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> (1)
            <option value="US">United States</option>
        </select>
    </td>
</tr>
  • (1) 请注意添加了selected属性。

如前面的示例所示,option标记与options标记的组合用法会生成相同的标准 HTML,但可以让您在 JSP 中显式指定一个仅用于显示(它所属的位置)的值,例如例如:“-请选择”。

items属性通常由项对象的集合或数组填充。 itemValueitemLabel引用这些项目对象的 bean 属性(如果已指定)。否则,项目对象本身将变成字符串。或者,您可以指定项目的Map,在这种情况下,Map 键将解释为选项值,并且 Map 值对应于选项标签。如果也恰好指定了itemValueitemLabel(或两者),则 item value 属性应用于 Map 键,item label 属性应用于 Map 值。

textarea 标签

此标记呈现 HTML textarea元素。以下 HTML 显示了其典型输出:

<tr>
    <td>Notes:</td>
    <td><form:textarea path="notes" rows="3" cols="20"/></td>
    <td><form:errors path="notes"/></td>
</tr>
隐藏的标签

该标签呈现 HTML input标签,其中type设置为hidden并具有绑定值。要提交未绑定的隐藏值,请使用type设置为hidden的 HTML input标记。以下 HTML 显示了其典型输出:

<form:hidden path="house"/>

如果我们选择将house值提交为隐藏值,则 HTML 如下所示:

<input name="house" type="hidden" value="Gryffindor"/>
错误代码

此标记在 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字段关联的所有错误。

  • 如果省略path,则仅显示对象错误。

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

<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.
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标签库 Descriptors(TLD)包含在spring-webmvc.jar中。有关单个标签的全面参考,请浏览API reference或查看标签库说明。

HTTP 方法转换

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

为了支持 HTTP 方法转换,Spring MVC 表单标签已更新为支持设置 HTTP 方法。例如,以下代码片段来自“宠物诊所”samples:

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

前面的示例执行 HTTP POST,并将“真实” DELETE 方法隐藏在请求参数后面。它由 web_xml 中定义的HiddenHttpMethodFilter拾取,如以下示例所示:

<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 Tags

Spring 表单标签库允许 Importing 动态属性,这意味着您可以 Importing 任何 HTML5 特定的属性。

input标签形式支持 Importingtext以外的 type 属性。这旨在允许呈现新的 HTML5 特定 Importing 类型,例如emaildaterange等。请注意,由于text是默认类型,因此不需要 Importingtype='text'

1.9.6. Tiles

您可以像使用其他视图技术一样,将 Tiles 集成到使用 Spring 的 Web 应用程序中。本节将广泛地描述如何执行此操作。

Note

本节重点介绍org.springframework.web.servlet.view.tiles3软件包中 Spring 对 Tiles 版本 3 的支持。

Dependencies

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

Configuration

为了能够使用 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

Note

由于下划线用于指示语言环境,因此我们建议不要在 Tiles 定义的文件名中使用下划线。

UrlBasedViewResolver

UrlBasedViewResolver为要解析的每个视图实例化给定的viewClass。以下 bean 定义了UrlBasedViewResolver

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

ResourceBundleViewResolver必须提供一个属性文件,其中包含解析程序可以使用的视图名称和视图类。以下示例显示了ResourceBundleViewResolver的 bean 定义以及相应的视图名称和视图类(摘自 Pet Clinic 示例):

<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 定义文件中使用ViewPreparer引用的详细信息,请参见 Tiles 文档。

您可以基于指定的准备器类,通过应用 Spring 的容器回调以及应用配置的 Spring BeanPostProcessor,来指定SimpleSpringPreparerFactory自动连接ViewPreparer实例。如果已激活 Spring 的上下文范围内的注解 配置,则会自动检测并应用ViewPreparer类中的注解。请注意,这与默认的PreparerFactory一样,期望 Tiles 定义文件中的准备程序类。

您可以指定SpringBeanPreparerFactory来对指定的准备器名称(而不是类)进行操作,从而从 DispatcherServlet 的应用程序上下文中获取相应的 Spring bean。在这种情况下,完整的 bean 创建过程由 Spring 应用程序上下文控制,从而允许使用显式依赖项注入配置,作用域 bean 等。注意,您需要为每个准备器名称定义一个 Spring bean 定义(在 Tiles 定义中使用)。下面的示例显示如何在TilesConfigurer bean 上定义一个SpringBeanPreparerFactory属性集:

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

1.9.7. RSS 和 Atom

AbstractAtomFeedViewAbstractRssFeedView都继承自AbstractFeedViewBase Class,并分别用于提供 Atom 和 RSS Feed 视图。它们基于 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()buildFeedEntries()方法传递 HTTP 请求,以防您需要访问区域设置。仅针对 Cookie 或其他 HTTPHeaders 的设置传入 HTTP 响应。方法返回后,该提要将自动写入响应对象。

有关创建 Atom 视图的示例,请参见 Alef Arendsen 的 Spring Team Blog entry

1.9.8. PDF 和 Excel

Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。本节介绍如何使用这些功能。

文档视图简介

HTML 页面并非始终是用户查看模型输出的最佳方法,而 Spring 使从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是视图,并从服务器以正确的 Content Type 进行流传输,以(有希望)使 Client 端 PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。

为了使用 Excel 视图,您需要将 Apache POI 库添加到 Classpath 中,要生成 PDF,需要添加(最好是)OpenPDF 库。

Note

如果可能,您应该使用基础文档生成库的最新版本。特别是,我们强烈建议您使用 OpenPDF(例如,OpenPDF 1.0.5)而不是过时的原始 iText 2.1.7,因为 OpenPDF 会得到积极维护并修复了不可信任 PDF 内容的重要漏洞。

PDF Views

单词列表的简单 PDF 视图可以扩展org.springframework.web.servlet.view.document.AbstractPdfView并实现buildPdfDocument()方法,如以下示例所示:

public class PdfWordList extends AbstractPdfView {

    protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
            HttpServletRequest request, HttpServletResponse response) throws Exception {

        List<String> words = (List<String>) model.get("wordList");
        for (String word : words) {
            doc.add(new Paragraph(word));
        }
    }
}

控制器可以从外部视图定义(按名称引用)或作为处理程序方法的View实例返回此类视图。

Excel Views

从 Spring Framework 4.2 开始,org.springframework.web.servlet.view.document.AbstractXlsView作为 Excel 视图的 Base Class 提供。它基于 Apache POI,具有取代过时的AbstractExcelView类的专用子类(AbstractXlsxViewAbstractXlsxStreamingView)。

编程模型类似于AbstractPdfView,其中buildExcelDocument()作为中心模板方法,控制器能够从外部定义(按名称)或从 handler 方法作为View实例返回这种视图。

1.9.9. Jackson

与 Spring WebFlux 中的相同

Spring 提供了对 Jackson JSON 库的支持。

基于 Jackson 的 JSON 视图

与 Spring WebFlux 中的相同

MappingJackson2JsonView使用 Jackson 库的ObjectMapper将响应内容呈现为 JSON。默认情况下,模型 Map 的所有内容(特定于框架的类除外)均编码为 JSON。对于需要过滤 Map 内容的情况,可以使用modelKeys属性指定一组特定的模型属性进行编码。您还可以使用extractValueFromSingleKeyModel属性来直接提取和序列化单键模型中的值,而不是将其作为模型属性的 Map。

您可以根据需要使用 Jackson 提供的注解 来自定义 JSONMap。当需要进一步控制时,可以在需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况下,通过ObjectMapper属性注入自定义ObjectMapper

基于 Jackson 的 XML 视图

与 Spring WebFlux 中的相同

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

您可以根据需要使用 JAXB 或 Jackson 提供的注解 自定义 XMLMap。当需要进一步控制时,可以通过ObjectMapper属性注入自定义XmlMapper,对于自定义 XML 的情况,您需要为特定类型提供序列化器和反序列化器。

1.9.10. XML 编组

MarshallingView使用 XML Marshaller(在org.springframework.oxm包中定义)将响应内容呈现为 XML。您可以使用MarshallingView实例的modelKey bean 属性来显式设置要编组的对象。或者,视图遍历所有模型属性,并封送Marshaller支持的第一个类型。有关org.springframework.oxm软件包中功能的更多信息,请参见使用 O/XMap 器编组 XML

1.9.11. XSLT 视图

XSLT 是 XML 的一种转换语言,在 Web 应用程序中作为一种视图技术而流行。如果您的应用程序自然地处理 XML,或者如果您的模型可以轻松转换为 XML,那么 XSLT 可以作为视图技术的不错选择。下一节说明如何将 XML 文档生成为模型数据,以及如何在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。

这个例子是一个普通的 Spring 应用程序,它在Controller中创建一个单词列表,并将它们添加到模型图中。返回该 Map 以及 XSLT 视图的视图名称。有关 Spring Web MVC 的Controller界面的详细信息,请参见Annotated Controllers。 XSLT 控制器将单词列表转换为准备转换的简单 XML 文档。

Beans

配置是简单的 Spring Web 应用程序的标准配置:MVC 配置必须定义XsltViewResolver bean 和常规 MVCComments 配置。以下示例显示了如何执行此操作:

@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;
    }
}
Controller

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

控制器逻辑封装在@Controller类中,其中 handler 方法的定义如下:

@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 文档并将其添加到 ModelMap 中。请注意,您还可以将 XML 文件作为Resource加载并使用它代替自定义 DOM 文档。

有可用的软件包自动“对象化”对象图,但是在 Spring 中,您可以完全灵活地以任何选择的方式从模型中创建 DOM。这样可以防止 XML 转换在模型数据的结构中扮演过多的角色,这在使用工具来 ManagementDOMification 流程时是一种危险。

Transformation

最后,XsltViewResolver解析“主” XSLT 模板文件,并将 DOM 文档合并到其中以生成我们的视图。如XsltViewResolver配置所示,XSLT 模板位于WEB-INF/xsl目录中的war文件中,并以xslt文件 extensions 结尾。

以下示例显示了 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:

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

1.10. MVC 配置

与 Spring WebFlux 中的相同

MVC Java 配置和 MVC XML 名称空间提供适用于大多数应用程序的默认配置以及用于对其进行自定义的配置 API。

有关配置 API 中没有的更高级的自定义设置,请参见高级 Java 配置高级 XML 配置

您不需要了解由 MVC Java 配置和 MVC 名称空间创建的基础 bean。如果您想了解更多信息,请参阅特殊 bean 类Web MVC 配置

1.10.1. 启用 MVC 配置

与 Spring WebFlux 中的相同

在 Java 配置中,可以使用@EnableWebMvc注解来启用 MVC 配置,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig {
}

在 XML 配置中,可以使用<mvc:annotation-driven>元素来启用 MVC 配置,如以下示例所示:

<?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 infrastructure beans,并适应了 Classpath 上可用的依赖项(例如,JSON,XML 等的有效负载转换器)。

1.10.2. MVC Config API

与 Spring WebFlux 中的相同

在 Java 配置中,您可以实现WebMvcConfigurer接口,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}

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

1.10.3. 类型转换

与 Spring WebFlux 中的相同

默认情况下,安装了NumberDate类型的格式化程序,包括对@NumberFormat@DateTimeFormat注解的支持。如果 Classpath 中存在 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>

Note

有关何时使用 FormatterRegistrar 实现的更多信息,请参见FormatterRegistrar SPIFormattingConversionServiceFactoryBean

1.10.4. Validation

与 Spring WebFlux 中的相同

默认情况下,如果Bean Validation存在于 Classpath 中(例如,Hibernate Validator),则LocalValidatorFactoryBean被注册为全局Validator,以便与控制器方法参数上的@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());
    }

}

Tip

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

1.10.5. Interceptors

在 Java 配置中,您可以注册拦截器以应用于传入的请求,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleChangeInterceptor());
        registry.addInterceptor(new ThemeChangeInterceptor()).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>

1.10.6. Content Type

与 Spring WebFlux 中的相同

您可以配置 Spring MVC 如何根据请求确定请求的媒体类型(例如AcceptHeaders,URL 路径扩展,查询参数等)。

默认情况下,将首先检查 URL 路径 extensions,即将jsonxmlrssatom注册为已知 extensions(取决于 Classpath 依赖项)。 AcceptHeaders 被第二次检查。

考虑将这些默认值仅更改为AcceptHeaders,并且,如果必须使用基于 URL 的 Content Type 解析,请考虑对路径扩展使用查询参数策略。有关更多详细信息,请参见Suffix Match后缀匹配和 RFD

在 Java 配置中,您可以自定义请求的 Content Type 解析,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下示例显示了如何在 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>

1.10.7. 讯息转换器

与 Spring WebFlux 中的相同

您可以通过覆盖configureMessageConverters()(以替换 Spring MVC 创建的默认转换器)或覆盖extendMessageConverters()(以自定义默认转换器或向默认转换器添加其他转换器)来自定义 Java 配置中的HttpMessageConverter

以下示例使用自定义的ObjectMapper而非默认的添加了 XML 和 Jackson JSON 转换器:

@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.createXmlMapper(true).build()));
    }
}

在前面的示例中,使用Jackson2ObjectMapperBuilder来为MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter创建启用缩进的通用配置,自定义的日期格式和jackson-module-parameter-names的注册,这增加了对访问参数名称的支持(Java 8 中添加的功能)。

此构建器自定义 Jackson 的默认属性,如下所示:

如果在 Classpath 中检测到以下知名模块,它还将自动注册以下知名模块:

Note

启用具有 Jackson XML 支持的缩进除了jackson-dataformat-xml个之外还需要woodstox-core-asl个依赖。

其他有趣的 Jackson 模块也可用:

以下示例显示了如何在 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"/>

1.10.8. 查看控制器

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

以下 Java 配置示例将对/的请求转发到名为home的视图:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下示例通过使用<mvc:view-controller>元素,实现了与上述示例相同的操作,但使用 XML:

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

1.10.9. 查看解析器

与 Spring WebFlux 中的相同

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

以下 Java 配置示例通过使用 JSP 和 Jackson 作为 JSON 渲染的默认View来配置内容协商视图解析:

@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 标记和脚本模板也需要配置基础视图技术。

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("/freemarker");
        return configurer;
    }
}

1.10.10. 静态资源

与 Spring WebFlux 中的相同

此选项提供了一种方便的方式来从基于Resource的位置列表中提供静态资源。

在下一个示例中,给定一个以/resources开头的请求,相对路径用于在 Web 应用程序根目录下或/static下的 Classpath 下相对于/public查找和提供静态资源。这些资源的有效期为一年,以确保最大程度地利用浏览器缓存并减少浏览器发出的 HTTP 请求。还将评估Last-ModifiedHeaders,并返回304状态代码(如果存在)。

以下 Lists 显示了如何使用 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 缓存支持

资源处理程序还支持ResourceResolver实现和ResourceTransformer实现的链,您可以使用它们来创建工具链以使用优化的资源。

您可以将VersionResourceResolver用于基于资源,固定应用程序版本或其他内容计算出的 MD5 哈希的版本化资源 URL。 ContentVersionStrategy(MD5 哈希)是一个不错的选择,其中有一些值得注意的 exception,例如与模块加载器一起使用的 JavaScript 资源。

以下示例显示了如何在 Java 配置中使用VersionResourceResolver

@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,以便可以将其注入其他对象。对于 Thymeleaf,JSP,FreeMarker 以及其他依赖于HttpServletResponse#encodeURL的 URL 标记,您也可以使用ResourceUrlEncodingFilter使重写透明。

请注意,同时使用EncodedResourceResolver(例如,用于提供压缩或 brotli 编码的资源)和VersionedResourceResolver时,必须按此 Sequences 注册它们。这样可以确保始终基于未编码文件可靠地计算基于内容的版本。

WebJars也受WebJarsResourceResolver支持,并且当org.webjars:webjars-locator存在于 Classpath 中时会自动注册。解析程序可以重写 URL 以包括 jar 的版本,还可以与不带版本的传入 URL 进行匹配,例如/jquery/jquery.min.js/jquery/1.2.0/jquery.min.js

1.10.11. 默认 Servlet

Spring MVC 允许将DispatcherServletMap 到/(从而覆盖了容器默认 Servlet 的 Map),同时仍允许容器默认 Servlet 处理静态资源请求。它将DefaultServletHttpRequestHandler配置为/**的 URLMap,并且相对于其他 URLMap 具有最低的优先级。

该处理程序将所有请求转发到默认 Servlet。因此,它必须按所有其他 URL HandlerMappings的 Sequences 保留在最后。如果使用<mvc:annotation-driven>就是这种情况。另外,如果您设置自己的自定义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/>

覆盖/ ServletMap 的警告是,默认 Servlet 的RequestDispatcher必须通过名称而不是通过路径来检索。 DefaultServletHttpRequestHandler尝试使用大多数主要 Servlet 容器(包括 Tomcat,Jetty,GlassFish,JBoss,Resin,WebLogic 和 WebSphere)的已知名称列表,在启动时自动检测容器的默认 Servlet。如果已使用其他名称自定义配置了默认 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"/>

1.10.12. 路径匹配

与 Spring WebFlux 中的相同

您可以自定义与路径匹配和 URL 处理有关的选项。有关各个选项的详细信息,请参见PathMatchConfigurer javadoc。

以下示例显示了如何在 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())
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController.class));
    }

    @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"/>

1.10.13. 高级 Java 配置

与 Spring WebFlux 中的相同

@EnableWebMvc导入DelegatingWebMvcConfiguration,其中:

  • 为 Spring MVC 应用程序提供默认的 Spring 配置

  • 检测并委托WebMvcConfigurer实现自定义该配置。

对于高级模式,可以删除@EnableWebMvc并直接从DelegatingWebMvcConfiguration扩展,而不是实现WebMvcConfigurer,如以下示例所示:

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    // ...

}

您可以将现有方法保留在WebConfig中,但是现在您还可以覆盖 Base Class 中的 bean 声明,并且在 Classpath 上仍然可以具有任意数量的其他WebMvcConfigurer实现。

1.10.14. 高级 XML 配置

MVC 命名空间没有高级模式。如果您需要在 bean 上自定义一个不能更改的属性,则可以使用 Spring ApplicationContextBeanPostProcessor生命周期钩子,如以下示例所示:

@Component
public class MyPostProcessor implements BeanPostProcessor {

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

注意,您需要将MyPostProcessor声明为 bean,或者以 XML 形式显式声明,或者通过<component-scan/>声明来检测它。

1.11. HTTP/2

与 Spring WebFlux 中的相同

需要 Servlet 4 容器支持 HTTP/2,并且 Spring Framework 5 与 Servlet API 4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置有关的注意事项。有关更多详细信息,请参见HTTP/2 Wiki 页面

Servlet API 确实公开了一种与 HTTP/2 相关的构造。您可以使用javax.servlet.http.PushBuilder主动将资源推送到 Client 端,并且受method argument@RequestMapping方法支持。

2. RESTClient 端

本节描述了 Client 端对 REST 端点的访问选项。

2.1. RestTemplate

RestTemplate是执行 HTTP 请求的同步 Client 端。它是原始的 Spring RESTClient 端,并在基础 HTTPClient 端库上公开了简单的模板方法 API。

Note

从 5.0 开始,无阻塞,ReactiveWebClient提供了RestTemplate的现代替代方案,并有效支持同步和异步以及流方案。 RestTemplate将在将来的版本中弃用,并且以后将不会添加主要的新功能。

有关详情,请参见REST Endpoints

2.2. WebClient

WebClient是用于执行 HTTP 请求的非阻塞,响应式 Client 端。它是在 5.0 中引入的,并提供了RestTemplate的现代替代方案,并有效支持同步和异步以及流方案。

RestTemplate相比,WebClient支持以下内容:

  • Non-blocking I/O.

  • Reactive 产生背压。

  • 高并发,硬件资源更少。

  • 利用 Java 8 lambda 的功能风格,流畅的 API。

  • 同步和异步交互。

  • 从服务器向上流或向下流。

有关更多详细信息,请参见WebClient

3. Testing

与 Spring WebFlux 相同

本节总结了spring-test中可用于 Spring MVC 应用程序的选项。

  • Servlet API 模拟:用于单元测试控制器,过滤器和其他 Web 组件的 Servlet APIContracts 的模拟实现。有关更多详细信息,请参见Servlet API模拟对象。

  • TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法高效地缓存已加载的配置,并支持通过MockServletContext加载WebApplicationContext。有关更多详细信息,请参见TestContext Framework

  • Spring MVC 测试:一种框架,也称为MockMvc,用于通过DispatcherServlet(即,支持注解)测试带注解 的控制器,该框架具有 Spring MVC 基础结构,但没有 HTTP 服务器。有关更多详细信息,请参见Spring MVC 测试

  • Client 端 REST:spring-test提供了一个MockRestServiceServer,您可以将其用作模拟服务器来测试内部使用RestTemplate的 Client 端代码。有关更多详细信息,请参见Client 端 REST 测试

  • WebTestClient:用于测试 WebFlux 应用程序,但也可以用于通过 HTTP 连接到任何服务器的端到端集成测试。它是一个无阻塞的反应式 Client 端,非常适合测试异步和流传输方案。

4. WebSockets

与 Spring WebFlux 中的相同

参考文档的此部分涵盖对 Servlet 堆栈的支持,包括原始 WebSocket 交互的 WebSocket 消息传递,通过 SockJS 进行 WebSocket 仿真以及通过 STOMP 作为 WebSocket 的子协议进行发布-订阅消息传递。

4.1. WebSocket 介绍

WebSocket 协议RFC 6455提供了一种标准化方法,可以通过单个 TCP 连接在 Client 端和服务器之间构建全双工双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过端口 80 和 443 在 HTTP 上工作,并允许重复使用现有的防火墙规则。

WebSocket 交互始于一个 HTTP 请求,该请求使用 HTTP UpgradeHeaders 进行升级,或者在这种情况下切换到 WebSocket 协议。以下示例显示了这种交互:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
  • (1) UpgradeHeaders。
  • (2) 使用Upgrade连接。

具有 WebSocket 支持的服务器代替通常的 200 状态代码,返回类似于以下内容的输出:

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
  • (1) 协议切换

成功握手后,HTTP 升级请求的基础 TCP 套接字将保持打开状态,Client 端和服务器均可 continue 发送和接收消息。

WebSockets 的工作原理的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节或 Web 上的许多简介和教程中的任何一个。

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

4.1.1. HTTP 与 WebSocket

尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头,但重要的是要了解这两个协议导致了截然不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,Client 端访问那些 URL,即请求-响应样式。服务器根据 HTTP URL,方法和 Headers 将请求路由到适当的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息在同一 TCP 连接上流动。这指向了完全不同的异步,事件驱动的消息传递体系结构。

WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息的内容规定任何语义。这意味着除非 Client 端和服务器就消息语义达成一致,否则就无法路由或处理消息。

WebSocketClient 端和服务器可以通过 HTTP 握手请求上的Sec-WebSocket-ProtocolHeaders 协商使用更高级别的消息传递协议(例如 STOMP)。在这种情况下,他们需要提出自己的约定。

4.1.2. 何时使用 WebSockets

WebSockets 可以使网页具有动态性和交互性。但是,在许多情况下,结合使用 Ajax 和 HTTP 流或长时间轮询可以提供一种简单有效的解决方案。

例如,新闻,邮件和社交订阅源需要动态更新,但是每隔几分钟这样做是完全可以的。另一方面,协作,游戏和金融应用程序需要更接近实时。

仅延迟并不是决定因素。如果消息量相对较少(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。低延迟,高频率和高音量的结合才是使用 WebSocket 的最佳案例。

还请记住,在 Internet 上,控件之外的限制性代理可能会阻止 WebSocket 交互,这可能是因为未将它们配置为传递UpgradeHeaders,或者是因为它们关闭了长期处于空闲状态的连接。这意味着与面向公众的应用程序相比,将 WebSocket 用于防火墙内部的应用程序是一个更直接的决定。

4.2. WebSocket API

与 Spring WebFlux 中的相同

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

4.2.1. WebSocketHandler

与 Spring WebFlux 中的相同

创建 WebSocket 服务器就像实现WebSocketHandler一样简单,或者更可能地扩展TextWebSocketHandlerBinaryWebSocketHandler。以下示例使用TextWebSocketHandler

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 配置和 XML 名称空间支持,用于将前面的 WebSocket 处理程序 Map 到特定的 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 服务环境中相对简单。

4.2.2. WebSocket 握手

与 Spring WebFlux 中的相同

定制初始 HTTP WebSocket 握手请求的最简单方法是通过HandshakeInterceptor,它公开了在“之前”和“之后”握手的方法。您可以使用此类拦截器来排除握手或使任何属性对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>

一个更高级的选项是扩展执行 WebSocket 握手步骤的DefaultHandshakeHandler,包括验证 Client 端来源,协商子协议以及其他详细信息。如果应用程序需要配置自定义RequestUpgradeStrategy以适应尚不支持的 WebSocket 服务器引擎和版本,则它可能还需要使用此选项(有关此主题的更多信息,请参见Deployment)。 Java 配置和 XML 名称空间均可配置自定义HandshakeHandler

Tip

Spring 提供了一个WebSocketHandlerDecoratorBase Class,您可以使用该 Base Class 来装饰WebSocketHandler并具有其他行为。使用 WebSocket Java 配置或 XML 名称空间时,默认情况下会提供并添加日志记录和异常处理实现。 ExceptionWebSocketHandlerDecorator捕获由任何WebSocketHandler方法引起的所有未捕获的异常,并关闭状态为1011的 WebSocket 会话,这表明服务器错误。

4.2.3. Deployment

易于将 Spring WebSocket API 集成到 Spring MVC 应用程序中,其中DispatcherServlet服务于 HTTP WebSocket 握手和其他 HTTP 请求。通过调用WebSocketHttpRequestHandler,也很容易将其集成到其他 HTTP 处理方案中。这是方便且易于理解的。但是,对于 JSR-356 运行时,需要特别注意。

Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的 Servlet 容器 Classpath 扫描(Servlet 3 功能)。另一个是在 Servlet 容器初始化时使用的注册 API。这两种机制都无法对所有 HTTP 处理(包括 WebSocket 握手和所有其他 HTTP 请求)(例如 Spring MVC 的DispatcherServlet)使用单个“前端控制器”。

这是 JSR-356 的一个重大限制,即使在 JSR-356 运行时中运行,Spring 的 WebSocket 支持使用特定于服务器的RequestUpgradeStrategy实现解决。 Tomcat,Jetty,GlassFish,WebLogic,WebSphere 和 Undertow(和 WildFly)目前存在此类策略。

Note

已经创建了克服 Java WebSocket API 中的上述限制的请求,可以在eclipse-ee4j/websocket-api#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>

4.2.4. 服务器配置

与 Spring WebFlux 中的相同

每个基础的 WebSocket 引擎都公开控制运行时 Feature 的配置属性,例如消息缓冲区大小的大小,空闲超时等。

对于 Tomcat,WildFly 和 GlassFish,可以将ServletServerContainerFactoryBean添加到 WebSocket Java 配置中,如以下示例所示:

@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>

Note

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

对于 Jetty,您需要提供一个预先配置的 Jetty WebSocketServerFactory,然后通过 WebSocket Java 配置将其插入 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)));
    }

}

下面的示例显示与前面的示例等效的 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>

4.2.5. 允许的来源

与 Spring WebFlux 中的相同

从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是仅接受同源请求。也可以允许所有或指定的来源列表。此检查主要用于浏览器 Client 端。没有任何措施可以阻止其他类型的 Client 端修改OriginHeaders 值(有关更多详细信息,请参见RFC 6454:Web 起源概念)。

三种可能的行为是:

  • 仅允许同源请求(默认):在此模式下,启用 SockJS 时,Iframe HTTP 响应 HeadersX-Frame-Options设置为SAMEORIGIN,并且 JSONP 传输被禁用,因为它不允许检查请求的来源。因此,启用此模式时,不支持 IE6 和 IE7.

  • 允许指定来源列表:每个允许的来源必须以http://https://开头。在此模式下,启用 SockJS 时,将禁用 IFrame 传输。因此,启用此模式时,不支持 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>

4.3. SockJS 后备

在公共 Internet 上,控件外部的限制性代理可能会阻止 WebSocket 交互,这可能是因为它们未配置为传递UpgradeHeaders,或者是因为它们关闭了长期处于空闲状态的连接。

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

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

4.3.1. Overview

SockJS 的目标是让应用程序使用 WebSocket API,但在运行时需要时使用非 WebSocket 替代方法,而无需更改应用程序代码。

SockJS 包括:

  • 以可执行文件narrated tests的形式定义的SockJS protocol

  • SockJS JavaScriptClient 端Client 端库,供浏览器使用。

  • SockJS 服务器实现,包括 Spring Framework spring-websocket模块中的一个。

  • spring-websocket模块中的 SockJS JavaClient 端(从 4.1 版开始)。

SockJS 设计用于浏览器。它使用多种技术来支持各种浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参见SockJS client页。传输分为三大类:WebSocket,HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参见此博客文章

SockJSClient 端首先发送GET /info以从服务器获取基本信息。在那之后,它必须决定使用哪种交通工具。如果可能,请使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流选项。如果不是,则使用 HTTP(长)轮询。

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

http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

where:

  • {server-id}对于在集群中路由请求很有用,但否则不使用。

  • {session-id}关联属于 SockJS 会话的 HTTP 请求。

  • {transport}表示传输类型(例如websocketxhr-streaming等)。

WebSocket 传输仅需要单个 HTTP 请求即可进行 WebSocket 握手。此后所有消息在该套接字上交换。

HTTP 传输需要更多请求。例如,Ajax/XHR 流依赖于对服务器到 Client 端消息的一个长时间运行的请求,以及对 Client 端到服务器消息的其他 HTTP POST 请求。长轮询与长轮询类似,不同之处在于长轮询在每次服务器到 Client 端发送后结束当前请求。

SockJS 添加了最少的消息框架。例如,服务器最初发送字母o(“打开”框架),消息以a["message1","message2"](JSON 编码数组)发送,如果在 25 秒内没有消息流(默认情况下),则以字母h(“心跳”框架)发送消息,和字母c(“关闭”框架)以关闭会话。

要了解更多信息,请在浏览器中运行示例并查看 HTTP 请求。 SockJSClient 端允许修复传输列表,因此可以一次查看每个传输。 SockJSClient 端还提供了调试标志,该标志可在浏览器控制台中启用有用的消息。在服务器端,您可以为org.springframework.web.socket启用TRACE日志记录。有关更多详细信息,请参见 SockJS 协议narrated test

4.3.2. 启用 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页和浏览器支持的传输类型列表。Client 端还提供了几个配置选项,例如用于指定要包括的传输。

4.3.3. IE 8 和 9

Internet Explorer 8 和 9 仍在使用。它们是拥有 SockJS 的关键原因。本节涵盖有关在这些浏览器中运行的重要注意事项。

SockJSClient 端通过使用 Microsoft 的XDomainRequest在 IE 8 和 9 中支持 Ajax/XHR 流。这适用于所有域,但不支持发送 Cookie。 Cookies 对于 Java 应用程序通常是必不可少的。但是,由于 SockJSClient 端可用于多种服务器类型(不仅是 Java 服务器类型),因此需要知道 cookie 是否重要。如果是这样,则 SockJSClient 端更喜欢 Ajax/XHR 进行流传输。否则,它依赖于基于 iframe 的技术。

SockJSClient 端发出的第一个/info请求是对可能影响 Client 端选择传输方式的信息的请求。这些详细信息之一是服务器应用程序是否依赖 Cookie(例如,出于身份验证目的或具有粘性会话的群集)。 Spring 的 SockJS 支持包括名为sessionCookieNeeded的属性。由于大多数 Java 应用程序都依赖JSESSIONID cookie,因此默认情况下启用该功能。如果您的应用程序不需要它,则可以关闭此选项,然后 SockJSClient 端应在 IE 8 和 9 中选择xdr-streaming

如果您确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应 HeadersX-Frame-Options设置为DENYSAMEORIGINALLOW-FROM <origin>来指示浏览器阻止在给定页面上使用 iframe。这用于防止clickjacking

Note

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

有关如何配置X-Frame-OptionsHeaders 设置的详细信息,请参见 Spring Security 文档的默认安全标题。您还可以看到SEC-2501以获得其他背景。

如果您的应用程序添加了X-Frame-Options响应 Headers(应如此!)并依赖于基于 iframe 的传输,则需要将 Headers 值设置为SAMEORIGINALLOW-FROM <origin>。 Spring SockJS 支持还需要知道 SockJSClient 端的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJSClient 端。最好将此选项配置为使用与应用程序源相同的 URL。

以下示例显示了如何在 Java 配置中执行此操作:

@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");
    }

    // ...

}

XML 名称空间通过<websocket:sockjs>元素提供了类似的选项。

Note

在最初的开发过程中,请启用 SockJSClient 端devel模式,以防止浏览器缓存本应缓存的 SockJS 请求(如 iframe)。有关如何启用它的详细信息,请参见SockJS client页。

4.3.4. Heartbeats

SockJS 协议要求服务器发送心跳消息,以防止代理断定连接已挂起。 Spring SockJS 配置具有一个名为heartbeatTime的属性,您可以使用该属性来自定义频率。默认情况下,假设没有其他消息在该连接上发送,则心跳将在 25 秒后发送。对于公共 Internet 应用程序,此 25 秒值与以下IETF recommendation一致。

Note

在 WebSocket 和 SockJS 上使用 STOMP 时,如果 STOMPClient 端和服务器协商要交换的心跳,则会禁用 SockJS 心跳。

Spring SockJS 支持还允许您配置TaskScheduler来安排心跳任务。任务调度程序由线程池支持,其默认设置基于可用处理器的数量。您应该考虑根据您的特定需求自定义设置。

4.3.5. Client 端断开连接

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

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

一个特定的问题是,Servlet API 不会为已离开的 Client 端提供通知。参见eclipse-ee4j/servlet-api#44。但是,Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下每 25 秒发送一次),这意味着通常会在该时间段内(或更早,如果消息发送频率更高)检测到 Client 端断开连接。

Note

结果,由于 Client 端已断开连接,可能会发生网络 I/O 故障,这可能会在日志中填充不必要的堆栈跟踪。 Spring 会尽最大努力找出代表 Client 端断开连接(特定于每个服务器)的此类网络故障,并使用专用日志类别DISCONNECTED_CLIENT_LOG_CATEGORY(在AbstractSockJsSession中定义)来记录最少的消息。如果需要查看堆栈跟踪,可以将该日志类别设置为 TRACE。

4.3.6. SockJS 和 CORS

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

也可以通过在 Spring 的 SockJsService 中设置suppressCors属性来禁用这些 CORSHeaders 的添加。

SockJS 需要以下 Headers 和值:

  • Access-Control-Allow-Origin:从Origin请求 Headers 的值初始化。

  • Access-Control-Allow-Credentials:始终设置为true

  • Access-Control-Request-Headers:从等效请求 Headers 中的值初始化。

  • Access-Control-Allow-Methods:传输支持的 HTTP 方法(请参阅TransportType枚举)。

  • Access-Control-Max-Age:设置为 31536000(1 年)。

有关确切的实现,请参见AbstractSockJsService中的addCorsHeaders和源代码中的TransportType枚举。

另外,如果 CORS 配置允许,请考虑排除带有 SockJS 端点前缀的 URL,从而让 Spring 的SockJsService处理它。

4.3.7. SockJsClient

Spring 提供了一个 SockJS JavaClient 端,无需使用浏览器即可连接到远程 SockJS 端点。当需要通过公共网络在两个服务器之间进行双向通信时(这是网络代理可以阻止使用 WebSocket 协议的地方),这特别有用。 SockJS JavaClient 端对于测试也非常有用(例如,模拟大量并发用户)。

SockJS JavaClient 端支持websocketxhr-streamingxhr-polling传输。其余的仅在浏览器中使用才有意义。

您可以使用以下方式配置WebSocketTransport

  • 在 JSR-356 运行时中为StandardWebSocketClient

  • JettyWebSocketClient通过使用 Jetty 9 本机 WebSocket API。

  • Spring 的WebSocketClient的任何实现。

顾名思义,XhrTransport支持xhr-streamingxhr-polling,因为从 Client 端的角度来看,除了用于连接服务器的 URL 之外没有其他区别。当前有两种实现:

  • RestTemplateXhrTransport使用 Spring 的RestTemplate进行 HTTP 请求。

  • JettyXhrTransport使用 Jetty 的HttpClient进行 HTTP 请求。

以下示例显示了如何创建 SockJSClient 端并连接到 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");

Note

SockJS 对消息使用 JSON 格式的数组。默认情况下,使用 Jackson 2,并且需要在 Classpath 上。另外,您可以配置SockJsMessageCodec的自定义实现,并在SockJsClient上对其进行配置。

要使用SockJsClient模拟大量并发用户,您需要配置基础 HTTPClient 端(用于 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) (1)
            .setHttpMessageCacheSize(1000) (2)
            .setDisconnectDelay(30 * 1000); (3)
    }

    // ...
}
  • (1)streamBytesLimit属性设置为 512KB(默认值为 128KB — 128 * 1024)。
  • (2)httpMessageCacheSize属性设置为 1,000(默认值为100)。
  • (3)disconnectDelay属性设置为 30 属性秒(默认值为 5 秒— 5 * 1000)。

4.4. STOMP

WebSocket 协议定义了两种消息类型(文本消息和二进制消息),但是其内容未定义。该协议定义了一种机制,Client 端和服务器可以协商子协议(即高级消息协议),以在 WebSocket 上使用该协议来定义每种协议可以发送的消息类型,格式,内容。每个消息,依此类推。子协议的使用是可选的,但是无论哪种方式,Client 端和服务器都需要就定义消息内容的某种协议达成共识。

4.4.1. Overview

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

STOMP 是基于帧的协议,其帧以 HTTP 为模型。以下 Lists 显示了 STOMP 帧的结构:

COMMAND
header1:value1
header2:value2

Body^@

Client 端可以使用SENDSUBSCRIBE命令发送或订阅消息,以及destinationHeaders,该 Headers 描述消息的内容以及应由谁接收。这启用了一种简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的 Client 端,或者将消息发送到服务器以请求执行某些工作。

当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序将充当 Client 端的 STOMP 代理。消息被路由到@Controller消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅的用户 Broadcast 消息。您还可以将 Spring 配置为与专用的 STOMP 代理(例如 RabbitMQ,ActiveMQ 等)一起使用,以实际 Broadcast 消息。在那种情况下,Spring 维护与代理的 TCP 连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的 WebSocketClient 端。因此,Spring Web 应用程序可以依靠基于 HTTP 的统一安全性,通用验证以及用于消息处理的熟悉的编程模型。

下面的示例显示了一个订阅以接收股票报价的 Client 端,服务器可能会定期发出该股票报价(例如,通过计划的任务,该任务通过SimpMessagingTemplate向代理发送消息):

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

^@

以下示例显示了一个 Client 端发送的 Transaction 请求,服务器可以通过@MessageMapping方法处理该 Transaction 请求:

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

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

执行后,服务器可以向 ClientBroadcastTransaction 确认消息和详细信息。

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

STOMP 服务器可以使用MESSAGE命令向所有订户 Broadcast 消息。以下示例显示了服务器向订阅的 Client 端发送股票报价的服务器:

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

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

服务器无法发送未经请求的消息。来自服务器的所有消息都必须响应特定的 Client 端订阅,并且服务器消息的subscription-idHeaders 必须与 Client 端订阅的idHeaders 匹配。

前面的概述旨在提供对 STOMP 协议的最基本的了解。我们建议您完整阅读协议specification

4.4.2. Benefits

与使用原始 WebSocket 相比,使用 STOMP 作为子协议可以使 Spring 框架和 Spring Security 提供更丰富的编程模型。关于 HTTP 与原始 TCP 以及它如何使 Spring MVC 和其他 Web 框架提供丰富的功能,可以得出相同的观点。以下是好处列表:

  • 无需发明自定义消息协议和消息格式。

  • 可以使用 STOMPClient 端,包括 Spring 框架中的Java client

  • 您可以(可选)使用消息代理(例如 RabbitMQ,ActiveMQ 和其他代理)来 Management 订阅和 Broadcast 消息。

  • 可以在任意数量的@Controller实例中组织应用程序逻辑,并且可以基于 STOMP 目标 Headers 将消息路由到它们,而对于给定的连接,使用单个WebSocketHandler处理原始 WebSocket 消息。

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

4.4.3. 启用 STOMP

spring-messagingspring-websocket模块中提供了 STOMP over WebSocket 支持。一旦有了这些依赖关系,就可以使用SockJS Fallback通过 WebSocket 公开 STOMP 端点,如以下示例所示:

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) {
        registry.addEndpoint("/portfolio").withSockJS();  (1)
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); (2)
        config.enableSimpleBroker("/topic", "/queue"); (3)
    }
}
  • (1) /portfolio是 WebSocket(或 SockJS)Client 端需要连接到 WebSocket 握手的端点的 HTTP URL。
  • (2) 目标 Headers 以/app开头的 STOMP 消息被路由到@Controller类中的@MessageMapping方法。
  • (3) 使用内置的消息代理进行订阅和 Broadcast,并将目标 Headers 以“ /topicor /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>

Note

对于内置的简单代理,/topic/queue前缀没有任何特殊含义。它们仅是区分发布订阅消息传递和点对点消息传递的约定(即,许多订户与一个 Consumer)。使用外部代理时,请检查代理的 STOMP 页面以了解其支持哪种 STOMP 目标和前缀。

要从浏览器进行连接,对于 SockJS,您可以使用sockjs-client。对于 STOMP,许多应用程序都使用了jmesnil/stomp-websocket库(也称为 stomp.js),该库功能齐全,已在 Producing 使用多年,但不再维护。目前,JSteunou/webstomp-client是该库中最活跃,Developing 最快的继承者。以下示例代码基于此:

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不需要指定loginpasscodeHeaders。即使这样做,它们也会在服务器端被忽略(或更确切地说,被覆盖)。有关身份验证的更多信息,请参见连接到 brokerAuthentication

有关更多示例代码,请参见:

4.4.4. WebSocket 服务器

要配置基础的 WebSocket 服务器,请使用Server Configuration中的信息。对于 Jetty,您需要通过StompEndpointRegistry设置HandshakeHandlerWebSocketPolicy

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").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)));
    }
}

4.4.5. 消息流

公开 STOMP 端点后,Spring 应用程序将成为已连接 Client 端的 STOMP 代理。本节描述服务器端的消息流。

spring-messaging模块包含对源自Spring Integration的消息传递应用程序的基础支持,后来被提取并合并到 Spring 框架中,以便在许多Spring projects和应用程序场景中更广泛地使用。下面的列表简要描述了一些可用的消息传递抽象:

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

消息流简单代理

上图显示了三个消息通道:

  • clientInboundChannel:用于传递从 WebSocketClient 端收到的消息。

  • clientOutboundChannel:用于向 WebSocketClient 端发送服务器消息。

  • brokerChannel:用于从服务器端应用程序代码内将消息发送到消息代理。

下图显示了将外部代理(例如 RabbitMQ)配置为用于 Management 订阅和 Broadcast 消息时使用的组件:

消息流代理中继

前面两个图之间的主要区别是使用“代理中继”将消息通过 TCP 传递到外部 STOMP 代理,以及将消息从代理向下传递到订阅的 Client 端。

从 WebSocket 连接接收到消息后,消息将被解码为 STOMP 帧,转换为 Spring Message表示形式,然后发送至clientInboundChannel进行进一步处理。例如,目标 Headers 以/app开头的 STOMP 消息可以路由到带注解 的控制器中的@MessageMapping方法,而/topic/queue消息可以直接路由到消息代理。

处理来自 Client 端的 STOMP 消息的带注解 的@Controller可以通过brokerChannel向消息代理发送消息,并且代理通过clientOutboundChannel将消息 Broadcast 给匹配的订户。相同的控制器还可以响应 HTTP 请求执行相同的操作,因此 Client 端可以执行 HTTP POST,然后@PostMapping方法可以将消息发送到消息代理,以 Broadcast 到订阅的 Client 端。

我们可以通过一个简单的示例跟踪流程。考虑以下示例,该示例设置了服务器:

@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;
    }

}

前面的示例支持以下流程:

  • Client 端连接到http://localhost:8080/portfolio,一旦构建了 WebSocket 连接,STOMP 帧就开始在其上流动。

  • Client 端发送带有目 Headers/topic/greeting的 SUBSCRIBE 帧。收到并解码后,该消息将发送到clientInboundChannel,然后路由到消息代理,该代理存储 Client 端订阅。

  • Client 端向/app/greeting发送一个 aSEND 帧。 /app前缀有助于将其路由到带注解 的控制器。除去/app前缀后,目标的其余/greeting部分将 Map 到GreetingController中的@MessageMapping方法。

  • GreetingController返回的值被转换为带有返回值和默认目 Headers/topic/greeting(从 Importing 目标中用/app替换为/topic的源目标)的有效负载的 Spring Message。结果消息将发送到brokerChannel并由消息代理处理。

  • 消息代理查找所有匹配的订户,并通过clientOutboundChannel向每个发送一个 MESSAGE 帧,消息从此处被编码为 STOMP 帧并通过 WebSocket 连接发送。

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

4.4.6. 带注解 的控制器

应用程序可以使用带注解 的@Controller类来处理来自 Client 端的消息。这样的类可以声明@MessageMapping@SubscribeMapping@ExceptionHandler方法,如以下主题所述:

@MessageMapping

您可以使用@MessageMappingComments 根据消息的目的地路由消息的方法。在方法级别和类型级别都支持它。在类型级别,@MessageMapping用于表示控制器中所有方法之间的共享 Map。

默认情况下,Map 值是 Ant 样式的路径模式(例如/thing*/thing/**),包括对模板变量的支持(例如/thing/{id})。可以通过@DestinationVariable方法参数引用这些值。应用程序还可以切换到以点分隔的 Map 目标约定,如点作为分隔符中所述。

支持的方法参数

下表描述了方法参数:

Method argument Description
Message 用于访问完整的消息。
MessageHeaders 用于访问Message中的标题。
MessageHeaderAccessorSimpMessageHeaderAccessorStompHeaderAccessor 用于通过类型化访问器方法访问 Headers。
@Payload 为了访问消息的有效负载,由配置的MessageConverter转换(例如,从 JSON 转换)。


不需要此注解,因为默认情况下会假定没有其他自变量匹配。
您可以使用@javax.validation.Valid或 Spring 的@ValidatedComments 有效负载参数,以使有效负载参数得到自动验证。
| @Header |用于访问特定的 Headers 值-必要时还可以使用org.springframework.core.convert.converter.Converter进行类型转换。
| @Headers |用于访问消息中的所有标题。此自变量必须可分配给java.util.Map
| @DestinationVariable |用于访问从消息目标提取的模板变量。根据需要将值转换为声明的方法参数类型。
| java.security.Principal |反映在 WebSocket HTTP 握手时登录的用户。

Return Values

默认情况下,来自@MessageMapping方法的返回值通过匹配的MessageConverter序列化为有效负载,并以Message的形式发送到brokerChannel,并从那里 Broadcast 给订户。出站消息的目的地与入站消息的目的地相同,但前缀为/topic

您可以使用@SendTo@SendToUserComments 来自定义输出消息的目的地。 @SendTo用于自定义目标目的地或指定多个目的地。 @SendToUser用于将输出消息仅定向到与 Importing 消息关联的用户。参见User Destinations

您可以在同一方法上同时使用@SendTo@SendToUser,并且它们在类级别都受支持,在这种情况下,它们将作为类中方法的默认值。但是,请记住,任何方法级别的@SendTo@SendToUserComments 都将在类级别覆盖所有此类注解。

消息可以异步处理,并且@MessageMapping方法可以返回ListenableFutureCompletableFutureCompletionStage

请注意,@SendTo@SendToUser仅仅是一种便利,等同于使用SimpMessagingTemplate发送消息。如有必要,对于更高级的方案,@MessageMapping方法可以直接使用SimpMessagingTemplate。这可以代替返回值,也可以附加于返回值。参见Sending Messages

@SubscribeMapping

@SubscribeMapping@MessageMapping类似,但是将 Map 范围缩小到仅订阅消息。它支持与@MessageMapping相同的method arguments。但是,对于返回值,默认情况下,将消息直接发送到 Client 端(通过clientOutboundChannel,作为对订阅的响应),而不发送给代理(通过brokerChannel,作为对匹配的订阅的 Broadcast)。添加@SendTo@SendToUser会覆盖此行为,而是发送给代理。

什么时候有用?假定代理 Map 到/topic/queue,而应用程序控制器 Map 到/app。在此设置中,代理将所有要重复 Broadcast 的/topic/queue订阅存储起来,并且不需要应用程序参与。Client 端还可以预订某个/app目的地,并且控制器可以响应该预订而返回一个值,而无需 broker 参与,而无需再次存储或使用该预订(实际上是一次请求-答复交换)。一个用例是在启动时用初始数据填充 UI。

什么时候没有用?不要尝试将代理和控制器 Map 到相同的目标前缀,除非出于某种原因您希望两者都独立处理消息(包括订阅)。入站消息是并行处理的。无法保证 broker 还是控制者首先处理给定的消息。如果要在存储预订并准备好 Broadcast 时通知目标,则 Client 端应请求服务器是否支持收据(简单代理不支持)。例如,对于 Java STOMP client,您可以执行以下操作添加收据:

@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
    // Subscription ready...
});

服务器端的选项是brokerChannel上的to registerExecutorChannelInterceptor,并实现afterMessageHandled方法,该方法在处理包括订阅在内的消息之后被调用。

@MessageExceptionHandler

应用程序可以使用@MessageExceptionHandler个方法来处理@MessageMapping个方法中的异常。如果要访问异常实例,则可以在注解本身中声明异常,也可以通过方法参数声明异常。下面的示例通过方法参数声明异常:

@Controller
public class MyController {

    // ...

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

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

通常,@MessageExceptionHandler方法适用于声明它们的@Controller类(或类层次结构)。如果要使此类方法更全局地应用(跨控制器),则可以在标有@ControllerAdvice的类中声明它们。这与 Spring MVC 中可用的类似的支持相当。

4.4.7. 传送讯息

如果要从应用程序的任何部分向连接的 Client 端发送消息怎么办?任何应用程序组件都可以将消息发送到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)对其进行限定。

4.4.8. 简单 broker

内置的简单消息代理处理来自 Client 端的订阅请求,将其存储在内存中,并将消息 Broadcast 到具有匹配目标的已连接 Client 端。该代理支持类似路径的目标,包括对 Ant 样式目标模式的订阅。

Note

应用程序还可以使用点分隔(而不是斜杠分隔)目标。参见点作为分隔符

如果配置了任务计划程序,则简单代理支持STOMP heartbeats。为此,您可以声明自己的调度程序,也可以使用内部自动声明和使用的调度程序。以下示例显示如何声明自己的调度程序:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private TaskScheduler messageBrokerTaskScheduler;

    @Autowired
    public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
        this.messageBrokerTaskScheduler = taskScheduler;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/queue/", "/topic/")
                .setHeartbeatValue(new long[] {10000, 20000})
                .setTaskScheduler(this.messageBrokerTaskScheduler);

        // ...
    }
}

4.4.9. 外部 broker

简单代理非常适合入门,但是仅支持 STOMP 命令的一个子集(它不支持 ack,回执和其他一些功能),依赖于简单的消息发送循环,并且不适合于集群。或者,您可以升级应用程序以使用功能齐全的消息代理。

请参阅 STOMP 文档以获取您所选择的消息代理(例如RabbitMQActiveMQ等),安装代理,并在启用 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 会话将从代理收到的所有消息转发给 Client 端。本质上,它充当双向转发消息的“中继”。

Note

io.projectreactor.netty:reactor-nettyio.netty:netty-all依赖项添加到您的项目中以进行 TCP 连接 Management。

此外,应用程序组件(例如 HTTP 请求处理方法,业务服务等)还可以将消息发送到代理中继(如Sending Messages中所述),以将消息 Broadcast 到订阅的 WebSocketClient 端。

实际上,代理中继可实现健壮且可伸缩的消息 Broadcast。

4.4.10. 连接到 broker

STOMP 代理中继器维护与代理的单个“系统” TCP 连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置 STOMP 凭据(即 STOMP 帧loginpasscodeHeaders)。这在 XML 名称空间和 Java 配置中都公开为systemLoginsystemPasscode属性,默认值为guestguest

STOMP 代理中继还为每个连接的 WebSocketClient 端创建一个单独的 TCP 连接。您可以配置用于代表 Client 端创建的所有 TCP 连接的 STOMP 凭据。这在 XML 名称空间和 Java 配置中均以默认值guestand guestclientLogin andclientPasscode`属性公开。

Note

STOMP 代理中继始终在代表 Client 端转发给代理的每个CONNECT帧上设置loginpasscodeHeaders。因此,WebSocketClient 端无需设置这些 Headers。他们被忽略。如Authentication部分所述,WebSocketClient 端应改为依靠 HTTP 身份验证来保护 WebSocket 端点并构建 Client 端身份。

STOMP 代理中继还通过“系统” TCP 连接向消息代理发送和从消息代理接收心跳。您可以配置发送和接收心跳的间隔(默认情况下,每个间隔为 10 秒)。如果与代理的连接断开,则代理中继每 5 秒 continue 尝试重新连接,直到成功。

当与代理的“系统”连接丢失并重新构建时,任何 Spring bean 都可以实现ApplicationListener<BrokerAvailabilityEvent>来接收通知。例如,当没有活动的“系统”连接时,Broadcast 股票报价的股票报价服务可以停止尝试发送消息。

默认情况下,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() {
        return new ReactorNettyTcpClient<>(
                client -> client.addressSupplier(() -> ... ),
                new StompReactorNettyCodec());
    }
}

您还可以使用virtualHost属性配置 STOMP 代理中继。此属性的值设置为每个CONNECT帧的hostHeaders,并且很有用(例如,在构建 TCP 连接的实际主机不同于提供基于云的 STOMP 服务的主机的云环境中)。

4.4.11. 点作为分隔符

将消息路由到@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("erd")
public class RedController {

    @MessageMapping("blue.{green}")
    public void handleGreen(@DestinationVariable String green) {
        // ...
    }
}

Client 端现在可以向/app/red.blue.green123发送消息。

在前面的示例中,我们没有更改“代理中继”上的前缀,因为这些前缀完全取决于外部消息代理。有关您使用的代理的信息,请参见 STOMP 文档页面,以查看其对目标 Headers 支持的约定。

另一方面,“简单代理”确实依赖于已配置的PathMatcher,因此,如果切换分隔符,该更改也将应用于代理以及代理将目标从消息匹配到订阅中的模式的方式。

4.4.12. Authentication

每个通过 WebSocket 进行的 STOMP 消息传递会话均以 HTTP 请求开头。这可以是升级到 WebSockets 的请求(即 WebSocket 握手),或者在 SockJS 后备情况下,是一系列 SockJS HTTP 传输请求。

许多 Web 应用程序已经具有身份验证和授权来保护 HTTP 请求。通常,使用某种机制(例如,登录页面,HTTP 基本认证或其他方式)通过 Spring Security 对用户进行认证。经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一基于 cookie 的会话中的后续请求关联。

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求,通常已经有一个通过HttpServletRequest#getUserPrincipal()访问的经过身份验证的用户。 Spring 会自动将该用户与为其创建的 WebSocket 或 SockJS 会话相关联,并随后与通过该用户 Headers 在该会话上传输的所有 STOMP 消息相关联。

简而言之,典型的 Web 应用程序除了已经为安全起见,就不需要做任何事情。通过基于 cookie 的 HTTP 会话(然后与为该用户创建的 WebSocket 或 SockJS 会话相关联)维护的安全上下文在 HTTP 请求级别对用户进行身份验证,并导致在每个Message流上标记用户 Headers 通过应用程序。

请注意,STOMP 协议在CONNECT帧上确实具有loginpasscodeHeaders。这些最初是设计用于并且仍然需要的,例如,基于 TCP 的 STOMP。但是,对于默认情况下,对于基于 WebSocket 的 STOMP,Spring 会在 STOMP 协议级别忽略授权 Headers,并假定该用户已经在 HTTP 传输级别进行了身份验证,并期望 WebSocket 或 SockJS 会话包含经过身份验证的用户。

Note

Spring Security 提供了WebSocket 子协议授权,该WebSocket 子协议授权使用ChannelInterceptor来基于消息中的用户 Headers 授权消息。另外,Spring Session 提供了一个WebSocket integration,以确保当 WebSocket 会话仍处于活动状态时,用户 HTTP 会话不会过期。

4.4.13. 令牌认证

Spring Security OAuth支持基于令牌的安全性,包括 JSON Web 令牌(JWT)。您可以将其用作 Web 应用程序中的身份验证机制,包括上一节中所述的 WebSocket 交互中的 STOMP(即通过基于 cookie 的会话维护身份)。

同时,基于 cookie 的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中或在通常使用 Headers 进行身份验证的移动应用程序中)。

WebSocket 协议,RFC 6455“没有规定服务器在 WebSocket 握手期间可以对 Client 端进行身份验证的任何特定方式。”但是,实际上,浏览器 Client 端只能使用标准身份验证 Headers(即基本 HTTP 身份验证)或 cookie,而不能(例如)提供自定义 Headers。同样,SockJS JavaScriptClient 端也不提供通过 SockJS 传输请求发送 HTTPHeaders 的方法。参见sockjs-Client 端问题 196。相反,它确实允许发送可用于发送令牌的查询参数,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。

Note

前面的限制适用于基于浏览器的 Client 端,不适用于基于 Spring Java 的 STOMPClient 端,该 Client 端确实支持通过 WebSocket 和 SockJS 请求发送 Headers。

因此,希望避免使用 cookie 的应用程序可能没有在 HTTP 协议级别进行身份验证的任何好选择。他们可能更喜欢在 STOMP 消息传递协议级别使用 Headers 进行身份验证,而不是使用 cookie。这样做需要两个简单的步骤:

  • 使用 STOMPClient 端在连接时传递身份验证头。

  • 使用ChannelInterceptor处理身份验证 Headers。

下一个示例使用服务器端配置来注册自定义身份验证拦截器。请注意,拦截器仅需要认证并在 CONNECT Message上设置用户 Headers。 Spring 记录并保存经过身份验证的用户,并将其与同一会话上的后续 STOMP 消息相关联。以下示例显示了如何注册自定义身份验证拦截器:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @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 之前 Order 身份验证ChannelInterceptor config。最好通过在其自己的WebSocketMessageBrokerConfigurer实现中声明用@Order(Ordered.HIGHEST_PRECEDENCE + 99)标记的自定义拦截器来完成。

4.4.14. 用户目的地

应用程序可以发送针对特定用户的消息,Spring 的 STOMP 支持为此识别前缀/user/的目标。例如,Client 端可能订阅了/user/queue/position-updates目标。该目的地由UserDestinationMessageHandler处理,并转换为用户会话唯一的目的地(例如/queue/position-updates-user123)。这提供了订阅通用命名目的地的便利,同时确保与预订同一目的地的其他用户不发生冲突,以便每个用户都可以接收唯一的库存头寸更新。

在发送方,消息可以发送到诸如/user/{username}/queue/position-updates之类的目的地,而该目的地又被UserDestinationMessageHandler转换为一个或多个目的地,每个与用户相关联的会话都需要一个目的地。这使应用程序内的任何组件都可以发送针对特定用户的消息,而不必知道他们的姓名和通用目的地。Comments 和消息传递模板也支持此功能。

消息处理方法可以通过@SendToUserComments 将消息发送给与正在处理的消息相关联的用户(在类级别上也支持共享一个公共目的地),如以下示例所示:

@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;
    }
}

Note

尽管用户目的地通常暗指经过身份验证的用户,但这并不是严格要求的。不与已认证用户关联的 WebSocket 会话可以订阅用户目的地。在这种情况下,@SendToUserComments 的行为与broadcast=false完全相同(也就是说,仅针对发送正在处理的消息的会话)。

您可以从任何应用程序组件向用户目标发送消息,例如,注入由 Java 配置或 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());
    }
}

Note

将用户目标与外部消息代理一起使用时,应查看代理文档以了解如何 Management 非活动队列,以便在用户会话结束时,将删除所有唯一的用户队列。例如,当您使用诸如/exchange/amq.direct/position-updates之类的目标时,RabbitMQ 将创建自动删除队列。因此,在这种情况下,Client 端可以订阅/user/exchange/amq.direct/position-updates。同样,ActiveMQ 具有configuration options用于清除非活动目标。

在多应用程序服务器方案中,由于用户连接到其他服务器,因此用户目的地可能仍然无法解析。在这种情况下,可以将目标配置为 Broadcast 未解析的消息,以便其他服务器可以尝试。这可以通过 Java 配置中MessageBrokerRegistryuserDestinationBroadcast属性和 XML 中message-broker元素的user-destination-broadcast属性来完成。

4.4.15. 消息 Sequences

来自代理的消息将发布到clientOutboundChannel,然后从那里写入 WebSocket 会话。由于该通道由ThreadPoolExecutor支持,因此将在不同的线程中处理消息,并且 Client 端接收到的结果序列可能与发布的确切 Sequences 不匹配。

如果这是一个问题,请启用setPreservePublishOrder标志,如以下示例所示:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    protected void configureMessageBroker(MessageBrokerRegistry registry) {
        // ...
        registry.setPreservePublishOrder(true);
    }

}

下面的示例显示与前面的示例等效的 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 preserve-publish-order="true">
        <!-- ... -->
    </websocket:message-broker>

</beans>

设置该标志后,同一 Client 端会话中的消息将一次发布到clientOutboundChannel,因此可以保证发布 Sequences。请注意,这会产生很小的性能开销,因此,只有在需要时才应启用它。

4.4.16. Events

几个ApplicationContext事件已发布,可以通过实现 Spring 的ApplicationListener接口来接收:

  • BrokerAvailabilityEvent:指示代理何时可用或不可用。当“简单”代理在启动时立即可用并保持运行状态时,STOMP“代理中继”可能会失去与功能齐全的代理的连接(例如,如果代理重新启动)。代理中继具有重新连接逻辑,并在代理返回时重新构建与代理的“系统”连接。结果,只要状态从连接变为断开,反之亦然,就会发布此事件。使用SimpMessagingTemplate的组件应订阅此事件,并避免在代理不可用时避免发送消息。无论如何,他们应该准备在发送消息时处理MessageDeliveryException

  • SessionConnectEvent:在收到新的 STOMP CONNECT 来指示新的 Client 端会话开始时发布。该事件包含代表连接的消息,包括会话 ID,用户信息(如果有)和 Client 端发送的所有自定义 Headers。这对于跟踪 Client 端会话很有用。预订此事件的组件可以使用SimpMessageHeaderAccessorStompMessageHeaderAccessor包装包含的消息。

  • SessionConnectedEvent:在SessionConnectEvent之后不久,当代理已发送 STOMP CONNECTED 帧以响应 CONNECT 时发布。此时,可以认为 STOMP 会话已完全构建。

  • SessionSubscribeEvent:在收到新的 STOMP SUBSCRIBE 时发布。

  • SessionUnsubscribeEvent:在收到新的 STOMP UNSUBSCRIBE 时发布。

  • SessionDisconnectEvent:在 STOMP 会话结束时发布。 DISCONNECT 可能已经从 Client 端发送,或者它可能在 WebSocket 会话关闭时自动生成。在某些情况下,每个会话多次发布该事件。组件应与多个断开事件有关。

Note

当您使用功能齐全的代理时,如果代理暂时不可用,则 STOMP“代理中继”会自动重新连接“系统”连接。但是,Client 端连接不会自动重新连接。假设启用了心跳,则 Client 端通常会注意到代理在 10 秒内没有响应。Client 需要实现自己的重新连接逻辑。

4.4.17. Interception

Events提供 STOMP 连接生命周期的通知,但不提供每条 Client 端消息的通知。应用程序还可以注册ChannelInterceptor来拦截处理链中任何部分的任何消息。以下示例显示了如何拦截来自 Client 端的入站消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

定制ChannelInterceptor可以使用StompHeaderAccessorSimpMessageHeaderAccessor访问有关消息的信息,如以下示例所示:

public class MyChannelInterceptor implements ChannelInterceptor {

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

应用程序还可以实现ExecutorChannelInterceptor,它是ChannelInterceptor的子接口,在处理消息的线程中具有回调。对于发送到通道的每个消息,一次调用ChannelInterceptor时,ExecutorChannelInterceptor在订阅该通道消息的每个MessageHandler的线程中提供了钩子。

请注意,与前面所述的SesionDisconnectEvent一样,DISCONNECT 消息可以来自 Client 端,也可以在关闭 WebSocket 会话时自动生成。在某些情况下,对于每个会话,拦截器可能会多次拦截此消息。组件应与多个断开事件有关。

4.4.18. STOMPClient 端

Spring 提供了一个基于 WebSocket 的 STOMPClient 端和一个基于 TCP 的 STOMPClient 端。

首先,您可以创建和配置WebSocketStompClient,如以下示例所示:

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

在前面的示例中,您可以将_替换为SockJsClient,因为这也是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) {
        // ...
    }
}

构建会话后,可以发送任何有效负载,并使用配置的MessageConverter对其进行序列化,如以下示例所示:

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

您还可以订阅目的地。 subscribe方法需要用于订阅消息的处理程序,并返回Subscription句柄,您可以使用该句柄取消订阅。对于每个收到的消息,处理程序可以指定有效负载应反序列化的目标Object类型,如以下示例所示:

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

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

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

});

要启用 STOMP 心跳,您可以将WebSocketStompClient配置为TaskScheduler,并可以选择自定义心跳间隔(写不活动(导致发送心跳)需要 10 秒,读不活动(可以关闭连接)需要 10 秒)。

Note

当您使用WebSocketStompClient进行性能测试以模拟同一台计算机上的数千个 Client 端时,请考虑关闭心跳 signal,因为每个连接都会调度自己的心跳任务,并且并未针对同一台计算机上运行的大量 Client 端进行优化。

STOMP 协议还支持回执,在该回执中,Client 端必须添加receipt报头,服务器在处理完发送或订阅后将以 RECEIPT 帧响应。为了支持这一点,StompSession提供了setAutoReceipt(boolean),该setAutoReceipt(boolean)会在每个后续的 send 或 subscription 事件上添加receiptHeaders。或者,您也可以手动将收据 Headers 添加到StompHeaders。发送和订阅都返回Receiptable的实例,您可以使用该实例来注册接收成功和失败的回调。要使用此功能,您必须为 Client 端配置TaskScheduler以及收据过期之前的时间(默认为 15 秒)。

请注意,StompSessionHandler本身是StompFrameHandler,除了用于处理来自消息的异常的handleException回调和用于ConnectionLostException的传输级错误的handleTransportError之外,它还可以处理 ERROR 帧。

4.4.19. WebSocket 范围

每个 WebSocket 会话都有一个属性 Map。该 Map 作为 Headers 附加到入站 Client 端消息,可以通过控制器方法进行访问,如以下示例所示:

@Controller
public class MyController {

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

您可以在websocket范围内声明一个 SpringManagement 的 bean。您可以将 WebSocket 作用域的 bean 注入控制器和在clientInboundChannel上注册的任何通道拦截器中。这些通常是单例,并且比任何单独的 WebSocket 会话寿命更长。因此,您需要对作用域 WebSocket 的 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 范围的 bean 调用了所有 Spring 生命周期方法,如前面的示例所示。

4.4.20. Performance

关于性能,没有万灵药。影响它的因素很多,包括消息的大小和数量,应用程序方法是否执行需要阻止的工作以及外部因素(例如网络速度和其他问题)。本部分的目的是提供可用配置选项的概述,以及有关如何进行扩展的一些想法。

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

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

如果注解 方法中的消息处理主要是受 CPU 限制的,则clientInboundChannel的线程数应保持接近处理器数。如果他们所做的工作更多地受到 IO 限制,并且需要阻塞或 await 数据库或其他外部系统,则可能需要增加线程池大小。

Note

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

常见的混淆点是,配置核心池大小(例如 10)和最大池大小(例如 20)会导致线程池具有 10 到 20 个线程。实际上,如果将容量保留为其默认值 Integer.MAX_VALUE,则由于所有其他任务都已排队,因此线程池永远不会增加到超出核心池大小的范围。

请参阅ThreadPoolExecutor的 Javadoc,以了解这些属性如何工作并了解各种排队策略。

clientOutboundChannel方面,所有操作都与向 WebSocketClient 端发送消息有关。如果 Client 端在快速网络上,则线程数应保持接近可用处理器数。如果它们很慢或带宽很低,它们将花费更长的时间来消耗消息并给线程池增加负担。因此,必须增加线程池的大小。

尽管可以预测clientInboundChannel的工作量(毕竟,这是基于应用程序的工作),但是如何配置“ clientOutboundChannel”却比较困难,因为它基于应用程序无法控制的因素。因此,还有两个与消息发送有关的属性:sendTimeLimitsendBufferSizeLimit。您可以使用这些方法来配置发送消息到 Client 端时允许发送多长时间以及可以缓冲多少数据。

通常的想法是,在任何给定时间,只能使用单个线程将其发送给 Client 端。同时,所有其他消息都将被缓冲,您可以使用这些属性来决定允许发送消息花费多长时间以及在此期间可以缓冲多少数据。有关其他重要信息,请参见 XML 模式的 javadoc 和文档。

以下示例显示了可能的配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    // ...

}

下面的示例显示与前面的示例等效的 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>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

您还可以使用前面显示的 WebSocket 传输配置来配置传入 STOMP 消息的最大允许大小。从理论上讲,WebSocket 消息的大小几乎是无限的。实际上,WebSocket 服务器施加了限制,例如 Tomcat 8K 和 Jetty 64K。因此,STOMPClient 端(例如 JavaScript webstomp-client等)会在 16K 边界处拆分较大的 STOMP 消息,并将其作为多个 WebSocket 消息发送,这需要服务器进行缓冲和重新组装。

Spring 的 STOMP-over-WebSocket 支持可以做到这一点,因此应用程序可以为 STOMP 消息配置最大大小,而与 WebSocket 服务器特定的消息大小无关。请记住,如有必要,将自动调整 WebSocket 消息的大小,以确保它们最多可以承载 16K WebSocket 消息。

以下示例显示了一种可能的配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    // ...

}

下面的示例显示与前面的示例等效的 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>
        <websocket:transport message-size="131072" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

关于扩展的重要一点涉及使用多个应用程序实例。当前,您无法使用简单代理执行此操作。但是,当您使用功能齐全的代理(例如 RabbitMQ)时,每个应用程序实例都连接到代理,并且从一个应用程序实例 Broadcast 的消息可以通过代理 Broadcast 到通过任何其他应用程序实例连接的 WebSocketClient 端。

4.4.21. Monitoring

当您使用@EnableWebSocketMessageBroker<websocket:message-broker>时,关键基础架构组件会自动收集统计信息和计数器,这些统计信息和计数器可提供对应用程序内部状态的重要了解。该配置还声明了一个类型为WebSocketMessageBrokerStats的 bean,该 bean 在一个地方收集所有可用信息,并且默认情况下,每 30 分钟在INFO级别收集一次。该 bean 可以通过 Spring 的MBeanExporter导出到 JMX,以便在运行时查看(例如,通过 JDK 的jconsole)。以下列表总结了可用信息:

  • Client 端 WebSocket 会话

      • Current

      • 指示当前有多少个 Client 端会话,并且通过 WebSocket 与 HTTP 流和轮询 SockJS 会话进一步细分计数。

    • Total

      • 指示已构建的会话总数。
    • Abnormally Closed

        • Connect Failures

        • 已构建但在 60 秒内未收到任何消息后关闭的会话。这通常表示代理或网络问题。

      • 超过发送限制

        • 超过配置的发送超时或发送缓冲区限制后,会话将关闭,慢 Client 端可能会发生这种情况(请参阅上一节)。
      • Transport Errors

        • 传输错误(例如无法读取或写入 WebSocket 连接或 HTTP 请求或响应)后,会话关闭。
    • STOMP Frames

      • 已处理的 CONNECT,CONNECTED 和 DISCONNECT 帧的总数,指示在 STOMP 级别上连接了多少个 Client 端。请注意,当会话异常关闭或 Client 端未发送 DISCONNECT 帧而关闭时,DISCONNECT 计数可能会降低。
  • STOMPbroker 接力

      • TCP Connections

      • 指示与代理构建了代表 Client 端 WebSocket 会话的 TCP 连接数。这应该等于 Client 端 WebSocket 会话的数量 1 个用于从应用程序内部发送消息的附加共享“系统”连接。

    • STOMP Frames

      • 代表 Client 端转发到代理或从代理接收的 CONNECT,CONNECTED 和 DISCONNECT 帧总数。请注意,无论 Client 端 WebSocket 会话如何关闭,DISCONNECT 帧都会发送到代理。因此,较低的 DISCONNECT 帧计数表示代理正在主动关闭连接(可能是由于未及时到达的心跳,无效的 Importing 帧或其他问题)。
  • Client 入站通道

    • 来自支持clientInboundChannel的线程池的统计信息,可深入了解传入消息处理的运行状况。此处排队的任务表明该应用程序可能太慢而无法处理消息。如果存在 I/O 绑定的任务(例如,慢速的数据库查询,对第三方 REST API 的 HTTP 请求等),请考虑增加线程池的大小。
  • Client 出站通道

    • 来自支持clientOutboundChannel的线程池的统计信息,该统计信息可深入了解向 Client 端 Broadcast 消息的运行状况。此处排队的任务表明 Client 端太慢而无法使用消息。解决此问题的一种方法是增加线程池大小,以容纳并发慢速 Client 端的预期数量。另一个选择是减少发送超时和发送缓冲区大小限制(请参阅上一节)。
  • SockJS 任务计划程序

    • 来自 SockJS 任务调度程序的线程池的统计信息,用于发送心跳。请注意,在 STOMP 级别协商心跳时,将禁用 SockJS 心跳。

4.4.22. Testing

当您使用 Spring 的 STOMP-over-WebSocket 支持时,有两种主要的方法来测试应用程序。首先是编写服务器端测试以验证控制器的功能及其带注解 的消息处理方法。第二种是编写涉及运行 Client 端和服务器的完整的端到端测试。

两种方法不是互斥的。相反,每个人在整体测试策略中都有自己的位置。服务器端测试更加集中,更易于编写和维护。另一方面,端到端集成测试更完整,测试更多,但是编写和维护它们的工作也更多。

服务器端测试的最简单形式是编写控制器单元测试。但是,这还不够有用,因为控制器所做的很多事情都取决于其注解。纯单元测试根本无法测试。

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

  • 在 Spring TestContext 框架的帮助下加载实际的 Spring 配置,将clientInboundChannel注入为测试字段,并使用它发送要由控制器方法处理的消息。

  • 手动设置调用控制器(即SimpAnnotationMethodMessageHandler)并将控制器消息直接传递给它所需的最低 Spring 框架基础结构。

股票投资组合测试示例应用程序中演示了这两种设置方案。

第二种方法是创建端到端集成测试。为此,您需要以嵌入式模式运行 WebSocket 服务器并将其作为 WebSocketClient 端连接到该服务器,该 Client 端发送包含 STOMP 帧的 WebSocket 消息。 股票投资组合测试示例应用程序还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单 STOMPClient 端,演示了这种方法。

5.其他 Web 框架

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

Spring Framework 的核心价值主张之一是启用* choice *。在一般意义上,Spring 不会强迫您使用或购买任何特定的体系结构,技术或方法(尽管它肯定比其他建议更重要)。可以自由选择与开发人员及其开发团队最相关的架构,技术或方法,这在 Web 领域最明显,在 Web 领域,Spring 提供了自己的 Web 框架(Spring MVC),而与此同时,提供与许多流行的第三方 Web 框架的集成。

5.1. 通用配置

在深入研究每个受支持的 Web 框架的集成细节之前,让我们首先看一下并非针对任何一个 Web 框架的 Spring 配置。 (本节同样适用于 Spring 自己的 Web 框架 Spring MVC.)

Spring 的轻量级应用程序模型拥护的一个概念(需要一个更好的词)是分层体系结构的概念。请记住,在“经典”分层体系结构中,Web 层只是许多层之一。它充当服务器端应用程序的入口点之一,并且委派服务层中定义的服务对象(外观),以满足特定于业务(与表示技术无关)的用例。在 Spring 中,这些服务对象,任何其他特定于业务的对象,数据访问对象和其他对象都存在于不同的“业务上下文”中,其中不包含 Web 或表示层对象(表示对象,例如 Spring MVC 控制器,通常是在不同的“展示环境”中进行配置)。本节详细介绍如何配置包含应用程序中所有“ Business Bean”的 Spring 容器(WebApplicationContext)。

continue 讲细节,您要做的就是在 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

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils类是为了方便起见,因此您无需记住ServletContext属性的名称。如果WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE键下的对象不存在,则其getWebApplicationContext()方法返回null。最好不要使用getRequiredWebApplicationContext()方法,而不要冒在应用程序中使用NullPointerExceptions的风险。 ApplicationContext丢失时,此方法将引发异常。

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

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

5.2. JSF

JavaServer Faces(JSF)是 JCP 的基于组件的标准,事件驱动的 Web 用户界面框架。从 Java EE 5 开始,它是 Java EE 总括的正式组成部分。

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

Note

Spring Web Flow 2.0 通过其新构建的 Spring Faces 模块提供了丰富的 JSF 支持,既可用于以 JSF 为中心的用法(如本节所述),又可用于以 Spring 为中心的用法(在 Spring MVC 调度程序中使用 JSF 视图)。有关详情,请参见Spring Web Flow 网站

Spring 的 JSF 集成中的关键元素是 JSF ELResolver机制。

5.2.1. 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>

5.2.2. 使用 FacesContextUtils

将属性 Map 到faces-config.xml中的 bean 时,自定义VariableResolver效果很好,但是有时您可能需要显式地获取 bean。 FacesContextUtils类使此操作变得容易。它与WebApplicationContextUtils类似,除了它采用FacesContext参数而不是ServletContext参数。

以下示例显示了如何使用FacesContextUtils

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

5.3. Apache Struts 2.x

Struts由 Craig McClanahan 发明,是由 Apache Software Foundation 托管的开源项目。当时,它极大地简化了 JSP/Servlet 编程范例,并赢得了许多使用专有框架的开发人员的青睐。它简化了编程模型,它是开源的(因此像啤酒一样是免费的),并且它具有庞大的社区,这使该项目得以 Developing 并在 Java Web 开发人员中广受欢迎。

查看 Struts Spring Plugin,了解 Struts 随附的内置 Spring 集成。

5.4. 挂毯 5.x

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

尽管 Spring 拥有自己的强大的网页层,但是通过将 Tapestry 用于 Web 用户界面并将 Spring 容器用于底层,来构建企业 Java 应用程序具有许多独特的优势。

有关更多信息,请参见 Tapestry 的专用Spring 集成模块

5.5. 更多资源

以下链接提供了有关本章中描述的各种 Web 框架的更多资源。