Spring Web MVC是构建在Servlet API上的原始Web框架,从一开始就包含在Spring Framework中。正式名称“Spring Web MVC”来自其源模块(spring-webmvc)的名称,但它通常被称为“Spring MVC”。
与Spring Web MVC并行,Spring Framework 5.0引入了一个反应堆栈Web框架,其名称“Spring WebFlux”也基于其源模块(spring-webflux)。本节介绍Spring Web MVC。在下一节 介绍spring WebFlux。
有关基本信息以及与Servlet容器和Java EE版本范围的兼容性,请参阅Spring Framework Wiki。
与Spring WebFlux相同
与其他许多Web框架一样,SpringMVC是围绕前控制器模式设计的,在前控制器模式中,中央servlet(DispatcherServlet)为请求处理提供共享算法,而实际工作则由可配置的委托组件执行。该模型灵活,支持多种工作流。
DispatcherServlet作为一个servlet,需要通过使用Java配置或Web.xml来根据servlet规范声明和映射。反过来,DispatcherServlet使用Spring配置来发现请求映射、视图解析、异常处理等所需的代理组件。
下面的Java配置示例注册并初始化Deservlet Servlet,该servlet由servlet容器自动检测(参见Servlet Config):
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletCxt) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
ac.register(AppConfig.class);
ac.refresh();
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(ac);
ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
除了直接使用ServletContext API之外,您还可以扩展 AbstractAnnotationConfigDispatcherServletInitializer和覆盖特定方法(请参阅Context Hierarchy下的示例)。
以下web.xml配置示例注册并初始化DispatcherServlet:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/app-context.xmlparam-value>
context-param>
<servlet>
<servlet-name>appservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>param-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>appservlet-name>
<url-pattern>/app/*url-pattern>
servlet-mapping>
web-app>
Spring Boot遵循不同的初始化顺序。Spring Boot使用Spring配置来引导自身和嵌入式Servlet容器,而不是挂钩到Servlet容器的生命周期。Filter和Servlet声明在Spring配置中检测到并在Servlet容器中注册。有关更多详细信息,请参阅 Spring Boot文档。
上下文层次结构
DispatcherServlet需要一个WebApplicationContext(纯ApplicationContext的扩展)来进行自己的配置。WebApplicationContext具有指向servletContext及其关联的servlet的链接。它还绑定到servletcontext,这样应用程序可以在requestContextUtils上使用静态方法在需要访问WebApplicationContext时查找它。
对于许多应用程序,拥有一个WebApplicationContext是简单且足够的。还可以有一个上下文层次结构,其中一个根WebApplicationContext在多个DispatcherServlet(或其他servlet)实例之间共享,每个实例都有自己的子WebApplicationContext配置。有关上下文层次结构功能的更多信息,请参阅ApplicationContext的其他功能。
webapplicationContext通常包含基础架构bean,例如需要跨多个servlet实例共享的数据存储库和业务服务。这些bean是有效继承的,可以在servlet特定的子webapplicationContext中重写(即重新声明),后者通常包含给定servlet的本地bean。下图显示了这种关系:
以下示例配置WebApplicationContext层次结构:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
如果不需要应用程序上下文层次结构,则应用程序可以通过getrootconfigclasss()返回所有配置,并从getservletconfigclasss()返回空值。
以下示例显示了web.xml等效项:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/root-context.xmlparam-value>
context-param>
<servlet>
<servlet-name>app1servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/app1-context.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>app1servlet-name>
<url-pattern>/app1/*url-pattern>
servlet-mapping>
web-app>
如果不需要应用程序上下文层次结构,则应用程序只能配置“root”上下文,并将ContextConfigLocation servlet参数保留为空。
Same as in Spring WebFlux
DispatcherServlet委托给特殊bean处理请求并提供适当的响应。“特殊bean”是指实现WebFlux框架契约的Spring管理对象实例。这些通常带有内置契约,但您可以自定义它们的属性并扩展或替换它们。
下表列出了DispatcherHandler检测到的特殊bean:
Bean type | Explanation |
---|---|
HandlerMapping | 将请求与拦截器列表一起映射到处理程序,以便进行预处理和后处理。映射基于某些标准,其细节因handlerMapping实现而异。 两个主要的handlerMapping实现是RequestMappingHandlerMapping(支持@RequestMapping注释方法)和SimpleUrlHandlerMapping(维护对处理程序的URI路径模式的显式注册)。 |
HandlerAdapter | 帮助DispatcherServlet调用映射到请求的处理程序,不管实际如何调用该处理程序。例如,调用带注释的控制器需要解析注释。handlerAdapter的主要目的是保护DispatcherServlet不受此类细节的影响。 |
HandlerExceptionResolver | 解决异常的策略,可能将异常映射到处理程序、HTML错误视图或其他目标。参见异常。 |
ViewResolver | 将从处理程序返回的基于逻辑字符串的视图名称解析为要用其呈现给响应的实际视图。请参见视图分辨率和视图技术。 |
LocaleResolver, LocaleContextResolver | 解析客户端正在使用的区域设置,并可能解析其时区,以便能够提供国际化的视图。查看区域设置。 |
ThemeResolver | 解决Web应用程序可以使用的主题,例如,提供个性化布局。参见主题。 |
MultipartResolver | 在某些多部分分析库的帮助下,用于分析多部分请求(例如,浏览器表单文件上载)的抽象。请参见多部分分解器。 |
FlashMapManager | 存储并检索“输入”和“输出”flashmap,这些flashmap可用于将属性从一个请求传递到另一个请求,通常是通过重定向传递的。请参见Flash属性。 |
Same as in Spring WebFlux
应用程序可以声明在处理请求所需的特殊bean类型中列出的基础结构bean。DispatcherServlet检查每个特殊bean的WebApplicationContext。如果没有匹配的bean类型,则返回DispatcherServlet.properties中列出的默认类型。
在大多数情况下,MVC配置是最好的起点。它在Java或XML中声明所需的bean,并提供更高级的配置回调API来定制它。
Spring Boot依赖于MVC Java配置来配置Spring MVC并提供许多额外的便捷选项。
在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("/");
}
}
WebApplicationInitializer是SpringMVC提供的一个接口,它确保检测到您的实现并自动用于初始化任何servlet 3容器。名为AbstractDispatcherServletInitializer的WebApplicationInitializer的抽象基类实现通过重写方法来指定servlet映射和DispatcherServlet配置的位置,使注册DispatcherServlet更加容易。
建议使用基于Java的Spring配置的应用程序,如以下示例所示:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
如果使用基于XML的Spring配置,则应直接扩展 AbstractDispatcherServletInitializer,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AbstractDispatcherServletInitializer还提供了一种方便的方法来添加Filter 实例并让它们自动映射到DispatcherServlet,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每个过滤器都会根据其具体类型添加一个默认名称,并自动映射到DispatcherServlet。
AbstractDispatcherServletInitializer 的isAsyncSupported protected方法提供了一个单独的位置,可以在DispatcherServlet和映射到它的所有筛选器上启用异步支持。默认情况下,此标志设置为“true”。
最后,如果需要进一步自定义DispatcherServlet本身,可以重写CreateSpatcherServlet方法。
Same as in Spring WebFlux
DispatcherServlet按如下方式处理请求:
WebApplicationContext中声明的handlerExceptionResolver bean用于解决请求处理期间引发的异常。这些异常解析器允许定制逻辑来处理异常。有关详细信息,请参阅例外。
Spring DispatcherServlet还支持返回servlet API指定的最后修改日期。确定特定请求的最后修改日期的过程很简单:DispatcherServlet查找适当的处理程序映射,并测试找到的处理程序是否实现了最后修改的接口。如果是这样,则将向客户端返回lastmodified接口的long getlastmodified(request)方法的值。
通过将servlet初始化参数(init param元素)添加到web.xml文件中的servlet声明,可以自定义各个DispatcherServlet实例。下表列出了支持的参数:
DispatcherServlet初始化参数
Parameter | Explanation |
---|---|
contextClass | 实现了ConfigurableWebApplicationContext的类,通过这个类来实例化和本地配置,默认XmlWebApplicationContext 被使用 |
contextConfigLocation | 传递到上下文实例(由ContextClass指定)的字符串,用于指示可以在何处找到上下文。字符串可能包含多个字符串(使用逗号作为分隔符),以支持多个上下文。如果有两次定义bean的多个上下文位置,则以最新位置为准。 |
namespace | WebApplicationContext的命名空间,默认[servlet-name]-servlet |
throwExceptionIfNoHandlerFound | 在找不到请求的处理程序时是否引发NoHandlerFoundException。然后,可以使用handlerExceptionResolver捕获异常(例如,使用@exceptionHandler控制器方法),并像任何其他方法一样进行处理。默认情况下,此设置为false,在这种情况下,DispatcherServlet将响应状态设置为404(未找到),而不会引发异常。注意,如果还配置了默认servlet处理,未解析的请求总是被转发到默认servlet,并且永远不会引发404。 |
所有handlerMapping实现都支持处理程序拦截器,当您希望将特定功能应用于某些请求时,这些拦截器很有用-例如,检查主体。拦截器必须使用三种方法从org.springframework.web.servlet包中实现handlerInterceptor,这三种方法应提供足够的灵活性来执行各种预处理和后处理:
prehandle(…)方法返回一个布尔值。可以使用此方法中断或继续执行链的处理。当此方法返回true时,处理程序执行链将继续。当返回false时,DispatcherServlet假定拦截器本身处理了请求(例如,呈现了适当的视图),并且不继续执行执行链中的其他拦截器和实际处理程序。
有关如何配置拦截器的示例,请参阅MVC配置部分中的拦截器。还可以通过在单个handlerMapping实现上使用setter直接注册它们。
请注意,posthandle对于在handlerAdapter和posthandle之前写入和提交响应的@responseBody和responseEntity方法不太有用。这意味着对响应进行任何更改(例如添加额外的头)都为时已晚。对于这种情况,您可以实现ResponseBodyAdvice,并将其声明为控制器建议bean,或者直接在RequestMappingHandlerAdapter上配置它。
Same as in Spring WebFlux
如果在请求映射期间发生异常或从请求处理程序(如@controller)抛出异常,则DispatcherServlet将委托给handlerExceptionResolver bean链以解决异常并提供替代处理,这通常是一个错误响应。
下表列出了可用的HandlerExceptionResolver实现:
HandlerExceptionResolver | Description |
---|---|
SimpleMappingExceptionResolver | 异常类名称和错误视图名称之间的映射。用于在浏览器应用程序中呈现错误页面 |
DefaultHandlerExceptionResolver | 解决Spring MVC引发的异常并将它们映射到HTTP状态代码。另请参阅替代ResponseEntityExceptionHandler和REST API异常。 |
ResponseStatusExceptionResolver | 使用@ResponseStatus注释解析异常,并根据注释中的值将它们映射到HTTP状态代码。 |
ExceptionHandlerExceptionResolver | 通过调用@controller或@controlleradvice类中的@exceptionhandler方法来解决异常。请参见@exceptionhandler方法。 |
通过在Spring配置中声明多个handlerExceptionResolver bean并根据需要设置它们的顺序属性,可以形成异常冲突解决程序链。Order属性越高,异常解决程序的位置就越晚。
handlerExceptionResolver的协定指定它可以返回:
MVC配置自动为默认的Spring MVC异常、@responseStatus注释的异常以及支持@exceptionhandler方法声明内置的冲突解决程序。您可以自定义或替换该列表。
如果异常仍然未被任何handlerExceptionResolver解决,因此仍保留传播状态,或者如果响应状态设置为错误状态(即4xx、5xx),servlet容器可以用HTML呈现默认错误页。要自定义容器的默认错误页,可以在web.xml中声明错误页映射。以下示例显示了如何执行此操作:
<error-page>
<location>/errorlocation>
error-page>
在前面的示例中,当异常冒泡或响应具有错误状态时,servlet容器会在容器内将错误分派到配置的URL(例如,/error)。然后,DispatcherServlet会对其进行处理,可能会将其映射到@Controller,该控制器可以实现为返回带有模型的错误视图名称或呈现JSON响应,如下例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}
Servlet API不提供在Java中创建错误页面映射的方法。但是,您可以同时使用WebApplicationInitializer和最小的web.xml。
Same as in Spring WebFlux
SpringMVC定义了视图解析器和视图接口,这些接口允许您在浏览器中呈现模型,而不必绑定到特定的视图技术。ViewResolver提供视图名称和实际视图之间的映射。视图解决了在移交给特定视图技术之前的数据准备问题。
下表提供了有关ViewResolver层次结构的详细信息:
ViewResolver | Description |
---|---|
AbstractCachingViewResolver | AbstractCachingViewResolver的子类缓存它们解析的视图实例。缓存提高了某些视图技术的性能。通过将cache属性设置为false,可以关闭缓存。此外,如果必须在运行时刷新某个视图(例如,修改FreeMarker模板时),则可以使用removeFromCache(string viewname,locale loc)方法。 |
XmlViewResolver | ViewResolver的实现,它接受用XML编写的配置文件,该配置文件的DTD与Spring的XML bean工厂相同。默认配置文件是/WEB-INF/views.xml。 |
ResourceBundleViewResolver | ViewResolver 的实现使用ResourceBundle中的bean定义,通过一些基础的名字被指定。对于应该解析的每个视图,它使用属性[viewname].(class)的值作为视图类,使用属性[viewname].url的值作为视图URL。您可以在有关视图技术的章节中找到示例。 |
UrlBasedViewResolver | viewresolver接口的简单实现,它影响逻辑视图名称到URL的直接解析,而无需显式映射定义。如果您的逻辑名称以直接的方式匹配视图资源的名称,而不需要任意映射,那么这是适当的。 |
InternalResourceViewResolver | 支持InternalResourceView(实际上是servlets和jsp)的URLBasedView解析器的方便子类,以及JSTLView和TilesView等子类。可以使用setViewClass(…)为该解析程序生成的所有视图指定视图类。有关详细信息,请参阅urlbasedview解析器javadoc。 |
FreeMarkerViewResolver | 方便的子类UrlBasedViewResolver支持FreeMarkerView它们的自定义子类。 |
ContentNegotiatingViewResolver | viewresolver接口的实现,该接口基于请求文件名或接受头解析视图。参见Content Negotiation.。 |
Same as in Spring WebFlux
可以通过声明多个冲突解决程序bean来链接视图冲突解决程序,如果需要,还可以通过设置order属性来指定排序。记住,order属性越高,视图解析器在链中的位置就越晚。
ViewResolver的协定指定它可以返回空值,以指示找不到该视图。但是,对于JSP和InternalResourceViewResolver,了解JSP是否存在的唯一方法是通过RequestDispatcher执行调度。因此,必须始终按照视图冲突解决程序的总体顺序将InternalResourceViewResolver配置为最后一个。
配置视图分辨率和向Spring配置中添加viewresolver bean一样简单。MVC配置为视图解析器和添加无逻辑视图控制器提供了专用的配置API,这些配置API对于不使用控制器逻辑的HTML模板呈现非常有用。
Same as in Spring WebFlux
视图名称中的特殊redirect:prefix允许您执行重定向。URLBasedView解析器(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向URL。
net effect与控制器返回RedirectView的效果相同,但现在控制器本身可以按照逻辑视图名称进行操作。逻辑视图名称(例如redirect:/myapp/some/resource)相对于当前servlet上下文重定向,而名称(例如redirect:http://myhost.com/some/arbitrary/path)则重定向到绝对URL。
注意,如果用@responseStatus注释控制器方法,则注释值优先于RedirectView设置的响应状态。
对于最终由URLBasedView解析器和子类解析的视图名称,也可以使用特殊的forward:prefix。这将创建一个InternalResourceView,它执行requestDispatcher.forward()。因此,此前缀对于InternalResourceViewResolver和InternalResourceView(对于JSP)不太有用,但如果您使用其他视图技术,但仍希望强制资源转发由servlet/JSP引擎处理,则此前缀可能会有所帮助。请注意,您也可以链接多个视图解析器。
Same as in Spring WebFlux
ContentNegotiangViewResolver不解析视图本身,而是委托给其他视图冲突解决程序,并选择类似于客户端请求的表示的视图。可以从accept头或查询参数(例如,"/path?format=pdf")确定表示形式。
ContentNegotiangViewResolver通过将请求媒体类型与与其每个视图冲突解决程序关联的视图所支持的媒体类型(也称为内容类型)进行比较来选择适当的视图来处理请求。列表中具有兼容内容类型的第一个视图将表示返回给客户端。如果viewresolver链无法提供兼容的视图,则将查询通过defaultviews属性指定的视图列表。后一个选项适用于单实例视图,该视图可以呈现当前资源的适当表示,而不考虑逻辑视图名称。接受头可以包含通配符(例如text/*),在这种情况下,内容类型为text/xml的视图是兼容的匹配项。
有关配置详细信息,请参阅MVC配置下的视图冲突解决程序。
与SpringWebMVC框架一样,Spring架构的大多数部分支持国际化。DispatcherServlet允许您使用客户端的区域设置自动解析消息。这是通过LocaleResolver对象完成的。
当一个请求进入时,DispatcherServlet会查找一个区域设置解析器,如果找到了一个,它会尝试使用它来设置区域设置。通过使用requestContext.getLocale()方法,可以始终检索由区域设置解析程序解析的区域设置。
除了自动区域设置解析之外,还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的详细信息,请参阅拦截器),以便在特定情况下(例如,基于请求中的参数)更改区域设置。
区域设置解析器和拦截器是在org.springframework.web.servlet.i18n包中定义的,并以正常方式在应用程序上下文中配置。以下区域设置冲突解决程序的选择包含在Spring中。
除了获得客户机的区域设置外,了解其时区通常也很有用。LocaleContextResolver接口提供了对LocaleResolver的扩展,允许冲突解决程序提供更丰富的LocaleContext,其中可能包括时区信息。
如果可用,可以使用requestContext.getTimeZone()方法获取用户的时区。时区信息由任何向Spring的ConversionService注册的日期/时间转换器和格式化程序对象自动使用。
此区域设置冲突解决程序检查客户端(例如,Web浏览器)发送的请求中的接受语言头。通常,此头字段包含客户端操作系统的区域设置。请注意,此冲突解决程序不支持时区信息。
此区域设置冲突解决程序检查客户端上可能存在的cookie,以查看是否指定了区域设置或时区。如果是,则使用指定的详细信息。通过使用此区域设置冲突解决程序的属性,可以指定cookie的名称以及最长使用期限。以下示例定义了一个CookieCalersolver:
bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<property name="cookieMaxAge" value="100000"/>
bean>
下表描述了这些属性CookieLocaleResolver:
Property | Default | Description |
---|---|---|
cookieName | classname + LOCALE | The name of the cookie |
cookieMaxAge | Servlet container default | cookie在客户端上的最长持续时间。如果指定了-1,则不会持久化cookie。只有在客户端关闭浏览器之前,它才可用。 |
cookiePath | / | 将cookie的可见性限制在网站的某个部分。当指定了cookie path时,cookie仅对该路径及其下面的路径可见。 |
sessionLocaleResolver允许您从可能与用户请求相关联的会话中检索区域设置和时区。与cookielocalersolver不同,此策略将本地选择的区域设置存储在servlet容器的httpsession中。因此,这些设置对于每个会话都是临时的,因此在每个会话终止时都会丢失。
请注意,与外部会话管理机制(如Spring会话项目)没有直接关系。此sessionLocalerResolver针对当前的httpServletRequest评估并修改相应的httpSession属性。
您可以通过将localeChangeInterceptor添加到一个handlerMapping定义来启用区域设置的更改。它在请求中检测到一个参数,并相应地更改区域设置,在调度程序的应用程序上下文中调用LocaleResolver上的setLocale方法。下一个示例显示,对包含名为sitelanguage的参数的所有*.view资源的调用现在都会更改区域设置。例如,请求URL,http://www.sf.net/home.view?site language=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=someControllervalue>
property>
bean>
您可以应用Spring Web MVC框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响应用程序的视觉样式。
要在Web应用程序中使用主题,必须设置org.springframework.ui.context.themesource接口的实现。WebApplicationContext接口扩展了ThemeSource,但将其职责委托给了专用的实现。默认情况下,委托是一个org.springframework.ui.context.support.resourcebundleThemeSource实现,它从类路径的根目录加载属性文件。要使用自定义ThemeSource实现或配置ResourceBundleThemeSource的基名称前缀,可以在应用程序上下文中使用保留名称ThemeSource注册bean。Web应用程序上下文自动检测具有该名称的bean并使用它。
使用ResourceBundleThemeSource时,主题在简单的属性文件中定义。属性文件列出组成主题的资源,如下示例所示:
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
属性的键是引用视图代码中主题元素的名称。对于JSP,您通常使用spring:theme自定义标记来完成此操作,该标记与spring:message标记非常类似。下面的JSP片段使用前面示例中定义的主题来定制外观和感觉:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="' 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的bean,以找出要使用的ThemeResolver实现。主题解析器的工作方式与本地解析器基本相同。它检测用于特定请求的主题,还可以更改请求的主题。下表介绍了Spring提供的主题解析器:
Class | Description |
---|---|
FixedThemeResolver | 选择一个通过使用DefaultThemeName属性设置的固定主题。 |
SessionThemeResolver | 主题在用户的HTTP会话中维护。它只需要为每个会话设置一次,但不会在会话之间持久化。 |
CookieThemeResolver | 所选主题存储在客户端的cookie中。 |
Spring还提供了一个ThemeChangeInterceptor,它允许使用一个简单的请求参数对每个请求进行主题更改。
Same as in Spring WebFlux
org.springframework.web.multipart包中的multipartresolver是一种分析包括文件上载在内的多部分请求的策略。有一种实现基于commons文件上传,另一种实现基于servlet 3.0 multipart 请求解析。
要启用multipart handling,需要在DispatcherServlet Spring配置中声明名为multipartResolver的multipartResolver bean。DispatcherServlet检测到它并将其应用于传入请求。当接收到内容类型为multipart/form数据的post时,解析程序将解析内容并将当前httpservletrequest包装为multipartttpservletrequest,以提供对已解析部分的访问,并将其作为请求参数公开。
要使用Apache Commons FileUpload,可以配置一个名为multipartResolver的类型为commonsMultipartResolver的bean。您还需要将commons文件上传作为对类路径的依赖。
Servlet 3
需要通过servlet容器配置启用servlet 3.0multipart parsing。这样做:
以下示例显示如何在servlet注册上设置multipartconfigement:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
一旦servlet 3.0配置就位,就可以添加一个名为multipartresolver的bean类型StandardServletMultipartResolver。
Same as in Spring WebFlux
SpringMVC中的调试级日志记录被设计为紧凑、最小化和人性化。它关注的是一次又一次有用的高价值信息位,而其他信息仅在调试特定问题时才有用。
跟踪级别日志记录通常遵循与调试相同的原则(例如,也不应该是fire hose),但可以用于调试任何问题。此外,一些日志消息可能在跟踪和调试时显示不同级别的详细信息。
良好的日志记录来自使用日志的经验。如果您发现任何不符合规定目标的地方,请通知我们。
Same as in Spring WebFlux
调试和跟踪日志记录可能会记录敏感信息。这就是为什么请求参数和头在默认情况下被屏蔽,并且必须通过DispatcherServlet上的EnableLoggingRequestDetails属性显式启用它们的完全登录。
下面的示例演示如何使用Java配置执行此操作:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
Same as in Spring WebFlux
SpringWeb模块提供了一些有用的过滤器:
浏览器只能通过HTTP GET或HTTP POST提交表单数据,但非浏览器客户端也可以使用HTTP PUT, PATCH, and DELETE。servlet API需要servletrequest.getParameter*()方法才能仅支持HTTP POST的表单字段访问。
SpringWeb模块提供了FormContentFilter,用于拦截HTTP PUT, PATCH, and DELETE内容类型为application/x-www-form-urlencoded的请求,从请求体中读取表单数据,并包装servletRequest,使表单数据通过servletRequest.getParameter*(方法系列)可用。
Same as in Spring WebFlux
当请求通过代理(如负载均衡器)时,主机、端口和方案可能会发生更改,这使得从客户机的角度创建指向正确主机、端口和方案的链接成为一个挑战。
RFC7239定义了代理可以用来提供原始请求信息的转发HTTP头。还有其他非标准头,包括x-forwarded-host、x-forwarded-port、x-forwarded-proto、x-forwarded-ssl和x-forwarded-prefix。
ForwardedHeaderFilter是一个servlet筛选器,它根据转发的头修改请求的主机、端口和方案,然后删除这些头。
由于应用程序不知道这些头是由代理(按预期)还是由恶意客户机添加的,所以对转发的头有安全考虑。这就是为什么应该将信任边界处的代理配置为删除来自外部的不受信任的转发头。您还可以将ForwardedHeaderFilter配置为removeOnly=true,在这种情况下,它将删除但不使用头。
shallowettagheaderfilter过滤器通过缓存写入响应的内容并从中计算MD5哈希来创建一个“shallow”etag。下一次客户端发送时,它执行相同的操作,但它还将计算的值与if-none-match请求头进行比较,如果两者相等,则返回304(NOT_MODIFIED未修改)。
此策略节省网络带宽,但不节省CPU,因为必须为每个请求计算完全响应。前面描述的控制器级别的其他策略可以避免计算。请参见HTTP缓存。
此筛选器有一个writeweaketag参数,该参数将筛选器配置为写入类似以下的弱etags:w/“02a2d595e6ed9a0b24f027f2b63b134d6”(如RFC 7232第2.3节所定义)。
Same as in Spring WebFlux
SpringMVC通过控制器上的注释为CORS配置提供细粒度支持。但是,当与Spring Security一起使用时,我们建议依赖内置的corsfilter,它必须在Spring Security的过滤器链之前订购。
有关详细信息,请参阅CORS和CORS过滤器部分。
带注释的控制器
Same as in Spring WebFlux
SpringMVC提供了一个基于注释的编程模型,其中@controller和@restcontroller组件使用注释来表示请求映射、请求输入、异常处理等。带注释的控制器具有灵活的方法签名,不必扩展基类,也不必实现特定的接口。以下示例显示由注释定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
在前面的示例中,该方法接受一个模型并以字符串形式返回一个视图名称,但是存在许多其他选项,本章后面将对此进行解释。
您可以使用servlet的WebApplicationContext中的标准SpringBean定义来定义控制器bean。@controller原型允许自动检测,与在类路径中检测@component类并为其自动注册bean定义的Spring通用支持保持一致。它还充当带注释类的构造型,指示它作为Web组件的角色。
为了实现对这种@ bean的自动检测,可以将组件扫描添加到Java配置中,如下面的示例显示:
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}
下面的示例显示了与前面示例等效的XML配置:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.web"/>
beans>
@RestController是一个组合注释,它本身是元注释的,@Controller并@ResponseBody指示一个控制器,其每个方法都继承了类型级@ResponseBody注释,因此,直接写入响应主体与视图分辨率(view resolution)和使用HTML模板呈现。
AOP代理
在某些情况下,您需要在运行时用AOP代理来修饰控制器。一个例子是,如果您选择直接在控制器上使用@transactional注释。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。这通常是控制器的默认选项。但是,如果控制器必须实现一个不是Spring上下文回调的接口(如initializingbean、*aware和其他),则可能需要显式配置基于类的代理。例如,使用
可以使用@requestmapping注释将请求映射到控制器方法。它具有各种属性,可以按URL、HTTP方法、请求参数、头和媒体类型进行匹配。您可以在类级别使用它来表示共享映射,或者在方法级别使用它来缩小到特定端点映射的范围。
@requestmapping还有特定于http方法的快捷方式变体:
快捷方式是提供的自定义注释,因为可以证明,大多数控制器方法应该映射到特定的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) {
// ...
}
}
您可以使用以下全局模式和通配符映射请求:
您还可以声明URI变量并使用@pathvariable访问其值,如下例所示:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
可以在类和方法级别声明URI变量,如下示例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
URI变量将自动转换为适当的类型,或者引发类型不匹配异常。默认情况下支持简单类型(int、long、date等),您可以注册对任何其他数据类型的支持。请参见类型转换和数据绑定器。
您可以显式地命名URI变量(例如,@ PosiValm(“CudiID”)),但是如果名称相同,并且代码是用调试信息编译的,或者Java 8上的参数编译器标志,则可以将该细节排除在外。
语法{varName:regex}声明一个URI变量,其正则表达式的语法为{varName:regex}。例如,给定URL “/spring-web-3.0.5 .jar”,以下方法提取名称,版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
// ...
}
URI路径模式还可以具有嵌入式${…}占位符,这些占位符在启动时通过使用PropertyPlaceHolderConfigurer针对本地,系统,环境和其他属性源来解析。例如,您可以使用它来根据某些外部配置参数化基本URL。
Spring MVC使用URI路径匹配的PathMatcher契约和AntPathMatcher实现 spring-core。
当多个模式匹配一个URL时,必须对它们进行比较以找到最佳匹配。这是通过使用antPathMatcher.getPatternComparator(字符串路径)来完成的,它查找更具体的模式。
如果一个模式的uri变量计数较低,单个通配符计数为1,而双通配符计数为2,那么该模式就不那么具体了。如果得分相等,则选择较长的模式。在相同的分数和长度下,将选择URI变量多于通配符的模式。
默认的映射模式(/)被排除在计分之外,并且总是最后排序。此外,前缀模式(如/public/)被认为比其他没有双通配符的模式更不特定。
有关完整的详细信息,请参阅AntPathMatcher中的AntPatternComparator,并记住您可以自定义PathMatcher实现。请参见配置部分中的路径匹配。
默认情况下,SpringMVC执行.后缀模式匹配,这样映射到/person的控制器也隐式映射到/person.。然后,文件扩展名用于解释请求用于响应的内容类型(即,而不是accept头)-例如,/person.pdf、/person.xml和其他内容类型。
当浏览器用于发送难以一致解释的accept头文件时,以这种方式使用文件扩展名是必要的。目前,这已不再是必要的,使用accept头应该是首选。
随着时间的推移,文件扩展名的使用在很多方面都存在问题。当覆盖使用uri变量、路径参数和uri编码时,它可能会导致歧义。关于基于URL的授权和安全性的推理(有关更多详细信息,请参阅下一节)也变得更加困难。
要完全禁用文件扩展名的使用,必须同时设置以下两项:
基于URL的内容协商仍然有用(例如,在浏览器中键入URL时)。为了实现这一点,我们建议使用基于查询参数的策略,以避免文件扩展名带来的大多数问题。或者,如果必须使用文件扩展名,请考虑通过ContentNegotiationConfigurer的MediaTypes属性将它们限制为显式注册的扩展名列表。
反射文件下载(rfd)攻击类似于XSS,因为它依赖于响应中反射的请求输入(例如,查询参数和URI变量)。然而,与将javascript插入HTML不同,rfd攻击依赖于浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。
在SpringMVC中,@responseBody和responseEntity方法存在风险,因为它们可以呈现不同的内容类型,客户机可以通过URL路径扩展请求这些类型。禁用后缀模式匹配和使用路径扩展进行内容协商可以降低风险,但不足以防止RFD攻击。
为了防止RFD攻击,在呈现响应主体之前,SpringMVC添加了一个内容处置:inline;filename=f.txt头,以建议一个固定的安全下载文件。只有当URL路径包含的文件扩展名既没有白名单,也没有为内容协商显式注册时,才能执行此操作。但是,当直接在浏览器中键入URL时,它可能会产生副作用。
默认情况下,许多常见的路径扩展被白名单显示。具有自定义httpmessageconverter实现的应用程序可以显式注册内容协商的文件扩展名,以避免为这些扩展名添加内容处置头。请参见内容类型。
有关RFD的其他建议,请参见CVE-2015-5211。
您可以根据请求的内容类型缩小请求映射,如下示例所示:
//使用consumes属性按内容类型缩小映射范围。
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}
consumes属性还支持否定表达式-,例如,!text/plain指除text/plain以外的任何内容类型。
可以在类级别声明shared consumes属性。但是,与大多数其他请求映射属性不同,当在类级别使用时,方法级别使用属性重写,而不是扩展类级别声明。
MediaType 为常用的媒体类型(如APPLICATION_JSON_VALUE and APPLICATION_XML_VALUE)提供常量。
您可以基于接受请求头和控制器方法生成的内容类型列表来缩小请求映射,如下例所示:
// 使用produces属性按内容类型缩小映射范围。
@GetMapping(path = "/pets/{petId}", produces = "application/json;charset=UTF-8")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
媒体类型可以指定字符集。支持否定表达式 - 例如, !text/plain表示“text / plain”以外的任何内容类型。
对于JSON内容类型,即使RFC7159 明确声明“没有为此注册定义charset参数”,也应指定UTF-8字符集 ,因为某些浏览器要求它正确解释UTF-8特殊字符。
您可以produces在类级别声明共享属性。但是,与大多数其他请求映射属性不同,在类级别使用时,方法级别produces属性会覆盖而不是扩展类级别声明。
MediaType提供常用媒体类型的常量,例如 APPLICATION_JSON_UTF8_VALUE和APPLICATION_XML_VALUE。
您可以根据请求参数条件缩小请求映射。您可以测试是否存在请求参数(myparam),是否缺少一个(!MyParam),或特定值(MyParam=MyValue)。以下示例显示如何测试特定值:
//测试是否myParam等于myValue。
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
您还可以将其与请求标头条件一起使用,如以下示例所示:
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}
您可以将Content-Type与Accept with the headers condition 结合,但最好使用consumes和products。
@GetMapping (and @RequestMapping(method=HttpMethod.GET))透明地支持HTTP头进行请求映射。控制器方法不需要更改。在javax.servlet.http.HttpServlet中应用的响应包装器确保将Content-Length头设置为已写入的字节数(而不实际写入响应)。
@GetMapping (and @RequestMapping(method=HttpMethod.GET))隐式映射到并支持HTTP头。处理一个HTTP头请求就像处理HTTP GET一样,只是计算字节数并设置内容长度头,而不是写入主体。
默认情况下,通过将Allow response头设置为所有@requestmapping方法中列出的具有匹配URL模式的HTTP方法列表来处理HTTP选项。
对于没有HTTP方法声明的@requestmapping,allow header设置为get、head、post、put、patch、delete、options。控制器方法应始终声明支持的HTTP方法(例如,通过使用特定于HTTP方法的变量:@getmapping、@postmapping和其他变量)。
您可以显式地将@requestmapping方法映射到http head(HTTP HEAD)和http选项(HTTP OPTIONS),但在常见情况下这不是必需的。
SpringMVC支持对请求映射使用组合注释。这些注释本身是用@requestmapping进行元注释的,它们组成的目的是重新声明@requestmapping属性的子集(或全部),目的更窄、更具体。
@getmapping、@postmapping、@putmapping、@deletemapping和@patchmapping是组合注释的示例。之所以提供它们,是因为,可以说,大多数控制器方法应该映射到特定的HTTP方法,而不是使用@requestmapping,默认情况下,它与所有HTTP方法匹配。如果您需要一个组合注释的示例,请看这些注释是如何声明的。
SpringMVC还支持带有定制请求匹配逻辑的定制请求映射属性。这是一个更高级的选项,需要对requestmappinghandlermapping进行子类化,并重写getCustommethodCondition方法,在该方法中,您可以检查自定义属性并返回自己的requestCondition。
您可以以编程方式注册处理程序方法,这些方法可用于动态注册或高级情况,例如同一处理程序在不同URL下的不同实例。以下示例注册了一个处理程序方法:
@Configuration
public class MyConfig {
@Autowired
// 为控制器注入目标处理程序和处理程序映射。
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
throws NoSuchMethodException {
//准备映射元数据的请求。
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build();
// 获取处理程序方法。
Method method = UserHandler.class.getMethod("getUser", Long.class);
//添加注册。
mapping.registerMapping(info, handler, method);
}
}
Same as in Spring WebFlux
@requestmapping handler方法具有灵活的签名,可以从一系列受支持的控制器方法参数和返回值中进行选择。
下一个表描述了支持的控制器方法参数。任何参数都不支持反应类型(Reactive types)。
JDK 8的java.util.optional作为方法参数与具有必需属性(例如@requestparam、@requestheader和其他属性)的注释一起支持,并且相当于required=false。
Controller method argument | Description |
---|---|
WebRequest, NativeWebRequest | 对请求参数、请求和会话属性的一般访问,而不直接使用servlet API。 |
javax.servlet.ServletRequest, javax.servlet.ServletResponse | 选择任何特定的请求或响应类型,例如servletrequest、httpservletrequest或spring的multipartrequest、multipartttpservletrequest。 |
javax.servlet.http.HttpSession | 强制会话的存在。因此,这样的参数永远不会为空。请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将RequestMappingHandlerAdapter实例的SynchronizeOnSession标志设置为true。 |
javax.servlet.http.PushBuilder | 用于编程HTTP/2资源推送的servlet 4.0推送生成器API。注意,根据servlet规范,如果客户端不支持HTTP/2功能,则注入的pushbuilder实例可以为空。 |
java.security.Principal | 当前已验证的user — possibly已知,可能是特定的主体实现类。 |
HttpMethod | 请求的HTTP方法。 |
java.util.Locale | 当前请求区域设置,由可用的最特定的localeresolver确定(实际上是配置的localeresolver或localecontextresolver)。 |
java.util.TimeZone + java.time.ZoneId | |
java.io.InputStream, java.io.Reader | |
java.io.OutputStream, java.io.Writer | |
@PathVariable | |
@MatrixVariable | |
@RequestParam | |
@RequestHeader | |
@CookieValue | |
@RequestBody | |
HttpEntity | |
@RequestPart | |
java.util.Map,org.springframework.ui.Model,org.springframework.ui.ModelMap | |
RedirectAttributes | |
@ModelAttribute | |
Errors, BindingResult | |
SessionStatus + class-level @SessionAttributes | |
UriComponentsBuilder | |
@SessionAttribute | |
@RequestAttribute | |
Any other argument |
下一个表描述了支持的控制器方法返回值。所有返回值都支持反应类型。
Controller method argument | Description |
---|---|
@ResponseBody | 返回值通过httpmessageconverter实现转换并写入响应。参见@responsebody。 |
HttpEntity, ResponseEntity | 指定完整响应(包括HTTP头和主体)的返回值将通过httpmessageconverter实现转换并写入响应。参见ResponseEntity。 |
HttpHeaders | |
String | |
View | |
java.util.Map, org.springframework.ui.Model | |
@ModelAttribute | |
ModelAndView object | |
void | |
DeferredResult |
|
Callable |
|
ListenableFuture |
|
ResponseBodyEmitter, SseEmitter | |
StreamingResponseBody | |
Reactive types — Reactor, RxJava, or others through ReactiveAdapterRegistry | |
Any other return value |
一些表示基于字符串的请求输入的带注释的控制器方法参数(如@requestparam、@requestheader、@pathvariable、@matrixvariable和@cookievalue)如果声明为字符串以外的参数,则可能需要类型转换。
对于这种情况,类型转换将根据配置的转换器自动应用。默认情况下,支持简单类型(int、long、date和其他类型)。您可以通过WebDataBinder(请参阅DataBinder)或使用FormattingConversionService注册格式化程序来自定义类型转换。请参见Spring Field Formatting.。
RFC3986讨论了路径段中的name-value对。在SpringMVC中,我们将它们称为基于TimBerners-Lee的“old post”的“matrix variables”,但它们也可以称为URI路径参数。
矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,/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 /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
矩阵变量可以定义为可选变量,并指定默认值,如下例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
要获取所有矩阵变量,可以使用多值映射,如下示例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
请注意,您需要启用矩阵变量的使用。在MVC Java配置中,您需要通过路径匹配来设置带有removeSemicolonContent=false的UrlPathHelper 。在MVC XML命名空间中,可以设置
可以使用@requestparam注释将servlet请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。
以下示例显示了如何执行此操作:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
// Using @RequestParam to bind petId.
public String setupForm(@RequestParam("petId") int petId, Model model) {
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
默认情况下,使用此注释的方法参数是必需的,但可以通过将@requestparam注释的必需标志设置为false或使用java.util.optional包装声明参数来指定方法参数是可选的。
如果目标方法参数类型不是字符串,则自动应用类型转换。请参见类型转换。
将参数类型声明为数组或列表允许解析同一参数名的多个参数值。
当@requestparam注释声明为map
请注意,使用@requestparam是可选的(例如,设置其属性)。默认情况下,作为简单值类型(由beanutils issimpleproperty确定)且未由任何其他参数冲突解决程序解析的任何参数都将被视为用@requestparam注释的参数。
可以使用@request header注释将请求头绑定到控制器中的方法参数。
考虑以下带有头的请求:
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,
@RequestHeader("Keep-Alive") long keepAlive) {
//...
}
如果目标方法参数类型不是字符串,则自动应用类型转换。请参见类型转换。
当@requestheader注释用于map
内置支持可用于将逗号分隔的字符串转换为字符串数组或集合或类型转换系统已知的其他类型。例如,用@requestheader(“accept”)注释的方法参数可以是string类型,也可以是string[]或list
Same as in Spring WebFlux
您可以使用@ModelAttribute注释:
本节讨论前面列表中的第二项@modelattribute methods-。控制器可以有任意数量的@modelattribute方法。在同一控制器中的@requestmapping methods之前调用所有此类方法。@modelattribute方法也可以通过@controlleradvice在控制器之间共享。有关更多详细信息,请参阅控制器建议部分。
@modelattribute方法具有灵活的方法签名。它们支持许多与@requestmapping方法相同的参数,除了@modelattribute本身或与请求主体相关的任何内容。
以下示例显示了@modelattribute方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
如果没有显式指定名称,将根据对象类型选择默认名称,如JavaDoc for Conventions中所述。您始终可以通过使用重载的addattribute方法或通过@modelattribute上的name属性(用于返回值)来分配显式名称。
您还可以将@model attribute用作@requestmapping methods的方法级注释,在这种情况下,@requestmapping方法的返回值被解释为模型属性。这通常不是必需的,因为它是HTML控制器中的默认行为,除非返回值是一个字符串,否则将被解释为视图名称。@model attribute还可以自定义模型属性名,如下例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
Same as in Spring WebFlux
@controller或@controlleradvice类可以具有初始化WebDataBinder实例的@initBinder方法,而这些方法反过来又可以:
@initbinder方法可以注册特定于控制器的java.bean.propertyeditor或spring converter和格式化程序组件。此外,还可以使用MVC配置在全局共享的格式化转换服务中注册转换器和格式化程序类型。
@initbinder方法支持与@requestmapping方法相同的许多参数,但@modelattribute(command object)参数除外。通常,它们是用webdatabinder参数(用于注册)和void返回值声明的。下面的列表显示了一个示例:
@Controller
public class FormController {
@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
或者,当通过共享格式化转换服务使用基于格式化程序的设置时,可以重复使用相同的方法并注册特定于控制器的格式化程序实现,如下例所示:
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
Same as in Spring WebFlux
@controller和@controlleradvice类可以有@exceptionhandler方法来处理来自控制器方法的异常,如下示例所示:
@Controller
public class SimpleController {
// ...
@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
异常可能与正在传播的顶级异常(即引发的直接IOException)匹配,也可能与顶级包装异常(例如,在IllegalstateException中包装的IOException)中的直接原因匹配。
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优先于原因异常匹配。更具体地说,exceptiondepthcomparator用于根据异常从抛出的异常类型的深度对异常进行排序。
或者,注释声明可以缩小异常类型的范围以匹配,如下示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}
您甚至可以使用具有非常通用参数签名的特定异常类型列表,如下示例所示:
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}
根异常和原因异常匹配之间的区别可能令人惊讶。
在前面显示的ioexception变量中,通常使用实际的filesystemexception或remoteexception实例作为参数调用方法,因为这两个实例都从ioexception扩展。但是,如果在包装异常(本身就是IOException)中传播任何此类匹配异常,则传入的异常实例就是包装异常。
在handle(exception)变量中,该行为更简单。在包装场景中,始终使用包装异常来调用此函数,在这种情况下,通过例如getcause()找到实际匹配的异常。传入的异常是实际的FileSystemException或RemoteException实例,仅当这些异常作为顶级异常引发时。
我们通常建议您在参数签名中尽可能具体,减少根异常类型和导致异常类型之间不匹配的可能性。考虑将多匹配方法拆分为单独的@exceptionhandler方法,每个方法通过其签名匹配一个特定的异常类型。
在多@controlleradvice安排中,我们建议在按相应顺序排列优先级的@controlleradvice上声明主根异常映射。虽然根异常匹配优先于原因,但这是在给定控制器或@controlleradvice类的方法中定义的。这意味着优先级较高的@controlleradvice bean上的原因匹配优先于优先级较低的@controlleradvice bean上的任何匹配(例如,根)。
最后但并非最不重要的是,@exceptionhandler方法实现可以通过以原始形式重新引发给定的异常实例来选择退出处理该异常实例。这在只对根级别匹配感兴趣的场景中很有用,或者在无法静态确定的特定上下文中对匹配感兴趣。一个rethrown异常通过剩余的解析链传播,就好像给定的@exceptionhandler方法一开始就不匹配一样。
SpringMVC中对@exceptionhandler方法的支持建立在DispatcherServlet级别的handlerExceptionResolver机制上。
@exceptionhandler方法支持以下参数:
Method argument | Description |
---|---|
Exception type | |
HandlerMethod | |
WebRequest, NativeWebRequest | |
javax.servlet.ServletRequest, javax.servlet.ServletResponse | |
javax.servlet.http.HttpSession | |
java.security.Principal | |
HttpMethod | |
java.util.Locale | |
java.util.TimeZone, java.time.ZoneId | |
java.io.OutputStream, java.io.Writer | |
java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMap | |
RedirectAttributes | |
@SessionAttribute | |
@RequestAttribute |
@ExceptionHandler methods support the following return values:
Return value | Description |
---|---|
@ResponseBody | |
HttpEntity, ResponseEntity | |
String | |
View | |
java.util.Map, org.springframework.ui.Model | |
@ModelAttribute | |
ModelAndView object | |
void | |
Any other return value |
REST服务的一个常见要求是在响应主体中包含错误详细信息。Spring框架不会自动这样做,因为响应主体中错误细节的表示是特定于应用程序的。但是,@restcontroller可以使用带有responseEntity返回值的@exceptionhandler方法来设置响应的状态和主体。这些方法也可以在@controlleradvice类中声明,以便全局应用它们。
在响应主体中使用错误详细信息实现全局异常处理的应用程序应考虑扩展ResponseEntityExceptionHandler,它提供对Spring MVC引发的异常的处理,并提供挂钩以自定义响应主体。要使用它,请创建responseentityexceptionhandler的子类,用@controlleradvice注释它,重写必要的方法,并将其声明为SpringBean。
通常,@exceptionhandler、@initbinder和@modelattribute方法应用于声明它们的@controller类(或类层次结构)中。如果希望这些方法在全局范围内(跨控制器)应用,可以在用@controlleradvice或@restcontrolleradvice标记的类中声明它们。
@controlleradvice用@component标记,这意味着这些类可以通过组件扫描注册为spring bean。@restcontrolleradvice也是用@controlleradvice和@response body标记的元注释,这实质上意味着@exceptionhandler方法通过消息转换(而不是视图解析或模板呈现)呈现到响应体。
启动时,@requestmapping和@exceptionhandler方法的基础结构类检测@controlleradvice类型的SpringBean,然后在运行时应用它们的方法。全局@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 {}
前面示例中的选择器在运行时进行评估,如果广泛使用,可能会对性能产生负面影响。有关详细信息,请参阅@controlleradvice javadoc。
Same as in Spring WebFlux
本节介绍Spring框架中可用于处理URI的各种选项。
Spring MVC and Spring WebFlux
UriComponentsBuilder 有助于使用变量从URI模板构建URI,如下示例所示:
UriComponents uriComponents = UriComponentsBuilder
//带有URI模板的静态工厂方法。
.fromUriString("http://example.com/hotels/{hotel}")
//添加或替换URI组件。
.queryParam("q", "{q}")
//请求对URI模板和URI变量进行编码。
.encode()
// 建立一个UriComponents。
.build();
//展开变量并获取URI。
URI uri = uriComponents.expand("Westin", "123").toUri();
前面的示例可以合并为一个链,并使用buildAndExpand进行缩短,如下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
您可以直接转到一个URI(这意味着编码),进一步缩短它,如下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
您可以使用完整的URI模板进一步缩短它,如下示例所示:
URI uri = UriComponentsBuilder
.fromUriString("http://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
UriComponentsBuilder实现UriBuilder。反过来,您可以使用UribuilderFactory创建一个Uribuilder。UribuilderFactory和Uribuilder一起提供了一种可插入机制,根据共享配置(如基本URL、编码首选项和其他详细信息)从URI模板构建URI。
可以使用uribuilderFactory配置resttemplate和webclient,以自定义URI的准备工作。DefaultUriBuilderFactory是UriBuilderFactory的默认实现,它在内部使用UriComponentsBuilder并公开共享配置选项。
以下示例显示如何配置RestTemplate:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
以下示例配置WebClient:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "http://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VARIABLES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
此外,还可以直接使用DefaultUriBuilderFactory。它类似于使用UriComponentsBuilder,但它不是静态工厂方法,而是一个保存配置和首选项的实际实例,如下示例所示:
String baseUrl = "http://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
UriComponentsBuilder在两个级别上公开编码选项:
这两个选项都用转义八位字节替换非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"
您可以通过直接转到URI(这意味着编码)来缩短前面的示例,如下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar")
您可以使用完整的URI模板进一步缩短它,如下示例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}?q={q}")
.build("New York", "foo+bar")
webclient和resttemplate通过uribuilderFactory策略在内部扩展和编码uri模板。两者都可以使用自定义策略进行配置。如下示例所示:
String baseUrl = "http://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
DefaultUriBuilderFactory实现在内部使用UriComponentsBuilder来扩展和编码URI模板。作为一个工厂,它提供了一个单独的位置来根据以下编码模式之一配置编码方法:
出于历史原因和向后兼容性,resttemplate被设置为EncodingMode.URI_COMPONENTS。WebClient依赖于DefaultUriBuilderFactory中的默认值,该值已从5.0.x中的EncodingMode.URI_COMPONENTS更改为5.1中的EncodingMode.TEMPLATE_AND_VALUES。
您可以使用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();
可以相对于上下文路径创建URI,如下示例所示:
// Re-uses host, port and context path...
ServletUriComponentsBuilder 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()
从5.1开始,ServletUriComponentsBuilder 忽略来自forwarded和X-Forwarded-*报头的信息,这些报头指定客户机的原始地址。考虑使用ForwardedHeaderFilter 提取和使用或丢弃此类头。
SpringMVC提供了一种机制来准备到控制器方法的链接。例如,以下MVC控制器允许创建链接:
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {
@GetMapping("/bookings/{booking}")
public ModelAndView getBooking(@PathVariable Long booking) {
// ...
}
}
您可以通过按名称引用方法来准备链接,如下示例所示:
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
在前面的示例中,我们提供了实际的方法参数值(在本例中,长值为21),用作路径变量并插入到URL中。此外,我们还提供值42来填充任何剩余的URI变量,例如从类型级请求映射继承的hotel变量。如果方法有更多参数,我们可以为URL不需要的参数提供空值。通常,只有@pathvariable和@requestparam参数与构造URL相关。
使用MvcUriComponentsBuilder还有其他方法。例如,您可以使用类似于通过代理进行模拟测试的技术,以避免按名称引用控制器方法,如下面的示例所示(该示例假定静态导入MvcUriComponentsBuilder.on):
UriComponents uriComponents = MvcUriComponentsBuilder
.fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
当需要使用FromMethodCall创建链接时,Controller方法签名的设计受到限制。除了需要适当的参数签名外,返回类型还存在技术限制(即为链接生成器调用生成运行时代理),因此返回类型不能是最终的。特别是,视图名称的公共字符串返回类型在这里不起作用。您应该使用ModelAndView 甚至是纯对象(带有字符串返回值)。
前面的示例在mvcuricomponentsbuilder中使用静态方法。在内部,它们依赖servleturicomponentsbuilder从当前请求的方案、主机、端口、上下文路径和servlet路径准备基URL。这在大多数情况下都很有效。然而,有时,这可能是不够的。例如,您可能不在请求的上下文中(例如准备链接的批处理过程),或者您可能需要插入路径前缀(例如,从请求路径中删除并需要重新插入到链接中的区域设置前缀)。
对于这种情况,您可以使用静态FromXXX重载方法来接受UriComponentsBuilder以使用基URL。或者,可以使用基URL创建mvcuricomponentsbuilder的实例,然后使用基于实例的xxx方法。例如,以下列表使用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();
从5.1开始,mvcuricomponentsbuilder忽略来自forwarded和x-forwarded-*报头的信息,这些报头指定客户机的原始地址。考虑使用ForwardedHeaderFilter提取和使用或丢弃此类头。
在thymeleaf、freemaker或jsp等视图中,可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注释的控制器的链接。
请考虑以下示例:
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity getAddress(@PathVariable String country) { ... }
}
对于前面的控制器,您可以从JSP准备一个链接,如下所示:
<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Addressa>
前面的示例依赖于Spring标记库(即META-INF/Spring.tld)中声明的mvcUrl 函数,但是很容易定义自己的函数或为其他模板技术准备类似的函数。
这就是它的工作原理。启动时,每个@requestmapping都通过handlerMethodMappingNamingStrategy分配一个默认名称,其默认实现使用类的大写字母和方法名(例如,ThingController 中的getting方法变为“TC#getThing”)。如果名称冲突,可以使用@RequestMapping(name="…")指定显式名称或实现自己的handlerMethodMappingNamingStrategy。
异步请求
Spring MVC与Servlet 3.0异步请求处理有广泛的集成:
在servlet容器中启用异步请求处理功能后,控制器方法可以使用deferredresult包装任何受支持的控制器方法返回值,如下例所示:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult quotes() {
DeferredResult deferredResult = new DeferredResult();
// Save the deferredResult somewhere..
return deferredResult;
}
// From some other thread...
deferredResult.setResult(data);
控制器可以从不同的线程(例如,响应外部事件(JMS消息)、计划任务或其他事件)异步生成返回值。
控制器可以用java.util.concurrent.callable包装任何支持的返回值,如下示例所示:
@PostMapping
public Callable processUpload(final MultipartFile file) {
return new Callable() {
public String call() throws Exception {
// ...
return "someView";
}
};
}
然后可以通过配置的taskexecutor运行给定的任务来获取返回值。
以下是对servlet异步请求处理的非常简洁的概述:
DeferredResult处理工作如下:
Callable processing工作如下:
为了进一步了解背景和上下文,您还可以阅读在SpringMVC 3.2中引入异步请求处理支持的博客文章。
使用DeferredResult时,可以选择是调用带有异常的setresult还是seterrorresult。在这两种情况下,SpringMVC将请求发回servlet容器以完成处理。然后将其视为控制器方法返回给定值,或者视为生成给定异常。然后,异常通过常规的异常处理机制(例如,调用@exceptionhandler方法)。
当使用可调用时,会出现类似的处理逻辑,主要区别在于结果是从可调用返回的,或者是由它引发异常。
handlerInterceptor实例的类型可以是AsyncHandlerInterceptor,以接收启动异步处理(而不是PostHandle和AfterCompletion)的初始请求的AfterConcurrentHandlingStarted回调。
handlerInterceptor实现还可以注册CallableProcessingInterceptor或DeferredResultProcessingInterceptor,以便更深入地集成异步请求的生命周期(例如,处理超时事件)。有关详细信息,请参阅AsyncHandlerInterceptor。
Deferredresult提供OnTimeout(可运行)和OnCompletion(可运行)回调。有关更多详细信息,请参见Deferredresult的JavaDoc。Callable可以替换为WebAsyncTask,后者公开了超时和完成回调的其他方法。
servlet API最初是为通过过滤器servlet链进行单次传递而构建的。在servlet 3.0中添加的异步请求处理允许应用程序退出过滤器servlet链,但保留响应以供进一步处理。SpringMVC异步支持是围绕这个机制构建的。当控制器返回延迟结果时,将退出筛选器servlet链,并释放servlet容器线程。稍后,当设置了deferedresult时,将进行异步调度(到同一个URL),在此期间再次映射控制器,但不调用它,而是使用deferedresult值(就像控制器返回它一样)恢复处理。
相比之下,SpringWebFlux既不构建在servlet API上,也不需要这样的异步请求处理特性,因为它是异步的。异步处理内置于所有框架合同中,并且在请求处理的所有阶段都得到了本质上的支持。
从编程模型的角度来看,SpringMVC和SpringWebFlux都支持异步和无功类型作为控制器方法中的返回值。SpringMVC甚至支持流式处理,包括反应性背压(reactive back pressure)。但是,对响应的个别写入仍然是阻塞的(并且在单独的线程上执行),与WebFlux不同,WebFlux依赖于非阻塞I/O,并且每个写入不需要额外的线程。
另一个基本区别是Spring MVC不支持控制器方法参数中的异步或反应类型(例如@requestbody、@requestpart和其他类型),也不明确支持异步和反应类型作为模型属性。SpringWebFlux确实支持这一切。
可以对单个异步返回值使用deferredresult和callable。如果您希望生成多个异步值并将这些值写入响应中,该怎么办?本节介绍如何执行此操作。
您可以使用responseBodyeMitter返回值生成对象流,其中每个对象都用httpMessageConverter序列化并写入响应,如下例所示:
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
return emitter;
}
// In some other thread
emitter.send("Hello once");
// and again later on
emitter.send("Hello again");
// and done at some point
emitter.complete();
您还可以使用ResponseBodyeMitter作为响应中的主体,让您自定义响应的状态和标题。
当发射器抛出IOException时(例如,如果远程客户机退出),应用程序不负责清理连接,不应调用Emitter.Complete或emitter.completeWithError。相反,servlet容器会自动启动AsyncListener错误通知,其中SpringMVC会发出一个completeWithError 调用。这个调用反过来执行到应用程序的最后一个异步调度,在此期间,SpringMVC调用配置的异常解析程序并完成请求。
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();
虽然SSE是流式传输到浏览器的主要选项,但请注意,Internet Explorer不支持服务器发送的事件。考虑将Spring的WebSocket消息与面向各种浏览器的SockJS回退传输(包括SSE)结合使用。
有关异常处理的注意事项,请参阅上一节。
有时,绕过消息转换并直接流到响应输出流(例如,对于文件下载)是很有用的。可以使用streamingResponseBody返回值类型来执行此操作,如下示例所示:
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
}
};
}
您可以使用streamingResponseBody作为响应中的主体来定制响应的状态和头。
Same as in Spring WebFlux
SpringMVC支持在控制器中使用反应式客户端库(也可以在WebFlux部分中读取反应式库)。这包括来自SpringWebFlux的WebClient和其他一些,如SpringDataReactive数据存储库。在这种情况下,可以方便地从控制器方法返回reactive types。
Reactive return values处理如下:
SpringMVC支持reactor和rxjava,通过Spring-Core的reactiveAdapterRegional,使其能够适应多个反应库。
对于流式处理到响应,支持 reactive back pressure,但是对响应的写入仍然是阻塞的,并且通过配置的TaskExecutor在单独的线程上执行,以避免阻塞上游源(例如从WebClient返回的流量)。默认情况下,simpleAsyncTaskExecutor用于阻塞写入,但不适用于加载。如果计划使用reactive type进行流式处理,那么应该使用MVC配置来配置任务执行器。
Same as in Spring WebFlux
当远程客户端离开时,servlet API不提供任何通知。因此,当流式传输到响应时,无论是通过sseemitter还是<
或者,考虑使用具有内置心跳机制的Web消息传递解决方案(如STOMP over WebSocket或WebSocket with SockJS)。
必须在servlet容器级别启用异步请求处理功能。MVC配置还公开了异步请求的几个选项。
筛选器和servlet声明具有AsyncSupported标志,需要将其设置为true才能启用异步请求处理。此外,应声明筛选器映射以处理异步javax.servlet.DispatchType。
在Java配置中,当使用AbstractAnnotationConfigDispatcherServletInitializer Servlet初始化器初始化Servlet容器时,这是自动完成的。
在web.xml配置中,可以向DispatcherServlet和筛选器声明中添加
MVC配置公开了与异步请求处理相关的以下选项:
您可以配置以下内容:
请注意,您还可以设置DeferredResult、ResponseBodyEmitter和SseEmitter的默认超时值。对于可调用文件,可以使用WebAsyncTask提供超时值。
Same as in Spring WebFlux
SpringMVC允许您处理CORS(跨源站资源共享)。本节介绍如何执行此操作。
出于安全原因,浏览器禁止Ajax调用当前源站以外的资源。例如,您可以在一个选项卡中设置银行帐户,在另一个选项卡中设置evil.com。来自evil.com的脚本不应该能够使用您的凭证向您的银行API发出Ajax请求-例如从您的帐户中提取资金!
跨源资源共享(cors)是由大多数浏览器实现的W3C规范,它允许您指定哪些跨域请求是经过授权的,而不是使用基于iframe或jsonp的安全性较低、功能较弱的解决方法。
CORS规范区分了between preflight请求、简单请求和实际请求。要了解CORS是如何工作的,您可以阅读本文和其他许多文章,或者查看规范了解更多详细信息。
SpringMVC handlerMapping实现为CORS提供了内置支持。在成功地将请求映射到处理程序之后,handlerMapping实现检查给定请求和处理程序的CORS配置,并采取进一步的操作。between preflight,请求直接处理,而简单和实际的CORS请求被拦截、验证,并设置了所需的CORS响应头。
为了启用跨源请求(也就是说,源报头存在并且与请求的主机不同),需要有一些显式声明的CORS配置。如果没有找到匹配的CORS配置,则会拒绝between preflight请求。没有向简单和实际的CORS请求的响应中添加CORS头,因此浏览器拒绝它们。
每个handlerMapping可以单独配置为基于URL模式的corsconformation映射。在大多数情况下,应用程序使用MVC Java配置或XML命名空间来声明这样的映射,从而导致将单个全局映射传递给所有的HandlerMappping 实例。
您可以将handlerMapping级别的全局CORS配置与更细粒度、处理程序级别的CORS配置结合起来。例如,带注释的控制器可以使用class或method-level@crossorigin注释(其他处理程序可以实现corsconformationsource)。
组合全局和本地配置的规则通常是相加的-例如,所有全局和所有本地源。对于只能接受单个值的那些属性(例如allowCredentials和maxage),本地值将覆盖全局值。有关详细信息,请参见Corsconformation Combine(Corsconformation)。
要从源代码了解更多信息或进行高级自定义,请检查以下代码:
- CorsConfiguration
- CorsProcessor, DefaultCorsProcessor
- AbstractHandlerMapping
@CrossOrigin annotation允许对带注释的控制器方法执行跨源请求,如下示例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
By default, @CrossOrigin allows:
默认情况下不启用allowedcredentials,因为它建立了一个信任级别,该级别公开敏感的用户特定信息(如cookie和csrf令牌),并且只应在适当的情况下使用。
Maxage设置为30分钟。
@crossorigin在类级别也受支持,并且由所有方法继承,如下例所示:
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
您可以在类级别和方法级别使用@crossorigin,如下示例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("http://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
除了细粒度的控制器方法级配置之外,您可能还需要定义一些全局CORS配置。您可以在任何handlerMapping上单独设置基于URL的CorsConfiguration 映射。然而,大多数应用程序使用MVC Java配置或MVC XNM命名空间来实现这一点。
默认情况下,全局配置启用以下功能:
默认情况下不启用allowedcredentials,因为它建立了一个信任级别,该级别公开敏感的用户特定信息(如cookie和csrf令牌),并且只应在适当的情况下使用。
Maxage设置为30分钟。
为了在MVC Java config中启用CORS,可以使用CorsRegistry回调,如下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
要在XML命名空间中启用CORS,可以使用
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="http://domain1.com, http://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="http://domain1.com" />
mvc:cors>
您可以通过内置的CorsFilter应用cors支持。
如果您尝试使用带有Spring安全性的corsfilter,请记住Spring安全性具有对cors的内置支持。
要配置过滤器,请将CorsConfigurationSource 传递给其构造函数,如下示例所示:
CorsConfiguration config = new CorsConfiguration();
// Possibly...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("http://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
Same as in Spring WebFlux
Spring安全项目提供了保护Web应用程序免受恶意攻击的支持。请参阅Spring安全参考文档
HDIV是另一个与SpringMVC集成的Web安全框架。
Same as in Spring WebFlux
HTTP缓存可以显著提高Web应用程序的性能。HTTP缓存围绕缓存控制响应头以及随后的条件请求头(如Last-Modified和ETag)旋转。缓存控制在如何缓存和重用响应方面向私有缓存(例如浏览器)和公共缓存(例如代理)提供建议。ETag头用于发出条件请求,如果内容没有更改,则可能导致304(NOT_MODIFIED)没有正文。ETag可以被看作是最后一个修改头的更复杂的继承者。
本节介绍SpringWebMVC中提供的与HTTP缓存相关的选项。
CacheControl提供对配置与Cache-Control标题相关的设置的支持,并在多个地方被接受为参数:
虽然RFC7234描述了Cache-Control response 头的所有可能的指令,但Cache-Control 类型采用面向用例的方法,重点关注常见的场景:
// 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();
WebContentGenerator还接受一个更简单的cachePeriod属性(以秒为单位定义),其工作方式如下:
控制器可以添加对HTTP缓存的显式支持。我们建议这样做,因为在将资源的lastmodified或etag值与条件请求头进行比较之前,需要对其进行计算。控制器可以向ResponseEntity添加etag头和缓存控制设置,如下例所示:
@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);
}
如果与条件请求头的比较表明内容没有更改,则前面的示例将发送一个带有空正文的304(NOT_MODIFIED)响应。否则,ETag和缓存控制头将添加到响应中。
您还可以在控制器中对条件请求头进行检查,如下例所示:
@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {
//特定于应用的计算。
long eTag = ...
if (request.checkNotModified(eTag)) {
//响应已设置为304(NOT_MODIFIED) - 无需进一步处理
return null;
}
//继续请求处理。
model.addAttribute(...);
return "myViewName";
}
根据etag值、lastmodified值或两者检查条件请求有三种变体。对于条件GET和HEAD请求,可以将响应设置为304(NOT_MODIFIED)。对于条件发布、放置和删除,您可以将响应设置为409(PRECONDITION_FAILED),以防止并发修改。
您应该为静态资源提供缓存控件和条件响应头,以获得最佳性能。请参阅有关配置静态资源的部分。
您可以使用shallowettageheaderfilter添加根据响应内容计算的“shallow”etag值,从而节省带宽而不是CPU时间。见 Shallow ETag。
Same as in Spring WebFlux
在SpringMVC中使用视图技术是可插拔的,无论您决定使用thymeleaf、groovy标记模板、JSP还是其他技术,都主要是配置更改的问题。本章介绍与SpringMVC集成的视图技术。我们假设您已经熟悉视图相关内容。
Thymeleaf 是一个现代服务器端Java模板引擎,它强调自然的HTML模板,它可以通过双点击在浏览器中预览,这对于独立于UI模板(例如,由设计器)的工作非常有用,而不需要运行服务器。如果您想替换JSP,那么thymeleaf提供了一组最广泛的特性,使这种转换更加容易。Thymeleaf 是积极发展和维护。有关更完整的介绍,请参见Thymeleaf项目主页。
Thymeleaf 与春季MVC的整合由Thymeleaf 项目管理。配置涉及一些bean声明,例如servletContextTemplateResolver、springTemplateEngine和thymeleafviewResolver。更多详情请参见Thymeleaf+Spring。
ApacheFreeMarker是一个模板引擎,用于生成从HTML到电子邮件等任何类型的文本输出。Spring框架有一个内置的集成,用于将SpringMVC与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;
}
}
下面的示例演示如何在XML中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
mvc:view-resolvers>
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
mvc:freemarker-configurer>
或者,您也可以声明FreeMarkerConfigurer Bean以完全控制所有属性,如下例所示:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
bean>
您的模板需要存储在前面示例中FreeMarkerConfigurer指定的目录中。在前面的配置下,如果您的控制器返回一个视图名welcome,解析器将查找/WEB-INF/freemarker/welcome.ftl模板。
通过在FreeMarkerConfigurer bean上设置适当的bean属性,可以将FreeMarker“设置”和“SharedValues”直接传递给FreeMarker配置对象(由Spring管理)。FreeMarkerSettings属性需要java.util.properties对象,FreeMarkerVariables属性需要java.util.map。以下示例显示了如何执行此操作:
<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文档。
Spring提供了一个用于JSP的标记库,其中包含一个
在spring-webmvc.jar文件中为两种语言维护了一组标准宏,因此它们始终可用于适当配置的应用程序。
Spring库中定义的一些宏被认为是内部的(私有的),但是宏定义中不存在这样的作用域,使得调用代码和用户模板时所有宏都可见。以下部分只关注需要从模板中直接调用的宏。如果您希望直接查看宏代码,则该文件称为spring.ftl,位于org.springframework.web.servlet.view.freemaker包中。
在充当Spring MVC控制器的表单视图的HTML表单(VM或FTL模板)中,可以使用类似于下一个示例的代码绑定到字段值,并以类似于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 "myModelObject.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”参数,该参数由命令对象的名称(除非在FormController属性中对其进行了更改,否则为“command”)和要绑定到的命令对象上的句点和字段名组成。还可以使用嵌套字段,如command.address.street。bind宏采用web.xml中servletContext参数defaulthtmlescape指定的默认HTML转义行为。
名为<@spring.bindEscaped>的宏的可选形式接受第二个参数,并显式指定是否应在状态错误消息或值中使用HTML转义。您可以根据需要将其设置为“真”或“假”。附加的表单处理宏简化了HTML转义的使用,您应该尽可能使用这些宏。它们将在下一节中进行解释。
输入宏指令(Input macros)
两种语言的附加便利宏简化了绑定和表单生成(包括验证错误显示)。不需要使用这些宏来生成表单输入字段,您可以将它们与简单的HTML混合并匹配,也可以直接调用前面突出显示的Spring绑定宏。
下表列出了可用宏的FTL定义和每个宏所采用的参数列表:
macro | FTL definition |
---|---|
message (根据代码参数从资源束输出字符串) | <@spring.message code/> |
messageText (根据代码参数从资源束中输出字符串,返回到默认参数的值) | <@spring.messageText code, text/> |
url (为相对URL加上应用程序上下文根的前缀) | <@spring.url relativeUrl/> |
formInput (用于收集用户输入的标准输入字段) | <@spring.formInput path, attributes, fieldType/> |
formHiddenInput(用于提交非用户输入的隐藏输入字段) | <@spring.formHiddenInput path, attributes/> |
formPasswordInput (用于收集密码的标准输入字段。请注意,此类型的字段中永远不会填充任何值。) | <@spring.formPasswordInput path, attributes/> |
formSingleSelect (允许选择单个必需值的选项下拉框) | <@spring.formTextarea path, attributes/> |
formMultiSelect (允许用户选择0个或更多值的选项列表框) | <@spring.formMultiSelect path, options, attributes/> |
formRadioButtons (一组单选按钮,允许从可用选项中进行单个选择。) | <@spring.formRadioButtons path, options separator, attributes/> |
formCheckboxes (允许选择0个或多个值的一组复选框) | <@spring.formCheckboxes path, options, separator, attributes/> |
formCheckbox (单个复选框) | <@spring.formCheckbox path, attributes/> |
showErrors(简化绑定字段的验证错误显示) | <@spring.showErrors separator, classOrStyle/> |
formTextarea (用于收集长的自由格式文本输入的大文本字段) | <@spring.formTextarea path, attributes/> |
上述任何宏的参数具有一致的含义:
以下部分概述了宏的示例(一些在FTL 中,一些在VTL中)。如果两种语言之间存在用法差异,则在注释中解释。
输入字段
forminput宏接受path参数(command.name)和附加的attributes参数(在下一个示例中为空)。宏与所有其他窗体生成宏一起,对路径参数执行隐式spring绑定。绑定在发生新绑定之前保持有效,因此showErrors宏不需要再次传递路径参数-它对上次为其创建绑定的字段进行操作。
showErrors宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数-这次是类名或样式属性。请注意,freemaker可以为attributes参数指定默认值。下面的示例演示如何使用formInput 和showWErrors 宏:
<@spring.formInput "command.name"/>
<@spring.showErrors "
>"/>
下一个示例显示表单片段的输出,生成名称字段,并在提交表单后显示验证错误,字段中没有值。通过Spring的验证框架进行验证。
生成的HTML类似于以下示例:
Name:
<input type="text" name="name" value="">
<br>
<b>requiredb>
<br>
<br>
formTextArea宏的工作方式与formInput宏相同,并且接受相同的参数列表。通常,第二个参数(属性)用于传递文本区域的样式信息或行和列属性。
选择字段
可以使用四个选择域宏在HTML表单中生成通用的UI值选择输入:
四个宏中的每一个都接受一个Map选项,这些选项包含表单字段的值和与该值对应的标签。值和标签可以相同。
下一个例子是FTL中的单选按钮。表单支持对象为此字段指定默认值“伦敦”,因此无需进行验证。渲染表单时,可以选择的整个城市列表作为参考数据在名为“cityMap”的模型中提供。以下清单显示了该示例:
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
前面的列表呈现一行单选按钮,每个按钮对应CityMap中的一个值,并使用分隔符“”。没有提供其他属性(缺少宏的最后一个参数)。citymap对映射中的每个键值对使用相同的字符串。映射的键是表单作为已发布请求参数实际提交的内容。映射值是用户看到的标签。在前面的示例中,给出了三个著名城市的列表和表单支持对象中的默认值,HTML类似于以下内容:
Town:
<input type="radio" name="address.town" value="London">Londoninput>
<input type="radio" name="address.town" value="Paris" checked="checked">Parisinput>
<input type="radio" name="address.town" value="New York">New Yorkinput>
如果应用程序希望按内部代码处理城市(例如),则可以使用适当的键创建代码映射,如下例所示:
protected Map<String, 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, String> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
代码现在生成输出,其中无线电值是相关代码,但用户仍然可以看到更友好的城市名称,如下所示:
Town:
<input type="radio" name="address.town" value="LDN">Londoninput>
<input type="radio" name="address.town" value="PRS" checked="checked">Parisinput>
<input type="radio" name="address.town" value="NYC">New Yorkinput>
HTML Escaping
前面描述的表单宏的默认用法会导致HTML元素符合HTML 4.01,并使用在web.xml文件中定义的HTML转义的默认值,如Spring的绑定支持所使用的。为了使元素符合XHTML或者覆盖默认的HTML转义值,您可以在模板中(或者在模型中,模板可以看到它们)指定两个变量。在模板中指定它们的好处是,可以在模板处理的稍后将它们更改为不同的值,以便为表单中的不同字段提供不同的行为。
要切换到标记的XHTML遵从性,请为名为XHTML模板的模型或上下文变量指定一个值true,如下例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理完这个指令后,Spring宏生成的任何元素现在都兼容XHTML。
以类似的方式,您可以为每个字段指定HTML转义,如下示例所示:
<#-- until this point, default HTML escaping is used -->
<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->
groovy标记模板引擎主要用于生成类似XML的标记(XML、XHTML、HTML5等),但您可以使用它生成任何基于文本的内容。Spring框架有一个内置的集成,用于将SpringMVC与groovy标记结合使用。
groovy标记模板引擎主要用于生成类似XML的标记(XML、XHTML、HTML5等),但您可以使用它生成任何基于文本的内容。Spring框架有一个内置的集成,用于将SpringMVC与groovy标记结合使用。
groovy标记模板引擎需要groovy 2.3.1+。
配置
下面的示例演示如何配置groovy标记模板引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
下面的示例演示如何在XML中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
mvc:view-resolvers>
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
Example
Unlike traditional template engines, Groovy Markup relies on a DSL that uses a builder syntax. The following example shows a sample template for an HTML page:
yieldUnescaped ''
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page')
}
body {
p('This is an example of HTML contents')
}
}
Same as in Spring WebFlux
Spring框架有一个内置的集成,用于使用Spring MVC和可以在JSR223 Java脚本引擎之上运行的任何模板库。我们在不同的脚本引擎上测试了以下模板库:
集成任何其他脚本引擎的基本规则是,它必须实现脚本引擎和可调用接口。
您需要将脚本引擎放在classpath,其详细信息因脚本引擎而异:
您需要拥有脚本模板库。对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;
}
}
以下示例显示了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.addObject("title", "Sample title");
model.addObject("body", "Sample body");
return "template";
}
}
以下示例显示了Mustache模板:
<html>
<head>
<title>{{title}}title>
head>
<body>
<p>{{body}}p>
body>
html>
使用以下参数调用render函数:
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;
}
}
当使用非线程安全脚本引擎时,需要将sharedEngine属性设置为false,模板库不是为并发而设计的,例如运行在Nashorn的Handlebars or React;在这种情况下,由于这个bug,需要Java8U60或更大。
polyfill.js仅定义通过Handlebars的运行属性( run properly)所需的窗口对象,如下所示:
var window = {};
这个基本的render.js实现在使用之前编译模板。生产就绪的实现还应该存储任何重用的缓存模板或预编译模板。您可以在脚本端这样做(例如,处理任何您需要的定制-管理模板引擎配置)。以下示例显示了如何执行此操作:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
检查Spring框架单元测试、Java和资源,以获得更多配置示例。
Spring框架有一个内置的集成,用于将SpringMVC与JSP和JSTL结合使用。
视图解析器
使用JSP开发时,可以声明InternalResourceViewResolver或ResourceBundleviewResolver bean。
ResourceBundleviewResolver依赖属性文件来定义映射到类和URL的视图名称。使用ResourceBundleviewResolver,您可以只使用一个Resolver混合不同类型的视图,如下示例所示:
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
bean>
# And a sample properties file is uses (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
InternalResourceBundleviewResolver也可用于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>
当使用Java标准标记库时,必须使用特殊视图类JSTLView,因为JSTL需要在I18N特性等工作之前进行一些准备。
Spring提供了请求参数到命令对象的数据绑定,如前几章所述。为了结合这些数据绑定特性促进JSP页面的开发,Spring提供了一些标签,使事情变得更加容易。所有Spring标记都具有HTML转义功能,可以启用或禁用字符转义。
spring.tld标记库描述符(tld)包含在spring-webmvc.jar中。有关单个标记的综合引用,请浏览API引用或参见标记库说明。
从2.0版开始,Spring提供了一套全面的数据绑定感知标签,用于在使用JSP和SpringWebMVC时处理表单元素。每个标签都支持对应的HTML标签对应的一组属性,使标签的使用既熟悉又直观。标记生成的HTML符合HTML 4.01/XHTML 1.0。
与其他表单/输入标记库不同,Spring的表单标记库与SpringWebMVC集成,使标记可以访问命令对象和控制器处理的参考数据。正如我们在下面的示例中所展示的,表单标记使JSP更易于开发、读取和维护。
我们将浏览表单标记,并查看如何使用每个标记的示例。我们已经包含了生成的HTML片段,其中某些标记需要进一步的注释。
表单标记库捆绑在spring-webmvc.jar中。库描述符称为spring-form.tld。
要使用此库中的标记,请在JSP页面顶部添加以下指令:
// 其中,form是要用于此库中标记的标记名称前缀。
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
此标记呈现HTML“form”元素,并向内部标记公开绑定路径以进行绑定。它将命令对象放在pageContext中,以便内部标记可以访问命令对象。此库中的所有其他标记都是表单标记的嵌套标记。
假设我们有一个称为用户的域对象。它是一个具有firstname和lastname等属性的JavaBean。我们可以使用它作为表单控制器的表单支持对象,它返回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中的命令对象中检索。继续阅读以了解内部标记如何与表单标记一起使用的更复杂示例。
下面的列表显示生成的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>
The input Tag
此标记将呈现具有绑定值的HTML输入元素,默认情况下键入=‘text’。有关此标记的示例,请参见表单标记。您还可以使用HTML5特定类型,如电子邮件、电话、日期和其他。
The checkbox Tag
此标记呈现类型设置为复选框的HTML输入标记。
假设我们的用户有preferences,如订阅时事通讯和兴趣列表。下面的示例显示了Preferences类:
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
相应的form.jsp可以类似于以下内容:
<form:form>
<table>
<tr>
<td>Subscribe to newsletter?:td>
<%-- Approach 1: Property is of type java.lang.Boolean --%>
<td><form:checkbox path="preferences.receiveNewsletter"/>td>
tr>
<tr>
<td>Interests:td>
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
<td>
Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
td>
tr>
<tr>
<td>Favourite Word:td>
<%-- Approach 3: Property is of type java.lang.Object --%>
<td>
Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
td>
tr>
table>
form:form>
checkbox标签有三种方法,可满足您的所有复选框需求。
需要注意的是,不论以何种方式,产生相同的HTML结构。下面的HTML片段定义了一些复选框:
<tr>
<td>Interests:td>
<td>
Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
<input type="hidden" value="1" name="_preferences.interests"/>
td>
tr>
您可能不希望在每个复选框后看到其他隐藏字段。如果未选中HTML页中的复选框,则在提交表单后,它的值不会作为HTTP请求参数的一部分发送到服务器,因此我们需要一个解决方案来解决HTML中的这个问题,以使Spring表单数据绑定正常工作。复选框标记遵循现有的Spring惯例,即为每个复选框包含一个带下划线前缀的隐藏参数。通过这样做,您可以有效地告诉Spring“复选框在表单中可见,我希望表单数据绑定到的对象反映复选框的状态,不管是什么。”
复选框标记
此标记呈现多个类型设置为复选框的HTML输入标记。
此部分基于上一个复选框标记部分的示例。有时,您不必在JSP页面中列出所有可能的爱好。您希望在运行时提供可用选项的列表,并将其传递给标记。这就是复选框标记的用途。可以传入数组、列表或包含“项”属性中可用选项的映射。通常,绑定属性是一个集合,这样它可以保存用户选择的多个值。以下示例显示了使用此标记的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是一个可用作模型属性的列表,其中包含要从中选择的值的字符串。如果使用Map,Map输入键将用作值,Map输入的值将用作要显示的标签。还可以使用自定义对象,其中可以使用itemValue提供值的属性名,使用itemLabel提供标签。
RadioButton标记
此标记呈现类型设置为radio的HTML input 元素。
典型的使用模式涉及多个绑定到同一属性但具有不同值的标记实例,如下示例所示:
<tr>
<td>Sex:td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
td>
tr>
radiobuttons Tag
此标记呈现类型设置为radio的多个HTML input 元素。
与复选框标记一样,您可能希望将可用选项作为运行时变量传入。对于这种用法,可以使用radiobuttons标记。传递一个数组、列表或包含items属性中可用选项的映射。如果使用Map,Map输入键将用作值,Map输入值将用作要显示的标签。还可以使用自定义对象,其中可以使用itemValue提供值的属性名,使用itemLabel提供标签,如下示例所示:
<tr>
<td>Sex:td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/>td>
tr>
The password Tag
此标记将呈现类型设置为带绑定值的密码的HTML输入标记。
<tr>
<td>Password:td>
<td>
<form:password path="password"/>
td>
tr>
注意,默认情况下,不会显示密码值。如果希望显示密码值,可以将showpassword属性的值设置为true,如下示例所示:
<tr>
<td>Password:td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
td>
tr>
The select Tag
此标记呈现HTML“select”元素。它支持与所选选项的数据绑定,以及使用嵌套选项和选项标记。
假设用户拥有技能列表。相应的HTML可以如下所示:
Skills:
如果用户的技能在Herbology中,“Skills”行的HTML源代码可以如下所示:
<tr>
<td>Skills:td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potionsoption>
<option value="Herbology" selected="selected">Herbologyoption>
<option value="Quidditch">Quidditchoption>
select>
td>
tr>
The option Tag
此标记呈现HTML选项元素。它根据绑定值设置选定项。以下HTML显示了它的典型输出:
<tr>
<td>House:td>
<td>
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
form:select>
td>
tr>
如果用户的房子在gryfindor中,“House”行的HTML源代码如下:
<tr>
<td>House:td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindoroption>
<option value="Hufflepuff">Hufflepuffoption>
<option value="Ravenclaw">Ravenclawoption>
<option value="Slytherin">Slytherinoption>
select>
td>
tr>
The options Tag
此标记呈现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>
If the User lived in the UK, the HTML source of the ‘Country’ row would be as follows:
<tr>
<td>Country:td>
<td>
<select name="country">
<option value="-">--Please Selectoption>
<option value="AT">Austriaoption>
<option value="UK" selected="selected">United Kingdomoption>
<option value="US">United Statesoption>
select>
td>
tr>
如前面的示例所示,选项标记与选项标记的组合使用将生成相同的标准HTML,但允许您在JSP中显式指定一个值,该值仅用于显示(它所属的位置),例如示例中的默认字符串:“-- Please Select”。
items属性通常由item对象的集合或数组填充。itemValue和itemLabel引用这些item对象的bean属性(如果指定)。否则,item对象本身将变成字符串。或者,可以指定项目的映射,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果同时指定了itemValue或itemLabel(或两者),则item value属性将应用于映射键,item label属性将应用于映射值。
The textarea Tag
此标记呈现HTML textarea元素。以下HTML显示了它的典型输出:
<tr>
<td>Notes:td>
<td><form:textarea path="notes" rows="3" cols="20"/>td>
<td><form:errors path="notes"/>td>
tr>
The hidden Tag
此标记将呈现类型设置为“使用绑定值隐藏”的HTML输入标记。要提交未绑定的隐藏值,请使用类型设置为hidden的HTML输入标记。以下HTML显示了它的典型输出:
<form:hidden path="house"/>
如果我们选择将house值作为隐藏值提交,则HTML将如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
The errors Tag
此标记在HTML跨距元素中呈现字段错误。它提供对在控制器中创建的错误或由与控制器关联的任何验证器创建的错误的访问。
假设我们希望在提交表单后显示firstname和lastname字段的所有错误消息。我们有一个名为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.");
}
}
The form.jsp could be as follows:
<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>
如果我们想显示给定页面的整个错误列表,该怎么办?下一个示例显示错误标记还支持一些基本的通配符功能。
以下示例在页面顶部显示错误列表,然后在字段旁边显示特定于字段的错误:
<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>
The HTML would be as follows:
<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,也可以使用“real”方法作为附加参数进行POST(在HTML表单中建模为隐藏输入字段)。Spring的HiddenHttpMethodFilter使用后一种技巧。这个过滤器是一个普通的Servlet过滤器,因此,它可以与任何Web框架(不仅仅是Spring MVC)结合使用。将此过滤器添加到web.xml,并将带有隐藏method参数的POST 转换为相应的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,请求参数后面隐藏“real” DELETE 方法。它由hiddenhttpmethodfilter获取,hiddenhttpmethodfilter在web.xml中定义,如下示例所示:
<filter>
<filter-name>httpMethodFilterfilter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilterfilter-class>
filter>
<filter-mapping>
<filter-name>httpMethodFilterfilter-name>
<servlet-name>petclinicservlet-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;
}
HTML5 Tags
Spring表单标记库允许输入动态属性,这意味着您可以输入任何特定于HTML5的属性。
表单输入标记支持输入文本以外的类型属性。这将允许呈现新的HTML5特定输入类型,如电子邮件、日期、范围和其他。请注意,输入type='text’不是必需的,因为text是默认类型。
您可以在使用Spring的Web应用程序中集成Tiles,就像其他任何视图技术一样。本节以宽泛的方式描述了如何做到这一点。
本节重点介绍Spring在org.springframework.web.servlet.view.tiles3包中对Tiles版本3的支持。
依赖
为了能够使用Tiles,您必须在Tiles 3.0.1或更高版本上添加依赖项及其 对项目的传递依赖性。
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xmlvalue>
<value>/WEB-INF/defs/widgets.xmlvalue>
<value>/WEB-INF/defs/administrator.xmlvalue>
<value>/WEB-INF/defs/customer.xmlvalue>
<value>/WEB-INF/defs/templates.xmlvalue>
list>
property>
bean>
前面的示例定义了五个包含定义的文件。这些文件都位于WEB-INF/DEFS目录中。初始化WebApplicationContext时,将加载文件,并初始化定义工厂。完成之后,定义文件中包含的图块可以用作SpringWeb应用程序中的视图。为了能够使用视图,您必须像使用Spring的任何其他视图技术一样拥有一个视图解析器。您可以使用两种实现中的任何一种,即UrlBasedViewResolver 和ResourceBundleViewResolver。
可以通过添加下划线,然后添加区域设置来指定特定于区域设置的图块定义,如下例所示:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/tiles.xmlvalue>
<value>/WEB-INF/defs/tiles_fr_FR.xmlvalue>
list>
property>
bean>
在前面的配置中,tiles_fr_fr.xml用于fr_fr区域设置的请求,tiles.xml默认使用。
由于下划线用于指示区域设置,我们建议不要在tiles定义的文件名中使用它们。
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定义以及相应的视图名称和视图类(取自pet clinic示例):
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="views"/>
bean>
...
welcomeView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
welcomeView.url=welcome (this is the name of a Tiles definition)
vetsView.(class)=org.springframework.web.servlet.view.tiles3.TilesView
vetsView.url=vetsView (again, this is the name of a Tiles definition)
findOwnersForm.(class)=org.springframework.web.servlet.view.JstlView
findOwnersForm.url=/WEB-INF/jsp/findOwners.jsp
...
使用ResourceBundleviewResolver时,可以轻松地混合不同的视图技术。
请注意,tilesView类支持JSTL(JSP标准标记库)。
作为高级功能,Spring还支持两个特殊的Tiles PreparerFactory实现。有关如何在Tiles定义文件中使用ViewPreparer引用的详细信息,请参阅Tiles文档。
可以基于指定的Preparer类将SimpleSpringPreparerFactory指定为AutoWire ViewPreparer实例,应用Spring的容器回调以及应用配置的Spring BeanPostProcessor。如果已激活Spring的上下文范围注释配置,则会自动检测并应用viewPreparer类中的注释。请注意,这需要Tiles定义文件中的Preparer类,就像默认的PreparerFactory一样。
您可以指定SpringBeanPreparerFactory对指定的Preparer名称(而不是类)进行操作,从DispatcherServlet的应用程序上下文中获取相应的SpringBean。在这种情况下,完整的bean创建过程由Spring应用程序上下文控制,允许使用显式依赖项注入配置、作用域bean等。请注意,您需要为每个preparer name定义一个SpringBean定义(在tiles定义中使用)。下面的示例演示如何在TileConfigurerBean上定义一组SpringBeanPreparerFactory属性:
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
<property name="definitions">
<list>
<value>/WEB-INF/defs/general.xmlvalue>
<value>/WEB-INF/defs/widgets.xmlvalue>
<value>/WEB-INF/defs/administrator.xmlvalue>
<value>/WEB-INF/defs/customer.xmlvalue>
<value>/WEB-INF/defs/templates.xmlvalue>
list>
property>
<property name="preparerFactoryClass"
value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>
bean>
AbstractAtomFeedView 和AbstractRssFeedView 都继承自AbstractFeedView 基类,分别用于提供Atom和RSS提要视图。它们基于java.net’s ROME project,位于包org.springframework.web.servlet.view.feed中。
AbstractomFeedView要求您实现buildFeedEntries()方法,并可选地重写buildFeedMetadata()方法(默认实现为空)。以下示例显示了如何执行此操作:
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
类似的要求也适用于实现AbstractrssFeedView,如下示例所示:
public class SampleContentAtomView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}
buildFeedItems()和buildFeedEntries()在HTTP请求方法传递,如果你需要访问的语言环境。HTTP响应仅用于设置cookie或其他HTTP标头。方法返回后,Feed会自动写入响应对象。
有关创建Atom视图的示例,请参见Alef Arendsen的Spring团队博客条目。
Spring提供了返回HTML以外的输出的方法,包括PDF和Excel电子表格。本节介绍如何使用这些功能。
HTML页面并不总是用户查看模型输出的最佳方式,而Spring可以轻松地从模型数据动态生成PDF文档或Excel电子表格。该文档是视图,并从具有正确内容类型的服务器流式传输到(希望)使客户端PC能够运行其电子表格或PDF查看器应用程序作为响应。
要使用Excel视图,需要将Apache POI库添加到类路径中。对于PDF生成,您需要添加(最好)OpenPDF库。
如果可能,您应该使用最新版本的基础文档生成库。特别是,我们强烈建议使用OpenPDF(例如,OpenPDF 1.0.5)而不是过时的原始iText 2.1.7,因为OpenPDF是主动维护的,并修复了不受信任的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));
}
}
}
控制器可以从外部视图定义(通过名称引用它)或作为View处理程序方法中的实例返回此类视图。
从SpringFramework4.2开始,org.springFramework.web.servlet.view.document.abstractXLsView作为Excel视图的基类提供。它基于Apache POI,具有专门的子类(AbstractXLSxView和AbstractXLSxStreamingView),取代了过时的AbstractExcelView类。
编程模型类似于abstractPDFview,其中buildExcelDocument()作为中心模板方法,控制器能够从外部定义(按名称)返回这样的视图,或者从handler方法返回视图实例。
Same as in Spring WebFlux
Spring提供了对Jackson JSON库的支持。
MappingJackson2JsonView 使用Jackson库的objectmapper将响应内容呈现为json。默认情况下,模型映射的全部内容(框架特定类除外)都被编码为JSON。对于需要筛选映射内容的情况,可以使用ModelKeys属性指定要编码的特定模型属性集。还可以使用ExtractValueFromSingleKeyModel属性直接提取和序列化单键模型中的值,而不是将其作为模型属性的映射。
您可以根据需要使用Jackson提供的注释自定义JSON映射。当需要进一步的控制时,可以通过Objectmapper属性插入自定义的Objectmapper,以满足需要为特定类型提供自定义JSON序列化程序和反序列化程序的情况。
MappingJackson2XmlView 使用Jackson XML扩展的xmlmapper将响应内容呈现为XML。如果模型包含多个条目,则应使用modelkey bean属性显式设置要序列化的对象。如果模型包含一个条目(a single entry),它将自动序列化。
您可以根据需要使用JAXB或Jackson提供的注释自定义XML映射。当需要进一步控制时,可以通过Objectmapper属性插入自定义的xmlmapper,对于需要为特定类型提供序列化程序和反序列化程序的自定义XML的情况。
MarshallingView 使用一个XML marshaller(在org.springframework.oxm包中定义)将响应内容呈现为XML。可以使用MarshalingView实例的ModelKey bean属性显式设置要封送的对象。或者,视图迭代所有模型属性,并封送封送器支持的第一个类型。有关org.springframework.oxm包中功能的更多信息,请参阅使用O/X映射器编组XML。
XSLT是XML的一种转换语言,在Web应用程序中作为一种视图技术广受欢迎。如果您的应用程序自然地处理XML或者您的模型可以很容易地转换为XML,那么作为一种视图技术,XSLT是一个不错的选择。下面的部分将展示如何生成XML文档作为模型数据,并在SpringWebMVC应用程序中使用XSLT对其进行转换。
这个例子是一个普通的Spring应用程序,它在控制器中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及XSLT视图的视图名称。有关SpringWebMVC的控制器接口的详细信息,请参见带注释的控制器。XSLT控制器将单词列表转换为一个简单的XML文档,以备转换。
配置是简单的SpringWeb应用程序的标准配置: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;
}
}
我们还需要一个封装我们的word-generation logic的控制器。
控制器逻辑封装在@controller类中,处理程序方法定义如下:
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
到目前为止,我们只创建了一个DOM文档并将其添加到模型映射中。请注意,您也可以将XML文件作为资源加载并使用它而不是自定义的DOM文档。
有一些软件包可以自动“定位(domify)”对象图,但是在Spring中,您可以完全灵活地从模型中以任何方式创建DOM。这可以防止XML的转换在模型数据的结构中发挥太大的作用,这在使用工具管理注册过程时是一个危险因素。
最后,xsltviewresolver解析“home”xslt模板文件,并将dom文档合并到其中以生成视图。如xsltviewresolver配置所示,xslt模板位于WEB-INF/xsl目录中的war文件中,以xslt文件扩展名结尾。
下面的示例显示了一个XSLT转换:
<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 Wordsh1>
<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 Wordsh1>
<ul>
<li>Helloli>
<li>Springli>
<li>Frameworkli>
ul>
body>
html>
Same as in Spring WebFlux
MVC Java配置和MVC XML命名空间提供适用于大多数应用程序的默认配置和配置API来定制它。
您不需要了解MVC Java配置和MVC命名空间创建的基础bean。
在Java配置中,可以使用@EnableWebMvc 注释来启用MVC配置,如下示例显示:
@Configuration
@EnableWebMvc
public class WebConfig {
}
在XML配置中,可以使用
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven/>
beans>
前面的示例注册了许多SpringMVC基础架构bean,并适应类路径上可用的依赖项(例如,JSON、XML等的有效负载转换器)。
在Java配置中,可以实现WebMvcConfigurer 接口,如下面的示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
// Implement configuration methods...
}
在XML中,可以检查
类型转换,默认情况下会安装对Number 和Date 类型的格式化程序,包括支持注释@NumberFormat and @DateTimeFormat。如果类路径中存在Joda-Time,则还会安装对Joda-Time格式库的完全支持。
在Java配置中,您可以注册自定义格式化程序和转换器,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
以下示例显示如何在XML中实现相同的配置:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://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>
有关何时使用FormatterRegstrar实现的详细信息,请参阅FormatterRegstrar SPI和FormattingConversionServiceFactoryBean。
默认情况下,如果类路径上存在bean验证(例如hibernate validator),则LocalValidatorFactoryBean 注册为全局验证程序,以便与@valid一起使用,并在控制器方法参数上进行验证。
在Java配置中,可以自定义全局验证器实例,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator(); {
// ...
}
}
下面的示例演示如何在XML中实现相同的配置:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven validator="globalValidator"/>
beans>
注意,您也可以在本地注册验证器实现,如下示例所示:
@Controller
public class MyController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
如果需要在某个地方注入LocalValidatorFactoryBean,请创建一个bean并用@primary标记它,以避免与MVC配置中声明的bean冲突。
在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/*");
}
}
下面的示例演示如何在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>
您可以配置SpringMVC如何根据请求确定请求的媒体类型(例如,接受头、URL路径扩展、查询参数和其他)。
默认情况下,首先检查URL路径扩展-,JSON、XML、RSS和Atom注册为已知扩展(取决于类路径依赖项)。第二个选中接受头。
考虑将这些默认值更改为仅接受头,如果必须使用基于URL的内容类型解析,则考虑在路径扩展上使用查询参数策略。更多详细信息,请参见后缀匹配和后缀匹配以及RFD。
在Java配置中,可以自定义请求的内容类型解析,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
下面的示例演示如何在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>
您可以通过重写configureMessageConverters()来替换Java配置中的HttpMessageConverter (以替换Spring MVC创建的默认转换器)或重写扩展extendMessageConverters()(以自定义默认转换器或向默认的转换器中添加额外的转换器)。
下面的示例使用自定义的对象映射器而不是默认的对象映射器添加了XML和Jackson JSON转换器:
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
在前面的示例中,jackson2ObjectmapperBuilder用于为启用缩进的mappingjackson2httpMessageConverter和mappingjackson2xmlhttpMessageConverter创建通用配置、自定义日期格式和注册Jackson模块参数名,这增加了对访问参数名的支持(java8的一个功能)
此编译器自定义 Jackson的默认属性,如下所示:
如果在类路径上检测到以下已知模块,它还会自动注册这些模块:
除了jackson-dataformat-xml之外,使用Jackson XML支持启用缩进还需要woodstox-core-asl 依赖项
其他Jackson 模块:
下面的示例演示如何在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"/>
这是定义ParameterizableViewController ,当调用时,它会立即转发到视图。在视图生成响应之前没有Java控制器逻辑执行时,可以在静态情况下使用它。
下面的Java配置示例将一个请求转发到一个称为home的视图:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
以下示例与前面的示例实现相同的功能,但使用XML,通过使用以下mvc:view-controller元素:
<mvc:view-controller path="/" view-name="home"/>
MVC配置简化了视图解析器的注册。
以下Java配置示例使用JSP和Jackson作为视图,JSON呈现的默认值来配置内容协商视图解析:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
以下示例显示如何在XML中实现相同的配置:
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
mvc:default-views>
mvc:content-negotiation>
<mvc:jsp/>
mvc:view-resolvers>
但请注意,FreeMarker,Tiles,Groovy 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配置中,您可以添加相应的Configurerbean,如以下示例所示:
@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;
}
}
此选项提供了一种从基于资源的位置列表中提供静态资源的方便方法。
在下一个示例中,给定以/resources开头的请求,相对路径用于在Web应用程序根目录下或在/static下的类路径上查找和服务相对于/public的静态资源。这些资源将在未来一年内到期,以确保最大限度地使用浏览器缓存,并减少浏览器发出的HTTP请求。最后一个修改的头也将被评估,如果存在,则返回304状态代码。
下面的列表显示了如何用Java配置实现:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCachePeriod(31556926);
}
}
下面的示例演示如何在XML中实现相同的配置:
<mvc:resources mapping="/resources/**"
location="/public, classpath:/static/"
cache-period="31556926" />
资源处理程序还支持ResourceResolver实现和ResourceTransformer实现的链,您可以使用它创建用于处理优化资源的工具链。
您可以使用VersionResourceResolver来基于从内容、固定应用程序版本或其他版本计算的MD5哈希对版本化资源URL进行解析。ContentVersionStrategy(MD5哈希)是一个不错的选择-,但也有一些明显的例外,例如与模块加载程序一起使用的javascript资源。
下面的示例演示如何在Java配置中使用VersionResourceResolver :
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
下面的示例演示如何在XML中实现相同的配置:
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain>
<mvc:resource-cache/>
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
mvc:version-resolver>
mvc:resolvers>
mvc:resource-chain>
mvc:resources>
然后,您可以使用ResourceUrlProvider重写URL并应用完整的解析器和转换器链 - 例如,插入版本。MVC配置提供了一个ResourceUrlProvider bean,以便它可以注入其他bean。您还可以使用ResourceUrlEncodingFilter 对Thymeleaf、JSP、FreeMarker和其他依赖HttpServletResponse#encodeURL的URL标记使重写透明。
请注意,当同时使用EncodedResourceResolver(例如,为gzip或brotli编码的资源提供服务)和VersionedResourceResolver时,必须按此顺序注册它们。这可以确保content-based总是基于编码文件的。
Webjars还通过WebjarsResourceResolver支持,并且org.webjars:webjars-locator在类路径中存在时自动注册。解析器可以重新写入URL以包含JAR的版本,也可以与没有版本-的传入URL匹配,例如,/jquery/jquery.min.js到/jquery/1.2.0/jquery.min.js。
SpringMVC允许将DispatcherServlet映射到/(从而覆盖容器的默认servlet的映射),同时仍然允许静态资源请求由容器的默认servlet处理。它配置了一个DefaultServletHttpRequestHandler ,其URL映射为/**且相对于其他URL映射的优先级最低。
此处理程序将所有请求转发到默认servlet。因此,它必须按照所有其他URL句柄映射的顺序排在最后。如果使用
以下示例显示如何使用默认设置启用功能:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
下面的示例演示如何在XML中实现相同的配置:
<mvc:default-servlet-handler/>
覆盖/servlet映射的注意事项是,必须按名称而不是按路径检索默认servlet的requestDispatcher。DefaultServletHttpRequestHandler 尝试在启动时自动检测容器的默认servlet,使用大多数主要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");
}
}
下面的示例演示如何在XML中实现相同的配置:
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
您可以自定义与路径匹配和URL处理相关的选项。有关各个选项的详细信息,请参阅PathMatchConfigurer JavaDoc。
下面的示例演示如何在Java配置中自定义路径匹配:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api",
HandlerTypePredicate.forAnnotation(RestController.class));
}
@Bean
public UrlPathHelper urlPathHelper() {
//...
}
@Bean
public PathMatcher antPathMatcher() {
//...
}
}
下面的示例演示如何在XML中实现相同的配置:
<mvc:annotation-driven>
<mvc:path-matching
suffix-pattern="true"
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"/>
@enableWebMVC导入DelegatingWebMvcConfiguration,:
对于高级模式,可以删除@enableWebMVC并直接从DelegatingWebMvcConfiguration 中扩展,而不是实现WebMvcConfigurer,如下示例所示:
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {
// ...
}
您可以在webconfig中保留现有的方法,但是现在您还可以覆盖基类中的bean声明,并且您仍然可以在类路径上拥有任何数量的其他webmvcconfiger实现。
MVC命名空间没有高级模式。如果您需要定制一个bean上的属性,否则您不能更改它,那么您可以使用SpringApplicationContext的BeanPostProcessor 生命周期挂钩,如下示例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
}
}
注意,您需要将MyPostProcessor 声明为bean,可以显式地使用XML,也可以通过
Servlet 4容器需要支持HTTP/2,Spring Framework 5与Servlet API 4兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,还有一些与服务器配置相关的注意事项。有关详细信息,请参阅http/2 wiki页面。
servlet API没有公开一个与HTTP/2相关的构造。您可以使用javax.servlet.http.pushbuilder主动地将资源推送到客户机,它作为@requestmapping方法的方法参数得到支持。
本节介绍客户端访问REST端点的选项。
RestTemplate 是执行HTTP请求的同步客户端。它是原始的SpringRest客户机,并在底层HTTP客户机库上公开一个简单的模板方法API。
从5.0开始,非阻塞、反应式WebClient提供了一种现代的RestTemplate替代方案,对同步和异步以及流场景都提供了有效的支持。RestTemplate将在未来的版本中被弃用,将来不会添加主要的新功能。
WebClient是一个执行HTTP请求的非阻塞、反应式客户端。它是在5.0中引入的,提供了一种现代的RestTemplate替代方案,对同步和异步以及流场景都提供了有效的支持。
与RestTemplate不同,WebClient支持以下功能:
Same in Spring WebFlux
本节总结了Spring MVC应用程序的Spring测试中可用的选项。
Same as in Spring WebFlux
参考文档的这一部分包括对servlet堆栈的支持、包含原始websocket交互的websocket消息传递、通过sockjs的websocket仿真以及通过stomp将订阅消息传递作为websocket上的子协议发布。
WebSocket协议(RFC6455)提供了一种标准化的方法,通过单个TCP连接在客户机和服务器之间建立全双工、双向的通信通道。它是一个不同于HTTP的TCP协议,但设计用于HTTP,使用端口80和443,并允许重新使用现有防火墙规则。
WebSocket交互从一个HTTP请求开始,该请求使用HTTP升级头(Upgrade header )进行升级,或者在本例中切换到WebSocket协议。下面的示例显示了这种交互:
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket -- The Upgrade header.
Connection: Upgrade --Using the Upgrade connection.
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
与通常的200状态代码不同,具有WebSocket支持的服务器返回类似于以下内容的输出:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
成功握手后,HTTP升级请求的基础TCP套接字将保持打开状态,以便客户端和服务器继续发送和接收消息。
有关WebSockets工作方式的完整介绍超出了本文档的范围。请参阅RFC6455、HTML5的WebSocket章节或Web上的许多介绍和教程。
请注意,如果WebSocket服务器在Web服务器(例如nginx)后面运行,则可能需要将其配置为将WebSocket升级请求传递到WebSocket服务器。同样,如果应用程序在云环境中运行,请检查与WebSocket支持相关的云提供程序的说明。
尽管WebSocket被设计为与HTTP兼容并且以HTTP请求开始,但重要的是要理解这两种协议会导致非常不同的体系结构和应用程序编程模型。
在HTTP和REST中,应用程序被建模为多个URL。要与应用程序进行交互,客户端将访问这些URL,请求 - 响应样式。服务器根据HTTP URL,方法和标头将请求路由到适当的处理程序。
相比之下,在WebSockets中,初始连接通常只有一个URL。随后,所有应用程序消息都在同一TCP连接上流动。这指向完全不同的异步,事件驱动的消息传递体系结构。
WebSocket也是一种低级传输协议,与HTTP不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息。
WebSocket客户端和服务器可以通过Sec-WebSocket-ProtocolHTTP握手请求中的标头协商使用更高级别的消息传递协议(例如,STOMP)。如果没有,他们需要提出自己的惯例。
WebSockets可以使网页变得动态和交互。但是,在许多情况下,Ajax和HTTP流式传输或长轮询的组合可以提供简单有效的解决方案。
例如,新闻,邮件和社交订阅源需要动态更新,但每隔几分钟就可以完全正常更新。另一方面,协作,游戏和财务应用程序需要更接近实时。
仅延迟不是决定因素。如果消息量相对较低(例如,监视网络故障),HTTP流式传输或轮询可以提供有效的解决方案。它是低延迟,高频率和高容量的组合,是使用WebSocket的最佳选择。
还要记住,在Internet上,受控制之外的限制性代理可能会阻止WebSocket交互,因为它们未配置为传递 Upgrade标头,或者因为它们关闭看似空闲的长期连接。这意味着将WebSocket用于防火墙内的内部应用程序是一个比面向公众的应用程序更直接的决策。
Spring Framework提供了一个WebSocket API,您可以使用它来编写处理WebSocket消息的客户端和服务器端应用程序。
创建WebSocket服务与实现WebSocketHandler一样简单,或者更可能扩展textWebSocketHandler或binaryWebSocketHandler。以下示例使用textWebSocketHandler:
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}
}
有专门的WebSocket Java配置和XML命名空间支持,用于将前面的WebSocket 处理程序映射到特定的URL,如下示例显示:
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
下面的示例显示了与前面示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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>
前面的示例用于SpringMVC应用程序,应该包含在DispatcherServlet的配置中。然而,弹簧的WebSocket支持并不依赖于弹簧MVC。在WebSocketHttpRequestHandler的帮助下,将WebSocketHandler 集成到其他HTTP服务环境中相对简单。
当直接或间接使用WebSocketHandler API时,例如通过stomp消息传递,应用程序必须同步消息的发送,因为基础标准WebSocket会话(JSR-356)不允许并发发送。一个选项是使用ConcurrentWebSocketSessionDecorator包装WebSocketSession。
WebSocket握手
自定义初始HTTP WebSocket握手请求的最简单方法是通过HandshakeInterceptor,它公开了握手“before”和“after”的方法。您可以使用这样的拦截器来阻止握手或使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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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方法产生的所有未捕获异常,并关闭状态为1011的websocket会话,这表示服务器错误。
Spring WebSocket API易于集成到Spring MVC应用程序中,其中DispatcherServlet既提供HTTP WebSocket握手又提供其他HTTP请求。通过调用WebSocketHttpPrequestHandler,还可以很容易地集成到其他HTTP处理场景中。这很方便,也很容易理解。但是,特别注意事项适用于JSR-356运行时。
Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的Servlet容器类路径扫描(Servlet 3功能)。另一个是在Servlet容器初始化时使用的注册API。这些机制都不能使用单个“前端控制器”进行所有HTTP处理 - 包括WebSocket握手和所有其他HTTP请求 - 例如Spring MVC DispatcherServlet。
这是JSR-356的一个重要限制,Spring的WebSocket支持通过特定于服务器的RequestUpgradeStegy实现来解决这一问题,即使在JSR-356运行时运行。这种策略目前适用于Tomcat、Jetty、Glassfish、Weblogic、WebSphere和Undertow(以及Wildfly)。
已经创建了克服Java WebSocket API中的上述限制的请求,并且可以在eclipse-ee4j / websocket-api#211中遵循该请求 。Tomcat,Undertow和WebSphere提供了自己的API替代方案,可以实现这一点,Jetty也可以实现。我们希望更多的服务器能够做到这一点。
第二个考虑因素是,支持JSR-356的Servlet容器可以执行ServletContainerInitializer(SCI)扫描,这可能会减慢应用程序的启动速度 - 在某些情况下会显着降低。如果在升级到支持JSR-356的Servlet容器版本后观察到重大影响,则应该可以通过使用元素来选择性地启用或禁用Web片段(和SCI扫描)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
http://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
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_webname>
absolute-ordering>
web-app>
每个底层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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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,您需要通过WebSocket Java配置提供预配置的Jetty WebSocketServerFactory 和插件到Spring的Debug和SakKeHunter中。以下示例显示了如何执行此操作:
@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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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>
从Spring Framework 4.1.5开始,WebSocket和SockJS的默认行为是仅接受同源请求。也可以允许所有或指定的起源列表。此检查主要是为浏览器客户端设计的。没有什么能阻止其他类型的客户端修改Origin标头值(有关更多详细信息,请参阅 RFC 6454:Web Origin Concept)。
三种可能的行为是:
您可以配置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("http://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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:handlers allowed-origins="http://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
beans>
在公共Internet上,您控制之外的限制性代理可能会阻止WebSocket交互,因为它们未配置为传递Upgrade标头,或者因为它们关闭看似空闲的长期连接。
这个问题的解决方案是WebSocket仿真 - 也就是说,首先尝试使用WebSocket,然后再回到基于HTTP的技术,这些技术模拟WebSocket交互并公开相同的应用程序级API。
在Servlet堆栈上,Spring Framework为SockJS协议提供服务器(以及客户端)支持。
SockJS的目标是让应用程序使用WebSocket API,但在运行时必要时可以回退到非WebSocket替代方案,而无需更改应用程序代码。
SockJS包括:
SockJS专为在浏览器中使用而设计。它使用各种技术来支持各种浏览器版本。有关SockJS传输类型和浏览器的完整列表,请参阅 SockJS客户端页面。传输分为三大类:WebSocket,HTTP Streaming和HTTP Long Polling。
SockJS客户端首先发送GET /info以从服务器获取基本信息。之后,它必须决定使用什么传输。如果可能,使用WebSocket。如果没有,在大多数浏览器中,至少有一个HTTP流选项。如果不是,则使用HTTP(长)轮询。
所有传输请求都具有以下URL结构:
/**
{server-id} 对于在群集中路由请求很有用,但不会以其他方式使用。
{session-id} 关联属于SockJS会话的HTTP请求。
{transport}指示传输类型(例如,websocket,xhr-streaming,和其它物质)。
*/
http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
WebSocket传输只需要一个HTTP请求即可进行WebSocket握手。之后的所有消息都在该套接字上交换。
HTTP传输需要更多请求。例如,Ajax / XHR流依赖于一个长期运行的服务器到客户端消息请求以及针对客户端到服务器消息的额外HTTP POST请求。长轮询类似,只是它在每个服务器到客户端发送后结束当前请求。
sockjs增加了最小的消息帧。例如,服务器最初发送字母O(“open”帧),消息以[“message1”、“message2”](json编码数组)、字母H(“heartbeat”帧(如果25秒内没有消息流)和字母C(“close”帧)的形式发送,以关闭会话。
要了解更多信息,请在浏览器中运行示例并观察HTTP请求。SockJS客户端允许修复传输列表,因此可以一次查看每个传输。SockJS客户端还提供调试标志,该标志在浏览器控制台中启用有用的消息。在服务器端,您可以启用 TRACE日志记录org.springframework.web.socket。
您可以通过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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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>
前面的示例用于SpringMVC应用程序,应该包含在DispatcherServlet的配置中。然而,Spring的WebSocket和SockJS支持并不依赖于SpringMVC。在sockjshttpRequestHandler的帮助下,集成到其他HTTP服务环境相对简单。
在浏览器方面,应用程序可以使用sockjs客户机(版本1.0.x)。它模拟W3CWebSocketAPI并与服务器通信,以根据运行它的浏览器选择最佳传输选项。请参阅sockjs客户机页面和浏览器支持的传输类型列表。客户机还提供几个配置选项-,例如,指定要包括哪些传输。
Internet Explorer 8和9仍在使用中。他们是拥有SockJS的关键原因。本节介绍在这些浏览器中运行的重要注意事项。
SockJS客户端使用Microsoft支持IE 8和9中的Ajax / XHR流 XDomainRequest。这适用于域,但不支持发送cookie。Cookie通常对Java应用程序至关重要。但是,由于SockJS客户端可以与许多服务器类型(不仅仅是Java)一起使用,因此需要知道cookie是否重要。如果是这样,SockJS客户端更喜欢Ajax / XHR进行流式传输。否则,它依赖于基于iframe的技术。
从SockJS客户端的第一个/info请求是针对可能影响客户的传输选择信息的请求。其中一个细节是服务器应用程序是否依赖于cookie(例如,用于身份验证或使用粘性会话进行群集)。Spring的SockJS支持包括一个名为sessionCookieNeeded的属性。它默认启用,因为大多数Java应用程序都依赖于JSESSIONID cookie。如果您的应用程序不需要它,您可以关闭此选项,然后SockJS客户端应该在IE 8和9中选择xdr-streaming。
如果你使用基于iframe的运输,记住,浏览器可以指示通过设置HTTP响应标题X-Frame-Options to DENY, SAMEORIGIN, or ALLOW-FROM
Spring Security 3.2+支持设置X-Frame-Options每个响应。默认情况下,Spring Security Java配置将其设置为DENY。在3.2中,Spring Security XML命名空间默认情况下不设置该标头,但可以配置为执行此操作。将来,它可以默认设置它。
如果您的应用程序添加X-Frame-Options响应标头(应该!)并依赖于基于iframe的传输,则需要将标头值设置为 SAMEORIGIN或ALLOW-FROM 。Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的。默认情况下,iframe设置为从CDN位置下载SockJS客户端。配置此选项以使用与应用程序相同的源的URL是个好主意。
以下示例显示了如何在Java配置中执行此操作:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@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客户端页面。
SockJS协议要求服务器发送心跳消息以阻止代理断定连接已挂起。Spring SockJS配置有一个名为heartbeatTime的属性,可用于自定义频率。默认情况下,假设在该连接上没有发送其他消息,则会在25秒后发送心跳。这个25秒的值符合以下 IETF对公共互联网应用的建议。
当通过WebSocket和SockJS使用STOMP时,如果STOMP客户端和服务器协商要交换的心跳,则禁用SockJS心跳。
Spring SockJS支持还允许您配置TaskScheduler计划心跳任务。任务计划程序由线程池支持,默认设置基于可用处理器的数量。您应该考虑根据您的特定需求自定义设置。
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)中记录最小消息。如果需要查看堆栈跟踪,可以将该日志类别设置为TRACE。
如果您允许跨源请求(请参阅允许的起源),则SockJS协议使用CORS在XHR流和轮询传输中进行跨域支持。因此,除非检测到响应中存在CORS头,否则会自动添加CORS头。因此,如果应用程序已配置为提供CORS支持(例如,通过Servlet过滤器),则Spring的SockJsService会跳过此部分。
也可以通过suppressCors在Spring的SockJsService中设置属性来禁用这些CORS头 的添加。
SockJS需要以下标头和值:
对于确切的实现,看到addCorsHeaders的AbstractSockJsService和TransportType在源代码中枚举。
或者,如果CORS配置允许,请考虑使用SockJS端点前缀排除URL,从而让Spring SockJsService处理它。
Spring提供了一个SockJS Java客户端,无需使用浏览器即可连接到远程SockJS端点。当需要通过公共网络在两个服务器之间进行双向通信时(即,网络代理可以排除使用WebSocket协议的情况),这尤其有用。SockJS Java客户端对于测试目的也非常有用(例如,模拟大量并发用户)。
SockJS Java客户端支持websocket,xhr-streaming以及xhr-polling 运输。其余的仅适用于浏览器。
您可以配置WebSocketTransport:
根据定义,XHR传输既支持XHR流,也支持XHR轮询,因为从客户机的角度来看,除了用于连接到服务器的URL之外,没有其他区别。目前有两种实现方式:
以下示例显示如何创建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()
//将streamBytesLimit属性设置为512KB(默认值为128KB - 128 * 1024)。
.setStreamBytesLimit(512 * 1024)
//将httpMessageCacheSize属性设置为1,000(默认值为100)。
.setHttpMessageCacheSize(1000)
// 将disconnectDelay属性设置为30属性秒(默认值为五秒 - 5 * 1000)。
.setDisconnectDelay(30 * 1000);
}
// ...
}
WebSocket协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,供客户端和服务器协商子协议(即更高级别的消息传递协议),以便在WebSocket上使用,以定义每种消息可以发送的消息类型,格式是什么,内容每条消息,等等。子协议的使用是可选的,但无论如何,客户端和服务器需要就定义消息内容的某些协议达成一致。
STOMP(简单文本导向的消息传递协议)最初是为脚本语言(如Ruby,Python和Perl)创建的,用于连接企业消息代理。它旨在解决常用消息传递模式的最小子集。STOMP可用于任何可靠的双向流网络协议,例如TCP和WebSocket。虽然STOMP是面向文本的协议,但消息有效负载可以是文本或二进制。
STOMP是一种基于帧的协议,其帧在HTTP上建模。以下清单显示了STOMP框架的结构:
COMMAND
header1:value1
header2:value2
Body^@
客户机可以使用send或subscribe命令发送或订阅消息,以及一个描述消息是关于什么以及应该由谁接收消息的目标头。这将启用一个简单的发布订阅机制,您可以使用该机制将消息通过代理发送到其他连接的客户机,或者将消息发送到服务器以请求执行某些工作。
当您使用Spring的stomp支持时,SpringWebSocket应用程序充当客户机的stomp代理。消息被路由到@controller消息处理方法,或者路由到一个简单的内存中代理,该代理跟踪订阅并向订阅的用户广播消息。还可以将Spring配置为使用专用的stomp代理(例如rabbitmq、activemq和其他代理)来实际广播消息。在这种情况下,Spring维护到代理的TCP连接,将消息传递给代理,并将消息从代理传递到连接的WebSocket客户机。因此,SpringWeb应用程序可以依赖统一的基于HTTP的安全性、通用验证和熟悉的消息处理编程模型。
下面的示例显示了订阅接收股票报价的客户端,服务器可能会定期发出这些报价(例如,通过一个计划任务,该任务通过简单消息模板向代理发送消息):
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}^@
执行之后,服务器可以向客户机广播交易确认消息和详细信息。
目的地(destination)的含义在stomp规范中被有意地保留为不透明的。它可以是任何字符串,并且完全由stomp服务器来定义它们支持的目的地的语义和语法。但是,对于目的地来说,在/topic/处是类似路径的字符串是很常见的。表示发布订阅(一对多)和/queue/表示点对点(一对一)消息交换。
stomp服务器可以使用message命令向所有订户广播消息。以下示例显示服务器向订阅的客户机发送股票报价:
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
服务器无法发送未经请求的消息。来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的订阅ID头必须与客户端订阅的ID头匹配。
上述概述旨在提供对STOMP协议最基本的理解。我们建议全面审查协议规范。
优点
使用STOMP作为子协议,Spring Framework和Spring Security提供了比使用原始WebSocket更丰富的编程模型。关于HTTP与原始TCP以及它如何让Spring MVC和其他Web框架提供丰富的功能,可以做出同样的观点。以下是一系列好处:
有关WebSocket支持的STOMP可在spring-messaging和 spring-websocket模块中使用。一旦有了这些依赖项,就可以通过带有SockJS Fallback的 WebSocket公开STOMP端点,如下例所示:
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) {
///portfolio 是WebSocket(或SockJS)客户端为WebSocket握手需要连接的端点的HTTP URL。
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
//目标头开头的STOMP消息/app将路由到 类中的@MessageMapping方法@Controller。
config.setApplicationDestinationPrefixes("/app");
//使用内置的消息代理进行订阅和广播,并将目标头以/topic`或`/queue开头的消息路由到代理。
config.enableSimpleBroker("/topic", "/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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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前缀没有任何特殊含义。它们仅仅是区分pub-sub和点对点消息传递的惯例(即,许多订阅者与一个消费者)。使用外部代理时,请检查代理的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标头。即使它确实如此,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的详细信息,请参阅 连接到代理和 身份验证。
有关更多示例代码,请参阅
要配置底层WebSocket服务器,将应用服务器配置中的信息。但是,对于Jetty,您需要通过StompendpointRegistry设置握手管理器和WebSocket策略:
@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)));
}
}
一旦暴露了STOMP端点,Spring应用程序就成为连接客户端的STOMP代理。本节介绍服务器端的消息流。
spring-messaging模块包含对源自Spring Integration的消息传递应用程序的基础支持,后来被提取并整合到Spring Framework中,以便在许多Spring项目和应用程序场景中得到更广泛的使用 。以下列表简要介绍了一些可用的消息传递抽象:
Java配置(即@EnableWebSocketMessageBroker)和XML命名空间配置(即
上图显示了三个消息通道:
下图显示了配置外部代理(例如RabbitMQ)以管理订阅和广播消息时使用的组件:
前两个图之间的主要区别在于使用“代理中继”通过TCP将消息传递到外部STOMP代理,以及将消息从代理传递到订阅客户端。
当从WebSocket连接接收消息时,它们被解码为STOMP帧,变成Spring Message表示,并发送到 clientInboundChannel进一步处理。例如,STOMP消息其目的地标头开始与/app可被路由到@MessageMapping在注释的控制器的方法,而/topic和/queue消息可以被直接路由到消息代理。
@Controller处理来自客户端的STOMP消息的带注释的消息可以通过消息代理向消息代理发送消息,并且代理通过消息将消息brokerChannel广播给匹配的订阅者clientOutboundChannel。相同的控制器也可以响应HTTP请求执行相同的操作,因此客户端可以执行HTTP POST,然后@PostMapping方法可以向消息代理发送消息以向订阅的客户端广播。
我们可以通过一个简单的例子来追踪流程。请考虑以下示例,该示例设置服务器:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting") {
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}
上面的示例支持以下流程:
应用程序可以使用带注释的@Controller类来处理来自客户端的消息。这些类可以声明@MessageMapping,@SubscribeMapping和@ExceptionHandler 方法,如以下主题中所述:
您可以使用@MessageMapping注释根据目标路由消息的方法。它在方法级别和类型级别受支持。在类型级别,@MessageMapping用于表示控制器中所有方法的共享映射。
默认情况下,映射值是Ant样式的路径模式(例如/thing*,/thing/**),包括对模板变量的支持(例如,/thing/{id})。可以通过@DestinationVariable方法参数引用这些值 。应用程序还可以切换到以点为单位的映射目标约定,如Dots as Separators中所述。
支持的方法参数
下表描述了方法参数:
方法论证 | 描述 |
---|---|
Message | 用于访问完整的消息。 |
MessageHeaders | 用于访问内部的标头Message。 |
MessageHeaderAccessor,SimpMessageHeaderAccessor和StompHeaderAccessor | 用于通过类型化访问器方法访问标头。 |
@Payload | 用于访问消息的有效负载,由已配置的转换(例如,从JSON) MessageConverter。 -----不需要存在此注释,因为默认情况下,假设没有其他参数匹配。 —您可以使用@javax.validation.Valid或Spring 注释有效负载参数@Validated,以自动验证有效负载参数。 |
@Header | 如有必要,可使用org.springframework.core.convert.converter.converter访问特定的头值-以及类型转换。 |
@Headers | 用于访问消息中的所有标头。此参数必须可分配给 java.util.Map。 |
@DestinationVariable | 用于访问从消息目标中提取的模板变量。根据需要将值转换为声明的方法参数类型。 |
java.security.Principal | 反映WebSocket HTTP握手时登录的用户。 |
返回值
默认情况下,@messagemapping方法的返回值通过匹配的messageconverter序列化为有效负载,并作为消息发送到brokerchannel,从中向订阅服务器广播。出站消息的目标与入站消息的目标相同,但前缀为/topic。
可以使用@sendto和@sendtouser注释自定义输出消息的目标。@sendto用于自定义目标目的地或指定多个目的地。@sendtouser用于将输出消息定向到与输入消息关联的用户。请参见用户目的地。
您可以在同一方法上同时使用@sendto和@sendtouser,这两种方法在类级别都受支持,在这种情况下,它们将作为类中方法的默认值。但是,请记住,任何方法级别的@sendto或@sendtouser注释都会覆盖类级别上的任何此类注释。
消息可以异步处理,@messagemapping方法可以返回listenablefuture、completeablefuture或completionstage。
请注意,@send to和@sendtouser只是一种便利,相当于使用simpmessagingtemplate发送消息。如果需要,对于更高级的方案,@messagemapping方法可以直接使用simpmessagingtemplate。可以这样做,而不是返回值,或者可能是返回值的附加值。请参见发送消息。
@SubscribeMapping类似于@MessageMapping但仅将映射缩小到订阅消息。它支持相同的 方法参数的@MessageMapping。但是对于返回值,默认情况下,消息将直接发送到客户端(通过 clientOutboundChannel响应订阅)而不是发送给代理(通过 brokerChannel作为匹配订阅的广播)。添加@SendTo或 @SendToUser覆盖此行为并发送给代理。
什么时候有用?假设代理映射到/topic和/queue,而应用程序控制器映射到/app。在这个设置中,代理存储了对/topic和/queue的所有订阅,这些订阅用于重复广播,并且不需要应用程序参与其中。客户机也可以订阅某些/app目的地,而控制器可以返回响应该订阅的值,而无需与代理联系,而无需再次存储或使用订阅(实际上是一次性请求-回复交换)。这方面的一个用例是在启动时用初始数据填充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...
});
服务器端选项是在brokerchannel上注册ExecutorChannelInterceptor,并实现在处理消息(包括订阅)之后调用的AfterMessageHandled方法。
应用程序可以使用@messageexceptionhandler方法处理来自@messagemapping方法的异常。如果要访问异常实例,可以在注释本身或通过方法参数声明异常。以下示例通过方法参数声明异常:
@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}
@MessageExceptionHandler方法支持灵活的方法签名,并支持相同的方法参数类型和返回值作为@MessageMapping方法。
通常,@messageexceptionhandler方法应用于声明它们的@controller类(或类层次结构)中。如果希望这些方法在全局范围内(跨控制器)应用,可以在用@controlleradvice标记的类中声明它们。这与SpringMVC中提供的类似支持类似。
如果您想从应用程序的任何部分向连接的客户机发送消息怎么办?任何应用程序组件都可以向brokerchannel发送消息。最简单的方法是注入一个简单的消息模板并使用它发送消息。通常,您将按类型注入它,如下示例所示:
@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}
但是,如果存在相同类型的另一个bean,也可以通过其名称(brokerMessagingTemplate)对其进行限定。
内置的简单消息代理处理来自客户机的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的已连接客户机。代理支持类似路径的目的地,包括对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);
// ...
}
}
简单代理非常适合入门,但只支持stomp命令的一个子集(它不支持ack、receipts和其他一些功能),它依赖于简单的消息发送循环,不适合集群。作为替代方案,您可以升级应用程序以使用功能齐全的消息代理。
请参阅所选消息代理的stomp文档(如rabbitmq、activemq和其他),安装代理,并在启用stomp支持的情况下运行它。然后,您可以在Spring配置中启用stomp代理中继(而不是简单的代理)。
下面的示例配置启用了全功能代理:
@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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
websocket:message-broker>
beans>
前面配置中的stomp代理中继是一个Spring消息处理程序,它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的TCP连接,将所有消息转发给它,然后通过WebSocket会话将从代理接收到的所有消息转发给客户机。本质上,它充当一个“中继”,在两个方向上转发消息。
添加io.projectreactor.netty:reactor-netty和io.netty:netty-all所有依赖于项目的TCP连接管理。
此外,应用程序组件(例如HTTP请求处理方法,业务服务等)也可以向代理中继发送消息,如发送消息中所述,以向订阅的WebSocket客户端广播消息。
实际上,代理中继实现了健壮且可扩展的消息广播。
stomp代理中继维护到代理的单个“系统”TCP连接。此连接仅用于来自服务器端应用程序的消息,不用于接收消息。您可以为此连接配置stomp凭据(即stomp帧登录和密码头)。这在XML命名空间和Java配置中都暴露为systemLogin 和systemPasscode 属性,其值为guest 和guest的默认值。
STOMP代理中继还为每个连接的WebSocket客户端创建单独的TCP连接。您可以配置用于代表客户端创建的所有TCP连接的STOMP凭据。这在XML命名空间和Java配置中都公开为clientLogin and `clientPasscode默认值为的属性guest`and `guest。
stomp-broker中继总是在每个连接帧上设置登录头和密码头,这些连接帧代表客户端转发给代理。因此,WebSocket客户机不需要设置这些头。他们被忽视了。正如身份验证部分所解释的,WebSocket客户端应该依赖HTTP身份验证来保护WebSocket端点并建立客户端标识。
STOMP代理中继还通过“系统”TCP连接向消息代理发送和接收心跳。您可以配置发送和接收心跳的间隔(默认情况下每个10秒)。如果与代理的连接丢失,代理中继将继续尝试每5秒重新连接一次,直到成功为止。
任何Spring bean都可以实现ApplicationListener
默认情况下,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属性配置stomp代理中继。此属性的值被设置为每个连接帧的主机头,并且非常有用(例如,在建立TCP连接的实际主机与提供基于云的stomp服务的主机不同的云环境中)。
当消息路由到@MessageMapping方法时,它们会匹配 AntPathMatcher。默认情况下,模式应使用slash(/)作为分隔符。这是Web应用程序中的一个很好的约定,类似于HTTP URL。但是,如果您更习惯于消息传递约定,则可以切换到使用dot(.)作为分隔符。
以下示例显示了如何在Java配置中执行此操作:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}
以下示例显示了与前面示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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>
之后,控制器可以使用dot(.)作为@MessageMapping方法中的分隔符,如下例所示:
@Controller
@MessageMapping("red")
public class RedController {
@MessageMapping("blue.{green}")
public void handleGreen(@DestinationVariable String green) {
// ...
}
}
客户端现在可以向其发送消息/app/red.blue.green123。
在前面的示例中,我们没有更改“代理中继”上的前缀,因为它们完全依赖于外部消息代理。请参阅您使用的代理的STOMP文档页面,以查看它为目标标头支持的约定。
另一方面,“简单代理”确实依赖于已配置的PathMatcher,因此,如果您切换分隔符,则该更改也适用于代理以及代理将目标从消息与订阅中的模式匹配的方式。
WebSocket消息传递会话中的每个STOMP都以HTTP请求开头。这可以是升级到WebSockets的请求(即WebSocket握手),或者在SockJS回退的情况下,是一系列SockJS HTTP传输请求。
许多Web应用程序已经具有用于保护HTTP请求的身份验证和授权。通常,通过使用某些机制(如登录页面,HTTP基本身份验证或其他方式)通过Spring Security对用户进行身份验证。经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一个基于cookie的会话中的后续请求相关联。
因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经有可通过身份验证的用户访问 HttpServletRequest#getUserPrincipal()。Spring自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后通过用户头与该会话上传输的所有STOMP消息相关联。
简而言之,典型的Web应用程序除了已经为安全性做的事情之外,不需要做任何事情。用户在HTTP请求级别进行身份验证,其安全上下文通过基于cookie的HTTP会话(然后与为该用户创建的WebSocket或SockJS会话相关联)进行维护,并导致每次Message流经时都标记用户标头应用程序。
请注意,stomp协议在connect框架上确实有login和passcode头。它们最初是为TCP上的Stomp而设计的,现在仍然需要。但是,对于Stomp over WebSocket,默认情况下,Spring忽略Stomp协议级别的授权头,假定用户已经在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。
Spring Security提供WebSocket子协议授权,使用ChannelInterceptor根据消息中的用户头对消息进行授权。另外,Spring会话提供了WebSocket集成,确保用户HTTP会话在WebSocket会话仍然处于活动状态时不会过期。
Spring Security OAuth 支持基于令牌的安全性,包括JSON Web令牌(JWT)。您可以将其用作Web应用程序中的身份验证机制,包括STOMP over WebSocket交互,如上一节所述(即通过基于cookie的会话维护身份)。
同时,基于cookie的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中,或者在通常使用标头进行身份验证的移动应用程序中)。
WebSocket协议RFC 6455“没有规定服务器在WebSocket握手期间对客户端进行身份验证的任何特定方式。”但是,在实践中,浏览器客户端只能使用标准身份验证头(即基本HTTP身份验证)或cookie,并且不能(例如)提供自定义头。同样,sockjs javascript客户机不提供使用sockjs传输请求发送HTTP头的方法。参见SOCKJS客户端问题196。相反,它允许发送查询参数,您可以使用这些参数发送令牌,但这有其自身的缺点(例如,令牌可能会在服务器日志中不经意地用URL记录)。
上述限制适用于基于浏览器的客户端,不适用于基于Spring Java的STOMP客户端,它支持使用WebSocket和SockJS请求发送标头。
因此,希望避免使用cookie的应用程序可能没有任何良好的HTTP协议级别的身份验证替代方案。他们可能更喜欢在STOMP消息传递协议级别使用标头进行身份验证而不是使用cookie。这样做需要两个简单的步骤:
下一个示例使用服务器端配置注册自定义身份验证拦截器。注意,拦截器只需要对连接消息进行身份验证和设置用户头。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的授权时,目前需要确保认证通道拦截器配置的顺序早于Spring Security的顺序。最好通过在WebSocketMessageBrokerConfigurer标记 自己的实现中声明自定义拦截器来完成@Order(Ordered.HIGHEST_PRECEDENCE + 99)。
应用程序可以发送以特定用户为目标的消息,Spring的stomp支持可以识别为此目的前缀为/user/的目的地。例如,客户机可以订阅/user/queue/position updates目标。此目标由userdestinationmessagehandler处理,并转换为用户会话唯一的目标(例如/queue/position-updates-user123)。这提供了订阅一般命名目的地的便利,同时确保与订阅同一目的地的其他用户没有冲突,以便每个用户可以接收唯一的库存位置更新。
在发送端,消息可以发送到目的地,如/user/username/queue/位置更新,然后由userdestinationmessagehandler转换为一个或多个目的地,每个与用户关联的会话对应一个目的地。这允许应用程序中的任何组件发送以特定用户为目标的消息,而不必知道他们的名称和通用目标以外的任何信息。这也可以通过注释和消息模板来支持。
消息处理方法可以将消息发送给与通过@send to user注释处理的消息相关联的用户(在类级别上也受支持以共享公共目标),如下示例所示:
@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完全相同(即,仅针对发送正在处理的消息的会话)。
例如,可以通过注入由Java配置或XML命名空间创建的SimpMessagingTemplate ,来向任何应用程序组件发送用户目的地消息。(如果使用@qualifier进行限定需要bean名称为“brokerMessagingTemplate”)下面的示例演示了如何执行此操作:
@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(
trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}
将用户目的地与外部消息代理一起使用时,应检查代理文档中有关如何管理非活动队列的信息,以便在用户会话结束时删除所有唯一的用户队列。例如,rabbitmq在使用/exchange/amq.direct/position-updates等目标时创建自动删除队列。因此,在这种情况下,客户机可以订阅/user/exchange/amq.direct/position-updates。同样,ActiveMQ具有清除非活动目标的配置选项。
在多应用程序服务器方案中,由于用户连接到其他服务器,因此用户目标可能保持未解析状态。在这种情况下,您可以配置一个目的地来广播未解析的消息,以便其他服务器有机会尝试。这可以通过Java配置中的MessageBrokerRegistry 的userDestinationBroadcast 属性和XML中的消息代理元素的用户目的地广播属性来实现。
消息顺序
来自代理的消息将发布到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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker preserve-publish-order="true">
websocket:message-broker>
beans>
设置标志后,同一客户端会话中的消息将一次一条地发布到ClientOutboundChannel,从而保证发布的顺序。请注意,这会带来很小的性能开销,因此只有在需要时才应启用它。
ApplicationContext发布了几个事件,可以通过实现Spring的ApplicationListener接口来接收:
当您使用功能齐全的代理时,如果代理暂时不可用,STOMP“代理中继”会自动重新连接“系统”连接。但是,客户端连接不会自动重新连接。假设启用了心跳,客户端通常会注意到代理在10秒内没有响应。客户端需要实现自己的重新连接逻辑。
事件为stomp连接的生命周期提供通知,但不为每个客户机消息提供通知。应用程序还可以注册一个通道拦截器来拦截任何消息和处理链的任何部分。以下示例显示如何截取来自客户端的入站消息:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new MyChannelInterceptor());
}
}
自定义通道拦截器可以使用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在订阅来自通道的消息的每个消息处理程序的线程中提供钩子。
注意,与前面描述的SesionDisconnectEvent 一样,断开连接的消息可以来自客户端,也可以在WebSocket会话关闭时自动生成。在某些情况下,拦截器可能会为每个会话多次截取此消息。对于多个断开连接事件,组件应该是等幂的。
Spring通过WebSocket客户端提供STOMP,通过TCP客户端提供STOMP。
首先,您可以创建和配置WebSocketStompClient,如以下示例所示:
WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats
在前面的示例中,您可以用sockjsclient替换StandardWebSocketClient,因为这也是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方法需要订阅消息的处理程序,并返回可用于取消订阅的订阅句柄。对于每个接收到的消息,处理程序可以指定负载应反序列化到的目标对象类型,如下示例所示:
session.subscribe("/topic/something", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});
要启用Stomp Heartbeat,可以使用任务调度程序配置WebSocketStompClient,还可以自定义Heartbeat间隔(写入不活动,这会导致心跳10秒发送,和10秒不活动读取,该关闭连接)。
当您使用WebSocketStompClient性能测试来模拟来自同一台计算机的数千个客户端时,请考虑关闭心跳,因为每个连接都会调度自己的心跳任务,而不是针对在同一台计算机上运行的大量客户端进行优化。
stomp协议还支持收据,其中客户机必须添加一个收据头,服务器在处理发送或订阅之后用收据帧对其作出响应。为了支持这一点,stompsession提供了setautoreceipt(布尔值),它会在随后的每个发送或订阅事件上添加一个收据头。或者,您也可以手动将收据头添加到stompheaders。send和subscribe都返回一个receiptable实例,您可以使用该实例注册接收成功和失败的回调。对于此功能,您必须使用TaskScheduler配置客户端,并配置接收过期前的时间量(默认为15秒)。
请注意,stompsessionhandler本身是一个stompframehandler,它除了处理消息异常的handleException回调之外,还可以处理错误帧,并且处理传输级别错误(包括ConnectionLostException)的handleTransportError 。
每个WebSocket会话都有一个属性映射。映射作为标头附加到入站客户端消息,可以从控制器方法访问,如以下示例所示:
@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map<String, Object> attrs = headerAccessor.getSessionAttributes();
// ...
}
}
您可以在WebSocket范围内声明一个Spring管理的bean。您可以将WebSocket范围的bean注入控制器和在clientinboundchannel上注册的任何通道拦截器。它们通常是单件的,比任何单个WebSocket会话都要长。因此,需要为WebSocket范围的bean使用范围代理模式,如下示例所示:
@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@Controller
public class MyController {
private final MyBean myBean;
@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}
与任何自定义范围一样,Spring在第一次从控制器访问新的MyBean实例时对其进行初始化,并将该实例存储在WebSocket会话属性中。随后返回相同的实例,直到会话结束。WebSocket范围的bean调用了所有Spring生命周期方法,如前面的示例所示。
在性能方面没有银弹。许多因素会影响它,包括消息的大小和数量,应用程序方法是否执行需要阻塞的工作,以及外部因素(如网络速度和其他问题)。本部分的目标是提供可用配置选项的概述以及有关如何推理扩展的一些想法。
在消息传递应用程序中,消息通过通道进行传递,以进行由线程池支持的异步执行。配置此类应用程序需要充分了解通道和消息流。因此,建议查看消息流。
最明显的开始是配置支持clientInboundChannel和clientOutboundChannel的线程池。默认情况下,两个处理器的配置都是可用处理器数量的两倍。
如果注释方法中的消息处理主要是CPU绑定的,则clientInboundChannel应该保持接近处理器数量的线程数。如果他们所做的工作更多地受IO限制并且需要阻塞或等待数据库或其他外部系统,则可能需要增加线程池大小。
ThreadPoolExecutor 有三个重要的属性:核心线程池大小,最大线程池大小,以及队列存储没有可用线程的任务的容量。
常见的混淆点是配置核心池大小(例如,10)和最大池大小(例如,20)会导致线程池具有10到20个线程。实际上,如果容量保留为其默认值Integer.MAX_VALUE,则线程池永远不会超出核心池大小,因为所有其他任务都会排队。
请参阅javadoc ThreadPoolExecutor以了解这些属性如何工作并了解各种排队策略。
在ClientOutboundChannel方面,这一切都是为了向WebSocket客户机发送消息。如果客户机在一个快速网络上,那么线程的数量应该保持接近可用处理器的数量。如果它们速度慢或带宽低,则需要更长的时间来消耗消息并给线程池带来负担。因此,增加线程池的大小是必要的。
虽然clientinboundchannel的工作负载可以预测-毕竟,它是基于应用程序所做的-如何配置“clientoutboundchannel”比较困难,因为它是基于应用程序无法控制的因素。因此,还有两个与消息发送相关的属性:sendTimeLimit和sendBufferSizeLimit。您可以使用这些方法来配置允许发送多长时间,以及在向客户机发送消息时可以缓冲多少数据。
一般的想法是,在任何给定的时间,只有一个线程可以用于发送到客户机。同时,所有附加的消息都会得到缓冲,您可以使用这些属性来决定允许发送消息的时间以及在此期间可以缓冲多少数据。有关重要的其他详细信息,请参阅XML模式的javadoc和文档。
以下示例显示了可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
}
// ...
}
以下示例显示了与前面示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://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 和其他客户端)将更大的STOMP消息拆分为16K边界,并将它们作为多个WebSocket消息发送,这需要服务器缓冲和重新组装。
Spring的STOMP-over-WebSocket支持实现了这一点,因此应用程序可以配置STOMP消息的最大大小,而不管WebSocket服务器特定的消息大小。请记住,如有必要,WebSocket消息大小会自动调整,以确保它们至少可以携带16K WebSocket消息。
以下示例显示了一种可能的配置:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.setMessageSizeLimit(128 * 1024);
}
// ...
}
以下示例显示了与前面示例等效的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
http://www.springframework.org/schema/websocket/spring-websocket.xsd">
<websocket:message-broker>
<websocket:transport message-size="131072" />
websocket:message-broker>
beans>
扩展的一个重要点是使用多个应用程序实例。目前,您不能用简单的代理来实现这一点。但是,当您使用全功能代理(如rabbitmq)时,每个应用程序实例都连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的WebSocket客户端。
当您使用@enableWebSocketMessageBroker或
指示当前有多少客户端会话,其中计数通过WebSocket与HTTP流式传输和轮询SockJS会话进一步细分。
表示已建立的会话总数。
处理的CONNECT,CONNECTED和DISCONNECT帧总数,表示STOMP级别连接的客户端数量。请注意,当会话异常关闭或客户端关闭而不发送DISCONNECT帧时,DISCONNECT计数可能会更低。
指示代表客户端WebSocket会话建立到代理的TCP连接数。这应该等于客户端WebSocket会话的数量+ 1个用于从应用程序内发送消息的额外共享“系统”连接。
代表客户端转发到代理或从代理接收的CONNECT,CONNECTED和DISCONNECT帧的总数。请注意,无论客户端WebSocket会话如何关闭,都会将DISCONNECT帧发送到代理。因此,较低的DISCONNECT帧计数表示代理主动关闭连接(可能是因为没有及时到达的心跳,无效的输入帧或其他问题)。
来自支持它的线程池的统计信息clientInboundChannel 提供了对传入消息处理的健康状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果存在I / O绑定任务(例如,慢速数据库查询,对第三方REST API的HTTP请求等),请考虑增加线程池大小。
支持线程池的统计信息clientOutboundChannel 可以深入了解向客户端广播消息的运行状况。在此排队的任务表明客户端消耗消息的速度太慢。解决此问题的一种方法是增加线程池大小以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(参见上一节)。
来自用于发送心跳的SockJS任务调度程序的线程池的统计信息。请注意,在STOMP级别协商心跳时,将禁用SockJS心跳。
当您使用Spring的STOMP-over-WebSocket支持时,有两种主要的方法来测试应用程序。第一种是编写服务器端测试来验证控制器的功能及其带注释的消息处理方法。第二种是编写涉及运行客户端和服务器的完整端到端测试。
这两种方法并不相互排斥。相反,每个人都在整体测试策略中占有一席之地。服务器端测试更集中,更易于编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。
最简单的服务器端测试形式是编写控制器单元测试。但是,这还不够用,因为控制器的大部分功能取决于其注释。纯单元测试根本无法测试。
理想情况下,测试中的控制器应该在运行时调用,就像测试使用Spring MVC测试框架处理HTTP请求的控制器一样 - 也就是说,不运行Servlet容器,而是依赖Spring Framework来调用带注释的控制器。与Spring MVC Test一样,这里有两个可能的替代方案,使用“基于上下文”或使用“独立”设置:
在Spring TestContext框架的帮助下加载实际的Spring配置,clientInboundChannel作为测试字段注入,并使用它来发送由控制器方法处理的消息。
手动设置调用控制器(即SimpAnnotationMethodMessageHandler)所需的最小Spring框架基础结构,并将控制器的消息直接传递给它。
这两种设置方案都在股票投资组合 示例应用程序的测试中得到了证明 。
第二种方法是创建端到端集成测试。为此,您需要以嵌入模式运行WebSocket服务器并将其作为WebSocket客户端连接到该服务器,该客户端发送包含STOMP帧的WebSocket消息。股票投资组合 示例应用程序的测试还通过使用Tomcat作为嵌入式WebSocket服务器和用于测试目的的简单STOMP客户端来演示此方法。
本章详细介绍了Spring与第三方Web框架的集成。
Spring框架的核心价值主张之一是实现 选择。从一般意义上讲,Spring并不强迫您使用或购买任何特定的体系结构,技术或方法(尽管它肯定会推荐一些其他的)。这种选择与开发人员及其开发团队最相关的架构,技术或方法的自由在Web领域最为明显,其中Spring提供了自己的Web框架(Spring MVC),同时,提供与许多流行的第三方Web框架的集成。
在深入研究每个受支持的Web框架的集成细节之前,让我们先看看Spring配置,它不是特定于任何一个Web框架的。(本节同样适用于Spring自己的Web框架SpringMVC。)
Spring的轻量级应用程序模型支持的一个概念(为了更好的表达)是分层体系结构。请记住,在“经典”分层体系结构中,Web层只是众多层中的一个。它是服务器端应用程序的入口点之一,它委托给服务层中定义的服务对象(外观),以满足特定于业务(和表示技术不可知)的用例。在Spring中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象都存在于不同的“业务上下文”中,其中不包含Web或表示层对象(表示对象,例如Spring MVC控制器,通常配置在不同的“表示上下文”中)。本节详细介绍如何配置包含应用程序中所有“业务bean”的Spring容器(WebApplicationContext)。
转到具体细节上,您需要做的就是在Web应用程序的标准JavaEE Servlet Web.xml文件中声明一个ContextLoaderListener,并添加一个contextConfigLocation
考虑以下
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
进一步考虑以下
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/applicationContext*.xmlparam-value>
context-param>
如果不指定ContextConfigLocation上下文参数,ContextLoaderListener将查找名为/WEB-INF/ApplicationContext.xml的文件进行加载。加载上下文文件后,Spring根据bean定义创建一个WebApplicationContext对象,并将其存储在Web应用程序的servletContext中。
所有Java Web框架都是建立在servlet API之上的,因此您可以使用下面的代码片段来访问ContextLoaderListener创建的“业务上下文”Apple上下文。
以下示例显示如何获取WebApplicationContext:
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
WebApplicationContextUtils类是为了方便起见,因此不需要记住servletContext属性的名称。如果WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE键下不存在对象,则其getWebApplicationContext()方法返回空值。与其冒险在应用程序中获取NullPointerException,不如使用getRequiredWebApplicationContext()方法。此方法在缺少ApplicationContext时引发异常。
一旦引用了WebApplicationContext,就可以通过bean的名称或类型来检索它们。大多数开发人员按名称检索bean,然后将其强制转换到实现的接口之一。
幸运的是,本节中的大多数框架都有更简单的方法来查找bean。它们不仅使从Spring容器中获取bean变得容易,而且还允许您在控制器上使用依赖注入。每个Web框架部分都有关于其特定集成策略的更多详细信息。
JavaServer Faces(JSF)是JCP标准的基于组件,事件驱动的Web用户界面框架。从Java EE 5开始,它是Java EE保护伞的官方部分。
对于流行的JSF运行时以及流行的JSF组件库,请查看 Apache MyFaces项目。MyFaces项目还提供了常见的JSF扩展,例如MyFaces Orchestra (一种基于Spring的JSF扩展,提供丰富的会话范围支持)。
Spring Web Flow 2.0通过其新建立的Spring Faces模块提供了丰富的JSF支持,既用于以JSF为中心的用法(如本节所述),也用于以Spring为中心的用法(在Spring MVC调度程序中使用JSF视图)。有关详细信息,请参见 Spring Web Flow网站。
Spring的JSF集成中的关键元素是JSF ELResolver机制。
SpringBeanFacesELResolver是一个JSF 1.2+兼容的ELResolver实现,与JSF 1.2和JSP 2.1使用的标准Unified EL集成。因为 SpringBeanVariableResolver,它首先委托Spring的“业务上下文” WebApplicationContext,然后委托给 底层JSF实现的默认解析器。
在配置方面,您可以SpringBeanFacesELResolver在JSF faces-context.xml文件中定义,如以下示例所示:
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolverel-resolver>
...
application>
faces-config>
在faces-config.xml中将属性映射到bean时,自定义变量解析器工作得很好,但有时可能需要显式地获取bean。FacesContextUtils类使这变得容易。它与WebApplicationContextUtils类似,只是它采用的是FacesContext参数而不是ServletContext参数。
下面的示例演示如何使用facesContextUtils:
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
Struts由Craig McClanahan发明,是一个由Apache Software Foundation主持的开源项目。当时,它大大简化了JSP / Servlet编程范例,并赢得了许多使用专有框架的开发人员。它简化了编程模型,它是开源的(因此在啤酒中是免费的),它有一个庞大的社区,让项目成长并在Java Web开发人员中变得流行。
查看Struts Spring插件,了解Struts附带的内置Spring集成。
Tapestry是一个“”面向组件的框架,用于在Java中创建动态,健壮,高度可扩展的Web应用程序。“
虽然Spring拥有自己强大的Web层,但是通过将Tapestry用于Web用户界面和Spring容器用于较低层,构建企业Java应用程序有许多独特的优势。
有关更多信息,请参阅Tapestry 针对Spring的专用 集成模块。
以下链接涉及有关本章中描述的各种Web框架的更多资源。