Web Servlet
1. Spring Web MVC
Spring Web MVC 是构建在 Servlet API 上的原始 Web 框架,已被包含在内
在 Spring Framework 中。正式名称“Spring Web MVC”,
来自其源模块的名称
(spring-webmvc
),
但它更通常被称为 “Spring MVC”。
与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个反应式堆栈 Web 框架
其名称“Spring WebFlux”也基于其源模块
(spring-webflux
).
本节介绍 Spring Web MVC。下一节介绍 Spring WebFlux。
有关基准信息以及与 Servlet 容器和 Java EE 版本的兼容性 范围,请参阅 Spring Framework Wiki。
1.1. DispatcherServlet
Spring MVC 与许多其他 Web 框架一样,是围绕前端控制器设计的
模式,其中 CENTRALServlet
这DispatcherServlet
提供共享算法
用于请求处理,而实际工作则由可配置的委托组件执行。
此模型非常灵活,并支持多种工作流。
这DispatcherServlet
一样,就像任何Servlet
,需要根据
到 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 并覆盖特定方法
(请参阅 Context Hierarchy 下的示例)。 |
以下示例web.xml
configuration 注册并初始化DispatcherServlet
:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
Spring Boot 遵循不同的初始化 Sequences。而不是挂接
Servlet 容器的生命周期中,Spring Boot 使用 Spring 配置来
bootstrap 本身和嵌入式 Servlet 容器。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
实例。那些豆子
可以有效地继承,并且可以在特定于 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
:
Bean 类型 | 解释 |
---|---|
|
将请求映射到处理程序以及用于预处理和后处理的拦截器列表。
映射基于一些标准,其详细信息因 两个主要的 |
|
帮助 |
解决异常的策略,可能将它们映射到处理程序,再到 HTML 错误 视图或其他目标。请参阅例外。 |
|
解析逻辑 |
|
解决 |
|
解决 Web 应用程序可以使用的主题,例如,提供个性化布局。 请参阅主题。 |
|
用于解析多部分请求(例如,浏览器表单文件上传)的抽象 一些 Multipart 解析库的帮助。请参阅Multipart Resolver。 |
|
存储和检索 “input” 和 “output” |
1.1.3. Web MVC 配置
应用程序可以声明处理请求所需的 Special Bean Types 中列出的基础结构 Bean。这DispatcherServlet
检查WebApplicationContext
对于每个特殊 bean。如果没有匹配的 bean 类型,则
它回退到DispatcherServlet.properties
.
在大多数情况下,MVC 配置是最好的起点。它声明了所需的 bean 中,并提供更高级别的配置回调 API,以 自定义它。
Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC 和 提供了许多额外的便捷选项。 |
1.1.4. Servlet 配置
在 Servlet 3.0+ 环境中,您可以选择配置 Servlet 容器
以编程方式作为替代方案或与web.xml
文件。以下内容
example 注册一个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
protected 的AbstractDispatcherServletInitializer
提供了一个位置来在DispatcherServlet
和所有
映射到它的 filters。默认情况下,此标志设置为true
.
最后,如果您需要进一步自定义DispatcherServlet
本身,您可以
覆盖createDispatcherServlet
方法。
1.1.5. 处理
这DispatcherServlet
按如下方式处理请求:
-
这
WebApplicationContext
在请求中作为属性进行搜索和绑定 控制器和进程中的其他元素可以使用。默认情况下它是绑定的 在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
钥匙。 -
区域设置解析器绑定到请求,以 let 进程中的元素 解析在处理请求(渲染视图、准备 data 等)。如果不需要 locale 解析,则不需要 locale 解析程序。
-
主题解析程序绑定到请求,让视图等元素确定 使用哪个主题。如果您不使用主题,则可以忽略它。
-
如果指定 multipart 文件解析程序,则会检查请求是否存在 multipart。如果 multipart 的 Fragment 中,请求被包装在
MultipartHttpServletRequest
为 在此过程中通过其他元素进行进一步处理。有关详细信息,请参阅 Multipart Resolver 有关分段处理的信息。 -
寻找合适的处理程序。如果找到处理程序,则执行链 与处理程序(预处理器、后处理器和控制器)关联的是 run 准备用于渲染的模型。或者,对于带注释的 控制器,响应可以渲染(在
HandlerAdapter
) 而不是 返回一个视图。 -
如果返回模型,则呈现视图。如果未返回任何模型(可能是由于 拦截请求的预处理器或后处理器,可能是为了安全 reasons),则不会呈现任何视图,因为请求可能已经完成。
这HandlerExceptionResolver
在WebApplicationContext
习惯于
解决请求处理期间引发的异常。这些异常解析程序允许
自定义逻辑以解决异常。有关更多详细信息,请参阅例外。
SpringDispatcherServlet
还支持返回last-modification-date
,由 Servlet API 指定。确定过程
特定请求的最后修改日期非常简单:该DispatcherServlet
查找适当的处理程序映射,并测试
处理程序实现LastModified
接口。如果是这样,则long getLastModified(request)
方法LastModified
interface 将返回到
客户端。
您可以自定义单个DispatcherServlet
实例
初始化参数 (init-param
元素)添加到web.xml
文件。下表列出了支持的参数:
参数 | 解释 |
---|---|
|
实现 |
|
传递给上下文实例(由 |
|
的命名空间 |
|
是否抛出 默认情况下,此设置为 请注意,如果默认 servlet 处理为 此外,未解析的请求始终转发到默认 servlet 并且 404 永远不会提高。 |
1.1.6. 拦截
都HandlerMapping
implementations 支持处理程序拦截器,这些拦截器在以下情况下很有用
您希望将特定功能应用于某些请求 — 例如,检查
一个 principal。拦截器必须实现HandlerInterceptor
从org.springframework.web.servlet
package 中,这三个方法应该提供足够的
灵活地进行各种前处理和后处理:
-
preHandle(..)
:在实际处理程序运行之前 -
postHandle(..)
:处理程序运行后 -
afterCompletion(..)
:完成请求完成后
这preHandle(..)
method 返回一个布尔值。您可以使用此方法 break 或
继续处理执行链。当此方法返回true
这
处理程序执行链继续。当它返回 false 时,DispatcherServlet
假设拦截器本身已经处理了请求(例如,渲染了一个
appropriate 视图),并且不会继续执行其他拦截器和实际的
处理程序。
有关如何
配置拦截器。您也可以通过使用 setter 对单个HandlerMapping
实现。
请注意,postHandle
不太有用@ResponseBody
和ResponseEntity
方法
中写入并提交响应,并在HandlerAdapter
和之前postHandle
.这意味着现在对响应进行任何更改都为时已晚,例如将
一个额外的标头。对于此类方案,您可以实现ResponseBodyAdvice
以及
将其声明为 Controller Advice bean 或直接在RequestMappingHandlerAdapter
.
1.1.7. 异常
如果在请求映射期间发生异常,或者从请求处理程序(例如
一个@Controller
)、DispatcherServlet
delegates 到HandlerExceptionResolver
bean 来解决异常并提供替代处理,这通常是
error 响应。
下表列出了可用的HandlerExceptionResolver
实现:
HandlerExceptionResolver |
描述 |
---|---|
|
异常类名称和错误视图名称之间的映射。用于渲染 浏览器应用程序中的错误页面。 |
解决 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码。
参见 alternative |
|
|
使用 |
|
通过调用 |
解析器链
您可以通过声明多个HandlerExceptionResolver
bean 并设置其order
属性。
order 属性越高,异常解析程序的位置就越晚。
的合同HandlerExceptionResolver
指定它可以返回:
-
一个
ModelAndView
这指向一个错误视图。 -
空的
ModelAndView
如果异常是在 Resolver 中处理的。 -
null
如果异常仍未解决,则供后续解析程序尝试,并且如果 exception 保留在末尾,则允许它向上冒泡到 Servlet 容器。
MVC Config 自动为默认 Spring MVC 声明内置解析器
exceptions, 对于@ResponseStatus
注解的异常,以及为了支持@ExceptionHandler
方法。您可以自定义或替换该列表。
容器错误页面
如果任何HandlerExceptionResolver
因此,
left 进行传播,或者如果响应状态设置为错误状态(即 4xx、5xx),
Servlet 容器可以在 HTML 中呈现默认错误页。自定义默认
error page 中,可以在web.xml
.
以下示例显示了如何执行此作:
<error-page>
<location>/error</location>
</error-page>
在前面的示例中,当异常冒泡或响应具有错误状态时,
Servlet 容器在容器内向配置的 URL 发出 ERROR 调度
(例如,/error
).然后由DispatcherServlet
,可能映射它
更改为@Controller
,该 URL 可以实现以返回带有 model 的错误视图名称
或呈现 JSON 响应,如下例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
@RestController
class ErrorController {
@RequestMapping(path = ["/error"])
fun handle(request: HttpServletRequest): Map<String, Any> {
val map = HashMap<String, Any>()
map["status"] = request.getAttribute("javax.servlet.error.status_code")
map["reason"] = request.getAttribute("javax.servlet.error.message")
return map
}
}
Servlet API 不提供在 Java 中创建错误页面映射的方法。您可以
但是,请同时使用WebApplicationInitializer 和最小的web.xml . |
1.1.8. 视图分辨率
Spring MVC 定义了ViewResolver
和View
允许您渲染
模型,而无需将您绑定到特定的视图技术。ViewResolver
提供视图名称和实际视图之间的映射。View
解决准备工作
的数据,然后再移交给特定的视图技术。
下表提供了有关ViewResolver
等级制度:
视图解析器 | 描述 |
---|---|
|
的子类 |
|
实现 |
|
实现 |
|
简单实现 |
|
方便的子类 |
|
方便的子类 |
|
实现 |
处理
您可以通过声明多个解析器 Bean 来链接视图解析器,如有必要,还可以通过
设置order
属性来指定排序。请记住,order 属性越高,
View Resolver 在链中的位置越晚。
一个ViewResolver
指定它可以返回 null,以指示
找不到视图。但是,对于 JSP 和InternalResourceViewResolver
,
确定 JSP 是否存在的唯一方法是通过RequestDispatcher
.因此,您必须始终配置InternalResourceViewResolver
在 View Resolver 的总体顺序中排在最后。
配置视图分辨率就像添加ViewResolver
beans 到你的 Spring
配置。MVC Config 为视图解析器和添加无逻辑视图控制器提供了一个专用的配置 API,这对 HTML 模板很有用
不使用控制器逻辑的渲染。
重 定向
特别的redirect:
prefix 允许您执行重定向。这UrlBasedViewResolver
(及其子类)将此视为一条指令,
redirect 是必需的。视图名称的其余部分是重定向 URL。
实际效果与控制器返回RedirectView
,但现在
控制器本身可以根据 logical view name 进行作。逻辑视图
name(例如redirect:/myapp/some/resource
) 相对于当前
Servlet 上下文中,而redirect:https://myhost.com/some/arbitrary/path
重定向到绝对 URL。
请注意,如果控制器方法带有@ResponseStatus
、批注
值优先于 Response Status (响应状态) 由RedirectView
.
转发
您还可以使用特殊的forward:
前缀 (前缀) 表示视图名称
最终由UrlBasedViewResolver
和子类。这将创建一个InternalResourceView
,它会执行RequestDispatcher.forward()
.
因此,此前缀对InternalResourceViewResolver
和InternalResourceView
(对于 JSP),但如果您使用其他视图,则可能会有所帮助
技术,但仍希望强制将资源的 forward 由
Servlet/JSP 引擎。请注意,您也可以链接多个视图解析器。
内容协商
ContentNegotiatingViewResolver
不解析视图本身,而是解析 delegates
添加到其他视图解析程序,并选择类似于所请求的表示的视图
由客户。表示可以从Accept
标头或从
query 参数(例如"/path?format=pdf"
).
这ContentNegotiatingViewResolver
选择适当的View
处理请求
通过将请求媒体类型与媒体类型(也称为Content-Type
) 由View
与其每个ViewResolvers
.这
第一View
在列表中,具有兼容的Content-Type
返回表示形式
到客户端。如果ViewResolver
链
通过DefaultViews
property 的这
后一个选项适用于单例Views
,这可以呈现适当的
表示当前资源,而不考虑逻辑视图名称。这Accept
header 可以包含通配符(例如text/*
),在这种情况下,会触发View
谁的Content-Type
是text/xml
是兼容的匹配项。
有关配置详细信息,请参阅 MVC Config 下的 View Resolvers。
1.1.9. 语言环境
Spring 架构的大部分都支持国际化,因为 Spring Web
MVC 框架可以。DispatcherServlet
用于自动解决消息
通过使用客户端的区域设置。这是通过LocaleResolver
对象。
当请求传入时,DispatcherServlet
查找 locale 解析程序,如果
找到一个,它会尝试使用它来设置 locale。通过使用RequestContext.getLocale()
方法,您始终可以检索由 locale 解析程序解析的区域设置。
除了自动 locale 解析之外,您还可以将拦截器附加到 handler 映射(有关 handler 的更多信息,请参阅 Interception mapping interceptor)来更改特定情况下的区域设置(例如 基于请求中的参数)。
区域设置解析器和拦截器在org.springframework.web.servlet.i18n
包中,并在您的应用程序中进行配置
context 的 intent 中。以下 locale 解析程序选择包含在
Spring。
时区
除了获取客户端的区域设置之外,了解其时区通常也很有用。
这LocaleContextResolver
interface 提供对LocaleResolver
那让我们
解析器提供更丰富的LocaleContext
,其中可能包括时区信息。
如果可用,用户的TimeZone
可以使用RequestContext.getTimeZone()
方法。自动使用时区信息
按任意日期/时间Converter
和Formatter
对象,这些对象在 Spring 的ConversionService
.
Cookie 解析程序
此 locale 解析器检查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
在 Dispatcher 的
应用程序上下文。下一个示例显示对*.view
资源
,其中包含一个名为siteLanguage
现在更改区域设置。所以,例如,
对 URL 的请求,https://www.sf.net/home.view?siteLanguage=nl
,更改站点
语言到荷兰语。以下示例显示如何拦截 locale:
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
1.1.10. 主题
您可以应用 Spring Web MVC 框架主题来设置 应用程序,从而增强用户体验。主题是 static 的集合 资源(通常是样式表和图像),这些资源会影响 应用。
定义主题
要在 Web 应用程序中使用主题,您必须设置org.springframework.ui.context.ThemeSource
接口。这WebApplicationContext
接口扩展ThemeSource
而是将其职责委托给专用的
实现。默认情况下,委托是一个org.springframework.ui.context.support.ResourceBundleThemeSource
实现
从 Classpath 的根目录加载属性文件。使用自定义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
使用空的 base name 前缀。因此,
属性文件是从 Classpath 的根目录加载的。因此,您将cool.properties
theme 定义(对于
示例,在/WEB-INF/classes
).这ResourceBundleThemeSource
使用标准的 Java
Resource Bundle 加载机制,允许主题完全国际化。为
例如,我们可以有一个/WEB-INF/classes/cool_nl.properties
引用一个特殊的
带有荷兰语文本的背景图像。
解决主题
定义主题后,如上一节所述,
您可以决定使用哪个主题。这DispatcherServlet
查找名为themeResolver
了解具体ThemeResolver
implementation 来使用。主题解析程序的工作方式大致相同
方式作为LocaleResolver
.它检测要用于特定请求的主题,还可以
更改请求的主题。下表描述了 Spring 提供的主题解析程序:
类 | 描述 |
---|---|
|
选择固定主题,使用 |
|
主题在用户的 HTTP 会话中维护。它只需要为 每个会话,但不会在会话之间保留。 |
|
所选主题存储在客户端上的 Cookie 中。 |
Spring 还提供了一个ThemeChangeInterceptor
这允许主题在每个
request 替换为简单的请求参数。
1.1.11. Multipart Resolver (分段解析程序)
MultipartResolver
从org.springframework.web.multipart
package 是一种策略
用于解析分段请求,包括文件上传。有一个实现
基于 Commons FileUpload 和另一个
基于 Servlet 3.0 多部分请求解析。
要启用分段处理,您需要声明一个MultipartResolver
bean 中的DispatcherServlet
名称为multipartResolver
.
这DispatcherServlet
检测到它并将其应用于传入请求。当带有
content-type 为multipart/form-data
收到,解析程序会解析内容,并且
将当前HttpServletRequest
如MultipartHttpServletRequest
自
除了将已解析的部件作为请求参数公开外,还提供对它们的访问权限。
Apache 共享资源FileUpload
使用 Apache CommonsFileUpload
中,您可以配置类型为CommonsMultipartResolver
替换为 namemultipartResolver
.您还需要
有commons-fileupload
作为 Classpath 的依赖项。
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
替换为 namemultipartResolver
.
1.1.12. 日志记录
Spring MVC 中的 DEBUG 级日志记录被设计为紧凑、最小和 人性化。它侧重于有用的高价值信息 over again 与仅在调试特定问题时有用的其他方法进行验证。
TRACE 级别的日志记录通常遵循与 DEBUG 相同的原则(例如,还遵循 不应是消防水带),但可用于调试任何问题。此外,一些日志 消息在 TRACE 和 DEBUG 中可能显示不同级别的详细信息。
良好的日志记录来自使用日志的经验。如果您发现任何 未达到既定目标,请告诉我们。
敏感数据
DEBUG 和 TRACE 日志记录可能会记录敏感信息。这就是为什么请求参数和
默认情况下,标头是屏蔽的,并且必须显式启用其完整日志记录
通过enableLoggingRequestDetails
属性DispatcherServlet
.
以下示例演示如何使用 Java 配置执行此作:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletMappings(): Array<String> {
return ...
}
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
registration.setInitParameter("enableLoggingRequestDetails", "true")
}
}
1.2. 过滤器
这spring-web
module 提供了一些有用的过滤器:
1.2.1. 表单数据
浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以
使用 HTTP PUT、PATCH 和 DELETE。Servlet API 需要ServletRequest.getParameter*()
方法仅支持 HTTP POST 的表单字段访问。
这spring-web
module 提供FormContentFilter
拦截 HTTP PUT、PATCH 和 DELETE
内容类型为application/x-www-form-urlencoded
中,从
请求正文,并将ServletRequest
创建表单数据
可通过ServletRequest.getParameter*()
方法系列。
1.2.2. 请求头转发
当请求通过代理(例如负载均衡器)时,主机、端口和 方案可能会更改,这使得创建指向正确 Host、Port 和 Scheme。
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
那
应该使用修改后的请求,而不是原始请求。
转发的 Headers 存在安全注意事项,因为应用程序无法知道
标头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么
应将信任边界的代理配置为删除 UntrustedForwarded
来自外部的标头。您还可以配置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 配置)
所有过滤器都会自动为所有 Dispatch 类型注册。但是,如果注册
通过web.xml
或在 Spring Boot 中通过FilterRegistrationBean
请务必包括DispatcherType.ASYNC
.
1.3. 带注解的控制器
Spring MVC 提供了一个基于 Comments 的编程模型,其中@Controller
和@RestController
组件使用注解来表示请求映射、请求输入、
异常处理等。带 Comments 的控制器具有灵活的方法签名和
不必扩展基类,也不必实现特定的接口。
以下示例显示了由 annotations 定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
import org.springframework.ui.set
@Controller
class HelloController {
@GetMapping("/hello")
fun handle(model: Model): String {
model["message"] = "Hello World!"
return "index"
}
}
在前面的示例中,该方法接受Model
并返回视图名称作为String
,
但还存在许多其他选项,本章稍后将对此进行解释。
spring.io 上的指南和教程使用基于注释的 编程模型。 |
1.3.1. 声明
您可以通过在
Servlet 的WebApplicationContext
.这@Controller
stereotype 允许自动检测,
与 Spring 对检测的常规支持一致@Component
类路径中的 classes
以及为它们自动注册 bean 定义。它还充当
annotated 类,指示其作为 Web 组件的角色。
要启用此类@Controller
beans 中,你可以将组件扫描添加到
您的 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 的接口
callback(例如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 模式
您可以使用 glob 模式和通配符映射请求:
模式 | 描述 | 例 |
---|---|---|
|
匹配一个字符 |
比赛 |
|
匹配路径段中的零个或多个字符 |
|
|
匹配零个或多个路径段,直到路径的结尾 |
|
|
匹配路径段并将其捕获为名为 “name” 的变量 |
|
|
匹配 regexp |
|
捕获的 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
compiler 标志。
语法{varName:regex}
声明一个带有正则表达式的 URI 变量,该正则表达式具有
语法{varName:regex}
.例如,给定的 URL"/spring-web-3.0.5 .jar"
,则使用以下方法
提取名称、版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
// ...
}
URI 路径模式也可以嵌入${…}
启动时解析的占位符
通过使用PropertyPlaceHolderConfigurer
针对 Local、System、Environment 和其他属性
来源。例如,您可以使用它来根据某些外部 URL 参数化
配置。
Spring MVC 使用PathMatcher Contract 和AntPathMatcher implementation fromspring-core 进行 URI 路径匹配。 |
形态比较
当多个模式与一个 URL 匹配时,必须比较它们以找到最佳匹配。此作已完成
通过使用AntPathMatcher.getPatternComparator(String path)
,它查找
特定。
如果模式的 URI 变量计数较少(计为 1),则模式的特异性较低,则单个 通配符 (计为 1) 和双通配符 (计为 2)。如果分数相等,则 更长的模式。给定相同的分数和长度,具有更多 URI 变量的模式 than 通配符。
默认映射模式 () 从评分中排除,并且始终
最后排序。此外,前缀模式(例如/**
/public/**
) 被视为较小
specific than 其他没有双通配符的模式。
有关完整详细信息,请参阅AntPatternComparator
在AntPathMatcher
还要记住
您可以自定义PathMatcher
实现。
请参阅配置部分中的 Path Matching。
后缀匹配
默认情况下,Spring MVC 执行.*
后缀模式匹配,以便
controller 映射到/person
也被隐式映射到/person.*
.
然后,使用文件扩展名解释请求的内容类型以用于
响应(即,而不是Accept
header) — 例如/person.pdf
,/person.xml
等。
当浏览器过去发送Accept
头
这很难始终如一地解释。目前,这不再是必需的,并且
使用Accept
header 应该是首选。
随着时间的推移,文件扩展名的使用已被证明以各种方式存在问题。 当与 URI 变量、路径参数和 URI 编码。基于 URL 的授权的推理 和安全性(有关更多详细信息,请参阅下一节)也变得更加困难。
要完全禁用文件扩展名,必须设置以下两项:
-
useSuffixPatternMatching(false)
,请参阅 PathMatchConfigurer -
favorPathExtension(false)
,请参阅 ContentNegotiationConfigurer
基于 URL 的内容协商仍然很有用(例如,在
browser) 的 Browser)。为了实现这一点,我们建议使用基于查询参数的策略来避免大部分
文件扩展名带来的问题。或者,如果必须使用文件扩展名,请考虑
将它们限制为显式注册的扩展列表,通过mediaTypes
ContentNegotiationConfigurer 的属性。
从 5.2.4 开始,不推荐使用用于RequestMappingHandlerMapping中的请求映射和ContentNegotiationManagerFactoryBean中的内容协商的路径扩展相关选项。请参阅 Spring Framework 问题 #24179 和相关 问题以制定进一步的计划。 |
后缀匹配和 RFD
反射文件下载 (RFD) 攻击与 XSS 类似,因为它依赖于请求输入 (例如,查询参数和 URI 变量)反映在响应中。但是,而不是 将 JavaScript 插入 HTML 中,RFD 攻击依赖于浏览器切换来执行 download 并在稍后双击时将响应视为可执行脚本。
在 Spring MVC 中,@ResponseBody
和ResponseEntity
方法存在风险,因为
它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展请求这些内容类型。
禁用后缀模式匹配并使用路径扩展进行内容协商
降低风险,但不足以防止 RFD 攻击。
为了防止 RFD 攻击,在渲染响应正文之前, Spring MVC 会添加一个Content-Disposition:inline;filename=f.txt
标头以建议固定且安全的下载
文件。仅当 URL 路径包含的文件扩展名既不是
允许作为 SAFE 或明确注册内容协商。但是,它可以
当 URL 直接输入到浏览器中时,可能会产生副作用。
默认情况下,许多常见的路径扩展名都是安全的。具有自定义HttpMessageConverter
实现可以显式地为内容注册文件扩展名
negotiation 以避免出现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
attribute 还支持否定表达式 — 例如!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
指除 “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 . |
您还可以将 SAME 与请求标头条件一起使用,如下例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
// ...
}
1 | 测试myHeader 等于myValue . |
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
// ...
}
HTTP 头, 选项
@GetMapping
(以及@RequestMapping(method=HttpMethod.GET)
) 支持 HTTP HEAD
transparently 进行请求映射。控制器方法不需要更改。
响应包装器,应用于javax.servlet.http.HttpServlet
,请确保Content-Length
header 设置为写入的字节数(不实际写入响应)。
@GetMapping
(以及@RequestMapping(method=HttpMethod.GET)
) 隐式映射到
并支持 HTTP HEAD。HTTP HEAD 请求的处理方式与它是 HTTP GET 一样,但
而不是写入正文,而是计算字节数,并且Content-Length
header 的
默认情况下,HTTP OPTIONS 是通过设置Allow
响应标头的 HTTP 列表
所有@RequestMapping
方法。
对于@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
是
组合注释的示例。提供这些 API 是因为可以说,大多数
控制器方法应该映射到特定的 HTTP 方法,而不是使用@RequestMapping
,
默认情况下,它与所有 HTTP 方法匹配。如果你需要一个组合的示例
annotations 中,看看这些是怎么声明的。
Spring MVC 还支持具有自定义请求匹配的自定义请求映射属性
逻辑。这是一个更高级的选项,需要子类化RequestMappingHandlerMapping
并覆盖getCustomMethodCondition
方法,其中
您可以检查 custom 属性并返回您自己的RequestCondition
.
显式注册
您可以通过编程方式注册处理程序方法,这些方法可用于动态 registrations 或高级情况,例如同一处理程序的不同实例 在不同的 URL 下。以下示例注册一个处理程序方法:
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); (2)
Method method = UserHandler.class.getMethod("getUser", Long.class); (3)
mapping.registerMapping(info, handler, method); (4)
}
}
1 | 注入 target handler 和 controller 的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
mapping.registerMapping(info, handler, method) (4)
}
}
1 | 注入 target handler 和 controller 的处理程序 Map。 |
2 | 准备请求映射元数据。 |
3 | 获取处理程序方法。 |
4 | 添加注册。 |
1.3.3. 处理程序方法
@RequestMapping
处理程序方法具有灵活的签名,可以从一系列
支持的控制器方法参数和返回值。
方法参数
下表描述了支持的控制器方法参数。不支持反应类型 对于任何参数。
JDK 8 的java.util.Optional
支持作为方法参数与
具有required
属性(例如@RequestParam
,@RequestHeader
,
和其他)并且等效于required=false
.
控制器方法参数 | 描述 |
---|---|
|
对请求参数以及请求和会话属性的通用访问,无需直接 使用 Servlet API。 |
|
选择任何特定的请求或响应类型,例如, |
|
强制会话的存在。因此,这样的论点从来都不是 |
|
用于编程 HTTP/2 资源推送的 Servlet 4.0 推送生成器 API。
请注意,根据 Servlet 规范,注入的 |
|
当前经过身份验证的用户 — 可能是特定的 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问 Servlet API 公开的原始请求正文。 |
|
用于访问 Servlet API 公开的原始响应正文。 |
|
用于访问 URI 模板变量。请参阅 URI 模式。 |
|
用于访问 URI 路径段中的名称-值对。请参见矩阵变量。 |
|
用于访问 Servlet 请求参数,包括 multipart 文件。参数值
转换为声明的方法参数类型。看 请注意,使用 |
|
用于访问请求标头。标头值将转换为声明的方法参数
类型。看 |
|
用于访问 Cookie。Cookie 值将转换为声明的方法参数
类型。看 |
|
用于访问 HTTP 请求正文。正文内容被转换为声明的方法
参数类型 |
|
用于访问请求标头和正文。body 使用 |
|
要访问 |
|
用于访问在 HTML 控制器中使用并作为模板公开的模型 视图渲染的一部分。 |
|
指定要在重定向时使用的属性(即,要附加到查询中) string) 和 flash 属性临时存储,直到重定向后的请求。 请参阅重定向属性和 Flash 属性。 |
|
要访问模型中的现有属性(如果不存在,则实例化),请使用
应用了数据绑定和验证。看 请注意,使用 |
|
用于访问命令对象的验证和数据绑定中的错误
(即 |
|
用于将表单处理标记为完成,从而触发会话属性的清理
通过类级别 |
|
用于准备相对于当前请求的主机、端口、方案、上下文路径和 Servlet 映射的 Literal 部分。请参阅 URI 链接。 |
|
用于访问任何 session 属性,与存储在 session 中的 model 属性相反
由于类级别 |
|
用于访问请求属性。看 |
任何其他参数 |
如果方法参数与此表中的任何先前值不匹配,并且
简单类型(由 BeanUtils#isSimpleProperty 确定,
它是一个 resolved 为 |
返回值
下表描述了支持的控制器方法返回值。响应式类型是 支持所有返回值。
控制器方法返回值 | 描述 |
---|---|
|
返回值通过 |
|
需要转换指定完整响应(包括 HTTP 头和正文)的返回值
通过 |
|
用于返回带有标头且没有正文的响应。 |
|
要解析的视图名称 |
|
一个 |
|
要添加到隐式模型的属性,其中视图名称是隐式确定的
通过 |
|
要添加到模型中的属性,视图名称通过
一个 请注意, |
|
要使用的 view 和 model 属性,以及响应状态(可选)。 |
|
具有 如果以上都不是真的,则 |
|
从任何线程异步生成上述任何返回值 — 例如,作为
某些事件或回调的结果。请参阅 异步请求 和 |
|
|
|
替代 |
|
异步发出对象流,以写入响应 |
|
|
反应式类型 — Reactor、RxJava 或其他 |
替代 对于流式处理方案(例如 |
任何其他返回值 |
与此表中的任何先前值不匹配的任何返回值,并且
是一个 |
类型转换
一些带注释的控制器方法参数,它们表示String
-基于请求的输入(例如@RequestParam
,@RequestHeader
,@PathVariable
,@MatrixVariable
和@CookieValue
)
如果参数声明为String
.
对于此类情况,将根据配置的转换器自动应用类型转换。
默认情况下,简单类型 (int
,long
,Date
等)支持。您可以自定义
类型转换WebDataBinder
(参见DataBinder
) 或通过注册Formatters
使用FormattingConversionService
.
参见 Spring Field Formatting。
矩阵变量
矩阵变量可以出现在任何路径段中,每个变量用分号和
用逗号分隔的多个值(例如/cars;color=red,green;year=2012
).倍数
值也可以通过重复的变量名称来指定(例如color=red;color=green;color=blue
).
如果 URL 应包含矩阵变量,则控制器的请求映射 方法必须使用 URI 变量来掩盖该变量内容,并确保请求可以 成功匹配,与矩阵变量顺序和存在无关。 以下示例使用 matrix 变量:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {
// petId == 42
// q == 11
}
鉴于所有路径段都可能包含矩阵变量,您有时可能需要 消除 matrix 变量预期位于哪个 path 变量的歧义。 以下示例显示了如何执行此作:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
@MatrixVariable(name = "q", pathVar = "petId") q2: Int) {
// q1 == 11
// q2 == 22
}
矩阵变量可以定义为可选变量,并指定默认值,如 以下示例显示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
// GET /pets/42
@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {
// q == 1
}
要获取所有矩阵变量,您可以使用MultiValueMap
,如下例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
@MatrixVariable matrixVars: MultiValueMap<String, String>,
@MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
请注意,您需要启用矩阵变量的使用。在 MVC Java 配置中,
您需要设置UrlPathHelper
跟removeSemicolonContent=false
通过 Path Matching。在 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 . |
默认情况下,使用此 Comments 的方法参数是必需的,但您可以指定
method 参数是可选的,只需将@RequestParam
注解的required
flag 设置为false
或者使用java.util.Optional
包装纸。
如果目标方法参数 type 不是 type,则会自动应用类型转换String
.请参阅类型转换。
将参数类型声明为数组或列表允许解析多个参数 值。
当@RequestParam
注解声明为Map<String, String>
或MultiValueMap<String, String>
,没有在注解中指定参数名称,
然后,映射中填充每个给定参数名称的请求参数值。
请注意,使用@RequestParam
是可选的(例如,设置其属性)。
默认情况下,任何作为简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且不由任何其他参数解析程序解析,则被视为已批注
跟@RequestParam
.
@RequestHeader
您可以使用@RequestHeader
注解将请求标头绑定到
控制器。
请考虑以下带有 headers 的请求:
Host localhost:8080 Accept text/html,application/xhtml+xml,application/xml;q=0.9 Accept-Language fr,en-gb;q=0.7,en;q=0.3 Accept-Encoding gzip,deflate Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive 300
以下示例获取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
参数,则 Map 会被填充
替换为所有标头值。
内置支持可用于将逗号分隔的字符串转换为
字符串的数组或集合,或者类型转换系统已知的其他类型的集合。为
example,一个带有@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
对 method 参数的注释来访问属性
模型,如果不存在,则对其进行实例化。model 属性还覆盖了
其名称与字段名称匹配的 HTTP Servlet 请求参数的值。这是指
to 作为数据绑定,并且它使您不必处理解析和转换单个
查询参数和表单字段。以下示例显示了如何执行此作:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 | 绑定 的实例Pet . |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 | 绑定 的实例Pet . |
这Pet
实例解析如下:
-
如果已使用 Model 添加,则从模型。
-
在 HTTP 会话中,使用
@SessionAttributes
. -
从通过
Converter
(请参阅下一个示例)。 -
从默认构造函数的调用。
-
来自具有与 Servlet 匹配的参数的“主构造函数”的调用 request 参数。参数名称是通过 JavaBeans 确定的
@ConstructorProperties
或通过字节码中运行时保留的参数名称。
虽然通常使用 Model 来填充模型
属性,另一种选择是依赖Converter<String, T>
组合
具有 URI 路径变量约定。在以下示例中,模型属性名称account
匹配 URI 路径变量account
和Account
通过传递
这String
账号Converter<String, Account>
:
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
// ...
}
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
// ...
}
获取 model 属性实例后,将应用数据绑定。这WebDataBinder
class 匹配 Servlet 请求参数名称(查询参数和表单
fields) 添加到目标上的字段名称Object
.匹配字段在 type 之后填充
必要时应用 conversion。有关数据绑定(和验证)的更多信息,请参阅验证。有关自定义数据绑定的更多信息,请参阅DataBinder
.
数据绑定可能会导致错误。默认情况下,BindException
被提升。但是,要检查
对于 controller 方法中的此类错误,您可以添加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 属性。对于这样的
cases,您可以注入Model
放入控制器并直接访问它,或者,
或者,将@ModelAttribute(binding=false)
,如下例所示:
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) { (1)
// ...
}
1 | 设置@ModelAttribute(binding=false) . |
@ModelAttribute
fun setUpForm(): AccountForm {
return AccountForm()
}
@ModelAttribute
fun findAccount(@PathVariable accountId: String): Account {
return accountRepository.findOne(accountId)
}
@PostMapping("update")
fun update(@Valid form: AccountForm, result: BindingResult,
@ModelAttribute(binding = false) account: Account): String { (1)
// ...
}
1 | 设置@ModelAttribute(binding=false) . |
您可以通过在数据绑定后自动应用验证,方法是添加javax.validation.Valid
注解或 Spring 的@Validated
annotation ( Bean Validation 和 Spring validation)。以下示例显示了如何执行此作:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
1 | 验证Pet 实例。 |
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
if (result.hasErrors()) {
return "petForm"
}
// ...
}
请注意,使用@ModelAttribute
是可选的(例如,设置其属性)。
默认情况下,任何不是简单值类型的参数(由 BeanUtils#isSimpleProperty 确定)
并且未由任何其他参数解析 resolver 被视为已批注
跟@ModelAttribute
.
@SessionAttributes
@SessionAttributes
用于将模型属性存储在 HTTP Servlet 会话中
请求。它是一个类型级注释,声明
特定控制器。这通常列出模型属性的名称或
model 属性,这些属性应该透明地存储在会话中以供后续使用
请求访问。
以下示例使用@SessionAttributes
注解:
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注解。 |
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
// ...
}
1 | 使用@SessionAttributes 注解。 |
在第一个请求中,当名称为pet
添加到模型中,
它会自动提升并保存在 HTTP Servlet 会话中。它仍然在那里
直到另一个控制器方法使用SessionStatus
method 参数清除
storage,如下例所示:
@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
到 controller 方法中。
作为控制器的一部分,在会话中临时存储模型属性
workflow,请考虑使用@SessionAttributes
如@SessionAttributes
.
@RequestAttribute
似@SessionAttribute
中,您可以使用@RequestAttribute
annotations 添加到
访问之前创建的预先存在的请求属性(例如,由 Servlet 创建的Filter
或HandlerInterceptor
):
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
// ...
}
1 | 使用@RequestAttribute 注解。 |
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
// ...
}
1 | 使用@RequestAttribute 注解。 |
重定向属性
默认情况下,所有模型属性都被视为在 重定向 URL。在其余属性中,那些是原始类型或 基元类型的集合或数组会自动附加为查询参数。
如果
model 实例是专门为重定向准备的。但是,在 annotated
控制器,则模型可以包含为渲染目的而添加的其他属性(例如
下拉字段值)。为避免此类属性出现在
URL、@RequestMapping
method 可以声明RedirectAttributes
和
使用它来指定要提供给的确切属性RedirectView
.如果方法
重定向时,会RedirectAttributes
被使用。否则,将
model 的
这RequestMappingHandlerAdapter
提供了一个名为ignoreDefaultModelOnRedirect
,可用于指示默认Model
如果控制器方法重定向,则永远不应使用。相反,控制器
method 应声明一个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 会话中(因此,确实如此 未显示在 URL 中)。有关详细信息,请参阅 Flash 属性。
Flash 属性
Flash 属性为一个请求提供了一种方法来存储旨在在 另一个。这在重定向时最常用 — 例如, Post-Redirect-Get 模式。Flash 属性在 redirect(通常在会话中)提供给请求,以便在 redirect 并立即删除。
Spring MVC 有两个主要的抽象来支持 flash 属性。FlashMap
已使用
以保留 Flash 属性,而FlashMapManager
用于存储、检索和管理FlashMap
实例。
Flash 属性支持始终处于“打开”状态,不需要显式启用。
但是,如果不使用,则永远不会导致 HTTP 会话创建。在每个请求中,都有一个
“输入”FlashMap
替换为从前一个请求传递的属性(如果有)和
“输出”FlashMap
替换为保存以供后续请求使用的属性。双FlashMap
实例可以通过RequestContextUtils
.
带注释的控制器通常不需要使用FlashMap
径直。相反,一个@RequestMapping
method 可以接受RedirectAttributes
并使用它
为重定向方案添加 Flash 属性。通过RedirectAttributes
会自动传播到“输出”FlashMap。同样地
重定向后,来自 “input” 的属性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>
,没有在注解中指定参数名称,
然后,地图中将填充每个给定参数名称的 Multipart 文件。
使用 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
注解来访问一个 Multipart,然后在使用 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
注解,这两者都会导致应用 Standard Bean Validation。
默认情况下,验证错误会导致MethodArgumentNotValidException
,该
转换为 400 (BAD_REQUEST) 响应。或者,您可以在本地处理验证错误
在控制器中,通过Errors
或BindingResult
论点
如下例所示:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
BindingResult result) {
// ...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
result: BindingResult): String {
// ...
}
@RequestBody
您可以使用@RequestBody
注解来读取请求正文并将其反序列化为Object
通过HttpMessageConverter
.
以下示例使用@RequestBody
论点:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
// ...
}
您可以使用 MVC Config 的 Message Converters 选项来 配置或自定义消息转换。
您可以使用@RequestBody
与javax.validation.Valid
或 Spring 的@Validated
注解,这两者都会导致应用 Standard Bean Validation。
默认情况下,验证错误会导致MethodArgumentNotValidException
,该
转换为 400 (BAD_REQUEST) 响应。或者,您可以在本地处理验证错误
在控制器中,通过Errors
或BindingResult
论点
如下例所示:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
// ...
}
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
// ...
}
HttpEntity
HttpEntity
与使用@RequestBody
但基于
Container 对象。下面的清单显示了一个示例:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
// ...
}
@ResponseBody
您可以使用@ResponseBody
对方法进行注释以序列化返回
传递给响应正文。
下面的清单显示了一个示例:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
// ...
}
@ResponseBody
在类级别也受支持,在这种情况下,它由
所有控制器方法。这是@RestController
,仅此而已
比标有@Controller
和@ResponseBody
.
您可以使用 MVC Config 的 Message Converters 选项来 配置或自定义消息转换。
您可以组合@ResponseBody
方法。
有关详细信息,请参阅 Jackson JSON。
ResponseEntity
ResponseEntity
就像@ResponseBody
但有 status 和 headers。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).build(body);
}
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
val body = ...
val etag = ...
return ResponseEntity.ok().eTag(etag).build(body)
}
Spring MVC 支持使用单值响应式类型来生成ResponseEntity
异步和/或单值和多值响应式
body 的类型。
Jackson JSON
Spring 提供对 Jackson JSON 库的支持。
JSON 视图
Spring MVC 为 Jackson 的序列化视图提供了内置支持,
,它们只允许在Object
.若要将其与@ResponseBody
或ResponseEntity
controller 方法,你可以使用 Jackson 的@JsonView
注解来激活序列化视图类,如下例所示:
@RestController
public class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView.class)
public User getUser() {
return new User("eric", "7!jd#h23");
}
}
public class User {
public interface WithoutPasswordView {};
public interface WithPasswordView extends WithoutPasswordView {};
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
}
@RestController
class UserController {
@GetMapping("/user")
@JsonView(User.WithoutPasswordView::class)
fun getUser() = User("eric", "7!jd#h23")
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许视图类数组,但您只能为每个
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
.有关更多详细信息,请参阅 Controller Advice 部分。
@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
method 被解释为模型
属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为。
除非返回值是String
否则,该名称将被解释为 View Name。@ModelAttribute
还可以自定义 Model 属性名称,如下例所示:
@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
,而这些 API 又可以:
-
将请求参数(即表单或查询数据)绑定到模型对象。
-
转换基于 String 的请求值(例如请求参数、路径变量、 headers、cookie 等)传递给 controller 方法参数的目标类型。
-
将模型对象值格式化为
String
值。
@InitBinder
方法可以注册特定于控制器的java.bean.PropertyEditor
或
SpringConverter
和Formatter
组件。此外,您还可以使用 MVC 配置来注册Converter
和Formatter
全局共享FormattingConversionService
.
@InitBinder
方法支持许多相同的参数@RequestMapping
方法
do 的@ModelAttribute
(command 对象) 参数。通常,它们被声明
替换为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
implementations,如下例所示:
@Controller
public class FormController {
@InitBinder (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
1 | 定义@InitBinder 方法。 |
@Controller
class FormController {
@InitBinder (1)
protected fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
}
// ...
}
1 | 定义@InitBinder 方法。 |
模型设计
在 Web 应用程序的上下文中,数据绑定涉及 HTTP 请求的绑定 parameters ((即表单数据或查询参数) ) 添加到模型对象中的属性,以及 其嵌套对象。
只public
遵循 JavaBeans 命名约定的属性将公开用于数据绑定,例如public String getFirstName()
和public void setFirstName(String)
方法firstName
财产。
模型对象及其嵌套对象图有时也称为命令对象、表单支持对象或 POJO (Plain Old Java Object)。 |
默认情况下, Spring 允许绑定到模型对象图中的所有公共属性。 这意味着您需要仔细考虑模型具有哪些公共属性,因为 客户端可以面向任何公共属性路径,甚至是一些不需要的公共属性路径 针对给定的使用案例。
例如,给定一个 HTTP 表单数据端点,恶意客户端可能会提供 存在于模型对象图中但不属于 HTML 表单的属性 在浏览器中显示。这可能会导致在模型对象上设置数据,并且任何 的嵌套对象中。
推荐的方法是使用仅公开
与表单提交相关的属性。例如,在用于更改
用户的电子邮件地址,则 Model 对象应声明一组最小属性,例如
如下所示ChangeEmailForm
.
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
如果您不能或不想为每个数据使用专用模型对象
binding 用例中,您必须限制允许进行数据绑定的属性。
理想情况下,您可以通过setAllowedFields()
method 开启WebDataBinder
.
例如,要在应用程序中注册允许的字段模式,您可以实现@InitBinder
方法中的@Controller
或@ControllerAdvice
组件,如下所示:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
除了注册允许的模式外,还可以注册不允许的
字段模式通过setDisallowedFields()
method 中DataBinder
及其子类。
但请注意,“允许列表”比“拒绝列表”更安全。因此setAllowedFields()
应该被青睐setDisallowedFields()
.
请注意,与允许的字段模式匹配区分大小写;鉴于匹配 Against Disallowed 字段模式不区分大小写。此外,匹配 不允许的模式将不被接受,即使它也恰好与 允许列表。
正确配置允许和不允许的字段模式非常重要 直接公开域模型以进行数据绑定时。否则,它是一个 安全风险大。 此外,强烈建议您不要使用域中的类型 模型(如 JPA 或 Hibernate 实体)作为数据绑定场景中的模型对象。 |
1.3.6. 异常
@Controller
@ControllerAdvice类可以具有@ExceptionHandler
方法处理来自 Controller 方法的异常,如下例所示:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
@Controller
class SimpleController {
// ...
@ExceptionHandler
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
该异常可能与正在传播的顶级异常(即直接IOException
被抛出)或针对顶级包装器异常中的直接原因
(例如,IOException
包装在IllegalStateException
).
对于匹配的异常类型,最好将目标异常声明为方法参数,
如前面的示例所示。当多个异常方法匹配时,根异常匹配通常是
首选而不是 Cause 异常匹配。更具体地说,ExceptionDepthComparator
是
用于根据异常从引发的异常类型中对异常的深度对异常进行排序。
或者,注释声明可以缩小异常类型的范围以匹配 如下例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
您甚至可以使用具有非常通用参数签名的特定异常类型列表 如下例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
// ...
}
根异常匹配和原因异常匹配之间的区别可能令人惊讶。 在 在 |
我们通常建议您在参数签名中尽可能具体。
减少 root 和 cause 异常类型之间不匹配的可能性。
考虑将多重匹配方法拆分为单独的@ExceptionHandler
方法,每个方法都通过其签名匹配单个特定的异常类型。
在多@ControllerAdvice
安排中,我们建议声明您的主根例外
mappings 在@ControllerAdvice
prioritizeed 和相应的顺序。虽然 root
exception match 优先于 cause,这是在给定
controller 或@ControllerAdvice
类。这意味着优先级更高的原因匹配@ControllerAdvice
bean 优先于较低优先级的任何匹配项(例如 root)@ControllerAdvice
豆。
最后但并非最不重要的一点是,一个@ExceptionHandler
方法实现可以选择 back
通过以原始形式重新引发给定的异常实例来处理该实例。
这在您只对根级别匹配或
无法静态确定的特定上下文中的匹配项。一个 rethrown
exception 通过剩余的解析链传播,就像
给定的@ExceptionHandler
method 一开始就不匹配。
支持@ExceptionHandler
方法构建在DispatcherServlet
级别、HandlerExceptionResolver 机制。
方法参数
@ExceptionHandler
方法支持以下参数:
Method 参数 | 描述 |
---|---|
异常类型 |
用于访问引发的异常。 |
|
用于访问引发异常的 controller 方法。 |
|
对请求参数以及请求和会话属性的通用访问,无需直接 使用 Servlet API。 |
|
选择任何特定的请求或响应类型(例如 |
|
强制会话的存在。因此,这样的论点从来都不是 |
|
当前经过身份验证的用户 — 可能是特定的 |
|
请求的 HTTP 方法。 |
|
当前请求区域设置,由最具体的 |
|
与当前请求关联的时区,由 |
|
用于访问 Servlet API 公开的原始响应正文。 |
|
用于访问错误响应的模型。永远是空的。 |
|
指定在重定向时使用的属性 — (即要附加到查询中 string) 和 flash 属性临时存储,直到重定向后的请求。 请参阅重定向属性和 Flash 属性。 |
|
对于任何 session 属性的访问,与存储在
session 作为类级别 |
|
用于访问请求属性。看 |
返回值
@ExceptionHandler
方法支持以下返回值:
返回值 | 描述 |
---|---|
|
返回值通过 |
|
返回值指定完整响应(包括 HTTP 标头和正文)
通过以下方式进行转换 |
|
要解析的视图名称 |
|
一个 |
|
要添加到隐式模型中且视图名称隐式确定的属性
通过 |
|
要添加到模型中的属性,其视图名称通过
一个 请注意, |
|
要使用的 view 和 model 属性,以及响应状态(可选)。 |
|
具有 如果以上都不是真的,则 |
任何其他返回值 |
如果返回值与上述任何内容都不匹配,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定), 默认情况下,它被视为要添加到模型的 Model 属性。如果是简单类型, 它仍然没有解决。 |
REST API 异常
REST 服务的一个常见要求是在
响应。Spring 框架不会自动执行此作,因为表示
响应正文中的错误详细信息是特定于应用程序的。但是,@RestController
可以使用@ExceptionHandler
方法中带有ResponseEntity
返回
值以设置响应的状态和正文。此类方法也可以声明
在@ControllerAdvice
类来全局应用它们。
在响应中实现全局异常处理且错误详细信息的应用程序
body 应考虑扩展ResponseEntityExceptionHandler
,
它为 Spring MVC 引发的异常提供处理,并为
自定义响应正文。要使用此功能,请创建一个ResponseEntityExceptionHandler
,用@ControllerAdvice
,覆盖
必要的方法,并将其声明为 Spring bean。
1.3.7. 控制器建议
通常@ExceptionHandler
,@InitBinder
和@ModelAttribute
方法适用于
这@Controller
类(或类层次结构)。如果你想要这样的
方法要更全局地应用(跨控制器),你可以在 class 中声明它们
注解@ControllerAdvice
或@RestControllerAdvice
.
@ControllerAdvice
被注释为@Component
,这意味着此类可以是
通过组件扫描注册为 Spring bean。@RestControllerAdvice
是带有注解的合成注解
同时@ControllerAdvice
和@ResponseBody
,这本质上意味着@ExceptionHandler
方法通过消息转换呈现到响应体
(相对于视图分辨率或模板渲染)。
启动时,用于@RequestMapping
和@ExceptionHandler
方法检测带有@ControllerAdvice
,然后应用其
方法。全球@ExceptionHandler
方法(来自@ControllerAdvice
) 是
在本地 Bean 的 Swift Controller 之后应用(从@Controller
).相比之下,全球@ModelAttribute
和@InitBinder
方法先于 local 方法应用。
默认情况下,@ControllerAdvice
方法适用于每个请求(即所有控制器),
但是,您可以通过使用
annotation 中,如下例所示:
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3
前面示例中的选择器在运行时进行评估,可能会产生负面影响
性能。请参阅@ControllerAdvice
javadoc 了解更多详情。
1.4. 功能端点
Spring Web MVC 包括 WebMvc.fn,这是一个轻量级函数式编程模型,其中函数 用于路由和处理请求,而 Contract 旨在实现不变性。 它是基于 Comments 的编程模型的替代方案,但在其他方面运行 相同的 DispatcherServlet。
1.4.1. 概述
在 WebMvc.fn 中,HTTP 请求使用HandlerFunction
:一个函数,该函数采用ServerRequest
并返回一个ServerResponse
.
请求和响应对象都有不可变的契约,这些契约提供对 JDK 8 友好的
访问 HTTP 请求和响应。HandlerFunction
等价于@RequestMapping
方法中的
基于注释的编程模型。
传入请求被路由到具有RouterFunction
:一个函数,该函数
需要ServerRequest
并返回一个可选的HandlerFunction
(即Optional<HandlerFunction>
).
当 router 函数匹配时,将返回一个处理程序函数;否则为空 Optional。RouterFunction
等价于@RequestMapping
注解,但带有 major
不同之处在于 router 函数不仅提供数据,还提供行为。
RouterFunctions.route()
提供便于创建路由器的 Router 构建器,
如下例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository)
val route = router { (1)
accept(APPLICATION_JSON).nest {
GET("/person/{id}", handler::getPerson)
GET("/person", handler::listPeople)
}
POST("/person", handler::createPerson)
}
class PersonHandler(private val repository: PersonRepository) {
// ...
fun listPeople(request: ServerRequest): ServerResponse {
// ...
}
fun createPerson(request: ServerRequest): ServerResponse {
// ...
}
fun getPerson(request: ServerRequest): ServerResponse {
// ...
}
}
1 | 使用路由器 DSL 创建路由器。 |
如果您注册了RouterFunction
作为 Bean 进行 Bean 处理,例如,通过在
@Configuration类中,它将由 servlet 自动检测,如 运行服务器中所述。
1.4.2. HandlerFunction 函数
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
方法创建它。您可以使用生成器设置响应状态,以添加响应
headers 或提供正文。以下示例使用 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()
处理程序类
我们可以将处理程序函数编写为 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 的 JSON 格式。 |
2 | createPerson 是一个处理程序函数,它将新的Person 包含在请求正文中。 |
3 | getPerson 是一个处理程序函数,它返回一个 person,由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 的 JSON 格式。 |
2 | createPerson 是一个处理程序函数,它将新的Person 包含在请求正文中。 |
3 | getPerson 是一个处理程序函数,它返回一个 person,由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
Router 函数用于将请求路由到相应的HandlerFunction
.
通常,您不会自己编写 router 函数,而是在RouterFunctions
Utility 类来创建一个。RouterFunctions.route()
(无参数)为您提供用于创建路由器的 Fluent 构建器
函数,而RouterFunctions.route(RequestPredicate, HandlerFunction)
提供直接方式
创建路由器。
通常建议使用route()
builder,因为它提供了
适用于典型映射场景的便捷捷径,无需难以发现
static imports。
例如,router 函数构建器提供了GET(String, HandlerFunction)
为 GET 请求创建映射;和POST(String, HandlerFunction)
用于 POST。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种引入其他
谓词。
对于每个 HTTP 方法,都有一个重载的变体,它采用RequestPredicate
作为
参数,但可以表示其他约束。
谓词
您可以编写自己的RequestPredicate
,但RequestPredicates
Utility 类
提供常用的实现,基于请求路径、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 被评估,依此类推。 因此,在一般路由之前声明更具体的路由是有意义的。 请注意,此行为与基于 Comments 的编程模型不同,其中 “最具体”控制器方法会自动选取。
当使用 router 函数构建器时,所有定义的路由都组合成一个RouterFunction
即从build()
.
还有其他方法可以将多个 router 功能组合在一起:
-
add(RouterFunction)
在RouterFunctions.route()
架构工人 -
RouterFunction.and(RouterFunction)
-
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— 的快捷方式RouterFunction.and()
with 嵌套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 匹配 JSON 的标头将路由到PersonHandler.getPerson |
2 | GET /person 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute = router { }
val route = router {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 | GET /person/{id} 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.getPerson |
2 | GET /person 替换为Accept 匹配 JSON 的标头将路由到PersonHandler.listPeople |
3 | POST /person 没有其他谓词映射到PersonHandler.createPerson 和 |
4 | otherRoute 是在其他地方创建并添加到 Route built 的 router 函数。 |
嵌套路由
一组 router 函数通常具有共享谓词,例如共享的
路径。
在上面的示例中,共享谓词将是匹配的路径谓词/person
,
由其中 3 条路由使用。
使用注释时,可以使用类型级@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("/person", handler::createPerson))
.build();
1 | 请注意,第二个参数path 是采用 router builder 的 consumer。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
GET("", accept(APPLICATION_JSON), handler::listPeople)
POST("/person", handler::createPerson)
}
}
尽管基于路径的嵌套是最常见的,但你可以使用
这nest
方法。
以上仍然包含一些共享Accept
-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("/person", handler::createPerson))
.build();
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
accept(APPLICATION_JSON).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
}
}
}
1.4.4. 运行服务器
通常在DispatcherHandler
-based 设置,它使用 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
路由上的 methods
函数构建器。
通过 annotations,您可以通过使用@ControllerAdvice
一个ServletFilter
和/或两者。
该筛选条件将应用于构建器构建的所有路由。
这意味着嵌套路由中定义的筛选条件不适用于 “top-level” 路由。
例如,请考虑以下示例:
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople)
.before(request -> ServerRequest.from(request) (1)
.header("X-RequestHeader", "Value")
.build()))
.POST("/person", handler::createPerson))
.after((request, response) -> logResponse(response)) (2)
.build();
1 | 这before 添加自定义请求标头的 filter 仅适用于两个 GET 路由。 |
2 | 这after filter 记录响应的 filter 应用于所有路由,包括嵌套路由。 |
import org.springframework.web.servlet.function.router
val route = router {
"/person".nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
before { (1)
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
}
}
POST("/person", handler::createPerson)
after { _, response -> (2)
logResponse(response)
}
}
1 | 这before 添加自定义请求标头的 filter 仅适用于两个 GET 路由。 |
2 | 这after filter 记录响应的 filter 应用于所有路由,包括嵌套路由。 |
这filter
方法采用HandlerFilterFunction
:一个
函数,该函数采用ServerRequest
和HandlerFunction
并返回一个ServerResponse
.
handler 函数参数表示链中的下一个元素。
这通常是路由到的处理程序,但也可以是另一个
filter (如果应用了多个)。
现在我们可以向路由添加一个简单的安全过滤器,假设我们有一个SecurityManager
那
可以确定是否允许特定路径。
以下示例显示了如何执行此作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET("", handler::listPeople))
.POST("/person", handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
import org.springframework.web.servlet.function.router
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST("/person", handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
前面的示例演示了调用next.handle(ServerRequest)
是可选的。
我们只允许在允许访问时运行处理程序函数。
除了使用filter
方法,则可以应用
过滤到现有的 router 函数RouterFunction.filter(HandlerFilterFunction)
.
对功能端点的 CORS 支持通过专用的CorsFilter . |
1.5. URI 链接
本节描述了 Spring Framework 中可用于 URI 的各种选项。
1.5.1. UriComponents
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
有助于使用变量从 URI 模板构建 URI,如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build(); (4)
URI uri = uriComponents.expand("Westin", "123").toUri(); (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
val uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") (1)
.queryParam("q", "{q}") (2)
.encode() (3)
.build() (4)
val uri = uriComponents.expand("Westin", "123").toUri() (5)
1 | 具有 URI 模板的静态工厂方法。 |
2 | 添加或替换 URI 组件。 |
3 | 请求对 URI 模板和 URI 变量进行编码。 |
4 | 构建一个UriComponents . |
5 | 展开变量并获取URI . |
前面的示例可以合并为一个链,并使用buildAndExpand
,
如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri()
您可以通过直接转到 URI(这意味着编码)来进一步缩短它, 如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
val uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123")
1.5.2. Uri生成器
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
实现UriBuilder
.您可以创建一个UriBuilder
,反过来,使用UriBuilderFactory
.一起UriBuilderFactory
和UriBuilder
提供一种可插拔的机制来从 URI 模板构建 URI,基于
共享配置,例如基本 URL、编码首选项和其他详细信息。
您可以配置RestTemplate
和WebClient
替换为UriBuilderFactory
以自定义 URI 的准备工作。DefaultUriBuilderFactory
是默认值
实现UriBuilderFactory
使用UriComponentsBuilder
internally 和
公开共享配置选项。
以下示例显示如何配置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
但是,它不是静态工厂方法,而是一个实际实例
,其中包含 configuration 和 preferences,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)
val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123")
1.5.3. URI 编码
Spring MVC 和 Spring WebFlux
UriComponentsBuilder
在两个级别公开编码选项:
-
UriComponentsBuilder#encode() 中: 首先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。
-
UriComponents#encode() 中: 在 URI 变量展开后对 URI 组件进行编码。
这两个选项都用转义的八位字节替换非 ASCII 字符和非法字符。但是,第一个选项 还会替换 URI 变量中出现的具有保留含义的字符。
请考虑 “;”,它在 path 中是合法的,但具有保留的含义。第一个选项将 “;” 在 URI 变量中带有 “%3B”,但在 URI 模板中没有。相比之下,第二个选项永远不会 替换 “;”,因为它是路径中的合法字符。 |
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量作为不透明数据进行完全编码,而选项 2 仅在以下情况下有用 URI 变量有意包含保留字符。
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri()
// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接转到 URI(这意味着编码)来缩短前面的示例, 如下例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
这WebClient
和RestTemplate
通过
这UriBuilderFactory
策略。两者都可以使用自定义策略进行配置。
如下例所示:
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
implementation usesUriComponentsBuilder
internally 到
展开并编码 URI 模板。作为工厂,它提供了一个配置位置
编码方法,基于以下编码模式之一:
-
TEMPLATE_AND_VALUES
:使用UriComponentsBuilder#encode()
,对应于 前面列表中的第一个选项,用于预编码 URI 模板并在 扩大。 -
VALUES_ONLY
:不对 URI 模板进行编码,而是应用严格编码 到 URI 变量UriUtils#encodeUriUriVariables
在将它们扩展到 模板。 -
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 host, scheme, port, path and query string...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode();
val request: HttpServletRequest = ...
// Re-uses host, scheme, port, path and query string...
val ucb = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}").build()
.expand("123")
.encode()
您可以创建相对于上下文路径的 URI,如下例所示:
// Re-uses host, port and context path...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts").build()
// Re-uses host, port and context path...
val ucb = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts").build()
您可以创建相对于 Servlet 的 URI(例如/main/*
),
如下例所示:
// Re-uses host, port, context path, and Servlet prefix...
ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts").build()
// Re-uses host, port, context path, and Servlet prefix...
val ucb = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts").build()
从 5.1 开始,ServletUriComponentsBuilder 忽略来自Forwarded 和X-Forwarded-* headers,用于指定客户端发起的地址。考虑使用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
.例如,您可以使用一种技术
类似于通过代理进行模拟测试,以避免通过名称引用控制器方法,如下例所示
(该示例假定 static importMvcUriComponentsBuilder.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 return value)来代替。 |
前面的示例在MvcUriComponentsBuilder
.在内部,他们依赖于
上ServletUriComponentsBuilder
要从 scheme、host、port 准备基本 URL,
当前请求的 context path 和 servlet path 的 servlet 路径。这在大多数情况下效果很好。
但是,有时,它可能不够。例如,您可能位于
请求(例如准备链接的批处理),或者可能需要插入路径
前缀(例如已从请求路径中删除的区域设置前缀,需要
重新插入到链接中)。
对于这种情况,您可以使用 staticfromXxx
接受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-* headers,用于指定客户端发起的地址。考虑使用 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
method 中ThingController
变为 “TC#getThing”)。如果存在名称冲突,您可以使用@RequestMapping(name="..")
分配显式名称或实现自己的名称HandlerMethodMappingNamingStrategy
.
1.6. 异步请求
Spring MVC 与 Servlet 3.0 异步请求处理进行了广泛的集成:
1.6.1.DeferredResult
在 Servlet 容器中启用异步请求处理功能后,控制器方法可以包装任何受支持的控制器方法
return value 替换为DeferredResult
,如下例所示:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<String>();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(result);
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
val deferredResult = DeferredResult<String>()
// Save the deferredResult somewhere..
return deferredResult
}
// From some other thread...
deferredResult.setResult(result)
控制器可以从不同的线程异步生成返回值 — 对于 例如,响应外部事件 (JMS 消息)、计划任务或其他事件。
1.6.2.Callable
控制器可以用java.util.concurrent.Callable
,
如下例所示:
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return new Callable<String>() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
// ...
"someView"
}
然后,可以通过配置的 TaskExecutor
.
1.6.3. 处理
以下是 Servlet 异步请求处理的非常简洁的概述:
-
一个
ServletRequest
可以通过调用request.startAsync()
. 这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但 响应将保持打开状态,以便稍后完成处理。 -
对
request.startAsync()
返回AsyncContext
,您可以将其用于 进一步控制异步处理。例如,它提供dispatch
方法 它类似于 Servlet API 的 forward,不同之处在于它允许 应用程序在 Servlet 容器线程上恢复请求处理。 -
这
ServletRequest
提供对当前DispatcherType
,您可以 用于区分处理初始请求、异步 dispatch、a forward 和其他 Dispatcher 类型。
DeferredResult
处理工作如下:
-
控制器返回一个
DeferredResult
并将其保存在内存中 queue 或 list 中。 -
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
对启动异步的初始请求的 callback
正在处理(而不是postHandle
和afterCompletion
).
HandlerInterceptor
实现还可以注册一个CallableProcessingInterceptor
或DeferredResultProcessingInterceptor
,以便与
异步请求的生命周期(例如,处理超时事件)。看AsyncHandlerInterceptor
了解更多详情。
DeferredResult
提供onTimeout(Runnable)
和onCompletion(Runnable)
回调。
请参阅javadoc 的DeferredResult
了解更多详情。Callable
可以替代WebAsyncTask
这会暴露额外的
timeout和completion回调的方法。
与 WebFlux 相比
Servlet API 最初是为通过 Filter-Servlet 进行一次传递而构建的
链。Servlet 3.0 中添加的异步请求处理允许应用程序退出
Filter-Servlet 链,但将响应保持开放状态以供进一步处理。The Spring MVC
异步支持是围绕该机制构建的。当控制器返回DeferredResult
,
退出 Filter-Servlet 链,并释放 Servlet 容器线程。稍后,当
这DeferredResult
设置后,会触发一个ASYNC
dispatch(到同一个 URL),在此期间,
controller 再次映射,但DeferredResult
value 被使用
(就像控制器返回一样)以恢复处理。
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的 asynchronous request processing 功能,因为它在设计上是异步的。异步 Handling 内置于所有框架 Contract 中,并且通过 ALL 请求处理阶段。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持 asynchronous 和 Reactive Types 作为控制器方法中的返回值。 Spring MVC 甚至支持流,包括反应式背压。但是,单个 对响应的写入仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同, 它依赖于非阻塞 I/O,并且每次写入不需要额外的线程。
另一个根本区别是 Spring MVC 不支持异步或反应式
控制器方法参数中的类型(例如@RequestBody
,@RequestPart
等)、
它也没有任何明确支持异步和反应类型作为模型属性。
Spring WebFlux 确实支持所有这些。
1.6.4. HTTP 流
您可以使用DeferredResult
和Callable
对于单个异步返回值。
如果您想生成多个异步值,并将这些值写入
响应?本节介绍如何执行此作。
对象
您可以使用ResponseBodyEmitter
return 值来生成对象流,其中
每个对象都使用HttpMessageConverter
并写入
响应,如下例所示:
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
您还可以使用ResponseBodyEmitter
作为ResponseEntity
,让您
自定义响应的状态和标头。
当emitter
抛出一个IOException
(例如,如果远程客户端消失)、应用程序
不负责清理连接,不应调用emitter.complete
或emitter.completeWithError
.相反,Servlet 容器会自动启动AsyncListener
错误通知,其中 Spring MVC 会创建一个completeWithError
叫。
此调用反过来会执行最后一个ASYNC
dispatch 到应用程序,在此期间 Spring MVC
调用配置的异常解析程序并完成请求。
上交所
SseEmitter
(ResponseBodyEmitter
) 提供对 Server-Sent Events 的支持,其中从服务器发送的事件
根据 W3C SSE 规范进行格式设置。生成 SSE
stream 中,返回SseEmitter
,如下例所示:
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
// Save the emitter somewhere..
}
// In some other thread
emitter.send("Hello once")
// and again later on
emitter.send("Hello again")
// and done at some point
emitter.complete()
虽然 SSE 是流式传输到浏览器的主要选项,但请注意,Internet Explorer 不支持 Server-Sent Events。考虑将 Spring 的 WebSocket 消息传递与 Sockjs 回退传输(包括 SSE)一起使用,该传输将 广泛的浏览器。
有关异常处理的说明,另请参阅上一节。
原始数据
有时,绕过消息转换并直接流式传输到响应很有用OutputStream
(例如,对于文件下载)。您可以使用StreamingResponseBody
return value 类型来执行此作,如下例所示:
@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 支持在控制器中使用反应式 Client 端库(另请阅读 WebFlux 部分中的反应式库)。
这包括WebClient
从spring-webflux
和其他
反应式数据存储库。在这种情况下,能够返回很方便
来自 controller 方法的 reactive 类型。
反应式返回值的处理方式如下:
-
单值 promise 适应,类似于使用
DeferredResult
.例子 包括Mono
(Reactor) 或Single
(RxJava) 的 Java 版本。 -
具有流媒体类型(如
application/stream+json
或text/event-stream
) 适应于,类似于使用ResponseBodyEmitter
或SseEmitter
.示例包括Flux
(Reactor) 或Observable
(RxJava) 的 Java 版本。 应用程序也可以返回Flux<ServerSentEvent>
或Observable<ServerSentEvent>
. -
具有任何其他媒体类型(例如
application/json
) 已适应 to 的 API API 中,类似于使用DeferredResult<List<?>>
.
Spring MVC 通过ReactiveAdapterRegistry 从spring-core ,这允许它适应多个响应式库。 |
1.6.6. 断开连接
当远程客户端消失时,Servlet API 不提供任何通知。 因此,在流式传输到响应时,无论是通过 SseEmitter 还是反应式类型,定期发送数据都很重要。 因为如果客户端已断开连接,则写入失败。发送可以采用 空(仅评论)SSE 事件或另一方必须解释的任何其他数据 作为心跳并忽略。
或者,考虑使用 Web 消息传递解决方案(例如基于 WebSocket 的 STOMP 或带有 SockJS 的 WebSocket) 具有内置心跳机制的 S S S 的 S S S 的 S T
1.6.7. 配置
必须在 Servlet 容器级别启用异步请求处理功能。 MVC 配置还为异步请求公开了几个选项。
Servlet 容器
Filter 和 Servlet 声明具有asyncSupported
标志,需要设置为true
以启用异步请求处理。此外,Filter 映射应为
declared 来处理ASYNC
javax.servlet.DispatchType
.
在 Java 配置中,当您使用AbstractAnnotationConfigDispatcherServletInitializer
初始化 Servlet 容器,此作会自动完成。
在web.xml
configuration 中,您可以添加<async-supported>true</async-supported>
到DispatcherServlet
以及Filter
声明并添加<dispatcher>ASYNC</dispatcher>
以筛选映射。
Spring MVC
MVC 配置公开了以下与异步请求处理相关的选项:
-
Java 配置:使用
configureAsyncSupport
回调开启WebMvcConfigurer
. -
XML 命名空间:使用
<async-support>
元素<mvc:annotation-driven>
.
您可以配置以下内容:
-
异步请求的默认超时值(如果未设置),则取决于 在底层 Servlet 容器上。
-
AsyncTaskExecutor
用于在使用 Reactive Types 进行流式处理时阻止写入和执行Callable
从 controller 方法。如果您满足以下条件,我们强烈建议您配置此属性 stream 或具有返回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 的工作原理,您可以阅读这篇文章,其中 许多其他方法,或者参见 规范 了解更多详情。
Spring MVCHandlerMapping
implementations 提供对 CORS 的内置支持。成功后
将请求映射到处理程序,HandlerMapping
implementations 检查 CORS 配置的
given request 和 handler 并采取进一步的作。处理印前检查请求
直接,而简单和实际的 CORS 请求被拦截、验证,并且具有
所需的 CORS 响应标头集。
为了启用跨域请求(即Origin
header 存在且
与请求的主机不同),您需要有一些显式声明的 CORS
配置。如果未找到匹配的 CORS 配置,则印前 Backup 请求为
拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中
因此,浏览器会拒绝它们。
每HandlerMapping
可以使用基于 URL 模式的 URL 进行单独配置CorsConfiguration
映射。在大多数情况下,应用程序
使用 MVC Java 配置或 XML 命名空间来声明此类映射,这将产生
在传递给所有HandlerMappping
实例。
您可以在HandlerMapping
Level with More (更多级别)
精细的处理程序级 CORS 配置。例如,带注解的控制器可以使用
类级或方法级@CrossOrigin
注解(其他处理程序可以实现CorsConfigurationSource
).
组合全局配置和本地配置的规则通常是累加的 — 例如,
所有全球和所有本地源。对于只能使用单个值
accepted(例如allowCredentials
和maxAge
),local 将覆盖 global 值。看CorsConfiguration#combine(CorsConfiguration)
了解更多详情。
要从源代码中了解更多信息或进行高级自定义,请检查背后的代码:
|
1.7.3.@CrossOrigin
这@CrossOrigin
annotation 支持对带注解的控制器方法进行跨域请求,
如下例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
-
所有来源。
-
所有标头。
-
控制器方法映射到的所有 HTTP 方法。
allowedCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。
maxAge
设置为 30 分钟。
@CrossOrigin
在类级别也受支持,并且被所有方法继承。
如下例所示:
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
您可以使用@CrossOrigin
在类级别和方法级别,
如下例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
fun retrieve(@PathVariable id: Long): Account {
// ...
}
@DeleteMapping("/{id}")
fun remove(@PathVariable id: Long) {
// ...
}
}
1.7.4. 全局配置
除了细粒度的 controller 方法级配置之外,您可能还希望
也定义一些全局 CORS 配置。您可以设置基于 URLCorsConfiguration
映射HandlerMapping
.但是,大多数应用程序都使用
MVC Java 配置或 MVC XML 命名空间来执行此作。
默认情况下,全局配置将启用以下功能:
-
所有来源。
-
所有标头。
-
GET
,HEAD
和POST
方法。
allowedCredentials
默认情况下不启用,因为这会建立信任级别
公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌)和
应仅在适当的情况下使用。
maxAge
设置为 30 分钟。
Java 配置
要在 MVC Java 配置中启用 CORS,您可以使用CorsRegistry
回调
如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
XML 配置
要在 XML 命名空间中启用 CORS,您可以使用<mvc:cors>
元素
如下例所示:
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
1.7.5. CORS 过滤器
您可以通过内置的CorsFilter
.
如果您尝试使用CorsFilter 使用 Spring Security,请记住
Spring Security 具有对 CORS 的内置支持。 |
要配置过滤器,请传递CorsConfigurationSource
添加到其构造函数中,如下例所示:
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
val config = CorsConfiguration()
// Possibly...
// config.applyPermitDefaultValues()
config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)
val filter = CorsFilter(source)
1.8. Web 安全
Spring Security 项目提供支持 用于保护 Web 应用程序免受恶意攻击。请参阅 Spring Security 参考文档,包括:
HDIV 是另一个与 Spring MVC 集成的 Web 安全框架。
1.9. HTTP 缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存
围绕Cache-Control
响应标头,然后是条件请求
标头(例如Last-Modified
和ETag
).Cache-Control
建议私有(例如,浏览器)
以及有关如何缓存和重用响应的公共(例如,代理)缓存。一ETag
header 的
要发出可能导致 304 (NOT_MODIFIED) 没有正文的条件请求,
如果内容未更改。ETag
可以看作是
这Last-Modified
页眉。
本节描述了 Spring Web MVC 中可用的 HTTP 缓存相关选项。
1.9.1.CacheControl
CacheControl
提供支持
配置与Cache-Control
header 并被接受为参数
在许多地方:
虽然 RFC 7234 描述了所有可能的
指令的Cache-Control
response 标头中,CacheControl
type 接受
面向用例的方法,侧重于常见场景:
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)
// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()
// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
WebContentGenerator
也接受一个更简单的cachePeriod
属性(以秒为单位定义),该属性
工作原理如下:
-
一个
-1
值不会生成Cache-Control
响应标头。 -
一个
0
值阻止缓存'Cache-Control: no-store'
命令。 -
一
n > 0
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
requests 中,您可以将响应设置为
304 (NOT_MODIFIED)。对于有条件的POST
,PUT
和DELETE
中,您可以改为设置响应
设置为 412 (PRECONDITION_FAILED),以防止并发修改。
1.9.4.ETag
Filter
您可以使用ShallowEtagHeaderFilter
添加 “浅”eTag
值,这些值是从
响应内容,因此可以节省带宽,但不能节省 CPU 时间。参见浅层 ETag。
1.10. 查看技术
Spring MVC 中视图技术的使用是可插拔的,无论您决定使用 Thymeleaf、Groovy Markup Templates、JSP 或其他技术主要是一个问题 配置更改。本章介绍与 Spring MVC。我们假设您已经熟悉 View Resolution。
Spring MVC 应用程序的视图位于该应用程序的内部信任边界内 应用。 视图可以访问应用程序上下文的所有 bean。 因此,不建议在以下应用程序中使用 Spring MVC 的模板支持: 模板可由外部源编辑,因为这可能会产生安全隐患。 |
1.10.1. 百里香叶
Thymeleaf 是一个现代的服务器端 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 模板一起使用的集成。
View 配置
以下示例显示了如何将 FreeMarker 配置为视图技术:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/WEB-INF/freemarker")
}
}
以下示例显示了如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
或者,您也可以声明FreeMarkerConfigurer
bean 用于完全控制所有
属性,如下例所示:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>
您的模板需要存储在FreeMarkerConfigurer
如前面的示例所示。给定上述配置,如果您的控制器
返回视图名称welcome
中,解析程序会查找/WEB-INF/freemarker/welcome.ftl
模板。
FreeMarker 配置
您可以将 FreeMarker 的 'Settings' 和 'SharedVariables' 直接传递给 FreeMarkerConfiguration
对象(由 Spring 管理)通过设置适当的 bean
属性FreeMarkerConfigurer
豆。这freemarkerSettings
property 需要
一个java.util.Properties
object 和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/>
元素。此元素主要允许表单显示来自
表单支持对象,并显示来自Validator
在
Web 或业务层。Spring 在 FreeMarker 中也支持相同的功能,
具有用于生成表单输入元素本身的附加便捷宏。
Bind 宏
一组标准的宏在spring-webmvc.jar
文件
FreeMarker,因此它们始终可用于适当配置的应用程序。
Spring 模板库中定义的一些宏被认为是内部的
(私有),但宏定义中不存在此类范围,因此所有宏都可见
调用代码和用户模板。以下各节仅重点介绍宏
您需要直接从模板中调用。如果您想查看宏代码
直接调用该文件spring.ftl
,并且位于org.springframework.web.servlet.view.freemarker
包。
简单绑定
在基于 FreeMarker 模板的 HTML 表单中,这些模板充当 Spring MVC 的表单视图
controller 中,您可以使用类似于以下示例的代码绑定到字段值和
显示每个输入字段的错误消息的方式与 JSP 等效项类似。这
以下示例显示了personForm
视图:
<!-- FreeMarker macros have to be imported into a namespace.
We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>
<@spring.bind>
需要一个 'path' 参数,该参数由命令的名称组成
对象(它是 'command',除非您在控制器配置中更改了它)
按句点和要绑定到的 Command 对象上的字段名称。你
也可以使用嵌套字段,例如command.address.street
.这bind
macro 假定
默认 HTML 转义行为,由ServletContext
参数defaultHtmlEscape
在web.xml
.
宏的另一种形式称为<@spring.bindEscaped>
采用第二个参数
显式指定是否应在状态错误中使用 HTML 转义
消息或值。您可以将其设置为true
或false
根据需要。附加表格
处理宏简化了 HTML 转义的使用,您应该使用这些宏
尽可能。它们将在下一节中解释。
输入宏
FreeMarker 的其他便捷宏简化了装订和表单生成 (包括验证错误显示)。永远没有必要使用这些宏来 生成表单输入字段,并且您可以使用简单的 HTML 或直接将它们混合和匹配 调用我们之前突出显示的 Spring 绑定宏。
下表显示了 FreeMarker 模板 (FTL) 定义 以及每个参数的参数列表:
宏 | FTL 定义 |
---|---|
|
<@spring.message code/> |
|
<@spring.message文本代码、文本/> |
|
<@spring.url relativeUrl/> |
|
<@spring.form输入路径、属性、fieldType/> |
|
<@spring.formHiddenInput 路径,attributes/> |
|
<@spring.formPasswordInput 路径、attributes/> |
|
<@spring.formText区域路径、attributes/> |
|
<@spring.formSingleSelect 路径、选项、属性/> |
|
<@spring.formMultiSelect path, options, attributes/> |
|
<@spring.formRadioButtons 路径、选项分隔符、attributes/> |
|
<@spring.formCheckboxes 路径、选项、分隔符、attributes/> |
|
<@spring.formCheckbox 路径,attributes/> |
|
<@spring.showErrors 分隔符 classOrStyle/> |
在 FreeMarker 模板中,formHiddenInput 和formPasswordInput 实际上并非
required,因为您可以使用正常的formInput 宏, 指定hidden 或password 作为fieldType 参数。 |
上述任何宏的参数都具有一致的含义:
-
path
:要绑定到的字段的名称(即 “command.name”) -
options
:一个Map
可以在输入中选择的所有可用值 田。映射的键表示从表单中 POST 回来的值 并绑定到 Command 对象。针对键存储的 Map 对象是标签 在表单上显示给用户,并且可能与相应的值不同 通过表单发回。通常,此类映射由 控制器。您可以使用任何Map
implementation 的 Instant,具体取决于所需的行为。 对于严格排序的地图,您可以使用SortedMap
(例如TreeMap
) 替换为 合适Comparator
并且,对于应在插入中返回值的任意 Map order 中使用LinkedHashMap
或LinkedMap
从commons-collections
. -
separator
:其中多个选项可用作隐蔽元素(单选按钮 或复选框)中,则用于分隔列表中每个字符的字符序列 (例如<br>
). -
attributes
:要包含在其中的任意标签或文本的附加字符串 HTML 标签本身。此字符串由宏逐字面回显。例如,在textarea
字段,您可以提供属性(例如 'rows=“5” cols=“60”'),或者您 可以传递样式信息,例如 'style=“border:1px solid silver”'。 -
classOrStyle
:对于showErrors
macro 的 CSS 类的名称,该类的span
元素。如果未提供任何信息(或者值为 empty),则错误将包含在<b></b>
标签。
以下各节概述了宏的示例。
这formInput
macro 将path
参数 (command.name
) 和额外的attributes
参数(在即将到来的示例中为空)。宏以及所有其他形式
generation 宏,对 path 参数执行隐式 Spring 绑定。绑定
在发生新绑定之前保持有效,因此showErrors
宏不需要传递
path 参数 — 它对上次为其创建绑定的字段进行作。
这showErrors
macro 接受一个 separator 参数(用于
分隔给定字段上的多个错误),并且还接受第二个参数 — this
time、类名或 style 属性。请注意,FreeMarker 可以指定 default
attributes 参数的值。以下示例演示如何使用formInput
和showWErrors
宏:
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>
下一个示例显示了表单片段的输出,生成 name 字段并显示 提交表单后出现验证错误,但字段中没有值。验证 通过 Spring 的 Validation 框架发生。
生成的 HTML 类似于以下示例:
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>
这formTextarea
宏的工作方式与formInput
宏并接受相同的
parameter 列表。通常,第二个参数 (attributes
) 用于传递样式
information 或rows
和cols
属性textarea
.
您可以使用四个选择字段宏在 您的 HTML 表单:
-
formSingleSelect
-
formMultiSelect
-
formRadioButtons
-
formCheckboxes
四个宏中的每一个都接受一个Map
of 选项,其中包含表单的值
field 和与该值对应的标签。值和标签可以是
相同。
下一个示例是 FTL 中的单选按钮。表单支持对象指定默认值 值为 'London' ,因此无需验证。当表单为 呈现时,可供选择的整个城市列表将作为参考数据提供在 model 替换为 'cityMap' 的下面的清单显示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
前面的清单呈现了一行单选按钮,每个按钮对应cityMap
,并使用
separator 的 。没有提供其他属性(宏的最后一个参数是
missing)。这""
cityMap
使用相同的String
对于映射中的每个键值对。地图的
键是表单实际提交的POST
request 参数。map 值是
标签。在前面的示例中,给定三个知名城市的列表
和表单支持对象中的默认值,则 HTML 类似于以下内容:
Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>
如果您的应用程序希望通过内部代码处理城市(例如),则可以创建 代码,如下例所示:
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");
Map<String, Object> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
protected fun referenceData(request: HttpServletRequest): Map<String, *> {
val cityMap = linkedMapOf(
"LDN" to "London",
"PRS" to "Paris",
"NYC" to "New York"
)
return hashMapOf("cityMap" to cityMap)
}
现在,该代码会生成输出,其中 radio 值是相关代码,但 用户仍然会看到对用户更友好的城市名称,如下所示:
Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>
HTML 转义
前面描述的表单宏的默认用法会导致 HTML 元素为 HTML 4.01
compliant 的 v.web.xml
文件,作为
由 Spring 的 bind 支持使用。使元素符合 XHTML 或覆盖
默认的 HTML 转义值,您可以在模板中指定两个变量(或在
您的模型,它们对您的模板可见)。指定
它们在模板中是可以稍后在
模板处理为表单中的不同字段提供不同的行为。
要切换到标记的 XHTML 合规性,请指定true
对于
模型或上下文变量xhtmlCompliant
,如下例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理此指令后,Spring 宏生成的任何元素现在都是 XHTML 顺从的。
以类似的方式,您可以为每个字段指定 HTML 转义,如下例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
1.10.3. Groovy 标记
Groovy 标记模板引擎主要旨在生成类似 XML 的标记(XML、XHTML、HTML5 等),但您可以 使用它来生成任何基于文本的内容。Spring Framework 有一个内置的 将 Spring MVC 与 Groovy Markup 结合使用的集成。
Groovy Markup Template 引擎需要 Groovy 2.3.1+。 |
配置
下面的示例演示如何配置 Groovy 标记模板引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
以下示例显示了如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
1.10.4. 脚本视图
Spring Framework 有一个内置的集成,用于将 Spring MVC 与任何 可以在 JSR-223 Java 脚本引擎上运行的模板库。我们测试了以下内容 在不同脚本引擎上模板化库:
脚本库 | 脚本引擎 |
---|---|
集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngine 和Invocable 接口。 |
要求
您需要在 Classpath 上具有脚本引擎,其详细信息因脚本引擎而异:
-
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
应添加 line 以支持 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()
与此签名本机兼容,因此您可以直接调用它。
如果您的模板技术需要一些自定义,则可以提供一个脚本,该脚本 实现自定义 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 property 设置为false 在使用非线程安全时是必需的
具有非并发性模板库的脚本引擎,例如 Handlebars 或
React 在 Nashorn 上运行。在这种情况下,由于此错误,需要 Java SE 8 Update 60,但通常是
建议在任何情况下使用最新的 Java SE 补丁版本。 |
polyfill.js
仅定义window
对象才能正常运行,如下所示:
var window = {};
这个基本render.js
implementation 在使用模板之前对其进行编译。生产就绪型
implementation 还应存储任何重复使用的缓存模板或预编译的模板。
您可以在脚本端执行此作(并处理您需要的任何自定义 — 管理
模板引擎配置)。以下示例显示了如何执行此作:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
1.10.5. JSP 和 JSTL
Spring Framework 具有用于将 Spring MVC 与 JSP 和 JSTL 一起使用的内置集成。
View 解析程序
使用 JSP 进行开发时,您可以声明InternalResourceViewResolver
或ResourceBundleViewResolver
豆。
ResourceBundleViewResolver
依赖属性文件来定义视图名称
映射到类和 URL。使用ResourceBundleViewResolver
,您可以混合
不同类型的视图,如下例所示:
<!-- the ResourceBundleViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
# And a sample properties file is used (views.properties in WEB-INF/classes):
welcome.(class)=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp
productList.(class)=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp
InternalResourceViewResolver
也可用于 JSP。作为最佳实践,我们强烈
建议将 JSP 文件放在'WEB-INF'
目录,所以那里
不能由客户端直接访问。
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
Spring 的 JSP 标记库
Spring 提供请求参数到命令对象的数据绑定,如 前面的章节。促进 JSP 页面的开发 data binding 功能,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 更易于开发、阅读和维护。
我们将浏览 form 标记,并查看如何使用每个标记的示例。我们有 包含生成的 HTML 片段,其中某些标记需要进一步注释。
配置
表单标记库捆绑在spring-webmvc.jar
.库描述符为
叫spring-form.tld
.
要使用此库中的标记,请将以下指令添加到 JSP 的顶部 页:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
哪里form
是要用于此库中的标签的标签名称前缀。
表单标签
此标签呈现一个 HTML“form”元素,并公开一个指向内部标签的绑定路径
捆绑。它将命令对象放在PageContext
,以便 Command 对象可以
通过 inner 标签访问。此库中的所有其他标签都是form
标记。
假设我们有一个名为User
.它是一个具有属性
如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
通过页面控制器。继续阅读以查看更复杂的示例
如何将 inner 标记与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-backing 对象以其他名称放入模型中
(绝对是最佳实践),您可以将表单绑定到命名变量,因为
以下示例显示:
<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'
默认情况下。
有关此标记的示例,请参阅 Form 标记。您还可以使用
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
如果 bound 值为true
.这value
属性对应于setValue(Object)
value 属性。 -
方法二:当绑定值为
array
或java.util.Collection
这input(checkbox)
标记为checked
如果配置的setValue(Object)
value 为 存在于 Bound 中Collection
. -
方法三:对于任何其他绑定值类型,
input(checkbox)
标记为checked
如果配置的setValue(Object)
等于 bound 值。
请注意,无论采用哪种方法,都会生成相同的 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
tag 部分。有时,您更喜欢
不必在 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
可用作 Model 属性
,其中包含要从中选择的值的字符串。如果您使用Map
,
映射条目键用作值,映射条目的值用作
要显示的标签。您还可以使用自定义对象,您可以在其中提供
value 的属性名称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
标记,您可能希望
将 available options 作为运行时变量传入。对于此用法,您可以使用radiobuttons
标记。您传入一个Array
一个List
或Map
,其中包含
的items
财产。如果您使用Map
,则映射入口键为
used as the value 和 map entry 的值用作要显示的标签。
您还可以使用自定义对象,您可以在其中为值提供属性名称
通过使用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 值。如果您确实希望使用
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
skill 在 Herbology 中,则 'Skills' 行的 HTML 源可以是
如下:
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
这option
标记
此标签呈现 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
标记
此标签呈现 HTMLoption
元素。它将selected
属性
基于 Bound 值。以下 HTML 显示了它的典型输出:
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>
如果User
居住在英国,则“Country”行的 HTML 源如下所示:
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> (1)
<option value="US">United States</option>
</select>
</td>
</tr>
1 | 请注意,添加了selected 属性。 |
如前面的示例所示,option
标签与options
标记
生成相同的标准 HTML,但允许您在
仅用于显示(属于它的位置)的 JSP,例如
示例:“-- 请选择”。
这items
属性通常填充 item 对象的集合或数组。itemValue
和itemLabel
引用这些 Item 对象的 bean 属性,如果
指定。否则,item 对象本身将转换为字符串。或者
您可以指定Map
of items,在这种情况下,映射键被解释为 option
values 和 map 值对应于选项标签。如果itemValue
或itemLabel
(或两者兼而有之)
恰好也被指定,则 item 值属性适用于 map 键,并且
item label 属性适用于 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
值设置为隐藏值,则 HTML 将如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
这errors
标记
此标签在 HTML 中呈现字段错误span
元素。它提供对错误的访问
在您的控制器中创建的,或者是由与
您的控制器。
假设我们想要显示firstName
和lastName
字段。我们有一个 Validator 用于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 的 DELETE 命令。对于每种方法,HTTP 规范定义了确切的语义。为
例如,GET 应该始终是一个安全的作,这意味着它没有副作用,
PUT 或 DELETE 应该是幂等的,这意味着您可以重复这些作
一遍又一遍,但最终结果应该是相同的。虽然 HTTP 定义了这些
四种方法,HTML 只支持两种:GET 和 POST。幸运的是,有两种可能
解决方法:您可以使用 JavaScript 执行 PUT 或 DELETE,也可以执行 POST
使用“real”方法作为附加参数(在
HTML 表单)。Spring的HiddenHttpMethodFilter
使用后一个技巧。这
filter 是一个普通的 Servlet 过滤器,因此,它可以与任何
Web 框架(不仅仅是 Spring MVC)。将此过滤器添加到您的 web.xml,然后 POST
带有隐藏的method
parameter 转换为对应的 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. 瓦片
您可以在 Web 中集成 Tiles - 就像任何其他视图技术一样 使用 Spring 的应用程序。本节以广泛的方式描述了如何执行此作。
本节重点介绍 Spring 对 Tiles 版本 3 的支持。org.springframework.web.servlet.view.tiles3 包。 |
依赖
为了能够使用 Tiles,您必须添加对 Tiles 版本 3.0.1 或更高版本的依赖项 及其对项目的传递依赖项。
配置
为了能够使用 Tiles,您必须使用包含定义的文件对其进行配置
(有关定义和其他 Tiles 概念的基本信息,请参阅 https://tiles.apache.org)。在 Spring 中,这是通过使用TilesConfigurer
.
以下示例ApplicationContext
配置显示了如何做到这一点:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xml</value>
<value>/WEB-INF/defs/widgets.xml</value>
<value>/WEB-INF/defs/administrator.xml</value>
<value>/WEB-INF/defs/customer.xml</value>
<value>/WEB-INF/defs/templates.xml</value>
</list>
</property>
</bean>
前面的示例定义了 5 个包含定义的文件。这些文件都位于
这WEB-INF/defs
目录。在初始化WebApplicationContext
这
加载文件,并初始化定义工厂。之后
完成后,定义文件中包含的 Tiles 可以用作
Spring Web 应用程序。为了能够使用这些视图,您必须有一个ViewResolver
与 Spring 一起使用的任何其他视图技术一样。您可以使用以下两种
implementations、UrlBasedViewResolver
和ResourceBundleViewResolver
.
您可以通过添加下划线,然后 区域设置,如下例所示:
<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>
ResourceBundleViewResolver
这ResourceBundleViewResolver
必须提供一个属性文件,其中包含
解析程序可以使用的视图名称和视图类。以下示例显示了一个 Bean
定义ResourceBundleViewResolver
以及相应的 view names 和 view
类(取自 Pet Clinic 示例):
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
</bean>
... welcomeView.(class)=org.springframework.web.servlet.view.tiles3.TilesView welcomeView.url=welcome (this is the name of a Tiles definition) vetsView.(class)=org.springframework.web.servlet.view.tiles3.TilesView vetsView.url=vetsView (again, this is the name of a Tiles definition) findOwnersForm.(class)=org.springframework.web.servlet.view.JstlView findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp ...
当您使用ResourceBundleViewResolver
,您可以轻松混合
不同的视图技术。
请注意,TilesView
类支持 JSTL(JSP 标准标记库)。
SimpleSpringPreparerFactory
和SpringBeanPreparerFactory
作为一项高级功能,Spring 还支持两个特殊的 TilePreparerFactory
实现。有关如何使用的详细信息,请参阅 Tiles 文档ViewPreparer
Tiles 定义文件中的引用。
您可以指定SimpleSpringPreparerFactory
自动装配ViewPreparer
基于
指定的 preparer 类,应用 Spring 的容器回调以及应用
配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围的注释配置具有
已激活,中的 AnnotationsViewPreparer
类被自动检测并
应用的。请注意,这需要 Tiles 定义文件中的 preparer 类,因为
默认的PreparerFactory
确实。
您可以指定SpringBeanPreparerFactory
要对指定的编制者名称进行作(而不是
类),从 DispatcherServlet 的
应用程序上下文。完整的 bean 创建过程由 Spring 控制
application 上下文,允许使用显式依赖注入
configuration、作用域 bean 等。请注意,您需要定义一个 Spring bean 定义
对于每个编制者名称(在 Tiles 定义中使用)。以下示例显示了
如何定义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()
方法(默认实现为
empty) 的 Null S以下示例显示了如何执行此作:
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentAtomView : AbstractAtomFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Feed, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedEntries(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Entry> {
// implementation omitted
}
}
类似的要求也适用于实施AbstractRssFeedView
,如下例所示:
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
class SampleContentRssView : AbstractRssFeedView() {
override fun buildFeedMetadata(model: Map<String, Any>,
feed: Channel, request: HttpServletRequest) {
// implementation omitted
}
override fun buildFeedItems(model: Map<String, Any>,
request: HttpServletRequest, response: HttpServletResponse): List<Item> {
// implementation omitted
}
}
这buildFeedItems()
和buildFeedEntries()
方法传入 HTTP 请求,以
您需要访问 Locale。HTTP 响应仅针对
Cookie 或其他 HTTP 标头。源会自动写入响应
object 返回。
有关创建 Atom 视图的示例,请参阅 Alef Arends 的 Spring Team 博客文章。
1.10.8. PDF 和 Excel
Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。 本节介绍如何使用这些功能。
文档视图简介
HTML 页面并不总是用户查看模型输出的最佳方式。 Spring 使生成 PDF 文档或 Excel 电子表格变得简单 从模型数据动态获取。文档是视图,并从 服务器,以(希望)使客户端 PC 能够运行其 电子表格或 PDF 查看器应用程序作为响应。
为了使用 Excel 视图,您需要将 Apache POI 库添加到您的 Classpath 中。 对于 PDF 生成,您需要添加(最好)OpenPDF 库。
您应该使用最新版本的基础文档生成库 如果可能的话。特别是,我们强烈推荐 OpenPDF(例如,OpenPDF 1.2.12) 而不是过时的原始 iText 2.1.7,因为 OpenPDF 得到了积极维护并且 修复了不受信任的 PDF 内容的重要漏洞。 |
PDF 视图
单词列表的简单 PDF 视图可以扩展org.springframework.web.servlet.view.document.AbstractPdfView
并实施buildPdfDocument()
方法,如下例所示:
public class PdfWordList extends AbstractPdfView {
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
class PdfWordList : AbstractPdfView() {
override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
request: HttpServletRequest, response: HttpServletResponse) {
val words = model["wordList"] as List<String>
for (word in words) {
doc.add(Paragraph(word))
}
}
}
控制器可以从外部视图定义返回此类视图
(按名称引用)或作为View
实例。
1.10.9. Jackson
Spring 提供对 Jackson JSON 库的支持。
基于 Jackson 的 JSON MVC 视图
这MappingJackson2JsonView
使用 Jackson 库的ObjectMapper
呈现响应
content 作为 JSON 格式。默认情况下,模型映射
特定于框架的类)编码为 JSON。对于
map 需要过滤,则可以指定一组特定的模型属性进行编码
通过使用modelKeys
财产。您还可以使用extractValueFromSingleKeyModel
属性直接提取和序列化单键模型中的值,而不是
than 作为模型属性的映射。
您可以根据需要使用 Jackson 提供的
附注。当您需要进一步控制时,您可以注入自定义ObjectMapper
通过ObjectMapper
属性,适用于需要提供自定义 JSON 的情况
特定类型的序列化器和反序列化器。
基于 Jackson 的 XML 视图
MappingJackson2XmlView
使用 Jackson XML 扩展的 XmlMapper
将响应内容呈现为 XML。如果模型包含多个条目,则应
使用modelKey
bean 属性。如果
model 包含一个条目,它会自动序列化。
您可以根据需要使用 JAXB 或 Jackson 提供的 XML 映射来自定义 XML 映射
附注。当您需要进一步控制时,您可以注入自定义XmlMapper
通过ObjectMapper
属性,适用于自定义 XML
您需要为特定类型提供序列化器和反序列化器。
1.10.10. XML 封送
这MarshallingView
使用 XMLMarshaller
(在org.springframework.oxm
package) 将响应内容呈现为 XML。您可以将对象显式设置为
使用MarshallingView
实例的modelKey
bean 属性。或者
该视图迭代所有模型属性并封送支持的第一个类型
由Marshaller
.有关org.springframework.oxm
包中,请参阅使用 O/X 映射器封送 XML。
1.10.11. XSLT 视图
XSLT 是一种 XML 转换语言,在 Web 中作为一种流行的视图技术 应用。如果您的应用程序 自然会处理 XML,或者如果您的模型可以很容易地转换为 XML。以下内容 部分介绍如何将 XML 文档生成为模型数据,并使用 Spring Web MVC 应用程序中的 XSLT 进行验证。
此示例是一个简单的 Spring 应用程序,它在Controller
并将其添加到模型映射中。将返回 map 以及视图
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 文档。
有一些软件包可以自动 “domify” 一个对象图,但是,在 Spring 中,您可以完全灵活地创建 DOM 从您的模型中以您选择的任何方式。这会阻止 XML 播放的转换 在模型数据的结构中占有太大的比重,这在使用工具时很危险 来管理 DOMification 过程。
转型
最后,XsltViewResolver
解析 “home” XSLT 模板文件并合并
DOM 文档添加到其中来生成我们的视图。如XsltViewResolver
配置中,XSLT 模板位于war
文件中的WEB-INF/xsl
目录
并以xslt
文件扩展名。
以下示例显示了 XSLT 转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
前面的转换呈现为以下 HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
1.11. MVC 配置
MVC Java 配置和 MVC XML 命名空间提供默认配置 适用于大多数应用程序,并提供配置 API 对其进行自定义。
对于更高级的自定义(在配置 API 中不可用), 请参见高级 Java 配置和高级 XML 配置。
您不需要了解 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 并适应依赖项 在 Classpath 上可用(例如,JSON、XML 等的有效负载转换器)。
1.11.2. MVC 配置 API
在 Java 配置中,您可以实现WebMvcConfigurer
接口,作为
以下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
// Implement configuration methods...
}
在 XML 中,您可以检查<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 值。这适用于日期表示为带有 “input” 形式的字符串的表单 领域。但是,对于“日期”和“时间”表单字段,浏览器使用定义的固定格式 在 HTML 规范中。对于此类情况,可以按如下方式自定义日期和时间格式:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addFormatters(registry: FormatterRegistry) {
val registrar = DateTimeFormatterRegistrar()
registrar.setUseIsoFormat(true)
registrar.registerFormatters(registry)
}
}
看这FormatterRegistrar SPI 系列和FormattingConversionServiceFactoryBean 有关何时使用
FormatterRegistrar 实现。 |
1.11.4. 验证
默认情况下,如果存在 Bean Validation
在 Classpath(例如,Hibernate Validator)上,LocalValidatorFactoryBean
是
注册为全局验证器以用于@Valid
和Validated
on controller 方法参数。
在 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/**");
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(LocaleChangeInterceptor())
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*")
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/admin/**"/>
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/secure/*"/>
<bean class="org.example.SecurityInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
1.11.6. 内容类型
您可以配置 Spring MVC 如何从请求中确定请求的媒体类型
(例如,Accept
header、URL 路径扩展名、查询参数等)。
默认情况下,首先检查 URL 路径扩展名 — 使用json
,xml
,rss
和atom
注册为已知扩展(取决于 Classpath 依赖项)。这Accept
页眉
将再次选中。
考虑将这些默认值更改为Accept
标头,并且,如果必须使用基于 URL 的
内容类型解析,请考虑对路径扩展使用 Query Parameter 策略。请参阅 Suffix Match 和 Suffix Match 和 RFD 以了解
更多细节。
在 Java 配置中,您可以自定义请求的内容类型解析,因为 以下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON)
configurer.mediaType("xml", MediaType.APPLICATION_XML)
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes">
<value>
json=application/json
xml=application/xml
</value>
</property>
</bean>
1.11.7. 消息转换器
您可以自定义HttpMessageConverter
在 Java 配置中,通过覆盖configureMessageConverters()
(替换 Spring MVC 创建的默认转换器)或通过覆盖extendMessageConverters()
(自定义默认转换器或向默认转换器添加其他转换器)。
以下示例使用自定义的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 的默认属性,如下所示:
如果在 Classpath 中检测到以下众所周知的模块,它还会自动注册它们:
-
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 dependency 之外jackson-dataformat-xml 一。 |
其他有趣的 Jackson 模块可用:
-
jackson-datatype-money:支持
javax.money
types (非官方模块)。 -
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
那立即
在调用时转发到 View。您可以在没有 Java 控制器的静态情况下使用它
在 View 生成响应之前运行的 logic 来运行。
以下 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
method 映射到任何 HTTP 方法的 URL,然后是视图
controller 不能用于处理相同的 URL。这是因为通过 URL 与
带注释的 controller 被认为是端点所有权的足够强的指示,因此
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 和脚本模板也需要 底层 View 技术的配置。
MVC 命名空间提供专用元素。以下示例适用于 FreeMarker:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache="false"/>
</mvc:view-resolvers>
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>
在 Java 配置中,您可以添加相应的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
标头也会被评估,如果存在,则还会评估304
返回 status code。
下面的清单显示了如何使用 Java 配置来实现这一点:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926);
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926)
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:resources mapping="/resources/**"
location="/public, classpath:/static/"
cache-period="31556926" />
另请参阅静态资源的 HTTP 缓存支持。
资源处理程序还支持ResourceResolver
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 并应用完整的解析器链,以及
transformers — 例如,插入版本。MVC 配置提供了一个ResourceUrlProvider
bean 的 bean 中,以便可以将其注入到其他 bean 中。您还可以使用ResourceUrlEncodingFilter
对于 Thymeleaf、JSP、FreeMarker 和其他带有 URL 标签的
恃HttpServletResponse#encodeURL
.
请注意,当同时使用EncodedResourceResolver
(例如,用于提供 gzip 压缩的或
brotli 编码的资源)和VersionResourceResolver
,则必须按此顺序注册它们。
这可确保始终基于未编码的文件可靠地计算基于内容的版本。
WebJar 也通过WebJarsResourceResolver
当org.webjars:webjars-locator-core
library 存在于 Classpath 中。解析程序可以
重写 URL 以包含 jar 的版本,并且还可以与传入的 URL 匹配
没有版本 — 例如,从/jquery/jquery.min.js
自/jquery/1.2.0/jquery.min.js
.
1.11.11. 默认 Servlet
Spring MVC 允许将DispatcherServlet
to (从而覆盖映射
),同时仍然允许静态资源请求为
由容器的默认 Servlet 处理。它配置了一个/
DefaultServletHttpRequestHandler
其中 URL 映射为 且优先级最低
相对于其他 URL 映射。/**
此处理程序将所有请求转发到默认 Servlet。因此,它必须
按所有其他 URL 的顺序保持最后HandlerMappings
.那就是
case 如果您使用<mvc:annotation-driven>
.或者,如果您设置了
自有定制HandlerMapping
实例,请务必将其order
property 设置为值
低于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
对于
default Servlet 必须按名称而不是按路径检索。这DefaultServletHttpRequestHandler
尝试自动检测 的默认 Servlet
容器,使用大多数主要 Servlet 的已知名称列表
容器(包括 Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)。
如果默认 Servlet 已使用其他名称进行自定义配置,或者如果
在默认 Servlet 名称未知的情况下使用了不同的 Servlet 容器,
然后,您必须显式提供默认 Servlet 的名称,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
configurer.enable("myCustomDefaultServlet")
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
1.11.12. 路径匹配
您可以自定义与路径匹配和 URL 处理相关的选项。
有关各个选项的详细信息,请参阅PathMatchConfigurer
javadoc 的
以下示例演示如何在 Java 配置中自定义路径匹配:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
@Bean
public UrlPathHelper urlPathHelper() {
//...
}
@Bean
public PathMatcher antPathMatcher() {
//...
}
}
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configurePathMatch(configurer: PathMatchConfigurer) {
configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
}
@Bean
fun urlPathHelper(): UrlPathHelper {
//...
}
@Bean
fun antPathMatcher(): PathMatcher {
//...
}
}
以下示例显示了如何在 XML 中实现相同的配置:
<mvc:annotation-driven>
<mvc:path-matching
trailing-slash="false"
registered-suffixes-only="true"
path-helper="pathHelper"
path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>
1.11.13. 高级 Java 配置
@EnableWebMvc
进口DelegatingWebMvcConfiguration
哪:
-
为 Spring MVC 应用程序提供默认 Spring 配置
-
检测并委托
WebMvcConfigurer
实现来自定义该配置。
对于高级模式,您可以删除@EnableWebMvc
并直接从DelegatingWebMvcConfiguration
而不是实现WebMvcConfigurer
,
如下例所示:
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {
// ...
}
您可以将现有方法保留在WebConfig
,但您现在也可以覆盖 Bean 声明
从基类中,您仍然可以拥有任意数量的其他WebMvcConfigurer
上的 implementations
类路径。
1.11.14. 高级 XML 配置
MVC 命名空间没有高级模式。如果需要自定义 上的属性
一个 Bean,否则你可以使用BeanPostProcessor
生命周期
弹簧之钩ApplicationContext
,如下例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
@Component
class MyPostProcessor : BeanPostProcessor {
override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
// ...
}
}
请注意,您需要声明MyPostProcessor
作为 Bean,在 XML 中显式或
通过让<component-scan/>
声明。
1.12. HTTP/2 协议
需要 Servlet 4 容器来支持 HTTP/2,并且 Spring Framework 5 是兼容的 使用 Servlet API 4.从编程模型的角度来看,没有什么具体的 应用程序需要这样做。但是,有一些与服务器配置相关的注意事项。 有关更多详细信息,请参阅 HTTP/2 wiki 页面。
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。
-
反应流背压。
-
高并发性,硬件资源较少。
-
函数式的 Fluent API,利用 Java 8 lambda。
-
同步和异步交互。
-
向服务器流式传输或从服务器向式传输。
有关更多详细信息,请参阅 WebClient 。
3. 测试
本部分总结了spring-test
用于 Spring MVC 应用程序。
-
Servlet API Mocks:用于单元测试控制器的 Servlet API 契约的模拟实现, 过滤器和其他 Web 组件。有关更多详细信息,请参阅 Servlet API 模拟对象。
-
TestContext 框架:支持在 JUnit 和 TestNG 测试中加载 Spring 配置, 包括跨测试方法对加载的配置进行高效缓存,并支持 加载一个
WebApplicationContext
替换为MockServletContext
. 有关更多详细信息,请参阅 TestContext Framework。 -
Spring MVC 测试:一个框架,也称为
MockMvc
,用于测试带注释的控制器 通过DispatcherServlet
(即支持注释),并带有 Spring MVC 基础设施,但没有 HTTP 服务器。 有关更多详细信息,请参见 Spring MVC Test。 -
客户端 REST:
spring-test
提供MockRestServiceServer
您可以将其用作 一个模拟服务器,用于测试内部使用RestTemplate
. 有关更多详细信息,请参阅客户端 REST 测试。 -
WebTestClient
:专为测试 WebFlux 应用程序而构建,但也可用于 通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个 非阻塞、反应式客户端,非常适合测试异步和流 场景。
4. 网络套接字
参考文档的这一部分涵盖了对 Servlet 堆栈、WebSocket 的支持 消息收发,包括原始 WebSocket 交互、通过 SockJS 进行的 WebSocket 仿真,以及 通过 STOMP 作为 WebSocket 上的子协议进行发布-订阅消息传递。
4.1. WebSocket 简介
WebSocket 协议 RFC 6455 提供了标准化的 在 Client 端和 Server 之间建立全双工双向通信通道的方法 通过单个 TCP 连接。它是与 HTTP 不同的 TCP 协议,但旨在 通过 HTTP 工作,使用端口 80 和 443,并允许重复使用现有防火墙规则。
WebSocket 交互以使用 HTTPUpgrade
页眉
进行升级,或者在本例中切换到 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 套接字将保留 open 以继续发送和接收消息。
有关 WebSockets 工作原理的完整介绍超出了本文档的范围。 请参阅 RFC 6455、HTML5 的 WebSocket 章节或许多介绍中的任何一个 Web 上的教程。
请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,则 可能需要对其进行配置,以便将 WebSocket 升级请求传递给 WebSocket 服务器。同样,如果应用程序在云环境中运行,请检查 云提供商与 WebSocket 支持相关的说明。
4.1.1. HTTP 与 WebSocket
尽管 WebSocket 设计为与 HTTP 兼容并以 HTTP 请求开头, 重要的是要了解这两种协议导致非常不同的结果 体系结构和应用程序编程模型。
在 HTTP 和 REST 中,应用程序被建模为多个 URL。要与应用程序交互, 客户端以请求-响应样式访问这些 URL。服务器将请求路由到 基于 HTTP URL、方法和标头的适当处理程序。
相比之下,在 WebSockets 中,初始连接通常只有一个 URL。 随后,所有应用程序消息都在同一 TCP 连接上流动。这指向 一种完全不同的异步、事件驱动的消息传递架构。
WebSocket 也是一种低级传输协议,与 HTTP 不同,它没有规定 消息内容的任何语义。这意味着无法路由或处理 一条消息,除非客户端和服务器在消息语义上达成一致。
WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议
(例如,STOMP),通过Sec-WebSocket-Protocol
标头。
如果没有这些,他们需要提出自己的惯例。
4.1.2. 何时使用 WebSockets
WebSockets 可以使网页具有动态和交互性。但是,在许多情况下, Ajax 和 HTTP 流式处理或长轮询的组合可以提供简单且 有效的解决方案。
例如,新闻、邮件和社交源需要动态更新,但可能需要 每隔几分钟这样做一次完全可以。协作、游戏和金融应用程序 另一方面,需要更接近实时。
延迟本身并不是决定因素。如果消息量相对较低(例如, 监控网络故障)HTTP 流或轮询可以提供有效的解决方案。 低延迟、高频率和高容量的组合造就了最好的 case 来使用 WebSocket。
另请记住,在 Internet 上,您无法控制的限制性代理
可能会排除 WebSocket 交互,因为它们未配置为传递Upgrade
标头,或者因为它们关闭了看起来空闲的长期连接。这
意味着将 WebSocket 用于防火墙内的内部应用程序是
比面向公众的应用程序更直接的决定。
4.2. WebSocket API
Spring Framework 提供了一个 WebSocket API,您可以使用它来编写 client- 和 处理 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
,它公开了握手的“之前”和“之后”的方法。
你可以使用这样的拦截器来排除握手或创建任何属性
available to 的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 配置时,默认提供和添加 implementations
或 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 容器 Classpath 扫描(Servlet 3 的一项功能)。
另一个是在 Servlet 容器初始化时使用的注册 API。
这两种机制都无法实现使用单个 “前端控制器”
用于所有 HTTP 处理 — 包括 WebSocket 握手和所有其他 HTTP
requests — 例如 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
header 值(有关详细信息,请参阅 RFC 6454:Web Origin 概念)。
三种可能的行为是:
-
仅允许同源请求(默认):在此模式下,启用 Sockjs 时, Iframe HTTP 响应标头
X-Frame-Options
设置为SAMEORIGIN
和 JSONP transport 被禁用,因为它不允许检查请求的来源。 因此,启用此模式时,不支持 IE6 和 IE7。 -
允许指定的源列表:每个允许的源必须以
http://
或https://
.在此模式下,启用 Sockjs 时,将禁用 IFrame 传输。 因此,IE6 到 IE9 在执行此作时不受支持 mode 已启用。 -
允许所有源:要启用此模式,您应该提供作为允许的源 价值。在此模式下,所有传输都可用。
*
您可以配置 WebSocket 和 Sockjs 允许的源,如下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
以下示例显示了与上述示例等效的 XML 配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
4.3. SockJS 回退
在公共 Internet 上,不受您控制的限制性代理可能会阻止 WebSocket
交互,要么是因为它们未配置为将Upgrade
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 (long)
轮询。
所有传输请求都具有以下 URL 结构:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
哪里:
-
{server-id}
对于在集群中路由请求很有用,但不会以其他方式使用。 -
{session-id}
关联属于 Sockjs 会话的 HTTP 请求。 -
{transport}
表示传输类型(例如websocket
,xhr-streaming
等)。
WebSocket 传输只需要一个 HTTP 请求即可执行 WebSocket 握手。 此后的所有消息都在该套接字上交换。
HTTP 传输需要更多请求。例如,Ajax/XHR 流依赖于 一个长时间运行的服务器到客户端消息请求和其他 HTTP POST 客户端到服务器消息的请求。长轮询与此类似,只不过它 在每次服务器到客户端发送后结束当前请求。
Sockjs 添加了最少的消息框架。例如,服务器将信件o
(“打开”框架)最初,消息以a["message1","message2"]
(JSON 编码数组),则字母h
(“heartbeat” 帧)如果没有消息流
25 秒(默认),并且字母c
(“close” frame) 以关闭会话。
要了解更多信息,请在浏览器中运行示例并观察 HTTP 请求。
Sockjs 客户端允许修复传输列表,因此可以
一次查看每种运输。Sockjs 客户端还提供了一个 debug 标志
,这将在浏览器控制台中启用有用的消息。在服务器端,您可以启用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 页面和
浏览器支持的传输类型。客户端还提供多个
Configuration Options (配置选项) — 例如,指定要包含的传输。
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
请求是
可以影响客户选择交通工具的信息。
其中一个细节是服务器应用程序是否依赖 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 的传输,您需要将 header 值设置为SAMEORIGIN
或ALLOW-FROM <origin>
.春季 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
可用于自定义频率。默认情况下,
检测信号在 25 秒后发送,假设没有发送其他消息
连接。此 25 秒值与以下针对公共 Internet 应用程序的 IETF 建议一致。
当通过 WebSocket 和 Sockjs 使用 STOMP 时,如果 STOMP 客户端和服务器协商 heartbeats 进行交换,则禁用 Sockjs 心跳。 |
Spring Sockjs 支持还允许您配置TaskScheduler
自
计划检测信号任务。任务调度器由线程池
使用基于可用处理器数量的默认设置。你
应考虑根据您的特定需求自定义设置。
4.3.5. 客户端断开连接
HTTP 流和 HTTP 长轮询 Sockjs 传输需要保持连接 开放时间比平时长。有关这些技术的概述,请参阅此博客文章。
在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该 允许退出 Servlet 容器线程,处理请求并继续 写入来自另一个线程的响应。
一个具体问题是 Servlet API 不为客户端提供通知 这已经消失了。参见 eclipse-ee4j/servlet-api#44。 但是,Servlet 容器会在后续写入尝试时引发异常 响应。由于 Spring 的 Sockjs 服务支持服务器发送的心跳(每个 默认为 25 秒),这意味着通常会在该时间内检测到客户端断开连接 时间段(如果消息发送更频繁,则更早)。
因此,网络 I/O 故障可能是因为客户端已断开连接,这
可能会用不必要的堆栈跟踪填充日志。Spring 尽最大努力识别
此类网络故障表示客户端断开连接(特定于每个服务器)并记录
使用专用日志类别的最小消息,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 需要以下 Headers 和值:
-
Access-Control-Allow-Origin
:从Origin
请求标头。 -
Access-Control-Allow-Credentials
:始终设置为true
. -
Access-Control-Request-Headers
:从等效请求标头中的值初始化。 -
Access-Control-Allow-Methods
:传输支持的 HTTP 方法(请参阅TransportType
enum 的 intent 中)。 -
Access-Control-Max-Age
:设置为 31536000(1 年)。
有关确切的实现,请参阅addCorsHeaders
在AbstractSockJsService
和
这TransportType
enum 的 intent 文件。
或者,如果 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 并需要
以位于 Classpath 上。或者,您可以配置SockJsMessageCodec 并在SockJsClient . |
要使用SockJsClient
要模拟大量并发用户,您需要
需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够的
连接数和线程数。以下示例显示了如何使用 Jetty 执行此作:
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
以下示例显示了服务器端与 Sockjs 相关的属性(有关详细信息,请参见 javadoc) 您还应考虑自定义:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) (1)
.setHttpMessageCacheSize(1000) (2)
.setDisconnectDelay(30 * 1000); (3)
}
// ...
}
1 | 将streamBytesLimit 属性设置为 512KB(默认值为 128KB —128 * 1024 ). |
2 | 将httpMessageCacheSize 属性设置为 1,000(默认值为100 ). |
3 | 将disconnectDelay property 设置为 30 个属性秒(默认值为 5 秒 -5 * 1000 ). |
4.4. STOMP踏
WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的 内容未定义。该协议定义了一种机制,供客户端和服务器协商 子协议(即更高级别的消息传递协议)在 WebSocket 上使用,以 定义每个可以发送的消息类型、格式、每个消息的内容 消息,依此类推。子协议的使用是可选的,但无论哪种方式,客户端和 服务器需要就定义消息内容的某个协议达成一致。
4.4.1. 概述
STOMP(简单 Text Oriented Messaging Protocol)最初是为脚本语言创建的 (例如 Ruby、Python 和 Perl)连接到企业消息代理。是的 旨在解决常用消息传递模式的最小子集。STOMP 可以是 通过任何可靠的双向流式处理网络协议(如 TCP 和 WebSocket)使用。 尽管 STOMP 是面向文本的协议,但消息有效负载可以是 text 或 binary。
STOMP 是一种基于帧的协议,其帧基于 HTTP。下面的清单显示了结构 的 STOMP 帧:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用SEND
或SUBSCRIBE
要发送或订阅的命令
消息以及destination
标头,该标头描述了
消息是关于以及谁应该接收它。这样,一个简单的
publish-subscribe 机制,可用于通过 broker 发送消息
发送到其他连接的客户端,或者向服务器发送消息以请求
执行一些工作。
当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序会执行
作为 STOMP 代理。消息被路由到@Controller
消息处理
方法或跟踪订阅的简单内存代理,以及
向订阅用户广播消息。您还可以配置 Spring 以工作
使用专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)进行实际
消息广播。在这种情况下, Spring 会维护
与代理的 TCP 连接,将消息中继到代理,并传递消息
从它向下到连接的 WebSocket 客户端。因此,Spring Web 应用程序可以
依赖于基于 HTTP 的统一安全性、通用验证和熟悉的编程
model 进行消息处理。
以下示例显示了订阅以接收股票报价的客户端,该
服务器可能会定期发出(例如,通过发送消息的计划任务
通过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
标头必须与id
标头的
客户端订阅。
前面的概述旨在提供对 STOMP 协议。我们建议完整地查看协议规范。
4.4.2. 好处
使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 提供更丰富的编程模型,而不是使用原始 WebSockets。同一点可以是 介绍了 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
endpoints,通过 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 methods 中的@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>
对于内置的 simple broker,/topic 和/queue 前缀没有任何特殊的
意义。它们只是区分 pub-sub 和 point-to-point 的约定
消息收发(即,多个订阅者与 1 个使用者)。当您使用外部代理时,
检查代理的 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
头。即使这样做了,他们也会被忽略(或者更确切地说,
overridden) 的有关身份验证的更多信息,请参见Connecting to a Broker and Authentication。
有关更多示例代码,请参阅:
-
使用 WebSocket 构建 交互式 Web 应用程序 — 入门指南。
-
Stock Portfolio — 示例 应用。
4.4.4. WebSocket 服务器
要配置底层 WebSocket 服务器,服务器配置中的信息适用。但是,对于 Jetty,您需要设置
这HandshakeHandler
和WebSocketPolicy
通过StompEndpointRegistry
:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}
@Bean
public DefaultHandshakeHandler handshakeHandler() {
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);
return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}
4.4.5. 消息流
一旦 STOMP 端点被公开,Spring 应用程序就会成为 STOMP 代理 连接的客户端。本节介绍服务器端的消息流。
这spring-messaging
模块包含对消息传递应用程序的基本支持
它源自 Spring Integration,并且是
后来提取并合并到 Spring 框架中,以便在许多 Spring 项目和应用程序场景中更广泛地使用。
以下列表简要介绍了一些可用的消息传递抽象:
-
消息: 消息的简单表示形式,包括 Headers 和 payload。
-
MessageHandler 中: 用于处理消息的协定。
-
MessageChannel 中: 用于发送支持生产者和使用者之间松散耦合的消息的 Contract。
-
SubscribableChannel 的
MessageChannel
跟MessageHandler
用户。 -
ExecutorSubscribableChannel 中:
SubscribableChannel
它使用Executor
用于传递消息。
Java 配置(即@EnableWebSocketMessageBroker
) 和 XML 命名空间配置
(即<websocket:message-broker>
) 使用上述组件来组合消息
工作流。下图显示了 simple 内置消息
broker 已启用:

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

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