0%

SpringFramework官方文档翻译-Servlet Stack上的Web

Servlet Stack 上的 Web

SpringWebMVC

Spring Web MVC 是在 Servlet API 上构建的原始 web framework,并且从一开始就包含在 Spring Framework 中。形式 name,“ Spring Web MVC”来自其源模块(spring-webmvc)的 name,但它通常被称为“Spring MVC”。

Parallel 到 Spring Web MVC,Spring Framework 5.0 引入了一个 reactive-stack web framework,其 name,“ Spring WebFlux”也是基于它的源模块(spring-webflux)。本节介绍 Spring Web MVC。 下一节涵盖 Spring WebFlux。

有关基线信息以及与 Servlet 容器和 Java EE version 范围的兼容性,请参阅 Spring Framework 维基

DispatcherServlet

与 Spring WebFlux 相同

Spring MVC 和许多其他 web 框架一样,是围绕前端控制器 pattern 设计的,其中中心ServletDispatcherServlet为请求处理提供了共享算法,而实际工作则由可配置的委托组件执行。这个 model 非常灵活,支持各种工作流程。

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

下面的 Java configuration 示例注册并初始化DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyWebApplicationInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext servletCxt) {

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

// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}

除了直接使用 ServletContext API 之外,您还可以扩展AbstractAnnotationConfigDispatcherServletInitializer并覆盖特定方法(请参阅Context 层次结构下的 example)。

以下web.xml configuration 的 example 注册并初始化DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<web-app>

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

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

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

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

</web-app>

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

Context层次结构

DispatcherServlet期望WebApplicationContext(普通ApplicationContext的扩展名)用于自己的 configuration。 WebApplicationContext有一个指向ServletContextServlet的链接。它也绑定到ServletContext,这样 applications 可以使用RequestContextUtils上的静态方法查找WebApplicationContext,如果它们需要访问它。

对于许多 applications 来说,拥有一个WebApplicationContext很简单而且足够了。也可以有一个 context 层次结构,其中一个根WebApplicationContext在多个DispatcherServlet(或其他Servlet)实例之间共享,每个实例都有自己的 child WebApplicationContext configuration。有关 context 层次结构 feature 的更多信息,请参阅ApplicationContext 的附加功能

WebApplicationContext通常包含基础结构 beans,例如需要跨多个Servlet实例共享的数据存储库和业务服务。那些 beans 是有效继承的,可以在 Servlet-specific child WebApplicationContext中覆盖(即 re-declared),它通常包含给定Servlet本地的 beans。下图显示了这种关系:

mvc context 层次结构

以下 example 配置WebApplicationContext层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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/*" };
}
}

如果不需要 application context 层次结构,applications 可以

以下 example 显示web.xml等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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>

如果不需要 application context 层次结构,则 applications 可以仅配置“root”context 并将contextConfigLocation Servlet 参数保留为空。

特殊的Bean类型

与 Spring WebFlux 相同

DispatcherServlet委托特殊的 beans 来处理请求并呈现适当的响应。 “special beans”是指实现 WebFlux framework contracts 的 Spring-managed Object实例。那些通常带有 built-in contracts,但您可以自定义它们的 properties 并扩展或替换它们。

以下 table lists 列出DispatcherHandler检测到的特殊 beans:

Bean 类型 说明
HandlerMapping 将一个请求映射到一个处理程序以及一个拦截器列表,用于 pre-和 post-processing。映射基于某些标准,其详细信息因HandlerMapping implementation 而异。 两个主要的HandlerMapping implementations 是RequestMappingHandlerMapping(它支持@RequestMapping带注释的方法)和SimpleUrlHandlerMapping(它维护 URI 路径模式到处理程序的显式注册)。
HandlerAdapter 无论实际调用处理程序如何,都帮助DispatcherServlet调用映射到请求的处理程序。例如,调用带注释的控制器需要解析 annotations。 HandlerAdapter的主要目的是保护DispatcherServlet免受这些细节的影响。
HandlerExceptionResolver 解决Spring MVC引发的异常,并将其映射到HTTP状态码。请参阅备用的ResponseEntityExceptionHandler和REST API异常
视图解析器 将从处理程序返回的逻辑String -based 视图名称解析为实际View,以便将其呈现给响应。见视图解析View技术
LocaleResolver, LocaleContextResolver 在 order 中解析Locale a client 正在使用并且可能是 time zone,以便能够提供国际化视图。见locale
ThemeResolver 解析 web application 可以使用的主题 - 例如,提供个性化布局。见主题
MultipartResolver 在一些 multipart 解析 library 的帮助下,解析 multi-part 请求(用于 example,浏览器表单文件上传)的抽象。见Multipart Resolver
FlashMapManager Store 并检索“输入”和“输出”FlashMap,它们可用于将属性从一个请求传递到另一个请求,通常是通过重定向。见Flash 属性

WebMVC配置

与 Spring WebFlux 相同

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

在大多数情况下,MVC 配置是最好的起点。它在 Java 或 XML 中声明了所需的 beans,并提供了 higher-level configuration 回调 API 来自定义它。

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

Servlet配置

在 Servlet 3.0 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方法或与web.xml文件组合使用。以下 example 注册DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 提供的接口,可确保检测到 implementation 并自动用于初始化任何 Servlet 3 容器。名为AbstractDispatcherServletInitializerWebApplicationInitializer的抽象 base class implementation 使得通过重写方法注册DispatcherServlet更容易,以指定 servlet 映射和DispatcherServlet configuration 的位置。

对于使用 Java-based Spring configuration 的 applications,建议使用此方法,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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-based Spring configuration,则应直接从AbstractDispatcherServletInitializer扩展,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

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

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

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

AbstractDispatcherServletInitializer还提供了添加Filter实例的便捷方式,并将它们自动映射到DispatcherServlet,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

// ...

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

每个过滤器根据其具体类型添加默认 name 并自动映射到DispatcherServlet

AbstractDispatcherServletInitializerisAsyncSupported protected 方法提供了一个单独的位置来启用DispatcherServlet上的异步支持以及映射到它的所有过滤器。默认情况下,此 flag 设置为true

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

Processing

与 Spring WebFlux 相同

DispatcherServlet处理请求如下:

  • 在请求中搜索并绑定WebApplicationContext作为控制器和 process 中的其他元素可以使用的属性。它默认绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE key 下。
  • locale 解析器绑定到请求,以便让 process 中的元素解析处理请求时使用的 locale(呈现视图,准备数据等)。如果您不需要 locale 解析,则不需要 locale 解析器。
  • 主题解析器绑定到请求,以允许视图等元素确定要使用的主题。如果您不使用主题,则可以忽略它。
  • 如果指定 multipart 文件解析程序,则会检查请求的多部分。如果找到多部分,请求将包装在MultipartHttpServletRequest中,以供 process 中的其他元素进一步处理。有关 multipart 处理的更多信息,请参见Multipart Resolver
  • 搜索适当的处理程序。如果找到了处理程序,则在 order 中执行与处理程序关联的执行链(预处理程序,后处理程序和控制器)以准备 model 或呈现。或者,对于带注释的控制器,可以呈现响应(在HandlerAdapter内)而不是返回视图。
  • 如果返回 model,则呈现视图。如果没有返回 model(可能是由于预处理器或后处理器拦截请求,可能是出于安全原因),则不会呈现任何视图,因为该请求可能已经完成。

WebApplicationContext中声明的HandlerExceptionResolver beans 用于解决在请求处理期间抛出的 exceptions。那些 exception 解析器允许自定义逻辑来解决 exceptions。有关详细信息,请参阅Exceptions

Spring DispatcherServlet还支持last-modification-date的 return,由 Servlet API 指定。确定特定请求的最后修改 date 的 process 很简单:DispatcherServlet查找适当的处理程序映射并测试找到的处理程序是否实现了LastModified接口。如果是这样,LastModified接口的long getLastModified(request)方法的 value 将返回到 client。

您可以通过将 Servlet 初始化参数(init-param元素)添加到web.xml文件中的 Servlet 声明来自定义单个DispatcherServlet实例。以下 table lists 支持的参数:

参数 说明
contextClass 实现ConfigurableWebApplicationContext的 Class,由此 Servlet 实例化并本地配置。默认情况下,使用XmlWebApplicationContext
contextConfigLocation String 传递给 context 实例(由contextClass指定)以指示可以找到上下文的位置。 string 可能包含多个 strings(使用逗号作为分隔符)以支持多个上下文。如果多个 context 位置的 beans 定义了两次,则最新位置优先。
namespace WebApplicationContext的命名空间。默认为[servlet-name]-servlet
throwExceptionIfNoHandlerFound 当没有找到请求的处理程序时是否抛出NoHandlerFoundException。然后可以使用HandlerExceptionResolver(对于 example,使用@ExceptionHandler控制器方法)捕获 exception,并像处理其他任何方法一样处理。 默认情况下,它设置为false,在这种情况下DispatcherServlet 将响应状态设置为 404(NOT_FOUND)而不引发 exception。 请注意,如果还配置了默认 servlet 处理,则始终将未解析的请求转发到默认的 servlet,并且永远不会引发 404。

Interception

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

  • preHandle(..):在执行实际处理程序之前
  • postHandle(..):处理程序执行后
  • afterCompletion(..):完成请求完成后

preHandle(..)方法返回 boolean value。您可以使用此方法 break 或继续处理执行链。当此方法返回true时,处理程序执行链继续。当它返回 false 时,DispatcherServlet假定拦截器本身已处理请求(并且,对于 example,呈现了适当的视图)并且不继续执行其他拦截器和执行链中的实际处理程序。

有关如何配置拦截器的示例,请参阅 MVC configuration 一节中的拦截器。您也可以使用单个HandlerMapping 实现上的 setter 直接注册它们。

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

Exceptions

与 Spring WebFlux 相同

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

以下 table lists 可用HandlerExceptionResolver 实现:

HandlerExceptionResolver 描述
SimpleMappingExceptionResolver exception class 名称和错误视图名称之间的映射。用于在浏览器 application 中呈现错误页面。
DefaultHandlerExceptionResolver 解析 Spring MVC 引发的 exceptions,并将它们 maps 转换为 HTTP 状态代码。另请参阅替代ResponseEntityExceptionHandlerREST API exceptions
ResponseStatusExceptionResolver 使用@ResponseStatus annotation 解析 exceptions,并根据 annotation 中的 value 将它们 maps 到 HTTP 状态代码。
ExceptionHandlerExceptionResolver 通过在@Controller@ControllerAdvice class 中调用@ExceptionHandler方法来解析 exceptions。见@ExceptionHandler 方法
解析器链

您可以通过在 Spring configuration 中声明多个HandlerExceptionResolver beans 并根据需要设置其order properties 来形成 exception 解析程序链。 order property 越高,exception 解析器的位置越晚。

的 contract 指定它可以 return:

  • a ModelAndView指向错误视图。
  • 如果在解析器中处理了 exception,则返回空ModelAndView
  • null如果 exception 仍未解析,则后续解析器尝试,如果 exception 仍然在结尾,则允许冒泡到 Servlet 容器。

MVC 配置自动声明 built-in 解析器用于默认的 Spring MVC exceptions,用于@ResponseStatus annotated exceptions,以及支持@ExceptionHandler方法。您可以自定义该列表或替换它。

容器错误页面

如果任何HandlerExceptionResolver仍然无法解析 exception,并且因此将其传播或者如果响应状态设置为错误状态(即 4xx,5xx),则 Servlet 容器可以呈现 HTML 中的默认错误页面。要自定义容器的默认错误页面,可以在web.xml中声明错误页面映射。以下 example 显示了如何执行此操作:

1
2
3
<error-page>
<location>/error</location>
</error-page>

给定前面的 example,当 exception 冒泡或响应具有错误状态时,Servlet 容器会在容器内对配置的 URL 进行 ERROR 调度(对于 example,/error)。然后由DispatcherServlet处理,可能将其映射到@Controller,可以使用 model 实现 return 错误视图 name 或呈现 JSON 响应,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
@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 中创建错误页面映射的方法。但是,您可以使用WebApplicationInitializer和最小web.xml

View解析器

与 Spring WebFlux 相同

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

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

视图解析器 描述
AbstractCachingViewResolver 他们解析的AbstractCachingViewResolverAbstractCachingViewResolver缓存视图实例。缓存提高了某些视图技术的性能。您可以通过将cache property 设置为false来关闭缓存。此外,如果必须在运行时刷新某个视图(对于 example,当修改 FreeMarker 模板时),可以使用removeFromCache(String viewName, Locale loc)方法。
XmlViewResolver 实现ViewResolver接受用 XML 编写的 configuration 文件,其中包含与 Spring 的 XML bean 工厂相同的 DTD。默认的 configuration 文件是/WEB-INF/views.xml
ResourceBundleViewResolver 的实现,在ResourceBundle中使用 bean 定义,由 bundle base name 指定。对于它应该解析的每个视图,它使用 property [viewname].(class)的 value 作为 view class,使用 property [viewname].url的 value 作为视图 URL。您可以在View技术章节中找到示例。
UrlBasedViewResolver ViewResolver接口的简单 implementation,它影响逻辑视图名称直接解析为没有显式映射定义的 URL。如果您的逻辑名称以直接的方式匹配视图资源的名称,而不需要任意映射,这是合适的。
InternalResourceViewResolver 方便的UrlBasedViewResolver子类,支持InternalResourceView(实际上是 Servlets 和 JSP)和子类,如JstlViewTilesView。您可以使用setViewClass(..)为此解析程序生成的所有视图指定视图 class。有关详细信息,请参阅作为 UrlBasedViewResolver javadoc。
FreeMarkerViewResolver 方便的UrlBasedViewResolver子类,支持FreeMarkerView及其自定义子类。
ContentNegotiatingViewResolver 根据请求文件 name 或Accept标头解析视图的ViewResolver接口的实现。见内容协商
Handling

与 Spring WebFlux 相同

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

的 contract 指定它可以 return null 以指示无法找到该视图。但是,对于 JSP 和InternalResourceViewResolver,确定 JSP 是否存在的唯一方法是通过RequestDispatcher执行调度。因此,您必须始终将InternalResourceViewResolver配置为视图解析器的整个 order 中的最后一个。

配置视图解析器就像在 Spring configuration 中添加ViewResolver beans 一样简单。 MVC 配置查看解析器提供专用的 configuration API,并添加 logic-less 查看控制器,这对于没有控制器逻辑的 HTML 模板渲染非常有用。

重定向

与 Spring WebFlux 相同

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

净效果与控制器返回RedirectView的效果相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图 name(例如redirect:/myapp/some/resource)相对于当前 Servlet context 重定向,而 name 等 name 重定向到绝对 URL。

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

转发

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

内容协商

与 Spring WebFlux 相同

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

ContentNegotiatingViewResolver通过将请求媒体类型与所支持的媒体类型(也称为Content-Type)进行比较来选择适当的View来处理请求。列表中具有兼容Content-Type的第一个View将表示返回给 client。如果ViewResolver链不能提供兼容的视图,则会查阅通过DefaultViews property 指定的视图列表。后一个选项适用于 singleton Views,它可以呈现当前资源的适当表示,而不管逻辑视图 name。 Accept标头可以包含通配符(对于 example text/*),在这种情况下,View Accepttext/xml是兼容的 match。

有关 configuration 详细信息,请参阅MVC 配置下的视图解析器

Locale

Spring 的 architecture 的大多数部分都支持国际化,就像 Spring web MVC framework 那样。 DispatcherServlet允许您使用 client 的 locale 自动解析消息。这是通过LocaleResolver objects 完成的。

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

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

Locale 解析器和拦截器在org.springframework.web.servlet.i18n包中定义,并以正常方式在 application context 中配置。 _Spoc 包含以下选择的 locale 解析器。

时区

除了获取 client 的 locale 之外,知道它的 time zone 通常很有用。 LocaleContextResolver接口提供了LocaleResolver的扩展,允许解析器提供更丰富的LocaleContext,其中可能包含 time zone 信息。

可用时,可以使用RequestContext.getTimeZone()方法获取用户的TimeZone。 Time zone 信息由 Spring 的ConversionService注册的任何 Date/Time ConverterFormatter objects 自动使用。

Header解析器

这个 locale 解析器检查 client 发送的请求中的accept-language标头(对于 example,一个 web 浏览器)。通常,此头字段包含 client 操作系统的 locale。请注意,此解析程序不支持 time zone 信息。

Cookie解析器

此 locale 解析程序检查 client 上可能存在的Cookie,以查看是否指定了LocaleTimeZone。如果是,则使用指定的详细信息。通过使用此 locale 解析程序的 properties,您可以指定 cookie 的 name 以及最大年龄。以下 example 定义CookieLocaleResolver

1
2
3
4
5
6
7
8
<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>

以下 table 描述 properties CookieLocaleResolver

属性 默认 描述
cookieName classname LOCALE cookie 的 name
cookieMaxAge Servlet 容器默认 client 上_chokie 持续的最大 time 时间。如果指定了-1,则不会保留 cookie。它仅在 client 关闭浏览器之前可用。
cookiePath / 限制 cookie 对您网站某个部分的可见性。指定cookiePath时,cookie 仅对该路径及其下方的_path 可见。
Session解析器

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

请注意,与外部 session management 机制没有直接关系,例如 Spring Session 项目。此SessionLocaleResolver针对当前HttpServletRequest评估和修改相应的HttpSession属性。

Locale拦截器

您可以通过将LocaleChangeInterceptor添加到HandlerMapping定义之一来启用区域设置的更改。它检测请求中的参数并相应地更改 locale,在调度程序的 application context 中调用LocaleResolver上的setLocale方法。下一个 example 显示 calls 到包含名为siteLanguage的参数的所有*.view资源现在更改 locale。因此,对于 example,对 URL 的请求http://www.sf.net/home.view?siteLanguage=nl会将网站语言更改为荷兰语。以下 example 显示了如何拦截 locale:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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>

主题

您可以应用 Spring Web MVC framework 主题来设置 application 的整体 look-and-feel,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响 application 的视觉样式。

定义主题

要在 web application 中使用主题,必须设置org.springframework.ui.context.ThemeSource接口的 implementation。 WebApplicationContext接口扩展ThemeSource但将其职责委托给专用的 implementation。默认情况下,委托是一个org.springframework.ui.context.support.ResourceBundleThemeSource implementation,它从 classpath 的根目录加载 properties files。要使用自定义ThemeSource implementation 或配置ResourceBundleThemeSource的基本 name 前缀,可以在 application context 中使用 reserved name,themeSource注册 bean。 web application context 会自动检测具有该 name 的 bean 并使用它。

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

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

properties 的键是引用 view code 中的主题元素的名称。对于 JSP,通常使用spring:theme自定义标记执行此操作,该标记与spring:message标记非常相似。以下 JSP 片段使用前一个 example 中定义的主题来自定义外观:

1
2
3
4
5
6
7
8
9
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>

默认情况下,ResourceBundleThemeSource使用空基 name 前缀。因此,properties files 从 classpath 的根加载。因此,您可以将cool.properties主题定义放在 classpath 根目录的目录中(对于 example,在/WEB-INF/classes中)。 ResourceBundleThemeSource使用标准的 Java 资源包 loading 机制,允许主题的完全国际化。对于 example,我们可以使用引用带有荷兰文本的特殊背景图像。

解决主题

定义主题后,如前一节中所述,您可以决定使用哪个主题。 DispatcherServlet查找名为themeResolver的 bean,以找出要使用的ThemeResolver implementation。主题解析器的工作方式与LocaleResolver非常相似。它检测用于特定请求的主题,还可以更改请求的主题。以下 table 描述了 Spring 提供的主题解析器:

描述
FixedThemeResolver 选择使用defaultThemeName property 设置的固定主题。
SessionThemeResolver 主题在用户的 HTTP session 中维护。它只需要为每个 session 设置一次,但不会在会话之间保留。
CookieThemeResolver 所选主题存储在 client 上的 cookie 中。

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

Multipart解析器

与 Spring WebFlux 相同

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

要启用 multipart 处理,您需要在DispatcherServlet Spring configuration 中使用 name multipartResolver声明MultipartResolver bean。 DispatcherServlet检测到它并将其应用于传入请求。当收到 content-type 为multipart/form-data的 POST 时,解析器会解析内容并将当前HttpServletRequest包装为MultipartHttpServletRequest,以提供对已解析部分的访问,并将其作为请求参数公开。

Apache常规FileUpload

要使用 Apache Commons FileUpload,您可以使用 name multipartResolver配置类型的 bean。您还需要将commons-fileupload作为 classpath 的依赖项。

Servlet3.0

需要通过 Servlet 容器 configuration 启用 Servlet 3.0 multipart 解析。为此:

  • 在 Java 中,在 Servlet 注册上设置MultipartConfigElement
  • web.xml中,将"<multipart-config>"部分添加到 servlet 声明中。

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

1
2
3
4
5
6
7
8
9
10
11
12
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 configuration 到位,就可以添加一个StandardServletMultipartResolver类型的 bean,其 name 为multipartResolver

Logging

与 Spring WebFlux 相同

Spring MVC 中的 DEBUG-level logging 设计为紧凑,最小和 human-friendly。它侧重于反复使用的 high-value 位信息,而只有在调试特定问题时才有用。

TRACE-level logging 通常遵循与 DEBUG 相同的原则(并且,例如,也不应该是消防水管),但可以用于调试任何问题。此外,一些 log 消息可能在 TRACE 与 DEBUG 中显示不同的级细节。

好 logging 来自使用日志的经验。如果您发现任何不符合既定目标的事件,请告诉我们。

敏感数据

与 Spring WebFlux 相同

DEBUG 和 TRACE logging 可以 log 敏感信息。这就是默认情况下屏蔽请求参数和 headers 的原因,并且必须通过DispatcherServlet上的enableLoggingRequestDetails property 显式启用它们的 logging。

以下 example 显示了如何使用 Java configuration 执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

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

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

}

过滤器

与 Spring WebFlux 相同

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

表单数据

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

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

转发Headers

与 Spring WebFlux 相同

当请求通过代理(例如负载平衡器)时,host, port 和 scheme 可能会发生变化,这使得从 client 透视图创建指向正确的 host,port 和 scheme 的链接成为一项挑战。

RFC 7239定义了代理可用于提供有关原始请求的信息的Forwarded HTTP 标头。还有其他 non-standard headers,包括X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

ForwardedHeaderFilter是一个 Servlet 过滤器,它根据Forwarded headers 修改请求的 host,port 和 scheme,然后删除那些 headers。

转发 headers 有安全注意事项,因为 application 无法知道代理是否按预期添加了 headers,还是恶意 client。这就是为什么应该配置信任边界的代理来删除来自外部的不受信任的Forwarded _header。您还可以使用removeOnly=true配置ForwardedHeaderFilter,在这种情况下,它会删除但不使用 headers。

浅ETag

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

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

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

为了支持异步请求,此筛选器必须与DispatcherType.ASYNC进行映射。异步,以便过滤器能够延迟并成功地生成一个ETag,直到最后一个异步分派的结束。如果使用Spring框架的AbstractAnnotationConfigDispatcherServletInitializer(参见Servlet配置),那么所有的过滤器都会自动为所有的分派类型注册。但是,如果通过web.xml或在Spring引导中通过FilterRegistrationBean注册过滤器,请确保包括DispatcherType.ASYNC

CORS

与 Spring WebFlux 相同

Spring MVC 通过控制器上的 annotations 为 CORS configuration 提供 fine-grained 支持。但是,当与 Spring Security 一起使用时,我们建议依赖于必须在 Spring Security 的过滤器链之前排序的 built-in CorsFilter

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

带注解的控制器

与 Spring WebFlux 相同

Spring MVC 提供 annotation-based 编程 model,其中@Controller@RestController组件使用 annotations 来表达请求映射,请求输入,exception 处理等。带注释的控制器具有灵活的方法签名,不必扩展 base classes,也不必实现特定的接口。以下 example 显示了 annotations 定义的控制器:

1
2
3
4
5
6
7
8
9
@Controller
public class HelloController {

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

在前面的 example 中,该方法接受Model并将 view name 作为String返回,但是存在许多其他选项,本章稍后将对此进行说明。

上的指南和教程使用本节中描述的 annotation-based 编程模型。

宣言

与 Spring WebFlux 相同

您可以使用 Servlet 的WebApplicationContext中的标准 Spring bean 定义来定义 controller beans。 @Controller构造型允许 auto-detection,与 Spring 一致支持,用于检测 classpath 中的@Component classes 和 auto-registering bean 定义。它还充当带注释的 class 的构造型,表明它作为 web component 的角色。

要启用 auto-detection 这样的@Controller beans,可以将 component 扫描添加到 Java configuration 中,如下面的 example 所示:

1
2
3
4
5
6
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

// ...
}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

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

<!-- ... -->

</beans>

@RestController是一个复合注解本身 meta-annotated,@Controller@ResponseBody表示一个控制器,其每个方法都继承 type-level @ResponseBody 注释,因此,直接写入响应主体与视图分辨率和使用 HTML 模板呈现。

AOPProxies

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

请求映射

与 Spring WebFlux 相同

您可以使用@RequestMapping annotation 来向控制器方法发送 map 请求。它具有 match 的各种属性,包括 URL,HTTP 方法,请求参数,headers 和媒体类型。您可以在 class level 中使用它来表示共享映射,或者在方法 level 中使用它来缩小到特定的端点映射。

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

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

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

以下 example 具有类型和方法 level 映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/persons")
class PersonController {

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

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

与 Spring WebFlux 相同

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

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

您还可以声明 URI 变量并使用@PathVariable访问它们的值,如下面的 example 所示:

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

您可以在 class 和方法级别声明 URI 变量,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

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

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

您可以显式 name URI 变量(对于 example,@PathVariable("customId")),但如果名称相同并且您的 code 是使用调试信息编译的,或者使用 Java 8 上的-parameters编译器 flag 编译,则可以保留该详细信息。

语法{varName:regex}声明一个 URI 变量,其正则表达式的语法为{varName:regex}。对于 example,给定 URL "/spring-web-3.0.5 .jar",以下方法提取 name,version 和文件扩展名:

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

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

Spring MVC 使用PathMatcher contract 和spring-core implementation 来实现 URI 路径匹配。

Pattern比较

与 Spring WebFlux 相同

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

如果 pattern 的 URI 变量数量较少而单个通配符计为 1 且 double 通配符计为 2,则 pattern 的特定性较低。给定相等的分数,选择较长的 pattern。给定相同的分数和长度,选择具有比通配符更多的 URI 变量的 pattern。

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

有关完整的详细信息,请参阅AntPathMatcher中的AntPatternComparator,并记住您可以自定义和 PathMatcher 实现。请参阅 configuration 部分中的路径匹配

后缀Match

默认情况下,Spring MVC 执行.*后缀 pattern 匹配,以便映射到/person的控制器也隐式映射到/person.*。然后,文件扩展名用于解释请求的 content type 以用于响应(即,而不是Accept标头) - 用于 example,/person.pdf/person.xml等。

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

在 time 期间,文件 name extensions 的使用已经证明在各种方面存在问题。当使用 URI 变量,路径参数和 URI 编码进行覆盖时,它可能会导致歧义。关于 URL-based 授权和安全性的推理(更多细节见下一节)也变得更加困难。

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

URL-based content negotiation 仍然有用(例如,在浏览器中 typing 一个 URL 时)。为了实现这一点,我们建议使用查询 parameter-based 策略来避免文件 extensions 带来的大多数问题。或者,如果必须使用文件 extensions,请考虑通过ContentNegotiationConfigurermediaTypes property 将它们限制为显式注册的 extensions 列表。

后缀匹配和RFD

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

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

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

默认情况下,许多 common path extensions 都列入白名单。具有自定义HttpMessageConverter 实现的应用程序可以显式注册文件 extensions 以进行内容 negotiation,以避免为这些 extensions 添加Content-Disposition标头。见内容类型

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

可消费的媒体类型

与 Spring WebFlux 相同

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

1
2
3
4
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
1 使用consumes属性通过 content type 缩小映射范围。

consumes属性还支持否定表达式 - 对于 example,!text/plain表示除text/plain之外的任何 content type。

您可以在 class level 声明共享的consumes属性。但是,与大多数其他 request-mapping 属性不同,当在 class level 中使用时,method-level consumes属性会覆盖而不是扩展 class-level 声明。

MediaType提供常用媒体类型的常量,例如APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE

可生产的媒体类型

与 Spring WebFlux 相同

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

1
2
3
4
5
@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
1 使用produces属性通过 content type 缩小映射范围。

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

对于 JSON content type,即使RFC7159明确指出“没有为此注册定义 charset 参数”,也应指定 UTF-8 charset,因为某些浏览器要求它正确解释 UTF-8 特殊字符。

您可以在 class level 声明共享的produces属性。但是,与大多数其他 request-mapping 属性不同,当在 class level 中使用时,method-level produces属性会覆盖而不是扩展 class-level 声明。

MediaType提供常用媒体类型的常量,例如APPLICATION_JSON_UTF8_VALUEAPPLICATION_XML_VALUE

参数和Headers

与 Spring WebFlux 相同

您可以根据请求参数条件缩小请求映射。您可以测试是否存在请求参数(myParam),缺少一个(!myParam)或特定 value(myParam=myValue)。以下 example 显示了如何测试特定的 value:

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

您还可以将其与请求标头条件一起使用,如下面的 example 所示:

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

您可以使用 headers 条件 match Content-TypeAccept,但最好使用消耗产生

HTTP头,OPTIONS

与 Spring WebFlux 相同

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

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

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

对于没有 HTTP 方法声明的@RequestMappingAllow标头设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明支持的 HTTP 方法(对于 example,使用 HTTP 方法特定的变体:@GetMapping@PostMapping和其他)。

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

自定义注解

与 Spring WebFlux 相同

Spring MVC 支持使用复合注释进行请求映射。这些是注释本身 meta-annotated 与@RequestMapping并且用于重新声明@RequestMapping属性的子集(或全部)以及更窄,更具体的目的。

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

Spring MVC 还支持使用自定义 request-matching 逻辑的自定义 request-mapping 属性。这是一个更高级的选项,需要子类化RequestMappingHandlerMapping并覆盖getCustomMethodCondition方法,您可以在其中检查自定义属性并 return 您自己的RequestCondition

显式注册

与 Spring WebFlux 相同

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 Inject 目标处理程序和控制器的处理程序映射。
2 准备映射元数据的请求。
3 获取处理程序方法。
4 添加注册。

处理程序方法

与 Spring WebFlux 相同

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

方法Arguments

与 Spring WebFlux 相同

下一个 table 描述了支持的控制器方法 arguments。 参数不支持 Reactive 类型。

支持 JDK 8 的java.util.Optional作为方法参数与具有required属性(对于 example,@RequestParam@RequestHeader和其他)的 annotations 结合使用,并且等同于required=false

控制器方法参数 描述
WebRequest , NativeWebRequest 无需直接使用 Servlet API 即可访问请求参数以及 request 和 session 属性。
javax.servlet.ServletRequest , javax.servlet.ServletResponse 选择任何特定请求或响应类型 - 对于 example,ServletRequestHttpServletRequest或 Spring 的MultipartRequestMultipartHttpServletRequest
javax.servlet.http.HttpSession 强制存在 session。因此,这样的论证永远不会null。请注意,session 访问不是 thread-safe。如果允许多个请求同时访问 session,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession flag 设置为true
javax.servlet.http.PushBuilder Servlet 4.0 push builder API 用于程序化 HTTP/2 资源推送。请注意,根据 Servlet 规范,如果 client 不支持 HTTP/2 feature,则注入的PushBuilder实例可以为 null。
java.security.Principal 目前经过身份验证的用户 - 如果已知,可能是特定的Principal implementation class。
HttpMethod 请求的 HTTP 方法。
java.util.Locale 当前请求 locale,由最具体的LocaleResolver可用(实际上是已配置的LocaleResolverLocaleContextResolver)确定。
java.util.TimeZone + java.time.ZoneId 与当前请求关联的 time zone,由LocaleContextResolver确定。
java.io.InputStream , java.io.Reader 用于访问 Servlet API 公开的原始请求主体。
java.io.OutputStream , java.io.Writer 用于访问 Servlet API 公开的原始响应主体。
@PathVariable 用于访问 URI 模板变量。见URI 模式
@MatrixVariable 用于访问 URI 路径段中的 name-value 对。见矩阵变量
@RequestParam 用于访问 Servlet 请求参数,包括 multipart files。参数值将转换为声明的方法参数类型。见@RequestParam以及Multipart。 注意,对于简单的参数值,使用@RequestParam是可选的。请参阅本 table 末尾的“任何其他参数”。
@RequestHeader 用于访问请求 headers。标头值将转换为声明的方法参数类型。见@RequestHeader
@CookieValue 访问 cookies。 Cookies 值将转换为声明的方法参数类型。见@CookieValue
@RequestBody 用于访问 HTTP 请求正文。使用HttpMessageConverter 实现将正文内容转换为声明的方法参数类型。见@RequestBody
HttpEntity<B> 用于访问请求 headers 和 body。身体用HttpMessageConverter转换。见HttpEntity
@RequestPart 要访问multipart/form-data请求中的零件,请使用HttpMessageConverter转换零件的主体。见Multipart
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap 用于访问 HTML 控制器中使用的 model,并作为视图呈现的一部分向模板公开。
RedirectAttributes 指定在重定向(即,要附加到查询 string)的情况下使用的属性,以及临时存储的 flash 属性,直到重定向后的请求为止。见重定向属性Flash 属性
@ModelAttribute 用于访问 model 中的现有属性(如果不存在则实例化),并应用数据 binding 和验证。见@ModelAttribute以及模型DataBinder。 请注意,使用@ModelAttribute是可选的(对于 example,设置其属性)。请参阅本 table 末尾的“任何其他参数”。
Errors , BindingResult 用于访问命令 object(即@ModelAttribute参数)的验证和数据 binding 的错误,或者验证@RequestBody@RequestPart arguments 的错误。您必须在经过验证的方法参数后立即声明ErrorsBindingResult参数。
SessionStatus class-level @SessionAttributes 用于标记表单处理完成,它触发通过 class-level @SessionAttributes 注释声明的 session 属性的清除。有关详细信息,请参阅@SessionAttributes
UriComponentsBuilder 用于准备相对于当前请求的 host, port,scheme,context 路径和 servlet 映射的文字部分的 URL。见URI 链接
@SessionAttribute 用于访问任何 session 属性,与 session 属性存储在 session 中的 model 属性相反。有关详细信息,请参阅@SessionAttribute
@RequestAttribute 用于访问请求属性。有关详细信息,请参阅@RequestAttribute
任何其他论点 如果方法参数与此 table 中的任何早期值不匹配,并且它是一个简单类型(由BeanUtils 的#isSimpleProperty确定,则将其解析为@RequestParam。否则,它将被解析为@ModelAttribute
Return值

与 Spring WebFlux 相同

下一个 table 描述了支持的控制器方法 return 值。所有 return 值都支持 Reactive 类型。

控制器方法 return value 描述
@ResponseBody return value 通过HttpMessageConverter implementations 转换并写入响应。见@ResponseBody
HttpEntity<B> , ResponseEntity<B> 指定完整响应(包括 HTTP headers 和 body)的 return value 将通过HttpMessageConverter implementations 转换并写入响应。见ResponseEntity
HttpHeaders 用 headers 返回响应,没有正文。
String 要使用ViewResolver implementations 解析并与隐式 model 一起使用的视图 name - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(参见明确的注册)以编程方式丰富 model。
View 一个View实例,用于与隐式 model 一起呈现 - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(参见明确的注册)以编程方式丰富 model。
java.util.Map , org.springframework.ui.Model 要添加到隐式 model 的属性,其中 view name 通过RequestToViewNameTranslator隐式确定。
@ModelAttribute 要添加到 model 的属性,其中 view name 通过RequestToViewNameTranslator隐式确定。 注意@ModelAttribute是可选的。请参阅本 table 末尾的“任何其他 return value”。
ModelAndView object 要使用的视图和 model 属性,以及可选的响应状态。
void 具有void return 类型(或null return value)的方法如果还具有ServletResponseOutputStream参数或@ResponseStatus 注释,则认为已完全处理了响应。如果控制器进行了正ETaglastModified时间戳检查,那么同样也是 true(详见控制器)。 如果上面的 none 是 true,则void return 类型也可以为 REST 控制器指示“无响应主体”或为 HTML 控制器指示默认视图 name 选择。
DeferredResult<V> 从任何线程异步生成任何前面的 return 值 - 对于 example,作为某些 event 或回调的结果。见异步请求DeferredResult
Callable<V> 在 Spring MVC-managed 线程中异步生成上述任何 return 值。见异步请求可赎回
ListenableFuture<V> , java.util.concurrent.CompletionStage<V> , java.util.concurrent.CompletableFuture<V> 作为方便的替代DeferredResult(例如,当底层服务返回其中一个时)。
ResponseBodyEmitter , SseEmitter 异步发送 objects 流以使用HttpMessageConverter implementations 写入响应。也支持ResponseEntity的主体。见异步请求HTTP流
StreamingResponseBody 异步写入响应OutputStream。也支持ResponseEntity的主体。见异步请求HTTP 流媒体
Reactive 类型 - Reactor,RxJava 或其他通过ReactiveAdapterRegistry DeferredResult替代 multi-value 流(对于 example,FluxObservable)收集到List。 对于流方案(对于 example,text/event-streamapplication/json+stream),使用SseEmitterResponseBodyEmitter,其中ServletOutputStream阻塞 I/O 在 Spring MVC-managed 线程上执行,并且对每次写入完成施加背压。 见异步请求Reactive 类型
任何其他 return value 任何 return value 不匹配此 table 中的任何早期值,并且或void被视为视图 name(默认视图 name 选择通过RequestToViewNameTranslator适用),前提是它不是一个简单类型,由BeanUtils 的#isSimpleProperty确定。简单类型的值仍未解决。
类型转换

与 Spring WebFlux 相同

如果参数声明为String以外的其他内容,则表示String -based 请求输入的某些带注释的控制器方法 arguments(例如@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue)可能需要进行类型转换。

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

矩阵变量

与 Spring WebFlux 相同

RFC 3986讨论路径段中的 name-value 对。在 Spring MVC 中,我们将它们称为基于 Tim Berners-Lee 的“老帖子”的“矩阵变量”,但它们也可以称为 URI 路径参数。

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

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

1
2
3
4
5
6
7
8
// GET /pets/42;q=11;r=22

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

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

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

1
2
3
4
5
6
7
8
9
10
// 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
}

矩阵变量可以定义为可选,并指定默认 value,如下面的 example 所示:

1
2
3
4
5
6
7
// GET /pets/42

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

// q == 1
}

要获取所有矩阵变量,可以使用MultiValueMap,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
// 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 configuration 中,您需要使用removeSemicolonContent=false路径匹配设置UrlPathHelper。在 MVC XML 命名空间中,您可以设置<mvc:annotation-driven enable-matrix-variables="true"/>

RequestParam注解

与 Spring WebFlux 相同

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@RequestMapping("/pets")
public class EditPetForm {

// ...

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

// ...

}
1 使用@RequestParam绑定petId

默认情况下,需要使用此 annotation 的方法参数,但您可以通过将@RequestParam annotation 的required flag 设置为false或通过使用java.util.Optional wrapper 声明参数来指定方法参数是可选的。

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

将参数类型声明为 array 或 list 允许为同一参数 name 解析多个参数值。

@RequestParam annotation 声明为Map<String, String>MultiValueMap<String, String>时,如果没有在 annotation 中指定参数 name,则使用每个给定参数 name 的请求参数值填充 map。

请注意,使用@RequestParam是可选的(对于 example,设置其属性)。默认情况下,任何作为简单 value 类型的参数(由BeanUtils 的#isSimpleProperty确定)并且不由任何其他参数解析器解析,将被视为使用@RequestParam进行注释。

RequestHeader注解

与 Spring WebFlux 相同

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

使用 headers 考虑以下请求:

1
2
3
4
5
6
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

以下 example 获取Accept-EncodingKeep-Alive headers 的 value:

1
2
3
4
5
6
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 获取Accept-Encoding标头的 value。
2 获取Keep-Alive标头的 value。

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

Map<String, String>MultiValueMap<String, String>HttpHeaders参数上使用@RequestHeader annotation 时,map 将填充所有标头值。

Built-in 支持可用于将 comma-separated string 转换为 array 或 strings 集合或类型转换系统已知的其他类型。对于 example,使用@RequestHeader("Accept")注释的方法参数可以是String类型,但也可以是String[]List<String>

CookieValue注解

与 Spring WebFlux 相同

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

考虑使用以下 cookie 的请求:

1
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下 example 显示了如何获取 cookie value:

1
2
3
4
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 获取JSESSIONID cookie 的 value。

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

ModelAttribute注解

与 Spring WebFlux 相同

您可以在方法参数上使用@ModelAttribute annotation 来从 model 访问属性,或者如果不存在则将其实例化。 model 属性还覆盖了 HTTP Servlet 请求参数的值,这些参数的名称为 match 到字段名称。这称为数据 binding,它使您不必处理解析和转换单个查询参数和表单字段。以下 example 显示了如何执行此操作:

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

上面的Pet实例解析如下:

  • 从 model 开始,如果已经使用模型添加了。
  • 通过使用@SessionAttributes从 HTTP session。
  • 从通过Converter的 URI 路径变量(参见下一个 example)。
  • 从默认构造函数的调用。
  • 从“主构造函数”调用 arguments match 到 Servlet 请求参数。参数名称通过字节码中的 JavaBeans @ConstructorProperties或 runtime-retained 参数名称确定。

虽然使用模型来使用属性填充_mode 是常见的,但另一种替代方法是依赖于Converter<String, T>与 URI 路径变量约定的组合。在下面的示例中,model 属性 name,account与 URI 路径变量account匹配,并通过将String帐号传递给已注册的Converter<String, Account>来加载Account

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

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

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

1
2
3
4
5
6
7
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 @ModelAttribute旁边添加BindingResult

在某些情况下,您可能希望在没有数据 binding 的情况下访问 model 属性。对于这种情况,您可以_将Model注入控制器并直接访问它,或者设置@ModelAttribute(binding=false),如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}

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

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

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

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

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

SessionAttributes注解

与 Spring WebFlux 相同

@SessionAttributes用于在请求之间的 HTTP Servlet session 中存储 model 属性。它是 type-level annotation,声明特定控制器使用的 session 属性。这通常列出 model 属性的名称或 model 属性的类型,这些属性应该透明地存储在 session 中以供后续访问请求使用。

以下 example 使用@SessionAttributes annotation:

1
2
3
4
5
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 使用@SessionAttributes annotation。

在第一个请求中,当 model 的 model 属性被添加到 model 时,它会自动提升并保存在 HTTP Servlet session 中。它保持不变,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

// ...

@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); (2)
// ...
}
}
}
1 Pet value 存储在 Servlet session 中。
2 清除 Servlet session 中的Pet value。
SessionAttribute注解

与 Spring WebFlux 相同

如果需要访问全局管理的 pre-existing session 属性(即,在控制器外部 - 对于 example,通过过滤器)并且可能存在或不存在,则可以在方法参数上使用@SessionAttribute annotation,如下所示 example 说明:

1
2
3
4
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 使用@SessionAttribute 注释。

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

要在 session 中临时存储 model 属性作为控制器工作流的一部分,请考虑使用@SessionAttributes,如@SessionAttributes中所述。

RequestAttribute注解

与 Spring WebFlux 相同

@SessionAttribute类似,您可以使用@RequestAttribute annotations 访问之前创建的 pre-existing 请求属性(对于 example,Servlet FilterHandlerInterceptor):

1
2
3
4
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 使用@RequestAttribute annotation。
重定向属性

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

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

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

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

1
2
3
4
5
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}

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

Flash属性

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

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

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

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

匹配闪存属性的请求

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

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

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

Multipart

与 Spring WebFlux 相同

MultipartResolver已经启用之后,具有multipart/form-data的 POST 请求的内容被解析并可作为常规请求参数访问。以下 example 访问一个常规表单字段和一个上载文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class FileUploadController {

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

if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}

将参数类型声明为List<MultipartFile>允许为同一参数 name 解析多个 files。

@RequestParam annotation 声明为Map<String, MultipartFile>MultiValueMap<String, MultipartFile>时,如果没有在 annotation 中指定参数 name,则 map 将使用 multipart files 为每个给定参数 name 填充。

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

您还可以将 multipart 内容用作数据 binding 到命令 object的一部分。对于 example,前面 example 中的表单字段和文件可以是表单 object 上的字段,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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";
}
}

Multipart 请求也可以在 RESTful 服务方案中从 non-browser clients 提交。以下 example 显示了一个带有 JSON 的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /someUrl
Content-Type: multipart/mixed

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

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

您可以使用@RequestParam作为String访问“meta-data”部分,但您可能希望它从 JSON 反序列化(类似于@RequestBody)。在使用HttpMessageConverter转换后,使用@RequestPart annotation 访问 multipart:

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

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

1
2
3
4
5
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}
RequestBody注解

与 Spring WebFlux 相同

您可以使用@RequestBody annotation 将请求主体读取并反序列化为ObjectHttpMessageConverter。以下 example 使用@RequestBody参数:

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

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

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

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

与 Spring WebFlux 相同

HttpEntity与使用@RequestBody或多或少相同,但是基于暴露请求 headers 和 body 的容器 object。以下清单显示了一个 example:

1
2
3
4
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
ResponseBody注解

与 Spring WebFlux 相同

您可以在方法上使用@ResponseBody annotation 通过HttpMessageConverter将 return 序列化到响应正文。以下清单显示了一个 example:

1
2
3
4
5
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}

class level 也支持@ResponseBody,在这种情况下,它由所有控制器方法继承。这是@RestController的效果,它只不过标有@Controller@ResponseBody

您可以将@ResponseBody与 reactive 类型一起使用。有关详细信息,请参阅异步请求Reactive 类型

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

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

ResponseEntity

与 Spring WebFlux 相同

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

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

Spring MVC 支持使用单个 value reactive 类型为主体生成ResponseEntity异步,and/or 单和 multi-valuereactive 类型。

JacksonJSON

Spring 支持 Jackson JSON library。

Jackson序列化视图

与 Spring WebFlux 相同

Spring MVC 为Jackson 的序列化视图提供了 built-in 支持,它允许只渲染Object中所有字段的子集。要将其与@ResponseBodyResponseEntity控制器方法一起使用,可以使用 Jackson 的@JsonView annotation 来激活序列化视图 class,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@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允许 array 视图 classes,但是每个控制器方法只能指定一个。如果需要激活多个视图,可以使用复合接口。

对于依赖于视图分辨率的控制器,可以将序列化视图 class 添加到 model,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@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";
}
}

Model

与 Spring WebFlux 相同

您可以使用@ModelAttribute annotation:

  • 方法论证 in @RequestMapping方法中,从 model 创建或访问Object并通过WebDataBinder将其绑定到请求。
  • 作为@Controller@ControllerAdvice classes 中的 method-level annotation,有助于在任何@RequestMapping方法调用之前初始化 model。
  • 在方法上标记其 return value 是 model 属性。

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

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

以下 example 显示了@ModelAttribute方法:

1
2
3
4
5
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}

以下 example 仅添加一个属性:

1
2
3
4
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}

如果未明确指定 name,则会根据Object类型选择默认 name,如约定的 javadoc 中所述。您始终可以使用重载的addAttribute方法或@ModelAttribute上的name属性(对于 return value)分配显式 name。

您还可以在@RequestMapping方法上使用@ModelAttribute作为 method-level annotation,在这种情况下,@RequestMapping方法的 return value 将被解释为 model 属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非 return value 是String否则将被解释为 view name。 @ModelAttribute还可以自定义 model 属性 name,如下面的 example 所示:

1
2
3
4
5
6
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}

DataBinder

与 Spring WebFlux 相同

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

  • 将请求参数(即表单或查询数据)绑定到 model object。
  • 将 String-based 请求值(例如请求参数,路径变量,headers,cookies 等)转换为控制器方法 arguments 的目标类型。
  • 在呈现 HTML 表单时将 model object 值格式化为String值。

@InitBinder方法可以注册 controller-specific java.bean.PropertyEditor或 Spring ConverterFormatter组件。此外,您可以使用MVC 配置在全局共享的FormattingConversionService中注册ConverterFormatter类型。

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

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class FormController {

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

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

或者,当您通过共享FormattingConversionService使用Formatter -based 设置时,您可以 re-use 采用相同的方法并注册 controller-specific Formatter实现,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class FormController {

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

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

异常

与 Spring WebFlux 相同

@Controller@ControllerAdvice classes 可以有@ExceptionHandler方法来处理来自控制器方法的 exceptions,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class SimpleController {

// ...

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

exception 可以_对正在传播的 top-level exception(即,直接IOException被抛出)或wrapper exception 中的直接原因匹配(对于 example,包裹在IllegalStateException内)。

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

或者,annotation 声明可以将 exception 类型缩小为 match,如下面的 example 所示:

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

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

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

root 和 cause exception 匹配之间的区别可能是令人惊讶的。

在前面显示的IOException变体中,通常使用实际的FileSystemExceptionRemoteException实例作为参数调用该方法,因为它们都从IOException延伸。但是,如果在 wrapper exception 中传播任何这样的匹配 exception,它本身就是IOException,那么 passed-in exception 实例就是 wrapper exception。

handle(Exception)变体中,行为更简单。这总是在包装场景中使用 wrapper exception 调用,在这种情况下通过ex.getCause()找到实际匹配的 exception。 passed-in exception 只有当它们被抛出为 top-level exceptions 时才是实际的FileSystemExceptionRemoteException实例。

我们通常建议您在参数签名中尽可能具体,减少 root 和 cause exception 类型之间不匹配的可能性。考虑将 multi-matching 方法分解为单独的@ExceptionHandler方法,每个方法通过其签名匹配单个特定的 exception 类型。

在多@ControllerAdvice排列中,我们建议在@ControllerAdvice上使用相应的 order 优先声明主根 exception 映射。虽然根 exception match 优先于某个原因,但这是在给定控制器或@ControllerAdvice class 的方法中定义的。这意味着 higher-priority @ControllerAdvice bean 上的原因 match 优先于 lower-priority @ControllerAdvice bean 上的任何 match(对于 example,root)。

最后但并非最不重要的是,@ExceptionHandler方法 implementation 可以选择通过以原始形式重新抛出它来退出处理给定的 exception 实例。这在您仅对 root-level 匹配或在无法静态确定的特定 context 中的匹配中感兴趣的情况下非常有用。重新生成的 exception 通过剩余的解析链传播,就好像给定的@ExceptionHandler方法首先不匹配一样。

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

方法Arguments

@ExceptionHandler方法支持以下 arguments:

方法论证 描述
Exception 类型 用于访问引发的 exception。
HandlerMethod 用于访问引发 exception 的控制器方法。
WebRequest , NativeWebRequest 无需直接使用 Servlet API 即可访问请求参数和 request 和 session 属性。
javax.servlet.ServletRequest , javax.servlet.ServletResponse 选择任何特定请求或响应类型(对于 example,ServletRequestHttpServletRequest或 Spring 的MultipartRequestMultipartHttpServletRequest)。
javax.servlet.http.HttpSession 强制存在 session。因此,这样的论证永远不会null。 注意 session 访问不是 thread-safe。如果允许多个请求同时访问 session,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession flag 设置为true
java.security.Principal 目前经过身份验证的用户 - 如果已知,可能是特定的Principal implementation class。
HttpMethod 请求的 HTTP 方法。
java.util.Locale 当前请求 locale,由最具体的LocaleResolver可用 - 确定 - 生效,配置为LocaleResolverLocaleContextResolver
java.util.TimeZone , java.time.ZoneId 与当前请求关联的 time zone,由LocaleContextResolver确定。
java.io.OutputStream , java.io.Writer 用于访问原始响应主体,由 Servlet API 公开。
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap 用于访问 model 以获取错误响应。永远是空的。
RedirectAttributes 指定在重定向的情况下使用的属性 - (将附加到查询 string)和临时存储的 flash 属性,直到重定向后的请求为止。见重定向属性Flash 属性
@SessionAttribute 用于访问任何 session 属性,与 session 属性存储在 session 中的 model 属性相反。有关详细信息,请参阅@SessionAttribute
@RequestAttribute 用于访问请求属性。有关详细信息,请参阅@RequestAttribute
Return值

@ExceptionHandler方法支持以下 return 值:

Return value 描述
@ResponseBody return value 通过HttpMessageConverter实例转换并写入响应。见@ResponseBody
HttpEntity<B> , ResponseEntity<B> return value 指定完整响应(包括 HTTP headers 和正文)通过HttpMessageConverter实例转换并写入响应。见ResponseEntity
String 要使用ViewResolver implementations 解析并与隐式 model 一起使用的视图 name - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(如前所述)以编程方式丰富 model。
View 一个View实例,用于与隐式 model 一起呈现 - 通过命令 objects 和@ModelAttribute方法确定。处理程序方法还可以通过声明Model参数(先前描述)以编程方式丰富 model。
java.util.Map , org.springframework.ui.Model 要添加到隐式 model 的属性,其中 view name 通过RequestToViewNameTranslator隐式确定。
@ModelAttribute 要添加到 model 的属性,其中 view name 通过RequestToViewNameTranslator隐式确定。 注意@ModelAttribute是可选的。请参阅本 table 末尾的“任何其他 return value”。
ModelAndView object 要使用的视图和 model 属性,以及可选的响应状态。
void 具有void return 类型(或null return value)的方法被认为已完全处理了响应,如果它还具有ServletResponse OutputStream参数或@ResponseStatus annotation。如果控制器进行了正ETaglastModified时间戳检查,那么同样也是 true(详见控制器)。 如果上面的 none 是 true,则void return 类型也可以为 REST 控制器指示“无响应主体”或为 HTML 控制器指定默认视图 name。
任何其他 return value 如果 return value 与上述任何一个都不匹配且不是简单类型(由BeanUtils 的#isSimpleProperty确定),则默认情况下,它被视为要添加到 model 的 model 属性。如果它是一个简单的类型,它仍然没有得到解决。
REST-API异常

与 Spring WebFlux 相同

REST 服务的 common 要求是在响应正文中包含错误详细信息。 Spring Framework 不会自动执行此操作,因为响应正文中的错误详细信息的表示形式为 application-specific。但是,@RestController可以使用@ExceptionHandler方法和ResponseEntity return value 来设置响应的状态和正文。此类方法也可以在@ControllerAdvice classes 中声明,以便全局应用它们。

在响应主体中实现带有错误详细信息的 global exception 处理的应用程序应考虑扩展ResponseEntityExceptionHandler,它提供 Spring MVC 引发的 exceptions 的处理,并提供钩子来自定义响应主体。要使用它,创建ResponseEntityExceptionHandler的子类,用@ControllerAdvice注释它,覆盖必要的方法,并将其声明为 Spring bean。

ControllerAdvice

与 Spring WebFlux 相同

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

@ControllerAdvice标有@Component,这意味着这样的 classes 可以通过component 扫描注册为 Spring beans。 @RestControllerAdvice也是 meta-annotation 标有@ControllerAdvice@ResponseBody,这实质上意味着通过消息转换(与视图分辨率或模板渲染相比)将@ExceptionHandler方法呈现给响应主体。

在启动时,@RequestMapping@ExceptionHandler方法的基础结构 classes 检测@ControllerAdvice类型的 Spring beans,然后在运行时应用它们的方法。 Global @ExceptionHandler方法(来自@ControllerAdvice)在本地方法(来自@Controller)之后应用。相比之下,global @ModelAttribute@InitBinder方法在本地方法之前应用。

默认情况下,@ControllerAdvice方法适用于每个请求(即所有控制器),但您可以使用 annotation 上的属性将其缩小到控制器的子集,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
// 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 {}

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

URI链接

与 Spring WebFlux 相同

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

UriComponents

Spring MVC 和 Spring WebFlux

UriComponentsBuilder有助于使用变量从 URI 模板中构建 URI,如下面的 example 所示:

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

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

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

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

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

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

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

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

UriBuilder

Spring MVC 和 Spring WebFlux

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

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

以下 example 显示了如何配置RestTemplate

1
2
3
4
5
6
7
8
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

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

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

以下 example 配置WebClient

1
2
3
4
5
6
7
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

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

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

此外,您还可以直接使用DefaultUriBuilderFactory。它类似于使用UriComponentsBuilder但是,它不是静态工厂方法,而是一个包含 configuration 和 preferences 的实际实例,如下面的 example 所示:

1
2
3
4
5
6
String baseUrl = "http://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

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

URI编码

Spring MVC 和 Spring WebFlux

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

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

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

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

以下 example 使用第一个选项:

1
2
3
4
5
6
7
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(这意味着编码)来缩短前面的 example,如下面的 example 所示:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
String baseUrl = "http://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

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

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

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

  • TEMPLATE_AND_VALUES:使用UriComponentsBuilder#encode()(对应于前面列表中的第一个选项)pre-encode URI 模板,并在扩展时严格编码 URI 变量。
  • VALUES_ONLY:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过UriUtils#encodeUriUriVariables对 URI 变量应用严格编码。
  • URI_COMPONENTS:使用UriComponents#encode()(对应于前面列表中的第二个选项),在扩展 URI 变量后对 URI component value 进行编码。
  • NONE:未应用编码。

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

相对Servlet请求

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

1
2
3
4
5
6
7
8
HttpServletRequest request = ...

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

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode();

您可以创建相对于 context 路径的 URI,如下面的 example 所示:

1
2
3
4
// Re-uses host, port and context path...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts").build()

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

1
2
3
4
// Re-uses host, port, context path, and Servlet prefix...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts").build()

从 5.1 开始,ServletUriComponentsBuilder忽略来自ForwardedX-Forwarded-* headers 的信息,这些信息指定了 client-originated 地址。考虑使用ForwardedHeaderFilter来提取和使用或丢弃此类_header。

控制器的链接

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

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

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

您可以通过 name 引用方法来准备链接,如下面的 example 所示:

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

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

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

还有其他方法可以使用MvcUriComponentsBuilder。对于 example,您可以使用类似于通过代理进行 mock 测试的技术,以避免通过 name 引用控制器方法,如下面的 example 所示(example 假定MvcUriComponentsBuilder.on的静态 import):

1
2
3
4
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

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

当控制器方法签名可用于与fromMethodCall创建链接时,它们的设计受到限制。除了需要正确的参数签名之外,return 类型还存在技术限制(即,为链接构建器调用生成运行时代理),因此 return 类型不能是final。特别是,视图名称的 common String return 类型在此处不起作用。您应该使用ModelAndView或甚至普通的Object(带有String return value)。

前面的示例在MvcUriComponentsBuilder中使用静态方法。在内部,它们依赖ServletUriComponentsBuilder从当前请求的 scheme,host,port,context 路径和 servlet 路径准备基本 URL。这在大多数情况下效果很好。但是,有时,它可能是不够的。例如,您可能位于请求的 context 之外(例如,准备链接的批处理 process),或者您可能需要插入路径前缀(例如从请求路径中删除的 locale 前缀,并且需要 re-inserted 进入链接)。

对于这种情况,您可以使用接受UriComponentsBuilder的静态fromXxx重载方法来使用基本 URL。或者,您可以使用基本 URL 创建MvcUriComponentsBuilder的实例,然后使用 instance-based withXxx方法。对于 example,以下列表使用withMethodCall

1
2
3
4
5
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 开始,MvcUriComponentsBuilder忽略来自ForwardedX-Forwarded-* headers 的信息,这些信息指定了 client-originated 地址。考虑使用ForwardedHeaderFilter来提取和使用或丢弃此类_header。

视图中的链接

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

考虑以下 example:

1
2
3
4
5
6
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

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

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

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

前面的 example 依赖于 Spring 标记 library(即 META-INF/spring.tld)中声明的mvcUrl function,但很容易定义自己的 function 或为其他模板技术准备类似的函数。

这是如何工作的。在启动时,每个@RequestMapping都通过HandlerMethodMappingNamingStrategy分配一个默认 name,其默认 implementation 使用 class 的大写字母和方法 name(对于 example,ThingController中的getThing方法变为“TC#getThing”)。如果存在 name 冲突,则可以使用@RequestMapping(name="..")分配显式 name 或实现自己的HandlerMethodMappingNamingStrategy

异步请求

与 WebFlux 相比

Spring MVC 与 Servlet 3.0 异步请求处理有一个广泛的 integration:

DeferredResult

与 WebFlux 相比

一旦异步请求处理 feature 在 Servlet 容器中启用,控制器方法就可以用DeferredResult包装任何支持的控制器方法 return value,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}

// From some other thread...
deferredResult.setResult(data);

控制器可以从不同的线程异步生成 return value - 例如,响应外部 event(JMS 消息),计划任务或其他 event。

Callable

与 WebFlux 相比

控制器可以使用java.util.concurrent.Callable包装任何受支持的 return value,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

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

}

然后可以通过配置 TaskExecutor运行给定任务来获得 return value。

处理

与 WebFlux 相比

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

  • 通过调用request.startAsync()可以将ServletRequest置于异步模式。这样做的主要作用是 Servlet(以及任何过滤器)可以退出,但响应保持打开状态以便稍后处理完成。
  • request.startAsync()的调用返回AsyncContext,您可以使用它来进一步控制异步处理。对于 example,它提供dispatch方法,类似于 Servlet API 中的 forward,除了它允许 Servlet 容器线程上的 application resume request 处理。
  • ServletRequest提供对当前DispatcherType的访问,您可以使用它来区分处理初始请求,异步调度,转发和其他调度程序类型。

DeferredResult处理工作如下:

  • 控制器返回DeferredResult并将其保存在可以访问的某个 in-memory 队列或列表中。
  • Spring MVC calls request.startAsync()
  • 同时,DispatcherServlet和所有已配置的过滤器退出请求处理线程,但响应仍保持打开状态。
  • application 从某个线程设置DeferredResult,Spring MVC 将请求调度回 Servlet 容器。
  • 再次调用DispatcherServlet,并使用异步生成的 return value 继续处理。

Callable处理工作如下:

  • 控制器返回Callable
  • Spring MVC calls request.startAsync()并将Callable提交给TaskExecutor以便在单独的线程中进行处理。
  • 同时,DispatcherServlet和所有过滤器退出 Servlet 容器线程,但响应仍保持打开状态。
  • 最终Callable产生一个结果,Spring MVC 将请求发送回 Servlet 容器以完成处理。
  • 再次调用DispatcherServlet,并使用Callable中异步生成的 return value 继续处理。

对于更多背景和 context,您还可以阅读在 Spring MVC 3.2 中引入异步请求处理支持的博客文章

Exception处理

使用DeferredResult时,可以选择是否使用 exception 调用setResultsetErrorResult。在这两种情况下,Spring MVC 都会将请求调度回 Servlet 容器以完成处理。然后将其视为控制器方法返回给定的 value 或者就像它产生给定的 exception 一样。然后 exception 遍历常规 exception 处理机制(对于 example,调用@ExceptionHandler方法)。

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

Interception

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

HandlerInterceptor implementations 还可以注册CallableProcessingInterceptorDeferredResultProcessingInterceptor,以更深入地集成异步请求的生命周期(例如,处理超时 event)。有关详细信息,请参阅AsyncHandlerInterceptor

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

与WebFlux相比

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

相比之下,Spring WebFlux 既不构建在 Servlet API 上,也不需要这样的异步请求处理 feature,因为它是异步的设计。异步处理内置于所有 framework contracts 中,并通过请求处理的所有阶段进行内在支持。

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

另一个根本区别是 Spring MVC 不支持控制器方法 arguments 中的异步或 reactive 类型(对于 example,@RequestBody@RequestPart和其他),也不支持异步和 reactive 类型作为 model 属性。 Spring WebFlux 确实支持所有这些。

HTTP流

与 Spring WebFlux 相同

您可以将DeferredResultCallable用于单个异步 return value。如果要生成多个异步值并将其写入响应,该怎么办?本节介绍如何执行此操作。

Objects

您可以使用ResponseBodyEmitter return value 生成 objects 流,其中每个 object 都使用HttpMessageConverter序列化并写入响应,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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();

您还可以在ResponseEntity中使用ResponseBodyEmitter作为正文,以便自定义响应的状态和 headers。

emitter抛出IOException时(对于 example,如果 remote client 消失了),applications 不负责清理连接,不应该调用emitter.completeemitter.completeWithError。相反,servlet 容器会自动启动AsyncListener错误通知,其中 Spring MVC 进行completeWithError调用。反过来,此调用会对 application 执行最后一次ASYNC调度,在此期间 Spring MVC 将调用已配置的 exception 解析器并完成请求。

SSE

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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 不支持 Server-Sent Events。考虑使用 Spring 的WebSocket 消息传递SockJS 后备,它们针对各种浏览器。

有关 exception 处理的注释,另请参阅上一节

原始数据

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

1
2
3
4
5
6
7
8
9
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}

您可以在ResponseEntity中使用StreamingResponseBody作为正文来自定义响应的状态和 headers。

Reactive类型

与 Spring WebFlux 相同

Spring MVC 支持在控制器中使用 reactive client libraries(也可以在 WebFlux 部分中读取Reactive Libraries)。这包括spring-webflux来自spring-webflux和其他,例如 Spring Data reactive data repositories。在这种情况下,能够从控制器方法 return reactive 类型很方便。

Reactive return 值按如下方式处理:

  • single-value promise 适用于,类似于使用DeferredResult。示例包括Mono(Reactor)或Single(RxJava)。
  • 具有流媒体类型(例如application/stream+jsontext/event-stream)的 multi-value 流适用于,类似于使用ResponseBodyEmitterSseEmitter。示例包括Flux(Reactor)或Observable(RxJava)。 Applications 也可以_ret Flux<ServerSentEvent>Observable<ServerSentEvent>
  • 具有任何其他媒体类型(例如application/json)的 multi-value 流适用于,类似于使用DeferredResult<List<?>>

Spring MVC 通过spring-corespring-core支持 Reactor 和 RxJava,这使得它可以适应多个 reactive libraries。

对于流式传输到响应,支持 reactive 背压,但是对响应的写入仍然是阻塞的,并且通过配置 TaskExecutor在单独的线程上执行,以避免阻塞上游源(例如从WebClient返回的Flux)。默认情况下,SimpleAsyncTaskExecutor用于阻塞写入,但在加载时不适用。如果您计划使用 reactive 类型进行流式处理,则应使用MVC configuration配置任务执行程序。

断开

与 Spring WebFlux 相同

当 remote client 消失时,Servlet API 不会提供任何通知。因此,在流式传输到响应时,无论是通过SseEmitter还是reactive types,定期发送数据都很重要,因为如果 client 断开连接,则写入失败。发送可以采用空(comment-only)SSE event 或任何其他数据的形式,另一方必须将其解释为心跳并忽略。

或者,考虑使用具有 built-in 心跳机制的 web 消息传递解决方案(例如STOMP over WebSocket或带有SockJS的 WebSocket)。

Configuration

与 WebFlux 相比

必须在 Servlet 容器 level 上启用异步请求处理 feature。 MVC configuration 还为异步请求公开了几个选项。

Servlet容器

Filter 和 Servlet 声明的asyncSupported flag 需要设置为true以启用异步请求处理。此外,应声明 Filter 映射以处理ASYNC javax.servlet.DispatchType

在 Java configuration 中,当您使用AbstractAnnotationConfigDispatcherServletInitializer初始化 Servlet 容器时,这将自动完成。

web.xml configuration 中,您可以将<async-supported>true</async-supported>添加到DispatcherServletFilter声明中,并将<dispatcher>ASYNC</dispatcher>添加到过滤器映射中。

SpringMVC

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

  • Java configuration:在WebMvcConfigurer上使用configureAsyncSupport回调。
  • XML 命名空间:使用<mvc:annotation-driven>下的<async-support>元素。

您可以配置以下内容:

  • 异步请求的默认超时 value,如果未设置,则取决于底层的 Servlet 容器(对于 example,Tomcat 上为 10 秒)。
  • AsyncTaskExecutor用于在使用Reactive 类型进行流式传输时阻止写入以及用于执行从控制器方法返回的Callable实例。如果您使用 reactive 类型进行流式处理或者使用 return Callable的控制器方法,我们强烈建议您配置此 property,因为默认情况下它是SimpleAsyncTaskExecutor
  • DeferredResultProcessingInterceptor implementations 和CallableProcessingInterceptor __mplementations。

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

CORS

与 Spring WebFlux 相同

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

介绍

与 Spring WebFlux 相同

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

Cross-Origin 资源共享(CORS)是由大多数浏览器实现的W3C 规范,它允许您指定授权的 cross-domain 请求类型,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的变通方法。

Processing

与 Spring WebFlux 相同

CORS 规范区分了预检,简单和实际请求。要了解 CORS 的工作原理,您可以阅读本文等许多其他内容,或者查看规范以获取更多详细信息。

Spring MVC HandlerMapping implementations 为 CORS 提供 built-in 支持。成功将请求映射到处理程序后,HandlerMapping implementations 检查给定请求和处理程序的 CORS configuration 并采取进一步操作。直接处理预检请求,同时拦截,验证简单和实际的 CORS 请求,并设置所需的 CORS 响应头。

在 order 中启用 cross-origin 请求(即,Origin标头存在且与请求的 host 不同),您需要有一些显式声明的 CORS configuration。如果未找到匹配的 CORS configuration,则拒绝预检请求。没有 CORS headers 被添加到简单和实际 CORS 请求的响应中,因此浏览器拒绝它们。

每个HandlerMapping可以_11单独使用 URL pattern-based CorsConfiguration映射。在大多数情况下,applications 使用 MVC Java configuration 或 XML 命名空间来声明这样的映射,这会导致单个 global map 传递给所有HandlerMappping实例。

您可以将HandlerMapping level 上的 global CORS configuration 与更多 fine-grained,handler-level CORS configuration 结合使用。对于 example,带注释的控制器可以使用 class-或 method-level @CrossOrigin 注释(其他处理程序可以实现CorsConfigurationSource)。

组合 global 和 local configuration 的规则通常是加法的 - 例如,所有 global 和所有本地起源。对于只能接受单个 value 的属性(例如allowCredentialsmaxAge),本地会覆盖 global value。有关详细信息,请参阅CorsConfiguration#combine(CorsConfiguration)

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

  • CorsConfiguration
  • CorsProcessor , DefaultCorsProcessor
  • AbstractHandlerMapping

CrossOrigin注解

与 Spring WebFlux 相同

@CrossOrigin 注解在带注解的控制器方法上启用 cross-origin 请求,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/account")
public class AccountController {

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

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

默认情况下,@CrossOrigin允许:

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

默认情况下不启用allowedCredentials,因为它建立了一个公开敏感 user-specific 信息的信任 level(例如 cookies 和 CSRF 令牌),并且只应在适当的地方使用。

maxAge设置为 30 分钟。

class level 也支持@CrossOrigin,并且由所有方法继承,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

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

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

您可以在 class level 和方法 level 中使用@CrossOrigin,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

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

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

全局配置

与 Spring WebFlux 相同

除了 fine-grained,控制器方法 level configuration 之外,您可能还想定义一些 global CORS configuration。您可以在任何HandlerMapping上单独设置 URL-based CorsConfiguration映射。但是,大多数 applications 使用 MVC Java configuration 或 MVC XNM 命名空间来执行此操作。

默认情况下,global configuration 启用以下内容:

  • 所有起源。
  • 所有 headers。
  • GETHEADPOST方法。

默认情况下不启用allowedCredentials,因为它建立了一个公开敏感 user-specific 信息的信任 level(例如 cookies 和 CSRF 令牌),并且只应在适当的地方使用。

maxAge设置为 30 分钟。

Java配置

与 Spring WebFlux 相同

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {

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

// Add more mappings...
}
}
XML配置

要在 XML 命名空间中启用 CORS,可以使用<mvc:cors>元素,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<mvc:cors>

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

<mvc:mapping path="/resources/**"
allowed-origins="http://domain1.com" />

</mvc:cors>

CORS过滤器

与 Spring WebFlux 相同

您可以通过 built-in CorsFilter应用 CORS 支持。

如果您尝试将CorsFilter与 Spring Security 一起使用,请记住 Spring Security 对于 CORS 有built-in 支持

要配置过滤器,请将CorsConfigurationSource传递给其构造函数,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CorsConfiguration config = new CorsConfiguration();

// Possibly...
// config.applyPermitDefaultValues()

config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

CorsFilter filter = new CorsFilter(source);

Web安全

与 Spring WebFlux 相同

Spring Security项目支持保护 web applications 免受恶意攻击。请参阅 Spring Security reference 文档,包括:

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

HTTP缓存

与 Spring WebFlux 相同

HTTP 缓存可以显着改善 web application 的 performance。 HTTP 缓存围绕Cache-Control响应头,随后是条件请求 headers(例如Last-ModifiedETag)。 Cache-Control建议私有(例如,浏览器)和公共(用于 example,代理)缓存如何缓存和 re-use 响应。如果内容未更改,ETag标头用于生成条件请求,该请求可能导致 304(NOT_MODIFIED)没有正文。 ETag可以被视为Last-Modified标题的更复杂的继承者。

本节介绍 Spring Web MVC 中可用的 HTTP caching-related 选项。

CacheControl

与 Spring WebFlux 相同

CacheControl支持配置与Cache-Control标头相关的设置,并且在许多地方被接受为参数:

虽然RFC 7234描述了Cache-Control响应头的所有可能的指令,但CacheControl类型采用了一种使用 case-oriented 方法,该方法专注于 common 场景:

1
2
3
4
5
6
7
8
9
10
// 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 property(以秒为单位定义),其工作方式如下:

  • -1 value 不会生成Cache-Control响应头。
  • 0 value 通过使用'Cache-Control: no-store'指令来防止缓存。
  • n > 0 value 使用'Cache-Control: max-age=n'指令缓存给定的响应n秒。

控制器

与 Spring WebFlux 相同

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的lastModifiedETagvalue 需要先计算才能与条件请求 headers 进行比较。控制器可以将ETag标头和Cache-Control设置添加到ResponseEntity,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

Book book = findBook(id);
String version = book.getVersion();

return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}

如果与条件请求 headers 的比较表明内容未更改,则前面的 example 将发送一个 304(NOT_MODIFIED)响应与空体。否则,ETagCache-Control headers 将添加到响应中。

您还可以在控制器中对条件请求 headers 进行检查,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

long eTag = ... (1)

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

model.addAttribute(...); (3)
return "myViewName";
}
1 Application-specific 计算。
2 响应已设置为 304(NOT_MODIFIED) - 无需进一步处理。
3 继续请求处理。

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

静态资源

与 Spring WebFlux 相同

您应该使用Cache-Control和条件响应 headers 为静态资源提供最佳的 performance。请参阅有关配置静态资源的部分。

ETag过滤器

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

View技术

与 Spring WebFlux 相同

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

Thymeleaf

与 Spring WebFlux 相同

Thymeleaf 是一个现代的 server-side Java 模板引擎,它强调自然的 HTML 模板,可以通过 double-clicking 在浏览器中预览,这对于 UI 模板的独立工作非常有用(例如,设计者),而不需要运行服务器。如果您想要替换 JSP,Thymeleaf 提供了一组最广泛的 features,以使这种转换更容易。 Thymeleaf 积极开发和维护。有关更完整的介绍,请参阅Thymeleaf项目主页。

Thymeleaf 与 Spring MVC 的整合由 Thymeleaf 项目管理。 configuration 涉及一些 bean 声明,例如ServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver。有关详细信息,请参阅Thymeleaf Spring

FreeMarker

与 Spring WebFlux 相同

Apache FreeMarker是一个模板引擎,用于生成从 HTML 到电子邮件和其他人的任何类型的文本输出。 Spring Framework 有一个 built-in integration 用于使用带有 FreeMarker 模板的 Spring MVC。

View配置

与 Spring WebFlux 相同

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

以下 example 显示了如何在 XML 中配置相同的内容:

1
2
3
4
5
6
7
8
9
10
<mvc:annotation-driven/>

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

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

或者,您也可以声明FreeMarkerConfigurer bean 以完全控制所有 properties,如下面的 example 所示:

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

您的模板需要存储在前面的 example 中显示的FreeMarkerConfigurer指定的目录中。给定前面的 configuration,如果控制器返回welcome的 view name,则解析器将查找/WEB-INF/freemarker/welcome.ftl模板。

FreeMarker配置

与 Spring WebFlux 相同

您可以通过在FreeMarkerConfigurer bean 上设置相应的 bean properties,将 FreeMarker’Settings’和’SharedVariables’直接传递给 FreeMarker Configuration object(由 Spring 管理)。 freemarkerSettings property 需要java.util.Properties object,而freemarkerVariables property 需要java.util.Map。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>

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

有关适用于Configuration object 的设置和变量的详细信息,请参阅 FreeMarker 文档。

表单处理

Spring 提供了一个标记 library,用于 JSP,其中包含<spring:bind/>元素。此元素主要允许表单显示 form-backing objects 中的值,并显示 web 或业务层中Validator的验证失败结果。 Spring 还支持 FreeMarker 中的相同功能,还有用于生成表单输入元素的额外便利宏。

绑定宏

在两个语言的spring-webmvc.jar文件中维护一组标准宏,因此它们始终可用于适当配置的 application。

Spring libraries 中定义的一些宏被认为是内部(私有),但宏定义中不存在这样的范围,使得所有宏都可以调用 code 和用户模板。以下部分仅关注您需要在模板中直接调用的宏。如果您希望直接查看宏 code,则该文件名为spring.ftl并位于org.springframework.web.servlet.view.freemarker包中。

简单Binding

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- freemarker macros have to be imported into a namespace. We strongly
recommend sticking to 'spring' -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "myModelObject.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br>
<#list spring.status.errorMessages as error> <b>${error}</b> <br> </#list>
<br>
...
<input type="submit" value="submit"/>
</form>
...
</html>

<@spring.bind>需要一个’path’参数,它包含命令 object 的 name(它是’command’,除非你在FormController properties 中更改它),后跟一个句点和_jject 命令 object 上的字段的 name 希望绑定。您还可以使用嵌套字段,例如command.address.streetbind宏假定web.xml中 ServletContext 参数defaultHtmlEscape指定的默认 HTML 转义行为。

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

输入宏

两种语言的附加便利宏简化了 binding 和表单生成(包括验证错误显示)。永远不必使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 混合和 match,或者直接 calls 到我们之前突出显示的 spring 绑定宏。

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

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

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

  • path:要绑定的字段的 name(即“command.name”)
  • options:可以在输入字段中选择的所有可用值的Map。 map 的键表示从表单返回并绑定到命令 object 的值。针对键存储的 Map objects 是表单上显示给用户的标签,可能与表单发回的相应值不同。通常,这样的 map 由控制器作为 reference 数据提供。您可以使用任何Map implementation,具体取决于所需的行为。对于严格排序的 maps,您可以使用SortedMap(例如TreeMap)和合适的Comparator,对于任意 Maps 应该 return insert order 中的值,使用LinkedHashMapLinkedMap来自commons-collections
  • separator:多个选项可用作谨慎元素(单选按钮或复选框),用于分隔列表中每个元素的字符序列(例如<br>)。
  • attributes:HTML 标记本身中包含的任意标记或文本的附加 string。这个 string 由宏按字面回显。例如,在textarea字段中,您可以提供属性(例如’rows =“5”cols =“60”’),或者您可以传递样式信息,例如’style =“border:1px solid silver”’。
  • classOrStyle:对于showErrors宏,包含每个错误的span元素使用的 CSS class 的 name。如果未提供任何信息(或 value 为空),则错误将包含在<b></b>标记中。

以下部分概述了宏的示例(一些在 FTL 中,一些在 VTL 中)。如果两种语言之间存在使用差异,则会在说明中对其进行说明。

formInput宏采用path参数(command.name)和一个额外的attributes参数(在即将发生的 example 中为空)。宏以及所有其他表单生成宏在 path 参数上执行隐式 Spring 绑定。 binding 在发生新绑定之前一直有效,因此showErrors宏不需要再次传递 path 参数 - 它在最后创建绑定的字段上运行。

showErrors宏采用 separator 参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数 - 此 time,class name 或 style 属性。请注意,FreeMarker 可以为 attributes 参数指定默认值。以下 example 显示了如何使用formInputshowWErrors宏:

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

下一个 example 显示表单片段的输出,生成 name 字段并在提交表单后在字段中没有 value 时显示验证错误。验证通过 Spring 的验证 framework 进行。

生成的 HTML 类似于以下 example:

1
2
3
4
5
6
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>

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

您可以使用四个选择字段宏在 HTML 表单中生成 common UI value 选择输入:

  • formSingleSelect
  • formMultiSelect
  • formRadioButtons
  • formCheckboxes

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

下一个示例用于 FTL 中的单选按钮。 form-backing object 为此字段指定了“伦敦”的默认值值,因此无需进行验证。呈现表单时,可以在 name’cityMap’下的 model 中作为 reference 数据提供可供选择的整个城市列表。以下清单显示了 example:

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

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

1
2
3
4
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>

如果您的 application 希望按内部代码处理城市(对于 example),您可以使用合适的密钥创建代码的 map,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
protected Map<String, String> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");

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

code 现在生成输出,其中无线电值是相关代码,但用户仍然看到更多 user-friendly 城市名称,如下所示:

1
2
3
4
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 4.01 的 HTML 元素,并使用文件中定义的 HTML 转义的默认值,如 Spring 的绑定支持所使用的那样。要使元素符合 XHTML 要求或覆盖默认的 HTML 转义 value,您可以在模板中指定两个变量(或者在 model 中指定模板可见的变量)。在模板中指定它们的优点是,它们可以在模板处理中稍后更改为不同的值,以便为表单中的不同字段提供不同的行为。

要切换为标记的 XHTML 合规性,请为名为xhtmlCompliant的 model 或 context 变量指定的 value,如下面的 example 所示:

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

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

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

1
2
3
4
5
6
7
8
<#-- 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 -->

Groovy标记

Groovy 标记模板引擎主要用于生成 XML-like 标记(XML,XHTML,HTML5 等),但您可以使用它来生成任何 text-based 内容。 Spring Framework 有一个 built-in integration 用于将 Spring MVC 与 Groovy Markup 一起使用。

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

Configuration

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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;
}
}

以下 example 显示了如何在 XML 中配置相同的内容:

1
2
3
4
5
6
7
8
<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。以下 example 显示了 HTML 页面的 sample 模板:

1
2
3
4
5
6
7
8
9
10
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')
}
}

脚本视图

与 Spring WebFlux 相同

Spring Framework 有一个 built-in integration 用于使用 Spring MVC 和任何可以在JSR-223 Java 脚本引擎之上运行的模板 library。我们在不同的脚本引擎上测试了以下模板 libraries:

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

需求

与 Spring WebFlux 相同

您需要在 classpath 上安装脚本引擎,其详细信息因脚本引擎而异:

  • Nashorn JavaScript 引擎随 Java 8 一起提供。强烈建议使用最新的更新版本。
  • 应该添加JRuby作为 Ruby 支持的依赖项。
  • 应该添加Jython作为 Python 支持的依赖项。
  • 应为 Kotlin 脚本支持添加org.jetbrains.kotlin:kotlin-script-util依赖项和包含org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory line 的META-INF/services/javax.script.ScriptEngineFactory文件。有关详细信息,请参阅这个 example

你需要有脚本模板 library。对 Javascript 执行此操作的一种方法是通过WebJars

脚本模板

与 Spring WebFlux 相同

您可以声明ScriptTemplateConfigurer bean 来指定要使用的脚本引擎,要加载的脚本 files,要调用渲染模板的 function,等等。以下 example 使用 Mustache 模板和 Nashorn JavaScript 引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}
}

以下 example 在 XML 中显示相同的排列:

1
2
3
4
5
6
7
8
9
<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 配置,控制器看起来没有什么不同,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class SampleController {

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

以下 example 显示了 Mustache 模板:

1
2
3
4
5
6
7
8
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>

使用以下参数调用 render function:

  • String template:模板内容
  • Map model:视图 model
  • RenderingContext renderingContextRenderingContext可以访问 application context,locale,模板加载器和 URL(自 5.0 以来)

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

如果您的模板技术需要一些自定义,您可以提供实现自定义 render function 的脚本。例如,Handlerbars需要在使用它们之前编译模板,并且需要填充工具来模拟 server-side 脚本引擎中不可用的某些浏览器工具。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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;
}
}

当您使用 non-thread-safe 脚本引擎以及不是为并发而设计的模板 libraries 时,需要将sharedEngine property 设置为false,例如 Handlebars 或 Nactorn 上的 React running。在这种情况下,由于这个 bug,需要 Java 8u60 或更高版本。

polyfill.js仅定义 Handlebars 所需的window object 以正确 run,如下所示:

1
var window = {};

这个基本的render.js implementation 在使用之前编译模板。 production-ready 实现还应该存储任何重用的缓存模板或 pre-compiled 模板。您可以在脚本端执行此操作(并处理所需的任何自定义 - 管理模板引擎 configuration,用于 example)。以下 example 显示了如何执行此操作:

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

查看 Spring Framework 单元测试,Java资源,了解更多配置示例。

JSP和JSTL

Spring Framework 有一个 built-in integration 用于将 Spring MVC 与 JSP 和 JSTL 一起使用。

View解析器

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

ResourceBundleViewResolver依赖于 properties 文件来定义映射到 class 和 URL 的视图名称。使用ResourceBundleViewResolver,您可以通过仅使用一个解析器来混合不同类型的视图,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
<!-- the ResourceBundleViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>

# And a sample properties file is uses (views.properties in WEB-INF/classes):
welcome.(class)=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp

productList.(class)=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp

InternalResourceBundleViewResolver也可以用于 JSP。作为最佳实践,我们强烈建议将 JSP files 放在'WEB-INF'目录下的目录中,以便 clients 无法直接访问。

1
2
3
4
5
<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>
JSPs与JSTL

使用 Java 标准标记 Library 时,必须使用特殊视图 class,JstlView,因为 JSTL 需要一些准备工作,比如 I18N features 之类的东西才能工作。

Spring的JSP标签库

Spring 为命令 objects 提供请求参数的数据 binding,如前面章节所述。为了便于 JSP 页面的开发与 binding features 数据的结合,Spring 提供了一些标签,使事情变得更加容易。所有 Spring 标签都具有 HTML 转义 features 以启用或禁用字符转义。

spring.tld标签 library 描述符(TLD)包含在spring-webmvc.jar中。有关单个标记的综合参考,请浏览API reference或查看标记 library 描述。

Spring的表单标签库

从 version 2.0 开始,Spring 提供了一组全面的数据 binding-aware 标签,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都支持其相应 HTML 标记对应的属性集,使标记熟悉且直观易用。 tag-generated HTML 符合 HTML 4.01/XHTML 1.0.

与其他 form/input tag libraries 不同,Spring 的表单标签 library 与 Spring Web MVC 集成,使标签可以访问控制器处理的命令 object 和 reference 数据。正如我们在以下示例中所示,表单标记使 JSP 更易于开发,读取和维护。

我们浏览表单标签,查看每个标签的使用方式的示例。我们已经包含生成的 HTML 代码段,其中某些代码需要进一步评论。

Configuration

表单标签 library 捆绑在spring-webmvc.jar中。 library 描述符称为spring-form.tld

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

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

其中form是您要用于此 library 的标记的标记 name 前缀。

表单标签

此标记呈现 HTML’form’元素,并为 binding 公开内部标记的 binding 路径。它将命令 object 放在PageContext中,以便内部标记可以访问命令 object。此 library 中的所有其他标记都是form标记的嵌套标记。

假设我们有一个名为User的域 object。它是一个带有 properties 的 JavaBean,例如firstNamelastName。我们可以将它用作表单控制器的 form-backing object,它返回form.jsp。以下 example 显示了form.jsp的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>

firstNamelastName值是从页面控制器放置在PageContext中的命令 object 中检索的。继续阅读以查看内部标记与form标记一起使用的更复杂示例。

以下清单显示了生成的 HTML,它看起来像标准表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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 假定 form-backing object 的变量 name 是command。如果已将 form-backing object 放入另一个 name 下的 model(绝对是最佳实践),则可以将表单绑定到命名变量,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>
输入标签

此标记默认使用绑定 value 和type='text'呈现 HTML input元素。有关此标记的 example,请参阅表单标签。您还可以使用 HTML5-specific 类型,例如emailteldate等。

复选框标签

此标记呈现 HTML input标记,type设置为checkbox

假设我们的User具有诸如简报订阅和爱好列表之类的偏好。以下 example 显示了Preferences class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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可能类似于以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<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.Boolean时,如果绑定的 value 为true,则input(checkbox)标记为checkedvalue属性对应于setValue(Object) value property 的已解析 value。
  • 方法二:当绑定 value 的类型为arrayjava.util.Collection时,如果配置的setValue(Object) value 存在于绑定Collection中,input(checkbox)将标记为checked
  • 方法三:对于任何其他绑定 value 类型,如果配置的setValue(Object)等于绑定 value,input(checkbox)将标记为checked

请注意,无论采用何种方法,都会生成相同的 HTML 结构。以下 HTML 代码段定义了一些复选框:

1
2
3
4
5
6
7
8
9
10
11
<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 页面中的复选框,则在提交表单后,其 value 不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要为 HTML 中的这个怪癖提供一种解决方法,以便 Spring 表单数据 binding 工作。 checkbox标记遵循现有的 Spring 约定,包括每个复选框的下划线(_)前缀的隐藏参数。通过这样做,你实际上告诉 Spring“复选框在表单中是可见的,我希望表单数据绑定的 object 反映复选框的 state,无论如何。”

checkboxes标签

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

此部分 build 位于前一个checkbox标记部分的 example 上。有时,您不希望在 JSP 页面中列出所有可能的爱好。您宁愿在运行时提供可用选项的列表,并将其传递给标记。这就是checkboxes标签的目的。您可以传入包含items property 中可用选项的Array,或Map。通常,绑定的 property 是一个集合,因此它可以包含用户选择的多个值。以下 example 显示了使用此标记的 JSP:

1
2
3
4
5
6
7
8
9
10
11
<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>

此 example 假定interestListList可用作 model 属性,该属性包含要从中选择的值的 strings。如果使用Map,则 map 条目 key 用作 value,map 条目的 value 用作要显示的标签。您还可以使用自定义 object,您可以使用itemValue为 value 提供 property 名称,使用itemLabel提供标签。

radiobutton标签

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

典型用法 pattern 涉及绑定到同一 property 但具有不同值的多个标记实例,如下面的 example 所示:

1
2
3
4
5
6
7
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
radiobuttons标签

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

复选框标记一样,您可能希望将可用选项作为运行时变量传递。对于此用法,您可以使用radiobuttons标记。传入ArrayListMap,其中包含items property 中的可用选项。如果使用Map,则 map 条目 key 用作 value,map 条目的 value 用作要显示的标签。您还可以使用自定义 object,您可以使用itemValue为 value 提供 property 名称,使用itemLabel提供标签,如下面的 example 所示:

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

此标记使用绑定 value 呈现类型设置为password的 HTML input标记。

1
2
3
4
5
6
<tr>
<td>Password:</td>
<td>
<form:password path="password"/>
</td>
</tr>

请注意,默认情况下,不显示密码 value。如果确实需要显示密码 value,可以将showPassword属性的 value 设置为true,如下面的 example 所示:

1
2
3
4
5
6
<tr>
<td>Password:</td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
</td>
</tr>
select标签

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

假设User有一个技能列表。相应的 HTML 可以如下:

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

如果User's技能在草药学中,则“技能”行的 HTML 源可能如下:

1
2
3
4
5
6
7
8
9
10
<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>
options标签

此标记呈现 HTML option元素。它根据绑定的 value sets selected。以下 HTML 显示了它的典型输出:

1
2
3
4
5
6
7
8
9
10
11
<tr>
<td>House:</td>
<td>
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
</form:select>
</td>
</tr>

如果User's房子在格兰芬多,那么’House’行的 HTML 源代码如下:

1
2
3
4
5
6
7
8
9
10
11
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
1 请注意添加selected属性。
选项标签

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

1
2
3
4
5
6
7
8
9
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>

如果User位于英国,则“Country”行的 HTML 源代码如下:

1
2
3
4
5
6
7
8
9
10
11
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> (1)
<option value="US">United States</option>
</select>
</td>
</tr>
1 请注意添加selected属性。

如前面的示例所示,option标记与options标记的组合使用生成相同的标准 HTML,但允许您在 JSP 中显式指定仅用于显示(它所属的位置)的 value,例如默认的 string example:“ - 请选择”。

items属性通常填充 item objects 的集合或 array。 itemValueitemLabel引用那些 item objects 的 bean properties,如果指定的话。否则,item objects 本身会变成 strings。或者,您可以指定Map项,在这种情况下,map 键被解释为选项值,map 值对应于选项标签。如果恰好也指定itemValueitemLabel(或两者),则 item value property 应用于 map key,item 标签 property 应用于 map value。

textarea标签

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

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

此标记将带有type的 HTML input标记设置为带有绑定 value 的hidden。要提交未绑定的隐藏 value,请使用设置为hidden的 HTML input标记。以下 HTML 显示了它的典型输出:

1
<form:hidden path="house"/>

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

1
<input name="house" type="hidden" value="Gryffindor"/>
错误标签

此标记在 HTML span元素中呈现字段错误。它可以访问控制器中创建的错误或由与控制器关联的任何验证器创建的错误。

假设我们要在提交表单后显示firstNamelastName字段的所有错误消息。我们有一个名为UserValidatorUser class 实例的验证器,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
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可以如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<%-- Show errors for firstName field --%>
<td><form:errors path="firstName"/></td>
</tr>

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

如果我们在firstNamelastName字段中提交带有空值的表单,则 HTML 将如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

如果我们想显示给定页面的整个错误列表怎么办?下一个 example 显示errors标记还支持一些基本的通配符功能。

  • path="*":显示所有错误。
  • path="lastName":显示与lastName字段关联的所有错误。
  • 如果省略path,则仅显示 object 错误。

以下 example 在页面顶部显示错误列表,然后在字段旁边显示 field-specific 错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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 将如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form method="POST">
<span name="*.errors" class="errorBox">Field is required.Field is required.</span>
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>

<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>

spring-form.tld标签 library 描述符(TLD)包含在spring-webmvc.jar中。有关单个标记的综合参考,请浏览API reference或查看标记 library 描述。

HTTP方法转换

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

为了支持 HTTP 方法转换,更新了 Spring MVC 表单标记以支持设置 HTTP 方法。对于 example,以下代码段来自 Pet Clinic sample:

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

前面的 example 执行 HTTP POST,“real”DELETE 方法隐藏在请求参数后面。它由HiddenHttpMethodFilter拾取,它在 web.xml 中定义,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
<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>

以下 example 显示了相应的@Controller方法:

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

Spring 表单标记 library 允许输入动态属性,这意味着您可以输入任何 HTML5 特定属性。

表单input标记支持输入text以外的类型属性。这旨在允许呈现新的 HTML5 特定输入类型,例如emaildaterange等。请注意,不需要输入type='text',因为text是默认类型。

Tiles

您可以在使用 Spring 的 web applications 中集成 Tiles - 就像任何其他视图技术一样。本节以广泛的方式介绍了如何执行此操作。

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

依赖关系

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

Configuration

为了能够使用 Tiles,您必须使用包含定义的 files 来配置它(有关定义和其他 Tiles 概念的基本信息,请参阅http://tiles.apache.org。在 Spring 中,这是通过使用TilesConfigurer来完成的。以下 example ApplicationContext configuration 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
<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>

前面的 example 定义了五个包含定义的 files。 files 都位于WEB-INF/defs目录中。在初始化WebApplicationContext时,将加载 files,并初始化定义工厂。完成之后,定义 files 中包含的 Tiles 可以用作 Spring web application 中的视图。为了能够使用视图,您必须使用ViewResolver与 Spring 使用的任何其他视图技术一样。您可以使用两个 implementations 中的任何一个,UrlBasedViewResolverResourceBundleViewResolver

您可以通过添加下划线然后添加 locale 来指定 locale-specific Tiles 定义,如下面的 example 所示:

1
2
3
4
5
6
7
8
<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>

使用前面的 configuration,tiles_fr_FR.xml用于具有fr_FR locale 的请求,默认情况下使用tiles.xml

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

UrlBasedViewResolver

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

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

必须为ResourceBundleViewResolver提供 property 文件,该文件包含解析程序可以使用的视图名称和视图 classes。以下 example 显示了ResourceBundleViewResolver的 bean 定义以及相应的视图名称和视图 classes(取自 Pet Clinic sample):

1
2
3
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
1
2
3
4
5
6
7
8
9
10
...
welcomeView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
welcomeView.url=welcome (this is the name of a Tiles definition)

vetsView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
vetsView.url=vetsView (again, this is the name of a Tiles definition)

findOwnersForm.(class)=org.springframework.web.servlet.view.JstlView
findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
...

使用ResourceBundleViewResolver时,可以轻松混合使用不同的视图技术。

请注意,TilesView class 支持 JSTL(JSP 标准标记 Library)。

SimpleSpringPreparerFactory和SpringBeanPreparerFactory

作为高级 feature,Spring 还支持两个特殊的 Tiles PreparerFactory implementations。有关如何在 Tiles 定义 files 中使用ViewPreparer references 的详细信息,请参阅 Tiles 文档。

您可以根据指定的 preparer classes 指定SimpleSpringPreparerFactory到 autowire ViewPreparer实例,应用 Spring 的容器回调以及应用已配置的 Spring BeanPostProcessors。如果已激活 Spring 的 context-wide annotation configuration,则会自动检测并应用ViewPreparer classes 中的注释。请注意,这需要 Tiles 定义 files 中的 preparer classes,如默认PreparerFactory所做的那样。

您可以指定SpringBeanPreparerFactory来操作指定的 preparer 名称(而不是 classes),从 DispatcherServlet 的 application context 中获取相应的 Spring bean。在这种情况下,完整的 bean creation process 控制着 Spring application context,允许使用显式依赖注入 configuration,作用域 beans 等。请注意,您需要为每个 preparer name 定义一个 Spring bean 定义(在 Tiles 定义中使用)。以下 example 显示了如何在TilesConfigurer bean 上定义SpringBeanPreparerFactory property 集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>

<!-- resolving preparer names as Spring bean definition names -->
<property name="preparerFactoryClass"
value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>

</bean>

RSS和Atom

AbstractAtomFeedViewAbstractRssFeedView都从AbstractFeedView base class 继承,分别用于提供 Atom 和 RSS Feed 视图。它们基于 java.net 的罗马项目,位于包org.springframework.web.servlet.view.feed中。

AbstractAtomFeedView要求您实现buildFeedEntries()方法并可选地覆盖buildFeedMetadata()方法(默认 implementation 为空)。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SampleContentAtomView extends AbstractRssFeedView {

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

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

如果您需要访问 Locale,buildFeedItems()buildFeedEntries()方法会传入 HTTP 请求。 HTTP 响应仅传递给 cookies 或其他 HTTP headers 的设置。方法返回后,Feed 会自动写入响应 object。

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

PDF和Excel

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

文档视图简介

HTML 页面并不总是用户查看 model 输出的最佳方式,而 Spring 使得从 model 数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是视图,并使用正确的 content type 从服务器流式传输,以(希望)使 client PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。

在 order 中使用 Excel 视图,您需要将 Apache POI library 添加到 classpath,对于 PDF 生成,您需要添加(最好)OpenPDF library。

如果可能,您应该使用底层 document-generation libraries 的最新版本。特别是,我们强烈建议使用 OpenPDF(例如,OpenPDF 1.0.5)而不是过时的原始 iText 2.1.7,因为 OpenPDF 是主动维护的,并修复了不受信任的 PDF 内容的重要漏洞。

PDF视图

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

1
2
3
4
5
6
7
8
9
10
11
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));
}
}
}

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

Excel视图

自 Spring Framework 4.2 以来,org.springframework.web.servlet.view.document.AbstractXlsView作为 Excel 视图的 base class 提供。它基于 Apache POI,具有专门的子类(AbstractXlsxViewAbstractXlsxStreamingView),取代过时的AbstractExcelView class。

编程 model 类似于AbstractPdfView,其中buildExcelDocument()作为中心模板方法,控制器能够从外部定义(通过 name)返回这样的视图,或者从处理程序方法返回View实例。

Jackson

与 Spring WebFlux 相同

Spring 支持 Jackson JSON library。

基于Jackson的JSON视图

与 Spring WebFlux 相同

MappingJackson2JsonView使用 Jackson library 的ObjectMapper将响应内容呈现为 JSON。默认情况下,model map 的全部内容(类的 exception)被编码为 JSON。对于需要过滤 map 内容的情况,您可以使用modelKeys property 指定要编码的特定 model 属性集。您还可以使用extractValueFromSingleKeyModel property 直接提取和序列化 single-key 模型中的 value,而不是 model 属性的 map。

您可以使用 Jackson 提供的 annotations 根据需要自定义 JSON 映射。当您需要进一步控制时,您可以 inject ObjectMapper通过ObjectMapper property,以便您需要为特定类型提供自定义 JSON 序列化程序和反序列化程序。

基于Jackson的XML视图

与 Spring WebFlux 相同

MappingJackson2XmlView使用Jackson XML 扩展 XmlMapper将响应内容呈现为 XML。如果 model 包含多个条目,则应使用modelKey bean property 显式设置 object 以进行序列化。如果 model 包含单个条目,则会自动序列化。

您可以使用 JAXB 或 Jackson 提供的 annotations 根据需要自定义 XML 映射。当您需要进一步控制时,您可以 inject XmlMapper通过ObjectMapper property,以便自定义 XML 需要为特定类型提供序列化程序和反序列化程序。

XML编组

MarshallingView使用 XML Marshaller(在org.springframework.oxm包中定义)将响应内容呈现为 XML。您可以使用MarshallingView实例的modelKey bean property 显式设置要被编组的对象。或者,视图迭代所有 model properties 并编组Marshaller支持的第一个类型。有关org.springframework.oxm包中功能的更多信息,请参阅使用 O/X Mappers 编组 XML

XSLT视图

XSLT 是 XML 的转换语言,在 web applications 中作为视图技术很受欢迎。如果您的 application 自然地处理 XML 或者您的 model 可以很容易地转换为 XML,那么 XSLT 可以作为一种视图技术。以下部分显示如何将 XML 文档生成为 model 数据,并在 Spring Web MVC application 中使用 XSLT 进行转换。

这个 example 是一个简单的 Spring application,可以在Controller中创建一个单词列表,并将它们添加到 model map 中。返回 map 以及 XSLT 视图的 view name。有关 Spring Web MVC 的Controller接口的详细信息,请参阅带注释的控制器。 XSLT 控制器将单词列表转换为准备转换的简单 XML 文档。

Beans

Configuration 是一个简单的 Spring web application 的标准:MVC configuration 必须定义XsltViewResolver bean 和常规的 MVC annotation configuration。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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;
}
}
控制器

我们还需要一个封装 word-generation 逻辑的 Controller。

控制器逻辑封装在@Controller class 中,处理程序方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 文档并将其添加到 Model map 中。请注意,您还可以将 XML 文件作为Resource加载,并使用它而不是自定义 DOM 文档。

有些软件包可以自动“统一”object 图形,但是,在 Spring 中,您可以完全灵活地以您选择的任何方式从 model 创建 DOM。这可以防止 XML 的转换在 model 数据的结构中扮演太大的角色,这在使用工具管理 DOMification process 时是一种危险。

转型

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

以下 example 显示了一个 XSLT 转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>

MVC配置

与 Spring WebFlux 相同

MVC Java configuration 和 MVC XML 名称空间提供适用于大多数 applications 的默认 configuration 和用于自定义它的 configuration API。

有关 configuration API 中未提供的更高级自定义,请参阅高级 Java 配置高级 XML 配置

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

启用MVC配置

与 Spring WebFlux 相同

在 Java configuration 中,您可以使用@EnableWebMvc annotation 来启用 MVC configuration,如下面的 example 所示:

1
2
3
4
@Configuration
@EnableWebMvc
public class WebConfig {
}

在 XML configuration 中,您可以使用<mvc:annotation-driven>元素启用 MVC configuration,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<mvc:annotation-driven/>

</beans>

前面的 example 注册了许多 Spring MVC 基础设施 beans并适应 classpath 上可用的依赖项(对于示例,JSON,XML 和其他的有效负载转换器)。

MVC配置API

与 Spring WebFlux 相同

在 Java configuration 中,您可以实现WebMvcConfigurer接口,如下面的 example 所示:

1
2
3
4
5
6
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

// Implement configuration methods...
}

在 XML 中,您可以检查<mvc:annotation-driven/>的属性和 sub-elements。您可以查看Spring MVC XML schema或使用 IDE 的 code completion feature 来发现可用的属性和 sub-elements。

类型转换

与 Spring WebFlux 相同

默认格式化,安装了NumberDate类型,包括对@NumberFormat@DateTimeFormat 注释的支持。如果 class 路径中存在 Joda-Time,则还会安装对 Joda-Time 格式 library 的完全支持。

在 Java configuration 中,您可以注册自定义格式化程序和转换器,如下面的示例所示:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

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

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

</beans>

有关何时使用 FormatterRegistrar implementations 的详细信息,请参阅FormatterRegistrar SPIFormattingConversionServiceFactoryBean

验证

与 Spring WebFlux 相同

默认情况下,如果 class 路径上存在Bean 验证(对于 example,Hibernate Validator),则LocalValidatorFactoryBean将注册为 global 验证器,以便与控制器方法 arguments 上的@ValidValidated一起使用。

在 Java configuration 中,您可以自定义 global Validator实例,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

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

</beans>

请注意,您还可以在本地注册Validator implementations,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Controller
public class MyController {

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

}

如果需要在某处注入LocalValidatorFactoryBean,请创建 bean 并在 order 中使用@Primary标记它以避免与 MVC configuration 中声明的冲突。

拦截器

在 Java configuration 中,您可以注册拦截器以应用传入请求,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/secure/*"/>
<bean class="org.example.SecurityInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>

内容类型

与 Spring WebFlux 相同

您可以配置 Spring MVC 如何根据请求确定所请求的媒体类型(对于 example,Accept标头,URL 路径扩展,查询参数等)。

默认情况下,首先检查 URL 路径扩展名 - 将jsonxmlrssatom注册为已知的 extensions(取决于 classpath 依赖项)。第二次检查Accept标题。

请考虑仅将这些默认值更改为Accept标头,如果必须使用 URL-based content type 解析,请考虑在路径 extensions 上使用查询参数策略。有关详细信息,请参阅后缀 Match后缀 Match 和 RFD

在 Java configuration 中,您可以自定义请求的 content type 解析,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@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);
}
}

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
<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>

消息转换器

与 Spring WebFlux 相同

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

以下 example 添加了带有自定义ObjectMapper而不是默认值的 XML 和 Jackson JSON 转换器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {

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

在上面的示例中,Jackson2ObjectMapperBuilder用于为启用缩进的MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter创建 common configuration,自定义 date 格式,以及jackson-module-parameter-names的注册,这增加了对访问参数名称的支持(在 Java 8 中添加了 feature)。

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

如果在 classpath 上检测到以下 well-known 模块,它还会自动注册:

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

其他有趣的 Jackson 模块可用:

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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"/>

View控制器

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

下面的 Java configuration 示例将/的请求转发给名为home的视图:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 与前面的 example 实现相同的功能,但使用 XML,使用<mvc:view-controller>元素:

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

View解析器

与 Spring WebFlux 相同

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

以下 Java configuration example 使用 JSP 和 Jackson 作为 JSON 呈现的默认View配置内容 negotiation view resolution:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
<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 名称空间提供专用元素。以下 example 适用于 FreeMarker:

1
2
3
4
5
6
7
8
9
10
11
12
<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 configuration 中,您可以添加相应的Configurer bean,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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;
}
}

静态资源

与 Spring WebFlux 相同

此选项提供了一种从资源 -based 位置列表中提供静态资源的便捷方法。

在下一个示例中,给定以/resources开头的请求,相对路径用于在 web application 根目录下或/static下的 classpath 上查找和提供相对于/public的静态资源。资源的使用期限为 one-year,以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还会评估Last-Modified标头,如果存在,则返回304 status code。

以下清单显示了如何使用 Java configuration 执行此操作:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

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

另见HTTP 缓存支持静态资源

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

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

以下 example 显示了如何在 Java configuration 中使用VersionResourceResolver

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain>
<mvc:resource-cache/>
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
</mvc:version-resolver>
</mvc:resolvers>
</mvc:resource-chain>
</mvc:resources>

然后,您可以使用ResourceUrlProvider来 rewrite URL 并将完整的解析器和变换器链应用于 example,以插入版本。 MVC configuration 提供ResourceUrlProvider bean,以便可以将其注入其他人。您还可以使用ResourceUrlEncodingFilter使用ResourceUrlEncodingFilter透明化 Thymeleaf,JSP,FreeMarker 以及其他依赖HttpServletResponse#encodeURL的 URL 标记。

请注意,当同时使用EncodedResourceResolver(对于 example,用于提供 gzip 或 brotli-encoded 资源)和VersionedResourceResolver时,必须在此 order 中注册它们。这样可以确保始终根据未编码的文件可靠地计算 content-based 版本。

WebJarsResourceResolver也通过WebJarsResourceResolver支持,并在 class 路径上存在org.webjars:webjars-locator时自动注册。解析器可以 re-write URL 包含 jar 的 version,也可以匹配到没有版本的传入 URL - 例如,/jquery/jquery.min.js/jquery/1.2.0/jquery.min.js

默认Servlet

Spring MVC 允许将DispatcherServlet映射到/(从而覆盖容器的默认 Servlet 的映射),同时仍允许容器的默认 Servlet 处理静态资源请求。它使用/**的 URL 映射配置DefaultServletHttpRequestHandler,并将相对于其他 URL 映射的最低优先级配置为DefaultServletHttpRequestHandler

此处理程序将所有请求转发到默认的 Servlet。因此,它必须保留在所有其他 URL HandlerMappings的 order 中的最后一个。如果你使用<mvc:annotation-driven>就是这种情况。或者,如果您设置自己的自定义HandlerMapping实例,请务必将其order property 设置为低于DefaultServletHttpRequestHandler的 value,即Integer.MAX_VALUE

以下 example 显示了如何使用默认设置启用 feature:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
<mvc:default-servlet-handler/>

覆盖/ Servlet 映射的警告是,必须通过 name 而不是 path 来检索默认 Servlet 的RequestDispatcherDefaultServletHttpRequestHandler使用大多数主要 Servlet 容器(包括 Tomcat,Jetty,GlassFish,JBoss,Resin,WebLogic 和 WebSphere)的已知名称列表,在启动 time 时尝试 auto-detect 为容器的默认 Servlet。如果默认 Servlet 是 custom-configured 且具有不同的 name,或者如果在默认的 Servlet name 未知的情况下使用了不同的 Servlet 容器,则必须显式提供默认的 Servlet 的 name,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

}

以下 example 显示了如何在 XML 中实现相同的 configuration:

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

路径匹配

与 Spring WebFlux 相同

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

以下 example 显示了如何在 Java configuration 中自定义路径匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}

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

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

}

以下 example 显示了如何在 XML 中实现相同的 configuration:

1
2
3
4
5
6
7
8
9
10
11
<mvc:annotation-driven>
<mvc:path-matching
suffix-pattern="true"
trailing-slash="false"
registered-suffixes-only="true"
path-helper="pathHelper"
path-matcher="pathMatcher"/>
</mvc:annotation-driven>

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

高级Java配置

与 Spring WebFlux 相同

@EnableWebMvc进口DelegatingWebMvcConfiguration,其中:

  • 为 Spring MVC applications 提供默认的 Spring configuration
  • 检测并委托WebMvcConfigurer implementations 来自定义 configuration。

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

1
2
3
4
5
6
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

// ...

}

您可以在WebConfig中保留现有方法,但现在您也可以从 base class 覆盖 bean 声明,并且您仍然可以在 classpath 上包含任意数量的其他WebMvcConfigurer __mplement。

高级XML配置

MVC 命名空间没有高级模式。如果你需要在 bean 上自定义 property,否则你无法改变,你可以使用 Spring ApplicationContextBeanPostProcessor生命周期 hook,如下面的 example 所示:

1
2
3
4
5
6
7
@Component
public class MyPostProcessor implements BeanPostProcessor {

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

请注意,您需要将MyPostProcessor声明为 bean,显式为 XML 或允许通过<component-scan/>声明进行检测。

HTTP/2

与 Spring WebFlux 相同

Servlet 4 容器需要支持 HTTP/2,而 Spring Framework 5 与 Servlet API 兼容 4.从编程 model 的角度来看,没有特定的 applications 需要做的事情。但是,有一些与 server configuration 相关的注意事项。有关更多详细信息,请参阅HTTP/2 维基页面

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

REST-Clients

本节介绍 client-side 访问 REST endpoints 的选项。

RestTemplate

RestTemplate是执行 HTTP 请求的同步 client。它是原始的 Spring REST client,并在底层的 HTTP client libraries 上公开了一个简单的 template-method API。

从 5.0 开始,non-blocking,reactive WebClient提供了RestTemplate的现代替代方案,同时有效地支持同步和异步以及流方案。 RestTemplate将在未来的 version 中弃用,并且不会在未来添加主要的新 features。

有关详细信息,请参阅REST Endpoints

Web客户端

WebClient是执行 HTTP 请求的 non-blocking,reactive client。它在 5.0 中引入并提供了RestTemplate的现代替代方案,同时有效支持同步和异步以及流方案。

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

  • Non-blocking I/O。
  • Reactive Streams 背压。
  • 高并发性,硬件资源更少。
  • Functional-style,fluent API 利用 Java 8 lambdas。
  • 同步和异步交互。
  • 从服务器流式传输或向下传输。

有关详细信息,请参阅Web 客户端

Testing

Spring WebFlux 中相同

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

  • Servlet API 模拟:Mock _实现 Servlet API contracts 用于单元测试控制器,过滤器和其他 web 组件。有关详细信息,请参阅Servlet API mock objects。
  • TestContext Framework:支持在 JUnit 和 TestNG 测试中 loading Spring configuration,包括跨测试方法高效缓存已加载的 configuration,并支持使用MockServletContext 加载WebApplicationContext。有关详细信息,请参阅TestContext Framework
  • Spring MVC 测试:framework,也称为MockMvc,用于通过DispatcherServlet(即支持 annotations)测试带注释的控制器,完成 Spring MVC 基础结构但没有 HTTP 服务器。有关详细信息,请参阅Spring MVC 测试
  • Client-side REST:spring-test提供MockRestServiceServer,您可以将其用作 mock 服务器来测试内部使用RestTemplate的 client-side code。有关详细信息,请参阅Client REST 测试
  • WebTestClient:专为测试 WebFlux applications 而构建,但它也可用于通过 HTTP 连接对任何服务器进行 end-to-end integration 测试。它是 non-blocking,reactive client,非常适合测试异步和流式方案。

WebSockets

与 Spring WebFlux 相同

reference 文档的这一部分包括对 Servlet 堆栈的支持,包括原始 WebSocket 交互的 WebSocket 消息传递,通过 SockJS 的 WebSocket 仿真,以及通过 STOMP 作为 sub-protocol over WebSocket 的 publish-subscribe 消息传递。

WebSocket 简介

WebSocket 协议RFC 6455提供了一种标准方法,可以通过单个 TCP 连接在 client 和服务器之间建立 full-duplex,two-way 通信 channel。它是来自 HTTP 的不同 TCP 协议,但设计为使用端口 80 和 443 并允许 re-use 现有防火墙规则通过 HTTP 工作。

WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用 HTTP Upgrade标头进行升级,或者在这种情况下,切换到 WebSocket 协议。以下 example 显示了这样的交互:

1
2
3
4
5
6
7
8
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 状态 code:

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

成功握手后,HTTP 升级请求所基于的 TCP socket 将保持打开状态,以便 client 和服务器继续发送和接收消息。

有关 WebSockets 如何工作的完整介绍超出了本文档的范围。请参阅 RFC 6455,HTML5 的 WebSocket 章节,或者 Web 上的任何介绍和教程。

请注意,如果 WebSocket 服务器在 web 服务器(e.g. nginx)后面运行,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果 application 在云环境中运行,请检查与 WebSocket 支持相关的云提供程序的说明。

HTTP与WebSocket

尽管 WebSocket 被设计为 HTTP-compatible 并以 HTTP 请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和 application 编程模型。

在 HTTP 和 REST 中,application 被建模为多个 URL。要与 application 进行交互,clients 访问这些 URL,request-response 样式。 Servers 根据 HTTP URL,方法和 headers 将请求路由到相应的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有 application 消息都在同一 TCP 连接上流动。这指向一个完全不同的异步,event-driven,messaging architecture。

WebSocket 也是一种 low-level 传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非 client 和服务器就消息语义达成一致,否则无法对路由进行 route 或 process。

WebSocket clients 和服务器可以通过 HTTP 握手请求上的Sec-WebSocket-Protocol标头协商使用 higher-level,消息传递协议(对于 example,STOMP)。如果没有,他们需要提出自己的惯例。

何时使用WebSockets

WebSockets 可以使 web 页面成为动态和交互式的。但是,在许多情况下,Ajax 和 HTTP 流式传输或 long 轮询的组合可以提供简单有效的解决方案。

例如,新闻,邮件和社交订阅源需要动态更新,但每隔几分钟就可以完全正常更新。另一方面,协作,游戏和财务应用程序需要更接近 real-time。

仅延迟不是决定因素。如果消息量相对较低(例如,监视网络故障),HTTP 流式传输或轮询可以提供有效的解决方案。它是低延迟,高频率和高容量的组合,是使用 WebSocket 的最佳选择。

还要记住,在 Internet 上,超出控制范围的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递Upgrade标头,或者因为它们关闭 long-lived 连接,这些连接显示为 idle。这意味着在防火墙内部使用 WebSocket 进行内部应用比使用面向公众的应用程序更直接。

WebSocketAPI

与 Spring WebFlux 相同

Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的 client-和 server-side applications。

WebSocketHandler

与 Spring WebFlux 相同

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

1
2
3
4
5
6
7
8
9
10
11
12
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 configuration 和 XML 名称空间支持,用于将前面的 WebSocket 处理程序映射到特定的 URL,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
}

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

pereceding example 用于 Spring MVC applications,应该包含在DispatcherServlet的 configuration 中。但是,Spring 的 WebSocket 支持不依赖于 Spring MVC。在WebSocketHttpRequestHandler的帮助下将WebSocketHandler集成到其他 HTTP-serving 环境中相对简单。

WebSocket信号交换

与 Spring WebFlux 相同

自定义初始 HTTP WebSocket 握手请求的最简单方法是通过HandshakeInterceptor,它在握手之前“之前”和“之后”暴露方法。您可以使用此类拦截器来阻止握手或使WebSocketSession可以使用任何属性。以下 example 使用 built-in 拦截器将 HTTP session 属性传递给 WebSocket session:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

更高级的选项是扩展执行 WebSocket 握手步骤的DefaultHandshakeHandler,包括验证 client 原点,协商 sub-protocol 和其他详细信息。如果需要在 order 中配置自定义RequestUpgradeStrategy以适应 WebSocket 服务器引擎和尚未支持的 version,则 application 可能还需要使用此选项(有关此主题的更多信息,请参阅部署。 Java configuration 和 XML 命名空间都可以配置自定义HandshakeHandler

Spring 提供WebSocketHandlerDecorator base class,您可以使用它来装饰带有其他行为的WebSocketHandler。使用 WebSocket Java configuration 或 XML 命名空间时,默认情况下会提供并添加 Logging 和 exception 处理 implementations。 ExceptionWebSocketHandlerDecorator捕获由任何WebSocketHandler方法引起的所有未捕获的 exceptions,并以状态1011关闭 WebSocket session,这表示服务器错误。

部署

Spring WebSocket API 易于集成到 Spring MVC application 中,其中DispatcherServlet同时提供 HTTP WebSocket 握手和其他 HTTP 请求。通过调用WebSocketHttpRequestHandler也可以轻松地集成到其他 HTTP 处理场景中。这很方便易懂。但是,有关 JSR-356 运行时的特殊注意事项。

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

这是 JSR-356 的一个重要限制,Spring 的 WebSocket 支持使用 server-specific RequestUpgradeStrategy __mplement 进行地址,即使在 JSR-356 运行时运行时也是如此。目前,Tomcat,Jetty,GlassFish,WebLogic,WebSphere 和 Undertow(以及 WildFly)都有这样的策略。

已经创建了一个克服 Java WebSocket API 中的上述限制的请求,可以在eclipse-ee4j/websocket-api#211处遵循。 Tomcat,Undertow 和 WebSphere 提供了自己的 API 替代方案,可以实现这一点,并且 Jetty 也可以。我们希望更多的服务器能够做到这一点。

第二个考虑因素是具有 JSR-356 支持的 Servlet 容器应该执行ServletContainerInitializer(SCI)扫描,这可以减慢 application 启动速度 - 在某些情况下,显着。如果在使用 JSR-356 支持升级到 Servlet 容器 version 后观察到重大影响,则应该可以通过使用web.xml中的<absolute-ordering />元素选择性地启用或禁用 web 片段(和 SCI 扫描),如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<absolute-ordering/>

</web-app>

然后,您可以通过 name 选择性地启用 web 片段,例如 Spring 自己的SpringServletContainerInitializer,它提供对 Servlet 3 Java 初始化 API 的支持。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

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

</web-app>

服务器Configuration

与 Spring WebFlux 相同

每个底层 WebSocket 引擎都会公开控制运行时特征的 configuration properties,例如消息缓冲区大小,idle 超时等等。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

对于 client-side WebSocket configuration,您应该使用WebSocketContainerFactoryBean(XML)或ContainerProvider.getWebSocketContainer()(Java configuration)。

对于 Jetty,您需要提供 pre-configured Jetty WebSocketServerFactory并通过 WebSocket Java 配置将其插入 Spring 的DefaultHandshakeHandler。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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)));
}

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

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

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

</beans>

允许Origins

与 Spring WebFlux 相同

从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是仅接受 same-origin 请求。也可以允许所有或指定的起源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的 clients 修改Origin标头 value(有关详细信息,请参阅RFC 6454:Web Origin 概念)。

三种可能的行为是:

  • 仅允许 same-origin 个请求(默认):在此模式下,启用 SockJS 时,Iframe HTTP 响应头X-Frame-Options设置为SAMEORIGIN,并且禁用 JSONP 传输,因为它不允许检查请求的来源。因此,启用此模式时不支持 IE6 和 IE7。
  • 允许指定的原始列表:每个允许的原点必须以http://https://开头。在此模式下,启用 SockJS 时,将禁用 IFrame 传输。因此,启用此模式时,不支持 IE6 到 IE9。
  • 允许所有来源:要启用此模式,您应提供*作为允许的原始值。在此模式下,所有传输都可用。

您可以配置 WebSocket 和 SockJS 允许的源,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

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

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

SockJS后备

在公共 Internet 上,受控制之外的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递Upgrade标头,或者因为它们关闭似乎为 idle 的 long-lived 连接。

这个问题的解决方案是 WebSocket 仿真 - 也就是说,首先尝试使用 WebSocket,然后再依靠 HTTP-based 技术来模拟 WebSocket 交互并公开相同的 application-level API。

在 Servlet 堆栈上,Spring Framework 为 SockJS 协议提供服务器(以及 client)支持。

概述

SockJS 的目标是让 applications 使用 WebSocket API,但在运行时必要时可以回退到 non-WebSocket 替代品,而无需更改 application code。

SockJS 包括:

  • SockJS 协议以可执行文件叙述测试的形式定义。
  • SockJS JavaScript client - 用于浏览器的 client library。
  • SockJS 服务器 implementations,包括 Spring Framework spring-websocket模块中的一个。
  • spring-websocket模块中的 SockJS Java client(自 version 4.1 起)。

SockJS 专为在浏览器中使用而设计。它使用各种技术来支持各种浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参阅SockJS client页面。传输分为三大类:WebSocket,HTTP Streaming 和 HTTP Long Polling。有关这些类别的概述,请参阅这篇博文

SockJS client 首先发送GET /info以从服务器获取基本信息。之后,它必须决定使用什么传输。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流选项。如果不是,则使用 HTTP(long)轮询。

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

1
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

哪里:

  • {server-id}对于在 cluster 中路由请求很有用,但是否则不会使用。
  • {session-id}关联属于 SockJS session 的 HTTP 请求。
  • {transport}表示传输类型(对于 example,websocketxhr-streaming和其他)。

WebSocket 传输只需要一个 HTTP 请求即可进行 WebSocket 握手。之后的所有消息都在该 socket 上交换。

HTTP 传输需要更多请求。 Ajax/XHR streaming,对于 example,依赖于 server-to-client 消息的 long-running 请求和 client-to-server 消息的其他 HTTP POST 请求。 Long 轮询类似,只是它在每次 server-to-client 发送后_end 当前请求。

SockJS 增加了最小的消息框架。例如,服务器最初发送字母o(“打开”框架),如果没有消息流动 25 秒(默认情况下),消息将发送为a["message1","message2"](JSON-encoded array),字母h(“心跳”帧),以及字母c(“关闭”框架)关闭 session。

要了解更多信息,请在浏览器中运行 example 并查看 HTTP 请求。 SockJS client 允许修复传输列表,因此可以在 time 时查看每个传输。 SockJS client 还提供了一个 debug flag,它在浏览器 console 中启用了有用的消息。在服务器端,您可以为org.springframework.web.socket启用TRACE logging。有关更多详细信息,请参阅 SockJS 协议叙述测试

启用SockJS

您可以通过 Java configuration 启用 SockJS,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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();
}

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

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

</beans>

前面的 example 用于 Spring MVC applications,应该包含在DispatcherServlet的 configuration 中。但是,Spring 的 WebSocket 和 SockJS 支持并不依赖于 Spring MVC。在SockJsHttpRequestHandler的帮助下集成到其他 HTTP 服务环境相对简单。

在浏览器端,applications 可以使用sockjs-client(version 1.0.x)。它模拟 W3C WebSocket API 并与服务器通信以选择最佳传输选项,具体取决于运行它的浏览器。请参阅sockjs-client页面和浏览器支持的传输类型列表。 client 还提供了几个 configuration 选项 - 对于 example,指定要包含的传输。

IE8和9

Internet Explorer 8 和 9 仍在使用中。他们是拥有 SockJS 的关键原因。本节介绍有关在这些浏览器中运行的重要注意事项。

SockJS client 通过使用 Microsoft 的XDomainRequest支持 IE 8 和 9 中的 Ajax/XHR 流式传输。这适用于跨域但不支持发送 cookies。 Cookies 通常对 Java applications 很重要。但是,由于 SockJS client 可以与许多服务器类型(不仅仅是 Java 类型)一起使用,因此需要知道 cookies 是否重要。如果是这样,SockJS client 更喜欢 Ajax/XHR 进行流式传输。否则,它依赖于 iframe-based 技术。

来自 SockJS client 的第一个/info请求是对可能影响客户端传输选择的信息的请求。其中一个细节是 server application 是否依赖于 cookies(用于 example,用于身份验证或使用粘性会话进行群集)。 Spring 的 SockJS 支持包括一个名为sessionCookieNeeded的 property。默认情况下启用它,因为大多数 Java applications 依赖于JSESSIONID cookie。如果您的 application 不需要它,您可以关闭此选项,然后 SockJS client 应该在 IE 8 和 9 中选择xdr-streaming

如果您确实使用 iframe-based 传输,请记住,可以通过将 HTTP 响应头X-Frame-Options设置为DENYSAMEORIGINALLOW-FROM <origin>来指示浏览器阻止在给定页面上使用 IFrame。这用于防止点击劫持

Spring Security 3.2 支持在每个响应上设置X-Frame-Options。默认情况下,Spring Security Java configuration 将其设置为DENY。在 3.2 中,Spring Security XML 命名空间默认情况下不设置该标头,但可以配置为执行此操作。将来,它可以默认设置它。

有关如何配置X-Frame-Options标头设置的详细信息,请参阅 Spring Security 文档的默认安全 Headers。您还可以查看SEC-2501以获取更多背景信息。

如果你的 application 添加X-Frame-Options响应头(因为它应该!)并依赖于 iframe-based 传输,你需要将头 value 设置为SAMEORIGINALLOW-FROM <origin>。 Spring SockJS 支持还需要知道 SockJS client 的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJS client。配置此选项以使用与 application 相同的源的 URL 是一个很好的 idea。

以下 example 显示了如何在 Java configuration 中执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

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

// ...

}

XML 命名空间通过<websocket:sockjs>元素提供类似的选项。

在初始开发期间,请启用 SockJS client devel模式,以防止浏览器缓存否则将被缓存的 SockJS 请求(如 iframe)。有关如何启用它的详细信息,请参阅SockJS client页面。

心跳

SockJS 协议要求服务器发送心跳消息以阻止代理断定连接已挂起。 Spring SockJS configuration 有一个名为heartbeatTime的 property,可用于自定义频率。默认情况下,假设在该连接上没有发送其他消息,则会在 25 秒后发送心跳。对于公共 Internet 应用,此 25-second value 位于 line 中,以下IETF 推荐

当通过 WebSocket 和 SockJS 使用 STOMP 时,如果 STOMP client 和服务器协商要交换的心跳,则禁用 SockJS 心跳。

Spring SockJS 支持还允许您配置TaskScheduler来安排心跳任务。任务计划程序由线程池支持,默认设置基于可用处理器的数量。您应该考虑根据您的特定需求自定义设置。

Client断开连接

HTTP 流和 HTTP long 轮询 SockJS 传输要求连接保持打开时间比平时长。有关这些技术的概述,请参阅这篇博文

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

一个特定的问题是 Servlet API 不为已经消失的客户端提供通知。见eclipse-ee4j/servlet-api#44。但是,Servlet 容器会在后续尝试写入响应时引发 exception。由于 Spring 的 SockJS 服务支持 server-sent 心跳(默认情况下每 25 秒),这意味着通常会在_time 期间(或更早,如果更频繁地发送消息)检测到 client 断开连接。

因此,可能会发生网络 I/O 故障,因为 client 已断开连接,这可能会使用不必要的堆栈跟踪填充 log。 Spring 尽最大努力识别代表 client 断开连接(特定于每个服务器)的此类网络故障,并使用专用 log 类别(在AbstractSockJsSession中定义)log 记录最小消息。如果需要查看堆栈跟踪,可以将 log 类别设置为 TRACE。

SockJS和CORS

如果允许 cross-origin 请求(请参阅允许起源),则 SockJS 协议在 XHR 流和轮询传输中使用 CORS 进行 cross-domain 支持。因此,除非检测到响应中存在 CORS headers,否则会自动添加 CORS headers。因此,如果已经将 application 配置为提供 CORS 支持(对于 example,通过 Servlet 过滤器),Spring 的SockJsService将跳过此部分。

也可以通过在 Spring 的 SockJsService 中设置suppressCors property 来禁用这些 CORS _header 的添加。

SockJS 期望以下 headers 和值:

  • Access-Control-Allow-Origin:从Origin请求标头的 value 初始化。
  • Access-Control-Allow-Credentials:始终设置为true
  • Access-Control-Request-Headers:从等效请求标头中的值初始化。
  • Access-Control-Allow-Methods:传输支持的 HTTP 方法(请参阅TransportType enum)。
  • Access-Control-Max-Age:设置为 31536000(1 年)。

对于确切的 implementation,请参阅AbstractSockJsService中的addCorsHeaders和 source code 中的TransportType enum。

或者,如果 CORS configuration 允许,请考虑使用 SockJS 端点前缀排除 URL,从而让 Spring 的SockJsService处理它。

SockJsClient

Spring 提供了一个 SockJS Java client,无需使用浏览器即可连接到 remote SockJS endpoints。当需要通过公共网络在两个服务器之间进行双向通信时(即,网络代理可以排除使用 WebSocket 协议的情况下),这尤其有用。 SockJS Java client 对于测试目的也非常有用(例如,模拟大量并发用户)。

SockJS Java client 支持websocketxhr-streamingxhr-polling传输。其余的仅适用于浏览器。

您可以使用以下命令配置WebSocketTransport

  • 在 JSR-356 运行时StandardWebSocketClient
  • JettyWebSocketClient使用 Jetty 9 本机 WebSocket API。
  • Spring 的WebSocketClient的任何 implementation。

根据定义,XhrTransport支持xhr-streamingxhr-polling,因为从 client 的角度来看,除了用于连接服务器的 URL 之外没有其他区别。目前有两个实现:

  • RestTemplateXhrTransport对 HTTP 请求使用 Spring 的RestTemplate
  • JettyXhrTransport使用 Jetty 的HttpClient进行 HTTP 请求。

以下 example 显示了如何创建 SockJS client 并连接到 SockJS 端点:

1
2
3
4
5
6
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 并且需要在 classpath 上。或者,您可以配置SockJsMessageCodec的自定义 implementation 并在SockJsClient上配置它。

要使用SockJsClient模拟大量并发用户,您需要配置基础 HTTP client(用于 XHR 传输)以允许足够数量的连接和线程。以下 example 显示了如何使用 Jetty 执行此操作:

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

以下 example 显示了您应该考虑自定义的 server-side SockJS-related properties(请参阅 javadoc 以获取详细信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

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

// ...
}
1 streamBytesLimit property 设置为 512KB(默认值为 128KB - 128 * 1024)。
2 httpMessageCacheSize property 设置为 1,000(默认值为100)。
3 disconnectDelay property 设置为 30 property 秒(默认值为 5 秒 - 5 * 1000)。

STOMP

WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了 client 和服务器协商 sub-protocol(即 higher-level 消息传递协议)以在 WebSocket 之上使用的机制,以定义每个消息可以发送什么类型的消息,格式是什么,每条消息的内容,以及等等。使用 sub-protocol 是可选的,但无论如何,client 和服务器需要就定义消息内容的某些协议达成一致。

概述

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

STOMP 是一个 frame-based 协议,其帧在 HTTP 上建模。以下清单显示了 STOMP 框架的结构:

1
2
3
4
5
COMMAND
header1:value1
header2:value2

Body^@

Clients 可以使用SENDSUBSCRIBE命令发送或订阅消息,以及描述消息内容和应该接收消息的destination标头。这启用了一个简单的 publish-subscribe 机制,您可以使用该机制通过 broker 将消息发送到其他连接的客户端,或者向服务器发送消息以请求执行某些工作。

当您使用 Spring 的 STOMP 支持时,Spring WebSocket application 充当客户端的 STOMP broker。消息被路由到@Controller message-handling 方法或简单的 in-memory broker,它跟踪订阅并向订阅用户广播消息。您还可以将 Spring 配置为使用专用的 STOMP broker(例如 RabbitMQ,ActiveMQ 和其他)来实现消息的实际 broadcasting。在这种情况下,Spring 维护与 broker 的 TCP 连接,将消息中继到它,并将消息从它传递给连接的 WebSocket clients。因此,Spring web applications 可以依赖统一的 HTTP-based 安全性,common 验证和熟悉的编程 model 来进行消息处理。

以下 example 显示了一个 client 订阅接收股票报价,服务器可以定期发出(例如,通过向 broker 发送消息的计划任务):

1
2
3
4
5
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

以下 example 显示了一个发送交易请求的客户端,服务器可以通过@MessageMapping方法处理该请求:

1
2
3
4
5
6
SEND
destination:/queue/trade
content-type:application/json
content-length:44

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

在执行之后,服务器可以_b 广播交易确认消息并向 client 详细说明。

目的地的含义在 STOMP 规范中故意保持不透明。它可以是任何 string,完全取决于 STOMP 服务器来定义它们支持的目标的语义和语法。然而,非常常见的是,目标是 path-like strings,其中/topic/..暗示 publish-subscribe(one-to-many),/queue/暗示 point-to-point(one-to-one)消息交换。

STOMP 服务器可以使用MESSAGE命令向所有订户 broadcast 消息。以下 example 显示了服务器向订阅的 client 发送股票报价:

1
2
3
4
5
6
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

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

服务器无法发送未经请求的消息。来自服务器的所有消息必须响应特定的 client 订阅,并且服务器消息的subscription-id标头必须 match client 订阅的id标头。

前面的概述旨在提供对 STOMP 协议的最基本的了解。我们建议全面审查协议规格

优点

使用 STOMP 作为 sub-protocol 可以让 Spring Framework 和 Spring Security 提供更丰富的编程模型,而不是使用原始 WebSockets。关于 HTTP 与原始 TCP 以及如何让 Spring MVC 和其他 web 框架提供丰富的功能,可以做出同样的观点。以下是一系列好处:

  • 无需发明自定义消息传递协议和消息格式。
  • STOMP clients,包括 Spring Framework 中的Java client,可用。
  • 您可以(可选)使用消息代理(例如 RabbitMQ,ActiveMQ 等)来管理订阅和 broadcast 消息。
  • Application 逻辑可以组织在任意数量的@Controller实例中,并且可以根据 STOMP 目标标头将消息路由到它们,而不是使用单个WebSocketHandler处理给定连接的原始 WebSocket 消息。
  • 您可以使用 Spring Security 来保护基于 STOMP 目标和消息类型的消息。

启用STOMP

spring-messagingspring-websocket模块中提供了对 WebSocket 支持的 STOMP。一旦拥有了这些依赖项,就可以通过 WebSocket 使用SockJS 后备公开 STOMP endpoints,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)客户端为 WebSocket 握手需要连接的端点的 HTTP URL。
2 目标头以/app开头的 STOMP 消息将路由到@Controller classes 中的@MessageMapping方法。
3 使用 built-in message broker 进行订阅,并使用 broadcasting 和 route 将目标标题以/topic/queue开头的消息发送到 broker。

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

对于 built-in simple broker,/topic/queue前缀没有任何特殊含义。它们只是区分 pub-sub 与 point-to-point 消息传递的惯例(即许多订阅者与一个消费者)。当您使用外部 broker 时,请检查 broker 的 STOMP 页面以了解它支持的 STOMP 目标和前缀类型。

要从浏览器连接,对于 SockJS,您可以使用sockjs-client。对于 STOMP,许多 applications 使用了jmesnil/stomp-websocket library(也称为 stomp.js),这是 feature-complete 并且已经在 production 中使用了多年但不再维护。目前,JSteunou/webstomp-client是该图书馆中最积极维护和不断发展的继承者。以下 example code 基于它:

1
2
3
4
5
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

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

或者,如果通过 WebSocket 连接(没有 SockJS),则可以使用以下 code:

1
2
3
4
5
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

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

请注意,前面的 example 中的stompClient不需要指定loginpasscode headers。即使它确实如此,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的详细信息,请参阅连接到 Broker认证

有关更多 example code,请参阅:

WebSocket服务器

要配置基础 WebSocket 服务器,服务器 Configuration中的信息适用。对于 Jetty,您需要通过StompEndpointRegistry设置HandshakeHandlerWebSocketPolicy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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)));
}
}

消息流

一旦暴露了 STOMP 端点,Spring application 就成为连接的 clients 的 STOMP broker。本节介绍服务器端的消息流。

spring-messaging模块包含对源自Spring Integration的消息传递应用程序的基础支持,后来被提取并合并到 Spring Framework 中,以便在许多Spring 项目和 application 场景中得到更广泛的使用。以下列表简要介绍了一些可用的消息传递抽象:

Java configuration(即@EnableWebSocketMessageBroker)和 XML 名称空间 configuration(即<websocket:message-broker>)都使用前面的组件来组合消息工作流。下图显示了启用简单 built-in 消息 broker 时使用的组件:

消息流简单 broker

上图显示了三个消息 channels:

  • clientInboundChannel:用于传递从 WebSocket clients 收到的消息。
  • clientOutboundChannel:用于将服务器消息发送到 WebSocket clients。
  • brokerChannel:用于从 server-side application code 中发送消息 broker。

下图显示了配置外部 broker(例如 RabbitMQ)以管理订阅和 broadcasting 消息时使用的组件:

消息流 broker relay

前两个图的主要区别在于使用“broker relay”将消息通过 TCP 传递到外部 STOMP broker,并将消息从 broker 传递到订阅的 clients。

当从 WebSocket 连接接收消息时,它们被解码为 STOMP 帧,变成 Spring Message表示,并发送到clientInboundChannel以进行进一步处理。对于 example,目标 headers 以/app开头的 STOMP 消息可以路由到带注释的控制器中的@MessageMapping方法,而/topic/queue消息可以直接路由到消息 broker。

处理来自 client 的 STOMP 消息的带注释的@Controller可以通过brokerChannel向消息 broker 发送消息,broker 通过clientOutboundChannel将消息广播给匹配的订阅者。同一个控制器也可以响应 HTTP 请求,因此 client 可以执行 HTTP POST,然后@PostMapping方法可以向消息 broker 发送消息以 broadcast 发送给订阅的 clients。

我们可以通过一个简单的 example 跟踪流程。考虑以下 example,它设置服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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;
}

}

前面的 example 支持以下流程:

  • client 连接到http://localhost:8080/portfolio,并且一旦建立 WebSocket 连接,STOMP 帧就开始在其上流动。
  • client 发送一个 SUBSCRIBE 帧,其目标头为/topic/greeting。收到并解码后,消息将发送到clientInboundChannel,然后路由到消息 broker,log 存储 client 订阅。
  • client 将_SEND 帧发送到/app/greeting/app前缀有助于将其路由到带注释的控制器。删除/app前缀后,目标的剩余/greeting部分将映射到GreetingController中的@MessageMapping方法。
  • GreetingController返回的 value 变为 Spring Message,其有效负载基于 return value 和/topic/greeting的默认目标头(从输入目标派生,/app替换为/topic)。生成的消息将发送到brokerChannel并由消息 broker 处理。
  • 消息 broker 找到所有匹配的订阅者,并通过clientOutboundChannel向每个订阅者发送一个 MESSAGE 帧,消息被编码为 STOMP 帧并在 WebSocket 连接上发送。

下一节提供了有关注释方法的更多详细信息,包括支持的 arguments 和 return 值的类型。

带注解的控制器

Applications 可以使用带注释的@Controller classes 来处理来自 clients 的消息。这样的 classes 可以声明@MessageMapping@SubscribeMapping@ExceptionHandler方法,如以下主题中所述:

MessageMapping注解

您可以使用@MessageMapping来注释根据目的地 route 消息的方法。方法 level 和 level 类型都支持它。在 level 类型中,@MessageMapping用于表示控制器中所有方法的共享映射。

默认情况下,映射值为 Ant-style 路径模式(对于 example /thing*/thing/**),包括对模板变量的支持(对于 example,/thing/{id})。可以通过@DestinationVariable method arguments 引用这些值。 Applications 还可以切换到映射的 dot-separated 目标约定,如点作为分隔符中所述。

支持的方法Arguments

以下 table 描述了方法 arguments:

方法论证 描述
Message 用于访问完整的消息。
MessageHeaders 用于访问Message中的 headers。
MessageHeaderAccessorSimpMessageHeaderAccessorStompHeaderAccessor 用于通过类型化访问器方法访问 headers。
@Payload 用于访问消息的有效负载,由已配置的MessageConverter转换(对于 example,来自 JSON)。 不需要此 annotation 的存在,因为默认情况下,假设没有其他参数匹配。 您可以使用@javax.validation.Valid或 Spring 的@Validated注释有效负载 arguments,以自动验证有效负载 arguments。
@Header 用于访问特定标头 value - 以及使用org.springframework.core.convert.converter.Converter进行类型转换(如有必要)。
@Headers 用于访问消息中的所有 headers。此参数必须可分配给java.util.Map
@DestinationVariable 用于访问从消息目标中提取的模板变量。根据需要将值转换为声明的方法参数类型。
java.security.Principal 反映在 WebSocket HTTP 握手的 time 登录的用户。
Return值

默认情况下,@MessageMapping方法的 return value 通过匹配MessageConverter序列化为有效负载,并作为Message发送到brokerChannel,从_b 广播到订阅者。出站消息的目的地与入站消息的目的地相同,但前缀为/topic

您可以使用@SendTo@SendToUser 注释来自定义输出消息的目标。 @SendTo用于自定义目标目标或指定多个目标。 @SendToUser用于将输出消息仅定向到与输入消息关联的用户。见用户目的地

您可以在同一个方法上同时使用@SendTo@SendToUser,并且 class level 都支持它们,在这种情况下它们作为 class 中方法的默认值。但是,请记住,任何 method-level @SendTo@SendToUser 注释都会覆盖 class level 上的任何此类注释。

消息可以异步处理,@MessageMapping方法可以_ret ,CompletableFutureCompletionStage

请注意,@SendTo@SendToUser仅仅是一种便利,相当于使用SimpMessagingTemplate发送消息。如有必要,对于更高级的方案,@MessageMapping方法可以直接使用SimpMessagingTemplate。这可以代替返回 value,或者可能另外返回 value。见发送消息

SubscribeMapping注解

@SubscribeMapping类似于@MessageMapping,但仅将映射缩小为订阅消息。它支持与@MessageMapping相同的方法 arguments。但是对于 return value,默认情况下,消息会直接发送到 client(通过clientOutboundChannel,以响应订阅)而不是 broker(通过brokerChannel,作为 broadcast 到匹配的订阅)。添加@SendTo@SendToUser会覆盖此行为并发送到 broker。

什么时候有用?假设 broker 映射到/topic/queue,而 application 控制器映射到/app。在此设置中,broker store 对/topic/queue的所有订阅都用于重复广播,并且不需要涉及 application。 client 也可以订阅某个/app目的地,并且控制器可以 return 一个 value 以响应该订阅而不涉及 broker 而不再存储或使用订阅(实际上是 one-time request-reply 交换)。一个用例是在启动时使用初始数据填充 UI。

这什么时候没用?不要尝试将 broker 和控制器 map 映射到相同的目标前缀,除非您由于某种原因希望两者都独立处理消息,包括订阅。入站消息在 parallel 中处理。无法保证 broker 或控制器是否首先处理给定的消息。如果在存储订阅并准备好广播时通知目标,则 client 应该在服务器支持时询问收据(简单的 broker 不支持)。对于 example,使用 Java STOMP client,您可以执行以下操作来添加收据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});

服务器端选项是brokerChannel上的注册ExecutorChannelInterceptor,并实现在处理完消息(包括订阅)后调用的afterMessageHandled方法。

MessageExceptionHandler注解

application 可以使用@MessageExceptionHandler方法来处理@MessageMapping方法的 exceptions。如果要访问 exception 实例,可以在 annotation 本身或通过方法参数声明 exceptions。以下 example 通过方法参数声明 exception:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class MyController {

// ...

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

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

通常,@MessageExceptionHandler方法适用于声明它们的@Controller class(或 class 层次结构)。如果您希望此类方法更全局地应用(跨控制器),则可以在标有@ControllerAdvice的 class 中声明它们。这与 Spring MVC 中可用的类似的支持相当。

发送消息

如果要从 application 的任何部分向连接的 clients 发送消息,该怎么办?任何 application component 都可以向brokerChannel发送消息。最简单的方法是 inject SimpMessagingTemplate并使用它来发送消息。通常,您可以按类型对其进行注入,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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,您也可以通过 name(brokerMessagingTemplate)限定它。

简单的Broker

built-in 简单消息 broker 处理来自 clients 的订阅请求,将它们存储在 memory 中,并将消息广播到具有匹配目标的已连接客户端。 broker 支持 path-like 目的地,包括对 Ant-style 目的地模式的订阅。

Applications 也可以使用 dot-separated(而不是 slash-separated)目的地。见点作为分隔符

如果配置了任务调度程序,则简单的 broker 支持STOMP 心跳。为此,您可以声明自己的调度程序或使用在内部自动声明和使用的调度程序。以下 example 显示了如何声明自己的调度程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private TaskScheduler messageBrokerTaskScheduler;

@Autowired
public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);

// ...
}
}

外部Broker

简单的 broker 非常适合入门,但仅支持 STOMP 命令的子集(它不支持 ack,收据和其他一些 features),依赖于简单的 message-sending 循环,不适合群集。作为替代方案,您可以升级 applications 以使用 full-featured 消息 broker。

请参阅 STOMP 文档以获取您选择的消息 broker(例如RabbitMQActiveMQ和其他),安装 broker,并在启用 STOMP 支持的情况下运行它。然后,您可以在 Spring configuration 中启用 STOMP broker 中继(而不是简单的 broker)。

以下 example configuration 启用 full-featured broker:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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");
}

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

前面的 configuration 中的 STOMP broker 中继是一个 Spring MessageHandler,它通过将消息转发到外部消息 broker 来处理消息。为此,它建立与 broker 的 TCP 连接,将所有消息转发给它,然后通过其 WebSocket 会话将从 broker 接收的所有消息转发给 clients。从本质上讲,它充当“转发”,可以在两个方向上转发消息。

io.projectreactor.netty:reactor-nettyio.netty:netty-all依赖项添加到项目以进行 TCP 连接管理。

此外,application 组件(例如 HTTP 请求处理方法,业务服务等)也可以向 broker 中继发送消息,如发送消息中所述,将 broadcast 消息发送到订阅的 WebSocket clients。

实际上,broker 中继实现了健壮且可扩展的消息广播。

连接到Broker

STOMP broker 中继维护与 broker 的单个“系统”TCP 连接。此连接仅用于源自 server-side application 的消息,而不用于接收消息。您可以为此连接配置 STOMP 凭据(即 STOMP 帧loginpasscode headers)。这在 XML 命名空间和 Java configuration 中都显示为systemLoginsystemPasscode properties,默认值为guestguest

STOMP broker 中继还为每个连接的 WebSocket client 创建单独的 TCP 连接。您可以配置用于代表 clients 创建的所有 TCP 连接的 STOMP 凭据。这在 XML 命名空间和 Java configuration 中都公开为clientLogin and clientPasscode properties with default values of guest and guest`。

STOMP broker 中继始终在每个CONNECT帧上设置loginpasscode headers,它们代表 clients 转发给 broker。因此,WebSocket clients 不需要设置那些 headers。他们被忽略了。正如认证部分所解释的那样,WebSocket clients 应该依赖 HTTP 身份验证来保护 WebSocket 端点并建立 client 身份。

STOMP broker 中继还通过“系统”TCP 连接向消息 broker 发送和接收心跳。您可以配置发送和接收心跳的间隔(默认情况下每个 10 秒)。如果与 broker 的连接丢失,broker 中继将继续尝试每 5 秒重新连接一次,直到成功为止。

任何 Spring bean 都可以实现ApplicationListener<BrokerAvailabilityEvent>以在与 broker 的“系统”连接丢失和 re-established 时接收通知。例如,广播股票报价的股票报价服务可以在没有 active“系统”连接时停止尝试发送消息。

默认情况下,STOMP broker 中继始终连接,并在连接丢失时根据需要重新连接到同一个 host 和 port。如果您希望提供多个地址,则在每次尝试连接时,您都可以配置地址供应商,而不是固定的 host 和 port。以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

// ...

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

private ReactorNettyTcpClient<byte[]> createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}

您还可以使用virtualHost property 配置 STOMP broker 中继。此 property 的 value 被设置为每个CONNECT帧的host标头,并且可能很有用(例如,在云环境中,建立 TCP 连接的实际 host 与提供 cloud-basedSTOMP 服务的 host 不同)。

点作为分隔符

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

以下 example 显示了如何在 Java configuration 中执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

// ...

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

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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


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


</beans>

之后,控制器可以使用点(.)作为@MessageMapping方法中的分隔符,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
@Controller
@MessageMapping("erd")
public class RedController {

@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}

client 现在可以向/app/red.blue.green123发送消息。

在前面的示例中,我们没有更改“broker relay”上的前缀,因为这些前缀完全依赖于外部消息 broker。请参阅您使用的 broker 的 STOMP 文档页面,以查看它为目标标头支持的约定。

另一方面,“simple broker”依赖于配置的PathMatcher,因此,如果切换分隔符,则该更改也适用于 broker 以及 broker 将消息中的目标与预订中的模式匹配的方式。

认证

WebSocket 消息传递 session 上的每个 STOMP 都以 HTTP 请求开头。这可以是升级到 WebSockets 的请求(即 WebSocket 握手),或者在 SockJS 回退的情况下,是一系列 SockJS HTTP 传输请求。

许多 web applications 已经具有用于保护 HTTP 请求的身份验证和授权。通常,通过使用某种机制(如登录页面,HTTP 基本身份验证或其他方式)通过 Spring Security 对用户进行身份验证。经过身份验证的用户的 security context 保存在 HTTP session 中,并与同一 cookie-based session 中的后续请求相关联。

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求,通常已经通过HttpServletRequest#getUserPrincipal()可访问经过身份验证的用户。 Spring 会自动将该用户与为其创建的 WebSocket 或 SockJS session 相关联,然后通过用户标头与通过该 session 传输的所有 STOMP 消息相关联。

简而言之,一个典型的 web application 除了它已经为安全做的事情之外什么都不做。用户在 HTTP 请求 level 上通过安全 context 进行身份验证,该安全 context 通过 cookie-based HTTP session 进行维护(然后与为该用户创建的 WebSocket 或 SockJS 会话相关联),并导致每个Message上标记的用户标头流经应用。

请注意,STOMP 协议在CONNECT帧上确实有loginpasscode headers。这些最初设计用于并且仍然需要,例如,用于 TCP 上的 STOMP。但是,对于 STOMP over WebSocket,默认情况下,Spring 忽略 STOMP 协议 level 上的授权 headers,假定用户已经在 HTTP 传输 level 上进行了身份验证,并期望 WebSocket 或 SockJS session 包含经过身份验证的用户。

Spring Security 提供了WebSocket sub-protocol 授权,它使用ChannelInterceptor根据其中的用户头来授权消息。此外,Spring Session 提供了一个WebSocket integration,确保当 WebSocket session 仍处于 active 时,用户 HTTP session 不会过期。

令牌认证

Spring Security OAuth提供对基于令牌的安全性的支持,包括 JSON Web Token(JWT)。您可以将此作为 Web applications 中的身份验证机制,包括 STOMP over WebSocket 交互,如上一节所述(即通过 cookie-based session 维护身份)。

在同一时间,cookie-based 会话并不总是最合适的(例如,在 applications 中不保持 server-side session 或在移动应用程序中,common 使用 headers 进行身份验证)。

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

上述限制适用于 browser-based clients,不适用于 Spring Java-based STOMP client,它支持使用 WebSocket 和 SockJS 请求发送 headers。

因此,希望避免使用 cookies 的应用程序可能没有任何好的替代方法来进行 HTTP 协议 level 的身份验证。他们可能更喜欢在 STOMP 消息传递协议中使用 headers 进行身份验证,而不是使用 cookies。这样做需要两个简单的步骤:

  • 使用 STOMP client 在 connect time 传递 authentication headers。
  • 使用ChannelInterceptor处理身份验证 headers。

下一个 example 使用 server-side configuration 来注册自定义身份验证拦截器。请注意,拦截器只需要在 CONNECT Message上进行身份验证并设置用户头。 Spring 记录并保存经过身份验证的用户,并将其与同一 session 上的后续 STOMP 消息相关联。以下 example 显示了如何注册自定义身份验证拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 之前排序身份验证ChannelInterceptor config。最好通过在WebSocketMessageBrokerConfigurer标记为@Order(Ordered.HIGHEST_PRECEDENCE + 99)的 implementation implementation 中声明自定义拦截器来完成。

用户目的地

application 可以发送针对特定用户的消息,并且 Spring 的 STOMP 支持可以识别以/user/为前缀的目标。对于 example,client 可能订阅/user/queue/position-updates目标。此目标由UserDestinationMessageHandler处理并转换为用户 session 唯一的目标(例如/queue/position-updates-user123)。这提供了订阅一般命名目的地的便利,同时在同一时间确保不与订阅相同目的地的其他用户发生冲突,以便每个用户可以接收唯一的库存位置更新。

在发送方,消息可以被发送到诸如/user/{username}/queue/position-updates的目的地,而/user/{username}/queue/position-updates又由UserDestinationMessageHandler转换成一个或多个目的地,一个用于与用户相关联的每个 session。这使得 application 中的任何 component 都可以发送针对特定用户的消息,而不必了解其 name 和通用目标。通过 annotation 和消息传递模板也支持此功能。

message-handling 方法可以向通过@SendToUser annotation 处理的消息关联的用户发送消息(class-level 也支持共享 common 目的地),如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class PortfolioController {

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

如果用户具有多个 session,则默认情况下,订阅给定目标的所有会话都是目标。但是,有时可能需要仅定位发送正在处理的消息的 session。您可以通过将broadcast属性设置为 false 来实现,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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 session 可以订阅用户目标。在这种情况下,@SendToUser annotation 的行为与broadcast=false完全相同(即,仅定位发送正在处理的消息的 session)。

您可以从任何 application component 向用户目标发送消息,例如,注入由 Java configuration 或 XML 命名空间创建的SimpMessagingTemplate。 (如果需要@Qualifier .),则 bean name 为"brokerMessagingTemplate"以下 example 显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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());
}
}

当您将用户目标与外部消息 broker 一起使用时,应检查 broker 文档,了解如何管理非活动队列,以便在用户 session 结束时删除所有唯一用户队列。对于 example,当您使用/exchange/amq.direct/position-updates等目标时,RabbitMQ 会创建 auto-delete 队列。因此,在这种情况下,client 可以订阅/user/exchange/amq.direct/position-updates。同样,ActiveMQ 有configuration 选项用于清除非活动目标。

在 multi-application 服务器方案中,用户目标可能仍未解析,因为用户已连接到其他服务器。在这种情况下,您可以将目标配置为 broadcast 未解析的消息,以便其他服务器有机会尝试。这可以通过 Java configuration 中MessageBrokerRegistryuserDestinationBroadcast property 和 XML 中message-broker元素的user-destination-broadcast属性来完成。

消息顺序

来自 broker 的消息将发布到clientOutboundChannel,从而将它们写入 WebSocket 会话。由于 channel 由ThreadPoolExecutor支持,因此消息在不同的线程中处理,client 接收的结果序列可能不匹配发布的确切 order。

如果这是一个问题,请启用setPreservePublishOrder flag,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker preserve-publish-order="true">
<!-- ... -->
</websocket:message-broker>

</beans>

设置 flag 时,同一 client session 中的消息将在 time 发布到clientOutboundChannel,以便保证发布的 order。请注意,这会产生很小的性能开销,因此只有在需要时才应启用它。

Events

发布了几个ApplicationContext events,可以通过实现 Spring 的ApplicationListener接口来接收:

  • BrokerAvailabilityEvent:表示 broker 何时可用或不可用。虽然“简单”broker 在启动时立即可用,并且在 application 运行时仍然如此,但 STOMP“broker relay”可能会失去与全功能 broker 的连接(对于 example,如果 broker 重新启动)。 broker 中继重新连接逻辑,re-establishes 与 broker 的“系统”连接返回时。因此,只要 state 从连接变为 disconnected 和 vice-versa,就会发布此 event。使用SimpMessagingTemplate的组件应订阅此 event,并避免在 broker 不可用时发送消息。无论如何,他们应该准备好在发送消息时处理MessageDeliveryException
  • SessionConnectEvent:收到新的 STOMP CONNECT 时发布,表示新 client session 的开始。 event 包含表示连接的消息,包括 session ID,用户信息(如果有)以及 client 发送的任何自定义 headers。这对于跟踪 client 会话非常有用。订阅此 event 的组件可以使用SimpMessageHeaderAccessorStompMessageHeaderAccessor包装所包含的消息。
  • SessionConnectedEvent:当 broker 发送 STOMP CONNECTED 帧以响应 CONNECT 时后不久发布。此时,可以认为 STOMP session 已完全建立。
  • SessionSubscribeEvent:收到新的 STOMP SUBSCRIBE 时发布。
  • SessionUnsubscribeEvent:收到新的 STOMP UNSUBSCRIBE 时发布。
  • SessionDisconnectEvent:在 STOMP session ends 时发布。 DISCONNECT 可能已从 client 发送,或者可能在 WebSocket session 关闭时自动生成。在某些情况下,每个 session 都会多次发布此 event。对于多个 disconnect events,组件应该是幂等的。

当您使用 full-featured broker 时,如果 broker 暂时不可用,则 STOMP“broker relay”会自动重新连接“系统”连接。但是,Client 连接不会自动重新连接。假设启用了心跳,client 通常会注意到 broker 在 10 秒内没有响应。 Clients 需要实现自己的重新连接逻辑。

Interceptor

Events提供 STOMP 连接生命周期的通知,但不提供每个 client 消息的通知。 Applications 还可以注册ChannelInterceptor来拦截任何消息以及处理链的任何部分。以下 example 显示了如何拦截来自 clients 的入站邮件:

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

自定义ChannelInterceptor可以使用StompHeaderAccessorSimpMessageHeaderAccessor来访问有关邮件的信息,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
public class MyChannelInterceptor implements ChannelInterceptor {

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

Applications 也可以实现ExecutorChannelInterceptor,这是一个 sub-interface ChannelInterceptor,在处理消息的线程中有回调。虽然为发送到 channel 的每条消息调用ChannelInterceptor一次,但ExecutorChannelInterceptor在每个MessageHandler的线程中提供了挂钩,这些挂钩订阅了来自 channel 的消息。

请注意,与前面描述的SesionDisconnectEvent一样,DISCONNECT 消息可以来自 client,也可以在 WebSocket session 关闭时自动生成。在某些情况下,拦截器可能会为每个 session 多次拦截此消息。对于多个 disconnect events,组件应该是幂等的。

STOMP-Client

Spring 通过 WebSocket client 提供 STOMP,通过 TCP client 提供 STOMP。

首先,您可以创建和配置WebSocketStompClient,如下面的 example 所示:

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

在前面的 example 中,您可以将StandardWebSocketClient替换为SockJsClient,因为它也是WebSocketClient的 implementation。 SockJsClient可以使用 WebSocket 或 HTTP-based transport 作为后备。有关更多详细信息,请参阅SockJsClient

接下来,您可以建立连接并为 STOMP session 提供处理程序,如下面的示例所示:

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

当 session 准备好使用时,将通知处理程序,如下面的 example 所示:

1
2
3
4
5
6
7
public class MyStompSessionHandler extends StompSessionHandlerAdapter {

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

建立 session 后,可以发送任何有效负载并使用配置的MessageConverter进行序列化,如下面的 example 所示:

1
session.send("/topic/something", "payload");

您也可以订阅目的地。 subscribe方法需要处理订阅消息的处理程序,并返回一个可用于取消订阅的Subscription句柄。对于每个收到的消息,处理程序可以指定有效负载应反序列化的目标Object类型,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
session.subscribe("/topic/something", new StompFrameHandler() {

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

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

});

要启用 STOMP 心跳,可以使用TaskScheduler配置WebSocketStompClient,并可选择自定义心跳间隔(写入不活动 10 秒,导致心跳发送,读取不活动 10 秒,关闭连接)。

当您使用WebSocketStompClient进行 performance 测试来模拟来自同一台计算机的数千个 clients 时,请考虑关闭心跳,因为每个连接都会安排自己的心跳任务,并且不会针对同一台计算机上的大量客户端进行优化。

STOMP 协议还支持收据,其中 client 必须添加receipt标头,服务器在处理发送或订阅后用 RECEIPT 帧响应。为了支持这一点,StompSession提供setAutoReceipt(boolean),导致在每个后续发送或订阅 event 上添加receipt标头。或者,您也可以手动将收据标题添加到StompHeaders。发送和订阅 return 都会返回Receiptable的实例,您可以使用它来注册接收成功和失败回调。对于此 feature,您必须在收据到期前(默认为 15 秒)为 client 配置TaskScheduler和 time 的数量。

注意StompSessionHandler本身是一个StompFrameHandler,它允许它处理 ERROR 帧以及handleException回调来处理消息的 exceptions 和handleTransportError来处理 transport-level 错误,包括ConnectionLostException

WebSocket范围

每个 WebSocket session 都有一个属性 map。 map 作为标题附加到入站 client 消息,可以从控制器方法访问,如下面的示例所示:

1
2
3
4
5
6
7
8
9
@Controller
public class MyController {

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

您可以在websocket范围内声明 Spring-managed bean。您可以 inject WebSocket-scoped beans 到控制器和clientInboundChannel上注册的任何 channel 拦截器。这些通常是单身,比任何单独的 WebSocket session 都更长寿。因此,您需要为 WebSocket-scoped beans 使用范围代理模式,如下面的 example 所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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 在从控制器访问第一个 time 时初始化一个新的MyBean实例,并在 WebSocket session 属性中存储实例。随后返回相同的实例,直到 session ends。 WebSocket-scoped beans 调用了所有 Spring 生命周期方法,如前面的示例所示。

性能

在绩效方面没有灵丹妙药。许多因素会影响它,包括消息的大小和数量,application 方法是否执行需要阻塞的工作,以及外部因素(如网络速度和其他问题)。本节的目标是提供可用 configuration 选项的概述以及有关如何推理扩展的一些想法。

在消息传递 application 中,消息通过 channels 传递给由线程池支持的异步执行。配置这样的 application 需要充分了解 channels 和消息流。因此,建议审核消息流

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

如果注释方法中的消息处理主要是 CPU-bound,则clientInboundChannel的线程数应保持接近处理器数。如果他们所做的工作更多 IO-bound 并且需要阻塞或等待数据库或其他外部系统,则可能需要增加线程池大小。

ThreadPoolExecutor有三个重要的 properties:核心线程池大小,最大线程池大小,以及队列为 store 任务没有可用线程的任务的容量。

一个常见的混淆点是,配置核心池大小(对于 example,10)和最大池大小(对于 example,20)会导致一个具有 10 到 20 个线程的线程池。实际上,如果容量保留为 Integer.MAXVALUE 的默认值_,则线程池永远不会超出核心池大小,因为所有其他任务都排队。

请参阅ThreadPoolExecutor的 javadoc 以了解这些 properties 如何工作并理解各种排队策略。

clientOutboundChannel方面,它是关于向 WebSocket clients 发送消息的全部内容。如果 clients 位于快速网络上,则线程数应保持接近可用处理器的数量。如果它们很慢或带宽较低,则消耗消息所需的时间会更长,并给线程池带来负担。因此,增加线程池大小变得必要。

虽然clientInboundChannel的工作负载可以预测 - 毕竟,它基于 application 的作用 - 如何配置“clientOutboundChannel”更难,因为它基于 application 无法控制的因素。因此,另外两个 properties 与发送消息有关:sendTimeLimitsendBufferSizeLimit。您可以使用这些方法配置允许发送 long 的时间以及向 client 发送消息时可以缓冲的数据量。

一般 idea 是,在任何给定的 time,只有一个线程可用于发送到 client。同时,所有其他消息都会被缓冲,您可以使用这些 properties 来决定如何允许发送消息的长度以及在此期间可以缓冲多少数据。有关重要的其他详细信息,请参阅 XML schema 的 javadoc 和文档。

以下 example 显示了可能的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

// ...

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

您还可以使用前面显示的 WebSocket 传输 configuration 配置传入 STOMP 消息的最大允许大小。理论上,WebSocket 消息的大小几乎是无限的。在实践中,WebSocket 服务器强加限制 - 例如,Tomcat 上的 8K 和 Jetty 上的 64K。出于这个原因,STOMP clients(例如 JavaScript webstomp-client和其他)在 16K 边界处拆分较大的 STOMP 消息,并将它们作为多个 WebSocket 消息发送,这需要服务器缓冲和 re-assemble。

Spring 的 STOMP-over-WebSocket 支持执行此操作,因此 applications 可以配置 STOMP 消息的最大大小,而不管 WebSocket server-specific 消息大小如何。请记住,如有必要,WebSocket 消息大小会自动调整,以确保它们至少可以携带 16K WebSocket 消息。

以下 example 显示了一个可能的 configuration:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

// ...

}

以下 example 显示了前面的 example 的 XML configuration 等效项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">

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

</beans>

关于扩展的一个重要问题涉及使用多个 application 实例。目前,你不能用简单的 broker 做到这一点。但是,当您使用 full-featured broker(例如 RabbitMQ)时,每个 application 实例都连接到 broker,而来自一个 application 实例的消息 broadcast 可以通过 broker broadcast 到通过任何其他 application 实例连接的 WebSocket clients。

监控

当您使用@EnableWebSocketMessageBroker<websocket:message-broker>时, key 基础结构组件会自动收集统计信息和计数器,以便对 application 的内部 state 提供重要的信息。 configuration 还声明一个类型的 bean,它在一个地方收集所有可用信息,默认情况下,每 30 分钟收集一次INFO level。这个 bean 可以通过 Spring 的MBeanExporter导出到 JMX,以便在运行时查看(对于 example,通过 JDK 的jconsole)。以下列表总结了可用信息:

  • Client WebSocket 会话
      • 当前
    • 指示当前有多少个 client 会话,其中计数进一步按 WebSocket 与 HTTP 流式传输和轮询 SockJS 会话进行细分。
    • 表示已建立的会话总数。
  • 异常关闭
      • 连接失败
    • 已建立但在 60 秒内未收到任何消息后关闭的会话。这通常表示代理或网络问题。
  • 超出发送限制
    • 超过配置的发送超时或发送缓冲区限制后,会话关闭,这可能发生在慢客户端(请参阅上一节)。
  • 运输错误
    • 传输错误后会话关闭,例如无法读取或写入 WebSocket 连接或 HTTP 请求或响应。
  • STOMP 框架
    • 处理的 CONNECT,CONNECTED 和 DISCONNECT 帧的总数,表示 STOMP level 上连接了多少个 clients。请注意,当会话异常关闭或 clients 关闭而不发送 DISCONNECT 帧时,DISCONNECT 计数可能会更低。
  • STOMP Broker Relay
      • TCP 连接
    • 表示为 broker 建立了代表 client WebSocket 会话的 TCP 连接数。这应该等于 client WebSocket 会话的数量 1 个额外的共享“系统”连接,用于从 application 中发送消息。
  • STOMP 框架
    • 代表 clients 转发到 broker 或从 broker 接收的 CONNECT,CONNECTED 和 DISCONNECT 帧的总数。请注意,无论 client WebSocket session 如何关闭,都会向 broker 发送 DISCONNECT 帧。因此,较低的 DISCONNECT 帧计数表示 broker 是 pro-actively 关闭连接(可能是因为心跳未在 time 到达,无效的输入帧或其他问题)。
  • Client Inbound Channel
    • 来自支持clientInboundChannel的线程池的统计信息,用于深入了解传入消息处理的运行状况。在此排队的任务表明 application 可能太慢而无法处理消息。如果有 I/O 绑定任务(对于 example,慢速数据库查询,对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。
  • Client Outbound Channel
    • 来自支持clientOutboundChannel的线程池的统计信息,可以深入了解 broadcasting 消息到 clients 的运行状况。在这里排队的任务表明客户端太慢而无法使用消息。解决此问题的一种方法是增加线程池大小以适应预期的并发慢客户端数。另一种选择是减少发送超时和发送缓冲区大小限制(参见上一节)。
  • SockJS 任务计划程序
    • 来自用于发送心跳的 SockJS 任务调度程序的线程池的统计信息。请注意,在 STOMP level 上协商心跳时,将禁用 SockJS 心跳。

测试

当您使用 Spring 的 STOMP-over-WebSocket 支持时,有两种主要的方法来测试 applications。第一种是编写 server-side 测试来验证控制器及其带注释的 message-handling 方法的功能。第二个是编写完整的 end-to-end 测试,涉及运行 client 和服务器。

这两种方法并不相互排斥。相反,每个人都在整体测试策略中占有一席之地。 Server-side 测试更集中,更容易编写和维护。另一方面,End-to-end integration 测试更完整,测试更多,但它们也更多地参与编写和维护。

最简单的 server-side 测试形式是编写控制器单元测试。但是,这还不够用,因为控制器的大部分功能取决于它的注释。纯单元测试根本无法测试。

理想情况下,测试中的控制器应该在运行时调用,就像测试使用 Spring MVC Test framework 处理 HTTP 请求的控制器的方法一样 - 也就是说,没有 running Servlet 容器但是依赖于 Spring Framework 来调用带注释的控制器。与 Spring MVC Test 一样,这里有两个可能的替代方案,使用“context-based”或使用“独立”设置:

  • 在 Spring TestContext framework,inject clientInboundChannel作为测试字段的帮助下加载实际的 Spring configuration,并使用它来发送要由控制器方法处理的消息。
  • 手动设置调用控制器(即SimpAnnotationMethodMessageHandler)所需的最小 Spring framework 基础结构,并将控制器的消息直接传递给它。

这两种设置方案都在测试股票投资组合 sample application 中进行了演示。

第二种方法是创建 end-to-end integration 测试。为此,您需要以嵌入模式运行 WebSocket 服务器并将其作为 WebSocket client 连接到它,以发送包含 STOMP 帧的 WebSocket 消息。 测试股票投资组合 sample application 还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单 STOMP client 来演示此方法。

其他Web框架

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

Spring Framework 的核心 value 命题之一是支持选择。从一般意义上讲,Spring 并不强迫您使用或购买任何特定的 architecture,技术或方法(尽管它肯定会推荐一些其他的)。这种自由选择与开发人员及其开发团队最相关的 architecture,技术或方法可以说是最明显的 web 区域,其中 Spring 提供了自己的 web framework(Spring MVCSpring WebFlux),而在同一时间,提供与一些流行的第三方 web 框架的整合。

Common配置

在深入研究每个支持的 web framework 的 integration 细节之前,让我们先看一下 Spring configuration,它不是任何一个 web framework 特有的。 (本节同样适用于 Spring 自己的 web framework,Spring MVC.)

Spring 的轻量级 application model 支持的一个概念(想要一个更好的词)是一个分层的 architecture。请记住,在“经典”分层 architecture 中,web 图层只是众多图层中的一个。它充当 server-side application 的入口点之一,并且它委托服务层中定义的服务 objects(Facade)以满足 business-specific(和 presentation-technology 不可知)用例。在 Spring 中,这些服务 objects,任何其他 business-specific objects,data-accessobjects 和其他服务存在于不同的“business context”中,其中不包含 web 或表示层 objects(表示 objects,例如 Spring MVC 控制器,通常以不同的方式配置) “presentation context”)。本节详细介绍了如何配置包含 application 中所有’business beans’的 Spring 容器(WebApplicationContext)。

转到具体细节,您需要做的就是在 web application 的标准 Java EE servlet web.xml文件中声明一个ContextLoaderListener并添加一个contextConfigLocation 部分(在同一个文件中),该部分定义要加载的 Spring XML configuration files 集合。

考虑以下<listener/> configuration:

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

进一步考虑以下<context-param/> configuration:

1
2
3
4
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

如果未指定contextConfigLocation context 参数,则ContextLoaderListener将查找要加载的名为/WEB-INF/applicationContext.xml的文件。加载 context files 后,Spring 会根据 bean 定义创建WebApplicationContext object,并将 store 存储在 web application 的ServletContext中。

所有 Java web 框架都构建在 Servlet API 之上,因此您可以使用以下 code 代码段来访问ContextLoaderListener创建的“business context”ApplicationContext

以下 example 显示了如何获取WebApplicationContext

1
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils class 是为了方便起见,因此您无需记住ServletContext属性的 name。如果WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE key 下不存在 object,则getWebApplicationContext()方法返回null。而不是冒险在 application 中获取NullPointerExceptions,最好使用getRequiredWebApplicationContext()方法。当ApplicationContext缺失时,此方法抛出 exception。

一旦你的 reference,你可以通过 name 或类型检索 beans。大多数开发人员通过 name 检索 beans,然后将它们转换为其实现的接口之一。

幸运的是,本节中的大多数框架都有更简单的查找 beans 的方法。它们不仅可以轻松地从 Spring 容器中获取 beans,而且还允许您在其控制器上使用依赖注入。每个 web framework 部分都有关于其特定 integration 策略的更多细节。

JSF

JavaServer Faces(JSF)是 JCP 的标准 component-based,event-driven web 用户界面 framework。从 Java EE 5 开始,它是 Java EE 保护伞的官方部分。

对于流行的 JSF 运行时以及流行的 JSF component libraries,请查看Apache MyFaces 项目。 MyFaces 项目还提供了 common JSF extensions,例如MyFaces Orchestra(一个提供丰富的会话范围支持的 Spring-based JSF 扩展)。

Spring Web Flow 2.0 通过其新建立的 Spring Faces 模块提供丰富的 JSF 支持,包括 JSF-centric 用法(如本节所述)和 Spring-centric 用法(使用 Spring MVC 调度程序中的 JSF 视图)。有关详细信息,请参阅Spring Web Flow 网站

Spring 的 JSF integration 中的 key 元素是 JSF ELResolver机制。

SpringBeanResolver

SpringBeanFacesELResolver是符合 JSF 1.2 的ELResolver implementation,与 JSF 1.2 和 JSP 2.1 使用的标准 Unified EL 集成。作为SpringBeanVariableResolver,它首先委托 Spring 的“business context”WebApplicationContext,然后委托给底层 JSF implementation 的默认解析器。

Configuration-wise,您可以在 JSF faces-context.xml文件中定义SpringBeanFacesELResolver,如下面的 example 所示:

1
2
3
4
5
6
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>

使用FacesContextUtils

faces-config.xml中将 properties 映射到 beans 时,自定义VariableResolver可以正常工作,但有时,您可能需要显式 grab bean。 这时,FacesContextUtils class 使这很容易。它类似于WebApplicationContextUtils,除了它采用FacesContext参数而不是ServletContext参数。

以下 example 显示了如何使用FacesContextUtils

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

ApacheStruts 2.x

由 Craig McClanahan 发明,支柱是由 Apache 软件基金会主办的 open-source 项目。在 time,它大大简化了 JSP/Servlet 编程范例,赢得了许多使用专有框架的开发人员。它简化了编程 model,它是开源的(因此在啤酒中是免费的),它有一个庞大的社区,让项目在 Java web 开发人员中成长并变得流行。

查看 Struts Spring 插件,了解 Struts 附带的 built-in Spring integration。

ApacheTapestry5.x

Tapestry是一个“Component 为 framework”,用于在 Java 中创建动态,健壮,高度可扩展的 web applications。“

虽然 Spring 有自己的强大的 web 层,但通过使用 Tapestry 用于 web 用户界面和 Spring 容器用于较低层,构建企业 Java 应用程序有许多独特的优势。

有关更多信息,请参阅 Tapestry 的专用Spring 的 integration 模块

更多资源

以下链接转到有关本章所述的各种 web 框架的更多资源。