Web Servlet
1. Spring Web MVC
Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,并且从一开始就包含在Spring 框架中。正式名称“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 框架一样,是围绕前端控制器设计的模式,其中中央Servlet
这DispatcherServlet
,提供共享算法用于请求处理,而实际工作由可配置的委托组件执行。该模型灵活,支持多种工作流程。
这DispatcherServlet
,与任何Servlet
,需要根据使用 Java 配置或在web.xml
. 反过来,DispatcherServlet
使用 Spring 配置来发现请求映射、视图解析、异常处理等所需的委托组件。
以下 Java 配置示例将 这DispatcherServlet
,由 Servlet 容器自动检测(参见 Servlet 配置):
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// Load Spring web application configuration
val context = AnnotationConfigWebApplicationContext()
context.register(AppConfig::class.java)
// Create and register the DispatcherServlet
val servlet = DispatcherServlet(context)
val registration = servletContext.addServlet("app", servlet)
registration.setLoadOnStartup(1)
registration.addMapping("/app/*")
}
}
除了直接使用 ServletContext API 之外,您还可以扩展AbstractAnnotationConfigDispatcherServletInitializer 并覆盖特定方法(请参阅上下文层次结构下的示例)。 |
对于编程用例,一个GenericWebApplicationContext 可以用作替代AnnotationConfigWebApplicationContext . 请参阅GenericWebApplicationContext javadoc 了解详情。 |
以下示例web.xml
配置寄存器并初始化DispatcherServlet
:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Spring Boot 遵循不同的初始化顺序。Spring Boot 不是挂接到Servlet 容器的生命周期,而是使用 Spring 配置来bootstrap 本身和嵌入式 Servlet 容器。Filter 和Servlet 声明 在 Spring 配置中检测到并注册到 Servlet 容器中。有关更多详细信息,请参阅 Spring Boot 文档。 |
1.1.1. 上下文层次结构
DispatcherServlet
期望WebApplicationContext
(平原的延伸ApplicationContext
) 用于自己的配置。WebApplicationContext
有一个指向ServletContext
和Servlet
与之关联。它还绑定到ServletContext
这样应用程序就可以在RequestContextUtils
查找WebApplicationContext
如果他们需要访问它。
对于许多应用程序,具有单个WebApplicationContext
很简单,就足够了。也可以有一个上下文层次结构,其中一个根WebApplicationContext
在多个DispatcherServlet
(或其他Servlet
) 实例,每个实例都有它自己的子WebApplicationContext
配置。 看的附加功能ApplicationContext
有关上下文层次结构功能的更多信息。
根WebApplicationContext
通常包含基础设施 bean,例如数据存储库和需要在多个之间共享的业务服务Servlet
实例。 这些 bean是有效继承的,并且可以在特定于 Servlet 的 孩子WebApplicationContext
,它通常包含给定Servlet
. 下图显示了这种关系:

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

上图显示了三个消息通道:
-
clientInboundChannel
:用于传递从 WebSocket 客户端接收的消息。 -
clientOutboundChannel
:用于向 WebSocket 客户端发送服务器消息。 -
brokerChannel
:用于从内部向消息代理发送消息 服务器端应用程序代码。
下图显示了外部代理(例如 RabbitMQ)时使用的组件 配置为管理订阅和广播消息:

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