这部分文档包括对基于 Servlet API 构建并部署到 Servlet 容器的 Servlet 栈中的 Web 应用程序的支持. 各个章节包括Spring MVC,视图技术, 跨域支持 和WebSocket 支持. 对于响应式栈的 Web 应用程序,请参阅 Web on Reactive Stack.
1. Spring Web MVC
Spring Web MVC 是构建在 Servlet API 上的原始 Web 框架,从一开始就包含在 Spring Framework 中.
正式名称 “Spring Web MVC,” 来自其源模块(spring-webmvc
)的名称,但它通常被称为 “Spring MVC”..
与 Spring Web MVC 一样,Spring Framework 5.0 引入了一个响应式栈 Web 框架,其名称 “Spring WebFlux”, 也基于其源模块(spring-webflux
).
本节介绍 Spring Web MVC. 下一节将介绍 Spring WebFlux. .
有关基本信息以及与 Servlet 容器和 Java EE 版本范围的兼容性,请参阅 Spring Framework Wiki.
1.1. DispatcherServlet
Spring MVC 和许多其他 Web 框架一样,围绕前端控制器模式设计,其中核心 DispatcherServlet
为请求处理提供通用的操作,而实际工作由可配置委托组件执行.该模型非常灵活,支持多种工作流程.
DispatcherServlet
与任何 Servlet
一样,需要使用 Java 或 web.xml
配置, 根据 Servlet
规范进行声明和映射.反过来,DispatcherServlet
使用 Spring 配置来发现请求映射,视图解析,异常处理 等等所需的委托组件.
下面的 Java 配置示例注册并初始化 DispatcherServlet
,它由 Servlet 容器自动检测(请参阅Servlet 配置) :
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// Load Spring web application configuration
val context = AnnotationConfigWebApplicationContext()
context.register(AppConfig::class.java)
// Create and register the DispatcherServlet
val servlet = DispatcherServlet(context)
val registration = servletContext.addServlet("app", servlet)
registration.setLoadOnStartup(1)
registration.addMapping("/app/*")
}
}
除了直接使用 ServletContext API 之外,您还可以扩展 AbstractAnnotationConfigDispatcherServletInitializer 并覆盖特定方法(请参阅 上下文层次结构下的示例) .
|
注意:对于编程用例,GenericWebApplicationContext 可以替代 AnnotationConfigWebApplicationContext 。 见
GenericWebApplicationContext
javadoc 了解详细信息。
|
以下 web.xml
配置示例注册并初始化 DispatcherServlet
:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Spring Boot 则有不同的初始化顺序. Spring Boot 使用 Spring 配置来引导自身和嵌入式 Servlet 容器,而不是挂钩到 Servlet 容器的生命周期. 在 Spring 配置中检测 Filter 和 Servlet 声明,并在 Servlet 容器中注册. 有关更多详细信息,请参阅 Spring Boot 文档 .
|
1.1.1. 上下文层次结构
DispatcherServlet
需要一个 WebApplicationContext
(ApplicationContext
的扩展) 来配置自己. WebApplicationContext
有一个指向 ServletContext
的链接以及与之关联的 Servlet
.
它还绑定到 ServletContext
,当需要访问它时,应用程序可以使用 RequestContextUtils
上的静态方法来查找 WebApplicationContext
.
对于许多应用程序,拥有一个简单的 WebApplicationContext
已经足够了. 它也有一个上下文层次结构,其中根 WebApplicationContext
在多个 DispatcherServlet
(或其他 Servlet
) 实例之间共享, 每个实例都有自己的子 WebApplicationContext
配置.
有关上下文层次结构功能的更多信息,请参阅 ApplicationContext
的其他功能.
根 WebApplicationContext
通常包含 bean 基础结构,例如需要跨多个 Servlet
实例共享的数据存储库和业务服务. 这些 bean 被有效继承,可以在特定于 Servlet
的子 WebApplicationContext
中重写(即重新声明) ,它通常包含给定 Servlet
本地的 bean. 下图显示了这种关系:

以下示例配置 WebApplicationContext
层次结构:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>> {
return arrayOf(RootConfig::class.java)
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(App1Config::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/app1/*")
}
}
如果不需要应用程序上下文层次结构,则应用程序可以通过 getRootConfigClasses() 返回所有配置,并从 getServletConfigClasses() 返回 null .
|
以下示例显示了 web.xml
配置(和上面效果一样) :
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
如果不需要应用程序上下文层次结构,则应用程序可以仅配置 “root” 上下文,并将 contextConfigLocation Servlet 参数保留为空.
|
1.1.2. 特殊的 Bean 类型
DispatcherServlet
委托特殊 bean 处理请求并渲染视图. “special beans” 是指实现 WebFlux 框架的 Spring 管理的 Object
实例. 这些通常带有内置联系,但您可以自定义其属性并扩展或替换它们.
下表列出了 DispatcherHandler
检测到的特殊 bean:
Bean 类型 | 说明 |
---|---|
|
将请求映射到处理程序以及用于预处理和后处理的拦截器列表. 其映射规则基于某些标准,其细节因 |
|
无论实际调用处理程序如何,都可以帮助 |
解决异常的策略,他可以将捕获到的异常映射到处理程序,HTML 错误视图或其他目标. 请参阅 |
|
将从处理程序返回的逻辑基于 |
|
解析客户端正在使用的. 以及可能的时区,以便能够提供国际化视图. 请参阅 国际化. |
|
解决 Web 应用程序可以使用的主题 - 例如,提供个性化布局. 见主题. |
|
解析 multi-part 的请求(例如: 浏览器表单文件上载) . 请参阅Multipart 解析器. |
|
存储和检索 |
1.1.3. Web MVC 配置
对于每种类型的 特殊 bean, DispatcherServlet
首先会检查 WebApplicationContext
. 如果没有匹配的 bean 类型,则会退回检查 DispatcherServlet.properties
.
在大多数情况下,MVC 配置是最佳起点. 它以 Java 或 XML 声明所需的 bean,并提供更高级别的配置回调 API 来自定义它.
Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC 并提供许多额外的便捷选项. |
1.1.4. Servlet 配置
在 Servlet 3.0+ 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方法,也可以与 web.xml
文件结合使用. 以下示例注册 DispatcherServlet
:
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("/");
}
}
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
WebApplicationInitializer
是 Spring MVC 提供的一个接口,实现此接口的任何 Servlet 3 容器都可被检测到并自动初始化.
AbstractDispatcherServletInitializer
抽象类实现了 WebApplicationInitializer
接口,通过重写方法来指定 servlet
映射和 DispatcherServlet
配置的地址, 从而更方便的注册 DispatcherServlet
.
对于使用基于 Java 的 Spring 配置的应用程序,建议使用此方法,如以下示例所示:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return arrayOf(MyWebConfig::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
如果使用基于 XML 的 Spring 配置,则应直接从 AbstractDispatcherServletInitializer
扩展,如以下示例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
override fun createRootApplicationContext(): WebApplicationContext? {
return null
}
override fun createServletApplicationContext(): WebApplicationContext {
return XmlWebApplicationContext().apply {
setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
}
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
AbstractDispatcherServletInitializer
还提供了一种便捷的方法来添加 Filter
实例并将它们自动映射到 DispatcherServlet
,如以下示例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
// ...
override fun getServletFilters(): Array<Filter> {
return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
}
}
每个过滤器都根据其具体类型添加默认名称,并自动映射到 DispatcherServlet
.
AbstractDispatcherServletInitializer
的 protected 方法 isAsyncSupported
提供了一个单独的地址来启用 DispatcherServlet
上的异步支持以及映射到它的所有过滤器. 默认情况下,此标志设置为 true
.
最后,如果您需要进一步自定义 DispatcherServlet
本身,则可以覆盖 createDispatcherServlet
方法.
1.1.5. Processing
DispatcherServlet
按如下方式处理请求:
-
首先,搜索应用的上下文对象
WebApplicationContext
,并把它作为一个属性(attribute)绑定到该请求上. 以便让控制器和其他组件能使用它. 属性的键名默认为DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
. -
将 locale resolver 绑定到请求上,并允许其他组件解析处理请求时使用的语言环境(渲染视图,准备数据等) . 如果您不需要区域解析,则不需要 locale resolver.
-
将 theme resolver 绑定到请求,以允许视图等组件确定要使用的 themes. 如果您不使用 themes,则可以忽略它.
-
如果指定 multipart 文件处理器,则会检查请求的文件是不是 multiparts 的, 如果是,请求将包装在
MultipartHttpServletRequest
中, 以便其他组件进一步处理. 有关 Multipart Resolver 的更多信息,请参见 Multipart Resolver. -
为该请求查找一个合适的处理器. 如果找到处理程序,则与该处理器关联的整条执行链(前处理器、后处理器、控制器等) 都会被执行,以完成相应模型的准备或视图的渲染. 或者,对于带注解的控制器,可以显示响应(在 HandlerAdapter 中) 而不是返回视图.
-
如果处理器返回模型,则渲染视图. 如果没有返回模型(可能是由于前处理器或后处理器拦截请求,可能是出于安全原因) ,则不会渲染任何视图,因为该请求可能已经完成.
在 WebApplicationContext
中声明的 HandlerExceptionResolver
用于解决请求处理过程中引发的异常. 这些异常解析程序允许使用自定义的逻辑来解决,有关详细信息,请参阅 异常 .
对于 HTTP 缓存支持,可以使用 WebRequest
的 checkNotModified
方法,以及注解控制器的更多选项,如 控制器的 HTTP 缓存.
您可以自定义通过 DispatcherServlet
的配置. 可以在 web.xml
文件中,声明元素 Servlet 的上添加 Servlet 的初始化参数(init-param
元素) . 下表列出了支持的参数:
参数 | 说明 |
---|---|
|
实现 |
|
一个指定了上下文配置文件路径的字符串,并传递给上下文实例(由 |
|
|
|
当没有找到请求的处理程序时是否抛出 默认情况下,此参数设置为 请注意,如果配置了 默认 servlet 处理 ,则始终将未解析的请求转发到 默认 servlet,并且永远不会引发 404. |
1.1.6. 路径匹配
Servlet API 将完整的请求路径公开为 requestURI
,并进一步将其细分为 contextPath
, servletPath
, and pathInfo
,其值根据 Servlet 的映射方式而有所不同。 根据这些输入,Spring MVC 需要确定查找路径用于处理程序映射,它是 DispatcherServlet
本身映射中的路径,不包括 contextPath
和任何 servletMapping
前缀(如果存在)。
servletPath
和 pathInfo
被解码,这使得它们无法直接与完整的 requestURI
进行比较导出 lookupPath
,因此有必要对 requestURI
进行解码。 然而,这引入了它自己的问题,因为 path 可能包含编码的保留字符,例如 "/"
或 ";"
,这些字符在解码后又会改变路径的结构,这也可能导致安全问题。 此外,Servlet 容器可能会在不同程度上规范化 servletPath
,这使得更不可能执行与 requestURI
的比较 startsWith
。
这就是为什么最好避免依赖基于前缀的 servletPath
映射类型附带的 servletPath
。 如果 DispatcherServlet
被映射为带有 "/"
的默认 Servlet 或者没有带有 "/*"
的前缀并且 Servlet 容器是 4.0+,那么 Spring MVC 能够检测到 Servlet 映射类型并避免完全使用 servletPath
和 pathInfo
。 在 3.1 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过在 MVC 配置中通过 路径匹配 提供具有 alwaysUseFullPath=true
的 UrlPathHelper
来实现等效。
幸运的是,默认的 Servlet 映射 "/"
是一个不错的选择。 但是,仍然存在一个问题,即需要对 requestURI
进行解码,以便与控制器映射进行比较。 这也是不可取的,因为可能会解码改变路径结构的保留字符。 如果这些字符不是预期的,那么您可以拒绝它们(如 Spring Security HTTP 防火墙),或者您可以使用 urlDecode=false
配置 UrlPathHelper
,但控制器映射需要与编码路径匹配,这可能并不总是有效 出色地。 此外,有时 DispatcherServlet
需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。
通过从 PathMatcher
切换到 5.3 或更高版本中可用的已解析 PathPattern
可以更全面地解决上述问题,请参阅 模式比较。 与需要解码查找路径或编码控制器映射的 AntPathMatcher
不同,解析的 PathPattern
匹配名为 RequestPath
的路径的解析表示,一次一个路径段。 这允许单独解码和清理路径段值,而不会有改变路径结构的风险。 解析后的 PathPattern
也支持使用 servletPath
前缀映射,只要前缀保持简单并且没有任何需要编码的字符即可。
1.1.7. 拦截器
所有 HandlerMapping
实现都支持处理拦截器,这些拦截器在需要为特定类型的请求应用一些功能时可能很有用非常有用.
例如,检查用户身份等,org.springframework.web.servlet
包中的 HandlerInterceptor
实现了三种方法,提供足够的灵活性来执行各种预处理和后处理:
-
preHandle(..)
: 在执行实际处理程序之前 -
postHandle(..)
: 在执行实际处理程序之后 -
afterCompletion(..)
: 完成请求后
preHandle(..)
方法返回一个布尔值. 您可以使用此方法来中断或继续执行链的处理. 当此方法返回 true
时,处理程序执行链继续.
当它返回 false
时,DispatcherServlet
假定拦截器本身已处理请求(例如,呈现适当的视图) 并且不继续执行执行链中的其他拦截器和实际处理程序.
有关如何配置 Interceptors 的示例,请参阅 MVC 配置一节中的拦截器. 您还可以使用各个 HandlerMapping
实现上的setter方法直接注册它们.
请注意,在 HandlerAdapter
和 postHandle
之前,响应被写入并提交. postHandle
对于 @ResponseBody
和 ResponseEntity
方法不太有用, 这意味着对响应进行任何更改都为时已晚.
例如添加额外的 header. 对于此类方案,您可以实现 ResponseBodyAdvice
并将其声明为 Controller Advice bean或直接在 RequestMappingHandlerAdapter
上进行配置.
1.1.8. 异常
如果在请求映射期间发生异常或从请求处理程序(例如 @Controller
) 抛出异常, 则 DispatcherServlet
委托给 HandlerExceptionResolver
bean 来处理并解决异常,这通常是错误响应.
下表列出了可用的 HandlerExceptionResolver
实现:
HandlerExceptionResolver |
描述 |
---|---|
|
异常类名称和错误视图名称之间的映射. 用于在浏览器应用程序中呈现错误页面. |
解决 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码. 另请参阅备用 |
|
|
使用 |
|
通过在 |
解析链
您可以通过在 Spring 配置中声明多个 HandlerExceptionResolver
bean 并根据需要设置其顺序属性来形成异常解析链. order
属性越高,异常解析器定位的越晚.
HandlerExceptionResolver
的约定指定它可以返回:
-
一个指向错误视图的
ModelAndView
. -
如果在解析程序中处理异常,则为空的
ModelAndView
. -
如果异常仍未解析,则为
null
,以供后续解析器尝试,如果异常保留在最后,则允许冒泡到Servlet
容器. .
MVC Config自动声明内置的解析器,用于默认的 Spring MVC 异常,@ResponseStatus
带注解的异常,以及对 @ExceptionHandler
方法的支持. 您可以自定义该列表或替换它.
容器错误页面
如果任何 HandlerExceptionResolver
仍未解析异常,并且因此将其传播给 servlet 容器或者如果响应状态设置为错误状态(即 4xx,5xx) ,则 Servlet 容器可以呈现 HTML 中的默认错误页面.
要自定义容器的默认错误页面,可以在 web.xml
.中声明错误页面映射. 以下示例显示了如何执行此操作:
<error-page>
<location>/error</location>
</error-page>
根据前面的示例,当异常冒泡或响应具有错误状态时,Servlet 容器会在容器内对配置的 URL 进行 ERROR 调度(例如,/error
) .
然后由 DispatcherServlet
处理,可能将其映射到 @Controller
,可以实现该控件以返回带有模型的错误视图名称或呈现 JSON 响应,如以下示例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
@RestController
class ErrorController {
@RequestMapping(path = ["/error"])
fun handle(request: HttpServletRequest): Map<String, Any> {
val map = HashMap<String, Any>()
map["status"] = request.getAttribute("javax.servlet.error.status_code")
map["reason"] = request.getAttribute("javax.servlet.error.message")
return map
}
}
Servlet API 没有提供在 Java 中创建错误页面映射的方法. 但是,您可以同时使用 WebApplicationInitializer 和简写的 web.xml .
|
1.1.9. View Resolution
Spring MVC 定义了 ViewResolver
和 View
接口,使您可以在浏览器中呈现模型,而无需将您与特定的视图技术联系起来. ViewResolver
提供视图名称和实际视图之间的映射.
View
接口负责准备请求,并将请求的渲染交给某种具体的视图技术实现.
下表提供了有关 ViewResolver
层次结构的更多详细信息:
ViewResolver | 描述 |
---|---|
|
|
|
|
|
|
|
|
|
实现 |
|
|
处理
您可以在视图解析器链中声明多个视图解析器,并在必要时通过设置 order
属性来指定排序. 请记住,order
属性越高,视图解析器在链中的位置越晚. .
ViewResolver
可以返回 null
以指示无法找到该视图. 但是,对于 JSP 和 InternalResourceViewResolver
, 确定 JSP 是否存在的唯一方法是通过 RequestDispatcher
执行调度. 因此,您必须始终将 InternalResourceViewResolver
配置为视图解析器的整体顺序中的最后一个.
配置视图解析就像将 ViewResolver
bean 添加到 Spring 配置一样简单. MVC Config为View 解析器提供专用配置 API,并添加无逻辑视图控制器(View Controllers ) ,这些控制器对于没有控制器逻辑的 HTML 模板渲染非常有用.
重定向
您可以在视图中使用 redirect:
前缀来执行重定向. UrlBasedViewResolver
(及其子类) 将此识别为需要重定向的指令. 视图名称的其余部分是重定向 URL.
控制器本身可以根据逻辑视图名称进行操作. 逻辑视图名称(例如 redirect:/myapp/some/resource
) 相对于当前 Servlet 上下文重定向,而名称如 redirect:http://myhost.com/some/arbitrary/path
重定向到绝对 URL.
请注意,如果使用 @ResponseStatus
注解控制器方法,则注解值优先于 RedirectView
设置的响应状态.
转发
你也可以在视图名称中使用 forward:
前缀,来作为 UrlBasedViewResolver
和其子类最终解析的视图名称. 这将创建一个 InternalResourceView
,它执行 RequestDispatcher.forward()
.
因此,此前缀对于 InternalResourceViewResolver
和 InternalResourceView
(对于 JSP) 没有用,但如果您使用其他视图技术时仍希望强制 Servlet/JSP 引擎处理资源的转发,则它可能会有所帮助. 请注意,您也可以链接多个视图解析器.
内容协商
ContentNegotiatingViewResolver
本身不解析视图,而是委托给其他视图解析器,并选择类似于客户端请求的表示的视图. 可以从 Accept
头或查询参数(例如, "/path?format=pdf"
) 确定表示.
ContentNegotiatingViewResolver
通过将请求的媒体类型与其每个 ViewResolvers
关联的 View
支持的媒体类型(也称为 Content-Type
) 进行比较,选择适当的 View
来处理请求. 列表中具有兼容 Content-Type
的第一个 View
将表示返回给客户端.
如果 ViewResolver
链无法提供兼容视图,则会查询通过 DefaultViews
属性指定的视图列表. 后一个选项适用于单个视图,它可以呈现当前资源的适当表示,而不管逻辑视图名称如何. Accept
头可以包含通配符(例如 text/*
) ,在这种情况下,Content-Type
为 text/xml
的 View
是兼容匹配.
有关配置详细信息,请参阅 MVC Config 下的View 解析器 .
1.1.10. 国际化
正如 Spring Web MVC 框架所做的那样,Spring 架构的大多数部分都支持国际化. DispatcherServlet
允许您使用客户端的语言环境自动解析消息. 这是通过 LocaleResolver
对象完成的.
当请求进入时,DispatcherServlet
会查找当前语言环境解析器,如果找到,则会尝试使用它来设置语言环境. 您可以通过使用 RequestContext.getLocale()
方法,来获取由区域解析器解析到的结果.
除了自动解析语言环境之外,您还可以在处理程序时添加拦截器(有关拦截器的更多信息,请参阅Interception ) ,以便于在特定情况下更改语言环境. 例如(通过请求中的参数来改变语言环境)
区域解析器和拦截器在 org.springframework.web.servlet.i18n
包中定义,并以正常方式在应用程序上下文中进行配置. Spring 中包含以下选择的语言环境解析器.
Time Zone
除了获取客户端的区域设置外,了解其时区通常也很有用. LocaleContextResolver
接口提供了 LocaleResolver
的扩展,它允许解析器提供更丰富的 LocaleContext
,其中可能包含时区信息.
当此解析器可用时,可以使用 RequestContext.getTimeZone()
方法获取用户的 TimeZone
. 时区信息由 Spring 的 ConversionService
注册的任何 Date/Time Converter
和 Formatter
对象自动使用.
Cookie 解析器
此区域解析器检查客户端上可能存在的 Cookie
,以查看是否指定了 Locale
或 TimeZone
. 如果是,则使用指定的详细信息. 通过使用此区域解析器的属性,您可以指定 cookie 的名称以及失效时间. 以下示例定义 CookieLocaleResolver
:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
下表描述了 CookieLocaleResolver
的属性:
属性 | 默认值 | 描述 |
---|---|---|
|
classname + LOCALE |
cookie 的名字 |
|
Servlet container default |
Cookie 在客户端上持续存在的最长时间. 如果指定 |
|
/ |
限制 cookie 对您网站某个部分的可见性. 当指定了 |
Session 解析器
您可以使用 SessionLocaleResolver
从与用户请求关联的 Session
中获取 Locale
和 TimeZone
. 与 CookieLocaleResolver
相比,此策略将本地选择的区域设置存储在 Servlet 容器的 HttpSession
中. 因此,这些设置对于每个会话都是临时的,这些设置在会话结束时会丢失.
请注意,与外部会话管理机制没有直接关系,例如 Spring Session 项目. 此 SessionLocaleResolver
根据当前的 HttpServletRequest
评估和修改相应的 HttpSession
属性.
Locale 拦截器
您可以通过将 LocaleChangeInterceptor
添加到其中一个 HandlerMapping
定义来启用语言环境的更改. 它会检测请求中的参数并相应地更改语言环境,在程序的应用程序上下文中调用 LocaleResolver
上的 setLocale
方法.
下一个示例显示,当调用包含名为 siteLanguage
的参数的所有 *.view
资源时更改了区域设置. 例如,对 URL 的请求 www.sf.net/home.view?siteLanguage=nl
将网站语言更改为荷兰语. 以下示例显示如何拦截区域设置:
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.11. 主题
您可以使用 Spring Web MVC 框架自带的主题来设置应用程序的整体外观,从而增强用户体验. 主题是静态资源的集合,通常是样式表和图片,它们会影响应用程序的视觉样式.
定义一个主题
要在 Web 应用程序中使用主题,必须设置 org.springframework.ui.context.ThemeSource
接口的实现. WebApplicationContext
接口扩展了 ThemeSource
, 但将其职责委托给专用实现.
默认情况下,委托是 org.springframework.ui.context.support.ResourceBundleThemeSource
的实现. 它从类路径的根目录加载属性文件.
要使用自定义 ThemeSource
实现或配置 ResourceBundleThemeSource
的名称前缀,可以在应用程序上下文中使用保留名称 themeSource
注册 bean. Web 应用程序上下文自动检测具有该名称的 bean 并使用它.
使用 ResourceBundleThemeSource
时,主题在简单属性文件中定义. 属性文件列出构成主题的资源,如以下示例所示:
styleSheet=/themes/cool/style.css background=/themes/cool/img/coolBg.jpg
属性的键是从视图代码引用主题元素的名称. 对于 JSP,通常使用 spring:theme
自定义标签执行此操作,该标记与 spring:message
标签非常相似. 以下 JSP 片段使用上一示例中定义的主题来自定义外观:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
默认情况下,ResourceBundleThemeSource
使用空的名称前缀. 因此,从类路径的根加载属性文件. 因此,您可以将 cool.properties
主题定义放在类路径根目录的目录中(例如,在 /WEB-INF/classes
中) .
ResourceBundleThemeSource
使用标准的 Java 资源包加载机制,从而使主题也具有国际化. 例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties
,它引用一个带有荷兰文本的特殊背景图片.
解析主题
定义主题后,如上一节所述,您可以决定使用哪个主题. DispatcherServlet
查找名为 themeResolver
的 bean,以找出要使用的 ThemeResolver
实现.
主题解析器的工作方式与 LocaleResolver
的工作方式大致相同. 它检测用于特定请求的主题,还可以更改请求的主题. 下表描述了 Spring 提供的主题解析器:
Class | 描述 |
---|---|
|
选择使用 |
|
主题在用户的 HTTP 会话中维护. 它只需要为每个会话设置一次,但不会在会话之间保留. |
|
所选主题存储在客户端的 cookie 中. |
Spring 还提供了一个 ThemeChangeInterceptor
,它允许通过简单的请求参数对每个请求进行主题更改.
1.1.12. Multipart 解析器
org.springframework.web.multipart
包中的 MultipartResolver
是一种用于解析包括文件上传在内的多部分请求的策略. 他包含了一个 Commons FileUpload 的实现,另一个基于 Servlet 3.0 多部分请求解析.
要启用多部分处理,在 Spring 的配置文件中, 为 DispatcherServlet
配置名称为 multipartResolver
的 MultipartResolver
bean. DispatcherServlet
会自动检测并将其应用于请求中.
当收到内容类型为 multipart/form-data
的 POST 请求时,解析器会解析内容并将当前的 HttpServletRequest
包装为 MultipartHttpServletRequest
,以提供对已解析部分的访问,并将其作为请求参数暴露.
Apache Commons FileUpload
要使用 Apache Commons FileUpload
,您可以配置名为 multipartResolver
的 CommonsMultipartResolver
类型的 bean. 您还需要添加 commons-fileupload
依赖.
此解析器变体委托给应用程序中的本地库,提供跨 Servlet 容器的最大可移植性。 作为替代方案,考虑通过容器自己的解析器进行标准 Servlet 多部分解析,如下所述。
Commons FileUpload 传统上仅适用于 POST 请求,但接受任何 |
Servlet 3.0
需要通过 Servlet 容器配置启用 Servlet 3.0 多部分解析:
-
在 Java 中,在注册 Servlet 时设置
MultipartConfigElement
. -
在
web.xml
中,将"<multipart-config>"
部分添加到 servlet 声明中.
以下示例显示如何在注册 Servlet 时设置 MultipartConfigElement
:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
// ...
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(MultipartConfigElement("/tmp"))
}
}
一旦您配置好 Servlet 3.0,您就可以添加名为 multipartResolver
的 StandardServletMultipartResolver
类型的 bean.
此解析器按原样使用您的 Servlet 容器的多部分解析器,可能会将应用程序暴露给容器实现差异。 默认情况下,它将尝试使用任何 HTTP 方法解析任何 |
1.1.13. 日志
Spring MVC 中的 DEBUG 级别日志记录旨在实现紧凑,简约和人性化. 它侧重于那些一次又一次使用的高价值信息,其他的只有在调试特定问题时才有用.
TRACE 级日志记录通常遵循与 DEBUG 相同的原则(例如,不应该是 fire hose) ,但可以用于调试任何问题. 此外,一些日志消息可能在 TRACE 与 DEBUG 中显示不同的详细程度.
良好的日志记录来自使用日志的经验. 如果您发现任何不符合既定目标的事件,请告知我们.
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息. 这就是默认情况下屏蔽请求参数和请求头的原因,并且必须通过 DispatcherServlet
上的 enableLoggingRequestDetails
属性显式启用它们的完整日志记录.
以下示例说明如何使用 Java 配置执行此操作:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletMappings(): Array<String> {
return ...
}
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
registration.setInitParameter("enableLoggingRequestDetails", "true")
}
}
1.2. 过滤器
spring-web
模块提供了一些有用的过滤器:
1.2.1. 表单数据
浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT,PATCH 和 DELETE 提交表单数据. Servlet API 要求 ServletRequest.getParameter*()
方法仅支持 HTTP POST 的表单字段访问. .
spring-web
模块提供 FormContentFilter
过滤器来拦截 HTTP PUT,PATCH 和 DELETE 请求,请求类型为 application/x-www-form-urlencoded
, FormContentFilter
从请求中读取表单数据, 并包装 ServletRequest
,然后可以通过 ServletRequest.getParameter*()
系列方法提供表单数据.
1.2.2. 转发请求头
当通过代理主机或者端口或者其他方案请求时(例如,负载均衡) ,从客户端角度看,创建正确的主机,端口或者其他 Schema 成为一项挑战,
RFC 7239 定义了代理可以用来提供有关原始请求信息的转发 HTTP 头. 还有其他非标准头文件,包括 X-Forwarded-Host
, X-Forwarded-Port
, X-Forwarded-Proto
, X-Forwarded-Ssl
, 和 X-Forwarded-Prefix
.
ForwardedHeaderFilter
是一个 Servlet 过滤器,它根据 Forwarded
头部信息修改请求的主机,端口和 Schema,然后删除请求头. 过滤器依赖于包装请求,并且 ,必须先于其他过滤器(例如 RequestContextFilter
) 进行排序, 应该与修改后的请求而不是原始请求一起使用.
当转发请求头时需要注意的安全事项,因为应用程序无法知道请求头是代理按我们想的那样添加还是由客户端恶意添加,这就是为什么应该将信任边界的代理配置为删除来自外部的不受信任的转发请求头. 您还可以使用 removeOnly=true
配置 ForwardedHeaderFilter
,在这种情况下,它会删除但不使用请求头.
为了支持异步请求并且可以处理错误的情况, 过滤器应使用 DispatcherType.ASYNC
和 DispatcherType.ERROR
映射.
如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer
(请参阅Servlet 配置) 所有过滤器都将自动注册 为 dispatch 类型. 但是,如果通过 web.xml
或在 Spring Boot 中 FilterRegistrationBean
,除了 DispatcherType.REQUEST
之外,还必须包含 DispatcherType.ASYNC
和 DispatcherType.ERROR
.
1.2.3. Shallow ETag
ShallowEtagHeaderFilter
过滤器通过缓存写入响应的内容并从中计算 MD5 哈希来创建 “shallow” ETag. 客户端下次发送时, 它会执行相同操作,但它也会将计算值与 If-None-Match
请求头进行比较,如果两者相等,则返回 304(NOT_MODIFIED) .
此策略可以节省网络带宽,但不能节省 CPU,因为必须为每个请求计算完整响应. 前面描述的控制器级别的其他策略可以避免计算. 请参阅 HTTP 缓存.
此过滤器具有 writeWeakETag
参数,该参数将过滤器配置为写入弱 ETag,类似于以下内容: W/"02a2d595e6ed9a0b24f027f2b63b134d6"
(如 RFC 7232 Section 2.3) .
为了支持<< mvc-ann-async,异步请求>>,此过滤器必须与 DispatcherType.ASYNC
映射,以便过滤器可以延迟并成功生成 ETag 到最后一个异步调度的末尾. 如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer
(请参阅 Servlet 配置) ,则会为所有调度类型自动注册所有过滤器. 但是,如果通过 web.xml
或在 Spring Boot 中通过 FilterRegistrationBean
注册过滤器,请确保包括
DispatcherType.ASYNC
1.3. 注解控制器
Spring MVC 提供了基于注解的编程模型,其中 @Controller
和 @RestController
组件使用注解来表示请求映射、请求输入、异常处理等. 被注解的控制器拥有灵活的方法签名,并且无需扩展基类或实现特定的接口. 以下示例显示了由注解定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
@Controller
class HelloController {
@GetMapping("/hello")
fun handle(model: Model): String {
model["message"] = "Hello World!"
return "index"
}
}
在前面的示例中,该方法接受 Model
并将视图名称作为 String
返回,但是存在许多其他选项,本章稍后将对其进行说明.
有关 spring.io 的指南和教程,请使用本节中介绍的基于注解的编程模型. |
1.3.1. Declaration
您可以在 Servlet 的 WebApplicationContext
中使用标准的 Spring bean 定义来定义控制器 bean. @Controller
模板允许自动检测, 与 Spring 支持检测类路径中的 @Component
类一样,会自动注册 bean 定义. 它还充当注解类的模板,表示它充当的是 Web 组件的角色.
要启用 @Controller
bean 的自动检测,您可以将组件扫描添加到 Java 配置中,如以下示例所示:
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
@Configuration
@ComponentScan("org.example.web")
class WebConfig {
// ...
}
以下示例显示了与前面示例等效的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.web"/>
<!-- ... -->
</beans>
@RestController
是一个组合注解,它本身由 @Controller
和 @ResponseBody
元注解组成. 其每个方法都是类级别(type-level) 的 @ResponseBody
注解,因此,直接写入响应主体与视图渲染和使用 HTML 模板.
AOP 代理
在某些情况下,您需要在运行时使用 AOP 代理装饰控制器. 例如,如果您想在控制器上直接使用 @Transactional
注解. 在这种情况下,对于控制器而言,我们建议使用基于类的代理. 这通常也是控制器的默认选择. 但是,如果控制器没有实现 Spring Context 回调的接口 (例如 InitializingBean
, *Aware
等) , 则可能需要显式配置基于类的代理.
例如,使用 <tx:annotation-driven/>
,您可以更改为 <tx:annotation-driven proxy-target-class="true"/>
. 使用 @EnableTransactionManagement
,你可以更改为 @EnableTransactionManagement(proxyTargetClass = true)
1.3.2. Request Mapping
@RequestMapping
注解用于将请求映射到控制器方法. 它具有各种属性,可以通过 URL、HTTP 方法、请求参数、请求头参数(headers) 和媒体类型进行匹配. 可以在类级别使用它来表示共享映射,或在方法级别上用于缩小到特定的端点映射范围.
还有 @RequestMapping
的 HTTP 方法特定的缩写变量:
-
@GetMapping
-
@PostMapping
-
@PutMapping
-
@DeleteMapping
-
@PatchMapping
这些简洁的注解是 自定义注解,因为,大多数的控制器方法应该映射到 HTTP 方法而不是使用 @RequestMapping
. 默认情况下, @RequestMapping
和所有 HTTP 方法匹配. 在类上定义的仍然需要 @RequestMapping
来表示共享映射.
以下示例具有类和方法级别映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式匹配
@RequestMapping
可以使用 URL patterns 来映射. 这里有两种选择:
-
PathPattern
— 与 URL 路径匹配的预解析模式 (也预定义为“ PathContainer”) . 专为 web 使用而设计, 该解决方案可有效处理编码和路径参数, 并有效匹配. -
AntPathMatcher
— 根据字符串路径匹配字符串模式. 这在 Spring 配置中还用于在类路径, 文件系统和其他位置上选择资源的原始解决方案. 它效率较低, 并且字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战.
PathPattern
是推荐用于 Web 应用程序的解决方案, 它是 Spring WebFlux 中的唯一选择. 在 5.3 版之前, AntPathMatcher
是Spring MVC 中的唯一选择, 并且是默认设置. 但是, 可以在 MVC config 启用 PathPattern
.
PathPattern
与 AntPathMatcher
支持相同的语法. 此外, 它还支持 capturing pattern, 例如. {*spring}
, 用于匹配路径末尾 0 个或多个路径. PathPattern
还限制了 **
的使用, 以匹配多个路径段, 因此仅在模式末尾才允许使用. 当为给定请求选择最佳匹配模式时, 这消除了许多歧义的情况.有关完整的语法, 请参阅 PathPattern 和 AntPathMatcher.
示例:
-
"/resources/ima?e.png"
- match one character in a path segment -
"/resources/*.png"
- match zero or more characters in a path segment -
"/resources/**"
- match multiple path segments -
"/projects/{project}/versions"
- match a path segment and capture it as a variable -
"/projects/{project:[a-z]+}/versions"
- match and capture a variable with a regex
您还可以使用 @PathVariable
声明 URI 变量并访问它们的值,如以下示例所示:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
您可以在类和方法级别声明 URI 变量,如以下示例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
@Controller
@RequestMapping("/owners/{ownerId}")
class OwnerController {
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
URI 变量会自动转换为适当的类型,或者引发 TypeMismatchException
. 默认情况下支持简单类型(int
, long
, Date
等) ,您也可以注册对任何其他数据类型的支持. 请参见 类型转换 和 DataBinder
.
你可以显示命名 URI 变量(例如, @PathVariable("customId")
),但是如果名称是相同的,并且代码是使用调试信息编译的,或者在 Java 8 中使用 -parameters
编译器标记. 则可以保留该详细信息.
语法 {varName:regex}
声明一个具有正则表达式的 URI 变量,其语法为 {varName:regex}
. 例如,给定 URL "/spring-web-3.0.5.jar"
,以下方法提取名称,版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式还可以嵌入 ${…}
,在启动时通过 PropertySourcesPlaceholderConfigurer
解析本地、系统、环境和其他属性源时解析的占位符. 例如,这种模式可以使用基于某些外部配置对基 URL 进行参数化
模式比较
当多个模式与 URL 匹配时, 必须选择最佳匹配. 根据是否启用了已解析的 PathPattern
, 使用以下方法之一完成此操作:
两者都有助于对模式进行排序, 并将最匹配的放在顶层.如果 URI 变量的数量较少且单个通配符计为 1 且双通配符计为 2,那么模式就不那么具体了. 如果模式得到的分数相等,那么会选择较长的模式匹配. 如果分数和长度都相同,则会选择拥有比通配符更多的 URI 变量的模式.
默认映射模式(/**
)从评分中排除,并始终排在最后. 此外,前缀模式(例如 /public/**
) 被认为比没有双通配符的其他模式更不具体.
有关完整的详细信息, 请单击上面的链接到模式比较.
后缀匹配
从 5.3 开始, 默认情况下 Spring MVC 不再执行 .*
后缀模式匹配,以便映射到 /person
的控制器也隐式映射到 /person.*
. 路径扩展不再用于解释请求的响应内容类型 - 例如,/person.pdf
,/person.xml
等.
当浏览器用于发送难以持续交互的 Accept
头时,必须以这种方式使用文件扩展名. 目前,这不再是必需的,判断 Accept
头应该是首选.
随着时间的推移,文件扩展名的使用已经证明有多种方式存在问题. 当使用 URI 变量,路径参数和 URI 编码进行覆盖时,它可能会导致歧义. 有关基于 URL 的授权和安全性的推理(有关更多详细信息,请参阅下一节) 也变得更加困难.
要完全禁用 5.3 之前的版本中的路径扩展, 请进行以下设置: :
-
useSuffixPatternMatching(false)
, PathMatchConfigurer -
favorPathExtension(false)
, ContentNegotiationConfigurer
除了通过 "Accept"
头之外, 还有一种方法可以请求内容类型, (例如,在浏览器中输入 URL 时). 路径扩展的一种安全替代方法是使用查询参数策略. 如果必须使用文件扩展名,请考虑通过 ContentNegotiationConfigurer 的 mediaTypes
属性将它们限制为只允许注册的扩展名列表.
后缀匹配和 RFD
反射文件下载(Reflected file download) 攻击与 XSS 类似,因为它依赖请求输入,例如查询参数、URI 变量,并且在响应中被反射. 但是,RFD 攻击不是将 JavaScript 插入 HTML,而是依赖浏览器切换来执行下载,进而在之后的双击时将响应作为可执行脚本处理.
在 Spring MVC 中,@ResponseBody
和 ResponseEntity
方法存在风险,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展来请求. 禁用后缀模式匹配并使用路径扩展进行内容协商可降低风险,但不足以防止 RFD 攻击.
为了防止 RFD 攻击,在呈现响应主体之前,需要在 Spring MVC 添加 Content-Disposition:inline;filename=f.txt
头用于提供固定和安全的下载文件. 只有在 URL 路径包含的文件扩展名中既不包含白名单,也没有为内容协商显式注册以时,才需要这样做. 但是,在浏览器直接输入 URL 时,可能会产生副作用.
默认情况下,有许多常见的路径扩展白名单. 具有自定义 HttpMessageConverter
实现的应用程序可以显式注册内容协商的文件扩展名,以避免为这些扩展添加 Content-Disposition
头. 请参阅内容类型
有关 RFD 的其他建议,请参阅 CVE-2015-5211
消费者媒体类型
您可以根据请求的 Content-Type
缩小请求映射范围,如以下示例所示:
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
1 | 使用 consumes 属性来缩小内容类型的映射. |
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
// ...
}
1 | 使用 consumes 属性来缩小内容类型的映射. |
consumes
属性还支持否定表达式 - 例如,!text/plain
表示除 text/plain
之外的任何内容类型.
您可以在类级别声明共享 consumes
属性. 但是,与大多数其他请求映射属性不同,在类级别使用时,方法级别会 consumes
属性覆盖而不是扩展类级别声明.
MediaType 为常用媒体类型提供常量,例如 APPLICATION_JSON_VALUE 和 APPLICATION_XML_VALUE .
|
生产者媒体类型
您可以根据 Accept
请求头和控制器方法生成的内容类型列表来缩小请求映射,如以下示例所示:
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
1 | 使用 produces 属性来缩小内容类型的映射. |
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
}
1 | 使用 produces 属性来缩小内容类型的映射. |
媒体类型可以指定字符集. 支持否定表达式 - 例如, !text/plain
表示 "text/plain" 以外的任何内容类型.
您可以在类级别声明共享的 produces
属性. 但是,与大多数其他请求映射属性不同,在类级别使用时,方法级别会生成属性覆盖,而不是扩展类级别声明.
MediaType 为常用媒体类型提供常量,例如 APPLICATION_JSON_UTF8_VALUE 和 APPLICATION_XML_VALUE .
|
参数, 请求头
您可以根据请求参数条件缩小请求映射. 您可以测试是否存在请求参数(myParam
) ,缺少一个(!myParam
) 或特定值(myParam=myValue
) . 以下示例显示如何测试特定值:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 测试 myParam 是否等于 myValue . |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 测试 myParam 是否等于 myValue . |
您还可以将其与请求头条件一起使用,如以下示例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 测试 myParam 是否等于 myValue . |
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
HTTP HEAD, OPTIONS
@GetMapping
(和 @RequestMapping(method=HttpMethod.GET)
)一样,为了请求映射的目的,透明地支持 HTTP HEAD 以进行请求映射. 控制器方法无需更改. 在 javax.servlet.http.HttpServlet
中应用的响应包确保有 Content-Length
头并且设置为写入的字节数,但实际上不会写入响应.
@GetMapping
(和 @RequestMapping(method=HttpMethod.GET)
)一样,为了请求映射的目的,被隐式映射到并支持 HTTP HEAD,处理 HTTP HEAD 请求就像它是 HTTP GET 一样,但不是写入正文,而是计算字节数并设置 Content-Length
头.
默认情况下,HTTP OPTIONS 通过设置 Allow
响应头来为所有具有匹配 URL 模式的 @RequestMapping
方法中列出的 HTTP 方法列表来处理 HTTP 选项.
对于没有 HTTP 方法声明的 @RequestMapping
,Allow
请求头可以设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
. 控制器方法应始终声明支持的 HTTP 方法(例如,通过使用特定于 HTTP 方法的变体: @GetMapping
, @PostMapping
等) .
您可以将 @RequestMapping
方法显式映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这不是必需的.
自定义注解
Spring MVC 支持使用 组合注解进行请求映射. 这些注解本身是使用 @RequestMapping
进行元注解的,并且用于重新声明具有更窄,更具体目的的 @RequestMapping
属性的子集(或全部) .
@GetMapping
, @PostMapping
, @PutMapping
, @DeleteMapping
, 和 @PatchMapping
就是组合注解最好的示例, 提供它们是因为.
可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping
,默认情况下,它与所有 HTTP 方法匹配. 如果您需要组合注解的示例,请查看如何声明这些注解.
Spring MVC 还支持使用自定义请求匹配逻辑的自定义请求映射属性. 这是一个更高级的选项,需要继承 RequestMappingHandlerMapping
并覆盖 getCustomMethodCondition
方法, 您可以在其中检查自定义属性并返回自己的 RequestCondition
.
显式注册
您可以以编程方式注册处理程序方法,您可以将其用于动态注册或高级情况,例如不同 URL 下的同一处理程序的不同实例. 以下示例注册处理程序方法:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
1 | 为控制器注入目标处理程序和处理程序映射 |
2 | 准备映射元数据的请求 |
3 | 获取处理程序方法 |
4 | 添加注册 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 为控制器注入目标处理程序和处理程序映射 |
2 | 准备映射元数据的请求 |
3 | 获取处理程序方法 |
4 | 添加注册 |
1.3.3. 程序处理方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列控制器方法参数和返回值中进行选择.
方法参数
下表显示了控制器方法参数,任何参数都不支持响应式(Reactive)类型.
JDK 8 java.util.Optional
作为方法参数来支持的,它与具有 required
属性的注解(例如 @RequestParam
, @RequestHeader
等相结合). 并且等同于 required=false
.
控制器方法参数 | 描述 |
---|---|
|
无需直接使用 Servlet API 即可访问请求参数以及 request 和 session 属性. |
|
选择任何特定的请求或响应类型 - 例如, |
|
强制进行会话. 因此,此类参数永远不可能为 |
|
Spring4.0 push 生成器 API 用于编程 HTTP/2 资源推送, 请注意,根据 Servlet 规范,如果客户端不支持该 HTTP/2 功能,则注入的 |
|
当前经过身份验证的用户 - 如果已知,可能是特定的 请注意,不会立即解析此参数,如果它被注解了以允许自定义解析器解析它
在通过 |
|
请求的 HTTP 方法. |
|
当前请求区域设置,由最可用的 |
|
与当前请求关联的时区,由 |
|
用于访问 Servlet API 暴露的原始请求主体. |
|
用于访问 Servlet API 暴露的原始响应主体. |
|
用于访问 URI 模板变量. 请参阅URI 模式匹配. |
|
用于访问 URI 路径段中的名称 - 值对. 请参见Matrix Variables. |
|
用于访问 Servlet 请求参数,包括多部分文件. 参数值将转换为声明的方法参数类型. 请参阅 |
|
用于访问请求头. 头的值将转换为声明的方法参数类型. 请参阅 |
|
用于访问 cookie. Cookie 值将转换为声明的方法参数类型. 请参阅 |
|
用于访问 HTTP 请求正文. 通过使用 |
|
用于访问请求头和正文. 使用 HttpMessageConverter 转换正文. 见HttpEntity. |
|
要访问 |
|
用于访问 HTML 控制器中使用的模型,并将其作为视图呈现的一部分暴露给模板. |
|
指定在重定向(即,要附加到查询字符串) 时使用的属性,以及临时存储的 flash 属性,直到重定向后的请求为止. 请参阅重定向属性 和Flash 属性. |
|
用于访问模型中的现有属性(如果不存在则实例化) ,并应用数据绑定和验证. 请参阅 请注意,使用 |
|
用于访问来自命令对象的验证和数据绑定的错误(即 |
|
用于标记表单处理完成,从而触发通过类级别 |
|
用于准备相对于当前请求的主机,端口,方案,上下文路径和 servlet 映射的文字部分的 URL. 请参阅URI 链接. |
|
用于访问任何会话属性,与由于类级别 |
|
用于访问请求属性. 有关更多详细信息,请参阅 |
Any other argument |
如果方法参数与此表中的任何值不匹配,并且它是一个简单类型(由 BeanUtils#isSimpleProperty 确定, 则它被解析为 |
返回值
下表描述了支持的控制器方法返回值. 所有返回值都支持响应式类型.
Controller method return value | Description |
---|---|
|
返回值通过 |
|
指定完整响应(包括 HTTP 头和主体) 的返回值将通过 |
|
用于返回带头部信息且没有正文的响应. |
|
要使用 |
|
用于与隐式模型一起呈现的 |
|
要添加到隐式模型的属性,通过 |
|
要添加到模型的属性,通过 请注意, |
|
要使用的视图和模型属性,以及(可选) 响应状态. |
|
如果具有 |
|
从任何线程异步生成任何前面的返回值 - 例如,由于某些事件或回调. 请参阅 异步请求 和 |
|
在 Spring MVC 管理的线程中异步生成上述任何返回值. 请参阅 和 Callable. |
|
作为替代 |
|
使用 |
|
异步写入响应 |
Reactive types — Reactor, RxJava, or others through |
使用 对于流式场景(例如, 请参阅 异步请求 和 Reactive Types(响应式类型). |
其他任何返回值 |
任何与此表中任何早期值不匹配且返回值为 |
类型转换
如果参数声明为 String
以外的其他参数,则表示某些带注解的控制器方法参数(例如 @RequestParam
, @RequestHeader
, @PathVariable
, @MatrixVariable
, 和 @CookieValue
) 可能需要进行类型转换.
对于此类情况,将根据配置的转换器自动应用类型转换. 默认情况下,支持简单类型(int
, long
, Date
和其他) . 您可以通过 WebDataBinder
(请参阅DataBinder
) 或使用 FormattingConversionService
注册 Formatters
来自定义类型转换. 请参见 Spring Field Formatting.
类型转换中的一个实际问题是处理空的 String 值. 如果该值由于类型转换而变为 null
, 则将其视为丢失. Long
, UUID
, 和其他目标类型. 如果要允许注入 null
, 则可以在参数注解中使用 required
标志, 或将参数声明为 @Nullable
.
从 5.3 开始, 即使在类型转换之后, 也将强制使用非 另外, 您也可以专门处理在必需的 |
Matrix Variables
RFC 3986 讨论了路径段中的携带键值对. 在 Spring MVC 中,我们将那些基于 Tim Berners-Lee 的 “old post” 称为 "`matrix variables(矩阵变量) `" ,但它们也可以称为 URI 路径参数.
矩阵变量可以在任意路径段落中出现,每对矩阵变量之间使用分号隔开,多个值可以用逗号隔开(例如,/cars;color=red,green;year=2012
) , 也可以通过重复的变量名称指定多个值(例如,color=red;color=green;color=blue
) .
如果 URL 有可能会包含矩阵变量,那么在请求路径的映射配置上就需要使用 URI 模板来体现. 这样才能确保请求可以被正确地映射,而不管矩阵变量在 URI 中是否出现、出现的次序是怎样的等. 以下示例使用矩阵变量:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
由于任意路径段落中都可以含有矩阵变量,在某些场景下,开发者需要用更精确的信息来指定矩阵变量的位置. 以下示例说明如何执行此操作:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
矩阵变量可以定义为可选,并指定默认值,如以下示例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,可以使用 MultiValueMap
,如以下示例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
请注意,您需要启用矩阵变量的使用. 在 MVC Java 配置中,您需要通过 路径匹配 将 removeSemicolonContent=false
设置为 UrlPathHelper
. 在 MVC XML 命名空间中,您可以设置 <mvc:annotation-driven enable-matrix-variables="true"/>
.
@RequestParam
您可以使用 @RequestParam
注解将 Servlet 请求参数(即查询参数或表单数据) 绑定到控制器中的方法参数.
以下示例显示了如何执行此操作:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | 使用 @RequestParam 绑定 petId . |
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = this.clinic.loadPet(petId);
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 使用 @RequestParam 绑定 petId . |
若参数使用了该注解,则该参数默认是必须提供的.但您可以通过将 @RequestParam
注解的 required
属性设置为 false
或通过使用 java.util.Optional
包装器声明参数来指定方法参数是可选的.
如果目标方法参数类型不是 String
,则会自动应用类型转换. 请参阅类型转换.
将参数类型声明为数组或列表允许为同一参数名称解析多个参数值.
当 @RequestParam
注解声明为 Map<String, String>
或 MultiValueMap<String, String>
时, 如果注解中未指定参数名称,则会使用每个给定参数名称的请求参数值填充映射.
请注意,使用 @RequestParam
是可选的(例如,设置其属性) . 默认情况下, 任何属于简单值类型的参数(由 BeanUtils#isSimpleProperty 确定) 并且未被任何其他参数解析器解析,都被视为使用 @RequestParam
进行注解.
@RequestHeader
您可以使用 @RequestHeader
注解将请求头绑定到控制器中的方法参数.
考虑以下请求,请求头为:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
以下示例获取 Accept-Encoding
和 Keep-Alive
头的值:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取 Accept-Encoding 头部信息 |
2 | 获取 Keep-Alive 头部信息. |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | 获取 Accept-Encoding 头部信息 |
2 | 获取 Keep-Alive 头部信息. |
如果目标方法参数类型不是 String,则会自动应用类型转换. 请参阅类型转换.
在 Map<String, String>,MultiValueMap<String, String>
或 HttpHeaders
参数上使用 @RequestHeader
注解时,将使用所有请求头值填充映射.
内置支持可用于将逗号分隔的字符串转换为字符串或字符串集或类型转换系统已知的其他类型. 例如,使用 @RequestHeader("Accept") 注解的方法参数可以是 String 类型,也可以是 String[] 或 List<String> .
|
@CookieValue
您可以使用 @CookieValue
注解将 HTTP cookie 的值绑定到控制器中的方法参数.
考虑使用以下 cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下示例显示了如何获取 cookie 值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 | 获取 JSESSIONID cookie 的值 |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
1 | 获取 JSESSIONID cookie 的值 |
如果目标方法参数类型不是 String,则会自动应用类型转换. 请参阅类型转换.
@ModelAttribute
您可以在方法参数上使用 @ModelAttribute
注解来从模型访问属性,或者如果不存在则将其实例化. model 属性还覆盖了名称与字段名称匹配的 HTTP Servlet 请求参数的值. 这称为数据绑定,它使您不必处理解析和转换单个查询参数和表单字段. 以下示例显示了如何执行此操作:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String {
// method logic...
}
上面的 Pet
实例来源于以下方式之一::
-
可能来自由 @ModelAttribute 方法 添加的模型中检索
-
如果出现模型属性,则从 HTTP 会话中检索类级别的
@SessionAttributes
注解 -
通过
Converter
获得,其中模型属性名称与请求值的名称匹配,例如路径变量或请求参数(下面会详细讲解)。 -
它可能是调用了自身的默认构造器被实例化出来的
-
他可能从调用具有与 Servlet 请求参数匹配的参数的
"primary constructor"
. 参数名称通过 JavaBeans@ConstructorProperties
或字节码中的运行时保留参数名称确定.
虽然通常使用 @ModelAttribute method 来使用属性填充模型,但另一种替代方法是依赖于 Converter<String, T>
和 URI 路径变量. 这适用于模型属性名称与请求值的名称(例如路径变量或请求参数)匹配,并且存在从 String
到模型属性类型的 Converter
。 在以下示例中, model 属性名称 account
匹配 URI 路径变量 account
,并通过将 String 字符串传递到已注册的 Converter<String, Account>
转换器来加载 Account
:
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
// ...
}
下一步就是数据的绑定,WebDataBinder
类能将请求参数,包括字符串的查询参数和表单字段等,通过名称匹配到 model 的属性上.
成功匹配的字段在需要的时候会进行一次类型转换(从 String 类型到目标字段的类型) ,然后被填充到 model 对应的属性中, 有关数据绑定(和验证) 的更多信息,请参阅Validation. 有关自定义数据绑定的更多信息,请参阅DataBinder
.
数据绑定可能导致错误. 默认情况下,会引发 BindException
. 但是,要在控制器方法中检查此类错误,可以在 @ModelAttribute
旁边添加一个 BindingResult
参数,如以下示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 在 @ModelAttribute 旁边添加 BindingResult . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 在 @ModelAttribute 旁边添加 BindingResult . |
在某些情况下,您可能希望在没有数据绑定的情况下访问 model
属性. 对于这种情况,您可以将 model 注入控制器并直接访问它,或者设置 @ModelAttribute(binding=false)
,如下例所示:
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) { (1)
// ...
}
1 | 设置 @ModelAttribute(binding=false) . |
@ModelAttribute
fun setUpForm(): AccountForm {
return AccountForm()
}
@ModelAttribute
fun findAccount(@PathVariable accountId: String): Account {
return accountRepository.findOne(accountId)
}
@PostMapping("update")
fun update(@Valid form: AccountForm, result: BindingResult,
@ModelAttribute(binding = false) account: Account): String { (1)
// ...
}
1 | 设置 @ModelAttribute(binding=false) . |
通过添加 javax.validation.Valid
注解或 Spring 的 @Validated
注解(Bean Validation 和Spring validation) ,您可以在数据绑定后自动应用验证. 以下示例显示了如何执行此操作:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 验证 Pet 实例. |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
请注意,使用 @ModelAttribute
是可选的(例如,设置其属性) . 默认情况下,任何非简单值类型的参数(由 BeanUtils#isSimpleProperty 确定) 并且未被任何其他参数解析器解析,都被视为使用 @ModelAttribute
进行注解.
@SessionAttributes
@SessionAttributes
用于在请求之间的 HTTP Servlet 会话中存储 model 属性. 它是一个类型级别的注解,用于声明特定控制器使用的会话属性. 这通常列出 model 属性的名称或 model 属性的类型,这些属性应该透明地存储在会话中以供后续访问请求使用.
以下示例使用 @SessionAttributes
注解:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用 @SessionAttributes 注解. |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | 使用 @SessionAttributes 注解. |
在第一个请求中,当名称为 pet
的 model 属性添加到模型中时,他会自动保存到 HTTP Servlet 会话中,并保持不变,直到另一个控制器方法使用 SessionStatus
方法参数来清除存储,如下例所示:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); (2)
// ...
}
}
1 | 在Servlet会话中存储 Pet 值. |
2 | 在Servlet会话中清除 Pet 值. |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String {
if (errors.hasErrors()) {
// ...
}
status.setComplete() (2)
// ...
}
}
1 | 在Servlet会话中存储 Pet 值. |
2 | 在Servlet会话中清除 Pet 值. |
@SessionAttribute
如果需要访问已存在的被全局 session 属性,例如在控制器之外(如通过过滤器) 的(可有可无) ,请在方法参数上使用 @SessionAttribute
注解:
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | 使用 @SessionAttribute 注解. |
@RequestMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
对于需要添加或删除会话属性的用例,请考虑将 org.springframework.web.context.request.WebRequest
或 javax.servlet.http.HttpSession
注入控制器方法.
作为控制器工作流的一部分,在会话中临时存储模型属性的方法可以使用 @SessionAttributes
,详情请参阅@SessionAttributes
.
@RequestAttribute
与 @SessionAttribute
类似,@RequestAttribute
注解可用于访问由过滤器(Filter
) 或拦截器(HandlerInterceptor
) 创建的已存在的请求属性:
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 使用 @RequestAttribute 注解. |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 使用 @RequestAttribute 注解. |
重定向属性
默认情况下,所有模型属性都被视为在重定向 URL 中暴露为 URI 模板变量. 在其余属性中,原始类型或集合或基本类型数组的属性将自动附加为查询参数.
如果专门为重定向准备了模型实例,期望的结果则是将原始类型属性作为查询参数. 但是,在带注解的控制器中,为了渲染目的,模型可以包含其他属性(例如,下拉字段值) .
为了避免在 URL 中出现此类属性的可能性,@RequestMapping
方法可以声明 RedirectAttributes
类型的参数, 并使用它来指定可供 RedirectView
使用的确切属性. 如果方法重定向,则使用 RedirectAttributes
的内容. 否则,使用模型的内容.
RequestMappingHandlerAdapter
提供了一个名为 ignoreDefaultModelOnRedirect
的标志,您可以使用该标志指示如果控制器方法重定向,则永远不应使用默认模型的内容.
相反,控制器方法应声明 RedirectAttributes
类型的属性,如果不这样做,则不应将任何属性传递给 RedirectView
. MVC 命名空间和 MVC Java 配置都将此标志设置为 false
,以保持向后兼容性. 但是,对于新应用程序,我们建议将其设置为 true
.
请注意,扩展重定向 URL 时,当前请求中的 URI 模板变量会自动可用,您需要通过 Model
或 RedirectAttributes
显式添加它们. 以下示例显示如何定义重定向:
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
@PostMapping("/files/{path}")
fun upload(...): String {
// ...
return "redirect:files/{path}"
}
将数据传递到重定向目标的另一种方法是使用Flash 属性. 与其他重定向属性不同,Flash 属性保存在 HTTP 会话中(因此,不会出现在 URL 中) . 有关更多信息,请参阅 Flash 属性.
Flash 属性
Flash 属性(flash attributes) 提供了一个请求为另一个请求存储有用属性的方法. 这在重定向的时候最常使用,比如常见的 POST/REDIRECT/GET 模式. Flash 属性会在重定向前被暂时地保存起来(通常是保存在 session 中) ,重定向后会重新被下一个请求取用并立即从原保存地移除.
为支持 flash 属性,Spring MVC 提供了两个抽象. FlashMap
被用来存储 flash 属性,而用 FlashMapManager
来存储、取回、管理 FlashMap
的实例.
对 flash 属性的支持默认是启用 “on” 的,并不需要显式声明,不过没用到它时它绝不会主动地去创建 HTTP 会话(session) . 对于每个请求,框架都会"`input`" 一个 FlashMap
,里面存储了从上个请求(如果有) 保存下来的属性;
同时,每个请求也会 “output” FlashMap
,里面保存了要给下个请求使用的属性. 两个 FlashMap 实例在 Spring MVC 应用中的任何地点都可以通过 RequestContextUtils
工具类的静态方法取得.
控制器通常不需要直接接触 FlashMap. 一般是通过 @RequestMapping
方法去接受 RedirectAttributes
类型的参数,然后直接地往其中添加 flash 属性.
通过 RedirectAttributes
对象添加进去的 flash 属性会自动被填充到请求的 “output” FlashMap
对象中去. 类似地,重定向后 “input” 的 FlashMap
属性也会自动被添加到服务重定向 URL 的控制器参数 Model
中去
Multipart
启用 MultipartResolver
后,将解析具有 multipart/form-data
的 POST 请求的内容,并将其作为常规请求参数进行访问. 以下示例访问一个常规表单字段和一个上载文件:
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(@RequestParam("name") name: String,
@RequestParam("file") file: MultipartFile): String {
if (!file.isEmpty) {
val bytes = file.bytes
// store the bytes somewhere
return "redirect:uploadSuccess"
}
return "redirect:uploadFailure"
}
}
将参数类型声明为 List<MultipartFile>
允许为同一参数名称解析多个文件.
当 @RequestParam
注解声明为 Map<String, MultipartFile>
或 MultiValueMap<String, MultipartFile>
时,如果注解中未指定参数名称,则会使用每个给定参数名称的多部分文件填充 map.
使用 Servlet 3.0 多部分解析,您也可以将 javax.servlet.http.Part 而不是 Spring 的 MultipartFile 声明为方法参数或集合值类型.
|
您还可以将多部分内容用作绑定到命令对象的数据的一部分. 例如,前面示例中的表单字段和文件可以是表单对象上的字段,如以下示例所示:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
if (!form.getFile().isEmpty()) {
byte[] bytes = form.getFile().getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
class MyForm(val name: String, val file: MultipartFile, ...)
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
if (!form.file.isEmpty) {
val bytes = form.file.bytes
// store the bytes somewhere
return "redirect:uploadSuccess"
}
return "redirect:uploadFailure"
}
}
还可以在 RESTful 中从非浏览器客户端提交多部分请求. 以下示例显示了带有 JSON 的文件:
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 ...
对于名称为 "meta-data" 的部分,可以通过控制器方法上的 @RequestParam
String
metadata 参数来获得.
但对于那部分请求体中为 JSON 格式数据的请求, 可能更想通过接受一个对应的强类型对象,就像 @RequestBody
通过 HttpMessageConverter 将一般请求的请求体转换成一个对象一样. 使用 @RequestPart
注解访问多部分:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData,
@RequestPart("file-data") file: MultipartFile): String {
// ...
}
您可以将 @RequestPart
与 javax.validation.Valid
结合使用,或使用 Spring 的 @Validated
注解,这两种注解都会启用标准 Bean 验证.
默认情况下,验证错误会导致 MethodArgumentNotValidException
, 并将其转换为 400(BAD_REQUEST) 响应. 或者,您可以通过 Errors
或 BindingResult
参数在控制器内本地处理验证错误,如以下示例所示:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
result: BindingResult): String {
// ...
}
@RequestBody
您可以使用 @RequestBody
注解通过 HttpMessageConverter
将请求主体读取并反序列化为 Object
. 以下示例使用 @RequestBody
参数:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
您可以将 @RequestBody
与 javax.validation.Valid
或Spring的 @Validated
注解结合使用,这两种注解都会启用标准 Bean 验证.
默认情况下,验证错误会导致 MethodArgumentNotValidException
,并将其转换为 400(BAD_REQUEST) 响应. 或者,您可以通过 Errors
或 BindingResult
参数在控制器内本地处理验证错误,如以下示例所示:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
// ...
}
HttpEntity
HttpEntity
与使用 @RequestBody
或多或少有些类似,但它基于一个暴露请求头和正文的容器对象. 以下清单显示了一个示例:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
您可以在方法上使用 @ResponseBody
注解,以通过HttpMessageConverter将返回序列化到响应主体. 以下清单显示了一个示例:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
类级别也支持 @ResponseBody
,在这种情况下,它由所有控制器方法继承. 例如 @RestController
的效果,它只不过是一个用 @Controller
和 @ResponseBody
标记的元注解.
您可以将 @ResponseBody
与 reactive 类型一起使用. 有关更多详细信息,请参阅 异步请求 和 Reactive Types(响应式类型).
您可以将 @ResponseBody
方法与 JSON 序列化视图结合使用. 有关详细信息,请参阅Jackson JSON .
ResponseEntity
ResponseEntity
与@ResponseBody
类似,但具有状态和响应头. 例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body = ...
val etag = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
Spring MVC 支持使用单值reactive type 异步生成 ResponseEntity
,and/or 主体的单值和多值 reactive 类型.这允许以下类型的异步响应:
-
ResponseEntity<Mono<T>>
或ResponseEntity<Flux<T>>
可以立即获得响应状态和响应头,而在稍后以异步方式提供正文时. 如果主体包含 0..1 个值, 请使用Mono
,如果主体可以产生多个值,请使用Flux
. -
Mono<ResponseEntity<T>>
在稍后的时间异步提供所有的 — response status, headers, 和 body. 这允许响应状态和响应头根据异步请求处理的结果而变化.
Jackson JSON
Spring 为 Jackson JSON 库提供支持.
JSON 序列化视图
Spring MVC 为 Jackson 的序列化视图 提供内置支持,允许仅渲染 Object 中所有字段的子集. 为了与 @ResponseBody
控制器方法或者返回 ResponseEntity
的控制器方法一起使用,可以简单地将 @JsonView
注解放在参数上,指定需要使用的视图类或接口即可. 如以下示例所示:
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser() = User("eric", "7!jd#h23")
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许一组视图类,但每个控制器方法只能指定一个. 如果需要激活多个视图,可以使用复合接口.
|
如果您要通过编程方式进行上述操作,而不是声明一个 @JsonView
注解, 用 MappingJacksonValue
包装返回值,并使用它提供序列化
@RestController
public class UserController {
@GetMapping("/user")
public MappingJacksonValue getUser() {
User user = new User("eric", "7!jd#h23");
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(User.WithoutPasswordView.class);
return value;
}
}
@RestController
class UserController {
@GetMapping("/user")
fun getUser(): MappingJacksonValue {
val value = MappingJacksonValue(User("eric", "7!jd#h23"))
value.serializationView = User.WithoutPasswordView::class.java
return value
}
}
对于依赖视图的控制器,只需将序列化视图类添加到 model 中即可. 如以下示例所示:
@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";
}
}
@Controller
class UserController : AbstractController() {
@GetMapping("/user")
fun getUser(model: Model): String {
model["user"] = User("eric", "7!jd#h23")
model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java
return "userView"
}
}
1.3.4. Model
您可以使用 @ModelAttribute
注解:
-
在
@RequestMapping
方法中的方法参数,用于从model
创建或访问 Object 并通过WebDataBinder
将其绑定到请求. -
作为
@Controller
或@ControllerAdvice
类中的方法级注解,有助于在任何@RequestMapping
方法调用之前初始化模型. -
在
@RequestMapping
方法上标记其返回值是一个模型属性.
本节讨论 @ModelAttribute
注解可被应用在方法或方法参数上 - 前面列表中的第二项. 控制器可以包含任意数量的 @ModelAttribute
方法.
在同一控制器中的 @RequestMapping
方法之前调用所有这些方法. @ModelAttribute
方法也可以通过 @ControllerAdvice
在控制器之间共享. 有关更多详细信息,请参阅 Controller Advice 部分.
@ModelAttribute
方法具有灵活的方法签名. 除了与 @ModelAttribute
本身或请求体相关的任何内容外,它们支持许多与 @RequestMapping
方法相同的参数.
以下示例显示了 @ModelAttribute
方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number)
}
如果未明确指定名称,框架将根据属性的类型给予一个默认名称,如 Conventions 的 javadoc 中所述. 你可以通过设置 @ModelAttribute 注解的值来改变默认值. 当向 Model 中直接添加属性时,请使用合适的重载方法 addAttribute .
|
@ModelAttribute
注解也可以被用在 @RequestMapping
方法上,这种情况下,@RequestMapping
方法的返回值将会被解释为 model 的一个属性,这通常不是必需的,因为它是 HTML 控制器中的默认行为,
除非返回值是一个 String
,否则会被解释为视图名称. @ModelAttribute
还可以自定义模型属性名称,如以下示例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.3.5. DataBinder
@Controller
或 @ControllerAdvice
类可以使用 @InitBinder
方法初始化 WebDataBinder
的实例,而这些方法又可以:
-
将请求参数(即表单或查询数据) 绑定到模型对象.
-
将基于字符串的请求值(例如请求参数,路径变量,请求头,cookie 等) 转换为目标类型的控制器方法参数.
-
在呈现 HTML 表单时将模型对象值格式化为
String
值.
@InitBinder
方法可以注册特定于控制器的 java.beans.PropertyEditor
或 Spring Converter
和 Formatter
组件. 此外,您可以使用MVC config 在全局共享的 FormattingConversionService
中注册 Converter
和 Formatter
类型.
@InitBinder
方法支持许多与 @RequestMapping
方法相同的参数,但 @ModelAttribute
(命令对象) 参数除外. 通常,它们使用 WebDataBinder
参数(用于注册) 和 void
返回值进行声明. 以下清单显示了一个示例:
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
1 | 使用 @InitBinder 注解. |
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
1 | 使用 @InitBinder 注解. |
或者,当使用基于 Formatter
的设置时,您可以通过共享的 FormattingConversionService
重复使用相同的方法并注册特定于控制器的 Formatter
实现,如以下示例所示:
@Controller
public class FormController {
@InitBinder (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
1 | 添加一个自定义 formatter ( 本例中为 DateFormatter ). |
@Controller
class FormController {
@InitBinder (1)
protected fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
}
// ...
}
1 | 添加一个自定义 formatter ( 本例中为 DateFormatter ). |
Model Design
In the context of web applications, data binding involves the binding of HTTP request parameters (that is, form data or query parameters) to properties in a model object and its nested objects.
Only public
properties following the
JavaBeans naming conventions
are exposed for data binding — for example, public String getFirstName()
and
public void setFirstName(String)
methods for a firstName
property.
在 web 应用的上下文中,data binding 涉及到 HTTP 请求参数(即表单数据或查询参数)到模型对象中的属性,以及 它的嵌套对象。
只有 public
属性遵循 JavaBeans 命名约定 并暴露为数据绑定参数 ——例如,public String getFirstName()
和 firstName
属性的 public void setFirstName(String)
方法。
模型对象及其嵌套对象有时也称为 command object、form-backing object 或 POJO(普通旧 Java 对象)。 |
默认情况下,Spring 允许绑定到 model object 中的所有公共属性。这意味着您需要仔细考虑 model 具有哪些公共属性,因为 客户端可以针对任何公共属性路径,包括一些您不期望的路径。
例如,给定一个 HTTP 表单数据端点,恶意客户端可以为模型对象中存在的属性,但是在 web 应用的上下文中,数据绑定 涉及到 HTTP 请求参数绑定 (即表单数据或查询参数)到模型对象中的属性,以及它的嵌套对象。
推荐的方法是使用仅暴露的 dedicated model object(专用模型对象) 与表单提交相关的属性。例如,在用于更改的表格上 用户的电子邮件地址,模型对象应声明一组最少的属性,例如 如下面的 ChangeEmailForm
。
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
如果您不能或不想为每个数据使用 专用模型对象 绑定用例,您 must 限制数据绑定允许的属性。 理想情况下,您可以通过注册 allowed field patterns 通过 WebDataBinder
上的 setAllowedFields()
方法。
例如,要在您的应用程序中注册允许的字段模式,您可以实现一个 @Controller
或 @ControllerAdvice
组件中的 @InitBinder
方法,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式,还可以注册 disallowed field patterns 通过 DataBinder
及其子类中的 setDisallowedFields()
方法。 但是请注意,"allow list" 比 "deny list" 更安全。 最后,setAllowedFields()
应该优于 setDisallowedFields()
。
请注意,与允许的字段模式匹配是区分大小写的; 而,匹配 针对不允许的字段模式不区分大小写。 此外,一个匹配的字段不允许的模式将不会被接受,即使它也恰好匹配 允许列表。
正确配置允许和不允许的字段模式非常重要直接暴露您的 domain 模型以进行数据绑定时。 否则,它的安全隐患大。 此外,强烈建议您不使用您域中的类型 JPA 或 Hibernate 实体等模型作为数据绑定场景中的模型对象。 |
1.3.6. 异常
@Controller
和 @ControllerAdvice 可以使用 @ExceptionHandler
方法来处理来自控制器方法的异常,如下例所示:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
@Controller
class SimpleController {
// ...
@ExceptionHandler
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
该异常可能与顶级异常(即抛出直接 IOException
) 或包装器中的异常(例如,包含在 IllegalStateException
内的 IOException
) 相匹配.从 5.3 开始, 这可以在任意原因级别上匹配, 而以前只考虑了直接原因.
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示. 当多个异常方法匹配时,根( root )异常匹配通常优先于原因( cause )异常匹配. 更具体地说,ExceptionDepthComparator
用于根据抛出的异常类型的深度对异常进行排序.
注解声明可以缩小要匹配的异常类型,如以下示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
您甚至可以使用特定异常类型列表中的非常通用的参数签名,如以下示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
// ...
}
root 和 cause 异常匹配之间的区别可能是令人惊讶的. 在前面显示的 在 |
我们通常建议您在参数签名中尽可能具体,减少 root 和 cause 异常类型之间不匹配的可能性. 考虑将多匹配方法分解为单独的 @ExceptionHandler
方法,每个方法通过其签名匹配单个特定异常类型.
在具有多个 @ControllerAdvice
组成中,我们建议在 @ControllerAdvice
上声明根异常映射,并使用相应的顺序进行优先级排序.
虽然根异常匹配优先于某个原因,但这是在给定控制器或 @ControllerAdvice
类的方法中定义的. 这意味着优先级较高的 @ControllerAdvice
bean上的原因匹配优先于较低优先级的 @ControllerAdvice
bean上的任何匹配(例如,root) .
最后但同样重要的是, 可以通过 @ExceptionHandler
方法的实现,讲异常以原始的形式重新抛出,并提供给特定的异常实例. 这在您仅对根级别匹配或在特定上下文中无法静态确定的匹配中感兴趣的情况下非常有用. 重新抛出的异常通过后续的解析链传播,就好像给定的 @ExceptionHandler
方法首先不匹配一样.
Spring MVC中对 @ExceptionHandler
方法的支持是基于 DispatcherServlet
级别的HandlerExceptionResolver机制构建的.
方法参数
`@ExceptionHandler`方法支持以下参数:
方法参数 | 描述 |
---|---|
Exception type |
用于访问引发的异常. |
|
访问控制器方法引发的异常 |
|
无需直接使用 Servlet API 即可访问请求参数以及请求和会话属性. |
|
选择任何特定的请求或响应类型(例如, |
|
强制进行会话. 因此,这样的结果永远不会是 |
|
当前经过身份验证的用户 - 如果已知,可能是特定的 |
|
请求的 HTTP 方法. |
|
当前请求区域设置,由最可用的 |
|
与当前请求关联的时区,由 |
|
用于访问 Servlet API 暴露的原始响应主体. |
|
用于访问模型以获取错误响应. 总是为空. |
|
指定在重定向的情况下使用的属性 - (将附加到查询字符串) 和临时存储的 flash 属性,直到重定向后的请求为止. 请参阅 重定向属性和 Flash 属性. |
|
用于访问任何会话属性,与由于类级别 |
|
用于访问请求属性. 有关更多详细信息,请参阅 |
返回值
@ExceptionHandler
方法支持以下返回值:
Return value | Description |
---|---|
|
返回值通过 |
|
指定完整响应(包括 HTTP 头和主体) 的返回值将通过 |
|
要使用 |
|
用于与隐式模型一起呈现的 |
|
要添加到隐式模型的属性,通过 |
|
要添加到模型的属性,通过隐式确定视图名称. 请注意, |
|
要使用的视图和模型属性,以及(可选) 响应状态. |
|
如果具有 如果以上都不是真的,则 |
Any other return value |
任何与此表中任何早期值不匹配且返回值为 |
REST API 异常
REST 服务的一个常见要求是在响应正文中包含错误详细信息. Spring Framework 不会自动执行此操作,因为响应正文中的错误详细信息的表示是特定于应用程序的. 但是,@RestController
可以使用带有 ResponseEntity
返回值的 @ExceptionHandler
方法来设置响应的状态和正文. 这些方法也可以在 @ControllerAdvice
类中声明,以全局应用它们.
在响应主体中实现具有错误详细信息的全局异常处理的应用程序应考虑扩展 ResponseEntityExceptionHandler
, 它提供对 Spring MVC 引发的异常的处理,并提供钩子来定制响应主体. 要使用它,请创建 ResponseEntityExceptionHandler
的子类,使用 @ControllerAdvice
注解它,覆盖必要的方法,并将其声明为 Spring bean.
1.3.7. Controller Advice
@ExceptionHandler
, @InitBinder
, 和 @ModelAttribute
注解仅适用于 @Controller
类或类层次结构上声明 .相反,如果它们在 @ControllerAdvice
或 @RestControllerAdvice
类中声明,则它们适用于任何控制器。 此外,从 5.3 开始,@ControllerAdvice
中的 @ExceptionHandler
方法可用于处理来自任何 @Controller
或任何其他处理程序的异常。
@ControllerAdvice
使用 @Component
元注解,这意味着可以通过 组件扫描 将这些类注册为 Spring bean. @RestControllerAdvice
也是一个用 @ControllerAdvice
和 @ResponseBody
标记的元注解,这实际上意味着 @ExceptionHandler
方法将通过响应正文消息转换而不是通过 HTML 视图呈现其返回值.
在启动时,RequestMappingHandlerMapping
和 ExceptionHandlerExceptionResolver
检测控制器通知 bean 并在运行时应用它们。 来自 @ControllerAdvice
的全局`@ExceptionHandler` 方法在 after 来自`@Controller` 的本地方法之后应用。相比之下,全局 @ModelAttribute
和 @InitBinder
方法在 before 本地方法之前应用。
全局 @ExceptionHandler
方法(来自 @ControllerAdvice
) 在本地方法之后(来自 @Controller
) 应用. 相比之下,全局 @ModelAttribute
和 @InitBinder
方法在本地方法之前应用.
默认情况下,@ControllerAdvice
可以通过使用注解上的属性将其缩小到控制器的子集,如以下示例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3
前面示例中的选择器在运行时进行评估,如果广泛使用,可能会对性能产生负面影响. 有关更多详细信息,请参阅 @ControllerAdvice
javadoc .
1.4. 功能端点
Spring Web MVC 包含 WebMvc.fn,这是一个轻量级的函数编程模型,其中的函数用于路由和处理请求,而约定则是为不变性而设计的. 它是基于注解的编程模型的替代方案,但可以在同一 DispatcherServlet 上运行.
1.4.1. 概述
在 WebMvc.fn 中,使用 HandlerFunction
处理 HTTP 请求: 该函数接受 ServerRequest
并返回 ServerResponse
. 作为请求对象的请求都具有不可变的约定,这些约定为 JDK 8 提供了对 HTTP 请求和响应的友好访问. HandlerFunction
等效于基于注解的编程模型中 @RequestMapping
方法的主体.
传入的请求通过 RouterFunction
路由到处理程序函数: 该函数接受 ServerRequest
并返回可选的 HandlerFunction
(即 Optional<HandlerFunction>
) . 当路由器功能匹配时,返回处理程序功能. 否则为空的 Optional
. RouterFunction
等效于 @RequestMapping
注解,但主要区别在于路由器功能不仅提供数据,还提供行为.
RouterFunctions.route()
提供了一个有助于构建路由器的路由器构建器,如以下示例所示:
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
1 | 使用路由器 DSL 创建路由器. |
如果将 RouterFunction
注册为 Bean (例如,通过将其暴露在 @Configuration
类中) ,则 Servlet 将自动检测到它,如运行服务器所述.
1.4.2. HandlerFunction
ServerRequest
和 ServerResponse
是不可变的接口,它们提供 JDK 8 友好的 HTTP 请求和响应访问,包括请求头,正文,方法和状态码.
ServerRequest
ServerRequest
提供对 HTTP 方法,URI,请求头和查询参数的访问,而通过 body
方法提供对主体的访问.
下面的示例将请求正文提取为 String
:
String string = request.body(String.class);
val string = request.body<String>()
以下示例将主体提取到 List<Person>
,其中 Person 对象从序列化形式(例如 JSON 或 XML) 解码:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
以下示例显示如何访问参数:
MultiValueMap<String, String> params = request.params();
val map = request.params()
ServerResponse
ServerResponse
提供对 HTTP 响应的访问,由于它是不可变的,因此您可以使用 build
方法来创建它. 您可以使用构建器来设置响应状态,添加响应头或提供正文. 以下示例使用 JSON 内容创建 200 (OK) 响应:
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
以下示例显示了如何使用 Location
头且不包含主体来构建 201 (CREATED) 响应:
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
您还可以使用异步结果作为主体, CompletableFuture
, Publisher
或 ReactiveAdapterRegistry
支持的任何其他类型. 例如:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
如果不仅仅包含主体,还包含基于异步类型的状态和头部信息,您可以在 ServerResponse
使用静态的 async
方法,他接受 CompletableFuture<ServerResponse>
, Publisher<ServerResponse>
或 ReactiveAdapterRegistry
支持的其他异步类型,例如:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
可以通过 ServerResponse
的静态 sse
方法提供 Server-Sent Events . 该方法提供的 builder 允许您发送字符串或将其他对象作为 JSON 发送,例如:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
fun sse(): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Save the sseBuilder object somewhere..
}
}
// In some other thread, sending a String
sseBuilder.send("Hello world")
// Or an object, which will be transformed into JSON
val person = ...
sseBuilder.send(person)
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person)
// and done at some point
sseBuilder.complete()
处理 Classes
我们可以将处理程序函数编写为 lambda,如以下示例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
这很方便,但是在应用程序中我们需要多个功能,并且多个内联 lambda 可能会变得凌乱. 因此,将相关的处理程序功能分组到一个处理程序类中很有用,该类的作用与基于注解的应用程序中的 @Controller
相似. 例如,以下类暴露了 reactive Person
存储库:
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person));
}
else {
return ServerResponse.notFound().build();
}
}
}
1 | listPeople 是一个处理函数,它以 JSON 格式返回存储库中找到的所有 Person 对象. |
2 | createPerson 是一个处理函数,用于存储请求正文中包含的新 Person . |
3 | getPerson 是一个处理程序函数,它返回由 id 路径变量标识的单个人. 我们从存储库中检索该 Person 并创建一个 JSON 响应(如果找到) . 如果未找到,我们将返回 404 Not Found 响应. |
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
}
fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
1 | listPeople 是一个处理函数,它以 JSON 格式返回存储库中找到的所有 Person 对象. |
2 | createPerson 是一个处理函数,用于存储请求正文中包含的新 Person . |
3 | getPerson 是一个处理程序函数,它返回由 id 路径变量标识的单个人. 我们从存储库中检索该 Person 并创建一个 JSON 响应(如果找到) . 如果未找到,我们将返回 404 Not Found 响应. |
验证
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
1 | 创建 Validator 实例. |
2 | 应用 validation. |
3 | 引发 400 响应的异常. |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().build()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
1 | 创建 Validator 实例. |
2 | 应用 validation. |
3 | 引发 400 响应的异常. |
处理程序还可以通过基于 LocalValidatorFactoryBean
创建和注入全局 Validator
实例来使用标准 Bean 验证 API(JSR-303) . 请参阅Spring Validation.
1.4.3. RouterFunction
路由器功能用于将请求路由到相应的 HandlerFunction
. 通常,您不是自己编写路由器功能,而是使用 RouterFunctions
实用工具类上的方法来创建一个.
RouterFunctions.route()
(无参数) 为您提供了流式的生成器,用于创建路由器功能,而 RouterFunctions.route(RequestPredicate,HandlerFunction)
提供了直接创建路由器的方法.
通常,建议使用 route()
构建器,因为它为典型的映射方案提供了便捷的快捷方式,而无需发现静态导入. 例如,路由器功能构建器提供了 GET(String, HandlerFunction)
方法来为 GET 请求创建映射. 和 POST(String, HandlerFunction)
进行 POST 映射.
除了基于 HTTP 方法的映射外,路由构建器还提供了一种在映射到请求时引入其他断言的方法. 对于每个 HTTP 方法,都有一个重载的方法,它以 RequestPredicate
作为参数,尽管可以表达其他约束.
断言
您可以编写自己的 RequestPredicate
,但是 RequestPredicates
实用程序类根据请求路径,HTTP 方法,内容类型等提供常用的实现. 以下示例使用请求断言基于 Accept
头创建约束:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
您可以使用以下命令组合多个请求断言:
-
RequestPredicate.and(RequestPredicate)
— 两个都必须匹配. -
RequestPredicate.or(RequestPredicate)
— 只需要匹配一个.
RequestPredicates
中的许多断言都是组成的.
例如,RequestPredicates.GET(String)
由 RequestPredicates.method(HttpMethod)
和 RequestPredicates.path(String)
组成. 上面显示的示例还使用了两个请求断言,因为构建器在内部使用 RequestPredicates.GET
并将其与 accept
断言组合在一起.
路由
路由器功能按顺序评估: 如果第一个路由不匹配,则评估第二个路由,依此类推. 因此,在通用路由之前声明更具体的路由是有意义的. 当将路由器功能注册为 Spring Bean 时, 这一点也很重要, 这将在后面进行描述. 请注意,此行为不同于基于注解的编程模型,在该模型中,将自动选择 "最特定" 的控制器方法.
使用路由器功能生成器时,所有定义的路由都组成一个 RouterFunction
,从 build()
返回. 还有其他方法可以将多个路由器功能组合在一起:
-
RouterFunctions.route()
构建器上添加add(RouterFunction)
-
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— Router 带有嵌套RouterFunctions.route()
的RouterFunction.and()
的快捷方式.
以下示例显示了四种路由的组成:
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
1 | 带有与 JSON 匹配的 Accept 头的 GET /person/{id} 被路由到 PersonHandler.getPerson |
2 | 带有与 JSON 匹配的 Accept 头的 GET /person 被路由到 PersonHandler.listPeople |
3 | 没有其他断言的 POST POST /person 被路由到 PersonHandler.createPerson |
4 | otherRoute 是在其他地方创建的路由器功能,并将其添加到构建的路由中. |
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | 带有与JSON匹配的 Accept 头的 GET /person/{id} 被路由到 PersonHandler.getPerson |
2 | 带有与JSON匹配的 Accept 头的 GET /person 被路由到 PersonHandler.listPeople |
3 | 没有其他断言的 POST POST /person 被路由到 PersonHandler.createPerson |
4 | otherRoute 是在其他地方创建的路由器功能,并将其添加到构建的路由中. |
嵌入路由
一组路由功能通常具有一个共享断言,例如一个共享路径. 在上面的示例中,共享断言将是与 /person
匹配的路径断言,由三个路由使用. 使用注解时,您可以通过使用映射到 /person
的类型级别 @RequestMapping
注解来删除此重复项.
在 WebMvc.fn 中,可以通过路由器功能构建器上的 path
方法共享路径断言. 例如,以上示例的最后几行可以通过使用嵌套路由以以下方式进行改进:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
1 | 请注意,path 的第二个参数是使用路由器构建器的使用者. |
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但是您可以通过使用构建器上的 nest
方法来嵌套在任何种类的断言上. 上面的内容仍然包含一些以共享的 Accept-header
断言形式出现的重复. 通过将 nest
方法与 accept
一起使用,我们可以进一步改进:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
}
}
}
1.4.4. 运行服务器
通常,您可以通过 MVC Config 在基于 DispatcherHandler
的设置中运行路由器功能,该MVC 配置 使用 Spring 配置来声明处理请求所需的组件. MVC Java 配置声明以下基础结构组件以支持功能端点:
-
RouterFunctionMapping
: 在 Spring 配置中检测一个或多个RouterFunction<?>
bean,通过RouterFunction.andOther
组合它们,orders them,并将请求路由到生成的组成RouterFunction
. -
HandlerFunctionAdapter
:简单的适配器,使DispatcherHandler
调用映射到请求的HandlerFunction
.
前面的组件使功能端点适合于 DispatcherServlet
请求处理生命周期,并且(可能) 与带注解的控制器(如果已声明) 并排运行. 这也是 Spring Boot Web 启动程序如何启用功能端点的方式.
以下示例显示了 WebFlux Java 配置:
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
1.4.5. 过滤器处理程序功能
您可以使用路由功能构建器上的 before
,after
或 filter
方法来过滤处理程序函数. 使用注解,可以通过使用 @ControllerAdvice
,ServletFilter
或同时使用两者来实现类似的功能.
该过滤器将应用于构建器构建的所有路由. 这意味着在嵌套路由中定义的过滤器不适用于 "top-level" 路由. 例如,考虑以下示例:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 添加自定义请求头的 before 过滤器仅应用于两个 GET 路由. |
2 | 记录响应的 after 过滤器将应用于所有路由,包括嵌套路由. |
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST(handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
1 | 添加自定义请求头的 before 过滤器仅应用于两个 GET 路由. |
2 | 记录响应的 after 过滤器将应用于所有路由,包括嵌套路由. |
路由器构建器上的 filter
方法采用 HandlerFilterFunction
: 该函数采用 ServerRequest
和 HandlerFunction
并返回 ServerResponse
. handler
函数参数代表链中的下一个元素. 这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器.
现在,我们可以在路由中添加一个简单的安全过滤器,假设我们拥有一个可以确定是否允许特定路径的 SecurityManager
. 以下示例显示了如何执行此操作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用 next.handle(ServerRequest)
是可选的. 当允许访问时,我们仅允许执行处理函数.
除了在路由器功能构建器上使用 filter
方法之外,还可以通过 RouterFunction.filter(HandlerFilterFunction)
将过滤器应用于现有路由器功能.
通过专用的 CorsWebFilter . 提供对功能端点的 CORS 支持.
|
1.5. URI 链接
本部分介绍了 Spring 框架中可用于 URI 的各种选项.
1.5.1. UriComponents
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
有助于从 URI 模板变量构建 URI.
如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 | 带有 URI 模板的静态工厂方法 |
2 | 添加或替换 URI 组件. |
3 | 请求编码 URI 模板和 URI 变量. |
4 | 构建一个 UriComponents . |
5 | 暴露变量并获取 URI . |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
1 | 带有 URI 模板的静态工厂方法 |
2 | 添加或替换 URI 组件. |
3 | 请求编码 URI 模板和 URI 变量. |
4 | 构建一个 UriComponents . |
5 | 暴露变量并获取 URI . |
前面的示例可以合并到一个链中,并使用 buildAndExpand
缩短,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()
您可以通过直接转到 URI(这意味着编码) 来进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
您使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.5.2. UriBuilder
Spring MVC and Spring WebFlux
UriComponentsBuilder
实现了 UriBuilder
.
您可以使用 UriBuilderFactory
创建一个 UriBuilder
. UriBuilderFactory
和 UriBuilder
一起提供了一种可插入机制,可以根据共享配置(例如基本 URL,编码首选项和其他详细信息) 从 URI 模板构建 URI.
您可以使用 UriBuilderFactory
配置 RestTemplate
和 WebClient
,为自定义 URI 做准备. DefaultUriBuilderFactory
是 UriBuilderFactory
的默认实现,该实现在内部使用 UriComponentsBuilder
并暴露共享的配置选项.
以下示例显示如何配置 RestTemplate
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory
下面的示例配置一个 WebClient
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val client = WebClient.builder().uriBuilderFactory(factory).build()
此外,您也可以直接使用 DefaultUriBuilderFactory
.
它类似于使用 UriComponentsBuilder
,但它不是静态工厂方法,而是一个保存配置和首选项的实际实例,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.5.3. URI Encoding
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别暴露编码选项:
-
UriComponentsBuilder#encode(): 首先对 URI 模板进行预编码,然后在扩展时严格编码 URI 变量.
-
UriComponents#encode(): 扩展 URI 变量后对 URI 组件进行编码.
这两个选项都使用转义的八位字节替换非 ASCII 和非法字符. 但是,第一个选项还会替换出现在 URI 变量中的保留含义的字符.
考虑 ";",这在路径中是合法的但具有保留意义. 第一个选项取代 ";" 在URI变量中使用 "%3B",但 URI 模板中没有. 但在URI模板中没有. 相比之下,第二个选项永远不会替换 ";",因为它是路经中的合法字符. |
在大多数情况下, 第一个选项可能会产生预期结果, 因为它将 URI 变量视为要完全编码的不透明数据, 而第二个选项在 URI 变量包含保留字符的情况下很有用. 当根本不扩展 URI 变量时, 第二个选项也很有用, 因为它还会对偶然看起来像URI 变量的任何内容进行编码.
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到 URI (这意味着编码) 来缩短前面的示例,如以下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的 URI 模板进一步缩短它,如以下示例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
WebClient
和 RestTemplate
通过 UriBuilderFactory
策略在内部扩展和编码 URI 模板.
两者都可以配置自定义策略.
如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
DefaultUriBuilderFactory
实现在内部使用 UriComponentsBuilder
来扩展和编码 URI 模板.
作为工厂,它提供了一个单独的位置来配置编码方法,基于以下编码模式之一:
-
TEMPLATE_AND_VALUES
: 使用UriComponentsBuilder#encode()
(对应于较早列表中的第一个选项) 对URI模板进行预编码,并在扩展时严格编码 URI 变量. -
VALUES_ONLY
: 不对 URI 模板进行编码,而是在将其扩展到模板之前通过UriUtils#encodeUriVariables
对 URI 变量进行严格编码. -
URI_COMPONENT
: 在扩展 URI 变量后,使用UriComponents#encode()
(对应于先前列表中的第二个选项) 对 URI 组件值进行编码. -
NONE
: 未应用编码.
由于历史原因和向后兼容性,将 RestTemplate
设置为 EncodingMode.URI_COMPONENT
. WebClient
依赖于 DefaultUriBuilderFactory
中的默认值,该默认值已从 5.0.x 中的 EncodingMode.URI_COMPONENT
更改为 5.1 中的 EncodingMode.TEMPLATE_AND_VALUES
.
1.5.4. 相对请求
您可以使用 ServletUriComponentsBuilder
创建相对于当前请求的 URI,如以下示例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, path, and query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, path, and query string...
val uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123")
您可以创建相对于上下文路径的 URI,如以下示例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, and context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, and context path...
val uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri()
您可以创建相对于 Servlet 的 URI(例如 /main/*
) ,如以下示例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
val uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri()
从 5.1 开始,ServletUriComponentsBuilder 会忽略来自 Forwarded 和 X-Forwarded-* 头部的信息,这些头部信息指定了客户端发起的地址. 考虑使用ForwardedHeaderFilter 来提取和使用或丢弃此类请求头.
|
1.5.5. 控制器链接
Spring MVC 也提供了构造指定控制器方法链接的机制. 例如,以下 MVC 控制器允许创建链接:
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}
@Controller
@RequestMapping("/hotels/{hotel}")
class BookingController {
@GetMapping("/bookings/{booking}")
fun getBooking(@PathVariable booking: Long): ModelAndView {
// ...
}
}
您可以通过引用方法名字的办法来准备一个链接,如以下示例所示:
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
在前面的示例中,为方法参数准备了填充值(在本例中,long 值: 21
) ,以用于填充路径变量并插入到 URL 中. 此外,我们提供了值 42
,来填充任何剩余的 URI 变量,比如从类层级的请求映射中继承来的 hotel
变量. 如果方法还有更多的参数,你可以为那些不需要参与 URL 构造的变量赋予 null 值. 通常,只有 @PathVariable
和 @RequestParam
参数与构造 URL 有关.
还有其他使用 MvcUriComponentsBuilder
的方法. 例如,例如可以通过类似 mock 测试对象的方法,用代理来避免直接通过名字引用一个控制,如以下示例所示(该示例假定静态导入 MvcUriComponentsBuilder.on
) :
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
当控制器方法签名可用于 fromMethodCall 的链接创建时,其设计受到限制. 除了需要适当的参数签名外,返回类型还存在技术限制(即,为链接生成器调用生成运行时代理) ,因此返回类型不得为最终值. 特别是,视图名称的通用 String 返回类型在这里不起作用. 您应该改用 ModelAndView 甚至普通对象(具有 String 返回值) .
|
较早的示例在 MvcUriComponentsBuilder
中使用静态方法. 在内部,它们依靠 ServletUriComponentsBuilder
从当前请求的方案,主机,端口,上下文路径和 Servlet 路径准备基本 URL. 在大多数情况下,此方法效果很好. 但是,有时可能不足. 例如,您可能不在请求的上下文之内(例如,准备链接的批处理过程) ,或者您可能需要插入路径前缀(例如,从请求路径中删除且需要重新设置的语言环境前缀) . 插入链接) .
在这种情况下,可以使用静态的 fromXxx
重载方法,这些方法接受 UriComponentsBuilder
以使用基本 URL. 或者,您可以使用基本URL创建 MvcUriComponentsBuilder
的实例,然后使用基于实例的 withXxx
方法. 例如,以下清单使用 withMethodCall
:
UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")
val builder = MvcUriComponentsBuilder.relativeTo(base)
builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
从 5.1 开始,ServletUriComponentsBuilder 会忽略来自 Forwarded 和 X-Forwarded-* 头部的信息,这些头部信息指定了客户端发起的地址. 考虑使用ForwardedHeaderFilter 来提取和使用或丢弃此类请求头.
|
1.5.6. 链接到视图
在 Thymeleaf,FreeMarker 或 JSP 之类的视图中,您可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注解的控制器的链接. 考虑以下示例:
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
@RequestMapping("/people/{id}/addresses")
class PersonAddressController {
@RequestMapping("/{country}")
fun getAddress(@PathVariable country: String): HttpEntity<PersonAddress> { ... }
}
给定前面的控制器,可以按照以下方式准备来自 JSP 的链接,如下所示:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
前面的示例依赖于 Spring 标签库中声明的 mvcUrl
函数(即 META-INF/spring.tld
) ,但可以很容易地定义自定义函数或使用自定义标签文件.
这是如何工作的,在启动时,每个 HandlerMethodMappingNamingStrategy
都通过 @RequestMapping
分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如,ThingController
中的 getThing
方法变为 "TC#getThing") .
如果名称冲突,则可以使 用@RequestMapping(name ="..")
分配显式名称或实现自己的 HandlerMethodMappingNamingStrategy
.
1.6. 异步请求
Spring MVC 与 Servlet 3.0 异步请求 处理 具有广泛的集成:
-
在控制器方法中返回
DeferredResult
和Callable
,并为单个异步返回值提供基本支持. -
控制器可以使用 reactive clients 并返回 reactive types 以进行响应处理.
1.6.1. DeferredResult
一旦在 Servlet 容器中 启用 了异步请求处理功能,控制器方法就可以使用 DeferredResult
包装任何受支持的控制器方法返回值,如以下示例所示:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// Save the deferredResult somewhere..
return deferredResult
}
// From some other thread...
deferredResult.setResult(result)
控制器可以从不同的线程异步生成返回值 - 例如,响应外部事件( JMS 消息) ,计划任务或其他事件.
1.6.2. Callable
控制器可以使用 java.util.concurrent.Callable
包装任何支持的返回值,如以下示例所示:
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
// ...
"someView"
}
然后可以通过配置的 configured TaskExecutor
运行给定任务来获取返回值.
1.6.3. Processing
以下是 Servlet 异步请求处理的简要概述:
-
Servlet 请求
ServletRequest
可以通过调用request.startAsync()
方法而进入异步模式. 这样做的主要结果就是该 Servlet 以及所有的过滤器都可以结束,但其响应(response) 会等待异步处理结束后再返回调用. -
request.startAsync()
方法会返回一个AsyncContext
对象 ,可用它对异步处理进行进一步的控制和操作. 比如说它也提供了一个与转向(forward) 很相似的dispatch
方法,只不过它允许应用恢复 Servlet 容器的请求处理进程. -
ServletRequest
提供了获取当前DispatcherType
的方式,后者可以用来区别当前处理的是原始请求、异步分发请求、转向、或是其他类型的请求分发类型.
DeferredResult
处理的工作方式如下:
-
控制器先返回一个
DeferredResult
对象,并把它存取在内存(队列或列表等) 中以便存取. -
Spring MVC调用
request.startAsync()
方法,开始进行异步处理. -
DispatcherServlet
和所有过滤器都退出 Servlet 容器线程,但此时方法的响应对象仍未返回. -
由处理该请求的线程对
DeferredResult
进行设值,然后 Spring MVC 会重新把请求分派回 Servlet 容器,恢复处理. -
DispatcherServlet
再次被调用, 恢复对该异步返回结果的处理.
Callable
处理的工作方式如下:
-
控制器先返回一个
Callable
对象. -
Spring MVC调用
request.startAsync()
方法,开始进行异步处理,并把该Callable
对象提交给另一个独立线程的 ActuatorTaskExecutor
处理. -
DispatcherServlet
和所有过滤器都退出 Servlet 容器线程,但此时方法的响应对象仍未返回. -
Callable
对象最终产生一个返回结果,此时 Spring MVC 会重新把请求分派回 Servlet 容器,恢复处理. -
DispatcherServlet
再次被调用,恢复对Callable
异步处理所返回结果的处理.
有关更多背景知识,您还可以阅读在 Spring MVC 3.2 中引入了异步请求处理支持的 博客文章.
异常处理
若方法返回的是一个 DeferredResult
对象,你可以选择调 Exception 实例的 setResult
方法还是 setErrorResult
方法. 在这两种情况下,Spring MVC 都会将请求发送回 Servlet 容器以完成处理. 然后将其视为控制器方法返回给定值或者就好像它产生了给定的异常一样. 然后异常通过常规异常处理机制(例如,调用 @ExceptionHandler
方法) . 更具体地说呢,当 Callable
抛出异常时,Spring MVC 会把一个 Exception
对象分派给 Servlet 容器进行处理,而不是正常返回方法的返回值,然后容器恢复对此异步请求异常的处理.
当您使用 Callable
时,会出现类似的处理逻辑,主要区别在于从 Callable
返回结果,或者由它引发异常.
拦截
处理器拦截器 HandlerInterceptor
可以实现 AsyncHandlerInterceptor
接口拦截异步请求,因为在异步请求开始时,被调用的回调方法是该接口的 afterConcurrentHandlingStarted
方法,而非一般的 postHandle
和 afterCompletion
方法.
如果需要与异步请求处理的生命流程有更深入的集成,比如需要处理 timeout 的事件等. 则 HandlerInterceptor
需要注册 CallableProcessingInterceptor
或 DeferredResultProcessingInterceptor
拦截器, 具体的细节可以参考 AsyncHandlerInterceptor
类的 Java 文档
DeferredResult
类还提供了 onTimeout(Runnable)
和 onCompletion(Runnable)
等回调, 具体的细节可以参考 javadoc of DeferredResult
类的 Java 文档 Callable
可以替代 WebAsyncTask
,它暴露了超时和完成回调的其他方法.
与 WebFlux 相比
Servlet API 最初是为通过 Filter-Servlet 链进行一次传递而构建的. Servlet 3.0 中添加了异步请求处理,使应用程序可以退出 Filter-Servlet 链,但保留响应以进行进一步处理. Spring MVC 异步支持围绕该机制构建.
当控制器返回 DeferredResult
时,退出 Filter-Servlet 链,并释放 Servlet 容器线程. 稍后,在设置 DeferredResult
时,将进行 ASYNC
调度(到相同的 URL) ,在此期间,控制器将再次映射,但不是调用它,而是使用 DeferredResult
值(就像控制器返回了它) 来恢复处理.
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这种异步请求处理功能,因为它在设计上是异步的. 异步处理已内置在所有框架协定中,并在请求处理的所有阶段得到内在支持.
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步和 Reactive Types(响应式类型) 作为控制器方法中的返回值. Spring MVC 甚至支持流式传输,包括响应性背压. 但是,与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O,并且每次写入都不需要额外的线程, 因此对响应的单个写入仍然处于阻塞状态(并在单独的线程上执行) .
另一个基本区别是,Spring MVC 在控制器方法参数中不支持异步或响应类型(例如,@RequestBody
,@RequestPart
等) ,也没有对异步和响应类型作为模型属性的任何显式支持. Spring WebFlux 确实支持所有这些.
1.6.4. HTTP 流
您可以将 DeferredResult
和 Callable
用于单个异步返回值. 如果要生成多个异步值并将其写入响应,该怎么办? 本节介绍如何执行此操作.
Objects
您可以使用 ResponseBodyEmitter
返回值来生成对象流,其中每个对象都使用 HttpMessageConverter
进行序列化并写入响应,如以下示例所示:
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
ResponseBodyEmitter
也可以被放到 ResponseEntity
体里面使用,这可以对响应状态和响应头做一些定制.
当 emitter
抛出 IOException
时(例如,如果远程客户端消失) ,应用程序不负责清理连接,不应调用 emitter.complete
或 emitter.completeWithError
. 相反,servlet 容器会自动启动 AsyncListener
错误通知,其中 Spring MVC 进行 completeWithError
调用. 反过来,此调用会对应用程序执行一次最终 ASYNC
调度,在此期间,Spring MVC 将调用已配置的异常解析程序并完成请求.
SSE
SseEmitter
(ResponseBodyEmitter
的子类) 为 Server-Sent Events 提供支持,其中从服务器发送的事件根据 W3C SSE 规范进行格式化. 要从控制器生成 SSE 流,请返回 SseEmitter
,如以下示例所示:
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
虽然 SSE 是流式传输到浏览器的主要选项,但请注意 Internet Explorer 不支持 Server-Sent Events. 考虑将 Spring 的 WebSocket messaging 传递与针对各种浏览器的 SockJS fallback传输(包括 SSE) 一起使用.
有关异常处理的说明,另请参见 上一节 .
Raw Data
有时,跳过消息转换的阶段,直接把数据写回响应的输出流 OutputStream
可能更有效,比如文件下载这样的场景,这可以通过返回一个 StreamingResponseBody
类型的对象来实现,如以下示例所示:
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
@GetMapping("/download")
fun handle() = StreamingResponseBody {
// write...
}
StreamingResponseBody
也可以被放到 ResponseEntity
体里面使用,这可以对响应状态和响应头做一些定制.
1.6.5. Reactive Types(响应式类型)
如果使用 spring-webflux
中的响应式 WebClient
,或其他客户端(也可以阅读 WebFlux 部分中的Reactive Libraries ) ,又或者是带响应式支持的数据存储,开发者可以直接从 Spring MVC 控制器方法返回响应式类型.
Reactive 返回值的处理方式如下:
-
如果返回类型有
single-value
流的语义,如Mono
(Reactor) 或Single
(RxJava),那么它是适配并等效于DeferredResult
. -
如果返回类型有 multi-value 流的语义,如
Flux
(Reactor)或Observable
(RxJava),并且如果媒体类型也表示为流,(例如,application/x-ndjson
或text/event-stream
) . 则它是适配并等效于使用ResponseBodyEmitter
或SseEmitter
. 还可以返回Flux<ServerSentEvent>
或Observable<ServerSentEvent>
. -
如果返回类型 multi-value 流的语义,但媒体类型并不表示为流. 例如
application/json
,则它是适配并等效于使用DeferredResult<List<?>>
.
Spring MVC 对使用中的响应式库进行了适配 – 例如,预计有多少值,这是在 spring-core 包的 ReactiveAdapterRegistry 的帮助下完成的. 它为响应式和异步类型提供可插拔的支持. 注册表内置了对 RxJava 的支持,但其他可以注册.
|
对于流式传输到响应,支持响应式响应,但是对响应的写入仍然是阻塞的,并且通过configured TaskExecutor
在单独的线程上执行, 以避免阻塞上游源(例如从 WebClient
返回的 Flux
) .
默认情况下,SimpleAsyncTaskExecutor
用于阻塞写入,但在加载时不适用. 如果计划使用响应类型进行流式处理,则应使用MVC configuration来配置任务执行程序.
1.6.6. 断开
当远程客户端消失时,Servlet API 不提供任何通知. 因此,在通过 stream 传输到响应时,无论是通过SseEmitter还是 reactive types,定期发送数据都很重要, 因为如果客户端断开连接,写入将失败. 发送可以采用空(仅限注解) SSE 事件或另一方必须解释为心跳并忽略的任何其他数据的形式.
或者,考虑使用具有内置心跳机制的 Web 消息传递解决方案(例如基于WebSocket 的 STOMP 或具有SockJS的 WebSocket) .
1.6.7. 配置
必须在 Servlet 容器级别启用异步请求处理功能. MVC 配置还暴露了异步请求的几个选项.
Servlet 容器
Filter 和 Servlet 声明具有 asyncSupported
标志,需要将其设置为 true
以启用异步请求处理. 此外,应声明 Filter
映射以处理 ASYNC
javax.servlet.DispatchType
.
在 Java 配置中,当您使用 AbstractAnnotationConfigDispatcherServletInitializer
初始化 Servlet 容器时,这将自动完成.
在 web.xml
配置中,您可以将 <async-supported>true</async-supported>
添加到 DispatcherServlet
和 Filter
声明,并添加 <dispatcher>ASYNC</dispatcher>
以过滤映射.
Spring MVC
MVC 配置暴露以下与异步请求处理相关的选项:
-
Java configuration: 在
WebMvcConfigurer
上使用configureAsyncSupport
回调. -
XML namespace: 使用
<mvc:annotation-driven>
下的<async-support>
元素.
您可以配置以下内容:
-
异步请求的默认超时值(如果未设置) 取决于底层Servlet容器.
-
AsyncTaskExecutor
用于在使用Reactive Types 进行流式处理时阻止写入,以及用于执行从控制器方法返回的Callable
实例. 如果您使用 reactive types 进行流式传输或者具有返回Callable
的控制器方法, 我们强烈建议您配置此属性,因为默认情况下,它是SimpleAsyncTaskExecutor
. -
DeferredResultProcessingInterceptor
实现和CallableProcessingInterceptor
实现.
请注意,您还可以在 DeferredResult
, ResponseBodyEmitter
和 SseEmitter
上设置默认超时值. 对于 Callable
,您可以使用 WebAsyncTask
来提供超时值.
1.7. CORS
Spring MVC 允许您处理 CORS (跨源资源共享) . 本节介绍如何执行此操作.
1.7.1. 简介
出于安全原因,浏览器禁止对当前源外的资源进行 AJAX 调用. 例如,您可以将您的银行帐户放在一个标签页中,将 evil.com 放在另一个标签页中. 来自 evil.com 的脚本不应该使用您的凭据向您的银行 API 发出 AJAX 请求 - 例如从您的帐户中提取资金!
1.7.2. 处理
CORS 规范区分了预检查,简单和实际请求. 要了解 CORS 的工作原理,您可以 阅读本文 以及其他许多内容,或者查看规范以获取更多详细信息.
Spring MVC HandlerMapping
为实现 CORS 提供内置支持. 成功将请求映射到处理程序后,HandlerMapping
实现检查给定请求和处理程序的 CORS 配置并采取进一步操作. 直接处理预检查请求,同时拦截,验证简单和实际的 CORS 请求,并设置所需的 CORS 响应头.
为了启用跨源请求(即,存在 Origin
头并且与请求的主机不同) ,您需要具有一些显式声明的 CORS 配置. 如果未找到匹配的 CORS 配置,则拒绝预检请求. 没有 CORS 头添加到简单和实际 CORS 请求的响应中,因此浏览器拒绝它们.
可以使用基于 URL 模式的 CorsConfiguration
映射单独 configured 每个 HandlerMapping
. 在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 HandlerMapping
实例.
您可以将 HandlerMapping
级别的全局 CORS 配置与更细粒度的处理程序级 CORS 配置相结合. 例如,带注解的控制器可以使用类或方法级别的 @CrossOrigin
注解(其他处理程序可以实现 CorsConfigurationSource
) .
组合全局和本地配置的规则通常是附加的 - 例如,所有全局和所有本地源. 对于只能接受单个值的属性(例如 allowCredentials
和 maxAge
) , 本地会覆盖全局值. 有关详细信息,请参阅 CorsConfiguration#combine(CorsConfiguration)
.
要从 source 中了解更多信息或进行高级自定义,请查看后面的代码:
|
1.7.3. @CrossOrigin
在带注解的控制器方法上使用 @CrossOrigin
注解启用跨源请求,如以下示例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
-
All origins.
-
All headers.
-
All HTTP methods to which the controller method is mapped.
默认情况下不启用 allowCredentials
,因为它建立了一个信任级别,该信任级别暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌) ,并且只应在适当的地方使用.启用后, 必须将 allowOrigins
设置为一个或多个特定 domain (而不是特殊值 "*"
) , 或者可以使用 allowOriginPatterns
属性来动态匹配一组 origins.
maxAge
设置为30 分钟.
@CrossOrigin
在类级别也受支持,并且由所有方法继承,如以下示例所示:
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
您可以在类级别和方法级别使用 @CrossOrigin
,如以下示例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
1.7.4. 全局配置
除了细粒度,基于注解的配置以外,您可能还希望定义一些全局 CORS 配置. 您可以在任何 HandlerMapping
上单独设置基于 URL 的 CorsConfiguration
映射. 但是,大多数应用程序使用 MVC Java 配置或 MVC XNM 命名空间来执行此操作.
默认情况下,全局配置启用以下内容:
-
All origins.
-
All headers.
-
GET
,HEAD
, andPOST
methods.
默认情况下不启用 allowCredentials
,因为它建立了一个信任级别,该信任级别暴露敏感的用户特定信息(例如 cookie 和 CSRF 令牌) ,并且只应在适当的地方使用.启用后, 必须将 allowOrigins
设置为一个或多个特定 domain (而不是特殊值 "*"
) , 或者可以使用 allowOriginPatterns
属性来动态匹配一组 origins.
maxAge
设置为 30 分钟.
Java 配置
要在 MVC Java 配置中启用 CORS,可以使用 CorsRegistry
回调,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
XML 配置
要在 XML 命名空间中启用 CORS,可以使用 <mvc:cors>
元素,如以下示例所示:
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
1.7.5. CORS 过滤器
您可以通过内置的 CorsFilter
应用 CORS 支持.
如果您尝试将 CorsFilter 与 Spring Security 一起使用,请记住 Spring Security 内置 了对 CORS 的支持.
|
要配置过滤器,请将 CorsConfigurationSource
传递给其构造函数,如以下示例所示:
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
val filter = CorsFilter(source)
1.8. Web 安全
Spring Security项目为保护 Web 应用程序免受恶意攻击提供支持. 请参阅 Spring Security 参考文档,包括:
HDIV 是另一个与 Spring MVC 集成的 Web 安全框架.
1.9. HTTP 缓存
HTTP 缓存可以显着提高 Web 应用程序的性能. HTTP 缓存围绕 Cache-Control
响应头,随后是条件请求头(例如 Last-Modified
和 ETag
) . HTTP 的响应头 Cache-Control
主要帮助私有缓存(比如浏览器端缓存) 和公共缓存(比如代理端缓存) 了解它们应该如果缓存HTTP响应.
如果内容未更改,则 ETag
头用于生成条件请求, 该条件请求可能导致 304(NOT_MODIFIED) 没有正文. 可以认为它是 Last-Modified
头的一个更精细的后续版本.
本节介绍 Spring Web MVC 中可用的与 HTTP 缓存相关的选项.
1.9.1. CacheControl
CacheControl
支持配置与 Cache-Control
头相关的设置,并在许多地方被接受为参数:
虽然 RFC 7234 描述了 Cache-Control
响应头的所有可能的指令,但 CacheControl
类型采用面向用例的方法,该方法侧重于常见场景:
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
WebContentGenerator
还接受一个更简单的 cachePeriod
属性(以秒为单位定义) ,其工作方式如下:
-
A
-1
值不会生成Cache-Control
的响应头. -
A
0
值将防止缓存使用'Cache-Control:no-store'
指令. -
An
n > 0
一个大于0的值将缓存给定的响应在n
秒使用'Cache-Control:max-age=n'
1.9.2. Controllers
控制器可以添加对 HTTP 缓存的显式支持. 我们建议这样做,因为资源的 lastModified
或 ETag
值需要先计算才能与条件请求头进行比较. 控制器可以向 ResponseEntity
添加 ETag
头和 Cache-Control
设置,如以下示例所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {
val book = findBook(id);
val version = book.getVersion()
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book)
}
如果与条件请求头的比较表明内容未更改,则前面的示例发送带有空响应体的 304 (NOT_MODIFIED) 响应. 否则,ETag
和 Cache-Control
头将添加到响应中.
您还可以对控制器中的条件请求头进行检查,如以下示例所示:
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ... (1)
if (request.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
1 | 特定于应用的计算. |
2 | 响应已设置为 304(NOT_MODIFIED) - 无需进一步处理. |
3 | 继续请求处理. |
@RequestMapping
fun myHandleMethod(request: WebRequest, model: Model): String? {
val eTag: Long = ... (1)
if (request.checkNotModified(eTag)) {
return null (2)
}
model[...] = ... (3)
return "myViewName"
}
1 | 特定于应用的计算. |
2 | 响应已设置为 304(NOT_MODIFIED) - 无需进一步处理. |
3 | 继续请求处理. |
有三种变体可用于检查针对 eTag
值,lastModified
值或两者的条件请求. 对于条件 GET
和 HEAD
请求, 您可以将响应设置为 304(NOT_MODIFIED) . 对于 POST
, PUT
, 和 DELETE
,您可以将响应设置为 412(PRECONDITION_FAILED) ,以防止并发修改.
1.9.4. ETag
过滤器
您可以使用 ShallowEtagHeaderFilter
添加从响应内容计算的 “shallow” eTag
值,从而节省带宽但不节省CPU时间. 见 Shallow ETag.
1.10. 视图技术
无论您决定使用 Thymeleaf,Groovy 标签模板,JSP 还是其他技术,Spring MVC 中视图技术的使用都是可配置的, 主要是配置更改的问题. 本章介绍了与 Spring MVC 集成的视图技术. 我们假设您已经熟悉 View Resolution.
Spring MVC 应用程序的视图应用于内部可信的应用中. 因为视图可以访问应用程序上下文中的所有 bean. 因此,不建议在以下情况的应用程序中使用 Spring MVC 的模板支持: 模板可由外部源编辑,因为这可能会带来安全隐患. |
1.10.1. Thymeleaf
Thymeleaf 是一个现代服务器端 Java 模板引擎,它强调可以通过双击在浏览器中预览的自然 HTML 模板,这对于 UI 模板的独立工作(例如,由设计人员) 非常有用,而无需运行服务器. 如果您想要替换 JSP, Thymeleaf 提供了一组最广泛的功能,使这种转换更容易. Thymeleaf 积极开发和维护. 有关更完整的介绍,请参阅 Thymeleaf 项目主页.
Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理. 配置涉及一些 bean 声明, 例如 ServletContextTemplateResolver
, SpringTemplateEngine
, 和 ThymeleafViewResolver
. 有关详细信息,请参阅 Thymeleaf+Spring.
1.10.2. FreeMarker
Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件和其他的任何类型的文本输出. Spring Framework 有一个内置的集成,可以将 Spring MVC 与 FreeMarker 模板结合使用.
View 配置
以下示例显示如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/WEB-INF/freemarker")
}
}
以下示例显示如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
或者,您也可以声明 FreeMarkerConfigurer
bean 以完全控制所有属性,如以下示例所示:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>
模板需要存储在上面所示的 FreeMarkerConfigurer
指定的目录中,根据前面的配置,如果您的控制器返回 welcome
视图名称,解析器将查找 /WEB-INF/freemarker/welcome.ftl
模板.
FreeMarker 配置
通过设置 FreeMarkerConfigurer
bean可以将 FreeMarker
的 'Settings' 和 'SharedVariables' 值直接传递 Spring 管理的 FreeMarker
对象. freemarkerSettings
属性需要 java.util.Properties
对象. 而 freemarkerVariables
属性需要 java.util.Map
. 以下示例显示了如何执行此操作:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
有关更多的 Configuration
内容的设置和变量可以查看 FreeMarker 文档
表单处理
Spring 本身提供了用于 JSP 的标签库,其中包含(当然还有很多) <spring:bind/>
标签,这个标签用来展示从 Web 上的 Validator
或业务层抛出的失败验证表单. Spring 还支持 FreeMarker 中的相同功能,并提供了方便的宏来生成表单输入元素.
绑定宏命令
spring-webmvc.jar
包文件包含 Velocity 和 FreeMarker 的一组标准宏,因此两者都适用.
Spring库中定义的某些宏被认为是内部的(私有的) ,但在宏定义中不存在这样的范围,其实所有宏都可以在调用代码和用户模板时看到. 以下各节仅集中于需要从模板中直接调用的宏, 如果希望直接查看宏代码, 那么可以看文件 spring.ftl
, 定义在 org.springframework.web.servlet.view.freemarker
包中.
简单的绑定
HTML 表单(vm 或 ftl 模板),充当了 Spring MVC 控制器的表单视图,可以使用类似下面的代码绑定字段值,也可以类似 JSP 那样在每个输入字段后面添加错误信息. 以下示例显示了之前配置的 personForm
视图:
<!-- FreeMarker macros have to be imported into a namespace.
We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>
<@spring.bind>
需要一个包含命令对象的 'path' 参数(默认是 'command',除非在 FormController 属性中被改变了) ,后面跟着写需要绑定到命令对象上的字段名. 可以使用嵌套字段,例如 command.address.street
,绑定宏可以在 web.xml
中设置 ServletContext
的参数 defaultHtmlEscape
,用于定义 HTML 的转义行为.
<@spring.bindEscaped>
宏命令是可选的,它接收第二个参数并显式地指定是否应在状态错误消息或值中使用 HTML 转义. 按需设置为 true
或 false
,还有很多其它的宏,它们将在下一节中介绍.
输入宏命令
Velocity 和 FreeMarker 都使用宏简化了绑定和表单的生成(包括验证错误的显示) ,没有必要使用这些宏来生成表单输入字段,实际上他们都可以直接绑定在简单的 HTML 中,并且可混合使用.
下表中的可用宏显示了 FTL 定义和每个参数列表:
宏命令 | FTL 定义表 |
---|---|
|
<@spring.message code/> |
|
<@spring.messageText code, text/> |
|
<@spring.url relativeUrl/> |
|
<@spring.formInput path, attributes, fieldType/> |
|
<@spring.formHiddenInput path, attributes/> |
|
<@spring.formPasswordInput path, attributes/> |
|
<@spring.formTextarea path, attributes/> |
|
<@spring.formSingleSelect path, options, attributes/> |
|
<@spring.formMultiSelect path, options, attributes/> |
|
<@spring.formRadioButtons path, options separator, attributes/> |
|
<@spring.formCheckboxes path, options, separator, attributes/> |
|
<@spring.formCheckbox path, attributes/> |
|
<@spring.showErrors separator, classOrStyle/> |
在FTL(FreeMarker) 中, formHiddenInput 和 formPasswordInput 这两个宏实际上并不需要,因为可以使用普通的 formInput 宏. 将 hidden 或 password 指定为 fieldType 参数的值
|
上述任何宏的参数都具有一致的含义
-
path
: 要绑定到的字段的名称(例如 "command.name") -
options
: 可从输入字段中选择的所有可用值的映射,map
的键表示从表单POST
后得到的对象的值(已绑定的) ,Map
对象保存这些键用于返回值后能在表单上显示出来. 通常这样map
由控制器提供数据,任何map都可以实现按需使用,可以使用SortedMap
,例如TreeMap
和适当的Comparator
为所有的值排序,使用来自commons-collections
包中的LinkedHashMap
或LinkedMap
也是相同的原理. -
separator
: 多个选项可以作为元素(单选按钮或复选框) 可以使用标签对字符序列进行分隔(例如<br>
) . -
attributes
: HTML标签本身中可以包含任意标签或文本的附加字符串. 字符串与上面的宏分别对应,例如,在一个文本字段提供属性'rows="5" cols="60"'
字段, 也可以添加css,例如'style="border:1px solid silver"'
. -
classOrStyle
: 对于showErrors
宏, 可以使用 span 标签包装每个错误的 CSS 类的名称. 如果未提供任何信息 (或该值为空) ,则错误将包含在<b></b>
标签中
以下部分概述了宏的示例(一些在 FTL 中,一些在 VTL 中) . 如果两种语言之间存在使用差异,则会在说明中对其进行说明.
formInput
宏采用 path
参数(command.name
) 和附加 attributes
参数(在下一个示例中为空) . 宏与所有其他表单生成宏一起在 path 参数上执行隐式 Spring 绑定. 在出现新绑定之前, 前一个绑定仍然有效,因此 showErrors
宏不需要再次传递 path
参数,它只对上次为其创建绑定的任何字段进行操作.
showErrors
宏采用分隔符参数(将用于分隔给定字段上的多个错误的字符,同时还接受第二个参数: 类名或样式属性. 请注意,FreeMarker
能够为属性参数指定默认值,这与 Velocity
不同, 以下示例显示如何使用 formInput
和 showErrors
宏:
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>
下一个示例显示表单片段的输出,生成名称字段并在提交表单后在字段中没有值时显示验证错误. 验证通过 Spring 的验证框架进行.
生成的 HTML 类似于以下示例:
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>
formTextarea
宏类似于 formInput
宏,连接收的参数都是相同的. 通常,第二个参数(attributes
) 将被使用用于传递格式信息或 rows
和 cols
的属性.
有四个字段宏可以用于生产 HTML 表单中的公共 UI 值作为选择的输入:
-
formSingleSelect
-
formMultiSelect
-
formRadioButtons
-
formCheckboxes
这四个宏都可以从表单字段中接收 Map
,其实需要的就是标签的值. 当然值和标签是可以取相同的名.
下一个例子是 FTL 中的单选按钮. 表单使用 'London' 作为这个字段的默认值,因此不需用进行验证. 当渲染表单时,要选择的整个城市列表都在 'cityMap' 中,cityMap
是数据模型. 以下清单显示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
前面的列表呈现一行单选按钮,一个用于 cityMap
中的每个值,并使用分隔符 ""
. 没有提供其他属性(缺少宏的最后一个参数) . cityMap
对Map中的每个键值对使用相同的 String
. 映射的键是表单实际提交为 POST
请求参数的键. map 值是用户看到的标签. 在前面的示例中,给定一个包含三个众所周知的城市的列表以及表单支持对象中的默认值,HTML 类似于以下内容:
Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>
如果您的应用程序希望通过内部代码来处理城市,可以写一个 name 为 cityMap 的 Map 传递给模板,如下面的例子:
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");
Map<String, Object> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
protected fun referenceData(request: HttpServletRequest): Map<String, *> {
val cityMap = linkedMapOf(
"LDN" to "London",
"PRS" to "Paris",
"NYC" to "New York"
)
return hashMapOf("cityMap" to cityMap)
}
代码将按你的设置输出,可以看到更多的城市名字.
Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>
HTML 转义
由于 HTML 的版本问题,上面的表单宏在 HTML 的 4.01 版本中需要使用到转义,转义可以在 web.xml
中通过 Spring 的绑定来定义. 为了使标签遵守 XHTML 的规定以及覆盖默认的 HTML 转义值, 可以在模板中定义两个变量(或者使你的模型设置为模板可见形式) . 在模板中指定的优点是: 它们可以在模板处理后更改为不同的值,以便为表单中的不同字段提供不同的行为.
要切换为标签的 XHTML 合规性,请为名为 xhtmlCompliant
的模型或上下文变量指定值 true
,如以下示例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理完该指令后,Spring 宏生成的任何元素现在都符合 XHTML 标准.
以类似的方式,您可以指定每个字段的 HTML 转义,如以下示例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
1.10.3. Groovy Markup
Groovy标签模板引擎主要用于生成类似 XML 的标签(XML,XHTML,HTML5 等) ,但您可以使用它来生成任何基于文本的内容. Spring Framework 有一个内置的集成,可以将 Spring MVC 与 Groovy Markup 结合使用.
目前要求使用 Groovy 2.3.1+ 的版本. |
配置
以下示例显示如何配置 Groovy 标签模板引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
以下示例显示如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
1.10.4. 脚本视图
Spring Framework 有一个内置的集成,可以将 Spring MVC 与任何可以在 JSR-223 Java 脚本引擎之上运行的模板库一起使用. 我们在不同的脚本引擎上测试了以下模板库:
Scripting Library | Scripting Engine |
---|---|
集成任何其他脚本引擎的基本规则是它必须实现 ScriptEngine 和 Invocable 接口.
|
要求
您需要在类路径上安装脚本引擎,其详细信息因脚本引擎而异:
-
Nashorn Javascript 引擎提供了内置的 Java 8+. 强烈建议使用最新的可用更新版本.
-
为了获得 JRuby 支持,应添加 JRuby 依赖性
-
为了获得 Jython 支持,应添加 Jython 依赖性.
-
org.jetbrains.kotlin:kotlin-script-util
依赖和包含在META-INF/services/javax.script.ScriptEngineFactory
文件里的org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
行应添加到 Kotlin 脚本支持中. 有关详细信息,请参阅此 示例 .
还需要为基于脚本的模板引擎添加依赖. 例如,对于 JavaScript,可以使用 WebJars.
脚本模板
您可以声明 ScriptTemplateConfigurer
bean 以指定要使用的脚本引擎,要加载的脚本文件,要调用以呈现模板的函数,等等. 以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
以下示例显示了 XML 中的相同排列:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>
对于 Java 和 XML 配置,控制器看起来没有什么不同,如以下示例所示:
@Controller
public class SampleController {
@GetMapping("/sample")
public String test(Model model) {
model.addAttribute("title", "Sample title");
model.addAttribute("body", "Sample body");
return "template";
}
}
@Controller
class SampleController {
@GetMapping("/sample")
fun test(model: Model): String {
model["title"] = "Sample title"
model["body"] = "Sample body"
return "template"
}
}
以下示例显示了 Mustache 模板:
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
使用以下参数调用 render 函数:
-
String template
: 模板内容 -
Map model
: 视图模型 -
RenderingContext renderingContext
:RenderingContext
提供对应用程序上下文,区域设置,模板加载器和 URL 的访问(自 5.0 起) .
Mustache.render()
方法会与本地兼容,因此可以直接调用.
如果模板化技术需要自定义,则可以提供实现自定义渲染函数的脚本. 例如, Handlerbars 需要在使用模板之前进行编译,并且需要使用 polyfill 以模拟服务器端脚本引擎中不可用的某些浏览器功能.
以下示例显示了如何执行此操作:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
当要求非线程安全地使用脚本引擎时,需要将 sharedEngine 的属性设置为 false ,因为模板库不是为了并发而设计的,具体可以看运行在 Nashorn 上的 Handlerbars 或 react. 据此,需要 Java 8u60+ 的版本来修复这个 this bug.
|
polyfill.js
只需定义一个 window
对象,就可以被 Handlerbars 运行,如下所示:
var window = {};
脚本 render.js
会在使用该模板之前被编译,一个好的产品应当保存和重用模板(使用缓存的方法) ,这样高效些. 这可以在脚本中完成,并且可以自定义它(例如管理模板引擎配置. 以下示例显示了如何执行此操作:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.10.5. JSP 和 JSTL
Spring 为 JSP 和 JSTL 视图提供了一些现成的解决方案
视图解析
使用 JSP 进行开发时,可以声明 InternalResourceViewResolver
bean.
InternalResourceViewResolver
可用于分发到任何 Servlet 资源, 尤其是 JSP. 作为最佳实践, 我们强烈建议您将 JSP 文件放在 'WEB-INF'
目录下的目录中, 以便客户端无法直接访问.
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
Spring 的 JSP 标签库
Spring 提供了请求参数与命令对象的数据绑定,如前面章节所述. 为了方便开发 JSP 页面,结合这些数据绑定功能,Spring 提供了一些使事情变得更容易的标签. 所有的 Spring 标签都 haveHTML 转义功能以启用或禁用字符转义.
spring.tld
标签库描述符(TLD) 在 spring-webmvc.jar
包中. 更多的信息,请浏览 API 参考 或查看标签库说明.
Spring 的表单标签库
从 2.0 版本开始, Spring 在使用 JSP 和 Spring Web MVC 时为处理表单元素提供了一套完整的数据绑定识别标签. 每个标签都支持其相应的 HTML 标签对应的属性集,使标签熟悉和直观地使用,标签生成的 HTML 4.01/XHTML 1.0 兼容.
不同于其他的表单或输入标签库,Spring 的表单标签库是集成在 Spring Web MVC 中,标签可以使用控制器处理的命令对象和引用数据. 因此在下面的例子中将会看到,表单标签使得 JSP 更加方便开发、阅读和维护.
让我们浏览一下表单标签,看看如何使用每个标签的例子. 其中已经包括了生成的 HTML 片段,而某些标签需要进一步的讨论.
配置
表单标签库捆绑在 spring-webmvc.jar
中. 库描述符名字为 spring-form.tld
.
如果需要使用到这些标签,在 JSP 页面的头部必须添加对应的标签库
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中 form
是后面引用标签的前缀.
Form 标签
标签 'form' 绑定了引用库的内部标签,可以被 HTML 解析. 它将命令对象放在 PageContext
中,以便可以通过内部标签访问命令对象. 此库中的所有其他标签都是 form 标签的嵌套标签.
假设我们有一个名为 User
的域对象. 它是一个 JavaBean,具有 firstName
和 lastName
等属性. 我们将使用它作为表单控制器的形式支持对象,输出给 form.jsp
. 以下示例显示了 form.jsp
的显示:
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
firstName
和 lastName
值会从页面控制器放置在 PageContext
的命令对象中查找. 更多复杂的例子都是这样延伸的,重点就是内部标签是如何与 form
标签一起使用的.
以下清单显示了生成的 HTML,它看起来像标准格式:
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value="Harry"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value="Potter"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
之前的 JSP 假设表单的变量名是 command
. 如果对象已经封装到另一个名称中了,表单也支持从自定义名称中绑定变量(这是最佳实践) . 如以下示例所示:
<form:form modelAttribute="user">
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
input
标签
这个标签其实就是 HTML 的 input
标签(当然是解析后的) ,此标签或默认绑定值和 type='text'
属性. 有关此的示例,请参阅Form 标签. 您还可以使用特定于 HTML5 的类型,例如 email
, tel
, date
等.
checkbox
标签
`checkbox` 也会解析成 HTML 的 `input` 标签.
假设 User
对象拥有新闻订阅和爱好列表属性,显示了 Preferences
类:
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
class Preferences(
var receiveNewsletter: Boolean,
var interests: StringArray,
var favouriteWord: String
)
相应的 form.jsp
可能类似于以下内容:
<form:form>
<table>
<tr>
<td>Subscribe to newsletter?:</td>
<%-- Approach 1: Property is of type java.lang.Boolean --%>
<td><form:checkbox path="preferences.receiveNewsletter"/></td>
</tr>
<tr>
<td>Interests:</td>
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
<td>
Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
</td>
</tr>
<tr>
<td>Favourite Word:</td>
<%-- Approach 3: Property is of type java.lang.Object --%>
<td>
Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
</td>
</tr>
</table>
</form:form>
checkbox
标签有三种方法,可满足您的所有复选框需求.
-
方法一: 当绑定值为
java.lang.Boolean
, 如果绑定值为true
. 则input(checkbox)
被标签为checked
.value
属性对应于setValue(Object)
的值(当然是解析后的) . -
方法二: 当绑定值是
array
或java.util.Collection
,如果绑定集合中存在已配置的setValue(Object)
则输入(复选框) 将标签为已选中. -
方法三: 对于任何其他绑定值类型, 如果配置的
setValue(Object)
等于绑定值,则input(checkbox)
被标签为已选中.
请注意,无论采用何种方法,都会生成相同的 HTML 结构. 以下 HTML 代码段定义了一些复选框:
<tr>
<td>Interests:</td>
<td>
Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
<input type="hidden" value="1" name="_preferences.interests"/>
</td>
</tr>
可能不希望看到的是每个复选框后都附加隐藏域,如果 html 页中的复选框一个都没有选中,则在提交表单后,它的值将不会作为 HTTP 请求参数的一部分发送到服务器,因此为了使 Spring 表单数据绑定工作.
需要在 html 中使用此奇怪的变通方法. checkbox
标签遵循现有的 Spring 约定,其中包括每个复选框都以下划线 _
为前缀的隐藏参数. 通过这样做,可以有效地告诉 Spring"该复选框在表单中是可见的,并且希望将表单数据绑定到其上的对象能够反映复选框的状态".
checkboxes
标签
checkbox
标签相当于多个HTML的 input
标签
上一个例子展示了复选框标签的生成. 有时候,不希望在 JSP 页面中列出 User
的所有爱好. 你更希望在运行提供可选的列表,并传递给复选框标签. 这是复选框标签的用途.
可以传入一个 Array
、 一个 List
或一个包含 items
属性中的可用选项的 Map
. 绑定属性通常是一个集合,因此它可以保存用户选择的多个值. 下面是使用此标签的 JSP 示例
<form:form>
<table>
<tr>
<td>Interests:</td>
<td>
<%-- Property is of an array or of type java.util.Collection --%>
<form:checkboxes path="preferences.interests" items="${interestList}"/>
</td>
</tr>
</table>
</form:form>
本实例假定 interestList
是一个模型的属性 List
,包含需要的字符串值. 在使用 MAP 的情况下,Map
的 key 将用作值,map 的 value 将用作要显示的标签. 还可以使用自定义对象,可以使用 itemValue
和使用 itemLabel
的标签作为该值提供属性名称.
radiobutton
标签
还有一个可以解析成 HTML input
标签的是 radio
标签
radio 很简单,提供多个值,但是一次只能选其中一个. 如以下示例所示:
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
radiobuttons
标签
这个形式的 radio
也可以解析成 HTML 的 input
标签,只是它是多个单选.
就像上面的checkboxes
tag一样,可能希望将可用选项作为运行时变量传入. 对于此用法,可以使用单选标签. 可以传入一个数组、一个列表或一个包含 items
属性的 Map
.
如果使用 map,map 的 key 将使用作为值并且 map 的值将使用作为标签来显示. 还可以使用自定义对象,可以使用 itemValue
和使用 itemLabel
的标签作为该值提供属性名称.
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
password
标签
password
标签页会解析成 HTML 的 input
标签 只是它有自己的特性.
<tr>
<td>Password:</td>
<td>
<form:password path="password"/>
</td>
</tr>
请注意,密码值是不可见的. 如果希望密码值可见,需要设置 showPassword
属性为 true
,如下所示:
<tr>
<td>Password:</td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
</td>
</tr>
select
标签
这个标签就是 HTML 的 select
元素. 支持单层选项或嵌套选项的选择,数据利用项来绑定.
让我们假设 User
,他有一个技能列表如下:
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果User选中的技能是Herbology,那么这个Skills的HTML源代码是这样的:
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
option
标签
这个标签就是 HTML 的 option
(配合 select
中) 元素. 它会对被绑定的值设置属性为 selected
,以下 HTML 显示了它的典型输出:
<tr>
<td>House:</td>
<td>
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
</form:select>
</td>
</tr>
如果 User 的家是在 Gryffindor,那么 House 的 HTML 源代码长这样:
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
1 | 注意增加了一个 selected 属性. |
options
标签
这个标签就是 HTML 的 option
(配合 select
中)元素,但是它处理的是一个列表,它会对被绑定的值设置属性为 selected
,如下所示:
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>
如果 User
住在 UK,那么 Country 的 HTML 源代码长这这样:
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> (1)
<option value="US">United States</option>
</select>
</td>
</tr>
1 | 注意增加了一个 selected 属性. |
看上面的两个例子, option
和 options
标签都生成了相同的标准的 HTML,但允许你在 JSP 中显式地按需显示属性值,例如默认的字符串在例子中是"-- Please Select"(就是默认的,选择为空的那个,这个很有用) .
items
属性通常使用项对象的集合或数组填充, itemValue
和 itemLabel
就是对应指定 bean 对象的属性,如果没有指定,对象将被转成字符串. 或者, 可以定义一个 Map
的 items
,Map 的 key 对应选项值,value 对应选项标签. 如果如果 itemValue
和 itemLabel
都被指定了,那么 item 值属性对应 key,item 标签属性对应 value.
textarea
标签
这个标签解析成 HTML 中的 textarea
标签:
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
The hidden
标签
hidden
标签解析为 HTML 的 hidden,用在 input
标签中用于 hidden
绑定值,目的很明显就是 hidden
,如下
<form:hidden path="house"/>
如果我们选择 house
值作为隐藏 domain 提交, HTML 长这样:
<input name="house" type="hidden" value="Gryffindor"/>
errors
标签
这个标签会在 HTML 的 span
标签中展示错误,它提供对在控制器中创建的错误的访问,或对与控制器关联的任何验证程序创建的出错信息进行显示.
假设我们希望在提交表单后显示 firstName
和 lastName
字段的所有错误信息,我们有一个验证器的实例的 User
类称为 UserValidator
. 如下例所示:
public class UserValidator implements Validator {
public boolean supports(Class candidate) {
return User.class.isAssignableFrom(candidate);
}
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
}
}
class UserValidator : Validator {
override fun supports(candidate: Class<*>): Boolean {
return User::class.java.isAssignableFrom(candidate)
}
override fun validate(obj: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")
}
}
这个 form.jsp
看起来是这样的:
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<%-- Show errors for firstName field --%>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<%-- Show errors for lastName field --%>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
如果我们将 firstName
和 lastName
的域设置空值并提交,则 html 看起来是这样的:
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<%-- Associated errors to firstName field displayed --%>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<%-- Associated errors to lastName field displayed --%>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
如果我们要显示给定页面的整个错误列表,该怎么办? 下面的示例显示了 errors
标签还支持一些基本的通用功能
-
path="*"
: 展示所有的错误. -
path="lastName"
: 展示lastName
域的所有错误 -
如果
path
被省略,只会显示当前对象的错误.
下面的示例将显示页面顶部的错误列表,后跟字段旁边的特定于字段的错误:
<form:form>
<form:errors path="*" cssClass="errorBox"/>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
html 看起来是这样的:
<form method="POST">
<span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
spring-webmvc.jar
中包含 spring-form.tld
标签库描述符(TLD) . 有关单个标签的全面参考,请浏览 API 参考 或查看标签库说明.
HTTP 方法转换
REST 的一个关键原则是使用统一的接口. 这意味着所有资源(URL)都可以使用相同的四种 HTTP 方法进行操作 GET, PUT, POST,和 DELETE. 对于每个方法,HTTP 规范都定义了精确的语义.
例如, GET 应该始终是一个安全的操作,这意味着它对服务器的数据没有任何影响. 而 PUT 或 DELETE 应该是幂等的,这意味着可以反复重复这些操作,其最终结果应该是相同的. 虽然 HTTP 定义了这四种方法,但是 HTML 只支持两个: GET 和 POST.
幸运的是,有两种可能的解决方法: 1,可以使用 JavaScript 来执行 PUT 或 DELETE. 2,简单地用 "real" 的方式作为附加参数(作为 HTML 表单中的隐藏输入字段)进行 POST. 后者是使用 Spring 的 HiddenHttpMethodFilter
做的.
这个过滤器是一个简单的 Servlet 过滤器,因此它可以与任何 Web 框架(不仅仅是 Spring MVC)结合使用,只需将此过滤器添加到 web.xml,并将具有隐藏域 method
参数转换为相应的 HTTP 方法请求.
为了支持 HTTP 方法转换,Spring MVC 表单标签已更新为支持设置 HTTP 方法. 例如,以下代码片段来自 "宠物诊所" 示例:
<form:form method="delete">
<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>
实际上它就是一个 HTTP POST,DELETE 方法只是隐藏在请求参数中的假正经方法而已,这个 DELETE 将被定义在 web.xml 的 HiddenHttpMethodFilter
来处理,如以下示例所示:
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
以下示例显示了相应的 @Controller
方法:
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
this.clinic.deletePet(petId);
return "redirect:/owners/" + ownerId;
}
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
clinic.deletePet(petId)
return "redirect:/owners/$ownerId"
}
1.10.6. Tiles
Spring Web 应用还可以集成 Tiles,就像其它视图技术一样. 下面将描述怎样集成.
本节重点介绍 Spring 在 org.springframework.web.servlet.view.tiles3 包中对 Tiles 版本 3 的支持.
|
依赖
为了能够使用 Tiles,您必须在 Tiles 3.0.1 或更高版本上添加依赖及其对项目的 依赖传递.
配置
为了能够使用 Tiles,您必须使用包含定义的文件对其进行配置(有关定义和其他 Tiles 概念的基本信息,请参阅 tiles.apache.org) . 在 Spring 中,这是通过使用 TilesConfigurer
完成的. 以下示例 ApplicationContext
配置显示了如何执行此操作:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
</bean>
这里的 Tiles 定义了五个文件,都位于 WEB-INF/defs
文件夹中. 在初始化 WebApplicationContext
时 ,文件将被加载,定义工厂将被初始化. 完成此操作之后,在 Spring Web 应用程序中,定义文件中包含的 Tiles 可以用作视图. 之后 Spring 使用 Tiles 与使用其他视图是一样的: 通常是一个便捷的 TilesViewResolver
..
您可以通过添加下划线然后添加区域设置来指定特定于区域设置的 Tiles 定义,如以下示例所示:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/tiles.xml</value>
<value>/WEB-INF/defs/tiles_fr_FR.xml</value>
</list>
</property>
</bean>
使用上述配置,tiles_fr_FR.xml
用于具有 fr_FR
语言环境的请求,默认情况下使用 tiles.xml
.
由于下划线用于表示区域设置,因此我们建议不要在 Tiles 定义的文件名中使用它们. |
UrlBasedViewResolver
UrlBasedViewResolver
对给定的 viewClass
进行实例化,即会解析所有的视图. 以下 bean 定义了 UrlBasedViewResolver
:
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
</bean>
SimpleSpringPreparerFactory
和 SpringBeanPreparerFactory
作为一个高级功能,Spring 还支持两个特殊的 Tiles PreparerFactory
实现,有关如何在 Tiles 定义文件中使用 ViewPreparer
引用的详细信息,请参阅 Tiles 文档.
您可以指定 SimpleSpringPreparerFactory
以基于以下条件自动装配 ViewPreparer
实例指定的准备器类,应用 Spring 的容器回调以及应用配置的 Spring BeanPostProcessors
. 如果 Spring 的上下文范围注解配置具有激活后,将自动检测 "ViewPreparer" 类中的注解并 应用. 请注意,这需要 Tiles 定义文件中的 preparer 类,如下所示: 默认的 "PreparerFactory" 会这么做.
您可以指定 SpringBeanPreparerFactory
来操作指定的 preparer 名称(而不是类) ,从 DispatcherServlet 的应用程序上下文中获取相应的 Spring bean. 在这种情况下,完整的 bean 创建过程控制着 Spring 应用程序上下文,允许使用显式依赖注入配置,作用域 bean 等.
请注意,您需要为每个 preparer 名称定义一个 Spring bean 定义(在 Tiles 定义中使用) . 以下示例显示如何在 TilesConfigurer
上定义一个 SpringBeanPreparerFactory
属性集:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
<!-- resolving preparer names as Spring bean definition names -->
<property name="preparerFactoryClass"
value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>
</bean>
1.10.7. RSS 和 Atom
AbstractAtomFeedView
和 AbstractRssFeedView
都继承自 AbstractFeedView
基类,分别用于提供 Atom 和 RSS Feed 视图. 它们基于java.net的 ROME 项目,位于 org.springframework.web.servlet.view.feed
包中.
AbstractAtomFeedView
要求实现 buildFeedEntries()
方法,并可选择重写 buildFeedMetadata()
方法(默认实现为空).以下示例显示了如何执行此操作:
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentAtomView : AbstractAtomFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Feed, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedEntries(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Entry> {
// implementation omitted
}
}
类似的要求适用于实现 AbstractRssFeedView
,如以下示例所示:
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentRssView : AbstractRssFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Channel, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedItems(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Item> {
// implementation omitted
}
}
buildFeedItems()
和 buildFeedEntries()
方法在 HTTP 请求中传递,以防需要访问区域设置. 仅为 cookie 或其他 http 头的设置传递 http 响应. 该 feed 将在方法返回后自动写入响应对象.
有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team Blog entry.
1.10.8. PDF 和 Excel
Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格. 本节介绍如何使用这些功能.
文档视图简介
返回 HTML 页并不总是用户查看模型输出的最佳方式,Spring 让开发者可以从模型数据动态生成 PDF 文档或 Excel 电子表格. 该文档是视图,将从具有正确内容类型的服务器流式传输到 HTML,使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序以进行响应.
要使用 Excel 视图,需要将 Apache POI 库添加到类路径中. 对于 PDF 生成,您需要添加(最好) OpenPDF 库.
如果可能,您应该使用最新版本的基础文档生成库. 特别是,我们强烈建议使用 OpenPDF(例如,OpenPDF 1.0.5) 而不是过时的原始 iText 2.1.7,因为 OpenPDF 是主动维护的,并修复了不受信任的 PDF 内容的重要漏洞. |
PDF 视图
单词列表的简单 PDF 视图可以扩展 org.springframework.web.servlet.view.document.AbstractPdfView
并实现 buildPdfDocument()
方法,如以下示例所示:
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
class PdfWordList : AbstractPdfView() {
override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
request: HttpServletRequest, response: HttpServletResponse) {
val words = model["wordList"] as List<String>
for (word in words) {
doc.add(Paragraph(word))
}
}
}
控制器可以从外部视图定义(通过名称引用它) 返回这样的视图,也可以从处理程序方法返回 View
实例.
Excel 视图
从 Spring Framework 4.2 开始,org.springframework.web.servlet.view.document.AbstractXlsView
作为 Excel 视图的基类提供. 它基于 Apache POI,具有专门的子类(AbstractXlsxStreamingView
和 AbstractExcelView
) ,取代了过时的 AbstractXlsxView
类.
编程模型类似于 AbstractPdfView
,buildExcelDocument()
作为核心模板方法,控制器能够从外部定义(通过名称) 返回这样的视图,或者从处理程序方法返回 View
实例.
1.10.9. Jackson
Spring 为 Jackson JSON 库提供支持.
基于 Jackson 的 JSON 视图
MappingJackson2JsonView
使用 Jackson 库的 ObjectMapper
将响应内容呈现为 JSON. 默认情况下,模型映射的全部内容(特定于框架的类除外) 都编码为 JSON. 对于需要过滤 Map 内容的情况,您可以使用 modelKeys
属性指定要编码的特定模型属性集. 您还可以使用 extractValueFromSingleKeyModel
属性将 single-key
模型中的值直接提取和序列化,而不是作为模型属性的映射.
您可以使用 Jackson 提供的注解根据需要自定义 JSON 映射. 当您需要进一步控制时,可以通过 ObjectMapper
属性注入自定义 ObjectMapper
,以用于需要为特定类型提供自定义 JSON 序列化程序和反序列化程序的情况.
基于 Jackson 的 XML 视图
MappingJackson2XmlView
使用 Jackson XML 扩展的 XmlMapper
将响应内容呈现为 XML. 如果模型包含多个条目,则应使用 modelKeybean
属性显式设置要序列化的对象. 如果模型包含单个条目,则会自动序列化.
您可以使用 JAXB 或 Jackson 提供的注解根据需要自定义 XML 映射. 当您需要进一步控制时,可以通过 ObjectMapper
属性注入自定义 XmlMapper
,以便自定义 XML 需要为特定类型提供序列化程序和反序列化程序.
1.10.10. XML编 组
MarshallingView
使用 XML Marshaller
(在 org.springframework.oxm
包中定义) 将响应内容呈现为 XML. 您可以使用 MarshallingView
实例的 modelKey
bean属性显式设置要编组的对象. 或者,视图会迭代所有模型属性,并封送 Marshaller
支持的第一种类型.
有关 org.springframework.oxm
包中功能的更多信息,请参阅使用 Marshalling XML using O/X Mappers.
1.10.11. XSLT 视图
XSLT 是一个用于转换 XML 的语言,能够在 web 的视图技术中使用.如果应用需要处理 XML(或者将模型转换为 XML) ,那么 XSLT 是一个很适合的视图技术. 以下部分显示如何将 XML 文档生成为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 进行转换.
这个例子是一个简单的 Spring 应用程序,它在 Controller
中创建一个单词列表并将它们添加到模型映射中. 该映射与使用的 XSLT 视图名称一起返回. 有关 Spring Web MVC 控制器接口的详细信息, 请参阅注解控制器. XSLT 控制器将单词列表转换为准备转换的简单 XML 文档.
Beans
Configuration 配置是 Spring 应用程序的标配,MVC 配置必须定义 XsltViewResolver
bean 和常规 MVC 注解配置,以下示例显示了如何执行此操作:
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun xsltViewResolver() = XsltViewResolver().apply {
setPrefix("/WEB-INF/xsl/")
setSuffix(".xslt")
}
}
Controller
并且我们需要一个控制器,用来处理单词的生成逻辑.
控制器逻辑封装在 @Controller
类中,处理程序方法定义如下:
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
@Controller
class XsltController {
@RequestMapping("/")
fun home(model: Model): String {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val root = document.createElement("wordList")
val words = listOf("Hello", "Spring", "Framework")
for (word in words) {
val wordNode = document.createElement("word")
val textNode = document.createTextNode(word)
wordNode.appendChild(textNode)
root.appendChild(wordNode)
}
model["wordList"] = root
return "home"
}
}
到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中. 请注意,您还可以将 XML 文件作为 Resource
加载,并使用它而不是自定义 DOM 文档.
当然,有软件包可以自动 'domify' 对象图,在 Spring 中,您可以完全灵活地以您选择的任何方式从模型中创建 DOM. 这可以防止 XML 在模型数据的结构中扮演太大的角色,这在使用工具管理 DOM 化过程时是一种危险.
转换
最后, XsltViewResolver
将解析 “home” XSLT 模板文件,并将 DOM 文档合并到其中以生成所需视图. 例如 XsltViewResolver
配置所示,XSLT 模板在 WEB-INF/xsl
目录中的 war
文件中, 并以 xslt
文件扩展名结束.
以下示例显示了 XSLT 转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
上述转换呈现为以下 HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
1.11. MVC 配置
MVC Java 配置和 MVC 命名空间提供了适用于大多数应用程序的默认配置以及配置 API 来对其进行自定义.
有关配置 API 中没有的高级自定义设置请参阅高级 Java 配置 和 高级 XML 配置.
您无需了解 MVC Java 配置和 MVC 命名空间创建的基础 bean. 如果您想了解更多信息,请参阅特殊的 Bean 类型和 Web MVC 配置.
1.11.1. 启用 MVC 配置
在 Java 配置中,您可以使用 @EnableWebMvc
注解启用 MVC 配置,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig {
}
@Configuration
@EnableWebMvc
class WebConfig
在 XML 配置中,您可以使用 <mvc:annotation-driven>
元素来启用 MVC 配置,如以下示例所示:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven/>
</beans>
前面的示例注册了许多 Spring MVC 基础结构 bean,并适应类路径上可用的依赖(例如,JSON,XML 等的有效负载转换器) .
1.11.2. MVC 配置 API
在 Java 配置中,您可以实现 WebMvcConfigurer
接口,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
// Implement configuration methods...
}
在 XML 中,您可以检查 <mvc:annotation-driven/>
的属性和子元素. 您可以查看 Spring MVC XML schema 或使用 IDE 的代码完成功能来发现可用的属性和子元素.
1.11.3. 类型转换
默认情况下,将安装各种数字和日期类型的格式化程序以及支持通过字段上的 @NumberFormat
和 @DateTimeFormat
进行定制.
在 Java 配置中,您可以注册自定义格式化程序和转换器,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
以下示例显示如何在 XML 中实现相同的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
</beans>
默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求区域设置. 这适用于使用 "input" 日期表示为字符串的表单. 但是,对于 "date" 和 "time" 表单字段,浏览器使用 HTML 规范中定义的固定格式. 在这种情况下,日期和时间格式可以按以下方式自定义:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
有关何时使用 the FormatterRegistrar SPI 实现的更多信息,请参阅 FormatterRegistrar SPI 和 FormattingConversionServiceFactoryBean .
|
1.11.4. 验证
默认情况下,如果类路径上存在Bean Validation (例如 Hibernate Validator) ,则 LocalValidatorFactoryBean
将注册为全局 Validator . 以便与 @Valid
和 Validated
一起使用并在控制器方法参数上进行验证.
在 Java 配置中,您可以自定义全局 Validator 实例,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun getValidator(): Validator {
// ...
}
}
以下示例显示如何在 XML 中实现相同的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven validator="globalValidator"/>
</beans>
请注意,您还可以在本地注册 Validator
实现,如以下示例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
如果需要在某处注入 LocalValidatorFactoryBean ,请创建一个 bean 并使用 @Primary 标记它,以避免与 MVC 配置中声明的那个冲突.
|
1.11.5. 拦截器
在 Java 配置中,注册拦截器应用于传入的请求. 如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(LocaleChangeInterceptor())
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
映射的拦截器不适合作为安全层,因为他对于带注解的控制器路径可能不匹配,它也可以匹配尾部的斜杠和路径扩展,以及其他路径匹配选项。 许多这些选项中的一些已被弃用,但仍然存在不匹配的可能性。 通常,我们建议使用 Spring Security,它有一个专门的 MvcRequestMatcher 与 Spring MVC 路径匹配保持一致,并且还有一个安全防火墙,可以阻止许多 URL 路径中不需要的字符。 |
1.11.6. 内容类型
您可以配置 Spring MVC 如何根据请求确定所请求的媒体类型(例如,Accept
头,URL 路径扩展,查询参数等) .
默认情况下,只检查 Accept
头.
在 Java 配置中,您可以自定义请求的内容类型解析,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON)
configurer.mediaType("xml", MediaType.APPLICATION_XML)
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes">
<value>
json=application/json
xml=application/xml
</value>
</property>
</bean>
1.11.7. 消息编解码器
使用 MVC Java 编程配置方式时,如果想替换 Spring MVC 提供的默认转换器,完全定制自己的 HttpMessageConverter
,这可以通过覆写 configureMessageConverters()
方法来实现.
如果只是想自定义,或者想在默认转换器之外再添加其他的转换器,那么可以通过覆写 extendMessageConverters()
方法来实现.
以下示例使用自定义的 ObjectMapper
而不是默认的 ObjectMapper
添加 XML 和 Jackson JSON 转换器:
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
@Configuration
@EnableWebMvc
class WebConfiguration : WebMvcConfigurer {
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
val builder = Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(ParameterNamesModule())
converters.add(MappingJackson2HttpMessageConverter(builder.build()))
converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))
在上面的例子中, Jackson2ObjectMapperBuilder
用于为 MappingJackson2HttpMessageConverter
和 MappingJackson2XmlHttpMessageConverter
转换器创建公共的配置,比如启用 tab 缩进、
定制的日期格式,并注册了模块 jackson-module-parameter-names
用于获取参数名(Java 8 新增的特性) .
该 builder 会使用以下的默认属性对 Jackson 进行配置
同时,如果检测到在 classpath 路径下存在这些模块,该 builder 也会自动地注册它们.
-
jackson-datatype-joda: 支持 Joda-Time 类型.
-
jackson-datatype-jsr310: 支持 Java 8 日期和时间 API 类型.
-
jackson-datatype-jdk8: 支持其他 Java 8 类型,例如
Optional
. -
jackson-module-kotlin
: 支持 Kotlin 类和数据类.
除了 jackson-dataformat-xml 之外,使用 Jackson XML 支持启用缩进还需要 woodstox-core-asl 依赖性.
|
-
jackson-datatype-money: 支持
javax.money
类型(非官方模块) . -
jackson-datatype-hibernate: 支持特定于 Hibernate 的类型和属性(包括延迟加载方面) .
以下示例显示如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
<bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
<property name="objectMapper" ref="xmlMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
p:indentOutput="true"
p:simpleDateFormat="yyyy-MM-dd"
p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>
<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>
1.11.8. 视图控制器
以下的一段代码相当于定义 ParameterizableViewController
视图控制器的快捷方式,该控制器会立即将请求转发(forwards) 给视图. 请确保仅在以下情景下才使用这个类: 当控制器除了将视图渲染到响应中外不需要执行任何逻辑时.
以下 Java 配置示例将对 /
的请求转发给名为 home
的视图:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/").setViewName("home")
}
}
以下示例与前面的示例实现相同的功能,但使用 XML,通过使用 <mvc:view-controller>
元素:
<mvc:view-controller path="/" view-name="home"/>
如果将 @RequestMapping
方法映射到任何 HTTP 方法的 URL,则使用视图 控制器不能用于处理相同的 URL. 这是因为通过 URL 匹配的带注解的控制器被视为端点所有权的足够有力的指示,因此 405(METHOD_NOT_ALLOWED) ,415(UNSUPPORTED_MEDIA_TYPE) 或类似的响应可以 发送给客户端以帮助调试. 因此,建议避免在带注解的控制器和视图控制器之间拆分 URL 处理.
1.11.9. View 解析器
MVC 提供的配置简化了视图解析器的注册工作
以下 Java 配置示例使用 JSP 和 Jackson 作为 JSON 呈现的默认视图来配置内容协商视图解析:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.jsp()
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
</mvc:view-resolvers>
但请注意,FreeMarker,Tiles,Groovy Markup 和脚本模板也需要配置底层视图技术.
MVC 命名空间提供专用的元素. 以下示例适用于 FreeMarker:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache="false"/>
</mvc:view-resolvers>
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>
在 Java 配置中,您可以添加相应的 Configurer
bean,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.freeMarker().cache(false);
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/freemarker");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.freeMarker().cache(false)
}
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/freemarker")
}
}
1.11.10. 静态资源
此选项提供了一种从 Resource
库位置列表中使用静态资源的便捷方法
在下面的示例中,给定以 /resources
开头的请求,相对路径用于在 Web 应用程序根目录下或在或在 /static
下的类路径上查找和提供相对于 /public
的静态资源. 资源的有效期为 1 年,以确保最大程度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求. Last-Modified
信息是从 Resource#lastModified
中推导出来的,以便 HTTP 条件请求带有 "Last-Modified"
headers..
以下清单显示了如何使用 Java 配置执行此操作:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**"
location="/public, classpath:/static/"
cache-period="31556926" />
资源处理还支持一系列 ResourceResolver
实现 和 ResourceTransformer
实现, 可用于创建用于使用优化资源的工具
VersionResourceResolver
可用于基于内容、固定应用程序版本或其他的 MD5 哈希计算的版本化资源 url. ContentVersionStrategy
(MD5 hash)方法是一个很好的选择, 有一些值得注意的例外,例如与模块加载器一起使用的 JavaScript 资源.
以下示例显示如何在 Java 配置中使用 VersionResourceResolver
:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain resource-cache="true">
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
</mvc:version-resolver>
</mvc:resolvers>
</mvc:resource-chain>
</mvc:resources>
您可以使用 ResourceUrlProvider
来重写 URL 并应用完整的解析器和转换器链,例如插入版本. MVC 配置提供了 ResourceUrlProvider
bean,因此可以将其注入到其他用户. 您还可以使用 ResourceUrlEncodingFilter
的 Thymeleaf、jsp、FreeMarker 和其他依赖于 HttpServletResponse#encodeURL
的URL标记来做重写转换.
请注意,当同时使用 EncodedResourceResolver
(例如,用于提供gzipped或brotli编码的资源) 和 VersionedResourceResolver
时,必须按此顺序注册它们. 这可确保始终基于未编码的文件可靠地计算基于内容的版本.
WebJars 也支持使用 WebJarsResourceResolver
和自动注册,当 org.webjars:webjars-locator
存在于类路径中时. 解析器可以重写 URL 来包含 jar 的版本,也可以与传入的 URL 匹配,而不需要版本 . 例如, /jquery/jquery.min.js
到 /jquery/1.2.0/jquery.min.js
.
基于 ResourceHandlerRegistry 的 Java 配置为细粒度控制提供了更多选项,例如 最后修改的行为和优化的资源解析。 |
1.11.11. 默认 Servlet
这些配置允许将 DispatcherServlet
映射到 /
路径(也即覆盖了容器默认 Servlet 的映射) ,但依然保留容器默认的 Servlet 以处理静态资源的请求. 这可以通过配置一个URL映射到 /**
的处理器 DefaultServletHttpRequestHandler
来实现,并且该处理器在其他所有 URL 映射关系中优先级应该是最低的.
该处理器会将所有请求转发(forward) 到默认的 Servlet,因此需要保证它在所有 URL 处理器映射 HandlerMappings
的最后. 如果你是通过 <mvc:annotation-driven>
的方式进行配置, 或自定义 HandlerMapping
实例,那么需要确保该处理器 order
属性的值比 DefaultServletHttpRequestHandler
的次序值 Integer.MAX_VALUE
小.
以下示例显示如何使用默认设置启用该功能:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable()
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:default-servlet-handler/>
不过需要注意,覆写了 /
的Servlet映射后,默认 Servlet 的 RequestDispatcher
就必须通过名字而非路径来取得了. DefaultServletHttpRequestHandler
会尝试在容器初始化的时候自动检测默认 Servlet, 这里它使用的是一份主流 Servlet 容器(包括 Tomcat, Jetty, GlassFish, JBoss, Resin, WebLogic, and WebSphere) 已知的名称列表. 如果默认 Servlet 被配置了一个其他的名字,或者使用了一个列表里未提供默认 Servlet 名称的容器,那么默认 Servlet 的名称必须被显式指定,正如下面代码所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable("myCustomDefaultServlet")
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
1.11.12. 路径匹配
您可以自定义与路径匹配和 URL 处理相关的选项. 有关各个选项的详细信息,请参阅 PathMatchConfigurer
javadoc.
以下示例显示如何在 Java 配置中自定义路径匹配:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setPatternParser(new PathPatternParser())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setPatternParser(patternParser)
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
}
fun patternParser(): PathPatternParser {
//...
}
}
以下示例显示如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:path-matching
trailing-slash="false"
path-helper="pathHelper"
path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
1.11.13. 高级 Java 配置
@EnableWebMvc
导入 DelegatingWebMvcConfiguration
, 其中
-
为 Spring MVC 应用程序提供了默认的 Spring 配置
-
检测到并委派到
WebMvcConfigurer
的自定义该配置
对于高级模式,请删除 @EnableWebMvc
并直接从 DelegatingWebMvcConfiguration
继承 ,而不是实现 WebMvcConfigurer
,如以下示例所示:
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {
// ...
}
可以在 WebConfig
中保留现有的方法,但现在也可以重写基类中的 bean 声明,并且在类路径上仍然可以有任意数量的其他 WebMvcConfigurer
.
1.11.14. 高级 XML 配置
MVC 命名空间没有高级模式,如果需要自定义无法更改的 bean 上的属性,可以使用 ApplicationContext
的 BeanPostProcessor
生命周期挂钩,如以下示例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
@Component
class MyPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
// ...
}
}
请注意,MyPostProcessor
需要用XML显式声明为 bean,或通过 <component-scan/>
声明检测.
1.12. HTTP/2
Servlet 4 容器需要支持 HTTP/2,Spring Framework 5 与 Servlet API 4 兼容. 从编程模型的角度来看,应用程序不需要特定的任何操作. 但是,存在与服务器配置相关的注意事项. 有关更多详细信息,请参阅 HTTP/2 wiki 页面.
Servlet API 确实暴露了一个与 HTTP/2 相关的构造. 您可以使用 javax.servlet.http.PushBuilder
主动将资源推送到客户端,并且它被支持作为 @RequestMapping
方法的方法参数.
2. REST 客户端
本节介绍客户端访问 REST 端点的选项.
2.1. RestTemplate
RestTemplate
是一个执行 HTTP 请求的同步客户端. 它是最初的 Spring REST 客户端,并在底层 HTTP 客户端库上暴露了一个简单的模板方法 API.
从 5.0 版本开始,RestTemplate 处于维护模式,以后只有很少的更改和 debug 请求被接受. 请考虑使用支持同步,异步和流方案的 WebClient .
|
有关详细信息,请参见 REST Endpoints.
2.2. WebClient
WebClient
是一个执行 HTTP 请求的非阻塞,响应式的客户端. 它在 5.0 中引入,提供了 RestTemplate
的现代替代方案,同时有效支持同步和异步以及流方案.
与 RestTemplate
相比,WebClient
支持以下内容:
-
非阻塞IO
-
Reactive Streams back pressure.
-
高并发,硬件资源更少.
-
利用 Java 8 lambda 的功能风格,流式,函数式的 API.
-
同步和异步交互.
-
Streaming up to or streaming down from a server.
有关更多详细信息,请参见 WebClient.
3. 测试
本节总结了Spring MVC 应用程序 spring-test
中可用的选项.
-
Servlet API Mocks: 用于单元测试控制器,过滤器和其他 Web 组件的 Servlet API 契约的模拟实现. 有关更多详细信息,请参阅 Servlet API 模拟对象.
-
TestContext Framework: 支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法高效缓存加载的配置,以及支持使用
MockServletContext
加载WebApplicationContext
. 有关更多详细信息,请参阅 TestContext Framework . -
Spring MVC Test: 一个框架,也称为
MockMvc
,用于通过DispatcherServlet
测试带注解的控制器(即支持注解) ,完成 Spring MVC 基础结构但没有 HTTP 服务器. 有关更多详细信息,请参阅Spring MVC Test . -
Client-side REST:
spring-test
提供了一个MockRestServiceServer
,您可以将其用作模拟服务器,用于测试内部使用 RestTemplate 的客户端代码. 有关详细信息,请参阅Client REST Tests. -
WebTestClient
: 专为测试 WebFlux 应用程序而构建,但它也可用于通过 HTTP 连接对任何服务器进行端到端集成测试. 它是一个非阻塞,响应式的客户端,非常适合测试异步和流式方案.
4. WebSockets
这一部分介绍 Spring 框架在 Web 应用程序中对 WebSocket 消息传递的支持,通过 SockJS 的 WebSocket 仿真,包括使用 STOMP 作为应用程序级 WebSocket 的子协议的发布 - 订阅消息传递.
4.1. WebSocket简介
WebSocket 协议 RFC 6455,提供了一种标准化方法,可通过单个 TCP 连接在客户端和服务器之间建立全双工双向通信通道. 它是来自 HTTP 的不同 TCP 协议,但设计为通过 HTTP 工作并使用 80 和 443 端口,并允许重用现有防火墙规则.
WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用HTTP Upgrade
头进行升级,或者在这种情况下,切换到 WebSocket 协议.
以下示例显示了这样的交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1 | Upgrade 头. |
2 | 使用 Upgrade 连接. |
具有 WebSocket 支持的服务器返回类似于以下内容的输出,而不是通常的 200 状态代码:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 | 协议切换 |
成功握手后,则 HTTP 升级请求背后的 TCP 套接字将保持打开状态,并且客户端和服务器都可以使用它来彼此发送消息.
有关 WebSockets 如何工作的完整介绍超出了本文档的范围. 请参阅 RFC 6455,HTML5 的 WebSocket 章节,或 Web 上的许多介绍和教程.
请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx) 后面运行,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器. 同样,如果应用程序在云环境中运行,请检查与 WebSocket 支持相关的云提供程序的说明.
4.1.1. HTTP 与 WebSocket
尽管 WebSocket 被设计为与 HTTP 兼容并且以 HTTP 请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和应用程序编程模型.
在 HTTP 和 REST 中,应用程序被设计为多个 URL. 要与应用程序进行交互,客户端将访问这些 URL 请求、响应样式. 服务器根据 HTTP URL,方法和请求头将请求路由到适当的处理程序.
相比之下,在 WebSockets 中,初始连接通常只有一个 URL. 随后,所有应用程序消息都在同一 TCP 连接上流动. 这指向完全不同的异步,事件驱动的消息传递体系结构.
WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息内容规定任何语义. 这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息.
WebSocket 客户端和服务器可以通过 HTTP 握手请求上的 Sec-WebSocket-Protocol
头协商使用更高级别的消息传递协议(例如,STOMP) . 如果没有,他们需要提出自己的惯例.
4.1.2. 何时使用WebSockets
WebSockets 可以使网页变得动态和交互. 但是,在许多情况下,Ajax 和 HTTP 流式传输或长轮询的组合可以提供简单有效的解决方案.
例如,新闻,邮件和社交订阅资源需要动态更新,但是正常更新的间隔时间为几分钟. 另一方面,协作,游戏和财务应用程序需要更接近实时.
仅延迟并不是使用 WebSocket 的决定因素. 如果消息量相对较低(例如,监视网络故障) ,HTTP 流式传输或轮询可以提供有效的解决方案. 而当需要它是低延迟,高频率和高容量的组合,那使用 WebSocket 是最佳选择.
还要记住,通过 Internet,受控制之外的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade
头, 或者因为它们关闭看似空闲的长期连接.
这意味着将 WebSocket 用于防火墙内的内部应用程序是一个比面向公众的应用程序更直接的决策.
4.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的客户端和服务器端应用程序.
4.2.1. WebSocketHandler
创建 WebSocket 服务器与实现 WebSocketHandler
或继承 TextWebSocketHandler
或 BinaryWebSocketHandler
一样简单以下示例使用 TextWebSocketHandler
:
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
有专门的 WebSocket Java 配置和 XML 命名空间支持,用于将上述 WebSocket 处理程序映射到特定的 URL. 如以下示例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
以上是在 Spring MVC 应用程序中使用的,应包含在 DispatcherServlet
的配置中. 但是,Spring 的 WebSocket 支持不依赖于 Spring MVC, 在 WebSocketHttpRequestHandler
的帮助下,将 WebSocketHandler
集成到其他 HTTP 服务环境中是相对简单的.
当直接或间接使用 WebSocketHandler
API 时,例如 通过 STOMP 消息传递,由于基础标准 WebSocket 会话 (JSR-356) 不允许并发发送,因此应用程序必须同步消息的发送. 一种选择是用 ConcurrentWebSocketSessionDecorator
包装 WebSocketSession
.
4.2.2. WebSocket 握手
自定义初始 HTTPWebSocket
握手请求的最简单的方法是通过 HandshakeInterceptor
,它暴露 “before” 和 “after” 的握手方法. 此类拦截器可用于阻止握手或使任何属性用于 WebSocketSession
. 例如,有一个内置的拦截器用于将 HTTP 会话属性传递给 WebSocket
会话:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
更高级的选项是继承执行 WebSocket 握手步骤的 DefaultHandshakeHandler
,包括验证客户端来源、协商协议和其他操作. 如果应用程序需要配置自定义 RequestUpgradeStrategy
,以便适应 WebSocket 服务器引擎和尚未受支持的版本(有关此主题的更多信息,请参阅部署) , 则可能还需要使用此选项. Java 配置和 XML 命名空间都可以配置自定义的 HandshakeHandler
.
Spring 提供了 WebSocketHandlerDecorator 基类,可用于用附加行为装饰 WebSocketHandler . 在使用 WebSocket 的 Java 配置或 XML 命名空间时,默认情况下提供和添加日志记录和异常处理实现. ExceptionWebSocketHandlerDecorator 捕获所有 WebSocketHandler 方法产生异常,并关闭具有指示服务器错误状态为 1011 的 WebSocket 会话.
|
4.2.3. 部署
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,DispatcherServlet
既服务于 HTTP WebSocket 握手,也提供其他 HTTP 请求. 通过调用 WebSocketHttpRequestHandler
也很容易集成到其他 HTTP 处理方案中. 这很方便易懂. 但是,在J SR-356 运行时,需要特别注意.
Java WebSocket API(JSR-356)提供了两种部署机制. 第一种是在启动时包含 Servlet 容器类路径扫描(Servlet 3 功能),另一个是在 Servlet 容器初始化时使用注册 API. 这两种机制都不能为所有 http 处理使用单一的 "前端控制器", 包括 WebSocket 握手和所有其他 HTTP 请求,例如 Spring MVC 的 DispatcherServlet
.
这是 JSR-356 的一个重要限制,Spring 的 WebSocket 支持解决了服务器特定的 RequestUpgradeStrategy
实现,即使在 JSR-356 运行时运行也是如此. 目前,Tomcat,Jetty,GlassFish,WebLogic,WebSphere 和 Undertow (以及 WildFly) 都有这样的策略.
在 Java WebSocket API 中已经有解决上面限制的方法,并且遵循 eclipse-ee4j/websocket-api#211 中的规定. 还要注意的是,Tomcat 和 Jetty 已经提供了本地的 API 替代品,可以实现这一点,Jetty 也是如此. 我们希望更多服务器能够做到这一点. |
第二个考虑因素是,具有 JSR-356 支持的 Servlet 容器将执行 ServletContainerInitializer
(SCI)扫描,从而在某些情况下大大降低应用程序启动速度. 如果在升级到带有 JSR-356 支持的 Servlet 容器版本后观察到了显著的影响, 则应该可以通过在 web.xml
中使用 <absolute-ordering />
元素来有选择地启用或禁用 web 片段(和 SCI 扫描).
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering/>
</web-app>
然后,可以按名称有选择地启用 web 片段,例如 Spring 自定义的 SpringServletContainerInitializer
,它为 Servlet 3 Java 初始化 API 提供支持. 以下示例显示了如何执行此操作:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
4.2.4. 服务器配置
每个底层的 WebSocket 引擎都暴露了控制运行时特性的配置属性,如消息缓冲区大小、空闲超时和其他设置.
对于 Tomcat Tomcat, WildFly, 和 GlassFish,可以将 ServletServerContainerFactoryBean
添加到 WebSocket Java 配置中. 如以下示例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
对于客户端 WebSocket 配置,您应该使用 WebSocketContainerFactoryBean (XML) 或 ContainerProvider.getWebSocketContainer() (Java 配置) .
|
对于 Jetty,您需要提供预配置的 Jetty WebSocketServerFactory
并通过 WebSocket Java 配置将其插入 Spring 的 DefaultHandshakeHandler
. 以下示例显示了如何执行此操作:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>
</beans>
4.2.5. Allowed Origins (允许来源)
在 Spring 4.1.55 版本之后,WebSocket 和 SockJS 的默认行为是仅接受同源请求. 也可以允许所有或指定的来源列表. 此检查主要是为浏览器客户端设计的. 没有什么能阻止其他类型的客户端修改 Origin
头值 (有关更多详细信息,请参阅 RFC 6454: The Web Origin Concept ) .
三种可能的行为是:
-
仅允许同源请求 (默认) : 在此模式下,启用 SockJS 时,Iframe HTTP 响应头
X-Frame-Options
设置为SAMEORIGIN
,并禁用 JSONP 传输,因为它不允许检查源的的请求. 因此,启用此模式时不支持 IE6 和 IE7. -
允许指定的来源列表: 每个允许的来源必须以
http://
或https://
开头. 在此模式下,启用 SockJS 时,将禁用 IFrame 传输. 因此,启用此模式时,不支持 IE6 到 IE9. -
允许所有来源: 要启用此模式,您应提供
*
作为允许的原始值. 在此模式下,所有传输都可用.
您可以配置 WebSocket 和 SockJS 允许的源,如以下示例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers all owed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
4.3. SockJS 回调选项
在公共 Internet 上,受控制之外的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade
头,或者因为它们关闭看似空闲的长期连接.
此问题的解决方案是 WebSocket 仿真 - 即,首先尝试使用 WebSocket,然后依靠基于 HTTP 的技术来模拟 WebSocket 交互并暴露相同的应用程序级 API.
在 Servlet 技术栈上,Spring Framework 为 SockJS 协议提供服务器 (以及客户端) 支持.
4.3.1. 简介
SockJS 的目标是让应用程序使用 WebSocket 的 API,但在运行时需要回退到非 WebSocket 的替代方案,即无需更改应用程序代码.
SockJS 包括:
-
SockJS JavaScript 客户端 — 一个用于浏览器的客户端库
-
SockJS 服务器实现是 Spring 框架中的
spring-websocket
模块. -
spring-websocket4.1 之后的版本还提供了一个 SockJS 的 Java 客户端
SockJS 是为在浏览器中使用而设计的. 使用各种技术来支持广泛的浏览器版本会有很大的不同. 有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS client 页面. 传输分为三大类: WebSocket,HTTP Streaming 和 HTTP Long Polling. 有关这些类别的概述,请参阅此 this blog post.
SockJS 客户端首先发送 GET/info
以从服务器获取基本信息. 之后,它必须决定使用什么传输. 如果可能,使用 WebSocket. 如果没有,在大多数浏览器中,至少有一个 HTTP 流选项. 如果不是,则使用 HTTP (长) 轮询.
所有传输请求都具有以下 URL 结构:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
where:
-
{server-id}
用于集群中的路由请求并且不在其他地方使用 -
{session-id}
将属于 SockJS 会话的 HTTP 请求关联起来 -
{transport}
指定传输的类型(例如,websocket
,xhr-streaming
等等).
WebSocket 传输只需要 HTTP 请求来执行 WebSocket 握手,此后所有消息都在该套接字上交换.
HTTP 传输需要更多请求, Ajax/XHR streaming 依赖于一个长时间运行的服务器消息请求和对客户机到服务器消息的其他 HTTP POST 请求. 长轮询也是类似的,只是它在每次服务器发送后结束当前请求.
SockJS 添加最小的消息帧,例如服务器最初发送字母 o
(“open” 帧),消息的格式会是 a["message1","message2"]
(JSON 编码的数组) ,字母 h
(“heartbeat” 帧), 如果在默认的 25 秒内没有消息流,将发送字母 c
(“close” 帧) 用于关闭会话.
要了解更多信息,请在浏览器中运行示例并观察 HTTP 请求. SockJS 客户端允许修复传输列表,因此可以一次查看每个传输. SockJS 客户端还提供了一个调试标志,它在浏览器控制台中启用有用的消息. 在服务器端启用 org.springframework.web.socket
的 TRACE
日志记录. 有关更多详细信息,请参阅 SockJS 协议 narrated test.
4.3.2. 开启 SockJS
您可以通过 Java 配置启用 SockJS,如以下示例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于 Spring MVC 应用程序,应该包含在 DispatcherServlet
的配置中. 但是,Spring 的 WebSocket 和 SockJS 支持并不依赖于 Spring MVC. 在 SockJsHttpRequestHandler
的帮助下,将其集成到其他 HTTP 服务环境中相对简单.
在浏览器端,应用程序可以使用 sockjs-client
(版本 1.0.x) 来模拟 W3C WebSocket API,并与服务器进行通信,根据它所运行的浏览器选择最佳传输选项. 查看 sockjs-client 页和浏览器支持的传输类型列表. 客户端还提供了几个配置选项,例如指定要包含哪些传输.
4.3.3. IE 8 和 9
Internet Explorer 8 和 9 仍在使用中. 他们是拥有 SockJS 的关键原因. 本节介绍在这些浏览器中运行的重要注意事项.
SockJS 客户端通过使用 Microsoft 的 XDomainRequest
. 在 IE 8 和 9 中支持 Ajax/XHR 流. 能够做到跨域工作,但不支持发送 cookie. 对于 Java 应用程序来说,cookie 通常是必不可少的. 但是,由于 SockJS 客户端可以与许多服务器类型一起使用(不仅仅是 Java) ,所以需要知道 cookie 是否重要. 如果是这样,SockJS 客户端更喜欢 Ajax/XHR 流,否则它依赖于 iframe-based 技术.
来自 SockJS 客户端的第一个 /info
请求是对可能影响客户选择传输的信息的请求. 其中一个细节是服务器应用程序是否依赖于 cookie,例如用于身份验证或使用粘性会话进行群集. Spring的SockJS支持包括一个称为 sessionCookieNeeded
的属性.
默认情况下,它是启用的,因为大多数Java应用程序都依赖 JSESSIONID
cookie. 如果应用程序不需要它,可以关闭此选项,SockJS 客户端应在 IE 8 和 9 中选择 xdr-streaming
.
如果您确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应头 X-Frame-Options
设置为 DENY
,SAMEORIGIN
或 ALLOW-FROM <origin>
来指示浏览器阻止在给定页面上使用 IFrames. 这用于防止 点击劫持.
Spring Security 3.2+ 版本为每个响应提供 |
如果您的应用程序添加了 X-Frame-Options
响应头 (其实应该这样做) ,并且是依赖 iframe-based 传输的,则需要将 header 设置为 SAMEORIGIN
或 ALLOW-FROM <origin>
. 随着 Spring SockJS 的支持也需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的. 默认情况下,iframe 被设置为从 CDN 位置下载 SockJS 客户端. 最好将此选项配置为来自应用程序相同的 URL 源.
以下示例显示了如何在 Java 配置中执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
XML 命名空间通过 <websocket:sockjs>
元素提供了类似的选项.
在初始开发期间,请启用 SockJS 客户端 devel 模式,以防止浏览器缓存否则将被缓存的 SockJS 请求 (如iframe) . 有关如何启用它的详细信息,请参阅 SockJS client 页面.
|
4.3.4. 心跳
SockJS 协议要求服务器发送心跳消息以防止代理结束了连接. Spring SockJS 配置有一个称为 heartbeatTime
的属性,可用于自定义频率. 默认情况下,如果没有在该连接上发送其他消息,它会在 25 秒后发送心跳. 这 25 秒的值符合以下 IETF 推荐 公共互联网应用程序的设置.
在 WebSocket/SockJS 中使用 STOMP 时,如果 STOMP 的客户端和服务器忽略心跳的交互,SockJS 的心跳可以被关闭. |
Spring SockJS 还允许配置 TaskScheduler
用于设置计划心跳任务. 任务计划程序由具有默认设置的线程池支持,该线程池基于可用处理器的数量. 应用程序应考虑根据特定需要而自定义设置.
4.3.5. 客户端断开连接
HTTP 流和 HTTP 长轮询 SockJS 传输要求连接保持比通常更长时间的停留,有关这些技术的概述,请参阅此 博客文章.
在 Servlet 容器中,这个功能是通过 Servlet 3 异步支持完成的,它允许退出 Servlet 容器线程处理请求并继续从另一个线程写入响应.
一个特殊的问题是,Servlet API 不会为已断开的客户端发送通知. 请参看 eclipse-ee4j/servlet-api#44. 但是,Servlet 容器在随后尝试写入响应时引发异常. 由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下是每隔25秒就发一次),这意味着在该时间段内或更早地发送消息时,通常会检测到客户端断开.
有时候客户端的断开连接会导致服务端发生网络 IO 故障,从而记录了不必要的堆栈跟踪日志. Spring 尽最大努力识别出代表客户端断开连接(特定于每台服务器)的网络故障, 并使用 AbstractSockJsSession 中定义的专用日志类别 DISCONNECTED_CLIENT_LOG_CATEGORY 记录最少的消息. 如果需要查看堆栈跟踪,请将该日志类别设置为 TRACE .
|
4.3.6. SockJS 和 CORS
如果允许 cross-origin 请求(请参阅第 Allowed Origins (允许来源)), 则 SockJS 协议使用 CORS 在 XHR 流和轮询传输中进行跨域支持. 因此,除非检测到响应中存在 CORS 头, 否则会自动添加 CORS 头. 因此,如果应用程序已经配置为提供 CORS 支持,例如通过 Servlet 过滤器,Spring 的 SockJsService
将跳过此部分.
也可以通过在 Spring 的 SockJsService 中设置 suppressCors
属性来禁用这些 CORS 头的添加.
以下是 SockJS 的默认头信息列表和默认值:
-
Access-Control-Allow-Origin
: 初始化来自请求头中Origin
的值 -
Access-Control-Allow-Credentials
: 始终设置为true
-
Access-Control-Request-Headers
: 初始化值来自相同的请求头 -
Access-Control-Allow-Methods
: HTTP方法传输支持(见TransportType
的枚举). -
Access-Control-Max-Age
: 设置为31536000 (1 年).
对于实现可以看 AbstractSockJsService
中的 addCorsHeaders
方法,也可以看 TransportType
枚举类的源代码.
或者, 如果 CORS 配置允许它考虑排除 URL 与 SockJS 终端前缀,从而让 Spring 的 SockJsService
处理它.
4.3.7. SockJsClient
提供 SockJS 的 Java 客户端版本,方便在不使用浏览器的情况下连接到远程 SockJS 端点. 当需要通过公用网络(即网络代理可能不使用 WebSocket 协议)在两个服务器之间进行双向通信时,这一点尤其有用. SockJS Java 客户端对于测试目的也非常有用,例如模拟大量并发用户.
SockJS 的 Java 客户端支持 websocket
, xhr-streaming
, 和 xhr-polling
传输.,其余的部分只在浏览器中使用才有意义.
WebSocketTransport
可以配置为:
-
JSR-356 运行时中的
StandardWebSocketClient
. -
JettyWebSocketClient
使用 Jetty9 本地的 WebSocket 的 API -
Spring
WebSocketClient
的任何实现.
根据定义,XhrTransport
支持 xhr-streaming
和 xhr-polling
,因为从客户端的角度来看,除了用于连接服务器的 URL 之外没有其他区别. 目前有两种实现方式:
-
RestTemplateXhrTransport
使用 Spring 的RestTemplate
进行 HTTP 请求. -
JettyXhrTransport
使用 Jetty 的HttpClient
进行HTTP请求.
以下示例显示如何创建 SockJS 客户端并连接到 SockJS 端点:
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS 使用 JSON 格式的数组进行消息传递. 默认情况下,使用 Jackson 2 并且需要在类路径上. 或者,您可以配置 SockJsMessageCodec 的自定义实现并在 SockJsClient 上配置它.
|
要使用 SockJsClient
模拟大量并发用户,您需要配置底层 HTTP 客户端 (用于 XHR 传输) 以允许足够数量的连接和线程. 以下示例显示了如何使用 Jetty 执行此操作:
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
以下示例显示了您应该考虑自定义的服务器端 SockJS 相关属性 (请参阅 javadoc 以获取详细信息) :
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
1 | 将 streamBytesLimit 属性设置为 512KB (默认值为 128KB- 128 * 1024 ) . |
2 | 将 httpMessageCacheSize 属性设置为 1000 (默认值为 100 ) . |
3 | 将断开连接延迟属性设置为 30 秒 (默认值为 5 秒- 5 * 1000 ) . |
4.4. STOMP
WebSocket 协议定义了两种类型的消息,即文本和二进制,但它们的内容是未定义的. 预计客户端和服务器可能会同意使用协议(即更高级别的消息传递协议) . 虽然使用协议与 WebSocket 是完全分离的, 无论哪种方式客户端和服务器将需要协商好用某种协议,以帮助解析消息.
4.4.1. 概述
STOMP (是一种简单的面向文本的消息传递协议) ,最初是为 Ruby、Python 和 Perl 等脚本语言创建的,用于连接企业消息代理. 它旨在解决常用的消息传递模式的一个子集. STOMP 可以用于任何可靠的双工流网络协议,如 TCP 和 WebSocket. 虽然 STOMP 是一种面向文本的协议,但消息的类型可以是文本或二进制.
STOMP 基于 HTTP 帧模型协议的,STOMP 的帧结构如下
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用 SEND
或 SUBSCRIBE
命令来发送或订阅消息,并且通过 destination
的头部信息描述消息的内容将由谁来接收. 这是一个简单的发布/订阅策略,可用于通过代理向其他连接的客户端发送消息,或向服务器发送消息以请求执行某些工作.
当使用 Spring 来支持的 STOMP 时,Spring WebSocket 的应用在客户端中扮演着 STOMP 代理的角色. 消息被路由到 @Controller
的消息处理方法或一个简单的内存代理,它跟踪订阅并向订阅用户广播消息. 还可以配置 Spring 与专门的 STOMP 代理(如 RabbitMQ,ActiveMQ 等)的实际广播的消息. 在这种情况下,Spring 维护到代理的 TCP 连接,将消息中继到它,并将消息从它传递到连接的 WebSocket 客户端. 因此,Spring Web 应用程序可以依赖统一的 HTTP 安全性、通用验证和熟悉的编程模型消息处理工作.
下面是一个客户端订阅接收股票报价的示例,服务器可能会周期性地发出此消息,例如通过 SimpMessagingTemplate
向代理发送邮件的计划任务:
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
下面是一个客户端发送贸易请求的示例,服务器可以通过 @MessageMapping
方法处理该交易请求:
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
执行后,服务器可以向客户端广播交易确认消息和详细信息.
在 STOMP 规范中故意将目的地的含义保持不透明. 它可以是任何字符串,完全取决于 STOMP 服务器来定义它们支持的目标的语义和语法. 然而,很常见的是,但是目的地是类似路径的字符串,其中 /topic/..
意味着发布 - 订阅 (一对多) 和 /queue/
意味着点对点 (一对一) 消息交流.
STOMP 服务器可以使用 MESSAGE
命令向所有订户广播消息. 以下是向已订阅的客户端发送股票报价的服务器示例:
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
服务器无法发送未经请求的消息. 来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的 subscription-id
头必须与客户端订阅的 id
头匹配.
上面的概述目的是对 STOMP 协议有一个基本的理解. 我们建议查看完整的 协议规范 .
4.4.2. 优点
使用 STOMP 作为子协议,Spring Framework 和 Spring Security 提供了比使用原始 WebSocket 更丰富的编程模型. 关于 HTTP 与原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,可以做出同样的观点. 以下是一系列好处:
-
无需自定义消息的格式
-
可以使用 STOMP 客户端,包括 Spring Framework 中的 Java client
-
您可以 (可选) 使用消息代理 (例如 RabbitMQ,ActiveMQ 等) 来管理订阅和广播消息.
-
可以在任意数量的
@Controller
实例中组织应用程序逻辑,并且可以基于 STOMP 目 header 将消息路由到它们,而不是使用给定连接的单个WebSocketHandler
处理原始 WebSocket 消息. -
您可以使用 Spring Security 根据 STOMP 目标和消息类型保护消息.
4.4.3. 启用 STOMP
spring-messaging
和 spring-websocket
模块提供 STOMP over WebSocket 支持. 一旦有了这些依赖,就可以通过带有 SockJS 回调选项 的 WebSocket 暴露 STOMP 端点,如下例所示:
@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 | 目 header 以 /app 开头的 STOMP 消息将路由到 @Controller 类中的 @MessageMapping 方法. |
3 | 使用内置消息代理进行订阅和广播,并将目 header 以 /topic or /queue 开头的消息路由到代理. |
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
</websocket:message-broker>
</beans>
对于内置的简单代理,/topic 和 /queue 前缀没有任何特殊含义. 它们仅仅是区分 pub-sub 和点对点消息传递的惯例 (即,许多订阅者与一个消费者) . 使用外部代理时,请检查代理的 STOMP 页面,以了解它支持的 STOMP 目标和前缀类型.
|
要从浏览器连接,对于SockJS,您可以使用 sockjs-client
. 对于 STOMP,许多应用程序使用了 jmesnil/stomp-websocket库 (也称为 stomp.js) ,它是功能完备的,已经在生产中使用多年但不再维护. 目前, JSteunou/webstomp-client 是该库中最积极维护和不断发展的继任者. 以下示例代码基于它:
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
或者,如果通过 WebSocket 连接 (没有 SockJS) ,则可以使用以下代码:
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
注意上面的 stompClient
不需要指定 login
和 passcode
的头信息. 即使是这样,它们也会在服务器端被忽略,或者被重写. 有关身份验证的详细信息,请参阅 连接到消息代理 和 身份验证.
有关更多示例代码,请参阅
-
Using WebSocket to build an interactive web application — 入门指南.
-
Stock Portfolio — 一个示例应用程序.
4.4.4. WebSocket 服务器
要配置基础 WebSocket 服务器,应用服务器配置 中的信息. 但是对于 Jetty,您需要通过 StompEndpointRegistry
设置 HandshakeHandler
和 WebSocketPolicy
:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
4.4.5. 消息流
一旦暴露了 STOMP 端点,Spring 应用程序就成为连接客户端的 STOMP 代理. 本节介绍服务器端的消息流.
spring-messaging
模块包含对源自 Spring Integration 的消息传递应用程序的基础支持,后来被提取并整合到 Spring Framework 中,以便在许多 Spring projects和应用程序场景中得到更广泛的使用. 以下列表简要介绍了一些可用的消息传递抽象:
-
Message: 携带有头和负载的消息
-
MessageHandler: 消息处理协议
-
MessageChannel: 发送消息的协议,使发件人和接收方弱耦合
-
SubscribableChannel: 继承自
MessageChannel
并且发送消息给注册的MessageHandler
订阅者 -
ExecutorSubscribableChannel: 使用
Executor
传递消息的SubscribableChannel
.
Java 配置 (即 @EnableWebSocketMessageBroker
) 和 XML 命名空间配置 (即 <websocket:message-broker>
) 都使用前面的组件来组装消息工作流. 下图显示了启用简单内置消息代理时使用的组件:

上图显示了三个消息通道:
-
clientInboundChannel
: 用于传递来自 WebSocket 客户端的消息 -
clientOutboundChannel
: 用于传递给 WebSocket 客户端的消息 -
brokerChannel
: 用于从应用程序中向代理发送消息
下图显示了配置外部代理 (例如 RabbitMQ) 以管理订阅和广播消息时使用的组件:

这两个图之间的主要区别在于使用 “broker relay” 通过 TCP 将消息传递到外部 STOMP 代理,以及将消息从代理传递到订阅的客户端.
当从 WebSocket 连接接收消息时,它们被解码为 STOMP 帧,变为 Spring 消息表示,并发送到 clientInboundChannel
以进行进一步处理. 例如, 目标 header 以 /app
开头的 STOMP 消息可以路由到带注解的控制器中的 @MessageMapping
方法,而 /topic
和 /queue
消息可以直接路由到消息代理.
带注解的 @Controller
可以处理由客户端通过 brokerChannel
发送到服务端的消息,并且通过 clientOutboundChannel
将消息广播给匹配的订阅者. 同一个控制器也可以响应 HTTP 请求执行相同操作,因此客户端可以执行 HTTP POST,然后 @PostMapping
方法可以向消息代理发送消息以向订阅的客户端广播.
我们可以通过一个简单的例子来追踪流程. 请考虑以下示例,该示例设置服务器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}
上面的示例支持以下流程:
-
客户端需要连接到
localhost:8080/portfolio
,一旦建立了WebSocket连接,STOMP帧就开始在其上流动. -
客户端发送 SUBSCRIBE 帧,其目 header 为
/topic/greeting
. 收到并解码后,消息将发送到clientInboundChannel
,然后路由到存储客户端订阅的消息代理. -
客户端向
/app/greeting
发送aSEND帧./app
前缀有助于将其路由到带注解的控制器. 除去/app
前缀后,目标的剩余/greeting
部分将映射到GreetingController
中的@MessageMapping
方法. -
从
GreetingController
返回的值变为Spring消息,其中有效负载基于返回值和/topic/greeting
的默认目 header (从/app
替换为/topic
) . 生成的消息将发送到brokerChannel
并由消息代理处理. -
消息代理找到所有匹配的订阅者,并通过
clientOutboundChannel
向每个订阅者发送一个 MESSAGE 帧,消息被编码为 STOMP 帧并在 WebSocket 连接上发送.
下一节提供了有关注解方法的更多详细信息,包括支持的参数类型和返回值.
4.4.6. 注解控制器
应用程序可以使用带注解的 @Controller
类来处理来自客户端的消息. 这些类可以声明 @MessageMapping
, @SubscribeMapping
, 和 @ExceptionHandler
方法,如以下主题中所述:
@MessageMapping
您可以使用 @MessageMapping
注解将方法映射到消息目标. 它在方法级别和类型级别受支持. 在类型级别,@MessageMapping
用于表示控制器中所有方法的共享映射.
默认目的地的匹配是 Ant 风格的使用斜线分隔(例如, /thing*
, /thing/**
),他们可以包含模板变量(例如, /thing/{id}
). 可以通过 @DestinationVariable
的注解方法参数引用. 应用程序还可以切换到以点为分隔符的映射目标约定,如 点作为分隔符 中所述.
支持以下方法参数
下表描述了方法参数:
方法参数 | 描述 |
---|---|
|
用于访问完整的消息. |
|
用于访问Message中的头部信息. |
|
通过类型化方法访问头部信息 |
|
用于访问消息的负载,,由已配置的 由于默认情况下是假定的,因此不需要注解的存在. 您可以使用 |
|
用于访问特定header值 - 以及使用 — |
|
用于访问消息中的所有 header . 此参数必须可分配给 |
|
用于访问从消息目标提取的模板变量. 根据需要将值转换为声明的方法参数类型. |
|
反射在 WebSocket HTTP 握手时登录的用户 |
返回值
默认的情况下,@MessageMapping
方法的返回值是用 MessageConverter
转换的,并作为新消息的正文. 默认情况下将其发送到与客户端消息具有相同目标的 brokerChannel
, 但默认情况下使用前缀 /topic
.
您可以使用 @SendTo
和 @SendToUser
注解来自定义输出消息的目标. @SendTo
用于自定义目标目的地或指定多个目的地. @SendToUser
用于将输出消息定向到仅与输入消息关联的用户. 请参阅用户的目的地.
您可以在同一方法上同时使用 @SendTo
和 @SendToUser
,并且在类级别都支持它们,在这种情况下,它们充当类中方法的默认值. 但是,请记住,任何方法级别的 @SendTo
或 @SendToUser
注解都会覆盖类级别的任何此类注解.
消息可以异步处理,@MessageMapping
方法可以返回 ListenableFuture
, CompletableFuture
, 或 CompletionStage
.
请注意,@SendTo
和 @SendToUser
仅仅是一种便利,相当于使用 SimpMessagingTemplate
发送消息. 如有必要,对于更高级的方案, @MessageMapping
方法可以直接使用 SimpMessagingTemplate
. 这可以代替返回值,或者可能另外返回值. 请参阅发送消息.
@SubscribeMapping
@SubscribeMapping
类似于 @MessageMapping
,但仅将映射缩小为订阅消息. 它支持与 @MessageMapping
相同的方法参数 . 但是对于返回值,默认情况下,消息将直接发送到客户端 (通过 clientOutboundChannel
,以响应订阅) 而不是发送到代理 (通过 brokerChannel
,作为匹配订阅的广播) . 添加 @SendTo
或 @SendToUser
会覆盖此行为并发送给代理.
什么时候有用? 假设代理映射到 /topic
和 /queue
,而应用程序控制器映射到 /app
. 在此设置中,代理将所有订阅存储到 /topic
和 /queue
,用于重复广播,并且不需要应用程序参与.
客户端还可以订阅某个 /app
目的地,并且控制器可以返回响应于该订阅的值而不涉及代理而不再存储或使用订阅 (实际上是一次性请求 - 回复交换) . 一个用例是在启动时使用初始数据填充 UI.
什么时候这没用? 不要尝试将代理和控制器映射到相同的目标前缀,除非您由于某种原因希望两者都独立处理消息 (包括订阅) . 入站消息是并行处理的. 无法保证代理或控制器是否首先处理给定的消息. 如果在存储订阅并准备好广播时通知目标,则客户端应该在服务器支持时询问收据 (简单代理不支持) . 例如,使用 Java STOMP client,您可以执行以下操作来添加收据:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});
服务器端选项是在 brokerChannel
上 注册 ExecutorChannelInterceptor
,并在处理完消息 (包括订阅) 后实现调用的 afterMessageHandled
方法.
@MessageExceptionHandler
应用程序可以使用 @MessageExceptionHandler
方法来处理来自 @MessageMapping
方法的异常. 如果要访问异常实例,可以在注解本身或通过方法参数声明异常. 以下示例通过方法参数声明异常:
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
@MessageExceptionHandler
方法支持灵活的方法签名,并支持与@MessageMapping
方法相同的方法参数类型和返回值.
通常,@MessageExceptionHandler
方法适用于声明它们的 @Controller
类 (或类层次结构) . 如果您希望此类方法更全局地应用 (跨控制器) , 则可以在标有 @ControllerAdvice
的类中声明它们. 这与 Spring MVC 中提供的类似支持相当.
4.4.7. 发送消息
如果要将消息从应用程序的任何组件发送到已连接的客户端,该怎么办? 任何应用程序的组件都可以向 brokerChannel
发送消息. 最简单的方法是 SimpMessagingTemplate
注入,并使用它来发送消息. 通常,它使用类型注入是如此方便的. 如以下示例所示:
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
但是,如果存在相同类型的另一个 bean,您还可以通过其名称 (brokerMessagingTemplate
) 对其进行限定.
4.4.8. 简单的消息代理
内置的简单消息代理处理来自客户端的订阅请求,首先会将它们存储在内存中,然后将消息广播到具有匹配目的地的已连接客户端. 代理支持类似路径的目的地,包括对 Ant 样式的目的地模式的订阅.
应用程序还可以使用点分隔 (而不是斜线分隔) 目标. 请查阅点作为分隔符. |
如果配置了任务调度程序,则简单代理支持 STOMP 心跳. 要配置调度程序,您可以声明自己的 TaskScheduler
bean 并通过 MessageBrokerRegistry
进行设置。 或者,您可以使用内置 WebSocket
配置中自动声明的那个,但是,您需要 @Lazy
来避免
内置 WebSocket 配置和您的 WebSocketMessageBrokerConfigurer
之间的循环。 例如:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private TaskScheduler messageBrokerTaskScheduler;
@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
// ...
}
}
4.4.9. 全功能的消息代理
简单的代理作为入门级别功能强大,但只是 STOMP 命令支持的一个子集(如没有 ack,接收者等),依赖于一个简单的消息循环发送,这不适合集群. 作为一种替代方法,应用程序可以升级到使用功能完备的消息代理.
请参阅 STOMP 文档以了解您选择的消息代理 (例如 RabbitMQ, ActiveMQ 等) ,安装代理,并在启用 STOMP 支持的情况下运行它. 然后,您可以在 Spring 配置中启用 STOMP 代理中继 (而不是简单代理) .
以下示例配置启用功能齐全的代理:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
</beans>
上面配置中的 STOMP broker relay 在spring的 MessageHandler
处理请求通过转发他们给外部的消息代理. 为了这么做需要建立 TCP 连接到消息代理,,转发所有的消息给代理并且转发所有的接收到的来自消息代理发送给客户端的内容(通过她们的 WebSocket 会话). 本质上对于传递消息在两个目的地之间,它扮演 "relay" 角色
将 io.projectreactor.netty:reactor-netty 和 io.netty:netty-all 依赖添加到项目中以进行TCP连接管理.
|
此外,应用组件 (例如 HTTP 请求处理方法、业务服务等) 也可以发送消息给消息代理. 如发送消息中所述,以向订阅的 WebSocket 客户端广播消息.
实际上,代理中继实现了强健且可伸缩的消息广播
4.4.10. 连接到消息代理
STOMP 代理中继会对代理维持单一的 “system” 级 TCP 连接. 此连接仅用于来自服务器端应用程序的消息,而不用于接收消息. 可以配置此连接的 STOMP 证书,即 STOMP 帧的 login
和 passcode
头, 这在 XML 命名空间和 Java 配置中都作为 systemLogin
和 systemPasscode
属性暴露,默认值是 guest
和 guest
.
STOMP 代理中继还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接,可以将 STOMP 证书配置为代表客户端创建的所有 TCP 连接使用. 这在 XML 命名空间和 Java配 置中都作为 clientLogin
和 clientPasscode
属性暴露,默认值是 guest
和 guest
.
STOMP 代理中继总是在每个连接帧前设置 login 和 passcode 头,它转发给代理客户端. 因此,WebSocket 客户端无需设置这些 header ,它们也将被忽略. 正如身份验证 部分所述,WebSocket 客户端应依赖 HTTP 身份验证来保护 WebSocket 端点并建立客户端标识.
|
STOMP 代理也能与消息代理在 “system” 级的 TCP 连接上发送和接收心跳,可以配置发送和接收心跳的间隔 (默认是每 10 秒) ,如果与代理的连接丢失,代理中继将继续每 5 秒尝试重新连接,直到成功.
当与代理的 "system" 级连接丢失并重新建立时,Spring bean 可以实现 ApplicationListener<BrokerAvailabilityEvent>
以接收通知. 例如,当没有 active 的 “system” 级连接时,股票报价服务广播股票行情可以停止尝试发送消息.
默认情况下,STOMP 代理中继始终连接,并在连接丢失时根据需要重新连接到同一主机和端口. 如果您希望提供多个地址,则在每次尝试连接时,您都可以配置地址供应商,而不是固定的主机和端口. 以下示例显示了如何执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
private ReactorNettyTcpClient<byte[]> createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}
您还可以使用 virtualHost
属性配置 STOMP 代理中继. 这个属性的值将被设置为每个 CONNECT 帧的 host
头信息,并且在云环境中是可能很有用,尤其是建立 TCP 连接的实际主机与提供基于云的 STOMP 服务的主机不同的时候.
4.4.11. 点作为分隔符
当消息路由到 @MessageMapping
方法时,它们与 AntPathMatcher
匹配. 默认情况下,模式应使用斜杠 (/
) 作为分隔符. 这是 Web 应用程序中的一个很好的约定,类似于 HTTP URL. 但是,如果您更习惯于消息传递约定,则可以切换到使用点 (.
) 作为分隔符.
以下示例显示了如何在Java配置中执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
<websocket:stomp-endpoint path="/stomp"/>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
<constructor-arg index="0" value="."/>
</bean>
</beans>
之后,控制器可以使用点 (.)
作为 @MessageMapping
方法中的分隔符,如以下示例所示:
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
客户现在可以向 /app/red.blue.green123
发送消息. .
在前面的示例中,我们没有更改 “broker relay” 上的前缀,因为它们完全依赖于外部消息代理. 请参阅您使用的代理的 STOMP 文档页面,以查看它为目标 header 支持的约定.
另一方面,"简单代理"确实依赖于配置的 PathMatcher
,因此,如果切换分隔符,则该更改也适用于代理以及代理将目标从消息与预订中的模式匹配的方式.
4.4.12. 身份验证
WebSocket 每一次 STOMP 的消息传递会话都是从 HTTP 请求开始的,可以是升级版的 WebSockets (即 WebSocket 握手)的请求,或者是 SockJS 回调的一系列 SockJS HTTP 传输请求的情况.
许多 Web 应用程序已经有身份验证和授权来保护 HTTP 请求. 通常,用户通过 Spring 安全性(如登录页、HTTP 基本身份验证或其他机制)进行身份验证. 经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一 cookie 会话中的后续请求相关联.
因此,对于 WebSocket 握手或 SockJS HTTP 传输请求,通常会有通过 HttpServletRequest#getUserPrincipal()
访问的经过身份验证的用户. Spring 会自动将该用户与为其创建的 WebSocket 或 SockJS 会话关联,随后用在该用户的所有 STOMP 消息的会话传输过程的头部.
简而言之,典型的 Web 应用程序除了已经为安全性做的事情之外,不需要做任何事情. 用户在 HTTP 请求级别进行身份验证,通过基于 cookie 的 HTTP 会话维护的安全上下文,然后与为该用户创建的 WebSocket 或 SockJS 会话关联,并通过应用程序在每个消息流中标记用户的头信息.
请注意,STOMP 协议在 CONNECT
帧上有 login
和 passcode
头. 这些都是最初的设计并且仍然现在有些还是需要的. 然而,对于 WebSocket 上的 STOMP 在默认情况下,Spring 会忽略 STOMP 协议级别的授权头,并假定用户已经在 HTTP 传输级别进行了身份验证,并希望 WebSocket 或 SockJS 会话包含经过身份验证的用户.
4.4.13. 基于 token 的验证
Spring Security OAuth为基于令牌的安全性(包括 JSON Web Token(JWT))提供支持. 您可以将其用作 Web 应用程序中的身份验证机制,包括 STOMP over WebSocket 交互,如上一节所述 (即通过基于 cookie 的会话维护身份) .
同时,cookie 会话并不总是最适合的,例如在不希望维护服务器端会话的应用程序中,或者在移动应用程序中,通常使用报头进行身份验证.
WebSocket 协议, WebSocket protocol, RFC 6455 没有规定服务器在 WebSocket 握手期间可以对客户端进行身份验证的任何特定方式. 但是在实践中, 浏览器客户端只能使用标准的身份验证报头(即基本的 HTTP 身份验证)或 cookie,例如不能提供自定义的 header . 同样,SockJS JavaScript 客户端不提供使用 SockJS 传输请求发送 HTTP 头的方法. 请参阅 sockjs-client issue 196. 相反,它确实允许发送可用于发送令牌的查询参数,但这有其自身的缺点 (例如,令牌可能无意中使用服务器日志中的 URL 记录) .
上述限制适用于基于浏览器的客户端,并且不适用于支持使用 WebSocket 和 SockJS 请求发送报头的 Spring Java 的 STOMP 客户端. |
因此,希望避免使用了 cookie 的应用程序在 HTTP 协议级别可能没有任何适合的身份验证供选择. 与其使用 cookie,他们可能更愿意在 STOMP 消息协议级别对头信息进行身份验证,两个简单的步骤可完成这个工作:
-
使 STMOP 客户端在连接时传递身份验证头
-
使用
ChannelInterceptor
方法处理身份验证头
下一个示例使用服务器端配置来注册自定义身份验证拦截器. 请注意,拦截器只需要在连接消息上进行身份验证并设置用户头. Spring 将注意到并保存经过身份验证的用户,并将其与同一会话中的后续 STOMP 消息相关联. 以下示例显示如何注册自定义身份验证拦截器:
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}
还要注意,在使用 Spring Security 对消息的授权时,目前需要确保在 Spring 安全之前做好了身份验证的 ChannelInterceptor
配置,最好通过在自己的 WebSocketMessageBrokerConfigurer
实现中声明自定义拦截器来实现,该实现使用 @Order(Ordered.HIGHEST_PRECEDENCE + 99)
进行标记.
4.4.14. Authorization
Spring Security 提供 WebSocket 子协议授权,该授权使用 ChannelInterceptor
根据其中的用户头来授权消息. 此外,Spring Session 提供了 WebSocket 集成,可确保在 WebSocket 会话仍处于 active 状态时用户 HTTP 会话不会过期.
4.4.15. 用户的目的地
应用程序可以发送针对特定用户的消息,Spring 的 STOMP 支持可识别以 /user/
为前缀的目标. 例如,客户端可能订阅 /user/queue/position-updates
目的地. 此目标由 UserDestinationMessageHandler
处理,并转换为用户会话唯一的目标 (例如 /queue/position-updates-user123
) . 这为订阅通用命名的目的地提供了方便,同时确保与订阅同一目的地的其他用户没有冲突,以便每个用户都能接收到唯一的库存位置更新.
使用用户目的地时,配置代理和应用程序目的地前缀很重要,如 启用 STOMP 中所示,否则代理将处理仅应由 UserDestinationMessageHandler 处理的 "/user" 前缀消息 .
|
在发送方,可以将消息发送到目的地,例如 /user/{username}/queue/position-updates
,,然后由 UserDestinationMessageHandler
将其转换为一个或多个目的地,每个目的地对应于与用户相关联的每个会话. 这允许应用程序中的任何组件发送针对特定用户的消息,而不必知道比其名称和通用目的地更重要的内容. 也可以通过注解和消息传递模板来支持这一点.
消息处理方法可以向与通过 @SendToUser
注解处理的消息相关的用户发送消息(也支持在类级别上共享一个公共目的地). 如以下示例所示:
@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}
如果用户有多个会话,默认情况下,所有会话都已预订 到给定目标的目标. . 但是,有时可能需要只针对发送所处理消息的会话. 您可以通过将 broadcast
属性设置为 false
来执行此操作,如以下示例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}
虽然用户目的地通常意味着是一个经过身份验证的用户,但这不需要严格执行. 未与经过身份验证的用户关联的 WebSocket 会话也可以订阅用户目的地. 在这种情况下, @SendToUser 注解的行为将与 broadcast=false 效果完全相同,即只针对发送所处理消息的会话.
|
您可以从任何应用程序组件向用户目标发送消息,例如,注入由 Java 配置或 XML 命名空间创建的 SimpMessagingTemplate
. (如果需要使用 @Qualifier
进行限定,则 bean 名称为 "brokerMessagingTemplate"
. ) 以下示例说明了如何执行此操作:
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
在使用具有外部消息代理的用户目的地时,请查看有关如何管理非 active 队列的代理文档,以便在用户会话结束时删除所有唯一的用户队列. 例如,当您使用目的地 (例如 /exchange/amq.direct/position-updates ) 时,RabbitMQ 会创建自动删除队列.
因此,在这种情况下,客户端可以订阅 /user/exchange/amq.direct/position-updates . 同样,ActiveMQ 具有用于清除非 active 目的地的 配置选项.
|
在多服务器方案中,用户目标可能仍未解决,因为用户已连接到其他服务器. 在这种情况下,您可以将目的地配置为广播未解析的消息,以便让其他服务器有机会尝试. 这可以通过 Java 配置中的 MessageBrokerRegistry
的 userDestinationBroadcast
属性和XML中 message-broker
元素的 user-destination-broadcast
属性来完成.
4.4.16. 消息顺序
来自代理的消息将发布到 clientOutboundChannel
,从那里将它们写入 WebSocket 会话. 由于通道由 ThreadPoolExecutor
支持,因此消息在不同的线程中处理,并且客户端接收的结果顺序可能与发布的确切顺序不匹配.
如果这是一个问题,请启用 setPreservePublishOrder
标志,如以下示例所示:
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker preserve-publish-order="true">
<!-- ... -->
</websocket:message-broker>
</beans>
设置标志后,同一客户端会话中的消息将一次发布到 clientOutboundChannel
,以便保证发布顺序. 请注意,这会导致较小的性能开销,因此只有在需要时才应启用它.
4.4.17. 事件
发布了几个 ApplicationContext
事件,可以通过实现 Spring 的 ApplicationListener
接口来接收它们:
-
BrokerAvailabilityEvent
: 表明代理何时可用/不可用. 当 “simple” 的代理在启动时立即可用,并且在应用程序运行时保持不变. STOMP "broker relay" 可能会失去与完整功能代理的连接 (例如代理重新启动) . 代理中继会启动重新连接逻辑,并将在重新连接后重建与代理的 “system” 级连接. 因此,只要状态从连接变为断开连接,就会发布此事件,反之亦然. 使用SimpMessagingTemplate
的组件应订阅此事件,并避免在代理不可用时发送消息. 在任何情况下,它们应该准备在发送消息时处理MessageDeliveryException
. -
SessionConnectEvent
: 在收到新的 STOMP CONNECT 时发布,表示新客户端会话的开始. 该事件包含表示连接的消息,包括会话 ID,用户信息 (如果有) 以及客户端发送的任何自定义 header . 这对于跟踪客户端会话很有用. 订阅此事件的组件可以使用SimpMessageHeaderAccessor
或StompMessageHeaderAccessor
包装所包含的消息. -
SessionConnectedEvent
: 在SessionConnectEvent
之后不久发布,当代理已发送 STOMP CONNECTED 帧以响应 CONNECT 时. 此时,可以认为 STOMP 会话已完全建立. -
SessionSubscribeEvent
: 在收到新的 STOMP SUBSCRIBE 时发布. -
SessionUnsubscribeEvent
: 在收到新的 STOMP UNSUBSCRIBE 时发布. -
SessionDisconnectEvent
: 当 STOMP 会话结束后被发布. DISCONNECT 是客户端发送的,也可能是在关闭 WebSocket 会话时自动生成的. 在某些情况下,此事件可能会在每个会话中发布一次以上. 对于多个断开事件,组件功能应该是幂等的.
当您使用功能齐全的代理时,如果代理暂时不可用,STOMP "代理中继" 会自动重新连接 “system” 连接. 但是,客户端连接不会自动重新连接. 假设启用了心跳,客户端通常会注意到代理在 10 秒内没有响应. 客户端需要实现自己的重新连接逻辑. |
4.4.18. 拦截
事件 为 STOMP 连接的生命周期提供通知,但不为每个客户端消息提供通知. 应用程序还可以注册 ChannelInterceptor
来拦截任何消息以及处理链的任何部分. 以下示例显示如何拦截来自客户端的入站邮件:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}
自定义 ChannelInterceptor
可以使用 StompHeaderAccessor
或 SimpMessageHeaderAccessor
来访问有关消息的信息,如以下示例所示:
public class MyChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getStompCommand();
// ...
return message;
}
}
应用程序还可以实现 ExecutorChannelInterceptor
,它是 ChannelInterceptor
的子接口,在处理消息的线程中具有回调. 虽然为发送到通道的每个消息调用一次 ChannelInterceptor
,但 ExecutorChannelInterceptor
在订阅来自通道的消息的每个 MessageHandler
的线程中提供挂钩.
请注意,与前面描述的 SesionDisconnectEvent
一样,DISCONNECT 消息可以来自客户端,也可以在 WebSocket 会话关闭时自动生成. 在某些情况下,拦截器可能会为每个会话多次拦截此消息. 对于多个断开连接事件,组件应该是幂等的.
4.4.19. STOMP 客户端
Spring 在 WebSocket 客户端和 TCP 客户端上分别提供了 STOMP
开始创建和配置 WebSocketStompClient
, 如下所示:
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
在前面的示例中,您可以将 StandardWebSocketClient
替换为 SockJsClient
,因为它也是 WebSocketClient
的实现. SockJsClient
可以使用 WebSocket 或基于 HTTP 的传输作为后备. 有关更多详细信息,请参阅 SockJsClient.
接下来,您可以建立连接并为 STOMP 会话提供处理程序,如以下示例所示:
String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);
当会话准备好使用时,将通知处理程序,如以下示例所示:
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
// ...
}
}
建立会话后,可以发送任何有效负载并使用配置的 MessageConverter
进行序列化,如以下示例所示:
session.send("/topic/something", "payload");
也可以订阅目的地,. subscribe
方法需要处理订阅消息的处理程序,并返回可用于取消订阅的 Subscription
句柄. 对于每个收到的消息,处理程序可以指定要对其进行反序列化的目标对象类型,如以下示例所示:
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
启用 STOMP 的心跳,需配置 WebSocketStompClient
与 TaskScheduler
,并可选择自定义心跳间隔,10 秒的写不 active 会导致心跳发送和 10 秒的读取不 active 将会关闭连接.
WebSocketStompClient
仅在不活动的情况下 (即未发送其他消息时) 发送心跳. 当使用外部代理时, 这可能会有些困难, 因为发送具有非代理目标的消息时, 实际上并未转发给代理. 在这种情况下, 您可以在初始化 全功能的消息代理 时配置 TaskScheduler
, 以确保即使仅发送具有非代理目标的消息, 也可以将心跳转发给代理.
当使用 WebSocketStompClient 进行性能测试以模拟来自同一台计算机的数千个客户端时,请考虑关闭心跳,因为每个连接都计划自己的心跳任务,而对于在同一台计算机上运行的大量客户端来说,这不是最优化的.
|
STOMP 协议还支持回执,在处理发送或订阅后,客户端必须添加一个 receipt
头,服务器在该报头中响应回执帧. 为了支持这一点, StompSession
提供了 setAutoReceipt(boolean)
,会在每个后续发送或订阅中添加 receipt
头.
另外,也可以手动向 StompHeaders
添加 receipt
头. 发送和订阅返回 Receiptable
的实例,可用于注册回执成功和失败回调. 对于此功能,客户端必须配置 TaskScheduler
,并且设置为回执到期前的时间(默认情况下为 15 秒).
请注意, StompSessionHandler
本身是一个 StompFrameHandler
,它允许它在处理消息的 handleException
回调之外处理 ERROR 帧,以及 handleTransportError
传输级别错误(包括 ConnectionLostException
)
4.4.20. WebSocket 作用域
每个 WebSocket 会话都有一个属性映射表. 这个表被附加为传入客户端消息的 header ,并且可以从控制器方法访问,如以下示例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
// ...
}
}
也可以在 websocket
作用域内声明 Spring 管理的 bean. WebSocket 作用域的 bean 可以被注入到控制器和任何在 clientInboundChannel
上注册的通道拦截器. 这些都是典型的单例和比任何单个 WebSocket 会话的存活时间都要长. 因此,将需要对 WebSocket 作用域的 bean 使用作用作用域代理模式. 如以下示例所示:
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@Controller
public class MyController {
private final MyBean myBean;
@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}
与任何自定义作用域一样,Spring 在第一次从控制器访问时初始化一个新的 MyBean
实例,并将该实例存储在 WebSocket 会话属性中. 随后返回同一实例,直到会话结束. WebSocket 作用域的 bean 将具有所有的 Spring 生命周期方法调用,如上面的示例所示.
4.4.21. 性能
在性能方面没有一个办法是可以解决任何问题的. 许多因素都会影响性能,包括消息的大小、分卷、应用方法执行以及是否阻塞,外部因素例如网络速度等等. 本节的目标是提供可用配置选项的概述,以及有关如何推理、缩放的一些想法.
在消息传递应用程序消息中,通过通道来传递由线程池支持的异步执行. 配置此类应用程序需要对通道和消息流有很好的了解. 因此,建议查看消息流.
一个明显需要重点配置的是两个线程池,clientInboundChannel
和 clientOutboundChannel
. 默认情况下,两者都配置为可用处理器数量的两倍.
如果注解方法处理消息是绑定主 CPU 的话,那么 clientInboundChannel
的线程数应该与处理器数量保持接近. 如果他们所做的工作是更多的 IO 绑定,需要阻塞或等待数据库或其他外部系统,那么线程池的大小将需要增加.
一个常见的混淆点是,配置核心池大小(如 10)和最大池大小(例如 20)会导致线程池中有 10 个至 20 个的线程. 实际上,如果容量是保留在其默认值为 请参阅 |
在 clientOutboundChannel
方面, 这是所有关于向 WebSocket 客户端发送消息的内容. 如果客户端处于快速网络中,则线程数应保持在可用处理器数量附近. 如果它们速度慢或带宽低,则会花费更长的时间来消耗消息并给线程池带来负担. 因此,增加线程池的大小将是必要的.
虽然 clientInboundChannel
的工作负载可以预测 - 毕竟,它基于应用程序的工作 - 如何配置"clientOutboundChannel" 更难,因为它基于应用程序无法控制的因素. 因此,有两个与发送消息相关的附加属性: sendTimeLimit
和 sendBufferSizeLimit
. 这些用于配置在向客户端发送消息时允许发送的时间以及可以缓冲多少数据.
一般的想法是,在任何给定的时间,只有一个线程可以用来发送到客户端. 同时,所有附加消息都得到缓冲,可以使用这些属性来决定允许发送消息的时间长度以及在平均时间内可以缓冲多少数据. 有关重要的其他详细信息,请参阅 XML 架构的 javadoc 和文档.
以下示例显示了可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
<!-- ... -->
</websocket:message-broker>
</beans>
上面显示的 WebSocket 传输配置也可用于配置传入的 STOMP 消息的允许的最大大小配置. 虽然理论上 WebSocket 的消息可以是几乎无限大,但在实践中 WebSocket 服务器通常会强加限制. 例如,在 Tomcat 是 8k、在 Jetty 中是 64k. 因为这个原因,STOMP 客户端 (例如JavaScript webstomp-client等) 可以将更大的 STOMP 信息分割为 16k 一个片,并将它们作为多个 WebSocket 消息发送,从而要求服务器进行缓冲和组装.
Spring 的 STOMP-over-WebSocket 支持实现了这一点,因此应用程序可以配置 STOMP 消息的最大大小,而不管 WebSocket 服务器特定的消息大小. 请记住,必要时会自动调整 WebSocket 消息大小,以确保它们至少可以携带 16K WebSocket 消息.
以下示例显示了一种可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
以下示例显示了与前面示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport message-size="131072" />
<!-- ... -->
</websocket:message-broker>
</beans>
关于伸缩性的一个重要问题是使用多个应用程序实例. 目前,简单的代理是不可能做到的. 但是,当使用功能完备的代理(如 RabbitMQ)时,每个应用程序实例连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的 WebSocket 客户端.
4.4.22. 监控
当使用 @EnableWebSocketMessageBroker
或 <websocket:message-broker>
关键架构组件时,将自动收集统计信息和计数器,以便对应用程序的内部状态提供重要的参考. 该配置还声明了一个类型为 WebSocketMessageBrokerStats
的 bean,它在一个位置收集所有可用信息. 默认情况下,会每隔 30
分钟记录一次日志到 INFO
. 可以通过Spring的 MBeanExporter
将此 bean 导出到 JMX,以便在运行时查看,例如通过 JDK 的 jconsole
. 下面是可用信息的摘要
- Client WebSocket Sessions (客户端WebSocket会话)
-
- Current (当前)
-
当前有多少客户端会话已通过 WebSocketvsHTTPstreaming 和 pollingSockJS 会话
- Total (总共)
-
已建立的总会话数
- Abnormally Closed (非正常关闭)
-
- Connect Failures (连接失败)
-
这些会话已经建立,但在 60 秒内没有收到任何消息后被关闭. 这通常是代理或网络的问题.
- Send Limit Exceeded (发送限制)
-
在超过配置的发送超时或可能与慢速客户端发生的发送缓冲区限制后,会话关闭(请参阅上一节)
- Transport Errors (传输错误)
-
在传输错误(如无法读取或写入 WebSocket 连接或 HTTP 请求/响应)后关闭会话.
- STOMP Frames (STOMP帧)
-
CONNECT、CONNECTED 和 DISCONNECT 帧的总数目表明有多少客户端在 STOMP 级别上连接. 请注意,当会话异常关闭或客户端关闭而不发送断开连接帧时,断开连接计数可能会较低.
- STOMP Broker Relay (STOMP消息代理回复)
-
- TCP Connections (TCP连接数)
-
指示将代表客户端 WebSocket 会话的 TCP 连接设置为代理的数量. 这应该等于客户端 WebSocket 会话数+额外的共享的"系统"级连接,用于从应用程序内发送消息.
- STOMP Frames (STOMP帧)
-
代表客户端转发到或从代理接收的 CONNECT、CONNECTED 和 DISCONNECT 的帧总数. 请注意,无论客户端 WebSocket 会话是如何关闭的,断开连接帧都将发送到代理. 因此,较低的断开连接帧计数是指代理正在主动关闭连接,可能是由于没有及时到达的心跳、无效的输入帧或其他.
- Client Inbound Channel (客户端输入channel)
-
线程池的统计数据支持
clientInboundChannel
提供对传入消息处理的健康的检测. 在这里排队的任务是指应用程序可能太慢,无法处理消息. 如果有 i/o 绑定任务(如慢速数据库查询、对第三方 RESTAPI 的 HTTP 请求等) ,请考虑增加线程池大小. - Client Outbound Channel (客户端输出 channel)
-
线程池的统计数据支持
clientOutboundChannel
提供对向客户端广播消息的健康状况的检测. 在这里排队的任务的问题可能是客户端太慢,无法使用消息. 解决这一问题的一种方法是增加线程池的大小,以适应预期的并发慢速客户端的数量. 另一个选项是减少发送超时和发送缓冲区大小限制(请参阅上一节). - SockJS Task Scheduler (SockJS的任务计划)
-
用于发送心跳的 SockJS 任务计划程序的线程池的统计信息. 请注意,当心跳在 STOMP 级别上协商时,SockJS 心跳将被禁用.
4.4.23. 测试
使用 Spring 的 WebSocket 支持来测试应用程序有两种主要方法. 第一种是编写服务器端测试,以验证控制器的功能及其注解的消息处理方法. 第二个是编写涉及运行客户端和服务器的完整的端到终端测试.
这两种方法不是互斥的. 相反,它们在总体测试策略中有一席之地. 服务器端测试更加集中,更易于编写和维护. 另一方面,端对点的集成测试更完整,更多,但它们也更多地涉及编写和维护
最简单的服务器端测试形式是编写控制器单元测试. 但是,这是不用够的,因为控制器的大部分内容取决于它的注解. 纯单元测试根本无法测试.
理想中的控制器应该在运行时调用,就像测试控制器使用 Spring MVC 测试框架处理 HTTP 请求的方法一样. 即不运行 Servlet 容器,而是依赖于 Spring 框架来调用带注解的控制器. 就像 Spring MVC 测试这里有两种可能的选择,要么使用 "基于上下文" 或 "单独" 设置.
-
在 SpringTestContext 框架的帮助下加载实际的 Spring 配置,将
clientInboundChannel
作为测试字段,并使用它发送要由控制器方法处理的消息. -
手动设置调用控制器(即
SimpAnnotationMethodMessageHandler
)所需的最低 Spring 框架基础结构,并直接向控制器传递消息.
这两种设置方案都在 股票投资组合示例应用程序的测试中演示
第二种方法是创建端到端的集成测试. 为此,需要在嵌入式模式下运行 WebSocket 服务器,并将其作为 WebSocket 客户端发送包含 STOMP 帧的 WebSocket 消息. 股票投资组合的应用程序的测试还演示了这种方法,它使用 Tomcat 作为嵌入式 WebSocket 服务器和简单的 STOMP 客户端进行测试.
5. 其他 Web 框架
本章详细介绍了 Spring 与第三方 Web 框架的集成.
Spring Framework 的核心价值主张之一就是支持选择. 从一般意义上讲,Spring 并没有强迫您使用或购买任何特定的架构,技术或方法(尽管它肯定会推荐一些其他架构,技术或方法) . 这个自由选择的好处在开发人员及其开发团队最相关的技术或方法在 Web 区域中最为明显,其中 Spring 提供了自己的 Web 框架((Spring MVC 和 Spring WebFlux) ) ,同时又提供了与许多流行的第三方 Web 框架的集成.
5.1. 通用的配置
在深入到每个受支持的 Web 框架的集成细节之前,让我们先来看看不特定于任何一个 Web 框架的 Spring 配置(本节同样适用于 Spring 自己的 Web 框架 Spring MVC. )
Spring 的轻量级应用程序模型支持的一个概念(缺少更好的词) 是分层架构的概念. 请记住,在 "经典" 分层架构中,Web 层只是众多层中的一个. 它充当服务器端应用程序的入口点之一,并且它委托给服务层中定义的服务对象(外观) ,以满足特定于业务(和表示技术不可知) 的用例.
在 Spring 中,这些服务对象,任何其他特定于业务的对象,数据访问对象和其他对象存在于不同的 "business context" 中,该业务上下文不包含 Web 或表示层对象(表示对象,例如 Spring MVC 控制器,通常是 在不同的 "presentation context" 中配置. 本节详细介绍了如何配置包含应用程序中所有 "business bean" 的 Spring 容器(WebApplicationContext
) .
在细节上,所需要做的就是在一个 Web 应用程序的标准 Java EE Servlet web.xml
文件中声明一个 ContextLoaderListener
, 并添加一个 contextConfigLocation<context-param/>
(在同一个文件中),它定义了要加载的 Spring xml 配置文件集.
请考虑以下 <listener/>
配置:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
进一步考虑以下 <context-param/>
配置:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
如果未指定 contextConfigLocation
上下文参数,ContextLoaderListener
将会查找一个名为 /WEB-INF/applicationContext.xml
的文件并加载. 加载上下文文件后, Spring 将基于 bean 定义创建一个 WebApplicationContext
对象,并将其存储在 Web 应用程序的 ServletContext
中.
所有 Java Web 框架都是在 Servlet API 的基础上构建的,因此可以使用下面的代码段来访问由 ApplicationContext
创建的 "business context" 的 ContextLoaderListener
.
以下示例显示如何获取 WebApplicationContext
:
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
WebApplicationContextUtils
是方便的工具类,使用它你就不需要记住 ServletContext 属性的名字. 如果对象不存在于 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
的情况下,则其 getWebApplicationContext()
方法将返回 null
.
与其冒着在应用程序中获取 NullPointerExceptions
的风险,不如使用 getRequiredWebApplicationContext()
. 当 ApplicationContext
丢失时,此方法抛出异常.
一旦引用了 WebApplicationContext
,就可以按名称或类型检索 bean. 大多数开发人员按名称检索 bean,然后将其转换为其实现的接口之一.
幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法. 它们不仅可以轻松地从 Spring 容器中获取 bean,而且还允许您在其控制器上使用依赖注入. 每个 Web 框架部分都有关于其特定集成策略的更多详细信息.
5.2. JSF
JavaServer Faces (JSF) 是 JCP 的标准的、基于组件的、事件驱动的 Web 用户界面框架. 从 Java EE 5 起,它就成为 Java EE 的正式子项目. 也可单独使用,例如 通过将 Mojarra 或 MyFaces 嵌入 Tomcat 中.
请注意,JSF 的最新版本与应用程序服务器中与 CDI 基础结构紧密相关,所以,JSF 某些新的功能只能在这样的环境使用. Spring 对 JSF 支持不再积极,主要是在现代化较旧的基于 JSF 的应用程序时,存在用于迁移的目的.
Spring 的 JSF 集成中的关键元素是 JSF ELResolver
机制.
5.2.1. Spring Bean Resolver
SpringBeanFacesELResolver
是符合 JSF 的 ELResolver
实现,与 JSF 和 JSP 使用的标准 Unified EL 集成. 它首先委托 Spring 的 "business context" WebApplicationContext,然后委托给底层 JSF 实现的默认解析器.
在配置方面,您可以在JSF faces-context.xml
文件中定义 SpringBeanFacesELResolver
,如以下示例所示:
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
5.2.2. 使用 FacesContextUtils
在 faces-config.xml
中将属性映射到 bean 时,自定义 ELResolver
很有效,但有时您可能需要显式获取 bean. FacesContextUtils
类使这很容易. 它类似于 WebApplicationContextUtils
,除了它采用 FacesContext
参数而不是 ServletContext
参数.
以下示例显示如何使用 FacesContextUtils
:
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
5.3. Apache Struts 2.x
由 Craig McClanahan 发明的 Struts 是 Apache Software Foundation 基金主办的一个开源项目. 当时,它大大简化了 JSP/Servlet 编程范例,并赢得了许多使用专有框架的开发者. 它简化了编程模型,它是开放源码的(因此是免费的),而且它有一个很大的社区,这使得项目能够在 Java Web 开发者中成长和流行.
查看 Struts Spring Spring Plugin ,了解 Struts 附带的内置 Spring 集成.
5.4. Apache Tapestry 5.x
Tapestry "面向组件"的框架,用于在 Java 中创建动态,健壮,高度可伸缩的 Web 应用程序.
虽然 Spring 拥有自己强大的 Web 层,但是通过将 Tapestry 用于 Web 用户界面和 Spring 容器用于较低层,构建企业 Java 应用程序有许多独特的优势.
有关更多信息,请参阅 Tapestry 针对 Spring 的集成模块