这部分文档涵盖了对基于 Servlet API 并部署到 Servlet 容器的 Servlet 堆栈 Web 应用程序的支持。各个章节包括Spring MVCView TechnologiesCORS 支持WebSocket 支持。对于响应式堆栈 Web 应用程序,请参阅响应式堆栈上的 Web

1. Spring Web MVC

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

与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个响应式堆栈 Web 框架,其名称“Spring WebFlux”也是基于其源模块 ( spring-webflux)。本节介绍 Spring Web MVC。下一节将 介绍 Spring WebFlux。

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

1.1。DispatcherServlet

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

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

以下 Java 配置示例注册并初始化DispatcherServletServlet 容器自动检测到的 (请参阅Servlet 配置):

java
public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}
科特林
class MyWebApplicationInitializer : WebApplicationInitializer {

    override fun onStartup(servletContext: ServletContext) {

        // Load Spring web application configuration
        val context = AnnotationConfigWebApplicationContext()
        context.register(AppConfig::class.java)

        // Create and register the DispatcherServlet
        val servlet = DispatcherServlet(context)
        val registration = servletContext.addServlet("app", servlet)
        registration.setLoadOnStartup(1)
        registration.addMapping("/app/*")
    }
}
除了直接使用 ServletContext API,您还可以扩展 AbstractAnnotationConfigDispatcherServletInitializer和覆盖特定的方法(参见Context Hierarchy下的示例)。
对于编程用例, aGenericWebApplicationContext可以用作AnnotationConfigWebApplicationContext. 有关详细信息,请参阅 GenericWebApplicationContext javadoc。

以下web.xml配置示例注册并初始化DispatcherServlet

<web-app>

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

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

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>
Spring Boot 遵循不同的初始化顺序。Spring Boot 不是挂钩到 Servlet 容器的生命周期,而是使用 Spring 配置来引导自身和嵌入式 Servlet 容器。FilterServlet声明在 Spring 配置中被检测并注册到 Servlet 容器中。有关更多详细信息,请参阅 Spring Boot 文档

1.1.1。上下文层次结构

DispatcherServlet期望 a WebApplicationContext(a 的扩展名 ApplicationContext) 用于自己的配置。WebApplicationContext有一个 与它相关联的ServletContext和 的链接。Servlet它也绑定到ServletContext 这样的应用程序可以使用静态方法RequestContextUtils来查找 WebApplicationContext是否需要访问它。

对于许多应用程序,拥有一个WebApplicationContext简单且足够。也可以有一个上下文层次结构,其中一个根WebApplicationContext 在多个DispatcherServlet(或其他Servlet)实例之间共享,每个实例都有自己的子WebApplicationContext配置。 有关上下文层次结构功能的更多信息,请参阅附加功能。ApplicationContext

WebApplicationContext通常包含基础设施 bean,例如需要跨多个Servlet实例共享的数据存储库和业务服务。这些 bean 是有效继承的,并且可以在特定于 Servlet 的 child 中被覆盖(即重新声明)WebApplicationContext,它通常包含给定的本地 beans Servlet。下图显示了这种关系:

mvc 上下文层次结构

以下示例配置WebApplicationContext层次结构:

java
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/*" };
    }
}
科特林
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>> {
        return arrayOf(RootConfig::class.java)
    }

    override fun getServletConfigClasses(): Array<Class<*>> {
        return arrayOf(App1Config::class.java)
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/app1/*")
    }
}
如果不需要应用程序上下文层次结构,应用程序可以通过getRootConfigClasses()null从返回所有配置getServletConfigClasses()

以下示例显示了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>
如果不需要应用程序上下文层次结构,应用程序可以只配置“根”上下文并将contextConfigLocationServlet 参数留空。

1.1.2。特殊豆类

特殊 bean的DispatcherServlet代表来处理请求并呈现适当的响应。“特殊 bean”是Object指实现框架契约的 Spring 管理的实例。这些通常带有内置合同,但您可以自定义它们的属性并扩展或替换它们。

下表列出了由 检测到的特殊 bean DispatcherServlet

豆类 解释

HandlerMapping

将请求与用于预处理和后处理的拦截器列表一起映射到处理程序 。映射基于一些标准,其细节因HandlerMapping 实施而异。

两个主要HandlerMapping实现是RequestMappingHandlerMapping (支持带@RequestMapping注释的方法)和SimpleUrlHandlerMapping (维护向处理程序的 URI 路径模式的显式注册)。

HandlerAdapter

帮助DispatcherServlet调用映射到请求的处理程序,而不管实际调用处理程序的方式。例如,调用带注释的控制器需要解析注释。a 的主要目的HandlerAdapter是屏蔽DispatcherServlet这些细节。

HandlerExceptionResolver

解决异常的策略,可能将它们映射到处理程序、HTML 错误视图或其他目标。请参阅例外

ViewResolver

String将从处理程序返回的基于逻辑的视图名称解析View 为用于呈现响应的实际视图名称。请参阅查看分辨率查看技术

LocaleResolver, LocaleContextResolver

解析Locale客户正在使用的可能以及他们的时区,以便能够提供国际化视图。请参阅区域设置

ThemeResolver

解决您的 Web 应用程序可以使用的主题——例如,提供个性化的布局。请参阅主题

MultipartResolver

在一些多部分解析库的帮助下解析多部分请求(例如,浏览器表单文件上传)的抽象。请参阅多部分解析器

FlashMapManager

FlashMap存储和检索可用于将属性从一个请求传递到另一个请求的“输入”和“输出” ,通常通过重定向。请参阅Flash 属性

1.1.3。Web MVC 配置

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

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

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

1.1.4。小服务程序配置

在 Servlet 3.0+ 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方案或与web.xml文件结合使用。以下示例注册了 a DispatcherServlet

java
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("/");
    }
}
科特林
import org.springframework.web.WebApplicationInitializer

class MyWebApplicationInitializer : WebApplicationInitializer {

    override fun onStartup(container: ServletContext) {
        val appContext = XmlWebApplicationContext()
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")

        val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
        registration.setLoadOnStartup(1)
        registration.addMapping("/")
    }
}

WebApplicationInitializer是 Spring MVC 提供的一个接口,可确保检测到您的实现并自动用于初始化任何 Servlet 3 容器。WebApplicationInitializernamed 的抽象基类实现通过重写方法来指定 servlet 映射和配置位置,从而AbstractDispatcherServletInitializer更容易注册 。DispatcherServletDispatcherServlet

建议使用基于 Java 的 Spring 配置的应用程序,如以下示例所示:

java
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[] { "/" };
    }
}
科特林
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>>? {
        return null
    }

    override fun getServletConfigClasses(): Array<Class<*>>? {
        return arrayOf(MyWebConfig::class.java)
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/")
    }
}

如果使用基于 XML 的 Spring 配置,则应直接从 扩展 AbstractDispatcherServletInitializer,如以下示例所示:

java
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[] { "/" };
    }
}
科特林
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {

    override fun createRootApplicationContext(): WebApplicationContext? {
        return null
    }

    override fun createServletApplicationContext(): WebApplicationContext {
        return XmlWebApplicationContext().apply {
            setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
        }
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/")
    }
}

AbstractDispatcherServletInitializer还提供了一种方便的方法来添加Filter 实例并将它们自动映射到DispatcherServlet,如以下示例所示:

java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }
}
科特林
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {

    // ...

    override fun getServletFilters(): Array<Filter> {
        return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
    }
}

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

isAsyncSupportedprotected 方法提供了一个单一的AbstractDispatcherServletInitializer 位置来启用对DispatcherServlet映射到它的所有过滤器的异步支持。默认情况下,此标志设置为true

最后,如果需要进一步自定义DispatcherServlet自身,可以重写该createDispatcherServlet方法。

1.1.5。加工

DispatcherServlet进程请求如下:

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

  • 区域设置解析器绑定到请求,以让流程中的元素解析处理请求(呈现视图、准备数据等)时使用的区域设置。如果不需要区域设置解析,则不需要区域设置解析器。

  • 主题解析器与请求绑定,让视图等元素决定使用哪个主题。如果你不使用主题,你可以忽略它。

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

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

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

中声明的HandlerExceptionResolverbeanWebApplicationContext用于解决请求处理期间抛出的异常。这些异常解析器允许自定义处理异常的逻辑。有关更多详细信息,请参阅例外

对于 HTTP 缓存支持,处理程序可以使用 的checkNotModified方法WebRequest以及注释控制器的更多选项,如控制器的 HTTP 缓存中所述。

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

表 1. DispatcherServlet 初始化参数
范围 解释

contextClass

实现ConfigurableWebApplicationContext, 的类,由该 Servlet 实例化和本地配置。默认情况下,XmlWebApplicationContext使用。

contextConfigLocation

传递给上下文实例(由 指定contextClass)以指示可以在何处找到上下文的字符串。该字符串可能包含多个字符串(使用逗号作为分隔符)以支持多个上下文。在具有两次定义的 bean 的多个上下文位置的情况下,最新的位置优先。

namespace

的命名空间WebApplicationContext。默认为[servlet-name]-servlet.

throwExceptionIfNoHandlerFound

NoHandlerFoundException当没有为请求找到处理程序时是否抛出一个。然后可以用 a 捕获异常HandlerExceptionResolver(例如,通过使用 @ExceptionHandler控制器方法)并像处理任何其他异常一样。

默认情况下,它设置为false,在这种情况下,DispatcherServlet将响应状态设置为 404 (NOT_FOUND) 而不会引发异常。

请注意,如果还配置了默认 servlet 处理,则始终将未解析的请求转发到默认 servlet,并且永远不会引发 404。

1.1.6。路径匹配

Servlet API 公开完整的请求路径,requestURI并将其进一步细分为contextPathservletPath、 ,并且pathInfo其值根据 Servlet 的映射方式而有所不同。从这些输入中,Spring MVC 需要确定用于处理程序映射的查找路径,它是DispatcherServlet 自身映射中的路径,不包括contextPath和任何servletMapping前缀(如果存在)。

servletPath和被解码,这pathInfo使得它们无法直接与完整比较requestURI以导出lookupPath,这使得有必要对requestURI. 然而,这引入了它自己的问题,因为路径可能包含编码的保留字符,例如"/"";"在它们被解码后反过来会改变路径的结构,这也可能导致安全问题。此外,Servlet 容器可能会在servletPath不同程度上startsWithrequestURI.

这就是为什么最好避免依赖基于前缀的映射类型servletPath附带的。servletPath如果DispatcherServlet被映射为带有"/"或不带有前缀 with的默认 Servlet,"/*"并且 Servlet 容器是 4.0+,则 Spring MVC 能够检测 Servlet 映射类型并完全避免使用servletPathand pathInfo。在 3.1 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过在 MVC 配置中提供UrlPathHelperwith alwaysUseFullPath=truevia Path Matching来实现等效。

幸运的是,默认的 Servlet 映射"/"是一个不错的选择。但是,仍然存在一个问题,即requestURI需要对这些数据进行解码以使其能够与控制器映射进行比较。这也是不可取的,因为可能会解码改变路径结构的保留字符。如果这些字符不是预期的,那么您可以拒绝它们(如 Spring Security HTTP 防火墙),或者您可以配置 UrlPathHelperurlDecode=false控制器映射将需要与编码路径匹配,这可能并不总是很好。此外,有时 DispatcherServlet需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。

上述问题可以通过切换到5.3 或更高版本中可用 PathMatcher的解析来更全面地解决,请参阅模式比较。与需要解码的查找路径或编码的控制器映射不同,解析 后的匹配路径的解析表示称为,一次一个路径段。这允许单独解码和清理路径段值,而不会有改变路径结构的风险。Parsed还支持使用前缀映射,只要前缀保持简单并且没有任何需要编码的字符。PathPatternAntPathMatcherPathPatternRequestPathPathPatternservletPath

1.1.7。拦截

所有HandlerMapping实现都支持处理程序拦截器,当您想要将特定功能应用于某些请求时很有用 - 例如,检查主体。拦截器必须HandlerInterceptororg.springframework.web.servlet包中实现三种方法,这些方法应该提供足够的灵活性来进行各种预处理和后处理:

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

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

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

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

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

请注意,在和 before 中写入和提交响应的和方法postHandle不太有用。这意味着对响应进行任何更改都为时已晚,例如添加额外的标头。对于这种情况,您可以实现并将其声明为Controller Advice bean 或直接在 .@ResponseBodyResponseEntityHandlerAdapterpostHandleResponseBodyAdviceRequestMappingHandlerAdapter

1.1.8。例外

如果在请求映射期间发生异常或从请求处理程序(例如 a @Controller)抛出异常,则DispatcherServlet委托到HandlerExceptionResolver bean 链以解决异常并提供替代处理,这通常是错误响应。

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

表 2. HandlerExceptionResolver 实现
HandlerExceptionResolver 描述

SimpleMappingExceptionResolver

异常类名称和错误视图名称之间的映射。对于在浏览器应用程序中呈现错误页面很有用。

DefaultHandlerExceptionResolver

解决 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码。另请参阅替代ResponseEntityExceptionHandlerREST API 异常

ResponseStatusExceptionResolver

使用注解解决异常并根据@ResponseStatus注解中的值将它们映射到 HTTP 状态代码。

ExceptionHandlerExceptionResolver

通过调用类或 类@ExceptionHandler中的方法来解决异常。请参阅@ExceptionHandler 方法@Controller@ControllerAdvice

解析器链

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

的合约HandlerExceptionResolver指定它可以返回:

  • aModelAndView指向错误视图。

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

  • null如果异常仍未解决,则供后续解析器尝试,如果异常仍然存在,则允许它冒泡到 Servlet 容器。

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

容器错误页面

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

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

给定前面的示例,当出现异常或响应具有错误状态时,Servlet 容器会在容器内将 ERROR 分派到配置的 URL(例如,/error)。然后由 处理DispatcherServlet,可能将其映射到 a @Controller,这可以实现为返回带有模型的错误视图名称或呈现 JSON 响应,如以下示例所示:

java
@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;
    }
}
科特林
@RestController
class ErrorController {

    @RequestMapping(path = ["/error"])
    fun handle(request: HttpServletRequest): Map<String, Any> {
        val map = HashMap<String, Any>()
        map["status"] = request.getAttribute("javax.servlet.error.status_code")
        map["reason"] = request.getAttribute("javax.servlet.error.message")
        return map
    }
}
Servlet API 不提供在 Java 中创建错误页面映射的方法。但是,您可以同时使用 aWebApplicationInitializer和 minimum web.xml

1.1.9。查看分辨率

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

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

表 3. ViewResolver 实现
视图解析器 描述

AbstractCachingViewResolver

AbstractCachingViewResolver它们解析的缓存视图实例的子类。缓存提高了某些视图技术的性能。cache您可以通过将属性设置为 来关闭缓存false。此外,如果您必须在运行时刷新某个视图(例如,当修改 FreeMarker 模板时),您可以使用该removeFromCache(String viewName, Locale loc)方法。

UrlBasedViewResolver

ViewResolver无需显式映射定义即可将逻辑视图名称直接解析为 URL的接口的简单实现。如果您的逻辑名称以直接的方式与视图资源的名称匹配,则这是合适的,不需要任意映射。

InternalResourceViewResolver

UrlBasedViewResolver支持的方便子类InternalResourceView(实际上是 Servlet 和 JSP)和子类,例如JstlViewTilesView. 您可以使用setViewClass(..). 有关详细信息,请参阅UrlBasedViewResolver javadoc。

FreeMarkerViewResolver

UrlBasedViewResolver支持它们的方便子FreeMarkerView类和它们的自定义子类。

ContentNegotiatingViewResolver

ViewResolver根据请求文件名或Accept标题解析视图的接口的实现。请参阅内容协商

BeanNameViewResolver

ViewResolver将视图名称解释为当前应用程序上下文中的 bean 名称的接口的实现。这是一个非常灵活的变体,它允许基于不同的视图名称混合和匹配不同的视图类型。每个此类View都可以定义为一个 bean,例如在 XML 或配置类中。

处理

您可以通过声明多个解析器 bean 并在必要时通过设置order属性来指定排序来链接视图解析器。请记住,order 属性越高,视图解析器在链中的位置就越晚。

a 的约定ViewResolver指定它可以返回 null 以指示无法找到视图。但是,在 JSP 和 的情况下,确定 JSPInternalResourceViewResolver是否存在的唯一方法是通过 RequestDispatcher. 因此,您必须始终将 an 配置InternalResourceViewResolver 为在视图解析器的整体顺序中排在最后。

配置视图分辨率就像ViewResolver在 Spring 配置中添加 bean 一样简单。MVC 配置为视图解析器和添加无逻辑 视图控制器提供了一个专用的配置 API, 这对于没有控制器逻辑的 HTML 模板渲染很有用。

重定向

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

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

请注意,如果控制器方法使用 注释@ResponseStatus,则注释值优先于由 设置的响应状态RedirectView

转发

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

内容协商

ContentNegotiatingViewResolver 不解析视图本身,而是委托给其他视图解析器并选择类似于客户端请求的表示的视图。表示可以根据Accept标头或查询参数(例如,"/path?format=pdf")确定。

通过将请求媒体类型与与其相关联的媒体类型(也称为 )进行比较,ContentNegotiatingViewResolver选择适当的处理请求。列表中具有兼容的第一个将表示返回给客户端。如果链不能提供兼容的视图,则查询通过属性指定的视图列表。后一个选项适用于可以呈现当前资源的适当表示的单例,而不管逻辑视图名称如何。标 头可以包含通配符(例如),在这种情况下,其 is是兼容匹配。ViewContent-TypeViewViewResolversViewContent-TypeViewResolverDefaultViewsViewsAccepttext/*ViewContent-Typetext/xml

有关配置详细信息,请参阅MVC 配置下的查看解析器

1.1.10。语言环境

Spring 架构的大部分部分都支持国际化,就像 Spring Web MVC 框架一样。DispatcherServlet允许您使用客户端的语言环境自动解析消息。这是用LocaleResolver对象完成的。

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

除了自动区域设置解析之外,您还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的更多信息,请参阅拦截)以在特定情况下更改区域设置(例如,基于请求中的参数)。

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

时区

除了获取客户端的语言环境之外,了解其时区通常也很有用。该LocaleContextResolver接口提供了一个扩展LocaleResolver,允许解析器提供更丰富的LocaleContext,其中可能包括时区信息。

当可用时,TimeZone可以使用该 RequestContext.getTimeZone()方法获得用户的。任何日期/时间Converter以及Formatter向 Spring 注册的对象 都会自动使用时区信息ConversionService

标头解析器

此语言环境解析器检查accept-language客户端(例如,Web 浏览器)发送的请求中的标头。通常,此标头字段包含客户端操作系统的区域设置。请注意,此解析程序不支持时区信息。

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

会话解析器

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

请注意,与外部会话管理机制没有直接关系,例如 Spring Session 项目。这会根据当前SessionLocaleResolver的 评估和修改相应的HttpSession属性HttpServletRequest

语言环境拦截器

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

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

定义主题

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

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

styleSheet=/themes/cool/style.css
背景=/themes/cool/img/coolBg.jpg

属性的键是从视图代码中引用主题元素的名称。对于 JSP,您通常使用与spring:theme标记非常相似的自定义标记来执行此操作spring:message。以下 JSP 片段使用前面示例中定义的主题来自定义外观:

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

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

解决主题

定义主题后,如前一节所述,您决定使用哪个主题。DispatcherServlet查找名为的 bean 以找出themeResolverThemeResolver使用的实现。主题解析器的工作方式与LocaleResolver. 它检测用于特定请求的主题,还可以更改请求的主题。下表描述了 Spring 提供的主题解析器:

表 5. ThemeResolver 实现
班级 描述

FixedThemeResolver

选择使用defaultThemeName属性设置的固定主题。

SessionThemeResolver

主题在用户的 HTTP 会话中维护。它只需要为每个会话设置一次,但不会在会话之间保留。

CookieThemeResolver

选定的主题存储在客户端的 cookie 中。

Spring 还提供了一个ThemeChangeInterceptor允许主题在每个请求上更改的简单请求参数。

1.1.12。多部分解析器

MultipartResolverorg.springframework.web.multipart包中是一种用于解析多部分请求(包括文件上传)的策略。有一种基于Commons FileUpload的实现,另一种基于 Servlet 3.0 多部分请求解析。

要启用多部分处理,您需要MultipartResolverDispatcherServletSpring 配置中声明一个名称为multipartResolver. 检测到它并将其DispatcherServlet应用于传入请求。当multipart/form-data接收到内容类型为 的 POST 时,解析器解析内容并将当前包装HttpServletRequest为 aMultipartHttpServletRequest以提供对已解析文件的访问,并将部分公开为请求参数。

阿帕奇公地FileUpload

要使用 Apache Commons FileUpload,您可以配置 CommonsMultipartResolver一个名称为multipartResolver. 您还需要将commons-fileuploadjar 作为类路径的依赖项。

此解析器变体委托给应用程序中的本地库,提供跨 Servlet 容器的最大可移植性。作为替代方案,考虑通过容器自己的解析器进行标准 Servlet 多部分解析,如下所述。

Commons FileUpload 传统上仅适用于 POST 请求,但接受任何 multipart/内容类型。有关详细信息和配置选项,请参阅 CommonsMultipartResolver javadoc。

小服务程序 3.0

Servlet 3.0 多部分解析需要通过 Servlet 容器配置来开启。为此:

  • 在Java 中,设置一个MultipartConfigElement关于Servlet 的注册。

  • 在中,向 servlet 声明web.xml添加一个部分。"<multipart-config>"

下面的例子展示了如何MultipartConfigElement在 Servlet 注册上设置一个:

java
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // ...

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {

        // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
    }

}
科特林
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    // ...

    override fun customizeRegistration(registration: ServletRegistration.Dynamic) {

        // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(MultipartConfigElement("/tmp"))
    }

}

一旦 Servlet 3.0 配置到位,您可以添加 StandardServletMultipartResolver一个名称为multipartResolver.

此解析器变体按原样使用您的 Servlet 容器的多部分解析器,可能会将应用程序暴露给容器实现差异。默认情况下,它将尝试multipart/使用任何 HTTP 方法解析任何内容类型,但这可能不支持所有 Servlet 容器。有关详细信息和配置选项,请参阅 StandardServletMultipartResolver javadoc。

1.1.13。日志记录

Spring MVC 中的 DEBUG 级日志记录设计为紧凑、最小且人性化。它侧重于反复有用的高价值信息,而不是仅在调试特定问题时才有用的信息。

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

好的日志记录来自于使用日志的经验。如果您发现任何不符合既定目标的内容,请告知我们。

敏感数据

DEBUG 和 TRACE 日志记录可能会记录敏感信息。这就是为什么请求参数和标头在默认情况下被屏蔽,并且必须通过enableLoggingRequestDetailson 属性显式启用它们的完整登录DispatcherServlet

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

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(ServletRegistration.Dynamic registration) {
        registration.setInitParameter("enableLoggingRequestDetails", "true");
    }

}
科特林
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>>? {
        return ...
    }

    override fun getServletConfigClasses(): Array<Class<*>>? {
        return ...
    }

    override fun getServletMappings(): Array<String> {
        return ...
    }

    override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
        registration.setInitParameter("enableLoggingRequestDetails", "true")
    }
}

1.2. 过滤器

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

1.2.1。表格数据

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

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

1.2.2。转发的标头

当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化,这使得从客户端的角度创建指向正确主机、端口和方案的链接成为一项挑战。

RFC 7239定义了Forwarded代理可以用来提供有关原始请求的信息的 HTTP 标头。还有其他非标准标题,包括X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, 和X-Forwarded-Prefix.

ForwardedHeaderFilter是一个 Servlet 过滤器,它修改请求以便 a) 根据Forwarded标头更改主机、端口和方案,以及 b) 删除这些标头以消除进一步的影响。过滤器依赖于包装请求,因此它必须在其他过滤器之前排序,例如RequestContextFilter,它应该与修改后的请求一起使用,而不是原始请求。

转发的标头存在安全考虑,因为应用程序无法知道标头是由代理(如预期的那样)还是由恶意客户端添加的。这就是为什么应该配置信任边界的代理以删除Forwarded 来自外部的不受信任的标头。您还可以配置ForwardedHeaderFilter with removeOnly=true,在这种情况下,它会删除但不使用标题。

为了支持异步请求和错误分派,这个过滤器应该映射到DispatcherType.ASYNCDispatcherType.ERROR。如果使用 Spring Framework AbstractAnnotationConfigDispatcherServletInitializer (请参阅Servlet 配置),所有过滤器都会自动为所有调度类型注册。但是,如果通过web.xmlSpring Boot 或在 Spring Boot 中 注册过滤器, FilterRegistrationBean请务必包含.DispatcherType.ASYNCDispatcherType.ERRORDispatcherType.REQUEST

1.2.3。浅层ETag

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

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

此过滤器有一个writeWeakETag参数,用于配置过滤器以编写类似于以下内容的弱 ETag:(如RFC 7232 第 2.3 节W/"02a2d595e6ed9a0b24f027f2b63b134d6"中所定义 )。

为了支持异步请求,必须映射此过滤器,DispatcherType.ASYNC以便过滤器可以延迟并成功生成 ETag 到最后一个异步调度的末尾。如果使用 Spring Framework AbstractAnnotationConfigDispatcherServletInitializer(请参阅Servlet 配置),所有过滤器都会自动为所有调度类型注册。但是,如果通过web.xmlSpring Boot 或在 Spring Boot 中注册过滤器,FilterRegistrationBean请务必包含 DispatcherType.ASYNC.

1.2.4。CORS

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

有关更多详细信息,请参阅有关CORSCORS 过滤器的部分。

1.3. 带注释的控制器

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

java
@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}
科特林
import org.springframework.ui.set

@Controller
class HelloController {

    @GetMapping("/hello")
    fun handle(model: Model): String {
        model["message"] = "Hello World!"
        return "index"
    }
}

在前面的示例中,该方法接受 aModel并将视图名称作为 a 返回String,但还有许多其他选项存在,本章稍后将对其进行解释。

spring.io 上的指南和教程使用本节中描述的基于注释的编程模型。

1.3.1。宣言

您可以使用 Servlet 中的标准 Spring bean 定义来定义控制器 bean WebApplicationContext@Controller原型允许自动检测,与 Spring 对检测@Component类路径中的类并为它们自动注册 bean 定义的一般支持保持一致。它还充当带注释类的原型,表明其作为 Web 组件的角色。

要启用此类 bean 的自动检测@Controller,您可以将组件扫描添加到 Java 配置中,如以下示例所示:

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

    // ...
}
科特林
@Configuration
@ComponentScan("org.example.web")
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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.example.web"/>

    <!-- ... -->

</beans>

@RestController是一个组合注解,它本身带有元注解,@Controller@ResponseBody指示一个控制器,其每个方法都继承类型级别的@ResponseBody注解,因此,直接写入响应正文而不是视图分辨率并使用 HTML 模板进行渲染。

AOP 代理

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

1.3.2. 请求映射

您可以使用@RequestMapping注释将请求映射到控制器方法。它有各种属性来匹配 URL、HTTP 方法、请求参数、标头和媒体类型。您可以在类级别使用它来表达共享映射,或者在方法级别使用它来缩小到特定的端点映射。

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

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

快捷方式是提供的自定义注释,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法而不是 using @RequestMapping,默认情况下,它匹配所有 HTTP 方法。@RequestMapping在类级别仍然需要A来表示共享映射。

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

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

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

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
科特林
@RestController
@RequestMapping("/persons")
class PersonController {

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

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}
URI 模式

@RequestMapping可以使用 URL 模式映射方法。有两种选择:

  • PathPattern — 与 URL 路径匹配的预解析模式也预解析为 PathContainer. 该解决方案专为 Web 使用而设计,可有效处理编码和路径参数,并有效匹配。

  • AntPathMatcher — 将字符串模式与字符串路径匹配。这是在 Spring 配置中用于选择类路径、文件系统和其他位置上的资源的原始解决方案。它的效率较低,并且字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战。

PathPattern是 Web 应用程序的推荐解决方案,它是 Spring WebFlux 中的唯一选择。在 5.3 版本之前,AntPathMatcher它是 Spring MVC 中的唯一选择,并且仍然是默认设置。但是PathPattern可以在 MVC 配置中启用。

PathPattern支持与AntPathMatcher. 此外,它还支持捕获模式,例如{*spring},用于匹配路径末端的 0 个或多个路径段。PathPattern还限制了**for 匹配多个路径段的使用,因此它只允许在模式的末尾。在为给定请求选择最佳匹配模式时,这消除了许多模棱两可的情况。有关完整的模式语法,请参阅 PathPatternAntPathMatcher

一些示例模式:

  • "/resources/ima?e.png"- 匹配路径段中的一个字符

  • "/resources/*.png"- 匹配路径段中的零个或多个字符

  • "/resources/**"- 匹配多个路径段

  • "/projects/{project}/versions"- 匹配路径段并将其捕获为变量

  • "/projects/{project:[a-z]+}/versions"- 使用正则表达式匹配和捕获变量

捕获的 URI 变量可以使用@PathVariable. 例如:

java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
科特林
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
    // ...
}

您可以在类和方法级别声明 URI 变量,如以下示例所示:

java
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

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

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

URI 变量会自动转换为适当的类型,或者TypeMismatchException 被引发。默认支持简单类型(intlongDate等),您可以注册对任何其他数据类型的支持。请参阅类型转换DataBinder.

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

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

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

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

模式比较

当多个模式匹配一​​个 URL 时,必须选择最佳匹配。这取决于是否PathPattern启用 parsed 的使用,通过以下方式之一完成:

两者都有助于对更具体的模式进行排序。如果模式具有较少的 URI 变量(计为 1)、单通配符(计为 1)和双通配符(计为 2),则它的特定性较低。给定相同的分数,选择较长的模式。给定相同的分数和长度,选择具有比通配符更多的 URI 变量的模式。

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

有关完整的详细信息,请按照上述指向模式比较器的链接。

后缀匹配

从 5.3 开始,默认情况下 Spring MVC 不再执行.*后缀模式匹配,其中映射到的控制器/person也隐式映射到 /person.*. 因此,路径扩展不再用于解释响应的请求内容类型——例如,、、/person.pdf等等/person.xml

当浏览器用来发送Accept难以一致解释的标头时,以这种方式使用文件扩展名是必要的。目前,这不再是必需品,使用Accept标头应该是首选。

随着时间的推移,文件扩展名的使用已被证明存在多种问题。当使用 URI 变量、路径参数和 URI 编码覆盖时,它可能会导致歧义。关于基于 URL 的授权和安全性的推理(更多详细信息请参见下一节)也变得更加困难。

要在 5.3 之前的版本中完全禁用路径扩展,请设置以下内容:

有一种方法来请求内容类型而不是通过"Accept"标头仍然很有用,例如在浏览器中键入 URL 时。路径扩展的安全替代方法是使用查询参数策略。如果您必须使用文件扩展名,请考虑通过ContentNegotiationConfigurermediaTypes的属性 将它们限制为显式注册的扩展名列表。

后缀匹配和 RFD

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

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

为了防止 RFD 攻击,在渲染响应正文之前,Spring MVC 添加了一个 Content-Disposition:inline;filename=f.txt标头来建议一个固定且安全的下载文件。仅当 URL 路径包含既不安全也不明确注册用于内容协商的文件扩展名时,才会执行此操作。但是,当直接在浏览器中输入 URL 时,它可能会产生副作用。

默认情况下,许多常见的路径扩展都是安全的。具有自定义 HttpMessageConverter实现的应用程序可以显式注册文件扩展名以进行内容协商,以避免Content-Disposition为这些扩展名添加标头。请参阅内容类型

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

耗材类型

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

java
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
    // ...
}
1 使用consumes属性按内容类型缩小映射范围。
科特林
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
    // ...
}
1 使用consumes属性按内容类型缩小映射范围。

consumes属性还支持否定表达式——例如,!text/plain表示除text/plain.

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

MediaType为常用媒体类型提供常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
可生产的媒体类型

您可以根据Accept请求标头和控制器方法生成的内容类型列表来缩小请求映射,如以下示例所示:

java
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
1 使用produces属性按内容类型缩小映射范围。
科特林
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
    // ...
}
1 使用produces属性按内容类型缩小映射范围。

媒体类型可以指定一个字符集。支持否定表达式——例如, !text/plain表示除“text/plain”之外的任何内容类型。

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

MediaType为常用媒体类型提供常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
参数、标题

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

java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 测试是否myParam等于myValue
科特林
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
1 测试是否myParam等于myValue

您也可以将其用于请求标头条件,如以下示例所示:

java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 测试是否myHeader等于myValue
科特林
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
您可以将Content-TypeandAccept与 headers 条件匹配,但最好使用 消费者生产者
HTTP 头,选项

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

@GetMapping(and @RequestMapping(method=HttpMethod.GET)) 隐式映射到并支持 HTTP HEAD。一个 HTTP HEAD 请求的处理就像它是 HTTP GET 一样,除了不是写入正文,而是计算字节数并设置Content-Length 标头。

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

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

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

自定义注释

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

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

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

显式注册

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

java
@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 为控制器注入目标处理程序和处理程序映射。
2 准备请求映射元数据。
3 获取处理程序方法。
4 添加注册。
科特林
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
        val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
        mapping.registerMapping(info, handler, method) (4)
    }
}
1 为控制器注入目标处理程序和处理程序映射。
2 准备请求映射元数据。
3 获取处理程序方法。
4 添加注册。

1.3.3. 处理程序方法

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

方法参数

下表描述了支持的控制器方法参数。任何参数都不支持反应类型。

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

控制器方法参数 描述

WebRequest,NativeWebRequest

对请求参数以及请求和会话属性的通用访问,无需直接使用 Servlet API。

javax.servlet.ServletRequest,javax.servlet.ServletResponse

选择任何特定的请求或响应类型,例如ServletRequest,HttpServletRequest或 Spring 的MultipartRequest, MultipartHttpServletRequest

javax.servlet.http.HttpSession

强制会话的存在。因此,这样的论点永远不会null。请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将 RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为。true

javax.servlet.http.PushBuilder

用于编程 HTTP/2 资源推送的 Servlet 4.0 推送构建器 API。请注意,根据 Servlet 规范,PushBuilder如果客户端不支持该 HTTP/2 功能,则注入的实例可以为空。

java.security.Principal

Principal当前经过身份验证的用户——如果已知,可能是特定的实现类。

请注意,此参数不会被急切地解析,如果它被注释是为了允许自定义解析器在通过HttpServletRequest#getUserPrincipal. 例如,Spring Security实现Authentication并且Principal将 通过.HttpServletRequest#getUserPrincipal@AuthenticationPrincipalAuthentication#getPrincipal

HttpMethod

请求的 HTTP 方法。

java.util.Locale

当前请求区域设置,由最具体的LocaleResolver可用区域(实际上是配置的LocaleResolveror LocaleContextResolver)确定。

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

@MatrixVariable

用于访问 URI 路径段中的名称-值对。请参阅矩阵变量

@RequestParam

用于访问 Servlet 请求参数,包括多部分文件。参数值被转换为声明的方法参数类型。参见@RequestParamMultipart 。

请注意,@RequestParam对于简单的参数值,使用 of 是可选的。请参阅本表末尾的“任何其他参数”。

@RequestHeader

用于访问请求标头。标头值被转换为声明的方法参数类型。见@RequestHeader

@CookieValue

用于访问 cookie。Cookies 值被转换为声明的方法参数类型。见@CookieValue

@RequestBody

用于访问 HTTP 请求正文。使用实现将正文内容转换为声明的方法参数类型HttpMessageConverter。见@RequestBody

HttpEntity<B>

用于访问请求标头和正文。正文用HttpMessageConverter. 请参阅HttpEntity

@RequestPart

要访问请求中的某个部分,multipart/form-data请使用HttpMessageConverter. 请参阅多部分

java.util.Map, org.springframework.ui.Model,org.springframework.ui.ModelMap

用于访问 HTML 控制器中使用的模型,并作为视图呈现的一部分暴露给模板。

RedirectAttributes

指定在重定向的情况下使用的属性(即附加到查询字符串)和闪存属性,以便在重定向后的请求之前临时存储。请参阅重定向属性Flash 属性

@ModelAttribute

用于访问模型中的现有属性(如果不存在则实例化)并应用数据绑定和验证。参见@ModelAttribute以及 模型DataBinder

请注意,使用@ModelAttribute是可选的(例如,设置其属性)。请参阅本表末尾的“任何其他参数”。

Errors,BindingResult

用于访问来自命令对象(即@ModelAttribute参数)的验证和数据绑定的错误或来自一个@RequestBody或多个 @RequestPart参数的验证的错误。您必须在经过验证的方法参数之后立即声明一个ErrorsBindingResult参数。

SessionStatus+ 班级级别@SessionAttributes

用于标记表单处理完成,这会触发对通过类级别@SessionAttributes注释声明的会话属性的清理。有关 @SessionAttributes更多详细信息,请参阅。

UriComponentsBuilder

用于准备与当前请求的主机、端口、方案、上下文路径和 servlet 映射的文字部分相关的 URL。请参阅URI 链接

@SessionAttribute

对于访问任何会话属性,与作为类级@SessionAttributes声明的结果存储在会话中的模型属性相反。有关 @SessionAttribute更多详细信息,请参阅。

@RequestAttribute

用于访问请求属性。有关@RequestAttribute更多详细信息,请参阅。

任何其他论点

如果方法参数与此表中的任何早期值都不匹配并且它是简单类型(由 BeanUtils#isSimpleProperty确定,则将其解析为@RequestParam. 否则,将其解析为@ModelAttribute.

返回值

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

控制器方法返回值 描述

@ResponseBody

返回值通过HttpMessageConverter实现转换并写入响应。见@ResponseBody

HttpEntity<B>,ResponseEntity<B>

指定完整响应(包括 HTTP 标头和正文)的返回值将通过HttpMessageConverter实现进行转换并写入响应。请参阅ResponseEntity

HttpHeaders

用于返回带有标题但没有正文的响应。

String

与实现一起解析并与隐式模型一起使用的视图名称ViewResolver- 通过命令对象和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数以编程方式丰富模型(请参阅显式注册)。

View

View与隐式模型一起用于渲染的实例——通过命令对象和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数以编程方式丰富模型(请参阅显式注册)。

java.util.Map,org.springframework.ui.Model

要添加到隐式模型的属性,视图名称通过RequestToViewNameTranslator.

@ModelAttribute

要添加到模型的属性,视图名称通过RequestToViewNameTranslator.

请注意,这@ModelAttribute是可选的。请参阅此表末尾的“任何其他返回值”。

ModelAndView目的

要使用的视图和模型属性,以及可选的响应状态。

void

如果具有void返回类型(或null返回值)的方法还具有ServletResponseOutputStream参数或@ResponseStatus注释,则认为该方法已完全处理了响应。ETag如果控制器进行了肯定或lastModified时间戳检查,情况也是如此 (有关详细信息,请参阅控制器)。

如果以上都不成立,则void返回类型还可以指示 REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。

DeferredResult<V>

从任何线程异步生成任何上述返回值——例如,作为某些事件或回调的结果。请参阅异步请求DeferredResult.

Callable<V>

在 Spring MVC 管理的线程中异步生成上述任何返回值。请参阅异步请求Callable.

ListenableFuture<V>, java.util.concurrent.CompletionStage<V>, java.util.concurrent.CompletableFuture<V>

替代DeferredResult, 为方便起见(例如,当底层服务返回其中之一时)。

ResponseBodyEmitter,SseEmitter

异步发出对象流,以通过 HttpMessageConverter实现写​​入响应。也支持作为 a 的主体ResponseEntity。请参阅异步请求HTTP 流

StreamingResponseBody

OutputStream异步写入响应。也支持作为 a 的主体 ResponseEntity。请参阅异步请求HTTP 流

响应式类型——Reactor、RxJava 或其他通过ReactiveAdapterRegistry

替代将DeferredResult多值流(例如Flux, Observable)收集到List.

对于流式场景(例如 , text/event-streamapplication/json+stream, 使用SseEmitterResponseBodyEmitter代替,其中ServletOutputStream 阻塞 I/O 在 Spring MVC 管理的线程上执行,并且在每次写入完成时应用背压。

请参阅异步请求反应类型

任何其他返回值

任何与此表中的任何早期值都不匹配的返回值,并且是 aString或被void视为视图名称(通过 RequestToViewNameTranslator应用默认视图名称选择),前提是它不是由 BeanUtils#isSimpleProperty确定的简单类型。简单类型的值仍未解析。

类型转换

如果参数被声明String为非 . @RequestParam_ @RequestHeader_@PathVariable@MatrixVariable@CookieValueString

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

类型转换中的一个实际问题是空字符串源值的处理。如果此类值是null类型转换的结果,则将其视为缺失。这可能是 、 和其他目标类型的Long情况UUID。如果要允许null 注入,请在参数注释上使用required标志,或将参数声明为@Nullable.

从 5.3 开始,即使在类型转换之后也将强制执行非空参数。如果您的处理程序方法也打算接受空值,请将您的参数声明为@Nullable 或将其标记为required=false相应@RequestParam的 等注释。这是 5.3 升级中遇到的回归问题的最佳实践和推荐解决方案。

或者,您可以MissingPathVariableException 在 required 的情况下专门处理例如结果@PathVariable。转换后的空值将被视为空原始值,因此Missing…​Exception将抛出相应的变体。

矩阵变量

RFC 3986讨论了路径段中的名称-值对。在 Spring MVC 中,我们根据 Tim Berners-Lee 的“旧帖子”将它们称为“矩阵变量” ,但它们也可以称为 URI 路径参数。

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

如果 URL 预期包含矩阵变量,则控制器方法的请求映射必须使用 URI 变量来屏蔽该变量内容,并确保可以成功匹配请求,而与矩阵变量顺序和存在无关。以下示例使用矩阵变量:

java
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}
科特林
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

    // petId == 42
    // q == 11
}

鉴于所有路径段都可能包含矩阵变量,您有时可能需要消除矩阵变量应该在哪个路径变量中的歧义。以下示例显示了如何执行此操作:

java
// 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 /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

    // q1 == 11
    // q2 == 22
}

矩阵变量可以定义为可选并指定默认值,如以下示例所示:

java
// GET /pets/42

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

    // q == 1
}
科特林
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}

要获取所有矩阵变量,您可以使用 a MultiValueMap,如以下示例所示:

java
// 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]
}
科特林
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable matrixVars: MultiValueMap<String, String>,
        @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}

请注意,您需要启用矩阵变量的使用。在 MVC Java 配置中,需要通过 Path MatchingUrlPathHelper设置一个with 。在 MVC XML 命名空间中,您可以设置 .removeSemicolonContent=false<mvc:annotation-driven enable-matrix-variables="true"/>

@RequestParam

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

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

java
@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.
科特林
import org.springframework.ui.set

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

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = this.clinic.loadPet(petId);
        model["pet"] = pet
        return "petForm"
    }

    // ...

}
1 用于@RequestParam绑定petId.

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

如果目标方法参数类型不是 ,则自动应用类型转换 String。请参阅类型转换

将参数类型声明为数组或列表允许解析相同参数名称的多个参数值。

@RequestParam注释被声明为 aMap<String, String>MultiValueMap<String, String>,而注释中没有指定参数名称时,映射将填充每个给定参数名称的请求参数值。

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

@RequestHeader

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

考虑以下带有标头的请求:

主机本地主机:8080
接受 text/html,application/xhtml+xml,application/xml;q=0.9
接受语言 fr,en-gb;q=0.7,en;q=0.3
接受编码 gzip,deflate
接受字符集 ISO-8859-1,utf-8;q=0.7,*;q=0.7
保活300

以下示例获取Accept-EncodingKeep-Alive标头的值:

java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1 获取Accept-Encoding标头的值。
2 获取Keep-Alive标头的值。
科特林
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
1 获取Accept-Encoding标头的值。
2 获取Keep-Alive标头的值。

如果目标方法参数类型不是 String,则自动应用类型转换。请参阅类型转换

在、 或参数@RequestHeader上使用注释时,映射将填充所有标头值。Map<String, String>MultiValueMap<String, String>HttpHeaders

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

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

考虑一个带有以下 cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

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

java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
1 获取JSESSIONIDcookie 的值。
科特林
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
1 获取JSESSIONIDcookie 的值。

如果目标方法参数类型不是String,则自动应用类型转换。请参阅类型转换

@ModelAttribute

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

java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
    // method logic...
}
科特林
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String {
    // method logic...
}

上面的Pet实例来源于以下方式之一:

  • 从可能由 @ModelAttribute 方法添加的模型中检索。

  • 如果模型属性列在类级别@SessionAttributes注释中,则从 HTTP 会话中检索。

  • 通过Converter模型属性名称匹配请求值的名称(例如路径变量或请求参数)获得(参见下一个示例)。

  • 使用其默认构造函数实例化。

  • 通过带有与 Servlet 请求参数匹配的参数的“主构造函数”实例化。参数名称通过 JavaBean @ConstructorProperties或字节码中运行时保留的参数名称确定。

使用@ModelAttribute 方法来提供它或依赖框架来创建模型属性的一种替代方法是 Converter<String, T>提供实例。这适用于模型属性名称与请求值的名称(例如路径变量或请求参数)匹配,并且模型属性类型存在ConverterfromString到模型属性类型时。在以下示例中,模型属性名称account与 URI 路径变量匹配account,并且有一个已注册的可以从数据存储Converter<String, Account>加载的:Account

java
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
    // ...
}
科特林
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
    // ...
}

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

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

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

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

java
@ModelAttribute
public AccountForm setUpForm() {
    return new AccountForm();
}

@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
    return accountRepository.findOne(accountId);
}

@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
        @ModelAttribute(binding=false) Account account) { (1)
    // ...
}
1 设置@ModelAttribute(binding=false)
科特林
@ModelAttribute
fun setUpForm(): AccountForm {
    return AccountForm()
}

@ModelAttribute
fun findAccount(@PathVariable accountId: String): Account {
    return accountRepository.findOne(accountId)
}

@PostMapping("update")
fun update(@Valid form: AccountForm, result: BindingResult,
           @ModelAttribute(binding = false) account: Account): String { (1)
    // ...
}
1 设置@ModelAttribute(binding=false)

javax.validation.Valid您可以通过添加注解或 Spring 的@Validated注解(Bean ValidationSpring 验证)在数据绑定后自动应用验证 。以下示例显示了如何执行此操作:

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

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

@SessionAttributes

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

以下示例使用@SessionAttributes注释:

java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用@SessionAttributes注释。
科特林
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
1 使用@SessionAttributes注释。

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

java
@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 Pet从 Servlet 会话中清除值。
科特林
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {

    // ...

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

如果您需要访问全局管理的预先存在的会话属性(即,在控制器外部 - 例如,通过过滤器)并且可能存在也可能不存在,您可以@SessionAttribute在方法参数上使用注释,如下所示示例显示:

java
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1 使用@SessionAttribute注释。
科特林
@RequestMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}

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

对于作为控制器工作流的一部分在会话中临时存储模型属性,请考虑@SessionAttributes按照 @SessionAttributes.

@RequestAttribute

与 类似@SessionAttribute,您可以使用@RequestAttribute注释来访问先前创建的预先存在的请求属性(例如,通过 ServletFilterHandlerInterceptor):

java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
1 使用@RequestAttribute注释。
科特林
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
1 使用@RequestAttribute注释。
重定向属性

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

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

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

请注意,当前请求中的 URI 模板变量在扩展重定向 URL 时会自动可用,您无需通过Model或显式添加它们RedirectAttributes。以下示例显示了如何定义重定向:

java
@PostMapping("/files/{path}")
public String upload(...) {
    // ...
    return "redirect:files/{path}";
}
科特林
@PostMapping("/files/{path}")
fun upload(...): String {
    // ...
    return "redirect:files/{path}"
}

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

Flash 属性

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

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

Flash 属性支持始终处于“开启”状态,不需要显式启用。但是,如果不使用,它永远不会导致 HTTP 会话创建。在每个请求上,都有一个FlashMap带有从先前请求(如果有)传递的属性的“输入”和一个FlashMap带有用于保存后续请求的属性的“输出”。这两个FlashMap 实例都可以从 Spring MVC 中的任何地方通过 RequestContextUtils.

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

将请求与闪存属性匹配

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

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

这并不能完全消除并发问题的可能性,但使用重定向 URL 中已有的信息可以大大减少它。因此,我们建议您将 flash 属性主要用于重定向场景。

多部分

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

java
@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";
    }
}
科特林
@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(@RequestParam("name") name: String,
                        @RequestParam("file") file: MultipartFile): String {

        if (!file.isEmpty) {
            val bytes = file.bytes
            // store the bytes somewhere
            return "redirect:uploadSuccess"
        }
        return "redirect:uploadFailure"
    }
}

将参数类型声明为 aList<MultipartFile>允许解析具有相同参数名称的多个文件。

@RequestParam注释被声明为 aMap<String, MultipartFile>MultiValueMap<String, MultipartFile>,而注释中没有指定参数名称时,映射将填充每个给定参数名称的多部分文件。

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

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

java
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";
    }
}
科特林
class MyForm(val name: String, val file: MultipartFile, ...)

@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(form: MyForm, errors: BindingResult): String {
        if (!form.file.isEmpty) {
            val bytes = form.file.bytes
            // store the bytes somewhere
            return "redirect:uploadSuccess"
        }
        return "redirect:uploadFailure"
    }
}

多部分请求也可以从 RESTful 服务场景中的非浏览器客户端提交。以下示例显示了一个带有 JSON 的文件:

发布 /someUrl
内容类型:多部分/混合

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
内容处置:表单数据;名称="元数据"
内容类型:应用程序/json;字符集=UTF-8
内容传输编码:8bit

{
    “名称”:“价值”
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
内容处置:表单数据;名称="文件数据"; 文件名="文件.properties"
内容类型:文本/xml
内容传输编码:8bit
... 文件数据 ...

@RequestParam您可以使用as访问“元数据”部分,String但您可能希望它从 JSON 反序列化(类似于@RequestBody)。使用 HttpMessageConverter@RequestPart转换后,使用注释访问多部分 :

java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") MultipartFile file) {
    // ...
}
科特林
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData,
        @RequestPart("file-data") file: MultipartFile): String {
    // ...
}

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

java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
        BindingResult result) {
    // ...
}
科特林
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
        result: BindingResult): String {
    // ...
}
@RequestBody

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

java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}

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

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

java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
    // ...
}
实体

HttpEntity与 using 或多或少相同,@RequestBody但基于公开请求标头和正文的容器对象。以下清单显示了一个示例:

java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
科特林
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
@ResponseBody

您可以使用@ResponseBody方法上的注释通过 HttpMessageConverter将返回序列化到响应正文。以下清单显示了一个示例:

java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
科特林
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}

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

您可以@ResponseBody与反应类型一起使用。有关更多详细信息,请参阅异步请求反应类型

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

您可以将@ResponseBody方法与 JSON 序列化视图结合使用。有关详细信息,请参阅杰克逊 JSON

响应实体

ResponseEntity就像@ResponseBody但有状态和标题。例如:

java
@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}
科特林
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body = ...
    val etag = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}

Spring MVC 支持使用单值响应类型为主体 生成ResponseEntity异步和/或单值和多值响应类型。这允许以下类型的异步响应:

  • ResponseEntity<Mono<T>>或者ResponseEntity<Flux<T>>在稍后异步提供正文时立即知道响应状态和标头。如果Mono主体由 0..1 个值组成,或者Flux它可以产生多个值,则使用。

  • Mono<ResponseEntity<T>>提供所有三个——响应状态、标头和正文,稍后异步提供。这允许响应状态和标头根据异步请求处理的结果而变化。

杰克逊 JSON

Spring 提供对 Jackson JSON 库的支持。

JSON 视图

Spring MVC 提供了对Jackson 的序列化视图的内置支持 ,它允许仅渲染Object. 要将其与 @ResponseBody控制器ResponseEntity方法一起使用,您可以使用 Jackson 的 @JsonView注解来激活序列化视图类,如以下示例所示:

java
@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;
    }
}
科特林
@RestController
class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView::class)
    fun getUser() = User("eric", "7!jd#h23")
}

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String) {

    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}
@JsonView允许一组视图类,但您只能为每个控制器方法指定一个。如果需要激活多个视图,可以使用复合界面。

如果您想以编程方式执行上述操作,而不是声明@JsonView注释,而是将返回值包装MappingJacksonValue并使用它来提供序列化视图:

java
@RestController
public class UserController {

    @GetMapping("/user")
    public MappingJacksonValue getUser() {
        User user = new User("eric", "7!jd#h23");
        MappingJacksonValue value = new MappingJacksonValue(user);
        value.setSerializationView(User.WithoutPasswordView.class);
        return value;
    }
}
科特林
@RestController
class UserController {

    @GetMapping("/user")
    fun getUser(): MappingJacksonValue {
        val value = MappingJacksonValue(User("eric", "7!jd#h23"))
        value.serializationView = User.WithoutPasswordView::class.java
        return value
    }
}

对于依赖视图解析的控制器,您可以将序列化视图类添加到模型中,如以下示例所示:

java
@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";
    }
}
科特林
import org.springframework.ui.set

@Controller
class UserController : AbstractController() {

    @GetMapping("/user")
    fun getUser(model: Model): String {
        model["user"] = User("eric", "7!jd#h23")
        model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java
        return "userView"
    }
}

1.3.4。模型

您可以使用@ModelAttribute注释:

  • 在方法中的方法参数@RequestMapping上,从模型创建或访问一个Object,并通过一个将其绑定到请求 WebDataBinder

  • @Controller作为一个或多个类中的方法级注释@ControllerAdvice,有助于在任何@RequestMapping方法调用之前初始化模型。

  • 在一个@RequestMapping方法上标记它的返回值是一个模型属性。

本节讨论@ModelAttribute方法——前面列表中的第二项。一个控制器可以有任意数量的@ModelAttribute方法。所有此类方法都@RequestMapping在同一控制器中的方法之前调用。一个@ModelAttribute 方法也可以通过@ControllerAdvice. 有关详细信息,请参阅 控制器建议部分。

@ModelAttribute方法具有灵活的方法签名。它们支持许多与方法相同的参数@RequestMapping,除了@ModelAttribute自身或与请求主体相关的任何内容。

以下示例显示了一个@ModelAttribute方法:

java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
科特林
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}

以下示例仅添加一个属性:

java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
科特林
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number)
}
如果未明确指定名称,则会根据Object 类型选择默认名称,如Conventions. 您始终可以通过使用重载addAttribute方法或通过name属性 on @ModelAttribute(用于返回值)来分配显式名称。

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

java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
科特林
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}

1.3.5。DataBinder

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

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

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

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

@InitBinder方法可以注册特定于控制器java.beans.PropertyEditor或 SpringConverterFormatter组件。此外,您可以使用 MVC 配置来注册ConverterFormatter 键入全局共享的FormattingConversionService.

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

java
@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方法。
科特林
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}
1 定义一个@InitBinder方法。

或者,当您Formatter通过 shared 使用基于 FormattingConversionService- 的设置时,您可以重复使用相同的方法并注册特定于控制器的Formatter实现,如以下示例所示:

java
@Controller
public class FormController {

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

    // ...
}
1 @InitBinder在自定义格式化程序上定义方法。
科特林
@Controller
class FormController {

    @InitBinder (1)
    protected fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
    }

    // ...
}
1 @InitBinder在自定义格式化程序上定义方法。
模型设计

在 Web 应用程序的上下文中,数据绑定涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。

只有public遵循 JavaBeans 命名约定 的属性才会暴露给数据绑定——例如,属性public String getFirstName()public void setFirstName(String)方法firstName

模型对象及其嵌套对象图有时也称为 命令对象表单支持对象POJO(普通旧 Java 对象)。

默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以针对任何公共属性路径,甚至一些不希望针对给定用例的公共属性路径。

例如,给定一个 HTTP 表单数据端点,恶意客户端可以为模型对象图中存在但不属于浏览器中呈现的 HTML 表单的一部分的属性提供值。这可能导致在模型对象及其任何嵌套对象上设置数据,预计不会更新。

推荐的方法是使用一个专门的模型对象,它只公开与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单上,模型对象应声明一组最小属性,如下所示ChangeEmailForm

public class ChangeEmailForm {

    private String oldEmailAddress;
    private String newEmailAddress;

    public void setOldEmailAddress(String oldEmailAddress) {
        this.oldEmailAddress = oldEmailAddress;
    }

    public String getOldEmailAddress() {
        return this.oldEmailAddress;
    }

    public void setNewEmailAddress(String newEmailAddress) {
        this.newEmailAddress = newEmailAddress;
    }

    public String getNewEmailAddress() {
        return this.newEmailAddress;
    }

}

如果您不能或不想为每个数据绑定用例使用专用模型对象,则必须限制数据绑定允许的属性。理想情况下,您可以通过 on 方法注册允许的字段模式来实现此目的。setAllowedFields()WebDataBinder

例如,要在您的应用程序中注册允许的字段模式,您可以 在 a or组件中实现一个@InitBinder方法,如下所示:@Controller@ControllerAdvice

@Controller
public class ChangeEmailController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
    }

    // @RequestMapping methods, etc.

}

除了注册允许的模式外,还可以通过in及其子类的方法注册不允许的字段模式。但是请注意,“允许列表”比“拒绝列表”更安全。因此, 应优先考虑。setDisallowedFields()DataBindersetAllowedFields()setDisallowedFields()

请注意,与允许的字段模式匹配是区分大小写的;然而,与不允许的字段模式匹配是不区分大小写的。此外,匹配不允许模式的字段将不会被接受,即使它也恰好匹配允许列表中的模式。

在直接公开域模型以进行数据绑定时,正确配置允许和不允许的字段模式非常重要。否则,就是很大的安全隐患。

此外,强烈建议您不要域模型中的类型(例如 JPA 或 Hibernate 实体)用作数据绑定场景中的模型对象。

1.3.6。例外

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

java
@Controller
public class SimpleController {

    // ...

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

    // ...

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

异常可以与正在传播的顶级异常(例如,直接 IOException抛出)或与包装异常中的嵌套原因(例如,IOException包装在 中IllegalStateException)匹配。从 5.3 开始,这可以匹配任意原因级别,而之前只考虑直接原因。

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

或者,注释声明可以缩小要匹配的异常类型,如以下示例所示:

java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
    // ...
}
科特林
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
    // ...
}

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

java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
    // ...
}
科特林
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
    // ...
}

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

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

变体中的行为甚至更简单handle(Exception)。这总是在包装场景中使用包装器异常调用,ex.getCause()在这种情况下可以找到实际匹配的异常。只有当这些异常作为顶级异常抛出时,传入的异常才是实际的FileSystemException或 实例。RemoteException

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

在多安排中,我们建议您以相应的顺序@ControllerAdvice在优先级上声明您的主根异常映射。@ControllerAdvice虽然根异常匹配优于原因,但这是在给定控制器或@ControllerAdvice类的方法中定义的。这意味着较高优先级 @ControllerAdvicebean 上的原因匹配优于较低优先级 bean 上的任何匹配(例如,根) @ControllerAdvice

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

Spring MVC 中对方法的支持@ExceptionHandler是建立在DispatcherServlet 层次上的,HandlerExceptionResolver机制。

方法参数

@ExceptionHandler方法支持以下参数:

方法参数 描述

异常类型

用于访问引发的异常。

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.TimeZone,java.time.ZoneId

与当前请求关联的时区,由LocaleContextResolver.

java.io.OutputStream,java.io.Writer

用于访问由 Servlet API 公开的原始响应主体。

java.util.Map, org.springframework.ui.Model,org.springframework.ui.ModelMap

用于访问模型以获取错误响应。总是空的。

RedirectAttributes

指定在重定向的情况下要使用的属性 - (即附加到查询字符串)和闪存属性要临时存储,直到重定向后的请求。请参阅重定向属性Flash 属性

@SessionAttribute

对于访问任何会话属性,与作为类级@SessionAttributes声明的结果存储在会话中的模型属性相反。有关@SessionAttribute更多详细信息,请参阅。

@RequestAttribute

用于访问请求属性。有关@RequestAttribute更多详细信息,请参阅。

返回值

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

返回值 描述

@ResponseBody

返回值通过HttpMessageConverter实例转换并写入响应。见@ResponseBody

HttpEntity<B>,ResponseEntity<B>

返回值指定完整的响应(包括 HTTP 标头和正文)通过HttpMessageConverter实例转换并写入响应。请参阅ResponseEntity

String

与实现一起解析并与隐式模型一起使用的视图名称ViewResolver- 通过命令对象和@ModelAttribute方法确定。处理程序方法还可以通过声明Model 参数以编程方式丰富模型(如前所述)。

View

View与隐式模型一起用于渲染的实例——通过命令对象和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(如前所述)以编程方式丰富模型。

java.util.Map,org.springframework.ui.Model

要添加到隐式模型的属性,其视图名称通过RequestToViewNameTranslator.

@ModelAttribute

要添加到模型的属性,其视图名称通过RequestToViewNameTranslator.

请注意,这@ModelAttribute是可选的。请参阅本表末尾的“任何其他返回值”。

ModelAndView目的

要使用的视图和模型属性,以及可选的响应状态。

void

具有void返回类型(或null返回值)的方法如果还具有参数或注释,则被认为已完全处理ServletResponseOutputStream响应@ResponseStatusETag如果控制器进行了肯定或lastModified时间戳检查,情况也是如此 (有关详细信息,请参阅控制器)。

如果以上都不成立,则void返回类型还可以指示 REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。

任何其他返回值

如果返回值与上述任何一个都不匹配并且不是简单类型(由 BeanUtils#isSimpleProperty确定),默认情况下,它被视为要添加到模型中的模型属性。如果它是一个简单类型,它仍然是未解决的。

REST API 异常

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

在响应正文中使用错误详细信息实现全局异常处理的应用程序应考虑扩展 ResponseEntityExceptionHandler,它为 Spring MVC 引发的异常提供处理并提供自定义响应正文的钩子。要使用它,创建一个 的子类 ResponseEntityExceptionHandler,用 注释它@ControllerAdvice,覆盖必要的方法,并将其声明为 Spring bean。

1.3.7。控制器建议

@ExceptionHandler, @InitBinder, 和@ModelAttribute方法仅适用 @Controller于声明它们的类或类层次结构。相反,如果它们在@ControllerAdviceor@RestControllerAdvice类中声明,则它们适用于任何控制器。此外,从 5.3 开始,@ExceptionHandler方法 in@ControllerAdvice 可用于处理来自任何@Controller或任何其他处理程序的异常。

@ControllerAdvice带有元注释,@Component因此可以通过组件扫描注册为 Spring bean 。 使用and@RestControllerAdvice进行元注释,这意味着方法将通过响应正文消息转换而不是通过 HTML 视图呈现其返回值。@ControllerAdvice@ResponseBody@ExceptionHandler

在启动时,RequestMappingHandlerMapping检测ExceptionHandlerExceptionResolver控制器建议 bean 并在运行时应用它们。来自 的全局@ExceptionHandler方法在来自 的局部方法之后@ControllerAdvice应用。相比之下,全局和方法在局部方法之前应用。@Controller@ModelAttribute@InitBinder

@ControllerAdvice注释具有允许您缩小它们应用的控制器和处理程序集的属性。例如:

java
// 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 {}
科特林
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3

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

1.4. 功能端点

Spring Web MVC 包括 WebMvc.fn,这是一个轻量级的函数式编程模型,其中函数用于路由和处理请求,并且为不可变性而设计的契约。它是基于注释的编程模型的替代方案,但在相同的DispatcherServlet上运行。

1.4.1。概述

在 WebMvc.fn 中,HTTP 请求使用HandlerFunction: 处理,该函数接受 ServerRequest并返回ServerResponse. 请求和响应对象都有不可变的契约,提供对 HTTP 请求和响应的 JDK 8 友好访问。 HandlerFunction相当于@RequestMapping基于注解的编程模型中的方法体。

传入的请求被路由到一个带有RouterFunction: 的处理函数,该函数接受ServerRequest并返回一个可选的HandlerFunction(即Optional<HandlerFunction>)。当路由函数匹配时,返回一个处理函数;否则为空 Optional。 RouterFunction相当于@RequestMapping注解,但主要区别在于路由器功能不仅提供数据,还提供行为。

RouterFunctions.route()提供了一个有助于创建路由器的路由器构建器,如以下示例所示:

java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();


public class PersonHandler {

    // ...

    public ServerResponse listPeople(ServerRequest request) {
        // ...
    }

    public ServerResponse createPerson(ServerRequest request) {
        // ...
    }

    public ServerResponse getPerson(ServerRequest request) {
        // ...
    }
}
科特林
import org.springframework.web.servlet.function.router

val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = router { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}


class PersonHandler(private val repository: PersonRepository) {

    // ...

    fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
1 使用路由器 DSL 创建路由器。

如果您将 注册RouterFunction为 bean,例如通过在 @Configuration类中公开它,它将被 servlet 自动检测到,如运行服务器中所述。

1.4.2. 处理函数

ServerRequest并且ServerResponse是不可变接口,提供对 JDK 8 友好的 HTTP 请求和响应访问,包括标头、正文、方法和状态代码。

服务器请求

ServerRequest提供对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问是通过body方法提供的。

以下示例将请求正文提取到String

java
String string = request.body(String.class);
科特林
val string = request.body<String>()

以下示例将正文提取到 a List<Person>,其中Person对象从序列化形式(例如 JSON 或 XML)解码:

java
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
科特林
val people = request.body<Person>()

以下示例显示了如何访问参数:

java
MultiValueMap<String, String> params = request.params();
科特林
val map = request.params()
服务器响应

ServerResponse提供对 HTTP 响应的访问,并且由于它是不可变的,因此您可以使用一种build方法来创建它。您可以使用构建器设置响应状态、添加响应标头或提供正文。以下示例使用 JSON 内容创建 200 (OK) 响应:

java
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
科特林
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

以下示例显示了如何构建带有Location标头但没有正文的 201 (CREATED) 响应:

java
URI location = ...
ServerResponse.created(location).build();
科特林
val location: URI = ...
ServerResponse.created(location).build()

您还可以使用异步结果作为主体,采用CompletableFuturePublisher或 . 支持的任何其他类型的形式ReactiveAdapterRegistry。例如:

java
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
科特林
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

如果不仅正文,而且状态或标头都基于异步类型,则可以使用静态async方法 on ServerResponse,它接受CompletableFuture<ServerResponse>Publisher<ServerResponse>或任何其他受ReactiveAdapterRegistry. 例如:

java
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
  .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);

服务器发送的事件可以通过 on 的静态sse方法提供ServerResponse。该方法提供的构建器允许您将字符串或其他对象作为 JSON 发送。例如:

java
public RouterFunction<ServerResponse> sse() {
    return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
                // Save the sseBuilder object somewhere..
            }));
}

// In some other thread, sending a String
sseBuilder.send("Hello world");

// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);

// Customize the event by using the other methods
sseBuilder.id("42")
        .event("sse event")
        .data(person);

// and done at some point
sseBuilder.complete();
科特林
fun sse(): RouterFunction<ServerResponse> = router {
    GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
        // Save the sseBuilder object somewhere..
    }
}

// In some other thread, sending a String
sseBuilder.send("Hello world")

// Or an object, which will be transformed into JSON
val person = ...
sseBuilder.send(person)

// Customize the event by using the other methods
sseBuilder.id("42")
        .event("sse event")
        .data(person)

// and done at some point
sseBuilder.complete()
处理程序类

我们可以将处理函数编写为 lambda,如以下示例所示:

java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");
科特林
val helloWorld: (ServerRequest) -> ServerResponse =
  { ServerResponse.ok().body("Hello World") }

这很方便,但是在应用程序中我们需要多个函数,并且多个内联 lambda 会变得混乱。因此,将相关的处理程序函数组合到一个处理程序类中是很有用的,该处理程序类的作用与@Controller基于注释的应用程序中的作用相似。例如,以下类公开了一个反应式Person存储库:

java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public ServerResponse listPeople(ServerRequest request) { (1)
        List<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people);
    }

    public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
        Person person = request.body(Person.class);
        repository.savePerson(person);
        return ok().build();
    }

    public ServerResponse getPerson(ServerRequest request) { (3)
        int personId = Integer.parseInt(request.pathVariable("id"));
        Person person = repository.getPerson(personId);
        if (person != null) {
            return ok().contentType(APPLICATION_JSON).body(person);
        }
        else {
            return ServerResponse.notFound().build();
        }
    }

}
1 listPeople是一个处理函数,它Person以 JSON 形式返回在存储库中找到的所有对象。
2 createPerson是一个处理函数,用于存储Person请求正文中包含的新内容。
3 getPerson是一个处理函数,它返回一个由id路径变量标识的人。我们Person从存储库中检索它并创建一个 JSON 响应(如果找到)。如果没有找到,我们返回 404 Not Found 响应。
科特林
class PersonHandler(private val repository: PersonRepository) {

    fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: List<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).body(people);
    }

    fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.body<Person>()
        repository.savePerson(person)
        return ok().build()
    }

    fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
                ?: ServerResponse.notFound().build()

    }
}
1 listPeople是一个处理函数,它Person以 JSON 形式返回在存储库中找到的所有对象。
2 createPerson是一个处理函数,用于存储Person请求正文中包含的新内容。
3 getPerson是一个处理函数,它返回一个由id路径变量标识的人。我们Person从存储库中检索它并创建一个 JSON 响应(如果找到)。如果没有找到,我们返回 404 Not Found 响应。
验证

功能端点可以使用 Spring 的验证工具将验证应用于请求正文。例如,给定一个自定义 Spring Validator实现Person

java
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

    public ServerResponse createPerson(ServerRequest request) {
        Person person = request.body(Person.class);
        validate(person); (2)
        repository.savePerson(person);
        return ok().build();
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
1 创建Validator实例。
2 应用验证。
3 为 400 响应引发异常。
科特林
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().build()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person")
        validator.validate(person, errors)
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
1 创建Validator实例。
2 应用验证。
3 为 400 响应引发异常。

处理程序还可以通过创建和注入Validator基于LocalValidatorFactoryBean. 请参阅Spring 验证

1.4.3.RouterFunction

路由器功能用于将请求路由到相应的HandlerFunction. 通常,您不会自己编写路由器函数,而是使用 RouterFunctions实用程序类上的方法来创建一个。 RouterFunctions.route()(无参数)为您提供了一个流畅的构建器来创建路由器功能,同时RouterFunctions.route(RequestPredicate, HandlerFunction)提供了一种直接的方式来创建路由器。

通常,建议使用route()构建器,因为它为典型的映射场景提供了方便的快捷方式,而无需难以发现的静态导入。例如,路由器函数构建器提供了GET(String, HandlerFunction)为 GET 请求创建映射的方法;和POST(String, HandlerFunction)帖子。

除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个以 aRequestPredicate作为参数的重载变体,通过它可以表达额外的约束。

谓词

您可以编写自己的RequestPredicate,但RequestPredicates实用程序类提供了常用的实现,基于请求路径、HTTP 方法、内容类型等。以下示例使用请求谓词基于Accept 标头创建约束:

java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World")).build();
科特林
import org.springframework.web.servlet.function.router

val route = router {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().body("Hello World")
    }
}

您可以使用以下方法将多个请求谓词组合在一起:

  • RequestPredicate.and(RequestPredicate) — 两者必须匹配。

  • RequestPredicate.or(RequestPredicate) ——两者都可以匹配。

很多谓词 fromRequestPredicates是组合的。例如,RequestPredicates.GET(String)RequestPredicates.method(HttpMethod) 和组成RequestPredicates.path(String)。上面显示的示例还使用了两个请求谓词,构建器在 RequestPredicates.GET内部使用,并将其与accept谓词组合在一起。

路线

路由器功能按顺序评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,在一般路由之前声明更具体的路由是有意义的。这在将路由器功能注册为 Spring bean 时也很重要,稍后将进行描述。请注意,此行为与基于注释的编程模型不同,后者自动选择“最具体”的控制器方法。

使用路由函数构建器时,所有定义的路由都组合成一个 RouterFunctionbuild(). 还有其他方法可以将多个路由器功能组合在一起:

  • add(RouterFunction)RouterFunctions.route()建设者上

  • RouterFunction.and(RouterFunction)

  • RouterFunction.andRoute(RequestPredicate, HandlerFunction)RouterFunction.and()—嵌套 的快捷方式 RouterFunctions.route()

以下示例显示了四个路由的组成:

java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
1 GET /person/{id}与 JSON 匹配的Accept标头被路由到 PersonHandler.getPerson
2 GET /person与 JSON 匹配的Accept标头被路由到 PersonHandler.listPeople
3 POST /person没有额外的谓词被映射到 PersonHandler.createPerson, 和
4 otherRoute是在其他地方创建的路由函数,并添加到构建的路由中。
科特林
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute = router {  }

val route = router {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 GET /person/{id}与 JSON 匹配的Accept标头被路由到 PersonHandler.getPerson
2 GET /person与 JSON 匹配的Accept标头被路由到 PersonHandler.listPeople
3 POST /person没有额外的谓词被映射到 PersonHandler.createPerson, 和
4 otherRoute是在其他地方创建的路由函数,并添加到构建的路由中。
嵌套路由

一组路由器功能通常具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是匹配的路径谓词,/person由三个路由使用。使用注解时,您可以使用@RequestMapping 映射到/person. 在 WebMvc.fn 中,路径谓词可以通过path路由器函数构建器上的方法共享。例如,上面示例的最后几行可以通过使用嵌套路由通过以下方式进行改进:

java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET(accept(APPLICATION_JSON), handler::listPeople)
        .POST(handler::createPerson))
    .build();
1 请注意,第二个参数path是使用路由器构建器的消费者。
科特林
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST(handler::createPerson)
    }
}

尽管基于路径的嵌套是最常见的,但您可以使用nest构建器上的方法嵌套在任何类型的谓词上。上面仍然包含一些以 shared Accept-header 谓词形式存在的重复。我们可以通过nest结合使用该方法来进一步改进accept

java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .build();
科特林
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST(handler::createPerson)
        }
    }
}

1.4.4。运行服务器

您通常通过MVC ConfigDispatcherHandler在基于设置的设置 中运行路由器功能,它使用 Spring 配置来声明处理请求所需的组件。MVC Java 配置声明了以下基础结构组件以支持功能端点:

  • RouterFunctionMapping: 检测 Spring 配置中的一个或多个RouterFunction<?>bean,对它们进行排序,通过它们组合它们 RouterFunction.andOther,并将请求路由到生成的组合RouterFunction.

  • HandlerFunctionAdapter:简单的适配器,可以DispatcherHandler调用HandlerFunction映射到请求的 a。

前面的组件让功能端点适合DispatcherServlet请求处理生命周期,并且(可能)与带注释的控制器(如果已声明)一起运行。这也是 Spring Boot Web 启动器启用功能端点的方式。

以下示例显示了 WebFlux Java 配置:

java
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
科特林
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}

1.4.5。过滤处理函数

您可以使用路由函数生成器上的 、 或 方法before过滤after处理函数。使用注释,您可以通过使用、 a或同时使用两者filter来实现类似的功能。过滤器将应用于构建器构建的所有路由。这意味着嵌套路由中定义的过滤器不适用于“顶级”路由。例如,考虑以下示例:@ControllerAdviceServletFilter

java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST(handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
1 添加自定义请求标头的before过滤器仅适用于两个 GET 路由。
2 记录响应的after过滤器适用于所有路由,包括嵌套路由。
科特林
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET(handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
    }
    POST(handler::createPerson)
    after { _, response -> (2)
        logResponse(response)
    }
}
1 添加自定义请求标头的before过滤器仅适用于两个 GET 路由。
2 记录响应的after过滤器适用于所有路由,包括嵌套路由。

filter路由器构建器上的方法接受一个 : 一个接受 a并返回 aHandlerFilterFunction的函数。处理函数参数表示链中的下一个元素。这通常是路由到的处理程序,但如果应用多个,它也可以是另一个过滤器。ServerRequestHandlerFunctionServerResponse

现在我们可以为我们的路由添加一个简单的安全过滤器,假设我们有一个SecurityManager可以确定是否允许特定路径的安全过滤器。以下示例显示了如何执行此操作:

java
SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
科特林
import org.springframework.web.servlet.function.router

val securityManager: SecurityManager = ...

val route = router {
    ("/person" and accept(APPLICATION_JSON)).nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        POST(handler::createPerson)
        filter { request, next ->
            if (securityManager.allowAccessTo(request.path())) {
                next(request)
            }
            else {
                status(UNAUTHORIZED).build();
            }
        }
    }
}

前面的示例演示了调用next.handle(ServerRequest)是可选的。我们只让处理函数在允许访问时运行。

除了使用filter路由器功能构建器上的方法外,还可以通过 将过滤器应用于现有路由器功能RouterFunction.filter(HandlerFilterFunction)

对功能端点的 CORS 支持是通过专用的 CorsFilter.

1.5。URI 链接

本节介绍 Spring Framework 中可用于处理 URI 的各种选项。

1.5.1。UriComponents

Spring MVC 和 Spring WebFlux

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

java
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://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.
科特林
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build() (4)

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

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

java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
科特林
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri()

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

java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
科特林
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")

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

java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");
科特林
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")

1.5.2. UriBuilder

Spring MVC 和 Spring WebFlux

UriComponentsBuilder实现UriBuilderUriBuilder反过来,您可以 使用UriBuilderFactory. 一起,UriBuilderFactoryUriBuilder提供一种可插入的机制,以基于共享配置(例如基本 URL、编码首选项和其他详细信息)从 URI 模板构建 URI。

您可以配置RestTemplateWebClient用 aUriBuilderFactory 来自定义编写 URI。DefaultUriBuilderFactory是内部UriBuilderFactory使用UriComponentsBuilder并公开共享配置选项的默认实现。

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

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

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

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
科特林
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory

以下示例配置了一个WebClient

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

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

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
科特林
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

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

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

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

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
科特林
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

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

1.5.3. URI 编码

Spring MVC 和 Spring WebFlux

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

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

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

在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而如果 URI 变量确实包含保留字符,则第二个选项很有用。当根本不扩展 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的东西进行编码。

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

java
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"
科特林
val 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(这意味着编码)来缩短前面的示例,如以下示例所示:

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

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

java
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar");
科特林
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")

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

java
String baseUrl = "https://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();
科特林
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

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

DefaultUriBuilderFactory实现在UriComponentsBuilder内部使用来扩展和编码 URI 模板。作为工厂,它提供了一个单一的地方来配置编码方法,基于以下编码模式之一:

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

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

  • URI_COMPONENT:使用UriComponents#encode(),对应于前面列表中的第二个选项,在URI 变量展开后对 URI 组件值进行编码。

  • NONE: 不应用编码。

RestTemplate设置为出于历史EncodingMode.URI_COMPONENT原因和向后兼容性。WebClient依赖于 中的默认值,该值DefaultUriBuilderFactoryEncodingMode.URI_COMPONENT5.0.x更改为EncodingMode.TEMPLATE_AND_VALUES5.1。

1.5.4。相对 Servlet 请求

您可以使用ServletUriComponentsBuilder创建相对于当前请求的 URI,如以下示例所示:

java
HttpServletRequest request = ...

// Re-uses scheme, host, port, path, and query string...

URI uri = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}")
        .build("123");
科特林
val request: HttpServletRequest = ...

// Re-uses scheme, host, port, path, and query string...

val uri = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}")
        .build("123")

您可以创建相对于上下文路径的 URI,如以下示例所示:

java
HttpServletRequest request = ...

// Re-uses scheme, host, port, and context path...

URI uri = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts")
        .build()
        .toUri();
科特林
val request: HttpServletRequest = ...

// Re-uses scheme, host, port, and context path...

val uri = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts")
        .build()
        .toUri()

您可以创建相对于 Servlet(例如/main/*)的 URI,如以下示例所示:

java
HttpServletRequest request = ...

// Re-uses scheme, host, port, context path, and Servlet mapping prefix...

URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts")
        .build()
        .toUri();
科特林
val request: HttpServletRequest = ...

// Re-uses scheme, host, port, context path, and Servlet mapping prefix...

val uri = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts")
        .build()
        .toUri()
从 5.1 开始,ServletUriComponentsBuilder忽略ForwardedX-Forwarded-*标头中的信息,这些信息指定了客户端发起的地址。考虑使用 ForwardedHeaderFilter来提取和使用或丢弃此类标头。

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

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

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

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

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

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

URI uri = uriComponents.encode().toUri();
科特林
val uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)

val uri = uriComponents.encode().toUri()

在前面的示例中,我们提供了实际的方法参数值(在本例中为 long 值21:)用作路径变量并插入到 URL 中。此外,我们提供值 ,42来填充任何剩余的 URI 变量,例如hotel从类型级请求映射继承的变量。如果该方法有更多参数,我们可以为 URL 不需要的参数提供 null。通常,只有@PathVariable@RequestParam参数与构造 URL 相关。

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

java
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
科特林
val uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

val uri = uriComponents.encode().toUri()
当控制器方法签名被认为可用于使用fromMethodCall. 除了需要正确的参数签名外,返回类型还有技术限制(即为链接构建器调用生成运行时代理),因此返回类型不能是final. 特别是,String视图名称的常见返回类型在这里不起作用。您应该改用ModelAndView 甚至是普通Object的(带有String返回值)。

前面的示例在MvcUriComponentsBuilder. 在内部,它们依赖于ServletUriComponentsBuilder从当前请求的方案、主机、端口、上下文路径和 servlet 路径中准备一个基本 URL。这在大多数情况下都很有效。然而,有时,它可能是不够的。例如,您可能在请求的上下文之外(例如准备链接的批处理),或者您可能需要插入路径前缀(例如从请求路径中删除并需要重新插入链接)。

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

java
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();
科特林
val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")
val builder = MvcUriComponentsBuilder.relativeTo(base)
builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

val uri = uriComponents.encode().toUri()
从 5.1 开始,MvcUriComponentsBuilder忽略ForwardedX-Forwarded-*标头中的信息,这些信息指定了客户端发起的地址。考虑使用 ForwardedHeaderFilter来提取和使用或丢弃此类标头。

在 Thymeleaf、FreeMarker 或 JSP 等视图中,您可以通过引用为每个请求映射隐式或显式分配的名称来构建到带注释的控制器的链接。

考虑以下示例:

java
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

    @RequestMapping("/{country}")
    public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
科特林
@RequestMapping("/people/{id}/addresses")
class PersonAddressController {

    @RequestMapping("/{country}")
    fun getAddress(@PathVariable country: String): HttpEntity<PersonAddress> { ... }
}

给定前面的控制器,您可以从 JSP 准备一个链接,如下所示:

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

前面的例子依赖于mvcUrlSpring 标记库(即 META-INF/spring.tld)中声明的函数,但是很容易定义自己的函数或为其他模板技术准备一个类似的函数。

这是它的工作原理。在启动时,@RequestMapping通过 为每个对象分配一个默认名称HandlerMethodMappingNamingStrategy,其默认实现使用类的大写字母和方法名称(例如,getThing方法中 的方法ThingController变为“TC#getThing”)。如果存在名称冲突,您可以使用 @RequestMapping(name="..")指定显式名称或实现自己的 HandlerMethodMappingNamingStrategy.

1.6. 异步请求

1.6.1.DeferredResult

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

java
@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(result);
科特林
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
    val deferredResult = DeferredResult<String>()
    // Save the deferredResult somewhere..
    return deferredResult
}

// From some other thread...
deferredResult.setResult(result)

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

1.6.2.Callable

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

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

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };
}
科特林
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
    // ...
    "someView"
}

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

1.6.3. 加工

下面是对 Servlet 异步请求处理的一个非常简洁的概述:

  • AServletRequest可以通过调用request.startAsync(). 这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。

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

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

DeferredResult处理工作如下:

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

  • Spring MVC 调用request.startAsync().

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

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

  • DispatcherServlet再次调用,处理以异步产生的返回值继续。

Callable处理工作如下:

  • 控制器返回一个Callable.

  • Spring MVC 调用request.startAsync()并提交CallableTaskExecutor一个单独的线程中进行处理。

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

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

  • DispatcherServlet再次调用 ,并使用来自 的异步生成的返回值继续处理Callable

有关更多背景和上下文,您还可以阅读 介绍 Spring MVC 3.2 中的异步请求处理支持的博客文章。

异常处理

当您使用 aDeferredResult时,您可以选择是调用setResult还是 setErrorResult异常调用。在这两种情况下,Spring MVC 都会将请求分派回 Servlet 容器以完成处理。然后将其视为控制器方法返回给定值或产生给定异常。然后异常通过常规异常处理机制(例如,调用 @ExceptionHandler方法)。

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

拦截

HandlerInterceptor实例可以是 type AsyncHandlerInterceptor,以接收 afterConcurrentHandlingStarted启动异步处理的初始请求的回调(而不是postHandleand afterCompletion)。

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

DeferredResult提供onTimeout(Runnable)onCompletion(Runnable)回调。有关更多详细信息,请参阅的javadocDeferredResultCallable可以替代WebAsyncTask它为超时和完成回调公开其他方法。

与 WebFlux 相比

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

相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的异步请求处理特性,因为它在设计上是异步的。异步处理内置于所有框架合同中,并且在请求处理的所有阶段都得到内在支持。

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

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

1.6.4. HTTP 流式传输

您可以将DeferredResultandCallable用于单个异步返回值。如果您想生成多个异步值并将这些值写入响应中怎么办?本节介绍如何执行此操作。

对象

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

java
@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();
科特林
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
    // Save the emitter somewhere..
}

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

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

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

您还可以ResponseBodyEmitter用作 a 中的主体ResponseEntity,让您自定义响应的状态和标题。

emitter抛出 an时IOException(例如,如果远程客户端离开),应用程序不负责清理连接,也不应该调用emitter.complete or emitter.completeWithError。相反,servlet 容器会自动启动 AsyncListener错误通知,Spring MVC 在其中进行completeWithError调用。反过来,此调用ASYNC对应用程序执行最后一次分派,在此期间 Spring MVC 调用配置的异常解析器并完成请求。

上证所

SseEmitter( 的子类ResponseBodyEmitter)提供对 Server-Sent Events的支持,其中从服务器发送的事件根据 W3C SSE 规范进行格式化。要从控制器生成 SSE 流,请返回SseEmitter,如以下示例所示:

java
@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();
科特林
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
    // Save the emitter somewhere..
}

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

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

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

虽然 SSE 是流式传输到浏览器的主要选项,但请注意 Internet Explorer 不支持服务器发送事件。考虑将 Spring 的 WebSocket 消息SockJS 后备传输(包括 SSE)一起使用,该传输针对各种浏览器。

有关异常处理的说明,另请参见上一节

原始数据

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

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

您可以StreamingResponseBody在 a 中用作主体来自ResponseEntity定义响应的状态和标头。

1.6.5。反应类型

Spring MVC 支持在控制器中使用响应式客户端库(另请阅读 WebFlux 部分中的响应式库)这包括WebClientfromspring-webflux和其他,例如 Spring Data 反应式数据存储库。在这种情况下,能够从控制器方法返回反应类型很方便。

响应式返回值处理如下:

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

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

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

ReactiveAdapterRegistrySpring MVC 通过from 支持 Reactor 和 RxJava spring-core,这让它可以适应多个响应式库。

对于流式传输到响应,支持响应式背压,但对响应的写入仍然是阻塞的,并且通过 配置 TaskExecutor的在单独的线程上运行,以避免阻塞上游源(例如Flux从返回WebClient)。默认情况下,SimpleAsyncTaskExecutor用于阻塞写入,但在负载下不适合。如果您计划使用响应式流式传输,则应使用 MVC 配置来配置任务执行器。

1.6.6。断开连接

当远程客户端离开时,Servlet API 不提供任何通知。因此,在流式传输到响应时,无论是通过SseEmitter 还是响应式类型,定期发送数据很重要,因为如果客户端断开连接,写入将失败。发送可以采用空的(仅注释的)SSE 事件或任何其他数据的形式,对方必须将其解释为心跳并忽略。

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

1.6.7。配置

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

小服务容器

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

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

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

SpringMVC

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

  • Java 配置:configureAsyncSupportWebMvcConfigurer.

  • XML 命名空间:<async-support>使用<mvc:annotation-driven>.

您可以配置以下内容:

  • 异步请求的默认超时值,如果未设置,则取决于底层 Servlet 容器。

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

  • DeferredResultProcessingInterceptor实施和CallableProcessingInterceptor实施。

请注意,您还可以在 a DeferredResult、 aResponseBodyEmitter和 an上设置默认超时值SseEmitter。对于 a Callable,您可以使用它 WebAsyncTask来提供超时值。

1.7. CORS

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

1.7.1。介绍

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

跨域资源共享 (CORS) 是 由大多数浏览器实现的W3C 规范,它允许您指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的不太安全且功能较弱的解决方法。

1.7.2. 加工

CORS 规范区分了预检请求、简单请求和实际请求。要了解 CORS 的工作原理,您可以阅读 本文等,或查看规范以了解更多详细信息。

Spring MVCHandlerMapping实现为 CORS 提供内置支持。在成功将请求映射到处理程序后,HandlerMapping实现检查给定请求和处理程序的 CORS 配置并采取进一步的操作。预检请求被直接处理,而简单和实际的 CORS 请求被拦截、验证并设置了所需的 CORS 响应标头。

为了启用跨域请求(即,Origin标头存在并且与请求的主机不同),您需要有一些明确声明的 CORS 配置。如果未找到匹配的 CORS 配置,则会拒绝预检请求。没有 CORS 标头添加到简单和实际 CORS 请求的响应中,因此,浏览器会拒绝它们。

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

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

组合全局和本地配置的规则通常是相加的——例如,所有全局和所有本地源。对于那些只能接受单个值的属性,例如allowCredentialsand maxAge,本地覆盖全局值。有关 CorsConfiguration#combine(CorsConfiguration) 更多详细信息,请参阅。

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

  • CorsConfiguration

  • CorsProcessor,DefaultCorsProcessor

  • AbstractHandlerMapping

1.7.3.@CrossOrigin

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

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

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

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
科特林
@RestController
@RequestMapping("/account")
class AccountController {

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

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }
}

默认情况下,@CrossOrigin允许:

  • 所有的起源。

  • 所有标题。

  • 控制器方法映射到的所有 HTTP 方法。

allowCredentials默认情况下不启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只能在适当的情况下使用。启用时,allowOrigins必须将其设置为一个或多个特定域(但不是特殊值"*"),或者该allowOriginPatterns属性可用于匹配一组动态的来源。

maxAge设置为 30 分钟。

@CrossOrigin在类级别也受支持,并且被所有方法继承,如以下示例所示:

java
@CrossOrigin(origins = "https://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(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }

您可以@CrossOrigin在类级别和方法级别使用,如以下示例所示:

java
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

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

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
科特林
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com")
    @GetMapping("/{id}")
    fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }
}

1.7.4。全局配置

除了细粒度的控制器方法级别配置之外,您可能还想定义一些全局 CORS 配置。您可以CorsConfiguration 在任何HandlerMapping. 然而,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来执行此操作。

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

  • 所有的起源。

  • 所有标题。

  • GET, HEAD, 和POST方法。

allowCredentials默认情况下不启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只能在适当的情况下使用。启用时,allowOrigins必须将其设置为一个或多个特定域(但不是特殊值"*"),或者该allowOriginPatterns属性可用于匹配一组动态的来源。

maxAge设置为 30 分钟。

Java 配置

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

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

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

        // Add more mappings...
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

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

        // Add more mappings...
    }
}
XML 配置

要在 XML 命名空间中启用 CORS,您可以使用该<mvc:cors>元素,如以下示例所示:

<mvc:cors>

    <mvc:mapping path="/api/**"
        allowed-origins="https://domain1.com, https://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="https://domain1.com" />

</mvc:cors>

1.7.5。CORS 过滤器

您可以通过内置的 CorsFilter.

如果您尝试使用CorsFilterSpring Security,请记住 Spring Security内置了对 CORS 的支持。

要配置过滤器,请将 a 传递CorsConfigurationSource给其构造函数,如以下示例所示:

java
CorsConfiguration config = new CorsConfiguration();

// Possibly...
// config.applyPermitDefaultValues()

config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

CorsFilter filter = new CorsFilter(source);
科特林
val config = CorsConfiguration()

// Possibly...
// config.applyPermitDefaultValues()

config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")

val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)

val filter = CorsFilter(source)

1.8. 网络安全

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

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

1.9。HTTP缓存

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

本节介绍 Spring Web MVC 中可用的与 HTTP 缓存相关的选项。

1.9.1。CacheControl

CacheControl为配置与标头相关的设置提供支持,Cache-Control并在许多地方被接受为参数:

虽然RFC 7234Cache-Control描述了响应标头的所有可能指令,但该CacheControl类型采用面向用例的方法,专注于常见场景:

java
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

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

// Prevent caching - "Cache-Control: no-store"
val 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"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()

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

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

  • 0通过使用'Cache-Control: no-store'指令来防止缓存。

  • 一个值通过使用 指令n > 0将给定的响应缓存几秒钟。n'Cache-Control: max-age=n'

1.9.2。控制器

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

java
@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);
}
科特林
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {

    val book = findBook(id);
    val version = book.getVersion()

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book)
}

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

您还可以在控制器中检查条件请求标头,如以下示例所示:

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

    long eTag = ... (1)

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

    model.addAttribute(...); (3)
    return "myViewName";
}
1 特定于应用程序的计算。
2 响应已设置为 304 (NOT_MODIFIED) — 没有进一步处理。
3 继续处理请求。
科特林
@RequestMapping
fun myHandleMethod(request: WebRequest, model: Model): String? {

    val eTag: Long = ... (1)

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

    model[...] = ... (3)
    return "myViewName"
}
1 特定于应用程序的计算。
2 响应已设置为 304 (NOT_MODIFIED) — 没有进一步处理。
3 继续处理请求。

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

1.9.3。静态资源

您应该使用带有Cache-Control条件响应标头的静态资源来提供最佳性能。请参阅配置静态资源部分。

1.9.4。ETag筛选

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

1.10。查看技术

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

Spring MVC 应用程序的视图存在于该应用程序的内部信任边界内。视图可以访问应用程序上下文的所有 bean。因此,不建议在模板可由外部源编辑的应用程序中使用 Spring MVC 的模板支持,因为这可能会产生安全隐患。

1.10.1。百里香叶

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

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

1.10.2。自由标记

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

查看配置

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

java
@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;
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("/WEB-INF/freemarker")
    }
}

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

或者,您也可以声明FreeMarkerConfigurerbean 以完全控制所有属性,如以下示例所示:

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

您可以通过在 bean 上设置适当的 bean 属性,将 FreeMarker 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker Configuration对象(由 Spring 管理)FreeMarkerConfigurer。该freemarkerSettings属性需要一个java.util.Properties对象,并且该freemarkerVariables属性需要一个 java.util.Map. 以下示例显示了如何使用 a FreeMarkerConfigurer

<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 文档。

表单处理

Spring 提供了一个用于 JSP 的标签库,其中包含一个 <spring:bind/>元素。此元素主要让表单显示来自表单支持对象的值,并显示来自ValidatorWeb 或业务层的验证失败的结果。Spring 还支持 FreeMarker 中的相同功能,并带有额外的便利宏,用于自己生成表单输入元素。

绑定宏

FreeMarker 文件中维护了一组标准宏spring-webmvc.jar,因此它们始终可用于适当配置的应用程序。

Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的作用域,使得所有宏对调用代码和用户模板可见。以下部分仅关注您需要从模板中直接调用的宏。如果您想直接查看宏代码,则调用该文件spring.ftl并在 org.springframework.web.servlet.view.freemarker包中。

简单绑定

在基于 FreeMarker 模板(充当 Spring MVC 控制器的表单视图)的 HTML 表单中,您可以使用类似于下一个示例的代码绑定到字段值并以与 JSP 等效的方式类似的方式显示每个输入字段的错误消息。以下示例显示了一个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 "personForm.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>需要一个“路径”参数,它由您的命令对象的名称(它是“命令”,除非您在控制器配置中更改它)后跟一个句点和您希望的命令对象上的字段名称组成绑定。您还可以使用嵌套字段,例如command.address.street. 该宏采用由 中的参数 指定的bind默认 HTML 转义行为。ServletContextdefaultHtmlEscapeweb.xml

调用的宏的另一种形式<@spring.bindEscaped>采用第二个参数,该参数明确指定是否应在状态错误消息或值中使用 HTML 转义。您可以根据需要将其设置为truefalse。额外的表单处理宏简化了 HTML 转义的使用,您应该尽可能使用这些宏。它们将在下一节中解释。

输入宏

FreeMarker 的其他便利宏简化了绑定和表单生成(包括验证错误显示)。从来没有必要使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 混合和匹配,或者直接调用我们之前强调的 Spring 绑定宏。

下表显示了 FreeMarker 模板 (FTL) 定义和每个宏所采用的参数列表:

表 6. 宏定义表
超光速定义

message(根据code参数从资源包中输出一个字符串)

<@spring.message code/>

messageText(根据 code 参数从资源包中输出一个字符串,回退到默认参数的值)

<@spring.messageText 代码,正文/>

url(使用应用程序的上下文根作为相对 URL 的前缀)

<@spring.url relativeUrl/>

formInput(用于收集用户输入的标准输入字段)

<@spring.formInput 路径、属性、fieldType/>

formHiddenInput(用于提交非用户输入的隐藏输入字段)

<@spring.formHiddenInput 路径、属性/>

formPasswordInput(用于收集密码的标准输入字段。请注意,这种类型的字段中不会填充任何值。)

<@spring.formPassword输入路径、属性/>

formTextarea(用于收集长的、自由格式的文本输入的大文本字段)

<@spring.formTextarea 路径、属性/>

formSingleSelect(允许选择单个所需值的选项下拉框)

<@spring.formSingleSelect 路径、选项、属性/>

formMultiSelect(允许用户选择 0 个或多个值的选项列表框)

<@spring.formMultiSelect 路径、选项、属性/>

formRadioButtons(一组单选按钮,可以从可用选项中进行单个选择)

<@spring.formRadioButtons 路径、选项分隔符、属性/>

formCheckboxes(一组允许选择 0 个或多个值的复选框)

<@spring.formCheckboxes 路径、选项、分隔符、属性/>

formCheckbox(一个复选框)

<@spring.formCheckbox 路径、属性/>

showErrors(简化绑定字段验证错误的显示)

<@spring.showErrors 分隔符,classOrStyle/>

在 FreeMarker 模板中,formHiddenInput实际上formPasswordInput并不是必需的,因为您可以使用普通formInput宏,指定hiddenpassword 作为fieldType参数的值。

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

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

  • optionsMap可以在输入字段中选择的所有可用值中的一个。映射的键代表从表单返回并绑定到命令对象的值。根据键存储的映射对象是表单上显示给用户的标签,可能与表单回传的相应值不同。通常,控制器会提供这样的地图作为参考数据。您可以使用任何Map实现,具体取决于所需的行为。对于严格排序的地图,您可以使用 a SortedMap(例如 a TreeMap)和合适的Comparator,对于应该按插入顺序返回值的任意地图,使用 aLinkedHashMap或 a LinkedMapfrom commons-collections

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

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

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

以下部分概述了宏的示例。

输入字段

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

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

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

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

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

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

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

选择字段

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

  • formSingleSelect

  • formMultiSelect

  • formRadioButtons

  • formCheckboxes

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

下一个示例是 FTL 中的单选按钮。form-b​​acking 对象为此字段指定了默认值“London”,因此无需进行验证。呈现表单时,可供选择的整个城市列表在模型中以名称“cityMap”作为参考数据提供。以下清单显示了该示例:

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

上面的清单呈现了一行单选按钮,一个对应于 中的每个值cityMap,并使用 分隔符""。没有提供其他属性(缺少宏的最后一个参数)。映射中的每个键值对都cityMap使用相同的方法。String地图的键是表单实际作为POST请求参数提交的内容。地图值是用户看到的标签。在前面的示例中,给定三个知名城市的列表和表单支持对象中的默认值,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>

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

java
protected Map<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, Object> model = new HashMap<>();
    model.put("cityMap", cityMap);
    return model;
}
科特林
protected fun referenceData(request: HttpServletRequest): Map<String, *> {
    val cityMap = linkedMapOf(
            "LDN" to "London",
            "PRS" to "Paris",
            "NYC" to "New York"
    )
    return hashMapOf("cityMap" to cityMap)
}

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

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

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

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

<#-- 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.10.3。Groovy 标记

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

Groovy 标记模板引擎需要 Groovy 2.3.1+。
配置

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

java
@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;
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.groovy()
    }

    // Configure the Groovy Markup Template Engine...

    @Bean
    fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
        resourceLoaderPath = "/WEB-INF/"
    }
}

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

<mvc:annotation-driven/>

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

<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
例子

与传统的模板引擎不同,Groovy 标记依赖于使用构建器语法的 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.10.4。脚本视图

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

脚本库 脚本引擎

车把

纳肖恩

胡子

纳肖恩

反应

纳肖恩

EJS

纳肖恩

再培训局

JRuby

字符串模板

杰通

Kotlin 脚本模板

科特林

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

你需要在你的类路径上有脚本引擎,其细节因脚本引擎而异:

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

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

  • 应将Jython添加为 Python 支持的依赖项。

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

您需要有脚本模板库。对 JavaScript 执行此操作的一种方法是通过WebJars

脚本模板

您可以声明一个ScriptTemplateConfigurerbean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来渲染模板等等。以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:

java
@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;
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}

以下示例显示了 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 配置,控制器看起来没有什么不同,如以下示例所示:

java
@Controller
public class SampleController {

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

    @GetMapping("/sample")
    fun test(model: Model): String {
        model["title"] = "Sample title"
        model["body"] = "Sample body"
        return "template"
    }
}

以下示例显示了 Mustache 模板:

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

使用以下参数调用渲染函数:

  • String template: 模板内容

  • Map model: 视图模型

  • RenderingContext renderingContextRenderingContext 提供对应用程序上下文、语言环境、模板加载器和 URL 的访问权限(从 5.0 开始)

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

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

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

java
@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;
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}
当使用非线程安全脚本引擎和不是为并发设计的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要 将该sharedEngine属性设置为。false在这种情况下,由于 此错误,需要 Java SE 8 更新 60,但通常建议在任何情况下使用最近的 Java SE 补丁版本。

polyfill.js只定义windowHandlebars 正常运行所需的对象,如下:

var window = {};

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

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

查看 Spring Framework 单元测试、 Java资源,了解更多配置示例。

1.10.5。JSP 和 JSTL

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

查看解析器

使用 JSP 进行开发时,通常会声明一个InternalResourceViewResolverbean。

InternalResourceViewResolver可用于分派到任何 Servlet 资源,尤其是 JSP。作为最佳实践,我们强烈建议将您的 JSP 文件放在该目录下的'WEB-INF'目录中,这样客户端就不能直接访问。

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

使用 JSP 标准标记库 (JSTL) 时,您必须使用特殊的视图类 , JstlView因为 JSTL 需要一些准备工作才能使 I18N 等功能起作用。

Spring 的 JSP 标签库

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

标记库spring.tld描述符 (TLD) 包含在spring-webmvc.jar. 有关单个标签的综合参考,请浏览 API 参考 或查看标签库描述。

Spring的表单标签库

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

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

我们浏览表单标签并查看如何使用每个标签的示例。我们已经包含了生成的 HTML 片段,其中某些标签需要进一步的评论。

配置

表单标签库捆绑在spring-webmvc.jar. 库描述符称为spring-form.tld.

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

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

form您要用于此库中的标签的标签名称前缀在哪里。

表单标签

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

假设我们有一个名为User. 它是一个具有 和 等属性的firstNameJavaBean lastName。我们可以将它用作表单控制器的表单支持对象,它返回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>

firstName和值是从页面控制器lastName放置的命令对象中检索的。PageContext继续阅读以查看内部标签如何与标签一起使用的更复杂示例form

以下清单显示了生成的 HTML,它看起来像一个标准表单:

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

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

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

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

checkbox标签_

此标记呈现一个设置为的 HTMLinput标记。typecheckbox

假设我们User有诸如时事通讯订阅和爱好列表之类的偏好。以下示例显示了Preferences该类:

java
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;
    }
}
科特林
class Preferences(
        var receiveNewsletter: Boolean,
        var interests: StringArray,
        var favouriteWord: String
)

对应的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时,将 input(checkbox)标记为checked绑定值为true。该value 属性对应于setValue(Object)value 属性的解析值。

  • 方法二:当绑定值的类型为arrayorjava.util.Collection时,将 input(checkbox)标记为checked好像配置的setValue(Object)值存在于绑定中Collection

  • 方法三:对于任何其他绑定值类型,将input(checkbox)其标记为 checked配置setValue(Object)等于绑定值。

请注意,无论采用何种方法,都会生成相同的 HTML 结构。以下 HTML 片段定义了一些复选框:

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

您可能不希望在每个复选框后看到额外的隐藏字段。当 HTML 页面中的复选框未被选中时,一旦提交表单,它的值就不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要一种解决方法来解决 HTML 中的这个怪癖,以便 Spring 表单数据绑定工作。该 标记遵循现有的 Spring 约定,即为每个复选框checkbox包含一个以下划线 ( ) 为前缀的隐藏参数。_通过这样做,您实际上是在告诉 Spring“复选框在表单中是可见的,并且我希望表单数据绑定到的对象能够反映复选框的状态,无论如何。”

checkboxes标签_

此标记呈现多个设置为input的 HTML 标记。typecheckbox

本节建立在上一个checkbox标签部分的示例之上。有时,您不希望在 JSP 页面中列出所有可能的爱好。您宁愿在运行时提供可用选项的列表并将其传递给标签。这就是checkboxes标签的目的。您可以传入包含属性中可用选项的Array、 aList或 a 。通常,绑定属性是一个集合,因此它可以保存用户选择的多个值。以下示例显示了使用此标记的 JSP:Mapitems

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

此示例假定interestList是一个List可用的模型属性,其中包含要从中选择的值的字符串。如果使用 a Map,则将映射条目键用作值,并将映射条目的值用作要显示的标签。您还可以使用自定义对象,您可以在其中通过 using 提供值的属性名称,并通过 using 提供itemValue标签itemLabel

radiobutton标签_

此标记呈现设置为的 HTMLinput元素。typeradio

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

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

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

checkboxestag一样,您可能希望将可用选项作为运行时变量传递。对于这种用法,您可以使用 radiobuttons标签。您传入包含属性中可用选项的Array、 aList或 a 。如果使用 a ,则映射条目键用作值,映射条目的值用作要显示的标签。您还可以使用自定义对象,您可以在其中通过 using 提供值的属性名称,并通过 using 提供标签,如以下示例所示:MapitemsMapitemValueitemLabel

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

此标记使用绑定值呈现input类型设置为的 HTML标记。password

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

请注意,默认情况下,密码值不显示。如果确实希望显示密码值,可以将showPassword属性的值设置为 true,如以下示例所示:

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

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

假设 aUser有一个技能列表。对应的 HTML 可能如下:

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

如果User’s技能在 Herbology 中,“技能”行的 HTML 源代码如下:

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

此标记呈现 HTMLoption元素。它设置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属性。
options标签_

此标记呈现 HTMLoption元素列表。它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居住在英国,“Country”行的 HTML 源代码如下:

<tr>
    <td>Country:</td>
    <td>
        <select name="country">
            <option value="-">--Please Select</option>
            <option value="AT">Austria</option>
            <option value="UK" selected="selected">United Kingdom</option> (1)
            <option value="US">United States</option>
        </select>
    </td>
</tr>
1 注意添加了一个selected属性。

如前面的示例所示,option标签与options标签的组合使用会生成相同的标准 HTML,但允许您在 JSP 中显式指定一个仅用于显示(它所属的位置)的值,例如示例中的默认字符串: “ - 请选择”。

items属性通常由项目对象的集合或数组填充。 itemValueitemLabel引用这些项目对象的 bean 属性(如果指定)。否则,项目对象本身就会变成字符串。或者,您可以指定 a Mapof items,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果同时指定了itemValueitemLabel(或两者),则项目值属性适用于映射键,项目标签属性适用于映射值。

textarea标签_

此标记呈现 HTMLtextarea元素。以下 HTML 显示了它的典型输出:

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

此标记呈现一个带有绑定值的HTMLinput标记。要提交未绑定的隐藏值,请使用设置为的 HTML标记。以下 HTML 显示了它的典型输出:typehiddeninputtypehidden

<form:hidden path="house"/>

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

<input name="house" type="hidden" value="Gryffindor"/>
errors标签_

span此标记在 HTML元素中呈现字段错误。它提供对在您的控制器中创建的错误或由与您的控制器关联的任何验证器创建的错误的访问。

假设我们希望在 提交表单后显示firstName和字段的所有错误消息。lastName我们有一个User名为 的类实例的验证器UserValidator,如以下示例所示:

java
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.");
    }
}
科特林
class UserValidator : Validator {

    override fun supports(candidate: Class<*>): Boolean {
        return User::class.java.isAssignableFrom(candidate)
    }

    override fun validate(obj: Any, 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>

如果我们提交一个在firstNameandlastName字段中包含空值的表单,那么 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.<br/>Field is required.</span>
    <table>
        <tr>
            <td>First Name:</td>
            <td><input name="firstName" type="text" value=""/></td>
            <td><span name="firstName.errors">Field is required.</span></td>
        </tr>

        <tr>
            <td>Last Name:</td>
            <td><input name="lastName" type="text" value=""/></td>
            <td><span name="lastName.errors">Field is required.</span></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes"/>
            </td>
        </tr>
    </table>
</form>

标记库spring-form.tld描述符 (TLD) 包含在spring-webmvc.jar. 有关单个标签的综合参考,请浏览 API 参考 或查看标签库描述。

HTTP 方法转换

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

为了支持 HTTP 方法转换,更新了 Spring MVC 表单标签以支持设置 HTTP 方法。例如,以下代码片段来自 Pet Clinic 示例:

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

前面的示例执行 HTTP POST,“真正的”DELETE 方法隐藏在请求参数后面。它由HiddenHttpMethodFilterweb.xml 中定义的 提取,如以下示例所示:

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

<filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

以下示例显示了相应的@Controller方法:

java
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
    this.clinic.deletePet(petId);
    return "redirect:/owners/" + ownerId;
}
科特林
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
    clinic.deletePet(petId)
    return "redirect:/owners/$ownerId"
}
HTML5 标签

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

forminput标签支持输入除 之外的类型属性text。这旨在允许呈现新的 HTML5 特定输入类型,例如emaildaterange等。请注意,type='text'不需要输入,因为text 它是默认类型。

1.10.6。瓷砖

您可以在使用 Spring 的 Web 应用程序中集成 Tiles - 就像任何其他视图技术一样。本节广泛地描述了如何做到这一点。

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

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

配置

为了能够使用 Tiles,您必须使用包含定义的文件对其进行配置(有关定义和其他 Tiles 概念的基本信息,请参阅 https://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 应用程序中的视图。为了能够使用视图,您必须拥有ViewResolver 与 Spring 中的任何其他视图技术一样的:通常是方便的TilesViewResolver.

您可以通过添加下划线和语言环境来指定特定于语言环境的 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_FRlocale的请求,tiles.xml默认使用。

由于下划线用于指示区域设置,因此我们建议不要在 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>
SimpleSpringPreparerFactorySpringBeanPreparerFactory

作为一项高级功能,Spring 还支持两种特殊的 TilesPreparerFactory 实现。有关如何 ViewPreparer在 Tiles 定义文件中使用引用的详细信息,请参阅 Tiles 文档。

您可以根据指定的准备器类指定SimpleSpringPreparerFactory自动装配ViewPreparer实例,应用 Spring 的容器回调以及应用配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围的注解配置已被激活,ViewPreparer类中的注解会被自动检测并应用。请注意,这需要 Tiles 定义文件中的准备程序类,就像默认值PreparerFactory一样。

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

<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.10.7。RSS 和 Atom

两者都AbstractAtomFeedViewAbstractRssFeedView承自基 AbstractFeedView类,分别用于提供 Atom 和 RSS Feed 视图。它们基于ROME项目并位于包中org.springframework.web.servlet.view.feed

AbstractAtomFeedView要求您实现该buildFeedEntries()方法并可选地覆盖该buildFeedMetadata()方法(默认实现为空)。以下示例显示了如何执行此操作:

java
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
    }
}
科特林
class SampleContentAtomView : AbstractAtomFeedView() {

    override fun buildFeedMetadata(model: Map<String, Any>,
            feed: Feed, request: HttpServletRequest) {
        // implementation omitted
    }

    override fun buildFeedEntries(model: Map<String, Any>,
            request: HttpServletRequest, response: HttpServletResponse): List<Entry> {
        // implementation omitted
    }
}

类似的要求适用于实施AbstractRssFeedView,如以下示例所示:

java
public class SampleContentRssView 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
    }
}
科特林
class SampleContentRssView : AbstractRssFeedView() {

    override fun buildFeedMetadata(model: Map<String, Any>,
                                feed: Channel, request: HttpServletRequest) {
        // implementation omitted
    }

    override fun buildFeedItems(model: Map<String, Any>,
            request: HttpServletRequest, response: HttpServletResponse): List<Item> {
        // implementation omitted
    }
}

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

有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team 博客 条目

1.10.8。PDF 和 Excel

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

文档视图简介

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

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

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

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

java
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));
        }
    }
}
科特林
class PdfWordList : AbstractPdfView() {

    override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
            request: HttpServletRequest, response: HttpServletResponse) {

        val words = model["wordList"] as List<String>
        for (word in words) {
            doc.add(Paragraph(word))
        }
    }
}

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

Excel 视图

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

编程模型类似于AbstractPdfViewbuildExcelDocument() 作为中心模板方法,控制器能够从外部定义(按名称)或作为View处理程序方法的实例返回这样的视图。

1.10.9。杰克逊

Spring 提供对 Jackson JSON 库的支持。

基于 Jackson 的 JSON MVC 视图

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

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

基于 Jackson 的 XML 视图

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

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

1.10.10。XML 编组

使用MarshallingViewXML Marshaller(在org.springframework.oxm 包中定义)将响应内容呈现为 XML。MarshallingView您可以使用实例的modelKeybean 属性显式设置要编组的对象。或者,视图迭代所有模型属性并编组Marshaller. 有关 org.springframework.oxm包中功能的更多信息,请参阅使用 O/X 映射器编组 XML

1.10.11。XSLT 视图

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

这个例子是一个简单的 Spring 应用程序,它创建一个单词列表 Controller并将它们添加到模型映射中。地图连同我们的 XSLT 视图的视图名称一起返回。有关Spring Web MVC 接口的详细信息,请参阅Annotated Controllers 。ControllerXSLT 控制器将单词列表转换为准备好转换的简单 XML 文档。

豆子

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

java
@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;
    }
}
科特林
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {

    @Bean
    fun xsltViewResolver() = XsltViewResolver().apply {
        setPrefix("/WEB-INF/xsl/")
        setSuffix(".xslt")
    }
}
控制器

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

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

java
@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";
    }
}
科特林
import org.springframework.ui.set

@Controller
class XsltController {

    @RequestMapping("/")
    fun home(model: Model): String {
        val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
        val root = document.createElement("wordList")

        val words = listOf("Hello", "Spring", "Framework")
        for (word in words) {
            val wordNode = document.createElement("word")
            val textNode = document.createTextNode(word)
            wordNode.appendChild(textNode)
            root.appendChild(wordNode)
        }

        model["wordList"] = root
        return "home"
    }
}

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

有一些可用的软件包可以自动“domify”一个对象图,但是,在 Spring 中,您可以完全灵活地以您选择的任何方式从您的模型创建 DOM。这可以防止 XML 的转换在模型数据的结构中发挥过大的作用,这在使用工具管理 DOM 化过程时是一种危险。

转型

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

以下示例显示了 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.11。MVC 配置

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

有关配置 API 中不可用的更高级自定义,请参阅高级 Java 配置高级 XML 配置

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

1.11.1。启用 MVC 配置

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

java
@Configuration
@EnableWebMvc
public class WebConfig {
}
科特林
@Configuration
@EnableWebMvc
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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

前面的示例注册了许多 Spring MVC 基础设施 bean,并适应类路径上可用的依赖项(例如,JSON、XML 等的有效负载转换器)。

1.11.2。MVC 配置 API

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

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    // Implement configuration methods...
}

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

1.11.3。类型转换

默认情况下,安装了各种数字和日期类型的格式化程序,并支持通过字段@NumberFormat@DateTimeFormat字段进行自定义。

要在 Java 配置中注册自定义格式化程序和转换器,请使用以下命令:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}

要在 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://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>

默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求 Locale。这适用于日期表示为带有“输入”表单字段的字符串的表单。然而,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,可以按如下方式自定义日期和时间格式:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
有关何时使用 FormatterRegistrar 实现的更多信息, FormatterRegistrar 参阅SPI 和。FormattingConversionServiceFactoryBean

1.11.4。验证

默认情况下,如果类路径中存在Bean Validation(例如,Hibernate Validator),则将LocalValidatorFactoryBean其注册为全局Validator ,以便与控制器方法参数一起使用@ValidValidated在控制器方法参数上使用。

在 Java 配置中,您可以自定义全局Validator实例,如以下示例所示:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public Validator getValidator() {
        // ...
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun getValidator(): Validator {
        // ...
    }
}

以下示例显示了如何在 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

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

</beans>

请注意,您还可以Validator在本地注册实现,如以下示例所示:

java
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }
}
科特林
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}
如果您需要在LocalValidatorFactoryBean某处注入,请创建一个 bean 并将其标记@Primary为以避免与 MVC 配置中声明的冲突。

1.11.5。拦截器

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

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/*");
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(LocaleChangeInterceptor())
        registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
        registry.addInterceptor(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.11.6。内容类型

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

默认情况下,仅Accept检查标题。

如果您必须使用基于 URL 的内容类型解析,请考虑使用查询参数策略而不是路径扩展。有关详细信息,请参阅 后缀匹配后缀匹配和 RFD

在 Java 配置中,您可以自定义请求的内容类型解析,如以下示例所示:

java
@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);
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
        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.11.7。消息转换器

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

以下示例添加了自定义的 XML 和 Jackson JSON 转换器, ObjectMapper而不是默认转换器:

java
@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()));
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfiguration : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        val builder = Jackson2ObjectMapperBuilder()
                .indentOutput(true)
                .dateFormat(SimpleDateFormat("yyyy-MM-dd"))
                .modulesToInstall(ParameterNamesModule())
        converters.add(MappingJackson2HttpMessageConverter(builder.build()))
        converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))

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

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

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

启用带有 Jackson XML 支持的缩进需要 woodstox-core-asl 另外一个依赖项jackson-dataformat-xml

其他有趣的 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.11.8。视图控制器

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

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

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addViewController("/").setViewName("home")
    }
}

<mvc:view-controller>以下示例通过使用元素实现与前面示例相同的功能,但使用 XML :

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

如果@RequestMapping方法映射到任何 HTTP 方法的 URL,则视图控制器不能用于处理相同的 URL。这是因为 URL 与带注释的控制器的匹配被认为是端点所有权的足够强的指示,因此可以向客户端发送 405 (METHOD_NOT_ALLOWED)、415 (UNSUPPORTED_MEDIA_TYPE) 或类似响应以帮助调试。出于这个原因,建议避免在带注释的控制器和视图控制器之间拆分 URL 处理。

1.11.9。查看解析器

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

以下 Java 配置示例通过使用 JSP 和 Jackson 作为ViewJSON 呈现的默认值来配置内容协商视图解析:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.jsp();
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.enableContentNegotiation(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 配置中,您可以添加相应的Configurerbean,如以下示例所示:

java
@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;
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.enableContentNegotiation(MappingJackson2JsonView())
        registry.freeMarker().cache(false)
    }

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("/freemarker")
    }
}

1.11.10。静态资源

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

在下一个示例中,给定一个以 开头的请求,/resources相对路径用于查找和提供相对于/publicWeb 应用程序根目录下或类路径下 的静态资源/static。这些资源的服务期限为一年,以确保最大限度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。该Last-Modified信息是从中推导出来的,Resource#lastModified 以便标头支持 HTTP 条件请求"Last-Modified"

以下清单显示了如何使用 Java 配置进行此操作:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
    }
}

以下示例显示了如何在 XML 中实现相同的配置:

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

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

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

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

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }
}

以下示例显示了如何在 XML 中实现相同的配置:

<mvc:resources mapping="/resources/**" location="/public/">
    <mvc:resource-chain resource-cache="true">
        <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,以便可以将其注入其他人。您还可以使用 ResourceUrlEncodingFilterThymeleaf、JSP、FreeMarker 和其他具有依赖HttpServletResponse#encodeURL.

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

WebJars也通过 当库出现在类路径上WebJarsResourceResolver时自动注册来 支持。org.webjars:webjars-locator-core解析器可以重写 URL 以包含 jar 的版本,还可以匹配没有版本的传入 URL——例如 from /jquery/jquery.min.jsto /jquery/1.2.0/jquery.min.js

基于 Java 的配置ResourceHandlerRegistry为细粒度控制提供了更多选项,例如最后修改的行为和优化的资源解析。

1.11.11。默认 Servlet

Spring MVC 允许映射DispatcherServlet/(从而覆盖容器的默认 Servlet 的映射),同时仍然允许由容器的默认 Servlet 处理静态资源请求。它配置一个 DefaultServletHttpRequestHandlerURL 映射/**和相对于其他 URL 映射的最低优先级。

此处理程序将所有请求转发到默认 Servlet。因此,它必须在所有其他 URL 的顺序中保持在最后HandlerMappings。如果您使用<mvc:annotation-driven>. 或者,如果您设置自己的自定义HandlerMapping实例,请确保将其order属性设置为低于 的值DefaultServletHttpRequestHandler,即Integer.MAX_VALUE.

以下示例显示如何使用默认设置启用该功能:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
        configurer.enable()
    }
}

以下示例显示了如何在 XML 中实现相同的配置:

<mvc:default-servlet-handler/>

覆盖/Servlet 映射的警告是RequestDispatcher默认 Servlet 的 必须按名称而不是按路径检索。DefaultServletHttpRequestHandler尝试在启动时自动检测容器的默认 Servlet,使用大多数主要 Servlet 容器(包括 Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)的已知名称列表。 如果默认 Servlet 已使用不同的名称进行自定义配置,或者如果在默认 Servlet 名称未知的情况下使用不同的 Servlet 容器,则必须显式提供默认 Servlet 的名称,如以下示例所示:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable("myCustomDefaultServlet");
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
        configurer.enable("myCustomDefaultServlet")
    }
}

以下示例显示了如何在 XML 中实现相同的配置:

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

1.11.12。路径匹配

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

以下示例显示了如何在 Java 配置中自定义路径匹配:

java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setPatternParser(new PathPatternParser())
            .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
    }

    private PathPatternParser patternParser() {
        // ...
    }
}
科特林
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setPatternParser(patternParser)
            .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
    }

    fun patternParser(): PathPatternParser {
        //...
    }
}

以下示例显示了如何在 XML 中实现相同的配置:

<mvc:annotation-driven>
    <mvc:path-matching
        trailing-slash="false"
        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.11.13。高级 Java 配置

@EnableWebMvc进口DelegatingWebMvcConfiguration,其中:

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

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

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

java
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    // ...
}
科特林
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {

    // ...
}

您可以将现有方法保留在 中WebConfig,但您现在也可以覆盖基类中的 bean 声明,并且您仍然可以WebMvcConfigurer在类路径中拥有任意数量的其他实现。

1.11.14。高级 XML 配置

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

java
@Component
public class MyPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
        // ...
    }
}
科特林
@Component
class MyPostProcessor : BeanPostProcessor {

    override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
        // ...
    }
}

请注意,您需要将其声明MyPostProcessor为 bean,或者在 XML 中显式地声明,或者通过声明让它被检测到<component-scan/>

1.12。HTTP/2

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

Servlet API 确实公开了一种与 HTTP/2 相关的结构。您可以使用 javax.servlet.http.PushBuilder主动向客户端推送资源,并且支持作为方法的方法参数@RequestMapping

2. REST 客户端

本节介绍客户端访问 REST 端点的选项。

2.1。RestTemplate

RestTemplate是执行 HTTP 请求的同步客户端。它是原始的 Spring REST 客户端,并在底层 HTTP 客户端库上公开了一个简单的模板方法 API。

从 5.0 开始,RestTemplate它处于维护模式,只有较小的更改请求和错误被接受。请考虑使用 提供更现代 API 并支持同步、异步和流式传输方案 的WebClient 。

有关详细信息,请参阅REST 端点

2.2.WebClient

WebClient是一个非阻塞的响应式客户端来执行 HTTP 请求。它是在 5.0 中引入的,提供了RestTemplate.

与 相比RestTemplateWebClient支持以下内容:

  • 非阻塞 I/O。

  • Reactive Streams 背压。

  • 高并发,硬件资源少。

  • 利用 Java 8 lambda 的函数式、流畅的 API。

  • 同步和异步交互。

  • 流式传输到服务器或从服务器流式传输。

有关详细信息,请参阅WebClient 。

3. 测试

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

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

  • TestContext 框架:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法有效缓存加载的配置,并支持WebApplicationContext使用MockServletContext. 有关详细信息,请参阅TestContext 框架。

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

  • 客户端 REST:spring-test提供了一个MockRestServiceServer可以用作模拟服务器来测试内部使用RestTemplate. 有关更多详细信息,请参阅客户端 REST 测试

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

4. WebSockets

这部分参考文档涵盖了对 Servlet 堆栈的支持、包含原始 WebSocket 交互的 WebSocket 消息传递、通过 SockJS 的 WebSocket 模拟以及通过 STOMP 作为 WebSocket 上的子协议的发布-订阅消息传递。

4.1。WebSocket 简介

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

WebSocket 交互以 HTTP 请求开始,该请求使用 HTTPUpgrade标头进行升级,或者在这种情况下,切换到 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 Upgrade头。
2 使用Upgrade连接。

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

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

握手成功后,HTTP 升级请求底层的 TCP 套接字保持打开状态,供客户端和服务器继续发送和接收消息。

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

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

4.1.1。HTTP 与 WebSocket

尽管 WebSocket 被设计为与 HTTP 兼容并以 HTTP 请求开始,但重要的是要了解这两种协议会导致非常不同的架构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为多个 URL。为了与应用程序交互,客户端以请求-响应方式访问这些 URL。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。

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

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

Sec-WebSocket-ProtocolWebSocket 客户端和服务器可以通过HTTP 握手请求的标头协商使用更高级别的消息传递协议(例如,STOMP) 。如果没有,他们需要提出自己的约定。

4.1.2. 何时使用 WebSocket

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

例如,新闻、邮件和社交订阅源需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是决定性因素。如果消息量相对较少(例如,监控网络故障),HTTP 流式传输或轮询可以提供有效的解决方案。正是低延迟、高频率和高容量的组合才成为使用 WebSocket 的最佳案例。

还要记住,在 Internet 上,不受您控制的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade标头,或者因为它们关闭了看起来空闲的长期连接。这意味着将 WebSocket 用于防火墙内的内部应用程序是一个比面向公众的应用程序更直接的决定。

4.2. WebSocket API

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

4.2.1。WebSocketHandler

创建 WebSocket 服务器就像实现WebSocketHandler或者更可能是扩展TextWebSocketHandler或一样简单BinaryWebSocketHandler。以下示例使用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 处理程序映射到特定的 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://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。在. WebSocketHandler_ WebSocketHttpRequestHandler

WebSocketHandler直接或间接使用 API 时,例如通过 STOMP消息传递,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话 (JSR-356) 不允许并发发送。一种选择是将 with 包装WebSocketSession起来 ConcurrentWebSocketSessionDecorator

4.2.2. WebSocket 握手

自定义初始 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

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

Spring 提供了一个WebSocketHandlerDecorator基类,你可以用它来装饰一个WebSocketHandler额外的行为。使用 WebSocket Java 配置或 XML 命名空间时,默认提供和添加日志记录和异常处理实现。ExceptionWebSocketHandlerDecorator捕获由任何方法引起的所有未捕获的异常,WebSocketHandler并使用 status 关闭 WebSocket 会话1011,这表示服务器错误。

4.2.3。部署

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

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

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

已经创建了克服 Java WebSocket API 中上述限制的请求,可以在 eclipse-ee4j/websocket-api#211处进行后续处理。Tomcat、Undertow 和 WebSphere 提供了它们自己的 API 替代方案,可以做到这一点,Jetty 也可以。我们希望更多的服务器也能做到这一点。

第二个考虑因素是,支持 JSR-356 的 Servlet 容器预计会执行ServletContainerInitializer(SCI) 扫描,这可能会减慢应用程序的启动速度——在某些情况下会显着降低。如果在升级到支持 JSR-356 的 Servlet 容器版本后观察到显着影响,则应该可以通过使用 中的<absolute-ordering />元素选择性地启用或禁用 Web 片段(和 SCI 扫描) web.xml,如下例所示:

<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
        https://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
        https://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. 服务器配置

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

对于 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>
对于客户端 WebSocket 配置,您应该使用WebSocketContainerFactoryBean (XML) 或ContainerProvider.getWebSocketContainer()(Java 配置)。

对于 Jetty,您需要提供一个预配置的 Jetty并通过您的 WebSocket Java 配置WebSocketServerFactory将其插入 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://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 Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是只接受同源请求。也可以允许所有或指定的来源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户端修改Origin标头值(有关更多详细信息,请参阅 RFC 6454:Web 起源概念)。

三种可能的行为是:

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

  • 允许指定的来源列表:每个允许的来源必须以http:// or开头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("https://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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

4.3. SockJS 后备

在公共 Internet 上,您无法控制的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递Upgrade标头,或者因为它们关闭了看似空闲的长期连接。

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

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

4.3.1。概述

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

SockJS 包括:

  • 以可执行 叙述测试的形式定义的SockJS 协议

  • SockJS JavaScript 客户端 ——一个用于浏览器的客户端库。

  • SockJS 服务器实现,包括 Spring Frameworkspring-websocket模块中的一个。

  • 模块中的 SockJS Java 客户端spring-websocket(从 4.1 版开始)。

SockJS 是为在浏览器中使用而设计的。它使用多种技术来支持各种浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS 客户端页面。传输分为三大类:WebSocket、HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参阅 此博客文章

SockJS 客户端首先发送GET /info以从服务器获取基本信息。之后,它必须决定使用哪种传输方式。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流选项。如果不是,则使用 HTTP(长)轮询。

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

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

在哪里:

  • {server-id}对于在集群中路由请求很有用,但在其他情况下不使用。

  • {session-id}关联属于 SockJS 会话的 HTTP 请求。

  • {transport}指示传输类型(例如 、websocketxhr-streaming)。

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

HTTP 传输需要更多请求。例如,Ajax/XHR 流式传输依赖于对服务器到客户端消息的一个长时间运行的请求以及对客户端到服务器消息的附加 HTTP POST 请求。长轮询与此类似,只是它在每次服务器到客户端发送后结束当前请求。

SockJS 添加了最小的消息框架。例如,服务器o 最初发送字母(“open”帧),消息以a["message1","message2"] (JSON 编码的数组)发送,h如果 25 秒内没有消息流(默认),则发送字母(“heartbeat”帧),然后发送字母c(“关闭”框架)关闭会话。

要了解更多信息,请在浏览器中运行示例并查看 HTTP 请求。SockJS 客户端允许修复传输列表,因此可以一次查看每个传输。SockJS 客户端还提供了一个调试标志,它可以在浏览器控制台中启用有用的消息。在服务器端,您可以 TRACEorg.springframework.web.socket. 有关更多详细信息,请参阅 SockJS 协议 叙述测试

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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://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.

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

4.3.3. IE 8 和 9

Internet Explorer 8 和 9 仍在使用中。它们是拥有 SockJS 的关键原因。本节介绍有关在这些浏览器中运行的重要注意事项。

SockJS 客户端通过使用 Microsoft 的 XDomainRequest. 这适用于跨域,但不支持发送 cookie。Cookies 对于 Java 应用程序通常是必不可少的。然而,由于 SockJS 客户端可以与许多服务器类型(不仅仅是 Java 类型)一起使用,它需要知道 cookie 是否重要。如果是这样,SockJS 客户端更喜欢 Ajax/XHR 进行流式传输。否则,它依赖于基于 iframe 的技术。

SockJS 客户端的第/info一个请求是对可能影响客户端选择传输的信息的请求。这些细节之一是服务器应用程序是否依赖 cookie(例如,出于身份验证目的或使用粘性会话进行集群)。Spring 的 SockJS 支持包括一个名为sessionCookieNeeded. 默认情况下启用它,因为大多数 Java 应用程序都依赖于JSESSIONID cookie。如果你的应用不需要它,你可以关闭这个选项,然后 SockJS 客户端应该xdr-streaming在 IE 8 和 9 中选择。

如果您确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应标头设置X-Frame-OptionsDENYSAMEORIGIN或来指示浏览器阻止在给定页面上使用 IFrame ALLOW-FROM <origin>。这用于防止 点击劫持

Spring Security 3.2+ 支持X-Frame-Options对每个响应进行设置。默认情况下,Spring Security Java 配置将其设置为DENY. 在 3.2 中,Spring Security XML 命名空间默认不设置该标头,但可以配置为这样做。以后可能会默认设置。

有关如何配置标头设置的详细信息,请参阅Spring Security 文档的默认安全X-Frame-Options标头。您还可以查看 gh-2718 了解更多背景信息。

如果您的应用程序添加了X-Frame-Options响应标头(应该如此!)并依赖于基于 iframe 的传输,您需要将标头值设置为 SAMEORIGINor ALLOW-FROM <origin>。Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJS 客户端。将此选项配置为使用与应用程序同源的 URL 是个好主意。

以下示例显示了如何在 Java 配置中执行此操作:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    // ...

}

<websocket:sockjs>XML 命名空间通过元素提供了类似的选项。

在初始开发期间,请启用 SockJS 客户端devel模式,以防止浏览器缓存 SockJS 请求(如 iframe),否则这些请求会被缓存。有关如何启用它的详细信息,请参阅 SockJS 客户端页面。

4.3.4. 心跳

SockJS 协议要求服务器发送心跳消息以阻止代理断定连接已挂起。Spring SockJS 配置有一个名为的属性heartbeatTime,您可以使用它来自定义频率。默认情况下,心跳会在 25 秒后发送,假设在该连接上没有发送其他消息。此 25 秒值符合以下 IETF对公共 Internet 应用程序的建议。

在 WebSocket 和 SockJS 上使用 STOMP 时,如果 STOMP 客户端和服务器协商要交换的心跳,则禁用 SockJS 心跳。

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

4.3.5。客户端断开连接

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

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

一个特定的问题是 Servlet API 不为已离开的客户端提供通知。请参阅eclipse-ee4j/servlet-api#44。但是,Servlet 容器会在后续尝试写入响应时引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认每 25 秒),这意味着通常会在该时间段内检测到客户端断开连接(或者更早,如果消息发送更频繁)。

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

4.3.6. SockJS 和 CORS

如果您允许跨域请求(请参阅Allowed Origins),SockJS 协议使用 CORS 在 XHR 流式传输和轮询传输中提供跨域支持。因此,会自动添加 CORS 标头,除非检测到响应中存在 CORS 标头。因此,如果应用程序已经配置为提供 CORS 支持(例如,通过 Servlet 过滤器),Spring 会SockJsService跳过这一部分。

也可以通过 suppressCors在 Spring 的 SockJsService 中设置属性来禁用这些 CORS 标头的添加。

SockJS 需要以下标头和值:

  • Access-Control-Allow-Origin:从Origin请求头的值初始化。

  • Access-Control-Allow-Credentials:始终设置为true

  • Access-Control-Request-Headers:从等效请求标头中的值初始化。

  • Access-Control-Allow-Methods:传输支持的 HTTP 方法(参见TransportType枚举)。

  • Access-Control-Max-Age:设置为 31536000(1 年)。

具体实现见源代码中的addCorsHeadersinAbstractSockJsServiceTransportTypeenum 。

或者,如果 CORS 配置允许,请考虑排除带有 SockJS 端点前缀的 URL,从而让 SpringSockJsService处理它。

4.3.7.SockJsClient

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

SockJS Java 客户端支持websocketxhr-streamingxhr-polling 传输。其余的仅对在浏览器中使用有意义。

您可以配置WebSocketTransport

  • StandardWebSocketClient在 JSR-356 运行时中。

  • JettyWebSocketClient通过使用 Jetty 9+ 本机 WebSocket API。

  • Spring 的任何实现WebSocketClient

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

  • RestTemplateXhrTransport使用 Spring 的RestTemplateHTTP 请求。

  • JettyXhrTransport使用 Jetty 的HttpClientHTTP 请求。

以下示例显示了如何创建 SockJS 客户端并连接到 SockJS 端点:

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

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS 使用 JSON 格式的数组来存储消息。默认情况下,使用 Jackson 2 并且需要在类路径上。或者,您可以配置自定义实现 SockJsMessageCodec并在SockJsClient.

要用于SockJsClient模拟大量并发用户,您需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。以下示例显示了如何使用 Jetty 执行此操作:

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

以下示例显示了您还应该考虑自定义的服务器端 SockJS 相关属性(有关详细信息,请参阅 javadoc):

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/sockjs").withSockJS()
            .setStreamBytesLimit(512 * 1024) (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. 跺脚

WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了客户端和服务器协商子协议(即更高级别的消息传递协议)的机制,用于在 WebSocket 之上定义每个可以发送什么样的消息,格式是什么,内容每条消息,依此类推。子协议的使用是可选的,但无论哪种方式,客户端和服务器都需要就定义消息内容的某些协议达成一致。

4.4.1。概述

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

STOMP 是一种基于帧的协议,其帧以 HTTP 为模型。以下清单显示了 STOMP 框架的结构:

命令
标头1:值1
标头2:值2

正文^@

客户端可以使用SENDorSUBSCRIBE命令发送或订阅消息,以及destination描述消息内容和谁应该接收消息的标题。这启用了一个简单的发布-订阅机制,您可以使用该机制通过代理向其他连接的客户端发送消息,或向服务器发送消息以请求执行某些工作。

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

以下示例显示了一个订阅接收股票报价的客户端,服务器可能会定期发出这些报价(例如,通过一个通过 aSimpMessagingTemplate向代理发送消息的计划任务):

订阅
编号:sub-1
目的地:/topic/price.stock.*

^@

以下示例显示了发送交易请求的客户端,服务器可以通过@MessageMapping方法处理该请求:

发送
目的地:/队列/交易
内容类型:应用程序/json
内容长度:44

{“行动”:“买入”,“股票代码”:“MMM”,“股票”,44}^@

执行后,服务器可以向客户端广播交易确认消息和详细信息。

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

STOMP 服务器可以使用该MESSAGE命令向所有订阅者广播消息。以下示例显示了服务器向订阅的客户端发送股票报价:

信息
消息 ID:nxahklf6-1
订阅:sub-1
目的地:/topic/price.stock.MMM

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

服务器不能发送未经请求的消息。来自服务器的所有消息都必须响应特定的客户端订阅,并且 subscription-id服务器消息的标头必须与id客户端订阅的标头匹配。

前面的概述旨在提供对 STOMP 协议的最基本的理解。我们建议全面审查协议 规范

4.4.2. 好处

使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 提供比使用原始 WebSockets 更丰富的编程模型。关于 HTTP 与原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,可以提出相同的观点。以下是福利清单:

  • 无需发明自定义消息传递协议和消息格式。

  • STOMP 客户端,包括 Spring 框架中的Java 客户端,都可用。

  • 您可以(可选)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来管理订阅和广播消息。

  • 应用程序逻辑可以组织在任意数量的@Controller实例中,并且可以根据 STOMP 目标标头将消息路由到它们,而不是使用单个WebSocketHandler给定连接处理原始 WebSocket 消息。

  • 您可以使用 Spring Security 来保护基于 STOMP 目标和消息类型的消息。

4.4.3. 启用 STOMP

STOMP over WebSocket 支持在spring-messagingspring-websocket模块中可用。一旦有了这些依赖项,就可以通过 WebSocket 和SockJS Fallback公开一个 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是端点的 HTTP URL,WebSocket(或 SockJS)客户端需要连接到该端点以进行 WebSocket 握手。
2 目标标头以开头的 STOMP 消息/app被路由到 类中的@MessageMapping方法@Controller
3 使用内置消息代理进行订阅和广播,并将目标标头以代理开头的消息路由/topic `or `/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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>
对于内置的简单代理,/topic/queue前缀没有任何特殊含义。它们只是区分 pub-sub 与点对点消息传递(即多个订阅者与一个消费者)的约定。当您使用外部代理时,请查看代理的 STOMP 页面以了解其支持的 STOMP 目的地和前缀类型。

要从浏览器连接,对于 SockJS,您可以使用 sockjs-client. 对于 STOMP,许多应用程序都使用了jmesnil/stomp-websocket库(也称为 stomp.js),该库功能齐全,已在生产中使用多年,但不再维护。目前, JSteunou/webstomp-client是该库最积极维护和发展的继任者。以下示例代码基于它:

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

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

或者,如果您通过 WebSocket(没有 SockJS)连接,您可以使用以下代码:

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

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

请注意,stompClient在前面的示例中不需要指定loginpasscode标头。即使这样做了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参阅连接到代理 和身份验证。

有关更多示例代码,请参见:

4.4.4. WebSocket 服务器

要配置底层 WebSocket 服务器, 服务器配置中的信息适用。但是,对于 Jetty,您需要通过以下方式设置HandshakeHandler和:WebSocketPolicyStompEndpointRegistry

@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 应用程序就会成为连接客户端的 STOMP 代理。本节介绍服务器端的消息流。

该模块包含对源自Spring Integrationspring-messaging的消息传递应用程序的基础支持,后来被提取并合并到 Spring Framework 中,以便在许多 Spring 项目和应用程序场景中更广泛地使用。以下列表简要描述了一些可用的消息传递抽象:

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

消息流简单代理

上图显示了三个消息通道:

  • clientInboundChannel:用于传递从 WebSocket 客户端接收到的消息。

  • clientOutboundChannel:用于向 WebSocket 客户端发送服务器消息。

  • brokerChannel:用于从服务器端应用程序代码中向消息代理发送消息。

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

消息流代理中继

前面两个图的主要区别在于使用“代理中继”通过 TCP 将消息向上传递到外部 STOMP 代理,并将消息从代理向下传递到订阅的客户端。

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

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

我们可以通过一个简单的例子来追踪流程。考虑以下示例,它设置了一个服务器:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

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

@Controller
public class GreetingController {

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

前面的示例支持以下流程:

  1. 客户端连接到http://localhost:8080/portfolio,一旦建立 WebSocket 连接,STOMP 帧开始在其上流动。

  2. 客户端发送一个 SUBSCRIBE 帧,其目标标头为/topic/greeting. 一旦接收到并解码,消息就会被发送到,clientInboundChannel然后被路由到存储客户端订阅的消息代理。

  3. 客户端发送一个 SEND 帧到/app/greeting. 前缀有助于将其/app路由到带注释的控制器。/app去除前缀后,/greeting 目标的剩余部分映射@MessageMappingGreetingController.

  4. 从返回的值GreetingController转换为Message具有基于返回值和默认目标标头的有效负载 的 Spring /topic/greeting(从输入目标派生,/app替换为 /topic)。生成的消息被发送到brokerChannel消息代理并由消息代理处理。

  5. 消息代理找到所有匹配的订阅者并通过 向每个订阅者发送一个 MESSAGE 帧clientOutboundChannel,从那里消息被编码为 STOMP 帧并在 WebSocket 连接上发送。

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

4.4.6。带注释的控制器

应用程序可以使用带注释@Controller的类来处理来自客户端的消息。此类类可以声明@MessageMapping@SubscribeMapping@ExceptionHandler 方法,如以下主题中所述:

@MessageMapping

您可以使用@MessageMapping注释根据目的地路由消息的方法。它在方法级别和类型级别都受支持。在类型级别,@MessageMapping用于表示控制器中所有方法的共享映射。

默认情况下,映射值是 Ant 样式的路径模式(例如/thing*/thing/**),包括对模板变量的支持(例如,/thing/{id})。这些值可以通过@DestinationVariable方法参数引用。应用程序还可以切换到点分隔的映射目标约定,如 Dots as Separators中所述。

支持的方法参数

下表描述了方法参数:

方法参数 描述

Message

用于访问完整的消息。

MessageHeaders

用于访问Message.

MessageHeaderAccessor, SimpMessageHeaderAccessor, 和StompHeaderAccessor

通过类型化的访问器方法访问标题。

@Payload

用于访问消息的有效负载,由配置的 MessageConverter.

不需要此注释的存在,因为默认情况下,如果没有其他参数匹配,则假定它。

@javax.validation.Valid您可以使用或 Spring注释有效负载参数@Validated,以自动验证有效负载参数。

@Header

用于访问特定标头值 - 以及使用 的类型转换 org.springframework.core.convert.converter.Converter(如有必要)。

@Headers

用于访问消息中的所有标头。此参数必须可分配给 java.util.Map

@DestinationVariable

用于访问从消息目标中提取的模板变量。必要时将值转换为声明的方法参数类型。

java.security.Principal

反映在 WebSocket HTTP 握手时登录的用户。

返回值

默认情况下,@MessageMapping方法的返回值通过匹配序列化为有效负载,MessageConverter并作为 a 发送MessagebrokerChannel,从那里广播给订阅者。出站消息的目的地与入站消息的目的地相同,但前缀为/topic

您可以使用@SendTo@SendToUser注释来自定义输出消息的目的地。@SendTo用于自定义目标目的地或指定多个目的地。@SendToUser用于将输出消息定向到仅与输入消息关联的用户。请参阅用户目的地

您可以在同一方法上同时使用两者,并且两者都在类级别受支持,在这种情况下,它们充当类中方法的默认值@SendTo@SendToUser但是,请记住,任何方法级别@SendTo@SendToUser注释都会覆盖类级别的任何此类注释。

消息可以异步处理,@MessageMapping方法可以返回 ListenableFutureCompletableFutureCompletionStage

请注意,@SendTo@SendToUser只是一种方便,相当于使用 SimpMessagingTemplate发送消息。如有必要,对于更高级的场景, @MessageMapping方法可以SimpMessagingTemplate直接使用。这可以代替返回值,或者可能作为返回值的补充。请参阅发送消息

@SubscribeMapping

@SubscribeMapping类似于@MessageMapping但仅将映射缩小到订阅消息。它支持 . @MessageMapping但是对于返回值,默认情况下,消息直接发送到客户端(通过 clientOutboundChannel,响应订阅)而不是代理(通过 brokerChannel,作为匹配订阅的广播)。添加@SendTo@SendToUser覆盖此行为并改为发送到代理。

这什么时候有用?假设代理映射到/topic/queue,而应用程序控制器映射到/app。在此设置中,代理存储所有订阅/topic/queue用于重复广播的订阅,应用程序无需参与其中。客户端也可以订阅某个/app目的地,并且控制器可以返回一个值来响应该订阅,而不涉及代理,而无需再次存储或使用订阅(实际上是一次性请求-回复交换)。一个用例是在启动时使用初始数据填充 UI。

这什么时候没用?不要尝试将代理和控制器映射到相同的目标前缀,除非出于某种原因您希望两者独立处理消息,包括订阅。入站消息是并行处理的。不能保证代理或控制器是否首先处理给定的消息。如果目标是在订阅被存储并准备好广播时得到通知,如果服务器支持它,客户端应该要求收据(简单的代理不支持)。例如,使用 Java STOMP 客户端,您可以执行以下操作来添加收据:

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

服务器端选项是注册一个 ExecutorChannelInterceptorbrokerChannel实现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. 发送消息

如果您想从应用程序的任何部分向连接的客户端发送消息怎么办?任何应用程序组件都可以向brokerChannel. 最简单的方法是注入 aSimpMessagingTemplate并使用它来发送消息。通常,您将按类型注入它,如以下示例所示:

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

}

brokerMessagingTemplate但是,如果存在另一个相同类型的 bean ,您也可以通过它的名称 ( ) 来限定它。

4.4.8。简单经纪人

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

应用程序还可以使用点分隔(而不是斜线分隔)的目标。请参阅点作为分隔符

如果配置了任务调度程序,则简单代理支持 STOMP 心跳。要配置调度程序,您可以声明自己的TaskSchedulerbean 并通过MessageBrokerRegistry. 或者,您可以使用内置 WebSocket 配置中自动声明的配置,但是,您需要@Lazy避免内置 WebSocket 配置和 WebSocketMessageBrokerConfigurer. 例如:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private TaskScheduler messageBrokerTaskScheduler;

    @Autowired
    public void setMessageBrokerTaskScheduler(@Lazy 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。外部经纪人

简单代理非常适合入门,但仅支持 STOMP 命令的子集(它不支持确认、收据和其他一些功能),依赖于简单的消息发送循环,不适合集群。作为替代方案,您可以升级应用程序以使用功能齐全的消息代理。

请参阅您选择的消息代理(例如 RabbitMQActiveMQ等)的 STOMP 文档,安装代理,并在启用 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://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 会话将从代理接收到的所有消息转发给客户端。本质上,它充当双向转发消息的“中继”。

为您的项目添加io.projectreactor.netty:reactor-nettyio.netty:netty-all 依赖项以进行 TCP 连接管理。

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

实际上,代理中继支持健壮且可扩展的消息广播。

4.4.10。连接到经纪人

STOMP 代理中继维护与代理的单个“系统”TCP 连接。此连接仅用于来自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置 STOMP 凭据(即 STOMP 帧login和标头)。passcode这在 XML 命名空间和 Java 配置中都公开为systemLoginsystemPasscode属性,默认值为guestguest

STOMP 代理中继还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接。您可以配置用于代表客户端创建的所有 TCP 连接的 STOMP 凭据。这在 XML 命名空间和 Java 配置中都公开为clientLoginclientPasscode属性,默认值为guestguest

STOMP 代理中继总是在 它代表客户端转发给代理的每一帧上设置login和标头。因此,WebSocket 客户端不需要设置这些标头。他们被忽略了。正如身份验证 部分所解释的,WebSocket 客户端应该依赖 HTTP 身份验证来保护 WebSocket 端点并建立客户端身份。 passcodeCONNECT

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

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

默认情况下,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 代理中继。此属性的值设置为host每个CONNECT帧的标头,并且可能很有用(例如,在建立 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
                https://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/websocket
                https://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("red")
public class RedController {

    @MessageMapping("blue.{green}")
    public void handleGreen(@DestinationVariable String green) {
        // ...
    }
}

客户端现在可以向/app/red.blue.green123.

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

另一方面,“简单代理”确实依赖于已配置的PathMatcher,因此,如果您切换分隔符,该更改也适用于代理以及代理将目标从消息匹配到订阅模式的方式。

4.4.12。验证

每个 STOMP over WebSocket 消息会话都以 HTTP 请求开始。这可以是升级到 WebSocket 的请求(即 WebSocket 握手),或者在 SockJS 回退的情况下,是一系列 SockJS HTTP 传输请求。

许多 Web 应用程序已经具备身份验证和授权来保护 HTTP 请求。通常,用户通过 Spring Security 使用某种机制进行身份验证,例如登录页面、HTTP 基本身份验证或其他方式。经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一基于 cookie 的会话中的后续请求相关联。

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求,通常已经有一个经过身份验证的用户可以通过 HttpServletRequest#getUserPrincipal(). Spring 自动将该用户与为他们创建的 WebSocket 或 SockJS 会话相关联,随后,通过用户标头与通过该会话传输的所有 STOMP 消息相关联。

简而言之,一个典型的 Web 应用程序除了已经为安全所做的事情之外,什么都不需要做。用户在 HTTP 请求级别使用安全上下文进行身份验证,该安全上下文通过基于 cookie 的 HTTP 会话(然后与为该用户创建的 WebSocket 或 SockJS 会话相关联)维护,并导致在每次Message流过时标记用户标头应用程序。

STOMP 协议确实在帧上有loginpasscode标头。CONNECT这些最初是为 STOMP over TCP 设计的并且是必需的。但是,对于基于 WebSocket 的 STOMP,默认情况下,Spring 会忽略 STOMP 协议级别的身份验证标头,并假定用户已经在 HTTP 传输级别进行身份验证。期望 WebSocket 或 SockJS 会话包含经过身份验证的用户。

4.4.13。令牌认证

Spring Security OAuth 提供对基于令牌的安全性的支持,包括 JSON Web Token (JWT)。您可以将其用作 Web 应用程序中的身份验证机制,包括 STOMP over WebSocket 交互,如上一节所述(即通过基于 cookie 的会话来维护身份)。

同时,基于 cookie 的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中或在通常使用标头进行身份验证的移动应用程序中)。

WebSocket 协议 RFC 6455 “没有规定服务器可以在 WebSocket 握手期间对客户端进行身份验证的任何特定方式。” 然而,在实践中,浏览器客户端只能使用标准身份验证标头(即基本 HTTP 身份验证)或 cookie,并且不能(例如)提供自定义标头。同样,SockJS JavaScript 客户端不提供通过 SockJS 传输请求发送 HTTP 标头的方法。请参阅 sockjs-client 问题 196。相反,它确实允许发送可用于发送令牌的查询参数,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。

上述限制适用于基于浏览器的客户端,不适用于基于 Spring Java 的 STOMP 客户端,后者支持使用 WebSocket 和 SockJS 请求发送标头。

因此,希望避免使用 cookie 的应用程序可能没有任何好的替代方案来进行 HTTP 协议级别的身份验证。他们可能更愿意使用 STOMP 消息传递协议级别的标头进行身份验证,而不是使用 cookie。这样做需要两个简单的步骤:

  1. 使用 STOMP 客户端在连接时传递身份验证标头。

  2. 使用ChannelInterceptor.

下一个示例使用服务器端配置来注册自定义身份验证拦截器。请注意,拦截器只需要在 CONNECT 上进行身份验证和设置用户标头Message。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 的消息授权时,目前,您需要确保身份验证ChannelInterceptor配置在 Spring Security 之前排序。这最好通过在其自己的实现中声明自定义拦截器来完成WebSocketMessageBrokerConfigurer,并用 标记 @Order(Ordered.HIGHEST_PRECEDENCE + 99)

4.4.14。授权

Spring Security 提供了 WebSocket 子协议授权 ,它使用 aChannelInterceptor根据消息中的用户标头对消息进行授权。此外,Spring Session 提供 WebSocket 集成 ,可确保用户的 HTTP 会话在 WebSocket 会话仍处于活动状态时不会过期。

4.4.15。用户目的地

应用程序可以发送针对特定用户的消息,并且 Spring 的 STOMP 支持识别带有前缀的目的地/user/。例如,客户端可能订阅/user/queue/position-updates目的地。 UserDestinationMessageHandler处理此目的地并将其转换为用户会话独有的目的地(例如/queue/position-updates-user123)。这为订阅通用命名的目的地提供了便利,同时确保不会与订阅同一目的地的其他用户发生冲突,以便每个用户都可以接收到唯一的库存头寸更新。

使用用户目的地时,重要的是配置代理和应用程序目的地前缀,如启用 STOMP中所示,否则代理将处理仅应由 处理的“/user”前缀消息 UserDestinationMessageHandler

在发送端,消息可以被发送到一个目的地,例如 /user/{username}/queue/position-updates,该目的地又被转换UserDestinationMessageHandler成一个或多个目的地,一个用于与用户关联的每个会话。这使得应用程序中的任何组件都可以发送针对特定用户的消息,而不必知道除了他们的名称和通用目的地之外的任何信息。这也通过注释和消息传递模板得到支持。

消息处理方法可以通过注解将消息发送给与正在处理的消息相关联的用户@SendToUser(在类级别也支持共享一个公共目的地),如以下示例所示:

@Controller
public class PortfolioController {

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

如果用户有多个会话,默认情况下,订阅给定目标的所有会话都是目标。但是,有时可能需要仅针对发送正在处理的消息的会话。您可以通过将broadcast属性设置为 false 来执行此操作,如以下示例所示:

@Controller
public class MyController {

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

    @MessageExceptionHandler
    @SendToUser(destinations="/queue/errors", broadcast=false)
    public ApplicationError handleException(MyBusinessException exception) {
        // ...
        return appError;
    }
}
虽然用户目的地通常意味着经过身份验证的用户,但这并不是严格要求的。未与经过身份验证的用户关联的 WebSocket 会话可以订阅用户目的地。在这种情况下,@SendToUser注释的行为与 with 完全相同broadcast=false(即,仅针对发送正在处理的消息的会话)。

您可以从任何应用程序组件向用户目的地发送消息,例如,注入SimpMessagingTemplate由 Java 配置或 XML 命名空间创建的消息。brokerMessagingTemplate(如果需要使用 bean 进行限定,则需要使用bean 名称@Qualifier。)以下示例显示了如何执行此操作:

@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());
    }
}
当您将用户目的地与外部消息代理一起使用时,您应该查看代理文档以了解如何管理非活动队列,以便在用户会话结束时删除所有唯一的用户队列。例如,当您使用诸如/exchange/amq.direct/position-updates. 因此,在这种情况下,客户端可以订阅/user/exchange/amq.direct/position-updates. 同样,ActiveMQ 具有 用于清除非活动目标的 配置选项。

在多应用服务器场景中,用户目的地可能仍未解决,因为用户连接到不同的服务器。在这种情况下,您可以将目标配置为广播未解析的消息,以便其他服务器有机会尝试。这可以通过Java 配置中的userDestinationBroadcast属性 和XML 中元素的属性来完成。MessageBrokerRegistryuser-destination-broadcastmessage-broker

4.4.16。消息顺序

来自代理的消息被发布到clientOutboundChannel, 从那里它们被写入 WebSocket 会话。由于通道由 a 支持ThreadPoolExecutor,消息在不同的线程中处理,客户端收到的结果序列可能与发布的确切顺序不匹配。

如果这是一个问题,请启用该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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker preserve-publish-order="true">
        <!-- ... -->
    </websocket:message-broker>

</beans>

设置该标志时,同一客户端会话中的消息一次发布到 clientOutboundChannel一个,从而保证发布的顺序。请注意,这会产生很小的性能开销,因此您应该仅在需要时启用它。

4.4.17。活动

发布了几个ApplicationContext事件,可以通过实现 Spring 的ApplicationListener接口来接收:

  • BrokerAvailabilityEvent:指示代理何时可用或不可用。虽然“简单”代理在启动时立即可用并在应用程序运行时保持可用,但 STOMP“代理中继”可能会失去与全功能代理的连接(例如,如果代理重新启动)。代理中继具有重新连接逻辑,并在它返回时重新建立与代理的“系统”连接。因此,只要状态从已连接变为已断开,反之亦然,就会发布此事件。使用 的组件SimpMessagingTemplate应订阅此事件,并避免在代理不可用时发送消息。无论如何,他们应该准备好在MessageDeliveryException 发送消息时进行处理。

  • SessionConnectEvent:在收到新的 STOMP CONNECT 以指示新的客户端会话开始时发布。该事件包含表示连接的消息,包括会话 ID、用户信息(如果有)和客户端发送的任何自定义标头。这对于跟踪客户端会话很有用。订阅此事件的组件可以使用SimpMessageHeaderAccessor或 包装包含的消息StompMessageHeaderAccessor

  • SessionConnectedEventSessionConnectEvent:在代理发送 STOMP CONNECTED 帧以响应 CONNECT后不久发布。此时,可以认为 STOMP 会话已完全建立。

  • SessionSubscribeEvent:在收到新的 STOMP SUBSCRIBE 时发布。

  • SessionUnsubscribeEvent:在收到新的 STOMP UNSUBSCRIBE 时发布。

  • SessionDisconnectEvent:在 STOMP 会话结束时发布。DISCONNECT 可能是从客户端发送的,也可能是在 WebSocket 会话关闭时自动生成的。在某些情况下,此事件在每个会话中发布不止一次。组件对于多个断开连接事件应该是幂等的。

当您使用全功能代理时,如果代理暂时不可用,STOMP“代理中继”会自动重新连接“系统”连接。但是,客户端连接不会自动重新连接。假设启用了心跳,客户端通常会在 10 秒内注意到代理没有响应。客户端需要实现自己的重新连接逻辑。

4.4.18。拦截

事件为 STOMP 连接的生命周期提供通知,但不是为每个客户端消息提供通知。应用程序还可以注册一个 ChannelInterceptor来拦截任何消息和处理链的任何部分。以下示例显示如何拦截来自客户端的入站消息:

@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对于发送到通道的每条消息都会调用一次 a,但 在每个 订阅来自通道的消息ExecutorChannelInterceptor的线程中提供挂钩。MessageHandler

请注意,与SessionDisconnectEvent前面描述的一样,DISCONNECT 消息可以来自客户端,也可以在 WebSocket 会话关闭时自动生成。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。组件对于多个断开连接事件应该是幂等的。

4.4.19。STOMP 客户端

Spring 提供了一个 STOMP over WebSocket 客户端和一个 STOMP over TCP 客户端。

首先,您可以创建和配置WebSocketStompClient,如以下示例所示:

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

在前面的示例中,您可以替换StandardWebSocketClientSockJsClient,因为这也是WebSocketClient. 可以使用 WebSocket 或基于 HTTP的SockJsClient传输作为后备。有关详细信息,请参阅 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 心跳,您可以配置WebSocketStompClientTaskScheduler 可选地自定义心跳间隔(写入不活动 10 秒,这会导致发送心跳,读取不活动 10 秒,这会关闭连接)。

WebSocketStompClient仅在不活动的情况下(即没有发送其他消息时)发送心跳。这在使用外部代理时可能会带来挑战,因为具有非代理目的地的消息代表活动,但实际上并未转发到代理。在这种情况下,您可以TaskScheduler 在初始化外部代理时配置一个,以确保在仅发送具有非代理目的地的消息时也将心跳转发到代理。

当您WebSocketStompClient用于性能测试以模拟来自同一台机器的数千个客户端时,请考虑关闭心跳,因为每个连接都会安排自己的心跳任务,并且没有针对在同一台机器上运行的大量客户端进行优化。

STOMP 协议还支持收据,其中客户端必须添加一个receipt 标头,服务器在处理发送或订阅后以 RECEIPT 帧响应该标头。为了支持这一点,会导致在每个后续发送或订阅事件中添加标头的StompSession提议 。或者,您也可以手动将收据标题添加到. 发送和订阅都返回一个实例 ,您可以使用它来注册接收成功和失败回调。对于此功能,您必须为客户端配置 a 和收据过期前的时间量(默认为 15 秒)。setAutoReceipt(boolean)receiptStompHeadersReceiptableTaskScheduler

请注意,StompSessionHandler它本身是一个StompFrameHandler,除了handleException处理消息的异常和handleTransportError传输级错误(包括ConnectionLostException.

4.4.20。WebSocket 范围

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

@Controller
public class MyController {

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

websocket你可以在作用域中声明一个 Spring 管理的 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
    }
}

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

4.4.21。表现

在性能方面没有灵丹妙药。影响它的因素很多,包括消息的大小和数量,应用方法是否执行需要阻塞的工作,以及外部因素(如网络速度和其他问题)。本节的目标是提供可用配置选项的概述以及有关如何推理扩展的一些想法。

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

clientInboundChannel显而易见的起点是配置支持 clientOutboundChannel. 默认情况下,两者都配置为可用处理器数量的两倍。

如果注释方法中的消息处理主要受 CPU 限制,则线程数clientInboundChannel应保持接近处理器数。如果他们所做的工作更受 IO 限制,并且需要在数据库或其他外部系统上阻塞或等待,则可能需要增加线程池大小。

ThreadPoolExecutor具有三个重要属性:核心线程池大小、最大线程池大小和队列存储没有可用线程的任务的容量。

一个常见的混淆点是配置核心池大小(例如,10)和最大池大小(例如,20)会导致线程池具有 10 到 20 个线程。事实上,如果将容量保留为其默认值 Integer.MAX_VALUE,则线程池的大小永远不会超过核心池大小,因为所有额外的任务都会排队。

请参阅的 javadocThreadPoolExecutor了解这些属性如何工作并了解各种排队策略。

另一方面clientOutboundChannel,这完全是关于向 WebSocket 客户端发送消息。如果客户端位于快速网络上,则线程数应保持接近可用处理器的数量。如果它们速度慢或带宽低,它们会花费更长的时间来消费消息并给线程池带来负担。因此,增加线程池大小变得很有必要。

虽然clientInboundChannel可以预测的工作负载 - 毕竟,它是基于应用程序所做的 - 如何配置“clientOutboundChannel”更难,因为它基于应用程序无法控制的因素。出于这个原因,另外两个属性与消息的发送有关:sendTimeLimitsendBufferSizeLimit。您可以使用这些方法来配置允许发送多长时间以及在向客户端发送消息时可以缓冲多少数据。

一般的想法是,在任何给定时间,只有一个线程可以用于发送到客户端。同时,所有其他消息都会被缓冲,您可以使用这些属性来决定允许发送消息需要多长时间以及同时可以缓冲多少数据。有关重要的附加详细信息,请参阅 XML 模式的 javadoc 和文档。

以下示例显示了一种可能的配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    // ...

}

以下示例显示了与前面示例等效的 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://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。出于这个原因,STOMP 客户端(例如 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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

关于扩展的一个重点涉及使用多个应用程序实例。目前,您不能使用简单的代理来做到这一点。但是,当您使用功能齐全的代理(例如 RabbitMQ)时,每个应用程序实例都连接到代理程序,并且从一个应用程序实例广播的消息可以通过代理程序广播到通过任何其他应用程序实例连接的 WebSocket 客户端。

4.4.22。监控

当您使用@EnableWebSocketMessageBrokeror<websocket:message-broker>时,关键的基础架构组件会自动收集统计数据和计数器,从而提供对应用程序内部状态的重要洞察。该配置还声明了一个 bean 类型WebSocketMessageBrokerStats,该 bean 将所有可用信息收集在一个地方,默认情况下INFO每 30 分钟在该级别记录一次。这个 bean 可以通过 Spring 导出到 JMX MBeanExporter以便在运行时查看(例如,通过 JDK jconsole)。以下列表总结了可用信息:

客户端 WebSocket 会话
当前的

指示当前有多少客户端会话,计数进一步细分为 WebSocket 与 HTTP 流和轮询 SockJS 会话。

全部的

指示已建立的总会话数。

异常关闭
连接失败

已建立但在 60 秒内未收到任何消息后关闭的会话。这通常表示代理或网络问题。

超出发送限制

会话在超过配置的发送超时或发送缓冲区限制后关闭,这可能发生在慢速客户端(请参阅上一节)。

传输错误

传输错误后关闭会话,例如无法读取或写入 WebSocket 连接或 HTTP 请求或响应。

STOMP 框架

处理的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数,指示在 STOMP 级别上连接的客户端数量。请注意,当会话异常关闭或客户端在未发送 DISCONNECT 帧的情况下关闭时,DISCONNECT 计数可能会较低。

STOMP 经纪人中继
TCP 连接

指示代表客户端 WebSocket 会话建立到代理的 TCP 连接数。这应该等于客户端 WebSocket 会话的数量 + 1 个额外的共享“系统”连接,用于从应用程序内部发送消息。

STOMP 框架

代表客户端转发到代理或从代理接收的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数。请注意,无论客户端 WebSocket 会话如何关闭,都会向代理发送 DISCONNECT 帧。因此,较低的 DISCONNECT 帧计数表明代理正在主动关闭连接(可能是因为心跳未及时到达、输入帧无效或其他问题)。

客户入站渠道

来自支持 的线程池的统计信息clientInboundChannel ,可以深入了解传入消息处理的健康状况。在这里排队的任务表明应用程序可能太慢而无法处理消息。如果存在 I/O 绑定任务(例如,慢速数据库查询、对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。

客户外呼渠道

来自支持的线程池的统计信息clientOutboundChannel ,可以深入了解向客户端广播消息的健康状况。在这里排队的任务表明客户端太慢而无法消费消息。解决此问题的一种方法是增加线程池大小以适应预期的并发慢速客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(参见上一节)。

SockJS 任务调度器

来自用于发送心跳的 SockJS 任务调度程序的线程池的统计信息。请注意,当在 STOMP 级别协商心跳时,SockJS 心跳被禁用。

4.4.23。测试

当您使用 Spring 的 STOMP-over-WebSocket 支持时,有两种主要方法可以测试应用程序。首先是编写服务器端测试来验证控制器的功能及其带注释的消息处理方法。第二个是编写涉及运行客户端和服务器的完整端到端测试。

这两种方法并不相互排斥。相反,每个都在整体测试策略中占有一席之地。服务器端测试更专注,更容易编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地涉及编写和维护。

服务器端测试的最简单形式是编写控制器单元测试。然而,这还不够有用,因为控制器所做的大部分工作都取决于它的注释。纯单元测试根本无法测试。

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

  • 借助 Spring TestContext 框架加载实际的 Spring 配置,clientInboundChannel作为测试字段注入,并使用它发送消息以由控制器方法处理。

  • 手动设置调用控制器(即SimpAnnotationMethodMessageHandler)所需的最小 Spring 框架基础结构并将控制器的消息直接传递给它。

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

第二种方法是创建端到端集成测试。为此,您需要以嵌入式模式运行 WebSocket 服务器并作为 WebSocket 客户端连接到它,该客户端发送包含 STOMP 帧的 WebSocket 消息。股票投资组合示例应用程序的测试 还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单 STOMP 客户端来演示这种方法。

5. 其他 Web 框架

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

Spring Framework 的核心价值主张之一是支持 选择。一般来说,Spring 不会强迫您使用或购买任何特定的架构、技术或方法(尽管它肯定会推荐一些而不是其他的)。这种选择与开发人员及其开发团队最相关的架构、技术或方法的自由可以说在 Web 领域最为明显,Spring 提供了自己的 Web 框架(Spring MVCSpring WebFlux),而在同时,支持与众多流行的第三方Web框架集成。

5.1。常用配置

在深入了解每个受支持的 Web 框架的集成细节之前,让我们先看一下不特定于任何 Web 框架的常见 Spring 配置。(本节同样适用于 Spring 自己的 Web 框架变体。)

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

进入细节,您需要做的就是 在您的 Web 应用程序 ContextLoaderListener 的标准 Java EE servlet文件中声明 a 并添加一个<context-param/> 部分(在同一文件中),该部分定义了哪一组 Spring XML 配置文件加载。web.xmlcontextConfigLocation

考虑以下<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 会 WebApplicationContext 根据 bean 定义创建一个对象并将其存储在ServletContextWeb 应用程序中。

所有 Java Web 框架都构建在 Servlet API 之上,因此您可以使用以下代码片段来访问ApplicationContextContextLoaderListener.

以下示例显示了如何获取WebApplicationContext

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

该类 WebApplicationContextUtils 是为了方便起见,因此您无需记住ServletContext 属性的名称。如果键下不存在对象,则其getWebApplicationContext()方法返回 。与其冒险进入您的应用程序,不如使用该方法。当缺少时,此方法会引发异常。nullWebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTENullPointerExceptionsgetRequiredWebApplicationContext()ApplicationContext

获得对 的引用后WebApplicationContext,您可以按名称或类型检索 bean。大多数开发人员按名称检索 bean,然后将它们转换为他们实现的接口之一。

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

5.2. JSF

JavaServer Faces (JSF) 是 JCP 的基于标准组件的、事件驱动的 Web 用户界面框架。它是 Java EE 保护伞的官方部分,但也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。

请注意,最新版本的 JSF 与应用服务器中的 CDI 基础结构密切相关,一些新的 JSF 功能只能在这样的环境中工作。Spring 的 JSF 支持不再积极发展,主要用于在对基于 JSF 的旧应用程序进行现代化改造时进行迁移。

Spring 的 JSF 集成中的关键元素是 JSFELResolver机制。

5.2.1。Spring Bean 解析器

SpringBeanFacesELResolver是一个符合 JSF 的ELResolver实现,与 JSF 和 JSP 使用的标准 Unified EL 集成。它首先委托给 Spring 的“业务上下文” WebApplicationContext,然后委托给底层 JSF 实现的默认解析器。

配置方面,您可以SpringBeanFacesELResolver在 JSF faces-context.xml文件中定义,如以下示例所示:

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

5.2.2. 使用FacesContextUtils

将您的属性映射到 中的 bean 时,自定义ELResolver效果很好 faces-config.xml,但有时您可能需要显式获取 bean。FacesContextUtils 类使这很容易。它类似于WebApplicationContextUtils,除了它接受一个FacesContext参数而不是一个ServletContext参数。

下面的例子展示了如何使用FacesContextUtils

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

5.3. Apache Struts 2.x

Struts由 Craig McClanahan 发明,是一个由 Apache 软件基金会托管的开源项目。当时,它极大地简化了 JSP/Servlet 编程范式,并赢得了许多使用专有框架的开发人员的青睐。它简化了编程模型,它是开源的(因此像啤酒一样免费),它拥有一个庞大的社区,这让该项目得以发展并在 Java Web 开发人员中流行起来。

As a successor to the original Struts 1.x, check out Struts 2.x and the Struts-provided Spring Plugin for the built-in Spring integration.

5.4. Apache Tapestry 5.x

Tapestry is a ""Component oriented framework for creating dynamic, robust, highly scalable web applications in Java.""

While Spring has its own powerful web layer, there are a number of unique advantages to building an enterprise Java application by using a combination of Tapestry for the web user interface and the Spring container for the lower layers.

For more information, see Tapestry’s dedicated integration module for Spring.

5.5. Further Resources

The following links go to further resources about the various web frameworks described in this chapter.


1. see XML Configuration