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