Web Servlet
1. Spring Web MVC
Spring Web MVC 是最初基于 Servlet API 构建的 Web 框架,自始至终都包含在 Spring 框架中。其正式名称
与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个响应式栈 Web 框架, 其名称
有关基准信息以及与 Servlet 容器和 Java EE 版本范围的兼容性,请参阅 Spring Framework Wiki。
1.1. DispatcherServlet
Spring MVC 与其他许多 Web 框架一样,围绕前端控制器(front controller)模式进行设计,其中由一个核心的 Servlet(即 DispatcherServlet)提供统一的请求处理算法,而实际的工作则由可配置的委托组件来完成。
该模型具有良好的灵活性,能够支持多种工作流程。
DispatcherServlet 与任何 Servlet 一样,需要根据 Servlet 规范,通过 Java 配置或在 web.xml 中进行声明和映射。
相应地,DispatcherServlet 会使用 Spring 配置来发现其所需的委托组件,以完成请求映射、视图解析、异常处理等功能。
以下 Java 配置示例注册并初始化了 DispatcherServlet,该 Servlet 会被 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 并重写特定的方法(参见上下文层次结构下的示例)。 |
以下 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 采用了一种不同的初始化顺序。它不是挂钩到 Servlet 容器的生命周期中,而是利用 Spring 配置来引导自身以及内嵌的 Servlet 容器。Filter 和 Servlet 的声明会在 Spring 配置中被检测到,并注册到 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() 返回所有配置,并从 null 返回 getServletConfigClasses()。 |
以下示例展示了等效的 web.xml 配置:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
如果不需要应用程序上下文层次结构,应用程序可以仅配置一个“根”上下文,并将 contextConfigLocation Servlet 参数留空。 |
1.1.2. 特殊 Bean 类型
DispatcherServlet 委托给特定的 bean 来处理请求并渲染相应的响应。这里的“特定 bean”指的是由 Spring 管理的、实现框架契约的 Object 实例。这些 bean 通常带有内置的契约,但你可以自定义它们的属性,也可以对其进行扩展或替换。
下表列出了由 DispatcherServlet 检测到的特殊 bean:
| Bean 类型 | 说明 |
|---|---|
|
将请求映射到一个处理器,并附带一个拦截器列表,用于预处理和后处理。
该映射基于某些条件,其具体细节因 两个主要的 |
|
帮助 |
用于解析异常的策略,可能将异常映射到处理器、HTML 错误视图或其他目标。参见 异常。 |
|
解析客户端正在使用的 |
|
解析您的Web应用程序可以使用的主题——例如,用于提供个性化布局。 参见 主题。 |
|
借助某些多部分解析库,对多部分请求(例如浏览器表单文件上传)进行解析的抽象。参见Multipart Resolver。 |
|
存储和检索可用于在一次请求与另一次请求之间(通常是在重定向过程中)传递属性的“输入”和“输出” |
1.1.3. Web MVC 配置
应用程序可以声明 特殊 Bean 类型 中列出的基础设施 Bean,这些 Bean 是处理请求所必需的。DispatcherServlet 会检查每个特殊 Bean 的 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:
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
import org.springframework.web.WebApplicationInitializer
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
WebApplicationInitializer 是 Spring MVC 提供的一个接口,可确保您的实现被自动检测并用于初始化任何 Servlet 3 容器。
WebApplicationInitializer 的一个抽象基类实现名为
AbstractDispatcherServletInitializer,通过重写方法来指定 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。
isAsyncSupported 类的受保护方法 AbstractDispatcherServletInitializer 提供了一个统一的位置,用于在 DispatcherServlet 及其映射的所有过滤器上启用异步支持。默认情况下,该标志被设置为 true。
最后,如果你需要进一步自定义 DispatcherServlet 本身,可以重写 createDispatcherServlet 方法。
1.1.5. 处理
DispatcherServlet 按如下方式处理请求:
-
WebApplicationContext会被查找并在请求中作为属性进行绑定,以便控制器和处理流程中的其他组件可以使用。默认情况下,它会以DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE键进行绑定。 -
区域设置解析器绑定到请求,以便在处理请求(渲染视图、准备数据等)过程中,相关元素能够解析出要使用的区域设置。如果您不需要区域设置解析功能,则无需使用区域设置解析器。
-
主题解析器绑定到请求上,以便视图等元素能够确定使用哪个主题。如果您不使用主题,可以忽略它。
-
如果你指定了一个多部分文件解析器(multipart file resolver),请求将被检查是否包含多部分内容。如果发现多部分内容,该请求会被包装成一个
MultipartHttpServletRequest,以便流程中的其他组件进行进一步处理。有关多部分处理的更多信息,请参见多部分解析器(Multipart Resolver)。 -
系统会搜索一个合适的处理器。如果找到处理器,则会执行与该处理器关联的执行链(包括预处理器、后处理器和控制器),以准备用于渲染的模型。或者,对于带注解的控制器,可以在
HandlerAdapter中直接渲染响应,而不是返回一个视图。 -
如果返回了一个模型,则会渲染视图。如果没有返回模型(可能是由于预处理器或后处理器出于安全等原因拦截了请求),则不会渲染视图,因为该请求可能已经被处理完成。
在 HandlerExceptionResolver 中声明的 WebApplicationContext Bean 用于
解析请求处理过程中抛出的异常。这些异常解析器允许
自定义处理异常的逻辑。更多详情请参见异常。
Spring 的 DispatcherServlet 还支持返回由 Servlet API 所规定的 last-modification-date(最后修改日期)。确定特定请求的最后修改日期的过程非常简单:DispatcherServlet 会查找合适的处理器映射(handler mapping),并检查找到的处理器是否实现了 LastModified 接口。如果实现了该接口,则将 long getLastModified(request) 接口中 LastModified 方法的返回值发送给客户端。
您可以通过在 DispatcherServlet 文件中的 Servlet 声明里添加 Servlet 初始化参数(init-param 元素)来定制各个 web.xml 实例。下表列出了所支持的参数:
| 参数 | 说明 |
|---|---|
|
实现 |
|
传递给上下文实例(由 |
|
|
|
当请求找不到处理器时,是否抛出 默认情况下,此值设置为 请注意,如果同时配置了默认 Servlet 处理, 所有无法解析的请求将始终被转发到默认 Servlet,而永远不会抛出 404 错误。 |
1.1.6. 拦截
所有 HandlerMapping 实现都支持处理器拦截器,当你希望对某些请求应用特定功能时(例如,检查主体身份),这些拦截器非常有用。拦截器必须实现来自 HandlerInterceptor 包的 org.springframework.web.servlet 接口,该接口包含三个方法,足以提供各种预处理和后处理操作所需的灵活性:
-
preHandle(..):在实际处理器执行之前 -
postHandle(..):在处理器执行之后 -
afterCompletion(..):在完整请求完成之后
preHandle(..) 方法返回一个布尔值。你可以使用此方法来中断或继续执行链的处理。当该方法返回 true 时,处理器执行链将继续执行。当它返回 DispatcherServlet 时,3 会认为拦截器自身已经处理了请求(例如,已渲染了适当的视图),因此将不再继续执行执行链中的其他拦截器和实际的处理器。
有关如何配置拦截器的示例,请参见 MVC 配置部分中的拦截器。您也可以通过在各个HandlerMapping实现上使用 setter 方法直接注册它们。
请注意,对于使用 postHandle 和 @ResponseBody 的方法,ResponseEntity 的作用较为有限,因为响应会在 HandlerAdapter 中被写入并提交,且发生在 postHandle 之前。这意味着此时再对响应进行任何修改(例如添加额外的响应头)都为时已晚。针对此类场景,您可以实现 ResponseBodyAdvice,并将其声明为一个控制器增强(Controller Advice) Bean,或者直接在 RequestMappingHandlerAdapter 上进行配置。
1.1.7. 异常
如果在请求映射期间发生异常,或从请求处理程序(例如 @Controller)抛出异常,DispatcherServlet 会委托给一个 HandlerExceptionResolver bean 链来解析该异常并提供替代处理方式,通常是返回一个错误响应。
下表列出了可用的 HandlerExceptionResolver 实现:
HandlerExceptionResolver |
描述 |
|---|---|
|
异常类名与错误视图名称之间的映射。适用于在浏览器应用程序中渲染错误页面。 |
解析由 Spring MVC 抛出的异常,并将其映射到 HTTP 状态码。
另请参阅替代方案 |
|
|
通过 |
|
通过调用 |
解析器链
你可以通过在 Spring 配置中声明多个 HandlerExceptionResolver bean 并根据需要设置它们的 order 属性来形成一个异常解析器链。
2 属性值越高,该异常解析器在链中的位置就越靠后。
HandlerExceptionResolver 的契约规定它可以返回:
-
一个指向错误视图的
ModelAndView。 -
如果异常已在解析器内部处理,则返回一个空的
ModelAndView。 -
null表示该异常尚未被解决,以便后续的解析器尝试处理;如果到最后异常仍未被解决,则允许其向上传播至 Servlet 容器。
MVC 配置会自动声明内置的解析器,用于处理默认的 Spring MVC 异常、带有 @ResponseStatus 注解的异常,以及支持 @ExceptionHandler 方法。您可以自定义该列表或将其替换。
容器错误页面
如果异常未被任何 HandlerExceptionResolver 解决,从而继续向上抛出,或者响应状态被设置为错误状态(即 4xx、5xx),
Servlet 容器可以渲染一个默认的 HTML 错误页面。要自定义容器的默认错误页面,
可以在 web.xml 中声明一个错误页面映射。
以下示例展示了如何实现这一点:
<error-page>
<location>/error</location>
</error-page>
根据前面的示例,当异常向上抛出或响应具有错误状态时,
Servlet 容器会在容器内部向配置的 URL(例如 /error)发起一次 ERROR 类型的转发(dispatch)。随后,该请求由 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.8. 视图解析
Spring MVC 定义了 ViewResolver 和 View 接口,使你能够在浏览器中渲染模型,而无需绑定到特定的视图技术。ViewResolver 提供了视图名称与实际视图之间的映射关系。View 负责在将数据交给特定视图技术之前进行数据准备。
下表提供了有关ViewResolver层次结构的更多详细信息:
| 视图解析器 | 描述 |
|---|---|
|
|
|
实现了 |
|
一种 |
|
|
|
|
|
|
|
实现 |
处理
你可以通过声明多个解析器 bean 来链接视图解析器,并在必要时设置 order 属性以指定顺序。请记住,1 属性值越大,该视图解析器在链中的位置就越靠后。
ViewResolver 的契约规定,当视图未找到时,它可以返回 null。然而,对于 JSP 和 InternalResourceViewResolver 而言,判断一个 JSP 是否存在的唯一方法是通过 RequestDispatcher 执行一次分发(dispatch)。因此,您必须始终将 InternalResourceViewResolver 配置为所有视图解析器中的最后一个。
正在重定向
视图名称中的特殊 redirect: 前缀可让你执行重定向。UrlBasedViewResolver(及其子类)会将此识别为需要进行重定向的指令。视图名称的其余部分即为重定向 URL。
其最终效果与控制器直接返回一个 RedirectView 相同,但现在控制器本身可以使用逻辑视图名称进行操作。逻辑视图名称(例如 redirect:/myapp/some/resource)会相对于当前 Servlet 上下文进行重定向,而像 redirect:https://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 属性指定的视图列表。后一种选项适用于单例 Views,无论逻辑视图名称如何,它都能渲染当前资源的适当表示形式。Accept 头可以包含通配符(例如 text/*),在这种情况下,Content-Type 为 text/xml 的 View 即为兼容匹配。
1.1.9. 本地化
Spring 架构的大部分组件都支持国际化,Spring Web MVC 框架也不例外。DispatcherServlet 允许你根据客户端的语言环境自动解析消息。这是通过 LocaleResolver 对象实现的。
当请求进入时,DispatcherServlet 会查找一个区域设置解析器(locale resolver),如果找到,就会尝试使用它来设置区域设置(locale)。通过使用 RequestContext.getLocale() 方法,你始终可以获取由区域设置解析器所解析出的区域设置。
除了自动的语言环境解析之外,您还可以将一个拦截器附加到处理器映射上(有关处理器映射拦截器的更多信息,请参见拦截),以便在特定情况下(例如,基于请求中的某个参数)更改语言环境。
区域设置解析器(Locale resolvers)和拦截器(interceptors)定义在
org.springframework.web.servlet.i18n 包中,并以常规方式在您的应用上下文中进行配置。Spring 中包含了以下几种区域设置解析器。
时区
除了获取客户端的区域设置(locale)之外,通常还需要了解其时区。
LocaleContextResolver 接口对 LocaleResolver 进行了扩展,
允许解析器提供一个更丰富的 LocaleContext,其中可能包含时区信息。
当可用时,可以通过使用 TimeZone 方法获取用户的RequestContext.getTimeZone()。时区信息会自动被任何注册到 Spring 的 Converter 中的日期/时间 Formatter 和 ConversionService 对象所使用。
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 的属性:
| 属性 | 默认 | 描述 |
|---|---|---|
|
类名 + 区域设置(LOCALE) |
Cookie 的名称 |
|
Servlet 容器默认值 |
客户端上 Cookie 的最大持久化时间。如果指定为 |
|
/ |
将 Cookie 的可见性限制在您网站的特定部分。当指定了 |
会话解析器
SessionLocaleResolver 允许你从与用户请求相关联的会话中获取 Locale 和 TimeZone。与 CookieLocaleResolver 不同,该策略将用户本地选择的区域设置存储在 Servlet 容器的 HttpSession 中。因此,这些设置对每个会话而言都是临时的,并在会话结束时丢失。
请注意,这与外部会话管理机制(例如 Spring Session 项目)没有直接关系。此 SessionLocaleResolver 会针对当前的 HttpSession 评估并修改相应的 HttpServletRequest 属性。
区域设置拦截器
你可以通过将 LocaleChangeInterceptor 添加到某个 HandlerMapping 的定义中,来启用区域(locale)切换功能。它会检测请求中的一个参数,并据此更改区域设置,调用分发器(dispatcher)应用上下文中的 setLocale 的 LocaleResolver 方法。下面的示例表明,所有对 *.view 资源的请求,只要包含名为 siteLanguage 的参数,就会更改区域设置。例如,请求 URL https://www.sf.net/home.view?siteLanguage=nl 会将网站语言更改为荷兰语。以下示例展示了如何拦截区域设置:
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.10. 主题
您可以应用 Spring Web MVC 框架的主题来设置应用程序的整体外观和风格,从而提升用户体验。主题是一组静态资源的集合,通常包括样式表和图像,用于影响应用程序的视觉样式。
定义主题
要在您的Web应用程序中使用主题(themes),您必须设置一个实现了
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 使用一个空的 basename 前缀。因此,属性文件会从类路径的根目录加载。所以,您应将 cool.properties 主题定义文件放在类路径根目录下的某个目录中(例如,放在 /WEB-INF/classes 中)。ResourceBundleThemeSource 使用标准的 Java 资源包加载机制,从而支持主题的完整国际化。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties 文件,其中引用一张带有荷兰语文本的特殊背景图片。
解析主题
按照前一节所述定义主题之后,
你需要决定使用哪个主题。DispatcherServlet会查找名为themeResolver的bean,
以确定要使用哪个ThemeResolver实现。主题解析器的工作方式与LocaleResolver非常相似。
它能够检测特定请求应使用的主题,也可以修改请求的主题。下表描述了Spring提供的主题解析器:
| 类 | 描述 |
|---|---|
|
通过使用 |
|
主题保存在用户的 HTTP 会话中。每个会话只需设置一次,但在不同会话之间不会被持久化。 |
|
所选主题存储在客户端的 Cookie 中。 |
Spring 还提供了一个 ThemeChangeInterceptor,它允许通过一个简单的请求参数在每次请求时更改主题。
1.1.11. 多部分解析器
MultipartResolver 来自 org.springframework.web.multipart 包,是一种用于解析多部分(multipart)请求(包括文件上传)的策略。其中一种实现基于 Commons FileUpload,另一种则基于 Servlet 3.0 的多部分请求解析。
要启用多部分(multipart)处理,您需要在 MultipartResolver 的 Spring 配置中声明一个名为 DispatcherServlet 的 multipartResolver Bean。
DispatcherServlet 会检测到该 Bean 并将其应用于传入的请求。当接收到内容类型(content-type)为 multipart/form-data 的 POST 请求时,解析器会解析请求内容,并将当前的 HttpServletRequest 包装为 MultipartHttpServletRequest,以便除了将已解析的部分作为请求参数暴露出来之外,还能提供对这些部分的访问。
Apache CommonsFileUpload
要使用 Apache Commons FileUpload,您可以配置一个类型为 CommonsMultipartResolver、名称为 multipartResolver 的 Bean。您还需要在类路径中包含 commons-fileupload 依赖项。
Servlet 3.0
需要通过 Servlet 容器配置启用 Servlet 3.0 的 multipart 解析功能。 操作步骤如下:
-
在 Java 中,在 Servlet 注册上设置一个
MultipartConfigElement。 -
在
web.xml中,向 servlet 声明添加一个"<multipart-config>"部分。
以下示例展示了如何在 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,您就可以添加一个类型为 StandardServletMultipartResolver、名称为 multipartResolver 的 Bean。
1.1.12. 日志记录
Spring MVC 中的 DEBUG 级别日志设计得简洁、精炼且对人类友好。它聚焦于那些反复有用的重要信息,而非仅在调试特定问题时才有用的其他信息。
TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,也不应产生大量冗余日志),但可用于调试任何问题。此外,某些日志消息在 TRACE 级别下可能会比在 DEBUG 级别下显示更详细的细节。
良好的日志记录源于使用日志的经验。如果您发现任何不符合所述目标的内容,请告知我们。
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息。因此,请求参数和请求头默认会被屏蔽,若要完整记录这些信息,必须通过 enableLoggingRequestDetails 上的 DispatcherServlet 属性显式启用。
以下示例展示了如何通过使用 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,用于拦截内容类型为 application/x-www-form-urlencoded 的 HTTP PUT、PATCH 和 DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest,使得可以通过 ServletRequest.getParameter*() 系列方法获取表单数据。
1.2.2. 转发标头
当请求经过代理(例如负载均衡器)时,主机、端口和协议方案可能会发生变化,这使得从客户端视角创建指向正确主机、端口和协议方案的链接变得具有挑战性。
RFC 7239 定义了 Forwarded HTTP 头部,
代理可以使用该头部提供有关原始请求的信息。此外,还存在其他
非标准的头部,包括 X-Forwarded-Host、X-Forwarded-Port、
X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderFilter 是一个 Servlet 过滤器,用于修改请求,以实现以下两个目的:
a) 根据 Forwarded 头信息更改主机、端口和协议(scheme),以及 b) 移除这些头信息,以避免后续产生影响。该过滤器通过包装请求来实现功能,因此必须排在其他过滤器(例如 RequestContextFilter)之前,以确保这些过滤器能够处理修改后的请求,而不是原始请求。
由于应用程序无法判断转发头(forwarded headers)是由代理按预期添加的,还是由恶意客户端伪造的,因此在使用转发头时存在安全方面的考量。这就是为什么应在信任边界的代理上进行配置,以移除来自外部的不可信 Forwarded 头信息。你也可以将 ForwardedHeaderFilter 配置为 removeOnly=true,在这种情况下,该过滤器会移除这些头信息但不会使用它们。
为了支持异步请求和错误分发,此过滤器应映射到DispatcherType.ASYNC以及DispatcherType.ERROR。
如果使用 Spring Framework 的AbstractAnnotationConfigDispatcherServletInitializer
(请参阅Servlet 配置),所有过滤器将自动为所有分发类型注册。但是,如果通过web.xml注册过滤器,或在 Spring Boot 中通过FilterRegistrationBean注册,请务必除了DispatcherType.REQUEST之外,还包含DispatcherType.ASYNC和DispatcherType.ERROR。
1.2.3. 浅层 ETag
ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希值来创建一个“浅层”ETag。下一次客户端发送请求时,它会执行相同的操作,但还会将计算出的值与 If-None-Match 请求头进行比较,如果两者相等,则返回 304(NOT_MODIFIED)。
该策略节省了网络带宽,但并未节省 CPU 资源,因为每次请求都必须完整计算响应内容。前面描述的控制器层面的其他策略可以避免这种计算。参见 HTTP 缓存。
该过滤器具有一个 writeWeakETag 参数,用于配置过滤器以写入弱 ETag,
类似于以下格式:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如
RFC 7232 第 2.3 节 中所定义)。
1.3. 带注解的控制器
Spring MVC 提供了一种基于注解的编程模型,其中 @Controller 和
@RestController 组件使用注解来表达请求映射、请求输入、
异常处理等功能。带注解的控制器具有灵活的方法签名,
无需继承基类,也无需实现特定接口。
以下示例展示了一个通过注解定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
import org.springframework.ui.set
@Controller
class HelloController {
@GetMapping("/hello")
fun handle(model: Model): String {
model["message"] = "Hello World!"
return "index"
}
}
在前面的示例中,该方法接受一个 Model 并以 String 形式返回视图名称,
但还存在许多其他选项,将在本章后续部分进行说明。
| spring.io 上的指南和教程使用本节所述的基于注解的编程模型。 |
1.3.1. 声明
你可以通过在 Servlet 的 WebApplicationContext 中使用标准的 Spring bean 定义来定义控制器 bean。@Controller 刻板注解(stereotype)支持自动检测,这与 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,用于表明该控制器的所有方法都继承了类级别的 @ResponseBody 注解,因此会直接将内容写入响应体,而不是通过视图解析并使用 HTML 模板进行渲染。
AOP 代理
在某些情况下,你可能需要在运行时使用 AOP 代理来包装控制器。
一个例子是,如果你选择直接在控制器上使用 @Transactional 注解。
在这种情况下,特别是对于控制器,我们建议使用基于类的代理。
这通常是控制器的默认选择。
然而,如果控制器必须实现一个非 Spring 上下文回调接口(例如 InitializingBean、*Aware 等),
你可能需要显式配置基于类的代理。
例如,使用 <tx:annotation-driven/> 时,你可以将其改为
<tx:annotation-driven proxy-target-class="true"/>;
使用 @EnableTransactionManagement 时,你可以将其改为
@EnableTransactionManagement(proxyTargetClass = true)。
1.3.2. 请求映射
你可以使用 @RequestMapping 注解将请求映射到控制器方法。它提供了多种属性,可用于根据 URL、HTTP 方法、请求参数、请求头和媒体类型进行匹配。你可以在类级别上使用该注解以表达共享的映射,也可以在方法级别上使用以精确指定某个端点的映射。
此外,还有针对特定 HTTP 方法的 @RequestMapping 快捷变体:
-
@GetMapping -
@PostMapping -
@PutMapping -
@DeleteMapping -
@PatchMapping
这些快捷方式是自定义注解,之所以提供它们,是因为可以说大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用默认情况下匹配所有 HTTP 方法的 @RequestMapping。同时,在类级别上仍然需要使用 @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 模式
你可以使用通配符模式(glob patterns)和通配符来映射请求:
| 模式 | 描述 | 例举 |
|---|---|---|
|
匹配一个字符 |
匹配 |
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径结束 |
|
|
匹配一个路径段,并将其捕获为名为“name”的变量 |
|
|
将正则表达式 |
|
可以使用 @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 路径模式还可以嵌入 ${…} 占位符,这些占位符在启动时通过 PropertyPlaceHolderConfigurer 从本地、系统、环境以及其他属性源中解析。例如,您可以使用此功能根据某些外部配置对基础 URL 进行参数化。
Spring MVC 使用来自 PathMatcher 的 AntPathMatcher 接口及其实现类 spring-core 来进行 URI 路径匹配。 |
模式对比
当多个模式匹配同一个 URL 时,必须对它们进行比较以找出最佳匹配。这是通过使用 AntPathMatcher.getPatternComparator(String path) 来实现的,该方法会寻找更为具体的模式。
如果一个模式包含的 URI 变量数量(计为1)、单通配符数量(计为1)和双通配符数量(计为2)总和较少,则该模式被认为更不具体。在得分相同的情况下,会选择较长的模式。如果得分和长度都相同,则优先选择 URI 变量数量多于通配符数量的模式。
默认的映射模式(/**)在评分时会被排除,并始终排在最后。此外,前缀模式(例如 /public/**)被认为比那些不包含双通配符的其他模式更不具体。
有关完整详情,请参阅 AntPatternComparator
在 AntPathMatcher 中,同时请记住
您可以自定义 PathMatcher 实现。
请参阅配置部分中的 路径匹配。
后缀匹配
默认情况下,Spring MVC 会执行 .* 后缀模式匹配,因此映射到 /person 的控制器也会隐式地映射到 /person.*。
然后,文件扩展名会被用来解析所请求的响应内容类型(即代替 Accept 请求头)——例如 /person.pdf、
/person.xml 等。
以这种方式使用文件扩展名在以前是必要的,因为浏览器发送的 Accept 请求头难以一致地解析。如今,这种做法已不再必要,应当优先选择使用 Accept 请求头。
随着时间的推移,使用文件扩展名在多个方面被证明存在问题。 当与URI变量、路径参数以及URI编码结合使用时,可能会引起歧义。 基于URL的授权和安全性(详见下一节)也变得更加复杂。
要完全禁用文件扩展名的使用,您必须同时设置以下两项:
-
useSuffixPatternMatching(false),参见 PathMatchConfigurer -
favorPathExtension(false),参见 ContentNegotiationConfigurer
基于 URL 的内容协商仍然很有用(例如,在浏览器中直接输入 URL 时)。为启用该功能,我们建议采用基于查询参数的策略,以避免文件扩展名所带来的大部分问题。或者,如果您必须使用文件扩展名,请考虑通过 ContentNegotiationConfigurer 的 #mvc-config-content-negotiation 属性,将扩展名限制在显式注册的扩展名列表中。
|
从 5.2.4 版本开始,RequestMappingHandlerMapping 中用于请求映射的路径扩展相关选项,以及 ContentNegotiationManagerFactoryBean 中用于内容协商的相关选项已被弃用。有关后续计划,请参见 Spring Framework 问题 #24179 及相关议题。 |
后缀匹配和 RFD
反射型文件下载(RFD)攻击与 XSS 类似,都是依赖于请求输入(例如查询参数和 URI 变量)在响应中被反射。然而,与将 JavaScript 插入 HTML 不同的是,RFD 攻击利用浏览器切换为下载模式,并在用户后续双击该文件时将其作为可执行脚本处理。
在 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 属性。然而,与其他大多数请求映射属性不同的是,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。
MediaType 提供了常用媒体类型的常量,例如
APPLICATION_JSON_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 | 测试 myHeader 是否等于 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 方法的 @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 是组合注解(composed annotations)的示例。之所以提供这些注解,是因为可以认为大多数控制器方法应当映射到特定的 HTTP 方法,而不是使用默认情况下匹配所有 HTTP 方法的 @RequestMapping。如果你需要组合注解的示例,请查看这些注解的声明方式。
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 处理方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。
方法参数
下表描述了支持的控制器方法参数。任何参数均不支持响应式类型。
JDK 8 的 java.util.Optional 可作为方法参数使用,与具有 required 属性的注解(例如 @RequestParam、@RequestHeader 等)结合使用时,等同于设置 required=false。
| 控制器方法参数 | 描述 |
|---|---|
|
无需直接使用 Servlet API,即可通用地访问请求参数、请求属性和会话属性。 |
|
选择任意特定的请求或响应类型——例如, |
|
强制要求会话的存在。因此,此类参数永远不会为 |
|
用于以编程方式执行 HTTP/2 资源推送的 Servlet 4.0 Push Builder API。
请注意,根据 Servlet 规范,如果客户端不支持该 HTTP/2 功能,则注入的 |
|
当前已认证的用户——如果已知,可能是某个特定的 |
|
请求的 HTTP 方法。 |
|
当前请求的区域设置,由可用的最具体的 |
|
由 |
|
用于访问 Servlet API 所暴露的原始请求体。 |
|
用于访问 Servlet API 所暴露的原始响应体。 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的键值对。请参阅矩阵变量。 |
|
用于访问 Servlet 请求参数,包括多部分文件。参数值将转换为声明的方法参数类型。另请参阅 请注意,对于简单的参数值,使用 |
|
用于访问请求头。请求头的值会被转换为声明的方法参数类型。请参阅 |
|
用于访问 Cookie。Cookie 值会被转换为声明的方法参数类型。请参阅 |
|
用于访问 HTTP 请求体。请求体内容通过 |
|
用于访问请求头和请求体。请求体会通过一个 |
|
用于访问 |
|
用于访问在 HTML 控制器中使用的模型,并在视图渲染时将其暴露给模板。 |
|
指定在重定向时使用的属性(即,附加到查询字符串中)以及临时存储的闪存属性,这些闪存属性将保留至重定向后的下一个请求。 参见 重定向属性 和 闪存属性。 |
|
用于访问模型中现有的属性(如果不存在则实例化),并应用数据绑定和验证。另请参阅 请注意,使用 |
|
用于访问命令对象(即 |
|
用于标记表单处理完成,这将触发清理通过类级别 |
|
用于根据当前请求的主机、端口、协议方案、上下文路径以及 Servlet 映射的字面部分来构建相对 URL。参见 URI 链接。 |
|
要访问任何会话属性,这与由于类级别的 |
|
用于访问请求属性。详见 |
任何其他参数 |
如果方法参数未匹配到本表中前面列出的任何值,并且它是一个简单类型(由
BeanUtils#isSimpleProperty 判定),
则将其解析为 |
返回值
下表描述了支持的控制器方法返回值。所有返回值均支持响应式类型。
| 控制器方法的返回值 | 描述 |
|---|---|
|
返回值通过 |
|
指定完整响应(包括 HTTP 头部和响应体)的返回值将通过 |
|
用于返回带有响应头但无响应体的响应。 |
|
一个视图名称,将通过 |
|
一个用于渲染的 |
|
要添加到隐式模型中的属性,视图名称将通过 |
|
一个要添加到模型中的属性,其视图名称通过 请注意, |
|
要使用的视图和模型属性,以及可选的响应状态。 |
|
如果一个方法的返回类型为 如果以上情况均不成立, |
|
从任何线程异步生成前述任何返回值——例如,作为某个事件或回调的结果。请参阅 异步请求 和 |
|
|
|
|
|
异步发出一个对象流,通过 |
|
异步写入响应的 |
响应式类型 —— Reactor、RxJava,或通过 |
在流式传输场景中(例如 |
任何其他返回值 |
任何返回值,只要不匹配本表中前述的任何类型,并且是 |
类型转换
某些表示基于 String 的请求输入的注解控制器方法参数(例如
@RequestParam、@RequestHeader、@PathVariable、@MatrixVariable 和 @CookieValue)
在参数声明类型不是 String 时,可能需要进行类型转换。
对于此类情况,类型转换会根据配置的转换器自动应用。
默认情况下,支持简单类型(int、long、Date 等)。您可以通过 WebDataBinder 自定义类型转换(请参阅 DataBinder),或者向 FormattingConversionService 注册 Formatters。
请参阅 Spring 字段格式化。
矩阵变量
矩阵变量可以出现在任意路径段中,每个变量以分号分隔,多个值以逗号分隔(例如:/cars;color=red,green;year=2012)。也可以通过重复变量名来指定多个值(例如:color=red;color=green;color=blue)。
如果某个 URL 预期包含矩阵变量(matrix variables),则控制器方法的请求映射必须使用 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]
}
请注意,您需要启用矩阵变量(matrix variables)的使用。在 MVC 的 Java 配置中,您需要通过路径匹配(Path Matching)设置一个 removeSemicolonContent=false,并将其 #mvc-config-path-matching 属性设为 <mvc:annotation-driven enable-matrix-variables="true"/>。在 MVC 的 XML 命名空间中,您可以设置 4。
@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。 |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = this.clinic.loadPet(petId);
model["pet"] = pet
return "petForm"
}
// ...
}
| 1 | 使用 @RequestParam 绑定 petId。 |
默认情况下,使用此注解的方法参数是必需的,但你可以通过将 @RequestParam 注解的 required 标志设置为 false,或者使用 java.util.Optional 包装器声明参数,来指定该方法参数是可选的。
如果目标方法参数类型不是 String,则会自动应用类型转换。参见类型转换。
将参数类型声明为数组或列表,可以解析同一参数名的多个参数值。
当 @RequestParam 注解被声明为 Map<String, String> 或
MultiValueMap<String, String>,且注解中未指定参数名称时,
该 Map 将使用请求中每个给定参数名对应的参数值进行填充。
请注意,使用 @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,则会自动应用类型转换。请参阅类型转换。
当在 @RequestHeader 注解用于 Map<String, String>、
MultiValueMap<String, String> 或 HttpHeaders 类型的参数时,该映射将被填充所有请求头的值。
内置支持将逗号分隔的字符串转换为字符串数组或集合,或者转换为类型转换系统所知的其他类型。例如,使用 @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 注解标注方法参数,以访问模型中的属性;如果该属性不存在,则会自动实例化。模型属性还会被 HTTP Servlet 请求参数中与字段名称相匹配的值覆盖。这一过程称为数据绑定(data binding),它可以避免你手动解析和转换各个查询参数及表单字段。以下示例展示了如何实现这一点:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
| 1 | 绑定一个 Pet 的实例。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
| 1 | 绑定一个 Pet 的实例。 |
上面的 Pet 实例解析方式如下:
-
如果已通过使用Model添加,则来自模型。
-
通过
@SessionAttributes从 HTTP 会话中获取。 -
通过
Converter传递的 URI 路径变量(参见下一个示例)。 -
通过调用默认构造函数。
-
通过调用一个“主构造函数”(primary constructor)来实现,该构造函数的参数与 Servlet 请求参数相匹配。参数名称通过 JavaBeans 的
@ConstructorProperties注解确定,或者通过字节码中保留的运行时参数名称确定。
虽然通常使用Model来向模型中填充属性,但另一种替代方式是结合URI路径变量约定,依赖Converter<String, T>。在下面的示例中,模型属性名称account与URI路径变量account相匹配,并通过已注册的Account将字符串形式的账号传递进去,从而加载String对象:
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
// ...
}
获取模型属性实例后,将应用数据绑定。WebDataBinder 类会将 Servlet 请求参数名称(查询参数和表单字段)与目标 Object 上的字段名称进行匹配。在必要时应用类型转换后,匹配的字段将被填充。有关数据绑定(及验证)的更多信息,请参阅 验证。有关自定义数据绑定的更多信息,请参阅 DataBinder。
数据绑定可能会导致错误。默认情况下,会抛出一个 BindException 异常。然而,若要在控制器方法中检查此类错误,可以在 BindingResult 参数之后立即添加一个 @ModelAttribute 参数,如下例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | 在 BindingResult 旁边添加一个 @ModelAttribute。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
| 1 | 在 BindingResult 旁边添加一个 @ModelAttribute。 |
在某些情况下,你可能希望访问模型属性而不进行数据绑定。对于这种情况,你可以将 Model 注入到控制器中并直接访问它,或者也可以设置 @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 验证),在数据绑定后自动应用验证。以下示例展示了如何实现这一点:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
| 1 | 验证 Pet 实例。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
请注意,使用 @ModelAttribute 是可选的(例如,用于设置其属性)。
默认情况下,任何不是简单值类型(由
BeanUtils#isSimpleProperty
判定)
且未被其他任何参数解析器解析的参数,都会被当作已使用 @ModelAttribute 注解处理。
@SessionAttributes
@SessionAttributes 用于在请求之间将模型属性存储在 HTTP Servlet 会话中。它是一个类级别的注解,用于声明特定控制器所使用的会话属性。通常,该注解列出模型属性的名称或类型,这些属性应透明地存储在会话中,以便后续请求访问。
以下示例使用了 @SessionAttributes 注解:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
| 1 | 使用 @SessionAttributes 注解。 |
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
| 1 | 使用 @SessionAttributes 注解。 |
在第一次请求时,当一个名为 pet 的模型属性被添加到模型中时,它会自动提升并保存到 HTTP Servlet 会话中。该属性将一直保留在会话中,直到另一个控制器方法使用 SessionStatus 方法参数来清除存储,如下例所示:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); (2)
// ...
}
}
}
| 1 | 将 Pet 值存储在 Servlet 会话中。 |
| 2 | 从 Servlet 会话中清除 Pet 值。 |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String {
if (errors.hasErrors()) {
// ...
}
status.setComplete() (2)
// ...
}
}
| 1 | 将 Pet 值存储在 Servlet 会话中。 |
| 2 | 从 Servlet 会话中清除 Pet 值。 |
@SessionAttribute
如果你需要访问全局管理的、已存在的会话属性(即在控制器之外管理的会话属性——例如由过滤器管理),而这些属性可能存在也可能不存在,你可以在方法参数上使用 @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 注解来访问之前已创建的请求属性(例如,由 Servlet 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 上。
如果模型实例是专门为重定向准备的,那么将基本类型属性作为查询参数附加可能是期望的结果。然而,在使用注解的控制器中,模型可能包含为渲染目的而添加的额外属性(例如,下拉字段的值)。为了避免这些属性出现在 URL 中,@RequestMapping 方法可以声明一个 RedirectAttributes 类型的参数,并使用它来精确指定要提供给 RedirectView 的属性。如果该方法执行了重定向,则使用 RedirectAttributes 的内容;否则,使用模型的内容。
RequestMappingHandlerAdapter 提供了一个名为
ignoreDefaultModelOnRedirect 的标志,你可以用它来指明:当控制器方法执行重定向时,绝不应使用默认 Model 中的内容。取而代之的是,控制器方法应声明一个类型为 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 Attributes
Flash 属性提供了一种机制,允许一个请求存储供另一个请求使用的属性。这在重定向时最为常见——例如 Post-Redirect-Get 模式。Flash 属性在重定向之前会临时保存(通常保存在会话中),以便在重定向后的请求中使用,并在使用后立即被移除。
Spring MVC 提供了两个主要的抽象来支持 flash 属性。FlashMap 用于保存 flash 属性,而 FlashMapManager 用于存储、检索和管理 FlashMap 实例。
Flash 属性支持始终处于“开启”状态,无需显式启用。
然而,如果不使用它,则永远不会导致创建 HTTP 会话。在每个请求中,都会有一个“输入”FlashMap,其中包含从前一个请求传递过来的属性(如果有的话),以及一个“输出”FlashMap,用于保存供后续请求使用的属性。这两个 FlashMap 实例均可通过 Spring MVC 中任意位置调用 RequestContextUtils 中的静态方法来访问。
带注解的控制器通常无需直接操作 FlashMap。相反,@RequestMapping 方法可以接受一个类型为 RedirectAttributes 的参数,并使用它在重定向场景中添加 flash 属性。通过 RedirectAttributes 添加的 flash 属性会自动传递到“输出”FlashMap 中。同样地,在重定向之后,“输入”FlashMap 中的属性也会自动添加到处理目标 URL 的控制器的 Model 中。
文件上传
在启用了 MultipartResolver 之后,#mvc-multipart 类型的 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 将使用每个给定参数名称对应的 multipart 文件进行填充。
使用 Servlet 3.0 的多部分解析功能时,您也可以在方法参数或集合值类型中声明 javax.servlet.http.Part,而无需使用 Spring 的 MultipartFile。 |
你也可以将多部分(multipart)内容作为命令对象数据绑定的一部分。例如,前面示例中的表单字段和文件可以作为表单对象上的字段,如下例所示:
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 服务场景中,多部分(multipart)请求也可以从非浏览器客户端提交。以下示例展示了一个包含 JSON 的文件:
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
你可以使用 @RequestParam 将“meta-data”部分作为 String 类型访问,但你很可能希望将其从 JSON 反序列化(类似于 @RequestBody)。使用 @RequestPart 注解,在通过 HttpMessageConverter 转换后访问 multipart 数据:
@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 Validation 验证。
默认情况下,验证错误会抛出 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 Validation 验证。
默认情况下,验证错误会抛出 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 方法与 JSON 序列化视图结合使用。
详见Jackson JSON。
ResponseEntity
ResponseEntity 类似于 @ResponseBody,但包含状态和响应头。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body = ...
val etag = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
Spring MVC 支持使用单值响应式类型来异步生成ResponseEntity,以及使用单值和多值响应式类型作为响应体。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring MVC 内置支持
Jackson 的序列化视图(Serialization Views),
它允许仅渲染 Object 中的部分字段。要在
@ResponseBody 或 ResponseEntity 控制器方法中使用该功能,
你可以使用 Jackson 的
@JsonView 注解来激活一个序列化视图类,如下例所示:
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@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
}
}
对于依赖视图解析的控制器,您可以将序列化视图类添加到模型中,如下例所示:
@Controller
public class UserController extends AbstractController {
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", new User("eric", "7!jd#h23"));
model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
return "userView";
}
}
import org.springframework.ui.set
@Controller
class UserController : AbstractController() {
@GetMapping("/user")
fun getUser(model: Model): String {
model["user"] = User("eric", "7!jd#h23")
model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java
return "userView"
}
}
1.3.4. 模型
你可以使用 @ModelAttribute 注解:
-
在
#mvc-ann-modelattrib-method-args方法的方法参数上, 用于从模型中创建或访问一个Object,并通过WebDataBinder将其绑定到请求。 -
作为
@Controller或@ControllerAdvice类中的方法级注解,用于在调用任何@RequestMapping方法之前帮助初始化模型。 -
在
@RequestMapping方法上,用于标记其返回值是一个模型属性。
本节讨论 @ModelAttribute 方法——即前述列表中的第二项。
一个控制器可以拥有任意数量的 @ModelAttribute 方法。所有这些方法都会在同一个控制器中的 @RequestMapping 方法之前被调用。@ModelAttribute 方法还可以通过 @ControllerAdvice 在多个控制器之间共享。更多详细信息,请参见控制器通知(Controller Advice)一节。
@ModelAttribute 方法具有灵活的方法签名。它们支持许多与 @RequestMapping 方法相同的参数,但不包括 @ModelAttribute 本身或任何与请求体相关的参数。
以下示例展示了一个 @ModelAttribute 方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
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)
}
当未显式指定名称时,将根据 Object 类型选择一个默认名称,如 Conventions 的 Javadoc 中所述。
您始终可以通过使用重载的 addAttribute 方法,或通过 @ModelAttribute 上的 name 属性(针对返回值)来分配一个显式名称。 |
你也可以在 @ModelAttribute 方法上将 @RequestMapping 用作方法级别的注解,
此时 @RequestMapping 方法的返回值将被解释为模型属性。
这通常不是必需的,因为在 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.bean.PropertyEditor 或 Spring 的 Converter 和 Formatter 组件。此外,你还可以使用 MVC 配置 在全局共享的 Converter 中注册 Formatter 和 FormattingConversionService 类型。
@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 | 在自定义格式化器上定义一个 @InitBinder 方法。 |
@Controller
class FormController {
@InitBinder (1)
protected fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
}
// ...
}
| 1 | 在自定义格式化器上定义一个 @InitBinder 方法。 |
模型设计
在 Web 应用程序的上下文中,数据绑定是指将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象的属性上。
只有遵循JavaBeans 命名规范的https://www.oracle.com/java/technologies/javase/javabeans-spec.html属性才会暴露用于数据绑定 —— 例如,对于public String getFirstName()属性,其对应的public void setFirstName(String)和firstName方法。
| 模型对象及其嵌套的对象图有时也被称为命令对象、表单支持对象或POJO(Plain Old Java Object,普通Java对象)。 |
默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以针对任意公共属性路径进行操作, 即使某些路径在特定用例中并不预期被访问。
例如,对于一个 HTTP 表单数据端点,恶意客户端可能会提供模型对象图中存在但未在浏览器所呈现的 HTML 表单中包含的属性值。这可能导致模型对象及其任意嵌套对象被设置意外更新的数据。
推荐的做法是使用一个专用的模型对象,该对象仅暴露与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单中,模型对象应声明最少的一组属性,如下列 ChangeEmailForm 所示。
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
如果你不能或不想为每个数据绑定用例使用专用的模型对象,那么你必须限制允许用于数据绑定的属性。
理想情况下,你可以通过在setAllowedFields()上调用WebDataBinder方法来注册允许的字段模式,从而实现这一目标。
例如,要在您的应用程序中注册允许的字段模式,您可以在 @InitBinder 或 @Controller 组件中实现一个 @ControllerAdvice 方法,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以通过 setDisallowedFields() 及其子类中的 DataBinder 方法来注册禁止的字段模式。
但请注意,“允许列表”比“禁止列表”更安全。因此,应优先使用 setAllowedFields() 而非 setDisallowedFields()。
请注意,匹配允许的字段模式是区分大小写的;而匹配禁止的字段模式则是不区分大小写的。此外,即使某个字段同时匹配了允许列表中的某个模式,只要它匹配了禁止的模式,该字段也不会被接受。
|
在直接暴露您的领域模型用于数据绑定时,正确配置允许和禁止的字段模式至关重要。否则,将带来严重的安全风险。 此外,强烈建议您不要在数据绑定场景中使用来自领域模型的类型(例如 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),也可能匹配顶层包装异常中的直接原因(例如,被包装在IOException内部的IllegalStateException)。
为了匹配异常类型,建议像前面示例所示那样,将目标异常声明为方法参数。
当存在多个异常处理方法匹配时,通常优先选择根异常匹配,而非原因异常(cause exception)匹配。
更具体地说,系统会使用 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> {
// ...
}
|
根异常与原因异常的匹配区别可能会令人感到意外。 在前面所示的 在 |
我们通常建议您在方法参数签名中尽可能具体,以减少根异常类型与原因异常类型之间不匹配的可能性。
考虑将一个匹配多种异常的方法拆分为多个单独的 @ExceptionHandler 方法,每个方法通过其签名仅匹配一种特定的异常类型。
在多个 @ControllerAdvice 的配置中,我们建议将主要的根异常映射声明在一个通过相应顺序(order)设置为高优先级的 @ControllerAdvice 上。虽然对于某个给定的控制器或 @ControllerAdvice 类中的方法而言,根异常匹配优于原因(cause)匹配,但这仅限于该类内部的方法之间进行比较。这意味着,高优先级 @ControllerAdvice Bean 中对异常原因的匹配,会优先于低优先级 @ControllerAdvice Bean 中的任何匹配(例如根异常匹配)。
最后但同样重要的是,@ExceptionHandler 方法的实现可以选择通过以原始形式重新抛出给定的异常实例,从而放弃处理该异常。
这在你只对根级别匹配或特定上下文中的匹配感兴趣(而这些上下文无法静态确定)的场景中非常有用。被重新抛出的异常会继续沿着剩余的解析链传播,就好像该 @ExceptionHandler 方法从一开始就没有匹配到一样。
Spring MVC 中对 @ExceptionHandler 方法的支持是构建在 DispatcherServlet 层级的 HandlerExceptionResolver 机制之上的。
方法参数
@ExceptionHandler 方法支持以下参数:
| 方法参数 | 描述 |
|---|---|
异常类型 |
用于访问抛出的异常。 |
|
用于访问引发异常的控制器方法。 |
|
无需直接使用 Servlet API,即可通用地访问请求参数、请求属性和会话属性。 |
|
选择任意特定的请求或响应类型(例如, |
|
强制要求会话存在。因此,此类参数永远不会 |
|
当前已认证的用户——如果已知,可能是某个特定的 |
|
请求的 HTTP 方法。 |
|
当前请求的区域设置,由可用的最具体的 |
|
由 |
|
用于访问由 Servlet API 暴露的原始响应体。 |
|
用于访问错误响应的模型。始终为空。 |
|
指定在重定向时使用的属性——(即附加到查询字符串中的属性)以及临时存储的 Flash 属性,这些属性会保留到重定向后的下一次请求为止。 参见 重定向属性 和 Flash 属性。 |
|
用于访问任何会话属性,这与由于类级别的 |
|
用于访问请求属性。详见 |
返回值
@ExceptionHandler 方法支持以下返回值:
| 返回值 | 描述 |
|---|---|
|
返回值通过 |
|
返回值指定整个响应(包括 HTTP 头部和响应体)应通过 |
|
一个视图名称,将通过 |
|
一个用于渲染的 |
|
通过 |
|
一个要添加到模型中的属性,其视图名称通过 请注意, |
|
要使用的视图和模型属性,以及可选的响应状态。 |
|
如果一个方法的返回类型为 如果以上情况均不成立, |
任何其他返回值 |
如果返回值不匹配上述任何一种情况,并且不是简单类型(由 BeanUtils#isSimpleProperty 判定), 默认情况下,它将被视为模型属性并添加到模型中。如果它是简单类型, 则保持未解析状态。 |
REST API 异常
REST 服务的一个常见需求是在响应体中包含错误详情。Spring 框架不会自动执行此操作,因为响应体中错误详情的表示形式是特定于应用程序的。然而,@RestController 可以使用带有 @ExceptionHandler 返回值的 ResponseEntity 方法来设置响应的状态码和响应体。这类方法也可以在 @ControllerAdvice 类中声明,以实现全局应用。
实现了全局异常处理并在响应体中包含错误详情应用程序,应考虑扩展
ResponseEntityExceptionHandler,
该类提供了对 Spring MVC 抛出异常的处理,并提供了自定义响应体的钩子。要使用此功能,请创建
ResponseEntityExceptionHandler 的子类,使用 @ControllerAdvice 对其进行注解,重写必要的方法,并将其声明为 Spring Bean。
1.3.7. 控制器建议
通常,@ExceptionHandler、@InitBinder 和 @ModelAttribute 方法仅在其所声明的 @Controller 类(或类层次结构)内生效。如果你希望这些方法具有更全局的作用范围(跨多个控制器),可以将它们声明在一个使用 @ControllerAdvice 或 @RestControllerAdvice 注解的类中。
@ControllerAdvice 注解本身带有 @Component 注解,这意味着此类可以通过组件扫描注册为 Spring Bean。@RestControllerAdvice 是一个组合注解,同时带有 @ControllerAdvice 和 @ResponseBody 注解,本质上意味着 @ExceptionHandler 方法会通过消息转换直接渲染到响应体中(而不是通过视图解析或模板渲染)。
在启动时,用于处理 @RequestMapping 和 @ExceptionHandler 方法的基础设施类会检测带有 @ControllerAdvice 注解的 Spring Bean,并在运行时应用它们的方法。全局的 @ExceptionHandler 方法(来自 @ControllerAdvice)会在本地方法(来自 @Controller)之后被应用。相比之下,全局的 @ModelAttribute 和 @InitBinder 方法则会在本地方法之前被应用。
默认情况下,@ControllerAdvice 方法会应用于每个请求(即所有控制器),
但你可以通过在注解上使用属性来将其限定为控制器的一个子集,如下例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3
前一个示例中的选择器是在运行时评估的,如果大量使用,可能会对性能产生负面影响。请参阅
@ControllerAdvice
javadoc 以获取更多详细信息。
1.4. 函数式端点
Spring Web MVC 包含 WebMvc.fn,这是一种轻量级的函数式编程模型,使用函数来路由和处理请求,并且其契约设计为不可变。 它是基于注解的编程模型的一种替代方案,但除此之外,它运行在相同的 DispatcherServlet 上。
1.4.1. 概述
在 WebMvc.fn 中,HTTP 请求由一个 HandlerFunction 处理:这是一个接收 ServerRequest 并返回 ServerResponse 的函数。
请求和响应对象均具有不可变的契约,提供了对 HTTP 请求和响应的 JDK 8 友好式访问方式。
HandlerFunction 在基于注解的编程模型中相当于 @RequestMapping 方法的方法体。
传入的请求通过一个 RouterFunction 路由到处理函数:这是一个接收 ServerRequest 并返回一个可选的 HandlerFunction(即 Optional<HandlerFunction>)的函数。
当路由函数匹配时,会返回一个处理函数;否则返回一个空的 Optional。
RouterFunction 相当于 @RequestMapping 注解,但主要区别在于路由函数不仅提供数据,还提供行为。
RouterFunctions.route() 提供了一个路由构建器,便于创建路由器,如下例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
| 1 | 使用路由 DSL 创建路由器。 |
如果你将 RouterFunction 注册为一个 bean,例如在 #webmvc-fn-running 类中将其暴露出来,那么如运行服务器一节所述,它将被 Servlet 自动检测到。
1.4.2. 处理函数
ServerRequest 和 ServerResponse 是不可变的接口,提供了对 HTTP 请求和响应(包括头信息、正文、方法和状态码)的 JDK 8 友好访问方式。
服务器请求
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 提供对 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()
处理器类
我们可以将处理函数编写为 lambda 表达式,如下例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
这样做很方便,但在实际应用中我们需要多个函数,而多个内联的 lambda 表达式可能会变得混乱。
因此,将相关的处理函数组合到一个处理类中会很有用,该类在基于注解的应用程序中扮演的角色类似于 @Controller。
例如,下面的类暴露了一个响应式的 Person 仓库:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
| 1 | listPeople 是一个处理函数,它将仓库中找到的所有 Person 对象以 JSON 格式返回。 |
| 2 | createPerson 是一个处理函数,用于存储请求体中包含的新 Person 对象。 |
| 3 | getPerson 是一个处理函数,它返回由路径变量 id 标识的单个人员。我们从仓库中检索该 Person 并创建一个 JSON 响应(如果找到)。如果未找到,则返回 404 Not Found 响应。 |
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
}
fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
| 1 | listPeople 是一个处理函数,它将仓库中找到的所有 Person 对象以 JSON 格式返回。 |
| 2 | createPerson 是一个处理函数,用于存储请求体中包含的新 Person 对象。 |
| 3 | getPerson 是一个处理函数,它返回由路径变量 id 标识的单个人员。我们从仓库中检索该 Person 并创建一个 JSON 响应(如果找到)。如果未找到,则返回 404 Not Found 响应。 |
验证
public class PersonHandler {
private final Validator validator = new PersonValidator(); (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对 400 响应抛出异常。 |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().build()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
| 1 | 创建 Validator 实例。 |
| 2 | 应用验证。 |
| 3 | 对 400 响应抛出异常。 |
处理器也可以通过创建并注入一个基于 Validator 的全局 LocalValidatorFactoryBean 实例,来使用标准的 Bean Validation API(JSR-303)。
参见 Spring 验证。
1.4.3. RouterFunction
路由函数(Router functions)用于将请求路由到相应的HandlerFunction。
通常,您不会自己编写路由函数,而是使用
RouterFunctions 工具类上的方法来创建路由函数。
RouterFunctions.route()(无参数)为您提供了一个流畅的构建器(fluent builder)来创建路由
函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction) 则提供了一种直接创建路由函数的方式。
通常建议使用 route() 构建器,因为它为典型的映射场景提供了便捷的快捷方式,而无需使用难以发现的静态导入。
例如,路由函数构建器提供了 GET(String, HandlerFunction) 方法来创建 GET 请求的映射;以及用于 POST 请求的 POST(String, HandlerFunction) 方法。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射请求时引入额外谓词(predicates)的方式。
对于每个 HTTP 方法,都提供了一个重载变体,该变体接受一个 RequestPredicate 参数,通过该参数可以表达额外的约束条件。
谓词
你可以编写自己的 RequestPredicate,但 RequestPredicates 工具类提供了常用的实现,这些实现基于请求路径、HTTP 方法、内容类型(content-type)等。
以下示例使用一个请求谓词(request predicate)根据 Accept 请求头创建约束条件:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
你可以通过使用以下方式将多个请求谓词组合在一起:
-
RequestPredicate.and(RequestPredicate)—— 两者都必须匹配。 -
RequestPredicate.or(RequestPredicate)—— 两者中任意一个匹配即可。
RequestPredicates 中的许多谓词都是组合而成的。
例如,RequestPredicates.GET(String) 就是由 RequestPredicates.method(HttpMethod)
和 RequestPredicates.path(String) 组合而成的。
上面所示的示例也使用了两个请求谓词,因为构建器在内部使用了
RequestPredicates.GET,并将其与 accept 谓词进行组合。
路由
路由函数按照顺序进行评估:如果第一个路由不匹配,则评估第二个,依此类推。 因此,将更具体的路由声明在通用路由之前是有意义的。 请注意,此行为与基于注解的编程模型不同,在基于注解的模型中,“最具体”的控制器方法会被自动选择。
使用路由函数构建器时,所有定义的路由会被组合成一个RouterFunction,并通过build()方法返回。
还有其他方式可以将多个路由函数组合在一起:
-
add(RouterFunction)方法位于RouterFunctions.route()构建器上 -
RouterFunction.and(RouterFunction) -
RouterFunction.andRoute(RequestPredicate, HandlerFunction)— 是RouterFunction.and()与嵌套的RouterFunctions.route()的快捷方式。
以下示例展示了四条路由的组合:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
| 1 | 带有匹配 JSON 的 GET /person/{id} 请求头的 Accept 请求会被路由到
PersonHandler.getPerson |
| 2 | 带有匹配 JSON 的 GET /person 请求头的 Accept 请求会被路由到
PersonHandler.listPeople |
| 3 | POST /person 请求在没有附加谓词的情况下被映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是在其他地方创建的路由器函数,并被添加到所构建的路由中。 |
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
| 1 | 带有匹配 JSON 的 GET /person/{id} 请求头的 Accept 请求会被路由到
PersonHandler.getPerson |
| 2 | 带有匹配 JSON 的 GET /person 请求头的 Accept 请求会被路由到
PersonHandler.listPeople |
| 3 | POST /person 请求在没有附加谓词的情况下被映射到
PersonHandler.createPerson,并且 |
| 4 | otherRoute 是在其他地方创建的路由器函数,并被添加到所构建的路由中。 |
嵌套路由
通常,一组路由函数会共享一个相同的谓词(predicate),例如共享一个路径。
在上面的示例中,共享的谓词是一个路径谓词,匹配 /person,
被其中三个路由所使用。
使用注解时,你可以通过在类级别上使用 @RequestMapping 注解
来映射到 /person,从而消除这种重复。
在 WebMvc.fn 中,可以通过路由器函数构建器(router function builder)上的 path 方法
来共享路径谓词。
例如,可以通过使用嵌套路由,以如下方式改进上述示例的最后几行代码:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson))
.build();
| 1 | 请注意,path 的第二个参数是一个接收路由构建器的消费者(Consumer)。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套最为常见,但你可以通过在构建器上使用 nest 方法,根据任意类型的谓词进行嵌套。
上述代码中仍存在一些重复,即共享的 Accept 请求头谓词。
我们可以通过将 nest 方法与 accept 结合使用来进一步优化:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.build();
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
1.4.4. 运行服务器
您通常会在基于 DispatcherHandler 的设置中通过
MVC 配置 运行路由函数,该配置使用 Spring 配置来声明处理请求所需的组件。MVC Java 配置会声明以下基础设施组件以支持函数式端点:
-
RouterFunctionMapping:检测 Spring 配置中一个或多个RouterFunction<?>Bean,通过RouterFunction.andOther将它们组合起来,并将请求路由到所生成的组合后的RouterFunction。 -
HandlerFunctionAdapter:一个简单的适配器,允许DispatcherHandler调用已映射到请求的HandlerFunction。
上述组件使函数式端点能够融入 DispatcherServlet 的请求处理生命周期,并且(如果存在)还可以与注解式控制器并行运行。这也是 Spring Boot Web Starter 启用函数式端点的方式。
以下示例展示了一个 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. 过滤处理器函数
你可以通过在路由函数构建器(routing function builder)上使用 before、after 或 filter 方法来过滤处理函数。
使用注解时,你可以通过 @ControllerAdvice、ServletFilter 或两者结合来实现类似的功能。
该过滤器将应用于由该构建器创建的所有路由。
这意味着在嵌套路由中定义的过滤器不会应用于“顶层”路由。
例如,考虑以下示例:
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("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
| 1 | 仅对两个 GET 路由应用了添加自定义请求头的 before 过滤器。 |
| 2 | 应用于所有路由(包括嵌套路由)的after过滤器用于记录响应。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST("/person", handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
| 1 | 仅对两个 GET 路由应用了添加自定义请求头的 before 过滤器。 |
| 2 | 应用于所有路由(包括嵌套路由)的after过滤器用于记录响应。 |
路由器构建器上的 filter 方法接收一个 HandlerFilterFunction:
这是一个函数,它接收一个 ServerRequest 和一个 HandlerFunction,并返回一个 ServerResponse。
其中的处理器函数参数代表链中的下一个元素。
这通常是被路由到的处理器,但如果应用了多个过滤器,它也可以是另一个过滤器。
现在,我们可以为路由添加一个简单的安全过滤器,前提是假设我们有一个 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("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例表明,调用 next.handle(ServerRequest) 是可选的。
我们仅在允许访问时才执行处理函数。
除了在路由函数构建器上使用 filter 方法外,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用于现有的路由函数。
函数式端点的 CORS 支持通过专用的
CorsFilter提供。 |
1.5. URI 链接
本节介绍了 Spring 框架中用于处理 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 和 Spring WebFlux
UriComponentsBuilder 实现 UriBuilder。您可以使用 UriBuilderFactory 创建一个
UriBuilder。UriBuilderFactory 和
UriBuilder 共同提供了一种可插拔的机制,用于基于共享配置(如基础 URL、编码偏好和其他细节)从 URI 模板构建 URI。
您可以使用 RestTemplate 来配置 WebClient 和 UriBuilderFactory,以自定义 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 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder 在两个层级上提供了编码选项:
-
UriComponentsBuilder#encode(): 先对 URI 模板进行预编码,然后在展开时严格编码 URI 变量。
-
UriComponents#encode(): 在 URI 变量展开之后对 URI 组件进行编码。
这两种选项都会将非ASCII字符和非法字符替换为转义的八位字节序列。然而,第一种选项还会替换出现在URI变量中具有保留意义的字符。
| 考虑“;”字符,它在路径中是合法的,但具有保留含义。第一种选项会在 URI 变量中将“;”替换为“%3B”,但在 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 模板进行编码,而是在将 URI 变量展开到模板之前,通过UriUtils#encodeUriUriVariables对 URI 变量应用严格的编码。 -
URI_COMPONENT:使用UriComponents#encode()(对应前面列表中的第二个选项),在 URI 变量展开后对 URI 组件值进行编码。 -
NONE:不应用任何编码。
出于历史原因和向后兼容性考虑,RestTemplate 的编码模式被设置为 EncodingMode.URI_COMPONENT。
WebClient 则依赖于 DefaultUriBuilderFactory 中的默认值,该默认值在 5.0.x 版本中为 EncodingMode.URI_COMPONENT,
而在 5.1 版本中已更改为 EncodingMode.TEMPLATE_AND_VALUES。
1.5.4. 相对 Servlet 请求
您可以使用 ServletUriComponentsBuilder 来创建相对于当前请求的 URI,如下例所示:
HttpServletRequest request = ...
// Re-uses host, scheme, port, path and query string...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode();
val request: HttpServletRequest = ...
// Re-uses host, scheme, port, path and query string...
val ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode()
您可以创建相对于上下文路径的 URI,如下例所示:
// Re-uses host, port and context path...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts").build()
// Re-uses host, port and context path...
val ucb = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts").build()
您可以创建相对于 Servlet 的 URI(例如,/main/*),如下例所示:
// Re-uses host, port, context path, and Servlet prefix...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts").build()
// Re-uses host, port, context path, and Servlet prefix...
val ucb = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts").build()
自 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 的方式。例如,你可以使用一种类似于通过代理进行模拟测试的技术,避免通过方法名引用控制器方法,如下例所示(该示例假定已静态导入 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 创建链接时,其方法签名在设计上会受到一定限制。除了需要具备合适的参数签名外,返回类型也存在技术上的限制(即,需要为链接构建器调用生成运行时代理),因此返回类型不能是 final 类型。特别是,通常用于视图名称的 String 返回类型在此场景下无法使用。此时应改用 ModelAndView,或者甚至直接使用普通的 Object(并返回一个 String 值)。 |
前面的示例使用了 MvcUriComponentsBuilder 中的静态方法。在内部,它们依赖于 ServletUriComponentsBuilder,根据当前请求的协议(scheme)、主机(host)、端口(port)、上下文路径(context path)和 Servlet 路径(servlet path)来构建基础 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 版本开始,MvcUriComponentsBuilder 会忽略来自 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),但你也可以轻松定义自己的函数,或为其他模板技术准备类似的函数。
其工作原理如下:在启动时,每个 @RequestMapping 都会通过 HandlerMethodMappingNamingStrategy 被分配一个默认名称,该策略的默认实现使用类名和方法名中的大写字母(例如,getThing 类中的 ThingController 方法会变成 "TC#getThing")。如果出现名称冲突,你可以使用 @RequestMapping(name="..") 来显式指定一个名称,或者实现你自己的 HandlerMethodMappingNamingStrategy。
1.6. 异步请求
Spring MVC 与 Servlet 3.0 异步请求处理有着广泛的集成:
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"
}
然后,可以通过在配置的 TaskExecutor 中运行给定任务来获取返回值。
1.6.3. 处理
以下是 Servlet 异步请求处理的简要概述:
-
可以通过调用
ServletRequest将request.startAsync()置于异步模式。 这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但响应仍保持打开状态,以便稍后完成处理。 -
调用
request.startAsync()会返回一个AsyncContext,可用于进一步控制异步处理。例如,它提供了dispatch方法,该方法类似于 Servlet API 中的转发(forward),不同之处在于它允许应用程序在 Servlet 容器线程上恢复请求处理。 -
ServletRequest提供对当前DispatcherType的访问,您可以使用它来区分初始请求处理、异步分发、转发以及其他分发类型。
DeferredResult 的处理流程如下:
-
控制器返回一个
DeferredResult,并将其保存在某个内存中的队列或列表中,以便后续访问。 -
Spring MVC 调用
request.startAsync()。 -
与此同时,
DispatcherServlet和所有已配置的过滤器退出请求处理线程,但响应仍保持打开状态。 -
应用程序从某个线程设置
DeferredResult,Spring MVC 将请求重新分发回 Servlet 容器。 -
DispatcherServlet再次被调用,并使用异步生成的返回值继续处理。
Callable 的处理流程如下:
-
控制器返回一个
Callable。 -
Spring MVC 调用
request.startAsync()并将Callable提交到一个TaskExecutor中,以便在单独的线程中进行处理。 -
与此同时,
DispatcherServlet和所有过滤器退出 Servlet 容器线程,但响应仍保持打开状态。 -
最终,
Callable会产生一个结果,Spring MVC 将请求重新分发回 Servlet 容器以完成处理。 -
DispatcherServlet再次被调用,并使用Callable异步生成的返回值继续处理。
如需进一步了解背景和上下文,您还可以阅读博客文章,这些文章介绍了 Spring MVC 3.2 中引入的异步请求处理支持。
异常处理
当你使用 DeferredResult 时,可以选择调用 setResult 或者使用异常调用 setErrorResult。在这两种情况下,Spring MVC 都会将请求重新分派回 Servlet 容器以完成处理。随后,该请求会被视为控制器方法返回了指定的值,或者被视为抛出了给定的异常。该异常随后会进入常规的异常处理机制(例如,调用 @ExceptionHandler 方法)。
当你使用 Callable 时,会发生类似的处理逻辑,主要区别在于结果是从 Callable 中返回的,或者由它抛出异常。
拦截
HandlerInterceptor 实例可以是 AsyncHandlerInterceptor 类型,以便在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted 回调(而不是 postHandle 和 afterCompletion)。
HandlerInterceptor 个实现还可以注册一个 CallableProcessingInterceptor
或一个 DeferredResultProcessingInterceptor,以便更深入地集成到异步请求的生命周期中(例如,处理超时事件)。请参阅
AsyncHandlerInterceptor
以获取更多详细信息。
DeferredResult 提供 onTimeout(Runnable) 和 onCompletion(Runnable) 回调。
请参阅 DeferredResult 的 Javadoc
以获取更多详情。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 都支持在控制器方法中使用异步和响应式类型作为返回值。 Spring MVC 甚至支持流式传输,包括响应式背压。然而,与 WebFlux 不同的是,对响应的单次写入仍然是阻塞的(并在单独的线程上执行),而 WebFlux 依赖于非阻塞 I/O,每次写入无需额外的线程。
另一个根本性的区别在于,Spring MVC 不支持在控制器方法参数中使用异步或响应式类型(例如 @RequestBody、@RequestPart 等),也不对模型属性中的异步和响应式类型提供任何显式支持。而 Spring WebFlux 则支持所有这些特性。
1.6.4. HTTP 流式传输
你可以使用 DeferredResult 和 Callable 来返回单个异步值。
如果你希望生成多个异步值并将它们写入响应,该如何实现呢?本节将介绍具体做法。
对象
您可以使用 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 会调用已配置的异常解析器并完成请求。
服务器发送事件(Server-Sent Events,简称 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 消息,并结合 SockJS 降级传输(包括 SSE),以支持更广泛的浏览器。
另请参阅上一节中关于异常处理的说明。
原始数据
有时,绕过消息转换并直接将内容流式传输到响应的 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. 响应式类型
Spring MVC 支持在控制器中使用响应式客户端库(另请参阅 WebFlux 章节中的响应式库)。
这包括来自 WebClient 的 spring-webflux 以及其他库,例如 Spring Data 响应式数据仓库。在这些场景下,能够从控制器方法返回响应式类型将非常方便。
响应式返回值按如下方式处理:
-
单值 Promise 被适配为类似于使用
DeferredResult的形式。示例包括Mono(Reactor)或Single(RxJava)。 -
具有流式媒体类型(例如
application/stream+json或text/event-stream)的多值流会被适配,类似于使用ResponseBodyEmitter或SseEmitter。示例包括Flux(Reactor)或Observable(RxJava)。应用程序也可以返回Flux<ServerSentEvent>或Observable<ServerSentEvent>。 -
一个多值流(multi-value stream)会适配为其他任意媒体类型(例如
application/json),其方式类似于使用DeferredResult<List<?>>。
Spring MVC 通过来自spring-core的ReactiveAdapterRegistry支持 Reactor 和 RxJava,这使其能够适配多种响应式库。 |
1.6.6. 断开连接
Servlet API 在远程客户端断开连接时不会发出任何通知。 因此,在向响应流式传输数据时(无论是通过 SseEmitter 还是 响应式类型),定期发送数据非常重要, 因为如果客户端已断开连接,写入操作将会失败。这种发送可以采用一个空的(仅包含注释的)SSE 事件形式, 或者采用其他任何对方需将其解释为心跳信号并忽略的数据形式。
或者,考虑使用内置心跳机制的 Web 消息解决方案(例如 基于 WebSocket 的 STOMP 或结合 SockJS 的 WebSocket)。
1.6.7. 配置
必须在 Servlet 容器级别启用异步请求处理功能。 MVC 配置还提供了多个用于异步请求的选项。
Servlet 容器
过滤器和 Servlet 的声明中有一个 asyncSupported 标志,需要将其设置为 true 以启用异步请求处理。此外,过滤器映射应声明为处理 ASYNC 类型的 javax.servlet.DispatchType。
在 Java 配置中,当你使用 AbstractAnnotationConfigDispatcherServletInitializer
来初始化 Servlet 容器时,这一操作会自动完成。
在 web.xml 配置中,您可以为 <async-supported>true</async-supported> 和 DispatcherServlet 声明添加 Filter,并为过滤器映射添加 <dispatcher>ASYNC</dispatcher>。
Spring MVC
MVC 配置公开了以下与异步请求处理相关的选项:
-
Java 配置:在
configureAsyncSupport上使用WebMvcConfigurer回调方法。 -
XML 命名空间:在
<async-support>元素下使用<mvc:annotation-driven>元素。
您可以配置以下内容:
-
异步请求的默认超时值,如果未设置,则取决于底层的 Servlet 容器。
-
用于在使用响应式类型进行流式传输时执行阻塞写入操作,以及执行控制器方法返回的
#mvc-ann-async-reactive-types实例的Callable。如果您使用响应式类型进行流式传输,或者控制器方法返回Callable,我们强烈建议配置此属性,因为默认情况下它是一个SimpleAsyncTaskExecutor。 -
DeferredResultProcessingInterceptor的实现类和CallableProcessingInterceptor的实现类。
请注意,您也可以在 DeferredResult、ResponseBodyEmitter 和 SseEmitter 上设置默认超时值。对于 Callable,您可以使用 WebAsyncTask 来指定超时值。
1.7. 跨域资源共享(CORS)
Spring MVC 允许你处理 CORS(跨域资源共享)。本节将介绍如何实现这一点。
1.7.1. 简介
出于安全原因,浏览器禁止向当前源(origin)之外的资源发起 AJAX 请求。 例如,你可能在一个标签页中打开了你的银行账户页面,而在另一个标签页中打开了 evil.com。来自 evil.com 的脚本不应能够使用你的凭据向你的银行 API 发起 AJAX 请求——比如从你的账户中取款!
1.7.2. 处理
CORS 规范区分了预检请求(preflight)、简单请求(simple)和实际请求(actual)。 要了解 CORS 的工作原理,您可以阅读 这篇文章, 以及其他许多资料,或查阅规范以获取更多详细信息。
Spring MVC 的 HandlerMapping 实现内置了对 CORS 的支持。在成功将请求映射到处理器之后,HandlerMapping 实现会检查该请求和处理器对应的 CORS 配置,并执行后续操作。预检(Preflight)请求会被直接处理,而简单请求和实际的 CORS 请求则会被拦截、验证,并设置所需的 CORS 响应头。
为了启用跨域请求(即请求中包含 Origin 头部,且其值与请求的主机不同),您需要显式声明一些 CORS 配置。如果未找到匹配的 CORS 配置,预检(preflight)请求将被拒绝。简单 CORS 请求和实际 CORS 请求的响应中不会添加任何 CORS 相关头部,因此浏览器会拒绝这些请求。
每个 HandlerMapping 都可以
单独配置
基于 URL 模式的 CorsConfiguration 映射。在大多数情况下,应用程序使用 MVC 的 Java 配置或 XML 命名空间来声明此类映射,从而将一个全局的映射传递给所有的 HandlerMappping 实例。
你可以在 HandlerMapping 级别上结合全局 CORS 配置与更细粒度的处理器级别 CORS 配置。例如,带注解的控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。
全局配置与本地配置的组合规则通常是累加的——例如,所有全局来源和所有本地来源。对于仅能接受单个值的属性(如 allowCredentials 和 maxAge),本地配置将覆盖全局配置的值。详见
CorsConfiguration#combine(CorsConfiguration)
以获取更多详细信息。
|
如需从源码中了解更多内容或进行高级自定义,请查看背后的代码:
|
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 允许:
-
所有源。
-
所有标头。
-
控制器方法所映射的所有 HTTP 方法。
allowedCredentials 默认未启用,因为这会建立一种信任级别,暴露敏感的用户特定信息(例如 Cookie 和 CSRF Tokens),仅应在适当的情况下使用。
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 配置。您可以在任意 CorsConfiguration 上单独设置基于 URL 的 HandlerMapping 映射。然而,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来实现这一点。
默认情况下,全局配置启用了以下内容:
-
所有源。
-
所有标头。
-
GET、HEAD和POST方法。
allowedCredentials 默认未启用,因为这会建立一种信任级别,暴露敏感的用户特定信息(例如 Cookie 和 CSRF Tokens),仅应在适当的情况下使用。
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 支持。
如果你尝试在 Spring Security 中使用 CorsFilter,请记住 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)展开。Cache-Control 用于指导私有缓存(例如浏览器)和公共缓存(例如代理服务器)如何缓存和重用响应。ETag 头用于发起条件请求,如果内容未发生变化,则可能返回不带响应体的 304(NOT_MODIFIED)状态码。ETag 可被视为比 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 属性(以秒为单位定义),其工作方式如下:
-
值为
-1时不会生成Cache-Control响应头。 -
值为
0时,将通过使用'Cache-Control: no-store'指令来禁止缓存。 -
一个
n > 0的值会通过使用n指令,将给定的响应缓存'Cache-Control: max-age=n'秒。
1.9.2. 控制器
控制器可以显式支持 HTTP 缓存。我们建议这样做,因为资源的 lastModified 或 ETag 值需要在与条件请求头进行比较之前进行计算。控制器可以向 ETag 添加 Cache-Control 头部和 ResponseEntity 设置,如下例所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
@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(未修改)响应。否则,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(未修改)——无需进一步处理。 |
| 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(未修改)——无需进一步处理。 |
| 3 | 继续处理请求。 |
有三种方式可用于根据 eTag 值、lastModified 值或两者同时进行条件请求的检查。对于条件 GET 和 HEAD 请求,您可以将响应设置为 304(未修改)。而对于条件 POST、PUT 和 DELETE 请求,则可以将响应设置为 412(前提条件失败),以防止并发修改。
1.9.4. ETag过滤器
您可以使用 ShallowEtagHeaderFilter 来添加“浅层”eTag 值,这些值是根据响应内容计算得出的,从而节省带宽,但不会节省 CPU 时间。参见浅层 ETag。
1.10. 视图技术
在 Spring MVC 中,视图技术的使用是可插拔的,无论你选择使用 Thymeleaf、Groovy Markup Templates、JSP 或其他技术,主要都只需进行配置上的更改。本章介绍与 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 模板的支持。
视图配置
以下示例展示了如何将 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 配置
你可以通过在 Configuration bean 上设置相应的 bean 属性,将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 的 FreeMarkerConfigurer 对象(该对象由 Spring 管理)。其中,freemarkerSettings 属性需要一个 java.util.Properties 对象,而 freemarkerVariables 属性则需要一个 java.util.Map。以下示例展示了如何使用 FreeMarkerConfigurer:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
有关设置和变量的详细信息,请参阅 FreeMarker 文档,这些内容适用于 Configuration 对象。
表单处理
Spring 提供了一个用于 JSP 的标签库,其中包含(但不限于)一个 <spring:bind/> 元素。该元素主要用于在表单中显示表单支持对象(form-backing objects)的值,并展示 Web 层或业务层中 Validator 验证失败的结果。Spring 还为 FreeMarker 提供了相同功能的支持,并额外提供了便捷的宏(macros),用于直接生成表单输入元素。
绑定宏
FreeMarker 的一组标准宏定义维护在 spring-webmvc.jar 文件中,因此对于经过适当配置的应用程序而言,这些宏始终可用。
Spring 模板库中定义的一些宏被视为内部(私有)宏,但在宏定义中并不存在此类作用域限制,因此所有宏对调用代码和用户模板都是可见的。以下各节仅聚焦于您需要在模板中直接调用的宏。如果您希望直接查看宏的源代码,该文件名为 spring.ftl,位于 org.springframework.web.servlet.view.freemarker 包中。
简单绑定
在基于 FreeMarker 模板的 HTML 表单中(这些表单作为 Spring MVC 控制器的表单视图),您可以使用类似于以下示例的代码来绑定字段值,并以与 JSP 等效方式类似的方式显示每个输入字段的错误消息。以下示例展示了一个 personForm 视图:
<!-- FreeMarker macros have to be imported into a namespace.
We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>
<@spring.bind> 需要一个 'path' 参数,该参数由您的命令对象的名称(默认为 'command',除非您在控制器配置中更改了它)后跟一个点号(.)以及您希望绑定的命令对象字段名称组成。您也可以使用嵌套字段,例如 command.address.street。bind 宏会采用 ServletContext 中 defaultHtmlEscape 参数 web.xml 所指定的默认 HTML 转义行为。
另一种形式的宏称为 <@spring.bindEscaped>,它接受第二个参数,
用于显式指定在状态错误消息或值中是否应使用 HTML 转义。您可以根据需要将其设置为 true 或 false。
额外的表单处理宏简化了 HTML 转义的使用,您应尽可能使用这些宏。
它们将在下一节中进行说明。
输入宏
FreeMarker 提供了额外的便捷宏,用于简化数据绑定和表单生成(包括验证错误信息的显示)。使用这些宏来生成表单输入字段并非必需,您可以将它们与普通 HTML 或之前介绍过的直接调用 Spring 绑定宏的方式混合使用。
下表列出了可用的宏,展示了每个宏对应的 FreeMarker 模板(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/> |
在 FreeMarker 模板中,formHiddenInput 和 formPasswordInput 实际上并不是必需的,因为你可以使用普通的 formInput 宏,并将 hidden 参数的值指定为 password 或 fieldType。 |
上述任意宏的参数具有统一的含义:
-
path:要绑定的字段名称(例如“command.name”) -
options:一个Map,包含输入字段中所有可供选择的值。该映射的键代表从表单提交并绑定到命令对象的值。存储在键对应的映射对象是显示在表单上供用户查看的标签,可能与表单提交回来的相应值不同。通常,此类映射由控制器作为参考数据提供。根据所需行为,您可以使用任何Map实现。对于严格排序的映射,您可以使用带有合适Comparator的SortedMap(例如TreeMap);对于需要按插入顺序返回值的任意映射,请使用来自commons-collections的LinkedHashMap或LinkedMap。 -
separator:当多个选项以独立元素(如单选按钮或复选框)形式提供时,用于分隔列表中每个选项的字符序列(例如<br>)。 -
attributes:一个额外的任意标签或文本字符串,将被包含在 HTML 标签本身之内。该字符串会被宏原样输出。例如,在textarea字段中,您可以提供属性(如 'rows="5" cols="60"'),也可以传入样式信息,例如 'style="border:1px solid silver"'。 -
classOrStyle:对于showErrors宏,指定用于包裹每个错误信息的span元素所使用的 CSS 类名。如果没有提供信息(或值为空),则错误信息将被包裹在<b></b>标签中。
以下各节概述了宏的示例。
formInput 宏接受 path 参数(command.name)以及一个额外的 attributes 参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一样,会对 showErrors 参数执行隐式的 Spring 绑定。此绑定会一直有效,直到发生新的绑定为止,因此 5 宏无需再次传递 6 参数——它会直接作用于最近一次创建绑定的字段。
showErrors 宏接受一个分隔符参数(用于分隔同一字段上的多个错误信息的字符),还接受第二个参数——这次是一个类名或样式属性。请注意,FreeMarker 可以为 attributes 参数指定默认值。以下示例展示了如何使用 formInput 和 showWErrors 宏:
<@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 和 textarea 属性。
您可以使用四个选择字段宏,在 HTML 表单中生成常用 UI 值选择输入控件:
-
formSingleSelect -
formMultiSelect -
formRadioButtons -
formCheckboxes
这四个宏中的每一个都接受一个包含表单字段值及其对应标签的 Map 选项。值和标签可以相同。
下一个示例是 FTL 中的单选按钮。表单绑定对象为此字段指定了默认值“London”,因此无需进行验证。在渲染表单时,可供选择的完整城市列表作为引用数据以名称“cityMap”提供在模型中。以下代码清单展示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
上述代码片段会渲染一行单选按钮,cityMap 中的每个值对应一个单选按钮,并使用 "" 作为分隔符。未提供额外的属性(宏的最后一个参数被省略了)。在 cityMap 中,映射的每个键值对都使用相同的 String。该映射的键才是表单实际作为 POST 请求参数提交的内容,而映射的值则是用户看到的标签。在前面的例子中,假设有三个知名城市的列表,并且表单支持对象中设定了默认值,生成的 HTML 类似如下:
Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>
如果你的应用程序需要通过内部代码(例如)来处理城市,你可以创建一个以合适键值为索引的代码映射,如下例所示:
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 转义
如前所述,默认使用表单宏(form macros)会生成符合 HTML 4.01 标准的 HTML 元素,并采用 web.xml 文件中定义的默认 HTML 转义值(该值由 Spring 的绑定支持所使用)。若要使生成的元素符合 XHTML 标准,或覆盖默认的 HTML 转义值,您可以在模板中(或在模型中,只要模板可以访问到这些变量)指定两个变量。在模板中指定这些变量的优势在于,在模板处理过程中,您可以随时将它们更改为不同的值,从而为表单中的不同字段提供不同的行为。
要使您的标签符合 XHTML 规范,请为名为 true 的模型或上下文变量指定值 xhtmlCompliant,如下例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理此指令后,Spring 宏生成的所有元素现在都符合 XHTML 标准。
同样地,您可以按字段指定 HTML 转义,如下例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
1.10.3. Groovy 标记语言
Groovy Markup 模板引擎主要用于生成类 XML 的标记(如 XML、XHTML、HTML5 等),但你也可以用它来生成任何基于文本的内容。Spring Framework 内置了对在 Spring MVC 中使用 Groovy Markup 的集成支持。
| Groovy 标记模板引擎需要 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 框架内置了对将 Spring MVC 与任何可在 JSR-223 Java 脚本引擎之上运行的模板库集成的支持。我们已在不同的脚本引擎上测试了以下模板库:
| 脚本库 | 脚本引擎 |
|---|---|
集成任何其他脚本引擎的基本规则是,它必须实现
ScriptEngine 和 Invocable 接口。 |
要求
您需要将脚本引擎放在类路径(classpath)中,具体细节因脚本引擎而异:
-
Nashorn JavaScript 引擎随 Java 8+ 一同提供。强烈建议使用最新的更新版本。
-
应将 JRuby 添加为依赖项以支持 Ruby。
-
Jython 应作为依赖项添加以支持 Python。
-
应添加
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() 原生兼容此签名,因此你可以直接调用它。
如果你的模板技术需要一些自定义配置,你可以提供一个脚本来实现自定义的渲染函数。例如,Handlebars 在使用模板之前需要先对其进行编译,并且需要一个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
}
}
当使用非线程安全的脚本引擎与未针对并发设计的模板库(例如在 Nashorn 上运行的 Handlebars 或 React)时,需要将 sharedEngine 属性设置为 false。在这种情况下,由于此 bug,需要 Java SE 8 更新版本 60 或更高版本,但无论如何,通常都建议使用较新的 Java SE 补丁版本。 |
polyfill.js 仅定义了 Handlebars 正常运行所需的 window 对象,如下所示:
var window = {};
这个基本的 render.js 实现在使用模板之前对其进行编译。一个可用于生产环境的实现还应缓存任何重复使用的模板或预编译的模板。
你可以在脚本端实现这一点(并处理所需的任何自定义操作,例如管理模板引擎的配置)。以下示例展示了如何实现:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.10.5. JSP 和 JSTL
Spring 框架内置了对在 Spring MVC 中使用 JSP 和 JSTL 的集成支持。
视图解析器
在使用 JSP 进行开发时,你可以声明一个 InternalResourceViewResolver 或 ResourceBundleViewResolver bean。
ResourceBundleViewResolver 依赖一个属性文件来定义视图名称与类及 URL 的映射关系。通过使用 ResourceBundleViewResolver,你可以仅使用一个解析器来混合不同类型的视图,如下例所示:
<!-- the ResourceBundleViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
# And a sample properties file is used (views.properties in WEB-INF/classes):
welcome.(class)=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp
productList.(class)=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp
InternalResourceViewResolver 也可用于 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 标签都具备 HTML 转义功能,可启用或禁用字符的转义。
spring.tld 标签库描述符(TLD)包含在 spring-webmvc.jar 中。
有关各个标签的完整参考,请浏览
API 参考文档
或查看标签库描述。
Spring 的表单标签库
从 2.0 版本开始,Spring 提供了一整套支持数据绑定的标签,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标签都支持其对应 HTML 标签的所有属性,使得这些标签易于理解和直观使用。这些标签生成的 HTML 符合 HTML 4.01/XHTML 1.0 规范。
与其他表单/输入标签库不同,Spring 的表单标签库与 Spring Web MVC 紧密集成,使这些标签能够访问控制器所处理的命令对象和引用数据。正如我们在以下示例中所展示的那样,表单标签使得 JSP 的开发、阅读和维护变得更加简单。
我们逐一介绍表单标签,并查看每个标签的使用示例。对于某些需要进一步说明的标签,我们还包含了生成的 HTML 代码片段。
配置
表单标签库内置于 spring-webmvc.jar 中。该库的描述文件名为 spring-form.tld。
要使用此库中的标签,请在您的 JSP 页面顶部添加以下指令:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中 form 是您希望用于此库中标签的标签名称前缀。
表单标签
此标签渲染一个 HTML 'form' 元素,并向内部标签暴露一个绑定路径以进行绑定。它将命令对象放入 PageContext 中,以便内部标签可以访问该命令对象。本标签库中的所有其他标签都是 form 标签的嵌套标签。
假设我们有一个名为User的领域对象。它是一个JavaBean,具有诸如firstName和lastName等属性。我们可以将其用作表单控制器的表单支持对象(form-backing object),该控制器返回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 假设表单支持对象(form-backing object)在模型中的变量名为
command。如果你将表单支持对象以其他名称放入模型中(这无疑是一种最佳实践),你可以将表单绑定到该命名变量,如下例所示:
<form:form modelAttribute="user">
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
这input标签
此标签默认渲染一个带有绑定值且 input 的 HTML type='text' 元素。
有关此标签的示例,请参见表单标签。您也可以使用 HTML5 特定的类型,例如 email、tel、date 等。
这checkbox标签
此标签会渲染一个 HTML input 标签,并将其 type 属性设置为 checkbox。
假设我们的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时,如果绑定值为input(checkbox),则checked会被标记为true。value属性对应于setValue(Object)方法所设置的值属性的解析结果。 -
方法二:当绑定的值类型为
array或java.util.Collection时,如果配置的input(checkbox)值存在于该绑定的checked中,则对应的setValue(Object)将被标记为Collection。 -
方法三:对于任何其他绑定的值类型,如果配置的
input(checkbox)方法所设置的值等于绑定的值,则checked将被标记为setValue(Object)。
请注意,无论采用哪种方式,生成的 HTML 结构都是相同的。以下 HTML 片段定义了一些复选框:
<tr>
<td>Interests:</td>
<td>
Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
<input type="hidden" value="1" name="_preferences.interests"/>
</td>
</tr>
你可能不会预料到在每个复选框后面都会看到一个额外的隐藏字段。
当 HTML 页面中的复选框未被选中时,在表单提交后,其值不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要针对 HTML 的这一特性采取变通方法,以确保 Spring 表单数据绑定能够正常工作。
checkbox 标签遵循了 Spring 现有的约定:为每个复选框包含一个以下划线(_)为前缀的隐藏参数。通过这种方式,你实际上是告诉 Spring:“该复选框在表单中是可见的,我希望表单数据绑定的目标对象能够反映该复选框的状态,无论其是否被选中。”
这checkboxes标签
此标签会渲染多个 HTML input 标签,并将 type 设置为 checkbox。
本节内容基于前一节 checkbox 标签示例。有时,您可能不希望在 JSP 页面中列出所有可能的兴趣爱好,而更愿意在运行时提供一个可用选项的列表,并将其传递给标签。这正是 checkboxes 标签的作用。您可以在 Array 属性中传入一个包含可用选项的 List、Map 或 items。通常情况下,绑定的属性是一个集合,以便能够保存用户选择的多个值。以下示例展示了一个使用该标签的 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 条目的键将用作值,而 map 条目的值将用作要显示的标签。
您也可以使用自定义对象,并通过 itemValue 指定用作值的属性名,
通过 itemLabel 指定用作显示标签的属性名。
这radiobutton标签
此标签渲染一个 HTML input 元素,并将其 type 属性设置为 radio。
一种典型的使用模式涉及多个标签实例绑定到同一个属性,但具有不同的值,如下例所示:
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
这radiobuttons标签
此标签会渲染多个 HTML input 元素,并将 type 属性设置为 radio。
与 checkboxes 标签 类似,您可能希望将可用选项作为运行时变量传入。对于这种用法,您可以使用 radiobuttons 标签。您需要传入一个包含可用选项的 Array、List 或 Map,这些选项位于其 items 属性中。如果您使用 Map,则映射条目的键将用作值,而映射条目的值将用作要显示的标签。您还可以使用自定义对象,通过 itemValue 指定值对应的属性名,并通过 itemLabel 指定标签对应的属性名,如下例所示:
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
这password标签
此标签会渲染一个 HTML input 标签,其类型设置为 password,并绑定相应的值。
<tr>
<td>Password:</td>
<td>
<form:password path="password"/>
</td>
</tr>
请注意,默认情况下密码值不会显示。如果您希望显示密码值,可以将 showPassword 属性的值设置为 true,如下例所示:
<tr>
<td>Password:</td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
</td>
</tr>
这select标签
此标签用于渲染一个 HTML 'select' 元素。它支持将所选选项进行数据绑定,并支持嵌套使用 option 和 options 标签。
假设一个 User 拥有一个技能列表。对应的 HTML 可能如下所示:
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果User’s的技能是草药学,那么“技能”行的 HTML 源代码可能如下所示:
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
这option标签
此标签用于渲染一个 HTML option 元素。它会根据绑定的值设置 selected 属性。以下 HTML 展示了其典型的输出结果:
<tr>
<td>House:</td>
<td>
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
</form:select>
</td>
</tr>
如果User’s的学院是格兰芬多,那么“学院”行的 HTML 源代码将如下所示:
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
| 1 | 注意添加了一个 selected 属性。 |
这options标签
此标签会渲染一个 HTML option 元素列表。它会根据绑定的值设置 selected 属性。以下 HTML 展示了其典型的输出结果:
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>
如果User居住在英国,'Country'行的HTML源代码将如下所示:
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> (1)
<option value="US">United States</option>
</select>
</td>
</tr>
| 1 | 注意添加了一个 selected 属性。 |
如上例所示,option 标签与 options 标签结合使用时,会生成相同的标准 HTML,但允许您在 JSP 中显式指定一个仅用于显示的值(该值应属于展示层),例如示例中的默认字符串:“-- 请选择”。
items 属性通常由一个集合或数组形式的项目对象填充。
如果指定了 itemValue 和 itemLabel,它们将引用这些项目对象的 bean 属性;
否则,项目对象本身将被转换为字符串。此外,你也可以指定一个 Map 类型的 items,
在这种情况下,map 的键将被解释为选项值(option values),而 map 的值则对应于选项标签(option labels)。
如果同时指定了 itemValue 或 itemLabel(或两者都指定),
那么 itemValue 属性将作用于 map 的键,而 itemLabel 属性将作用于 map 的值。
这textarea标签
此标签用于渲染一个 HTML textarea 元素。以下 HTML 展示了其典型的输出结果:
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
这hidden标签
此标签会渲染一个 HTML input 标签,其 type 属性设置为 hidden,并绑定相应的值。若要提交一个未绑定的隐藏值,请直接使用 HTML 的 input 标签,并将 type 设置为 hidden。
以下 HTML 展示了该标签典型的输出结果:
<form:hidden path="house"/>
如果我们选择将 house 值作为隐藏字段提交,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-form.tld 标签库描述符(TLD)包含在 spring-webmvc.jar 中。
有关各个标签的完整参考,请浏览
API 参考文档
或查看标签库描述。
HTTP 方法转换
REST 的一个关键原则是使用“统一接口”。这意味着所有资源(URL)都可以使用相同的四种 HTTP 方法进行操作:GET、PUT、POST 和 DELETE。对于每个方法,HTTP 规范都定义了其确切的语义。例如,GET 请求应当始终是安全的操作,即它不应产生任何副作用;而 PUT 或 DELETE 请求应当是幂等的,即你可以反复执行这些操作,但最终结果应保持一致。虽然 HTTP 定义了这四种方法,但 HTML 仅支持其中两种:GET 和 POST。幸运的是,有两种可能的解决方法:你可以使用 JavaScript 来执行 PUT 或 DELETE 请求,或者也可以通过 POST 请求,并将“真实”的 HTTP 方法作为额外参数传递(在 HTML 表单中以隐藏输入字段的形式实现)。Spring 的 HiddenHttpMethodFilter 使用了后一种技巧。此过滤器是一个普通的 Servlet 过滤器,因此它可以与任何 Web 框架结合使用(不仅限于 Spring MVC)。将此过滤器添加到您的 Web 应用中。xml, 和一个隐藏的 method 参数被转换成相应的HTTP方法请求。
为了支持 HTTP 方法转换,Spring MVC 表单标签已更新以支持设置 HTTP 方法。例如,以下代码片段来自 Pet Clinic 示例:
<form:form method="delete">
<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>
前面的示例执行了一个 HTTP POST 请求,其中“真正的” DELETE 方法通过一个请求参数隐藏起来。该参数会被在 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
您可以将 Tiles(就像其他任何视图技术一样)集成到使用 Spring 的 Web 应用程序中。本节将大致介绍如何进行此类集成。
本节重点介绍 Spring 在 org.springframework.web.servlet.view.tiles3 包中对 Tiles 3 版本的支持。 |
依赖项
要使用 Tiles,您必须在项目中添加对 Tiles 3.0.1 或更高版本的依赖,以及其传递依赖项。
配置
要使用 Tiles,您必须通过包含定义(definition)的文件对其进行配置
(有关定义及其他 Tiles 概念的基本信息,请参见
https://tiles.apache.org)。在 Spring 中,这是通过使用 TilesConfigurer 来完成的。
以下示例中的 ApplicationContext 配置展示了如何进行此项配置:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
</bean>
前面的示例定义了五个包含定义的文件。这些文件都位于 WEB-INF/defs 目录中。在初始化 WebApplicationContext 时,这些文件会被加载,并初始化定义工厂。完成此操作后,定义文件中包含的 Tiles 即可在您的 Spring Web 应用程序中作为视图使用。要使用这些视图,您需要像使用 Spring 中其他任何视图技术一样配置一个 ViewResolver。您可以使用两种实现中的任意一种:UrlBasedViewResolver 或 ResourceBundleViewResolver。
您可以通过添加下划线,然后加上区域设置(locale),来指定特定于区域的 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。
| 由于下划线用于表示区域设置(locales),我们建议不要在 Tiles 定义的文件名中使用下划线。 |
UrlBasedViewResolver
UrlBasedViewResolver 会为其需要解析的每个视图实例化指定的 viewClass。以下 bean 定义了一个 UrlBasedViewResolver:
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
</bean>
ResourceBundleViewResolver
必须为 ResourceBundleViewResolver 提供一个属性文件,其中包含解析器可使用的视图名称和视图类。以下示例展示了一个 ResourceBundleViewResolver 的 Bean 定义,以及相应的视图名称和视图类(摘自 Pet Clinic 示例):
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
...
welcomeView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
welcomeView.url=welcome (this is the name of a Tiles definition)
vetsView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
vetsView.url=vetsView (again, this is the name of a Tiles definition)
findOwnersForm.(class)=org.springframework.web.servlet.view.JstlView
findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
...
当你使用 ResourceBundleViewResolver 时,可以轻松混合不同的视图技术。
请注意,TilesView 类支持 JSTL(JSP 标准标签库)。
SimpleSpringPreparerFactory和SpringBeanPreparerFactory
作为一项高级功能,Spring 还支持两种特殊的 Tiles PreparerFactory 实现。有关如何在 Tiles 定义文件中使用 ViewPreparer 引用的详细信息,请参阅 Tiles 文档。
您可以指定 SimpleSpringPreparerFactory,根据所指定的 preparer 类自动装配 ViewPreparer 实例,同时应用 Spring 容器的回调方法以及已配置的 Spring BeanPostProcessor。如果已启用 Spring 上下文范围的注解配置,则 ViewPreparer 类中的注解将被自动检测并应用。请注意,这要求在 Tiles 定义文件中使用 preparer 类,与默认的 PreparerFactory 行为一致。
你可以指定 SpringBeanPreparerFactory,使其根据指定的 preparer 名称(而非类)从 DispatcherServlet 的应用上下文中获取对应的 Spring Bean。在这种情况下,完整的 Bean 创建过程由 Spring 应用上下文控制,从而允许使用显式的依赖注入配置、作用域 Bean 等功能。请注意,你需要为每个 preparer 名称(即在 Tiles 定义中使用的名称)定义一个 Spring Bean 定义。以下示例展示了如何在 SpringBeanPreparerFactory Bean 上定义 TilesConfigurer 属性:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
<!-- resolving preparer names as Spring bean definition names -->
<property name="preparerFactoryClass"
value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>
</bean>
1.10.7. RSS 和 Atom
AbstractAtomFeedView 和 AbstractRssFeedView 均继承自基类
AbstractFeedView,分别用于提供 Atom 和 RSS Feed 视图。它们基于 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 请求,以便在需要时访问 Locale。HTTP 响应仅用于设置 Cookie 或其他 HTTP 头信息。方法返回后,feed 会自动写入响应对象。
有关创建 Atom 视图的示例,请参见 Alef Arendsen 在 Spring 团队博客上的文章。
1.10.8. PDF 和 Excel
Spring 提供了返回非 HTML 格式输出的方式,包括 PDF 和 Excel 电子表格。 本节将介绍如何使用这些功能。
文档视图介绍
HTML 页面并不总是用户查看模型输出的最佳方式,而 Spring 使得从模型数据动态生成 PDF 文档或 Excel 电子表格变得非常简单。该文档即为视图,并以正确的 MIME 类型从服务器流式传输,(希望)能够使客户端计算机自动启动其电子表格或 PDF 查看器应用程序来响应。
为了使用 Excel 视图,您需要将 Apache POI 库添加到您的类路径中。 对于 PDF 生成,您需要(最好)添加 OpenPDF 库。
| 如果可能,请使用底层文档生成库的最新版本。特别是,我们强烈推荐使用 OpenPDF(例如 OpenPDF 1.2.12),而不是过时的原始 iText 2.1.7,因为 OpenPDF 目前仍在积极维护,并修复了针对不可信 PDF 内容的一个重要安全漏洞。 |
PDF 视图
一个用于单词列表的简单 PDF 视图可以继承
org.springframework.web.servlet.view.document.AbstractPdfView 并实现
buildPdfDocument() 方法,如下例所示:
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
class PdfWordList : AbstractPdfView() {
override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
request: HttpServletRequest, response: HttpServletResponse) {
val words = model["wordList"] as List<String>
for (word in words) {
doc.add(Paragraph(word))
}
}
}
控制器可以从外部视图定义(通过名称引用)或从处理方法中以 View 实例的形式返回此类视图。
Excel 视图
自 Spring Framework 4.2 起,
org.springframework.web.servlet.view.document.AbstractXlsView 被提供作为 Excel 视图的基类。
它基于 Apache POI,并提供了专门的子类(AbstractXlsxView
和 AbstractXlsxStreamingView),用以取代过时的 AbstractExcelView 类。
该编程模型类似于 AbstractPdfView,以 buildExcelDocument() 作为核心模板方法,控制器可以从外部定义(按名称)返回此类视图,或从处理方法中直接返回一个 View 实例。
1.10.9. Jackson
Spring 提供对 Jackson JSON 库的支持。
基于 Jackson 的 JSON MVC 视图
MappingJackson2JsonView 使用 Jackson 库的 ObjectMapper 将响应内容渲染为 JSON。默认情况下,模型映射(model map)中的全部内容(框架特定的类除外)都会被编码为 JSON。当需要对映射中的内容进行过滤时,可以通过 modelKeys 属性指定要编码的一组特定模型属性。此外,还可以使用 extractValueFromSingleKeyModel 属性,使单键模型中的值被直接提取并序列化,而不是作为模型属性的映射进行序列化。
你可以使用 Jackson 提供的注解按需自定义 JSON 映射。当你需要更精细的控制时,可以通过 ObjectMapper 属性注入一个自定义的 ObjectMapper,以便为特定类型提供自定义的 JSON 序列化器和反序列化器。
基于 Jackson 的 XML 视图
MappingJackson2XmlView 使用
Jackson XML 扩展 的 XmlMapper
将响应内容渲染为 XML。如果模型包含多个条目,您应通过使用 modelKey bean 属性显式指定要序列化的对象。如果模型仅包含一个条目,则会自动进行序列化。
你可以根据需要使用 JAXB 或 Jackson 提供的注解来自定义 XML 映射。当你需要更精细的控制时,可以通过 XmlMapper 属性注入一个自定义的 ObjectMapper,以便为特定类型提供自定义的序列化器和反序列化器。
1.10.10. XML 编组
MarshallingView 使用一个 XML Marshaller(定义在 org.springframework.oxm 包中)将响应内容渲染为 XML。你可以通过设置 MarshallingView 实例的 modelKey bean 属性,显式指定要进行序列化的对象。或者,该视图会遍历所有模型属性,并对第一个被 Marshaller 支持的类型进行序列化。有关 org.springframework.oxm 包中功能的更多信息,请参阅使用 O/X 映射器进行 XML 序列化。
1.10.11. XSLT 视图
XSLT 是一种用于 XML 的转换语言,在 Web 应用程序中作为视图技术广受欢迎。如果你的应用程序天然处理 XML,或者你的模型可以轻松转换为 XML,那么 XSLT 可以作为视图技术的一个不错选择。以下章节展示了如何在 Spring Web MVC 应用程序中生成 XML 文档作为模型数据,并使用 XSLT 对其进行转换。
这个示例是一个简单的 Spring 应用程序,它在 Controller 中创建一个单词列表,并将其添加到模型映射(model map)中。该映射连同 XSLT 视图的视图名称一起被返回。有关 Spring Web MVC 的 #mvc-controller 接口的详细信息,请参见注解式控制器。XSLT 控制器会将单词列表转换为一个简单的 XML 文档,以供后续转换使用。
Bean
配置对于一个简单的 Spring Web 应用程序来说是标准的: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
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
import org.springframework.ui.set
@Controller
class XsltController {
@RequestMapping("/")
fun home(model: Model): String {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val root = document.createElement("wordList")
val words = listOf("Hello", "Spring", "Framework")
for (word in words) {
val wordNode = document.createElement("word")
val textNode = document.createTextNode(word)
wordNode.appendChild(textNode)
root.appendChild(wordNode)
}
model["wordList"] = root
return "home"
}
}
到目前为止,我们仅创建了一个 DOM 文档并将其添加到了 Model 映射中。请注意,您也可以将一个 XML 文件作为 Resource 加载,并用它来替代自定义的 DOM 文档。
有一些可用的软件包可以自动将对象图“转换为DOM”(domify),但在Spring中,你可以完全灵活地以自己选择的任何方式从模型创建DOM。这样可以避免XML转换过程过多地影响模型数据的结构,而当你使用工具来管理DOM转换过程时,这种影响正是一种潜在的风险。
转换
最后,XsltViewResolver 会解析名为“home”的 XSLT 模板文件,并将 DOM 文档合并到该模板中以生成我们的视图。如 XsltViewResolver 的配置所示,XSLT 模板位于 war 文件的 WEB-INF/xsl 目录下,并以 xslt 作为文件扩展名。
以下示例展示了一个 XSLT 转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
上述转换将渲染为以下 HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
1.11. MVC 配置
MVC Java 配置和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,以及用于自定义该配置的配置 API。
对于配置 API 中不可用的更高级自定义,请参阅高级 Java 配置和高级 XML 配置。
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 模式,或使用您 IDE 的代码自动补全功能来发现有哪些可用的属性和子元素。
1.11.3. 类型转换
默认情况下,系统会安装各种数字和日期类型的格式化器,并支持通过在字段上使用 @NumberFormat 和 @DateTimeFormat 进行自定义。
要在 Java 配置中注册自定义的格式化器(formatters)和转换器(converters),请使用以下方式:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
要在 XML 配置中实现相同的功能,请使用以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
</beans>
默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求的区域设置(Locale)。这适用于表单中使用 "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)
}
}
请参阅 FormatterRegistrar SPI
以及 FormattingConversionServiceFactoryBean 以获取更多关于何时使用 FormatterRegistrar 实现的信息。 |
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 配置中声明的 bean 发生冲突。 |
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/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(LocaleChangeInterceptor())
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*")
}
}
以下示例展示了如何在 XML 中实现相同的配置:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/secure/*"/>
<bean class="org.example.SecurityInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
1.11.6. 内容类型
您可以配置 Spring MVC 如何从请求中确定所请求的媒体类型
(例如,Accept 请求头、URL 路径扩展名、查询参数等)。
默认情况下,首先检查 URL 路径扩展名——其中 json、xml、rss 和 atom 已注册为已知扩展名(具体取决于类路径依赖项)。其次检查 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. 消息转换器
您可以通过重写 configureMessageConverters()(以替换 Spring MVC 创建的默认转换器)或重写 extendMessageConverters()(以自定义默认转换器或向默认转换器添加额外的转换器),在 Java 配置中自定义 HttpMessageConverter。
以下示例添加了 XML 和 Jackson JSON 转换器,并使用自定义的 ObjectMapper,而不是默认的转换器:
@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 创建通用配置,启用了缩进、自定义日期格式,
并注册了
jackson-module-parameter-names,
从而添加了访问参数名的支持(这是 Java 8 中新增的功能)。
该构建器对 Jackson 的默认属性进行了如下自定义:
如果在类路径中检测到以下知名模块,它还会自动注册这些模块:
-
jackson-datatype-joda:对 Joda-Time 类型的支持。
-
jackson-datatype-jsr310:支持 Java 8 日期和时间 API 类型。
-
jackson-datatype-jdk8:支持其他 Java 8 类型,例如
Optional。 -
jackson-module-kotlin:支持 Kotlin 类和数据类。
启用带有 Jackson XML 支持的缩进功能,除了需要 jackson-dataformat-xml 依赖项外,还需要 woodstox-core-asl 依赖项。 |
还有其他有趣的 Jackson 模块可用:
-
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 的快捷方式,该控制器在被调用时会立即转发到一个视图。你可以在静态场景中使用它,即在视图生成响应之前无需执行任何 Java 控制器逻辑的情况。
以下 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")
}
}
以下示例通过使用 <mvc:view-controller> 元素,以 XML 的方式实现了与前述示例相同的功能:
<mvc:view-controller path="/" view-name="home"/>
如果一个 @RequestMapping 方法被映射到某个 URL(无论使用何种 HTTP 方法),则不能使用视图控制器(view controller)来处理相同的 URL。这是因为通过 URL 匹配到带注解的控制器(annotated controller)被视为足够明确的端点归属指示,从而可以向客户端返回 405(METHOD_NOT_ALLOWED)、415(UNSUPPORTED_MEDIA_TYPE)或类似的响应,以协助调试。因此,建议避免将同一 URL 的处理逻辑拆分到带注解的控制器和视图控制器之间。
1.11.9. 视图解析器
MVC 配置简化了视图解析器的注册。
以下 Java 配置示例通过使用 JSP 和 Jackson 来配置内容协商视图解析,其中 Jackson 作为 JSON 渲染的默认 View:
@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 应用程序根目录下的 /public 目录或类路径下的 /static 目录中查找并提供静态资源。这些资源设置了为期一年的未来过期时间,以确保最大限度地利用浏览器缓存,并减少浏览器发起的 HTTP 请求次数。同时还会检查 Last-Modified 响应头,如果存在该头信息,则返回 304 状态码。
以下示例展示了如何通过 Java 配置实现这一点:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926)
}
}
以下示例展示了如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**"
location="/public, classpath:/static/"
cache-period="31556926" />
另请参阅 静态资源的 HTTP 缓存支持。
资源处理器还支持一系列
ResourceResolver 实现和
ResourceTransformer 实现,
您可以利用它们构建工具链,以处理优化后的资源。
您可以使用 VersionResourceResolver 来基于内容计算出的 MD5 哈希值、固定的应用程序版本或其他方式生成带版本的资源 URL。
ContentVersionStrategy(MD5 哈希)是一个不错的选择——但有一些显著的例外情况,例如与模块加载器一起使用的 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 等模板引擎透明化,这些模板引擎的 URL 标签依赖于 HttpServletResponse#encodeURL。
请注意,当同时使用 EncodedResourceResolver(例如,用于提供 gzip 或 brotli 编码的资源)和 VersionResourceResolver 时,必须按此顺序注册它们。
这样才能确保基于内容的版本始终可靠地根据未编码的文件进行计算。
WebJars 也通过 WebJarsResourceResolver 得到支持,当类路径中存在 org.webjars:webjars-locator-core 库时,该解析器会自动注册。该解析器可以重写 URL 以包含 JAR 文件的版本号,也可以匹配不带版本号的传入 URL——例如,将 /jquery/jquery.min.js 映射到 /jquery/1.2.0/jquery.min.js。
1.11.11. 默认 Servlet
Spring MVC 允许将 DispatcherServlet 映射到 /(从而覆盖容器默认 Servlet 的映射),同时仍然允许静态资源请求由容器的默认 Servlet 处理。它会配置一个 DefaultServletHttpRequestHandler,其 URL 映射为 /**,并且相对于其他 URL 映射具有最低的优先级。
此处理器将所有请求转发给默认的 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 和 WebSphere)。
如果默认 Servlet 已被自定义配置为其他名称,或者正在使用一个未知默认 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
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
@Bean
public UrlPathHelper urlPathHelper() {
//...
}
@Bean
public PathMatcher antPathMatcher() {
//...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
}
@Bean
fun urlPathHelper(): UrlPathHelper {
//...
}
@Bean
fun antPathMatcher(): PathMatcher {
//...
}
}
以下示例展示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:path-matching
trailing-slash="false"
registered-suffixes-only="true"
path-helper="pathHelper"
path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
1.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 的属性,而该属性又无法通过其他方式修改,你可以使用 Spring BeanPostProcessor 的 ApplicationContext 生命周期钩子,如下例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
@Component
class MyPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
// ...
}
}
请注意,您需要将 MyPostProcessor 声明为一个 bean,可以通过在 XML 中显式声明,也可以通过 <component-scan/> 声明让其被自动检测到。
1.12. HTTP/2
Servlet 4 容器需要支持 HTTP/2,而 Spring Framework 5 兼容 Servlet API 4。从编程模型的角度来看,应用程序无需进行任何特殊操作。然而,仍需考虑与服务器配置相关的问题。更多详情,请参阅 HTTP/2 Wiki 页面。
Servlet API 确实公开了一个与 HTTP/2 相关的构造。您可以使用 javax.servlet.http.PushBuilder 主动将资源推送给客户端,并且它被支持作为 #mvc-ann-arguments 方法的方法参数。
2. REST 客户端
本节介绍用于客户端访问 REST 端点的选项。
2.1. RestTemplate
RestTemplate 是一个用于执行 HTTP 请求的同步客户端。它是 Spring 最初的 REST 客户端,基于底层 HTTP 客户端库提供了一个简单、基于模板方法的 API。
从 5.0 版本起,RestTemplate 已进入维护模式,今后仅接受对小范围变更和 bug 修复的请求。请考虑使用
WebClient,它提供了更现代化的 API,并支持同步、异步和流式处理场景。 |
有关详细信息,请参见REST 端点。
2.2. WebClient
WebClient 是一个非阻塞的响应式客户端,用于执行 HTTP 请求。它在 5.0 版本中引入,为 RestTemplate 提供了一种现代化的替代方案,能够高效地支持同步、异步以及流式处理场景。
与 RestTemplate 相比,WebClient 支持以下功能:
-
非阻塞 I/O。
-
响应式流背压。
-
使用更少的硬件资源实现高并发。
-
利用 Java 8 Lambda 表达式的函数式、流畅 API。
-
同步与异步交互。
-
向服务器上传流或从服务器下载流。
有关更多详情,请参见WebClient。
3. 测试
本节总结了在 Spring MVC 应用程序中 spring-test 所提供的可用选项。
-
Servlet API 模拟对象:用于对控制器、过滤器及其他 Web 组件进行单元测试的 Servlet API 接口的模拟实现。更多详情请参见 Servlet API 模拟对象。
-
测试上下文(TestContext)框架:支持在 JUnit 和 TestNG 测试中加载 Spring 配置, 包括在测试方法之间高效缓存已加载的配置,以及支持使用
WebApplicationContext加载MockServletContext。 更多详情请参见 TestContext 框架。 -
Spring MVC Test:一个用于测试带注解控制器的框架,也称为
MockMvc,通过DispatcherServlet(即支持注解)进行测试,具备完整的 Spring MVC 基础设施,但无需 HTTP 服务器。 更多详情请参见 Spring MVC Test。 -
客户端 REST:
spring-test提供了一个MockRestServiceServer,你可以将其用作模拟服务器,以测试内部使用RestTemplate的客户端代码。 更多详情请参见客户端 REST 测试。 -
WebTestClient:专为测试 WebFlux 应用程序而构建,但也可用于通过 HTTP 连接对任意服务器进行端到端集成测试。它是一个非阻塞的响应式客户端,非常适合测试异步和流式场景。
4. WebSockets
本参考文档的这一部分涵盖了对 Servlet 栈的支持,以及 WebSocket 消息传递,包括原始的 WebSocket 交互、通过 SockJS 实现的 WebSocket 仿真,以及在 WebSocket 之上使用 STOMP 子协议进行的发布-订阅消息传递。
4.1. WebSocket 简介
WebSocket 协议(RFC 6455)提供了一种标准化的方式,可在客户端与服务器之间通过单一 TCP 连接建立全双工、双向通信通道。它是一种不同于 HTTP 的 TCP 协议,但被设计为可在 HTTP 上运行,使用 80 和 443 端口,并允许复用现有的防火墙规则。
WebSocket 交互始于一个使用 HTTP Upgrade 头的 HTTP 请求,
以升级(或在此情况下切换)到 WebSocket 协议。以下示例
展示了此类交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
| 1 | Upgrade 头部。 |
| 2 | 使用 Upgrade 连接。 |
与通常的 200 状态码不同,支持 WebSocket 的服务器会返回类似如下的输出:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
| 1 | 协议切换 |
握手成功后,HTTP 升级请求底层的 TCP 套接字将保持打开状态,供客户端和服务器继续发送和接收消息。
本文档的范围不包括对 WebSocket 工作原理的完整介绍。 有关详细信息,请参阅 RFC 6455、HTML5 中的 WebSocket 章节,或网上众多的入门指南和教程。
请注意,如果 WebSocket 服务器运行在 Web 服务器(例如 nginx)之后,您很可能需要配置该 Web 服务器,使其将 WebSocket 升级请求转发给 WebSocket 服务器。同样,如果应用程序运行在云环境中,请查阅云服务提供商关于 WebSocket 支持的相关说明。
4.1.1. HTTP 与 WebSocket 对比
尽管 WebSocket 被设计为与 HTTP 兼容,并以 HTTP 请求开始, 但理解这两种协议会导致截然不同的架构和应用程序编程模型非常重要。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。客户端通过访问这些 URL,以请求-响应的方式与应用程序进行交互。服务器根据 HTTP URL、方法和请求头,将请求路由到相应的处理器。
相比之下,在 WebSocket 中,通常只有一个用于初始连接的 URL。 随后,所有应用消息都通过该相同的 TCP 连接进行传输。这指向了一种完全不同的异步、事件驱动的消息架构。
WebSocket 也是一种低层传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息的语义达成一致,否则无法对消息进行路由或处理。
WebSocket 客户端和服务器可以通过 HTTP 握手请求中的 Sec-WebSocket-Protocol 头协商使用更高层的消息协议(例如 STOMP)。如果没有该头信息,它们就需要自行约定通信规范。
4.1.2. 何时使用 WebSocket
WebSocket 可以使网页变得动态且具有交互性。然而,在许多情况下,结合使用 Ajax 与 HTTP 流式传输或长轮询即可提供一种简单而有效的解决方案。
例如,新闻、邮件和社交动态需要动态更新,但每隔几分钟更新一次可能就完全足够了。而协作应用、游戏和金融类应用则需要更接近实时的更新。
仅延迟本身并不是一个决定性因素。如果消息量相对较低(例如,监控网络故障),HTTP 流式传输或轮询可以提供一种有效的解决方案。 只有在低延迟、高频率和高吞吐量三者结合的情况下,才最能体现使用 WebSocket 的优势。
还需注意的是,在互联网上,您无法控制的限制性代理可能会阻止 WebSocket 通信,原因可能是它们未配置为传递 Upgrade 头部,或者因为它们会关闭看似空闲的长连接。这意味着,对于防火墙内部的应用程序而言,使用 WebSocket 是一个更为直接明确的选择,而对于面向公众的应用程序则不然。
4.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,可用于编写处理 WebSocket 消息的客户端和服务器端应用程序。
4.2.1. WebSocketHandler
创建一个 WebSocket 服务器非常简单,只需实现 WebSocketHandler,或者更常见的是,继承 TextWebSocketHandler 或 BinaryWebSocketHandler。以下示例使用了 TextWebSocketHandler:
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
有专门的 WebSocket Java 配置和 XML 命名空间支持,用于将上述 WebSocket 处理器映射到特定的 URL,如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
上述示例适用于 Spring MVC 应用程序,应包含在 DispatcherServlet 的配置中。然而,Spring 的 WebSocket 支持并不依赖于 Spring MVC。借助 WebSocketHttpRequestHandler,将 WebSocketHandler 集成到其他 HTTP 服务环境中相对简单。
当直接使用 WebSocketHandler API 而非间接使用(例如通过 STOMP 消息传递)时,应用程序必须同步消息的发送,因为底层的标准 WebSocket 会话(JSR-356)不允许并发发送。一种选择是将 WebSocketSession 包装在 ConcurrentWebSocketSessionDecorator 中。
4.2.2. WebSocket 握手
自定义初始 HTTP WebSocket 握手请求最简单的方法是通过一个 HandshakeInterceptor,它提供了“握手前”和“握手后”的方法。
您可以使用此类拦截器来阻止握手,或者将任意属性传递给 WebSocketSession。
以下示例使用了一个内置拦截器,将 HTTP 会话属性传递到 WebSocket 会话中:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
一种更高级的选项是扩展 DefaultHandshakeHandler,该处理器负责执行 WebSocket 握手的各个步骤,包括验证客户端来源、协商子协议以及其他细节。如果应用程序需要配置自定义的 RequestUpgradeStrategy,以适配尚未被支持的 WebSocket 服务器引擎和版本,也可能需要使用此选项(有关此主题的更多内容,请参见部署)。Java 配置和 XML 命名空间都支持配置自定义的 HandshakeHandler。
Spring 提供了一个 WebSocketHandlerDecorator 基类,可用于为 WebSocketHandler 添加额外的行为。日志记录和异常处理的实现已提供,并在使用 WebSocket 的 Java 配置或 XML 命名空间时默认添加。ExceptionWebSocketHandlerDecorator 会捕获所有从任意 WebSocketHandler 方法中抛出的未捕获异常,并以状态码 1011(表示服务器错误)关闭 WebSocket 会话。 |
4.2.3. 部署
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中 DispatcherServlet 同时处理 HTTP WebSocket 握手和其他 HTTP 请求。通过调用 WebSocketHttpRequestHandler,也很容易将其集成到其他 HTTP 处理场景中。这种方式既方便又易于理解。然而,在 JSR-356 运行时环境下,需要考虑一些特殊事项。
Java WebSocket API(JSR-356)提供了两种部署机制。第一种是在启动时通过 Servlet 容器进行类路径扫描(Servlet 3 的特性)。另一种是在 Servlet 容器初始化时使用的注册 API。这两种机制都无法使用单一的“前端控制器”来处理所有 HTTP 请求——包括 WebSocket 握手以及所有其他 HTTP 请求——例如 Spring MVC 中的 DispatcherServlet。
这是 JSR-356 的一个重大限制,Spring 的 WebSocket 支持通过服务器特定的 RequestUpgradeStrategy 实现来解决该问题,即使在 JSR-356 运行时环境中也是如此。目前,Tomcat、Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及 WildFly)都已有相应的策略实现。
| 已提交一项请求,旨在克服 Java WebSocket API 中前述的限制, 可在 eclipse-ee4j/websocket-api#211 跟踪该请求的进展。 Tomcat、Undertow 和 WebSphere 均提供了各自的替代 API, 使得实现此功能成为可能,Jetty 同样也支持该功能。我们希望 更多的服务器也能采取类似的做法。 |
另一个次要的考虑因素是,支持 JSR-356 的 Servlet 容器会执行 ServletContainerInitializer(SCI)扫描,这可能会减慢应用程序的启动速度——在某些情况下,甚至会显著降低。如果在升级到支持 JSR-356 的 Servlet 容器版本后观察到明显的性能影响,则应可通过在 <absolute-ordering /> 中使用 web.xml 元素,有选择地启用或禁用 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、WildFly 和 GlassFish,您可以在 WebSocket 的 Java 配置中添加一个 ServletServerContainerFactoryBean,如下例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
对于客户端 WebSocket 配置,应使用 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. 允许的源站
从 Spring Framework 4.1.5 起,WebSocket 和 SockJS 的默认行为是仅接受同源请求。也可以配置为允许所有来源或指定的来源列表。
此检查主要是为浏览器客户端设计的。其他类型的客户端仍然可以随意修改 Origin 请求头的值(更多详情请参见
RFC 6454:Web 源概念)。
三种可能的行为是:
-
仅允许同源请求(默认):在此模式下,当启用 SockJS 时,Iframe 的 HTTP 响应头
X-Frame-Options会被设置为SAMEORIGIN,同时 JSONP 传输方式将被禁用,因为它无法检查请求的来源。 因此,启用此模式后,IE6 和 IE7 将不再受支持。 -
允许指定的来源列表:每个允许的来源必须以
http://或https://开头。在此模式下,当启用 SockJS 时,IFrame 传输将被禁用。 因此,启用此模式后,IE6 至 IE9 将不再受支持。 -
允许所有源:要启用此模式,您应将
*作为允许的源值提供。在此模式下,所有传输方式均可用。
您可以配置 WebSocket 和 SockJS 的允许来源(allowed origins),如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
4.3. SockJS 回退方案
在公共互联网上,您无法控制的限制性代理可能会阻止 WebSocket 通信,原因可能是它们未配置为传递 Upgrade 头部,或者因为它们会关闭看似空闲的长连接。
解决此问题的方法是 WebSocket 仿真——即首先尝试使用 WebSocket,如果不可用,则回退到基于 HTTP 的技术,这些技术可以模拟 WebSocket 交互,并暴露相同的应用层 API。
在 Servlet 栈上,Spring Framework 为 SockJS 协议提供了服务器端(以及客户端)支持。
4.3.1. 概述
SockJS 的目标是让应用程序能够使用 WebSocket API,但在运行时必要时可回退到非 WebSocket 的替代方案,而无需更改应用程序代码。
SockJS 包含以下部分:
-
SockJS JavaScript 客户端 —— 一个用于浏览器的客户端库。
-
SockJS 服务器端实现,包括 Spring Framework
spring-websocket模块中的一种实现。 -
spring-websocket模块中提供的 SockJS Java 客户端(自 4.1 版本起)。
SockJS 专为在浏览器中使用而设计。它采用多种技术,以支持广泛版本的浏览器。 有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS 客户端页面。传输方式 大致可分为三类:WebSocket、HTTP 流(HTTP Streaming)和 HTTP 长轮询(HTTP Long Polling)。 有关这些类别的概述,请参见 这篇博客文章。
SockJS 客户端首先发送 GET /info 请求,
从服务器获取基本信息。之后,它必须决定使用哪种传输方式。
如果可能,将使用 WebSocket。如果不可用,在大多数浏览器中,
至少还有一种 HTTP 流式传输选项。如果仍不可用,则使用 HTTP(长)轮询。
所有传输请求都具有以下 URL 结构:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
其中:
-
{server-id}对于在集群中路由请求很有用,但除此之外不会被使用。 -
{session-id}用于关联属于同一个 SockJS 会话的 HTTP 请求。 -
{transport}表示传输类型(例如websocket、xhr-streaming等)。
WebSocket 传输仅需一次 HTTP 请求即可完成 WebSocket 握手。 此后所有消息均通过该套接字进行交换。
HTTP 传输需要更多的请求。例如,Ajax/XHR 流式传输依赖一个长时间运行的请求来实现服务器到客户端的消息传递,同时还需要额外的 HTTP POST 请求来实现客户端到服务器的消息传递。长轮询与此类似,不同之处在于它在每次服务器向客户端发送消息后都会结束当前请求。
SockJS 添加了最小的消息帧格式。例如,服务器最初会发送字母 o(“打开”帧),消息以 a["message1","message2"](JSON 编码的数组)形式发送,如果 25 秒内(默认值)没有消息传输,则发送字母 h(“心跳”帧),并在关闭会话时发送字母 c(“关闭”帧)。
要了解更多内容,请在浏览器中运行一个示例并观察 HTTP 请求。
SockJS 客户端允许固定传输方式列表,因此可以一次只查看一种传输方式。
SockJS 客户端还提供了一个调试标志(debug flag),可在浏览器控制台中启用有用的提示信息。
在服务器端,您可以为 TRACE 启用 org.springframework.web.socket 级别日志记录。
如需更详细的说明,请参阅 SockJS 协议的
带注释的测试。
4.3.2. 启用 SockJS
您可以通过 Java 配置启用 SockJS,如下例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
上述示例适用于 Spring MVC 应用程序,应包含在 DispatcherServlet 的配置中。然而,Spring 的 WebSocket 和 SockJS 支持并不依赖于 Spring MVC。借助 SockJsHttpRequestHandler,可以相对简单地将其集成到其他 HTTP 服务环境中。
在浏览器端,应用程序可以使用
sockjs-client(版本 1.0.x)。它模拟了 W3C WebSocket API,并根据运行它的浏览器与服务器通信以选择最佳传输选项。请参阅
sockjs-client 页面以及浏览器支持的传输类型列表。该客户端还提供了多种配置选项——例如,用于指定要包含哪些传输方式。
4.3.3. IE 8 和 9
Internet Explorer 8 和 9 仍在使用中,它们是采用 SockJS 的主要原因之一。本节介绍了在这些浏览器中运行时需要考虑的重要事项。
SockJS 客户端在 IE 8 和 IE 9 中通过利用微软的
XDomainRequest支持 Ajax/XHR 流式传输。
该方案支持跨域,但不支持发送 Cookie。
Cookie 对于 Java 应用程序而言通常是必不可少的。
然而,由于 SockJS 客户端可与多种服务器类型(不仅限于 Java)配合使用,因此需要判断 Cookie 是否重要。
如果需要 Cookie,SockJS 客户端将优先选择 Ajax/XHR 进行流式传输;否则,它将依赖基于 iframe 的技术。
SockJS 客户端发起的第一个 /info 请求用于获取信息,这些信息会影响客户端对传输方式的选择。
其中一个细节是服务器应用程序是否依赖 Cookie
(例如,用于身份验证或使用粘性会话进行集群)。
Spring 的 SockJS 支持包含一个名为 sessionCookieNeeded 的属性。
该属性默认启用,因为大多数 Java 应用程序都依赖 JSESSIONID
Cookie。如果你的应用程序不需要它,可以关闭此选项,
这样 SockJS 客户端在 IE 8 和 IE 9 中就会选择 xdr-streaming 传输方式。
如果你确实使用了基于 iframe 的传输方式,请注意,浏览器可以通过设置 HTTP 响应头 X-Frame-Options 为 DENY、SAMEORIGIN 或 ALLOW-FROM <origin> 来禁止在特定页面上使用 iframe。这是用于防止点击劫持(clickjacking)的措施。
|
Spring Security 3.2 及以上版本支持在每个响应中设置 |
如果你的应用程序添加了 X-Frame-Options 响应头(这是应该做的!)
并且依赖基于 iframe 的传输方式,那么你需要将该响应头的值设置为
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 客户端 页面。 |
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 秒一次),这意味着通常在该时间段内(或更早,如果消息发送更频繁的话)即可检测到客户端断开连接。
因此,可能会发生网络 I/O 故障,因为客户端已断开连接,这会导致日志中充斥着不必要的堆栈跟踪信息。Spring 会尽最大努力识别此类表示客户端断开连接的网络故障(针对每个服务器的具体情况),并通过专用的日志类别 DISCONNECTED_CLIENT_LOG_CATEGORY(定义在 AbstractSockJsSession 中)记录一条简短的消息。如果您需要查看堆栈跟踪信息,可以将该日志类别设置为 TRACE 级别。 |
4.3.6. SockJS 与 CORS
如果你允许跨域请求(参见允许的源),SockJS 协议会在 XHR 流式传输和轮询传输中使用 CORS 来支持跨域。因此,除非检测到响应中已存在 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 年)。
有关具体实现,请参见源代码中的 addCorsHeaders 类里的 AbstractSockJsService 方法以及 TransportType 枚举。
或者,如果 CORS 配置允许,可以考虑排除带有 SockJS 端点前缀的 URL,从而让 Spring 的 SockJsService 来处理它。
4.3.7. SockJsClient
Spring 提供了一个 SockJS Java 客户端,用于在不使用浏览器的情况下连接到远程的 SockJS 端点。这在需要通过公共网络(即网络代理可能阻止使用 WebSocket 协议的场景)实现两个服务器之间的双向通信时尤其有用。SockJS Java 客户端在测试场景中也非常有用(例如,用于模拟大量并发用户)。
SockJS Java 客户端支持 websocket、xhr-streaming 和 xhr-polling 传输方式。其余的传输方式仅适用于浏览器环境。
你可以通过以下方式配置 WebSocketTransport:
-
StandardWebSocketClient在 JSR-356 运行时中。 -
JettyWebSocketClient,使用 Jetty 9+ 原生 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,并且需要将其放在类路径(classpath)中。或者,你也可以配置一个自定义的 SockJsMessageCodec 实现,并将其设置到 SockJsClient 上。 |
要使用 SockJsClient 模拟大量并发用户,您需要配置底层的 HTTP 客户端(用于 XHR 传输),以允许足够数量的连接和线程。以下示例展示了如何在 Jetty 中进行此类配置:
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
以下示例展示了与服务器端 SockJS 相关的属性(详见 Javadoc), 您也应考虑对其进行自定义:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
| 1 | 将 streamBytesLimit 属性设置为 512KB(默认值为 128KB — 128 * 1024)。 |
| 2 | 将 httpMessageCacheSize 属性设置为 1,000(默认值为 100)。 |
| 3 | 将 disconnectDelay 属性设置为 30 秒(默认值为五秒 — 5 * 1000)。 |
4.4. STOMP
WebSocket 协议定义了两种类型的消息(文本和二进制),但其内容是未定义的。该协议定义了一种机制,允许客户端和服务器协商一个子协议(即一种更高级别的消息协议),用于在 WebSocket 之上定义各方可以发送哪些类型的消息、消息的格式、每条消息的内容等。使用子协议是可选的,但无论是否使用子协议,客户端和服务器都需要就某种协议达成一致,以明确消息内容的定义。
4.4.1. 概述
STOMP(Simple Text Oriented Messaging Protocol,简单文本定向消息协议)最初是为脚本语言(如 Ruby、Python 和 Perl)连接企业级消息代理而创建的。它旨在涵盖常用消息模式的一个最小功能子集。STOMP 可在任何可靠的双向流网络协议(如 TCP 和 WebSocket)之上使用。尽管 STOMP 是一种面向文本的协议,但其消息负载既可以是文本,也可以是二进制数据。
STOMP 是一种基于帧的协议,其帧结构以 HTTP 为模型。以下列表展示了 STOMP 帧的结构:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用 SEND 或 SUBSCRIBE 命令来发送消息或订阅消息,并附带一个 destination 头部,用于描述消息的内容以及应由谁接收。这实现了一种简单的发布-订阅机制,可用于通过代理(broker)向其他已连接的客户端发送消息,或向服务器发送消息以请求执行某些工作。
当你使用 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 规范有意未对目的地(destination)的含义做出明确界定。它可以是任意字符串,完全由 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 作为子协议,相较于直接使用原始 WebSocket,能够让 Spring Framework 和 Spring Security 提供更丰富的编程模型。这一点与 HTTP 相较于原始 TCP 的优势类似——HTTP 使得 Spring MVC 及其他 Web 框架能够提供丰富的功能。以下是使用 STOMP 的一些优势:
-
无需发明自定义的消息协议和消息格式。
-
提供了 STOMP 客户端,包括 Spring Framework 中的Java 客户端。
-
您可以(可选地)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来管理订阅和广播消息。
-
应用程序逻辑可以组织在任意数量的
@Controller实例中,消息可以根据 STOMP 目标头进行路由,而不是针对给定连接使用单个WebSocketHandler来处理原始 WebSocket 消息。 -
你可以使用 Spring Security 根据 STOMP 目的地和消息类型来保护消息的安全。
4.4.3. 启用 STOMP
STOMP over WebSocket 的支持由 spring-messaging 和
spring-websocket 模块提供。一旦你添加了这些依赖项,就可以通过以下示例所示的方式,结合 SockJS 回退,暴露一个基于 WebSocket 的 STOMP 端点:
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS(); (1)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app"); (2)
config.enableSimpleBroker("/topic", "/queue"); (3)
}
}
| 1 | /portfolio 是 WebSocket(或 SockJS)客户端为进行 WebSocket 握手而需要连接的端点的 HTTP URL。 |
| 2 | 目标地址(destination)头部以 /app 开头的 STOMP 消息会被路由到 @MessageMapping 类中的 @Controller 方法。 |
| 3 | 使用内置的消息代理处理订阅和广播,并将目标头(destination header)以 /topic `or `/queue 或 1 开头的消息路由到该代理。 |
以下示例展示了与前述示例等效的 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>
对于内置的简单代理(simple broker),/topic 和 /queue 前缀没有任何特殊含义。它们只是一种约定,用于区分发布-订阅(pub-sub)模式与点对点(point-to-point)消息传递(即多个订阅者与单一消费者)。当你使用外部代理时,请查阅该代理的 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 头部。即使指定了,这些头部也会在服务器端被忽略(更准确地说,会被覆盖)。有关认证的更多信息,请参阅连接到代理和认证。
更多示例代码请参见:
-
使用 WebSocket 构建交互式 Web 应用程序 — 入门指南。
-
股票投资组合 — 一个示例应用程序。
4.4.4. WebSocket 服务器
要配置底层的 WebSocket 服务器,请参考服务器配置中的说明。对于 Jetty,您需要通过 HandshakeHandler 设置 WebSocketPolicy 和 StompEndpointRegistry:
@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 项目和应用场景中更广泛地使用。
以下列表简要描述了一些可用的消息传递抽象:
-
Message: 消息的简单表示形式,包含头部和有效载荷。
-
MessageHandler: 用于处理消息的契约。
-
MessageChannel: 用于发送消息的契约,可实现生产者与消费者之间的松耦合。
-
SubscribableChannel: 带有
MessageChannel订阅者的MessageHandler。 -
ExecutorSubscribableChannel: 使用
SubscribableChannel来传递消息的Executor。
Java 配置(即 @EnableWebSocketMessageBroker)和 XML 命名空间配置(即 <websocket:message-broker>)均使用上述组件来组装消息工作流。下图展示了启用内置简单消息代理时所使用的组件:
上图显示了三个消息通道:
-
clientInboundChannel:用于传递从 WebSocket 客户端接收到的消息。 -
clientOutboundChannel:用于向 WebSocket 客户端发送服务器消息。 -
brokerChannel:用于从服务器端应用程序代码中向消息代理发送消息。
下图展示了在配置外部代理(例如 RabbitMQ)用于管理订阅和广播消息时所使用的组件:
上述两个图之间的主要区别在于使用了“代理中继”(broker relay),通过 TCP 将消息传递给外部 STOMP 代理,并将消息从代理传递给已订阅的客户端。
当从 WebSocket 连接接收到消息时,这些消息会被解码为 STOMP 帧,
转换成 Spring 的 Message 表示形式,并发送到
clientInboundChannel 以进行进一步处理。例如,目标地址头(destination header)以 /app 开头的 STOMP 消息
可能会被路由到带注解控制器中的 @MessageMapping 方法,而 /topic 和 /queue 消息则可能直接路由到消息代理。
一个带有注解的 @Controller 可以处理来自客户端的 STOMP 消息,并通过 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;
}
}
前面的示例支持以下流程:
-
客户端连接到
http://localhost:8080/portfolio,一旦 WebSocket 连接建立,STOMP 帧就开始在其上流动。 -
客户端发送一个 SUBSCRIBE 帧,其 destination 头部为
/topic/greeting。该消息一旦被接收并解码,就会被发送到clientInboundChannel,然后路由到消息代理,由消息代理存储客户端的订阅。 -
客户端向
/app/greeting发送一个 aSEND 帧。/app前缀用于将其路由到带注解的控制器。在去除/app前缀后,目标地址中剩余的/greeting部分将被映射到@MessageMapping中的GreetingController方法。 -
从
GreetingController返回的值会被转换为一个 SpringMessage,其有效载荷(payload)基于返回值,并带有默认的目标地址头(destination header)/topic/greeting(该地址是通过将输入目标地址中的/app替换为/topic得到的)。生成的消息会被发送到brokerChannel,并由消息代理(message broker)进行处理。 -
消息代理会找到所有匹配的订阅者,并通过
clientOutboundChannel向每个订阅者发送一个 MESSAGE 帧,消息在此通道中被编码为 STOMP 帧,并通过 WebSocket 连接发送出去。
下一节将详细介绍带注解的方法,包括所支持的参数类型和返回值类型。
4.4.6. 注解控制器
应用程序可以使用带注解的 @Controller 类来处理来自客户端的消息。
此类可以声明 @MessageMapping、@SubscribeMapping 和 @ExceptionHandler
方法,如下文各节所述:
@MessageMapping
你可以使用 @MessageMapping 注解来标注那些根据消息目标(destination)进行路由的方法。该注解既支持在方法级别使用,也支持在类级别使用。在类级别上,@MessageMapping 用于表示控制器中所有方法共享的映射。
默认情况下,映射值采用 Ant 风格的路径模式(例如 /thing*、/thing/**),
并支持模板变量(例如 /thing/{id})。这些值可以通过 @DestinationVariable 方法参数进行引用。
应用程序也可以切换为使用点号分隔的目标地址约定来进行映射,如
使用点号作为分隔符 中所述。
支持的方法参数
下表描述了方法参数:
| 方法参数 | 描述 |
|---|---|
|
用于访问完整消息。 |
|
用于访问 |
|
通过类型化的访问器方法来访问头部信息。 |
|
用于访问消息的有效载荷,该有效载荷由已配置的 由于在没有其他参数匹配时,默认会假定存在此注解,因此该注解的显式声明并非必需。 您可以使用 |
|
用于访问特定的请求头值——必要时,可配合使用 |
|
用于访问消息中的所有标头。该参数必须可赋值给 |
|
用于访问从消息目标中提取的模板变量。 必要时,这些值会转换为所声明的方法参数类型。 |
|
反映在 WebSocket HTTP 握手时登录的用户。 |
返回值
默认情况下,@MessageMapping 方法的返回值会通过匹配的 MessageConverter 序列化为消息负载,并作为 Message 发送到 brokerChannel,然后从该通道广播给订阅者。出站消息的目标地址与入站消息的目标地址相同,但会加上 /topic 前缀。
您可以使用 @SendTo 和 @SendToUser 注解来自定义输出消息的目标地址。@SendTo 用于自定义目标地址或指定多个目标地址。@SendToUser 用于将输出消息仅发送给与输入消息关联的用户。请参阅用户目标地址。
你可以在同一个方法上同时使用 @SendTo 和 @SendToUser,并且这两个注解在类级别上也受支持,在这种情况下,它们将作为该类中方法的默认设置。但请注意,任何方法级别的 @SendTo 或 @SendToUser 注解都会覆盖类级别上的相应注解。
消息可以异步处理,@MessageMapping 方法可以返回 ListenableFuture、CompletableFuture 或 CompletionStage。
请注意,@SendTo 和 @SendToUser 仅是一种便捷方式,其本质是使用 SimpMessagingTemplate 来发送消息。在更高级的场景中,如有必要,@MessageMapping 方法可以直接回退到使用 SimpMessagingTemplate。
这种做法可以替代返回值,或者也可能与返回值结合使用。
参见发送消息。
@SubscribeMapping
@SubscribeMapping 与 @MessageMapping 类似,但其映射范围仅限于订阅消息。它支持与 #websocket-stomp-message-mapping 相同的方法参数。然而,对于返回值,默认情况下,消息会直接发送给客户端(通过 clientOutboundChannel,作为对订阅的响应),而不是发送给代理(通过 brokerChannel,向匹配的订阅进行广播)。添加 @SendTo 或 @SendToUser 注解将覆盖此默认行为,改为将消息发送给代理。
这在什么情况下有用呢?假设代理(broker)被映射到 /topic 和 /queue,而应用程序控制器则被映射到 /app。在此设置下,代理会存储所有针对 /topic 和 /queue 的订阅,这些订阅用于重复广播,应用程序无需参与其中。客户端也可以订阅某个 /app 目的地,此时控制器可以针对该订阅直接返回一个值,而无需代理介入,也无需存储或再次使用该订阅(本质上是一次性的请求-响应交互)。这种模式的一个典型用例是在应用启动时为用户界面填充初始数据。
在什么情况下这没有用?除非你出于某种原因希望代理(broker)和控制器(controller)都能独立处理消息(包括订阅),否则不要尝试将它们映射到相同的目标前缀。入站消息是并行处理的,无法保证某个特定消息会先由代理还是控制器处理。如果目标是在订阅被存储并准备好接收广播时收到通知,客户端应请求回执(receipt),前提是服务器支持该功能(简单代理不支持)。例如,使用 Java 的STOMP 客户端,你可以通过以下方式添加回执:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});
一种服务器端选项是注册一个ExecutorChannelInterceptor到brokerChannel上,并实现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 风格目标模式的订阅。
| 应用程序也可以使用点号分隔(而非斜杠分隔)的目的地。 参见 使用点号作为分隔符。 |
如果配置了任务调度器,简单代理(simple broker)支持 STOMP 心跳。 为此,您可以声明自己的调度器,也可以使用框架内部自动声明并使用的调度器。以下示例展示了如何声明您自己的调度器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private TaskScheduler messageBrokerTaskScheduler;
@Autowired
public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
// ...
}
}
4.4.9. 外部代理
简单代理非常适合入门使用,但它仅支持部分 STOMP 命令(不支持 ack、receipts 以及其他一些特性),依赖于一个简单的消息发送循环,并且不适合用于集群环境。作为替代方案,您可以升级应用程序以使用功能完备的消息代理。
请参阅您所选消息代理(例如 RabbitMQ、 ActiveMQ 等)的 STOMP 文档,安装该代理, 并启用 STOMP 支持来运行它。然后,您可以在 Spring 配置中启用 STOMP 代理中继 (而不是简单代理)。
以下示例配置启用了一个功能齐全的代理(broker):
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
</beans>
前述配置中的 STOMP 代理中继是一个 Spring
MessageHandler
,它通过将消息转发到外部消息代理来处理消息。
为此,它会与代理建立 TCP 连接,将所有消息转发给代理,
然后通过客户端的 WebSocket 会话将从代理接收到的所有消息转发给客户端。
本质上,它充当一个在两个方向上转发消息的“中继”。
为 TCP 连接管理,在您的项目中添加 io.projectreactor.netty:reactor-netty 和 io.netty:netty-all 依赖项。 |
此外,应用程序组件(例如 HTTP 请求处理方法、业务服务等)也可以按照发送消息中所述,向代理中继发送消息,以向已订阅的 WebSocket 客户端广播消息。
实际上,代理中继(broker relay)能够实现强大且可扩展的消息广播。
4.4.10. 连接到代理(Broker)
STOMP 代理中继(broker relay)会与代理维持一个单一的“系统”TCP 连接。
该连接仅用于源自服务器端应用程序的消息,而不用于接收消息。你可以为该连接配置 STOMP 凭据(即 STOMP 帧中的 login 和 passcode 头部)。
在 XML 命名空间和 Java 配置中,这分别通过 systemLogin 和
systemPasscode 属性进行设置,默认值均为 guest 和 guest。
STOMP 代理中继还会为每个已连接的 WebSocket 客户端创建一个独立的 TCP 连接。您可以配置用于代表客户端创建的所有 TCP 连接的 STOMP 凭据。在 XML 命名空间和 Java 配置中,这分别通过 clientLogin 和 clientPasscode 属性进行暴露,默认值均为 guest 和 guest。
STOMP 代理中继在代表客户端转发给代理的每个 login 帧上,始终会设置 passcode 和 CONNECT 头。因此,WebSocket 客户端无需设置这些头信息,它们会被忽略。正如认证部分所解释的那样,WebSocket 客户端应依赖 HTTP 认证来保护 WebSocket 端点并确立客户端身份。 |
STOMP代理中继还会通过“系统”TCP连接向消息代理发送和接收心跳。您可以配置发送和接收心跳的时间间隔(默认均为10秒)。如果与代理的连接丢失,代理中继会每5秒尝试重新连接一次,直到连接成功为止。
任何 Spring Bean 都可以实现 ApplicationListener<BrokerAvailabilityEvent>,
以便在与代理(broker)的“系统”连接断开并重新建立时接收通知。例如,一个广播股票行情的服务可以在没有活跃的“系统”连接时停止尝试发送消息。
默认情况下,STOMP 代理中继始终连接到同一个主机和端口,并在连接丢失时按需重新连接。如果你希望在每次尝试连接时提供多个地址,可以配置一个地址供应器(supplier of addresses),而不是使用固定的主机和端口。以下示例展示了如何实现这一点:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
private ReactorNettyTcpClient<byte[]> createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}
您还可以通过 virtualHost 属性来配置 STOMP 代理中继。
该属性的值将被设置为每个 host 帧的 CONNECT 头,
这在某些场景下非常有用(例如在云环境中,建立 TCP 连接的实际主机与提供
基于云的 STOMP 服务的主机不同)。
4.4.11. 使用点号作为分隔符
当消息被路由到 @MessageMapping 方法时,会使用 AntPathMatcher 进行匹配。
默认情况下,模式预期使用斜杠(/)作为分隔符。
这在 Web 应用程序中是一种良好的约定,类似于 HTTP URL。然而,
如果你更习惯于消息传递的惯例,也可以切换为使用点号(.)作为分隔符。
以下示例展示了如何在 Java 配置中实现这一点:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例展示了与前述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
<websocket:stomp-endpoint path="/stomp"/>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
<constructor-arg index="0" value="."/>
</bean>
</beans>
此后,控制器可以在 . 方法中使用点号(@MessageMapping)作为分隔符,如下例所示:
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
客户端现在可以向 /app/red.blue.green123 发送消息。
在前面的示例中,我们没有更改“代理中继”(broker relay)的前缀,因为这些前缀完全取决于外部消息代理。请查阅您所使用的代理的 STOMP 文档页面,以了解其对目标(destination)头支持哪些约定。
另一方面,“简单代理”(simple broker)确实依赖于所配置的PathMatcher,因此,如果你更改了分隔符,该更改也会应用于代理,以及代理将消息中的目标与订阅中的模式进行匹配的方式。
4.4.12. 认证
每个通过 WebSocket 的 STOMP 消息会话都始于一个 HTTP 请求。 该请求可能是升级到 WebSocket 的请求(即 WebSocket 握手), 或者在使用 SockJS 回退机制的情况下,是一系列 SockJS HTTP 传输请求。
许多 Web 应用程序已经内置了身份验证和授权机制,用于保护 HTTP 请求。通常,用户通过 Spring Security 使用某种机制(例如登录页面、HTTP 基本身份验证或其他方式)进行身份验证。已认证用户的安全上下文会保存在 HTTP 会话中,并与同一基于 Cookie 的会话中的后续请求相关联。
因此,对于 WebSocket 握手请求或 SockJS HTTP 传输请求,通常已经可以通过 HttpServletRequest#getUserPrincipal() 获取到已认证的用户。Spring 会自动将该用户与为其创建的 WebSocket 或 SockJS 会话关联起来,并随后通过用户头(user header)将其与通过该会话传输的所有 STOMP 消息关联。
简而言之,一个典型的Web应用程序在安全性方面无需做任何超出其已有操作之外的事情。用户在HTTP请求级别通过一个安全上下文进行身份验证,该上下文通过基于Cookie的HTTP会话进行维护(随后与为该用户创建的WebSocket或SockJS会话相关联),并导致每个流经应用程序的Message都会被打上用户头信息。
请注意,STOMP 协议确实在 login 帧中包含 passcode 和 CONNECT 头部。这些头部最初是为 STOMP over TCP 等场景设计的,至今仍然需要。然而,对于基于 WebSocket 的 STOMP,默认情况下,Spring 会忽略 STOMP 协议层面的授权头部,假定用户已在 HTTP 传输层完成身份认证,并期望 WebSocket 或 SockJS 会话中已包含经过认证的用户信息。
Spring Security 提供了
WebSocket 子协议授权,
它使用 ChannelInterceptor 根据消息中的用户头信息对消息进行授权。
此外,Spring Session 还提供了
WebSocket 集成,
以确保在 WebSocket 会话仍处于活动状态时,用户的 HTTP 会话不会过期。 |
4.4.13. Tokens认证
Spring Security OAuth 提供了基于Tokens的安全性支持,包括 JSON Web Token(JWT)。 您可以将其用作 Web 应用程序中的身份验证机制, 包括上一节所述的 WebSocket 上的 STOMP 通信(即通过基于 Cookie 的会话来维持身份)。
同时,基于 Cookie 的会话并不总是最合适的选择(例如,在不维护服务器端会话的应用程序中,或在通常使用请求头进行身份验证的移动应用程序中)。
WebSocket 协议(RFC 6455)“并未规定服务器在 WebSocket 握手过程中认证客户端的任何特定方式。”然而在实践中,浏览器客户端只能使用标准的认证头(即基本 HTTP 认证)或 Cookie,而无法(例如)提供自定义头信息。同样地,SockJS JavaScript 客户端也没有提供在 SockJS 传输请求中发送 HTTP 头的方法。参见 sockjs-client 问题 196。 取而代之的是,它允许发送查询参数,你可以利用该机制传递Tokens(token),但这也有其自身的缺点(例如,该Tokens可能会随 URL 被意外记录到服务器日志中)。
| 上述限制仅适用于基于浏览器的客户端,不适用于 Spring 的 Java STOMP 客户端,后者支持在 WebSocket 和 SockJS 请求中发送头部信息。 |
因此,希望避免使用 Cookie 的应用程序在 HTTP 协议层面上可能没有其他合适的认证替代方案。它们可以改为在 STOMP 消息协议层面通过请求头进行认证。 这样做只需两个简单步骤:
-
在连接时使用 STOMP 客户端传递身份验证头信息。
-
使用
ChannelInterceptor处理身份验证头信息。
下一个示例使用服务器端配置来注册一个自定义的身份验证拦截器。请注意,拦截器只需对 CONNECT Message 进行身份验证并设置用户头信息即可。Spring 会记录并保存已认证的用户,并将其与同一会话中的后续 STOMP 消息关联起来。以下示例展示了如何注册一个自定义的身份验证拦截器:
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}
此外,请注意,当你使用 Spring Security 对消息进行授权时,目前需要确保认证的 ChannelInterceptor 配置排在 Spring Security 的配置之前。最佳做法是将自定义拦截器声明在它自己的 WebSocketMessageBrokerConfigurer 实现类中,并使用 @Order(Ordered.HIGHEST_PRECEDENCE + 99) 注解进行标记。
4.4.14. 用户目标地址
应用程序可以发送针对特定用户的消息,Spring 的 STOMP 支持通过识别以 /user/ 为前缀的目的地来实现此功能。
例如,客户端可能会订阅 /user/queue/position-updates 目的地。
该目的地由 UserDestinationMessageHandler 处理,并转换为对该用户会话唯一的目的地
(例如 /queue/position-updates-user123)。这样既提供了订阅通用名称目的地的便利性,
又能确保与其他订阅相同目的地的用户之间不会发生冲突,从而保证每位用户都能接收到
专属的股票持仓更新。
在发送端,消息可以被发送到一个目标地址,例如
/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;
}
}
虽然用户目标(user destinations)通常意味着已认证的用户,但这并不是严格必需的。
一个未与已认证用户关联的 WebSocket 会话也可以订阅用户目标。
在这种情况下,@SendToUser 注解的行为与设置 broadcast=false 时完全相同(即仅针对处理消息的发送会话)。 |
你可以通过注入由 Java 配置或 XML 命名空间创建的 SimpMessagingTemplate,从任意应用程序组件向用户目标发送消息。(如果需要使用 brokerMessagingTemplate 进行限定,该 bean 的名称为 @Qualifier。)以下示例展示了如何实现这一点:
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
当你在使用外部消息代理(message broker)配合用户目的地(user destinations)时,应查阅该代理的文档,了解如何管理非活跃队列,以便在用户会话结束时,自动移除所有唯一的用户队列。例如,当你使用类似 /exchange/amq.direct/position-updates 的目的地时,RabbitMQ 会创建自动删除(auto-delete)队列。因此,在这种情况下,客户端可以订阅 /user/exchange/amq.direct/position-updates。
同样地,ActiveMQ 也提供了
配置选项
用于清理非活跃的目的地。 |
在多应用服务器场景中,用户目标可能因用户连接到不同的服务器而无法解析。在这种情况下,您可以配置一个目标,以广播未解析的消息,使其他服务器有机会尝试处理。这可以通过 Java 配置中 userDestinationBroadcast 的 MessageBrokerRegistry 属性,或 XML 中 user-destination-broadcast 元素的 message-broker 属性来实现。
4.4.15. 消息顺序
来自代理(broker)的消息会被发布到 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.16. 事件
多个 ApplicationContext 事件会被发布,可以通过实现 Spring 的 ApplicationListener 接口来接收这些事件:
-
BrokerAvailabilityEvent:指示代理(broker)变为可用或不可用的事件。 虽然“简单”代理在启动时立即变为可用,并且在应用程序运行期间始终保持可用状态,但 STOMP “代理中继”(broker relay)可能会与其功能完整的代理断开连接(例如,当代理被重启时)。该代理中继具有重连逻辑,当代理恢复后会重新建立与代理的“系统”连接。因此,每当连接状态在已连接与断开之间切换时,都会发布此事件。使用SimpMessagingTemplate的组件应订阅此事件,并在代理不可用时避免发送消息。无论如何,它们在发送消息时都应准备好处理MessageDeliveryException异常。 -
SessionConnectEvent:当接收到新的 STOMP CONNECT 请求时发布,用于表示新客户端会话的开始。该事件包含代表连接请求的消息,其中包含会话 ID、用户信息(如果有)以及客户端发送的任何自定义头信息。这对于跟踪客户端会话非常有用。订阅此事件的组件可以使用SimpMessageHeaderAccessor或StompMessageHeaderAccessor对所包含的消息进行封装。 -
SessionConnectedEvent:在代理(broker)响应 CONNECT 请求并发送了 STOMP CONNECTED 帧之后,紧接着SessionConnectEvent发布。此时,STOMP 会话可被视为已完全建立。 -
SessionSubscribeEvent:当接收到新的 STOMP SUBSCRIBE 时发布。 -
SessionUnsubscribeEvent:在收到新的 STOMP UNSUBSCRIBE 请求时发布。 -
SessionDisconnectEvent:当 STOMP 会话结束时发布。DISCONNECT 可能由客户端发送,也可能在 WebSocket 会话关闭时自动生成。在某些情况下,每个会话可能会多次发布此事件。组件在处理多次断开连接事件时应具有幂等性。
| 当你使用功能完整的代理(broker)时,如果代理暂时不可用,STOMP“代理中继”(broker relay)会自动重新连接“系统”连接。然而,客户端连接不会自动重新连接。假设启用了心跳机制,客户端通常会在10秒内检测到代理无响应。客户端需要自行实现重连逻辑。 |
4.4.17. 拦截
事件为 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.18. STOMP 客户端
Spring 提供了基于 WebSocket 的 STOMP 客户端和基于 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 句柄,你可以使用该句柄来取消订阅。对于每条接收到的消息,处理器可以指定应将消息负载反序列化成的目标 Object 类型,如下例所示:
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
要启用 STOMP 心跳,你可以使用 WebSocketStompClient 配置 TaskScheduler,
并可选择自定义心跳间隔(写空闲时间为 10 秒,此时会发送一个心跳;读空闲时间也为 10 秒,此时会关闭连接)。
当你使用 WebSocketStompClient 进行性能测试,从同一台机器模拟成千上万个客户端时,请考虑关闭心跳机制,因为每个连接都会调度自己的心跳任务,而这种方式并未针对在同一台机器上运行大量客户端的场景进行优化。 |
STOMP 协议还支持回执(receipts)机制,客户端必须添加一个 receipt 头部,服务器在处理完 send 或 subscribe 操作后会返回一个 RECEIPT 帧作为响应。为支持此功能,StompSession 提供了 setAutoReceipt(boolean) 方法,该方法会在后续的每次 send 或 subscribe 操作中自动添加 receipt 头部。
此外,你也可以手动向 StompHeaders 添加回执头部。send 和 subscribe 方法均返回一个 Receiptable 实例,你可以利用它注册回执成功和失败的回调函数。
要使用此功能,你必须为客户端配置一个 TaskScheduler,并指定回执过期前的等待时间(默认为 15 秒)。
请注意,StompSessionHandler 本身就是一个 StompFrameHandler,这使得它除了可以通过 handleException 回调处理消息处理过程中产生的异常,以及通过 handleTransportError 处理传输层错误(包括 ConnectionLostException)之外,还能处理 ERROR 帧。
4.4.19. WebSocket 作用域
每个 WebSocket 会话都包含一个属性映射(map)。该映射作为头部信息附加到从客户端传入的消息上,并可在控制器方法中访问,如下例所示:
@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.20. 性能
在性能方面,并不存在一劳永逸的解决方案。许多因素都会影响性能,包括消息的大小和数量、应用程序方法是否执行需要阻塞的操作,以及外部因素(例如网络速度及其他问题)。本节旨在概述可用的配置选项,并就如何思考系统扩展性提供一些思路。
在消息传递应用程序中,消息通过通道进行传递,并由线程池支持异步执行。配置此类应用程序需要对通道和消息流有充分的了解。因此,建议先阅读消息流。
显然,首先要配置的是支撑 clientInboundChannel 和 clientOutboundChannel 的线程池。默认情况下,这两个线程池的大小均被设置为可用处理器数量的两倍。
如果带注解方法中的消息处理主要是 CPU 密集型的,那么 clientInboundChannel 的线程数量应保持接近处理器核心数。如果这些任务更多是 I/O 密集型的,并且需要阻塞或等待数据库或其他外部系统,则可能需要增加线程池的大小。
|
一个常见的误解是,配置核心线程池大小(例如10)和最大线程池大小(例如20)会得到一个包含10到20个线程的线程池。 实际上,如果队列容量保持默认值 Integer.MAX_VALUE 不变, 线程池永远不会超过核心线程池大小,因为所有额外的任务都会被放入队列中。 请参阅 |
在 clientOutboundChannel 这一侧,所有操作都是向 WebSocket 客户端发送消息。如果客户端处于高速网络中,线程数量应保持接近可用处理器的数量。如果客户端速度较慢或带宽较低,它们消耗消息的时间会更长,从而给线程池带来负担。因此,增加线程池的大小就变得必要了。
虽然 clientInboundChannel 的工作负载是可以预测的——毕竟它取决于应用程序本身的行为——但配置“clientOutboundChannel”则更加困难,因为其受应用程序无法控制的因素影响。因此,有两个额外的属性与消息发送相关:sendTimeLimit 和 sendBufferSizeLimit。您可以使用这些属性来配置向客户端发送消息时允许的最大耗时以及可缓冲的数据量。
其基本思想是,在任意给定时刻,只能使用一个线程向客户端发送数据。与此同时,所有额外的消息都会被缓冲起来,你可以使用这些属性来决定发送一条消息允许花费多长时间,以及在此期间可以缓冲多少数据。有关重要的附加细节,请参阅 JavaDoc 和 XML Schema 文档。
以下示例展示了一种可能的配置:
@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 消息大小会自动调整,以确保至少能够承载 16KB 的 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>
关于扩展性,有一个重要要点涉及使用多个应用程序实例。 目前,使用简单代理(simple broker)无法实现这一点。 然而,当你使用功能齐全的代理(例如 RabbitMQ)时,每个应用程序实例都会连接到该代理, 并且从一个应用程序实例广播的消息可以通过代理转发给通过其他任何应用程序实例连接的 WebSocket 客户端。
4.4.21. 监控
当你使用 @EnableWebSocketMessageBroker 或 <websocket:message-broker> 时,关键的基础设施组件会自动收集统计信息和计数器,这些数据可提供对应用程序内部状态的重要洞察。该配置还会声明一个类型为 WebSocketMessageBrokerStats 的 Bean,它将所有可用信息汇总到一处,默认情况下每 30 分钟以 INFO 级别记录一次日志。此 Bean 可通过 Spring 的 MBeanExporter 导出到 JMX,以便在运行时查看(例如,通过 JDK 自带的 jconsole 工具)。
以下列表总结了可用的信息:
- 客户端 WebSocket 会话
-
- 当前
-
指示当前有多少个客户端会话,其中进一步细分为 WebSocket 会话与 HTTP 流式传输和轮询的 SockJS 会话。
- 总计
-
指示已建立的会话总数。
- 异常关闭
-
- 连接失败
-
已建立但因在60秒内未收到任何消息而被关闭的会话。这通常表明存在代理或网络问题。
- 发送次数超出限制
-
会话在超过配置的发送超时时间或发送缓冲区限制后被关闭,这种情况可能发生在慢速客户端上(参见前一节)。
- 传输错误
-
会话在传输错误(例如无法读取或写入 WebSocket 连接,或 HTTP 请求或响应)之后关闭。
- STOMP 帧
-
已处理的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数,表示在 STOMP 层有多少客户端进行了连接。请注意,当会话异常关闭或客户端在未发送 DISCONNECT 帧的情况下直接关闭时,DISCONNECT 帧的数量可能会偏低。
- STOMP 代理中继
-
- TCP 连接
-
指示代表客户端 WebSocket 会话建立到代理的 TCP 连接数量。该值应等于客户端 WebSocket 会话数 + 1 个额外的共享“系统”连接,用于从应用程序内部发送消息。
- STOMP 帧
-
代表客户端转发给代理或从代理接收的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数。请注意,无论客户端 WebSocket 会话以何种方式关闭,都会向代理发送一个 DISCONNECT 帧。因此,较低的 DISCONNECT 帧计数表明代理正在主动关闭连接(可能是因为心跳未及时到达、接收到无效的输入帧或其他问题)。
- 客户端入站通道
-
用于支持
clientInboundChannel的线程池的统计信息,可帮助了解传入消息处理的健康状况。此处任务排队积压表明应用程序处理消息的速度可能过慢。如果存在 I/O 密集型任务(例如,缓慢的数据库查询、向第三方 REST API 发起的 HTTP 请求等),请考虑增大线程池的大小。 - 客户端出站通道
-
用于支持
clientOutboundChannel的线程池的统计信息, 可帮助了解向客户端广播消息的健康状况。此处任务排队积压表明客户端处理消息的速度过慢。 一种解决方法是增加线程池大小,以容纳预期数量的并发慢速客户端。 另一种选择是减少发送超时时间和发送缓冲区大小限制(参见上一节)。 - SockJS 任务调度器
-
用于发送心跳的 SockJS 任务调度器线程池的统计信息。请注意,当在 STOMP 层面协商了心跳机制时,SockJS 的心跳将被禁用。
4.4.22. 测试
使用 Spring 的 STOMP-over-WebSocket 支持时,测试应用程序主要有两种方法。第一种是编写服务器端测试,以验证控制器及其带注解的消息处理方法的功能。第二种是编写完整的端到端测试,涉及同时运行客户端和服务器。
这两种方法并非相互排斥。相反,它们各自在整体测试策略中都占有一席之地。服务器端测试更加聚焦,也更容易编写和维护。另一方面,端到端集成测试则更为全面,覆盖的测试内容更多,但编写和维护起来也更为复杂。
最简单的服务端测试形式是编写控制器单元测试。然而,这种方式并不足够有用,因为控制器的许多行为都依赖于其注解。纯单元测试根本无法对此进行测试。
理想情况下,被测试的控制器应以运行时的方式进行调用,这与使用 Spring MVC Test 框架测试处理 HTTP 请求的控制器的方法非常相似——即无需运行 Servlet 容器,而是依靠 Spring 框架来调用带注解的控制器。与 Spring MVC Test 一样,这里你有两种可选方案:要么使用“基于上下文的”设置,要么使用“独立式”设置:
-
借助 Spring TestContext 框架加载实际的 Spring 配置,将
clientInboundChannel注入为测试字段,并使用它发送消息以由控制器方法处理。 -
手动设置调用控制器所需的最小 Spring 框架基础设施(即
SimpAnnotationMethodMessageHandler),并将控制器的消息直接传递给它。
这两种设置场景都在股票投资组合示例应用程序的测试中进行了演示。
第二种方法是创建端到端的集成测试。为此,你需要以嵌入模式运行一个 WebSocket 服务器,并作为 WebSocket 客户端连接到该服务器,发送包含 STOMP 帧的 WebSocket 消息。 股票投资组合示例应用的测试也通过使用 Tomcat 作为嵌入式 WebSocket 服务器以及一个简单的 STOMP 客户端来进行测试,展示了这种方法。
5. 其他 Web 框架
本章详细介绍了 Spring 与第三方 Web 框架的集成。
Spring 框架的核心价值主张之一在于赋予开发者选择权。总体而言,Spring 并不会强制你使用或绑定任何特定的架构、技术或方法论(尽管它确实会推荐某些选项优于其他)。这种自由选择最适合开发者及其开发团队的架构、技术或方法论的权利,在 Web 领域体现得尤为明显:Spring 不仅提供了自己的 Web 框架(Spring MVC 和 Spring WebFlux),同时还支持与多种流行的第三方 Web 框架进行集成。
5.1. 通用配置
在深入探讨每个受支持的 Web 框架的具体集成细节之前,我们先来看一下与任何特定 Web 框架无关的通用 Spring 配置。(本节同样适用于 Spring 自身的各种 Web 框架变体。)
Spring 轻量级应用模型所倡导的概念之一(姑且这么说)就是分层架构。请记住,在“经典”的分层架构中,Web 层只是众多层次中的一个。它作为服务端应用程序的入口点之一,会将请求委托给在服务层中定义的服务对象(外观类),以满足特定业务需求(且与表现层技术无关)的用例。在 Spring 中,这些服务对象、其他任何业务相关对象、数据访问对象等都存在于一个独立的“业务上下文”中,该上下文中不包含任何 Web 层或表现层对象(表现层对象,例如 Spring MVC 控制器,通常配置在一个独立的“表现上下文”中)。本节将详细介绍如何配置一个 Spring 容器(WebApplicationContext),使其包含应用程序中所有的“业务 Bean”。
接下来具体说明,您只需在 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 创建的这个“业务上下文”ContextLoaderListener。
以下示例展示了如何获取 WebApplicationContext:
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
WebApplicationContextUtils
类是为了方便起见,因此您无需记住ServletContext
属性的名称。它的getWebApplicationContext()方法在WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
键下不存在对象时返回null。为了避免应用程序中出现NullPointerExceptions的风险,最好使用getRequiredWebApplicationContext()方法。当缺少ApplicationContext时,此方法会抛出异常。
一旦你获得了 WebApplicationContext 的引用,就可以通过 Bean 的名称或类型来获取它们。大多数开发者通过名称获取 Bean,然后将其转换为其实现的某个接口。
幸运的是,本节中大多数框架都提供了更简单的方式来查找 Bean。 它们不仅使从 Spring 容器中获取 Bean 变得轻而易举,还允许你在其控制器上使用依赖注入。 每个 Web 框架部分都详细介绍了其特定的集成策略。
5.2. JSF
JavaServer Faces (JSF) 是 JCP(Java 社区进程)制定的标准组件化、事件驱动的 Web 用户界面框架。它是 Java EE 技术体系的官方组成部分,但也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。
请注意,较新版本的 JSF 已与应用服务器中的 CDI 基础设施紧密绑定,某些新的 JSF 功能仅在此类环境中才能正常工作。Spring 对 JSF 的支持已不再积极演进,主要仅用于在现代化旧版基于 JSF 的应用程序时提供迁移支持。
Spring 对 JSF 集成的关键元素是 JSF 的 ELResolver 机制。
5.2.1. Spring Bean 解析器
SpringBeanFacesELResolver 是一个符合 JSF 规范的 ELResolver 实现,
它与 JSF 和 JSP 所使用的标准 Unified EL 进行集成。它首先委托给
Spring 的“业务上下文”WebApplicationContext,然后再委托给底层 JSF 实现的默认解析器。
在配置方面,您可以在 JSF 的 SpringBeanFacesELResolver 文件中定义 faces-context.xml,如下例所示:
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
5.2.2. 使用FacesContextUtils
自定义的 ELResolver 在将属性映射到 faces-config.xml 中的 Bean 时效果很好,但有时您可能需要显式获取一个 Bean。
FacesContextUtils
类让这一操作变得简单。它类似于 WebApplicationContextUtils,不同之处在于它接受一个 FacesContext 参数,而不是 ServletContext 参数。
下面的例子展示了如何使用FacesContextUtils:
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
5.3. Apache Struts 2.x
由 Craig McClanahan 发明的 Struts 是一个由 Apache 软件基金会托管的开源项目。当时,它极大地简化了 JSP/Servlet 编程范式,并赢得了许多原本使用专有框架的开发者的青睐。它简化了编程模型,是开源的(因此免费),并且拥有庞大的社区支持,这使得该项目不断发展,并在 Java Web 开发者中广受欢迎。
作为原始 Struts 1.x 的继任者,请查看 Struts 2.x 以及 Struts 提供的 Spring 插件,以实现内置的 Spring 集成。