这部分文档涵盖了对基于Servlet API构建并部署到Servlet容器的Servlet堆栈Web应用程序的支持。单独的章节包括Spring MVC、查看技术、CORS支持和WebSocket支持。有关反应式堆栈Web应用程序,请参阅Web on Reactive Stack。
1. Spring Web MVC
Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。正式名称“Spring Web MVC”来自其源模块的名称(Spring-webmvc
),但更常见的名称是“Spring MVC”。
与Spring Web MVC类似,Spring Framework5.0引入了一个反应式堆栈Web框架,其名称“Spring WebFlux”也基于其源代码模块(Spring-WebFlux
)。本节介绍了Spring Web MVC。下一节将介绍Spring WebFlux。
有关基线信息以及与Servlet容器和Jakarta EE版本范围的兼容性,请参阅Spring框架Wiki。
1.1. DispatcherServlet
与许多其他Web框架一样,Spring MVC是围绕前端控制器模式设计的,其中中央Servlet
,DispatcherServlet
为请求处理提供共享算法,而实际工作由可配置的委托组件执行。该模型灵活,支持多种工作流程。
DispatcherServlet
和任何Servlet
一样,需要使用Java配置或在web.xml
中根据Servlet规范进行声明和映射。反过来,DispatcherServlet
使用Spring配置来发现请求映射、视图解析、异常处理以及等所需的委托组件。
以下Java配置示例注册并初始化由Servlet容器自动检测的DispatcherServlet
(请参阅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/*");
}
}
In addition to using the ServletContext API directly, you can also extend AbstractAnnotationConfigDispatcherServletInitializer and override specific methods (see the example under Context Hierarchy). |
For programmatic use cases, a GenericWebApplicationContext can be used as an alternative to AnnotationConfigWebApplicationContext . See the GenericWebApplicationContext javadoc for details. |
以下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 follows a different initialization sequence. Rather than hooking into the lifecycle of the Servlet container, Spring Boot uses Spring configuration to bootstrap itself and the embedded Servlet container. Filter and Servlet declarations are detected in Spring configuration and registered with the Servlet container. For more details, see the Spring Boot documentation. |
1.1.1. Context Hierarchy
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/*" };
}
}
If an application context hierarchy is not required, applications can return all configuration through getRootConfigClasses() and null from 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>
If an application context hierarchy is not required, applications may configure a “root” context only and leave the contextConfigLocation Servlet parameter empty. |
1.1.2. Special Bean Types
DispatcherServlet
委托特殊的Bean处理请求并呈现适当的响应。所谓“特殊的Bean”,我们指的是实现框架契约的Spring管理的对象
实例。它们通常带有内置的约定,但您可以自定义它们的属性并扩展或替换它们。
下表列出了DispatcherServlet
检测到的特殊Bean:
Bean type | Explanation |
---|---|
|
将请求映射到处理程序以及用于前处理和后处理的拦截器列表。该映射基于一些条件,具体情况因 两个主要的 |
|
帮助 |
解决异常的策略,可能会将它们映射到处理程序、HTML错误视图或其他目标。请参阅例外。 |
|
将处理程序返回的基于 |
|
解析客户端正在使用的 |
|
例如,解决您的Web应用程序可以使用 - 提供个性化布局的主题。请参阅主题。 |
|
抽象,用于在某个多部分解析库的帮助下解析多部分请求(例如,浏览器表单文件上传)。请参阅多部分解析器。 |
|
存储和检索可用于将属性从一个请求传递到另一个请求的“输入”和“输出” |
1.1.3. Web MVC Config
应用程序可以声明处理请求所需的特殊Bean类型中列出的基础设施Bean。DispatcherServlet
检查每个特殊Bean的WebApplicationContext
。如果没有匹配的Bean类型,它将使用DispatcherServlet.properties
.中列出的缺省类型
大多数情况下,MVC配置是最佳起点。它在Java或XML中声明所需的Bean,并提供更高级别的配置回调API来对其进行定制。
Spring Boot relies on the MVC Java configuration to configure Spring MVC and provides many extra convenient options. |
1.1.4. Servlet Config
在Servlet环境中,您可以选择以编程方式配置Servlet容器作为替代方案,也可以与web.xml
文件结合使用。下面的示例注册一个DispatcherServlet
:
public class MyWebApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext container) { XmlWebApplicationContext appContext = new XmlWebApplicationContext(); appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext)); registration.setLoadOnStartup(1); registration.addMapping("/"); } }
WebApplicationInitializer
是Spring MVC提供的一个接口,它确保您的实现被检测到并自动用于初始化任何Servlet 3容器。WebApplicationInitializer
的抽象基类实现名为AbstractDispatcherServletInitializer
,它通过重写方法来指定Servlet映射和Dispatcher Servlet
配置的位置,从而使注册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[] { "/" };
}
}
如果您使用基于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[] { "/" };
}
}
AbstractDispatcherServletInitializer
还提供了一种方便的方法来添加Filter
实例,并将它们自动映射到DispatcherServlet
,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每个筛选器都添加了基于其具体类型的默认名称,并自动映射到DispatcherServlet
。
AbstractDispatcherServletInitializer
的isAsyncSupport
受保护的方法提供了一个位置,用于在Dispatcher Servlet
和映射到它的所有筛选器上启用异步支持。默认情况下,此标志设置为true
。
最后,如果您需要进一步定制DispatcherServlet
本身,您可以重写createDispatcherServlet
方法。
1.1.5. Processing
DispatcherServlet
按如下方式处理请求:
-
搜索
WebApplicationContext
,并在请求中将其绑定为控制器和流程中的其他元素可以使用的属性。默认情况下,它绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
密钥下。 -
区域设置解析器绑定到请求,让流程中的元素解析要在处理请求(呈现视图、准备数据等)时使用的区域设置。如果不需要区域设置解析,则不需要区域设置解析程序。
-
主题解析器绑定到请求,以让视图等元素确定要使用的主题。如果您不使用主题,您可以忽略它。
-
如果指定多部分文件解析器,则会检查请求中是否有多部分。如果找到多个部分,则请求被包装在
MultipartHttpServletRequest
中,以供流程中的其他元素进一步处理。有关多部分处理的详细信息,请参阅多部分解析器。 -
搜索适当的处理程序。如果找到处理程序,则运行与该处理程序相关联的执行链(预处理器、后处理器和控制器),以准备用于呈现的模型。或者,对于带注释的控制器,可以(在
HandlerAdapter
中)呈现响应,而不是返回视图。 -
如果返回模型,则渲染该视图。如果没有返回任何模型(可能是因为预处理器或后处理器截取了请求,可能是出于安全原因),则不会呈现任何视图,因为请求可能已经完成。
在WebApplicationContext
中声明的HandlerExceptionResolver
Bean用于解决请求处理过程中引发的异常。这些异常解析器允许定制处理异常的逻辑。有关详细信息,请参阅例外。
对于HTTP缓存支持,处理程序可以使用WebRequest
的check NotModified
方法,以及带注释的控制器的其他选项,如控制器的HTTP缓存中所述。
您可以通过向web.xml
文件中的Servlet声明添加Servlet初始化参数(init-param
元素)来自定义单个DispatcherServlet
实例。下表列出了支持的参数:
Parameter | Explanation |
---|---|
|
类,该类实现要由该Servlet实例化和本地配置的 |
|
传递给Context实例(由 |
|
|
|
在找不到请求的处理程序时是否引发 默认情况下,它设置为 请注意,如果还配置了默认Servlet处理,则始终将未解析的请求转发到默认Servlet,并且永远不会引发404。 |
1.1.6. Path Matching
Servlet API将完整的请求路径公开为questURI
,并将其进一步细分为contextPath
、servletPath
和pathInfo
,它们的值根据Servlet的映射方式而有所不同。根据这些输入,Spring MVC需要确定用于映射处理程序的查找路径,该路径应该排除ConextPath
和任何servletmap
前缀(如果适用)。
servletPath
和pathInfo
被解码,这使得它们不可能直接与完整的请求URI
进行比较以派生lookupPath,这使得有必要对请求URI
进行解码。但是,这会带来其自身的问题,因为路径可能包含已编码的保留字符,如“/”
或“;”
,这些保留字符在解码后可能会改变路径的结构,这也可能导致安全问题。此外,Servlet容器可能会在不同程度上标准化ServletPath
,从而进一步无法对请求URI
执行startswith
比较。
这就是为什么最好避免依赖ServletPath
,它与基于前缀的ServletPath
映射类型一起提供。如果DispatcherServlet
被映射为带有“/”
或没有“/*”
前缀的默认Servlet,并且Servlet容器是4.0+,那么Spring MVC能够检测到Servlet映射类型,并避免同时使用servletPath
和pathInfo
。在3.1版的Servlet容器上,假定Servlet映射类型相同,可以通过在MVC配置中通过路径匹配提供一个UrlPath Helper
和always sUseFullPath=true
来实现相同的映射类型。
幸运的是,默认的Servlet映射“/”
是一个很好的选择。但是,仍然存在一个问题,即需要对请求URI
进行解码,以便能够与控制器映射进行比较。这也是不希望的,因为有可能对改变路径结构的保留字符进行解码。如果这些字符不是预期的,那么您可以拒绝它们(如Spring Security HTTP防火墙),或者您可以使用urlDecode=False
配置UrlPathHelper
,但控制器映射需要与编码的路径匹配,这可能并不总是很好地工作。此外,有时DispatcherServlet
需要与另一个Servlet共享URL空间,并且可能需要通过前缀进行映射。
当使用PathPatternParser
和解析的模式作为AntPathMatcher
字符串路径匹配的替代方案时,上述问题已得到解决。PathPatternParser
从5.3版开始可以在Spring MVC中使用,并且从6.0版开始默认启用。与需要对查找路径进行解码或对控制器映射进行编码的AntPathMatcher
不同,经过解析的PathPattern
与名为RequestPath
的路径的解析表示形式匹配,一次匹配一个路径段。这允许单独解码和清理路径段值,而不会有改变路径结构的风险。解析的PathPattern
还支持使用ServletPath
前缀映射,只要使用Servlet路径映射并且前缀保持简单,即没有编码的字符。有关模式语法的详细信息和比较,请参阅模式比较。
1.1.7. Interception
所有Handlermap
实现都支持处理程序拦截器,当您想要将特定功能应用于特定请求 - (例如,检查主体)时,这些拦截器非常有用。拦截器必须从org.springFrawork.web.Servlet
包实现HandlerInterceptor
,并使用三个方法提供足够的灵活性来执行所有类型的前处理和后处理:
-
preHandle(..)
:在实际处理程序运行之前 -
postHandle(..)
:在处理程序运行之后 -
After Completion(..)
:完成请求后
preHandle(..)
方法返回一个布尔值。您可以使用此方法中断或继续执行链的处理。当此方法返回true
时,处理程序执行链继续。当它返回FALSE时,DispatcherServlet
假定拦截器本身已经处理了请求(例如,呈现了适当的视图),并且不会继续执行执行链中的其他拦截器和实际的处理程序。
有关如何配置拦截器的示例,请参阅MVC配置一节中的拦截器。您还可以通过在各个Handlermap
实现上使用setter来直接注册它们。
postHandle
方法对@ResponseBody
和ResponseEntity
方法不太有用,这些方法的响应是在HandlerAdapter
内和postHandle
之前编写和提交的。这意味着对响应进行任何更改都为时已晚,例如添加额外的标头。对于这样的场景,您可以实现ResponseBodyAdvice
并将其声明为控制器建议Bean,或者直接在RequestMappingHandlerAdapter
上配置它。
1.1.8. Exceptions
如果在请求映射过程中发生异常或从请求处理程序(如@控制器
)抛出异常,DispatcherServlet
将委托给HandlerExceptionResolver
Bean链来解决异常并提供替代处理,这通常是错误响应。
下表列出了可用的HandlerExceptionResolver
实现:
HandlerExceptionResolver |
Description |
---|---|
|
异常类名和错误视图名之间的映射。用于在浏览器应用程序中呈现错误页。 |
解决由Spring MVC引发的异常,并将它们映射到HTTP状态代码。另请参阅备选 |
|
|
使用 |
|
通过调用 |
Chain of Resolvers
您可以通过在您的Spring配置中声明多个HandlerExceptionResolver
Bean并根据需要设置它们的顺序
属性来形成异常解析器链。Order属性越高,异常解决程序的定位就越晚。
HandlerExceptionResolver
的约定指定它可以返回:
-
指向错误视图的
ModelAndView
。 -
如果异常是在冲突解决程序中处理的,则为空
ModelAndView
。 -
如果异常仍未解决,则为空
,以供后续解析程序尝试;如果异常仍位于末尾,则允许它冒泡到Servlet容器。
MVC配置自动为默认的Spring MVC异常、@ResponseStatus
注释的异常以及对@ExceptionHandler
方法的支持声明内置的解析器。您可以自定义或替换该列表。
Container Error Page
如果任何HandlerExceptionResolver
仍未解决异常,并因此将其留给传播,或者如果响应状态设置为错误状态(即4xx、5xx),则Servlet容器可以在HTML中呈现默认错误页面。要定制容器的默认错误页,可以在web.xml
中声明错误页映射。以下示例显示了如何执行此操作:
<error-page>
<location>/error</location>
</error-page>
在前面的示例中,当出现异常或响应具有错误状态时,Servlet容器会在容器内将错误分派到配置的URL(例如,/error
)。然后由DispatcherServlet
进行处理,可能会将其映射到@Controller
,可以实现该方法来返回带有模型的错误视图名或呈现JSON响应,如下面的示例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
The Servlet API does not provide a way to create error page mappings in Java. You can, however, use both a WebApplicationInitializer and a minimal web.xml . |
1.1.9. View Resolution
Spring MVC定义了ViewResolver
和View
接口,使您可以在浏览器中呈现模型,而无需使用特定的视图技术。ViewResolver
提供视图名称和实际视图之间的映射。View
解决了在将数据移交给特定视图技术之前的准备工作。
下表提供了有关ViewResolver
层次结构的更多详细信息:
ViewResolver | Description |
---|---|
|
|
|
|
|
|
|
支持 |
|
|
|
|
Handling
您可以通过声明多个解析器Bean来链接视图解析器,如果需要,还可以通过设置order
属性来指定排序。请记住,Order属性越高,视图解析器在链中的位置就越晚。
ViewResolver
的约定指定它可以返回NULL以指示找不到该视图。然而,对于JSP和InternalResourceViewResolver
,确定是否存在JSP的唯一方法是通过RequestDispatcher
执行分派。因此,您必须始终将InternalResourceViewResolver
配置为在视图解析器的总体顺序中最后。
Redirecting
视图名称中的特殊reDirect:
前缀允许您执行重定向。UrlBasedViewResolver
(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向URL。
实际效果与控制器返回RedirectView
相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(如reDirect:/myapp/ome/resource
)相对于当前Servlet上下文进行重定向,而诸如redirect:https://myhost.com/some/arbitrary/path
之类的名称则重定向到绝对URL。
请注意,如果控制器方法使用@ResponseStatus
进行注释,则该注释值优先于RedirectView
设置的响应状态。
Forwarding
您还可以为最终由UrlBasedViewResolver
和子类解析的视图名称使用特殊的Forward:
前缀。这将创建一个InternalResourceView
,它执行RequestDispatcher.ward()
。因此,这个前缀对于InternalResourceViewResolver
和InternalResourceView
(对于JSP)没有用处,但如果您使用另一种视图技术,但仍然希望强制由Servlet/JSP引擎处理资源的转发,则它可能会很有帮助。请注意,您也可以改为链接多个视图解析器。
Content Negotiation
ContentNegotiatingViewResolver
本身并不解析视图,而是委托给其他视图解析器,并选择与客户端请求的表示类似的视图。表示形式可以从Accept
头或查询参数(例如,“/Path?Format=pdf”
)确定。
通过将请求的媒体类型与与其每个ViewResolver
相关联的
),在这种情况下,其Content-Type为View
所支持的媒体类型(也称为Content-Type
)进行比较,Content NeatheratingViewResolver
选择适当的View
来处理请求。列表中具有兼容Content-Type
的第一个View
将表示形式返回给客户端。如果ViewResolver
链无法提供兼容的视图,则会参考通过DefaultViews
属性指定的视图列表。后一种选项适用于单独的视图
,它可以呈现当前资源的适当表示形式,而不考虑逻辑视图名称。Accept标头可以包括通配符(例如,Text/*Text/XML
视图是兼容匹配。
1.1.10. Locale
与Spring web MVC框架一样,Spring架构的大多数部分都支持国际化。DispatcherServlet
允许您使用客户端的区域设置自动解析消息。这是通过LocaleResolver
对象完成的。
当请求进入时,DispatcherServlet
查找区域设置解析器,如果找到,则尝试使用它来设置区域设置。通过使用RequestContext.getLocale()
方法,您始终可以检索由区域设置解析器解析的区域设置。
除了自动区域设置解析之外,您还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的详细信息,请参阅拦截器),以在特定情况下(例如,基于请求中的参数)更改区域设置。
区域设置解析器和拦截器在org.springframework.web.servlet.i18n
包中定义,并以正常方式在应用程序上下文中配置。以下区域设置解析器的选择包括在Spring中。
Time Zone
除了获取客户的区域设置外,了解其时区通常也很有用。LocaleContextResolver
接口提供了LocaleResolver
的扩展,使解析程序可以提供更丰富的LocaleContext
,其中可能包括时区信息。
如果可用,则可以使用RequestContext.getTimeZone()
方法获取用户的时区
。时区信息由向Spring的ConversionService
注册的任何日期/时间Converter
和Formatter
对象自动使用。
Header Resolver
此区域设置解析器检查客户端(例如,Web浏览器)发送的请求中的Accept-Language
头。通常,此标头字段包含客户端操作系统的区域设置。请注意,此解析程序不支持时区信息。
Cookie Resolver
此区域设置解析器检查客户端上可能存在的Cookie
,以确定是否指定了区域设置
或时区
。如果是,则使用指定的详细信息。通过使用此区域设置解析器的属性,您可以指定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
属性:
Property | Default | Description |
---|---|---|
|
类名+区域设置 |
Cookie的名称 |
|
Servlet容器默认设置 |
Cookie在客户端上保留的最长时间。如果指定 |
|
/ |
将Cookie的可见性限制在站点的特定部分。当指定 |
Session Resolver
SessionLocaleResolver
允许您从可能与用户请求相关联的会话中检索区域设置
和时区
。与CookieLocaleResolver
不同,该策略将本地选择的区域设置存储在Servlet容器的HttpSession
中。因此,这些设置对于每个会话都是临时的,因此在每个会话结束时会丢失。
请注意,这与外部会话管理机制(如Spring Session项目)没有直接关系。此SessionLocaleResolver
针对当前HttpServletRequest
计算和修改相应的HttpSession
属性。
Locale Interceptor
您可以通过将LocaleChangeInterceptor
添加到一个HandlerMapping
定义来启用区域设置更改。它检测请求中的参数并相应地更改区域设置,在Dispatcher的应用程序上下文中对LocaleResolver
调用setLocale
方法。下一个示例显示,对包含名为siteLanguage
的参数的所有*.view
资源的调用现在更改了区域设置。例如,对
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.11. Themes
您可以应用Spring Web MVC框架主题来设置应用程序的整体外观,从而增强用户体验。主题是影响应用程序视觉样式的静态资源的集合,通常是样式表和图像。
as of 6.0 support for themes has been deprecated theme in favor of using CSS, and without any special support on the server side. |
Defining a theme
要在Web应用程序中使用主题,必须设置org.springframework.ui.context.ThemeSource
接口的实现。WebApplicationContext
接口扩展ThemeSource
,但将其职责委托给专用实现。默认情况下,委托是一个org.springframework.ui.context.support.ResourceBundleThemeSource
实现,它从类路径的根目录加载属性文件。若要使用自定义ThemeSource
实现或配置ResourceBundleThemeSource
的基本名称前缀,您可以在应用程序上下文中使用保留名称ThemeSource
注册一个Bean。Web应用程序上下文自动检测具有该名称的Bean并使用它。
当您使用ResourceBundleThemeSource
时,主题是在一个简单的属性文件中定义的。属性文件列出了构成主题的资源,如下例所示:
styleSheet=/themes/cool/style.css background=/themes/cool/img/coolBg.jpg
属性的键是引用视图代码中的主题元素的名称。对于一个JSP,您通常使用Spring:Theme
定制标记来实现这一点,这与Spring:Message
标记非常相似。下面的JSP片段使用上一个示例中定义的主题来自定义外观:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
默认情况下,ResourceBundleThemeSource
使用空基名称前缀。因此,将从类路径的根目录加载属性文件。因此,您应该将Cool.Properties
主题定义放在类路径根目录下的一个目录中(例如,/WEB-INF/CLASSES
)。ResourceBundleThemeSource
使用标准的Java资源包加载机制,允许主题完全国际化。例如,我们可以有一个引用带有荷兰语文本的特殊背景图像的/WEB-INF/classes/cool_nl.properties
。
1.1.12. Multipart Resolver
是一种用于解析包括文件上传在内的多部分请求的策略。有一个实现基于Commons FileUpload,另一个实现基于Servlet多部分请求解析。org.springframework.web.multipart
包中的MultipartResolver
要启用多部分处理,您需要在DispatcherServlet
Spring配置中声明一个名为MultipartResolver
的MultipartResolver
Bean。DispatcherServlet
检测它并将其应用于传入的请求。当收到内容类型为Multipart/Form-Data
的POST时,解析器分析内容,将当前HttpServletRequest
包装为MultipartHttpServletRequest
,以提供对已解析文件的访问,并将部分作为请求参数公开。
Apache Commons FileUpload
要使用Apache CommonsFileUpload
,可以将CommonsMultipartResolver
类型的Bean配置为MultipartResolver
。您还需要将Commons-Fileupload
JAR作为类路径的依赖项。
这个解析器变体委托给应用程序中的一个本地库,从而提供最大限度的跨Servlet容器的可移植性。作为替代方案,可以考虑通过容器自己的解析器进行标准的Servlet多部分解析,如下所述。
Commons FileUpload传统上只适用于POST请求,但接受任何 |
Servlet Multipart Parsing
Servlet多部分解析需要通过Servlet容器配置来启用。要执行此操作,请执行以下操作:
-
在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"));
}
}
一旦Servlet多部分配置就绪,您就可以添加名为MultipartResolver
的类型为StandardServletMultipartResolver
的Bean。
这个解析器变体按原样使用Servlet容器的多部分解析器,这可能会使应用程序暴露于容器实现的差异。默认情况下,它将尝试使用任何HTTP方法解析任何 |
1.1.13. Logging
Spring MVC中的调试级日志记录被设计为紧凑、最少并且对人友好。它侧重于反复有用的高价值信息位,而不是仅在调试特定问题时有用的其他信息位。
跟踪级别日志记录通常遵循与调试相同的原则(例如,也不应该是消防软管),但可以用于调试任何问题。此外,某些日志消息在跟踪和调试时可能显示不同级别的详细信息。
好的日志记录来自使用日志的经验。如果您发现任何不符合所述目标的情况,请通知我们。
Sensitive Data
调试和跟踪日志记录可能会记录敏感信息。这就是为什么默认情况下会屏蔽请求参数和标头,并且必须通过DispatcherServlet
上的enableLoggingRequestDetails
属性显式启用它们的完整日志记录。
以下示例显示如何使用Java配置执行此操作:
public class MyInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
1.2. Filters
Spring-web
模块提供了一些有用的过滤器:
1.2.1. Form Data
浏览器只能通过HTTP GET或HTTP POST提交表单数据,但非浏览器客户端也可以使用HTTP PUT、PATCH和DELETE。Servlet API要求ServletRequest.getParameter*()
方法仅支持对HTTP POST的表单字段访问。
Spring-web
模块提供FormContent Filter
来拦截内容类型为application/x-www-form-urlencoded
,的HTTP PUT、Patch和Delete请求,从请求的正文中读取表单数据,并包装ServletRequest
以通过ServletRequest.getParameter*()
系列方法获得表单数据。
1.2.2. Forwarded Headers
当请求通过代理(如负载均衡器)时,主机、端口和方案可能会发生变化,这使得从客户端角度创建指向正确主机、端口和方案的链接成为一项挑战。
RFC 7239定义了转发的
HTTP标头,代理可以使用该标头提供有关原始请求的信息。还有其他非标准标头,包括X-Forwarded-Host
、X-Forwarded-Port
、X-Forwarded-Proto
、X-Forwarded-SSL
和X-Forwarded-Prefix
。
ForwardedHeaderFilter
是一个Servlet筛选器,它修改请求,以便a)基于转发的
标头更改主机、端口和方案,以及b)删除这些标头以消除进一步影响。筛选器依赖于包装请求,因此它必须排在其他筛选器之前,例如RequestConextFilter
,这些筛选器应该处理修改后的请求,而不是原始请求。
转发的报头有安全方面的考虑,因为应用程序无法知道报头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么应该将信任边界处的代理配置为删除来自外部的不受信任的转发
头。您还可以将ForwardedHeaderFilter
配置为emoveOnly=true
,在这种情况下,它将移除但不使用Header。
为了支持异步请求和错误调度,此筛选器应与DispatcherType.ASYNC
和DispatcherType.ERROR
映射。如果使用Spring框架的AbstractAnnotationConfigDispatcherServletInitializer
(请参阅Servlet配置),则会自动为所有分派类型注册所有过滤器。但是,如果通过web.xml
注册筛选器,或者在Spring Boot中通过FilterRegistrationBean
注册筛选器,请确保除了DispatcherType.REQUEST
之外,还包括Dispatcher Type.ASYNC
和Dispatcher Type.ERROR
。
1.2.3. Shallow ETag
ShallowEtag HeaderFilter
过滤器通过缓存写入响应的内容并根据它计算MD5散列来创建“浅”ETag。下一次客户端发送时,它会执行相同的操作,但也会将计算出的值与If-None-Match
请求头进行比较,如果两者相等,则返回304(NOT_MODIFIED)。
此策略节省网络带宽,但不会节省CPU,因为必须为每个请求计算完整的响应。前面描述的控制器级别的其他策略可以避免计算。请参阅HTTP缓存。
此筛选器具有一个参数,该参数将筛选器配置为写入类似以下内容的弱eTag:W/“02a2d595e6ed9a0b24f027f2b63b134d6”
(如RFC7232第2.3节中所定义)。
1.3. Annotated Controllers
Spring MVC提供了一个基于注释的编程模型,其中@控制器
和@RestController
组件使用注释来表示请求映射、请求输入、异常处理等。带注释的控制器具有灵活的方法签名,不必扩展基类,也不必实现特定的接口。下面的示例显示了由注释定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
在前面的示例中,该方法接受Model
,并以字符串
的形式返回一个视图名称,但还有许多其他选项,本章稍后将对其进行解释。
Guides and tutorials on spring.io use the annotation-based programming model described in this section. |
1.3.1. Declaration
您可以在Servlet的WebApplicationContext
中使用标准的Spring Bean定义来定义控制器Bean。@Controller
构造型允许自动检测,与Spring对检测类路径中的@Component
类并自动注册它们的Bean定义的支持保持一致。它还充当带注释的类的构造型,指示其作为Web组件的角色。
要启用此类@Controller
Bean的自动检测,您可以将组件扫描添加到Java配置中,如下例所示:
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
以下示例显示了与前面的示例等效的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 Proxies
在某些情况下,您可能需要在运行时使用AOP代理来装饰控制器。一个例子是,如果您选择在控制器上直接使用@Transaction
注释。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。直接在控制器上使用此类注释时会自动出现这种情况。
如果控制器实现了一个接口,并且需要AOP代理,您可能需要显式地配置基于类的代理。例如,使用<代码>@EnableTransactionManagement
可以更改为@EnableTransactionManagement(proxyTargetClass=TRUE)
,使用<代码><;tx:批注驱动/>;
可以更改为<代码><;tx:批注驱动的代理-目标类=“True”/>;
。
Keep in mind that as of 6.0, with interface proxying, Spring MVC no longer detects controllers based solely on a type-level @RequestMapping annotation on the interface. Please, enable class based proxying, or otherwise the interface must also have an @Controller annotation. |
1.3.2. Request Mapping
您可以使用@Requestmap
注释将请求映射到控制器方法。它具有按URL、HTTP方法、请求参数、标头和媒体类型匹配的各种属性。您可以在类级别使用它来表示共享映射,也可以在方法级别使用它来缩小到特定的端点映射。
还有特定于HTTP方法的快捷方式变体@Requestmap
:
-
@Getmap
-
@Postmap
-
@Putmap
-
@Deletemap
-
@Patchmap
提供的快捷方式是Custom Annotation,因为可以说,大多数控制器方法都应该映射到特定的HTTP方法,而不是使用@Requestmap
,后者默认情况下与所有HTTP方法匹配。在类级别仍然需要@Requestmap
来表示共享映射。
下面的示例具有类型和方法级别映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
URI patterns
@Requestmap
方法可以使用URL模式进行映射。有两种选择:
-
路径模式
- 与URL路径匹配的预解析模式,也被预解析为路径容器
。该解决方案专为Web使用而设计,可以有效地处理编码和路径参数,并高效地进行匹配。 -
AntPath Matcher
- 根据字符串路径匹配字符串模式。这也是在Spring配置中使用的原始解决方案,用于选择类路径、文件系统和其他位置上的资源。它的效率较低,并且字符串路径输入对于有效处理编码和URL的其他问题是一个挑战。
PathPattern
是推荐的Web应用解决方案,也是Spring WebFlux的唯一选择。它是从5.3版开始在Spring MVC中启用的,从6.0版开始默认启用。有关路径匹配选项的自定义,请参阅MVC配置。
PathPattern
支持与AntPathMatcher
相同的模式语法。此外,它还支持捕获模式,例如{*Spring}
,匹配路径末尾的0个或多个路径段。PathPattern
还限制使用**
匹配多个路径段,以便只允许在模式的末尾使用。这消除了在为给定请求选择最佳匹配模式时的许多模糊情况。有关完整的模式语法,请参考PathPattern和AntPathMatcher。
以下是一些模式示例:
-
“/Resources/ima?e.png”
-匹配路径段中的一个字符 -
“/Resources/*.png”
-匹配路径段中的零个或多个字符 -
“/resource/**”
-匹配多个路径段 -
“/Projects/{project}/Versions”
-匹配路径段并将其作为变量捕获 -
“/projects/{project:[a-z]+}/versions”
-使用正则表达式匹配并捕获变量
捕获的URI变量可以通过@PathVariable
访问。例如:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
您可以在类和方法级别声明URI变量,如下面的示例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
URI变量将自动转换为适当的类型,否则将引发TypeMismatchException
。默认支持简单类型(int
、long
、Date
等),您可以注册对任何其他数据类型的支持。请参阅类型转换和数据活页夹
。
您可以显式命名URI变量(例如,@PathVariable(“CustomID”)
),但如果名称相同,并且您的代码是使用调试信息或使用Java 8上的-参数
编译器标志编译的,则可以省略该细节。
语法{varName:regex}
使用语法为{varName:regex}
的正则表达式声明URI变量。例如,给定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) {
// ...
}
URI路径模式还可以嵌入${…通过对本地、系统、环境和其他属性源使用PropertySourcesPlaceholderConfigurer
在启动时解析的}占位符。例如,您可以使用它来基于某些外部配置对基本URL进行参数化。
Pattern Comparison
当多个模式与一个URL匹配时,必须选择最佳匹配。这是通过以下方法之一完成的,具体取决于是否启用了已解析的PathPattern
:
两者都有助于对模式进行排序,更具体的模式位于顶部。如果模式具有较少的URI变量(计数为1)、单通配符(计数为1)和双通配符(计数为2),则该模式不太具体。如果得分相等,则选择较长的模式。在给定相同分数和长度的情况下,选择URI变量多于通配符的模式。
默认映射模式(/**
)从评分中排除,始终排在最后。此外,前缀模式(如/public/**
)被认为没有其他没有双通配符的模式那么具体。
有关完整的详细信息,请访问上面的模式比较器链接。
Suffix Match
从5.3开始,默认情况下,Spring MVC不再执行.*
后缀模式匹配,其中映射到/Person
的控制器也隐式映射到/Person.*
。因此,不再使用路径扩展来解释响应 - 所请求的内容类型,例如,<代码>/Pers.pdf
、<代码>/Pers.xml
等等。
当浏览器发送难以一致解释的接受
头时,必须以这种方式使用文件扩展名。目前,这不再是必需的,使用Accept
头应该是首选。
随着时间的推移,文件扩展名的使用在各种方面都被证明是有问题的。当使用URI变量、路径参数和URI编码进行覆盖时,可能会导致歧义。关于基于URL的授权和安全性(有关更多详细信息,请参阅下一节)的推理也变得更加困难。
要在5.3之前的版本中完全禁用路径扩展,请设置以下设置:
-
useSuffixPatternMatching(FALSE)
,请参阅PathMatchConfigurer -
收藏夹路径扩展(FALSE)
,请参阅ContentNeairationConfigurer
除了通过“Accept”
头请求内容类型之外,还有一种方法仍然很有用,例如在浏览器中键入URL时。路径扩展的安全替代方案是使用查询参数策略。如果您必须使用文件扩展名,则可以考虑通过Content NeatherationConfigurer的mediaTypes
属性将它们限制为显式注册的扩展名列表。
Suffix Match and RFD
反射文件下载(RFD)攻击类似于XSS,因为它依赖于响应中反映的请求输入(例如,查询参数和URI变量)。然而,RFD攻击不是将JavaScript插入到HTML中,而是依靠浏览器切换来执行下载,并在以后双击时将响应视为可执行脚本。
在Spring MVC中,@ResponseBody
和ResponseEntity
方法面临风险,因为它们可以呈现不同的内容类型,客户端可以通过URL路径扩展请求这些内容类型。禁用后缀模式匹配并使用路径扩展进行内容协商可以降低风险,但不足以防止RFD攻击。
为了防止RFD攻击,在呈现响应正文之前,Spring MVC添加了一个Content-Disposition:inline;filename=f.txt
头来建议一个固定且安全的下载文件。仅当URL路径包含既不允许作为安全的文件扩展名,也不显式注册用于内容协商的文件扩展名时,才会执行此操作。然而,当URL直接输入到浏览器中时,它可能会产生副作用。
默认情况下,许多常见路径扩展都是安全的。具有自定义HttpMessageConverter
实现的应用程序可以显式注册用于内容协商的文件扩展名,以避免为这些扩展名添加Content-Disposal
头。参见内容类型。
有关RFD的其他建议,请参阅CVE-2015-5211。
Consumable Media Types
您可以根据请求的Content-Type
缩小请求映射范围,如下例所示:
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
1 | Using a consumes attribute to narrow the mapping by the content type. |
<代码>消耗 属性还支持否定表达式 - 例如,<代码>!文本/普通 表示除<代码>文本/普通 以外的任何内容类型。
您可以在类级别声明共享的消耗
属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的使用
属性覆盖,而不是扩展类级别声明。
MediaType provides constants for commonly used media types, such as APPLICATION_JSON_VALUE and APPLICATION_XML_VALUE . |
Producible Media Types
您可以根据Accept
请求头和控制器方法生成的内容类型列表来缩小请求映射范围,如下例所示:
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
1 | Using a produces attribute to narrow the mapping by the content type. |
媒体类型可以指定一个字符集。 - 支持否定表达式例如,<代码>!文本/纯文本 表示除“文本/纯文本”之外的任何内容类型。
您可以在类级别声明共享的Products
属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的会生成
属性覆盖,而不是扩展类级别声明。
MediaType provides constants for commonly used media types, such as APPLICATION_JSON_VALUE and APPLICATION_XML_VALUE . |
Parameters, headers
您可以根据请求参数条件缩小请求映射范围。您可以测试请求参数(myParam
)是否存在(!myParam
)或特定值(myParam=myValue
)。以下示例显示如何测试特定值:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | Testing whether myParam equals myValue . |
您还可以对请求标头条件使用相同的方法,如下例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | Testing whether myHeader equals myValue . |
HTTP HEAD, OPTIONS
@Getmap
(和@RequestMapping(method=HttpMethod.GET)
)透明支持HTTPHead进行请求映射。控制器方法不需要更改。在jakarta.servlet.http.HttpServlet
,中应用的响应包装器确保将Content-Length
头设置为写入的字节数(而不实际写入响应)。
@Getmap
(和@RequestMapping(method=HttpMethod.GET)
)隐式映射到HTTPHead,支持HTTPHead。HTTP Head请求被当作HTTP GET进行处理,不同之处在于,不是写入正文,而是计算字节数并设置Content-Length
头。
默认情况下,通过将Allow
响应标头设置为具有匹配URL模式的所有@Requestmap
方法中列出的HTTP方法列表来处理HTTP选项。
对于没有HTTP方法声明的@Requestmap
,Allow
标头设置为GET、Head、POST、PUT、Patch、Delete、Options
。控制器方法应该始终声明受支持的HTTP方法(例如,使用特定于HTTP方法的变量:@Getmap
、@Postmap
等)。
您可以显式地将@Requestmap
方法映射到HTTPHead和HTTP选项,但在常见情况下这不是必需的。
Custom Annotations
Spring MVC支持使用组合注释进行请求映射。这些批注本身使用@RequestMapping
进行了元批注,并被组合以使用更窄、更具体的目的重新声明@Requestmap
属性的一个子集(或全部)。
@Getmap
、@Postmap
、@Putmap
、@Deletemap
和@Patchmap
是合成批注的示例。之所以提供它们,是因为可以说,大多数控制器方法都应该映射到特定的HTTP方法,而不是使用@Requestmap
,后者在默认情况下与所有HTTP方法匹配。如果您需要组合注释的示例,请看一下这些注释是如何声明的。
Spring MVC还支持使用定制请求匹配逻辑的定制请求映射属性。这是一个更高级的选项,它需要将RequestMappingHandlerMapping
子类化并覆盖getCustomMethodCondition
方法,在该方法中您可以检查自定义属性并返回您自己的RequestCondition
。
Explicit Registrations
您可以通过编程方式注册处理程序方法,这些方法可用于动态注册或高级情况,例如同一处理程序在不同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 | Inject the target handler and the handler mapping for controllers. |
2 | Prepare the request mapping meta data. |
3 | Get the handler method. |
4 | Add the registration. |
1.3.3. Handler Methods
@Requestmap
处理程序方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。
Method Arguments
下表描述了支持的控制器方法参数。任何参数都不支持反应类型。
支持将JDK 8的java.util.Optional
作为方法参数,并将其与具有RequestParam
属性(例如,@RequestParam
、@RequestHeader
等)的批注结合使用,并等同于RequestHeader=False
。
Controller method argument | Description |
---|---|
|
对请求参数以及请求和会话属性的通用访问,而不直接使用Servlet API。 |
|
选择任何特定的请求或响应类型 - ,例如<代码>ServletRequest、 |
|
强制会话的存在。因此,这样的参数永远不会 |
|
Servlet 4.0推送构建器API,用于程序化的HTTP/2资源推送。请注意,根据Servlet规范,如果客户端不支持该HTTP/2特性,则注入的 |
|
当前经过身份验证的用户 - 可能是特定的<代码>主体 实现类(如果已知)。 请注意,如果对此参数进行了注释以允许自定义解析程序在通过 |
|
请求的HTTP方法。 |
|
当前请求区域设置,由可用的最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问Servlet API公开的原始请求正文。 |
|
用于访问Servlet API公开的原始响应体。 |
|
用于访问URI模板变量。请参阅URI模式。 |
|
用于访问URI路径段中的名称-值对。请参阅矩阵变量。 |
|
用于访问Servlet请求参数,包括多部分文件。参数值将转换为声明的方法参数类型。请参阅 注意,对于简单的参数值, |
|
用于访问请求标头。标头值将转换为声明的方法参数类型。参见 |
|
用于访问Cookie。Cookie值被转换为声明的方法参数类型。参见 |
|
用于访问HTTP请求正文。主体内容通过使用 |
|
用于访问请求头和正文。正文使用 |
|
若要访问 |
|
用于访问在HTML控制器中使用的模型,该模型作为视图呈现的一部分公开给模板。 |
|
指定要在重定向的情况下使用的属性(即,要追加到查询字符串中)和在重定向后的请求之前临时存储的闪存属性。请参阅重定向属性和Flash属性。 |
|
用于访问模型中的现有属性(如果不存在,则实例化),并应用数据绑定和验证。请参阅 注意, |
|
用于访问命令对象的验证和数据绑定错误(即 |
|
用于将表单处理标记为完成,这将触发通过类级 |
|
用于准备相对于当前请求的主机、端口、方案、上下文路径和Servlet映射的文字部分的URL。请参阅URI链接。 |
|
用于访问任何会话属性,而不是作为类级 |
|
用于访问请求属性。有关详细信息,请参阅 |
任何其他论点 |
如果一个方法参数与该表中前面的任何值都不匹配,并且它是一个简单类型(由BeanUtils#isSimpleProperty确定),那么它将被解析为 |
Return Values
下表描述了支持的控制器方法返回值。所有返回值都支持反应类型。
Controller method return value | Description |
---|---|
|
返回值通过 |
|
指定完整响应(包括HTTP头和正文)的返回值将通过 |
|
用于返回带有标头但没有正文的响应。 |
|
要在正文中呈现具有详细信息的RFC 7807错误响应,请参阅错误响应 |
|
要在正文中呈现具有详细信息的RFC 7807错误响应,请参阅错误响应 |
|
要通过 |
|
用于呈现的 |
|
要添加到隐式模型的属性,视图名称通过 |
|
要添加到模型的属性,视图名称通过 请注意, |
|
要使用的视图和模型属性以及响应状态(可选)。 |
|
如果具有 如果上述情况都不成立,则 |
|
|
|
|
|
替代 |
|
使用 |
|
|
通过 |
单个值类型(例如 |
其他返回值 |
如果返回值仍未以任何其他方式解析,则它将被视为模型属性,除非它是由BeanUtils#isSimpleProperty确定的简单类型,在这种情况下它仍未解析。 |
Type Conversion
一些表示基于字符串
的请求输入的带注释的控制器方法参数(如@RequestParam
、@RequestHeader
、@PathVariable
、@MatrixVariable
和@CookieValue
)如果声明为字符串
以外的内容,可能需要类型转换。
对于此类情况,将根据配置的转换器自动应用类型转换。默认支持简单类型(int
、long
、Date
等)。您可以通过WebDataBinder
(请参阅DataBinder
)或通过向FormattingConversionService
注册FormattingConversionService
来自定义类型转换。请参阅Spring字段格式。
类型转换中的一个实际问题是空字符串源值的处理。如果此类值由于类型转换而变为NULL
,则将其视为缺少。long
、uuid
和其他目标类型可能是这种情况。如果要允许注入Null
,请在参数批注上使用Required
标志,或者将参数声明为@Nullable
。
从5.3开始,即使在类型转换之后,也将强制使用非空参数。如果您的处理程序方法也打算接受空值,则要么将参数声明为 或者,在必需的 |
Matrix Variables
RFC 3986讨论路径段中的名称-值对。在Spring MVC中,我们将基于Tim Berners-Lee的“old post”的那些称为“矩阵变量”,但它们也可以称为URI路径参数。
矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,/car;COLOR=红、绿;Year=2012
)。还可以通过重复的变量名(例如,color=red;color=green;color=blue
).)指定多个值
如果URL预期包含矩阵变量,则控制器方法的请求映射必须使用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 /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 /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// 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]
}
注意,您需要启用矩阵变量的使用。在MVC Java配置中,需要通过路径匹配设置一个UrlPath Helper
,emoveSemicolonContent=FALSE
。在MVC XML命名空间中,您可以设置<;mvc:注解驱动的启用矩阵变量=“true”/>;
。
@RequestParam
您可以使用@RequestParam
注释将Servlet请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
以下示例显示了如何执行此操作:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | Using @RequestParam to bind petId . |
默认情况下,使用该批注的方法参数是必需的,但您可以通过将@RequestParam
批注的Required
标志设置为FALSE
,或通过使用java.util.Optional
包装器声明参数,来指定方法参数是可选的。
如果目标方法参数类型不是字符串
,则自动应用类型转换。请参阅类型转换。
将参数类型声明为数组或列表允许为同一参数名称解析多个参数值。
当@RequestParam
批注声明为Map<;字符串
或MultiValueMap<;字符串
,而没有在批注中指定参数名称时,则使用每个给定参数名称的请求参数值填充映射。
注意,@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-Ending
和Keep-Alive
标头的值:
@GetMapping("/demo")
public void handle( @RequestHeader("Accept-Encoding") String encoding, (1) @RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | Get the value of the Accept-Encoding header. |
2 | Get the value of the Keep-Alive header. |
如果目标方法参数类型不是字符串
,则自动应用类型转换。请参阅类型转换。
当@RequestHeader
批注用于Map<;字符串、字符串>;
、MultiValueMap<;字符串、字符串>;
或HttpHeaders
参数时,映射将填充所有标头值。
Built-in support is available for converting a comma-separated string into an array or collection of strings or other types known to the type conversion system. For example, a method parameter annotated with @RequestHeader("Accept") can be of type String but also String[] or List<String> . |
@CookieValue
您可以使用@CookieValue
注释将HTTP cookie的值绑定到控制器中的方法参数。
考虑具有以下Cookie的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下示例显示如何获取Cookie值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 | Get the value of the JSESSIONID cookie. |
如果目标方法参数类型不是字符串
,则自动应用类型转换。请参阅类型转换。
@ModelAttribute
您可以在方法参数上使用@ModelAttribute
注释来访问模型中的属性,如果该属性不存在,则将其实例化。模型属性还覆盖了来自HTTP Servlet请求参数的值,这些参数的名称与字段名称匹配。这称为数据绑定,它使您不必解析和转换单个查询参数和表单域。以下示例显示了如何执行此操作:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}
1 | Adding a BindingResult next to the @ModelAttribute . |
上面的Pet
实例的来源如下:
-
从可能已由@ModelAttribute方法添加的模型中检索。
-
如果模型属性列在类级
@SessionAttributes
注释中,则从HTTP会话中检索。 -
通过
Converter
获得,其中模型属性名称与请求值的名称匹配,例如路径变量或请求参数(请参见下一个示例)。 -
使用其默认构造函数实例化。
-
通过带有与Servlet请求参数匹配的参数的“主构造函数”实例化。参数名称通过JavaBeans
@ConstructorProperties
或通过字节码中的运行时保留的参数名称来确定。
使用@ModelAttribute方法提供它或依赖框架创建模型属性的一种替代方法是使用Converter<;字符串
来提供实例。当模型属性名称与请求值的名称(如路径变量或请求参数)匹配,并且存在字符串
到模型属性类型的转换器
时,将应用此方法。在下面的示例中,模型属性名称为
Account
,它与URI路径变量Account
匹配,并且有一个注册的Converter<;字符串Account&>可以从数据存储加载
Account
:
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
1 | Adding a BindingResult next to the @ModelAttribute . |
获取模型属性实例后,应用数据绑定。WebDataBinder
类将Servlet请求参数名称(查询参数和表单字段)与目标对象
上的字段名称匹配。必要时,在应用类型转换后填充匹配的字段。有关数据绑定(和验证)的更多信息,请参阅验证。有关定制数据绑定的更多信息,请参阅DataBinder
。
数据绑定可能会导致错误。默认情况下,会引发绑定异常
。但是,要检查控制器方法中的此类错误,可以在@ModelAttribute
旁边添加BindingResult
参数,如下面的示例所示:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | Setting @ModelAttribute(binding=false) . |
在某些情况下,您可能希望在没有数据绑定的情况下访问模型属性。对于这种情况,您可以将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 | Setting @ModelAttribute(binding=false) . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | Validate the Pet instance. |
注意,使用@ModelAttribute
是可选的(例如,设置其属性)。默认情况下,任何不是简单值类型(由BeanUtils#isSimpleProperty确定)且不能由任何其他参数解析器解析的参数都被视为使用@ModelAttribute
进行了注释。
@SessionAttributes
@SessionAttributes
用于在请求之间的HTTP Servlet会话中存储模型属性。它是一个类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,它们应该透明地存储在会话中,以供后续请求访问。
下面的示例使用@SessionAttributes
批注:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | Using the @SessionAttributes annotation. |
在第一次请求时,当名为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 | Storing the Pet value in the Servlet session. |
2 | Clearing the Pet value from the Servlet session. |
@SessionAttribute
如果您需要访问预先存在的会话属性,这些属性是全局管理的(即,在控制器 - 之外,例如由筛选器管理),并且可能存在也可能不存在,则可以在方法参数上使用@SessionAttribute
批注,如下面的示例所示:
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | Using a @SessionAttribute annotation. |
对于需要添加或删除会话属性的用例,可以考虑将org.springframework.web.context.request.WebRequest
或jakarta.servlet.http.HttpSession
注入控制器方法。
要将模型属性临时存储在会话中作为控制器工作流的一部分,请考虑使用@SessionAttributes
,如@SessionAttributes
中所述。
@RequestAttribute
与@SessionAttribute
类似,您可以使用@RequestAttribute
注释来访问先前创建的预先存在的请求属性(例如,通过ServletFilter
或HandlerInterceptor
):
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | Using the @RequestAttribute annotation. |
Redirect Attributes
默认情况下,所有模型属性都被视为在重定向URL中公开为URI模板变量。在剩余的属性中,那些是基元类型、基元类型集合或基元类型数组的属性会自动附加为查询参数。
如果专门为重定向准备了模型实例,则将原始类型属性附加为查询参数可能是所需的结果。但是,在带注释的控制器中,模型可以包含出于呈现目的而添加的其他属性(例如,下拉字段值)。为了避免此类属性出现在URL中的可能性,@Requestmap
方法可以声明RedirectAttributes
类型的参数,并使用它来指定使RedirectView
可用的确切属性。如果该方法确实重定向,则使用RedirectAttributes
的内容。否则,将使用模型的内容。
RequestMappingHandlerAdapter
提供了一个名为Ignore reDefaultModelOnReDirect
的标志,您可以使用该标志来指示如果控制器方法重定向,则永远不应使用默认Model
的内容。相反,控制器方法应该声明一个RedirectAttributes
类型的属性,如果它没有这样做,则不应该将任何属性传递给RedirectView
。MVC名称空间和MVC Java配置都将该标志设置为False
,以保持向后兼容性。但是,对于新应用程序,我们建议将其设置为true
。
请注意,当前请求中的URI模板变量在展开重定向URL时自动可用,您不需要通过Model
或RedirectAttributes
显式添加它们。以下示例显示如何定义重定向:
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
将数据传递到重定向目标的另一种方式是使用闪存属性。与其他重定向属性不同,Flash属性保存在HTTP会话中(因此不会出现在URL中)。有关详细信息,请参阅Flash属性。
Flash Attributes
Flash属性为一个请求提供了一种存储要在另一个请求中使用的属性的方法。这是在重定向 - 时最常用的,例如,后重定向-获取模式。闪存属性在重定向之前临时保存(通常在会话中),以供重定向后的请求使用,并立即删除。
Spring MVC有两个主要的抽象来支持Flash属性。FlashMap
用于保存Flash属性,FlashMapManager
用于存储、检索和管理FlashMap
实例。
Flash属性支持始终处于“打开”状态,不需要显式启用。但是,如果不使用它,它永远不会导致创建HTTP会话。对于每个请求,都有一个“输入”FlashMap
和一个“输出”FlashMap
,前者具有从前一个请求传递的属性(如果有的话),后者具有保存以备后续请求使用的属性。这两个FlashMap
实例都可以通过RequestContextUtils
中的静态方法从Spring MVC中的任何位置访问。
带注释的控制器通常不需要直接使用FlashMap
。相反,@Requestmap
方法可以接受RedirectAttributes
类型的参数,并使用它为重定向场景添加闪存属性。通过重定向属性
添加的Flash属性会自动传播到“输出”FlashMap。类似地,在重定向之后,来自“输入”FlashMap
的属性会自动添加到提供目标URL的控制器的Model
中。
Multipart
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name, @RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
将参数类型声明为list<;MultipartFile>;
允许为同一参数名称解析多个文件。
当@RequestParam
批注声明为Map<;字符串,MultipartFileMap;
或MultiValueMap<;字符串,但没有在批注中指定参数名称时,则使用每个给定参数名称的多部分文件填充映射。
With Servlet multipart parsing, you may also declare jakarta.servlet.http.Part instead of Spring’s MultipartFile , as a method argument or collection value type. |
您还可以使用多部分内容作为命令对象数据绑定的一部分。例如,上例中的表单域和文件可以是Form对象上的域,如下面的示例所示:
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";
}
}
在REST式服务场景中,还可以从非浏览器客户端提交多部分请求。以下示例显示了一个包含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”部分,但您可能希望将其从JSON反序列化(类似于@RequestBody
)。在使用HttpMessageConverter转换分块后,使用@RequestPart
注释可以访问该分块:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata, @RequestPart("file-data") MultipartFile file) {
// ...
}
您可以将@RequestPart
与jakarta.validation.Valid
结合使用,也可以使用Spring的@valated
注释,这两种方法都会导致应用标准Bean验证。默认情况下,验证错误会导致MethodArgumentNotValidException
,该异常被转换为400(BAD_REQUEST)响应。或者,您可以通过Errors
或BindingResult
参数在控制器内本地处理验证错误,如下面的示例所示:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, BindingResult result) {
// ...
}
@RequestBody
您可以使用
HttpMessageConverter
.>@RequestBody
注释来读取请求正文,并通过
将其反序列化为<代码>对象
下面的示例使用
@RequestBody
参数:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
您可以将@RequestBody
与jakarta.validation.Valid
或Spring的@valated
注释结合使用,这两个注释都会导致应用标准Bean验证。默认情况下,验证错误会导致MethodArgumentNotValidException
,该异常被转换为400(BAD_REQUEST)响应。或者,您可以通过Errors
或BindingResult
参数在控制器内本地处理验证错误,如下面的示例所示:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
HttpEntity
HttpEntity
与使用@RequestBody
基本相同,但基于公开请求头和正文的容器对象。下面的清单显示了一个示例:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@ResponseBody
您可以在方法上使用@ResponseBody
注释,通过HttpMessageConverter将返回序列化到响应体。下面的清单显示了一个示例:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@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).body(body);
}
Spring MVC支持使用单值反应类型为Body异步生成ResponseEntity
和/或单值和多值反应类型。这允许以下类型的异步响应:
-
ResponseEntity<;Mono<;T>;>;
或ResponseEntity<;Flux<;T>;>;
使响应状态和标头立即可用,而正文则在以后以异步方式提供。如果Body由0..1值组成,则使用Mono
,如果可以生成多个值,则使用Flux
。 -
Mono<;ResponseEntity<;T>;>;
稍后以异步方式提供所有三个 - 响应状态、头和正文。这允许响应状态和标头根据异步请求处理的结果而变化。
Jackson JSON
Spring提供对Jackson JSON库的支持。
JSON Views
Spring MVC提供了对Jackson的序列化视图的内置支持,它只允许呈现对象
中所有字段的子集。要将其与@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;
}
}
@JsonView allows an array of view classes, but you can specify only one per controller method. If you need to activate multiple views, you can use a composite interface. |
如果您希望以编程方式完成上述操作,请使用MappingJacksonValue
包装返回值并使用它来提供序列化视图,而不是声明@JsonView
注释:
@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;
}
}
对于依赖于视图分辨率的控制器,您可以将序列化视图类添加到模型中,如下例所示:
@Controller
public class UserController extends AbstractController {
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", new User("eric", "7!jd#h23"));
model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
return "userView";
}
}
1.3.4. Model
您可以使用@ModelAttribute
注释:
-
在
@Requestmap
方法中的方法参数上,从模型创建或访问对象
,并通过WebDataBinder
将其绑定到请求。 -
作为
@Controller
或@ControllerAdance
类中的方法级批注,帮助在任何@Requestmap
方法调用之前初始化模型。 -
在
@Requestmap
方法上标记其返回值的是一个模型属性。
本节讨论<代码>@模型属性
方法 - ,这是前面列表中的第二项。控制器可以有任意数量的@ModelAttribute
方法。所有此类方法都在同一控制器中的@Requestmap
方法之前调用。@ModelAttribute
方法也可以通过@ControllerAdance
在控制器之间共享。有关详细信息,请参阅控制器建议一节。
@ModelAttribute
方法具有灵活的方法签名。它们支持许多与@RequestMap
方法相同的参数,除了@ModelAttribute
本身或与请求正文相关的任何内容。
下面的示例显示了@ModelAttribute
方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
下面的示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
When a name is not explicitly specified, a default name is chosen based on the Object type, as explained in the javadoc for Conventions . You can always assign an explicit name by using the overloaded addAttribute method or through the name attribute on @ModelAttribute (for a return value). |
您还可以使用@ModelAttribute
作为@RequestMap
方法的方法级批注,在这种情况下,@RequestMapping
方法的返回值被解释为模型属性。这通常不是必需的,因为这是HTML控制器中的默认行为,除非返回值是字符串
,否则会被解释为视图名。@ModelAttribute
也可以自定义模型属性名称,如下例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
1.3.5. DataBinder
@控制器
或@ControllerAdance
类可以具有初始化WebDataBinder
实例的@InitBinder
方法,而这些方法又可以:
-
将请求参数(即表单或查询数据)绑定到模型对象。
-
将基于字符串的请求值(如请求参数、路径变量、标头、Cookie等)转换为控制器方法参数的目标类型。
-
呈现HTML表单时,将模型对象值格式化为
字符串
值。
@InitBinder
方法可以注册特定于控制器的java.beans.PropertyEditor
或SpringConverter
和Formatter
组件。此外,您可以使用MVC配置在全局共享的FormattingConversionService
中注册Converter
和Formatter
类型。
@InitBinder
方法支持与@RequestMap
方法相同的许多参数,但@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 | Defining an @InitBinder method. |
或者,当您通过共享的FormattingConversionService
使用基于格式化程序
的设置时,可以重复使用相同的方法并注册特定于控制器的格式化程序
实现,如下面的示例所示:
@Controller
public class FormController {
@InitBinder (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
1 | Defining an @InitBinder method on a custom formatter. |
Model Design
在Web应用程序的上下文中,数据绑定涉及将HTTP请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。
只有遵循JavaBeans命名约定的公共
属性才公开用于数据绑定-例如,FirstName()
属性的公共字符串getFirstName()
方法和FirstName
属性的公共空setFirstName(字符串)
方法。
The model object, and its nested object graph, is also sometimes referred to as a command object, form-backing object, or POJO (Plain Old Java Object). |
默认情况下,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;
}
}
如果不能或不想为每个数据绑定用例使用专用模型对象,则必须限制数据绑定允许的属性。理想情况下,您可以通过在WebDataBinder
上注册允许的字段模式来实现这一点。
例如,要在应用程序中注册允许的字段模式,您可以在@控制器
或@ControllerAdvice
组件中实现@InitBinder
方法,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以通过DataBinder
及其子类中的setDisalloweFields()
方法注册不允许的字段模式。但是,请注意,“允许列表”比“拒绝列表”更安全。因此,应该优先使用setAllowweFields()
,而不是setDisalloweFields()
。
请注意,与允许的字段模式匹配区分大小写;而与不允许的字段模式匹配不区分大小写。此外,与不允许的模式匹配的字段将不被接受,即使它碰巧也与允许的列表中的模式匹配。
在出于数据绑定的目的直接公开您的域模型时,正确配置允许和不允许的字段模式非常重要。否则,这是一个很大的安全风险。 此外,强烈建议您不要将域模型中的类型(如JPA或Hibernate实体)用作数据绑定场景中的模型对象。 |
1.3.6. Exceptions
@Controller
和@ControllerAdacy类可以有@ExceptionHandler
方法来处理来自控制器方法的异常,如下面的示例所示:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
异常可能与正在传播的顶级异常匹配(例如,抛出直接IOException
),或者与包装异常中的嵌套原因匹配(例如,包装在IlLegalStateException
内的IOException
)。在5.3版中,这可以在任意原因级别上匹配,而以前只考虑直接原因。
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,通常首选根异常匹配而不是原因异常匹配。更具体地说,ExceptionDepthCompator
用于根据抛出的异常类型中的异常深度对异常进行排序。
或者,注释声明可以缩小异常类型以匹配,如下面的示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
您甚至可以使用带有非常通用的参数签名的特定异常类型列表,如下面的示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
根异常匹配和原因异常匹配之间的区别可能会令人惊讶。 在前面显示的
|
我们通常建议您在参数签名中尽可能具体,以减少根异常类型和原因异常类型之间不匹配的可能性。考虑将多匹配方法分解为单独的@ExceptionHandler
方法,每个方法通过其签名匹配一个特定的异常类型。
在多个@ControllerAdance
排列中,我们建议在具有相应顺序的@ControllerAdance
上声明您的主根异常映射。虽然根异常匹配比原因更可取,但这是在给定控制器或@ControllerAdance
类的方法中定义的。这意味着优先级较高的@ControllerAdance
Bean上的原因匹配比优先级较低的@ControllerAdance
Bean上的任何匹配(例如,根)更好。
最后但并非最不重要的一点是,@ExceptionHandler
方法实现可以通过以其原始形式重新抛出给定的异常实例来选择退出处理。如果您只对根级别的匹配感兴趣,或者对无法静态确定的特定上下文中的匹配感兴趣,这将非常有用。重新抛出的异常通过剩余的解析链传播,就好像给定的@ExceptionHandler
方法一开始就不匹配一样。
在Spring MVC中对@ExceptionHandler
方法的支持构建在DispatcherServlet
级别,HandlerExceptionResolver机制上。
Method Arguments
@ExceptionHandler
方法支持以下参数:
Method argument | Description |
---|---|
例外类型 |
用于访问引发的异常。 |
|
用于访问引发异常的控制器方法。 |
|
对请求参数以及请求和会话属性的通用访问,而不直接使用Servlet API。 |
|
选择任何特定的请求或响应类型(例如, |
|
强制会话的存在。因此,这样的参数永远不会 |
|
当前经过身份验证的用户 - 可能是特定的<代码>主体 实现类(如果已知)。 |
|
请求的HTTP方法。 |
|
当前请求区域设置,由有效的最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问由Servlet API公开的原始响应体。 |
|
用于访问模型以获取错误响应。总是空荡荡的。 |
|
指定在重定向 - (要追加到查询字符串)的情况下使用的属性,以及在重定向之后的请求之前临时存储的闪存属性。请参阅重定向属性和Flash属性。 |
|
用于访问任何会话属性,而不是作为类级 |
|
用于访问请求属性。有关详细信息,请参阅 |
Return Values
@ExceptionHandler
方法支持以下返回值:
Return value | Description |
---|---|
|
返回值通过 |
|
返回值指定通过 |
|
要在正文中呈现具有详细信息的RFC 7807错误响应,请参阅错误响应 |
|
要在正文中呈现具有详细信息的RFC 7807错误响应,请参阅错误响应 |
|
要通过 |
|
用于呈现的 |
|
要添加到隐式模型的属性,其视图名称通过 |
|
要添加到模型的属性,其视图名称通过 请注意, |
|
要使用的视图和模型属性以及响应状态(可选)。 |
|
如果方法还具有 如果上述情况都不成立,则 |
任何其他返回值 |
如果返回值与上面的任何一个都不匹配,并且不是简单类型(由BeanUtils#isSimpleProperty确定),则默认情况下,它将被视为要添加到模型中的模型属性。如果它是简单类型,则保持未解析状态。 |
1.3.7. Controller Advice
@ExceptionHandler
、@InitBinder
和@ModelAttribute
方法仅应用于在其中声明它们的@Controller
类或类层次结构。相反,如果它们在@ControllerAdance
或@RestControllerAdvice
类中声明,则它们将应用于任何控制器。此外,从5.3开始,@ControllerAdance
中的@ExceptionHandler
方法可用于处理来自任何@控制器
或任何其他处理程序的异常。
@ControllerAdacy
使用@Component
进行元注释,因此可以通过组件扫描注册为Spring Bean。@RestControllerAdance
用@ControllerAdance
和@ResponseBody
进行了元注释,这意味着@ExceptionHandler
方法将通过响应正文消息转换呈现其返回值,而不是通过HTML视图。
在启动时,RequestMappingHandlermap
和ExceptionHandlerExceptionResolver
检测控制器建议Bean并在运行时应用它们。来自@ControllerAdvice
的全局@ExceptionHandler
方法从@控制器
在之后应用局部方法。相比之下,全局@ModelAttribute
和@InitBinder
方法在本地方法之前应用。
@ControllerAdacy
注释具有允许您缩小它们所应用的控制器和处理程序集的属性。例如:
// 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 {}
前面示例中的选择器是在运行时计算的,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参阅@ControllerAdvice
javadoc。
1.4. Functional Endpoints
Spring Web MVC包括WebMvc.fn,这是一种轻量级的函数式编程模型,在该模型中,函数用于路由和处理请求,契约被设计为不变性。它是基于注释的编程模型的替代方案,但在其他方面运行在相同的DispatcherServlet上。
1.4.1. Overview
在WebMvc.fn中,使用HandlerFunction
处理HTTP请求:该函数接受ServerRequest
并返回ServerResponse
。请求和响应对象都有不变的约定,这些约定提供对HTTP请求和响应的JDK 8友好访问。HandlerFunction
相当于基于注释的编程模型中的@RequestMap
方法体。
传入的请求被路由到具有<代码>路由器函数
的处理程序函数:该函数接受<代码>服务器请求
并返回可选的<代码>处理程序函数
(即Optional<;HandlerFunction>;
).当路由器函数匹配时,返回处理程序函数;否则返回空的可选。RouterFunction
等同于@Requestmap
注释,但主要区别在于路由器功能不仅提供数据,还提供行为。
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) { // ... } }
1 | Create router using the router DSL. |
如果您将RouterFunction
注册为一个Bean,例如,通过在@Configuration
类中公开它,则Servlet将自动检测它,如运行服务器中所述。
1.4.2. HandlerFunction
ServerRequest
和ServerResponse
是不变的接口,它们提供对HTTP请求和响应的JDK 8友好访问,包括头、正文、方法和状态代码。
ServerRequest
ServerRequest
提供对HTTP方法、URI、Header和查询参数的访问,而通过Body
方法提供对Body的访问。
下面的示例将请求正文提取为字符串
:
String string = request.body(String.class);
下面的示例将正文提取到列表<;Person>;
,其中Person
对象是从序列化格式(如JSON或XML)解码的:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
以下示例显示如何访问参数:
MultiValueMap<String, String> params = request.params();
ServerResponse
ServerResponse
提供对HTTP响应的访问,并且由于它是不可变的,您可以使用Build
方法来创建它。您可以使用构建器来设置响应状态、添加响应头或提供正文。下面的示例使用JSON内容创建一个200(OK)响应:
Person person = ... ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
以下示例显示如何构建带有Location
头而没有正文的201(已创建)响应:
URI location = ... ServerResponse.created(location).build();
您还可以使用CompletableFuture
、Publisher
或Reactive AdapterRegistry
支持的任何其他类型的异步结果作为正文。例如:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class); ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
如果不仅正文,而且状态或标头都基于异步类型,则可以对<代码>服务器响应
使用静态<代码>异步
方法,该方法接受CompletableFuture<;ServerResponse>;
,<代码>发布者<代码>服务器响应
或<代码>Reactive AdapterRegistry
支持的任何其他异步类型。例如:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
服务器发送的事件可以通过ServerResponse
上的静态SSE
方法提供。该方法提供的构建器允许您将字符串或其他对象作为JSON发送。例如:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
Handler Classes
我们可以将处理程序函数编写为lambda,如下面的示例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
1 | listPeople is a handler function that returns all Person objects found in the repository as JSON. |
2 | createPerson is a handler function that stores a new Person contained in the request body. |
3 | getPerson is a handler function that returns a single person, identified by the id path variable. We retrieve that Person from the repository and create a JSON response, if it is found. If it is not found, we return a 404 Not Found response. |
这很方便,但在应用程序中,我们需要多个函数,而多个内联lambda可能会变得混乱。因此,将相关的处理程序函数组合到一个处理程序类中非常有用,该处理程序类与基于注释的应用程序中的@Controller
具有类似的角色。例如,下面的类公开了一个被动的Person
存储库:
public class PersonHandler { private final PersonRepository repository; public PersonHandler(PersonRepository repository) { this.repository = repository; } public ServerResponse listPeople(ServerRequest request) { (1) List<Person> people = repository.allPeople(); return ok().contentType(APPLICATION_JSON).body(people); } public ServerResponse createPerson(ServerRequest request) throws Exception { (2) Person person = request.body(Person.class); repository.savePerson(person); return ok().build(); } public ServerResponse getPerson(ServerRequest request) { (3) int personId = Integer.parseInt(request.pathVariable("id")); Person person = repository.getPerson(personId); if (person != null) { return ok().contentType(APPLICATION_JSON).body(person); } else { return ServerResponse.notFound().build(); } } }
1 | listPeople is a handler function that returns all Person objects found in the repository as JSON. |
2 | createPerson is a handler function that stores a new Person contained in the request body. |
3 | getPerson is a handler function that returns a single person, identified by the id path variable. We retrieve that Person from the repository and create a JSON response, if it is found. If it is not found, we return a 404 Not Found response. |
Validation
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 | Create Validator instance. |
2 | Apply validation. |
3 | Raise exception for a 400 response. |
处理程序还可以使用标准的Bean验证API(JSR-303),方法是基于LocalValidatorFactoryBean
创建并注入一个全局Validator
实例。请参阅Spring验证。
1.4.3. RouterFunction
路由器函数用于将请求路由到相应的HandlerFunction
。通常,您不会自己编写路由器函数,而是使用RouterFunctions
实用程序类上的方法来创建一个。RouterFunctions.route()
(无参数)为您提供了一个流畅的构建器来创建路由器函数,而RouterFunctions.route(RequestPredicate,HandlerFunction)
提供了一种直接创建路由器的方法。
一般来说,建议使用route()
构建器,因为它为典型的映射场景提供了方便的快捷方式,而不需要难以发现的静态导入。例如,路由器函数构建器提供方法GET(字符串,HandlerFunction)
为GET请求创建映射;为POST提供POST(字符串,HandlerFunction)
方法。
除了基于HTTP方法的映射之外,路由构建器还提供了一种在映射到请求时引入附加谓词的方法。对于每个HTTP方法,都有一个重载的变量,该变量接受RequestPredicate
作为参数,通过该参数可以表达额外的约束。
Predicates
您可以编写自己的RequestPredicate
,但是RequestPredicates
实用程序类提供了基于请求路径、HTTP方法、内容类型等的常用实现。下面的示例使用请求谓词基于Accept
头创建约束:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
您可以使用以下命令将多个请求谓词组合在一起:
-
RequestPredicate.and(RequestPredicate)
— both必须匹配。 -
RequestPredicate.or(RequestPredicate)
— either可以匹配。
RequestPredicates
中的许多谓词都是组合的。例如,<代码>RequestPredicates.GET(字符串)
由RequestPredicates.method(HttpMethod)
和<代码>RequestPredicates.Path(字符串)
组成。上面显示的示例还使用了两个请求谓词,因为构建器在内部使用RequestPredicates.GET
,并将其与Accept
谓词组合在一起。
Routes
按顺序评估路由器功能:如果第一条路由不匹配,则评估第二条,依此类推。因此,在宣布一般路线之前宣布更具体的路线是有意义的。这在将路由器功能注册为SpringBean时也很重要,稍后将对此进行描述。请注意,此行为不同于基于注释的编程模型,在该模型中,“最具体的”控制器方法是自动选取的。
使用路由器函数构建器时,所有定义的路由被组合成一个RouterFunction
,该函数从Build()
返回。还可以通过其他方式将多个路由器功能组合在一起:
-
在
RouterFunctions.route()
生成器上添加(RouterFunction) -
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate,处理程序函数)
-使用嵌套的<代码>RouterFunctions.route() 的RouterFunction.and()
的快捷方式。
以下示例显示了四条路由的组成:
PersonRepository repository = ... PersonHandler handler = new PersonHandler(repository); RouterFunction<ServerResponse> otherRoute = ... RouterFunction<ServerResponse> route = route() .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1) .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2) .POST("/person", handler::createPerson) (3) .add(otherRoute) (4) .build();
1 | GET /person/{id} with an Accept header that matches JSON is routed to PersonHandler.getPerson |
2 | GET /person with an Accept header that matches JSON is routed to PersonHandler.listPeople |
3 | POST /person with no additional predicates is mapped to PersonHandler.createPerson , and |
4 | otherRoute is a router function that is created elsewhere, and added to the route built. |
Nested Routes
一组路由器功能通常具有共享谓词,例如共享路径。在上面的示例中,共享谓词将是匹配/Person
的路径谓词,由三个路由使用。在使用批注时,您可以通过使用映射到/Person
的类型级@Requestmap
批注来消除这种重复。在WebMvc.fn中,路径谓词可以通过路由器函数构建器上的Path
方法共享。例如,上面示例的最后几行可以通过使用嵌套路由以以下方式进行改进:
RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
1 | Note that second parameter of path is a consumer that takes the router builder. |
尽管基于路径的嵌套是最常见的,但是通过使用构建器上的Nest
方法,您可以在任何类型的谓词上进行嵌套。以上代码仍然以共享接受
-标头谓词的形式包含一些重复项。我们可以通过使用Nest
方法和Accept
来进一步改进:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
1.4.4. Running a Server
您通常通过MVC Configer在DispatcherHandler
-based设置中运行路由器功能,它使用Spring配置来声明处理请求所需的组件。MVC Java配置声明了以下基础架构组件以支持功能端点:
-
RouterFunctionmap
:在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...
}
}
1.4.5. Filtering Handler Functions
您可以使用路由函数构建器上的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(handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | The before filter that adds a custom request header is only applied to the two GET routes. |
2 | The after filter that logs the response is applied to all routes, including the nested ones. |
路由器生成器上的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(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
前面的示例演示了调用next.Handle(ServerRequest)
是可选的。我们只在允许访问时才让处理程序函数运行。
除了在路由器函数构建器上使用Filter
方法外,还可以通过RouterFunction.filter(HandlerFilterFunction)
.将过滤器应用于现有的路由器函数
CORS support for functional endpoints is provided through a dedicated CorsFilter . |
1.5. URI Links
本节描述了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 | Static factory method with a URI template. |
2 | Add or replace URI components. |
3 | Request to have the URI template and URI variables encoded. |
4 | Build a UriComponents . |
5 | Expand variables and obtain the URI . |
上面的示例可以合并到一个链中,并使用BuildAndExpand
缩写,如下例所示:
URI 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");
您可以使用完整的URI模板进一步缩短它,如下例所示:
URI 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。
您可以使用UriBuilderFactory
配置RestTemplate
和WebClient
,以自定义URI的准备。DefaultUriBuilderFactory
是UriBuilderFactory
的默认实现,它在内部使用UriComponentsBuilder
并公开共享配置选项。
以下示例显示如何配置RestTemplate
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
以下示例配置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();
另外,您也可以直接使用DefaultUriBuilderFactory
。它类似于使用UriComponentsBuilder
,但它不是静态工厂方法,而是保存配置和首选项的实际实例,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
1.5.3. URI Encoding
Spring MVC和Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode():首先预编码URI模板,然后在展开时严格编码URI变量。
-
UriComponents#encode():在URI变量展开之后对URI组件进行编码。
这两个选项都用转义的八位字节替换非ASCII和非法字符。但是,第一个选项还会替换URI变量中出现的具有保留含义的字符。
Consider ";", which is legal in a path but has reserved meaning. The first option replaces ";" with "%3B" in URI variables but not in the URI template. By contrast, the second option never replaces ";", since it is a legal character in a path. |
对于大多数情况,第一个选项可能会给出预期的结果,因为它将URI变量视为要完全编码的不透明数据,而第二个选项在URI变量确实有意包含保留字符时非常有用。第二个选项在根本不展开URI变量时也很有用,因为它还会编码任何偶然看起来像URI变量的内容。
下面的示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到URI(这意味着编码)来缩短前面的示例,如下面的示例所示:
URI 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");
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();
DefaultUriBuilderFactory
实现在内部使用UriComponentsBuilder
来展开和编码URI模板。作为工厂,它根据以下编码模式之一提供单一位置来配置编码方法:
-
TEMPLATE_AND_VALUES
:使用UriComponentsBuilder#encode()
,对应于前面列表中的第一个选项,预编码URI模板,并在展开时严格编码URI变量。 -
VALUES_ONLY
:不对URI模板进行编码,而是在将URI变量展开到模板中之前,通过UriUtils#encodeUriVariables
对它们进行严格编码。 -
URI_Components
:在URI变量展开后,使用UriComponents#encode()
,对应于前面列表中的第二个选项,对URI组件值进行编码。 -
无
:未应用编码。
出于历史原因和向后兼容性的原因,将RestTemplate
设置为EncodingMode.URI_Component
。WebClient
依赖DefaultUriBuilderFactory
中的默认值,从5.0.x中的EncodingMode.URI_Component
更改为5.1中的EncodingMode.TEMPLATE_AND_VALUES
。
1.5.4. Relative Servlet Requests
您可以使用ServletUriComponentsBuilder
创建相对于当前请求的URI,如下例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, path, and query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");
您可以相对于上下文路径创建URI,如下例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, and context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
您可以创建与Servlet相关的URI(例如,/main/*
),如下例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
As of 5.1, ServletUriComponentsBuilder ignores information from the Forwarded and X-Forwarded-* headers, which specify the client-originated address. Consider using the ForwardedHeaderFilter to extract and use or to discard such headers. |
1.5.5. Links to Controllers
Spring MVC提供了一种机制来准备指向控制器方法的链接。例如,以下MVC控制器允许创建链接:
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}
您可以通过按名称引用该方法来准备链接,如下例所示:
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
在前面的示例中,我们提供了实际的方法参数值(在本例中是长值: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();
Controller method signatures are limited in their design when they are supposed to be usable for link creation with fromMethodCall . Aside from needing a proper parameter signature, there is a technical limitation on the return type (namely, generating a runtime proxy for link builder invocations), so the return type must not be final . In particular, the common String return type for view names does not work here. You should use ModelAndView or even plain Object (with a String return value) instead. |
前面的示例使用了MvcUriComponentsBuilder
中的静态方法。在内部,它们依赖ServletUriComponentsBuilder
从当前请求的方案、主机、端口、上下文路径和Servlet路径准备一个基本URL。这在大多数情况下都很有效。然而,有时,这可能是不够的。例如,您可能在请求的上下文之外(例如准备链接的批处理),或者可能需要插入路径前缀(例如从请求路径中删除并需要重新插入到链接中的区域设置前缀)。
对于这种情况,您可以使用接受UriComponentsBuilder
的静态from Xxx
重载方法来使用基本URL。或者,您可以使用基本URL创建MvcUriComponentsBuilder
的实例,然后使用基于实例的with Xxx
方法。例如,下面的清单使用with MethodCall
:
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();
As of 5.1, MvcUriComponentsBuilder ignores information from the Forwarded and X-Forwarded-* headers, which specify the client-originated address. Consider using the ForwardedHeaderFilter to extract and use or to discard such headers. |
1.5.6. Links in Views
在Thymeleaf、FreeMarker或JSp等视图中,您可以通过引用为每个请求映射隐式或显式分配的名称来构建到带注释的控制器的链接。
请考虑以下示例:
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
给定前面的控制器,您可以准备一个来自JSP的链接,如下所示:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
前面的示例依赖于在Spring标记库(即META-INF/spring.tld)中声明的mvcUrl
函数,但是很容易定义您自己的函数,或者为其他模板技术准备一个类似的函数。
这就是它的工作原理。在启动时,通过HandlerMethodMappingNamingStrategy
,为每个@Requestmap
分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如,ThingControler
中的getThing
方法变为“TC#getThing”)。如果存在名称冲突,您可以使用@RequestMap(NAME=“..”)
来分配一个显式名称或实现您自己的HandlerMethodMappingNamingStrategy
.
1.6. Asynchronous Requests
Spring MVC广泛集成了Servlet异步请求处理:
-
控制器方法中的
DeferredResult
和返回值为单个异步返回值提供了基本支持。
-
控制器可以使用被动客户端并返回被动类型以进行响应处理。
1.6.1. 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);
例如,控制器可以从不同的线程 - 异步生成返回值,以响应外部事件(JMS消息)、计划任务或其他事件。
1.6.2. Callable
控制器可以使用java.util.concurent.Callable
包装任何受支持的返回值,如下面的示例所示:
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
1.6.3. Processing
下面是对Servlet异步请求处理的非常简洁的概述:
-
ServletRequest
可以通过调用quest.startAsync()
进入异步模式。这样做的主要效果是Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。 -
对
quest.startAsync()
的调用返回AsyncContext
,您可以使用它来进一步控制异步处理。例如,它提供了调度
方法,该方法类似于来自Servlet API的转发,不同之处在于它允许应用程序在Servlet容器线程上恢复请求处理。 -
ServletRequest
提供对当前DispatcherType
的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度程序类型。
DeferredResult
处理流程如下:
-
控制器返回一个
DeferredResult
,并将其保存在可以访问的某个内存队列或列表中。 -
Spring MVC调用
quest.startAsync()
。 -
同时,
DispatcherServlet
和所有配置的筛选器退出请求处理线程,但响应保持打开状态。 -
应用程序从某个线程设置
DeferredResult
,然后Spring MVC将请求分派回Servlet容器。 -
再次调用
DispatcherServlet
,并使用异步生成的返回值继续处理。
可调用
处理过程如下:
-
控制器返回一个可调用的
。
-
Spring MVC调用
quest.startAsync()
并将可调用的提交给
TaskExecutor
以便在单独的线程中进行处理。 -
同时,
DispatcherServlet
和所有过滤器退出Servlet容器线程,但响应保持打开状态。 -
最终,
可调用
产生一个结果,而Spring MVC将请求分派回Servlet容器以完成处理。 -
再次调用
DispatcherServlet
,并使用从可调用的中异步生成的返回值继续处理。
有关更多背景和背景,您还可以阅读介绍Spring MVC 3.2中的异步请求处理支持的博客文章。
Exception Handling
当您使用DeferredResult
时,您可以选择是调用setResult
还是调用setErrorResult
并引发异常。在这两种情况下,Spring MVC都会将请求分派回Servlet容器以完成处理。然后,它要么被视为控制器方法返回给定值,要么被视为生成给定异常。然后,异常通过常规异常处理机制(例如,调用@ExceptionHandler
方法)。
当您使用Callable
时,会出现类似的处理逻辑,主要区别在于结果是从Callable
返回,或者由它引发异常。
Interception
HandlerInterceptor
实例可以是AsyncHandlerInterceptor
类型,以在启动异步处理的初始请求上接收After ConcurentHandlingStarted
回调(而不是postHandle
和After Completion
)。
HandlerInterceptor
实现还可以注册一个CallableProcessingInterceptor
DeferredResultProcessingInterceptor
,>或一个HandlerInterceptor,以便与异步请求的生命周期更深入地集成(例如,处理超时事件)。有关更多详细信息,请参阅AsyncHandlerInterceptor
。
DeferredResult
提供onTimeout(Runnable)
和onCompletion(Runnable)
回调。有关更多详细信息,请参阅DeferredResult
的javadoc。Callable
可以替换为超时和完成回调公开附加方法的WebAsyncTask
。
Compared to WebFlux
Servlet API最初是为通过过滤器-Servlet链进行单次传递而构建的。异步请求处理允许应用程序退出筛选器-Servlet链,但保持响应开放以供进一步处理。Spring MVC异步支持就是围绕该机制构建的。当控制器返回DeferredResult
时,将退出过滤器-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 Streaming
您可以对单个异步返回值使用DeferredResult
和Callable
。如果您希望生成多个异步值并将这些值写入响应,该怎么办?本节介绍如何执行此操作。
Objects
您可以使用
HttpMessageConverter
>ResponseBodyEmitter返回值来生成对象流,其中每个对象都使用<代码序列化并写入到响应中,如下例所示:
@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();
您还可以使用ResponseBodyEmitter
作为ResponseEntity
的主体,允许您自定义响应的状态和头部。
当发射器
抛出IOException
(例如,如果远程客户端离开)时,应用程序不负责清理连接,并且不应调用emitter.Complete
或emitter.Complete WithError
。相反,Servlet容器会自动启动AsyncListener
错误通知,在该通知中,Spring MVC会进行Complete WithError
调用。这个调用又向应用程序执行最后一次ASYNC
分派,在此期间,Spring MVC调用已配置的异常解析器并完成请求。
SSE
SseEmitter
(ResponseBodyEmitter
的子类)提供对服务器发送的事件的支持,其中从服务器发送的事件按照W3CSSE规范进行格式化。要从控制器生成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();
虽然SSE是流到浏览器的主要选项,但请注意,Internet Explorer不支持服务器发送的事件。考虑将Spring的WebSocket消息与面向广泛浏览器的SockJS回退传输(包括SSE)一起使用。
有关异常处理的说明,另请参阅上一节。
Raw Data
有时,绕过消息转换而直接流到响应OutputStream
很有用(例如,对于文件下载)。您可以使用StreamingResponseBody
返回值类型来执行此操作,如下例所示:
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
您可以使用StreamingResponseBody
作为ResponseEntity
中的Body来自定义响应的状态和头部。
1.6.5. Reactive Types
Spring MVC支持在控制器中使用反应式客户端库(也可以阅读WebFlux部分中的反应式库)。这包括Spring-webflow
中的WebClient
和其他内容,如Spring data反应数据存储库。在这种情况下,能够从控制器方法返回反应类型是很方便的。
反应返回值的处理方法如下:
-
适用于单值承诺,类似于使用
DeferredResult
。例如Mono
(反应器)或Single
(RxJava)。 -
与使用
ResponseBodyEmitter
或SseEmitter
类似,适配流媒体类型的多值流(如应用程序/x-ndjson
或文本/事件流
)。例如通量
(反应堆)或可观测
(RxJava)。应用程序还可以返回Flux<;ServerSentEvent;
或Observable<;ServerSentEvent>;
. -
适用于具有任何其他媒体类型(如<代码>应用程序/JSON )的多值流,类似于使用
DeferredResult<;List<;?>;>;
.
Spring MVC supports Reactor and RxJava through the ReactiveAdapterRegistry from spring-core , which lets it adapt from multiple reactive libraries. |
1.6.6. Context Propagation
通过java.lang.ThreadLocal
传播上下文是很常见的。这对于在同一线程上的处理是透明的,但对于跨多个线程的异步处理则需要额外的工作。微米上下文传播库简化了跨线程和跨上下文机制(如线程本地值、反应器上下文、GraphQL Java<2“>上下文)的上下文传播。
如果类路径上存在微米上下文传播,则当控制器方法返回诸如Flux或Mono之类的反应性类型时,存在注册io.micrometer.ThreadLocalAccessor
,的所有线程本地值都将使用由线程局部访问器分配的键-值对写入到反应器上下文中。
对于其他异步处理场景,您可以直接使用上下文传播库。例如:
// Capture ThreadLocal values from the main thread ...
ContextSnapshot snapshot = ContextSnapshot.captureAll();
// On a different thread: restore ThreadLocal values
try (ContextSnapshot.Scope scoped = snapshot.setThreadLocals()) {
// ...
}
有关更多详细信息,请参阅千分尺上下文传播库的文档。
1.6.7. Disconnects
当远程客户端离开时,Servlet API不提供任何通知。因此,在流式传输到响应时,无论是通过SseEmitter还是通过反应类型,定期发送数据都很重要,因为如果客户端断开连接,写入就会失败。发送可以采用空(仅评论)SSE事件或任何其他数据的形式,对方将不得不将其解释为心跳并忽略这些数据。
或者,可以考虑使用具有内置心跳机制的Web消息传递解决方案(如Stopp over WebSocket或SockJS)。
1.6.8. Configuration
必须在Servlet容器级别启用异步请求处理功能。MVC配置还公开了几个用于异步请求的选项。
Servlet Container
筛选器和Servlet声明有一个asyncSupported
标志,需要将该标志设置为true
才能启用异步请求处理。此外,应该声明筛选器映射以处理ASYNC
jakarta.servlet.DispatchType
。
在Java配置中,当您使用AbstractAnnotationConfigDispatcherServletInitializer
初始化Servlet容器时,这是自动完成的。
在web.xml
配置中,您可以将<;async-supported>;true<;/async-supported>;
添加到DispatcherServlet
和筛选
声明中,并将<;dispatcher>;ASYNC<;/dispatcher>;
添加到筛选映射中。
Spring MVC
MVC配置公开了以下与异步请求处理相关的选项:
-
Java配置:在
WebMvcConfigurer
上使用configureAsyncSupport
回调。 -
XML命名空间:使用
<;mvc:Annotation-Driven>;
下的<;async-Support>;
元素。
您可以配置以下内容:
-
异步请求的默认超时值,如果未设置,则取决于基础Servlet容器。
-
AsyncTaskExecutor
,用于在使用反应类型流式传输时阻止写入,以及用于执行从控制器方法返回的可调用
实例。我们强烈建议,如果您使用反应类型流或具有返回Callable
的控制器方法,则配置此属性,因为在默认情况下,它是一个SimpleAsyncTaskExecutor
。 -
DeferredResultProcessingInterceptor
实现和CallableProcessingInterceptor
实现。
注意,您还可以在DeferredResult
、ResponseBodyEmitter
和SseEmitter
上设置默认超时值。对于可调用的,可以使用
WebAsyncTask
提供超时值。
1.7. CORS
Spring MVC允许您处理CORS(跨域资源共享)。本节介绍如何执行此操作。
1.7.1. Introduction
出于安全原因,浏览器禁止对当前来源之外的资源进行AJAX调用。例如,您可以将银行账户放在一个选项卡中,而将evil.com放在另一个选项卡中。来自evil.com的脚本应该不能使用您的凭据 - 向您的银行API发出AJAX请求,例如从您的帐户中提取资金!
1.7.2. Processing
CORS规范区分了印前检查、简单请求和实际请求。要了解CORS的工作原理,您可以阅读本文,或查看规范以了解更多详细信息。
Spring MVCHandlermap
实现提供了对CORS的内置支持。在成功地将请求映射到处理程序之后,Handlermap
实现将检查给定请求和处理程序的CORS配置,并采取进一步的操作。印前检查请求被直接处理,而简单和实际的CORS请求被拦截、验证,并设置了所需的CORS响应头。
为了启用跨域请求(即存在Origin
头部,且与请求的host不同),您需要有一些显式声明的CORS配置。如果未找到匹配的CORS配置,则会拒绝印前检查请求。没有CORS头添加到简单和实际的CORS请求的响应中,因此,浏览器会拒绝它们。
可以使用基于URL模式的CorsConfiguration
映射单独配置每个HandlerMap
。在大多数情况下,应用程序使用MVC Java配置或XML命名空间来声明此类映射,这会导致将单个全局映射传递给所有Handlermap
实例。
您可以将Handlermap
级别的全局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) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
-
都是起源。
-
所有标题。
-
控制器方法映射到的所有HTTP方法。
AllowCredentials
默认情况下未启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(如Cookie和CSRF令牌),并且只应在适当的情况下使用。启用时,必须将AllowOrigins
设置为一个或多个特定域(但不是特殊值“*”
),或者可以使用AllowOriginPatterns
属性来匹配一组动态来源。
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
,如下面的示例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
1.7.4. Global Configuration
除了细粒度的控制器方法级配置之外,您可能还希望定义一些全局CORS配置。您可以在任何HandlerMap
上分别设置基于URL的CorsConfiguration
映射。然而,大多数应用程序使用MVC Java配置或MVC XML命名空间来实现这一点。
默认情况下,全局配置启用以下功能:
-
都是起源。
-
所有标题。
-
GET
、Head
和POST
方法。
AllowCredentials
默认情况下未启用,因为这会建立一个信任级别,该级别会公开敏感的用户特定信息(如Cookie和CSRF令牌),并且只应在适当的情况下使用。启用时,必须将AllowOrigins
设置为一个或多个特定域(但不是特殊值“*”
),或者可以使用AllowOriginPatterns
属性来匹配一组动态来源。
MaxAge
设置为30分钟。
Java Configuration
要在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...
}
}
XML Configuration
要在XML命名空间中启用CORS,可以使用<;mvc:cos>;
元素,如下例所示:
<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 Filter
您可以通过内置的CorsFilter
来申请CORS支持。
If you try to use the CorsFilter with Spring Security, keep in mind that Spring Security has built-in support for 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);
1.8. Error Responses
REST服务的一个常见要求是在错误响应正文中包含详细信息。Spring框架支持RFC7807的“HTTAPIs问题详细信息”规范。
以下是此支持的主要抽象:
-
ProblemDetail
RFC7807问题详细信息的 - 表示形式;规范中定义的标准字段和非标准字段的简单容器。 -
ErrorResponse
- 约定公开HTTp错误响应详细信息,包括HTTp状态、响应头和RFC7807格式的正文;这允许异常封装和公开它们如何映射到HTTp响应的详细信息。所有的Spring MVC异常都实现了这一点。 -
<代码>错误响应异常 - Basic<代码>错误响应实现,其他人可以将其用作方便的基类。
-
@ControllerAdvice的
ResponseEntityExceptionHandler
— convenient基类,它处理所有Spring MVC异常和任何ErrorResponseException
,并呈现一个带有正文的错误响应。
1.8.1. Render
您可以从任何@ExceptionHandler
或任何@Requestmap
方法返回ProblemDetail
或ErrorResponse
,以呈现RFC 7807响应。其处理过程如下:
-
ProblemDetail
的Status
属性确定HTTP状态。 -
ProblemDetail
的实例
属性是从当前URL路径设置的,如果尚未设置的话。 -
对于内容协商,在呈现
ProblemDetail
时,JacksonHttpMessageConverter
优先选择应用程序/问题+json而不是应用程序/json,如果找不到兼容的媒体类型,也会求助于它。
要为Spring WebFlux异常和任何ErrorResponseException
启用RFC 7807响应,请扩展ResponseEntityExceptionHandler
,并在Spring配置中将其声明为@ControllerAdvice。该处理程序有一个@ExceptionHandler
方法,用于处理任何ErrorResponse
异常,其中包括所有内置的Web异常。您可以添加更多异常处理方法,并使用受保护的方法将任何异常映射到ProblemDetail
。
1.8.2. Non-Standard Fields
您可以通过以下两种方式之一使用非标准字段扩展RFC 7807响应。
一、插入到ProblemDetail
的“属性”映射
中。当使用Jackson库时,Spring框架注册ProblemDetailJacksonMixin
,以确保这个“属性”Map
被解包并呈现为响应中的顶级JSON属性,同样,在反序列化过程中的任何未知属性都被插入到这个Map
中。
您还可以扩展ProblemDetail
以添加专用的非标准属性。ProblemDetail
中的复制构造函数允许从现有的ProblemDetail
创建一个子类。这可以例如从诸如ResponseEntityExceptionHandler
之类的@ControllerAdvice
集中完成,该将异常的
ProblemDetail
重新创建到具有附加非标准字段的子类中。
1.8.3. Internationalization
国际化错误响应细节是一种常见的需求,而定制Spring MVC异常的问题细节是一种良好的实践。其支持方式如下:
-
每个
ErrorResponse
都公开一个消息代码和参数,以通过MessageSource解析“Detail”字段。实际的消息代码值使用占位符进行参数化,例如“不支持的HTTP方法{0}”
从参数展开。 -
每个
ErrorResponse
还公开一个消息代码来解析“标题”字段。 -
ResponseEntityExceptionHandler
使用消息代码和参数来解析“详细信息”和“标题”字段。
默认情况下,“Detail”字段的消息代码是“problemDetail.”+完全限定的异常类名。某些异常可能会公开其他消息代码,在这种情况下,会在默认消息代码中添加后缀。下表列出了Spring MVC异常的消息参数和代码:
Exception | Message Code | Message Code Arguments |
---|---|---|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(默认)+“.parseError” |
|
|
(违约) |
|
|
(默认)+“.parseError” |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
|
(违约) |
|
默认情况下,“TITLE”字段的消息代码是“problemDetail.title.”+完全限定的异常类名。
1.9. Web Security
Spring Security项目为保护Web应用程序免受恶意攻击提供支持。请参阅Spring安全参考文档,其中包括:
HDIV是另一个集成了Spring MVC的Web安全框架。
1.10. HTTP Caching
HTTP缓存可以显著提高Web应用程序的性能。HTTP缓存围绕缓存-控制
响应头,随后是条件请求头(如Last-Modify
和Etag
)。缓存控制
建议私有(例如浏览器)和公共(例如代理)缓存如何缓存和重用响应。Etag
头用于进行条件请求,如果内容没有更改,则可能导致304(NOT_MODIFIED)没有正文。Etag
可以被视为Last-Modify
头的更复杂的继承者。
本节描述了Spring Web MVC中可用的与HTTP缓存相关的选项。
1.10.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();
WebContent Generator
还接受一个更简单的cachePeriod
属性(以秒为单位定义),其工作原理如下:
-
-1
值不会生成缓存控制
响应头。 -
0
值使用‘Cache-Control:no-store’
指令阻止缓存。 -
n>;0
值使用‘Cache-Control:max-age=n’
指令将给定响应缓存了n
秒。
1.10.2. Controllers
控制器可以添加对HTTP缓存的显式支持。我们建议这样做,因为需要计算资源的lastModified
或Etag
值,然后才能将其与条件请求头部进行比较。控制器可以将Etag
头和缓存-控制
设置添加到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);
}
1 | Application-specific calculation. |
2 | The response has been set to 304 (NOT_MODIFIED) — no further processing. |
3 | Continue with the request processing. |
如果与条件请求头的比较表明内容没有更改,则前面的示例发送带有空正文的304(NOT_MODIFIED)响应。否则,Etag
和Cache-Control
头被添加到响应中。
您还可以在控制器中对条件请求头进行检查,如下面的示例所示:
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ... (1)
if (request.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
1 | Application-specific calculation. |
2 | The response has been set to 304 (NOT_MODIFIED) — no further processing. |
3 | Continue with the request processing. |
有三种变量用于根据eTag
值和/或lastModified
值检查条件请求。对于有条件的GET
和Head
请求,可以将响应设置为304(NOT_MODIFIED)。对于有条件的POST
、PUT
和DELETE
,您可以将响应设置为412(Predition_FAILED),以防止并发修改。
1.10.4. ETag
Filter
您可以使用ShallowEtag HeaderFilter
来添加根据响应内容计算出的“浅”eTag
值,从而节省带宽但不会节省CPU时间。请参阅浅层标签。
1.11. View Technologies
在Spring MVC中使用视图技术是可插拔的。您是否决定使用Thymeleaf、Groovy标记模板、JSP或其他技术,这主要是一个配置更改的问题。本章介绍了与Spring MVC集成的视图技术。我们假定您已经熟悉查看分辨率。
The views of a Spring MVC application live within the internal trust boundaries of that application. Views have access to all the beans of your application context. As such, it is not recommended to use Spring MVC’s template support in applications where the templates are editable by external sources, since this can have security implications. |
1.11.1. Thymeleaf
Thymeleaf是一个现代的服务器端Java模板引擎,它强调了可以通过双击在浏览器中预览的自然HTML模板,这对于独立处理UI模板(例如,由设计师)非常有用,而不需要运行的服务器。如果您想要取代JSP,Thymeleaf提供了一组最广泛的特性之一,可以使这种转换变得更容易。百里香叶是积极发展和维护的。有关更完整的介绍,请参阅Thymeleaf项目主页。
Thymeleaf与Spring MVC的集成由Thymeleaf项目管理。配置涉及几个Bean声明,如ServletContextTemplateResolver
、SpringTemplateEngine
和ThymeleafViewResolver
。有关更多详细信息,请参阅百里香+弹簧。
1.11.2. FreeMarker
Apache FreeMarker是一个模板引擎,用于生成从HTML到电子邮件等的任何类型的文本输出。Spring框架具有内置的集成,可以将Spring MVC与FreeMarker模板结合使用。
View Configuration
以下示例显示如何将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;
}
}
以下示例显示如何在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
指定的目录中。根据前面的配置,如果您的控制器返回一个欢迎
的视图名称,那么解析器将查找/WEB-INF/freemarker/e.ftl
模板。
FreeMarker Configuration
通过在FreeMarkerConfigurer
Bean上设置适当的Bean属性,可以将FreeMarker的“设置”和“SharedVariables”直接传递给FreeMarker配置
对象(由Spring管理)。freemarkerSetting
属性需要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文档。
Form Handling
Spring提供了一个在JSP中使用的标记库,其中包含一个<;Spring:Bind/>;
元素。该元素主要允许表单显示来自表单支持对象的值,并显示来自Web或业务层中的验证器
的失败验证的结果。Spring还支持在FreeMarker中使用相同的功能,并增加了用于生成表单输入元素本身的便利宏。
The Bind Macros
在FreeMarker的Spring-webmvc.jar
文件中维护了一组标准的宏,因此它们始终可用于适当配置的应用程序。
在Spring模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的作用域,从而使所有的宏对调用代码和用户模板都可见。以下部分仅集中介绍需要从模板内直接调用的宏。如果您希望直接查看宏代码,则该文件名为spring.ftl
,位于org.springframework.web.servlet.view.freemarker
包中。
Simple Binding
在基于作为Spring MVC控制器的表单视图的FreeMarker模板的HTML表单中,您可以使用类似于下一个示例的代码来绑定到字段值,并以与JSP等效项类似的方式显示每个输入字段的错误消息。下面的示例显示Personal Form
视图:
<!-- 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.ind>;
需要一个‘Path’参数,它由命令对象的名称(它是‘Command’,除非您在控制器配置中更改了它)后跟句点和要绑定到的命令对象上的字段的名称组成。您还可以使用嵌套字段,例如命令.地址.街道
。绑定
宏采用由web.xml
中的ServletContext
参数defaultHtmlEscape
指定的默认HTML转义行为。
宏的另一种形式<;@spring.bindEscaped>;
接受第二个参数,该参数显式指定是否应在状态错误消息或值中使用HTML转义。您可以根据需要设置为True
或False
。附加的表单处理宏简化了HTML转义的使用,您应该尽可能使用这些宏。它们将在下一节中进行解释。
Input Macros
另外,FreeMarker的方便宏简化了绑定和表单生成(包括验证错误显示)。从来不需要使用这些宏来生成表单输入字段,您可以将它们与简单的HTML混合搭配,或者直接调用前面突出显示的Spring BIND宏。
下表显示了可用宏的FreeMarker模板(FTL)定义和每个宏所采用的参数列表:
macro | FTL definition |
---|---|
|
<;@spring.Message code/>; |
|
<;@spring.MessageText code,Text/>; |
|
<;@spring.url relativeUrl/>; |
|
<;@spring.form输入路径、属性、fieldType/>; |
|
<;@spring.formHiddenInput路径,属性/>; |
|
<;@spring.formPasswordInput路径,属性/>; |
|
<;@spring.form文本区域路径,属性/>; |
|
<;@spring.formSingle选择路径、选项、属性/>; |
|
<;@spring.form多选路径、选项、属性/>; |
|
<;@spring.formRadioButton路径、选项分隔符、属性/>; |
|
<;@spring.form复选框路径、选项、分隔符、属性/>; |
|
<;@spring.formCheckbox路径,属性/>; |
|
<;@spring.showErrors分隔符,classOrStyle/>; |
In FreeMarker templates, formHiddenInput and formPasswordInput are not actually required, as you can use the normal formInput macro, specifying hidden or password as the value for the fieldType parameter. |
上述任何宏的参数都具有一致的含义:
-
路径
:要绑定的字段的名称(如“命令名”) -
选项
:可以在输入字段中选择的所有可用值的映射
。映射键表示从表单回发并绑定到命令对象的值。针对键存储的地图对象是在表单上向用户显示的标签,并且可能不同于表单回发的相应值。通常,这样的地图由控制器作为参考数据提供。您可以使用任何Map
实现,具体取决于所需的行为。对于严格排序的映射,您可以将SortedMap
(如treemmap
)与适当的比较器
一起使用,而对于应该以插入顺序返回值的任意映射,可以使用LinkedHashMap
或Commons-Collection
中的LinkedMap
。 -
分隔符
:当有多个选项作为离散元素(单选按钮或复选框)时,用于分隔列表中每个选项的字符序列(如<;br>;
)。 -
属性
:要包含在HTML标记本身中的任意标记或文本的附加字符串。此字符串在字面上由宏回显。例如,在文本区域
字段中,您可以提供属性(如‘row=“5”oles=“60”’),也可以传递诸如‘style=“borde:1px纯银”’之类的样式信息。 -
classOrStyle
:对于showErrors
宏,是包装每个错误的span
元素使用的CSS类的名称。如果没有提供信息(或者值为空),则错误被包装在<;b>;<;/b>;
标记中。
以下各节概述了宏的示例。
formInput
宏接受路径
参数(命令名
)和一个附加的属性
参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一起,在Path参数上执行隐式的Spring绑定。绑定在发生新绑定之前保持有效,因此showErrors
宏不需要再次传递 - 参数,它对上次为其创建绑定的字段进行操作。
showErrors
宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),并接受第二个参数 - ,这一次是类名或样式属性。请注意,FreeMarker可以为Attributes参数指定默认值。下面的示例说明如何使用formInput
和showErrors
宏:
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>
下一个示例显示了表单片段的输出,生成了名称字段,并在提交了没有该字段中的值的表单后显示了一个验证错误。验证通过Spring的验证框架进行。
生成的HTML类似于下面的示例:
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>
formTextarea
宏与formInput
宏的工作方式相同,并接受相同的参数列表。通常,第二个参数(属性
)用于传递样式信息或文本区域
的行
和cols
属性。
您可以使用四个选择域宏在您的HTML表单中生成常见的UI值选择输入:
-
formSingleSelect
-
form多选
-
Form单选按钮
-
Form Checkbox
这四个宏中的每一个都接受一个Map
选项,其中包含表单域的值以及与该值相对应的标签。值和标签可以相同。
下一个示例是FTL中的单选按钮。表单支持对象为该字段指定了默认值‘London’,因此不需要进行验证。当呈现表单时,可供选择的整个城市列表将作为参考数据提供给模型中的名称‘cityMap’。下面的清单显示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
上面的清单呈现了一行单选按钮,cityMap
中的每个值对应一个单选按钮,并使用分隔符“”
。未提供其他属性(缺少宏的最后一个参数)。cityMap
为映射中的每个键-值对使用相同的字符串
。映射的键是表单实际作为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;
}
代码现在生成的输出中,单选项值是相关代码,但用户仍会看到更便于用户使用的城市名称,如下所示:
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 Escaping
前面描述的表单宏的缺省使用将导致符合HTML4.01并且使用web.xml
文件中定义的HTML转义缺省值的HTML元素,就像Spring的绑定支持所使用的那样。要使元素符合XHTML或覆盖默认的HTML转义值,可以在模板中(或在模板可见的模型中)指定两个变量。在模板中指定它们的好处是可以在稍后的模板处理中将它们更改为不同的值,以便为表单中的不同域提供不同的行为。
若要切换到标记的XHTML合规性,请为名为xhtmlComplants
的模型或上下文变量指定值True
,如下例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
在处理该指令之后,由Spring宏生成的任何元素现在都是XHTML兼容的。
以类似的方式,您可以为每个字段指定HTML转义,如下例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
1.11.3. Groovy Markup
Groovy标记模板引擎主要用于生成类似于XML的标记(XML、XHTML、HTML5等),但您可以使用它来生成任何基于文本的内容。Spring框架有一个内置的集成,可以使用带有Groovy标记的Spring MVC。
The Groovy Markup Template engine requires Groovy 2.3.1+. |
Configuration
下面的示例显示如何配置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;
}
}
以下示例显示如何在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.11.4. Script Views
Spring框架有一个内置的集成,可以将Spring MVC与任何可以在JSR-223Java脚本引擎上运行的模板库一起使用。我们已经在不同的脚本引擎上测试了以下模板库:
Scripting Library | Scripting Engine |
---|---|
The basic rule for integrating any other script engine is that it must implement the ScriptEngine and Invocable interfaces. |
Requirements
您的类路径上需要有脚本引擎,其详细信息因脚本引擎而异:
-
NashornJavaScript引擎随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脚本。有关详细信息,请参阅此示例。
您需要有脚本模板库。实现这一点的一种方法是通过WebJars。
Script Templates
您可以声明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;
}
}
下面的示例显示了在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";
}
}
下面的示例显示了Mustache模板:
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
使用以下参数调用Render函数:
-
字符串模板
:模板内容 -
映射模型
:视图模型 -
RenderingContext renderingContext
:RenderingContext
,用于访问应用程序上下文、区域设置、模板加载器和URL(从5.0开始)
Mustahe.render()
与该签名原生兼容,可以直接调用。
如果您的模板技术需要一些定制,您可以提供一个实现定制呈现功能的脚本。例如,Handlerbar需要在使用模板之前编译它们,并且需要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;
}
}
Setting the sharedEngine property to false is required when using non-thread-safe script engines with templating libraries not designed for concurrency, such as Handlebars or React running on Nashorn. In that case, Java SE 8 update 60 is required, due to this bug, but it is generally recommended to use a recent Java SE patch release in any case. |
Polyfit.js
仅定义Handlebar正常运行所需的Window
对象,如下所示:
var window = {};
这个基本的render.js
实现在使用模板之前对其进行编译。生产就绪的实现还应该存储任何重复使用的缓存模板或预编译模板。您可以在脚本端这样做(并处理您需要的任何定制,例如, - 管理模板引擎配置)。以下示例显示了如何执行此操作:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.11.5. JSP and JSTL
Spring框架具有内置的集成,可以将Spring MVC与JSP和JSTL结合使用。
View Resolvers
当使用JSP进行开发时,您通常声明一个InternalResourceViewResolver
Bean。
InternalResourceViewResolver
可用于分派到任何Servlet资源,但特别适用于JSP。作为最佳实践,我们强烈建议将您的JSP文件放在‘WEB-INF’
目录下的目录中,这样客户端就不会直接访问。
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
Spring’s JSP Tag Library
Spring提供了请求参数到命令对象的数据绑定,如前面几章所述。为了便于结合这些数据绑定功能开发JSP页面,Spring提供了一些标记,使事情变得更加简单。所有的Spring标记都有用于启用或禁用字符转义的HTML转义功能。
spring.tld
标记库描述符(TLD)包含在Spring-webmvc.jar
中。有关单个标签的全面参考,请浏览API参考或查看标签库说明。
Spring’s form tag library
从2.0版开始,Spring提供了一套全面的数据绑定感知型标记,用于在使用JSP和Spring Web MVC时处理表单元素。每个标记都支持其对应的HTML标记的属性集,从而使标记更熟悉、更直观地使用。标记生成的HTML与HTML4.01/XHTML1.0兼容。
与其他表单/输入标记库不同,Spring的表单标记库与Spring Web MVC集成在一起,允许标记访问控制器处理的命令对象和引用数据。如我们在下面的示例中所示,form标记使JSP更易于开发、阅读和维护。
我们将浏览表单标记,并查看如何使用每个标记的示例。我们已经包含了生成的HTML代码片段,其中某些标记需要进一步的注释。
Configuration
表单标记库捆绑在Spring-webmvc.jar
中。库描述符称为Spring-form.tld
。
要使用该库中的标记,请将以下指令添加到您的JSP页面的顶部:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中Form
是要用于此库中的标记的标记名前缀。
The Form Tag
该标记呈现一个HTML‘form’元素,并向内部标记公开绑定路径以进行绑定。它将命令对象放在PageContext
中,以便可以通过内部标记访问命令对象。此库中的所有其他标记都是表单
标记的嵌套标记。
假设我们有一个名为User
的域对象。它是一个具有名字
和姓氏
属性的Java Bean。我们可以将其用作表单控制器的表单支持对象,该对象返回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: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>
The input
Tag
默认情况下,此标记呈现具有绑定值和type=‘Text’
的HTML输入
元素。有关此标记的示例,请参阅表单标记。您还可以使用HTML5特定的类型,如电子邮件
、电话
、日期
等。
The checkbox
Tag
此标记呈现一个type
设置为复选框
的HTML输入
标记。
假设我们的用户
有订阅时事通讯和爱好列表等首选项。下面的示例显示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;
}
}
相应的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
标记,它们应该可以满足您所有的CheckBox需求。
-
方法一:当绑定值的类型为
java.lang.Boolean
时,如果绑定值为true
,则输入(复选框)
标记为选中
。值
属性对应于setValue(对象)
值属性的解析值。 -
方法二:当绑定值是
数组
或java.util.Collection
类型时,如果绑定的集合
中存在配置的setValue(Object)
值,则输入(CheckBox)
标记为选中
。 -
方法三:对于任何其他绑定值类型,如果配置的
setValue(对象)
等于绑定值,则输入(复选框)
被标记为选中
。
请注意,无论采用哪种方法,都会生成相同的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“复选框在表单中是可见的,我希望表单数据绑定到的对象反映复选框的状态,不管是什么情况。”
The checkboxes
Tag
此标记呈现多个类型
设置为复选框
的HTML输入
标记。
本节以前面复选框
标记部分中的示例为基础。有时,您希望不必在您的JSP页面中列出所有可能的爱好。您宁愿在运行时提供可用选项的列表,并将其传递给标记。这就是复选框
标记的用途。您可以传入数组
、列表
或映射
,其中包含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>
本例假定interest List
是一个list
,可用作模型属性,其中包含要从中选择的值的字符串。如果使用Map
,则使用映射条目键作为值,并将映射条目的值用作要显示的标签。您还可以使用自定义对象,其中可以通过使用itemValue
为值提供属性名称,通过使用itemLabel
提供标签。
The radiobutton
Tag
此标记呈现一个类型
设置为单选
的HTML输入
元素。
典型的使用模式涉及多个绑定到同一属性但具有不同值的Tag实例,如下例所示:
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
The radiobuttons
Tag
此标记呈现多个类型
设置为单选
的HTML输入
元素。
与复选框
标记一样,您可能希望将可用选项作为运行时变量传递。对于这种用法,您可以使用单选按钮
标记。传入一个数组
、一个列表
或一个映射
,其中包含Items
属性中的可用选项。如果使用Map
,则使用映射条目键作为值,并将映射条目的值用作要显示的标签。您还可以使用自定义对象,在该对象中可以通过使用itemValue
来为值提供属性名称,并通过使用itemLabel
来提供标签,如下例所示:
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
The password
Tag
该标记使用绑定值呈现一个类型设置为password
的HTML输入
标记。
<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>
The select
Tag
此标记呈现一个HTML‘SELECT’元素。它支持将数据绑定到所选选项以及使用嵌套的选项
和选项
标记。
假设用户
有一个技能列表。对应的HTML可能如下所示:
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果用户的
技能是草药专业的,则‘Skills’行的HTML源可能如下所示:
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
The option
Tag
此标记呈现一个HTML选项
元素。它根据绑定值设置选定
。下面的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>
如果用户的
住宅位于Gryffindor,则‘House’行的HTML源如下所示:
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
1 | Note the addition of a selected attribute. |
The options
Tag
此标记呈现一组HTML选项
元素。它根据绑定值设置选定
属性。下面的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>
如果用户
居住在英国,‘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 | Note the addition of a selected attribute. |
如前面的示例所示,选项
标记与选项
标记的组合使用会生成相同的标准HTML,但允许您在JSP中显式指定一个仅用于显示的值(它所属的位置),例如示例中的默认字符串:“--请选择”。
Items
属性通常由Item对象的集合或数组填充。itemValue
和itemLabel
引用这些Item对象的Bean属性(如果指定)。否则,Item对象本身就会变成字符串。或者,您可以指定项目的Map
,在这种情况下,映射键被解释为选项值,而映射值对应于选项标签。如果同时指定了itemValue
或itemLabel
(或两者都指定),则Item Value属性将应用于映射键,Item Label属性将应用于映射值。
The textarea
Tag
该标记呈现一个HTML文本区域
元素。下面的HTML显示了它的典型输出:
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
The hidden
Tag
此标记呈现具有绑定值的类型
设置为隐藏
的HTML输入
标记。若要提交未绑定的隐藏值,请使用类型
设置为隐藏
的HTML输入
标记。下面的HTML显示了它的典型输出:
<form:hidden path="house"/>
如果我们选择将house
值作为隐藏值提交,则HTML将如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
The errors
Tag
此标记在HTMLspan
元素中呈现字段错误。它提供对控制器中创建的错误或与控制器关联的任何验证器创建的错误的访问。
假设我们希望在提交表单后显示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.");
}
}
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
字段相关的所有错误。 -
如果省略
路径
,则只显示对象错误。
下面的示例在页面顶部显示错误列表,后跟字段旁边的特定于字段的错误:
<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 Method Conversion
REST的一个关键原则是使用“统一接口”。这意味着可以使用相同的四个HTTP方法来操作所有资源(URL):GET、PUT、POST和DELETE。对于每种方法,HTTP规范都定义了确切的语义。例如,GET应该始终是安全的操作,这意味着它没有副作用,而PUT或DELETE应该是幂等的,这意味着您可以反复重复这些操作,但最终结果应该是相同的。虽然HTTP定义了这四种方法,但HTML只支持两种:GET和POST。幸运的是,有两种可能的解决方法:您可以使用JavaScript来执行PUT或DELETE,或者您可以使用“Real”方法作为附加参数(建模为HTML表单中的隐藏输入字段)来执行POST。Spring的HiddenHttpMethodFilter
使用后一种技巧。这个过滤器是一个普通的Servlet过滤器,因此,它可以与任何Web框架(不仅仅是Spring MVC)结合使用。将此过滤器添加到您的web.xml中,带有隐藏的方法
参数的POST将被转换为相应的HTTP方法请求。
为了支持HTTP方法转换,对Spring MVC Form标记进行了更新,以支持设置HTTP方法。例如,以下代码片段来自Pet Clinic示例:
<form:form method="delete">
<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>
前面的示例执行一个HTTP POST,并将“真正的”删除方法隐藏在请求参数之后。它由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>
下面的示例显示了相应的@控制器
方法:
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
this.clinic.deletePet(petId);
return "redirect:/owners/" + ownerId;
}
1.11.6. Tiles
您可以将Tiles集成到使用Spring的Web应用程序中,就像任何其他视图技术一样。本节概括性地描述了如何做到这一点。
This section focuses on Spring’s support for Tiles version 3 in the org.springframework.web.servlet.view.tiles3 package. |
Dependencies
为了能够使用Tiles,您必须向项目添加对Tiles版本3.0.1或更高版本的依赖项及其传递依赖项。
Configuration
要使用切片,必须使用包含定义的文件对其进行配置(有关定义和其他切片概念的基本信息,请参阅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
时,加载文件,并初始化定义工厂。完成此操作后,定义文件中包含的平铺可以用作您的Spring web应用程序中的视图。为了能够使用这些视图,您必须拥有一个ViewResolver
,就像Spring中的任何其他视图技术一样:通常是一个方便的TilesViewResolver
。
您可以通过添加下划线,然后添加区域设置来指定特定于区域设置的磁贴定义,如下例所示:
<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
。
Since underscores are used to indicate locales, we recommended not using them otherwise in the file names for Tiles definitions. |
UrlBasedViewResolver
UrlBasedViewResolver
为它必须解析的每个视图实例化给定的viewClass
。以下Bean定义UrlBasedViewResolver
:
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
</bean>
SimpleSpringPreparerFactory
and SpringBeanPreparerFactory
作为一项高级功能,Spring还支持两个特殊的TilepreparerFactory
实现。有关如何在Tiles定义文件中使用ViewPrepeller
引用的详细信息,请参阅Tiles文档。
您可以指定SimpleSpringPreparerFactory
根据指定的预备器类自动绑定ViewPrepeller
实例,同时应用Spring的容器回调和已配置的Spring BeanPostProcessors。如果已经激活了Spring的上下文范围的批注配置,则会自动检测并应用ViewPrepeller
类中的批注。注意,这需要在Tiles定义文件中包含预备器类,就像默认的preparerFactory
所做的那样。
您可以指定SpringBeanPrepreparerFactory
对指定的预备者名称(而不是类)进行操作,从DispatcherServlet的应用程序上下文中获取相应的Spring Bean。在本例中,完整的Bean创建过程由Spring应用程序上下文控制,允许使用显式依赖项注入配置、作用域Bean等。请注意,您需要为每个预备者名称定义一个Spring Bean定义(与您的Tiles定义中使用的一样)。下面的示例显示如何在TilesConfigurer
Bean上定义SpringBeanPrepreparerFactory
属性:
<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.11.7. RSS and Atom
AbstractAir FeedView
和AbstractRssFeedView
都继承自AbstractFeedView
基类,分别用于提供Atom和RSS Feed视图。它们基于罗马项目,位于包org.springframework.web.servlet.view.feed
.中
AbstractAir FeedView
要求您实现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
}
}
实现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
}
}
BuildFeedItems()
和BuildFeedEntry()
方法传入HTTP请求,以防您需要访问区域设置。仅针对Cookie或其他HTTP头的设置传入HTTP响应。在方法返回后,提要会自动写入响应对象。
有关创建Atom视图的示例,请参阅Alef Arendsen的Spring团队博客条目。
1.11.8. PDF and Excel
Spring提供了返回除HTML之外的输出的方法,包括PDF和Excel电子表格。本节介绍如何使用这些功能。
Introduction to Document Views
对于用户来说,查看模型输出的最佳方式并不总是使用HTML页面,而Spring使从模型数据动态生成PDF文档或Excel电子表格变得非常简单。文档就是视图,并使用正确的内容类型从服务器流传输,以(希望)使客户端PC能够运行其电子表格或PDF查看器应用程序作为响应。
为了使用Excel视图,您需要将Apache POI库添加到类路径中。对于PDF生成,您需要添加(最好是)OpenPDF库。
You should use the latest versions of the underlying document-generation libraries, if possible. In particular, we strongly recommend OpenPDF (for example, OpenPDF 1.2.12) instead of the outdated original iText 2.1.7, since OpenPDF is actively maintained and fixes an important vulnerability for untrusted PDF content. |
PDF Views
单词列表的简单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));
}
}
}
控制器可以从外部视图定义(通过名称引用它)返回这样的视图,也可以作为处理程序方法的View
实例返回。
1.11.9. Jackson
Spring提供对Jackson JSON库的支持。
Jackson-based JSON MVC Views
MappingJackson2JsonView
使用Jackson库的ObjectMapper
将响应内容呈现为JSON。默认情况下,模型映射的全部内容(特定于框架的类除外)都编码为JSON。对于需要过滤映射内容的情况,您可以使用ModelKeys
属性指定要编码的一组特定模型属性。您还可以使用fettValueFromSingleKeyModel
属性直接提取和序列化单键模型中的值,而不是将其作为模型属性的映射。
您可以使用Jackson提供的注释根据需要定制JSON映射。当您需要进一步控制时,您可以通过ObjectMapper
属性注入一个自定义的对象映射器
,用于需要为特定类型提供自定义JSON序列化程序和反序列化程序的情况。
Jackson-based XML Views
MappingJackson2XmlView
使用Jackson XML扩展的XmlMapper
将响应内容呈现为XML。如果模型包含多个条目,则应该使用Model Key
Bean属性显式设置要序列化的对象。如果模型包含单个条目,则会自动将其序列化。
您可以使用JAXB或Jackson提供的注释根据需要定制XML映射。当您需要进一步控制时,您可以通过ObjectMapper
属性注入一个自定义XmlMapper
,用于自定义XML需要为特定类型提供序列化和反序列化的情况。
1.11.10. XML Marshalling
MarshallingView
使用XMLMarshaller
(在org.springFrawork.oxm
包中定义)将响应内容呈现为XML。您可以使用MarshallingView
实例的Model Key
Bean属性显式设置要封送的对象。或者,该视图遍历所有模型属性并封送封送程序
支持的第一个类型。有关org.springFrawork.oxm
包中功能的更多信息,请参阅使用O/X映射器编组XML。
1.11.11. XSLT Views
XSLT是一种XML转换语言,作为Web应用程序中的一种视图技术很受欢迎。如果您的应用程序自然地处理XML,或者如果您的模型可以很容易地转换为XML,那么作为一种视图技术,XSLT可能是一个很好的选择。以下部分将展示如何将XML文档作为模型数据生成,并在Spring Web MVC应用程序中使用XSLT对其进行转换。
这个例子是一个简单的Spring应用程序,它在控制器
中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及我们的XSLT视图的视图名称。有关Spring Web MVC的控制器
接口的详细信息,请参阅带注释的控制器。XSLT控制器将单词列表转换为简单的可供转换的XML文档。
Beans
配置是简单的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;
}
}
Controller
我们还需要一个控制器来封装我们的单词生成逻辑。
控制器逻辑封装在@Controller
类中,处理程序方法定义如下:
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
到目前为止,我们只创建了一个DOM文档并将其添加到模型映射中。请注意,您还可以将XML文件作为资源
加载并使用它,而不是使用自定义DOM文档。
有一些软件包可以自动“控制”对象图,但在Spring中,您可以完全灵活地以您选择的任何方式从您的模型创建DOM。这可以防止XML转换在模型数据的结构中扮演太重要的角色,这在使用工具管理DOM化过程时是危险的。
Transformation
最后,XsltViewResolver
解析“home”XSLT模板文件,并将DOM文档合并到其中以生成我们的视图。如XsltViewResolver
配置中所示,XSLT模板位于WEB-INF/xsl
目录的war
文件中,并以XSLT
文件扩展名结尾。
下面的示例显示了一个XSLT转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
前面的转换将呈现为以下HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
1.12. MVC Config
MVC Java配置和MVC XML命名空间提供了适用于大多数应用程序的默认配置以及用于对其进行自定义的配置API。
1.12.1. Enable MVC Configuration
在Java配置中,您可以使用@EnableWebMvc
注释来启用MVC配置,如下例所示:
@Configuration
@EnableWebMvc
public 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.12.2. MVC Config API
在Java配置中,您可以实现WebMvcConfigurer
接口,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
在XML中,您可以检查<;mvc:Annotation-Driven/>;
的属性和子元素。您可以查看Spring MVC XML模式,或者使用您的IDE的代码完成功能来发现哪些属性和子元素可用。
1.12.3. Type Conversion
默认情况下,安装了各种数字和日期类型的格式化程序,并支持通过@NumberFormat
和@DateTimeFormat
对字段进行自定义。
要在Java配置中注册自定义格式化程序和转换器,请使用以下命令:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
要在XML配置中执行相同的操作,请使用以下命令:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
</beans>
默认情况下,在解析和格式化日期值时,Spring MVC会考虑请求区域设置。这适用于将日期表示为带有“输入”表单域的字符串的表单。然而,对于“日期”和“时间”表单域,浏览器使用在HTML规范中定义的固定格式。对于这种情况,可以按如下方式自定义日期和时间格式:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
See the FormatterRegistrar SPI and the FormattingConversionServiceFactoryBean for more information on when to use FormatterRegistrar implementations. |
1.12.4. Validation
默认情况下,如果类路径上存在Bean验证(例如,Hibernate Validator),则LocalValidatorFactoryBean
注册为全局验证器,以便与控制器方法参数上的@Valid
和valated
一起使用。
在Java配置中,您可以自定义全局Validator
实例,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
以下示例显示如何在XML中实现相同的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven validator="globalValidator"/>
</beans>
请注意,您还可以在本地注册Validator
实现,如下面的示例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
If you need to have a LocalValidatorFactoryBean injected somewhere, create a bean and mark it with @Primary in order to avoid conflict with the one declared in the MVC configuration. |
1.12.5. Interceptors
在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/**");
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
Mapped interceptors are not ideally suited as a security layer due to the potential for a mismatch with annotated controller path matching, which can also match trailing slashes and path extensions transparently, along with other path matching options. Many of these options have been deprecated but the potential for a mismatch remains. Generally, we recommend using Spring Security which includes a dedicated MvcRequestMatcher to align with Spring MVC path matching and also has a security firewall that blocks many unwanted characters in URL paths. |
1.12.6. Content Types
您可以配置Spring MVC如何根据请求确定所请求的媒体类型(例如,Accept
头、URL路径扩展、查询参数等)。
默认情况下,仅选中Accept
标头。
在Java配置中,您可以自定义请求的内容类型解析,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
以下示例显示如何在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.12.7. Message Converters
您可以在JAVA配置中通过覆盖configureMessageConverters()
(替换Spring MVC创建的默认转换器)或覆盖extendMessageConverters()
(定制默认转换器或在默认转换器基础上添加其他转换器)来定制HttpMessageConverter
。
下面的示例使用自定义的对象映射器
而不是默认的对象映射器添加XML和Jackson JSON转换器:
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
在前面的示例中,Jackson2ObjectMapperBuilder
用于为启用缩进的MappingJackson2HttpMessageConverter
和MappingJackson2XmlHttpMessageConverter
创建通用配置、定制的日期格式以及jackson-module-parameter-names
,的注册,该注册添加了对访问参数名称的支持(这是在Java8中添加的特性)。
该构建器按如下方式定制Jackson的默认属性:
如果在类路径上检测到以下众所周知的模块,它还会自动注册这些模块:
-
jackson-datatype-joda:支持joda-time类型。
-
jackson-datatype-jsr310:支持Java 8日期和时间API类型。
-
jackson-datatype-jdk8:支持其他Java 8类型,比如
可选
。
Enabling indentation with Jackson XML support requires woodstox-core-asl dependency in addition to jackson-dataformat-xml one. |
还提供了其他有趣的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.12.8. View Controllers
这是定义参数化视图控制器
的快捷方式,它在被调用时立即转发到视图。当在视图生成响应之前没有Java控制器逻辑要运行时,您可以在静态情况下使用它。
以下Java配置示例将/
的请求转发到名为home
的视图:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
下面的示例通过使用<;mvc:view-control>;
元素实现了与上一个示例相同的功能,但使用了XML:
<mvc:view-controller path="/" view-name="home"/>
如果@Requestmap
方法被映射到任何HTTP方法的URL,则不能使用视图控制器来处理相同的URL。这是因为URL与带注释的控制器的匹配被认为是端点所有权的足够强指示,因此可以将405(METHOD_NOT_ALLOWED)、415(UNSUPPORTED_MEDIA_TYPE)或类似响应发送到客户端以帮助调试。因此,建议避免在带注释的控制器和视图控制器之间拆分URL处理。
1.12.9. View Resolvers
MVC配置简化了视图解析器的注册。
以下Java配置示例使用JSON呈现的默认View
配置内容协商视图分辨率:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
</mvc:view-resolvers>
但是,请注意,FreeMarker、Tiles、Groovy标记和脚本模板还需要配置底层的视图技术。
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配置中,您可以添加各自的figurer
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;
}
}
1.12.10. Static Resources
此选项提供了一种从基于资源
的位置列表中提供静态资源的便捷方法。
在下一个示例中,给定一个以/resource
开头的请求,相对路径用于查找和提供相对于Web应用程序根目录下的/public
或/静态
下的类路径上的静态资源。这些资源的未来有效期为一年,以确保最大限度地使用浏览器缓存并减少浏览器发出的HTTP请求。Last-Modified
信息是从Resource#lastModified
中推导出来的,因此“Last-Modified”
头支持HTTP条件请求。
下面的清单显示了如何使用Java配置执行此操作:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:resources mapping="/resources/**" location="/public, classpath:/static/" cache-period="31556926" />
另请参阅静态资源的HTTP缓存支持。
资源处理程序还支持一系列ResourceResolver
实现和资源转换器
实现,您可以使用它们来创建使用优化资源的工具链。
您可以将VersionResourceResolver
用于基于从内容、固定应用程序版本或其他计算得出的MD5散列的版本化资源URL。ContentVersionStrategy
(MD5 Hash)是一个很好的 - 选择,但有一些值得注意的例外,比如与模块加载器一起使用的JAVASCRIPT资源。
以下示例说明如何在Java配置中使用VersionResourceResolver
:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain resource-cache="true">
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
</mvc:version-resolver>
</mvc:resolvers>
</mvc:resource-chain>
</mvc:resources>
然后,您可以使用ResourceUrlProvider
重写URL,并应用完整的解析器和转换器链 - 来插入版本。MVC配置提供了一个ResourceUrlProvider
Bean,以便可以将其注入到其他组件中。您还可以使用ResourceUrlEncodingFilter
为Thymeleaf、JSP、FreeMarker和其他具有依赖HttpServletResponse#encodeURL
的URL标记的对象使重写变得透明。
请注意,当同时使用EncodedResourceResolver
(例如,用于提供gzift或brotli编码的资源)和VersionResourceResolver
时,您必须按此顺序注册它们。这确保始终根据未编码的文件可靠地计算基于内容的版本。
对于WebJars,像/webjars/jquery/1.2.0/jquery.min.js
这样的版本化URL是推荐和最有效的使用方式。相关的资源位置由Spring Boot现成配置(也可以通过ResourceHandlerRegistry
手动配置),不需要添加org.webjars:webjars-Locator-core
依赖项。
通过WebJarsResourceResolver
支持像/webjars/jQuery/jquery.min.js
这样的无版本URL,当类路径上存在org.webjars:webjars-Locator-core
库时,会自动注册该URL,代价是进行类路径扫描,这可能会降低应用程序的启动速度。解析器可以重写URL以包括JAR的版本,还可以与从/Webjars/jquery/jquery.min.js到/webjars/jquery/1.2.0/jquery.min.js
.的没有 - 版本的传入URL进行匹配
The Java configuration based on ResourceHandlerRegistry provides further options for fine-grained control, e.g. last-modified behavior and optimized resource resolution. |
1.12.11. Default Servlet
Spring MVC允许将DispatcherServlet
映射到/
(从而覆盖容器的默认Servlet的映射),同时仍然允许容器的默认Servlet处理静态资源请求。它使用URL映射/**
配置DefaultServletHttpRequestHandler
,并且相对于其他URL映射优先级最低。
该处理程序将所有请求转发到默认的Servlet。因此,它必须保持在所有其他URLHandlerMappings
顺序的最后。如果您使用<;MVC:Annotation-Driven>;
就是这种情况。或者,如果您设置了自己的自定义<DefaultServletHttpRequestHandler
,>HandlerMap实例,请确保将其Order
属性设置为小于Integer.MAX_VALUE
的值。
以下示例显示如何使用默认设置启用该功能:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:default-servlet-handler/>
覆盖/
Servlet映射的注意事项是,必须按名称而不是按路径检索默认Servlet的RequestDispatcher
。DefaultServletHttpRequestHandler
使用大多数主要Servlet容器(包括Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic和WebSphere)的已知名称列表,尝试在启动时自动检测容器的默认Servlet。如果默认Servlet已使用不同的名称进行了自定义配置,或者如果在默认Servlet名称未知的情况下使用了不同的Servlet容器,则必须显式提供默认Servlet的名称,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
1.12.12. Path Matching
您可以自定义与路径匹配和URL处理相关的选项。有关各个选项的详细信息,请参阅PathMatchConfigurer
javadoc。
以下示例显示如何在Java配置中自定义路径匹配:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
// ...
}
}
以下示例显示如何在XML配置中自定义路径匹配:
<mvc:annotation-driven>
<mvc:path-matching 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.12.13. Advanced Java Config
@EnableWebMvc
导入DelegatingWebMvcConfiguration
,其中:
-
为Spring MVC应用程序提供默认的Spring配置
-
检测并委托
WebMvcConfigurer
实现来自定义该配置。
对于高级模式,您可以移除@EnableWebMvc
,直接从DelegatingWebMvcConfigurer
扩展,而不是实现WebMvcConfigurer
,如下例所示:
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
您可以将现有方法保留在WebConfig
中,但现在也可以覆盖来自基类的Bean声明,并且您仍然可以在类路径上拥有任意数量的其他WebMvcConfigurer
实现。
1.12.14. Advanced XML Config
MVC命名空间没有高级模式。如果需要在Bean上自定义无法更改的属性,则可以使用SpringApplicationContext
的BeanPostProcessor
生命周期挂钩,如下例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
注意,您需要将MyPostProcessor
声明为一个Bean,或者显式地在XML中声明,或者通过<;Component-Scan/>;
声明来检测它。
1.13. HTTP/2
Servlet 4容器需要支持HTTP/2,而Spring Framework5与Servlet API 4兼容。从编程模型的角度来看,应用程序没有具体需要做的事情。但是,有一些与服务器配置相关的注意事项。有关更多详细信息,请参阅HTTP/2维基页面。
Servlet API确实公开了一个与HTTP/2相关的构造。您可以使用jakarta.servlet.http.PushBuilder
将资源主动推送到客户端,并且它被支持作为@Requestmap
方法的方法参数。
2. REST Clients
本节介绍客户端访问REST端点的选项。
2.1. RestTemplate
RestTemplate
是执行HTTP请求的同步客户端。它是最初的Spring rest客户端,在底层的HTTP客户端库上公开了一个简单的模板方法API。
As of 5.0 the RestTemplate is in maintenance mode, with only requests for minor changes and bugs to be accepted. Please, consider using the WebClient which offers a more modern API and supports sync, async, and streaming scenarios. |
有关详细信息,请参阅REST Endpoint。
2.2. WebClient
WebClient
是执行HTTP请求的非阻塞、反应式客户端。它是在5.0中引入的,提供了RestTemplate
的现代替代方案,对同步和异步以及流场景都提供了有效的支持。
与RestTemplate
相比,WebClient
支持以下功能:
-
非阻塞I/O。
-
反作用力流反压。
-
高并发性和更少的硬件资源。
-
函数式、流畅的API,它利用了Java 8 lambdas。
-
同步和异步交互。
-
流到服务器或从服务器流下来。
有关详细信息,请参阅WebClient。
2.3. HTTP Interface
Spring框架允许您将HTTP服务定义为具有HTTP交换方法的Java接口。然后,您可以生成实现此接口并执行交换的代理。这有助于简化HTTP远程访问,并为选择同步或反应式等API风格提供了额外的灵活性。
有关详细信息,请参阅REST Endpoint。
3. Testing
本节总结了Spring-test
中可用于Spring MVC应用程序的选项。
-
Servlet API Mock:单元测试控制器、过滤器和其他Web组件的Servlet API契约的模拟实现。有关更多详细信息,请参阅Servlet API模拟对象。
-
TestContext框架:支持在JUnit和TestNG测试中加载Spring配置,包括跨测试方法高效缓存加载的配置,以及支持使用
MockServletContext
加载WebApplicationContext
。有关详细信息,请参阅TestContext框架。 -
Spring MVC测试:一个框架,也称为
MockMvc
,用于通过DispatcherServlet
(即,支持注释)测试带注释的控制器,带有Spring MVC基础设施,但没有HTTP服务器。有关更多详细信息,请参阅Spring MVC测试。 -
客户端REST:
Spring-test
提供了一个MockRestServiceServer
,您可以将其用作模拟服务器来测试在内部使用RestTemplate
的客户端代码。有关详细信息,请参阅客户端REST测试。 -
WebTestClient
:用于测试WebFlux应用程序,但也可以用于端到端的集成测试,通过HTTP连接到任何服务器。它是一个非阻塞、反应式客户端,非常适合测试异步和流场景。
4. WebSockets
参考文档的这一部分涵盖了对Servlet堆栈、包括原始WebSocket交互的WebSocket消息传递、通过SockJS的WebSocket模拟以及通过作为WebSocket上子协议的STOMP的发布-订阅消息传递的支持。
=WebSocket简介
WebSocket协议RFC 6455提供了一种标准化方法,用于通过单个TCP连接在客户端和服务器之间建立全双工双向通信通道。它是与HTTP不同的TCP协议,但设计为在HTTP上工作,使用端口80和443,并允许重复使用现有的防火墙规则。
WebSocket交互以使用HTTPUpgrade
头的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 | The Upgrade header. |
2 | Using the Upgrade connection. |
支持WebSocket的服务器返回类似以下内容的输出,而不是通常的200状态代码:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 | Protocol switch |
成功握手后,作为HTTP升级请求基础的TCP套接字保持打开状态,以便客户端和服务器继续发送和接收消息。
对WebSockets工作原理的完整介绍超出了本文的范围。请参阅RFC 6455,HTML5的WebSocket章节,或Web上许多介绍和教程中的任何一个。
请注意,如果WebSocket服务器在Web服务器(例如nginx)之后运行,您可能需要将其配置为将WebSocket升级请求传递到WebSocket服务器。同样,如果应用程序在云环境中运行,请查看云提供商关于WebSocket支持的说明。
尽管WebSocket被设计为与HTTP兼容并以HTTP请求开始,但了解这两个协议导致非常不同的体系结构和应用程序编程模型是很重要的。
在HTTP和REST中,一个应用程序被建模为多个URL。为了与应用程序交互,客户端以请求-响应的方式访问这些URL。服务器根据HTTP URL、方法和标头将请求路由到适当的处理程序。
相比之下,在WebSockets中,初始连接通常只有一个URL。随后,所有应用程序消息都在同一个TCP连接上流动。这指向了一个完全不同的异步、事件驱动的消息传递体系结构。
WebSocket也是一种低级传输协议,与HTTP不同,它没有为消息内容规定任何语义。这意味着,除非客户端和服务器在消息语义上达成一致,否则无法路由或处理消息。
WebSocket客户端和服务器可以通过HTTP握手请求上的SEC-WebSocket-Protocol
头协商使用更高级别的消息传递协议(例如STOMP)。在缺乏这一点的情况下,他们需要拿出自己的惯例。
WebSockets可以使网页变得动态和交互。然而,在许多情况下,结合使用AJAX和HTTP流或长轮询可以提供简单而有效的解决方案。
例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没有问题。另一方面,协作、游戏和金融应用程序需要更接近实时。
延迟本身并不是一个决定性因素。如果消息量相对较小(例如,监控网络故障),HTTP流或轮询可以提供有效的解决方案。正是低延迟、高频率和大容量的组合为WebSocket的使用提供了最佳选择。
还要记住,在Internet上,您无法控制的限制性代理可能会阻止WebSocket交互,因为它们没有配置为传递Upgrade
头,或者因为它们关闭了看起来空闲的长期连接。这意味着,将WebSocket用于防火墙内的内部应用程序比用于面向公众的应用程序是一个更直接的决定。
4.1. WebSocket API
Spring框架提供了一个WebSocket API,您可以使用它来编写处理WebSocket消息的客户端和服务器端应用程序。
4.1.1. WebSocketHandler
创建WebSocket服务器就像实现WebSocketHandler
一样简单,或者更有可能的是,扩展TextWebSocketHandler
或BinaryWebSocketHandler
。下面的示例使用TextWebSocketHandler
:
public class MyHandler extends TextWebSocketHandler { @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // ... } }
有专用的WebSocket Java配置和XML命名空间支持,用于将前面的WebSocket处理程序映射到特定URL,如下面的示例所示:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
以下示例显示了与前面的示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于Spring MVC应用程序,应该包含在DispatcherServlet
的配置中。然而,Spring的WebSocket支持并不依赖于Spring MVC。在WebSocketHttpRequestHandler
.的帮助下,将<代码>WebSocketHandler
集成到其他HTTP服务环境中相对简单
当直接使用WebSocketHandler
API而不是间接使用stomp消息时,应用程序必须同步消息的发送,因为底层标准WebSocket会话(JSR-356)不允许并发发送。一种选择是用
ConcurrentWebSocketSessionDecorator
.>包装<代码>WebSocketSession
4.1.2. WebSocket Handshake
定制初始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 provides a WebSocketHandlerDecorator base class that you can use to decorate a WebSocketHandler with additional behavior. Logging and exception handling implementations are provided and added by default when using the WebSocket Java configuration or XML namespace. The ExceptionWebSocketHandlerDecorator catches all uncaught exceptions that arise from any WebSocketHandler method and closes the WebSocket session with status 1011 , which indicates a server error. |
4.1.3. Deployment
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的Dispatcher Servlet
。
这是JSR-356的一个重要限制,即使在JSR-356运行时运行时,Spring的WebSocket支持也使用特定于服务器的RequestUpgradeStrategy
实现来解决该问题。此类策略目前适用于Tomcat、Jetty、GlassFish、WebLogic、WebSphere和Undertow(以及WildFly)。
A request to overcome the preceding limitation in the Java WebSocket API has been created and can be followed at eclipse-ee4j/websocket-api#211. Tomcat, Undertow, and WebSphere provide their own API alternatives that make it possible to do this, and it is also possible with Jetty. We are hopeful that more servers will do the same. |
第二个考虑因素是,支持JSR356的Servlet容器应该执行<代码>ServletContainerInitializer(SCI)扫描,这在某些情况下会显著降低应用程序启动 - 的速度。如果在升级到支持JSR-356的Servlet容器版本后观察到显著影响,则应该可以通过使用web.xml
中的<;Abte-orting/>;
元素选择性地启用或禁用Web片段(和SCI扫描),如下例所示:
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0">
<absolute-ordering/>
</web-app>
然后,您可以有选择地按名称启用Web片段,例如Spring自己的SpringServletContainerInitializer
,它提供了对Servlet3Java初始化API的支持。以下示例显示了如何执行此操作:
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
4.1.4. Server Configuration
每个基础WebSocket引擎都公开控制运行时特征的配置属性,例如消息缓冲区大小、空闲超时等。
对于Tomcat、WildFly和GlassFish,您可以将ServletServerContainerFactoryBean
添加到WebSocket Java配置中,如下例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
以下示例显示了与前面的示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
For client-side WebSocket configuration, you should use WebSocketContainerFactoryBean (XML) or ContainerProvider.getWebSocketContainer() (Java configuration). |
对于Jetty,您需要提供一个预配置的JettyWebSocketServerFactory
,并通过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.1.5. Allowed Origins
从Spring Framework4.1.5开始,WebSocket和SockJS的默认行为是只接受同源请求。也可以允许所有来源或指定的来源列表。此检查主要是为浏览器客户端设计的。不会阻止其他类型的客户端修改Origin
头值(有关详细信息,请参阅RFC 6454:Web Origin概念)。
这三种可能的行为是:
-
只允许同源请求(默认):在这种模式下,当启用SockJS时,IFRAME HTTP响应头
X-Frame-Options
被设置为SAMEORIGIN
,并且禁用JSONP传输,因为它不允许检查请求的来源。因此,当启用此模式时,不支持IE6和IE7。 -
允许指定的来源列表:每个允许的来源必须以http://或https://.开头在此模式下,当启用SockJS时,将禁用iFrame传输。因此,当启用此模式时,不支持IE6到IE9。
-
允许所有原点:要启用此模式,您应该提供
*
作为允许的原点值。在此模式下,所有传输均可用。
您可以配置WebSocket和SockJS允许的来源,如下例所示:
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com"); } @Bean public WebSocketHandler myHandler() { return new MyHandler(); } }
以下示例显示了与前面的示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
4.2. SockJS Fallback
在公共Internet上,您控制之外的限制性代理可能会阻止WebSocket交互,因为它们没有配置为传递Upgrade
头,或者因为它们关闭了看起来空闲的长期连接。
这个问题的解决方案是WebSocket模拟 - ,即首先尝试使用WebSocket,然后使用基于HTTp的技术来模拟WebSocket交互并公开相同的应用程序级API。
在Servlet堆栈上,Spring框架提供了对SockJS协议的服务器(和客户端)支持。
4.2.1. Overview
SockJS的目标是让应用程序使用WebSocket API,但在运行时在必要时退回到非WebSocket替代方案,而不需要更改应用程序代码。
SockJS包括:
-
SockJS JavaScript客户端 - 浏览器中使用的客户端库。
-
SockJS服务器实现,包括Spring框架
Spring-WebSocket
模块中的一个。 -
Spring-WebSocket
模块中的SockJS Java客户端(从4.1版开始)。
SockJS专为在浏览器中使用而设计。它使用各种技术来支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅SockJS客户端页面。传输一般分为三类:WebSocket、HTTP流和HTTP长轮询。有关这些类别的概述,请参阅此博客文章。
SockJS客户端首先发送GET/INFO
以从服务器获取基本信息。在那之后,它必须决定使用哪种交通工具。如果可能,使用WebSocket。如果不是,在大多数浏览器中,至少有一个HTTP流选项。如果不是,则使用HTTP(长)轮询。
所有传输请求都具有以下URL结构:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
其中:
-
{server-id}
用于在集群中路由请求,但不用于其他用途。 -
{Session-id}
关联属于SockJS会话的HTTP请求。 -
{Transport}
表示传输类型(例如WebSocket
、xhr-Streaming
等)。
WebSocket传输只需要一个HTTP请求来进行WebSocket握手。此后的所有消息都在该套接字上交换。
HTTP传输需要更多请求。例如,AJAX/XHR流依赖于一个针对服务器到客户端消息的长时间运行的请求和针对客户端到服务器消息的额外的HTTP POST请求。长轮询类似,不同之处在于它在每次服务器到客户端发送之后结束当前请求。
SockJS添加了最少的消息帧。例如,服务器最初发送字母o
(“打开”帧),消息以a[“Message1”,“Message2”]
(JSON编码的数组)的形式发送,如果25秒内没有消息流动(默认情况下),则以字母h
(“心跳”帧)发送,而字母c
(“关闭”帧)则关闭会话。
要了解更多信息,请在浏览器中运行一个示例并观察HTTP请求。SockJS客户端允许固定传输列表,因此可以一次查看一个传输。SockJS客户端还提供了一个调试标志,可以在浏览器控制台中启用帮助消息。在服务器端,您可以为org.springFrawork.web.套接字
启用跟踪
日志记录。有关更多详细信息,请参阅SockJS协议旁白测试。
4.2.2. Enabling 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版)。它模拟W3CWebSocket API并与服务器通信以选择最佳传输选项,具体取决于它运行的浏览器。请参阅sockjs-client页面和浏览器支持的传输类型列表。例如,客户端还提供了几个配置选项 - ,以指定要包括哪些传输。
4.2.3. IE 8 and 9
Internet Explorer 8和9仍在使用中。它们是拥有SockJS的一个关键原因。本节介绍有关在这些浏览器中运行的重要注意事项。
SockJS客户端通过使用微软的XDomainRequest
,支持IE8和9中的AJAX/XHR流。这可以跨域工作,但不支持发送Cookie。Cookie对于Java应用程序通常是必不可少的。但是,由于SockJS客户端可以与许多服务器类型(不仅仅是Java服务器类型)一起使用,因此它需要知道Cookie是否重要。如果是这样的话,SockJS客户端更喜欢使用AJAX/XHR进行流媒体传输。否则,它依赖于基于iframe的技术。
来自SockJS客户端的第一个/INFO
请求是对可能影响客户端的传输选择的信息的请求。其中一个细节是服务器应用程序是否依赖Cookie(例如,出于身份验证目的或使用粘滞会话进行集群)。Spring的SockJS支持包括一个名为sessionCookieNeeded
的属性。默认情况下,它是启用的,因为大多数Java应用程序依赖jessionid
cookie。如果您的应用程序不需要它,您可以关闭该选项,然后SockJS客户端应该在IE8和9中选择XDR-Streaming
。
如果您确实使用基于IFRAME的传输,请记住,可以通过将HTTP响应头X-Frame-Options
设置为Deny
、SAMEORIGIN
或Allow-from<;Origin&>
来指示浏览器阻止在给定页面上使用IFrame。这用于防止点击劫持。
Spring Security3.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名称空间通过<;web套接字:sockjs>;
元素提供了类似的选项。
During initial development, do enable the SockJS client devel mode that prevents the browser from caching SockJS requests (like the iframe) that would otherwise be cached. For details on how to enable it see the SockJS client page. |
4.2.4. Heartbeats
SockJS协议要求服务器发送心跳消息,以防止代理得出连接挂起的结论。Spring SockJS配置有一个名为HearbeatTime
的属性,您可以使用该属性来自定义频率。默认情况下,假设该连接上没有发送其他消息,则在25秒后发送心跳。这个25秒的值符合以下针对公共Internet应用程序的IETF建议。
When using STOMP over WebSocket and SockJS, if the STOMP client and server negotiate heartbeats to be exchanged, the SockJS heartbeats are disabled. |
Spring SockJS支持还允许您配置TaskScheduler
来调度心跳任务。任务调度程序由线程池支持,其默认设置基于可用处理器的数量。您应该考虑根据您的特定需求自定义设置。
4.2.5. Client Disconnects
HTTP流和HTTP长轮询SockJS传输需要连接保持比平时更长的打开时间。有关这些技术的概述,请参阅这篇博客文章。
在Servlet容器中,这是通过Servlet 3异步支持实现的,该支持允许退出Servlet容器线程、处理请求并继续从另一个线程写入响应。
一个特定的问题是,Servlet API不会为已经离开的客户端提供通知。请参阅eclipse-ee4j/Servlet-api#44。但是,Servlet容器在后续尝试写入响应时会引发异常。由于Spring的SockJS服务支持服务器发送的心跳(默认情况下为每25秒),这意味着通常会在该时间段内检测到客户端断开(如果消息发送更频繁,则检测到更早的时间)。
As a result, network I/O failures can occur because a client has disconnected, which can fill the log with unnecessary stack traces. Spring makes a best effort to identify such network failures that represent client disconnects (specific to each server) and log a minimal message by using the dedicated log category, DISCONNECTED_CLIENT_LOG_CATEGORY (defined in AbstractSockJsSession ). If you need to see the stack traces, you can set that log category to TRACE. |
4.2.6. SockJS and CORS
如果您允许跨域请求(请参阅Allowed Origins),则SockJS协议在XHR流和轮询传输中使用CORS来实现跨域支持。因此,除非检测到响应中存在CORS报头,否则会自动添加CORS报头。因此,如果应用程序已经配置为提供CORS支持(例如,通过Servlet过滤器),则Spring的SockJsService
将跳过这一部分。
还可以通过设置Spring的SockJsService中的suppressCors
属性来禁用这些CORS标头的添加。
SockJS需要以下标头和值:
-
Access-Control-Allow-Origin
:从Origin
请求头部的值初始化。 -
Access-Control-Allow-Credentials
:始终设置为<代码>真 。 -
Access-Control-RequestHeaders
:从等价请求头部的值初始化。 -
Access-Control-Allow-Methods
:传输支持的HTTP方法(请参阅TransportType
枚举)。 -
访问控制最长年龄
:设置为31536000(1岁)。
有关具体实现,请参阅AbstractSockJsService
中的addCorsHeaders
和源代码中的TransportType
枚举。
或者,如果CORS配置允许,考虑排除带有SockJS端点前缀的URL,从而让Spring的SockJsService
处理它。
4.2.7. SockJsClient
Spring提供了一个SockJS Java客户端,无需使用浏览器即可连接到远程SockJS端点。当需要通过公共网络在两台服务器之间进行双向通信时(即网络代理可能会阻止使用WebSocket协议),这可能特别有用。SockJS Java客户端对于测试目的也非常有用(例如,模拟大量并发用户)。
SockJS Java客户端支持WebSocket
、xhr-Streaming
和xhr-Polling
传输。其余的只有在浏览器中使用才有意义。
WebSocketTransport
配置如下:
-
JSR-356运行时中的StandardWebSocketClient
。 -
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 uses JSON formatted arrays for messages. By default, Jackson 2 is used and needs to be on the classpath. Alternatively, you can configure a custom implementation of SockJsMessageCodec and configure it on the 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 | Set the streamBytesLimit property to 512KB (the default is 128KB — 128 * 1024 ). |
2 | Set the httpMessageCacheSize property to 1,000 (the default is 100 ). |
3 | Set the disconnectDelay property to 30 property seconds (the default is five seconds — 5 * 1000 ). |
4.3. STOMP
WebSocket协议定义了两种类型的消息(文本和二进制),但其内容未定义。该协议定义了一种机制,供客户端和服务器协商在WebSocket之上使用的子协议(即更高级别的消息传递协议),以定义各自可以发送的消息类型、格式、每条消息的内容等。子协议的使用是可选的,但无论采用哪种方式,客户端和服务器都需要就定义消息内容的某种协议达成一致。
4.3.1. Overview
stomp(简单面向文本的消息传递协议)最初是为脚本语言(如Ruby、Python和Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP可以在任何可靠的双向流网络协议上使用,例如TCP和WebSocket。尽管STOMP是面向文本的协议,但消息有效负载可以是文本或二进制。
STOMP是一种基于帧的协议,其帧以HTTP为模型。下面的清单显示了踩踏框架的结构:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用Send
或SUBSCRIBE
命令发送或订阅消息,以及描述消息内容和接收对象的Destination
标头。这启用了一种简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的客户端,或将消息发送到服务器以请求执行某些工作。
当您使用Spring的STOP支持时,Spring WebSocket应用程序充当客户端的STOP代理。消息被路由到@控制器
消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅用户广播消息。您还可以将Spring配置为与专用STOP代理(如RabbitMQ、ActiveMQ等)配合使用,以实现消息的实际广播。在这种情况下,Spring维护到代理的TCP连接,将消息转发到代理,并将消息从代理向下传递到连接的WebSocket客户端。因此,Spring Web应用程序可以依赖统一的基于HTTP的安全性、通用验证和熟悉的消息处理编程模型。
下面的示例显示了一个订阅接收股票报价的客户端,服务器可能会定期(例如,通过通过SimpMessagingTemplate
向经纪人发送消息的计划任务)接收股票报价:
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
下面的示例显示了一个发送交易请求的客户端,服务器可以通过@Messagemap
方法处理该请求:
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
在执行之后,服务器可以向下向客户端广播交易确认消息和细节。
目的地的含义在STOP规范中故意保持不透明。它可以是任何字符串,完全由STOMP服务器来定义它们支持的目的地的语义和语法。然而,目的地通常是类似路径的字符串,其中/主题/..
表示发布-订阅(一对多),/Queue/
表示点对点(一对一)消息交换。
STOMP服务器可以使用Message
命令向所有订阅者广播消息。以下示例显示服务器向订阅客户端发送股票报价:
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
服务器不能发送未经请求的消息。来自服务器的所有消息都必须响应特定的客户端订阅,并且服务器消息的订阅
标头必须与客户端订阅的id
标头匹配。
前面的概述旨在提供对STOMP协议的最基本了解。我们建议您全面查看协议规范。
4.3.2. Benefits
与使用原始WebSocket相比,使用STOMP作为子协议允许Spring框架和Spring Security提供更丰富的编程模型。同样的观点也可以用在HTTP和原始TCP之间,以及它如何让Spring MVC和其他Web框架提供丰富的功能。以下是一系列好处:
-
无需发明定制消息传递协议和消息格式。
-
STOMP客户端,包括Spring框架中的Java客户端,都是可用的。
-
您可以(可选)使用消息代理(如RabbitMQ、ActiveMQ等)来管理订阅和广播消息。
-
应用程序逻辑可以组织在任意数量的
@Controler
实例中,并且可以根据STOMP目标标头将消息路由到它们,而不是使用给定连接的单个WebSocketHandler
处理原始WebSocket消息。 -
您可以使用Spring Security根据STOP目的地和消息类型来保护消息。
4.3.3. Enable STOMP
Spring-Messaging
和Spring-WebSocket
模块提供了对WebSocket的支持。一旦拥有了这些依赖项,您就可以使用SockJS回退在WebSocket上公开STOP端点,如下面的示例所示:
@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 is the HTTP URL for the endpoint to which a WebSocket (or SockJS) client needs to connect for the WebSocket handshake. |
2 | STOMP messages whose destination header begins with /app are routed to @MessageMapping methods in @Controller classes. |
3 | Use the built-in message broker for subscriptions and broadcasting and route messages whose destination header begins with /topic `or `/queue to the broker. |
以下示例显示了与前面的示例等效的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>
For the built-in simple broker, the /topic and /queue prefixes do not have any special meaning. They are merely a convention to differentiate between pub-sub versus point-to-point messaging (that is, many subscribers versus one consumer). When you use an external broker, check the STOMP page of the broker to understand what kind of STOMP destinations and prefixes it supports. |
要从浏览器连接,对于SockJS,可以使用sockjs-client
。对于STOMP,许多应用程序使用了jMesnil/stomp-web套接字库(也称为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) {
}
有关更多示例代码,请参阅:
-
使用WebSocket构建交互式Web应用程序 - a入门指南。
-
股票投资组合 - 一个示例应用程序。
4.3.4. WebSocket Server
要配置底层WebSocket服务器,请使用服务器配置中的信息。但是,对于Jetty,您需要通过StompEndpoint注册表
设置HandshakeHandler
和WebSocketPolicy
:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
4.3.5. Flow of Messages
一旦暴露了STOP端点,Spring应用程序就变成了连接的客户端的STOP代理。本节介绍服务器端的消息流。
Spring消息传递
模块包含对消息传递应用程序的基本支持,这些应用程序起源于Spring集成,后来被提取并合并到Spring框架中,以便在许多Spring项目和应用程序场景中更广泛地使用。下面的列表简要描述了一些可用的消息传递抽象:
-
Message:消息的简单表示,包括头部和有效负载。
-
MessageHandler:处理消息的约定。
-
MessageChannel:用于发送支持生产者和消费者之间松散耦合的消息的合同。
-
SubscribableChannel:具有
MessageHandler
订阅者的MessageChannel
。 -
ExecutorSubscribableChannel:
SubscribableChannel
,它使用Executor
传递消息。
Java配置(即@EnableWebSocketMessageBroker
)和XML名称空间配置(即<;websocket:message-broker>;
)使用前面的组件来组装消息工作流。下图显示了启用简单内置Message Broker时使用的组件:
上图显示了三个消息通道:
-
clientInundChannel
:用于传递从WebSocket客户端接收到的消息。 -
clientOutundChannel
:用于向WebSocket客户端发送服务器消息。 -
brokerChannel
:用于从服务器端应用程序代码中向消息代理发送消息。
下图显示了将外部代理(如RabbitMQ)配置为管理订阅和广播消息时使用的组件:
前面两个图之间的主要区别是使用了“代理中继”,用于通过TCP将消息向上传递到外部STOP代理,以及将消息从代理向下传递到订阅的客户端。
当从WebSocket连接接收到消息时,它们被解码为踩踏帧,转换为Spring消息
表示形式,并发送到客户端InundChannel
以进行进一步处理。例如,目标标头以/app
开头的STOMP消息可以被路由到带注释的控制器中的@Messagemap
方法,而/Theme
和/Queue
消息可以被直接路由到消息代理。
处理来自客户端的STOMP消息的带注释的@Controler
可以通过brokerChannel
向消息代理发送消息,而代理通过客户端出边界频道
将该消息广播给匹配的订阅者。同样的控制器也可以响应HTTP请求执行相同的操作,因此客户端可以执行HTTP POST,然后@postmap
方法可以将消息发送到消息代理以广播到订阅的客户端。
我们可以通过一个简单的例子来跟踪流程。考虑以下设置服务器的示例:
@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帧。收到消息并进行解码后,消息将被发送到客户端InundChannel
,然后被路由到存储客户端订阅的消息代理。 -
客户端将发送帧发送到
/app/greting
。/app
前缀帮助将其路由到带注释的控制器。去掉/app
前缀后,目标的其余/greting
部分被映射到GreetingController
中的@MessageMapping
方法。 -
GreetingController
返回的值被转换为一个Spring消息
,其有效负载基于返回值和一个默认的目的地头部/Theme/Greting
(从输入目的地派生而来,将/app
替换为/Theme
)。生成的消息被发送到brokerChannel
并由消息代理处理。 -
Message Broker查找所有匹配的订阅者,并通过
客户端出边界通道
向每个订阅者发送一个消息帧,其中的消息被编码为STOMP帧并通过WebSocket连接发送。
下一节提供有关带注释的方法的更多详细信息,包括支持的参数和返回值的类型。
4.3.6. Annotated Controllers
应用程序可以使用带注释的@控制器
类来处理来自客户端的消息。此类类可以声明@MessageMapping
、@SubscribeMap
和@ExceptionHandler
方法,如以下主题中所述:
@MessageMapping
您可以使用@Messagemap
来注释根据消息的目的地来路由消息的方法。它在方法级别和类型级别上都受支持。在类型级别,@Messagemap
用于表示控制器中所有方法之间的共享映射。
默认情况下,映射值是Ant样式的路径模式(例如/thing*
、/thing/**
),包括对模板变量的支持(例如/thing/{id}
)。可以通过@DestinationVariable
方法参数引用这些值。应用程序还可以切换到点分隔的映射目的地约定,如点作为分隔符中所述。
Supported Method Arguments
下表描述了方法参数:
Method argument | Description |
---|---|
|
用于访问完整的消息。 |
|
用于访问 |
|
用于通过类型化访问器方法访问标头。 |
|
用于访问消息的有效负载,由配置的 此注释的存在不是必需的,因为默认情况下,如果没有匹配其他参数,则假定此注释存在。 您可以使用 |
|
用于访问特定标头值的 |
|
用于访问邮件中的所有标头。此参数必须可赋给 |
|
用于访问从消息目的地提取的模板变量。根据需要将值转换为声明的方法参数类型。 |
|
反映在WebSocket HTTP握手时登录的用户。 |
Return Values
默认情况下,@MessageMapping
方法的返回值通过匹配的MessageConverter
被序列化为有效负载,并作为消息
发送到brokerChannel
,在那里广播给订阅者。出站消息的目的地与入站消息相同,但前缀为/Theme
。
您可以使用@SendTo
和@SendToUser
注释来自定义输出消息的目的地。@SendTo
用于自定义目标目的地或指定多个目的地。@SendToUser
用于将输出消息仅定向到与输入消息关联的用户。请参阅用户目的地。
您可以在同一方法上同时使用@SendTo
和@SendToUser
,并且这两种方法在类级别都受支持,在这种情况下,它们将作为类中方法的默认设置。但是,请记住,任何方法级别的@SendTo
或@SendToUser
注释都会覆盖类级别的任何此类注释。
消息可以异步处理,@MessageMapping
方法可以返回ListenableFuture
、CompletableFuture
或CompletionStage
。
注意,@SendTo
和@SendToUser
仅仅是为了方便起见,相当于使用SimpMessagingTemplate
来发送消息。如果需要,对于更高级的场景,@Messagemap
方法可以直接使用SimpMessagingTemplate
。这可以代替返回值,也可以作为返回值的补充。请参阅发送消息。
@SubscribeMapping
@SubscribeMap
类似于@MessageMapping
,但只将映射范围缩小到订阅消息。它支持与@Messagemap
相同的方法参数。然而,对于返回值,默认情况下,消息直接发送到客户端(通过clientOutundChannel
,以响应订阅),而不是发送到代理(通过brokerChannel
,作为对匹配订阅的广播)。添加@SendTo
或@SendToUser
会覆盖此行为,并改为发送到代理。
这在什么时候有用?假设代理被映射到/Theme
和/Queue
,而应用程序控制器被映射到/app
。在此设置中,代理存储用于重复广播的所有/Theme
和/Queue
订阅,应用程序不需要参与其中。客户端还可以订阅某个/app
目的地,控制器可以在不涉及代理的情况下返回一个值来响应该订阅,而无需再次存储或使用该订阅(实际上是一次性的请求-回复交换)。这样做的一个用例是在启动时使用初始数据填充UI。
这在什么时候是无用的?不要尝试将代理和控制器映射到相同的目标前缀,除非您出于某种原因希望两者独立处理消息(包括订阅)。入站消息是并行处理的。不能保证是代理还是控制器首先处理给定的消息。如果目标是在订阅存储并准备好广播时收到通知,则如果服务器支持,客户端应该要求提供收据(Simple Broker不支持)。例如,使用Javastomp客户端,您可以执行以下操作来添加收据:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> {
// Subscription ready...
});
服务器端选项是,以在brokerChannel
上注册ExecutorChannelInterceptor,并实现在处理了包括订阅在内的消息之后调用的After MessageHanded
方法。
@MessageExceptionHandler
应用程序可以使用@MessageExceptionHandler
方法处理来自@Messagemap
方法的异常。如果希望访问异常实例,可以在注释本身中声明异常,也可以通过方法参数声明异常。下面的示例通过方法参数声明异常:
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
@MessageExceptionHandler
方法支持灵活的方法签名,并支持与@MessageMap
方法相同的方法参数类型和返回值。
通常,@MessageExceptionHandler
方法在声明它们的@Controller
类(或类层次结构)中应用。如果希望这样的方法在全局范围内(跨控制器)应用,可以在标记为@ControllerAdacy
的类中声明它们。这可以与Spring MVC中提供的类似支持相媲美。
4.3.7. Sending Messages
如果要从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向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.3.8. Simple Broker
内置的Simple Message Broker处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的已连接客户端。该代理支持类似路径的目的地,包括订阅Ant样式的目的地模式。
Applications can also use dot-separated (rather than slash-separated) destinations. See Dots as Separators. |
如果配置了任务调度程序,Simple Broker将支持STOMP心跳。要配置调度程序,您可以声明自己的TaskScheduler
Bean,并通过MessageBrokerRegistry
设置它。或者,您也可以使用在内置WebSocket配置中自动声明的WebSocketMessageBrokerConfigurer
.,但是,您需要@lazy
来避免内置WebSocket配置和您的WebSocket之间的循环例如:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private TaskScheduler messageBrokerTaskScheduler;
@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
// ...
}
}
4.3.9. External Broker
Simple Broker非常适合入门,但只支持STOMP命令的一个子集(它不支持ack、回执和其他一些功能),依赖于简单的消息发送循环,并且不适合集群。作为替代方案,您可以将应用程序升级为使用功能齐全的Message 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>
前面配置中的STOP Broker中继是一个SpringMessageHandler
,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发到代理,然后通过客户端的WebSocket会话将从代理接收的所有消息转发到客户端。从本质上讲,它充当双向转发消息的“中继器”。
Add io.projectreactor.netty:reactor-netty and io.netty:netty-all dependencies to your project for TCP connection management. |
此外,应用程序组件(如HTTP请求处理方法、业务服务和其他组件)还可以向代理中继发送消息,以将消息广播到订阅的WebSocket客户端,如发送消息中所述。
实际上,代理中继实现了健壮且可伸缩的消息广播。
4.3.10. Connecting to a Broker
STOMP Broker中继维护到代理的单个“系统”TCP连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。您可以为此连接配置STOMP凭据(即STOMP帧登录
和passcode
头)。这在XML名称空间和Java配置中都公开为systemLogin
和systemPasscode
属性,缺省值为Guest
和Guest
。
STOMP Broker中继器还为每个连接的WebSocket客户端创建单独的TCP连接。您可以配置用于代表客户端创建的所有TCP连接的STOP凭据。这在XML名称空间和Java配置中都公开为ClientLogin
和ClientPasscode
属性,缺省值为Guest
和Guest
。
The STOMP broker relay always sets the login and passcode headers on every CONNECT frame that it forwards to the broker on behalf of clients. Therefore, WebSocket clients need not set those headers. They are ignored. As the Authentication section explains, WebSocket clients should instead rely on HTTP authentication to protect the WebSocket endpoint and establish the client identity. |
STOP Broker中继器还通过“系统”TCP连接向消息代理发送心跳信号和从消息代理接收心跳信号。您可以配置发送和接收心跳的间隔(默认情况下各10秒)。如果与代理的连接中断,代理中继将每隔5秒继续尝试重新连接,直到成功。
任何SpringBean都可以实现ApplicationListener<;BrokerAvailabilityEvent>;
,以便在与代理的“系统”连接丢失并重新建立时接收通知。例如,当没有活动的“系统”连接时,广播股票报价的Stock Quote服务可以停止尝试发送消息。
默认情况下,STOP Broker中继始终连接到相同的主机和端口,并在连接中断时根据需要重新连接。如果您希望在每次尝试连接时提供多个地址,则可以配置地址供应商,而不是配置固定的主机和端口。以下示例显示了如何执行此操作:
@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
属性配置STOP Broker中继。此属性的值被设置为每个连接
帧的host
标头,并且可能非常有用(例如,在与其建立了TCP连接的实际主机与提供基于云的STOP服务的主机不同的云环境中)。
4.3.11. Dots as Separators
当消息被路由到@MessageMapping
方法时,它们与AntPathMatcher
匹配。默认情况下,模式应该使用斜杠(/
)作为分隔符。这是Web应用程序中的一个很好的约定,类似于HTTPURL。但是,如果您更习惯于消息传递约定,则可以改用点(.
)作为分隔符。
以下示例显示如何在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>
此后,控制器可以在@Messagemap
方法中使用点(.
)作为分隔符,如下面的示例所示:
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
客户端现在可以向/app/red.Blue.green123
发送消息。
在前面的示例中,我们没有更改“Broker Relay”上的前缀,因为这些前缀完全依赖于外部消息代理。请参阅您使用的代理的STOMP文档页面,以了解它支持哪些Destination Header约定。
另一方面,“简单代理”确实依赖于配置的PathMatcher
,因此,如果您切换分隔符,该更改也适用于代理以及代理将目的地从消息匹配到订阅中的模式的方式。
4.3.12. Authentication
每个基于WebSocket的消息传递会话都以一个HTTP请求开始。这可以是升级到WebSockets的请求(即WebSocket握手),或者在SockJS回退的情况下是一系列SockJS HTTP传输请求。
许多Web应用程序已经有了身份验证和授权来保护HTTP请求。通常,通过Spring Security使用某种机制(如登录页面、HTTP基本身份验证或其他方式)对用户进行身份验证。经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一个基于Cookie的会话中的后续请求相关联。
因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有可通过HttpServletRequest#getUserPrincipal()
.访问的经过身份验证的用户Spring会自动将该用户与为其创建WebSocket或SockJS会话相关联,并随后通过用户标头与该会话中传输的所有STOMP消息相关联。
简而言之,典型的Web应用程序不需要做任何事情,只需要做它已经做的安全工作。用户在HTTP请求级别使用通过基于Cookie的HTTP会话(然后与为该用户创建的WebSocket或SockJS会话相关联)维护的安全上下文进行身份验证,并导致在流经应用程序的每个消息
上标记用户标头。
STOMP协议在CONNECT
框架上确实有登录
和passcode
头。它们最初是为在TCP上践踏而设计的,现在也是需要的。然而,对于WebSocket上的STOP,默认情况下,Spring忽略STOMP协议层的身份验证头,并假定用户已经在HTTP传输层进行了身份验证。期望WebSocket或SockJS会话包含经过身份验证的用户。
4.3.13. Token Authentication
Spring Security OAuth提供了对基于令牌的安全的支持,包括JSON Web Token(JWT)。您可以将其用作Web应用程序中的身份验证机制,包括通过WebSocket交互进行践踏,如上一节所述(即,通过基于Cookie的会话维护身份)。
同时,基于Cookie的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中,或者在通常使用头部进行身份验证的移动应用程序中)。
WebSocket协议,RFC 6455“没有规定服务器可以在WebSocket握手期间对客户端进行身份验证的任何特定方式。”然而,在实践中,浏览器客户端只能使用标准身份验证头(即基本的HTTP身份验证)或cookie,而不能(例如)提供自定义头。同样,SockJS JavaScript客户端不提供使用SockJS传输请求发送HTTP头的方法。请参阅sockjs-客户端问题196。相反,它确实允许发送可用于发送令牌的查询参数,但这有其自身的缺陷(例如,令牌可能会在无意中与服务器日志中的URL一起记录)。
The preceding limitations are for browser-based clients and do not apply to the Spring Java-based STOMP client, which does support sending headers with both WebSocket and SockJS requests. |
因此,希望避免使用Cookie的应用程序可能没有任何在HTTP协议级进行身份验证的好选择。与使用Cookie不同,他们可能更喜欢在STOP消息传递协议级别使用报头进行身份验证。要做到这一点,只需两个简单步骤:
-
使用STOMP客户端在连接时传递身份验证头。
-
使用
ChannelInterceptor
处理身份验证头。
下一个示例使用服务器端配置注册一个定制的身份验证拦截器。注意,拦截器只需要对连接消息
进行身份验证并设置用户标头。Spring记下并保存经过身份验证的用户,并将其与同一会话中的后续STOMP消息相关联。以下示例显示如何注册自定义身份验证拦截器:
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}
另外,请注意,当您使用Spring Security对消息的授权时,目前需要确保身份验证ChannelInterceptor
配置先于Spring Security的配置排序。最好是在WebSocketMessageBrokerConfigurer
的自身实现中声明标记为@Order(Ordered.HIGHEST_PRORCESS+99)
的自定义拦截器。
4.3.14. Authorization
Spring Security提供了WebSocket子协议授权,它使用ChannelInterceptor
根据消息中的用户头对消息进行授权。此外,Spring Session还提供了WebSocket集成,确保在WebSocket会话仍处于活动状态时用户的HTTP会话不会过期。
4.3.15. User Destinations
应用程序可以发送针对特定用户的消息,为此,Spring的STOMP支持识别以/user/
为前缀的目的地。例如,客户端可能订阅了/user/Queue/Position-Upments
目的地。UserDestinationMessageHandler
处理此目标并将其转换为用户会话唯一的目标(如/queue/position-updates-user123
).这提供了订阅一般命名的目的地的便利性,同时确保与订阅相同目的地的其他用户不冲突,从而每个用户可以接收唯一的股票头寸更新。
When working with user destinations, it is important to configure broker and application destination prefixes as shown in Enable STOMP, or otherwise the broker would handle "/user" prefixed messages that should only be handled by UserDestinationMessageHandler . |
在发送端,消息可以被发送到一个目的地,比如/user/{username}/queue/position-updates
,,然后UserDestinationMessageHandler
将其转换为一个或多个目的地,每个目的地对应一个与用户相关联的会话。这允许应用程序中的任何组件发送以特定用户为目标的消息,而不必知道他们的名称和通用目的地以外的任何信息。这也是通过注释和消息模板来支持的。
消息处理方法可以通过@SendToUser
注释(在类级别上也受支持以共享公共目的地)将消息发送给与正在处理的消息相关联的用户,如下面的示例所示:
@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}
如果用户有多个会话,则默认情况下,订阅给定目标的所有会话都是目标会话。但是,有时可能需要只针对发送正在处理的消息的会话。您可以通过将Broadcast
属性设置为False来执行此操作,如下面的示例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}
While user destinations generally imply an authenticated user, it is not strictly required. A WebSocket session that is not associated with an authenticated user can subscribe to a user destination. In such cases, the @SendToUser annotation behaves exactly the same as with broadcast=false (that is, targeting only the session that sent the message being handled). |
您可以从任何应用程序组件向用户目的地发送消息,例如,通过注入由Java配置或XML命名空间创建的SimpMessagingTemplate
。(如果需要使用@限定符
进行限定,则Bean名称为brokerMessagingTemplate
。)以下示例显示了如何执行此操作:
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
When you use user destinations with an external message broker, you should check the broker documentation on how to manage inactive queues, so that, when the user session is over, all unique user queues are removed. For example, RabbitMQ creates auto-delete queues when you use destinations such as /exchange/amq.direct/position-updates . So, in that case, the client could subscribe to /user/exchange/amq.direct/position-updates . Similarly, ActiveMQ has configuration options for purging inactive destinations. |
在多应用程序服务器方案中,用户目标可能仍未解析,因为该用户连接到不同的服务器。在这种情况下,您可以将目标配置为广播未解析的消息,以便其他服务器有机会尝试。这可以通过Java配置中的MessageBrokerRegistry
的userDestinationBroadcast
属性和XML中的Message-Broker
元素的user-estinationBroadcast
属性来完成。
4.3.16. Order of Messages
来自代理的消息被发布到ClientOutundChannel
,从那里它们被写入WebSocket会话。由于通道由ThreadPoolExecutor
支持,因此消息在不同的线程中处理,客户端收到的结果序列可能与发布的顺序不完全匹配。
如果这是一个问题,请启用setPReserve vePublishOrder
标志,如下例所示:
@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>
当设置该标志时,同一客户端会话中的消息将一次发布到客户端OutundChannel
,从而保证发布的顺序。请注意,这会带来很小的性能开销,因此您应该仅在需要时启用它。
4.3.17. Events
发布了几个ApplicationContext
事件,可以通过实现Spring的ApplicationListener
接口来接收这些事件:
-
BrokerAvailablityEvent
:Broker何时变为可用或不可用。虽然“简单”代理在启动时立即可用,并在应用程序运行期间保持可用,但STOMP“代理中继”可能会失去与功能齐全的代理的连接(例如,如果重新启动代理)。代理中继具有重新连接逻辑,并在返回时重新建立到代理的“系统”连接。因此,只要状态从已连接更改为已断开,就会发布此事件,反之亦然。使用SimpMessagingTemplate
的组件应该订阅此事件,并避免在代理不可用时发送消息。在任何情况下,它们都应该准备好在发送消息时处理MessageDeliveryException
。 -
SessionConnectEvent
:收到新的STOP CONNECT时发布,以指示新客户端会话的开始。该事件包含表示连接的消息,其中包括会话ID、用户信息(如果有)以及客户端发送的任何自定义标头。这对于跟踪客户端会话很有用。订阅此事件的组件可以使用SimpMessageHeaderAccessor
或StompMessageHeaderAccessor
包装所包含的消息。 -
SessionConnectedEvent
:发布在SessionConnectEvent
之后不久,此时代理已发送STOMP Connected帧以响应连接。在这一点上,STOMP会议可以被认为完全建立。 -
SessionSubscribeEvent
:收到新的STOP订阅时发布。 -
SessionUnbecbeEvent
:收到新的STOMP取消订阅时发布。 -
SessionDisConnectEvent
:在STOMP会话结束时发布。断开连接可能是从客户端发送的,也可能是在关闭WebSocket会话时自动生成的。在某些情况下,每个会话多次发布此事件。对于多个断开事件,组件应该是幂等的。
When you use a full-featured broker, the STOMP “broker relay” automatically reconnects the “system” connection if broker becomes temporarily unavailable. Client connections, however, are not automatically reconnected. Assuming heartbeats are enabled, the client typically notices the broker is not responding within 10 seconds. Clients need to implement their own reconnecting logic. |
4.3.18. Interception
事件为STOP连接的生命周期提供通知,但不是为每条客户端消息提供通知。应用程序还可以注册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
的线程中提供挂钩。
注意,与前面描述的SessionDisConnectEvent
一样,断开消息可以来自客户端,也可以在WebSocket会话关闭时自动生成。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开事件,组件应该是幂等的。
4.3.19. STOMP Client
Spring提供了对WebSocket客户端的踩踏和对TCP客户端的踩踏。
首先,您可以创建并配置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");
您还可以订阅目的地。订阅
方法需要订阅消息的处理程序,并返回可用于取消订阅的订阅
句柄。对于每个接收到的消息,处理程序可以指定有效负载应反序列化为的目标对象
类型,如下面的示例所示:
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
要启用STOMP心跳,您可以使用TaskScheduler
配置WebSocketStompClient
,并选择性地自定义心跳间隔(10秒表示写入不活动,这将导致发送心跳;10秒表示读取不活动,则关闭连接)。
WebSocketStompClient
仅在处于非活动状态时发送心跳,即没有发送其他消息时。在使用外部代理时,这可能会带来挑战,因为带有非代理目的地的消息代表活动,但实际上不会转发到代理。在这种情况下,您可以在初始化外部代理时配置TaskScheduler
,以确保在仅发送带有非代理目的地的消息时也将心跳转发到代理。
When you use WebSocketStompClient for performance tests to simulate thousands of clients from the same machine, consider turning off heartbeats, since each connection schedules its own heartbeat tasks and that is not optimized for a large number of clients running on the same machine. |
STOMP协议还支持回执,其中客户端必须添加回执
标头,服务器在处理发送或订阅后使用回执帧对其进行响应。为了支持这一点,StompSession
提供了setAutoReceipt(Boolean)
,这会导致在每个后续的发送或订阅事件上添加一个Receipt
头。或者,您也可以手动将收据标头添加到StompHeaders
。Send和Subscribe都返回一个Receiptable
实例,您可以使用该实例注册接收成功和失败回调。对于此功能,您必须为客户端配置TaskScheduler
和回执过期前的时间量(默认为15秒)。
请注意,StompSessionHandler
本身是一个StompFrameHandler
,这使它除了处理来自消息处理的异常的handleException
回调和包括ConnectionLostException
在内的传输级错误的handleTransportError
外,还允许它处理错误帧。
4.3.20. WebSocket Scope
每个WebSocket会话都有一个属性映射。映射作为报头附加到入站客户端消息,并可从控制器方法访问,如下例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
// ...
}
}
您可以在WebSocket
作用域中声明一个Spring管理的Bean。您可以将WebSocket作用域的Bean注入客户端InundChannel
上注册的控制器和任何通道拦截器。这些都是典型的单例,比任何单独的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.3.21. Performance
当谈到表现时,没有灵丹妙药。影响它的因素很多,包括消息的大小和数量、应用程序方法是否执行需要阻止的工作,以及外部因素(如网络速度和其他问题)。本部分的目标是概述可用的配置选项,以及一些关于如何考虑可伸缩性的想法。
在消息传递应用程序中,消息通过由线程池支持的异步执行通道传递。配置这样的应用程序需要对通道和消息流有很好的了解。因此,建议查看消息流。
显而易见的起点是配置支持clientInundChannel
和clientOutundChannel
的线程池。默认情况下,两者的配置都是可用处理器数量的两倍。
如果带注释的方法中的消息处理主要是受CPU限制的,则clientInundChannel
的线程数量应该与处理器的数量接近。如果它们所做的工作更受IO限制,并且需要在数据库或其他外部系统上阻塞或等待,则可能需要增加线程池大小。
一个常见的混淆是,配置核心池大小(例如10)和最大池大小(例如20)会导致线程池具有10到20个线程。事实上,如果将容量保留为其缺省值Integer.MAX_VALUE,线程池的大小永远不会超过核心池大小,因为所有其他任务都要排队。 请参阅 |
在clientOutundChannel
端,这一切都是为了向WebSocket客户端发送消息。如果客户端位于快速网络上,则线程数量应保持在接近可用处理器数量的水平。如果它们速度慢或带宽低,它们使用消息的时间会更长,并会给线程池带来负担。因此,增加线程池大小变得很有必要。
虽然客户端的工作负载< -
毕竟是可以预测的,但它是基于应用程序所做的 - 如何配置“客户端外界通道”比较困难,因为它基于应用程序无法控制的因素。因此,还有两个属性与发送消息相关:sendTimeLimit
和sendBufferSizeLimit
。您可以使用这些方法来配置在向客户端发送消息时允许发送的时间以及可以缓冲的数据量。
一般的想法是,在任何给定的时间,只有一个线程可以用于发送到客户端。与此同时,所有其他消息都会被缓冲,您可以使用这些属性来确定允许发送消息的时间以及在此期间可以缓冲的数据量。有关重要的附加细节,请参阅该XML模式的javadoc和文档。
以下示例显示了一种可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
以下示例显示了与前面的示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
<!-- ... -->
</websocket:message-broker>
</beans>
您还可以使用前面显示的WebSocket传输配置来配置传入STOMP消息的最大允许大小。理论上,WebSocket消息的大小几乎可以是无限的。在实践中,WebSocket服务器对 - 施加限制,例如,对Tomcat设置8K限制,对Jetty设置64K限制。因此,STOMP客户端(如JavaScriptWebstomp-Client等)在16K边界拆分较大的STOMP消息,并将它们作为多个WebSocket消息发送,这需要服务器进行缓冲和重新组装。
Spring的基于WebSocket的践踏支持做到了这一点,因此应用程序可以为践踏消息配置最大大小,而无需考虑特定于WebSocket服务器的消息大小。请记住,如果需要,WebSocket消息大小会自动调整,以确保它们最少可以承载16K WebSocket消息。
以下示例显示了一种可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
以下示例显示了与前面的示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport message-size="131072" />
<!-- ... -->
</websocket:message-broker>
</beans>
有关伸缩的一个重要问题是使用多个应用程序实例。目前,您不能使用简单代理执行此操作。但是,当您使用功能齐全的代理(如RabbitMQ)时,每个应用程序实例都连接到该代理,并且从一个应用程序实例广播的消息可以通过该代理广播到通过任何其他应用程序实例连接的WebSocket客户端。
4.3.22. Monitoring
当您使用<代码>@EnableWebSocketMessageBroker
或<;websocket:message-broker>;
,时,关键基础设施组件会自动收集统计信息和计数器,为了解应用程序的内部状态提供重要信息。该配置还声明了一个WebSocketMessageBrokerStats
类型的Bean,该Bean将所有可用信息收集在一个位置,并在默认情况下每30分钟将其记录在INFO
级别。可以通过Spring的MBeanExporter
将该Bean导出到JMX,以便在运行时查看(例如,通过JDK的j控制台
)。以下列表总结了可用的信息:
- Client WebSocket Sessions
-
- Current
-
指示当前有多少客户端会话,计数按WebSocket与HTTP流和轮询SockJS会话进一步细分。
- Total
-
指示已建立的会话总数。
- Abnormally Closed
-
- Connect Failures
-
已建立但在60秒内未收到任何消息而关闭的会话。这通常是代理或网络问题的指示。
- Send Limit Exceeded
-
会话在超过配置的发送超时或发送缓冲区限制后关闭,慢速客户端可能会出现这种情况(请参阅上一节)。
- Transport Errors
-
出现传输错误后,会话关闭,例如无法读取或写入WebSocket连接或HTTP请求或响应。
- STOMP Frames
-
已处理的CONNECT、CONNECTED和DISCONNECT帧的总数,表示在STOMP级别上连接的客户端数量。请注意,当会话异常关闭或客户端在未发送断开帧的情况下关闭时,断开计数可能会更低。
- STOMP Broker Relay
-
- TCP Connections
-
指示代表客户端WebSocket会话与代理建立的TCP连接的数量。这应该等于客户端WebSocket会话数+1个用于从应用程序内发送消息的额外共享“系统”连接。
- STOMP Frames
-
代表客户端转发到代理或从代理接收的CONNECT、CONNECTED和DISCONNECT帧总数。请注意,无论客户端WebSocket会话是如何关闭的,都会向代理发送断开连接帧。因此,较低的断开帧计数表示代理正在主动关闭连接(可能是因为未及时到达的心跳、无效的输入帧或其他问题)。
- Client Inbound Channel
-
来自支持
客户端的线程池的统计信息
,这些统计信息提供了对传入消息处理的健康状况的洞察。在这里排队的任务表明应用程序可能太慢,无法处理消息。如果存在I/O受限任务(例如,缓慢的数据库查询、对第三方REST API的HTTP请求等),请考虑增加线程池大小。 - Client Outbound Channel
-
来自支持
ClientOutundChannel
的线程池的统计信息,该线程池提供对向客户端广播消息的运行状况的洞察。在这里排队的任务表明客户端速度太慢,无法使用消息。解决此问题的一种方法是增加线程池大小,以适应预期数量的并发慢速客户端。另一种选择是减少发送超时和发送缓冲区大小限制(请参阅上一节)。 - SockJS Task Scheduler
-
来自用于发送心跳的SockJS任务计划程序的线程池的统计信息。请注意,当在STOMP级别协商心跳时,将禁用SockJS心跳。
4.3.23. Testing
当您使用Spring的基于WebSocket的踩踏支持时,有两种主要方法来测试应用程序。第一种方法是编写服务器端测试,以验证控制器及其带注释的消息处理方法的功能。第二种方法是编写完整的端到端测试,包括运行客户端和服务器。
这两种方法并不是相互排斥的。相反,每一个都在总体测试策略中占有一席之地。服务器端测试更有针对性,更易于编写和维护。另一方面,端到端集成测试更完整,测试也更多,但它们也更涉及编写和维护。
服务器端测试的最简单形式是编写控制器单元测试。然而,这并不是很有用,因为控制器所做的很多事情都依赖于它的注释。纯粹的单元测试根本无法测试这一点。
理想情况下,被测试的控制器应该像它们在运行时一样被调用,非常类似于通过使用Spring MVC测试框架 - 来测试处理HTTP请求的控制器的方法,也就是说,不运行Servlet容器,但依赖于Spring框架来调用带注释的控制器。与Spring MVC测试一样,这里有两种可能的选择,要么使用“基于上下文的”设置,要么使用“独立的”设置:
-
借助Spring TestContext框架加载实际的Spring配置,注入
clientInundChannel
作为测试字段,并使用它发送要由控制器方法处理的消息。 -
手动设置调用控制器所需的最低Spring框架基础设施(即
SimpAnnotationMethodMessageHandler
),并将控制器的消息直接传递给它。
股票投资组合示例应用程序的测试演示了这两个设置场景。
第二种方法是创建端到端集成测试。为此,您需要在嵌入式模式下运行WebSocket服务器,并作为发送包含STOMP帧的WebSocket消息的WebSocket客户端连接到它。股票投资组合示例应用程序的测试也演示了这种方法,它使用Tomcat作为嵌入式WebSocket服务器,并使用一个简单的STOMP客户端进行测试。
5. Other Web Frameworks
本章详细介绍了Spring与第三方Web框架的集成。
Spring框架的核心价值主张之一是支持选择。在一般意义上,Spring不会强迫您使用或购买任何特定的体系结构、技术或方法(尽管它肯定会推荐一些)。这种挑选与开发人员及其开发团队最相关的架构、技术或方法的自由可以说在Web领域最为明显,在Web领域,Spring提供了自己的Web框架(Spring MVC和Spring WebFlux),同时支持与许多流行的第三方Web框架集成。
5.1. Common Configuration
在深入讨论每个支持的Web框架的集成细节之前,让我们先来看看并非特定于任何一个Web框架的常见的Spring配置。(这一节同样适用于Spring自己的Web框架变体。)
Spring的轻量级应用程序模型支持的概念之一是分层体系结构。请记住,在“经典的”分层体系结构中,Web层只是众多层中的一层。它充当服务器端应用程序的入口点之一,并委托服务层中定义的服务对象(外观)来满足特定于业务(和表示技术不可知)的用例。在Spring中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象存在于不同的“业务上下文”中,不包含Web或表示层对象(表示对象,如Spring MVC控制器,通常配置在不同的“表示上下文”中)。本节详细介绍如何配置包含应用程序中所有“业务Bean”的Spring容器(WebApplicationContext
)。
接下来,您需要做的就是在Web应用程序的标准Jakarta EE Servlet
ContextLoaderListener
>web.xml
contextConfigLocation<;context-param/>;>文件中声明一个
请考虑以下<;侦听器/
配置:
<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>
如果不指定ConextConfigLocation
上下文参数,ConextLoaderListener
将查找名为/WEB-INF/ApplationContext.xml
的文件进行加载。加载上下文文件后,Spring会根据Bean定义创建
WebApplicationContext
对象,并将其存储在Web应用程序的
ServletContext
中。
所有Java Web框架都构建在Servlet API之上,因此您可以使用以下代码片段来访问由ConextLoaderListener
创建的“业务上下文”ApplicationContext
。
以下示例显示如何获取WebApplicationContext
:
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
一旦有了对WebApplicationContext
的引用,就可以按它们的名称或类型检索Bean。大多数开发人员按名称检索Bean,然后将它们转换为他们实现的接口之一。
幸运的是,本节中的大多数框架都有更简单的方法来查找Bean。它们不仅使从Spring容器中获取Bean变得容易,而且还允许您在其控制器上使用依赖项注入。每个Web框架部分都有关于其具体集成策略的更多详细信息。
5.2. JSF
JavaServer Faces(JSF)是JCP基于组件、事件驱动的标准Web用户界面框架。它是Jakarta EE保护伞的官方部分,但也可以单独使用,例如通过在Tomcat中嵌入mojarra或MyFaces。
请注意,JSF的最新版本与应用程序服务器中的CDI基础设施紧密相关,一些新的JSF功能只能在这样的环境中工作。Spring的JSF支持不再是主动演变的,主要是为了在更新旧的基于JSF的应用程序时进行迁移。
Spring的JSF集成中的关键元素是JSFELResolver
机制。
5.2.1. Spring Bean Resolver
SpringBeanFacesELResolver
是符合JSF的ELResolver
实现,与JSF和JSP使用的标准统一EL集成。它首先委托给Spring的“业务上下文”WebApplicationContext
,然后委托给底层JSF实现的默认解析器。
在配置方面,可以在JSFFaces-context.xml
文件中定义SpringBeanFacesELResolver
,如下例所示:
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
5.2.2. Using FacesContextUtils
在将属性映射到Faces-config.xml
中的Bean时,定制的ELResolver
可以很好地工作,但有时您可能需要显式获取Bean。FacesContextUtils
类使这一点变得很容易。它类似于WebApplicationContextUtils
,不同之处在于它接受FacesContext
参数,而不是ServletContext
参数。
以下示例显示如何使用FacesContextUtils
:
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
5.3. Apache Struts 2.x
Struts由Craig McClanahan发明,是由阿帕奇软件基金会托管的开源项目。当时,它极大地简化了JSP/Servlet编程范例,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此是免费的,就像在Beer中一样),而且它有一个庞大的社区,这让该项目得以发展,并在Java Web开发人员中流行起来。
作为原始Struts 1.x的后继者,请查看Struts 2.x和Struts-为内置的Spring集成提供的Spring插件。