文档的这一部分介绍了对基于 Servlet API 并部署到 Servlet 容器。各个章节包括 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. 调度程序Servlet

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

和任何 一样,需要根据 通过使用 Java 配置或在 中添加到 Servlet 规范。 反过来,使用 Spring 配置来发现 请求映射、视图解析、异常所需的委托组件 处理等等DispatcherServletServletweb.xmlDispatcherServlet

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

爪哇岛
Kotlin
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/*");
    }
}
除了直接使用 ServletContext API 之外,您还可以扩展和覆盖特定方法 (请参阅上下文层次结构下的示例)。AbstractAnnotationConfigDispatcherServletInitializer
对于编程用例,a 可以用作 替代 。有关详细信息,请参阅 GenericWebApplicationContext javadoc。GenericWebApplicationContextAnnotationConfigWebApplicationContext

以下配置示例注册并初始化:web.xmlDispatcherServlet

<web-app>

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

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

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

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

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

1.1.1. 上下文层次结构

DispatcherServlet期望 a (plain 的扩展 ) 用于其自己的配置。 具有指向 和 的链接。它还绑定到,以便应用程序可以使用静态方法来查找 如果他们需要访问它。WebApplicationContextApplicationContextWebApplicationContextServletContextServletServletContextRequestContextUtilsWebApplicationContext

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

根通常包含基础架构 Bean,例如数据存储库和 需要跨多个实例共享的业务服务。那些豆子 有效地继承,并且可以在特定于 Servlet 的 子项,通常包含给定的本地 Bean 。 下图显示了这种关系:WebApplicationContextServletWebApplicationContextServlet

MVC 上下文层次结构

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

爪哇岛
Kotlin
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/app1/*" };
    }
}
如果不需要应用程序上下文层次结构,则应用程序可以返回所有 通过 和从 进行配置。getRootConfigClasses()nullgetServletConfigClasses()

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

1.1.2. 特殊 Bean 类型

委托给特殊 bean 来处理请求并呈现 适当的回应。我们所说的“特殊 bean”是指 Spring 管理的实例 实现框架协定。这些通常带有内置合同,但 您可以自定义其属性并扩展或替换它们。DispatcherServletObject

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

豆类类型 解释

HandlerMapping

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

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

HandlerAdapter

帮助调用映射到请求的处理程序,而不考虑 处理程序的实际调用方式。例如,调用带注释的控制器 需要解析注释。一个的主要目的是 以保护这些细节。DispatcherServletHandlerAdapterDispatcherServlet

HandlerExceptionResolver

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

ViewResolver

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

LocaleResolver、LocaleContextResolver

解析客户端正在使用的内容,以及可能的时区,以便能够 提供国际化的观点。请参阅区域设置Locale

ThemeResolver

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

MultipartResolver

用于解析多部分请求(例如,浏览器样式文件上传)的抽象 一些多部分解析库的帮助。请参见多部件解析程序

FlashMapManager

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

1.1.3. Web MVC配置

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

在大多数情况下,MVC 配置是最佳起点。它声明所需的 Bean 或 XML 格式,并提供更高级别的配置回调 API 自定义它。

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

1.1.4. Servlet配置

在 Servlet 3.0+ 环境中,您可以选择配置 Servlet 容器 以编程方式作为替代方法或与文件结合使用。以下 示例寄存器:web.xmlDispatcherServlet

爪哇岛
Kotlin
import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }
}

WebApplicationInitializer是 Spring MVC 提供的接口,可确保 实现被检测并自动用于初始化任何 Servlet 3 容器。 named 的抽象基类实现使注册 变得更加容易,通过重写方法来指定 servlet 映射和 配置的位置。WebApplicationInitializerAbstractDispatcherServletInitializerDispatcherServletDispatcherServlet

对于使用基于 Java 的 Spring 配置的应用程序,建议这样做,因为 以下示例显示:

爪哇岛
Kotlin
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

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

爪哇岛
Kotlin
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}

AbstractDispatcherServletInitializer还提供了一种方便的方式来添加实例并将它们自动映射到 ,因为 以下示例显示:FilterDispatcherServlet

爪哇岛
Kotlin
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

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

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

受保护的方法提供了一个位置来启用异步支持,并且所有 映射到它的过滤器。默认情况下,此标志设置为 。isAsyncSupportedAbstractDispatcherServletInitializerDispatcherServlettrue

最后,如果你需要进一步自定义它本身,你可以 重写该方法。DispatcherServletcreateDispatcherServlet

1.1.5. 处理

按如下方式处理请求:DispatcherServlet

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

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

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

  • 如果指定多部分文件解析程序,则会检查请求中的多部分。如果 找到多个部分,请求包装在 for 中 由工艺中的其他元素进一步处理。有关详细信息,请参阅多部分解析程序 有关多部件处理的信息。MultipartHttpServletRequest

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

  • 如果返回模型,则呈现视图。如果未返回任何模型(可能是由于 拦截请求的预处理器或后处理器,可能是为了安全起见 reasons),不会呈现任何视图,因为请求可能已经得到满足。

中声明的 bean 用于 解决请求处理期间引发的异常。这些异常解析程序允许 自定义逻辑以解决异常。有关更多详细信息,请参阅例外情况。HandlerExceptionResolverWebApplicationContext

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

您可以通过添加 Servlet 来定制单个实例 初始化参数(元素)添加到文件中的 Servlet 声明中。支持的参数如下表所示:DispatcherServletinit-paramweb.xml

Table 1. DispatcherServlet initialization parameters
参数 解释

contextClass

实现 、 实例化和 由此 Servlet 本地配置。默认情况下,使用。ConfigurableWebApplicationContextXmlWebApplicationContext

contextConfigLocation

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

namespace

命名空间。缺省值为 .WebApplicationContext[servlet-name]-servlet

throwExceptionIfNoHandlerFound

是否在未找到请求的处理程序时抛出。 然后,可以使用 (例如,通过使用控制器方法) 捕获异常,并像处理任何其他异常一样处理。NoHandlerFoundExceptionHandlerExceptionResolver@ExceptionHandler

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

请注意,如果缺省 servlet 处理是 此外,未解析的请求始终转发到默认 Servlet 并且从未提出过 404。

1.1.6. 路径匹配

Servlet API 将完整的请求路径公开为,并进一步细分它 into , , , Servlet 已映射。从这些输入中,Spring MVC 需要确定查找路径 用于处理程序映射,这是自身映射中的路径,不包括 和 任何前缀(如果存在)。requestURIcontextPathservletPathpathInfoDispatcherServletcontextPathservletMapping

和被解码,这使得它们无法比较 直接到完整,以便派生 lookupPath,这使得它 解码 .但是,这引入了自己的问题,因为 path 可以包含编码的保留字符,例如 或 在解码后更改路径的结构,这也可以提高安全性 问题。此外,Servlet 容器可以将 度,这使得进一步无法进行比较 这。servletPathpathInforequestURIrequestURI"/"";"servletPathstartsWithrequestURI

这就是为什么最好避免依赖 基于前缀的映射类型。如果 被映射为 默认 Servlet,带或不带前缀 with 和 Servlet 容器为 4.0+,则 Spring MVC 能够检测 Servlet 映射类型并避免 完全使用 和。在 3.1 Servlet 容器上, 假设相同的 Servlet 映射类型,可以通过提供 a 与 via 路径匹配 MVC 配置。servletPathservletPathDispatcherServlet"/""/*"servletPathpathInfoUrlPathHelperalwaysUseFullPath=true

幸运的是,默认的 Servlet 映射是一个不错的选择。但是,仍然有 一个问题,需要解码才能与 控制器映射。这又是不可取的,因为有可能解码 更改路径结构的保留字符。如果不需要这样的字符, 然后你可以拒绝它们(如Spring Security HTTP防火墙),或者你可以配置,但控制器映射需要与 编码路径,可能并不总是正常工作。此外,有时需要与另一个 Servlet 共享 URL 空间,并且可能需要 按前缀映射。"/"requestURIUrlPathHelperurlDecode=falseDispatcherServlet

上述问题可以通过从 切换到 5.3 或更高版本中可用的解析,请参阅模式比较。不像哪些需求 无论是解码的查找路径还是编码的控制器映射,解析的路径都与路径的解析表示形式匹配,称为 ,一个路径段 一次。这允许单独解码和清理路径段值,而无需 改变路径结构的风险。Parsed 还支持 使用前缀映射,只要前缀保持简单,就可以了 没有任何需要编码的字符。PathMatcherPathPatternAntPathMatcherPathPatternRequestPathPathPatternservletPath

1.1.7. 拦截

所有实现都支持处理程序拦截器,这些拦截器在以下情况下很有用 您希望将特定功能应用于某些请求,例如,检查 委托人。拦截器必须使用三种方法从包中实现,这些方法应提供足够的 灵活地进行各种前处理和后处理:HandlerMappingHandlerInterceptororg.springframework.web.servlet

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

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

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

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

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

postHandle方法对 和 方法不太有用 响应是在 和 之前编写和提交的。这意味着对响应进行任何更改为时已晚,例如添加 一个额外的标题。对于此类方案,您可以实现和 将其声明为 Controller Advice bean 或直接在 上配置它。@ResponseBodyResponseEntityHandlerAdapterpostHandleResponseBodyAdviceRequestMappingHandlerAdapter

1.1.8. 异常

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

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

Table 2. HandlerExceptionResolver implementations
HandlerExceptionResolver 描述

SimpleMappingExceptionResolver

异常类名称和错误视图名称之间的映射。用于渲染 浏览器应用程序中的错误页面。

DefaultHandlerExceptionResolver

解决 Spring MVC 引发的异常,并将其映射到 HTTP 状态代码。 另请参阅 alternative 和 REST API 例外。ResponseEntityExceptionHandler

ResponseStatusExceptionResolver

使用注释解决异常并将其映射到 HTTP 状态 基于注释中值的代码。@ResponseStatus

ExceptionHandlerExceptionResolver

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

旋转变压器链

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

的协定指定它可以返回:HandlerExceptionResolver

  • 指向错误视图的 a。ModelAndView

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

  • null如果异常仍未解决,则供后续解析程序尝试,并且,如果 异常保留在末尾,它被允许冒泡到 Servlet 容器。

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

容器错误页面

如果异常仍未被任何人解决,因此, left 传播,或者如果响应状态设置为错误状态(即 4xx、5xx), Servlet 容器可以在 HTML 中呈现缺省错误页面。自定义默认值 错误页面,可以在 中声明错误页面映射。 以下示例演示如何执行此操作:HandlerExceptionResolverweb.xml

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

在前面的示例中,当异常冒泡或响应具有错误状态时, Servlet 容器在容器内向配置的 URL 进行 ERROR 分派 (例如,)。然后由 进行处理,可能对其进行映射 复制到 ,可以实现以返回带有模型的错误视图名称 或呈现 JSON 响应,如以下示例所示:/errorDispatcherServlet@Controller

爪哇岛
Kotlin
@RestController
public class ErrorController {

    @RequestMapping(path = "/error")
    public Map<String, Object> handle(HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("status", request.getAttribute("javax.servlet.error.status_code"));
        map.put("reason", request.getAttribute("javax.servlet.error.message"));
        return map;
    }
}
Servlet API 不提供在 Java 中创建错误页面映射的方法。您可以 但是,请同时使用 a 和 minimal .WebApplicationInitializerweb.xml

1.1.9. 视图分辨率

Spring MVC 定义了 和 接口,用于渲染 在浏览器中建模,而不会将您绑定到特定的视图技术。 提供视图名称和实际视图之间的映射。 解决准备问题 的数据,然后再移交给特定的视图技术。ViewResolverViewViewResolverView

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

Table 3. ViewResolver implementations
ViewResolver 描述

AbstractCachingViewResolver

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

UrlBasedViewResolver

简单实现接口,直接影响 将逻辑视图名称解析为没有显式映射定义的 URL。 如果逻辑名称与视图资源的名称匹配,则此操作适用 以直接的方式,无需任意映射。ViewResolver

InternalResourceViewResolver

方便的子类支持(在 effect、Servlet 和 JSP)和子类,例如 和 。您可以 使用 指定此解析程序生成的所有视图的视图类。 有关详细信息,请参阅 UrlBasedViewResolver javadoc。UrlBasedViewResolverInternalResourceViewJstlViewTilesViewsetViewClass(..)

FreeMarkerViewResolver

方便的子类,支持和 它们的自定义子类。UrlBasedViewResolverFreeMarkerView

ContentNegotiatingViewResolver

基于 请求文件名或标头。请参阅内容协商ViewResolverAccept

BeanNameViewResolver

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

处理

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

a 的合约指定它可以返回 null 以指示 找不到视图。但是,对于 JSP 和 , 确定 JSP 是否存在的唯一方法是通过 执行调度。因此,必须始终将视图解析程序配置为视图解析程序总体顺序中的最后一个。ViewResolverInternalResourceViewResolverRequestDispatcherInternalResourceViewResolver

配置视图分辨率就像向 Spring 添加 Bean 一样简单 配置。MVC Config 为视图解析程序和添加无逻辑视图控制器提供了专用的配置 API,这些 API 对 HTML 模板很有用 无控制器逻辑的渲染。ViewResolver

重 定向

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

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

请注意,如果控制器方法使用 ,则注解 值优先于 。@ResponseStatusRedirectView

转发

您还可以为以下视图名称使用特殊前缀: 最终由 和 子类解决。这将创建一个 ,它执行 . 因此,此前缀对 and(对于 JSP)没有用,但如果使用其他视图,则会很有帮助 技术,但仍然希望强制一个资源的前向由 Servlet/JSP 引擎。请注意,您也可以改为链接多个视图解析器。forward:UrlBasedViewResolverInternalResourceViewRequestDispatcher.forward()InternalResourceViewResolverInternalResourceView

内容协商

ContentNegotiatingViewResolver 不解析视图本身,而是委托视图 添加到其他视图解析器,然后选择与所请求的表示相似的视图 由客户。表示形式可以从标头或 查询参数(例如,)。Accept"/path?format=pdf"

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

有关配置详细信息,请参阅“MVC 配置”下的“查看解析程序”。

1.1.10. 语言环境

Spring 架构的大部分部分都支持国际化,就像 Spring Web 一样 MVC 框架可以。 允许您自动解析邮件 通过使用客户端的区域设置。这是通过对象完成的。DispatcherServletLocaleResolver

当请求传入时,会查找区域设置解析器,如果 找到一个,它会尝试使用它来设置区域设置。通过使用该方法,您始终可以检索区域设置解析程序解析的区域设置。DispatcherServletRequestContext.getLocale()

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

区域设置解析程序和侦听器在包中定义,并在应用程序中进行配置 上下文。以下选择的区域设置解析器包含在 春天。org.springframework.web.servlet.i18n

时区

除了获取客户端的区域设置外,了解其时区通常也很有用。 该接口提供了一个扩展,让 解析器提供更丰富的 ,其中可能包括时区信息。LocaleContextResolverLocaleResolverLocaleContext

如果可用,可以使用该方法获取用户。自动使用时区信息 通过任何日期/时间和在 Spring 中注册的对象。TimeZoneRequestContext.getTimeZone()ConverterFormatterConversionService

标头解析程序

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

此区域设置解析程序检查客户端上可能存在的 a,以查看是否指定了 or。如果是这样,它将使用指定的详细信息。通过使用 属性,您可以指定 cookie 的名称以及 最大年龄。以下示例定义了一个:CookieLocaleTimeZoneCookieLocaleResolver

<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

会话解析程序

允许您检索和从 可能与用户请求关联的会话。与 相反,此策略将本地选择的区域设置存储在 Servlet 容器的 .因此,这些设置是临时的 因此,在每个会话结束时丢失。SessionLocaleResolverLocaleTimeZoneCookieLocaleResolverHttpSession

请注意,它与外部会话管理机制没有直接关系, 比如 Spring Session 项目。这评估和 针对当前 修改相应的属性。SessionLocaleResolverHttpSessionHttpServletRequest

区域设置拦截器

您可以通过将 添加到其中一个定义来启用区域设置的更改。它检测请求中的参数并更改区域设置 因此,在调度程序的 应用程序上下文。下一个示例显示对所有资源的调用 包含名为 now 的参数会更改区域设置。因此,例如, 对 URL 的请求 ,更改站点 语言到荷兰语。以下示例演示如何截获区域设置:LocaleChangeInterceptorHandlerMappingsetLocaleLocaleResolver*.viewsiteLanguagehttps://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 应用程序中使用主题,必须设置接口的实现。该接口扩展,但将其职责委托给专用的 实现。默认情况下,委托是一个实现 从类路径的根目录加载属性文件。要使用自定义实现或配置 、 的基本名称前缀,请执行以下操作。 您可以在应用程序上下文中使用保留名称 . Web 应用程序上下文会自动检测具有该名称的 Bean 并使用它。org.springframework.ui.context.ThemeSourceWebApplicationContextThemeSourceorg.springframework.ui.context.support.ResourceBundleThemeSourceThemeSourceResourceBundleThemeSourcethemeSource

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

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

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

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

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

解决主题

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

Table 5. ThemeResolver implementations
描述

FixedThemeResolver

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

SessionThemeResolver

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

CookieThemeResolver

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

Spring 还提供了一个允许主题更改每个 request 替换为简单的 request 参数。ThemeChangeInterceptor

1.1.12. 多部分解析器

MultipartResolver从包装上是一种策略 用于解析分段请求,包括文件上传。有一个实现 基于 Commons FileUpload 和 另一个基于 Servlet 3.0 多部分请求解析。org.springframework.web.multipart

要启用多部分处理,您需要在 Spring 配置中声明一个名为 的 bean。 检测到它并将其应用于传入请求。当 POST 当内容类型为 is 接收时,解析器会解析 content 将当前包装为 to 除了将部件公开为请求参数外,还提供对已解析文件的访问权限。MultipartResolverDispatcherServletmultipartResolverDispatcherServletmultipart/form-dataHttpServletRequestMultipartHttpServletRequest

Apache 共享资源FileUpload

要使用 Apache Commons ,您可以配置名称为 的 Bean 类型。您还需要有 jar 作为类路径的依赖项。FileUploadCommonsMultipartResolvermultipartResolvercommons-fileupload

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

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

Servlet 3.0

Servlet 3.0 多部分解析需要通过 Servlet 容器配置来启用。 为此,请执行以下操作:

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

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

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

爪哇岛
Kotlin
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // ...

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

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

}

一旦 Servlet 3.0 配置就位,就可以添加名称为 的 Bean 类型。StandardServletMultipartResolvermultipartResolver

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

1.1.13. 日志记录

Spring MVC 中的 DEBUG 级日志记录被设计为紧凑、最小和 人性化。它侧重于对 再次与其他仅在调试特定问题时有用的其他问题相比。

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

良好的日志记录来自使用日志的体验。如果你发现任何这样做的东西 未达到规定的目标,请告知我们。

敏感数据

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

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

爪哇岛
Kotlin
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");
    }

}

1.2. 过滤器

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

1.2.1. 表单数据

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

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

1.2.2. 转发的标头

当请求通过代理(例如负载均衡器)时,主机、端口和 方案可能会改变,这使得创建指向正确 从客户端的角度来看,主机、端口和方案。

RFC 7239 定义 HTTP 标头 代理可以使用它来提供有关原始请求的信息。还有其他的 非标准标头也是如此,包括 、 、 、 和 。ForwardedX-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

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

转发标头存在安全注意事项,因为应用程序无法知道 如果标头是由代理添加的,或者是由恶意客户端添加的。这就是为什么 应将信任边界处的代理配置为删除来自外部的不受信任的标头。您还可以配置 with ,在这种情况下,它会删除但不使用标头。ForwardedForwardedHeaderFilterremoveOnly=true

为了支持异步请求和错误调度 过滤器应与 和 映射。 如果使用 Spring Framework 的(参见 Servlet Config),则所有过滤器都会自动注册以进行所有调度 类型。但是,如果通过 Spring Boot 或在 Spring Boot 中注册过滤器,请确保包括 和 。DispatcherType.ASYNCDispatcherType.ERRORAbstractAnnotationConfigDispatcherServletInitializerweb.xmlFilterRegistrationBeanDispatcherType.ASYNCDispatcherType.ERRORDispatcherType.REQUEST

1.2.3. 浅层 ETag

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

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

此筛选器具有一个参数,用于将筛选器配置为写入弱 ETag 类似于以下内容:(如 RFC 7232 第 2.3 节中所定义)。writeWeakETagW/"02a2d595e6ed9a0b24f027f2b63b134d6"

为了支持异步请求,必须映射此筛选器 这样过滤器就可以延迟并成功生成一个 ETag 到上次异步调度的末尾。如果使用 Spring Framework 的(参见 Servlet Config) 所有过滤器都会自动注册所有派单类型。但是,如果注册 通过 Spring Boot 或在 Spring Boot 中通过的过滤器确保包括 .DispatcherType.ASYNCAbstractAnnotationConfigDispatcherServletInitializerweb.xmlFilterRegistrationBeanDispatcherType.ASYNC

1.2.4. CORS

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

有关详细信息,请参阅有关 CORSCORS 筛选器的部分。

1.3. 带注释的控制器

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

爪哇岛
Kotlin
@Controller
public class HelloController {

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

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

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

1.3.1. 声明

您可以通过在 Servlet 的 .构造型允许自动检测, 与 Spring 对检测类路径中的类的一般支持保持一致 并为它们自动注册 Bean 定义。它也充当了 Annotated 类,指示其作为 Web 组件的角色。WebApplicationContext@Controller@Component

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

爪哇岛
Kotlin
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

    // ...
}

以下示例显示了与前面示例等效的 XML 配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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是一个组合注释,它是 本身带有元注释,并指示一个控制器,其 每个方法都继承类型级批注,因此写入 直接到响应正文与视图分辨率和使用 HTML 模板呈现。@Controller@ResponseBody@ResponseBody

AOP 代理

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

1.3.2. 请求映射

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

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

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

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

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

爪哇岛
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {

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

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

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

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

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

PathPattern是 Web 应用程序的推荐解决方案,也是 Spring WebFlux 中。在 5.3 版本之前,是 Spring MVC 中的唯一选择 并继续为默认值。但是,可以在 MVC 配置中启用。AntPathMatcherPathPattern

PathPattern支持与 相同的模式语法。此外,它还 支持捕获模式,例如,用于匹配 0 个或多个路径段 在路径的尽头。 还限制了 用于匹配多个 路径段,以便只允许在模式末尾使用。这消除了许多 为给定请求选择最佳匹配模式时出现歧义的情况。 有关完整的模式语法,请参阅 PathPatternAntPathMatcherAntPathMatcher{*spring}PathPattern**

一些示例模式:

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

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

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

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

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

可以使用 访问捕获的 URI 变量。例如:@PathVariable

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

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

爪哇岛
Kotlin
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

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

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

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

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

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

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

模式比较

当多个模式与一个 URL 匹配时,必须选择最佳匹配。这是通过 根据是否启用了 Parsed 的使用,执行以下操作之一:PathPattern

两者都有助于对模式进行排序,并在顶部添加更具体的模式。如果出现以下情况,则模式不太具体 它具有较少的 URI 变量计数(计为 1)、单个通配符(计为 1)、 和双通配符(计为 2)。给定相等的分数,选择较长的模式。 在相同的分数和长度下,具有比通配符更多的 URI 变量的模式是 选择。

默认映射模式 () 从评分中排除,并且始终 最后排序。此外,前缀模式(例如 )被认为较少 特定于其他没有双通配符的模式。/**/public/**

有关完整的详细信息,请点击上面的链接进入模式 比较器。

后缀匹配

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

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

随着时间的流逝,文件扩展名的使用已被证明在各种方面存在问题。 当使用 URI 变量、路径参数和 URI 编码。关于基于 URL 的授权的推理 安全性(有关详细信息,请参阅下一节)也变得更加困难。

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

有一种方式来请求除标头之外的内容类型仍然可以 有用,例如在浏览器中键入 URL 时。路径扩展的一个安全替代方法是 以使用查询参数策略。如果必须使用文件扩展名,请考虑限制 它们通过 ContentNegotiationConfigurer 的属性添加到显式注册的扩展列表。"Accept"mediaTypes

后缀匹配和 RFD

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

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

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

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

请参阅 CVE-2015-5211 了解其他内容 与RFD相关的建议。

易耗品介质类型

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

爪哇岛
Kotlin
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
    // ...
}
1 使用属性按内容类型缩小映射范围。consumes

该属性还支持否定表达式,例如,表示任何 除 以外的内容类型。consumes!text/plaintext/plain

可以在类级别声明共享属性。与大多数其他公司不同 但是,在类级别使用时,请求映射属性为方法级属性 重写而不是扩展类级声明。consumesconsumes

MediaType为常用媒体类型(如 和)提供常量。APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
可生产的介质类型

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

爪哇岛
Kotlin
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
1 使用属性按内容类型缩小映射范围。produces

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

可以在类级别声明共享属性。与大多数其他公司不同 但是,在类级别使用时,请求映射属性为方法级属性 重写而不是扩展类级声明。producesproduces

MediaType为常用媒体类型(如 和)提供常量。APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
参数、标头

您可以根据请求参数条件缩小请求映射范围。您可以测试 存在请求参数 (),如果缺少一个 (),或者 具体值 ()。以下示例演示如何测试特定值:myParam!myParammyParam=myValue

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

您还可以将相同的请求标头条件一起使用,如以下示例所示:

爪哇岛
Kotlin
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 测试是否等于 .myHeadermyValue
您可以将 和 与 headers 条件匹配,但最好改用 consumesproducesContent-TypeAccept
HTTP 头,选项

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

@GetMapping(和 ) 隐式映射到 并支持 HTTP HEAD。HTTP HEAD 请求的处理方式与 HTTP GET 相同,但 这样,不是写入正文,而是计算字节数并设置标头。@RequestMapping(method=HttpMethod.GET)Content-Length

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

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

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

自定义批注

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

@GetMapping、 、 、 和 组合批注的示例。之所以提供它们,是因为可以说,大多数 控制器方法应映射到特定的 HTTP 方法,而不是使用 , 默认情况下,它与所有 HTTP 方法匹配。如果你需要一个组合的例子 注解,看看这些是如何声明的。@PostMapping@PutMapping@DeleteMapping@PatchMapping@RequestMapping

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

显式注册

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

爪哇岛
Kotlin
@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 添加注册。

1.3.3. 处理程序方法

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

方法参数

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

JDK 8 支持作为方法参数与 具有属性(例如、、、 等)并等价于 .java.util.Optionalrequired@RequestParam@RequestHeaderrequired=false

Controller 方法参数 描述

WebRequest,NativeWebRequest

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

javax.servlet.ServletRequest,javax.servlet.ServletResponse

选择任何特定的请求或响应类型,例如、、 或 Spring 的 , .ServletRequestHttpServletRequestMultipartRequestMultipartHttpServletRequest

javax.servlet.http.HttpSession

强制会话的存在。因此,这样的论点从来都不是. 请注意,会话访问不是线程安全的。请考虑将实例的标志设置为 if multiple 允许请求并发访问会话。nullRequestMappingHandlerAdaptersynchronizeOnSessiontrue

javax.servlet.http.PushBuilder

Servlet 4.0 推送构建器 API,用于以编程方式推送 HTTP/2 资源。 请注意,根据 Servlet 规范,如果客户端 不支持该 HTTP/2 功能。PushBuilder

java.security.Principal

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

请注意,如果对此参数进行注释以允许自定义解析程序解析它,则不会急切地解析此参数 在通过 回退到默认分辨率之前。 例如,Spring Security 实现并将通过 注入,除非它还被注释,在这种情况下它 由自定义 Spring Security 解析器通过 解析。HttpServletRequest#getUserPrincipalAuthenticationPrincipalHttpServletRequest#getUserPrincipal@AuthenticationPrincipalAuthentication#getPrincipal

HttpMethod

请求的 HTTP 方法。

java.util.Locale

当前请求区域设置,由最具体的可用区域设置(在 效果、配置或 )。LocaleResolverLocaleResolverLocaleContextResolver

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 请求参数,包括多部分文件。参数值 转换为声明的方法参数类型。另请参阅@RequestParam 作为 Multipart

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

@RequestHeader

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

@CookieValue

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

@RequestBody

用于访问 HTTP 请求正文。正文内容转换为声明的方法 参数类型。请参见@RequestBodyHttpMessageConverter

HttpEntity<B>

用于访问请求标头和正文。正文使用 . 请参阅 HttpEntityHttpMessageConverter

@RequestPart

要访问请求中的部件,请转换部件的主体 带有 .请参见多部分multipart/form-dataHttpMessageConverter

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

用于访问在 HTML 控制器中使用的模型,并将其公开给模板 视图渲染的一部分。

RedirectAttributes

指定在重定向时要使用的属性(即要追加到查询) string) 和 flash 属性,以临时存储,直到重定向后的请求。 请参阅重定向属性Flash 属性

@ModelAttribute

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

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

Errors,BindingResult

用于访问命令对象的验证和数据绑定中的错误 (即参数)或验证 OR 参数时出现的错误。必须声明一个 或 参数 紧跟在已验证的方法参数之后。@ModelAttribute@RequestBody@RequestPartErrorsBindingResult

SessionStatus+ 班级级别@SessionAttributes

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

UriComponentsBuilder

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

@SessionAttribute

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

@RequestAttribute

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

任何其他参数

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

返回值

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

控制器方法返回值 描述

@ResponseBody

返回值通过实现进行转换并写入 响应。请参见@ResponseBodyHttpMessageConverter

HttpEntity<B>,ResponseEntity<B>

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

HttpHeaders

用于返回带有标头且没有正文的响应。

String

要通过实现解析并与隐式 model — 通过命令对象和方法确定。处理程序 方法还可以通过声明参数以编程方式扩充模型 (请参阅显式注册)。ViewResolver@ModelAttributeModel

View

用于与隐式模型一起渲染的实例 — 已确定 通过命令对象和方法。handler 方法还可以 通过声明参数以编程方式扩充模型 (请参阅显式注册)。View@ModelAttributeModel

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

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

@ModelAttribute

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

请注意,这是可选的。请参阅 这张表。@ModelAttribute

ModelAndView对象

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

void

具有返回类型(或返回值)的方法被视为具有完全 如果响应还具有 、 参数或 注释。如果控制器进行了正检查或时间戳检查,情况也是如此(有关详细信息,请参阅控制器)。voidnullServletResponseOutputStream@ResponseStatusETaglastModified

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

DeferredResult<V>

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

Callable<V>

在 Spring MVC 管理的线程中异步生成上述任何返回值。 请参阅异步请求可调用对象

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

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

ResponseBodyEmitter,SseEmitter

异步发出对象流,以通过实现写入响应。还支持作为 . 请参阅异步请求HTTP 流式处理HttpMessageConverterResponseEntity

StreamingResponseBody

异步写入响应。还支持作为 .请参阅异步请求HTTP 流式处理OutputStreamResponseEntity

反应器和其他反应类型通过ReactiveAdapterRegistry

单个值类型(例如 )相当于返回 。 多值类型(例如,)可以被视为流,具体取决于请求的 媒体类型,例如“text/event-stream”、“application/json+stream”或其他 收集到列表并呈现为单个值。请参阅异步请求反应式类型MonoDeferredResultFlux

其他返回值

如果返回值仍未以任何其他方式解析,则将其视为模型 属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型, 在这种情况下,它仍然未得到解决。

类型转换

一些带注释的控制器方法参数,这些参数表示基于请求的输入(如 、 、 、 和 ) 如果参数声明为 以外的内容,则可能需要类型转换。String@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValueString

在这种情况下,将根据配置的转换器自动应用类型转换。 默认情况下,支持简单类型(、、等)。您可以自定义 通过 (请参阅 DataBinder) 或向 . 请参阅 Spring 字段格式设置intlongDateWebDataBinderFormattersFormattingConversionService

类型转换中的一个实际问题是处理空的 String 源值。 如果由于类型转换而导致此类值丢失,则将其视为缺失。 、 和其他目标类型可能就是这种情况。如果要允许注入,请在参数注释上使用标志,或者声明 参数为 。nullLongUUIDnullrequired@Nullable

从 5.3 开始,即使在类型转换后,也会强制执行非 null 参数。如果您的处理程序 方法也打算接受一个 null 值,要么将你的参数声明为 ,要么在相应的 ,等注解中将其标记为。这是 针对 5.3 升级中遇到的回归问题,这是最佳实践和建议的解决方案。@Nullablerequired=false@RequestParam

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

矩阵变量

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

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

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

爪哇岛
Kotlin
// GET /pets/42;q=11;r=22

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

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

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

爪哇岛
Kotlin
// 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
}

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

爪哇岛
Kotlin
// GET /pets/42

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

    // q == 1
}

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

爪哇岛
Kotlin
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

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

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

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

@RequestParam

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

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

爪哇岛
Kotlin
@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 用于绑定 .@RequestParampetId

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

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

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

当注释声明为 或 时,如果注释中没有指定参数名称, 然后,使用每个给定参数名称的请求参数值填充映射。@RequestParamMap<String, String>MultiValueMap<String, String>

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

@RequestHeader

您可以使用注解将请求标头绑定到 控制器。@RequestHeader

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

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

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

爪哇岛
Kotlin
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1 获取标头的值。Accept-Encoding
2 获取标头的值。Keep-Alive

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

在 、 或参数上使用注记时,将填充映射 替换为所有标头值。@RequestHeaderMap<String, String>MultiValueMap<String, String>HttpHeaders

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

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

考虑使用以下 Cookie 的请求:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

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

爪哇岛
Kotlin
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
1 获取 Cookie 的值。JSESSIONID

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

@ModelAttribute

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

爪哇岛
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
    // method logic...
}

上述实例的来源如下:Pet

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

  • 如果模型属性列在 类级@SessionAttributes批注。

  • 通过 其中模型属性名称与 请求值,例如路径变量或请求参数(请参阅下一个示例)。Converter

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

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

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

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

获取模型属性实例后,将应用数据绑定。该类匹配 Servlet 请求参数名称(查询参数和表单 fields) 添加到目标 上的字段名称。匹配字段填充在类型之后 必要时应用转换。有关数据绑定(和验证)的详细信息,请参阅验证。有关自定义数据绑定的详细信息,请参阅 DataBinderWebDataBinderObject

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

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

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

爪哇岛
Kotlin
@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)

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

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

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

@SessionAttributes

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

以下示例使用注释:@SessionAttributes

爪哇岛
Kotlin
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用注释。@SessionAttributes

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

爪哇岛
Kotlin
@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 将值存储在 Servlet 会话中。Pet
2 从 Servlet 会话中清除值。Pet
@SessionAttribute

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

爪哇岛
Kotlin
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1 使用批注。@SessionAttribute

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

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

@RequestAttribute

与 类似,您可以使用注释来 访问之前创建的预先存在的请求属性(例如,由 Servlet 或 ):@SessionAttribute@RequestAttributeFilterHandlerInterceptor

爪哇岛
Kotlin
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
1 使用注释。@RequestAttribute
重定向属性

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

如果 模型实例是专门为重定向准备的。但是,在注释中 控制器,模型可以包含为渲染目的添加的其他属性(例如, 下拉字段值)。为了避免此类属性出现在 URL,一个方法可以声明一个类型和 使用它来指定要提供给 的确切属性。如果方法 做重定向,则使用的内容。否则,内容 使用模型。@RequestMappingRedirectAttributesRedirectViewRedirectAttributes

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

请注意,当前请求中的 URI 模板变量是自动创建的 在展开重定向 URL 时可用,并且无需显式添加它们 通过 或 .以下示例演示如何定义重定向:ModelRedirectAttributes

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

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

Flash 属性

Flash 属性为一个请求提供了一种存储属性的方法,这些属性用于 另一个。这在重定向时最常需要 - 例如, Post-Redirect-Get 模式。Flash 属性在 重定向(通常在会话中)在 重定向并立即删除。

Spring MVC 有两个主要的抽象来支持 flash 属性。 被使用 用于保存 flash 属性,while 用于存储、检索和管理实例。FlashMapFlashMapManagerFlashMap

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

带注释的控制器通常不需要直接使用。相反,方法可以接受 type 的参数并使用它 为重定向方案添加 Flash 属性。通过添加的 Flash 属性会自动传播到“输出”FlashMap。同样地 重定向后,“输入”中的属性会自动添加到提供目标 URL 的控制器中。FlashMap@RequestMappingRedirectAttributesRedirectAttributesFlashMapModel

将请求与闪存属性匹配

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

为了减少出现此类问题的可能性,请使用目标重定向 URL 的路径和查询参数自动“标记”实例。在 转,则在以下情况下,默认将该信息与传入请求匹配 它查找 “输入” 。RedirectViewFlashMapFlashMapManagerFlashMap

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

多部分

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

爪哇岛
Kotlin
@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";
    }
}

将参数类型声明为 a 允许解析多个 文件。List<MultipartFile>

当注释声明为 或 时,如果注释中没有指定参数名称, 然后,使用每个给定参数名称的多部分文件填充映射。@RequestParamMap<String, MultipartFile>MultiValueMap<String, MultipartFile>

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

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

爪哇岛
Kotlin
class MyForm {

    private String name;

    private MultipartFile file;

    // ...
}

@Controller
public class FileUploadController {

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

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

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

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

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

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

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

可以使用注释读取请求正文,并通过 HttpMessageConverter 将其反序列化为 以下示例使用参数:@RequestBodyObject@RequestBody

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

可以使用 MVC 配置“消息转换器”选项来 配置或自定义消息转换。

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

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

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

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

可以使用方法上的批注来序列化返回值 通过 HttpMessageConverter 添加到响应正文。 下面的清单显示了一个示例:@ResponseBody

爪哇岛
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}

@ResponseBody在类级别也受支持,在这种情况下,它由 所有控制器方法。这就是 的效果,仅此而已 而不是标有 和 的元注释。@RestController@Controller@ResponseBody

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

可以使用 MVC 配置“消息转换器”选项来 配置或自定义消息转换。

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

响应实体

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

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

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

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

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

杰克逊 JSON

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

JSON 视图

Spring MVC 提供了对 Jackson 的序列化视图的内置支持, 它只允许呈现 .若要将其与 or 控制器方法一起使用,可以使用 Jackson 的注释来激活序列化视图类,如以下示例所示:Object@ResponseBodyResponseEntity@JsonView

爪哇岛
Kotlin
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}
@JsonView允许视图类数组,但只能指定一个视图类 controller 方法。如果需要激活多个视图,可以使用复合界面。

如果你想以编程方式完成上述操作,而不是声明一个注释, 将返回值包装起来,并使用它来提供序列化视图:@JsonViewMappingJacksonValue

爪哇岛
Kotlin
@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;
    }
}

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

爪哇岛
Kotlin
@Controller
public class UserController extends AbstractController {

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

1.3.4. 模型

您可以使用注解:@ModelAttribute

  • 在方法中的方法参数上 从模型创建或访问 ,并通过 .@RequestMappingObjectWebDataBinder

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

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

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

@ModelAttribute方法具有灵活的方法签名。他们支持许多相同的 参数作为方法,除了它本身或任何东西 与请求正文相关。@RequestMapping@ModelAttribute

下面的示例演示了一个方法:@ModelAttribute

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

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

爪哇岛
Kotlin
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
如果未显式指定名称,则会根据类型选择缺省名称,如约定的 javadoc 中所述。 您始终可以使用重载方法分配显式名称,或者 通过属性 on(用于返回值)。ObjectaddAttributename@ModelAttribute

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

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

1.3.5.DataBinder

@Controller或者类可以具有以下方法: 初始化 的实例,而这些实例又可以:@ControllerAdvice@InitBinderWebDataBinder

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

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

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

@InitBinder方法可以注册特定于控制器或 弹簧和组件。此外,您可以使用 MVC 配置在全局共享的 .java.beans.PropertyEditorConverterFormatterConverterFormatterFormattingConversionService

@InitBinder方法支持许多与方法相同的参数 do,但 (command object) 参数除外。通常,它们被声明 带有参数(用于注册)和返回值。 下面的清单显示了一个示例:@RequestMapping@ModelAttributeWebDataBindervoid

爪哇岛
Kotlin
@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

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

爪哇岛
Kotlin
@Controller
public class FormController {

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

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

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

只有遵循 JavaBeans 命名约定的属性才会公开用于数据绑定,例如,属性的方法。publicpublic 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;
    }

}

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

例如,若要在应用程序中注册允许的字段模式,可以在 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

爪哇岛
Kotlin
@Controller
public class SimpleController {

    // ...

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

该异常可能与正在传播的顶级异常(例如,直接抛出)或包装器异常中的嵌套原因(例如 包裹在 )。从 5.3 开始,这可以匹配 在任意原因水平上,而以前只考虑直接原因。IOExceptionIOExceptionIllegalStateException

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

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

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

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

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

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

在前面所示的变体中,该方法通常使用 实际或实例作为参数, 因为它们都从 .但是,如果有任何此类匹配 异常在包装器异常中传播,包装器异常本身就是一个 , 传入的异常实例是该包装器异常。IOExceptionFileSystemExceptionRemoteExceptionIOExceptionIOException

在变体中,行为甚至更简单。这是 在包装方案中,总是使用 Wrapper 异常调用,并使用 在这种情况下,实际匹配异常才能找到。 仅当这些异常作为顶级异常引发时,传入的异常才是实际异常或实例。handle(Exception)ex.getCause()FileSystemExceptionRemoteException

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

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

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

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

方法参数

@ExceptionHandler方法支持以下参数:

方法参数 描述

异常类型

用于访问引发的异常。

HandlerMethod

用于访问引发异常的控制器方法。

WebRequest,NativeWebRequest

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

javax.servlet.ServletRequest,javax.servlet.ServletResponse

选择任何特定的请求或响应类型(例如,或 Spring 的 或 )。ServletRequestHttpServletRequestMultipartRequestMultipartHttpServletRequest

javax.servlet.http.HttpSession

强制会话的存在。因此,这样的论点从来都不是.
请注意,会话访问不是线程安全的。请考虑将实例的标志设置为 if multiple 允许请求并发访问会话。
nullRequestMappingHandlerAdaptersynchronizeOnSessiontrue

java.security.Principal

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

HttpMethod

请求的 HTTP 方法。

java.util.Locale

当前请求区域设置,由最具体的可用区域设置确定 — 在 效果,配置或 .LocaleResolverLocaleResolverLocaleContextResolver

java.util.TimeZone,java.time.ZoneId

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

java.io.OutputStream,java.io.Writer

用于访问 Servlet API 公开的原始响应正文。

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

用于访问模型以进行错误响应。始终为空。

RedirectAttributes

指定在重定向时要使用的属性 — (即要附加到查询中 string) 和 flash 属性,直到重定向后的请求。 请参阅重定向属性Flash 属性

@SessionAttribute

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

@RequestAttribute

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

返回值

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

返回值 描述

@ResponseBody

返回值通过实例进行转换,并写入 响应。请参见@ResponseBodyHttpMessageConverter

HttpEntity<B>,ResponseEntity<B>

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

String

要通过实现解析并与 隐式模型 — 通过命令对象和方法确定。 处理程序方法还可以通过声明参数(如前所述)以编程方式扩充模型。ViewResolver@ModelAttributeModel

View

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

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

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

@ModelAttribute

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

请注意,这是可选的。请参阅 这张表。@ModelAttribute

ModelAndView对象

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

void

具有返回类型(或返回值)的方法被视为具有完全 如果响应也有一个参数,则处理响应,或者 注释。如果控制器进行了正检查或时间戳检查,情况也是如此(有关详细信息,请参阅控制器)。voidnullServletResponseOutputStream@ResponseStatusETaglastModified

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

任何其他返回值

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

REST API 异常

REST 服务的一个常见要求是在 响应。Spring Framework 不会自动执行此操作,因为表示 响应正文中的错误详细信息是特定于应用程序的。但是,可以使用返回值为 值来设置响应的状态和正文。也可以声明此类方法 在课堂上,在全球范围内应用它们。@RestController@ExceptionHandlerResponseEntity@ControllerAdvice

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

1.3.7. 控制器建议

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

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

启动时,并检测 控制器建议 bean 并在运行时应用它们。全局方法, from an ,在本地 之后应用,从 . 相比之下,全局和方法先于局部方法应用。RequestMappingHandlerMappingExceptionHandlerExceptionResolver@ExceptionHandler@ControllerAdvice@Controller@ModelAttribute@InitBinder

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

爪哇岛
Kotlin
// 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 {}

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

1.4. 功能端点

Spring Web MVC 包括 WebMvc.fn,这是一个轻量级的函数式编程模型,其中函数 用于路由和处理请求,合约旨在实现不可变性。 它是基于注释的编程模型的替代方法,但在其他方面可以 相同的 DispatcherServlet

1.4.1. 概述

在 WebMvc.fn 中,HTTP 请求使用 : 进行处理,该函数接受并返回 . 请求和响应对象都具有不可变的协定,这些协定提供 JDK 8 友好 访问 HTTP 请求和响应。 等同于 基于注释的编程模型。HandlerFunctionServerRequestServerResponseHandlerFunction@RequestMapping

传入请求被路由到一个处理程序函数,该函数具有 : 获取并返回一个可选的(即)。 当路由器函数匹配时,将返回一个处理程序函数;否则为空 Optional。 相当于一个注释,但带有 路由器功能不仅提供数据,还提供行为。RouterFunctionServerRequestHandlerFunctionOptional<HandlerFunction>RouterFunction@RequestMapping

RouterFunctions.route()提供路由器构建器,便于创建路由器, 如以下示例所示:

爪哇岛
Kotlin
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) {
        // ...
    }
}

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

1.4.2. Handler函数

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

服务器请求

ServerRequest提供对 HTTP 方法、URI、标头和查询参数的访问, 而通过这些方法提供对身体的访问。body

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

爪哇岛
Kotlin
String string = request.body(String.class);

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

爪哇岛
Kotlin
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});

以下示例演示如何访问参数:

爪哇岛
Kotlin
MultiValueMap<String, String> params = request.params();
服务器响应

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

爪哇岛
Kotlin
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

以下示例演示如何生成具有标头且无正文的 201 (CREATED) 响应:Location

爪哇岛
Kotlin
URI location = ...
ServerResponse.created(location).build();

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

爪哇岛
Kotlin
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);

如果不仅是正文,而且状态或标头都基于异步类型, 您可以在 上使用 静态方法,该方法 接受 、 或 支持的任何其他异步类型。例如:asyncServerResponseCompletableFuture<ServerResponse>Publisher<ServerResponse>ReactiveAdapterRegistry

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

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

爪哇岛
Kotlin
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();
处理程序类

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

爪哇岛
Kotlin
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");

这很方便,但在一个应用程序中,我们需要多个函数和多个内联 Lambda 可能会变得凌乱。 因此,将相关的处理程序函数组合到一个处理程序类中是很有用的,该处理程序类 具有与基于注释的应用程序类似的角色。 例如,以下类公开反应式存储库:@ControllerPerson

爪哇岛
Kotlin
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是一个处理程序函数,它将存储库中找到的所有对象返回为 JSON格式。Person
2 createPerson是一个处理程序函数,用于存储请求正文中包含的新内容。Person
3 getPerson是一个处理程序函数,它返回由路径标识的单个人员 变量。我们从存储库中检索它并创建一个 JSON 响应(如果是) 发现。如果未找到,我们将返回 404 Not Found 响应。idPerson
验证

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

爪哇岛
Kotlin
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 响应的异常。

处理程序还可以通过创建和注入来使用标准 Bean 验证 API (JSR-303) 基于 的全局实例。 请参阅 Spring ValidationValidatorLocalValidatorFactoryBean

1.4.3.RouterFunction

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

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

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

谓词

你可以自己写,但实用程序类 提供常用的实现,基于请求路径、HTTP 方法、content-type、 等等。 以下示例使用请求谓词基于标头创建约束:RequestPredicateRequestPredicatesAccept

爪哇岛
Kotlin
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World")).build();

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

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

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

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

路线

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

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

  • add(RouterFunction)在生成器上RouterFunctions.route()

  • RouterFunction.and(RouterFunction)

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

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

爪哇岛
Kotlin
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 匹配的标头被路由到AcceptPersonHandler.getPerson
2 GET /person与 JSON 匹配的标头被路由到AcceptPersonHandler.listPeople
3 POST /person没有额外的谓词映射到 ,并且PersonHandler.createPerson
4 otherRoute是在其他地方创建的路由器函数,并添加到构建的路由中。
嵌套路由

一组路由器函数通常具有共享谓词,例如共享谓词 路径。 在上面的示例中,共享谓词将是匹配 的路径谓词 , 由其中三条路线使用。 使用批注时,可以通过使用映射到 的类型级批注来删除此重复项。 在 WebMvc.fn 中,路径谓词可以通过路由器函数构建器上的方法共享。 例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行:/person@RequestMapping/personpath

爪哇岛
Kotlin
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

尽管基于路径的嵌套是最常见的,但您可以使用 生成器上的方法。 上面仍然包含一些共享 -header 谓词形式的重复项。 我们可以通过将该方法与以下方法一起使用来进一步改进:nestAcceptnestaccept

爪哇岛
Kotlin
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();

1.4.4. 运行服务器

您通常通过 MVC Config 在基于 DispatcherHandler 的设置中运行路由器函数,MVC Config 使用 Spring 配置声明 处理请求所需的组件。MVC Java 配置声明以下内容 支持功能终结点的基础结构组件:

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

  • HandlerFunctionAdapter:允许调用的简单适配器 映射到请求的 a。DispatcherHandlerHandlerFunction

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

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

爪哇岛
Kotlin
@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...
    }
}

1.4.5. 过滤处理函数

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

爪哇岛
Kotlin
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 添加自定义请求标头的筛选器仅适用于两个 GET 路由。before
2 记录响应的筛选器将应用于所有路由,包括嵌套路由。after

路由器生成器上的方法采用:a 接受 and 并返回 . handler 函数参数表示链中的下一个元素。 这通常是路由到的处理程序,但它也可以是另一个处理程序 如果应用了多个,则进行筛选。filterHandlerFilterFunctionServerRequestHandlerFunctionServerResponse

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

爪哇岛
Kotlin
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();

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

除了在路由器函数构建器上使用该方法外,还可以将 通过 过滤到现有路由器功能。filterRouterFunction.filter(HandlerFilterFunction)

对功能终结点的 CORS 支持通过专用的 CorsFilter 提供。

1.5. URI 链接

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

1.5.1. Uri组件

Spring MVC 和 Spring WebFlux

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

爪哇岛
Kotlin
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

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

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

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

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

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

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

1.5.2. UriBuilder的

Spring MVC 和 Spring WebFlux

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

您可以配置 和 使用 来自定义 URI 的准备。 是默认值 实现该在内部使用和 公开共享配置选项。RestTemplateWebClientUriBuilderFactoryDefaultUriBuilderFactoryUriBuilderFactoryUriComponentsBuilder

以下示例演示如何配置:RestTemplate

爪哇岛
Kotlin
// 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);

以下示例配置:WebClient

爪哇岛
Kotlin
// 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();

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

爪哇岛
Kotlin
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI 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 模板中不带有 “%3B”。相比之下,第二种选择从不 替换 “;”,因为它是路径中的合法字符。

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

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

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

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

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

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

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

爪哇岛
Kotlin
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar");

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

爪哇岛
Kotlin
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();

该实现在内部用于 展开并编码 URI 模板。作为工厂,它提供了一个配置位置 基于以下编码模式之一的编码方法:DefaultUriBuilderFactoryUriComponentsBuilder

  • TEMPLATE_AND_VALUES:用途 ,对应 前面列表中的第一个选项,用于对 URI 模板进行预编码,并在以下情况下对 URI 变量进行严格编码 扩大。UriComponentsBuilder#encode()

  • VALUES_ONLY:不对 URI 模板进行编码,而是应用严格的编码 到 URI 变量,然后再将它们扩展到 模板。UriUtils#encodeUriVariables

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

  • NONE:不应用编码。

设置为历史性 原因和向后兼容性。依赖于默认值 in ,从 in 改为 5.0.x 到 5.1。RestTemplateEncodingMode.URI_COMPONENTWebClientDefaultUriBuilderFactoryEncodingMode.URI_COMPONENTEncodingMode.TEMPLATE_AND_VALUES

1.5.4. 相对 Servlet 请求

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

爪哇岛
Kotlin
HttpServletRequest request = ...

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

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

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

爪哇岛
Kotlin
HttpServletRequest request = ...

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

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

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

爪哇岛
Kotlin
HttpServletRequest request = ...

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

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

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

爪哇岛
Kotlin
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

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

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

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

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

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

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

爪哇岛
Kotlin
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
控制器方法签名在设计上受到限制,当它们应该可用于 使用 创建链接。除了需要适当的参数签名外, 返回类型(即生成运行时代理)存在技术限制 对于链接生成器调用),因此返回类型不能为 .特别 视图名称的常见返回类型在此处不起作用。您应该改用 or 甚至 plain(带有返回值)。fromMethodCallfinalStringModelAndViewObjectString

前面的示例在 中使用了 中的静态方法。在内部,他们依赖 on 准备来自方案、主机、端口、 上下文路径和当前请求的 servlet 路径。这在大多数情况下效果很好。 但是,有时,它可能是不够的。例如,您可能不在上下文中 请求(例如准备链接的批处理),或者可能需要插入路径 前缀(例如已从请求路径中删除的区域设置前缀,需要 重新插入到链接中)。MvcUriComponentsBuilderServletUriComponentsBuilder

对于这种情况,可以使用接受 的静态重载方法使用基 URL。或者,您可以使用基 URL 创建 的实例,然后使用基于实例的方法。例如, 以下列表使用:fromXxxUriComponentsBuilderMvcUriComponentsBuilderwithXxxwithMethodCall

爪哇岛
Kotlin
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();
从 5.1 开始,忽略来自 和 标头的信息,这些标头指定客户端发起的地址。请考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃 这样的标头。MvcUriComponentsBuilderForwardedX-Forwarded-*

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

请看以下示例:

爪哇岛
Kotlin
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

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

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

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

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

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

1.6. 异步请求

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

1.6.1.DeferredResult

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

爪哇岛
Kotlin
@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);

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

1.6.2.Callable

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

爪哇岛
Kotlin
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

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

然后可以通过配置的 .TaskExecutor

1.6.3. 处理

以下是对 Servlet 异步请求处理的非常简明的概述:

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

  • 对 returns 的调用,可用于 进一步控制异步处理。例如,它提供了方法, 这类似于来自 Servlet API 的转发,只不过它允许 在 Servlet 容器线程上处理应用程序恢复请求。request.startAsync()AsyncContextdispatch

  • 提供对电流的访问,您可以 用于区分处理初始请求、异步请求 调度、转发和其他调度程序类型。ServletRequestDispatcherType

DeferredResult处理工作原理如下:

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

  • Spring MVC 调用 。request.startAsync()

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

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

  • 再次调用,然后继续处理,并使用 异步生成的返回值。DispatcherServlet

Callable处理工作原理如下:

  • 控制器返回一个 .Callable

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

  • 同时,和所有过滤器都退出 Servlet 容器线程, 但回应仍然是开放的。DispatcherServlet

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

  • 再次调用,然后继续处理,并使用 异步生成的返回值。DispatcherServletCallable

有关进一步的背景和上下文,您还可以阅读 在 Spring MVC 3.2 中引入异步请求处理支持的博客文章。

异常处理

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

当您使用 时,会出现类似的处理逻辑,主要区别在于 结果从 或 引发异常。CallableCallable

拦截

HandlerInterceptor实例的类型可以是 ,以接收启动异步的初始请求的回调 处理(而不是 和 )。AsyncHandlerInterceptorafterConcurrentHandlingStartedpostHandleafterCompletion

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

DeferredResult提供和回调。 有关更多详细信息,请参阅 DeferredResult 的 javadoc。 可以替代暴露额外的 超时和完成回调的方法。onTimeout(Runnable)onCompletion(Runnable)CallableWebAsyncTask

与 WebFlux 相比

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

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

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

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

1.6.4. HTTP 流

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

对象

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

爪哇岛
Kotlin
@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();

你也可以用作 中的身体,让你 自定义响应的状态和标头。ResponseBodyEmitterResponseEntity

当抛出一个(例如,如果远程客户端消失)时,应用程序 不负责清理连接,也不应调用 或 。相反,servlet 容器会自动启动错误通知,Spring MVC 会在其中进行调用。 反过来,此调用会向应用程序执行最后一次调度,在此期间,Spring MVC 调用配置的异常解析程序并完成请求。emitterIOExceptionemitter.completeemitter.completeWithErrorAsyncListenercompleteWithErrorASYNC

上交所

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

爪哇岛
Kotlin
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
    SseEmitter emitter = new SseEmitter();
    // Save the emitter somewhere..
    return emitter;
}

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

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

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

虽然 SSE 是流式传输到浏览器的主要选项,但请注意 Internet Explorer 不支持服务器发送的事件。考虑将 Spring 的 WebSocket 消息传递SockJS 回退传输(包括 SSE)一起使用,这些传输以 广泛的浏览器。

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

原始数据

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

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

您可以用作 to 中的正文 自定义响应的状态和标头。StreamingResponseBodyResponseEntity

1.6.5. 反应式类型

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

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

  • 单值 promise 适用于,类似于使用 .例子 include (Reactor) 或 (RxJava)。DeferredResultMonoSingle

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

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

Spring MVC 通过 ReactiveAdapterRegistry 支持 Reactor 和 RxJava,这使得它可以从多个响应式库进行调整。spring-core

对于流式传输到响应,支持反应式背压,但写入 响应仍然阻塞,并通过配置的在单独的线程上运行,以避免 阻塞上游源(例如从 返回的 )。 默认情况下,用于阻塞写入,但事实并非如此 适合在负载下。如果计划使用反应式类型进行流式处理,则应使用 MVC 配置来配置任务执行程序。TaskExecutorFluxWebClientSimpleAsyncTaskExecutor

1.6.6. 断开连接

当远程客户机消失时,Servlet API 不提供任何通知。 因此,在流式传输到响应时,无论是通过 SseEmitter 还是响应式类型,定期发送数据都很重要。 因为如果客户端断开连接,写入将失败。发送可以采用以下形式 空(仅注释)SSE 事件或另一方必须解释的任何其他数据 作为心跳和忽略。

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

1.6.7. 配置

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

Servlet 容器

过滤器和 Servlet 声明有一个标志,需要设置该标志才能启用异步请求处理。此外,筛选器映射应 声明要处理 .asyncSupportedtrueASYNCjavax.servlet.DispatchType

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

在配置中,可以向 and 声明添加和添加到筛选器映射。web.xml<async-supported>true</async-supported>DispatcherServletFilter<dispatcher>ASYNC</dispatcher>

Spring MVC的

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

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

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

您可以配置以下内容:

  • 异步请求的默认超时值,如果未设置,则取决于 在底层 Servlet 容器上。

  • AsyncTaskExecutor用于在使用响应式类型进行流式处理时阻止写入,以及用于执行从 控制器方法。如果满足以下条件,我们强烈建议您配置此属性 具有反应式类型的流或具有返回 的控制器方法,因为 默认情况下,它是 .CallableCallableSimpleAsyncTaskExecutor

  • DeferredResultProcessingInterceptor实现和实现。CallableProcessingInterceptor

请注意,您还可以在 上设置默认超时值 , a 和 .对于 ,可用于提供超时值。DeferredResultResponseBodyEmitterSseEmitterCallableWebAsyncTask

1.7. CORS

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

1.7.1. 简介

出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。 例如,您可以将您的银行帐户放在一个选项卡中,而将 evil.com 放在另一个选项卡中。脚本 from evil.com 应该不能用你的 凭据 — 例如从您的帐户中提取资金!

跨域资源共享 (CORS) 是大多数浏览器实现的 W3C 规范,可用于指定 授权什么样的跨域请求,而不是使用安全性较低且较少的 基于 IFRAME 或 JSONP 的强大解决方法。

1.7.2. 处理

CORS 规范区分预检请求、简单请求和实际请求。 要了解 CORS 的工作原理,您可以阅读本文,其中 还有很多其他的,或者有关更多详细信息,请参阅规范。

Spring MVC 实现为 CORS 提供了内置支持。成功后 将请求映射到处理程序,实现会检查 CORS 配置中 给定请求和处理程序,并采取进一步操作。处理印前检查请求 直接,而简单和实际的 CORS 请求被拦截、验证并具有 所需的 CORS 响应标头集。HandlerMappingHandlerMapping

为了启用跨域请求(即,标头存在且 与请求的主机不同),您需要有一些显式声明的 CORS 配置。如果未找到匹配的 CORS 配置,则预检请求为 拒绝。不会将 CORS 标头添加到简单和实际 CORS 请求的响应中 因此,浏览器会拒绝它们。Origin

每个都可以使用基于 URL 模式的映射单独配置。在大多数情况下,应用程序 使用 MVC Java 配置或 XML 命名空间声明此类映射,这会导致 在传递给所有实例的单个全局映射中。HandlerMappingCorsConfigurationHandlerMapping

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

组合全局和局部配置的规则通常是累加的,例如, 所有全球和所有本地原产地。对于只有单个值的属性 接受,例如 并且,局部值将覆盖全局值。有关详细信息,请参阅 CorsConfiguration#combine(CorsConfiguration)。allowCredentialsmaxAge

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

  • CorsConfiguration

  • CorsProcessor,DefaultCorsProcessor

  • AbstractHandlerMapping

1.7.3.@CrossOrigin

@CrossOrigin 注解支持对带注解的控制器方法进行跨域请求, 如以下示例所示:

爪哇岛
Kotlin
@RestController
@RequestMapping("/account")
public class AccountController {

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

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

默认情况下,允许:@CrossOrigin

  • 所有起源。

  • 所有标头。

  • 控制器方法映射到的所有 HTTP 方法。

allowCredentials默认情况下不启用,因为这会建立信任级别 暴露敏感的用户特定信息(例如 Cookie 和 CSRF 令牌),以及 应仅在适当的情况下使用。启用后,必须 设置为一个或多个特定域(但不是特殊值)或 该属性可用于匹配一组动态的源。allowOrigins"*"allowOriginPatterns

maxAge设置为 30 分钟。

@CrossOrigin在类级别也受支持,并且被所有方法继承, 如以下示例所示:

爪哇岛
Kotlin
@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

爪哇岛
Kotlin
@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) {
        // ...
    }
}

1.7.4. 全局配置

除了细粒度的控制器方法级配置外,您可能还希望 也定义一些全局 CORS 配置。您可以在任何 .但是,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来执行此操作。CorsConfigurationHandlerMapping

默认情况下,全局配置支持以下功能:

  • 所有起源。

  • 所有标头。

  • GET、 和 方法。HEADPOST

allowCredentials默认情况下不启用,因为这会建立信任级别 暴露敏感的用户特定信息(例如 Cookie 和 CSRF 令牌),以及 应仅在适当的情况下使用。启用后,必须 设置为一个或多个特定域(但不是特殊值)或 该属性可用于匹配一组动态的源。allowOrigins"*"allowOriginPatterns

maxAge设置为 30 分钟。

Java 配置

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

爪哇岛
Kotlin
@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...
    }
}
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 应用 CORS 支持。

如果您尝试将 Spring Security 与 Spring Security 一起使用,请记住 Spring 安全性内置了对 科斯。CorsFilter

要配置筛选器,请将 a 传递给其构造函数,作为 以下示例显示:CorsConfigurationSource

爪哇岛
Kotlin
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);

1.8. 网络安全

Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。查看 Spring 安全性 参考文档,包括:

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

1.9. HTTP缓存

HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存 围绕响应标头和随后的条件请求 标头(例如 和 )。 建议私有(例如,浏览器) 以及关于如何缓存和重用响应的公共(例如,代理)缓存。使用标头 提出可能导致 304 (NOT_MODIFIED) 没有正文的有条件请求, 如果内容没有改变。 可以看作是更复杂的继任者 标头。Cache-ControlLast-ModifiedETagCache-ControlETagETagLast-Modified

本节介绍 Spring Web MVC 中可用的 HTTP 缓存相关选项。

1.9.1.CacheControl

CacheControl 提供对 配置与标头相关的设置,并接受为参数 在许多地方:Cache-Control

虽然 RFC 7234 描述了所有可能的 指令,类型采用 侧重于常见方案的面向用例的方法:Cache-ControlCacheControl

爪哇岛
Kotlin
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();

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

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

  • 值通过使用指令来防止缓存。0'Cache-Control: no-store'

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

1.9.2. 控制器

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为需要先计算资源的 or 值,然后才能进行比较 针对条件请求标头。控制器可以向 添加标头和设置,如以下示例所示:lastModifiedETagETagCache-ControlResponseEntity

爪哇岛
Kotlin
@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);
}

前面的示例发送一个 304 (NOT_MODIFIED) 响应,如果比较 到条件请求标头表示内容未更改。否则,和标头将添加到响应中。ETagCache-Control

您还可以根据控制器中的条件请求标头进行检查, 如以下示例所示:

爪哇岛
Kotlin
@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 继续处理请求。

有三种变体可用于根据值和/或值检查条件请求。对于条件和请求,您可以将响应设置为 304(NOT_MODIFIED)。对于条件 、 和 ,您可以改为设置响应 设置为 412 (PRECONDITION_FAILED),以防止并发修改。eTaglastModifiedGETHEADPOSTPUTDELETE

1.9.3. 静态资源

应使用条件响应标头提供静态资源 以获得最佳性能。请参阅有关配置静态资源的部分。Cache-Control

1.9.4. 过滤器ETag

您可以使用 添加从 响应内容,从而节省带宽,但不节省 CPU 时间。请参见浅层 ETagShallowEtagHeaderFiltereTag

1.10. 查看技术

Spring MVC 中视图技术的使用是可插拔的。是否决定使用 Thymeleaf、Groovy 标记模板、JSP 或其他技术主要是 配置更改。本章介绍与 Spring MVC 集成的视图技术。 我们假设您已经熟悉视图分辨率

Spring MVC 应用程序的视图位于内部信任边界内 该应用程序。视图可以访问应用程序上下文的所有 bean。如 因此,不建议在以下应用程序中使用 Spring MVC 的模板支持 这些模板可由外部源编辑,因为这可能会带来安全隐患。

1.10.1. 百里香叶

Thymeleaf 是一个现代的服务器端 Java 模板引擎,强调自然的 HTML 可以通过双击在浏览器中预览的模板,这非常有帮助 用于独立处理 UI 模板(例如,由设计师完成),而无需 正在运行的服务器。如果您想替换 JSP,Thymeleaf 提供了最 广泛的功能集使这种过渡更容易。百里香叶很活跃 开发和维护。有关更完整的介绍,请参阅 Thymeleaf 项目主页。

Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。 该配置涉及一些 Bean 声明,例如 、 和 。 有关更多详细信息,请参阅 Thymeleaf+SpringServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver

1.10.2. 自由标记

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

查看配置

以下示例演示如何将 FreeMarker 配置为视图技术:

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

    // Configure FreeMarker...

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

下面的示例演示如何在 XML 中配置相同的内容:

<mvc:annotation-driven/>

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

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

或者,您也可以声明 Bean 以完全控制所有 属性,如以下示例所示:FreeMarkerConfigurer

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

您的模板需要存储在前面示例中所示的目录中。根据上述配置,如果您的控制器 返回视图名称 ,解析程序将查找模板。FreeMarkerConfigurerwelcome/WEB-INF/freemarker/welcome.ftl

FreeMarker 配置

您可以通过设置适当的 bean 将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 对象(由 Spring 管理) Bean 上的属性。该属性需要 一个对象,并且该属性需要一个 .以下示例演示如何使用 :ConfigurationFreeMarkerConfigurerfreemarkerSettingsjava.util.PropertiesfreemarkerVariablesjava.util.MapFreeMarkerConfigurer

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

<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>

请参阅 FreeMarker 文档,了解适用于以下设置和变量的详细信息 对象。Configuration

表单处理

Spring 提供了一个用于 JSP 的标签库,其中包含一个元素。此元素主要允许窗体显示以下位置的值 form-backing 对象,并显示 Web 或业务层。Spring 也支持 FreeMarker 中的相同功能, 具有用于生成表单输入元素本身的额外便利宏。<spring:bind/>Validator

绑定宏

在文件中维护一组标准的宏 FreeMarker,因此它们始终可用于适当配置的应用程序。spring-webmvc.jar

Spring 模板库中定义的一些宏被视为内部宏 (private),但宏定义中不存在此类范围,使所有宏都可见 调用代码和用户模板。以下各节仅重点介绍宏 您需要直接从模板中调用。如果要查看宏代码 直接调用该文件并位于包中。spring.ftlorg.springframework.web.servlet.view.freemarker

简单绑定

在基于 FreeMarker 模板的 HTML 表单中,这些模板充当 Spring MVC 的表单视图 控制器,您可以使用类似于下一个示例的代码来绑定到字段值和 以与 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>需要一个“path”参数,该参数由命令的名称组成 对象(它是“命令”,除非您在控制器配置中更改了它)后跟 按句点和要绑定到的命令对象上的字段名称。你 也可以使用嵌套字段,例如 .该宏假定 中的参数指定的缺省 HTML 转义行为。command.address.streetbindServletContextdefaultHtmlEscapeweb.xml

称为的宏的另一种形式采用第二个参数 显式指定是否应在状态错误中使用 HTML 转义 消息或值。您可以根据需要将其设置为或。附加表格 处理宏简化了 HTML 转义的使用,您应该使用这些宏 尽可能。下一节将对此进行说明。<@spring.bindEscaped>truefalse

输入宏

FreeMarker 的其他便利宏简化了绑定和表单生成 (包括验证错误显示)。永远没有必要使用这些宏来 生成表单输入字段,您可以将它们与简单的 HTML 或直接混合和匹配 调用我们之前强调的 Spring 绑定宏。

下表显示了 FreeMarker 模板 (FTL) 定义 以及每个参数的参数列表:

Table 6. Table of macro definitions
宏观 FTL 定义

message(根据 code 参数从资源包中输出字符串)

<@spring.消息代码/>

messageText(根据 code 参数从资源包中输出一个字符串, 回退到默认参数的值)

<@spring.message文本代码、文本/>

url(在相对 URL 前面加上应用程序的上下文根)

<@spring.url relativeUrl/>

formInput(用于收集用户输入的标准输入字段)

<@spring.form输入路径、属性、fieldType/>

formHiddenInput(用于提交非用户输入的隐藏输入字段)

<@spring.formHiddenInput 路径,attributes/>

formPasswordInput(用于收集密码的标准输入字段。请注意,没有 值将永远填充到此类型的字段中。

<@spring.formPasswordInput 路径,attributes/>

formTextarea(用于收集长的自由格式文本输入的大文本字段)

<@spring.formText区域路径,属性/>

formSingleSelect(选项的下拉框,允许单个必需值 选择)

<@spring.formSingleSelect 路径、选项、属性/>

formMultiSelect(允许用户选择 0 个或多个值的选项列表框)

<@spring.formMultiSelect 路径、选项、属性/>

formRadioButtons(一组单选按钮,用于进行单个选择 从可用选项中)

<@spring.formRadioButtons 路径、选项分隔符、属性/>

formCheckboxes(一组允许选择 0 个或多个值的复选框)

<@spring.formCheckboxes 路径、选项、分隔符、属性/>

formCheckbox(单个复选框)

<@spring.formCheckbox 路径,attributes/>

showErrors(简化绑定字段验证错误的显示)

<@spring.showErrors 分隔符,classOrStyle/>

在 FreeMarker 模板中,实际上并非如此 必需,因为您可以使用普通宏,指定 或 作为参数的值。formHiddenInputformPasswordInputformInputhiddenpasswordfieldType

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

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

  • options:输入中可选择的所有可用值之一 田。映射的键表示从窗体 POST 回来的值 并绑定到命令对象。针对键存储的地图对象是标签 在表单上显示给用户,并且可能与相应的值不同 由表单发回。通常,此类地图由 控制器。您可以使用任何实现,具体取决于所需的行为。 对于严格排序的地图,可以使用带有 适用于应在插入中返回值的任意映射 order,请使用 a 或 a from .MapMapSortedMapTreeMapComparatorLinkedHashMapLinkedMapcommons-collections

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

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

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

以下各节概述了宏的示例。

输入字段

该宏采用参数 () 和一个附加参数(在即将到来的示例中为空)。宏,以及所有其他形式 生成宏,对 path 参数执行隐式 Spring 绑定。绑定 在发生新绑定之前保持有效,因此宏不需要传递 path 参数 — 它对上次为其创建绑定的字段进行操作。formInputpathcommand.nameattributesshowErrors

宏采用分隔符参数(用于 在给定字段上分隔多个错误),并且还接受第二个参数 — this time,类名或样式属性。请注意,FreeMarker 可以指定默认值 attributes 参数的值。下面的示例演示如何使用 and 宏:showErrorsformInputshowErrors

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

下一个示例显示了表单片段的输出,生成了 name 字段并显示 提交表单后出现验证错误,字段中没有值。验证 通过 Spring 的 Validation 框架发生。

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

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

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

选择字段

您可以使用四个选择字段宏在 您的 HTML 表单:

  • formSingleSelect

  • formMultiSelect

  • formRadioButtons

  • formCheckboxes

四个宏中的每一个都接受包含窗体值的选项 字段和与该值对应的标签。value 和 label 可以是 相同。Map

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

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

前面的清单呈现了一行单选按钮,每个按钮对应一个值,并使用 的分隔符。未提供其他属性(宏的最后一个参数是 缺失)。对映射中的每个键值对使用相同的方法。地图的 键是表单实际提交的请求参数。映射值是 用户看到的标签。在前面的示例中,给定了三个知名城市的列表 和表单支持对象中的默认值,HTML 如下所示:cityMap""cityMapStringPOST

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>

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

爪哇岛
Kotlin
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;
}

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

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 兼容,并使用文件中定义的 HTML 转义的默认值,如 由 Spring 的绑定支持使用。使元素符合 XHTML 或重写 默认的 HTML 转义值,您可以在模板中指定两个变量(或 您的模型,它们对您的模板可见)。指定 它们在模板中是它们可以稍后在 模板处理,为表单中的不同字段提供不同的行为。web.xml

要切换到标记的 XHTML 合规性,请为 名为 的模型或上下文变量,如以下示例所示:truexhtmlCompliant

<#-- 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 Framework 有一个内置的 集成,用于将 Spring MVC 与 Groovy Markup 一起使用。

Groovy 标记模板引擎需要 Groovy 2.3.1+。
配置

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

    // Configure the Groovy Markup Template Engine...

    @Bean
    public GroovyMarkupConfigurer groovyMarkupConfigurer() {
        GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
        configurer.setResourceLoaderPath("/WEB-INF/");
        return configurer;
    }
}

下面的示例演示如何在 XML 中配置相同的内容:

<mvc:annotation-driven/>

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

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

与传统的模板引擎不同,Groovy Markup 依赖于使用构建器的 DSL 语法。以下示例显示了 HTML 页面的示例模板:

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

1.10.4. 脚本视图

Spring Framework 有一个内置的集成,可以将 Spring MVC 与任何 可以在 JSR-223 Java 脚本引擎上运行的模板库。我们测试了以下内容 不同脚本引擎上的模板库:

脚本库 脚本引擎

车把

纳斯霍恩

胡子

纳斯霍恩

反应

纳斯霍恩

EJS的

纳斯霍恩

雇员再培训局

JRuby的

字符串模板

杰通

Kotlin 脚本模板

Kotlin

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

您需要在类路径上安装脚本引擎,其详细信息因脚本引擎而异:

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

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

  • 应将 Jython 添加为 Python 支持的依赖项。

  • org.jetbrains.kotlin:kotlin-script-util依赖项和包含一行的文件应添加用于 Kotlin 脚本支持。有关详细信息,请参阅此示例META-INF/services/javax.script.ScriptEngineFactoryorg.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

您需要有脚本模板库。对 JavaScript 来说,一种方法是 通过 WebJars

脚本模板

您可以声明一个 Bean 来指定要使用的脚本引擎, 要加载的脚本文件、要调用什么函数来呈现模板等。 以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:ScriptTemplateConfigurer

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}

下面的示例演示了 XML 中的相同排列方式:

<mvc:annotation-driven/>

<mvc:view-resolvers>
    <mvc:script-template/>
</mvc:view-resolvers>

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

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

爪哇岛
Kotlin
@Controller
public class SampleController {

    @GetMapping("/sample")
    public String test(Model model) {
        model.addAttribute("title", "Sample title");
        model.addAttribute("body", "Sample body");
        return "template";
    }
}

以下示例显示了 Mustache 模板:

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

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

  • String template:模板内容

  • Map model:视图模型

  • RenderingContext renderingContextRenderingContext,用于访问应用程序上下文、语言环境、模板加载器和 URL(自 5.0 起)

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

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

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

爪哇岛
Kotlin
@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;
    }
}
使用非线程安全时,需要将该属性设置为 脚本引擎具有不是为并发而设计的模板库,例如 Handlebars 或 React 在 Nashorn 上运行。在这种情况下,由于此错误,需要 Java SE 8 更新 60,但通常 在任何情况下,都建议使用最新的 Java SE 补丁版本。sharedEnginefalse

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

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 Framework 具有将 Spring MVC 与 JSP 和 JSTL 一起使用的内置集成。

查看解析程序

使用 JSP 进行开发时,通常会声明一个 Bean。InternalResourceViewResolver

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) 时,必须使用特殊的视图类 ,即 ,因为 JSTL 需要一些准备工作才能实现 I18N 功能部件 工作。JstlView

Spring 的 JSP 标签库

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

标记库描述符 (TLD) 包含在 . 有关单个标签的综合参考,请浏览 API 参考或查看标签库说明。spring.tldspring-webmvc.jar

Spring 的表单标签库

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

与其他表单/输入标签库不同,Spring 的表单标签库集成了 Spring Web MVC,为标记提供对命令对象的访问权限,并引用 控制器处理。正如我们在以下示例中所示,表单标记使 JSP 更易于开发、阅读和维护。

我们浏览表单标签,并查看每个标签的使用示例。我们有 包括生成的 HTML 片段,其中某些标签需要进一步注释。

配置

表单标记库捆绑在 中。库描述符是 叫。spring-webmvc.jarspring-form.tld

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

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

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

表单标签

此标记呈现 HTML“form”元素,并公开内部标记的绑定路径 捆绑。它将命令对象放在 通过内部标记进行访问。此库中的所有其他标记都是标记的嵌套标记。PageContextform

假设我们有一个名为 的域对象。它是一个具有属性的 JavaBean 例如 和 .我们可以将它用作我们的表单支持对象 表单控制器,返回 .以下示例显示了可以执行的操作 肖:UserfirstNamelastNameform.jspform.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>

和值是从放置在 由页面控制器。继续阅读以查看更复杂的示例 内部标签如何与标签一起使用。firstNamelastNamePageContextform

下面的清单显示了生成的 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

默认情况下,此标记呈现具有绑定值的 HTML 元素。 有关此标记的示例,请参阅 Form 标记。您还可以使用 特定于 HTML5 的类型,例如 、 、 等。inputtype='text'emailteldate

标签checkbox

此标记呈现一个 HTML 标记,其设置为 。inputtypecheckbox

假设我们有首选项,例如时事通讯订阅和 爱好。下面的示例演示该类:UserPreferences

爪哇岛
Kotlin
public class Preferences {

    private boolean receiveNewsletter;
    private String[] interests;
    private String favouriteWord;

    public boolean isReceiveNewsletter() {
        return receiveNewsletter;
    }

    public void setReceiveNewsletter(boolean receiveNewsletter) {
        this.receiveNewsletter = receiveNewsletter;
    }

    public String[] getInterests() {
        return interests;
    }

    public void setInterests(String[] interests) {
        this.interests = interests;
    }

    public String getFavouriteWord() {
        return favouriteWord;
    }

    public void setFavouriteWord(String favouriteWord) {
        this.favouriteWord = favouriteWord;
    }
}

然后,相应的内容可能类似于以下内容:form.jsp

<form:form>
    <table>
        <tr>
            <td>Subscribe to newsletter?:</td>
            <%-- Approach 1: Property is of type java.lang.Boolean --%>
            <td><form:checkbox path="preferences.receiveNewsletter"/></td>
        </tr>

        <tr>
            <td>Interests:</td>
            <%-- Approach 2: Property is of an array or of type java.util.Collection --%>
            <td>
                Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
                Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
                Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
            </td>
        </tr>

        <tr>
            <td>Favourite Word:</td>
            <%-- Approach 3: Property is of type java.lang.Object --%>
            <td>
                Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
            </td>
        </tr>
    </table>
</form:form>

标记有三种方法,应能满足您的所有复选框需求。checkbox

  • 方法一:当绑定值为 的类型时,标记为绑定值为 。该特性对应于 value 属性的已解析值。java.lang.Booleaninput(checkbox)checkedtruevaluesetValue(Object)

  • 方法二:当绑定值的类型为 或 时,标记为配置的值为 存在于绑定的 .arrayjava.util.Collectioninput(checkbox)checkedsetValue(Object)Collection

  • 方法三:对于任何其他绑定值类型,将 标记为配置的值等于绑定值。input(checkbox)checkedsetValue(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 约定,即包含隐藏参数 每个复选框都带有下划线 () 作为前缀。通过这样做,您可以有效地 告诉 Spring,“复选框在表单中是可见的,我希望我的对象 无论如何,表单数据都会绑定以反映复选框的状态。checkbox_

标签checkboxes

此标记呈现多个 HTML 标记,并将 设置为 。inputtypecheckbox

本部分基于上一标记部分中的示例。有时,你更喜欢 不必在JSP页面中列出所有可能的爱好。你宁愿提供 运行时可用选项的列表,并将其传递给标记。那就是 标记的用途。您可以传入 、 或包含 属性中的可用选项。通常,绑定属性是 集合,以便它可以保存用户选择的多个值。以下示例 显示了使用此标记的 JSP:checkboxcheckboxesArrayListMapitems

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

此示例假定 是 可用作模型属性 包含要从中选择的值的字符串。如果使用 , 地图条目键用作值,地图条目的值用作 要显示的标签。您还可以使用自定义对象,您可以在其中提供 value by using 和标签的属性名称。interestListListMapitemValueitemLabel

标签radiobutton

此标记呈现一个 HTML 元素,其设置为 。inputtyperadio

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

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

此标记呈现多个 HTML 元素,并将 设置为 。inputtyperadio

复选框标记一样,您可能希望 将可用选项作为运行时变量传入。对于此用法,您可以使用标记。传入一个 、 a 或 a 包含 物业中的可用选项。如果使用 ,则映射输入键为 用作值,映射条目的值用作要显示的标签。 还可以使用自定义对象,在该对象中可以提供值的属性名称 by using 和标签 by using ,如以下示例所示:radiobuttonsArrayListMapitemsMapitemValueitemLabel

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

此标记呈现一个 HTML 标记,其类型设置为 具有绑定值。inputpassword

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

请注意,默认情况下,不显示密码值。如果您确实需要 密码值,可以将属性的值设置为 ,如以下示例所示:showPasswordtrue

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

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

假设 a 有一个技能列表。相应的 HTML 可以如下所示:User

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

如果技能在草药学中,则“技能”行的 HTML 源代码可能是 如下:User’s

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

此标记呈现 HTML 元素。它根据 bound, 设置 价值。以下 HTML 显示了它的典型输出:optionselected

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

如果房子在格兰芬多,那么“房子”行的 HTML 源代码将是 如下:User’s

<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

此标记呈现 HTML 元素列表。它设置属性, 基于绑定值。以下 HTML 显示了它的典型输出:optionselected

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

如果居住在英国,“国家/地区”行的 HTML 源代码如下:User

<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

如前面的示例所示,标记与标记的组合用法 生成相同的标准 HTML,但允许您在 仅用于显示(它所属的位置)的 JSP,例如 示例:“-- 请选择”。optionoptions

该属性通常使用项对象的集合或数组进行填充。 并引用这些 item 对象的 Bean 属性,如果 指定。否则,项对象本身将转换为字符串。或者 您可以指定 A of items,在这种情况下,映射键将解释为 option 值和映射值对应于选项标签。如果或(或两者兼而有之) 碰巧也被指定,item value 属性将应用于 map 键,并且 Item Label 属性应用于 Map 值。itemsitemValueitemLabelMapitemValueitemLabel

标签textarea

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

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

此标记呈现一个 HTML 标记,其值设置为绑定值。提交 未绑定的隐藏值,请使用设置为 的 HTML 标记。 以下 HTML 显示了它的典型输出:inputtypehiddeninputtypehidden

<form:hidden path="house"/>

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

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

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

假设我们希望在提交表单后显示 和 字段的所有错误消息。我们有一个用于类实例的验证器 调用,如以下示例所示:firstNamelastNameUserUserValidator

爪哇岛
Kotlin
public class UserValidator implements Validator {

    public boolean supports(Class candidate) {
        return User.class.isAssignableFrom(candidate);
    }

    public void validate(Object obj, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
    }
}

可能如下所示:form.jsp

<form:form>
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName"/></td>
            <%-- Show errors for firstName field --%>
            <td><form:errors path="firstName"/></td>
        </tr>

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

如果我们提交的表单在 和 字段中的值为空, HTML 将如下所示:firstNamelastName

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

标记库描述符 (TLD) 包含在 . 有关单个标签的综合参考,请浏览 API 参考或查看标签库说明。spring-form.tldspring-webmvc.jar

HTTP 方法转换

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

为了支持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 方法 请求参数。它由 ,在 中定义 web.xml,如以下示例所示:HiddenHttpMethodFilter

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

<filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

以下示例显示了相应的方法:@Controller

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

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

表单标记支持输入除 以外的 type 属性。这是 旨在允许呈现新的 HTML5 特定输入类型,例如 、 、 等。请注意,输入不是必需的,因为是默认类型。inputtextemaildaterangetype='text'text

1.10.6. 图块

您可以像在 Web 中集成 Tiles 一样 - 就像任何其他视图技术一样 使用 Spring 的应用程序。本节从广义上介绍了如何执行此操作。

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

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

配置

为了能够使用磁贴,您必须使用包含定义的文件对其进行配置 (有关定义和其他磁贴概念的基本信息,请参阅 https://tiles.apache.org)。在 Spring 中,这是通过使用 . 以下示例配置演示了如何执行此操作:TilesConfigurerApplicationContext

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

前面的示例定义了五个包含定义的文件。文件都是 位于目录中。在初始化 时, 加载文件,并初始化定义工厂。在那之后有 完成定义文件中包含的 Tiles 可以用作 Spring Web 应用程序。为了能够使用这些视图,您必须拥有与 Spring 中的任何其他视图技术一样:通常是一个方便的 .WEB-INF/defsWebApplicationContextViewResolverTilesViewResolver

可以通过添加下划线来指定特定于区域设置的磁贴定义,然后 区域设置,如以下示例所示:

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

在前面的配置中,用于具有 locale, 并默认使用。tiles_fr_FR.xmlfr_FRtiles.xml

由于下划线用于指示区域设置,因此建议不要使用 否则,它们将出现在磁贴定义的文件名中。
UrlBasedViewResolver

它为每个视图实例化给定的 解决。以下 bean 定义了一个:UrlBasedViewResolverviewClassUrlBasedViewResolver

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

作为一项高级功能,Spring 还支持两种特殊的 Tiles 实现。有关如何在切片定义文件中使用引用的详细信息,请参阅切片文档。PreparerFactoryViewPreparer

您可以指定根据 指定的 preparer 类,应用 Spring 的容器回调以及应用 配置了 Spring BeanPostProcessors。如果 Spring 的上下文范围注解配置具有 被激活,则会自动检测类中的注释,并且 应用的。请注意,这需要 Tiles 定义文件中的 preparer 类,因为 默认值为。SimpleSpringPreparerFactoryViewPreparerViewPreparerPreparerFactory

您可以指定对指定的准备者名称进行操作(而不是 的 classes),从 DispatcherServlet 的 应用程序上下文。完整的 Bean 创建过程由 Spring 控制 在这种情况下,应用程序上下文允许使用显式依赖注入 配置、作用域 Bean 等。请注意,您需要定义一个 Spring bean 定义 每个编制者名称(如磁贴定义中使用的名称)。以下示例显示 如何在 Bean 上定义属性:SpringBeanPreparerFactorySpringBeanPreparerFactoryTilesConfigurer

<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 和 RSS 源视图。他们 基于ROME项目,位于 包。AbstractAtomFeedViewAbstractRssFeedViewAbstractFeedVieworg.springframework.web.servlet.view.feed

AbstractAtomFeedView要求你实现方法和 (可选)重写该方法(默认实现为 空的)。以下示例演示如何执行此操作:buildFeedEntries()buildFeedMetadata()

爪哇岛
Kotlin
public class SampleContentAtomView extends AbstractAtomFeedView {

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

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

类似的要求也适用于实现,如以下示例所示:AbstractRssFeedView

爪哇岛
Kotlin
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
    }
}

和方法传入 HTTP 请求,以防万一 您需要访问区域设置。HTTP 响应仅用于设置 Cookie 或其他 HTTP 标头。源会自动写入响应 对象。buildFeedItems()buildFeedEntries()

有关创建 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.AbstractPdfViewbuildPdfDocument()

爪哇岛
Kotlin
public class PdfWordList extends AbstractPdfView {

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

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

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

Excel 视图

从 Spring Framework 4.2 开始,作为基础提供 Excel 视图的类。它基于 Apache POI,具有专门的子类 ( 和 ) 来取代过时的类。org.springframework.web.servlet.view.document.AbstractXlsViewAbstractXlsxViewAbstractXlsxStreamingViewAbstractExcelView

编程模型类似于 ,作为中心模板方法,控制器能够从 外部定义(按名称)或作为处理程序方法的实例。AbstractPdfViewbuildExcelDocument()View

1.10.9. 杰克逊

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

基于 Jackson 的 JSON MVC 视图

使用 Jackson 库来呈现响应 内容作为 JSON。默认情况下,模型映射的全部内容(除了 特定于框架的类)编码为 JSON。对于 地图需要过滤,可以指定一组特定的模型属性进行编码 通过使用属性。还可以使用该属性直接提取和序列化单键模型中的值 而不是作为模型属性的映射。MappingJackson2JsonViewObjectMappermodelKeysextractValueFromSingleKeyModel

您可以使用 Jackson 提供的 根据需要自定义 JSON 映射 附注。当您需要进一步控制时,可以通过属性注入自定义,以备需要提供自定义 JSON 的情况 特定类型的序列化程序和反序列化程序。ObjectMapperObjectMapper

基于 Jackson 的 XML 视图

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

您可以根据需要使用 JAXB 或 Jackson 提供的 XML 映射 附注。当您需要进一步控制时,可以通过属性注入自定义,对于自定义 XML 您需要为特定类型提供序列化程序和解串程序。XmlMapperObjectMapper

1.10.10.XML 编组

使用 XML(在包中定义)将响应内容呈现为 XML。您可以将对象显式设置为 使用实例的 Bean 属性进行封送。或者 该视图循环访问所有模型属性,并封送支持的第一个类型 由 .有关包中的功能的详细信息,请参阅使用 O/X 映射器封送 XMLMarshallingViewMarshallerorg.springframework.oxmMarshallingViewmodelKeyMarshallerorg.springframework.oxm

1.10.11. XSLT视图

XSLT 是 XML 的一种转换语言,在 Web 中作为一种视图技术很流行 应用。如果您的应用程序,XSLT 作为视图技术可能是一个不错的选择 自然而然地处理 XML,或者您的模型可以很容易地转换为 XML。以下 部分展示了如何生成 XML 文档作为模型数据,并将其转换为 Spring Web MVC 应用程序中的 XSLT。

这个例子是一个简单的 Spring 应用程序,它在 中创建了一个单词列表,并将它们添加到模型映射中。将返回地图和视图 XSLT 视图的名称。有关Spring Web MVC接口的详细信息,请参阅带注释的控制器。XSLT 控制器将单词列表转换为简单的 XML 文档已准备好进行转换。ControllerController

配置是一个简单的 Spring Web 应用程序的标准配置:MVC 配置 必须定义 Bean 和常规 MVC 注解配置。 以下示例演示如何执行此操作:XsltViewResolver

爪哇岛
Kotlin
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public XsltViewResolver xsltViewResolver() {
        XsltViewResolver viewResolver = new XsltViewResolver();
        viewResolver.setPrefix("/WEB-INF/xsl/");
        viewResolver.setSuffix(".xslt");
        return viewResolver;
    }
}
控制器

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

控制器逻辑封装在一个类中,其中 处理程序方法定义如下:@Controller

爪哇岛
Kotlin
@Controller
public class XsltController {

    @RequestMapping("/")
    public String home(Model model) throws Exception {
        Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
        Element root = document.createElement("wordList");

        List<String> words = Arrays.asList("Hello", "Spring", "Framework");
        for (String word : words) {
            Element wordNode = document.createElement("word");
            Text textNode = document.createTextNode(word);
            wordNode.appendChild(textNode);
            root.appendChild(wordNode);
        }

        model.addAttribute("wordList", root);
        return "home";
    }
}

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

有一些软件包可以自动“支配” 一个对象图,但是,在 Spring 中,您可以完全灵活地创建 DOM 以您选择的任何方式从您的模型中。这样可以防止 XML 播放的转换 模型数据结构中过多的部分,这在使用工具时是一种危险 来管理 DOMification 过程。

转型

最后,解析“home”XSLT 模板文件,并将 DOM 文档来生成我们的视图。如配置中所示,XSLT 模板位于目录中的文件中 并以文件扩展名结尾。XsltViewResolverXsltViewResolverwarWEB-INF/xslxslt

下面的示例演示 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 配置创建的基础 Bean 和 MVC 命名空间。如果要了解更多信息,请参阅特殊 Bean 类型Web MVC 配置

1.11.1. 启用 MVC 配置

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig {
}

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

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}

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

1.11.3. 类型转换

默认情况下,会安装各种数字和日期类型的格式化程序,并提供支持 用于通过字段和字段进行自定义。@NumberFormat@DateTimeFormat

要在 Java 配置中注册自定义格式化程序和转换器,请使用以下命令:

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

若要在 XML 配置中执行相同的操作,请使用以下命令:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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 规范中。对于这种情况,可以按如下方式自定义日期和时间格式:

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
请参阅 FormatterRegistrar SPI 和 有关何时使用的更多信息 FormatterRegistrar 实现。FormattingConversionServiceFactoryBean

1.11.4. 验证

缺省情况下,如果存在 Bean Validation 在类路径(例如,Hibernate Validator)上,是 注册为全局验证器,用于控制器方法参数和控制器方法参数。LocalValidatorFactoryBean@ValidValidated

在 Java 配置中,您可以自定义全局实例,如 以下示例显示:Validator

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

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

爪哇岛
Kotlin
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }
}
如果您需要在某处注入一个,请创建一个 bean 和 标记它以避免与 MVC 配置中声明的冲突。LocalValidatorFactoryBean@Primary

1.11.5. 拦截器

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleChangeInterceptor());
        registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
    }
}

下面的示例演示如何在 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:interceptors>
映射拦截器并不适合作为安全层,因为潜在的 对于带注释的控制器路径匹配不匹配,也可以匹配尾随 透明的斜杠和路径扩展,以及其他路径匹配选项。多 这些选项已被弃用,但不匹配的可能性仍然存在。 通常,我们建议使用 Spring Security,它包括一个专用的 MvcRequestMatcher 以与 Spring MVC 路径匹配保持一致,并且还具有阻止许多 URL 路径中不需要的字符。

1.11.6. 内容类型

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

默认情况下,仅检查标头。Accept

如果必须使用基于 URL 的内容类型解析,请考虑使用 query 参数 路径扩展的策略。请参阅后缀匹配和后缀匹配和 RFD 更多细节。

在 Java 配置中,您可以自定义请求的内容类型解析,因为 以下示例显示:

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

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

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

1.11.7. 消息转换器

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

以下示例使用自定义转换器(而不是默认转换器)添加 XML 和 Jackson JSON 转换器:ObjectMapper

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {

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

在前面的示例中,Jackson2ObjectMapperBuilder 用于为两者创建通用配置,并在启用缩进的情况下,使用自定义日期格式、 以及 jackson-module-parameter-names 的注册, 它增加了对访问参数名称的支持(Java 8 中添加的功能)。MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter

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

如果在类路径上检测到以下已知模块,它还会自动注册这些模块:

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

其他有趣的 Jackson 模块可用:

下面的示例演示如何在 XML 中实现相同的配置:

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

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

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

1.11.8. 视图控制器

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

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例实现与前面的示例相同的功能,但使用 XML 时,通过 使用元素:<mvc:view-controller>

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

如果方法映射到任何 HTTP 方法的 URL,则视图 控制器不能用于处理相同的 URL。这是因为 URL 匹配到 带注释的控制器被认为是端点所有权的足够有力的指示,因此 405 (METHOD_NOT_ALLOWED)、415 (UNSUPPORTED_MEDIA_TYPE) 或类似响应可以 发送到客户端以帮助进行调试。因此,建议避免 在带注释的控制器和视图控制器之间拆分 URL 处理。@RequestMapping

1.11.9. 查看解析器

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

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

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

但请注意,FreeMarker、Tiles、Groovy Markup 和脚本模板也需要 基础视图技术的配置。

MVC 命名空间提供专用元素。以下示例适用于 FreeMarker:

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

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

在 Java 配置中,您可以添加相应的 bean, 如以下示例所示:Configurer

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

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

1.11.10. 静态资源

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

在下一个示例中,给定一个以 开头的请求,相对路径为 用于查找和提供相对于 Web 应用程序下的静态资源 根目录或位于 下的类路径上。这些资源在一年内提供 过期,以确保最大限度地使用浏览器缓存并减少 HTTP 请求 由浏览器制作。从中推断出信息,以便标头支持 HTTP 条件请求。/resources/public/staticLast-ModifiedResource#lastModified"Last-Modified"

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

爪哇岛
Kotlin
@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)));
    }
}

下面的示例演示如何在 XML 中实现相同的配置:

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

资源处理程序还支持 ResourceResolver 实现和 ResourceTransformer 实现链。 可用于创建用于处理优化资源的工具链。

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

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

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

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

然后,您可以使用重写 URL 并应用解析器的完整链和 转换器 — 例如,插入版本。MVC 配置提供了一个 bean,以便可以将其注入到其他 bean 中。您还可以使用 Thymeleaf、JSP、FreeMarker 和其他带有 URL 标记的重写透明 恃。ResourceUrlProviderResourceUrlProviderResourceUrlEncodingFilterHttpServletResponse#encodeURL

请注意,当同时使用两者时(例如,用于提供 gzip 或 brotli-encoded resources) 和 ,您必须按此顺序注册它们。 这可确保始终根据未编码的文件可靠地计算基于内容的版本。EncodedResourceResolverVersionResourceResolver

对于 WebJars,版本化 URL like 是推荐且最有效的使用方式。 相关资源位置是使用 Spring Boot 开箱即用配置的(或者可以配置 手动通过 ),并且不需要添加依赖项。/webjars/jquery/1.2.0/jquery.min.jsResourceHandlerRegistryorg.webjars:webjars-locator-core

无版本的URL是通过当库存在于类路径上时自动注册的,但代价是 类路径扫描,这可能会减慢应用程序启动速度。解析器可以将 URL 重写为 包括 jar 的版本,还可以与没有版本的传入 URL 匹配,例如,from to ./webjars/jquery/jquery.min.jsWebJarsResourceResolverorg.webjars:webjars-locator-core/webjars/jquery/jquery.min.js/webjars/jquery/1.2.0/jquery.min.js

基于 Java 的配置提供了更多选项 用于细粒度控制,例如上次修改行为和优化的资源解析。ResourceHandlerRegistry

1.11.11. 默认 Servlet

Spring MVC 允许将映射到 (从而覆盖映射 容器的默认 Servlet),同时仍允许静态资源请求 由容器的默认 Servlet 处理。它使用 URL 映射和最低优先级配置 相对于其他 URL 映射。DispatcherServlet/DefaultServletHttpRequestHandler/**

此处理程序将所有请求转发到缺省 Servlet。因此,它必须 在所有其他 URL 的顺序中保持最后。那就是 如果使用 .或者,如果您设置了 自己的自定义实例,请务必将其属性设置为一个值 低于 ,即 。HandlerMappings<mvc:annotation-driven>HandlerMappingorderDefaultServletHttpRequestHandlerInteger.MAX_VALUE

以下示例演示如何使用默认设置启用该功能:

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

<mvc:default-servlet-handler/>

覆盖 Servlet 映射的注意事项是,对于 缺省 Servlet 必须按名称而不是路径进行检索。尝试自动检测 的默认 Servlet 启动时的容器,使用大多数主要 Servlet 的已知名称列表 容器(包括 Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)。 如果缺省 Servlet 已使用其他名称进行定制配置,或者如果 在缺省 Servlet 名称未知的情况下,正在使用不同的 Servlet 容器, 那么,您必须显式提供缺省 Servlet 的名称,如以下示例所示:/RequestDispatcherDefaultServletHttpRequestHandler

爪哇岛
Kotlin
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

下面的示例演示如何在 XML 中实现相同的配置:

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

1.11.12. 路径匹配

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

以下示例说明如何在 Java 配置中自定义路径匹配:

爪哇岛
Kotlin
@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() {
        // ...
    }
}

下面的示例演示如何在 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

对于高级模式,您可以直接从 中删除和扩展,而不是实现 , 如以下示例所示:@EnableWebMvcDelegatingWebMvcConfigurationWebMvcConfigurer

爪哇岛
Kotlin
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    // ...
}

您可以将现有方法保留在 中,但现在也可以覆盖 Bean 声明 从基类中,您仍然可以拥有任意数量的其他实现 类路径。WebConfigWebMvcConfigurer

1.11.14. 高级XML配置

MVC 命名空间没有高级模式。如果您需要自定义属性 一个无法更改的 bean,否则,您可以使用生命周期 弹簧的钩子,如以下示例所示:BeanPostProcessorApplicationContext

爪哇岛
Kotlin
@Component
public class MyPostProcessor implements BeanPostProcessor {

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

请注意,您需要在 XML 中显式声明为 bean,或者 通过声明让它被检测到。MyPostProcessor<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 开始,它处于维护模式,只有少量请求 今后要接受的更改和错误。请考虑使用提供更现代 API 和 支持同步、异步和流式处理方案。RestTemplate

有关详细信息,请参阅 REST 端点

2.2.WebClient

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

与 相反,支持以下功能:RestTemplateWebClient

  • 无阻塞 I/O。

  • 反应流背压。

  • 高并发,硬件资源少。

  • 函数式、流畅的 API,利用 Java 8 lambda。

  • 同步和异步交互。

  • 向上流式传输到服务器或从服务器向式处理。

有关详细信息,请参阅 WebClient

3. 测试

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

  • Servlet API Mocks:用于单元测试控制器的 Servlet API 合约的模拟实现, 筛选器和其他 Web 组件。有关更多详细信息,请参阅 Servlet API 模拟对象。

  • TestContext 框架:支持在 JUnit 和 TestNG 测试中加载 Spring 配置, 包括跨测试方法对加载的配置进行高效缓存,并支持 使用 . 有关更多详细信息,请参阅 TestContext FrameworkWebApplicationContextMockServletContext

  • Spring MVC Test:用于测试带注释的控制器的框架,也称为 通过(即支持注解),完成 Spring MVC 基础结构,但没有 HTTP 服务器。 有关更多详细信息,请参阅 Spring MVC TestMockMvcDispatcherServlet

  • 客户端 REST:提供可以用作 一个模拟服务器,用于测试内部使用 . 有关详细信息,请参阅客户端 REST 测试spring-testMockRestServiceServerRestTemplate

  • WebTestClient:专为测试 WebFlux 应用程序而构建,但也可用于 通过 HTTP 连接对任何服务器进行端到端集成测试。这是一个 非阻塞、响应式客户端,非常适合测试异步和流式处理 场景。

4. 网络套接字

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

4.1. WebSocket 简介

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

WebSocket 交互从使用 HTTP 标头的 HTTP 请求开始 升级,或者在本例中切换到 WebSocket 协议。以下示例 显示了这样的交互:Upgrade

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

支持 WebSocket 的服务器会返回输出,而不是通常的 200 状态代码 类似于以下内容:

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

握手成功后,HTTP 升级请求的基础 TCP 套接字仍保留 打开客户端和服务器以继续发送和接收消息。

对 WebSocket 工作原理的完整介绍超出了本文档的范围。 请参阅 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 不同,它没有规定 消息内容的任何语义。这意味着无法路由或处理 除非客户端和服务器在消息语义上达成一致,否则消息。

WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议 (例如,STOMP),通过 HTTP 握手请求上的标头。 如果没有这一点,他们需要提出自己的惯例。Sec-WebSocket-Protocol

4.1.2. 何时使用 WebSockets

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

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

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

还要记住,在互联网上,您无法控制的限制性代理 可能会阻止 WebSocket 交互,因为它们未配置为传递标头,或者因为它们关闭了显示为空闲的长期连接。这 意味着将 WebSocket 用于防火墙内的内部应用程序是 与面向公众的应用程序相比,决策更简单。Upgrade

4.2. WebSocket 接口

Spring Framework 提供了一个 WebSocket API,您可以使用它来编写 client- 和 处理 WebSocket 消息的服务器端应用程序。

4.2.1.WebSocketHandler

创建 WebSocket 服务器就像实现或更多一样简单 可能,扩展 或 .以下 示例用法:WebSocketHandlerTextWebSocketHandlerBinaryWebSocketHandlerTextWebSocketHandler

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 的配置中。然而,春天的 WebSocket 支持不依赖于 Spring MVC。相对简单 在 WebSocketHttpRequestHandler 的帮助下集成到其他 HTTP 服务环境中。WebSocketHandler

当直接与间接使用 API 时,例如通过 STOMP 消息传递,应用程序必须同步消息的发送 因为底层标准 WebSocket 会话 (JSR-356) 不允许并发 发送。一种选择是使用 ConcurrentWebSocketSessionDecorator 包装。WebSocketHandlerWebSocketSession

4.2.2. WebSocket握手

自定义初始 HTTP WebSocket 握手请求的最简单方法是通过 a ,它公开了握手“之前”和“之后”的方法。 您可以使用此类拦截器来阻止握手或创建任何属性 可用于 .以下示例使用内置侦听器 要将 HTTP 会话属性传递给 WebSocket 会话,请执行以下操作:HandshakeInterceptorWebSocketSession

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

更高级的选项是扩展执行的 WebSocket 握手的步骤,包括验证客户端源, 协商子协议,以及其他细节。应用程序可能还需要使用它 选项,如果它需要配置自定义以 适配尚不支持的 WebSocket 服务器引擎和版本 (有关此主题的更多信息,请参阅部署)。 Java 配置和 XML 命名空间都可以配置自定义 .DefaultHandshakeHandlerRequestUpgradeStrategyHandshakeHandler

Spring 提供了一个可用于装饰的基类 具有其他行为。日志记录和异常处理 使用 WebSocket Java 配置时,默认提供并添加实现 或 XML 命名空间。渔获物全部未获渔获 从任何方法产生并关闭 WebSocket 的异常 会话状态为 ,表示服务器错误。WebSocketHandlerDecoratorWebSocketHandlerExceptionWebSocketHandlerDecoratorWebSocketHandler1011

4.2.3. 部署

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

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

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

克服 Java WebSocket API 中上述限制的请求已 创建并可在 eclipse-ee4j/websocket-api#211 上关注。 Tomcat、Undertow 和 WebSphere 提供了自己的 API 替代方案, 使做到这一点成为可能,使用 Jetty 也是可能的。我们充满希望 更多的服务器也会做同样的事情。

第二个考虑因素是,需要支持 JSR-356 的 Servlet 容器 执行 (SCI) 扫描,这可能会减慢应用程序的速度 启动——在某些情况下,戏剧性地。如果在 升级到支持 JSR-356 的 Servlet 容器版本,它应该 可以有选择地启用或禁用 Web 片段(和 SCI 扫描) 通过使用 中的元素,如以下示例所示:ServletContainerInitializer<absolute-ordering />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 自己的 Web 片段,它为 Servlet 3 提供支持 Java 初始化 API。以下示例演示如何执行此操作:SpringServletContainerInitializer

<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,您可以将 WebSocket Java 配置,如以下示例所示:ServletServerContainerFactoryBean

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

}

以下示例显示了与前面示例等效的 XML 配置:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        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 配置,应使用 (XML) 或 (Java 配置)。WebSocketContainerFactoryBeanContainerProvider.getWebSocketContainer()

对于 Jetty,您需要提供预配置的 Jetty 和插头 通过您的 WebSocket Java 配置将其放入 Spring 中。 以下示例演示如何执行此操作:WebSocketServerFactoryDefaultHandshakeHandler

@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 的默认行为是接受 仅限同源请求。也可以允许所有或指定的源列表。 此检查主要针对浏览器客户端设计。没有什么能阻止其他类型 的客户端修改标头值(有关详细信息,请参阅 RFC 6454:Web 源概念)。Origin

三种可能的行为是:

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

  • 允许指定的源列表:每个允许的源必须以 或 开头。在此模式下,启用 SockJS 时,将禁用 IFrame 传输。 因此,当 IE6 到 IE9 出现以下情况时,不支持 IE6 到 IE9 模式已启用。http://https://

  • 允许所有源:若要启用此模式,应提供作为允许的源 价值。在此模式下,所有传输都可用。*

您可以配置 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 首先,然后回退到模拟 WebSocket 的基于 HTTP 的技术 交互并公开相同的应用程序级 API。

在 Servlet 堆栈上,Spring Framework 提供服务器(和客户端)支持 用于 SockJS 协议。

4.3.1. 概述

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

SockJS 由以下部分组成:

  • 以可执行叙述测试的形式定义的 SockJS 协议

  • SockJS JavaScript 客户端 — 用于浏览器的客户端库。

  • SockJS 服务器实现,包括 Spring Framework 模块中的一个。spring-websocket

  • 模块中的 SockJS Java 客户端(从 4.1 版开始)。spring-websocket

SockJS 专为在浏览器中使用而设计。它使用多种技术 以支持各种浏览器版本。 有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS 客户端页面。运输 分为三大类:WebSocket、HTTP 流和 HTTP 长轮询。 有关这些类别的概述,请参阅此博客文章

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

所有传输请求都具有以下 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 添加了最少的消息框架。例如,服务器最初发送字母(“打开”帧),消息作为(JSON编码的数组)发送,如果没有消息流,则发送字母(“心跳”帧) 25 秒(默认),然后用字母(“关闭”框)关闭会话。oa["message1","message2"]hc

若要了解详细信息,请在浏览器中运行示例并监视 HTTP 请求。 SockJS 客户端允许修复传输列表,因此可以 一次查看一个传输。SockJS 客户端还提供了一个调试标志, 这会在浏览器控制台中启用有用的消息。在服务器端,您可以为 启用日志记录。 有关更多详细信息,请参阅 SockJS 协议叙述测试TRACEorg.springframework.web.socket

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 的帮助下集成到其他 HTTP 服务环境中。

在浏览器端,应用程序可以使用 sockjs-client(版本 1.0.x)。它 模拟 W3C WebSocket API 并与服务器通信以选择最佳 transport 选项,具体取决于运行该选项的浏览器。请参阅 sockjs-client 页面和 浏览器支持的传输类型。客户端还提供几个 配置选项 — 例如,指定要包含的传输。

4.3.3. IE 8 和 9

Internet Explorer 8 和 9 仍在使用中。他们是 拥有 SockJS 的一个关键原因。本节涵盖重要的内容 有关在这些浏览器中运行的注意事项。

SockJS 客户端通过使用 Microsoft 的 XDomainRequest 在 IE 8 和 9 中支持 Ajax/XHR 流式处理。 这适用于跨域,但不支持发送 Cookie。 Cookie 通常对于 Java 应用程序是必不可少的。 但是,由于 SockJS 客户端可以与许多服务器一起使用 类型(不仅仅是 Java 类型),它需要知道 cookie 是否重要。 如果是这样,SockJS 客户端更喜欢 Ajax/XHR 进行流式处理。否则,它 依赖于基于 iframe 的技术。

来自 SockJS 客户端的第一个请求是 可能影响客户选择运输的信息。 其中一个细节是服务器应用程序是否依赖于 cookie (例如,出于身份验证目的或使用粘性会话进行群集)。 Spring 的 SockJS 支持包括一个名为 . 默认情况下,它是启用的,因为大多数 Java 应用程序都依赖于 cookie。如果您的应用程序不需要它,您可以关闭此选项, 然后,SockJS 客户端应该在 IE 8 和 9 中进行选择。/infosessionCookieNeededJSESSIONIDxdr-streaming

如果您使用基于 iframe 的传输,请记住 可以通过以下方式指示浏览器阻止在给定页面上使用 IFrames 将 HTTP 响应标头设置为 、 或 。这用于防止点击劫持X-Frame-OptionsDENYSAMEORIGINALLOW-FROM <origin>

Spring Security 3.2+ 支持在每个 响应。默认情况下,Spring Security Java 配置将其设置为 。 在 3.2 中,Spring Security XML 命名空间默认不设置该标头 但可以配置为这样做。将来,它可能会默认设置它。X-Frame-OptionsDENY

有关如何配置 标头的设置。您还可以查看 gh-2718 了解其他背景信息。X-Frame-Options

如果应用程序添加了响应标头(理应如此! 并且依赖于基于 iframe 的传输,您需要将标头值设置为 或 。The Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它已加载 从 iframe。默认情况下,iframe 设置为下载 SockJS 客户端 从 CDN 位置。最好将此选项配置为使用 与应用程序来自同一来源的 URL。X-Frame-OptionsSAMEORIGINALLOW-FROM <origin>

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

    // ...

}

XML 命名空间通过元素提供类似的选项。<websocket:sockjs>

在初始开发期间,请启用 SockJS 客户端模式,以防止 浏览器缓存 SockJS 请求(如 iframe),否则会 被缓存。有关如何启用它的详细信息,请参阅 SockJS 客户端页面。devel

4.3.4. 心跳

SockJS 协议要求服务器发送心跳消息以排除代理 从得出连接已挂起的结论。Spring SockJS 配置有一个属性 调用,可用于自定义频率。默认情况下,一个 检测信号在 25 秒后发送,假设没有发送其他消息 连接。此 25 秒值符合以下 IETF 对公共 Internet 应用程序的建议。heartbeatTime

使用 STOMP over WebSocket 和 SockJS 时,如果 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 尽最大努力识别 此类网络故障表示客户端断开连接(特定于每个服务器)和日志 使用专用日志类别的最小消息(在 中定义)。如果需要查看堆栈跟踪,可以将其设置为 将 log 类别设置为 TRACE。DISCONNECTED_CLIENT_LOG_CATEGORYAbstractSockJsSession

4.3.6. SockJS 和 CORS

如果允许跨域请求(请参阅允许的源),则 SockJS 协议 使用 CORS 在 XHR 流式处理和轮询传输中提供跨域支持。因此 CORS 标头是自动添加的,除非响应中存在 CORS 标头 被检测到。因此,如果应用程序已配置为提供 CORS 支持(例如, 通过 Servlet 过滤器),Spring 跳过了这一部分。SockJsService

也可以通过在 Spring 的 SockJsService 中设置属性来禁用这些 CORS 标头的添加。suppressCors

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 年)。

有关确切的实现,请参阅 in 和 源代码中的枚举。addCorsHeadersAbstractSockJsServiceTransportType

或者,如果 CORS 配置允许,请考虑排除具有 SockJS 端点前缀,从而让 Spring 处理它。SockJsService

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

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

  • RestTemplateXhrTransport将 Spring 用于 HTTP 请求。RestTemplate

  • JettyXhrTransport将 Jetty 用于 HTTP 请求。HttpClient

以下示例演示如何创建 SockJS 客户端并连接到 SockJS 终结点:

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

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS 使用 JSON 格式的数组来存储消息。默认情况下,使用 Jackson 2 并且需要 位于类路径上。或者,您可以配置 的自定义实现,并在 上对其进行配置。SockJsMessageCodecSockJsClient

要用于模拟大量并发用户,您需要 需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够的 连接数和线程数。以下示例演示如何使用 Jetty 执行此操作:SockJsClient

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 将该属性设置为 512KB(默认值为 128KB — )。streamBytesLimit128 * 1024
2 将该属性设置为 1,000(默认值为 )。httpMessageCacheSize100
3 将属性设置为 30 个属性秒(默认值为 5 秒 — )。disconnectDelay5 * 1000

4.4. 踩踏

WebSocket 协议定义了两种类型的消息(文本和二进制),但它们 内容未定义。该协议定义了客户端和服务器协商 子协议(即更高级别的消息传递协议),以在 WebSocket 之上使用 定义每个可以发送的消息类型、格式、每个消息的内容 消息等。子协议的使用是可选的,但无论哪种方式,客户端和 服务器需要就定义消息内容的某个协议达成一致。

4.4.1. 概述

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

STOMP 是一种基于帧的协议,其帧以 HTTP 为模型。下面的清单显示了结构 STOMP 帧:

COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用 or 命令发送或订阅 消息,以及描述什么的标头 消息是关于谁应该接收的。这使得一个简单的 发布-订阅机制,可用于通过代理发送消息 发送到其他连接的客户端或向服务器发送消息以请求 执行一些工作。SENDSUBSCRIBEdestination

当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序会起作用 作为客户的 STOMP 经纪人。消息被路由到消息处理 方法或跟踪订阅和 向订阅用户广播消息。您还可以将 Spring 配置为工作 使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)进行实际 消息广播。在这种情况下,Spring 维护 与代理的 TCP 连接,向代理中继消息,并传递消息 从它到连接的 WebSocket 客户端。因此,Spring Web 应用程序可以 依靠基于 HTTP 的统一安全性、通用验证和熟悉的编程 消息处理模型。@Controller

以下示例显示了一个订阅接收股票报价的客户,该 服务器可能会定期发出(例如,通过发送消息的计划任务 通过 A 到经纪人):SimpMessagingTemplate

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

^@

以下示例显示了一个发送交易请求的客户端,该服务器 可以通过以下方法进行处理:@MessageMapping

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

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

执行后,服务器可以 向客户广播交易确认消息和详细信息。

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

STOMP 服务器可以使用该命令向所有订阅者广播消息。 以下示例显示了向订阅客户端发送股票报价的服务器:MESSAGE

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

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

服务器无法发送未经请求的邮件。所有消息 from a server 必须响应特定的客户端订阅,并且服务器消息的标头必须与 客户端订阅。subscriptionid

前面的概述旨在提供对 STOMP 协议。我们建议全面查看协议规范

4.4.2. 好处

使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 与使用原始 WebSocket 相比,提供更丰富的编程模型。同一点可以是 关于HTTP与原始TCP以及它如何让Spring MVC和其他Web框架 提供丰富的功能。以下是一系列好处:

  • 无需发明自定义消息传递协议和消息格式。

  • STOMP 客户端(包括 Spring Framework 中的 Java 客户端)可用。

  • 您可以(可选)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来 管理订阅和广播消息。

  • 应用程序逻辑可以组织在任意数量的实例中,消息可以 根据 STOMP 目标标头路由到它们,而不是处理原始 WebSocket 消息 对于给定的连接,使用单个。@ControllerWebSocketHandler

  • 您可以使用 Spring Security 根据 STOMP 目标和消息类型来保护消息。

4.4.3. 启用 STOMP

和模块中提供了基于 WebSocket 的 STOMP 支持。一旦你有了这些依赖项,你就可以公开一个 STOMP 端点,通过带有 SockJS 回退的 WebSocket,如以下示例所示:spring-messagingspring-websocket

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

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); (2)
        config.enableSimpleBroker("/topic", "/queue"); (3)
    }
}
1 /portfolio是 WebSocket(或 SockJS)所到的终结点的 HTTP URL 客户端需要连接才能进行 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>
对于内置的简单代理,和前缀没有任何特殊 意义。它们只是区分 pub-sub 与点对点的约定 消息传递(即,多个订阅者与一个使用者)。当您使用外部代理时, 查看代理的 STOMP 页面,了解什么样的 STOMP 目的地和 它支持的前缀。/topic/queue

要从浏览器连接,对于 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) {
}

请注意,在前面的示例中不需要指定 和 标头。即使这样做了,它们也会被忽略(或者更确切地说, overrided)在服务器端。有关身份验证的更多信息,请参阅连接到代理身份验证stompClientloginpasscode

有关更多示例代码,请参阅:

4.4.4. WebSocket 服务器

若要配置基础 WebSocket 服务器,应应用“服务器配置”中的信息。但是,对于 Jetty,您需要设置 和通过:HandshakeHandlerWebSocketPolicyStompEndpointRegistry

@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 应用程序就会成为 连接的客户端。本节介绍服务器端的消息流。

该模块包含对消息传递应用程序的基础支持 起源于 Spring Integration,是 后来被提取并合并到 Spring Framework 中,以便在许多 Spring 项目和应用程序场景中更广泛地使用。 以下列表简要介绍了一些可用的消息传递抽象:spring-messaging

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

消息流简单代理

上图显示了三个消息通道:

  • clientInboundChannel:用于传递从 WebSocket 客户端接收的消息。

  • clientOutboundChannel:用于向 WebSocket 客户端发送服务器消息。

  • brokerChannel:用于从内部向消息代理发送消息 服务器端应用程序代码。

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

消息流代理中继

前面两个图之间的主要区别在于使用“代理中继”进行传递 通过 TCP 向上传递到外部 STOMP 代理的消息,并将消息从 代理订阅客户端。

当从 WebSocket 连接接收消息时,它们被解码为 STOMP 帧, 转换为 Spring 表示,并发送到 进行进一步处理。例如,STOMP 消息 以 开头的目标标头可以路由到 带注释的控制器,而 和 消息可以直接路由 到消息代理。MessageclientInboundChannel/app@MessageMapping/topic/queue

处理来自客户端的 STOMP 消息的注释可以将消息发送到 消息代理通过 ,并且代理广播 通过 .一样 控制器也可以执行相同的操作来响应 HTTP 请求,因此客户端可以执行 HTTP POST,然后一个方法可以向消息代理发送消息 广播到订阅的客户端。@ControllerbrokerChannelclientOutboundChannel@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. 客户端连接到 WebSocket 连接,一旦 WebSocket 连接 建立后,STOMP 帧开始在其上流动。http://localhost:8080/portfolio

  2. 客户端发送目标标头为 的 SUBSCRIBE 帧。一旦收到 并解码,消息被发送到 然后路由到 Message Broker,用于存储客户端订阅。/topic/greetingclientInboundChannel

  3. 客户端将 SEND 帧发送到 。前缀有助于将其路由到 带注释的控制器。去除前缀后,目标的其余部分将映射到 中的方法。/app/greeting/app/app/greeting@MessageMappingGreetingController

  4. 返回的值将转换为 Spring 基于返回值的有效负载和默认目标标头(派生自输入目标,替换为 )。生成的消息被发送到 并进行处理 由消息代理提供。GreetingControllerMessage/topic/greeting/app/topicbrokerChannel

  5. 消息代理查找所有匹配的订阅者,并向每个订阅者发送一个 MESSAGE 帧 通过 ,从中将消息编码为 STOMP 帧 并在 WebSocket 连接上发送。clientOutboundChannel

下一节将提供有关带注释的方法的更多详细信息,包括 支持的参数和返回值的种类。

4.4.6. 带注释的控制器

应用程序可以使用带批注的类来处理来自客户端的消息。 此类可以声明 、 和方法,如以下主题中所述:@Controller@MessageMapping@SubscribeMapping@ExceptionHandler

@MessageMapping

您可以使用以下方法对路由消息的方法进行批注: 目的地。它在方法级别和类型级别都受支持。在类型 level,用于表示 控制器。@MessageMapping@MessageMapping

默认情况下,映射值为 Ant 样式的路径模式(例如 、 )、 包括对模板变量的支持(例如,)。这些值可以是 通过方法参数引用。应用程序还可以切换到 映射的点分隔目标约定,如点作为分隔符中所述。/thing*/thing/**/thing/{id}@DestinationVariable

支持的方法参数

下表描述了方法参数:

方法参数 描述

Message

用于访问完整消息。

MessageHeaders

用于访问 .Message

MessageHeaderAccessorSimpMessageHeaderAccessorStompHeaderAccessor

用于通过类型化访问器方法访问标头。

@Payload

为了访问消息的有效负载,由配置的 .MessageConverter

此注释的存在不是必需的,因为默认情况下,如果没有,则假定它不存在 其他参数匹配。

你可以用 或 Spring 的 , 以自动验证有效负载参数。@javax.validation.Valid@Validated

@Header

用于访问特定的标头值,以及在必要时使用 的类型转换。org.springframework.core.convert.converter.Converter

@Headers

用于访问邮件中的所有标头。此参数必须可分配给 。java.util.Map

@DestinationVariable

用于访问从消息目标中提取的模板变量。 根据需要,将值转换为声明的方法参数类型。

java.security.Principal

反映在 WebSocket HTTP 握手时登录的用户。

返回值

默认情况下,方法的返回值将序列化为有效负载 通过匹配并作为 a 发送到 , 从哪里广播给订阅者。出站消息的目的地是 与入站消息相同,但以 为前缀。@MessageMappingMessageConverterMessagebrokerChannel/topic

您可以使用 和 注释来自定义 输出消息。 用于自定义目标目标或 指定多个接收方。 用于定向输出消息 仅与输入消息关联的用户。请参阅用户目标@SendTo@SendToUser@SendTo@SendToUser

您可以在同一方法上同时使用两者,也可以同时使用两者 在类级别受支持,在这种情况下,它们充当 类。但是,请记住,任何方法级别或注释 在类级别覆盖任何此类注释。@SendTo@SendToUser@SendTo@SendToUser

消息可以异步处理,方法可以返回 、 或 。@MessageMappingListenableFutureCompletableFutureCompletionStage

请注意,和只是为了方便,相当于使用 发送消息。如有必要,对于更高级的方案,方法可以直接回退到使用 。 这可以代替返回值,或者可能除了返回值之外。 请参阅发送消息@SendTo@SendToUserSimpMessagingTemplate@MessageMappingSimpMessagingTemplate

@SubscribeMapping

@SubscribeMapping类似于,但将映射范围缩小到 仅限订阅消息。它支持与 相同的方法参数。然而 对于返回值,默认情况下,消息直接发送到客户端(通过 ,以响应订阅),而不是发送到代理(通过 ,作为对匹配订阅的广播)。添加或覆盖此行为并改为发送到代理。@MessageMapping@MessageMappingclientOutboundChannelbrokerChannel@SendTo@SendToUser

这在什么时候有用?假设代理映射到 和 ,而 应用程序控制器映射到 。在此设置中,代理存储所有 订阅并用于重复广播,以及 应用程序无需参与其中。客户端也可以订阅 某个目标,控制器可以返回一个值来响应该目标 在不涉及代理的情况下进行订阅,而无需再次存储或使用订阅 (实际上是一次性的请求-回复交换)。其中一个用例是填充 UI 启动时的初始数据。/topic/queue/app/topic/queue/app

什么时候这没有用?不要尝试将代理和控制器映射到同一目标 前缀,除非您希望两者独立处理消息,包括订阅、 出于某种原因。入站邮件是并行处理的。无法保证是否 代理或控制器首先处理给定的消息。如果要通知目标 当订阅被存储并准备好进行广播时,客户端应该要求一个 如果服务器支持,则为收据(Simple Broker 不支持)。例如,使用 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(receiptHeaders -> {
    // Subscription ready...
});

服务器端选项是在处理消息(包括订阅)后注册并实现调用的方法。ExecutorChannelInterceptorbrokerChannelafterMessageHandled

@MessageExceptionHandler

应用程序可以使用方法来处理来自方法的异常。您可以在注释中声明异常 本身或通过方法参数(如果要访问异常实例)。 下面的示例通过方法参数声明异常:@MessageExceptionHandler@MessageMapping

@Controller
public class MyController {

    // ...

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

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

通常,方法在类中应用 (或类层次结构)。如果您希望应用此类方法 更全局地(跨控制器),您可以在标有 .这与 Spring MVC 中提供的类似支持相当。@MessageExceptionHandler@Controller@ControllerAdvice

4.4.7. 发送消息

如果要从 应用?任何应用程序组件都可以向 . 最简单的方法是注入 和 使用它来发送消息。通常,您可以通过以下方式注入它 类型,如以下示例所示:brokerChannelSimpMessagingTemplate

@Controller
public class GreetingController {

    private SimpMessagingTemplate template;

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

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

}

但是,您也可以通过其名称 () 来限定它,如果另一个 存在相同类型的 bean。brokerMessagingTemplate

4.4.8. 简单代理

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

应用程序还可以使用点分隔(而不是斜杠分隔)目标。 请参阅点作为分隔符

如果配置了任务调度程序,那么简单代理将支持 STOMP 检测信号。 要配置调度程序,您可以声明自己的 Bean 并设置它 这。或者,您可以使用自动的那个 但是,您需要避免在内置 WebSocket 配置中声明 内置 WebSocket 配置和 .例如:TaskSchedulerMessageBrokerRegistry@LazyWebSocketMessageBrokerConfigurer

@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 命令(它不支持 acks、receipts 和其他一些功能), 依赖于简单的消息发送循环,不适合群集。 或者,您可以升级应用程序以使用功能齐全的 消息代理。

请参阅您选择的消息代理(例如 RabbitMQ、ActiveMQ 等)的 STOMP 文档,安装代理, 并在启用 STOMP 支持的情况下运行它。然后,您可以启用 STOMP 代理中继 (而不是简单的代理)在 Spring 配置中。

以下示例配置启用功能齐全的代理:

@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 会话。从本质上讲,它充当转发消息的“中继” 在两个方向上。

向项目添加依赖项以进行 TCP 连接管理。io.projectreactor.netty:reactor-nettyio.netty:netty-all

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

实际上,代理中继支持可靠且可扩展的消息广播。

4.4.10. 连接到 Broker

STOMP 代理中继维护与代理的单个“系统”TCP 连接。 此连接用于源自服务器端应用程序的消息 仅用于接收消息。您可以配置 STOMP 凭证(即 STOMP 帧和标头)用于此连接。这是公开的 在 XML 命名空间和 Java 配置中作为 和 属性,默认值为 和 。loginpasscodesystemLoginsystemPasscodeguestguest

STOMP 代理中继还为每个连接的 TCP 连接创建一个单独的 TCP 连接 WebSocket 客户端。您可以配置用于所有 TCP 的 STOMP 凭证 代表客户端创建的连接。这在 XML 命名空间中公开 和 Java 配置作为 和 属性,默认 和 的值。clientLoginclientPasscodeguestguest

STOMP 代理中继始终在代表客户端转发给代理的每个帧上设置 and 标头。因此,WebSocket 客户端 无需设置这些标头。它们将被忽略。正如身份验证部分所述,WebSocket 客户端应改为依赖 HTTP 身份验证来保护 WebSocket 端点并建立客户端标识。loginpasscodeCONNECT

STOMP 代理中继还向消息发送和接收检测信号 通过“系统”TCP 连接进行代理。您可以配置发送间隔 和接收心跳(默认每次 10 秒)。如果连接到代理 丢失时,Broker 中继继续尝试重新连接,每 5 秒一次, 直到它成功。

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

缺省情况下,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());
    }
}

您还可以使用属性配置 STOMP 代理中继。 此属性的值设置为每个帧的标题 并且可能很有用(例如,在云环境中,实际主机 建立的 TCP 连接与提供 基于云的 STOMP 服务)。virtualHosthostCONNECT

4.4.11. 点作为分隔符

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

以下示例显示了如何在 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. 身份验证

每个基于 WebSocket 的 STOMP 消息传递会话都以 HTTP 请求开头。 这可以是升级到 WebSockets 的请求(即 WebSocket 握手) 或者,在 SockJS 回退的情况下,一系列 SockJS HTTP 传输请求。

许多 Web 应用程序已经具备身份验证和授权功能,以便 保护 HTTP 请求。通常,用户通过 Spring Security 进行身份验证 通过使用某种机制,例如登录页面、HTTP 基本身份验证或其他方式。 经过身份验证的用户的安全上下文保存在 HTTP 会话中 并与同一基于 Cookie 的会话中的后续请求相关联。

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求, 通常,已经有一个经过身份验证的用户可通过 访问。Spring 会自动关联该用户 为它们创建 WebSocket 或 SockJS 会话,随后使用 通过用户标头通过该会话传输的 STOMP 消息。HttpServletRequest#getUserPrincipal()

简而言之,典型的 Web 应用程序不需要执行任何操作 超越了它已经为安全所做的工作。用户在以下位置进行身份验证 HTTP 请求级别,其安全上下文通过基于 Cookie 的 HTTP 会话(然后与创建的 WebSocket 或 SockJS 会话相关联 ,并导致在每个流上都标记一个用户标头 通过应用程序。Message

STOMP 协议在帧上确实有 和 标头。 它们最初是为 STOMP over TCP 而设计的,并且是 STOMP over TCP 所必需的。但是,对于 STOMP 在 WebSocket 上,默认情况下,Spring 会忽略 STOMP 协议中的身份验证标头 级别,并假定用户已在 HTTP 传输级别进行身份验证。 预期是 WebSocket 或 SockJS 会话包含经过身份验证的用户。loginpasscodeCONNECT

4.4.13. 令牌身份验证

Spring Security OAuth 提供对基于令牌的安全性的支持,包括 JSON Web 令牌 (JWT)。 您可以将其用作 Web 应用程序中的身份验证机制。 包括 STOMP over WebSocket 交互,如前面所述 部分(即,通过基于 cookie 的会话维护身份)。

同时,基于 cookie 的会话并不总是最合适的(例如, 在不维护服务器端会话的应用程序中或 通常使用标头进行身份验证的移动应用程序)。

WebSocket 协议 RFC 6455“没有规定服务器可以在 WebSocket 握手。然而,在实践中,浏览器客户端只能使用标准 身份验证标头(即基本 HTTP 身份验证)或 cookie 和 Cannot (例如) 提供自定义标头。同样,SockJS JavaScript 客户端不提供 一种使用 SockJS 传输请求发送 HTTP 标头的方法。请参阅 sockjs-client 问题 196。 相反,它确实允许发送可用于发送令牌的查询参数。 但这有其自身的缺点(例如,令牌可能是无意的 使用服务器日志中的 URL 记录)。

上述限制适用于基于浏览器的客户端,不适用于 基于 Spring Java 的 STOMP 客户端,它支持发送带有两者的标头 WebSocket 和 SockJS 请求。

因此,希望避免使用 cookie 的应用程序可能没有任何好处 HTTP 协议级别身份验证的替代方法。不使用 cookie, 他们可能更愿意在 STOMP 消息传递协议级别使用标头进行身份验证。 这样做需要两个简单的步骤:

  1. 使用 STOMP 客户端在连接时传递身份验证标头。

  2. 使用 .ChannelInterceptor

下一个示例使用服务器端配置来注册自定义身份验证 拦截 器。请注意,拦截器只需进行身份验证和设置 CONNECT 上的用户标头。Spring 记录并保存经过身份验证的 用户,并将其与同一会话上的后续 STOMP 消息相关联。以下 示例演示如何注册自定义身份验证拦截器:Message

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

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

另外,请注意,当您对消息使用 Spring Security 的授权时,目前, 您需要确保身份验证配置是有序的 领先于 Spring Security。最好通过在 它自己的实现用 标记。ChannelInterceptorWebSocketMessageBrokerConfigurer@Order(Ordered.HIGHEST_PRECEDENCE + 99)

4.4.14. 授权

Spring Security 提供 WebSocket 子协议授权,该授权使用 a 根据其中的用户标头对消息进行授权。 此外,Spring Session 还提供 WebSocket 集成,确保用户的 HTTP 会话在 WebSocket 会话仍处于活动状态时不会过期。ChannelInterceptor

4.4.15. 用户目标

应用程序可以发送针对特定用户的消息,以及 Spring 的 STOMP 支持 识别以此目的为前缀的目标。 例如,客户端可能订阅目标。 处理此目标并将其转换为 用户会话特有的目标(如 )。 这提供了订阅通用命名目标的便利,同时, 同时,确保不会与订阅相同的其他用户发生冲突 目的地,以便每个用户都可以收到唯一的股票头寸更新。/user//user/queue/position-updatesUserDestinationMessageHandler/queue/position-updates-user123

使用用户目标时,配置代理和 应用程序目标前缀,如启用 STOMP 中所示,否则 Broker 将处理 “/user” 前缀消息,而这些消息只能由 处理。UserDestinationMessageHandler

在发送端,消息可以发送到目标,例如 ,而目标又被翻译 由一个或多个目的地,每个目的地一个 与用户关联的会话。这允许应用程序中的任何组件 发送针对特定用户的消息,而不必知道更多信息 而不是它们的名称和通用目的地。这也可以通过 注释和消息传递模板。/user/{username}/queue/position-updatesUserDestinationMessageHandler

消息处理方法可以将消息发送给与 通过注解处理的消息(也支持 共享公共目标的类级别),如以下示例所示:@SendToUser

@Controller
public class PortfolioController {

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

如果用户有多个会话,默认情况下,所有会话都订阅了 到给定的目的地是有针对性的。但是,有时,可能需要 仅以发送正在处理的消息的会话为目标。您可以通过以下方式执行此操作 将该属性设置为 false,如以下示例所示:broadcast

@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 会话 可以订阅用户目标。在这种情况下,注释 行为与 with 完全相同(即,仅以 发送正在处理的消息的会话)。@SendToUserbroadcast=false

您可以从任何应用程序向用户目标发送消息 组件,例如,注入由 Java 配置创建的组件,或者 XML 命名空间。(如果需要,则为 Bean 名称 用于资格认证。以下示例演示如何执行此操作:SimpMessagingTemplatebrokerMessagingTemplate@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());
    }
}
将用户目标与外部消息代理一起使用时,应检查代理 有关如何管理非活动队列的文档,以便当用户会话 最后,将删除所有唯一用户队列。例如,RabbitMQ 创建自动删除 使用目标(如 )时的队列。 因此,在这种情况下,客户端可以订阅 . 同样,ActiveMQ 具有用于清除非活动目标的配置选项/exchange/amq.direct/position-updates/user/exchange/amq.direct/position-updates

在多应用程序服务器方案中,用户目标可能保持未解析状态,因为 用户已连接到其他服务器。在这种情况下,您可以配置 destination 广播未解析的消息,以便其他服务器有机会尝试。 这可以通过 Java 配置中的属性和属性来完成 XML 中的元素。userDestinationBroadcastMessageBrokerRegistryuser-destination-broadcastmessage-broker

4.4.16. 消息的顺序

来自代理的消息将发布到 ,从它们所在的位置发布到 。 写入 WebSocket 会话。由于通道由 、 消息提供支持 在不同的线程中处理,客户端接收到的结果序列可能 与确切的发布顺序不匹配。clientOutboundChannelThreadPoolExecutor

如果这是一个问题,请启用该标志,如以下示例所示: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. 事件

已发布多个事件,并且可以 通过实现 Spring 的接口收到:ApplicationContextApplicationListener

  • BrokerAvailabilityEvent:指示代理何时可用或不可用。 虽然“简单”代理在启动时立即可用,并且保持如此 应用程序正在运行,STOMP“代理中继”可能会失去连接 到功能齐全的代理(例如,如果代理重新启动)。代理中继 具有重新连接逻辑并重新建立与代理的“系统”连接 当它回来时。因此,每当状态从“已连接”更改为“已连接”时,就会发布此事件 断开连接,反之亦然。使用 should 的组件 订阅此事件并避免在代理不在时发送消息 可用。在任何情况下,他们都应该准备好在发送消息时进行处理。SimpMessagingTemplateMessageDeliveryException

  • SessionConnectEvent:在接收到新的 STOMP CONNECT 时发布 指示新客户端会话的开始。该事件包含表示 connect,包括会话 ID、用户信息(如果有)和客户端的任何自定义标头 送。这对于跟踪客户端会话非常有用。订阅的组件 此事件可以用 或 包装包含的消息。SimpMessageHeaderAccessorStompMessageHeaderAccessor

  • SessionConnectedEvent:在 broker 已发送 STOMP CONNECTED 帧以响应 CONNECT。此时, STOMP 会话可以认为是完全建立的。SessionConnectEvent

  • SessionSubscribeEvent:在收到新的 STOMP SUBSCRIBE 时发布。

  • SessionUnsubscribeEvent:在收到新的 STOMP 取消订阅时发布。

  • SessionDisconnectEvent:在 STOMP 会话结束时发布。DISCONNECT 可能 已从客户端发送,或者可能会在 WebSocket 会话已关闭。在某些情况下,此事件会多次发布 每节课。对于多个断开连接事件,组件应该是幂等的。

当您使用功能齐全的代理时,STOMP “代理中继” “system” 连接,如果代理暂时不可用。客户端连接, 但是,不会自动重新连接。假设启用了检测信号,则客户端 通常会注意到代理在 10 秒内没有响应。客户需要 实现自己的重新连接逻辑。

4.4.18. 拦截

事件提供生命周期的通知 的 STOMP 连接,但不是每个客户端消息。应用程序还可以注册一个,以拦截任何消息和处理链的任何部分。 以下示例演示如何截获来自客户端的入站消息:ChannelInterceptor

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

自定义项可以使用 或 访问有关消息的信息,如以下示例所示:ChannelInterceptorStompHeaderAccessorSimpMessageHeaderAccessor

public class MyChannelInterceptor implements ChannelInterceptor {

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

应用程序还可以实现,这是一个子接口 of 替换为处理消息的线程中的回调。 虽然 a 为发送到通道的每条消息调用一次,但 在从通道订阅的每条消息的线程中提供钩子。ExecutorChannelInterceptorChannelInterceptorChannelInterceptorExecutorChannelInterceptorMessageHandler

请注意,与前面所述的一样,DISCONNECT 消息 可以来自客户端,也可以在以下情况下自动生成 WebSocket 会话已关闭。在某些情况下,拦截器可能会拦截此 每个会话多次发送消息。组件在以下方面应该是幂等的 多个断开连接事件。SessionDisconnectEvent

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

在前面的示例中,可以替换为 , 因为这也是 的实现。罐头 使用 WebSocket 或基于 HTTP 的传输作为后备。有关详细信息,请参阅 SockJsClientStandardWebSocketClientSockJsClientWebSocketClientSockJsClient

接下来,您可以建立连接并为 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) {
        // ...
    }
}

建立会话后,可以发送任何有效负载,并且是 使用 configured 进行序列化,如以下示例所示:MessageConverter

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

您还可以订阅目标。这些方法需要处理程序 对于订阅上的消息,并返回一个句柄,您可以 用于取消订阅。对于收到的每条消息,处理程序可以指定有效负载应反序列化的目标类型,如以下示例所示:subscribeSubscriptionObject

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

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

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

});

要启用 STOMP 检测信号,您可以配置并选择性地自定义检测信号间隔(写入不活动为 10 秒、 这会导致发送检测信号,并且 10 秒表示读取处于非活动状态,这 关闭连接)。WebSocketStompClientTaskScheduler

WebSocketStompClient仅在不活动的情况下发送心跳,即当没有 发送其他消息。在使用外部代理时,这可能会带来挑战 因为具有非代理目标的消息表示活动,但实际上并非如此 转发给经纪人。在这种情况下,您可以在初始化外部代理时配置 当只有与非代理的消息时,心跳也会转发到代理 目的地已发送。TaskScheduler

当您用于性能测试以模拟数千个时 来自同一台计算机的客户端,请考虑关闭检测信号,因为每个 连接计划自己的检测信号任务,但未针对 大量客户端在同一台计算机上运行。WebSocketStompClient

STOMP 协议还支持回执,其中客户端必须添加一个标头,服务器在发送或 订阅被处理。为了支持这一点,导致标头的选件是 添加到每个后续发送或订阅事件中。 或者,您也可以手动将收据标头添加到 . send 和 subscribe 都会返回一个实例,您可以使用该实例注册接收成功和失败回调。 对于此功能,您必须为客户端配置收据过期前的时间量(默认为 15 秒)。receiptStompSessionsetAutoReceipt(boolean)receiptStompHeadersReceiptableTaskScheduler

请注意,它本身是一个 ,它使 除了 消息处理和 for 的异常 传输级错误,包括 .StompSessionHandlerStompFrameHandlerhandleExceptionhandleTransportErrorConnectionLostException

4.4.20. WebSocket范围

每个 WebSocket 会话都有一个属性映射。该地图作为标题附加到 入站客户端消息,可以从控制器方法访问,如以下示例所示:

@Controller
public class MyController {

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

您可以在作用域中声明 Spring 管理的 bean。 您可以将 WebSocket 范围的 Bean 注入控制器和任何通道拦截器 在 .这些通常是单例和实时的 比任何单个 WebSocket 会话都长。因此,您需要使用 WebSocket 范围的 Bean 的作用域代理模式,如以下示例所示:websocketclientInboundChannel

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

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

    // ...

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

@Controller
public class MyController {

    private final MyBean myBean;

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

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

与任何自定义范围一样,Spring 首先初始化一个新实例 从控制器访问实例并将实例存储在 WebSocket 中的时间 会话属性。随后将返回相同的实例,直到会话 结束。WebSocket 范围的 Bean 调用了所有 Spring 生命周期方法,作为 如前面的示例所示。MyBean

4.4.21. 性能

在性能方面,没有灵丹妙药。许多因素 影响它,包括消息的大小和数量,是否应用程序 方法执行需要阻塞的工作和外部因素 (例如网络速度和其他问题)。本节的目标是提供 可用配置选项的概述以及一些想法 关于如何推理缩放。

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

显而易见的起点是配置支持 和 的线程池。默认情况下,两者 配置为可用处理器数的两倍。clientInboundChannelclientOutboundChannel

如果在带注释的方法中对消息的处理主要是受 CPU 限制的,则 的线程数应保持接近 处理器数量。如果他们所做的工作更受 IO 限制并且需要阻塞 或等待数据库或其他外部系统,线程池大小 可能需要增加。clientInboundChannel

ThreadPoolExecutor具有三个重要属性:核心线程池大小、 最大线程池大小和队列要存储的容量 没有可用线程的任务。

一个常见的混淆点是配置核心池大小(例如,10) 最大池大小(例如,20)将生成具有 10 到 20 个线程的线程池。 实际上,如果容量保留为默认值 Integer.MAX_VALUE, 线程池永远不会超过核心池大小,因为 所有其他任务都已排队。

请参阅 javadoc 以了解这些属性的工作原理和 了解各种排队策略。ThreadPoolExecutor

另一方面,这一切都是关于向 WebSocket 发送消息 客户。如果客户端位于快速网络上,则线程数应 保持接近可用处理器的数量。如果它们很慢或开启 带宽低,它们需要更长的时间来消耗消息,并给 线程池。因此,增加线程池大小变得必要。clientOutboundChannel

虽然 的工作负载是可以预测的 - 毕竟,它基于应用程序的功能 - 如何配置 “clientOutboundChannel”更难,因为它基于其他因素 应用程序的控制。出于这个原因,另外两个 属性与消息的发送相关:和 。您可以使用这些方法来配置多长时间 发送时允许获取以及可以缓冲多少数据 向客户端发送消息。clientInboundChannelsendTimeLimitsendBufferSizeLimit

一般的想法是,在任何给定时间,只能使用单个线程 发送给客户端。同时,所有其他消息都会被缓冲,而您 可以使用这些属性来决定允许发送消息的时间 获取以及在此期间可以缓冲多少数据。请参阅 javadoc 和 XML 架构的文档,了解重要的其他详细信息。

以下示例显示了可能的配置:

@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 消息大小是自动的 如有必要,进行调整,以确保它们可以在 最低。

以下示例显示了一种可能的配置:

@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)时,每个应用程序 实例连接到代理,消息从一个应用程序广播 实例可以通过 broker 广播到连接的 WebSocket 客户端 通过任何其他应用程序实例。

4.4.22. 监控

使用 或 时,键 基础结构组件自动收集统计信息和计数器,以提供 对应用程序内部状态的重要见解。配置 还声明了一个 bean 类型,该 bean 收集了所有 可用信息位于一个位置,默认情况下,在级别记录一次 每30分钟一班。这个bean可以通过Spring导出到JMX,以便在运行时查看(例如,通过JDK的)。 以下列表汇总了可用信息:@EnableWebSocketMessageBroker<websocket:message-broker>WebSocketMessageBrokerStatsINFOMBeanExporterjconsole

客户端 WebSocket 会话
当前

指示有多少个客户端会话 目前,按 WebSocket 与 HTTP 进一步细分计数 流式传输和轮询 SockJS 会话。

指示已建立的会话总数。

异常关闭
连接失败

已建立但已建立的会话 在 60 秒内未收到任何消息后关闭。这是 通常表示代理或网络问题。

超出发送限制

会话在超过配置的发送后关闭 超时或发送缓冲区限制,这可能发生在速度较慢的客户端上 (见上一节)。

传输错误

会话在传输错误后关闭,例如 无法读取或写入 WebSocket 连接,或者 HTTP 请求或响应。

STOMP 框架

CONNECT、CONNECTED 和 DISCONNECT 帧的总数 已处理,指示在 STOMP 级别上连接的客户端数。请注意, 当会话异常关闭或 客户端关闭而不发送 DISCONNECT 帧。

STOMP 代理中继
TCP 连接

指示代表客户端的 TCP 连接数 与代理建立 WebSocket 会话。这应该等于 客户端 WebSocket 会话数 + 1 个额外的共享“系统”连接 用于从应用程序内部发送消息。

STOMP 框架

CONNECT、CONNECTED 和 DISCONNECT 帧的总数 代表客户转发给经纪人或从经纪人那里接收。请注意,一个 无论客户端 WebSocket 如何,DISCONNECT 帧都会发送到代理 会议已结束。因此,较低的 DISCONNECT 帧数是一个指示 经纪人正在主动关闭连接(可能是因为 检测信号未及时到达、输入帧无效或其他问题)。

客户端入站通道

线程池中的统计信息支持传入消息处理的运行状况,提供对传入消息处理运行状况的见解。任务排队 此处表明应用程序可能太慢而无法处理消息。 如果存在 I/O 绑定任务(例如,慢速数据库查询、对第三方的 HTTP 请求 REST API 等),请考虑增加线程池大小。clientInboundChannel

客户端出站通道

线程池中的统计信息,支持线程池,提供对向客户端广播消息的运行状况的见解。任务 此处排队表示客户端使用消息的速度太慢。 解决此问题的一种方法是增加线程池大小以适应 并发慢速客户端的预期数量。另一种选择是减少 发送超时和发送缓冲区大小限制(请参阅上一节)。clientOutboundChannel

SockJS 任务调度程序

来自 SockJS 任务调度程序的线程池的统计信息 用于发送心跳。请注意,当在 STOMP 级别,则 SockJS 心跳被禁用。

4.4.23. 测试

使用 Spring 的 STOMP-over-WebSocket 时,有两种主要方法可以测试应用程序 支持。第一种是编写服务器端测试来验证功能 控制器及其带注释的消息处理方法。二是写 涉及运行客户端和服务器的完整端到端测试。

这两种方法并不相互排斥。相反,每个人都有一席之地 在整体测试策略中。服务器端测试更集中,更易于编写 并维护。另一方面,端到端集成测试更完整,并且 测试要多得多,但他们也更多地参与编写和维护。

服务器端测试的最简单形式是编写控制器单元测试。然而 这还不够有用,因为控制器的大部分功能都依赖于其 附注。纯粹的单元测试根本无法测试这一点。

理想情况下,应按运行时调用受测控制器,就像 使用 Spring MVC Test 测试处理 HTTP 请求的控制器的方法 框架 — 也就是说,不运行 Servlet 容器,而是依赖于 Spring Framework 调用带注释的控制器。与 Spring MVC Test 一样,您有两个 此处可能的替代方案,要么使用“基于上下文”,要么使用“独立”设置:

  • 在 Spring TestContext 框架,作为测试字段注入,以及 使用它来发送要由控制器方法处理的消息。clientInboundChannel

  • 手动设置调用所需的最低 Spring 框架基础结构 控制器(即 )并传递消息 控制器直接连接到它。SimpAnnotationMethodMessageHandler

这两种设置方案都在股票投资组合示例应用程序的测试中进行了演示。

第二种方法是创建端到端集成测试。为此,您需要 以嵌入式模式运行 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 中,这些服务对象,任何其他 特定于业务的对象、数据访问对象和其他对象存在于不同的“业务”中 context“,它不包含 Web 或表示层对象(表示对象、 例如Spring MVC控制器,通常以不同的“表示形式”进行配置 上下文“)。本节详细介绍了如何配置一个 Spring 容器 (a ),该容器包含应用程序中的所有“业务 bean”。WebApplicationContext

继续具体操作,您需要做的就是在 Web 应用程序的标准 Java EE servlet 文件中声明一个 ContextLoaderListener,并添加一个 <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>

如果未指定 context 参数,则查找调用 负荷。加载上下文文件后,Spring 会根据 bean 定义创建一个 WebApplicationContext 对象,并将其存储在 Web 中 应用。contextConfigLocationContextLoaderListener/WEB-INF/applicationContext.xmlServletContext

所有 Java Web 框架都构建在 Servlet API 之上,因此您可以使用 以下代码片段来访问由 .ApplicationContextContextLoaderListener

以下示例演示如何获取:WebApplicationContext

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 类是为了方便起见,因此无需记住属性的名称。它的方法返回一个对象 密钥下不存在。与其冒险进入您的应用程序,不如这样做 以使用该方法。此方法引发异常 当缺少时。ServletContextgetWebApplicationContext()nullWebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTENullPointerExceptionsgetRequiredWebApplicationContext()ApplicationContext

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

幸运的是,本节中的大多数框架都有更简单的查找 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 集成中的关键元素是 JSF 机制。ELResolver

5.2.1. Spring Bean 解析器

SpringBeanFacesELResolver是符合 JSF 的实现, 与 JSF 和 JSP 使用的标准 Unified EL 集成。它委托给 Spring 的“业务上下文”首先到 底层 JSF 实现的缺省解析器。ELResolverWebApplicationContext

在配置方面,您可以在 JSF 文件中定义,如以下示例所示:SpringBeanFacesELResolverfaces-context.xml

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

5.2.2. 使用FacesContextUtils

自定义在将属性映射到 中的 bean 时效果很好,但有时可能需要显式获取 bean。 FacesContextUtils 类使此操作变得简单。它类似于 ,只是 它需要一个参数而不是一个参数。ELResolverfaces-config.xmlWebApplicationContextUtilsFacesContextServletContext

以下示例演示如何使用:FacesContextUtils

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

5.3. Apache Struts 2.x

Struts 由 Craig McClanahan 发明,是一个开源项目 由 Apache 软件基金会托管。当时,它大大简化了 JSP/Servlet 编程范式,并赢得了许多使用专有技术的开发人员的青睐 框架。它简化了编程模型,它是开源的(因此是免费的 啤酒),它有一个庞大的社区,这让该项目发展壮大并变得流行起来 Java Web 开发人员。

作为原始 Struts 1.x 的后继者,请查看 Struts 2.x 和 Struts 提供的 Spring 插件。 内置 Spring 集成。

5.4. Apache Tapestry 5.x

Tapestry是一个面向组件的框架,用于创建 Java 中动态、健壮、高度可扩展的 Web 应用程序。

虽然 Spring 有自己强大的 Web 层,但也有许多独特的 结合使用 Tapestry 构建企业 Java 应用程序的优势 用于 Web 用户界面,Spring 容器用于较低层。

有关更多信息,请参阅 Tapestry 的 Spring 专用集成模块

5.5. 更多资源

以下链接指向有关 本章。