Web Servlet
1. Spring Web MVC
Spring Web MVC 是构建在 Servlet API 上的原始 Web 框架,已被包含在内
在 Spring Framework 中。正式名称“Spring Web MVC”,
来自其源模块的名称
(spring-webmvc
)、
但它更通常被称为 “Spring MVC”。
与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个反应式堆栈 Web 框架
其名称“Spring WebFlux”也基于其源模块
(spring-webflux
) 中。
本章介绍 Spring Web MVC。下一章将介绍 Spring WebFlux。
有关基准信息以及与 Servlet 容器和 Java EE 版本的兼容性 范围,请参阅 Spring Framework Wiki。
1.1. DispatcherServlet
Spring MVC 与许多其他 Web 框架一样,是围绕前端控制器设计的
模式,其中 Central , the , 提供共享算法
用于请求处理,而实际工作则由可配置的委托组件执行。
此模型非常灵活,并支持多种工作流。Servlet
DispatcherServlet
与 any 一样,需要根据 声明和映射
到 Servlet 规范中。
反过来,使用 Spring 配置来发现
请求映射、视图解析、异常所需的委托组件
处理等。DispatcherServlet
Servlet
web.xml
DispatcherServlet
下面的 Java 配置示例注册并初始化
,它由 Servlet 容器自动检测
(请参阅 Servlet 配置):DispatcherServlet
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// Load Spring web application configuration
val context = AnnotationConfigWebApplicationContext()
context.register(AppConfig::class.java)
// Create and register the DispatcherServlet
val servlet = DispatcherServlet(context)
val registration = servletContext.addServlet("app", servlet)
registration.setLoadOnStartup(1)
registration.addMapping("/app/*")
}
}
除了直接使用 ServletContext API 之外,您还可以扩展和覆盖特定方法
(请参阅 Context Hierarchy 下的示例)。AbstractAnnotationConfigDispatcherServletInitializer |
对于编程用例,a 可用作
替代 。有关详细信息,请参见 GenericWebApplicationContext javadoc。GenericWebApplicationContext AnnotationConfigWebApplicationContext |
下面的配置示例注册并初始化 :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 遵循不同的初始化 Sequences。而不是挂接
Servlet 容器的生命周期中,Spring Boot 使用 Spring 配置来
bootstrap 本身和嵌入式 Servlet 容器。 和声明
在 Spring 配置中检测到并注册到 Servlet 容器中。
有关更多详细信息,请参阅 Spring Boot 文档。Filter Servlet |
1.1.1. 上下文层次结构
DispatcherServlet
期望 a (plain 的扩展 )用于其自己的配置。 具有指向与其关联的 和 的链接。它还绑定到 ,以便应用程序可以使用静态方法 on 来查找是否需要访问它。WebApplicationContext
ApplicationContext
WebApplicationContext
ServletContext
Servlet
ServletContext
RequestContextUtils
WebApplicationContext
对于许多应用程序,拥有一个简单而足够的。
也可以有一个上下文层次结构,其中一个根在多个(或其他)实例之间共享,每个实例具有
它自己的子配置。
有关上下文层次结构功能的更多信息,请参见ApplicationContext
的其他功能。WebApplicationContext
WebApplicationContext
DispatcherServlet
Servlet
WebApplicationContext
根通常包含基础设施 Bean,例如数据存储库和
需要在多个实例之间共享的业务服务。那些豆子
可以有效地继承,并且可以在特定于 Servlet 的
child ,它通常包含给定 的本地 bean 。
下图显示了此关系:WebApplicationContext
Servlet
WebApplicationContext
Servlet
以下示例配置层次结构:WebApplicationContext
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>> {
return arrayOf(RootConfig::class.java)
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(App1Config::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/app1/*")
}
}
如果不需要应用程序上下文层次结构,则应用程序可以返回所有
配置 through 和 from 。getRootConfigClasses() null getServletConfigClasses() |
以下示例显示了等效项:web.xml
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
如果不需要应用程序上下文层次结构,应用程序可以配置
“root” 上下文,并将 Servlet 参数留空。contextConfigLocation |
1.1.2. 特殊 bean 类型
委托给特殊 bean 来处理请求并呈现
适当的回应。我们所说的“特殊 bean”是指 Spring 管理的实例,这些实例
实施框架协定。这些通常带有内置合约,但
您可以自定义其属性并扩展或替换它们。DispatcherServlet
Object
下表列出了 检测到的特殊 bean:DispatcherServlet
Bean 类型 | 解释 |
---|---|
|
将请求映射到处理程序以及用于预处理和后处理的拦截器列表。
映射基于一些标准,其详细信息因实施而异。 两个主要的实现是(支持带注释的方法)和(维护对处理程序的 URI 路径模式的显式注册)。 |
|
帮助 调用映射到请求的处理程序,而不管
处理程序的实际调用方式。例如,调用带注释的控制器
需要解析注释。a 的主要目的是
以保护 免受此类细节的影响。 |
解决异常的策略,可能将它们映射到处理程序,再到 HTML 错误 视图或其他目标。请参阅例外。 |
|
将从处理程序返回的基于 logical 的视图名称解析为要呈现给响应的 actual。请参阅视图分辨率和视图技术。 |
|
解决客户端正在使用的时区,以便能够
提供国际化视图。请参阅 区域设置。 |
|
解决 Web 应用程序可以使用的主题,例如,提供个性化布局。 请参阅主题。 |
|
用于解析多部分请求(例如,浏览器表单文件上传)的抽象 一些 Multipart 解析库的帮助。请参阅Multipart Resolver。 |
|
存储和检索可用于传递的 “input” 和 “output”
属性从一个请求到另一个请求,通常通过重定向。
请参阅 Flash 属性。 |
1.1.3. Web MVC 配置
应用程序可以声明处理请求所需的 Special Bean Types 中列出的基础结构 Bean。检查每个特殊 bean。如果没有匹配的 bean 类型,则
它回退到 DispatcherServlet.properties
中列出的默认类型。DispatcherServlet
WebApplicationContext
在大多数情况下,MVC 配置是最好的起点。它声明了所需的 bean 中,并提供更高级别的配置回调 API,以 自定义它。
Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC 和 提供了许多额外的便捷选项。 |
1.1.4. Servlet 配置
在 Servlet 3.0+ 环境中,您可以选择配置 Servlet 容器
以编程方式作为替代项或与文件组合。以下内容
示例寄存器 A :web.xml
DispatcherServlet
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
import org.springframework.web.WebApplicationInitializer
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
WebApplicationInitializer
是 Spring MVC 提供的一个接口,可确保您的
实现,并自动用于初始化任何 Servlet 3 容器。
named 的抽象基类实现使得通过覆盖方法来指定 servlet 映射和
配置的位置。WebApplicationInitializer
AbstractDispatcherServletInitializer
DispatcherServlet
DispatcherServlet
对于使用基于 Java 的 Spring 配置的应用程序,建议这样做,因为 以下示例显示:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return arrayOf(MyWebConfig::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
如果使用基于 XML 的 Spring 配置,则应直接从 扩展,如下例所示:AbstractDispatcherServletInitializer
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
override fun createRootApplicationContext(): WebApplicationContext? {
return null
}
override fun createServletApplicationContext(): WebApplicationContext {
return XmlWebApplicationContext().apply {
setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
}
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
AbstractDispatcherServletInitializer
还提供了一种添加实例并将其自动映射到 的便捷方法,因为
以下示例显示:Filter
DispatcherServlet
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
// ...
override fun getServletFilters(): Array<Filter> {
return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
}
}
每个过滤器都会根据其具体类型自动添加默认名称
映射到 .DispatcherServlet
受保护的方法 of 提供了一个位置来启用 和 all 的异步支持
映射到它的 filters。默认情况下,此标志设置为 .isAsyncSupported
AbstractDispatcherServletInitializer
DispatcherServlet
true
最后,如果需要进一步自定义自身,可以
重写该方法。DispatcherServlet
createDispatcherServlet
1.1.5. 处理
按如下方式处理请求:DispatcherServlet
-
在请求中搜索并绑定 控制器和进程中的其他元素可以使用。默认情况下它是绑定的 在键下。
WebApplicationContext
DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
-
区域设置解析器绑定到请求,以 let 进程中的元素 解析在处理请求(渲染视图、准备 data 等)。如果不需要 locale 解析,则不需要 locale 解析程序。
-
主题解析程序绑定到请求,让视图等元素确定 使用哪个主题。如果您不使用主题,则可以忽略它。
-
如果指定 multipart 文件解析程序,则会检查请求是否存在 multipart。如果 multipart 的 API 中,请求将包装在 for 在此过程中通过其他元素进行进一步处理。有关详细信息,请参阅 Multipart Resolver 有关分段处理的信息。
MultipartHttpServletRequest
-
寻找合适的处理程序。如果找到处理程序,则执行链 与处理程序(预处理器、后处理器和控制器)关联的是 run 准备用于渲染的模型。或者,对于带注释的 控制器,响应可以呈现(在 ) 而不是 返回一个视图。
HandlerAdapter
-
如果返回模型,则呈现视图。如果未返回任何模型(可能是由于 拦截请求的预处理器或后处理器,可能是为了安全 reasons),则不会呈现任何视图,因为请求可能已经完成。
在 中声明的 bean 用于
解决请求处理期间引发的异常。这些异常解析程序允许
自定义逻辑以解决异常。有关更多详细信息,请参阅例外。HandlerExceptionResolver
WebApplicationContext
对于 HTTP 缓存支持,处理程序可以使用 ,
以及带注释的控制器的更多选项,如 控制器的 HTTP 缓存中所述。checkNotModified
WebRequest
您可以通过添加 Servlet 来自定义单个实例
初始化参数 ( 元素) 添加到文件中的 Servlet 声明中。下表列出了支持的参数:DispatcherServlet
init-param
web.xml
参数 | 解释 |
---|---|
|
实现 、 要实例化的类,并且
由此 Servlet 在本地配置。默认情况下,使用。 |
|
传递给上下文实例(由 指定 )的字符串
指示可以找到上下文的位置。字符串可能包含多个
字符串(使用逗号作为分隔符)来支持多个上下文。在
具有定义两次的 bean 的多个上下文位置,即最新的位置
优先。 |
|
的命名空间。默认为 。 |
|
当找不到请求的处理程序时是否引发 。
然后,可以使用 a 捕获异常(例如,通过使用控制器方法)并像处理任何其他异常一样处理。 默认情况下,此设置为 ,在这种情况下,会将
响应状态设置为 404 (NOT_FOUND),而不引发异常。 请注意,如果默认 servlet 处理为 此外,未解析的请求始终转发到默认 servlet 并且 404 永远不会提高。 |
1.1.6. 路径匹配
Servlet API 公开了完整的请求路径,并进一步对其进行细分
转换为 、 ,并且其值根据
Servlet 已映射。从这些输入中, Spring MVC 需要确定到
用于处理程序映射,这是自身映射中的路径,不包括 和 any 前缀(如果存在)。requestURI
contextPath
servletPath
pathInfo
DispatcherServlet
contextPath
servletMapping
和 被解码,这使得它们无法比较
直接转换为 full,以便派生 lookupPath,这使得它
解码 .但是,这引入了它自己的问题,因为
path 可能包含编码的保留字符,例如 或
在解码后更改路径的结构,这也可能导致安全性
问题。此外,Servlet 容器可以将
度,这使得无法进一步执行与
这。servletPath
pathInfo
requestURI
requestURI
"/"
";"
servletPath
startsWith
requestURI
这就是为什么最好避免依赖 which 附带的
基于前缀的映射类型。如果 映射为
default Servlet 带有或不带有前缀 with,并且 Servlet
container 为 4.0+,则 Spring MVC 能够检测 Servlet 映射类型并避免
使用 和 一起。在 3.1 Servlet 容器上,
假设相同的 Servlet 映射类型,则可以通过提供
a with via Path Matching in
MVC 配置。servletPath
servletPath
DispatcherServlet
"/"
"/*"
servletPath
pathInfo
UrlPathHelper
alwaysUseFullPath=true
幸运的是,默认的 Servlet 映射是一个不错的选择。然而,仍然有
一个问题在于需要解码才能与
controller 映射。这同样是不可取的,因为有可能解码
更改路径结构的保留字符。如果不需要此类字符,
然后你可以拒绝它们(如 Spring Security HTTP 防火墙),或者你可以配置,但控制器映射需要与
编码路径,这可能并不总是正常工作。此外,有时需要与另一个 Servlet 共享 URL 空间,并且可能需要
由 prefix 映射。"/"
requestURI
UrlPathHelper
urlDecode=false
DispatcherServlet
上述问题可以通过从 切换到
parsed 在 5.3 或更高版本中可用,请参阅 Pattern Comparison。不同于需要
无论是解码的查找路径还是编码的控制器映射,解析的 A 都会匹配名为 的 PATH 的解析表示,一个路径段
一次。这允许单独解码和清理 path segment 值,而无需
改变路径结构的风险。Parsed 还支持
使用前缀映射只要前缀保持简单并且确实
没有任何需要编码的字符。PathMatcher
PathPattern
AntPathMatcher
PathPattern
RequestPath
PathPattern
servletPath
1.1.7. 拦截
所有实现都支持处理程序拦截器,这些拦截器在以下情况下很有用
您希望将特定功能应用于某些请求 — 例如,检查
一个 principal。拦截器必须从包中实现三种方法,这些方法应该提供足够的
灵活地进行各种前处理和后处理:HandlerMapping
HandlerInterceptor
org.springframework.web.servlet
-
preHandle(..)
:在实际处理程序运行之前 -
postHandle(..)
:处理程序运行后 -
afterCompletion(..)
:完成请求完成后
该方法返回一个布尔值。您可以使用此方法 break 或
继续处理执行链。当此方法返回 时,
处理程序执行链继续。当它返回 false 时,假设拦截器本身已经处理了请求(例如,渲染了一个
appropriate 视图),并且不会继续执行其他拦截器和实际的
处理程序。preHandle(..)
true
DispatcherServlet
有关如何
配置拦截器。您还可以通过在各个 implementations 上使用 setter 直接注册它们。HandlerMapping
postHandle
method 对 和 methods 的用处不大
,响应在 和 之前写入和提交。这意味着现在对响应进行任何更改都为时已晚,例如将
一个额外的标头。对于此类方案,您可以实施 and either
将其声明为 Controller Advice bean 或直接在 上配置它。@ResponseBody
ResponseEntity
HandlerAdapter
postHandle
ResponseBodyAdvice
RequestMappingHandlerAdapter
1.1.8. 异常
如果在请求映射期间发生异常,或者从请求处理程序(例如
a ),将委托给 bean 链以解决异常并提供替代处理,这通常是
error 响应。@Controller
DispatcherServlet
HandlerExceptionResolver
下表列出了可用的实施:HandlerExceptionResolver
HandlerExceptionResolver |
描述 |
---|---|
|
异常类名称和错误视图名称之间的映射。用于渲染 浏览器应用程序中的错误页面。 |
解决 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码。
另请参阅替代和 REST API 例外。 |
|
|
使用注释解决异常并将其映射到 HTTP 状态
代码。 |
|
通过调用 a 或 a 类中的方法来解决异常。请参阅 @ExceptionHandler方法。 |
解析器链
您可以通过在 Spring 配置中声明多个 bean 并根据需要设置其属性来形成异常解析器链。
order 属性越高,异常解析程序的位置就越晚。HandlerExceptionResolver
order
的 Contract 指定它可以返回:HandlerExceptionResolver
-
a 指向错误视图。
ModelAndView
-
如果异常是在解析程序中处理的,则为空。
ModelAndView
-
null
如果异常仍未解决,则供后续解析程序尝试,并且如果 exception 保留在末尾,则允许它向上冒泡到 Servlet 容器。
MVC Config 自动为默认 Spring MVC 声明内置解析器
exceptions,用于带注释的异常,以及用于方法的支持。您可以自定义或替换该列表。@ResponseStatus
@ExceptionHandler
容器错误页面
如果异常仍未由 any 解决,并且是,则
left 进行传播,或者如果响应状态设置为错误状态(即 4xx、5xx),
Servlet 容器可以在 HTML 中呈现默认错误页。自定义默认
error page 中,您可以在 中声明错误页面映射。
以下示例显示了如何执行此操作:HandlerExceptionResolver
web.xml
<error-page>
<location>/error</location>
</error-page>
在前面的示例中,当异常冒泡或响应具有错误状态时,
Servlet 容器在容器内向配置的 URL 发出 ERROR 调度
(例如,)。然后由 处理,可能会将其映射为
更改为 ,该 URL 可以实现以返回带有 Model 的错误视图名称
或呈现 JSON 响应,如下例所示:/error
DispatcherServlet
@Controller
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
@RestController
class ErrorController {
@RequestMapping(path = ["/error"])
fun handle(request: HttpServletRequest): Map<String, Any> {
val map = HashMap<String, Any>()
map["status"] = request.getAttribute("javax.servlet.error.status_code")
map["reason"] = request.getAttribute("javax.servlet.error.message")
return map
}
}
Servlet API 不提供在 Java 中创建错误页面映射的方法。您可以
但是,请同时使用 a 和 minimal 。WebApplicationInitializer web.xml |
1.1.9. 视图分辨率
Spring MVC 定义了 and 接口,允许您渲染
模型,而无需将您绑定到特定的视图技术。 提供视图名称和实际视图之间的映射。 解决准备工作
的数据,然后再移交给特定的视图技术。ViewResolver
View
ViewResolver
View
下表提供了有关层次结构的更多详细信息:ViewResolver
视图解析器 | 描述 |
---|---|
|
它们解析的缓存视图实例的子类。
缓存可以提高某些视图技术的性能。您可以关闭
cache 通过将属性设置为 .此外,如果您必须刷新
运行时的某个视图(例如,当修改 FreeMarker 模板时),
您可以使用该方法。 |
|
影响直接
将逻辑视图名称解析为没有显式映射定义的 URL。
如果您的逻辑名称与视图资源的名称匹配,则此选项是合适的
以简单的方式,无需任意映射。 |
|
方便的子类支持 (在
effect、Servlet 和 JSP)和子类(如 和 )。您可以
使用 为此解析程序生成的所有视图指定视图类。
有关详细信息,请参见 |
|
方便的子类 that 支持 和
它们的自定义子类。 |
|
基于
请求文件名或标头。请参阅 内容协商。 |
|
将视图名称解释为
Bean 名称。这是一个非常灵活的变体,它
允许根据不同的视图名称混合和匹配不同的视图类型。
每个这样的都可以定义为一个 bean,例如在 XML 或配置类中。 |
处理
您可以通过声明多个解析器 Bean 来链接视图解析器,如有必要,还可以通过
设置属性以指定 ordering。请记住,order 属性越高,
View Resolver 在链中的位置越晚。order
a 的 Contract 指定它可以返回 null,以指示
找不到视图。但是,对于 JSP 和 ,
确定 JSP 是否存在的唯一方法是通过 执行分派。因此,您必须始终将 配置为视图解析程序总体顺序中的最后一个。ViewResolver
InternalResourceViewResolver
RequestDispatcher
InternalResourceViewResolver
配置视图分辨率就像向 Spring 添加 bean 一样简单
配置。MVC Config 为视图解析器和添加无逻辑视图控制器提供了一个专用的配置 API,这对 HTML 模板很有用
不使用控制器逻辑的渲染。ViewResolver
重 定向
视图名称中的特殊前缀允许您执行重定向。(及其子类)将此视为一个指令,即
redirect 是必需的。视图名称的其余部分是重定向 URL。redirect:
UrlBasedViewResolver
实际效果与控制器返回 相同,但现在
控制器本身可以根据 logical view name 进行操作。逻辑视图
name(例如 )相对于当前
Servlet 上下文,而 Names (如 Redirects) 重定向到绝对 URL.RedirectView
redirect:/myapp/some/resource
redirect:https://myhost.com/some/arbitrary/path
请注意,如果控制器方法使用 、 、
值优先于 .@ResponseStatus
RedirectView
转发
您还可以对
最终由 and 子类解决。这将创建一个 ,该 执行一个 .
因此,此前缀对 and(对于 JSP)没有用,但如果您使用其他视图,则可能会有所帮助
技术,但仍希望强制将资源的 forward 由
Servlet/JSP 引擎。请注意,您也可以链接多个视图解析器。forward:
UrlBasedViewResolver
InternalResourceView
RequestDispatcher.forward()
InternalResourceViewResolver
InternalResourceView
内容协商
ContentNegotiatingViewResolver
不解析视图本身,而是解析 delegate
添加到其他视图解析程序,并选择类似于所请求的表示的视图
由客户。表示可以从标题或
query 参数(例如 )。Accept
"/path?format=pdf"
选择适当的
通过将请求媒体类型与与其每个 关联的媒体类型(也称为 )进行比较。这
列表中的第一个具有 compatible 的 API 将返回表示
到客户端。如果链无法提供兼容的视图,则
将查阅通过属性指定的视图列表。这
后一个选项适用于可以呈现适当
表示当前资源,而不考虑逻辑视图名称。标头可以包含通配符(例如 ),在这种情况下,其 is 是兼容的匹配项。ContentNegotiatingViewResolver
View
Content-Type
View
ViewResolvers
View
Content-Type
ViewResolver
DefaultViews
Views
Accept
text/*
View
Content-Type
text/xml
有关配置详细信息,请参阅 MVC Config 下的 View Resolvers。
1.1.10. 区域设置
Spring 架构的大部分都支持国际化,因为 Spring Web
MVC 框架可以。 用于自动解决消息
通过使用客户端的区域设置。这是对对象完成的。DispatcherServlet
LocaleResolver
当请求传入时,它会查找 locale 解析器,如果
找到一个,它会尝试使用它来设置 locale。通过使用该方法,您始终可以检索由区域设置解析程序解析的区域设置。DispatcherServlet
RequestContext.getLocale()
除了自动 locale 解析之外,您还可以将拦截器附加到 handler 映射(有关 handler 的更多信息,请参阅 Interception mapping interceptor)来更改特定情况下的区域设置(例如 基于请求中的参数)。
区域设置解析器和拦截器在包中定义,并在应用程序中配置
context 的 intent 中。以下 locale 解析程序选择包含在
Spring。org.springframework.web.servlet.i18n
时区
除了获取客户端的区域设置之外,了解其时区通常也很有用。
该接口提供了一个扩展,允许
解析程序提供更丰富的 ,其中可能包括时区信息。LocaleContextResolver
LocaleResolver
LocaleContext
如果可用,可以使用 该方法获取 user's。自动使用时区信息
按任何日期/时间和向 Spring 的 .TimeZone
RequestContext.getTimeZone()
Converter
Formatter
ConversionService
Cookie 解析程序
此区域设置解析程序检查客户端上可能存在的 a,以查看是否指定了 or。如果是这样,它将使用指定的详细信息。通过使用
属性,您可以指定 Cookie 的名称以及
最大年龄。以下示例定义了一个 :Cookie
Locale
TimeZone
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
财产 | 违约 | 描述 |
---|---|---|
|
类名 + 区域设置 |
Cookie 的名称 |
|
Servlet 容器默认值 |
Cookie 在客户端上保留的最长时间。如果指定,则
Cookie 不会持久化。它仅在客户端关闭之前可用
浏览器。 |
|
/ |
将 Cookie 的可见性限制在网站的某个部分。时间
指定,则 Cookie 仅对该路径及其下面的路径可见。 |
会话解析程序
允许您从
会话。与 相反,此策略将本地选择的区域设置存储在
Servlet 容器的 .因此,这些设置是临时的
,因此,在每个会话结束时都会丢失。SessionLocaleResolver
Locale
TimeZone
CookieLocaleResolver
HttpSession
请注意,它与外部会话管理机制没有直接关系,
例如 Spring Session 项目。这将评估
针对当前 .SessionLocaleResolver
HttpSession
HttpServletRequest
区域设置拦截器
您可以通过将 to 其中一个定义来启用区域设置更改。它检测请求中的参数并更改区域设置
因此,在 Dispatcher 的
应用程序上下文。下一个示例显示对所有资源的调用
,其中包含名为 now 的参数会更改区域设置。所以,例如,
对 URL 的请求 ,将更改站点
语言到荷兰语。以下示例显示如何拦截 locale:LocaleChangeInterceptor
HandlerMapping
setLocale
LocaleResolver
*.view
siteLanguage
https://www.sf.net/home.view?siteLanguage=nl
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.11. 主题
您可以应用 Spring Web MVC 框架主题来设置 应用程序,从而增强用户体验。主题是 static 的集合 资源(通常是样式表和图像),这些资源会影响 应用。
定义主题
要在 Web 应用程序中使用主题,必须设置界面的实现。该接口扩展但将其职责委托给专用的
实现。默认情况下,委托是一个实现,它
从 Classpath 的根目录加载属性文件。要使用自定义实施或配置 ,
您可以在应用程序上下文中使用保留名称 .
Web 应用程序上下文会自动检测具有该名称的 bean 并使用它。org.springframework.ui.context.ThemeSource
WebApplicationContext
ThemeSource
org.springframework.ui.context.support.ResourceBundleThemeSource
ThemeSource
ResourceBundleThemeSource
themeSource
当您使用 时,主题将在简单的属性
文件。属性文件列出了构成主题的资源,如下例所示:ResourceBundleThemeSource
styleSheet=/themes/cool/style.css background=/themes/cool/img/coolBg.jpg
属性的键是引用视图中主题元素的名称
法典。对于 JSP,通常使用自定义标记执行此操作,该标记为
与标签非常相似。以下 JSP 片段使用主题
在前面的示例中定义,以自定义外观:spring:theme
spring:message
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
默认情况下,它使用空的基本名称前缀。因此,
属性文件是从 Classpath 的根目录加载的。因此,您可以将主题定义放在 Classpath 根目录下的目录中(对于
示例,在 中)。使用标准的 Java
Resource Bundle 加载机制,允许主题完全国际化。为
例如,我们可以有一个引用特殊
带有荷兰语文本的背景图像。ResourceBundleThemeSource
cool.properties
/WEB-INF/classes
ResourceBundleThemeSource
/WEB-INF/classes/cool_nl.properties
解决主题
定义主题后,如上一节所述,
您可以决定使用哪个主题。该 find find a bean named 以找出要使用的实现。主题解析程序的工作方式大致相同
方式设置为 .它检测要用于特定请求的主题,还可以
更改请求的主题。下表描述了 Spring 提供的主题解析程序:DispatcherServlet
themeResolver
ThemeResolver
LocaleResolver
类 | 描述 |
---|---|
|
选择使用 property 设置的固定主题。 |
|
主题在用户的 HTTP 会话中维护。它只需要为 每个会话,但不会在会话之间保留。 |
|
所选主题存储在客户端上的 Cookie 中。 |
Spring 还提供了一个 that let theme change on every
request 替换为简单的请求参数。ThemeChangeInterceptor
1.1.12. Multipart Resolver (多部分解析程序)
MultipartResolver
从包中是一种策略
用于解析分段请求,包括文件上传。有一个实现
基于 Commons FileUpload 和
另一个基于 Servlet 3.0 多部分请求解析。org.springframework.web.multipart
要启用多部分处理,您需要在 Spring 配置中声明一个名称为 .
它会检测到它并将其应用于传入请求。当 POST
当内容类型为 is received 时,解析程序会解析
content 将当前包装为 To
除了将部件作为请求参数公开外,还提供对已解析文件的访问。MultipartResolver
DispatcherServlet
multipartResolver
DispatcherServlet
multipart/form-data
HttpServletRequest
MultipartHttpServletRequest
Apache 共享资源FileUpload
要使用 Apache Commons ,您可以配置名称为 的 类型的 bean。您还需要拥有
将 jar 作为 Classpath 上的依赖项。FileUpload
CommonsMultipartResolver
multipartResolver
commons-fileupload
此解析程序变体委托给应用程序内的本地库,提供 在 Servlet 容器之间实现最大的可移植性。作为替代方案,请考虑标准 通过容器自己的解析器进行 Servlet 多部分解析,如下所述。
Commons FileUpload 传统上仅适用于 POST 请求,但接受任何内容类型。有关详细信息和配置选项,请参阅 |
Servlet 3.0
Servlet 3.0 多部分解析需要通过 Servlet 容器配置来启用。 为此,请执行以下操作:
-
在 Java 中,在 Servlet 注册上设置 a。
MultipartConfigElement
-
在 中,向 Servlet 声明添加一个部分。
web.xml
"<multipart-config>"
以下示例显示了如何在 Servlet 注册上设置 a:MultipartConfigElement
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
// ...
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(MultipartConfigElement("/tmp"))
}
}
一旦 Servlet 3.0 配置就位,您就可以添加一个 name 为 的 Bean 类型。StandardServletMultipartResolver
multipartResolver
此解析器变体按原样使用 Servlet 容器的多部分解析器,
可能会使应用程序面临容器实现差异。
默认情况下,它将尝试使用任何 HTTP 解析任何内容类型
方法,但并非所有 Servlet 容器都支持此方法。有关详细信息和配置选项,请参见 |
1.1.13. 日志记录
Spring MVC 中的 DEBUG 级日志记录被设计为紧凑、最小和 人性化。它侧重于有用的高价值信息 over again 与仅在调试特定问题时有用的其他方法进行验证。
TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,还遵循 不应是消防水带),但可用于调试任何问题。此外,一些日志 消息在 TRACE 和 DEBUG 中可能显示不同级别的详细信息。
良好的日志记录来自使用日志的经验。如果您发现任何 未达到既定目标,请告诉我们。
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息。这就是为什么请求参数和
默认情况下,标头是屏蔽的,并且必须显式启用其完整日志记录
通过 上的属性。enableLoggingRequestDetails
DispatcherServlet
以下示例演示如何使用 Java 配置执行此操作:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletMappings(): Array<String> {
return ...
}
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
registration.setInitParameter("enableLoggingRequestDetails", "true")
}
}
1.2. 过滤器
该模块提供了一些有用的过滤器:spring-web
1.2.1. 表单数据
浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以
使用 HTTP PUT、PATCH 和 DELETE。Servlet API 要求方法仅支持 HTTP POST 的表单字段访问。ServletRequest.getParameter*()
该模块提供拦截 HTTP PUT、PATCH 和 DELETE
内容类型为 的请求,从中读取表单数据
请求的正文,并包装 以生成表单数据
可通过 Methods系列获得。spring-web
FormContentFilter
application/x-www-form-urlencoded
ServletRequest
ServletRequest.getParameter*()
1.2.2. 请求头转发
当请求通过代理(例如负载均衡器)时,主机、端口和 方案可能会更改,这使得创建指向正确 Host、Port 和 Scheme。
RFC 7239 定义了 HTTP 标头
代理可用于提供有关原始请求的信息。还有其他
非标准标头,包括 、 和 .Forwarded
X-Forwarded-Host
X-Forwarded-Port
X-Forwarded-Proto
X-Forwarded-Ssl
X-Forwarded-Prefix
ForwardedHeaderFilter
是一个 Servlet 过滤器,它修改请求以便
a) 根据标头更改主机、端口和方案,以及 b) 删除它们
标头以消除进一步的影响。过滤器依赖于包装请求,并且
因此,它必须排在其他过滤器之前,例如 ,
应该使用修改后的请求,而不是原始请求。Forwarded
RequestContextFilter
转发的 Headers 存在安全注意事项,因为应用程序无法知道
标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么
应将信任边界的代理配置为删除来自外部的不受信任的标头。您还可以配置 with ,在这种情况下,它会删除但不使用 Headers。Forwarded
ForwardedHeaderFilter
removeOnly=true
为了支持异步请求和错误调度,此
filter 应该与 和 映射。
如果使用 Spring Framework 的(参见 Servlet Config),则所有过滤器都会自动注册所有 dispatch
类型。但是,如果通过 注册过滤器,或者在 Spring Boot 中通过 注册过滤器,请确保除了 之外还包括 。DispatcherType.ASYNC
DispatcherType.ERROR
AbstractAnnotationConfigDispatcherServletInitializer
web.xml
FilterRegistrationBean
DispatcherType.ASYNC
DispatcherType.ERROR
DispatcherType.REQUEST
1.2.3. 浅层 ETag
过滤器通过缓存内容来创建 “浅层” ETag
写入响应并从中计算 MD5 哈希。下次客户端发送时,
它执行相同的操作,但它还会将计算的值与请求标头进行比较,如果两者相等,则返回 304 (NOT_MODIFIED)。ShallowEtagHeaderFilter
If-None-Match
此策略可节省网络带宽,但不会节省 CPU,因为必须计算完整响应 对于每个请求。前面描述的控制器级别的其他策略可以避免 计算。请参阅 HTTP 缓存。
此过滤器具有一个参数,该参数将过滤器配置为写入弱 ETag
类似于以下内容:(如 RFC 7232 第 2.3 节中所定义)。writeWeakETag
W/"02a2d595e6ed9a0b24f027f2b63b134d6"
为了支持异步请求,必须映射此过滤器
替换为 API 的 API API 的 API 请求,以便筛选器可以延迟并成功生成
ETag 添加到最后一个异步调度的末尾。如果使用 Spring Framework 的(参见 Servlet Config)
所有过滤器都会自动为所有 Dispatch 类型注册。但是,如果注册
过滤器 via 或在 Spring Boot via 中确保包含 .DispatcherType.ASYNC
AbstractAnnotationConfigDispatcherServletInitializer
web.xml
FilterRegistrationBean
DispatcherType.ASYNC
1.3. 带注解的控制器
Spring MVC 提供了一种基于 Comments 的编程模型,其中组件使用 Comments 来表示请求映射、请求输入、
异常处理等。带 Comments 的控制器具有灵活的方法签名和
不必扩展基类,也不必实现特定的接口。
以下示例显示了由 annotations 定义的控制器:@Controller
@RestController
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
import org.springframework.ui.set
@Controller
class HelloController {
@GetMapping("/hello")
fun handle(model: Model): String {
model["message"] = "Hello World!"
return "index"
}
}
在前面的示例中,该方法接受 a 并返回视图名称作为 ,
但还存在许多其他选项,本章稍后将对此进行解释。Model
String
spring.io 上的指南和教程使用基于注释的 编程模型。 |
1.3.1. 声明
您可以通过在
Servlet 的 .构造型允许自动检测,
与 Spring 对检测 Classpath 中的类的一般支持一致
以及为它们自动注册 bean 定义。它还充当
annotated 类,指示其作为 Web 组件的角色。WebApplicationContext
@Controller
@Component
要启用此类 bean 的自动检测,您可以将组件扫描添加到
您的 Java 配置,如下例所示:@Controller
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
@Configuration
@ComponentScan("org.example.web")
class WebConfig {
// ...
}
以下示例显示了与上述示例等效的 XML 配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.web"/>
<!-- ... -->
</beans>
@RestController
是一个组合的注释,它是
本身的 API 中,并使用 和 表示其
每个方法都继承了类型级注释,因此,写入
直接到响应正文与视图解析和使用 HTML 模板进行渲染。@Controller
@ResponseBody
@ResponseBody
AOP 代理
在某些情况下,您可能需要在运行时使用 AOP 代理装饰控制器。
例如,如果您选择直接在
控制器。在这种情况下,特别是对于控制器,我们建议
使用基于类的代理。这通常是控制器的默认选择。
但是,如果控制器必须实现不是 Spring Context 的接口
callback(例如 、 、 等)中,您可能需要显式
配置基于类的代理。例如,使用 you can
更改为 ,并且 可以更改为 。@Transactional
InitializingBean
*Aware
<tx:annotation-driven/>
<tx:annotation-driven proxy-target-class="true"/>
@EnableTransactionManagement
@EnableTransactionManagement(proxyTargetClass = true)
1.3.2. 请求映射
你可以使用 Comments 将请求映射到 controllers 方法。它有
按 URL、HTTP 方法、请求参数、标头和媒体匹配的各种属性
类型。您可以在类级别使用它来表示共享映射,也可以在方法级别使用它
以缩小到特定终端节点映射的范围。@RequestMapping
还有 HTTP 方法特定的快捷方式变体:@RequestMapping
-
@GetMapping
-
@PostMapping
-
@PutMapping
-
@DeleteMapping
-
@PatchMapping
快捷方式是提供的自定义注释,因为:
可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是
using ,默认情况下,它与所有 HTTP 方法匹配。
在类级别仍然需要 A 来表示共享映射。@RequestMapping
@RequestMapping
以下示例具有类型和方法级别映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
fun getPerson(@PathVariable id: Long): Person {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun add(@RequestBody person: Person) {
// ...
}
}
URI 模式
@RequestMapping
方法可以使用 URL 模式进行映射。有两种选择:
-
PathPattern
— 与 URL 路径匹配的预解析模式,也预先解析为 .该解决方案专为 Web 使用而设计,可有效地处理编码和 path 参数进行匹配,并有效地进行匹配。PathContainer
-
AntPathMatcher
— 将 String 模式与 String 路径匹配。这是原始的 解决方案也用于选择 Classpath 上的资源,在 filesystem 和其他位置。它的效率较低,并且 String path input 是一个 有效处理 URL 的编码和其他问题的挑战。
PathPattern
是 Web 应用程序的推荐解决方案,也是
Spring WebFlux 的 Web Flux 中。在 5.3 版本之前,是 Spring MVC 中的唯一选择
并继续为默认值。但是,可以在 MVC 配置中启用。AntPathMatcher
PathPattern
PathPattern
支持与 .此外,它还
支持捕获模式,例如 ,用于匹配 0 个或多个路径段
在路径的末尾。 还限制了 for matching multiple 的使用
path segments 的 URL 中,则只允许在 pattern 的末尾使用。这消除了许多
为给定请求选择最佳匹配模式时出现歧义的情况。
有关完整的模式语法,请参阅 PathPattern 和 AntPathMatcher。AntPathMatcher
{*spring}
PathPattern
**
一些示例模式:
-
"/resources/ima?e.png"
- 匹配路径段中的一个字符 -
"/resources/*.png"
- 匹配路径段中的零个或多个字符 -
"/resources/**"
- 匹配多个路径段 -
"/projects/{project}/versions"
- 匹配路径段并将其捕获为变量 -
"/projects/{project:[a-z]+}/versions"
- 使用正则表达式匹配和捕获变量
捕获的 URI 变量可以使用 进行访问。例如:@PathVariable
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
您可以在类和方法级别声明 URI 变量,如下例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
@Controller
@RequestMapping("/owners/{ownerId}")
class OwnerController {
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
// ...
}
}
URI 变量会自动转换为适当的类型,或者被引发。默认情况下支持简单类型(、、 、 等),您可以
注册对任何其他数据类型的支持。
请参见类型转换和 DataBinder
。TypeMismatchException
int
long
Date
您可以显式命名 URI 变量(例如 ),但您可以
如果名称相同并且您的代码是使用调试编译的,则省略该详细信息
信息或 Java 8 上的 compiler 标志。@PathVariable("customId")
-parameters
该语法使用正则表达式声明一个 URI 变量,该正则表达式具有
的语法。例如,给定 URL ,以下方法
提取名称、版本和文件扩展名:{varName:regex}
{varName:regex}
"/spring-web-3.0.5.jar"
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式还可以具有在启动时解析的嵌入占位符
通过对 local、system、environment 和
其他属性来源。例如,您可以使用它来根据
一些外部配置。${…}
PropertySourcesPlaceholderConfigurer
形态比较
当多个模式与一个 URL 匹配时,必须选择最佳匹配项。这是通过
以下选项之一,具体取决于是否启用了 Use of Parsed:PathPattern
两者都有助于对模式进行排序,其中更具体的模式位于顶部。如果 它具有较少的 URI 变量(计为 1)、单个通配符(计为 1)、 和双通配符(计为 2)。如果得分相等,则选择较长的模式。 在给定相同的分数和长度的情况下,URI 变量多于通配符的模式为 选择。
默认映射模式 () 从评分中排除,并且始终
最后排序。此外,前缀模式(例如 )被视为较少
specific than 其他没有双通配符的模式。/**
/public/**
有关完整详细信息,请点击上述链接进入 Comparators 模式。
后缀匹配
从 5.3 开始,默认情况下 Spring MVC 不再执行后缀模式
控制器映射到的匹配也会隐式映射到 。因此,不再使用路径扩展来解释
响应请求的内容类型 — 例如, , ,
等等。.*
/person
/person.*
/person.pdf
/person.xml
当浏览器用于发送标头时,以这种方式使用文件扩展名是必要的
这很难始终如一地解释。目前,这不再是必需的,并且
使用 header 应该是首选。Accept
Accept
随着时间的推移,文件扩展名的使用已被证明以各种方式存在问题。 当与 URI 变量、路径参数和 URI 编码。基于 URL 的授权的推理 安全性(有关更多详细信息,请参阅下一节)也变得更加困难。
要在 5.3 之前的版本中完全禁用路径扩展名,请设置以下内容:
-
useSuffixPatternMatching(false)
,请参阅 PathMatchConfigurer -
favorPathExtension(false)
,请参阅 ContentNegotiationConfigurer
如果可以通过标头以外的方式请求内容类型
很有用,例如,在浏览器中键入 URL 时。路径扩展的安全替代方案是
以使用 Query Parameter 策略。如果必须使用文件扩展名,请考虑限制
它们通过 ContentNegotiationConfigurer 的属性添加到显式注册的扩展列表中。"Accept"
mediaTypes
后缀匹配和 RFD
反射文件下载 (RFD) 攻击与 XSS 类似,因为它依赖于请求输入 (例如,查询参数和 URI 变量)反映在响应中。但是,而不是 将 JavaScript 插入 HTML 中,RFD 攻击依赖于浏览器切换来执行 download 并在稍后双击时将响应视为可执行脚本。
在 Spring MVC 中,方法处于风险中,因为
它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展请求这些内容类型。
禁用后缀模式匹配并使用路径扩展进行内容协商
降低风险,但不足以防止 RFD 攻击。@ResponseBody
ResponseEntity
为了防止 RFD 攻击,在渲染响应正文之前, Spring MVC 会添加一个标头来建议固定且安全的下载
文件。仅当 URL 路径包含的文件扩展名既不是
允许作为 SAFE 或明确注册内容协商。但是,它可以
当 URL 直接输入到浏览器中时,可能会产生副作用。Content-Disposition:inline;filename=f.txt
默认情况下,许多常见的路径扩展名都是安全的。具有自定义实现的应用程序可以显式注册内容的文件扩展名
negotiation 以避免为这些扩展添加标头。
请参阅内容类型。HttpMessageConverter
Content-Disposition
有关其他信息,请参阅 CVE-2015-5211 与 RFD 相关的建议。
易耗品介质类型
您可以根据请求的 缩小请求映射的范围,
如下例所示:Content-Type
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
// ...
}
1 | 使用属性按内容类型缩小映射范围。consumes |
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
// ...
}
1 | 使用属性按内容类型缩小映射范围。consumes |
该属性还支持否定表达式 — 例如,表示任何
内容类型不是 .consumes
!text/plain
text/plain
您可以在类级别声明共享属性。与大多数其他
request-mapping 属性,但是,当在类级别使用时,方法级别属性
覆盖而不是扩展类级声明。consumes
consumes
MediaType 为常用媒体类型提供常量,例如 和 。APPLICATION_JSON_VALUE APPLICATION_XML_VALUE |
可生产的培养基类型
您可以根据请求标头和
控制器方法生成的内容类型,如下例所示:Accept
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
1 | 使用属性按内容类型缩小映射范围。produces |
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
// ...
}
1 | 使用属性按内容类型缩小映射范围。produces |
媒体类型可以指定字符集。支持否定表达式 — 例如,表示除 “text/plain” 之外的任何内容类型。!text/plain
您可以在类级别声明共享属性。与大多数其他
request-mapping 属性,但是,当在类级别使用时,方法级别属性
覆盖而不是扩展类级声明。produces
produces
MediaType 为常用媒体类型提供常量,例如 和 。APPLICATION_JSON_VALUE APPLICATION_XML_VALUE |
参数、标头
您可以根据请求参数条件缩小请求映射的范围。您可以测试
存在请求参数 ()、缺少 () 或
特定值 ()。以下示例显示如何测试特定值:myParam
!myParam
myParam=myValue
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 测试 是否等于 .myParam myValue |
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
1 | 测试 是否等于 .myParam myValue |
您还可以将 SAME 与请求标头条件一起使用,如下例所示:
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 测试 是否等于 .myHeader myValue |
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
HTTP 头, 选项
@GetMapping
(和 ) 支持 HTTP HEAD
transparently 进行请求映射。控制器方法不需要更改。
中应用的响应包装器可确保将标头设置为写入的字节数(而不实际写入响应)。@RequestMapping(method=HttpMethod.GET)
javax.servlet.http.HttpServlet
Content-Length
@GetMapping
(和 ) 被隐式映射到
并支持 HTTP HEAD。HTTP HEAD 请求的处理方式与它是 HTTP GET 一样,但
这样,不是写入正文,而是计算字节数并设置 Headers。@RequestMapping(method=HttpMethod.GET)
Content-Length
默认情况下,HTTP OPTIONS 通过将响应头设置为 HTTP 列表来处理
方法。Allow
@RequestMapping
对于没有 HTTP 方法声明的 Headers,标头设置为 。控制器方法应始终声明
支持的 HTTP 方法(例如,通过使用特定于 HTTP 方法的变体:、 和 others)。@RequestMapping
Allow
GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
@GetMapping
@PostMapping
您可以将该方法显式映射到 HTTP HEAD 和 HTTP OPTIONS,但是
在常见情况下不是必需的。@RequestMapping
自定义注释
Spring MVC 支持使用组合注释进行请求映射。这些注释本身是元注释和组合的,以重新声明具有更窄、更具体目的的属性的子集(或全部)。@RequestMapping
@RequestMapping
@GetMapping
、 、 、 和 是
组合注释的示例。提供这些 API 是因为可以说,大多数
控制器方法应映射到特定的 HTTP 方法,而不是使用 ,
默认情况下,它与所有 HTTP 方法匹配。如果你需要一个组合的示例
annotations 中,看看这些是怎么声明的。@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
@RequestMapping
Spring MVC 还支持具有自定义请求匹配的自定义请求映射属性
逻辑。这是一个更高级的选项,需要子类化和覆盖方法,其中
您可以检查 custom 属性并返回您自己的 .RequestMappingHandlerMapping
getCustomMethodCondition
RequestCondition
显式注册
您可以通过编程方式注册处理程序方法,这些方法可用于动态 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 | 注入 target handler 和 controller 的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 注入 target handler 和 controller 的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
1.3.3. 处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列
支持的控制器方法参数和返回值。
方法参数
下表描述了支持的控制器方法参数。不支持反应类型 对于任何参数。
JDK 8 支持作为方法参数与
具有属性(例如、、 、
和其他)并且等效于 .java.util.Optional
required
@RequestParam
@RequestHeader
required=false
控制器方法参数 | 描述 |
---|---|
|
对请求参数以及请求和会话属性的通用访问,无需直接 使用 Servlet API。 |
|
选择任何特定的请求或响应类型 — 例如、、 、
或 Spring 的 , . |
|
强制会话的存在。因此,这样的参数永远不会 。
请注意,会话访问不是线程安全的。考虑将实例的标志设置为 if multiple
允许请求并发访问会话。 |
|
用于编程 HTTP/2 资源推送的 Servlet 4.0 推送生成器 API。
请注意,根据 Servlet 规范,如果客户端
不支持该 HTTP/2 功能。 |
|
Currently authenticated user — 如果已知,则可能是特定的 implementation class。 请注意,如果为了允许自定义解析程序解析此参数而对其进行批注,则不会预先解析此参数
在通过 回退到默认分辨率之前。
例如,Spring Security 实现并将通过以下方式注入,除非它也被注释为,在这种情况下,它
由自定义 Spring Security 解析程序通过 . |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的可用区域设置(在
效果、配置的 或 )。 |
|
与当前请求关联的时区,由 确定。 |
|
用于访问 Servlet API 公开的原始请求正文。 |
|
用于访问 Servlet API 公开的原始响应正文。 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的名称-值对。请参见矩阵变量。 |
|
用于访问 Servlet 请求参数,包括 multipart 文件。参数值
转换为声明的方法参数类型。另请参阅 请注意,对于简单参数值,使用 of 是可选的。
请参阅本表末尾的“任何其他参数”。 |
|
用于访问请求标头。标头值将转换为声明的方法参数
类型。请参阅 |
|
用于访问 Cookie。Cookie 值将转换为声明的方法参数
类型。请参阅 |
|
用于访问 HTTP 请求正文。正文内容被转换为声明的方法
参数类型。请参阅 |
|
用于访问请求标头和正文。正文使用 .
请参阅 HttpEntity。 |
|
要访问请求中的某个部分,请将该部分的主体
替换为 .请参见 Multipart。 |
|
用于访问在 HTML 控制器中使用并作为模板公开的模型 视图渲染的一部分。 |
|
指定要在重定向时使用的属性(即,要附加到查询中) string) 和 flash 属性临时存储,直到重定向后的请求。 请参阅重定向属性和 Flash 属性。 |
|
要访问模型中的现有属性(如果不存在,则实例化),请使用
应用了数据绑定和验证。请参见 请注意,使用 of 是可选的(例如,设置其属性)。
请参阅此表末尾的“任何其他参数”。 |
|
用于访问命令对象的验证和数据绑定中的错误
(即参数)或验证 a 或 参数时出错。您必须声明一个 , 或 参数
紧跟在 validated method 参数之后。 |
|
用于将表单处理标记为完成,从而触发会话属性的清理
通过类级注解声明。有关详细信息 |
|
用于准备相对于当前请求的主机、端口、方案、上下文路径和 Servlet 映射的 Literal 部分。请参阅 URI 链接。 |
|
用于访问任何 session 属性,与存储在 session 中的 model 属性相反
作为类级声明的结果。有关详细信息 |
|
用于访问请求属性。有关详细信息 |
任何其他参数 |
如果方法参数与此表中的任何先前值不匹配,并且
简单类型(由 BeanUtils#isSimpleProperty 确定),
它解析为 .否则,它将解析为 . |
返回值
下表描述了支持的控制器方法返回值。响应式类型是 支持所有返回值。
控制器方法返回值 | 描述 |
---|---|
|
返回值通过 implementations 进行转换并写入
响应。请参阅 |
|
需要转换指定完整响应(包括 HTTP 头和正文)的返回值
通过 implementations 并写入响应。
请参阅 ResponseEntity。 |
|
用于返回带有标头且没有正文的响应。 |
|
一个视图名称,可以通过 implementations 解析并与 implicit 一起使用
model — 通过命令对象和方法确定。处理程序
方法还可以通过声明参数以编程方式丰富模型
(请参阅 显式注册)。 |
|
用于与隐式模型一起呈现的实例 — determined
通过命令对象和方法。handler 方法还可以
通过声明参数以编程方式丰富模型
(请参阅 显式注册)。 |
|
要添加到隐式模型的属性,其中视图名称是隐式确定的
通过 . |
|
要添加到模型中的属性,视图名称通过
一个。 请注意,这是可选的。请参阅 末尾的 “任何其他返回值”
这个表。 |
|
要使用的 view 和 model 属性,以及响应状态(可选)。 |
|
具有返回类型(或返回值)的方法被视为完全具有
如果响应还具有 、 参数或
注释。如果控制器进行了肯定或时间戳检查,情况也是如此(有关详细信息,请参阅 控制器 )。 如果以上都不是 true,则返回类型也可以指示 “no response body”
REST 控制器或 HTML 控制器的默认视图名称选择。 |
|
从任何线程异步生成上述任何返回值 — 例如,作为
某些事件或回调的结果。请参阅 异步请求 和 |
|
|
|
Alternative to ,为了方便起见(例如,当底层服务
返回其中一个)。 |
|
异步发出对象流,以使用实现写入响应。还支持作为 .
请参阅 异步请求和 HTTP 流式处理。 |
|
异步写入响应。还支持作为 .请参阅 异步请求和 HTTP 流式处理。 |
通过 |
单个值类型(例如 )相当于返回 。
多值类型(例如 )可能被视为流,具体取决于请求
media 类型,例如 “text/event-stream”、“application/json+stream” 或其他
收集到 List 并呈现为单个值。请参阅 异步请求和反应类型。 |
其他返回值 |
如果返回值以任何其他方式保持未解析状态,则将其视为模型 属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型, 在这种情况下,它仍然未解决。 |
类型转换
一些带注释的控制器方法参数,表示基于 的请求输入(例如 、 、 、 和 )
如果参数声明为 .String
@RequestParam
@RequestHeader
@PathVariable
@MatrixVariable
@CookieValue
String
对于此类情况,将根据配置的转换器自动应用类型转换。
默认情况下,支持简单类型(、、 、 和其他)。您可以自定义
类型转换(请参阅 DataBinder
)或通过注册 .
参见 Spring Field Formatting。int
long
Date
WebDataBinder
Formatters
FormattingConversionService
类型转换中的一个实际问题是空 String 源值的处理。
如果此类值因类型转换而变为缺失值,则将其视为缺失。
、 和其他目标类型可能就是这种情况。如果要允许注入,请在参数注释上使用标志,或者声明
参数设置为 .null
Long
UUID
null
required
@Nullable
从 5.3 开始,即使在类型转换之后,也会强制使用非 null 参数。如果您的处理程序
method 也打算接受 null 值,要么将你的参数声明为 ,要么将其标记为在相应的 , etc. 注释中。这是
针对 5.3 升级中遇到的回归的最佳实践和推荐解决方案。 或者,您可以专门处理例如在必需的情况下的结果。转换后的 null 值将被视为
一个空的原始值,因此将引发相应的变体。 |
矩阵变量
矩阵变量可以出现在任何路径段中,每个变量用分号和
用逗号分隔的多个值(例如 )。倍数
还可以通过重复的变量名称(例如 )来指定值。/cars;color=red,green;year=2012
color=red;color=green;color=blue
如果 URL 应包含矩阵变量,则控制器的请求映射 方法必须使用 URI 变量来掩盖该变量内容,并确保请求可以 成功匹配,与矩阵变量顺序和存在无关。 以下示例使用 matrix 变量:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有路径段都可能包含矩阵变量,您有时可能需要 消除 matrix 变量预期位于哪个 path 变量的歧义。 以下示例显示了如何执行此操作:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
矩阵变量可以定义为可选变量,并指定默认值,如 以下示例显示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,您可以使用 ,如下例所示:MultiValueMap
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
请注意,您需要启用矩阵变量的使用。在 MVC Java 配置中,
您需要通过 Path Matching 设置 一个。在 MVC XML 命名空间中,可以将 .UrlPathHelper
removeSemicolonContent=false
<mvc:annotation-driven enable-matrix-variables="true"/>
@RequestParam
您可以使用注释来绑定 Servlet 请求参数(即
查询参数或表单数据)添加到控制器中的方法参数。@RequestParam
以下示例显示了如何执行此操作:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
1 | 用于绑定 .@RequestParam petId |
import org.springframework.ui.set
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@GetMapping
fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
val pet = this.clinic.loadPet(petId);
model["pet"] = pet
return "petForm"
}
// ...
}
1 | 用于绑定 .@RequestParam petId |
默认情况下,使用此 Comments 的方法参数是必需的,但您可以指定
方法参数是可选的,只需将 Annotation 的标志设置为 Feature,或者通过使用包装器声明参数即可。@RequestParam
required
false
java.util.Optional
如果目标方法参数 type 不是 ,则会自动应用类型转换。请参阅类型转换。String
将参数类型声明为数组或列表允许解析多个参数 值。
当注释声明为 a 或 ,而注释中没有指定参数名称时,
然后,映射中填充每个给定参数名称的请求参数值。@RequestParam
Map<String, String>
MultiValueMap<String, String>
请注意,使用 of 是可选的(例如,设置其属性)。
默认情况下,任何作为简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且不由任何其他参数解析程序解析,则被视为已批注
跟。@RequestParam
@RequestParam
@RequestHeader
您可以使用注释将请求标头绑定到
控制器。@RequestHeader
请考虑以下带有 headers 的请求:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
以下示例获取 and 标头的值:Accept-Encoding
Keep-Alive
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, (1)
@RequestHeader("Keep-Alive") long keepAlive) { (2)
//...
}
1 | 获取标头的值。Accept-Encoding |
2 | 获取标头的值。Keep-Alive |
@GetMapping("/demo")
fun handle(
@RequestHeader("Accept-Encoding") encoding: String, (1)
@RequestHeader("Keep-Alive") keepAlive: Long) { (2)
//...
}
1 | 获取标头的值。Accept-Encoding |
2 | 获取标头的值。Keep-Alive |
如果目标方法参数 type 不是 ,则会自动应用类型转换。请参阅类型转换。String
当对 、 或 参数使用注释时,将填充映射
替换为所有标头值。@RequestHeader
Map<String, String>
MultiValueMap<String, String>
HttpHeaders
内置支持可用于将逗号分隔的字符串转换为
字符串的数组或集合,或者类型转换系统已知的其他类型的集合。为
例如,带注释的方法参数可以是 type (也可以是 or)。@RequestHeader("Accept") String String[] List<String> |
@CookieValue
您可以使用注释将 HTTP Cookie 的值绑定到方法参数
在控制器中。@CookieValue
考虑具有以下 Cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
以下示例演示如何获取 Cookie 值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
//...
}
1 | 获取 Cookie 的值。JSESSIONID |
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
//...
}
1 | 获取 Cookie 的值。JSESSIONID |
如果目标方法参数 type 不是 ,则会自动应用类型转换。
请参阅类型转换。String
@ModelAttribute
您可以在 method 参数上使用注解来访问属性
模型,如果不存在,则对其进行实例化。model 属性还覆盖了
其名称与字段名称匹配的 HTTP Servlet 请求参数的值。这是指
to 作为数据绑定,并且它使您不必处理解析和转换单个
查询参数和表单字段。以下示例显示了如何执行此操作:@ModelAttribute
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) {
// method logic...
}
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String {
// method logic...
}
上述实例通过以下方式之一获取:Pet
-
从可能已通过 @ModelAttribute 方法添加的模型中检索。
-
如果 model 属性列在 类级别
@SessionAttributes
注释。 -
通过 where model attribute name 与 请求值,例如 path 变量或 request 参数(请参阅下一个示例)。
Converter
-
使用其默认构造函数实例化。
-
通过具有与 Servlet 匹配的参数的 “primary constructor” 实例化 request 参数。参数名称是通过 JavaBeans 或字节码中运行时保留的参数名称确定的。
@ConstructorProperties
使用 @ModelAttribute 方法的一种替代方法
提供它或依赖框架来创建 model 属性,就是要有一个来提供实例。这在 model 属性
name 与请求值的名称匹配,例如 path 变量或 request
参数,并且有一个 from 到 model 属性类型。
在以下示例中,模型属性名称与 URI 匹配
path 变量 ,并且有一个已注册的
可以从数据存储中加载Converter<String, T>
Converter
String
account
account
Converter<String, Account>
Account
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
// ...
}
获取 model 属性实例后,将应用数据绑定。该类与 Servlet 请求参数名称(查询参数和表单
fields) 添加到目标 上的字段名称 。匹配字段在 type 之后填充
必要时应用 conversion。有关数据绑定(和验证)的更多信息,请参阅验证。有关自定义数据绑定的更多信息,请参阅 DataBinder
。WebDataBinder
Object
数据绑定可能会导致错误。默认情况下,会引发 a。但是,要检查
对于 controller 方法中的此类错误,您可以立即添加一个参数
更改为 ,如下例所示:BindException
BindingResult
@ModelAttribute
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 在 .BindingResult @ModelAttribute |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
1 | 在 .BindingResult @ModelAttribute |
在某些情况下,您可能希望访问没有数据绑定的 model 属性。对于这样的
情况下,你可以将 注入到控制器中并直接访问它,或者,
或者,设置 ,如下例所示:Model
@ModelAttribute(binding=false)
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) { (1)
// ...
}
1 | 设置。@ModelAttribute(binding=false) |
@ModelAttribute
fun setUpForm(): AccountForm {
return AccountForm()
}
@ModelAttribute
fun findAccount(@PathVariable accountId: String): Account {
return accountRepository.findOne(accountId)
}
@PostMapping("update")
fun update(@Valid form: AccountForm, result: BindingResult,
@ModelAttribute(binding = false) account: Account): String { (1)
// ...
}
1 | 设置。@ModelAttribute(binding=false) |
您可以通过添加 annotation 或 Spring 的 annotation 在数据绑定后自动应用验证
(Bean 验证和 Spring 验证)。以下示例显示了如何执行此操作:javax.validation.Valid
@Validated
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 验证实例。Pet |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
请注意,using 是可选的(例如,设置其属性)。
默认情况下,任何不是简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟。@ModelAttribute
@ModelAttribute
@SessionAttributes
@SessionAttributes
用于将模型属性存储在 HTTP Servlet 会话中
请求。它是一个类型级注释,声明
特定控制器。这通常列出模型属性的名称或
model 属性,这些属性应该透明地存储在会话中以供后续使用
请求访问。
以下示例使用注释:@SessionAttributes
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用注释。@SessionAttributes |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
}
1 | 使用注释。@SessionAttributes |
在第一个请求中,当将名称为 的 model 属性添加到模型时,
它会自动提升并保存在 HTTP Servlet 会话中。它仍然在那里
直到另一个控制器方法使用 method 参数清除
storage,如下例所示:pet
SessionStatus
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); (2)
// ...
}
}
1 | 将值存储在 Servlet 会话中。Pet |
2 | 从 Servlet 会话中清除该值。Pet |
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
// ...
@PostMapping("/pets/{id}")
fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String {
if (errors.hasErrors()) {
// ...
}
status.setComplete() (2)
// ...
}
}
1 | 将值存储在 Servlet 会话中。Pet |
2 | 从 Servlet 会话中清除该值。Pet |
@SessionAttribute
如果您需要访问全局管理的预先存在的会话属性
(即,在控制器外部 — 例如,通过过滤器),并且可能存在也可能不存在,
您可以在 method 参数上使用 Comments ,
如下例所示:@SessionAttribute
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
// ...
}
1 | 使用批注。@SessionAttribute |
@RequestMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
// ...
}
对于需要添加或删除会话属性的使用案例,请考虑将 or 注入控制器方法。org.springframework.web.context.request.WebRequest
javax.servlet.http.HttpSession
作为控制器的一部分,在会话中临时存储模型属性
工作流程,请考虑按照 @SessionAttributes
中所述使用。@SessionAttributes
@RequestAttribute
与 类似,您可以使用注释来
访问之前创建的预先存在的请求属性(例如,由 Servlet 或 ):@SessionAttribute
@RequestAttribute
Filter
HandlerInterceptor
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 使用注释。@RequestAttribute |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 使用注释。@RequestAttribute |
重定向属性
默认情况下,所有模型属性都被视为在 重定向 URL。在其余属性中,那些是原始类型或 基元类型的集合或数组会自动附加为查询参数。
如果
model 实例是专门为重定向准备的。但是,在 annotated
控制器,则模型可以包含为渲染目的而添加的其他属性(例如
下拉字段值)。为避免此类属性出现在
URL 中,方法可以声明 type 为 和
使用它来指定要提供给 的确切属性。如果方法
重定向,则使用 的内容。否则,将
model 的@RequestMapping
RedirectAttributes
RedirectView
RedirectAttributes
它提供了一个名为 的标志,您可以使用它来指示如果控制器方法重定向,则永远不应使用 default 的内容。相反,控制器
method 应该声明一个 type 为 attribute 的属性,或者,如果它不这样做,则声明
不应将任何属性传递给 。MVC 命名空间和 MVC
Java 配置将此标志设置为 ,以保持向后兼容性。
但是,对于新应用程序,我们建议将其设置为 。RequestMappingHandlerAdapter
ignoreDefaultModelOnRedirect
Model
RedirectAttributes
RedirectView
false
true
请注意,当前请求中的 URI 模板变量是自动创建的
在展开重定向 URL 时可用,并且您不需要显式添加它们
通过 或 .以下示例显示如何定义重定向:Model
RedirectAttributes
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
@PostMapping("/files/{path}")
fun upload(...): String {
// ...
return "redirect:files/{path}"
}
将数据传递到重定向目标的另一种方法是使用 flash 属性。与 其他重定向属性,则 Flash 属性将保存在 HTTP 会话中(因此,确实如此 未显示在 URL 中)。有关详细信息,请参阅 Flash 属性。
Flash 属性
Flash 属性为一个请求提供了一种方法来存储旨在在 另一个。这在重定向时最常用 — 例如, Post-Redirect-Get 模式。Flash 属性在 redirect(通常在会话中)提供给请求,以便在 redirect 并立即删除。
Spring MVC 有两个主要的抽象来支持 flash 属性。 已使用
保存 Flash 属性,while 用于存储、检索和管理实例。FlashMap
FlashMapManager
FlashMap
Flash 属性支持始终处于“打开”状态,不需要显式启用。
但是,如果不使用,则永远不会导致 HTTP 会话创建。在每个请求中,都有一个
“input”,其中包含从上一个请求传递的属性(如果有)和一个
“output” 替换为要保存以供后续请求使用的属性。这两个实例都可以通过 中的静态方法从 Spring MVC 中的任何位置访问。FlashMap
FlashMap
FlashMap
RequestContextUtils
带 Comments 的 controller 通常不需要直接使用。相反,方法可以接受 type 的参数并使用它
为重定向方案添加 Flash 属性。通过添加的 Flash 属性会自动传播到“输出”FlashMap。同样地
重定向后,来自“input”的属性会自动添加到提供目标 URL 的控制器中。FlashMap
@RequestMapping
RedirectAttributes
RedirectAttributes
FlashMap
Model
多部分
启用 后,POST 的内容
请求 被解析并作为常规请求进行访问
参数。以下示例访问一个常规表单字段和一个已上传的表单字段
文件:MultipartResolver
multipart/form-data
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(@RequestParam("name") String name,
@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(@RequestParam("name") name: String,
@RequestParam("file") file: MultipartFile): String {
if (!file.isEmpty) {
val bytes = file.bytes
// store the bytes somewhere
return "redirect:uploadSuccess"
}
return "redirect:uploadFailure"
}
}
将参数类型声明为 a 允许解析多个
文件。List<MultipartFile>
当注释声明为 a 或 时,如果注释中没有指定参数名称,
然后,地图中将填充每个给定参数名称的 Multipart 文件。@RequestParam
Map<String, MultipartFile>
MultiValueMap<String, MultipartFile>
使用 Servlet 3.0 多部分解析,您还可以将 Spring 的 声明为方法参数或集合值类型。javax.servlet.http.Part MultipartFile |
您还可以将多部分内容用作数据绑定到命令对象的一部分。例如,表单域 和前面示例中的 file 可以是表单对象上的字段, 如下例所示:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
if (!form.getFile().isEmpty()) {
byte[] bytes = form.getFile().getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
class MyForm(val name: String, val file: MultipartFile, ...)
@Controller
class FileUploadController {
@PostMapping("/form")
fun handleFormUpload(form: MyForm, errors: BindingResult): String {
if (!form.file.isEmpty) {
val bytes = form.file.bytes
// store the bytes somewhere
return "redirect:uploadSuccess"
}
return "redirect:uploadFailure"
}
}
也可以从 RESTful 服务中的非浏览器客户端提交分段请求 场景。以下示例显示了一个包含 JSON 的文件:
POST /someUrl Content-Type: multipart/mixed --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="meta-data" Content-Type: application/json; charset=UTF-8 Content-Transfer-Encoding: 8bit { "name": "value" } --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="file-data"; filename="file.properties" Content-Type: text/xml Content-Transfer-Encoding: 8bit ... File Data ...
你可以用 “meta-data” 部分来访问 ,但你会
可能希望从 JSON 反序列化它(类似于 )。使用 HttpMessageConverter 转换后,使用 Comments 访问 multipart:@RequestParam
String
@RequestBody
@RequestPart
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData,
@RequestPart("file-data") file: MultipartFile): String {
// ...
}
您可以结合使用 Spring 的 Comments ,这两者都会导致应用 Standard Bean Validation。
默认情况下,验证错误会导致 ,该 被
转换为 400 (BAD_REQUEST) 响应。或者,您可以在本地处理验证错误
在控制器中通过 OR 参数,
如下例所示:@RequestPart
javax.validation.Valid
@Validated
MethodArgumentNotValidException
Errors
BindingResult
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
result: BindingResult): String {
// ...
}
@RequestBody
你可以使用 Comments 让请求正文通过 HttpMessageConverter
读取并反序列化为 an。
以下示例使用参数:@RequestBody
Object
@RequestBody
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
您可以使用 MVC Config 的 Message Converters 选项来 配置或自定义消息转换。
您可以与 Spring 的 Comments 结合使用,这两者都会导致应用 Standard Bean Validation。
默认情况下,验证错误会导致 ,该 被
转换为 400 (BAD_REQUEST) 响应。或者,您可以在本地处理验证错误
在控制器中通过 OR 参数,
如下例所示:@RequestBody
javax.validation.Valid
@Validated
MethodArgumentNotValidException
Errors
BindingResult
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
// ...
}
HttpEntity
HttpEntity
与使用 @RequestBody
大致相同,但基于
Container 对象。下面的清单显示了一个示例:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
您可以在方法上使用 Comments 来序列化返回值
传递给响应正文。
下面的清单显示了一个示例:@ResponseBody
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别也受支持,在这种情况下,它由
所有控制器方法。这就是 的效果,仅此而已
比标有 和 的元注释 .@RestController
@Controller
@ResponseBody
您可以使用 MVC Config 的 Message Converters 选项来 配置或自定义消息转换。
您可以将方法与 JSON 序列化视图结合使用。
有关详细信息,请参阅 Jackson JSON。@ResponseBody
ResponseEntity
ResponseEntity
与 @ResponseBody 类似,
但具有 status 和 headers。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body = ...
val etag = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
Spring MVC 支持使用单值反应类型来生成异步和/或单值和多值反应式
body 的类型。这允许以下类型的异步响应:ResponseEntity
-
ResponseEntity<Mono<T>>
或将响应状态设为 标头,而正文将在稍后异步提供。 如果主体由 0..1 个值组成,或者它可以生成多个值,则使用。ResponseEntity<Flux<T>>
Mono
Flux
-
Mono<ResponseEntity<T>>
提供所有三个 — 响应状态、标头和正文, 异步的。这允许响应状态和标头发生变化 取决于异步请求处理的结果。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring MVC 为 Jackson 的序列化视图提供了内置支持,
,它们只允许渲染 .要将其与 or 控制器方法一起使用,您可以使用 Jackson 的 annotation 来激活序列化视图类,如下例所示:Object
@ResponseBody
ResponseEntity
@JsonView
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser() = User("eric", "7!jd#h23")
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许视图类数组,但您只能为每个
controller 方法。如果需要激活多个视图,可以使用复合界面。 |
如果您想以编程方式执行上述操作,而不是声明注解,
将返回值包装起来,并使用它来提供序列化视图:@JsonView
MappingJacksonValue
@RestController
public class UserController {
@GetMapping("/user")
public MappingJacksonValue getUser() {
User user = new User("eric", "7!jd#h23");
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(User.WithoutPasswordView.class);
return value;
}
}
@RestController
class UserController {
@GetMapping("/user")
fun getUser(): MappingJacksonValue {
val value = MappingJacksonValue(User("eric", "7!jd#h23"))
value.serializationView = User.WithoutPasswordView::class.java
return value
}
}
对于依赖视图分辨率的控制器,您可以添加序列化视图类 添加到模型中,如下例所示:
@Controller
public class UserController extends AbstractController {
@GetMapping("/user")
public String getUser(Model model) {
model.addAttribute("user", new User("eric", "7!jd#h23"));
model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
return "userView";
}
}
import org.springframework.ui.set
@Controller
class UserController : AbstractController() {
@GetMapping("/user")
fun getUser(model: Model): String {
model["user"] = User("eric", "7!jd#h23")
model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java
return "userView"
}
}
1.3.4. 模型
您可以使用注释:@ModelAttribute
-
在方法中的方法参数上 从模型创建或访问 an,并通过 .
@RequestMapping
Object
WebDataBinder
-
作为 或 类中的方法级注释,以帮助 在调用任何方法之前初始化模型。
@Controller
@ControllerAdvice
@RequestMapping
-
在方法上标记其返回值是 model 属性。
@RequestMapping
本节讨论方法 — 前面列表中的第二项。
控制器可以有任意数量的方法。所有这些方法都是
在同一个控制器中的方法之前调用。还可以通过 在控制器之间共享方法。有关更多详细信息,请参阅 Controller Advice 部分。@ModelAttribute
@ModelAttribute
@RequestMapping
@ModelAttribute
@ControllerAdvice
@ModelAttribute
方法具有灵活的方法签名。它们支持许多相同的
参数作为方法,除了它自己或任何
与请求正文相关。@RequestMapping
@ModelAttribute
以下示例显示了一种方法:@ModelAttribute
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
return accountRepository.findAccount(number)
}
如果未显式指定名称,则根据类型选择默认名称,如约定 的 javadoc 中所述。
您始终可以使用重载的方法或
通过属性 on(对于返回值)。Object addAttribute name @ModelAttribute |
您还可以用作方法的方法级注释,
在这种情况下,该方法的返回值将解释为模型
属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为。
除非返回值为 A,否则将解释为视图名称。 还可以自定义 Model 属性名称,如下例所示:@ModelAttribute
@RequestMapping
@RequestMapping
String
@ModelAttribute
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.3.5.DataBinder
@Controller
或类可以具有
初始化 的实例,而这些实例反过来又可以:@ControllerAdvice
@InitBinder
WebDataBinder
-
将请求参数(即表单或查询数据)绑定到模型对象。
-
转换基于 String 的请求值(例如请求参数、路径变量、 headers、cookie 等)传递给 controller 方法参数的目标类型。
-
在呈现 HTML 表单时将模型对象值格式化为值。
String
@InitBinder
方法可以注册特定于控制器的 或
弹簧和组件。此外,您还可以使用 MVC 配置在全局共享的 .java.beans.PropertyEditor
Converter
Formatter
Converter
Formatter
FormattingConversionService
@InitBinder
方法支持许多与方法
do,但 (Command Object) 参数除外。通常,它们被声明
带有参数 (用于注册) 和返回值。
下面的清单显示了一个示例:@RequestMapping
@ModelAttribute
WebDataBinder
void
@Controller
public class FormController {
@InitBinder (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
1 | 定义方法。@InitBinder |
@Controller
class FormController {
@InitBinder (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
1 | 定义方法。@InitBinder |
或者,当您通过共享 使用基于 的设置时,可以重复使用相同的方法并注册
特定于 Controller 的实现,如下例所示:Formatter
FormattingConversionService
Formatter
@Controller
public class FormController {
@InitBinder (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
1 | 在自定义格式化程序上定义方法。@InitBinder |
@Controller
class FormController {
@InitBinder (1)
protected fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
}
// ...
}
1 | 在自定义格式化程序上定义方法。@InitBinder |
模型设计
在 Web 应用程序的上下文中,数据绑定涉及 HTTP 请求的绑定 parameters ((即表单数据或查询参数) ) 添加到模型对象中的属性,以及 其嵌套对象。
仅公开遵循 JavaBeans 命名约定的属性以进行数据绑定,例如,公开属性的方法。public
public String getFirstName()
public void setFirstName(String)
firstName
模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO (Plain Old Java Object)。 |
默认情况下, Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为 客户端可以面向任何公共属性路径,甚至是一些不需要的公共属性路径 针对给定的使用案例。
例如,给定一个 HTTP 表单数据端点,恶意客户端可能会提供 存在于模型对象图中但不属于 HTML 表单的属性 在浏览器中显示。这可能会导致在模型对象上设置数据,并且任何 的嵌套对象中。
推荐的方法是使用仅公开
与表单提交相关的属性。例如,在用于更改
用户的电子邮件地址,则 Model 对象应声明一组最小属性,例如
如下所示。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;
}
}
如果您不能或不想为每个数据使用专用模型对象
binding 用例中,您必须限制允许进行数据绑定的属性。
理想情况下,您可以通过 上的方法注册允许的字段模式来实现此目的。setAllowedFields()
WebDataBinder
例如,要在应用程序中注册允许的字段模式,您可以在 or 组件中实现一个方法,如下所示:@InitBinder
@Controller
@ControllerAdvice
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以注册不允许的
field 模式。
但请注意,“允许列表”比“拒绝列表”更安全。因此, 应该优先于 .setDisallowedFields()
DataBinder
setAllowedFields()
setDisallowedFields()
请注意,与允许的字段模式匹配区分大小写;鉴于匹配 Against Disallowed 字段模式不区分大小写。此外,匹配 不允许的模式将不被接受,即使它也恰好与 允许列表。
正确配置允许和不允许的字段模式非常重要 直接公开域模型以进行数据绑定时。否则,它是一个 安全风险大。 此外,强烈建议您不要使用域中的类型 模型(如 JPA 或 Hibernate 实体)作为数据绑定场景中的模型对象。 |
1.3.6. 异常
@Controller
@ControllerAdvice类可以具有处理控制器方法异常的方法,如下例所示:@ExceptionHandler
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
@Controller
class SimpleController {
// ...
@ExceptionHandler
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
该异常可能与正在传播的顶级异常(例如,引发的直接异常)或包装器异常中的嵌套原因(例如:
一个包裹在 一个 )。从 5.3 开始,这可以匹配
在任意原因水平上,而以前只考虑直接原因。IOException
IOException
IllegalStateException
对于匹配的异常类型,最好将目标异常声明为方法参数,
如前面的示例所示。当多个异常方法匹配时,根异常匹配为
通常优先于 Cause 异常匹配。更具体地说,这用于根据异常与引发的异常类型的深度对异常进行排序。ExceptionDepthComparator
或者,注释声明可以缩小异常类型的范围以匹配 如下例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
您甚至可以使用具有非常通用参数签名的特定异常类型列表 如下例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
// ...
}
根异常匹配和原因异常匹配之间的区别可能令人惊讶。 在前面显示的变体中,该方法通常使用
actual 或 instance 作为参数,
因为它们都从 .但是,如果有任何此类匹配
exception 在包装器 exception 中传播,该 exception 本身是一个 ,
传入的 Exception 实例是该包装器 Exception。 变体中的行为甚至更简单。这是
always 调用时,在 wrapping 场景中使用 wrapper 异常调用,并且
实际上,在那种情况下,要找到的匹配异常。
传入的异常仅在作为顶级异常引发时,才是实际异常或实例。 |
我们通常建议您在参数签名中尽可能具体。
减少 root 和 cause 异常类型之间不匹配的可能性。
考虑将多重匹配方法分解为单独的方法,每个方法通过其签名匹配单个特定的异常类型。@ExceptionHandler
在多 Arrangement 中,我们建议声明您的主根异常
具有相应顺序的 prioritized 上的 mappings。虽然 root
exception match 优先于 cause,这是在给定
controller 或 class 的这意味着优先级较高的 Bean 上的原因匹配优先于优先级较低的 Bean 上的任何匹配项(例如,root)。@ControllerAdvice
@ControllerAdvice
@ControllerAdvice
@ControllerAdvice
@ControllerAdvice
最后但并非最不重要的一点是,方法实现可以选择 back
通过以原始形式重新引发给定的异常实例来处理该实例。
这在您只对根级别匹配或
无法静态确定的特定上下文中的匹配项。一个 rethrown
exception 通过剩余的解析链传播,就像
给定的方法一开始就不匹配。@ExceptionHandler
@ExceptionHandler
对 Spring MVC 中方法的支持建立在HandlerExceptionResolver机制级别上。@ExceptionHandler
DispatcherServlet
方法参数
@ExceptionHandler
方法支持以下参数:
Method 参数 | 描述 |
---|---|
异常类型 |
用于访问引发的异常。 |
|
用于访问引发异常的 controller 方法。 |
|
对请求参数以及请求和会话属性的通用访问,无需直接 使用 Servlet API。 |
|
选择任何特定的请求或响应类型(例如,或 Spring 的 或 )。 |
|
强制会话的存在。因此,这样的参数永远不会 。 |
|
Currently authenticated user — 如果已知,则可能是特定的 implementation class。 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的可用区域设置确定 — 在
效果、配置的 或 。 |
|
与当前请求关联的时区,由 确定。 |
|
用于访问 Servlet API 公开的原始响应正文。 |
|
用于访问错误响应的模型。永远是空的。 |
|
指定在重定向时使用的属性 — (即要附加到查询中 string) 和 flash 属性临时存储,直到重定向后的请求。 请参阅重定向属性和 Flash 属性。 |
|
对于任何 session 属性的访问,与存储在
session 作为类级声明的结果。
有关详细信息 |
|
用于访问请求属性。有关详细信息 |
返回值
@ExceptionHandler
方法支持以下返回值:
返回值 | 描述 |
---|---|
|
返回值通过实例转换并写入
响应。请参阅 |
|
返回值指定完整响应(包括 HTTP 标头和正文)
通过实例转换并写入响应。
请参阅 ResponseEntity。 |
|
一个视图名称,要通过 implementations 解析并与
隐式模型 — 通过命令对象和方法确定。
handler 方法还可以通过声明参数(如前所述)以编程方式丰富模型。 |
|
用于与隐式模型一起呈现的实例 — determined
通过命令对象和方法。handler 方法还可以
通过声明一个参数(前面描述)以编程方式丰富模型。 |
|
要添加到隐式模型中且视图名称隐式确定的属性
通过 . |
|
要添加到模型中的属性,其视图名称通过
一个。 请注意,这是可选的。请参阅 末尾的 “任何其他返回值”
这个表。 |
|
要使用的 view 和 model 属性,以及响应状态(可选)。 |
|
具有返回类型(或返回值)的方法被视为完全具有
如果响应也具有 an 参数,则处理响应,或者
注释。如果控制器进行了肯定或时间戳检查,情况也是如此(有关详细信息,请参阅 控制器 )。 如果以上都不是 true,则返回类型也可以指示 “no response body”
REST 控制器或 HTML 控制器的默认视图名称选择。 |
任何其他返回值 |
如果返回值与上述任何内容都不匹配,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定), 默认情况下,它被视为要添加到模型的 model 属性。如果是简单类型, 它仍然没有解决。 |
REST API 异常
REST 服务的一个常见要求是在
响应。Spring 框架不会自动执行此操作,因为表示
of error details 是特定于应用程序的。但是,可以使用带有 return 的方法
值以设置响应的状态和正文。此类方法也可以声明
在类中将它们应用于全局。@RestController
@ExceptionHandler
ResponseEntity
@ControllerAdvice
在响应中实现全局异常处理且错误详细信息的应用程序
body 应考虑扩展 ResponseEntityExceptionHandler
,
它为 Spring MVC 引发的异常提供处理,并为
自定义响应正文。要利用它,请创建一个子类 ,用 、 override
必要的方法,并将其声明为 Spring bean。ResponseEntityExceptionHandler
@ControllerAdvice
1.3.7. 控制器建议
@ExceptionHandler
、 和 方法仅适用于在其中声明它们的类或类层次结构。相反,如果他们
在 OR 类中声明,则它们将应用
到任何控制器。此外,从 5.3 开始,method in 可用于处理来自任何或任何其他处理程序的异常。@InitBinder
@ModelAttribute
@Controller
@ControllerAdvice
@RestControllerAdvice
@ExceptionHandler
@ControllerAdvice
@Controller
@ControllerAdvice
进行了元注释,因此可以注册为
通过组件扫描的 Spring Bean。 使用 和 进行元注释,这意味着方法将返回
值通过响应正文消息转换呈现,而不是通过 HTML 视图呈现。@Component
@RestControllerAdvice
@ControllerAdvice
@ResponseBody
@ExceptionHandler
启动时,并检测
controller 建议 bean 并在运行时应用它们。全局方法、
从 , 应用于本地 , 从 .
相比之下,global 和 methods 在 local ones 之前应用。RequestMappingHandlerMapping
ExceptionHandlerExceptionResolver
@ExceptionHandler
@ControllerAdvice
@Controller
@ModelAttribute
@InitBinder
该注释具有一些属性,可用于缩小控制器集的范围
以及它们应用于的处理程序。例如:@ControllerAdvice
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3
前面示例中的选择器在运行时进行评估,可能会产生负面影响
性能。有关更多详细信息,请参阅 @ControllerAdvice
javadoc。
1.4. 功能端点
Spring Web MVC 包括 WebMvc.fn,这是一个轻量级函数式编程模型,其中函数 用于路由和处理请求,而 Contract 旨在实现不变性。 它是基于 Comments 的编程模型的替代方案,但在其他方面运行 相同的 DispatcherServlet。
1.4.1. 概述
在 WebMvc.fn 中,HTTP 请求使用 : 一个函数处理,该函数接受并返回一个 .
请求和响应对象都有不可变的契约,这些契约提供对 JDK 8 友好的
访问 HTTP 请求和响应。 等效于
基于注释的编程模型。HandlerFunction
ServerRequest
ServerResponse
HandlerFunction
@RequestMapping
传入请求被路由到具有 : 的处理程序函数,该函数
接受并返回一个可选值(即 )。
当 router 函数匹配时,将返回一个处理程序函数;否则为空 Optional。 等同于注释,但使用 major
不同之处在于 router 函数不仅提供数据,还提供行为。RouterFunction
ServerRequest
HandlerFunction
Optional<HandlerFunction>
RouterFunction
@RequestMapping
RouterFunctions.route()
提供便于创建路由器的 Router 构建器,
如下例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
1 | 使用路由器 DSL 创建路由器。 |
如果将 注册为 bean,例如通过在类中公开它,则 servlet 将自动检测到它,如 运行服务器中所述。RouterFunction
@Configuration
1.4.2. HandlerFunction 函数
ServerRequest
并且是提供 JDK 8 友好的不可变接口
访问 HTTP 请求和响应,包括标头、正文、方法和状态代码。ServerResponse
服务器请求
ServerRequest
提供对 HTTP 方法、URI、标头和查询参数的访问,
而对 body 的访问是通过 methods.body
以下示例将请求正文提取为 :String
String string = request.body(String.class);
val string = request.body<String>()
以下示例将正文提取为 ,
其中对象是从序列化形式(如 JSON 或 XML)解码的:List<Person>
Person
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
val people = request.body<Person>()
以下示例显示如何访问参数:
MultiValueMap<String, String> params = request.params();
val map = request.params()
服务器响应
ServerResponse
提供对 HTTP 响应的访问,并且由于它是不可变的,因此您可以使用
一个创建它的方法。您可以使用生成器设置响应状态,以添加响应
headers 或提供正文。以下示例使用 JSON 创建 200 (OK) 响应
内容:build
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
以下示例显示如何构建带有标头但无正文的 201 (CREATED) 响应:Location
URI location = ...
ServerResponse.created(location).build();
val location: URI = ...
ServerResponse.created(location).build()
您还可以使用异步结果作为正文,采用 、 或 .例如:CompletableFuture
Publisher
ReactiveAdapterRegistry
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
val person = webClient.get().retrieve().awaitBody<Person>()
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)
如果不仅 body 基于 body,而且 status 或 headers 都基于异步类型,
您可以在 上使用 static 方法,该方法
接受 、 或
.例如:async
ServerResponse
CompletableFuture<ServerResponse>
Publisher<ServerResponse>
ReactiveAdapterRegistry
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.name()).body(p));
ServerResponse.async(asyncResponse);
Server-Sent Events 可以通过
static 方法 。该方法提供的生成器
允许您以 JSON 格式发送字符串或其他对象。例如:sse
ServerResponse
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere..
}));
}
// In some other thread, sending a String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point
sseBuilder.complete();
fun sse(): RouterFunction<ServerResponse> = router {
GET("/sse") { request -> ServerResponse.sse { sseBuilder ->
// Save the sseBuilder object somewhere..
}
}
// In some other thread, sending a String
sseBuilder.send("Hello world")
// Or an object, which will be transformed into JSON
val person = ...
sseBuilder.send(person)
// Customize the event by using the other methods
sseBuilder.id("42")
.event("sse event")
.data(person)
// and done at some point
sseBuilder.complete()
处理程序类
我们可以将处理程序函数编写为 lambda,如下例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
val helloWorld: (ServerRequest) -> ServerResponse =
{ ServerResponse.ok().body("Hello World") }
这很方便,但在应用程序中,我们需要多个函数和多个内联
Lambda 可能会变得混乱。
因此,将相关的处理程序函数一起分组到一个处理程序类中是很有用的,该
具有与基于注释的应用程序类似的角色。
例如,以下类公开了一个反应式存储库:@Controller
Person
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
1 | listPeople 是一个处理程序函数,它将在存储库中找到的所有对象作为
JSON 的 JSON 格式。Person |
2 | createPerson 是一个处理程序函数,用于存储请求正文中包含的 new。Person |
3 | getPerson 是一个处理程序函数,它返回一个 person,由路径
变量。我们从存储库中检索该响应并创建一个 JSON 响应(如果是
发现。如果未找到,我们将返回 404 Not Found 响应。id Person |
class PersonHandler(private val repository: PersonRepository) {
fun listPeople(request: ServerRequest): ServerResponse { (1)
val people: List<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).body(people);
}
fun createPerson(request: ServerRequest): ServerResponse { (2)
val person = request.body<Person>()
repository.savePerson(person)
return ok().build()
}
fun getPerson(request: ServerRequest): ServerResponse { (3)
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
?: ServerResponse.notFound().build()
}
}
1 | listPeople 是一个处理程序函数,它将在存储库中找到的所有对象作为
JSON 的 JSON 格式。Person |
2 | createPerson 是一个处理程序函数,用于存储请求正文中包含的 new。Person |
3 | getPerson 是一个处理程序函数,它返回一个 person,由路径
变量。我们从存储库中检索该响应并创建一个 JSON 响应(如果是
发现。如果未找到,我们将返回 404 Not Found 响应。id Person |
验证
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 instance 创建实例。Validator |
2 | 应用验证。 |
3 | 引发 400 响应的异常。 |
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() (1)
// ...
fun createPerson(request: ServerRequest): ServerResponse {
val person = request.body<Person>()
validate(person) (2)
repository.savePerson(person)
return ok().build()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person")
validator.validate(person, errors)
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) (3)
}
}
}
1 | Create instance 创建实例。Validator |
2 | 应用验证。 |
3 | 引发 400 响应的异常。 |
处理程序还可以通过创建和注入来使用标准 bean 验证 API (JSR-303)
基于 的全局实例。
参见 Spring Validation。Validator
LocalValidatorFactoryBean
1.4.3.RouterFunction
Router 函数用于将请求路由到相应的 .
通常,您不会自己编写 router 函数,而是使用 Utility 类上的方法创建一个。 (无参数)为您提供用于创建路由器的 Fluent 构建器
函数,而提供直接的
创建路由器。HandlerFunction
RouterFunctions
RouterFunctions.route()
RouterFunctions.route(RequestPredicate, HandlerFunction)
通常,建议使用构建器,因为它提供了
适用于典型映射场景的便捷捷径,无需难以发现
static imports。
例如,router 函数构建器提供了为 GET 请求创建 Map 的方法;和 POST。route()
GET(String, HandlerFunction)
POST(String, HandlerFunction)
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种引入其他
谓词。
对于每个 HTTP 方法,都有一个重载的变体,它将 a 作为
参数,通过该参数可以表示其他约束。RequestPredicate
谓词
您可以编写自己的 ,但 utility 类
提供常用的实现,基于请求路径、HTTP 方法、内容类型、
等等。
以下示例使用请求谓词创建基于 header 的约束:RequestPredicate
RequestPredicates
Accept
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
import org.springframework.web.servlet.function.router
val route = router {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().body("Hello World")
}
}
您可以使用以下方法将多个请求谓词组合在一起:
-
RequestPredicate.and(RequestPredicate)
— 两者必须匹配。 -
RequestPredicate.or(RequestPredicate)
— 两者都可以匹配。
许多谓词都是组合的。
例如, 由 和 组成。
上面显示的示例还使用了两个请求谓词,因为生成器在内部使用,并使用谓词组合它。RequestPredicates
RequestPredicates.GET(String)
RequestPredicates.method(HttpMethod)
RequestPredicates.path(String)
RequestPredicates.GET
accept
路线
路由器功能按顺序评估:如果第一个路由不匹配,则 second 被评估,依此类推。 因此,在一般路由之前声明更具体的路由是有意义的。 在将路由器函数注册为 Spring bean 时,这一点也很重要,也是如此 稍后描述。 请注意,此行为与基于 Comments 的编程模型不同,其中 “最具体”控制器方法会自动选取。
使用 router 函数构建器时,所有定义的路由都组合成一个路由,该路由从 返回。
还有其他方法可以将多个 router 功能组合在一起:RouterFunction
build()
-
add(RouterFunction)
在构建器上RouterFunctions.route()
-
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— 嵌套 的快捷方式 。RouterFunction.and()
RouterFunctions.route()
以下示例显示了四个路由的组合:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
.POST("/person", handler::createPerson) (3)
.add(otherRoute) (4)
.build();
1 | GET /person/{id} 将匹配 JSON 的标头路由到Accept PersonHandler.getPerson |
2 | GET /person 将匹配 JSON 的标头路由到Accept PersonHandler.listPeople |
3 | POST /person 在没有其他谓词的情况下映射到 ,并且PersonHandler.createPerson |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | GET /person/{id} 将匹配 JSON 的标头路由到Accept PersonHandler.getPerson |
2 | GET /person 将匹配 JSON 的标头路由到Accept PersonHandler.listPeople |
3 | POST /person 在没有其他谓词的情况下映射到 ,并且PersonHandler.createPerson |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
嵌套路由
一组 router 函数通常具有共享谓词,例如共享的
路径。
在上面的示例中,共享谓词将是与 、 匹配的路径谓词 ,
由其中 3 条路由使用。
使用注释时,可以通过使用映射到 .
在 WebMvc.fn 中,可以通过路由器函数构建器上的方法共享路径谓词。
例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行:/person
@RequestMapping
/person
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 | 请注意,第二个参数 of 是采用路由器构建器的使用者。path |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET(accept(APPLICATION_JSON), handler::listPeople)
POST(handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以使用
构建器上的方法。
上面仍然包含一些共享 -header 谓词形式的重复。
我们可以通过将该方法与以下方法结合使用来进一步改进:nest
Accept
nest
accept
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
}
}
}
1.4.4. 运行服务器
通常通过 MVC Config 在基于 DispatcherHandler
的设置中运行路由器函数,该配置使用 Spring 配置来声明
处理请求所需的组件。MVC Java 配置声明以下内容
支持功能终端节点的基础设施组件:
-
RouterFunctionMapping
:在 Spring 中检测到一个或多个 bean 配置,对它们进行排序,通过 组合它们,并将请求路由到生成的 composed 。RouterFunction<?>
RouterFunction.andOther
RouterFunction
-
HandlerFunctionAdapter
:允许调用 a 映射到请求。DispatcherHandler
HandlerFunction
前面的组件允许功能端点适应请求
处理生命周期,并且(可能)与带注释的控制器并行运行,如果
任何 (any) 都已声明。这也是 Spring Boot Web 启用功能端点的方式
起动机。DispatcherServlet
以下示例显示了 WebFlux Java 配置:
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
1.4.5. 过滤处理程序函数
您可以通过在路由上使用 、 或 方法来筛选处理程序函数
函数构建器。
使用注释,您可以通过使用 、 a 或两者来实现类似的功能。
该筛选条件将应用于构建器构建的所有路由。
这意味着嵌套路由中定义的筛选条件不适用于 “top-level” 路由。
例如,请考虑以下示例: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 | 添加自定义请求标头的筛选条件仅适用于两个 GET 路由。before |
2 | 记录响应的筛选条件将应用于所有路由,包括嵌套路由。after |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST(handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
1 | 添加自定义请求标头的筛选条件仅适用于两个 GET 路由。before |
2 | 记录响应的筛选条件将应用于所有路由,包括嵌套路由。after |
路由器构建器上的方法采用 : 一个
函数,该函数采用 AND 并返回 .
handler 函数参数表示链中的下一个元素。
这通常是路由到的处理程序,但也可以是另一个
filter (如果应用了多个)。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();
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用 is optional。
我们只允许在允许访问时运行处理程序函数。next.handle(ServerRequest)
除了在路由器函数构建器上使用该方法外,还可以应用
通过 Filter 筛选到现有路由器函数。filter
RouterFunction.filter(HandlerFilterFunction)
对功能终端节点的 CORS 支持通过专用的 CorsFilter 提供。 |
1.5. URI 链接
本节描述了 Spring Framework 中可用于 URI 的各种选项。
1.5.1. UriComponents
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
有助于使用变量从 URI 模板构建 URI,如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个 .UriComponents |
5 | 展开变量并获取 .URI |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个 .UriComponents |
5 | 展开变量并获取 .URI |
前面的示例可以合并为一个链,并用 ,
如下例所示:buildAndExpand
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()
您可以通过直接转到 URI(这意味着编码)来进一步缩短它, 如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.5.2. Uri生成器
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
实现 .您可以依次使用 .一起提供一种可插入的机制,以基于
共享配置,例如基本 URL、编码首选项和其他详细信息。UriBuilder
UriBuilder
UriBuilderFactory
UriBuilderFactory
UriBuilder
您可以配置 和 来自定义 URI 的准备。 是默认值
实现 that uses internal,
公开共享配置选项。RestTemplate
WebClient
UriBuilderFactory
DefaultUriBuilderFactory
UriBuilderFactory
UriComponentsBuilder
以下示例显示如何配置 :RestTemplate
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory
以下示例配置了一个 :WebClient
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode
val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES
val client = WebClient.builder().uriBuilderFactory(factory).build()
此外,您也可以直接使用。它类似于 using ,但它不是静态工厂方法,而是一个实际的实例
,其中包含 configuration 和 preferences,如下例所示:DefaultUriBuilderFactory
UriComponentsBuilder
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.5.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode() 中: 首先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。
-
UriComponents#encode() 中: 在 URI 变量展开后对 URI 组件进行编码。
这两个选项都用转义的八位字节替换非 ASCII 字符和非法字符。但是,第一个选项 还会替换 URI 变量中出现的具有保留含义的字符。
请考虑 “;”,它在 path 中是合法的,但具有保留的含义。第一个选项将 “;” 在 URI 变量中带有 “%3B”,但在 URI 模板中没有。相比之下,第二个选项永远不会 替换 “;”,因为它是路径中的合法字符。 |
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量作为不透明数据进行完全编码,而第二个选项在 URI 变量确实有意包含保留字符。第二个选项也很有用 当根本不扩展 URI 变量时,因为这也将编码任何 顺便说一句,看起来像一个 URI 变量。
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到 URI(这意味着编码)来缩短前面的示例, 如下例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
和 扩展和编码 URI 模板在 内部通过
策略。两者都可以使用自定义策略
如下例所示:WebClient
RestTemplate
UriBuilderFactory
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
该实现在内部使用
展开并编码 URI 模板。作为工厂,它提供了一个配置位置
编码方法,基于以下编码模式之一:DefaultUriBuilderFactory
UriComponentsBuilder
-
TEMPLATE_AND_VALUES
: 使用 ,对应于 前面列表中的第一个选项,用于预编码 URI 模板并在 扩大。UriComponentsBuilder#encode()
-
VALUES_ONLY
:不对 URI 模板进行编码,而是应用严格编码 到 URI 变量中,然后再将它们扩展到 模板。UriUtils#encodeUriVariables
-
URI_COMPONENT
:使用对应于前面列表中的第二个选项的 ,以 在 URI 变量展开后对 URI 组件值进行编码。UriComponents#encode()
-
NONE
:不应用编码。
设置为 historic
原因和向后兼容性。依赖于默认值
in ,该 URL 已从 in 更改
5.0.x 复制到 5.1 中。RestTemplate
EncodingMode.URI_COMPONENT
WebClient
DefaultUriBuilderFactory
EncodingMode.URI_COMPONENT
EncodingMode.TEMPLATE_AND_VALUES
1.5.4. 相对 Servlet 请求
您可以使用它来创建相对于当前请求的 URI。
如下例所示:ServletUriComponentsBuilder
HttpServletRequest request = ...
// Re-uses scheme, host, port, path, and query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, path, and query string...
val uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123")
您可以创建相对于上下文路径的 URI,如下例所示:
HttpServletRequest request = ...
// Re-uses scheme, host, port, and context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, and context path...
val uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri()
您可以创建相对于 Servlet 的 URI(例如、
如下例所示:/main/*
HttpServletRequest request = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
val request: HttpServletRequest = ...
// Re-uses scheme, host, port, context path, and Servlet mapping prefix...
val uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri()
从 5.1 开始,忽略 和 标头中的信息,这些标头指定客户端发起的地址。考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃
这样的标头。ServletUriComponentsBuilder Forwarded X-Forwarded-* |
1.5.5. 控制器链接
Spring MVC 提供了一种机制来准备指向控制器方法的链接。例如 以下 MVC 控制器允许创建链接:
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}
@Controller
@RequestMapping("/hotels/{hotel}")
class BookingController {
@GetMapping("/bookings/{booking}")
fun getBooking(@PathVariable booking: Long): ModelAndView {
// ...
}
}
您可以通过按名称引用方法来准备链接,如下例所示:
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
在前面的示例中,我们提供了实际的方法参数值(在本例中为 long 值:)
用作路径变量并插入到 URL 中。此外,我们还提供
value , , 填充任何剩余的 URI 变量,例如继承的变量
从类型级请求映射。如果该方法有更多的参数,我们可以为
URL 不需要参数。通常,只有 and 参数
与构造 URL 相关。21
42
hotel
@PathVariable
@RequestParam
还有其他使用 .例如,您可以使用一种技术
类似于通过代理进行模拟测试,以避免通过名称引用控制器方法,如下例所示
(该示例假定 static import 的 ):MvcUriComponentsBuilder
MvcUriComponentsBuilder.on
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
控制器方法签名在设计上受到限制,而它们应该可用于
使用 创建链接。除了需要适当的参数签名之外,
返回类型存在技术限制(即生成运行时代理
),因此返回类型不能为 .特别
视图名称的通用返回类型在此处不起作用。你应该使用 甚至 plain (带有返回值)。fromMethodCall final String ModelAndView Object String |
前面的示例在 中使用了 中的静态方法。在内部,他们依赖于
on 从方案、host、port、
当前请求的 context path 和 servlet path 的 servlet 路径。这在大多数情况下效果很好。
但是,有时,它可能不够。例如,您可能位于
请求(例如准备链接的批处理),或者可能需要插入路径
前缀(例如已从请求路径中删除的区域设置前缀,需要
重新插入到链接中)。MvcUriComponentsBuilder
ServletUriComponentsBuilder
对于此类情况,您可以使用接受 a 的 static 重载方法来使用基 URL。或者,您也可以使用基 URL 创建实例,然后使用基于实例的方法。例如,
以下 列表用途 :fromXxx
UriComponentsBuilder
MvcUriComponentsBuilder
withXxx
withMethodCall
UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")
val builder = MvcUriComponentsBuilder.relativeTo(base)
builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)
val uri = uriComponents.encode().toUri()
从 5.1 开始,忽略 和 标头中的信息,这些标头指定客户端发起的地址。考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃
这样的标头。MvcUriComponentsBuilder Forwarded X-Forwarded-* |
1.5.6. 视图中的链接
在 Thymeleaf、FreeMarker 或 JSP 等视图中,您可以构建指向带注释的控制器的链接 通过引用每个请求映射的隐式或显式分配的名称。
请考虑以下示例:
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
@RequestMapping("/people/{id}/addresses")
class PersonAddressController {
@RequestMapping("/{country}")
fun getAddress(@PathVariable country: String): HttpEntity<PersonAddress> { ... }
}
给定前面的控制器,您可以从 JSP 准备一个链接,如下所示:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>
前面的示例依赖于 Spring 标记库中声明的函数
(即 META-INF/spring.tld),但定义自己的函数或准备
其他模板技术类似。mvcUrl
这是它的工作原理。启动时,每个 each 都分配了一个默认名称
通过其默认实现使用
类和方法名称的大写字母(例如,method in 变为 “TC#getThing”)。如果存在名称冲突,则可以使用 分配显式名称或实现自己的 .@RequestMapping
HandlerMethodMappingNamingStrategy
getThing
ThingController
@RequestMapping(name="..")
HandlerMethodMappingNamingStrategy
1.6. 异步请求
Spring MVC 与 Servlet 3.0 异步请求处理进行了广泛的集成:
1.6.1.DeferredResult
在 Servlet 容器中启用异步请求处理功能后,控制器方法可以包装任何受支持的控制器方法
返回值,如下例所示:DeferredResult
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// Save the deferredResult somewhere..
return deferredResult
}
// From some other thread...
deferredResult.setResult(result)
控制器可以从不同的线程异步生成返回值 — 对于 例如,响应外部事件 (JMS 消息)、计划任务或其他事件。
1.6.2.Callable
控制器可以用 、
如下例所示:java.util.concurrent.Callable
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
// ...
"someView"
}
然后,可以通过配置的 .TaskExecutor
1.6.3. 处理
以下是 Servlet 异步请求处理的非常简洁的概述:
-
可以通过调用 将 A 置于异步模式。 这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但 响应将保持打开状态,以便稍后完成处理。
ServletRequest
request.startAsync()
-
对 returns 的调用,您可以将其用于 进一步控制异步处理。例如,它提供了方法 它类似于 Servlet API 的 forward,不同之处在于它允许 应用程序在 Servlet 容器线程上恢复请求处理。
request.startAsync()
AsyncContext
dispatch
-
提供对当前 的访问,您可以 用于区分处理初始请求、异步 dispatch、a forward 和其他 Dispatcher 类型。
ServletRequest
DispatcherType
DeferredResult
处理工作如下:
-
控制器返回 a 并将其保存在内存中的某个 queue 或 list 中。
DeferredResult
-
Spring MVC 调用 .
request.startAsync()
-
同时,和 all configured 过滤器退出请求 processing 线程,但响应保持打开状态。
DispatcherServlet
-
应用程序从某个线程和 Spring MVC 设置 将请求分派回 Servlet 容器。
DeferredResult
-
再次调用 ,并使用 异步生成的返回值。
DispatcherServlet
Callable
处理工作如下:
-
控制器返回一个 .
Callable
-
Spring MVC 调用并将 a 在单独的线程中进行处理。
request.startAsync()
Callable
TaskExecutor
-
同时,和 all 过滤器退出 Servlet 容器线程 但回应仍然开放。
DispatcherServlet
-
最终 会生成一个结果,Spring MVC 将请求分派回来 添加到 Servlet 容器中以完成处理。
Callable
-
再次调用 ,并使用 从 .
DispatcherServlet
Callable
有关更多背景和上下文,您还可以阅读 在 Spring MVC 3.2 中引入异步请求处理支持的博客文章。
异常处理
使用 时,可以选择是调用 还是使用异常。在这两种情况下,Spring MVC 都会将请求分派回来
添加到 Servlet 容器中以完成处理。然后,它被视为
controller 方法返回给定的值,或者就像它产生了给定的异常一样。
然后,异常通过常规异常处理机制(例如,调用方法)。DeferredResult
setResult
setErrorResult
@ExceptionHandler
当您使用 时,会出现类似的处理逻辑,主要区别在于
结果从 返回 或 异常 由它引发。Callable
Callable
拦截
HandlerInterceptor
instances 可以是 type ,以接收异步启动的初始请求的回调
processing (而不是 and )。AsyncHandlerInterceptor
afterConcurrentHandlingStarted
postHandle
afterCompletion
HandlerInterceptor
实现还可以注册 a 或 a ,以便与
异步请求的生命周期(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptor
。CallableProcessingInterceptor
DeferredResultProcessingInterceptor
DeferredResult
提供和回调。
有关更多详细信息,请参阅 DeferredResult
的 javadoc。 可以替换为
timeout和completion回调的方法。onTimeout(Runnable)
onCompletion(Runnable)
Callable
WebAsyncTask
与 WebFlux 相比
Servlet API 最初是为通过 Filter-Servlet 进行一次传递而构建的
链。Servlet 3.0 中添加的异步请求处理允许应用程序退出
Filter-Servlet 链,但将响应保持开放状态以供进一步处理。The Spring MVC
异步支持是围绕该机制构建的。当控制器返回 ,
退出 Filter-Servlet 链,并释放 Servlet 容器线程。稍后,当
,则进行分派(到同一 URL),在此期间,
controller 的 controller 再次映射,但不是调用它,而是使用该值
(就像控制器返回一样)以恢复处理。DeferredResult
DeferredResult
ASYNC
DeferredResult
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的 asynchronous request processing 功能,因为它在设计上是异步的。异步 Handling 内置于所有框架 Contract 中,并且通过 ALL 请求处理阶段。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持 asynchronous 和 Reactive Types 作为控制器方法中的返回值。 Spring MVC 甚至支持流,包括反应式背压。但是,单个 对响应的写入仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同, 它依赖于非阻塞 I/O,并且每次写入不需要额外的线程。
另一个根本区别是 Spring MVC 不支持异步或反应式
控制器方法参数中的类型(例如, , , 等),
它也没有任何明确支持异步和反应类型作为模型属性。
Spring WebFlux 确实支持所有这些。@RequestBody
@RequestPart
1.6.4. HTTP 流
您可以将 and 用于单个异步返回值。
如果您想生成多个异步值,并将这些值写入
响应?本节介绍如何执行此操作。DeferredResult
Callable
对象
您可以使用 return 值生成对象流,其中
每个对象都使用 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();
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
你也可以用作 中的 body ,让你
自定义响应的状态和标头。ResponseBodyEmitter
ResponseEntity
当 an 抛出一个 (例如,如果远程客户端消失) 时,应用程序
不负责清理连接,并且不应调用 或 .相反,servlet 容器会自动启动错误通知,其中 Spring MVC 进行调用。
反过来,此调用对应用程序执行一次最终调度,在此期间 Spring MVC
调用配置的异常解析程序并完成请求。emitter
IOException
emitter.complete
emitter.completeWithError
AsyncListener
completeWithError
ASYNC
上交所
SseEmitter
() 的子类 ) 提供对 Server-Sent Events 的支持,其中从服务器发送的事件
根据 W3C SSE 规范进行格式设置。生成 SSE
stream 中,返回 ,如下例所示:ResponseBodyEmitter
SseEmitter
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
虽然 SSE 是流式传输到浏览器的主要选项,但请注意,Internet Explorer 不支持 Server-Sent Events。考虑将 Spring 的 WebSocket 消息传递与 Sockjs 回退传输(包括 SSE)一起使用,该传输将 广泛的浏览器。
有关异常处理的说明,另请参阅上一节。
原始数据
有时,绕过消息转换并直接流式传输到响应(例如,对于文件下载)非常有用。您可以使用返回值类型来执行此操作,如下例所示:OutputStream
StreamingResponseBody
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
@GetMapping("/download")
fun handle() = StreamingResponseBody {
// write...
}
您可以用作 to 中的主体
自定义响应的状态和标头。StreamingResponseBody
ResponseEntity
1.6.5. 响应式类型
Spring MVC 支持在控制器中使用反应式 Client 端库(另请阅读 WebFlux 部分中的反应式库)。
这包括 from 和其他 s,例如 Spring Data
反应式数据存储库。在这种情况下,能够返回很方便
来自 controller 方法的 reactive 类型。WebClient
spring-webflux
反应式返回值的处理方式如下:
-
单值 promise 适用于,类似于使用 .例子 include (Reactor) 或 (RxJava)。
DeferredResult
Mono
Single
-
具有流媒体类型(如 或 )的多值流适用于,类似于使用 或 。示例包括 (Reactor) 或 (RxJava)。 应用程序还可以返回 或 。
application/x-ndjson
text/event-stream
ResponseBodyEmitter
SseEmitter
Flux
Observable
Flux<ServerSentEvent>
Observable<ServerSentEvent>
-
适配具有任何其他媒体类型(如 )的多值流 to 的 e 示例,类似于使用 .
application/json
DeferredResult<List<?>>
Spring MVC 通过 ReactiveAdapterRegistry from 支持 Reactor 和 RxJava,这使它能够适应多个反应式库。spring-core |
1.6.6. 断开连接
当远程客户端消失时,Servlet API 不提供任何通知。 因此,在流式传输到响应时,无论是通过 SseEmitter 还是反应式类型,定期发送数据都很重要。 因为如果客户端已断开连接,则写入失败。发送可以采用 空(仅评论)SSE 事件或另一方必须解释的任何其他数据 作为心跳并忽略。
或者,考虑使用 Web 消息传递解决方案(例如基于 WebSocket 的 STOMP 或带有 SockJS 的 WebSocket) 具有内置心跳机制的 S S S 的 S S S 的 S T
1.6.7. 配置
必须在 Servlet 容器级别启用异步请求处理功能。 MVC 配置还为异步请求公开了几个选项。
Servlet 容器
Filter 和 Servlet 声明有一个标志,需要设置该标志才能启用异步请求处理。此外,Filter 映射应为
声明处理 .asyncSupported
true
ASYNC
javax.servlet.DispatchType
在 Java 配置中,当您使用初始化 Servlet 容器时,这是自动完成的。AbstractAnnotationConfigDispatcherServletInitializer
在 configuration 中,您可以添加到 和 to 声明以及 add to filter 映射。web.xml
<async-supported>true</async-supported>
DispatcherServlet
Filter
<dispatcher>ASYNC</dispatcher>
Spring MVC
MVC 配置公开了以下与异步请求处理相关的选项:
-
Java 配置:使用 on 上的回调。
configureAsyncSupport
WebMvcConfigurer
-
XML 命名空间:使用 下的元素。
<async-support>
<mvc:annotation-driven>
您可以配置以下内容:
-
异步请求的默认超时值(如果未设置),则取决于 在底层 Servlet 容器上。
-
AsyncTaskExecutor
用于在使用 Reactive Types 进行流式处理时阻止写入,以及执行从 controller 方法。如果您满足以下条件,我们强烈建议您配置此属性 stream 或具有返回 的控制器方法,因为 默认情况下,它是一个 .Callable
Callable
SimpleAsyncTaskExecutor
-
DeferredResultProcessingInterceptor
implementations 和 implementations。CallableProcessingInterceptor
请注意,您还可以在 、
a 和 .对于 ,可用于提供超时值。DeferredResult
ResponseBodyEmitter
SseEmitter
Callable
WebAsyncTask
1.7. CORS
Spring MVC 允许你处理 CORS(跨域资源共享)。本节 介绍如何执行此操作。
1.7.1. 简介
出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。 例如,您可以将银行账户放在一个选项卡中,而 evil.com 放在另一个选项卡中。脚本 evil.com 中,应该无法使用 凭证 — 例如,从您的账户中取款!
1.7.2. 处理
CORS 规范区分了印前检查请求、简单请求和实际请求。 要了解 CORS 的工作原理,您可以阅读这篇文章,其中 许多其他方法,或者参见 规范 了解更多详情。
Spring MVC 实现提供了对 CORS 的内置支持。成功后
将请求映射到处理程序,实现会检查 CORS 配置的
given request 和 handler 并采取进一步的操作。处理印前检查请求
直接,而简单和实际的 CORS 请求被拦截、验证,并且具有
所需的 CORS 响应标头集。HandlerMapping
HandlerMapping
为了启用跨域请求(即,标头存在且
与请求的主机不同),您需要有一些显式声明的 CORS
配置。如果未找到匹配的 CORS 配置,则印前 Backup 请求为
拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中
因此,浏览器会拒绝它们。Origin
每个都可以使用基于 URL 模式的映射单独配置。在大多数情况下,应用程序
使用 MVC Java 配置或 XML 命名空间来声明此类映射,这将产生
在传递给所有实例的单个全局映射中。HandlerMapping
CorsConfiguration
HandlerMapping
您可以将 级别的全局 CORS 配置与更多
精细的处理程序级 CORS 配置。例如,带注解的控制器可以使用
类或方法级注释(其他处理程序可以实现)。HandlerMapping
@CrossOrigin
CorsConfigurationSource
组合全局配置和本地配置的规则通常是累加的 — 例如,
所有全球和所有本地源。对于只能使用单个值
accepted,例如 和 ,local 将覆盖 global 值。有关更多详细信息,请参见 CorsConfiguration#combine(CorsConfiguration)。
allowCredentials
maxAge
要从源代码中了解更多信息或进行高级自定义,请检查背后的代码:
|
1.7.3. 凭证请求
将 CORS 与凭证请求一起使用需要启用 。请注意,
此选项与配置的域建立高级别的信任,并且还会增加
通过公开敏感的用户特定信息,Web 应用程序的攻击面
例如 Cookie 和 CSRF 令牌。allowedCredentials
启用凭证还会影响已配置 CORS 通配符的处理方式:"*"
-
通配符在 中未获得授权,但可以选择 该属性可用于匹配一组动态的 Origins。
allowOrigins
allowOriginPatterns
-
当设置为 or 时,和响应标头通过复制相关的 标头和方法。
allowedHeaders
allowedMethods
Access-Control-Allow-Headers
Access-Control-Allow-Methods
-
设置为 on 时,将设置响应标头 添加到配置的标头列表或通配符。虽然 CORS 规范 不允许使用通配符 when 设置为 ,大多数浏览器都支持通配符,并且响应标头在 CORS 处理,因此通配符是 无论属性的值如何,都指定。
exposedHeaders
Access-Control-Expose-Headers
Access-Control-Allow-Credentials
true
allowCredentials
虽然这种通配符配置可能很方便,但建议在可能的情况下配置 一组有限的值,以提供更高级的安全性。 |
1.7.4.@CrossOrigin
@CrossOrigin
注解支持对带注解的控制器方法进行跨域请求,
如下例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,允许:@CrossOrigin
-
所有来源。
-
所有标头。
-
控制器方法映射到的所有 HTTP 方法。
allowCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。启用后,必须
设置为一个或多个特定域(但不是 Special Value )或
该属性可用于匹配一组动态的 Origins。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(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
您可以在类级别和方法级别使用
如下例所示:@CrossOrigin
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
1.7.5. 全局配置
除了细粒度的 controller 方法级配置之外,您可能还希望
也定义一些全局 CORS 配置。您可以在任何 .但是,大多数应用程序都使用
MVC Java 配置或 MVC XML 命名空间来执行此操作。CorsConfiguration
HandlerMapping
默认情况下,全局配置将启用以下功能:
-
所有来源。
-
所有标头。
-
GET
、 和 方法。HEAD
POST
allowCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。启用后,必须
设置为一个或多个特定域(但不是 Special Value )或
该属性可用于匹配一组动态的 Origins。allowOrigins
"*"
allowOriginPatterns
maxAge
设置为 30 分钟。
Java 配置
要在 MVC Java 配置中启用 CORS,您可以使用回调
如下例所示:CorsRegistry
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
XML 配置
要在 XML 命名空间中启用 CORS,可以使用元素
如下例所示:<mvc:cors>
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
1.7.6. CORS 过滤器
您可以通过内置的 CorsFilter 应用 CORS
支持。
如果您尝试将 与 Spring Security 一起使用,请记住 Spring
Security 内置了对
CORS 的。CorsFilter |
要配置过滤器,请将 a 传递给其构造函数,作为
以下示例显示:CorsConfigurationSource
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
val filter = CorsFilter(source)
1.8. Web 安全
Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。请参阅 Spring Security 参考文档,包括:
HDIV 是另一个与 Spring MVC 集成的 Web 安全框架。
1.9. HTTP 缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存
围绕响应标头,随后围绕条件请求
标头(如 和 )。 建议私有(例如,浏览器)
以及有关如何缓存和重用响应的公共(例如,代理)缓存。使用标头
要发出可能导致 304 (NOT_MODIFIED) 没有正文的条件请求,
如果内容未更改。 可以看作是
标题。Cache-Control
Last-Modified
ETag
Cache-Control
ETag
ETag
Last-Modified
本节描述了 Spring Web MVC 中可用的 HTTP 缓存相关选项。
1.9.1.CacheControl
CacheControl
支持
配置与标头相关的设置,并接受为参数
在许多地方:Cache-Control
虽然 RFC 7234 描述了所有可能的
directives 的 Response,该类型采用
面向用例的方法,侧重于常见场景:Cache-Control
CacheControl
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
WebContentGenerator
还接受一个 Simpler 属性(以秒为单位定义),该属性
工作原理如下:cachePeriod
-
值不会生成响应标头。
-1
Cache-Control
-
值通过使用指令阻止缓存。
0
'Cache-Control: no-store'
-
值使用指令将给定的响应缓存几秒钟。
n > 0
n
'Cache-Control: max-age=n'
1.9.2. 控制器
控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为需要先计算资源的 or 值,然后才能进行比较
针对条件请求标头。控制器可以向 添加标头和设置,如下例所示:lastModified
ETag
ETag
Cache-Control
ResponseEntity
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {
val book = findBook(id);
val version = book.getVersion()
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book)
}
前面的示例发送一个 304 (NOT_MODIFIED) 响应,如果比较
添加到条件请求标头中,表示内容未更改。否则,将 and 标头添加到响应中。ETag
Cache-Control
你也可以在控制器中对条件请求头进行检查, 如下例所示:
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ... (1)
if (request.checkNotModified(eTag)) {
return null; (2)
}
model.addAttribute(...); (3)
return "myViewName";
}
1 | 特定于应用程序的计算。 |
2 | 响应已设置为 304 (NOT_MODIFIED) — 不再进一步处理。 |
3 | 继续进行请求处理。 |
@RequestMapping
fun myHandleMethod(request: WebRequest, model: Model): String? {
val eTag: Long = ... (1)
if (request.checkNotModified(eTag)) {
return null (2)
}
model[...] = ... (3)
return "myViewName"
}
1 | 特定于应用程序的计算。 |
2 | 响应已设置为 304 (NOT_MODIFIED) — 不再进一步处理。 |
3 | 继续进行请求处理。 |
有三种变体可用于根据值和/或值检查条件请求。对于 conditional 和 requests,您可以将响应设置为
304 (NOT_MODIFIED)。对于条件 、 、 和 ,您可以改为设置响应
设置为 412 (PRECONDITION_FAILED),以防止并发修改。eTag
lastModified
GET
HEAD
POST
PUT
DELETE
1.9.4. 过滤器ETag
您可以使用 添加从
响应内容,因此可以节省带宽,但不能节省 CPU 时间。参见浅层 ETag。ShallowEtagHeaderFilter
eTag
1.10. 查看技术
在 Spring MVC 中使用视图技术是可插拔的。无论您决定使用 Thymeleaf、Groovy 标记模板、JSP 或其他技术主要是一个问题 配置更改。本章介绍与 Spring MVC 集成的视图技术。 我们假设您已经熟悉 View Resolution。
Spring MVC 应用程序的视图位于内部信任边界内 该应用程序。视图可以访问应用程序上下文的所有 bean。如 因此,不建议在满足以下条件的应用程序中使用 Spring MVC 的模板支持 模板可由外部源编辑,因为这可能会产生安全隐患。 |
1.10.1. 百里香叶
Thymeleaf 是一个现代的服务器端 Java 模板引擎,强调自然 HTML 可以通过双击在浏览器中预览的模板,这非常有用 用于独立处理 UI 模板(例如,由设计人员),而无需 正在运行的服务器。如果您想替换 JSP,Thymeleaf 提供了最 广泛的功能集,使这种过渡更容易。Thymeleaf 正在积极地 开发和维护。有关更完整的介绍,请参阅 Thymeleaf 项目主页。
Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。
该配置涉及一些 bean 声明,例如 , , 和 。
有关更多详细信息,请参见 Thymeleaf+Spring。ServletContextTemplateResolver
SpringTemplateEngine
ThymeleafViewResolver
1.10.2. 自由标记
Apache FreeMarker 是一个模板引擎,用于生成任何 从 HTML 到电子邮件等的文本输出类型。Spring Framework 内置了 用于将 Spring MVC 与 FreeMarker 模板一起使用的集成。
View 配置
以下示例显示了如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/WEB-INF/freemarker")
}
}
以下示例显示了如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
或者,您也可以声明 bean 以完全控制所有
属性,如下例所示:FreeMarkerConfigurer
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>
您的模板需要存储在前面示例中所示的目录中。给定上述配置,如果您的控制器
返回视图名称 ,解析程序将查找模板。FreeMarkerConfigurer
welcome
/WEB-INF/freemarker/welcome.ftl
FreeMarker 配置
你可以通过设置适当的 bean 将 FreeMarker 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker 对象(由 Spring 管理)。
属性。该属性需要
对象,并且该属性需要 .以下示例演示如何使用 :Configuration
FreeMarkerConfigurer
freemarkerSettings
java.util.Properties
freemarkerVariables
java.util.Map
FreeMarkerConfigurer
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
请参阅 FreeMarker 文档,了解设置和变量的详细信息,因为它们适用于这些设置
对象。Configuration
表单处理
Spring 提供了一个标记库,用于 JSP,其中包括一个元素。此元素主要允许表单显示来自
表单支持对象,并显示
Web 或业务层。Spring 在 FreeMarker 中也支持相同的功能,
具有用于生成表单输入元素本身的附加便捷宏。<spring:bind/>
Validator
Bind 宏
文件中维护了一组标准的宏
FreeMarker,因此它们始终可用于适当配置的应用程序。spring-webmvc.jar
Spring 模板库中定义的一些宏被认为是内部的
(私有),但宏定义中不存在此类范围,因此所有宏都可见
调用代码和用户模板。以下各节仅重点介绍宏
您需要直接从模板中调用。如果您想查看宏代码
直接调用该文件,并位于包中。spring.ftl
org.springframework.web.servlet.view.freemarker
简单绑定
在基于 FreeMarker 模板的 HTML 表单中,这些模板充当 Spring MVC 的表单视图
controller 中,您可以使用类似于以下示例的代码绑定到字段值和
显示每个输入字段的错误消息的方式与 JSP 等效项类似。这
以下示例显示了一个视图:personForm
<!-- FreeMarker macros have to be imported into a namespace.
We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>
<@spring.bind>
需要一个 'path' 参数,该参数由命令的名称组成
对象(它是 'command',除非您在控制器配置中更改了它)
按句点和要绑定到的 Command 对象上的字段名称。你
也可以使用嵌套字段,如 .宏假定
由 中的参数指定的默认 HTML 转义行为。command.address.street
bind
ServletContext
defaultHtmlEscape
web.xml
称为 (Ar) 的宏的另一种形式采用第二个参数
显式指定是否应在状态错误中使用 HTML 转义
消息或值。您可以将其设置为 或 根据需要。附加表格
处理宏简化了 HTML 转义的使用,您应该使用这些宏
尽可能。它们将在下一节中解释。<@spring.bindEscaped>
true
false
输入宏
FreeMarker 的其他便捷宏简化了装订和表单生成 (包括验证错误显示)。永远没有必要使用这些宏来 生成表单输入字段,并且您可以使用简单的 HTML 或直接将它们混合和匹配 调用我们之前突出显示的 Spring 绑定宏。
下表显示了 FreeMarker 模板 (FTL) 定义 以及每个参数的参数列表:
宏 | FTL 定义 |
---|---|
|
<@spring.message code/> |
|
<@spring.message文本代码、文本/> |
|
<@spring.url relativeUrl/> |
|
<@spring.form输入路径、属性、fieldType/> |
|
<@spring.formHiddenInput 路径,attributes/> |
|
<@spring.formPasswordInput 路径、attributes/> |
|
<@spring.formText区域路径、attributes/> |
|
<@spring.formSingleSelect 路径、选项、属性/> |
|
<@spring.formMultiSelect path, options, attributes/> |
|
<@spring.formRadioButtons 路径、选项分隔符、attributes/> |
|
<@spring.formCheckboxes 路径、选项、分隔符、attributes/> |
|
<@spring.formCheckbox 路径,attributes/> |
|
<@spring.showErrors 分隔符 classOrStyle/> |
在 FreeMarker 模板中,实际上并不是
required,因为您可以使用 normal 宏,指定 or 作为参数的值。formHiddenInput formPasswordInput formInput hidden password fieldType |
上述任何宏的参数都具有一致的含义:
-
path
:要绑定到的字段的名称(即 “command.name”) -
options
:A 输入中可从中选择的所有可用值 田。映射的键表示从表单中 POST 回来的值 并绑定到 Command 对象。针对键存储的 Map 对象是标签 在表单上显示给用户,并且可能与相应的值不同 通过表单发回。通常,此类映射由 控制器。您可以使用任何实现,具体取决于所需的行为。 对于严格排序的映射,您可以将 a(例如 )与 suitable 和,对于应该在插入中返回值的任意 Map order 中,请使用 a 或 a from 。Map
Map
SortedMap
TreeMap
Comparator
LinkedHashMap
LinkedMap
commons-collections
-
separator
:其中多个选项可用作隐蔽元素(单选按钮 或复选框)中,则用于分隔列表中每个字符的字符序列 (例如 )。<br>
-
attributes
:要包含在其中的任意标签或文本的附加字符串 HTML 标签本身。此字符串由宏逐字面回显。例如,在字段中,您可以提供属性(例如 'rows=“5” cols=“60”'),或者您 可以传递样式信息,例如 'style=“border:1px solid silver”'。textarea
-
classOrStyle
:对于宏,包装每个错误的元素使用的 CSS 类的名称。如果未提供任何信息(或者值为 empty),则错误将包装在 tags 中。showErrors
span
<b></b>
以下各节概述了宏的示例。
该宏采用参数 () 和一个附加参数(在下一个示例中为空)。宏以及所有其他形式
generation 宏,对 path 参数执行隐式 Spring 绑定。绑定
在发生新绑定之前保持有效,因此宏不需要传递
path 参数 — 它对上次为其创建绑定的字段进行操作。formInput
path
command.name
attributes
showErrors
宏采用 separator 参数(用于
分隔给定字段上的多个错误),并且还接受第二个参数 — this
time、类名或 style 属性。请注意,FreeMarker 可以指定 default
attributes 参数的值。以下示例演示如何使用 and 宏:showErrors
formInput
showErrors
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>
下一个示例显示了表单片段的输出,生成 name 字段并显示 提交表单后出现验证错误,但字段中没有值。验证 通过 Spring 的 Validation 框架发生。
生成的 HTML 类似于以下示例:
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>
宏的工作方式与宏相同,并接受相同的
parameter 列表。通常,第二个参数 () 用于传递样式
information 或 and 属性。formTextarea
formInput
attributes
rows
cols
textarea
您可以使用四个选择字段宏在 您的 HTML 表单:
-
formSingleSelect
-
formMultiSelect
-
formRadioButtons
-
formCheckboxes
四个宏中的每一个都接受一个 of 选项,其中包含表单的值
field 和与该值对应的标签。值和标签可以是
相同。Map
下一个示例是 FTL 中的单选按钮。表单支持对象指定默认值 值为 'London' ,因此无需验证。当表单为 呈现时,可供选择的整个城市列表将作为参考数据提供在 model 替换为 'cityMap' 的下面的清单显示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
前面的清单呈现了一行单选按钮,每个单选按钮对应 中的每个值,并使用
separator 的 。没有提供其他属性(宏的最后一个参数是
missing)。对映射中的每个键值对使用相同的方法。地图的
键是表单实际作为请求参数提交的内容。map 值是
标签。在前面的示例中,给定三个知名城市的列表
和表单支持对象中的默认值,则 HTML 类似于以下内容:cityMap
""
cityMap
String
POST
Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>
如果您的应用程序希望通过内部代码处理城市(例如),则可以创建 代码,如下例所示:
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");
Map<String, Object> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
protected fun referenceData(request: HttpServletRequest): Map<String, *> {
val cityMap = linkedMapOf(
"LDN" to "London",
"PRS" to "Paris",
"NYC" to "New York"
)
return hashMapOf("cityMap" to cityMap)
}
现在,该代码会生成输出,其中 radio 值是相关代码,但 用户仍然会看到对用户更友好的城市名称,如下所示:
Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>
HTML 转义
前面描述的表单宏的默认用法会导致 HTML 元素为 HTML 4.01
compliant 的 v,并且使用文件中定义的 HTML 转义的默认值,如
由 Spring 的 bind 支持使用。使元素符合 XHTML 或覆盖
默认的 HTML 转义值,您可以在模板中指定两个变量(或在
您的模型,它们对您的模板可见)。指定
它们在模板中是可以稍后在
模板处理为表单中的不同字段提供不同的行为。web.xml
要切换到标记的 XHTML 合规性,请为
model 或上下文变量,如下例所示:true
xhtmlCompliant
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理此指令后,Spring 宏生成的任何元素现在都是 XHTML 顺从的。
以类似的方式,您可以为每个字段指定 HTML 转义,如下例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
1.10.3. Groovy 标记
Groovy 标记模板引擎主要旨在生成类似 XML 的标记(XML、XHTML、HTML5 等),但您可以 使用它来生成任何基于文本的内容。Spring Framework 有一个内置的 将 Spring MVC 与 Groovy Markup 结合使用的集成。
Groovy Markup Template 引擎需要 Groovy 2.3.1+。 |
配置
下面的示例演示如何配置 Groovy 标记模板引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
以下示例显示了如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
1.10.4. 脚本视图
Spring Framework 有一个内置的集成,用于将 Spring MVC 与任何 可以在 JSR-223 Java 脚本引擎上运行的模板库。我们测试了以下内容 在不同脚本引擎上模板化库:
脚本库 | 脚本引擎 |
---|---|
集成任何其他脚本引擎的基本规则是它必须实现 and 接口。ScriptEngine Invocable |
要求
您需要在 Classpath 上具有脚本引擎,其详细信息因脚本引擎而异:
-
Nashorn JavaScript 引擎随 Java 8+ 的强烈建议使用可用的最新更新版本。
-
应该将 JRuby 添加为 Ruby 支持的依赖项。
-
应将 Jython 添加为 Python 支持的依赖项。
-
org.jetbrains.kotlin:kotlin-script-util
dependency 并添加包含一行的文件以支持 Kotlin 脚本。有关更多详细信息,请参阅此示例。META-INF/services/javax.script.ScriptEngineFactory
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
您需要有脚本模板库。对 JavaScript 执行此操作的一种方法是 通过 WebJars 进行。
脚本模板
您可以声明一个 bean 来指定要使用的脚本引擎,
要加载的脚本文件、要调用的函数来渲染模板,等等。
以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:ScriptTemplateConfigurer
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
下面的示例显示了 XML 中的相同排列:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>
对于 Java 和 XML 配置,控制器看起来没有什么不同,如下例所示:
@Controller
public class SampleController {
@GetMapping("/sample")
public String test(Model model) {
model.addAttribute("title", "Sample title");
model.addAttribute("body", "Sample body");
return "template";
}
}
@Controller
class SampleController {
@GetMapping("/sample")
fun test(model: Model): String {
model["title"] = "Sample title"
model["body"] = "Sample body"
return "template"
}
}
以下示例显示了 Mustache 模板:
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
使用以下参数调用 render 函数:
-
String template
:模板内容 -
Map model
:视图模型 -
RenderingContext renderingContext
:RenderingContext
,用于访问应用程序上下文、区域设置、模板加载程序和 URL(自 5.0 起)
Mustache.render()
与此签名本机兼容,因此您可以直接调用它。
如果您的模板技术需要一些自定义,则可以提供一个脚本,该脚本 实现自定义 render 函数。例如,Handlerbars 需要在使用模板之前编译模板,并且需要一个 polyfill 来模拟一些 服务器端脚本引擎中不可用的浏览器工具。
以下示例显示了如何执行此操作:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
使用非线程安全时,需要将属性设置为
具有非并发性模板库的脚本引擎,例如 Handlebars 或
React 在 Nashorn 上运行。在这种情况下,由于此错误,需要 Java SE 8 Update 60,但通常是
建议在任何情况下使用最新的 Java SE 补丁版本。sharedEngine false |
polyfill.js
仅定义 Handlebars 正常运行所需的对象,如下所示:window
var window = {};
此基本实现在使用模板之前对其进行编译。生产就绪型
implementation 还应存储任何重复使用的缓存模板或预编译的模板。
您可以在脚本端执行此操作(并处理您需要的任何自定义 — 管理
模板引擎配置)。以下示例显示了如何执行此操作:render.js
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.10.5. JSP 和 JSTL
Spring Framework 具有用于将 Spring MVC 与 JSP 和 JSTL 一起使用的内置集成。
View 解析程序
使用 JSP 进行开发时,通常会声明一个 Bean。InternalResourceViewResolver
InternalResourceViewResolver
可用于分派到任何 Servlet 资源,但在
特别是对于 JSP 来说。作为最佳实践,我们强烈建议将 JSP 文件放在
目录下的目录,因此客户端无法直接访问。'WEB-INF'
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
Spring 的 JSP 标记库
Spring 提供请求参数到命令对象的数据绑定,如 前面的章节。促进 JSP 页面的开发 data binding 功能,Spring 提供了一些标签,使事情变得更加容易。都 Spring 标签具有 HTML 转义功能,用于启用或禁用字符转义。
标记库描述符 (TLD) 包含在 .
有关各个标记的全面参考,请浏览 API 参考或查看标记库描述。spring.tld
spring-webmvc.jar
Spring 的表单标记库
从版本 2.0 开始, Spring 提供了一组全面的数据绑定感知标签,用于 在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标签都支持 其对应的 HTML 标签对应的属性集,使标签 熟悉且直观易用。标记生成的 HTML 符合 HTML 4.01/XHTML 1.0 标准。
与其他表单/输入标记库不同, Spring 的表单标记库与 Spring Web MVC,为标签提供对命令对象和引用数据的 控制器处理。正如我们在以下示例中所示,表单标记使 JSP 更易于开发、阅读和维护。
我们将浏览 form 标记,并查看如何使用每个标记的示例。我们有 包含生成的 HTML 片段,其中某些标记需要进一步注释。
配置
表单标记库捆绑在 中。库描述符为
叫。spring-webmvc.jar
spring-form.tld
要使用此库中的标记,请将以下指令添加到 JSP 的顶部 页:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中 是要用于此库中的标签的标签名称前缀。form
表单标签
此标签呈现一个 HTML“form”元素,并公开一个指向内部标签的绑定路径
捆绑。它将 command 对象放在 中,以便 command 对象可以
通过 inner 标签访问。此库中的所有其他标签都是该标签的嵌套标签。PageContext
form
假设我们有一个名为 .它是一个具有属性
例如 和 。我们可以将其用作
表单控制器,它返回 .以下示例显示了可以
肖:User
firstName
lastName
form.jsp
form.jsp
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
和 值是从放置在
由 Page 控制器。继续阅读以查看更复杂的示例
内部标记如何与标记一起使用。firstName
lastName
PageContext
form
下面的清单显示了生成的 HTML,它看起来像一个标准表单:
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value="Harry"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value="Potter"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
前面的 JSP 假定表单支持对象的变量名称为 。如果您已将 form-backing 对象以其他名称放入模型中
(绝对是最佳实践),您可以将表单绑定到命名变量,因为
以下示例显示:command
<form:form modelAttribute="user">
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
标签input
此标记默认呈现具有 bound 值的 HTML 元素。
有关此标记的示例,请参阅 Form 标记。您还可以使用
特定于 HTML5 的类型,例如 、 、 、 等。input
type='text'
email
tel
date
标签checkbox
此标记呈现一个 HTML 标记,并将 设置为 。input
type
checkbox
假设我们有偏好,例如时事通讯订阅和
爱好。以下示例显示了该类:User
Preferences
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
class Preferences(
var receiveNewsletter: Boolean,
var interests: StringArray,
var favouriteWord: String
)
相应的结果可能类似于以下内容:form.jsp
<form:form>
<table>
<tr>
<td>Subscribe to newsletter?:</td>
<%-- Approach 1: Property is of type java.lang.Boolean --%>
<td><form:checkbox path="preferences.receiveNewsletter"/></td>
</tr>
<tr>
<td>Interests:</td>
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
<td>
Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
</td>
</tr>
<tr>
<td>Favourite Word:</td>
<%-- Approach 3: Property is of type java.lang.Object --%>
<td>
Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
</td>
</tr>
</table>
</form:form>
有三种方法可用于标记,它们应该可以满足您的所有复选框需求。checkbox
-
方法一:当绑定值为 类型 时,将标记为绑定值为 。该属性对应于 value 属性的 resolved 值。
java.lang.Boolean
input(checkbox)
checked
true
value
setValue(Object)
-
方法二:当绑定值为 或 类型时,将标记为配置值为 存在于绑定 .
array
java.util.Collection
input(checkbox)
checked
setValue(Object)
Collection
-
方法三:对于任何其他绑定值类型,将标记为 configured 等于绑定值。
input(checkbox)
checked
setValue(Object)
请注意,无论采用哪种方法,都会生成相同的 HTML 结构。以下内容 HTML 代码段定义了一些复选框:
<tr>
<td>Interests:</td>
<td>
Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
<input type="hidden" value="1" name="_preferences.interests"/>
</td>
</tr>
您可能不希望在每个复选框后看到额外的隐藏字段。
如果未选中 HTML 页面中的复选框,则其值不会发送到
server 作为 HTTP 请求参数的一部分,因此我们需要一个
HTML 中此怪癖的解决方法,以便 Spring 表单数据绑定正常工作。该标签遵循现有的 Spring 约定,即包含一个隐藏的参数
为每个复选框添加下划线 () 前缀。通过这样做,您可以有效地
告诉 Spring “复选框在表单中可见,我希望我的对象
无论如何,表单数据都会绑定以反映复选框的状态。checkbox
_
标签checkboxes
此标记呈现多个 HTML 标记,并将 设置为 。input
type
checkbox
本节基于上一个 tag 部分中的示例。有时,您更喜欢
不必在 JSP 页中列出所有可能的爱好。您宁愿提供
一个可用选项的列表,并将其传递给标签。那就是
标签的用途。您可以传入包含
属性中的可用选项。通常,绑定属性是
集合,以便它可以保存用户选择的多个值。以下示例
显示了使用此标记的 JSP:checkbox
checkboxes
Array
List
Map
items
<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>
此示例假定 is a available as a model 属性
,其中包含要从中选择的值的字符串。如果您使用 ,
映射条目键用作值,映射条目的值用作
要显示的标签。您还可以使用自定义对象,您可以在其中提供
value 的属性名称 (using using ) 和标签的 label (使用 )。interestList
List
Map
itemValue
itemLabel
标签radiobutton
此标记呈现一个 HTML 元素,并将 设置为 .input
type
radio
典型的使用模式涉及绑定到同一属性的多个标记实例 但具有不同的值,如下例所示:
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
标签radiobuttons
此标记呈现多个 HTML 元素,并将 设置为 .input
type
radio
与 checkboxes
标签一样,您可能希望
将 available options 作为运行时变量传入。对于此用法,您可以使用 tag.您传入包含
属性中的可用选项。如果使用 ,则映射入口键为
used as the value 和 map entry 的值用作要显示的标签。
您还可以使用自定义对象,您可以在其中为值提供属性名称
by using 和 label by using ,如下例所示:radiobuttons
Array
List
Map
items
Map
itemValue
itemLabel
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
标签password
此标签呈现 type 设置为 to 的 HTML 标签,并带有 bound 值。input
password
<tr>
<td>Password:</td>
<td>
<form:password path="password"/>
</td>
</tr>
请注意,默认情况下,不会显示 password 值。如果您确实希望使用
password 值,您可以将该属性的值设置为 ,如下例所示:showPassword
true
<tr>
<td>Password:</td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
</td>
</tr>
标签select
此标签呈现 HTML 'select' 元素。它支持将数据绑定到选定的
选项以及 nested 和 tags 的使用。option
options
假设 a 有一个技能列表。相应的 HTML 可能如下所示:User
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果技能是草药学,则“技能”行的 HTML 源可以是
如下:User’s
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
标签option
此标记呈现 HTML 元素。它根据 bound 设置
价值。以下 HTML 显示了它的典型输出:option
selected
<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>
如果学院位于格兰芬多,则 'House' 行的 HTML 源将为
如下:User’s
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> (1)
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
1 | 请注意添加的属性。selected |
标签options
此标签呈现 HTML 元素列表。它设置属性
基于 Bound 值。以下 HTML 显示了它的典型输出:option
selected
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>
如果居住在英国,则 'Country' 行的 HTML 源如下所示:User
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> (1)
<option value="US">United States</option>
</select>
</td>
</tr>
1 | 请注意添加的属性。selected |
如前面的示例所示,标记与标记
生成相同的标准 HTML,但允许您在
仅用于显示(属于它的位置)的 JSP,例如
示例:“-- 请选择”。option
options
该属性通常填充 item 对象的集合或数组。 并引用这些 item 对象的 bean 属性,如果
指定。否则,item 对象本身将转换为字符串。或者
您可以指定 OF 项,在这种情况下,映射键将解释为 option
values 和 map 值对应于选项标签。如果 或 (或两者)
恰好也被指定,则 item 值属性适用于 map 键,并且
item label 属性适用于 map 值。items
itemValue
itemLabel
Map
itemValue
itemLabel
标签textarea
此标记呈现 HTML 元素。以下 HTML 显示了它的典型输出:textarea
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
标签hidden
此标签呈现一个 HTML 标签,其 set to 具有绑定值。提交
未绑定的隐藏值,请使用 HTML 标记并将 设置为 。
以下 HTML 显示了它的典型输出:input
type
hidden
input
type
hidden
<form:hidden path="house"/>
如果我们选择将值作为隐藏值提交,则 HTML 将如下所示:house
<input name="house" type="hidden" value="Gryffindor"/>
标签errors
此标签在 HTML 元素中呈现字段错误。它提供对错误的访问
在您的控制器中创建的,或者是由与
您的控制器。span
假设我们希望在提交表单后显示 and 字段的所有错误消息。我们有一个用于类实例的验证器
调用,如下例所示:firstName
lastName
User
UserValidator
public class UserValidator implements Validator {
public boolean supports(Class candidate) {
return User.class.isAssignableFrom(candidate);
}
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
}
}
class UserValidator : Validator {
override fun supports(candidate: Class<*>): Boolean {
return User::class.java.isAssignableFrom(candidate)
}
override fun validate(obj: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")
}
}
这可能如下所示:form.jsp
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<%-- Show errors for firstName field --%>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<%-- Show errors for lastName field --%>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
如果我们提交的表单的 and 字段中有空值,
HTML 将如下所示:firstName
lastName
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<%-- Associated errors to firstName field displayed --%>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<%-- Associated errors to lastName field displayed --%>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
如果我们想显示给定页面的整个错误列表怎么办?下一个例子
显示该标签还支持一些基本的通配符功能。errors
-
path="*"
:显示所有错误。 -
path="lastName"
:显示与字段关联的所有错误。lastName
-
如果省略,则仅显示对象错误。
path
以下示例在页面顶部显示错误列表,后跟 字段旁边的特定于字段的错误:
<form:form>
<form:errors path="*" cssClass="errorBox"/>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
HTML 将如下所示:
<form method="POST">
<span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
标记库描述符 (TLD) 包含在 .
有关各个标记的全面参考,请浏览 API 参考或查看标记库描述。spring-form.tld
spring-webmvc.jar
HTTP 方法转换
REST 的一个关键原则是使用“统一接口”。这意味着所有
可以使用相同的四种 HTTP 方法操作资源 (URL):GET、PUT、POST、
和 DELETE 的 DELETE 命令。对于每种方法,HTTP 规范定义了确切的语义。为
例如,GET 应该始终是一个安全的操作,这意味着它没有副作用,
PUT 或 DELETE 应该是幂等的,这意味着您可以重复这些操作
一遍又一遍,但最终结果应该是相同的。虽然 HTTP 定义了这些
四种方法,HTML 只支持两种:GET 和 POST。幸运的是,有两种可能
解决方法:您可以使用 JavaScript 执行 PUT 或 DELETE,也可以执行 POST
使用 “real” 方法作为附加参数(建模为
HTML 表单)。Spring 使用了后一种技巧。这
filter 是一个普通的 Servlet 过滤器,因此,它可以与任何
Web 框架(不仅仅是 Spring MVC)。将此过滤器添加到您的 web.xml,然后 POST
替换为 hidden 参数的 HTTP 方法
请求。HiddenHttpMethodFilter
method
为了支持 HTTP 方法转换,更新了 Spring MVC 表单标记以支持设置 HTTP 方法。例如,以下代码片段来自 Pet Clinic 示例:
<form:form method="delete">
<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>
前面的示例执行 HTTP POST,其中隐藏了 “真正的” DELETE 方法
请求参数。它由 拾取,它在
web.xml,如下例所示:HiddenHttpMethodFilter
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
以下示例显示了相应的方法:@Controller
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
this.clinic.deletePet(petId);
return "redirect:/owners/" + ownerId;
}
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
clinic.deletePet(petId)
return "redirect:/owners/$ownerId"
}
1.10.6. 瓦片
您可以在 Web 中集成 Tiles - 就像任何其他视图技术一样 使用 Spring 的应用程序。本节以广泛的方式描述了如何执行此操作。
本节重点介绍 Spring 对包中 Tiles 版本 3 的支持。org.springframework.web.servlet.view.tiles3 |
依赖
为了能够使用 Tiles,您必须添加对 Tiles 版本 3.0.1 或更高版本的依赖项 及其对项目的传递依赖项。
配置
为了能够使用 Tiles,您必须使用包含定义的文件对其进行配置
(有关定义和其他 Tiles 概念的基本信息,请参阅 https://tiles.apache.org)。在 Spring 中,这是通过使用 .
以下示例配置显示了如何执行此操作:TilesConfigurer
ApplicationContext
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
</bean>
前面的示例定义了 5 个包含定义的文件。文件全部
位于目录中。在初始化 时,
加载文件,并初始化 Definitions Factory。之后
完成后,定义文件中包含的 Tiles 可以用作
Spring Web 应用程序。为了能够使用这些视图,您必须具有与 Spring 中任何其他视图技术一样:通常是一个方便的.WEB-INF/defs
WebApplicationContext
ViewResolver
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>
在前面的配置中,用于具有 locale 的请求,
,并默认使用。tiles_fr_FR.xml
fr_FR
tiles.xml
由于下划线用于表示区域设置,因此我们建议不要使用 否则,它们将显示在 Tiles 定义的文件名中。 |
UrlBasedViewResolver
实例化它必须为每个视图实例化 given
解决。以下 Bean 定义了一个 :UrlBasedViewResolver
viewClass
UrlBasedViewResolver
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
</bean>
SimpleSpringPreparerFactory
和SpringBeanPreparerFactory
作为一项高级功能, Spring 还支持两个特殊的 Tiles 实现。有关如何在 Tiles 定义文件中使用引用的详细信息,请参阅 Tiles 文档。PreparerFactory
ViewPreparer
您可以指定根据
指定的 preparer 类,应用 Spring 的容器回调以及应用
配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围的注释配置具有
被激活后,将自动检测类中的 Comments,并且
应用的。请注意,这需要 Tiles 定义文件中的 preparer 类,因为
默认值是。SimpleSpringPreparerFactory
ViewPreparer
ViewPreparer
PreparerFactory
您可以指定对指定的编制者名称进行操作(而不是
类),从 DispatcherServlet 的
应用程序上下文。完整的 bean 创建过程由 Spring 控制
application 上下文,允许使用显式依赖注入
configuration、作用域 bean 等。请注意,您需要定义一个 Spring bean 定义
对于每个编制者名称(在 Tiles 定义中使用)。以下示例显示了
如何在 bean 上定义属性:SpringBeanPreparerFactory
SpringBeanPreparerFactory
TilesConfigurer
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
<!-- resolving preparer names as Spring bean definition names -->
<property name="preparerFactoryClass"
value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>
</bean>
1.10.7. RSS 和 Atom
和 inherit 自基类 and 分别用于提供 Atom 和 RSS Feed 视图。他们
基于 ROME 项目,位于
包。AbstractAtomFeedView
AbstractRssFeedView
AbstractFeedView
org.springframework.web.servlet.view.feed
AbstractAtomFeedView
需要您实现 method 和
(可选)覆盖 method(默认实现为
empty) 的 Null S以下示例显示了如何执行此操作:buildFeedEntries()
buildFeedMetadata()
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentAtomView : AbstractAtomFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Feed, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedEntries(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Entry> {
// implementation omitted
}
}
类似的要求也适用于 implementation ,如下例所示:AbstractRssFeedView
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentRssView : AbstractRssFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Channel, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedItems(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Item> {
// implementation omitted
}
}
和 方法传入 HTTP 请求,以防
您需要访问 Locale。HTTP 响应仅针对
Cookie 或其他 HTTP 标头。源会自动写入响应
object 返回。buildFeedItems()
buildFeedEntries()
有关创建 Atom 视图的示例,请参阅 Alef Arends 的 Spring Team 博客文章。
1.10.8. PDF 和 Excel
Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。 本节介绍如何使用这些功能。
文档视图简介
HTML 页面并不总是用户查看模型输出的最佳方式。 Spring 使生成 PDF 文档或 Excel 电子表格变得简单 从模型数据动态获取。文档是视图,并从 服务器,以(希望)使客户端 PC 能够运行其 电子表格或 PDF 查看器应用程序作为响应。
为了使用 Excel 视图,您需要将 Apache POI 库添加到您的 Classpath 中。 对于 PDF 生成,您需要添加(最好)OpenPDF 库。
您应该使用最新版本的基础文档生成库 如果可能的话。特别是,我们强烈推荐 OpenPDF(例如,OpenPDF 1.2.12) 而不是过时的原始 iText 2.1.7,因为 OpenPDF 得到了积极维护并且 修复了不受信任的 PDF 内容的重要漏洞。 |
PDF 视图
单词列表的简单 PDF 视图可以扩展和实现该方法,如下例所示:org.springframework.web.servlet.view.document.AbstractPdfView
buildPdfDocument()
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
class PdfWordList : AbstractPdfView() {
override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
request: HttpServletRequest, response: HttpServletResponse) {
val words = model["wordList"] as List<String>
for (word in words) {
doc.add(Paragraph(word))
}
}
}
控制器可以从外部视图定义返回此类视图
(按名称引用它)或作为处理程序方法中的实例。View
1.10.9. Jackson
Spring 提供对 Jackson JSON 库的支持。
基于 Jackson 的 JSON MVC 视图
使用 Jackson 库的 cookie 来呈现响应
content 作为 JSON 格式。默认情况下,模型映射
特定于框架的类)编码为 JSON。对于
map 需要过滤,则可以指定一组特定的模型属性进行编码
通过使用 Property 来获取。您还可以使用该属性直接提取和序列化单键模型中的值,而不是
than 作为模型属性的映射。MappingJackson2JsonView
ObjectMapper
modelKeys
extractValueFromSingleKeyModel
您可以根据需要使用 Jackson 提供的
附注。当您需要进一步控制时,您可以通过属性注入自定义,以用于需要提供自定义 JSON 的情况
特定类型的序列化器和反序列化器。ObjectMapper
ObjectMapper
基于 Jackson 的 XML 视图
MappingJackson2XmlView
使用 Jackson XML 扩展将响应内容呈现为 XML。如果模型包含多个条目,则应
使用 Bean 属性显式设置要序列化的对象。如果
model 包含一个条目,它会自动序列化。XmlMapper
modelKey
您可以根据需要使用 JAXB 或 Jackson 提供的 XML 映射来自定义 XML 映射
附注。当您需要进一步的控制时,您可以通过属性注入自定义,对于自定义 XML
您需要为特定类型提供序列化器和反序列化器。XmlMapper
ObjectMapper
1.10.10. XML 封送
它使用 XML(在包中定义)将响应内容呈现为 XML。您可以将对象显式设置为
使用实例的 bean 属性进行封送。或者
该视图迭代所有模型属性并封送支持的第一个类型
由 .有关包中功能的更多信息,请参阅使用 O/X 映射器封送 XML。MarshallingView
Marshaller
org.springframework.oxm
MarshallingView
modelKey
Marshaller
org.springframework.oxm
1.10.11. XSLT 视图
XSLT 是一种 XML 转换语言,在 Web 中作为一种流行的视图技术 应用。如果您的应用程序 自然会处理 XML,或者如果您的模型可以很容易地转换为 XML。以下内容 部分介绍如何将 XML 文档生成为模型数据,并使用 Spring Web MVC 应用程序中的 XSLT 进行验证。
这个例子是一个普通的 Spring 应用程序,它在 the 中创建了一个单词列表并将它们添加到模型映射中。将返回 map 以及视图
XSLT 视图的名称。有关 Spring Web MVC 接口的详细信息,请参见带注释的控制器。XSLT 控制器将单词列表转换为简单的 XML
文档已准备好进行转换。Controller
Controller
豆
配置是简单 Spring Web 应用程序的标准配置:MVC 配置
必须定义 bean 和常规 MVC 注释配置。
以下示例显示了如何执行此操作:XsltViewResolver
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun xsltViewResolver() = XsltViewResolver().apply {
setPrefix("/WEB-INF/xsl/")
setSuffix(".xslt")
}
}
控制器
我们还需要一个封装我们的单词生成逻辑的 Controller。
控制器逻辑封装在一个类中,其中
handler 方法定义如下:@Controller
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
import org.springframework.ui.set
@Controller
class XsltController {
@RequestMapping("/")
fun home(model: Model): String {
val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val root = document.createElement("wordList")
val words = listOf("Hello", "Spring", "Framework")
for (word in words) {
val wordNode = document.createElement("word")
val textNode = document.createTextNode(word)
wordNode.appendChild(textNode)
root.appendChild(wordNode)
}
model["wordList"] = root
return "home"
}
}
到目前为止,我们只创建了一个 DOM 文档并将其添加到 Model 映射中。请注意,您
还可以将 XML 文件加载为 a 并使用它来代替自定义 DOM 文档。Resource
有一些软件包可以自动 “domify” 一个对象图,但是,在 Spring 中,您可以完全灵活地创建 DOM 从您的模型中以您选择的任何方式。这会阻止 XML 播放的转换 在模型数据的结构中占有太大的比重,这在使用工具时很危险 来管理 DOMification 过程。
转型
最后,解析 “home” XSLT 模板文件并合并
DOM 文档添加到其中来生成我们的视图。如配置中所示,XSLT 模板位于目录下的文件中
,并以文件扩展名结尾。XsltViewResolver
XsltViewResolver
war
WEB-INF/xsl
xslt
以下示例显示了 XSLT 转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
前面的转换呈现为以下 HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
1.11. MVC 配置
MVC Java 配置和 MVC XML 命名空间提供默认配置 适用于大多数应用程序,并提供配置 API 对其进行自定义。
对于更高级的自定义(在配置 API 中不可用), 请参见高级 Java 配置和高级 XML 配置。
您不需要了解 MVC Java 配置创建的基础 bean 和 MVC 命名空间。如果要了解更多信息,请参阅特殊 Bean 类型和 Web MVC 配置。
1.11.1. 启用 MVC 配置
在 Java 配置中,您可以使用注释来启用 MVC
配置,如下例所示:@EnableWebMvc
@Configuration
@EnableWebMvc
public class WebConfig {
}
@Configuration
@EnableWebMvc
class WebConfig
在 XML 配置中,可以使用 element 来启用 MVC
配置,如下例所示:<mvc:annotation-driven>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven/>
</beans>
前面的示例注册了许多 Spring MVC 基础结构 bean 并适应依赖项 在 Classpath 上可用(例如,JSON、XML 等的有效负载转换器)。
1.11.2. MVC 配置 API
在 Java 配置中,您可以实现接口,如
以下示例显示:WebMvcConfigurer
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
// Implement configuration methods...
}
在 XML 中,您可以检查 的属性和子元素。您可以
查看 Spring MVC XML 模式或使用
IDE 的代码补全功能,用于发现哪些属性和
子元素可用。<mvc:annotation-driven/>
1.11.3. 类型转换
默认情况下,会安装各种数字和日期类型的格式化程序,以及支持
用于自定义 via 和 on 字段。@NumberFormat
@DateTimeFormat
要在 Java 配置中注册自定义格式化程序和转换器,请使用以下内容:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
// ...
}
}
要在 XML 配置中执行相同的操作,请使用以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<bean class="org.example.MyConverter"/>
</set>
</property>
<property name="formatters">
<set>
<bean class="org.example.MyFormatter"/>
<bean class="org.example.MyAnnotationFormatterFactory"/>
</set>
</property>
<property name="formatterRegistrars">
<set>
<bean class="org.example.MyFormatterRegistrar"/>
</set>
</property>
</bean>
</beans>
默认情况下,Spring MVC 在解析和格式化日期时会考虑请求 Locale 值。这适用于日期表示为带有 “input” 形式的字符串的表单 领域。但是,对于“日期”和“时间”表单字段,浏览器使用定义的固定格式 在 HTML 规范中。对于此类情况,可以按如下方式自定义日期和时间格式:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
请参阅 FormatterRegistrar SPI 和有关何时使用
FormatterRegistrar 实现。FormattingConversionServiceFactoryBean |
1.11.4. 验证
默认情况下,如果存在 Bean Validation
在 Classpath(例如,Hibernate Validator)上,是
注册为全局 Validator,以便与控制器方法参数一起使用。LocalValidatorFactoryBean
@Valid
Validated
在 Java 配置中,您可以自定义全局实例,因为
以下示例显示:Validator
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun getValidator(): Validator {
// ...
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven validator="globalValidator"/>
</beans>
请注意,你也可以在本地注册 implementations,如下所示
示例显示:Validator
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
@Controller
class MyController {
@InitBinder
protected fun initBinder(binder: WebDataBinder) {
binder.addValidators(FooValidator())
}
}
如果你需要在某个地方注入一个 Bean,创建一个 bean 并
将其标记为 ,以避免与 MVC 配置中声明的冲突。LocalValidatorFactoryBean @Primary |
1.11.5. 拦截器
在 Java 配置中,您可以注册拦截器以应用于传入请求,例如 以下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(LocaleChangeInterceptor())
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
映射拦截器并不适合作为安全层,因为可能存在 对于与 annotated controller path matching不匹配,也可以匹配 trailing 斜杠和路径扩展,以及其他路径匹配选项。多 的选项已被弃用,但不匹配的可能性仍然存在。 通常,我们建议使用 Spring Security,它包括一个专用的 MvcRequestMatcher 以与 Spring MVC 路径匹配保持一致,并且还有一个安全防火墙可以阻止许多 URL 路径中不需要的字符。 |
1.11.6. 内容类型
您可以配置 Spring MVC 如何从请求中确定请求的媒体类型
(例如,标头、URL 路径扩展名、查询参数等)。Accept
默认情况下,仅选中标头。Accept
如果必须使用基于 URL 的内容类型解析,请考虑使用 query 参数 策略而不是路径扩展。请参阅 Suffix Match 和 Suffix Match 和 RFD 以了解 更多细节。
在 Java 配置中,您可以自定义请求的内容类型解析,因为 以下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON)
configurer.mediaType("xml", MediaType.APPLICATION_XML)
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes">
<value>
json=application/json
xml=application/xml
</value>
</property>
</bean>
1.11.7. 消息转换器
你可以通过覆盖configureMessageConverters()(
替换 Spring MVC 创建的默认转换器)或覆盖extendMessageConverters()(
自定义默认转换器或向默认转换器添加其他转换器)来自定义 Java 配置。HttpMessageConverter
以下示例使用自定义而不是默认转换器添加 XML 和 Jackson JSON 转换器:ObjectMapper
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
@Configuration
@EnableWebMvc
class WebConfiguration : WebMvcConfigurer {
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
val builder = Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(ParameterNamesModule())
converters.add(MappingJackson2HttpMessageConverter(builder.build()))
converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))
在前面的示例中,Jackson2ObjectMapperBuilder
用于为两者创建一个通用配置,并在启用缩进的情况下创建自定义日期格式
以及 jackson-module-parameter-names
的注册,
这增加了对访问参数名称的支持(Java 8 中新增的功能)。MappingJackson2HttpMessageConverter
MappingJackson2XmlHttpMessageConverter
此构建器自定义 Jackson 的默认属性,如下所示:
如果在 Classpath 中检测到以下众所周知的模块,它还会自动注册它们:
-
jackson-datatype-joda:支持 Joda-Time 类型。
-
jackson-datatype-jsr310:支持 Java 8 日期和时间 API 类型。
-
jackson-datatype-jdk8:支持其他 Java 8 类型,例如 .
Optional
-
jackson-module-kotlin
:支持 Kotlin 类和数据类。
使用 Jackson XML 支持启用缩进除了 jackson-dataformat-xml 之外,还需要 woodstox-core-asl 依赖项。 |
其他有趣的 Jackson 模块可用:
-
jackson-datatype-money: 支持类型(非官方模块)。
javax.money
-
jackson-datatype-hibernate:支持 Hibernate 特定的类型和属性(包括延迟加载方面)。
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
<bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
<property name="objectMapper" ref="xmlMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
p:indentOutput="true"
p:simpleDateFormat="yyyy-MM-dd"
p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>
<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>
1.11.8. 视图控制器
这是立即定义
在调用时转发到 View。您可以在没有 Java 控制器的静态情况下使用它
在 View 生成响应之前运行的 logic 来运行。ParameterizableViewController
以下 Java 配置示例将请求转发到名为 :/
home
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addViewControllers(registry: ViewControllerRegistry) {
registry.addViewController("/").setViewName("home")
}
}
以下示例实现了与前面示例相同的功能,但使用 XML,通过
使用元素:<mvc:view-controller>
<mvc:view-controller path="/" view-name="home"/>
如果方法映射到任何 HTTP 方法的 URL,则视图
controller 不能用于处理相同的 URL。这是因为通过 URL 与
带注释的 controller 被认为是端点所有权的足够强的指示,因此
405 (METHOD_NOT_ALLOWED)、415 (UNSUPPORTED_MEDIA_TYPE) 或类似响应可以
发送到客户端以帮助调试。因此,建议避免
在带注解的控制器和视图控制器之间拆分 URL 处理。@RequestMapping
1.11.9. 查看解析器
MVC 配置简化了视图解析程序的注册。
以下 Java 配置示例配置内容协商视图
使用 JSP 和 Jackson 作为 JSON 渲染的默认值来解析:View
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.jsp()
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
</mvc:view-resolvers>
但请注意,FreeMarker、Tiles、Groovy Markup 和脚本模板也需要 底层 View 技术的配置。
MVC 命名空间提供专用元素。以下示例适用于 FreeMarker:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache="false"/>
</mvc:view-resolvers>
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>
在 Java 配置中,您可以添加相应的 bean,
如下例所示:Configurer
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.freeMarker().cache(false);
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/freemarker");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.freeMarker().cache(false)
}
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/freemarker")
}
}
1.11.10. 静态资源
此选项提供了一种从基于 Resource
的位置列表提供静态资源的便捷方法。
在下一个示例中,给定一个以 开头的请求,相对路径为
用于查找和提供相对于 Web 应用程序下的静态资源
root 或类路径下的 .这些资源的未来期为一年
expiration 以确保最大限度地使用浏览器缓存并减少 HTTP 请求
由浏览器创建。从中推断出该信息,以便 Headers 支持 HTTP 条件请求。/resources
/public
/static
Last-Modified
Resource#lastModified
"Last-Modified"
下面的清单显示了如何使用 Java 配置来实现这一点:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**"
location="/public, classpath:/static/"
cache-period="31556926" />
另请参阅静态资源的 HTTP 缓存支持。
资源处理程序还支持一系列 ResourceResolver
实现和 ResourceTransformer
实现。
可用于创建用于处理优化资源的工具链。
您可以使用基于 MD5 哈希的版本控制资源 URL
根据内容、固定应用程序版本或其他计算。A (MD5 哈希) 是一个不错的选择,但也有一些值得注意的例外,例如
与模块加载器一起使用的 JavaScript 资源。VersionResourceResolver
ContentVersionStrategy
以下示例显示了如何在 Java 配置中使用:VersionResourceResolver
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain resource-cache="true">
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
</mvc:version-resolver>
</mvc:resolvers>
</mvc:resource-chain>
</mvc:resources>
然后,您可以使用 重写 URL 并应用解析程序和
transformers — 例如,插入版本。MVC 配置提供了一个 Bean,以便可以将其注入到其他 bean 中。您还可以使用 Lymeleaf、JSP、FreeMarker 和其他带有 URL 标记的 URL 标记使重写透明
恃。ResourceUrlProvider
ResourceUrlProvider
ResourceUrlEncodingFilter
HttpServletResponse#encodeURL
请注意,当同时使用两者时(例如,用于提供 gzip 压缩的或
brotli 编码的资源)和 ,您必须按此顺序注册它们。
这可确保始终基于未编码的文件可靠地计算基于内容的版本。EncodedResourceResolver
VersionResourceResolver
对于 WebJar,版本化 URL (如)是推荐且最有效的使用方式。
相关资源位置使用 Spring Boot 开箱即用地配置(也可以配置
手动 ) 并且不需要添加依赖项。/webjars/jquery/1.2.0/jquery.min.js
ResourceHandlerRegistry
org.webjars:webjars-locator-core
无版本的 URL (如 )通过 类路径中存在库时自动注册的 来支持,但代价是
类路径扫描可能会减慢应用程序启动速度。解析器可以将 URL 重写为
包括 jar 的版本,还可以与没有版本的传入 URL 匹配 — 例如, from to 。/webjars/jquery/jquery.min.js
WebJarsResourceResolver
org.webjars:webjars-locator-core
/webjars/jquery/jquery.min.js
/webjars/jquery/1.2.0/jquery.min.js
基于 的 Java 配置提供了更多选项
进行精细控制,例如上次修改的行为和优化的资源解析。ResourceHandlerRegistry |
1.11.11. 默认 Servlet
Spring MVC 允许将 映射到(从而覆盖 Map
),同时仍然允许静态资源请求为
由容器的默认 Servlet 处理。它使用 URL 映射和最低优先级配置
相对于其他 URL 映射。DispatcherServlet
/
DefaultServletHttpRequestHandler
/**
此处理程序将所有请求转发到默认 Servlet。因此,它必须
按所有其他 URL 的顺序保持最后。那就是
case 如果您使用 .或者,如果您设置了
own 自定义实例,请务必将其属性设置为 value
低于 的 ,即 。HandlerMappings
<mvc:annotation-driven>
HandlerMapping
order
DefaultServletHttpRequestHandler
Integer.MAX_VALUE
以下示例显示如何使用默认设置启用该功能:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable()
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:default-servlet-handler/>
覆盖 Servlet 映射的注意事项是,对于
default Servlet 必须按名称而不是按路径检索。尝试自动检测
容器,使用大多数主要 Servlet 的已知名称列表
容器(包括 Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)。
如果默认 Servlet 已使用其他名称进行自定义配置,或者如果
在默认 Servlet 名称未知的情况下使用了不同的 Servlet 容器,
然后,您必须显式提供默认 Servlet 的名称,如下例所示:/
RequestDispatcher
DefaultServletHttpRequestHandler
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable("myCustomDefaultServlet")
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
1.11.12. 路径匹配
您可以自定义与路径匹配和 URL 处理相关的选项。
有关各个选项的详细信息,请参见 PathMatchConfigurer
javadoc。
以下示例演示如何在 Java 配置中自定义路径匹配:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setPatternParser(new PathPatternParser())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
// ...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setPatternParser(patternParser)
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
}
fun patternParser(): PathPatternParser {
//...
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:path-matching
trailing-slash="false"
path-helper="pathHelper"
path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
1.11.13. 高级 Java 配置
@EnableWebMvc
imports ,其中:DelegatingWebMvcConfiguration
-
为 Spring MVC 应用程序提供默认 Spring 配置
-
检测并委托给 implementations 以自定义该配置。
WebMvcConfigurer
对于高级模式,您可以直接从 中删除和扩展,而不是实现 ,
如下例所示:@EnableWebMvc
DelegatingWebMvcConfiguration
WebMvcConfigurer
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {
// ...
}
您可以将现有方法保留在 中,但现在也可以覆盖 bean 声明
从基类中,您仍然可以在
类路径。WebConfig
WebMvcConfigurer
1.11.14. 高级 XML 配置
MVC 命名空间没有高级模式。如果需要自定义 上的属性
无法更改的 bean,则可以使用生命周期
hook 的 Spring ,如下例所示:BeanPostProcessor
ApplicationContext
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
@Component
class MyPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
// ...
}
}
请注意,您需要在 XML 或
通过让 Declaration 检测它。MyPostProcessor
<component-scan/>
1.12. HTTP/2 协议
需要 Servlet 4 容器来支持 HTTP/2,并且 Spring Framework 5 是兼容的 使用 Servlet API 4.从编程模型的角度来看,没有什么具体的 应用程序需要这样做。但是,有一些与服务器配置相关的注意事项。 有关更多详细信息,请参阅 HTTP/2 wiki 页面。
Servlet API 确实公开了一个与 HTTP/2 相关的结构。您可以使用 主动将资源推送到客户端,它
支持作为 methods 的方法参数。javax.servlet.http.PushBuilder
@RequestMapping
2. REST 客户端
本节介绍客户端访问 REST 终端节点的选项。
2.1.RestTemplate
RestTemplate
是执行 HTTP 请求的同步客户端。它是原始的
Spring REST 客户端,并在底层 HTTP 客户端上公开了一个简单的模板方法 API
图书馆。
从 5.0 开始,它处于维护模式,只有对
今后将接受更改和错误。请考虑使用 WebClient,它提供更现代的 API 和
支持同步、异步和流式处理方案。RestTemplate |
有关详细信息,请参阅 REST 端点。
2.2.WebClient
WebClient
是执行 HTTP 请求的非阻塞反应式客户端。它是
在 5.0 中引入,并提供了 的现代替代方案,具有高效的
支持同步和异步以及流式处理方案。RestTemplate
与 相比,支持以下内容:RestTemplate
WebClient
-
非阻塞 I/O。
-
反应流背压。
-
高并发性,硬件资源较少。
-
函数式的 Fluent API,利用 Java 8 lambda。
-
同步和异步交互。
-
向服务器流式传输或从服务器向式传输。
有关更多详细信息,请参阅 WebClient 。
3. 测试
本节总结了 Spring MVC 应用程序中可用的选项。spring-test
-
Servlet API Mocks:用于单元测试控制器的 Servlet API 契约的模拟实现, 过滤器和其他 Web 组件。有关更多详细信息,请参阅 Servlet API 模拟对象。
-
TestContext 框架:支持在 JUnit 和 TestNG 测试中加载 Spring 配置, 包括跨测试方法对加载的配置进行高效缓存,并支持 加载带有 . 有关更多详细信息,请参阅 TestContext Framework。
WebApplicationContext
MockServletContext
-
Spring MVC Test:一个框架,也称为 ,用于测试带注释的控制器 通过(即支持注释),以 Spring MVC 基础设施,但没有 HTTP 服务器。 有关更多详细信息,请参见 Spring MVC Test。
MockMvc
DispatcherServlet
-
客户端 REST:提供可用作 一个模拟服务器,用于测试内部使用 . 有关更多详细信息,请参阅客户端 REST 测试。
spring-test
MockRestServiceServer
RestTemplate
-
WebTestClient
:专为测试 WebFlux 应用程序而构建,但也可用于 通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个 非阻塞、反应式客户端,非常适合测试异步和流 场景。
4. 网络套接字
参考文档的这一部分涵盖了对 Servlet 堆栈、WebSocket 的支持 消息收发,包括原始 WebSocket 交互、通过 SockJS 进行的 WebSocket 仿真,以及 通过 STOMP 作为 WebSocket 上的子协议进行发布-订阅消息传递。
4.1. WebSocket 简介
WebSocket 协议 RFC 6455 提供了标准化的 在 Client 端和 Server 之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与 HTTP 不同的 TCP 协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重复使用现有防火墙规则。
WebSocket 交互以使用 HTTP 标头的 HTTP 请求开始
进行升级,或者在本例中切换到 WebSocket 协议。以下示例
显示了这样的交互:Upgrade
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1 | 标头。Upgrade |
2 | 使用连接。Upgrade |
支持 WebSocket 的服务器返回输出,而不是通常的 200 状态代码 类似于以下内容:
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 | 协议切换 |
握手成功后,HTTP 升级请求的基础 TCP 套接字将保留 open 以继续发送和接收消息。
有关 WebSockets 工作原理的完整介绍超出了本文档的范围。 请参阅 RFC 6455、HTML5 的 WebSocket 章节或许多介绍中的任何一个 Web 上的教程。
请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则 可能需要对其进行配置,以便将 WebSocket 升级请求传递给 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查 云提供商与 WebSocket 支持相关的说明。
4.1.1. HTTP 与 WebSocket
尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头, 重要的是要了解这两种协议导致非常不同的结果 体系结构和应用程序编程模型。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。要与应用程序交互, 客户端以请求-响应样式访问这些 URL。服务器将请求路由到 基于 HTTP URL、方法和标头的适当处理程序。
相比之下,在 WebSockets 中,初始连接通常只有一个 URL。 随后,所有应用程序消息都在同一 TCP 连接上流动。这指向 一种完全不同的异步、事件驱动的消息传递架构。
WebSocket 也是一种低级传输协议,与 HTTP 不同,它没有规定 消息内容的任何语义。这意味着无法路由或处理 一条消息,除非客户端和服务器在消息语义上达成一致。
WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议
(例如,STOMP),通过 HTTP 握手请求上的标头。
如果没有这些,他们需要提出自己的惯例。Sec-WebSocket-Protocol
4.1.2. 何时使用 WebSockets
WebSockets 可以使网页具有动态和交互性。但是,在许多情况下, Ajax 和 HTTP 流式处理或长轮询的组合可以提供简单且 有效的解决方案。
例如,新闻、邮件和社交源需要动态更新,但可能需要 每隔几分钟这样做一次完全可以。协作、游戏和金融应用程序 另一方面,需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如, 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 低延迟、高频率和高容量的组合造就了最好的 case 来使用 WebSocket。
另请记住,在 Internet 上,您无法控制的限制性代理
可能会排除 WebSocket 交互,因为它们未配置为传递 Headers,或者因为它们关闭了看起来空闲的长期连接。这
意味着将 WebSocket 用于防火墙内的内部应用程序是
比面向公众的应用程序更直接的决定。Upgrade
4.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写 client- 和 处理 WebSocket 消息的服务器端应用程序。
4.2.1.WebSocketHandler
创建 WebSocket 服务器就像实现 WebSocket 服务器一样简单,或者更多
可能扩展 或 。以下内容
示例用途 :WebSocketHandler
TextWebSocketHandler
BinaryWebSocketHandler
TextWebSocketHandler
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
有专用的 WebSocket Java 配置和 XML 命名空间支持,用于将前面的 WebSocket 处理程序添加到特定 URL,如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于 Spring MVC 应用程序,应该包含在内
在 DispatcherServlet
的配置中。然而,Spring 的
WebSocket 支持不依赖于 Spring MVC。它相对简单
在 WebSocketHttpRequestHandler
的帮助下集成到其他 HTTP 服务环境中。WebSocketHandler
当直接使用和间接使用 API 时,例如通过 STOMP 消息传递,应用程序必须同步消息的发送
因为底层标准 WebSocket 会话 (JSR-356) 不允许并发
发送。一种选择是用 ConcurrentWebSocketSessionDecorator
包装。WebSocketHandler
WebSocketSession
4.2.2. WebSocket 握手
自定义初始 HTTP WebSocket 握手请求的最简单方法是通过
a ,它公开了握手的“之前”和“之后”的方法。
你可以使用这样的拦截器来排除握手或创建任何属性
可用于 .以下示例使用内置侦听器
要将 HTTP 会话属性传递给 WebSocket 会话:HandshakeInterceptor
WebSocketSession
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
更高级的选项是扩展执行
WebSocket 握手的步骤,包括验证客户端源,
协商子协议和其他细节。应用程序可能还需要使用此
选项(如果需要配置自定义),以便
适配尚不支持的 WebSocket 服务器引擎和版本
(有关此主题的更多信息,请参阅部署)。
Java 配置和 XML 名称空间都可以配置自定义 .DefaultHandshakeHandler
RequestUpgradeStrategy
HandshakeHandler
Spring 提供了一个可用于装饰的基类
a 具有附加行为。日志记录和异常处理
使用 WebSocket Java 配置时,默认提供和添加 implementations
或 XML 命名空间。渔获物均未捕获
异常,并关闭 WebSocket
session with status ,这表示服务器错误。WebSocketHandlerDecorator WebSocketHandler ExceptionWebSocketHandlerDecorator WebSocketHandler 1011 |
4.2.3. 部署
Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中
它同时提供 HTTP WebSocket 握手和其他
HTTP 请求。它也很容易集成到其他 HTTP 处理场景中
通过调用 .这既方便又容易
理解。但是,对于 JSR-356 运行时,需要特别注意。DispatcherServlet
WebSocketHttpRequestHandler
Java WebSocket API (JSR-356) 提供了两种部署机制。第一个
涉及启动时的 Servlet 容器类路径扫描(Servlet 3 的一项功能)。
另一个是在 Servlet 容器初始化时使用的注册 API。
这两种机制都无法实现使用单个 “前端控制器”
用于所有 HTTP 处理 — 包括 WebSocket 握手和所有其他 HTTP
请求 — 例如 Spring MVC 的 .DispatcherServlet
这是 JSR-356 的一个重大限制,Spring 的 WebSocket 支持解决了这个限制
特定于服务器的实现,即使在 JSR-356 运行时中运行时也是如此。
目前,Tomcat、Jetty、GlassFish、WebLogic、WebSphere 和
Undertow(和 WildFly)。RequestUpgradeStrategy
克服 Java WebSocket API 中上述限制的请求是 创建并可以在 eclipse-ee4j/websocket-api#211 中遵循。 Tomcat、Undertow 和 WebSphere 提供了自己的 API 替代方案,这些 API 替代方案 使这成为可能,Jetty 也是可能的。我们满怀希望 更多的服务器也会做同样的事情。 |
第二个考虑因素是需要支持 JSR-356 的 Servlet 容器
执行可能减慢应用程序速度的 (SCI) 扫描
启动 — 在某些情况下,效果会非常显著。如果在
升级到支持 JSR-356 的 Servlet 容器版本,它应该
可以有选择地启用或禁用 Web 片段(和 SCI 扫描)
通过使用 中的 元素,如下例所示:ServletContainerInitializer
<absolute-ordering />
web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering/>
</web-app>
然后,您可以按名称有选择地启用 Web 片段,例如为 Servlet 3 提供支持的 Spring 自己的 Web 片段
Java 初始化 API。以下示例显示了如何执行此操作:SpringServletContainerInitializer
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
4.2.4. 服务器配置
每个底层 WebSocket 引擎都公开了控制 运行时特征,例如消息缓冲区大小、空闲超时、 和其他。
对于 Tomcat、WildFly 和 GlassFish,您可以将
WebSocket Java 配置,如下例所示:ServletServerContainerFactoryBean
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>
</beans>
对于客户端 WebSocket 配置,您应该使用 (XML) 或 (Java configuration)。WebSocketContainerFactoryBean ContainerProvider.getWebSocketContainer() |
对于 Jetty,您需要提供预配置的 Jetty 和插头
通过 WebSocket Java 配置将其转换为 Spring。
以下示例显示了如何执行此操作:WebSocketServerFactory
DefaultHandshakeHandler
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>
</beans>
4.2.5. 允许的来源
从 Spring Framework 4.1.5 开始,WebSocket 和 Sockjs 的默认行为是接受
仅限同源请求。也可以允许所有或指定的源列表。
此检查主要针对浏览器客户端设计。没有什么能阻止其他类型
的客户端修改标头值(有关更多详细信息,请参阅 RFC 6454:Web Origin 概念)。Origin
三种可能的行为是:
-
仅允许同源请求(默认):在此模式下,启用 Sockjs 时, Iframe HTTP 响应标头设置为 ,JSONP transport 被禁用,因为它不允许检查请求的来源。 因此,启用此模式时,不支持 IE6 和 IE7。
X-Frame-Options
SAMEORIGIN
-
允许指定的源列表:每个允许的源都必须以 或 开头。在此模式下,启用 Sockjs 时,将禁用 IFrame 传输。 因此,IE6 到 IE9 在执行此操作时不受支持 mode 已启用。
http://
https://
-
允许所有源:要启用此模式,您应该提供作为允许的源 价值。在此模式下,所有传输都可用。
*
您可以配置 WebSocket 和 Sockjs 允许的源,如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
4.3. SockJS 回退
在公共 Internet 上,不受您控制的限制性代理可能会阻止 WebSocket
交互,因为它们未配置为传递标头或
,因为它们会关闭似乎处于空闲状态的长期连接。Upgrade
此问题的解决方案是 WebSocket 仿真 — 即尝试使用 WebSocket 首先,然后回退到模拟 WebSocket 的基于 HTTP 的技术 交互并公开相同的应用程序级 API。
在 Servlet 堆栈上,Spring Framework 提供服务器(和客户端)支持 对于 Sockjs 协议。
4.3.1. 概述
Sockjs 的目标是让应用程序使用 WebSocket API,但回退到 非 WebSocket 替代方案,而无需 更改应用程序代码。
Sockjs 包括:
-
Sockjs JavaScript 客户端 — 用于浏览器的客户端库。
-
Sockjs 服务器实现,包括 Spring Framework 模块中的一个实现。
spring-websocket
-
模块中的 Sockjs Java 客户端(自版本 4.1 起)。
spring-websocket
Sockjs 专为在浏览器中使用而设计。它使用了多种技术 以支持各种浏览器版本。 有关 Sockjs 传输类型和浏览器的完整列表,请参阅 Sockjs 客户端页面。运输 分为三大类:WebSocket、HTTP 流和 HTTP 长轮询。 有关这些类别的概述,请参阅此博客文章。
Sockjs 客户端首先发送到
从服务器获取基本信息。之后,它必须决定哪种传输方式
使用。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,
至少有一个 HTTP 流式处理选项。如果不是,则 HTTP (long)
轮询。GET /info
所有传输请求都具有以下 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 添加了最少的消息框架。例如,服务器最初发送字母(“打开”帧),如果没有消息流,则消息作为(JSON 编码数组)发送,字母(“心跳”帧)发送
25 秒(默认),并使用字母(“close” frame)以关闭会话。o
a["message1","message2"]
h
c
要了解更多信息,请在浏览器中运行示例并观察 HTTP 请求。
Sockjs 客户端允许修复传输列表,因此可以
一次查看每种运输。Sockjs 客户端还提供了一个 debug 标志
,这将在浏览器控制台中启用有用的消息。在服务器端,您可以为 .
有关更多详细信息,请参阅 Sockjs 协议旁白测试。TRACE
org.springframework.web.socket
4.3.2. 启用 SockJS
您可以通过 Java 配置启用 Sockjs,如下例所示:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
前面的示例用于 Spring MVC 应用程序,应包含在
DispatcherServlet
的配置。但是,Spring 的 WebSocket
并且 Sockjs 支持不依赖于 Spring MVC。它相对简单
在 SockJsHttpRequestHandler
的帮助下集成到其他 HTTP 服务环境中。
在浏览器端,应用程序可以使用 sockjs-client
(版本 1.0.x)。它
模拟 W3C WebSocket API 并与服务器通信以选择最佳
transport 选项,具体取决于运行它的浏览器。参见 sockjs-client 页面和
浏览器支持的传输类型。客户端还提供多个
Configuration Options (配置选项) — 例如,指定要包含的传输。
4.3.3. IE 8 和 9
Internet Explorer 8 和 9 仍在使用中。他们是 拥有 Sockjs 的关键原因。本节涵盖重要的 有关在这些浏览器中运行的注意事项。
Sockjs 客户端通过使用 Microsoft 的 XDomainRequest
在 IE 8 和 9 中支持 Ajax/XHR 流。
这可以跨域工作,但不支持发送 Cookie。
Cookie 通常对于 Java 应用程序至关重要。
但是,由于 Sockjs 客户端可以与许多服务器一起使用
类型(不仅仅是 Java 类型)中,它需要知道 cookie 是否重要。
如果是这样,则 Sockjs 客户端首选 Ajax/XHR 进行流式处理。否则,它会
依赖于基于 iframe 的技术。
来自 Sockjs 客户端的第一个请求是对
可以影响客户选择交通工具的信息。
其中一个细节是服务器应用程序是否依赖 Cookie
(例如,用于身份验证目的或使用粘性会话进行集群)。
Spring 的 Sockjs 支持包括一个名为 .
默认情况下,它是启用的,因为大多数 Java 应用程序都依赖于 Cookie。如果您的应用程序不需要它,您可以关闭此选项,
和 Sockjs 客户端应该在 IE 8 和 9 中选择。/info
sessionCookieNeeded
JSESSIONID
xdr-streaming
如果您确实使用基于 iframe 的传输,请记住
可以通过以下方式指示浏览器在给定页面上阻止使用 IFrames
将 HTTP 响应标头设置为 、 、 或 。这用于防止点击劫持。X-Frame-Options
DENY
SAMEORIGIN
ALLOW-FROM <origin>
Spring Security 3.2+ 支持在每个
响应。默认情况下,Spring Security Java 配置将其设置为 。
在 3.2 中, Spring Security XML 命名空间默认情况下不设置该 Headers
但可以配置为执行此操作。将来,它可能会默认设置它。 |
如果您的应用程序添加了响应标头(正如它应该的那样!
并依赖于基于 iframe 的传输,则需要将标头值设置为 或 。春季 SockJS
支持还需要知道 Sockjs 客户端的位置,因为它已加载
从 iframe 中。默认情况下,iframe 设置为下载 Sockjs 客户端
从 CDN 位置。最好将此选项配置为使用
与应用程序来自同一源的 URL。X-Frame-Options
SAMEORIGIN
ALLOW-FROM <origin>
以下示例显示了如何在 Java 配置中执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
XML 命名空间通过元素提供类似的选项。<websocket:sockjs>
在初始开发期间,请启用 Sockjs 客户端模式,以防止
浏览器缓存 Sockjs 请求(如 iframe),否则
被缓存。有关如何启用它的详细信息,请参阅 Sockjs 客户端页面。devel |
4.3.4. 心跳
Sockjs 协议要求服务器发送心跳消息以排除代理
得出连接已挂起的结论。Spring Sockjs 配置有一个属性
调用,可用于自定义频率。默认情况下,
检测信号在 25 秒后发送,假设没有发送其他消息
连接。此 25 秒值符合以下针对公共 Internet 应用程序的 IETF 建议。heartbeatTime
当通过 WebSocket 和 Sockjs 使用 STOMP 时,如果 STOMP 客户端和服务器协商 heartbeats 进行交换,则禁用 Sockjs 心跳。 |
Spring Sockjs 支持还允许您将
计划检测信号任务。任务调度器由线程池
使用基于可用处理器数量的默认设置。你
应考虑根据您的特定需求自定义设置。TaskScheduler
4.3.5. 客户端断开连接
HTTP 流和 HTTP 长轮询 Sockjs 传输需要保持连接 开放时间比平时长。有关这些技术的概述,请参阅此博客文章。
在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该 允许退出 Servlet 容器线程,处理请求并继续 写入来自另一个线程的响应。
一个具体问题是 Servlet API 不为客户端提供通知 这已经消失了。参见 eclipse-ee4j/servlet-api#44。 但是,Servlet 容器会在后续写入尝试时引发异常 响应。由于 Spring 的 Sockjs 服务支持服务器发送的心跳(每个 默认为 25 秒),这意味着通常会在该时间内检测到客户端断开连接 时间段(如果消息发送更频繁,则更早)。
因此,网络 I/O 故障可能是因为客户端已断开连接,这
可能会用不必要的堆栈跟踪填充日志。Spring 尽最大努力识别
此类网络故障表示客户端断开连接(特定于每个服务器)并记录
使用专用日志类别(在 中定义)的最小消息。如果您需要查看堆栈跟踪,可以将其
log 类别设置为 TRACE。DISCONNECTED_CLIENT_LOG_CATEGORY AbstractSockJsSession |
4.3.6. SockJS 和 CORS
如果您允许跨域请求(请参阅允许的源),则 Sockjs 协议
在 XHR 流和轮询传输中使用 CORS 实现跨域支持。因此
CORS 标头会自动添加,除非响应中存在 CORS 标头
检测到。因此,如果应用程序已配置为提供 CORS 支持(例如,
通过 Servlet Filter),Spring 跳过了这部分。SockJsService
也可以通过在 Spring 的 SockJsService 中设置属性来禁用这些 CORS 标头的添加。suppressCors
Sockjs 需要以下 Headers 和值:
-
Access-Control-Allow-Origin
:从请求头的值初始化。Origin
-
Access-Control-Allow-Credentials
:始终设置为 。true
-
Access-Control-Request-Headers
:从等效请求标头中的值初始化。 -
Access-Control-Allow-Methods
:传输支持的 HTTP 方法(请参阅 enum)。TransportType
-
Access-Control-Max-Age
:设置为 31536000(1 年)。
有关确切的实现,请参阅 in 和
源代码中的 enum 中。addCorsHeaders
AbstractSockJsService
TransportType
或者,如果 CORS 配置允许,请考虑排除
Sockjs 端点前缀,从而让 Spring 处理它。SockJsService
4.3.7.SockJsClient
Spring 提供了一个 Sockjs Java 客户端来连接到远程 Sockjs 端点,而无需 使用浏览器。当需要双向 两个服务器之间通过公共网络(即网络代理可以 排除使用 WebSocket 协议)。Sockjs Java 客户端也非常有用 用于测试目的(例如,模拟大量并发用户)。
Sockjs Java 客户端支持 、 和 transports。其余的只有在浏览器中使用才有意义。websocket
xhr-streaming
xhr-polling
您可以使用以下方式配置WebSocketTransport
-
StandardWebSocketClient
在 JSR-356 运行时中。 -
JettyWebSocketClient
通过使用 Jetty 9+ 原生 WebSocket API。 -
Spring 的 .
WebSocketClient
根据定义,An 同时支持 和 ,因为
从客户端的角度来看,除了用于连接的 URL 之外,没有其他区别
添加到服务器。目前有两种实现方式:XhrTransport
xhr-streaming
xhr-polling
-
RestTemplateXhrTransport
将 Spring 的用于 HTTP 请求。RestTemplate
-
JettyXhrTransport
将 Jetty 用于 HTTP 请求。HttpClient
以下示例演示如何创建 Sockjs 客户端并连接到 Sockjs 端点:
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
Sockjs 对消息使用 JSON 格式的数组。默认情况下,使用 Jackson 2 并需要
以位于 Classpath 上。或者,您也可以配置 的自定义实现并在 .SockJsMessageCodec SockJsClient |
要使用 来模拟大量并发用户,您需要
需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够的
连接数和线程数。以下示例显示了如何使用 Jetty 执行此操作:SockJsClient
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
以下示例显示了服务器端与 Sockjs 相关的属性(有关详细信息,请参见 javadoc) 您还应考虑自定义:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
1 | 将属性设置为 512KB(默认值为 128KB — )。streamBytesLimit 128 * 1024 |
2 | 将属性设置为 1,000 (默认值为 )。httpMessageCacheSize 100 |
3 | 将属性设置为 30 个属性秒(默认值为 5 秒 — )。disconnectDelay 5 * 1000 |
4.4. STOMP踏
WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的 内容未定义。该协议定义了一种机制,供客户端和服务器协商 子协议(即更高级别的消息传递协议)在 WebSocket 上使用,以 定义每个可以发送的消息类型、格式、每个消息的内容 消息,依此类推。子协议的使用是可选的,但无论哪种方式,客户端和 服务器需要就定义消息内容的某个协议达成一致。
4.4.1. 概述
STOMP(简单 Text Oriented Messaging Protocol)最初是为脚本语言创建的 (例如 Ruby、Python 和 Perl)连接到企业消息代理。是的 旨在解决常用消息传递模式的最小子集。STOMP 可以是 通过任何可靠的双向流式处理网络协议(如 TCP 和 WebSocket)使用。 尽管 STOMP 是面向文本的协议,但消息有效负载可以是 text 或 binary。
STOMP 是一种基于帧的协议,其帧基于 HTTP。下面的清单显示了结构 的 STOMP 帧:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用 or 命令发送或订阅
messages 的 API 中,以及描述
消息是关于以及谁应该接收它。这样,一个简单的
publish-subscribe 机制,可用于通过 broker 发送消息
发送到其他连接的客户端,或者向服务器发送消息以请求
执行一些工作。SEND
SUBSCRIBE
destination
当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序会执行
作为 STOMP 代理。消息被路由到消息处理
方法或跟踪订阅的简单内存代理,以及
向订阅用户广播消息。您还可以配置 Spring 以工作
使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)进行实际
消息广播。在这种情况下, Spring 会维护
与代理的 TCP 连接,将消息中继到代理,并传递消息
从它向下到连接的 WebSocket 客户端。因此,Spring Web 应用程序可以
依赖于基于 HTTP 的统一安全性、通用验证和熟悉的编程
model 进行消息处理。@Controller
以下示例显示了订阅以接收股票报价的客户端,该
服务器可能会定期发出(例如,通过发送消息的计划任务
通过 a 到经纪人):SimpMessagingTemplate
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
以下示例显示了一个发送交易请求的客户端,该请求服务器
可以通过方法处理:@MessageMapping
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
执行后,服务器可以 向客户广播交易确认消息和详细信息。
目标的含义在 STOMP 规范中故意保持不透明。它可以
可以是任何字符串,则完全由 STOMP 服务器来定义语义和
它们支持的目标的语法。然而,对于
destinations 为路径式字符串,其中表示发布-订阅
(一对多) 和隐含的点对点 (one-to-one) 消息
交流。/topic/..
/queue/
STOMP 服务器可以使用该命令向所有订阅者广播消息。
以下示例显示了向订阅的客户端发送股票报价的服务器:MESSAGE
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
服务器无法发送未经请求的消息。所有消息
from a server 必须响应特定的客户端订阅,并且服务器消息的标头必须与
客户端订阅。subscription
id
前面的概述旨在提供对 STOMP 协议。我们建议完整地查看协议规范。
4.4.2. 好处
使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 提供更丰富的编程模型,而不是使用原始 WebSockets。同一点可以是 介绍了 HTTP 与原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架 提供丰富的功能。以下是好处列表:
-
无需发明自定义消息协议和消息格式。
-
STOMP 客户端(包括 Spring Framework 中的 Java 客户端)可用。
-
您可以(可选地)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来 管理订阅和广播消息。
-
应用程序逻辑可以组织在任意数量的实例中,并且消息可以 根据 STOMP 目标标头路由到它们,而不是处理原始 WebSocket 消息 替换为给定连接的 single 。
@Controller
WebSocketHandler
-
您可以使用 Spring Security 根据 STOMP 目标和消息类型保护消息。
4.4.3. 启用 STOMP
STOMP over WebSocket 支持在 和 模块中可用。拥有这些依赖项后,就可以公开 STOMP
endpoints,通过 WebSocket 和 Sockjs Fallback,如下例所示:spring-messaging
spring-websocket
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS(); (1)
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app"); (2)
config.enableSimpleBroker("/topic", "/queue"); (3)
}
}
1 | /portfolio 是 WebSocket(或 SockJS)到的端点的 HTTP URL。
客户端需要连接以进行 WebSocket 握手。 |
2 | 目标 Headers 开头的 STOMP 消息将路由到类中的方法。/app @MessageMapping @Controller |
3 | 使用内置的消息代理进行订阅和广播,以及
将 destination header 开头 with 的消息路由到 broker。/topic `or `/queue |
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
</websocket:message-broker>
</beans>
对于内置的 simple broker,和 前缀没有任何特殊的
意义。它们只是区分 pub-sub 和 point-to-point 的约定
消息收发(即,多个订阅者与 1 个使用者)。当您使用外部代理时,
检查代理的 STOMP 页面以了解哪种类型的 STOMP 目标和
它支持的前缀。/topic /queue |
要从浏览器连接,对于 Sockjs,你可以使用 sockjs-client
。对于 STOMP,许多应用程序都有
使用了 jmesnil/stomp-websocket 库
(也称为 stomp.js),功能齐全,已在生产环境中用于
年,但不再维护。目前 JSteunou/webstomp-client 是最多的
该库的积极维护和不断发展的继任者。以下示例代码
基于它:
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);
stompClient.connect({}, function(frame) {
}
或者,如果你通过 WebSocket 连接(没有 Sockjs),你可以使用以下代码:
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
}
请注意,在前面的示例中,不需要指定 and headers。即使这样做了,他们也会被忽略(或者更确切地说,
overridden) 的有关身份验证的更多信息,请参见Connecting to a Broker and Authentication。stompClient
login
passcode
有关更多示例代码,请参阅:
-
使用 WebSocket 构建 交互式 Web 应用程序 — 入门指南。
-
Stock Portfolio — 示例 应用。
4.4.4. WebSocket 服务器
要配置底层 WebSocket 服务器,服务器配置中的信息适用。但是,对于 Jetty,您需要设置
和 通过 :HandshakeHandler
WebSocketPolicy
StompEndpointRegistry
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
4.4.5. 消息流
一旦 STOMP 端点被公开,Spring 应用程序就会成为 STOMP 代理 连接的客户端。本节介绍服务器端的消息流。
该模块包含对消息传递应用程序的基本支持
它源自 Spring Integration,并且是
后来提取并合并到 Spring 框架中,以便在许多 Spring 项目和应用程序场景中更广泛地使用。
以下列表简要介绍了一些可用的消息传递抽象:spring-messaging
-
消息: 消息的简单表示形式,包括 Headers 和 payload。
-
MessageHandler 中: 用于处理消息的协定。
-
MessageChannel 中: 用于发送支持生产者和使用者之间松散耦合的消息的 Contract。
-
SubscribableChannel:有订阅者。
MessageChannel
MessageHandler
-
ExecutorSubscribableChannel:它使用 来发送消息。
SubscribableChannel
Executor
Java 配置(即)和 XML 名称空间配置
(即)使用上述组件来组装消息
工作流。下图显示了 simple 内置消息
broker 已启用:@EnableWebSocketMessageBroker
<websocket:message-broker>
上图显示了三个消息通道:
-
clientInboundChannel
:用于传递从 WebSocket 客户端接收的消息。 -
clientOutboundChannel
:用于向 WebSocket 客户端发送服务器消息。 -
brokerChannel
:用于从内部向消息代理发送消息 服务器端应用程序代码。
下图显示了外部代理(例如 RabbitMQ) 配置为管理订阅和广播消息:
前面两个图之间的主要区别在于使用 “broker relay” 来传递 消息通过 TCP 传输到外部 STOMP 代理,并用于从 broker 添加到订阅的客户端。
当从 WebSocket 连接接收到消息时,它们被解码为 STOMP 帧。
转换为 Spring 表示形式,并发送给 进行进一步处理。例如,其 STOMP 消息
目标标头开头的 URL 可以路由到
带注释的控制器,while 和 消息可以直接路由
发送到消息代理。Message
clientInboundChannel
/app
@MessageMapping
/topic
/queue
处理来自客户端的 STOMP 消息的注释可以将消息发送到
消息代理通过 ,代理广播
消息通过 .一样
controller 也可以执行相同的操作来响应 HTTP 请求,因此客户端可以执行
HTTP POST,然后方法可以向消息代理发送消息
向订阅的客户端广播。@Controller
brokerChannel
clientOutboundChannel
@PostMapping
我们可以通过一个简单的例子来追踪流程。请考虑以下示例,该示例设置了一个服务器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}
前面的示例支持以程:
-
客户端连接到 WebSocket 连接,并在 WebSocket 连接后 建立,STOMP 帧开始在其上流动。
http://localhost:8080/portfolio
-
客户端发送目标标头为 .一旦收到 并解码后,消息将发送到 ,然后路由到 Message Broker,用于存储客户端订阅。
/topic/greeting
clientInboundChannel
-
客户端向 发送 SEND 帧。前缀有助于将其路由到 带注释的控制器。去除前缀后,目标的其余部分将映射到 中的方法。
/app/greeting
/app
/app
/greeting
@MessageMapping
GreetingController
-
返回的值 from 将转换为 Spring 基于返回值和默认目标标头的有效负载(派生自输入目标,替换为 )。生成的消息将发送到 并进行处理 由 Message Broker 提供。
GreetingController
Message
/topic/greeting
/app
/topic
brokerChannel
-
消息代理查找所有匹配的订阅者,并向每个订阅者发送一个 MESSAGE 帧 通过 ,从中将消息编码为 STOMP 帧 并通过 WebSocket 连接发送。
clientOutboundChannel
下一节将提供有关带注释方法的更多详细信息,包括 支持的参数和返回值的种类。
4.4.6. 带注解的控制器
应用程序可以使用带注释的类来处理来自客户端的消息。
此类类可以声明 、 和 方法,如以下主题所述:@Controller
@MessageMapping
@SubscribeMapping
@ExceptionHandler
@MessageMapping
您可以使用它来注释根据消息的路由消息的方法
目的地。它在方法级别和类型级别都受支持。在类型
level 用于表示
控制器。@MessageMapping
@MessageMapping
默认情况下,映射值为 Ant 样式路径模式(例如 、 、 )、
包括对模板变量的支持(例如 )。值可以是
通过方法参数引用。应用程序还可以切换到
映射的点分隔目标约定,如 点作为分隔符中所述。/thing*
/thing/**
/thing/{id}
@DestinationVariable
支持的方法参数
下表描述了方法参数:
Method 参数 | 描述 |
---|---|
|
用于访问完整的消息。 |
|
要访问 . |
|
用于通过类型化访问器方法访问标头。 |
|
要访问消息的有效负载,请由配置的 . 此注释的存在不是必需的,因为默认情况下,如果不存在 other 参数。 您可以使用 Spring 的 ,
以自动验证 payload 参数。 |
|
用于访问特定标头值 — 以及使用 的类型转换(如有必要)。 |
|
用于访问消息中的所有标头。此参数必须可分配给 。 |
|
用于访问从消息目标提取的模板变量。 值将根据需要转换为声明的方法参数类型。 |
|
反映在 WebSocket HTTP 握手时登录的用户。 |
返回值
默认情况下,方法的返回值将序列化为有效负载
通过匹配并作为 发送到 ,
从那里向订阅者广播。出站消息的目的地是
与入站消息的 ID 相同,但前缀为 .@MessageMapping
MessageConverter
Message
brokerChannel
/topic
您可以使用 和 注解来自定义
输出消息。 用于自定义目标目标或
指定多个目标。 用于定向输出消息
仅与输入消息关联的用户。请参阅 用户目标。@SendTo
@SendToUser
@SendTo
@SendToUser
您可以在同一方法上同时使用两者和 ,也可以同时使用两者
在类级别受支持,在这种情况下,它们充当
类。但是,请记住,任何方法级别或 annotation
在类级别覆盖任何此类注释。@SendTo
@SendToUser
@SendTo
@SendToUser
消息可以异步处理,并且方法可以返回 、 或 。@MessageMapping
ListenableFuture
CompletableFuture
CompletionStage
请注意,和 只是为了方便使用 来发送消息。如有必要,对于更高级的方案,方法可以回退到直接使用 。
这可以代替返回值,也可能同时返回值。
请参阅发送消息。@SendTo
@SendToUser
SimpMessagingTemplate
@MessageMapping
SimpMessagingTemplate
@SubscribeMapping
@SubscribeMapping
类似于,但会将映射缩小到
仅限订阅消息。它支持与 .然而
对于返回值,默认情况下,消息将直接发送到客户端(通过 ,以响应订阅),而不是发送到代理(通过 ,作为对匹配订阅的广播)。添加或覆盖此行为,并改为发送到代理。@MessageMapping
@MessageMapping
clientOutboundChannel
brokerChannel
@SendTo
@SendToUser
这在什么情况下有用?假设代理映射到 和 ,而
应用程序控制器映射到 。在此设置中,代理存储所有
订阅 和 用于重复广播,以及
应用程序无需参与。客户端还可以订阅
some destination 的 Target,并且控制器可以返回一个值来响应该
订阅,而不涉及代理,而无需再次存储或使用订阅
(实际上是一次性的请求-回复交换)。其中一个用例是填充 UI
使用启动时的初始数据。/topic
/queue
/app
/topic
/queue
/app
这在什么情况下没有用?不要尝试将 broker 和 controller 映射到同一个目的地 前缀,除非您希望两者都独立处理消息(包括订阅)、 出于某种原因。入站消息是并行处理的。无法保证 broker 或 controller 首先处理给定的消息。如果目标是收到通知 当订阅存储并准备好进行广播时,客户端应请求 receipt (如果服务器支持) (Simple Broker 不支持)。例如,使用 Java STOMP 客户端,您可以执行以下操作来添加收据:
@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(receiptHeaders -> {
// Subscription ready...
});
服务器端选项是注册 on 并实现在处理消息(包括订阅)后调用的方法。ExecutorChannelInterceptor
brokerChannel
afterMessageHandled
@MessageExceptionHandler
应用程序可以使用方法处理方法中的异常。您可以在 annotation 中声明异常
本身或通过 method 参数(如果你想访问异常实例)。
下面的示例通过 method 参数声明一个 exception:@MessageExceptionHandler
@MessageMapping
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
@MessageExceptionHandler
方法支持灵活的方法签名和支持
方法参数类型和返回值与 @MessageMapping
方法相同。
通常,方法在类中应用
(或类层次结构)中声明它们。如果您希望此类方法适用
更全局地(跨控制器),您可以在标有 .这与 Spring MVC 中提供的类似支持相当。@MessageExceptionHandler
@Controller
@ControllerAdvice
4.4.7. 发送消息
如果您想从
应用?任何应用程序组件都可以向 .
最简单的方法是注入 和
使用它来发送消息。通常,您可以通过以下方式注入它
type,如下例所示:brokerChannel
SimpMessagingTemplate
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
但是,您也可以通过其名称 () 来限定它,如果另一个
存在相同类型的 bean。brokerMessagingTemplate
4.4.8. 简单代理
内置的 Simple Message Broker 处理来自客户端的订阅请求, 将它们存储在内存中,并将消息广播到具有匹配项的已连接客户端 目的地。代理支持类似路径的目标,包括订阅 更改为 Ant 样式的目标模式。
应用程序还可以使用点分隔(而不是斜杠分隔)的目标。 请参阅点作为分隔符。 |
如果配置了任务计划程序,则简单代理支持 STOMP 检测信号。
要配置调度程序,您可以声明自己的 Bean 并通过
这。或者,您可以使用自动
但是,您需要避免
内置 WebSocket 配置和您的 .例如:TaskScheduler
MessageBrokerRegistry
@Lazy
WebSocketMessageBrokerConfigurer
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private TaskScheduler messageBrokerTaskScheduler;
@Autowired
public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);
// ...
}
}
4.4.9. 外部代理
simple 代理非常适合入门,但仅支持 STOMP 命令(它不支持 acks、收据和其他一些功能), 依赖于简单的消息发送循环,不适合集群。 或者,您可以升级应用程序以使用功能齐全的 消息代理。
请参阅所选消息代理(例如 RabbitMQ、ActiveMQ 等)的 STOMP 文档,安装代理、 并在启用 STOMP 支持的情况下运行它。然后,您可以启用 STOMP 代理中继 (而不是简单的代理)在 Spring 配置中。
以下示例配置启用功能齐全的代理:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
</beans>
上述配置中的 STOMP 代理中继是一个 Spring MessageHandler
,它通过将消息转发到外部消息代理来处理消息。
为此,它与代理建立 TCP 连接,将所有消息转发给代理,
然后,通过
WebSocket 会话。从本质上讲,它充当转发消息的 “中继”
双向。
将依赖项添加到您的项目中以进行 TCP 连接管理。io.projectreactor.netty:reactor-netty io.netty:netty-all |
此外,应用程序组件(例如 HTTP 请求处理方法、 业务服务等)也可以将消息发送到 Broker Relay,如 ,将消息广播到订阅的 WebSocket 客户端。
实际上,代理中继支持健壮且可扩展的消息广播。
4.4.10. 连接到 Broker
STOMP 代理中继维护与代理的单个“系统”TCP 连接。
此连接用于来自服务器端应用程序的消息
仅用于接收消息。您可以配置 STOMP 凭证(即
STOMP 帧和标头)。这是暴露的
在 XML 命名空间和 Java 配置中,作为 and 属性,默认值为 and。login
passcode
systemLogin
systemPasscode
guest
guest
STOMP 代理中继还为每个连接的 TCP 连接创建一个单独的 TCP 连接
WebSocket 客户端。您可以配置用于所有 TCP 的 STOMP 凭证
代表客户创建的连接。这在 XML 命名空间中公开
和 Java 配置作为 和 default 属性
的值 和 。clientLogin
clientPasscode
guest
guest
STOMP 代理中继始终在代表客户端转发给代理的每个帧上设置 and 标头。因此,WebSocket 客户端
不需要设置这些标头。它们将被忽略。正如 Authentication 部分所解释的那样,WebSocket 客户端应该依赖 HTTP 身份验证来保护
WebSocket 终端节点并建立客户端身份。login passcode CONNECT |
STOMP 代理中继还向消息发送和接收检测信号 broker 通过 “system” TCP 连接。您可以配置发送的间隔 和接收检测信号(默认每个 10 秒)。如果连接到代理 丢失,则代理中继会继续尝试每 5 秒重新连接一次, 直到它成功。
任何 Spring bean 都可以实现在与 broker 的 “system” 连接丢失时接收通知,并且
重新建立。例如,广播股票报价的 Stock Quote 服务可以
当没有活动的 “system” 连接时停止尝试发送消息。ApplicationListener<BrokerAvailabilityEvent>
默认情况下,STOMP 代理中继始终连接,并在 if 与同一主机和端口的连接丢失。如果您希望提供多个地址, 在每次尝试连接时,您可以配置地址的供应商,而不是 固定主机和端口。下面的示例展示了如何做到这一点:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
private ReactorNettyTcpClient<byte[]> createTcpClient() {
return new ReactorNettyTcpClient<>(
client -> client.addressSupplier(() -> ... ),
new StompReactorNettyCodec());
}
}
您还可以使用属性配置 STOMP 代理中继。
此属性的值设置为每个帧的标头
)并且可能很有用(例如,在云环境中,实际主机连接到哪个
建立的 TCP 连接与提供
基于云的 STOMP 服务)。virtualHost
host
CONNECT
4.4.11. 点作为分隔符
当消息路由到方法时,它们将与 .默认情况下,模式应使用 slash () 作为分隔符。
这是 Web 应用程序中的良好约定,类似于 HTTP URL。但是,如果
您更习惯于消息传递约定,您可以切换到使用 Dot () 作为分隔符。@MessageMapping
AntPathMatcher
/
.
以下示例显示了如何在 Java 配置中执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
<websocket:stomp-endpoint path="/stomp"/>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
<constructor-arg index="0" value="."/>
</bean>
</beans>
之后,控制器可以使用点 () 作为方法中的分隔符,
如下例所示:.
@MessageMapping
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
客户端现在可以向 发送消息。/app/red.blue.green123
在前面的示例中,我们没有更改 “broker relay” 上的前缀,因为这些 完全依赖于外部 Message Broker。请参阅 STOMP 文档页面 用于查看 Destination Headers 支持的约定的代理。
另一方面,“simple broker” 确实依赖于配置的 ,因此,如果
切换分隔符,该更改也适用于 broker 和 broker 的匹配方式
destinations 从 message 到 subscriptions 中的模式。PathMatcher
4.4.12. 认证
每个 STOMP over WebSocket 消息收发会话都以 HTTP 请求开头。 这可以是升级到 WebSockets 的请求(即 WebSocket 握手) 或者,在 Sockjs 回退的情况下,一系列 Sockjs HTTP 传输请求。
许多 Web 应用程序已经进行了身份验证和授权,以便 安全 HTTP 请求。通常,用户通过 Spring Security 进行身份验证 通过使用某些机制,例如登录页面、HTTP 基本身份验证或其他方式。 经过身份验证的用户的安全上下文保存在 HTTP 会话中 ,并与同一基于 Cookie 的会话中的后续请求相关联。
因此,对于 WebSocket 握手或 Sockjs HTTP 传输请求,
通常,已经有一个经过身份验证的用户可通过 访问。Spring 会自动关联该用户
替换为它们创建的 WebSocket 或 Sockjs 会话,然后,使用所有
通过用户标头通过该会话传输的 STOMP 消息。HttpServletRequest#getUserPrincipal()
简而言之,典型的 Web 应用程序不需要执行任何操作
超越了它已经为安全所做的工作。用户通过
具有安全上下文的 HTTP 请求级别,该上下文通过基于 Cookie 的
HTTP 会话(然后与创建的 WebSocket 或 Sockjs 会话相关联
),并导致在每个流
通过应用程序。Message
STOMP 协议在帧上确实有 and 标头。
这些最初是为 TCP 上的 STOMP 设计的,并且是 STOMP 所必需的。但是,对于 STOMP
默认情况下,Spring 会忽略 STOMP 协议的身份验证标头
级别,并假定用户已在 HTTP 传输级别进行身份验证。
预期是 WebSocket 或 Sockjs 会话包含经过身份验证的用户。login
passcode
CONNECT
4.4.13. 令牌认证
Spring Security OAuth 支持基于令牌的安全性,包括 JSON Web 令牌 (JWT)。 您可以将其用作 Web 应用程序中的身份验证机制。 包括 WebSocket 交互上的 STOMP,如前面的 部分(即,通过基于 Cookie 的会话维护身份)。
同时,基于 Cookie 的会话并不总是最合适的(例如, 在不维护服务器端会话的应用程序中或在 移动应用程序,其中通常使用 Headers 进行身份验证)。
WebSocket 协议 RFC 6455“没有规定服务器在 WebSocket 握手。但是,在实践中,浏览器客户端只能使用标准的 authentication headers (即基本 HTTP authentication) 或 cookie 和 cannot (例如) 提供自定义标头。同样,Sockjs JavaScript 客户端也不提供 一种使用 Sockjs 传输请求发送 HTTP 标头的方法。参见 sockjs-client 问题 196。 相反,它确实允许发送可用于发送令牌的查询参数。 但这也有其自身的缺点(例如,令牌可能会无意中 使用服务器日志中的 URL 记录)。
上述限制适用于基于浏览器的客户端,不适用于 Spring 基于 Java 的 STOMP 客户端,它确实支持使用 WebSocket 和 Sockjs 请求。 |
因此,希望避免使用 cookie 的应用程序可能没有任何好处 HTTP 协议级别的身份验证替代方案。不使用 cookie, 他们可能更喜欢在 STOMP 消息传递协议级别使用 Headers 进行身份验证。 执行此操作需要两个简单的步骤:
-
使用 STOMP 客户端在连接时传递身份验证标头。
-
使用 .
ChannelInterceptor
下一个示例使用服务器端配置来注册自定义身份验证
拦截 器。请注意,拦截器只需要进行身份验证并将
CONNECT 上的 user 标头。Spring 记录并保存经过身份验证的
用户,并将其与同一会话上的后续 STOMP 消息相关联。以下内容
示例显示了如何注册自定义身份验证侦听器:Message
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ... ; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}
另请注意,当您使用 Spring Security 的消息授权时,目前,
您需要确保 authentication config 已排序
领先于 Spring Security 的。最好通过在
它自己的 implementation 用 .ChannelInterceptor
WebSocketMessageBrokerConfigurer
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
4.4.14. 授权
Spring Security 提供了 WebSocket 子协议授权,该授权使用 a 根据其中的 user Headers 对消息进行授权。
此外,Spring Session 还提供 WebSocket 集成,以确保用户的 HTTP 会话在 WebSocket 会话仍处于活动状态时不会过期。ChannelInterceptor
4.4.15. 用户目标
应用程序可以发送针对特定用户的消息,以及 Spring 的 STOMP 支持
识别前缀为 的目标 。
例如,客户端可能会订阅目标。 处理此目标并将其转换为
destination 对用户会话唯一(如 )。
这提供了订阅通用命名目标的便利性,而
同时,确保不会与订阅相同的其他用户发生冲突
目标,以便每个用户都可以接收唯一的 Stock Position 更新。/user/
/user/queue/position-updates
UserDestinationMessageHandler
/queue/position-updates-user123
使用用户目标时,配置 broker 和
应用程序目标前缀,如启用 STOMP 中所示,否则
broker 将处理带有 “/user” 前缀的消息,这些消息只能由 处理。UserDestinationMessageHandler |
在发送端,可以将消息发送到目标(如 ),而该目标又会被翻译
通过进入一个或多个目的地,每个目的地 1 个
会话。这允许应用程序中的任何组件
发送针对特定用户的消息,而不必了解更多信息
而不是其名称和通用目标。这也通过
annotation 和消息传递模板。/user/{username}/queue/position-updates
UserDestinationMessageHandler
消息处理方法可以将消息发送给与
通过 Annotation 处理的消息(在
类级别共享一个公共目标),如下例所示:@SendToUser
@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}
如果用户有多个会话,则默认情况下,订阅了所有会话
的目标。但是,有时,可能需要
仅以发送正在处理的消息的会话为目标。您可以通过以下方式执行此操作
将属性设置为 false,如下例所示:broadcast
@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception{
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}
虽然用户目标通常意味着经过身份验证的用户,但这并不是严格要求的。
未与已验证的用户关联的 WebSocket 会话
可以订阅用户目标。在这种情况下,注解
的行为与 with 完全相同(即,仅针对
会话发送正在处理的消息)。@SendToUser broadcast=false |
您可以从任何应用程序向用户目标发送消息
组件,例如,注入由 Java 配置创建的 API,或者
XML 命名空间。(如果需要,则为 bean 名称
用于限定。以下示例显示了如何执行此操作:SimpMessagingTemplate
brokerMessagingTemplate
@Qualifier
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
当您将用户目标与外部消息代理一起使用时,应检查代理
有关如何管理非活动队列的文档,以便在用户会话为
over,则删除所有唯一用户队列。例如,RabbitMQ 会创建自动删除
queues (当您使用目标时,例如 .
因此,在这种情况下,客户端可以订阅 。
同样,ActiveMQ 具有用于清除非活动目标的配置选项。/exchange/amq.direct/position-updates /user/exchange/amq.direct/position-updates |
在多应用程序服务器方案中,用户目标可能保持未解析状态,因为
用户已连接到其他服务器。在这种情况下,您可以配置
destination 广播未解析的消息,以便其他服务器有机会尝试。
这可以通过 Java 配置中的 property 和
的元素。userDestinationBroadcast
MessageBrokerRegistry
user-destination-broadcast
message-broker
4.4.16. 消息顺序
来自代理的消息将从它们所在的位置发布到 。
写入 WebSocket 会话。由于通道由 , messages 提供支持
在不同的线程中处理,并且客户端接收到的结果序列可能会
与发布的确切顺序不匹配。clientOutboundChannel
ThreadPoolExecutor
如果这是一个问题,请启用该标志,如下例所示:setPreservePublishOrder
@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker preserve-publish-order="true">
<!-- ... -->
</websocket:message-broker>
</beans>
设置该标志后,同一客户端会话中的消息将一次发布到一个客户端会话,以便保证发布顺序。
请注意,这会产生较小的性能开销,因此应仅在需要时启用它。clientOutboundChannel
4.4.17. 事件
发布了多个事件,并且可以
通过实现 Spring 的接口接收:ApplicationContext
ApplicationListener
-
BrokerAvailabilityEvent
:指示代理何时可用或不可用。 虽然 “simple” broker 在启动时立即可用,并且在 应用程序正在运行,则 STOMP“代理中继”可能会丢失其连接 添加到功能齐全的 Broker 中(例如,如果 Broker 已重新启动)。代理中继 具有 reconnect 逻辑并重新建立与 broker 的 “system” 连接 当它回来时。因此,每当状态从 connected 到 disconnected,反之亦然。使用 should 订阅此事件,并避免在 Broker 未发送消息时发送消息 可用。无论如何,他们都应该准备好在发送消息时进行处理。SimpMessagingTemplate
MessageDeliveryException
-
SessionConnectEvent
:在收到新的 STOMP CONNECT 时发布 指示新客户端会话的开始。该事件包含表示 connect,包括会话 ID、用户信息(如果有)和客户端的任何自定义标头 送。这对于跟踪客户端会话非常有用。订购的组件 )中,可以将包含的消息与 或 包装在一起。SimpMessageHeaderAccessor
StompMessageHeaderAccessor
-
SessionConnectedEvent
:在 a 之后不久发布 broker 已发送 STOMP CONNECTED 帧以响应 CONNECT.此时, STOMP 会话可以认为是完全建立的。SessionConnectEvent
-
SessionSubscribeEvent
:在收到新的 STOMP SUBSCRIBE 时发布。 -
SessionUnsubscribeEvent
:在收到新的 STOMP UNSUBSCRIBE 时发布。 -
SessionDisconnectEvent
:在 STOMP 会话结束时发布。DISCONNECT 可能会 已从客户端发送,或者它可能在 WebSocket 会话已关闭。在某些情况下,此事件会多次发布 每个会话。组件对于多个断开连接事件应该是幂等的。
当您使用功能齐全的代理时,STOMP“代理中继”会自动重新连接 “system” 连接。客户端连接、 但是,不会自动重新连接。假设启用了检测信号,则客户端 通常会注意到 Broker 在 10 秒内没有响应。客户需要 实现自己的 reconnecting logic。 |
4.4.18. 拦截
事件为生命周期提供通知
的 STOMP 连接,但并非针对每条客户端消息。应用程序还可以注册 a 来拦截任何消息和处理链的任何部分。
以下示例显示如何拦截来自客户端的入站消息:ChannelInterceptor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}
自定义可以使用 or 来访问有关消息的信息,如下例所示: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;
}
}
应用程序还可以实现 ,这是一个子接口
of 替换为处理消息的线程中的回调。
虽然 a 对发送到通道的每条消息调用一次,但 provides 钩子在每个订阅来自通道的消息的线程中。ExecutorChannelInterceptor
ChannelInterceptor
ChannelInterceptor
ExecutorChannelInterceptor
MessageHandler
请注意,与前面描述的一样,DISCONNECT 消息
可以来自客户端,也可以在
WebSocket 会话已关闭。在某些情况下,拦截器可能会拦截此
message 的请求。组件在
多个断开连接事件。SessionDisconnectEvent
4.4.19. STOMP 客户端
Spring 提供了一个基于 WebSocket 的 STOMP 客户端和一个基于 TCP 的 STOMP 客户端。
首先,您可以 create 和 configure ,如下例所示:WebSocketStompClient
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
在前面的示例中,您可以将 替换为 ,
因为这也是 .罐
使用 WebSocket 或基于 HTTP 的传输作为后备。有关详细信息,请参阅 SockJsClient
。StandardWebSocketClient
SockJsClient
WebSocketClient
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) {
// ...
}
}
建立会话后,可以发送任何有效负载,并且
serialized 替换为 ,如下例所示:MessageConverter
session.send("/topic/something", "payload");
您还可以订阅目标。这些方法需要一个处理程序
,并返回一个句柄,您可以
用于取消订阅。对于收到的每条消息,处理程序可以指定有效负载应反序列化为目标类型的目标类型,如下例所示:subscribe
Subscription
Object
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
要启用 STOMP 检测信号,您可以配置并选择性地自定义检测信号间隔(写入不活动为 10 秒,
这会导致发送检测信号,10 秒为读取不活动状态,这会导致
关闭连接)。WebSocketStompClient
TaskScheduler
WebSocketStompClient
仅在不活动的情况下发送心跳,即当 no
发送其他消息。使用外部代理时,这可能会带来挑战
因为具有非代理目标的消息表示活动,但实际上并非
转发给代理。在这种情况下,您可以在初始化 External Broker 时配置 一个,以确保
当只有带有非 broker 的消息时,heartbeat 也会转发到代理
destination 的 Destination 发送。TaskScheduler
当您用于性能测试以模拟数千个
的客户端,请考虑关闭检测信号,因为每个
Connection 会计划自己的检测信号任务,并且未针对
在同一台计算机上运行的大量客户端。WebSocketStompClient |
STOMP 协议还支持收据,其中 Client 端必须添加一个 Headers,服务器在发送或
subscribe 的 intent 请求。为了支持这一点,导致标头为
在每个后续 send 或 subscribe 事件上添加。
或者,您也可以手动将收据标头添加到 .
send 和 subscribe 都会返回一个实例,您可以使用该实例来注册接收成功和失败回调。
对于此功能,您必须为客户端配置收据过期前的时间(默认为 15 秒)。receipt
StompSession
setAutoReceipt(boolean)
receipt
StompHeaders
Receiptable
TaskScheduler
请注意,它本身是一个 ,它允许
除了
消息处理的异常和
传输级错误,包括 .StompSessionHandler
StompFrameHandler
handleException
handleTransportError
ConnectionLostException
4.4.20. WebSocket 作用域
每个 WebSocket 会话都有一个属性映射。该映射作为标题附加到 入站客户端消息,可以从控制器方法访问,如下例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
// ...
}
}
你可以在 scope 中声明一个 Spring 托管的 bean。
你可以将 WebSocket 范围的 bean 注入控制器和任何通道拦截器
已在 上注册。这些通常是单例和实时
比任何单个 WebSocket 会话都长。因此,您需要使用
范围 proxy 模式,如下例所示:websocket
clientInboundChannel
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@Controller
public class MyController {
private final MyBean myBean;
@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}
与任何自定义范围一样, Spring 会初始化一个新实例
从控制器访问实例并将实例存储在 WebSocket 中
会话属性。随后返回相同的实例,直到会话
结束。WebSocket 范围的 bean 调用了所有 Spring 生命周期方法,如
如前面的示例所示。MyBean
4.4.21. 性能
在性能方面没有灵丹妙药。许多因素 影响它,包括消息的大小和音量,是否应用 方法执行需要阻塞和外部因素的工作 (例如网络速度和其他问题)。本节的目标是提供 可用配置选项的概述以及一些想法 关于如何推断扩展。
在消息传递应用程序中,消息通过通道进行异步传递 由线程池支持的执行。配置此类应用程序需要 对频道和消息流有很好的了解。因此,它是 建议查看消息流。
显而易见的起点是配置支持 和 的线程池。默认情况下,两者
配置为可用处理器数量的两倍。clientInboundChannel
clientOutboundChannel
如果带注解的方法中消息的处理主要是 CPU 绑定的,则
的线程数应保持接近
处理器数量。如果他们所做的工作更受 IO 限制并且需要阻塞
或者等待数据库或其他外部系统,则线程池大小
可能需要增加。clientInboundChannel
一个常见的混淆点是配置核心池大小(例如,10) 最大池大小(例如,20)将导致线程池具有 10 到 20 个线程。 实际上,如果容量保留为默认值 Integer.MAX_VALUE, 线程池永远不会超过核心池大小,因为 所有其他任务都已排队。 请参阅 javadoc 了解这些属性的工作原理,以及
了解各种排队策略。 |
另一方面,这一切都与向 WebSocket 发送消息有关
客户。如果客户端位于快速网络上,则线程数应
保持接近可用处理器的数量。如果它们速度较慢或开启
低带宽,它们需要更长的时间来消耗消息,并给
thread 池。因此,增加线程池大小变得必要。clientOutboundChannel
虽然 的工作负载是可以预测的 — 毕竟,它基于应用程序的功能 — 如何配置
“clientOutboundChannel” 更难,因为它基于其他因素
应用程序的控制。因此,另外两个
属性与消息发送相关:和 .您可以使用这些方法来配置
send 允许接收以及发送时可以缓冲多少数据
消息。clientInboundChannel
sendTimeLimit
sendBufferSizeLimit
一般的思路是,在任何给定时间,只能使用单个线程 以发送到客户端。同时,所有其他消息都会被缓冲,而您 可以使用这些属性来决定允许发送消息的时间 take 以及在此期间可以缓冲多少数据。请参阅 javadoc 和 XML 架构的文档,以获取重要的其他详细信息。
以下示例显示了可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport send-timeout="15000" send-buffer-size="524288" />
<!-- ... -->
</websocket:message-broker>
</beans>
您还可以使用前面显示的 WebSocket 传输配置来配置 传入 STOMP 消息的最大允许大小。理论上,WebSocket 消息的大小几乎可以是无限的。在实践中,WebSocket 服务器强加 限制 — 例如,Tomcat 上为 8K,Jetty 上为 64K。因此,STOMP 客户端 (例如 JavaScript webstomp-client 等)在 16K 边界处拆分较大的 STOMP 消息,并将其作为多个发送 WebSocket 消息,这需要服务器缓冲并重新组装。
Spring 的 STOMP-over-WebSocket 支持可以做到这一点,因此应用程序可以配置 STOMP 消息的最大大小,与 WebSocket 服务器特定的消息无关 大小。请记住,WebSocket 消息大小会自动 如有必要,以确保它们可以在 最低。
以下示例显示了一种可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport message-size="131072" />
<!-- ... -->
</websocket:message-broker>
</beans>
关于扩展的一个重要点涉及使用多个应用程序实例。 目前,您无法使用 simple broker 执行此操作。 但是,当您使用功能齐全的代理(如 RabbitMQ)时,每个应用程序 实例连接到代理,并且从一个应用程序广播消息 实例可以通过 broker 广播到连接的 WebSocket 客户端 通过任何其他应用程序实例。
4.4.22. 监控
当您使用 或 、 key 时
基础设施组件自动收集统计数据和计数器,这些统计数据和计数器提供
对应用程序内部状态的重要见解。配置
还声明了一个 bean 类型,该 bean 将所有
可用信息,默认情况下,它会记录在级别一次
每 30 分钟一班。此 bean 可以通过 Spring 导出到 JMX,以便在运行时查看(例如,通过 JDK)。
以下列表总结了可用信息:@EnableWebSocketMessageBroker
<websocket:message-broker>
WebSocketMessageBrokerStats
INFO
MBeanExporter
jconsole
- 客户端 WebSocket 会话
-
- 当前
-
指示有多少个客户端会话 目前,计数进一步按 WebSocket 与 HTTP 细分 流式传输和轮询 Sockjs 会话。
- 总
-
指示已建立的会话总数。
- 异常闭合
-
- 连接失败
-
已建立但 在 60 秒内未收到任何消息后关闭。这是 通常表示代理或网络问题。
- 超出发送限制
-
会话在超过配置的发送后关闭 timeout 或 send buffer 限制(可能发生在慢速客户端) (请参阅上一节)。
- 传输错误
-
会话在传输错误后关闭,例如 无法读取或写入 WebSocket 连接,或者 HTTP 请求或响应。
- STOMP 框架
-
CONNECT、CONNECTED 和 DISCONNECT 帧的总数 processed,指示在 STOMP 级别连接的客户端数。请注意, 当会话异常关闭或 客户端关闭而不发送 DISCONNECT 帧。
- STOMP 代理中继
-
- TCP 连接
-
指示代表客户端的 TCP 连接数 为代理建立 WebSocket 会话。这应该等于 客户端 WebSocket 会话数 + 1 个额外的共享“系统”连接 用于从应用程序内发送消息。
- STOMP 框架
-
CONNECT、CONNECTED 和 DISCONNECT 帧的总数 转发给经纪人或从经纪人代表客户接收。请注意, DISCONNECT 帧将发送到代理,而不管客户端 WebSocket 如何 会话已关闭。因此,DISCONNECT 帧数较低 代理正在主动关闭连接(可能是由于 检测信号未及时到达、输入帧无效或其他问题)。
- 客户端入站通道
-
来自支持线程池的统计信息,用于深入了解传入消息处理的运行状况。任务排队 此处指示应用程序可能太慢,无法处理消息。 如果存在 I/O 绑定任务(例如,数据库查询速度慢、对第三方的 HTTP 请求 REST API 等),请考虑增加线程池大小。
clientInboundChannel
- 客户端出站通道
-
来自支持线程池的统计信息,该线程池提供了对向客户端广播消息的运行状况的见解。任务 此处排队表示客户端使用消息的速度太慢。 解决此问题的一种方法是增加线程池大小以容纳 预期的并发慢速客户端数。另一种选择是将 发送超时和发送缓冲区大小限制(请参阅上一节)。
clientOutboundChannel
- Sockjs 任务调度程序
-
来自 Sockjs 任务调度程序线程池的统计信息 用于发送检测信号。请注意,当在 STOMP 级别,则禁用 SockJS 心跳。
4.4.23. 测试
使用 Spring 的 STOMP-over-WebSocket 时,有两种主要方法可以测试应用程序 支持。第一种是编写服务器端测试来验证功能 控制器及其带注释的消息处理方法。第二种是写 涉及运行客户端和服务器的完整端到端测试。
这两种方法并不相互排斥。相反,每个都有其位置 在整体测试策略中。服务器端测试更集中,更易于编写 并维护。另一方面,端到端集成测试更完整,并且 测试的次数更多,但它们的编写和维护也更多。
服务器端测试的最简单形式是编写控制器单元测试。然而 这还不够有用,因为控制器的大部分工作都取决于其 附注。纯粹的单元测试根本无法测试这一点。
理想情况下,被测控制器应该像在运行时一样被调用,这与 使用 Spring MVC Test 测试处理 HTTP 请求的控制器的方法 框架 — 也就是说,不运行 Servlet 容器,而是依赖 Spring Framework 调用带注释的控制器。与 Spring MVC Test 一样,您有两个 此处可能的替代方案,要么使用 “context-based” 要么使用 “standalone” 设置:
-
在 Spring TestContext 框架,作为测试字段注入,以及 使用它来发送要由 Controller 方法处理的消息。
clientInboundChannel
-
手动设置调用 控制器(即 )并传递消息 控制器直接连接到它。
SimpAnnotationMethodMessageHandler
这两个设置场景都在 tests for the stock portfolio 示例应用程序中进行了演示。
第二种方法是创建端到端集成测试。为此,您需要 以嵌入式模式运行 WebSocket 服务器并作为 WebSocket 客户端连接到它 发送包含 STOMP 帧的 WebSocket 消息。 Stock portfolio 示例应用程序的测试也通过使用 Tomcat 作为嵌入式 WebSocket 服务器和一个简单的 STOMP 客户端进行测试。
5. 其他 Web 框架
本章详细介绍了 Spring 与第三方 Web 框架的集成。
Spring 框架的核心价值主张之一是支持选择。一般来说,Spring 不会强迫您使用或购买任何 特定的架构、技术或方法(尽管它肯定建议 一些优于其他)。这种自由选择架构、技术或 与开发人员及其开发团队最相关的方法是 可以说在 Web 领域最为明显,Spring 提供了自己的 Web 框架 (Spring MVC 和 Spring WebFlux),同时, 支持与许多流行的第三方 Web 框架集成。
5.1. 通用配置
在深入研究每个受支持的 Web 框架的集成细节之前,让我们 首先看一下不特定于任何一个 Web 的常见 Spring 配置 框架。(本节同样适用于 Spring 自己的 Web 框架变体.)
Spring 的轻量级
应用程序模型是分层体系结构的模型。请记住,在“经典”
分层架构,Web 层只是众多层之一。它充当
入口点,并委托给服务对象
(立面)来满足特定于业务的(以及
presentation-technology agnostic) 用例。在 Spring 中,这些服务对象、任何其他
特定于业务的对象、数据访问对象和其他对象存在于不同的“业务
context“,该对象不包含 Web 或表示层对象(表示对象、
,通常在不同的“表示
context“)的本节详细介绍了如何配置包含应用程序中的所有“业务 bean”的 Spring 容器 (a)。WebApplicationContext
继续具体操作,您需要做的就是在 Web 应用程序的标准 Java EE servlet 文件中声明一个 ContextLoaderListener
,并添加一个 <context-param/> 部分(在同一个文件中)来定义哪个
要加载的 Spring XML 配置文件集。web.xml
contextConfigLocation
请考虑以下配置:<listener/>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
进一步考虑以下配置:<context-param/>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
如果未指定 context 参数,则会查找调用
负荷。一旦上下文文件被加载,Spring 就会根据 bean 定义创建一个 WebApplicationContext
对象,并将其存储在 Web 中
应用。contextConfigLocation
ContextLoaderListener
/WEB-INF/applicationContext.xml
ServletContext
所有 Java Web 框架都是在 Servlet API 之上构建的,因此您可以使用
以下代码片段来访问由 .ApplicationContext
ContextLoaderListener
以下示例演示如何获取 :WebApplicationContext
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
WebApplicationContextUtils
类是为了方便起见,因此您无需记住属性的名称。其方法返回
在 key 下不存在。与其冒险进入您的应用程序,不如
以使用该方法。此方法会引发异常
当 缺失。ServletContext
getWebApplicationContext()
null
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
NullPointerExceptions
getRequiredWebApplicationContext()
ApplicationContext
一旦你有了对 的引用,就可以按它们的 bean 来检索 bean
name 或 type。大多数开发人员按名称检索 bean,然后将它们强制转换为他们的
实现的接口。WebApplicationContext
幸运的是,本节中的大多数框架都有更简单的方法来查找 bean。 它们不仅可以轻松地从 Spring 容器中获取 bean,而且还允许您 在他们的控制器上使用依赖注入。每个 Web 框架部分都有更多详细信息 关于其具体的整合策略。
5.2. JSF
JavaServer Faces (JSF) 是 JCP 的标准、基于组件、事件驱动的 Web 用户界面框架。它是 Java EE 保护伞的正式组成部分,但也是 可单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。
请注意,最新版本的 JSF 与 CDI 基础设施密切相关 在应用服务器中,一些新的 JSF 功能只能在这样的 环境。Spring 的 JSF 支持不再积极发展,主要是 存在是为了在对基于 JSF 的旧应用程序进行现代化改造时进行迁移。
Spring 的 JSF 集成中的关键元素是 JSF 机制。ELResolver
5.2.1. Spring Bean 解析器
SpringBeanFacesELResolver
是符合 JSF 的实现,
与 JSF 和 JSP 使用的标准统一 EL 集成。它委托给
Spring 的 “business context” (业务上下文),然后转到
基础 JSF 实现的 default 解析程序。ELResolver
WebApplicationContext
在配置方面,您可以在 JSF 文件中定义,如下例所示:SpringBeanFacesELResolver
faces-context.xml
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
5.2.2. 使用FacesContextUtils
在将属性映射到 中的 bean 时,自定义效果很好,但有时可能需要显式获取 bean。
FacesContextUtils
类使此操作变得简单。它与 类似,只是
它需要一个参数而不是一个参数。ELResolver
faces-config.xml
WebApplicationContextUtils
FacesContext
ServletContext
以下示例演示如何使用:FacesContextUtils
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
5.3. Apache Struts 2.x
Struts 由 Craig McClanahan 发明,是一个开源项目 由 Apache Software Foundation 托管。当时,它大大简化了 JSP/Servlet 编程范式,并赢得了许多使用专有 框架。它简化了编程模型,它是开源的(因此是免费的,如 啤酒),并且它有一个庞大的社区,这让该项目得以发展并在其中流行起来 Java Web 开发人员。
作为原始 Struts 1.x 的后续版本,请查看 Struts 2.x 和 Struts 提供的 Spring 插件。 内置的 Spring 集成。
5.4. Apache Tapestry 5.x
Tapestry 是一个面向组件的框架,用于创建 动态、健壮、高度可扩展的 Java Web 应用程序。
虽然 Spring 有自己强大的 Web 层,但还有许多独特的 使用 Tapestry 组合构建企业 Java 应用程序的优势 用于 Web 用户界面,Spring 容器用于较低层。
有关更多信息,请参阅 Tapestry 的 Spring 专用集成模块。