Web Servlet

1. Spring Web MVC

Spring Web MVC 是基于 Servlet API 的原始 web 框架,并且从一开始就包含在 Spring 框架中。正式名称“Spring Web MVC”来自其源模块 (spring-webmvc), 但它更常被称为“Spring MVC”。spring-doc.cadn.net.cn

与Spring Web MVC并行,Spring Framework 5.0引入了一个响应式堆栈Web框架 其名称为“Spring WebFlux”,也基于其源模块 (spring-webflux)。 本节介绍Spring Web MVC。下一节 介绍Spring WebFlux。spring-doc.cadn.net.cn

有关基准信息和与Servlet容器及Java EE版本范围的兼容性,请参见Spring Framework 维基spring-doc.cadn.net.cn

1.1. 分发器Servlet

Spring MVC,像许多其他Web框架一样,是围绕前端控制器模式设计的,其中中央Servlet,即DispatcherServlet,为请求处理提供了一个共享算法,而实际工作则由可配置的委托组件完成。这种模型非常灵活,支持多种工作流程。spring-doc.cadn.net.cn

The DispatcherServlet, as any Servlet, needs to be declared and mapped according to the Servlet specification by using Java configuration or in web.xml. In turn, the DispatcherServlet uses Spring configuration to discover the delegate components it needs for request mapping, view resolution, exception handling, 等等.spring-doc.cadn.net.cn

以下Java配置的示例注册并初始化DispatcherServlet,该DispatcherServlet由Servlet容器自动检测(请参见Servlet配置):spring-doc.cadn.net.cn

Java
public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletContext) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}
Kotlin
class MyWebApplicationInitializer : WebApplicationInitializer {

    override fun onStartup(servletContext: ServletContext) {

        // Load Spring web application configuration
        val context = AnnotationConfigWebApplicationContext()
        context.register(AppConfig::class.java)

        // Create and register the DispatcherServlet
        val servlet = DispatcherServlet(context)
        val registration = servletContext.addServlet("app", servlet)
        registration.setLoadOnStartup(1)
        registration.addMapping("/app/*")
    }
}
除了直接使用ServletContext API,你还可以扩展AbstractAnnotationConfigDispatcherServletInitializer并重写特定方法(参见上下文层次结构下的示例)。

以下示例的web.xml配置注册并初始化DispatcherServletspring-doc.cadn.net.cn

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>
Spring Boot 采用了一种不同的初始化顺序。它不依赖于 Servlet 容器的生命周期,而是使用 Spring 配置来启动自身和嵌入式 Servlet 容器。FilterServlet 声明在 Spring 配置中被检测到,并注册到 Servlet 容器中。有关详细信息,请参阅 Spring Boot 文档

1.1.1. 上下文层次结构

DispatcherServlet 期望一个 WebApplicationContext(一个普通 ApplicationContext 的扩展)用于其自身的配置。WebApplicationContext 有一个指向 ServletContext 和与之关联的 Servlet 的链接。它还绑定到 ServletContext,以便应用程序可以使用 RequestContextUtils 上的静态方法来查找 WebApplicationContext,如果它们需要访问它。spring-doc.cadn.net.cn

对于许多应用程序,拥有一个单独的WebApplicationContext是简单且足够的。 也可以有一个上下文层次结构,其中一个根WebApplicationContext 被多个DispatcherServlet(或其他Servlet)实例共享,每个实例都有 自己的子WebApplicationContext配置。 请参见Spring上下文的附加功能 以了解更多关于上下文层次结构的功能。spring-doc.cadn.net.cn

WebApplicationContext 通常包含基础设施 bean,例如数据存储库和需要在多个 Servlet 实例之间共享的业务服务。这些 bean 实际上是继承的,并且可以在特定于 Servlet 的子 WebApplicationContext 中覆盖(即重新声明),该 WebApplicationContext 通常包含给定 Servlet 的本地 bean。 下图显示了这种关系:spring-doc.cadn.net.cn

mvc context hierarchy

以下示例配置了一个WebApplicationContext层级:spring-doc.cadn.net.cn

Java
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/*" };
    }
}
Kotlin
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>> {
        return arrayOf(RootConfig::class.java)
    }

    override fun getServletConfigClasses(): Array<Class<*>> {
        return arrayOf(App1Config::class.java)
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/app1/*")
    }
}
如果不需要应用程序上下文层次结构,应用程序可以通过 getRootConfigClasses()nullgetServletConfigClasses() 返回所有配置。

以下示例显示了 web.xml 的等效内容:spring-doc.cadn.net.cn

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>
如果不需要应用程序上下文层次结构,应用程序可以仅配置一个“根”上下文,并将contextConfigLocation Servlet参数留空。

1.1.2. 特殊的Bean类型

The DispatcherServlet 委托给特殊bean来处理请求并渲染相应的响应。我们所说的“特殊bean”是指实现框架契约的Spring管理的Object实例。这些通常带有内置契约,但您可以自定义它们的属性并扩展或替换它们。spring-doc.cadn.net.cn

以下表格列出了由DispatcherServlet检测到的特殊bean:spring-doc.cadn.net.cn

Beans类型 说明

HandlerMappingspring-doc.cadn.net.cn

将请求映射到处理器,并附带一个拦截器列表,用于预处理和后处理。 映射基于某些标准,具体细节因HandlerMapping实现而异。spring-doc.cadn.net.cn

两个主要的HandlerMapping实现是RequestMappingHandlerMapping (它支持@RequestMapping注解的方法)和SimpleUrlHandlerMapping (它维护URI路径模式到处理器的显式注册)。spring-doc.cadn.net.cn

HandlerAdapterspring-doc.cadn.net.cn

帮助DispatcherServlet调用映射到请求的处理器,无论处理器实际是如何被调用的。例如,调用一个注解控制器需要解析注解。HandlerAdapter的主要目的是屏蔽DispatcherServlet这样的细节。spring-doc.cadn.net.cn

HandlerExceptionResolverspring-doc.cadn.net.cn

解决异常的策略,可能将它们映射到处理器、HTML错误视图或其他目标。见异常spring-doc.cadn.net.cn

ViewResolverspring-doc.cadn.net.cn

Resolve logical String-based view names returned from a handler to an actual View with which to render to the response. See 视图解析 and 视图技术.spring-doc.cadn.net.cn

LocaleResolver, LocaleContextResolverspring-doc.cadn.net.cn

Resolve the Locale a client is using and possibly their time zone, in order to be able to offer internationalized views. See Locale.spring-doc.cadn.net.cn

ThemeResolverspring-doc.cadn.net.cn

解决你的Web应用程序可以使用的主题——例如,提供个性化的布局。 参见 主题spring-doc.cadn.net.cn

MultipartResolverspring-doc.cadn.net.cn

多部分请求解析的抽象(例如,浏览器表单文件上传),借助于某些多部分解析库。请参见多部分解析器spring-doc.cadn.net.cn

FlashMapManagerspring-doc.cadn.net.cn

存储和检索可以用于在请求之间传递属性的“input”和“output”FlashMap,通常是在重定向时。参见闪存属性spring-doc.cadn.net.cn

1.1.3. Web MVC 配置

应用程序可以声明在特殊Bean类型中列出的基础设施bean,这些bean是处理请求所必需的。DispatcherServlet检查WebApplicationContext中的每个特殊bean。如果没有匹配的bean类型,它将回退到DispatcherServlet.properties中列出的默认类型。spring-doc.cadn.net.cn

在大多数情况下,MVC配置是最好的起点。它声明了所需的bean,无论是Java还是XML,并提供了一个更高层次的配置回调API来定制它。spring-doc.cadn.net.cn

Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外的便捷选项。

1.1.4. Servlet 配置

在Servlet 3.0+环境中,您可以选择通过编程方式配置Servlet容器,作为替代方案或与web.xml文件结合使用。以下示例注册了一个DispatcherServletspring-doc.cadn.net.cn

Java
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("/");
    }
}
Kotlin
import org.springframework.web.WebApplicationInitializer

class MyWebApplicationInitializer : WebApplicationInitializer {

    override fun onStartup(container: ServletContext) {
        val appContext = XmlWebApplicationContext()
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")

        val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
        registration.setLoadOnStartup(1)
        registration.addMapping("/")
    }
}

WebApplicationInitializer 是 Spring MVC 提供的一个接口,确保您的实现被检测到并自动用于初始化任何 Servlet 3 容器。 一个名为 AbstractDispatcherServletInitializerWebApplicationInitializer 抽象基类实现使得通过重写方法来指定 servlet 映射和 DispatcherServlet 配置的位置,从而更轻松地注册 DispatcherServletspring-doc.cadn.net.cn

这对于使用基于Java的Spring配置的应用程序是推荐的,正如以下示例所示:spring-doc.cadn.net.cn

Java
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[] { "/" };
    }
}
Kotlin
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>>? {
        return null
    }

    override fun getServletConfigClasses(): Array<Class<*>>? {
        return arrayOf(MyWebConfig::class.java)
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/")
    }
}

如果您使用基于XML的Spring配置,您应该直接从 AbstractDispatcherServletInitializer继承,如下例所示:spring-doc.cadn.net.cn

Java
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[] { "/" };
    }
}
Kotlin
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {

    override fun createRootApplicationContext(): WebApplicationContext? {
        return null
    }

    override fun createServletApplicationContext(): WebApplicationContext {
        return XmlWebApplicationContext().apply {
            setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
        }
    }

    override fun getServletMappings(): Array<String> {
        return arrayOf("/")
    }
}

AbstractDispatcherServletInitializer 还提供了一种方便的方式来添加 Filter 实例,并使它们自动映射到 DispatcherServlet,如下例所示:spring-doc.cadn.net.cn

Java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] {
            new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }
}
Kotlin
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {

    // ...

    override fun getServletFilters(): Array<Filter> {
        return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
    }
}

每个过滤器都使用基于其具体类型的默认名称添加,并自动映射到DispatcherServletspring-doc.cadn.net.cn

The isAsyncSupported 受保护的方法在 AbstractDispatcherServletInitializer 提供了一个启用异步支持的单一位置,适用于 DispatcherServlet 和所有 映射到它的过滤器。默认情况下,此标志设置为 truespring-doc.cadn.net.cn

最后,如果你需要进一步自定义DispatcherServlet本身,你可以重写createDispatcherServlet方法。spring-doc.cadn.net.cn

1.1.5. 处理

The DispatcherServlet 处理请求如下:spring-doc.cadn.net.cn

  • The WebApplicationContext is searched for and bound in the request as an attribute that the controller and other elements in the process can use. It is bound by default under the DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE key.spring-doc.cadn.net.cn

  • 本地化解析器绑定到请求,以便处理过程中的元素可以解析要使用的本地(渲染视图、准备数据等)。如果你不需要本地化解析,你就不需要本地化解析器。spring-doc.cadn.net.cn

  • 主题解析器绑定到请求,以便视图等元素可以确定使用哪个主题。如果你不使用主题,可以忽略它。spring-doc.cadn.net.cn

  • 如果您指定了一个multipart文件解析器,请求将被检查是否存在multipart。如果找到multipart,请求将被包装在MultipartHttpServletRequest中,以便由处理过程中的其他元素进行进一步处理。有关multipart处理的更多信息,请参见Multipart Resolverspring-doc.cadn.net.cn

  • 正在搜索合适的处理器。如果找到处理器,将运行与该处理器关联的执行链(预处理器、后处理器和控制器)以准备要渲染的模型。或者,对于注解控制器,可以在HandlerAdapter中直接渲染响应,而不是返回视图。spring-doc.cadn.net.cn

  • 如果返回了模型,则会渲染视图。如果没有返回模型(可能是由于预处理器或后处理器拦截了请求,也许是为了安全原因),则不会渲染视图,因为请求可能已经被满足。spring-doc.cadn.net.cn

The HandlerExceptionResolver beans declared in the WebApplicationContext are used to resolve exceptions thrown during request processing. Those exception resolvers allow customizing the logic to address exceptions. See 异常处理 for more details.spring-doc.cadn.net.cn

Spring DispatcherServlet 还支持返回 last-modification-date,如 Servlet API 所指定的。确定特定请求的最后一次修改日期的过程很简单:DispatcherServlet 查找适当的处理器映射,并测试找到的处理器是否实现了 LastModified 接口。如果是,则将 LastModified 接口的 long getLastModified(request) 方法的值返回给客户端。spring-doc.cadn.net.cn

您可以自定义单个DispatcherServlet实例,方法是向web.xml文件中的Servlet声明添加Servlet初始化参数(init-param元素)。下表列出了支持的参数:spring-doc.cadn.net.cn

表 1. DispatcherServlet 初始化参数
参数 说明

contextClassspring-doc.cadn.net.cn

实现 ConfigurableWebApplicationContext 的类,由该 Servlet 实例化并本地配置。默认情况下,使用 XmlWebApplicationContextspring-doc.cadn.net.cn

contextConfigLocationspring-doc.cadn.net.cn

传递给上下文实例(由contextClass指定)的字符串,用于指示上下文的位置。该字符串可能包含多个字符串(使用逗号作为分隔符),以支持多个上下文。在具有多个上下文位置且定义了两次的bean的情况下,最新的位置优先。spring-doc.cadn.net.cn

namespacespring-doc.cadn.net.cn

Namespace of the WebApplicationContext. Defaults to [servlet-name]-servlet.spring-doc.cadn.net.cn

throwExceptionIfNoHandlerFoundspring-doc.cadn.net.cn

是否在未找到请求的处理器时抛出一个NoHandlerFoundException。 然后可以通过HandlerExceptionResolver(例如,通过使用一个@ExceptionHandler控制器方法)捕获该异常,并像处理其他异常一样处理它。spring-doc.cadn.net.cn

默认情况下,这设置为 false,在这种情况下,DispatcherServlet 将响应状态设置为 404 (NOT_FOUND),而不引发异常。spring-doc.cadn.net.cn

请注意,如果也配置了默认Servlet处理,未解析的请求将始终转发到默认Servlet,并且永远不会引发404。spring-doc.cadn.net.cn

1.1.6. 拦截

所有 HandlerMapping 实现都支持处理器拦截器,当您希望将特定功能应用于某些请求时非常有用——例如,检查主体。拦截器必须实现 HandlerInterceptor 来自 org.springframework.web.servlet 包中的接口,并且需要提供三种方法,这些方法应该足够灵活,可以进行各种预处理和后处理:spring-doc.cadn.net.cn

The preHandle(..) 方法返回一个布尔值。您可以使用此方法来中断或继续执行链的处理。当此方法返回 true 时,处理器执行链将继续。当它返回 false 时,DispatcherServlet 假设拦截器本身已经处理了请求(例如,渲染了适当的视图)并且不会继续执行其他拦截器和实际处理器。spring-doc.cadn.net.cn

请参阅拦截器在MVC配置部分中的示例,了解如何配置拦截器。您还可以通过使用单独的HandlerMapping实现上的setter直接注册它们。spring-doc.cadn.net.cn

请注意,postHandle 对于 @ResponseBodyResponseEntity 方法来说用处不大,因为这些方法在 HandlerAdapter 内写入并提交响应,并且在 postHandle 之前。这意味着对响应进行任何更改(例如添加额外的标头)已经太晚了。对于此类场景,您可以实现 ResponseBodyAdvice 并将其声明为 控制器建议 bean 或直接配置在 RequestMappingHandlerAdapter 上。spring-doc.cadn.net.cn

1.1.7. 异常

如果在请求映射期间发生异常或从请求处理器(例如 @Controller)抛出异常,DispatcherServlet 会委托给一系列的 HandlerExceptionResolver bean 来解析异常并提供替代处理,通常是错误响应。spring-doc.cadn.net.cn

以下表格列出了可用的HandlerExceptionResolver实现:spring-doc.cadn.net.cn

表 2. HandlerExceptionResolver 实现
HandlerExceptionResolver 描述

SimpleMappingExceptionResolverspring-doc.cadn.net.cn

异常类名与错误视图名之间的映射。对于在浏览器应用程序中呈现错误页面非常有用。spring-doc.cadn.net.cn

DefaultHandlerExceptionResolverspring-doc.cadn.net.cn

解决由Spring MVC引发的异常并将它们映射到HTTP状态代码。 另见替代方案ResponseEntityExceptionHandlerREST API异常spring-doc.cadn.net.cn

ResponseStatusExceptionResolverspring-doc.cadn.net.cn

解决带有@ResponseStatus注解的异常,并根据注解中的值将它们映射到HTTP状态代码。spring-doc.cadn.net.cn

ExceptionHandlerExceptionResolverspring-doc.cadn.net.cn

通过调用 @ExceptionHandler 方法在 @Controller 或一个 @ControllerAdvice 类中解决异常。请参见 @ExceptionHandler 方法spring-doc.cadn.net.cn

解析器链

你可以通过在你的Spring配置中声明多个HandlerExceptionResolver beans并按需设置它们的order属性来形成一个异常解析器链。 order属性越高,异常解析器的位置越靠后。spring-doc.cadn.net.cn

0 的契约规定它可以返回:spring-doc.cadn.net.cn

The MVC配置 自动声明了用于默认Spring MVC异常的内置解析器,用于@ResponseStatus注解异常,以及对@ExceptionHandler方法的支持。您可以自定义该列表或替换它。spring-doc.cadn.net.cn

容器错误页面

如果某个异常未被任何HandlerExceptionResolver解决并且因此被传播,或者响应状态被设置为错误状态(即4xx、5xx),Servlet容器可以渲染一个默认的HTML错误页面。要自定义容器的默认错误页面,你可以在web.xml中声明一个错误页面映射。以下示例展示了如何操作:spring-doc.cadn.net.cn

<error-page>
    <location>/error</location>
</error-page>

根据前面的例子,当异常冒泡或响应具有错误状态时,Servlet容器会在容器内部向配置的URL进行ERROR调度 (例如,/error)。然后由DispatcherServlet处理,可能将其映射到一个@Controller,该实现可以返回一个带有模型的错误视图名称或渲染JSON响应,如下例所示:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@RestController
class ErrorController {

    @RequestMapping(path = ["/error"])
    fun handle(request: HttpServletRequest): Map<String, Any> {
        val map = HashMap<String, Any>()
        map["status"] = request.getAttribute("javax.servlet.error.status_code")
        map["reason"] = request.getAttribute("javax.servlet.error.message")
        return map
    }
}
Servlet API 没有提供在 Java 中创建错误页面映射的方法。但是,您可以使用 WebApplicationInitializer 和一个最小的 web.xml

1.1.8. 视图解析

Spring MVC 定义了 ViewResolverView 接口,这些接口允许你在浏览器中渲染模型而不将你绑定到特定的视图技术。ViewResolver 提供了视图名称和实际视图之间的映射。View 解决了在将数据交给特定视图技术之前的数据准备问题。spring-doc.cadn.net.cn

以下表格提供了有关ViewResolver层次结构的更多详细信息:spring-doc.cadn.net.cn

表 3. ViewResolver 实现
ViewResolver 描述

AbstractCachingViewResolverspring-doc.cadn.net.cn

子类缓存它们解析的视图实例。 缓存可以提高某些视图技术的性能。你可以通过将cache属性设置为false来关闭缓存。此外,如果你必须在运行时刷新某个视图(例如,当FreeMarker模板被修改时),你可以使用removeFromCache(String viewName, Locale loc)方法。spring-doc.cadn.net.cn

XmlViewResolverspring-doc.cadn.net.cn

Implementation of ViewResolver that accepts a configuration file written in XML with the same DTD as Spring’s XML bean factories. The default configuration file is /WEB-INF/views.xml.spring-doc.cadn.net.cn

ResourceBundleViewResolverspring-doc.cadn.net.cn

Implementation of ViewResolver that uses bean definitions in a ResourceBundle, specified by the bundle base name. For each view it is supposed to resolve, it uses the value of the property [viewname].(class) as the view class and the value of the property [viewname].url as the view URL. You can find examples in the chapter on 视图技术.spring-doc.cadn.net.cn

UrlBasedViewResolverspring-doc.cadn.net.cn

Simple implementation of the ViewResolver interface that affects the direct resolution of logical view names to URLs without an explicit mapping definition. This is appropriate if your logical names match the names of your view resources in a straightforward manner, without the need for arbitrary mappings.spring-doc.cadn.net.cn

InternalResourceViewResolverspring-doc.cadn.net.cn

Convenient subclass of UrlBasedViewResolver that supports InternalResourceView (in effect, Servlets and JSPs) and subclasses such as JstlView and TilesView. You can specify the view class for all views generated by this resolver by using setViewClass(..). See the UrlBasedViewResolver javadoc for details.spring-doc.cadn.net.cn

FreeMarkerViewResolverspring-doc.cadn.net.cn

Convenient subclass of UrlBasedViewResolver that supports FreeMarkerView and custom subclasses of them.spring-doc.cadn.net.cn

ContentNegotiatingViewResolverspring-doc.cadn.net.cn

Implementation of the ViewResolver interface that resolves a view based on the request file name or Accept header. See 内容协商.spring-doc.cadn.net.cn

处理

您可以链接视图解析器,方法是声明多个解析器bean,并且如果必要,通过设置order属性来指定顺序。请记住,order属性越高,视图解析器在链中的位置越靠后。spring-doc.cadn.net.cn

The contract of a ViewResolver specifies that it can return null to indicate that the view could not be found. However, in the case of JSPs and InternalResourceViewResolver, the only way to figure out if a JSP exists is to perform a dispatch through RequestDispatcher. Therefore, you must always configure an InternalResourceViewResolver to be last in the overall order of view resolvers.spring-doc.cadn.net.cn

配置视图解析器就像在您的Spring配置中添加ViewResolver个bean一样简单。MVC配置提供了一个专门的配置API,用于视图解析器和添加无逻辑的视图控制器,这对于HTML模板渲染而无需控制器逻辑非常有用。spring-doc.cadn.net.cn

重定向

特殊前缀 redirect: 在视图名称中允许你执行重定向。UrlBasedViewResolver(及其子类)将其识别为需要重定向的指令。视图名称的其余部分是重定向URL。spring-doc.cadn.net.cn

最终效果与控制器返回 RedirectView 相同,但现在控制器本身可以使用逻辑视图名称进行操作。逻辑视图名称(如 redirect:/myapp/some/resource)相对于当前的 Servlet 上下文进行重定向,而名称如 redirect:https://myhost.com/some/arbitrary/path 则重定向到绝对 URL。spring-doc.cadn.net.cn

请注意,如果控制器方法被注解@ResponseStatus,则注解值优先于通过RedirectView设置的响应状态。spring-doc.cadn.net.cn

转发

您还可以使用一个特殊的forward:前缀用于视图名称,这些视图名称最终由UrlBasedViewResolver及其子类解析。这会创建一个InternalResourceView,它执行一个RequestDispatcher.forward()。 因此,这个前缀对于InternalResourceViewResolverInternalResourceView(用于JSP)没有用处,但如果您使用另一种视图技术但仍希望强制资源转发到Servlet/JSP引擎处理,可能会有所帮助。请注意,您也可以链接多个视图解析器。spring-doc.cadn.net.cn

内容协商

ContentNegotiatingViewResolver does not resolve views itself but rather delegates to other view resolvers and selects the view that resembles the representation requested by the client. The representation can be determined from the Accept header or from a query parameter (for example, "/path?format=pdf")。spring-doc.cadn.net.cn

The ContentNegotiatingViewResolver 选择一个合适的 View 来处理请求,通过比较请求的媒体类型与每个 ViewResolvers 关联的 View 支持的媒体类型(也称为 Content-Type)。列表中的第一个具有兼容 Content-TypeView 将表示返回给客户端。如果 ViewResolver 链无法提供兼容的视图,则会咨询通过 DefaultViews 属性指定的视图列表。后一种选项适用于可以渲染当前资源适当表示形式的单例 Views,而无需考虑逻辑视图名称。Accept 标头可以包含通配符(例如 text/*),在这种情况下,View 其中 Content-Typetext/xml 是一个兼容的匹配项。spring-doc.cadn.net.cn

请参阅 视图解析器MVC配置 下的配置详情。spring-doc.cadn.net.cn

1.1.9. 本地化

Spring架构的大部分支持国际化,就像Spring Web MVC框架一样。DispatcherServlet允许你通过使用客户端的区域设置自动解析消息。这是通过LocaleResolver对象完成的。spring-doc.cadn.net.cn

当一个请求到来时,DispatcherServlet 会查找一个区域设置解析器,并且如果找到的话,它会尝试使用它来设置区域设置。通过使用 RequestContext.getLocale() 方法,你可以始终检索由区域设置解析器解析的区域设置。spring-doc.cadn.net.cn

除了自动本地化解析外,你还可以将拦截器附加到处理器映射(有关处理器映射拦截器的更多信息,请参见拦截)以在特定情况下更改本地化(例如,基于请求中的参数)。spring-doc.cadn.net.cn

Locale resolvers 和 interceptors 在 org.springframework.web.servlet.i18n 包中定义,并以正常方式在您的应用程序上下文中配置。以下是一些包含在 Spring 中的 locale resolver 选择。spring-doc.cadn.net.cn

时区

除了获取客户端的区域设置,通常还希望知道其时区。LocaleContextResolver接口提供了一个扩展到LocaleResolver的方法,允许解析器提供更丰富的LocaleContext,其中可能包含时区信息。spring-doc.cadn.net.cn

当可用时,可以通过使用RequestContext.getTimeZone()方法获取用户的TimeZone。时区信息会自动用于任何已注册到Spring的ConversionService中的Date/TimeConverterFormatter对象。spring-doc.cadn.net.cn

标题解析器

此本地化解析器检查客户端(例如,网络浏览器)发送的请求中的accept-language标头。通常,此标头字段包含客户端操作系统的本地设置。请注意,此解析器不支持时区信息。spring-doc.cadn.net.cn

此本地化解析器检查客户端上可能存在的Cookie,以查看是否指定了LocaleTimeZone。如果指定了,则使用指定的详细信息。通过使用此本地化解析器的属性,您可以指定cookie的名称以及最大年龄。以下示例定义了一个CookieLocaleResolverspring-doc.cadn.net.cn

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

    <property name="cookieName" value="clientlanguage"/>

    <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
    <property name="cookieMaxAge" value="100000"/>

</bean>

以下表格描述了属性CookieLocaleResolverspring-doc.cadn.net.cn

会话解析器

The SessionLocaleResolver 让你从可能与用户请求相关的会话中检索 LocaleTimeZone。与 CookieLocaleResolver 不同,这种策略将本地化设置存储在 Servlet 容器的 HttpSession 中。因此,这些设置对每个会话都是临时的,并且在每个会话结束时丢失。spring-doc.cadn.net.cn

请注意,这里没有与外部会话管理机制的直接关系, 例如Spring Session项目。这个SessionLocaleResolver评估并 修改与当前HttpServletRequest对应的HttpSession属性。spring-doc.cadn.net.cn

本地化拦截器

您可以通过在其中一个HandlerMapping定义中添加LocaleChangeInterceptor来启用更改区域设置。它检测请求中的参数并相应地更改区域设置,调用dispatcher的应用上下文中的LocaleResolversetLocale方法。下一个示例显示,所有包含名为siteLanguage参数的*.view资源的调用现在都会更改区域设置。因此,例如,对URL的请求https://www.sf.net/home.view?siteLanguage=nl会将站点语言更改为荷兰语。以下示例说明了如何拦截区域设置:spring-doc.cadn.net.cn

<bean id="localeChangeInterceptor"
        class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
        class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
        class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>

1.1.10. 主题

您可以将Spring Web MVC框架的主题应用于设置应用程序的整体外观和感觉,从而提升用户体验。主题是一组静态资源的集合,通常是样式表和图像,这些资源会影响应用程序的视觉风格。spring-doc.cadn.net.cn

定义主题

要在你的Web应用程序中使用主题,你必须设置一个org.springframework.ui.context.ThemeSource接口的实现。WebApplicationContext接口扩展了ThemeSource,但将其职责委托给一个专门的实现。默认情况下,委托实现是一个org.springframework.ui.context.support.ResourceBundleThemeSource实现,该实现从类路径的根目录加载属性文件。要使用自定义的ThemeSource实现或配置ResourceBundleThemeSource的基本名称前缀,你可以在应用程序上下文中注册一个名为themeSource的bean。Web应用程序上下文会自动检测具有该名称的bean并使用它。spring-doc.cadn.net.cn

当你使用 ResourceBundleThemeSource 时,主题会在一个简单的属性文件中定义。属性文件列出了构成主题的资源,如下例所示:spring-doc.cadn.net.cn

styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg

属性键是用于从视图代码中引用主题化元素的名称。对于JSP,你通常使用spring:theme自定义标签来实现这一点,这与spring:message标签非常相似。以下JSP片段使用上一个示例中定义的主题来定制外观和感觉:spring-doc.cadn.net.cn

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
    <head>
        <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
    </head>
    <body style="background=<spring:theme code='background'/>">
        ...
    </body>
</html>

默认情况下,ResourceBundleThemeSource 使用一个空的基本名称前缀。因此,属性文件从类路径的根目录加载。因此,您将 cool.properties 主题定义放在类路径根目录下的目录中(例如,在 /WEB-INF/classes 中)。ResourceBundleThemeSource 使用标准的 Java 资源捆绑加载机制,允许主题完全国际化。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties,它引用了一个带有荷兰文背景图像的特殊主题定义。spring-doc.cadn.net.cn

解析主题

在定义主题之后,如前一节所述,你需要决定使用哪个主题。DispatcherServlet 会查找名为 themeResolver 的 bean 来确定要使用的 ThemeResolver 实现。主题解析器的工作方式与 LocaleResolver 类似。它检测特定请求要使用哪个主题,并且还可以更改请求的主题。下表描述了 Spring 提供的主题解析器:spring-doc.cadn.net.cn

表 5. ThemeResolver 实现
描述

FixedThemeResolverspring-doc.cadn.net.cn

选择一个固定的主题,通过使用defaultThemeName属性设置。spring-doc.cadn.net.cn

SessionThemeResolverspring-doc.cadn.net.cn

主题在用户的HTTP会话中维护。每个会话只需设置一次,但不会在会话之间持久化。spring-doc.cadn.net.cn

CookieThemeResolverspring-doc.cadn.net.cn

选定的主题存储在客户端的cookie中。spring-doc.cadn.net.cn

Spring 还提供了一个 ThemeChangeInterceptor,它允许通过简单的请求参数在每个请求中更改主题。spring-doc.cadn.net.cn

1.1.11. 多部分解析器

MultipartResolver 来自 org.springframework.web.multipart 包是一种策略,用于解析包含文件上传的multipart请求。有一个基于 Commons FileUpload 的实现,另一个基于Servlet 3.0 multipart请求解析。spring-doc.cadn.net.cn

要启用多部分处理,您需要在您的DispatcherServlet Spring配置中声明一个MultipartResolver bean,名称为multipartResolverDispatcherServlet检测到它并将它应用于传入的请求。当接收到内容类型为multipart/form-data的POST请求时,解析器会解析内容并包装当前的HttpServletRequest作为MultipartHttpServletRequest以提供对解析的部分的访问,同时将它们作为请求参数暴露。spring-doc.cadn.net.cn

Apache Commons FileUpload

要使用Apache Commons FileUpload,你可以配置一个类型为CommonsMultipartResolver的bean,并将其命名为multipartResolver。你还需要在类路径上将commons-fileupload作为依赖。spring-doc.cadn.net.cn

Servlet 3.0

Servlet 3.0 多部分解析需要通过 Servlet 容器配置启用。 要这样做:spring-doc.cadn.net.cn

以下示例展示了如何在Servlet注册中设置MultipartConfigElementspring-doc.cadn.net.cn

Java
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    // ...

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {

        // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
    }

}
Kotlin
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    // ...

    override fun customizeRegistration(registration: ServletRegistration.Dynamic) {

        // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
        registration.setMultipartConfig(MultipartConfigElement("/tmp"))
    }

}

一旦Servlet 3.0配置就绪,您可以添加一个名为multipartResolverStandardServletMultipartResolver类型的bean。spring-doc.cadn.net.cn

1.1.12. 日志记录

Spring MVC 中的 DEBUG 级别日志设计为紧凑、简洁且对人类友好。它侧重于那些反复有用的关键信息,而不是仅在调试特定问题时才有用的信息。spring-doc.cadn.net.cn

TRACE级别的日志记录通常遵循与DEBUG相同的原理(例如,也不应该是数据洪流),但可以用于调试任何问题。此外,一些日志消息在TRACE级别和DEBUG级别之间可能会显示不同级别的详细信息。spring-doc.cadn.net.cn

良好的日志记录来自于使用日志的经验。如果您发现任何不符合既定目标的情况,请告知我们。spring-doc.cadn.net.cn

敏感数据

DEBUG 和 TRACE 日志记录可能会记录敏感信息。这就是为什么默认情况下会屏蔽请求参数和标头,并且必须通过在 DispatcherServlet 上设置 enableLoggingRequestDetails 属性来显式启用它们的完整日志记录。spring-doc.cadn.net.cn

以下示例展示了如何通过使用Java配置来实现这一点:spring-doc.cadn.net.cn

Java
public class MyInitializer
        extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return ... ;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return ... ;
    }

    @Override
    protected String[] getServletMappings() {
        return ... ;
    }

    @Override
    protected void customizeRegistration(ServletRegistration.Dynamic registration) {
        registration.setInitParameter("enableLoggingRequestDetails", "true");
    }

}
Kotlin
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {

    override fun getRootConfigClasses(): Array<Class<*>>? {
        return ...
    }

    override fun getServletConfigClasses(): Array<Class<*>>? {
        return ...
    }

    override fun getServletMappings(): Array<String> {
        return ...
    }

    override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
        registration.setInitParameter("enableLoggingRequestDetails", "true")
    }
}

1.2. 过滤器

The spring-web 模块提供了一些有用的过滤器:spring-doc.cadn.net.cn

1.2.1. 表单数据

浏览器只能通过HTTP GET或HTTP POST提交表单数据,但非浏览器客户端也可以使用HTTP PUT、PATCH和DELETE。Servlet API要求ServletRequest.getParameter*()方法仅支持HTTP POST的表单字段访问。spring-doc.cadn.net.cn

The spring-web 模块提供了 FormContentFilter 来拦截具有 application/x-www-form-urlencoded 内容类型的 HTTP PUT、PATCH 和 DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest 以使表单数据可以通过 ServletRequest.getParameter*() 系列方法获取。spring-doc.cadn.net.cn

1.2.2. 转发的请求头

当请求通过代理(如负载均衡器)时,主机、端口和协议可能会发生变化,这使得从客户端的角度创建指向正确主机、端口和协议的链接成为一个挑战。spring-doc.cadn.net.cn

RFC 7239 定义了 Forwarded HTTP 标头,代理可以使用该标头提供有关原始请求的信息。还有其他非标准标头,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefixspring-doc.cadn.net.cn

ForwardedHeaderFilter 是一个Servlet过滤器,用于修改请求以 a) 根据 Forwarded 头信息更改主机、端口和协议,并 b) 移除这些头信息以消除进一步的影响。该过滤器依赖于包装请求,并且必须在其他过滤器之前进行排序,例如 RequestContextFilter,以便它们可以使用修改后而不是原始的请求。spring-doc.cadn.net.cn

请求头转发存在安全考虑,因为应用程序无法知道这些标头是被代理按预期添加的,还是被恶意客户端添加的。这就是为什么在信任边界的代理应该被配置为移除来自外部的不可信Forwarded标头。你还可以配置ForwardedHeaderFilter使用removeOnly=true,在这种情况下,它会移除但不使用这些标头。spring-doc.cadn.net.cn

为了支持异步请求和错误分发,此过滤器应映射为DispatcherType.ASYNCDispatcherType.ERROR。如果使用Spring Framework的AbstractAnnotationConfigDispatcherServletInitializer(参见Servlet配置),所有过滤器会自动注册到所有分发类型。但是,如果通过web.xml或在Spring Boot中通过FilterRegistrationBean进行注册,请确保包括DispatcherType.ASYNCDispatcherType.ERROR以及DispatcherType.REQUESTspring-doc.cadn.net.cn

1.2.3. 浅层ETag

The ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并从中计算MD5哈希来创建一个“浅层”ETag。下次客户端发送请求时,它会执行相同的操作,但还会将计算出的值与If-None-Match 请求头进行比较,如果两者相等,则返回304(未修改)。spring-doc.cadn.net.cn

这种策略节省了网络带宽,但没有节省CPU,因为每个请求都必须计算完整的响应。在控制器级别上的其他策略,如前面所述,可以避免计算。请参见HTTP 缓存spring-doc.cadn.net.cn

此过滤器有一个 writeWeakETag 参数,用于配置过滤器以写入类似于以下的弱 ETags: W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如在 RFC 7232 第 2.3 节 中定义)。spring-doc.cadn.net.cn

为了支持异步请求,此过滤器必须映射为DispatcherType.ASYNC,以便过滤器可以在最后一次异步分发结束时延迟并成功生成ETag。如果使用Spring Framework的AbstractAnnotationConfigDispatcherServletInitializer(参见Servlet配置),所有过滤器会自动注册到所有分发类型。但是,如果通过web.xml或在Spring Boot中通过FilterRegistrationBean注册过滤器,请确保包含DispatcherType.ASYNCspring-doc.cadn.net.cn

1.2.4. 跨域资源共享(CORS)

Spring MVC 通过注解在控制器上提供了对 CORS 配置的细粒度支持。然而,当与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,该组件必须在 Spring Security 的过滤器链之前进行排序。spring-doc.cadn.net.cn

请参阅CORSCORS过滤器部分以获取更多详细信息。spring-doc.cadn.net.cn

1.3. 注解控制器

Spring MVC 提供了一个基于注解的编程模型,其中 @Controller@RestController 组件使用注解来表达请求映射、请求输入、异常处理等。带有注解的控制器具有灵活的方法签名,不需要扩展基类或实现特定接口。 以下示例展示了一个由注解定义的控制器:spring-doc.cadn.net.cn

Java
@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}
Kotlin
import org.springframework.ui.set

@Controller
class HelloController {

    @GetMapping("/hello")
    fun handle(model: Model): String {
        model["message"] = "Hello World!"
        return "index"
    }
}

在前面的例子中,该方法接受一个Model并返回一个视图名称作为String, 但存在许多其他选项,并在本章后面进行了解释。spring-doc.cadn.net.cn

Guides and tutorials on spring.io use the annotation-based programming model described in this section.

1.3.1. 声明

你可以通过在Servlet的WebApplicationContext中使用标准的Spring bean定义来定义控制器bean。@Controller注解允许自动检测,与Spring对classpath中的@Component类的一般支持相一致,并自动注册它们的bean定义。它还作为注解类的刻板印象,表明其角色为web组件。spring-doc.cadn.net.cn

要启用此类@Controller bean的自动检测,您可以在Java配置中添加组件扫描,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

    // ...
}
Kotlin
@Configuration
@ComponentScan("org.example.web")
class WebConfig {

    // ...
}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.example.web"/>

    <!-- ... -->

</beans>

@RestController 是一个 复合注解,它本身被元注解 @Controller@ResponseBody 标注,以指示一个控制器,该控制器的每个方法继承类型级别的 @ResponseBody 注解,并因此直接写入响应体,而不是使用 HTML 模板进行视图解析和渲染。spring-doc.cadn.net.cn

AOP代理

在某些情况下,你可能需要在运行时使用AOP代理装饰控制器。一个例子是如果你选择直接在控制器上使用@Transactional注解。当出现这种情况时,对于控制器特别是,我们建议使用基于类的代理。这通常是控制器的默认选择。但是,如果控制器必须实现一个不是Spring上下文回调的接口(例如InitializingBean*Aware和其他),你可能需要显式配置基于类的代理。例如,使用<tx:annotation-driven/>你可以改为<tx:annotation-driven proxy-target-class="true"/>,使用@EnableTransactionManagement你可以改为@EnableTransactionManagement(proxyTargetClass = true)spring-doc.cadn.net.cn

1.3.2. 请求映射

您可以使用@注解将请求映射到控制器方法。它有多种属性可以匹配URL、HTTP方法、请求参数、头信息和媒体类型。您可以在类级别使用它来表达共享映射,也可以在方法级别使用它来缩小到特定的端点映射。spring-doc.cadn.net.cn

还有针对HTTP方法的@RequestMapping的快捷变体:spring-doc.cadn.net.cn

这些快捷方式是自定义注解,之所以提供它们是因为,可以说,大多数控制器方法都应该映射到特定的HTTP方法,而不是使用@RequestMapping,默认情况下,@RequestMapping匹配所有HTTP方法。同时,在类级别仍然需要一个@RequestMapping来表示共享映射。spring-doc.cadn.net.cn

以下示例具有类型和方法级别的映射:spring-doc.cadn.net.cn

Java
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    fun getPerson(@PathVariable id: Long): Person {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}
URI 模式

您可以使用通配符模式和通配符来映射请求:spring-doc.cadn.net.cn

模式 描述 示例

?spring-doc.cadn.net.cn

匹配一个字符spring-doc.cadn.net.cn

"/pages/t?st.html"spring-doc.cadn.net.cn

匹配 "/pages/test.html""/pages/t3st.html"spring-doc.cadn.net.cn

*spring-doc.cadn.net.cn

匹配路径段中的零个或多个字符spring-doc.cadn.net.cn

"/resources/*.png" 个匹配项 "/resources/file.png"spring-doc.cadn.net.cn

"/projects/*/versions" 匹配 "/projects/spring/versions" 但不匹配 "/projects/spring/boot/versions"spring-doc.cadn.net.cn

**spring-doc.cadn.net.cn

匹配零个或多个路径段,直到路径结束spring-doc.cadn.net.cn

"/resources/**" 匹配 "/resources/file.png""/resources/images/file.png"spring-doc.cadn.net.cn

{name}spring-doc.cadn.net.cn

匹配路径段并将其捕获为名为"name"的变量spring-doc.cadn.net.cn

"/projects/{project}/versions" 个匹配 "/projects/spring/versions" 并捕获 project=springspring-doc.cadn.net.cn

{name:[a-z]+}spring-doc.cadn.net.cn

匹配正则表达式 "[a-z]+" 作为名为 "name" 的路径变量spring-doc.cadn.net.cn

"/projects/{project:[a-z]+}/versions" 匹配 "/projects/spring/versions" 但不匹配 "/projects/spring1/versions"spring-doc.cadn.net.cn

捕获的URI变量可以通过@PathVariable访问,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
Kotlin
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
    // ...
}

您可以在类和方法级别声明URI变量,如下例所示:spring-doc.cadn.net.cn

Java
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

    @GetMapping("/pets/{petId}")
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
Kotlin
@Controller
@RequestMapping("/owners/{ownerId}")
class OwnerController {

    @GetMapping("/pets/{petId}")
    fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
        // ...
    }
}

URI变量会自动转换为适当的类型,或者抛出TypeMismatchException。简单类型(intlongDate等)默认受到支持,你可以注册对任何其他数据类型的支持。 请参见类型转换DataBinderspring-doc.cadn.net.cn

您可以显式命名URI变量(例如,@PathVariable("customId")),但如果名称相同且您的代码是使用调试信息编译的或在Java 8中启用了-parameters编译器标志,则可以省略该细节。spring-doc.cadn.net.cn

语法{varName:regex}声明了一个具有正则表达式语法的URI变量,该正则表达式的语法为{varName:regex}。例如,给定URL"/spring-web-3.0.5 .jar",以下方法提取名称、版本和文件扩展名:spring-doc.cadn.net.cn

Java
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
    // ...
}
Kotlin
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {
    // ...
}

URI 路径模式也可以包含嵌入的 ${…​} 占位符,这些占位符在启动时通过使用 PropertyPlaceHolderConfigurer 解析本地、系统、环境和其他属性源。例如,您可以使用此功能根据某些外部配置参数化基础 URL。spring-doc.cadn.net.cn

Spring MVC 使用 PathMatcher 合同和 AntPathMatcher 实现从 spring-core 进行 URI 路径匹配。
模式比较

当多个模式匹配一个URL时,必须比较它们以找到最佳匹配。这是通过使用AntPathMatcher.getPatternComparator(String path)完成的,它会查找更具体的模式。spring-doc.cadn.net.cn

一个模式如果包含较少的URI变量(计为1)、单个通配符(计为1)和双通配符(计为2),则被认为更不具体。在得分相同的情况下,选择较长的模式。如果得分和长度都相同,则选择包含更多URI变量而不是通配符的模式。spring-doc.cadn.net.cn

默认的映射模式 (/**) 不参与评分,并且始终排序在最后。此外,前缀模式(如 /public/**)被认为比没有双通配符的其他模式更不具体。spring-doc.cadn.net.cn

有关完整详细信息,请参见 AntPatternComparatorAntPathMatcher 中,同时请记住 您可以自定义 PathMatcher 实现。 请参阅配置部分中的 路径匹配spring-doc.cadn.net.cn

后缀匹配

默认情况下,Spring MVC 执行 .* 后缀模式匹配,因此映射到 /person 的控制器也隐式映射到 /person.*。然后使用文件扩展名来解释请求的内容类型以用于响应(即,而不是 Accept 标头)——例如,/person.pdf/person.xml 和其他。spring-doc.cadn.net.cn

使用文件扩展名这种方式在过去是必要的,因为浏览器会发送难以一致解释的Accept头信息。目前,这已经不再是必需的,并且应该优先选择使用Accept头信息。spring-doc.cadn.net.cn

随着时间的推移,使用文件名扩展名在多种方面证明了其问题性。它可能会导致歧义,尤其是在与 URI 变量、路径参数和 URI 编码结合使用时。关于基于 URL 的授权和安全性的推理(详见下一部分)也会变得更加困难。spring-doc.cadn.net.cn

要完全禁用文件扩展名的使用,必须设置以下两项:spring-doc.cadn.net.cn

基于URL的内容协商仍然很有用(例如,在浏览器中输入URL时)。为了启用这一点,我们建议使用基于查询参数的策略来避免大多数与文件扩展名相关的问题。或者,如果您必须使用文件扩展名,请考虑通过ContentNegotiationConfigurermediaTypes属性将它们限制为显式注册的扩展名列表。spring-doc.cadn.net.cn

从5.2.4版本开始,与请求映射相关的路径扩展选项在 RequestMappingHandlerMapping 以及内容协商相关的选项在 ContentNegotiationManagerFactoryBean 中已被弃用。请参阅Spring Framework问题 #24179 和相关问题以了解进一步的计划。spring-doc.cadn.net.cn

后缀匹配和RFD

反射文件下载(RFD)攻击与跨站脚本(XSS)攻击类似,因为它依赖于请求输入(例如,查询参数和URI变量)在响应中被反射。然而,与在HTML中插入JavaScript不同,RFD攻击依赖于浏览器切换以执行下载,并在稍后双击时将响应视为可执行脚本。spring-doc.cadn.net.cn

在Spring MVC中,@ResponseBodyResponseEntity方法存在风险,因为 它们可以渲染不同的内容类型,客户端可以通过URL路径扩展名请求这些内容类型。 禁用后缀模式匹配并使用路径扩展名进行内容协商 可以降低风险,但不足以防止RFD攻击。spring-doc.cadn.net.cn

为了防止RFD攻击,在渲染响应体之前,Spring MVC会添加一个Content-Disposition:inline;filename=f.txt标头,建议使用固定且安全的下载文件。这仅在URL路径包含既不被视为安全也不明确注册用于内容协商的文件扩展名时执行。然而,当直接在浏览器中输入URL时,它可能会产生副作用。spring-doc.cadn.net.cn

许多常见的路径扩展默认被允许为安全的。具有自定义HttpMessageConverter实现的应用程序可以显式注册文件扩展名以进行内容协商,从而避免为这些扩展名添加Content-Disposition头。请参见内容类型spring-doc.cadn.net.cn

请参阅 CVE-2015-5211 了解有关RFD的其他建议。spring-doc.cadn.net.cn

可消耗的媒体类型

您可以根据请求的Content-Type来缩小请求映射,如下例所示:spring-doc.cadn.net.cn

Java
@PostMapping(path = "/pets", consumes = "application/json") (1)
public void addPet(@RequestBody Pet pet) {
    // ...
}
1 使用 consumes 属性来根据内容类型缩小映射范围。
Kotlin
@PostMapping("/pets", consumes = ["application/json"]) (1)
fun addPet(@RequestBody pet: Pet) {
    // ...
}
1 使用 consumes 属性来根据内容类型缩小映射范围。

The consumes 属性还支持否定表达式——例如,!text/plain 表示除了 text/plain 之外的任何内容类型。spring-doc.cadn.net.cn

您可以在类级别声明一个共享的consumes属性。然而,与其他大多数请求映射属性不同的是,当在类级别使用时,方法级别的consumes属性会覆盖而不是扩展类级别的声明。spring-doc.cadn.net.cn

MediaType 提供了常用媒体类型的常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
可生成的媒体类型

您可以根据Accept请求头和控制器方法生成的内容类型列表来缩小请求映射,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
1 使用 produces 属性来根据内容类型缩小映射范围。
Kotlin
@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)
@ResponseBody
fun getPet(@PathVariable petId: String): Pet {
    // ...
}
1 使用 produces 属性来根据内容类型缩小映射范围。

媒体类型可以指定字符集。支持否定表达式——例如,!text/plain 表示除了 "text/plain" 之外的任何内容类型。spring-doc.cadn.net.cn

您可以在类级别声明一个共享的produces属性。然而,与其他大多数请求映射属性不同的是,当在类级别使用时,方法级别的produces属性会覆盖而不是扩展类级别的声明。spring-doc.cadn.net.cn

MediaType 提供了常用媒体类型的常量,例如 APPLICATION_JSON_VALUEAPPLICATION_XML_VALUE
参数、头部

你可以根据请求参数条件缩小请求映射。你可以测试请求参数是否存在(myParam),是否不存在(!myParam),或者是否有特定值(myParam=myValue)。以下示例展示了如何测试特定值:spring-doc.cadn.net.cn

Java
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 测试是否 myParam 等于 myValue
Kotlin
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
1 测试是否 myParam 等于 myValue

你也可以在请求头条件中使用相同的方法,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
1 测试是否 myHeader 等于 myValue
Kotlin
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
你可以使用 headers 条件匹配 Content-TypeAccept,但最好使用 consumesproduces 代替。
HTTP HEAD, OPTIONS

@GetMapping (和 @RequestMapping(method=HttpMethod.GET)) 支持 HTTP HEAD 透明地用于请求映射。控制器方法不需要更改。 一个响应包装器,在 javax.servlet.http.HttpServlet 中应用,确保 Content-Length 头设置为已写入的字节数(实际上不写入响应)。spring-doc.cadn.net.cn

@GetMapping(和@RequestMapping(method=HttpMethod.GET))被隐式映射到 并支持HTTP HEAD。一个HTTP HEAD请求被处理得好像它是HTTP GET一样,只是 不写入正文,而是计算字节数并将Content-Length 头设置。spring-doc.cadn.net.cn

默认情况下,HTTP OPTIONS 通过将 Allow 响应头设置为所有具有匹配 URL 模式的 @RequestMapping 方法中列出的 HTTP 方法列表来处理。spring-doc.cadn.net.cn

对于一个@RequestMapping没有HTTP方法声明,Allow标头将被设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应该始终声明支持的HTTP方法(例如,通过使用特定的HTTP方法变体:@GetMapping@PostMapping等)。spring-doc.cadn.net.cn

您可以显式地将@RequestMapping方法映射到HTTP HEAD和HTTP OPTIONS,但在常见情况下这并不是必须的。spring-doc.cadn.net.cn

自定义注解

Spring MVC 支持使用 组合注解 进行请求映射。这些注解本身带有 @RequestMapping 的元注解,并组合起来重新声明 @RequestMapping 属性的子集(或全部),具有更窄、更特定的目的。spring-doc.cadn.net.cn

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping, 和 @PatchMapping 是组合注解的例子。它们被提供是因为,可以说,大多数控制器方法应该映射到特定的HTTP方法,而不是使用 @RequestMapping,后者默认匹配所有HTTP方法。如果你需要组合注解的例子,看看这些是如何声明的。spring-doc.cadn.net.cn

Spring MVC 也支持带有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要子类化 RequestMappingHandlerMapping 并重写 getCustomMethodCondition 方法,在这里你可以检查自定义属性并返回你自己的 RequestConditionspring-doc.cadn.net.cn

显式注册

您可以编程式地注册处理器方法,这些方法可用于动态注册或用于高级情况,例如在不同的URL下注册同一个处理器的不同实例。以下示例注册了一个处理器方法:spring-doc.cadn.net.cn

Java
@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }
}
1 注入目标处理器和控制器的处理器映射。
2 准备请求映射元数据。
3 获取处理器方法。
4 添加注册。
Kotlin
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)
        val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)
        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)
        mapping.registerMapping(info, handler, method) (4)
    }
}
1 注入目标处理器和控制器的处理器映射。
2 准备请求映射元数据。
3 获取处理器方法。
4 添加注册。

1.3.3. 处理方法

@RequestMapping 处理方法具有灵活的签名,并可以从一系列支持的控制器方法参数和返回值中进行选择。spring-doc.cadn.net.cn

方法参数

下表描述了支持的控制器方法参数。不支持任何参数的响应式类型。spring-doc.cadn.net.cn

JDK 8 的 java.util.Optional 作为方法参数与具有 required 属性的注解(例如,@RequestParam@RequestHeader 等)结合使用,并等价于 required=falsespring-doc.cadn.net.cn

控制器方法参数 描述

WebRequest, NativeWebRequestspring-doc.cadn.net.cn

通用访问请求参数和请求及会话属性,无需直接使用Servlet API。spring-doc.cadn.net.cn

javax.servlet.ServletRequest, javax.servlet.ServletResponsespring-doc.cadn.net.cn

选择任何特定的请求或响应类型——例如,ServletRequest, HttpServletRequest, 或者Spring的MultipartRequest, MultipartHttpServletRequestspring-doc.cadn.net.cn

javax.servlet.http.HttpSessionspring-doc.cadn.net.cn

强制存在会话。因此,此类参数永远不会是null。 请注意,会话访问不是线程安全的。如果允许多个请求并发访问会话,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为truespring-doc.cadn.net.cn

javax.servlet.http.PushBuilderspring-doc.cadn.net.cn

Servlet 4.0 推送构建器 API 用于编程式 HTTP/2 资源推送。 请注意,根据 Servlet 规范,注入的 PushBuilder 实例可能为 null,如果客户端不支持该 HTTP/2 功能。spring-doc.cadn.net.cn

java.security.Principalspring-doc.cadn.net.cn

当前已认证的用户——如果已知,可能是特定的Principal实现类。spring-doc.cadn.net.cn

HttpMethodspring-doc.cadn.net.cn

请求的HTTP方法。spring-doc.cadn.net.cn

java.util.Localespring-doc.cadn.net.cn

当前请求的本地化设置,由最具体的LocaleResolver确定(实际上,是配置的LocaleResolverLocaleContextResolver)。spring-doc.cadn.net.cn

java.util.TimeZone + java.time.ZoneIdspring-doc.cadn.net.cn

与当前请求相关联的时区,由LocaleContextResolver确定。spring-doc.cadn.net.cn

java.io.InputStream, java.io.Readerspring-doc.cadn.net.cn

要访问由Servlet API暴露的原始请求正文。spring-doc.cadn.net.cn

java.io.OutputStream, java.io.Writerspring-doc.cadn.net.cn

获取由Servlet API暴露的原始响应体。spring-doc.cadn.net.cn

@PathVariablespring-doc.cadn.net.cn

访问URI模板变量。请参见URI模式spring-doc.cadn.net.cn

@MatrixVariablespring-doc.cadn.net.cn

用于访问URI路径段中的名称值对。请参见矩阵变量spring-doc.cadn.net.cn

@RequestParamspring-doc.cadn.net.cn

对于访问Servlet请求参数,包括多部分文件。参数值将转换为声明的方法参数类型。请参见 @RequestParam 以及 多部分spring-doc.cadn.net.cn

注意,对于简单的参数值,使用 @RequestParam 是可选的。 请参见本表末尾的“任何其他参数”。spring-doc.cadn.net.cn

@RequestHeaderspring-doc.cadn.net.cn

用于访问请求头。头值将转换为声明的方法参数类型。请参见 @RequestHeaderspring-doc.cadn.net.cn

@CookieValuespring-doc.cadn.net.cn

用于访问cookie。Cookie值将转换为声明的方法参数类型。请参见@CookieValuespring-doc.cadn.net.cn

@RequestBodyspring-doc.cadn.net.cn

对于访问HTTP请求正文。正文内容通过使用HttpMessageConverter实现转换为声明的方法参数类型。请参见@RequestBodyspring-doc.cadn.net.cn

HttpEntity<B>spring-doc.cadn.net.cn

访问请求头和正文。正文通过HttpMessageConverter进行转换。 参见HttpEntityspring-doc.cadn.net.cn

@RequestPartspring-doc.cadn.net.cn

对于在 multipart/form-data 请求中访问某个部分,使用 HttpMessageConverter 转换该部分的主体。请参见 Multipartspring-doc.cadn.net.cn

java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMapspring-doc.cadn.net.cn

用于访问在HTML控制器中使用的模型,并作为视图渲染的一部分暴露给模板。spring-doc.cadn.net.cn

RedirectAttributesspring-doc.cadn.net.cn

指定重定向时要使用的属性(即,附加到查询字符串的属性)以及临时存储直到重定向后请求的闪存属性。请参见重定向属性闪存属性spring-doc.cadn.net.cn

@ModelAttributespring-doc.cadn.net.cn

对于访问模型中现有属性(如果不存在则实例化)并应用数据绑定和验证。请参见 @ModelAttribute 以及 ModelDataBinderspring-doc.cadn.net.cn

注意 @ModelAttribute 的使用是可选的(例如,设置其属性)。 请参见本表末尾的“任何其他参数”。spring-doc.cadn.net.cn

Errors, BindingResultspring-doc.cadn.net.cn

对于访问验证和数据绑定错误的命令对象 (即,一个 @ModelAttribute 参数)或验证 @RequestBody@RequestPart 参数的错误。您必须声明一个 Errors,或 BindingResult 参数 在经过验证的方法参数之后立即声明。spring-doc.cadn.net.cn

SessionStatus + 类级别的 @SessionAttributesspring-doc.cadn.net.cn

用于标记表单处理完成,这将触发通过类级别@SessionAttributes注解声明的会话属性的清理。 有关更多详细信息,请参见@SessionAttributesspring-doc.cadn.net.cn

UriComponentsBuilderspring-doc.cadn.net.cn

用于根据当前请求的主机、端口、协议、上下文路径以及servlet映射的字面部分准备一个相对URL。请参见URI链接spring-doc.cadn.net.cn

@SessionAttributespring-doc.cadn.net.cn

对于访问任何会话属性,与作为类级别@SessionAttributes声明的结果存储在会话中的模型属性不同。请参见 @SessionAttribute以获取更多详细信息。spring-doc.cadn.net.cn

@RequestAttributespring-doc.cadn.net.cn

用于访问请求属性。有关更多详细信息,请参见 @RequestAttributespring-doc.cadn.net.cn

任何其他参数spring-doc.cadn.net.cn

如果一个方法参数没有匹配到此表中的任何先前值,并且它是简单类型(由 BeanUtils#isSimpleProperty, 它将被解析为 @RequestParam。否则,它将被解析为 @ModelAttributespring-doc.cadn.net.cn

返回值

下表描述了支持的控制器方法返回值。所有返回值都支持响应式类型。spring-doc.cadn.net.cn

控制器方法返回值 描述

@ResponseBodyspring-doc.cadn.net.cn

返回值通过HttpMessageConverter个实现进行转换,并写入响应。请参见@ResponseBodyspring-doc.cadn.net.cn

HttpEntity<B>, ResponseEntity<B>spring-doc.cadn.net.cn

指定完整响应(包括HTTP头和正文)的返回值应通过HttpMessageConverter实现进行转换,并写入响应。 参见ResponseEntityspring-doc.cadn.net.cn

HttpHeadersspring-doc.cadn.net.cn

对于返回带有标头但无正文的响应。spring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

一个视图名称,将通过ViewResolver个实现进行解析,并与隐式模型一起使用——通过命令对象和@ModelAttribute方法确定。处理器方法还可以通过声明Model参数(参见显式注册)来程序化地丰富模型。spring-doc.cadn.net.cn

Viewspring-doc.cadn.net.cn

一个 View 实例用于与隐式模型一起渲染——通过命令对象和 @ModelAttribute 方法确定。处理器方法还可以通过声明一个 Model 参数来程序化地丰富模型(参见 显式注册)。spring-doc.cadn.net.cn

java.util.Map, org.springframework.ui.Modelspring-doc.cadn.net.cn

要添加到隐式模型的属性,视图名称通过RequestToViewNameTranslator隐式确定spring-doc.cadn.net.cn

@ModelAttributespring-doc.cadn.net.cn

要添加到模型中的属性,视图名称通过RequestToViewNameTranslator隐式确定。spring-doc.cadn.net.cn

请注意,@ModelAttribute 是可选的。请参见本表末尾的“任何其他返回值”。spring-doc.cadn.net.cn

ModelAndView 对象spring-doc.cadn.net.cn

要使用的视图和模型属性,以及可选的响应状态。spring-doc.cadn.net.cn

voidspring-doc.cadn.net.cn

具有 void 返回类型(或 null 返回值)的方法被认为完全处理了响应,如果它还具有 ServletResponseOutputStream 参数或 @ResponseStatus 注解。同样,如果控制器进行了积极的 ETaglastModified 时间戳检查(详情请参见控制器)。spring-doc.cadn.net.cn

如果以上都不正确,返回类型为 void 也可以表示“无响应体”对于 REST 控制器,或者对于 HTML 控制器选择默认视图名称。spring-doc.cadn.net.cn

DeferredResult<V>spring-doc.cadn.net.cn

生成上述任何返回值,可以从任何线程异步地完成——例如,作为某些事件或回调的结果。请参见异步请求DeferredResultspring-doc.cadn.net.cn

Callable<V>spring-doc.cadn.net.cn

在 Spring MVC 管理的线程中异步生成上述任何返回值。 见 异步请求Callablespring-doc.cadn.net.cn

ListenableFuture<V>, java.util.concurrent.CompletionStage<V>, java.util.concurrent.CompletableFuture<V>spring-doc.cadn.net.cn

Alternative to DeferredResult, as a convenience (for example, when an underlying service returns one of those).spring-doc.cadn.net.cn

ResponseBodyEmitter, SseEmitterspring-doc.cadn.net.cn

异步发射对象流以写入响应,使用HttpMessageConverter实现。也支持作为ResponseEntity的主体。请参见异步请求HTTP流式传输spring-doc.cadn.net.cn

StreamingResponseBodyspring-doc.cadn.net.cn

异步写入响应 OutputStream。也支持作为ResponseEntity的正文。请参见异步请求HTTP流式传输spring-doc.cadn.net.cn

响应式类型 — Reactor、RxJava 或其他通过 ReactiveAdapterRegistryspring-doc.cadn.net.cn

Alternative to DeferredResult with multi-value streams (for example, Flux, Observable) collected to a List.spring-doc.cadn.net.cn

对于流式场景(例如,text/event-streamapplication/json+stream), SseEmitterResponseBodyEmitter 被用来替代,其中 ServletOutputStream 在 Spring MVC 管理的线程上执行阻塞 I/O 操作,并针对每次写操作应用回压。spring-doc.cadn.net.cn

请参阅 异步请求响应式类型spring-doc.cadn.net.cn

任何其他返回值spring-doc.cadn.net.cn

任何返回值如果不匹配此表中的早期值,并且是Stringvoid,则被视为视图名称(通过RequestToViewNameTranslator进行默认视图名称选择),前提是它不是由BeanUtils#isSimpleProperty确定的简单类型。简单类型的值保持未解析。spring-doc.cadn.net.cn

类型转换

某些注解控制器方法参数表示基于String的请求输入(例如@RequestParam@RequestHeader@PathVariable@MatrixVariable@CookieValue),如果声明的参数类型不是String,可能需要类型转换。spring-doc.cadn.net.cn

对于此类情况,将根据配置的转换器自动应用类型转换。 默认情况下,支持简单类型(intlongDate等)。您可以通过 WebDataBinder(参见 DataBinder) 或通过向 FormattingConversionService 注册 Formatters 来自定义类型转换。 请参见 Spring 字段格式化spring-doc.cadn.net.cn

矩阵变量

RFC 3986 讨论了路径段中的名称值对。在 Spring MVC 中,我们根据 Tim Berners-Lee 的 “旧帖子” 将其称为“矩阵变量”,但它们也可以被称为 URI 路径参数。spring-doc.cadn.net.cn

矩阵变量可以出现在任何路径段中,每个变量以分号分隔,多个值以逗号分隔(例如,/cars;color=red,green;year=2012)。多个值也可以通过重复的变量名指定(例如,color=red;color=green;color=blue)。spring-doc.cadn.net.cn

如果预期URL包含矩阵变量,则控制器方法的请求映射必须使用URI变量来屏蔽该变量内容,以确保无论矩阵变量的顺序和存在与否,请求都能成功匹配。 以下示例使用了矩阵变量:spring-doc.cadn.net.cn

Java
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}
Kotlin
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

    // petId == 42
    // q == 11
}

鉴于所有路径段都可能包含矩阵变量,有时您可能需要区分矩阵变量预期位于哪个路径变量中。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

Java
// 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
}
Kotlin
// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

    // q1 == 11
    // q2 == 22
}

矩阵变量可以被定义为可选的,并且可以指定一个默认值,如下例所示:spring-doc.cadn.net.cn

Java
// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}
Kotlin
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}

要获取所有矩阵变量,你可以使用 MultiValueMap,如下例所示:spring-doc.cadn.net.cn

Java
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
Kotlin
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable matrixVars: MultiValueMap<String, String>,
        @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}

请注意,您需要启用矩阵变量的使用。在MVC Java配置中,您需要通过路径匹配UrlPathHelper设置为removeSemicolonContent=false。在MVC XML命名空间中,您可以设置<mvc:annotation-driven enable-matrix-variables="true"/>spring-doc.cadn.net.cn

@RequestParam

您可以使用@注解将Servlet请求参数(即,查询参数或表单数据)绑定到控制器方法参数。spring-doc.cadn.net.cn

以下示例展示了如何操作:spring-doc.cadn.net.cn

Java
@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...

}
1 使用 @RequestParam 来绑定 petId
Kotlin
import org.springframework.ui.set

@Controller
@RequestMapping("/pets")
class EditPetForm {

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = this.clinic.loadPet(petId);
        model["pet"] = pet
        return "petForm"
    }

    // ...

}
1 使用 @RequestParam 来绑定 petId

默认情况下,使用此注解的方法参数是必需的,但您可以通过将@RequestParam注解的required标志设置为false或将参数声明为java.util.Optional包装器来指定方法参数是可选的。spring-doc.cadn.net.cn

类型转换会自动应用,如果目标方法参数类型不是String。参见类型转换spring-doc.cadn.net.cn

将参数类型声明为数组或列表可以解决同一参数名称的多个参数值。spring-doc.cadn.net.cn

当一个@RequestParam注解被声明为Map<String, String>MultiValueMap<String, String>,并且在注解中没有指定参数名称时,则该映射将使用每个给定参数名称的请求参数值进行填充。spring-doc.cadn.net.cn

请注意,使用 @RequestParam 是可选的(例如,设置其属性)。 默认情况下,任何简单值类型(由 BeanUtils#isSimpleProperty 确定) 且未被任何其他参数解析器解析的参数将被视为带有 @RequestParam 注解。spring-doc.cadn.net.cn

@RequestHeader

您可以使用@RequestHeader注解将请求头绑定到控制器中的方法参数。spring-doc.cadn.net.cn

考虑以下带有头信息的请求:spring-doc.cadn.net.cn

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-EncodingKeep-Alive 的值:spring-doc.cadn.net.cn

Java
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
1 获取 Accept-Encoding 标头的值。
2 获取 Keep-Alive 标头的值。
Kotlin
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
1 获取 Accept-Encoding 标头的值。
2 获取 Keep-Alive 标头的值。

如果目标方法参数类型不是String,将自动应用类型转换。请参见类型转换spring-doc.cadn.net.cn

当一个@RequestHeader注解用于一个Map<String, String>MultiValueMap<String, String>HttpHeaders参数时,该映射将填充所有标头值。spring-doc.cadn.net.cn

内置支持可用于将逗号分隔的字符串转换为字符串数组或集合或其他类型转换系统已知类型的集合。例如,带有@RequestHeader("Accept")注解的方法参数可以是String类型,也可以是String[]List<String>
@CookieValue

您可以使用@CookieValue注解将HTTP cookie的值绑定到控制器方法参数上。spring-doc.cadn.net.cn

考虑一个带有以下cookie的请求:spring-doc.cadn.net.cn

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

以下示例展示了如何获取cookie值:spring-doc.cadn.net.cn

Java
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
1 获取 JSESSIONID cookie 的值。
Kotlin
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
1 获取 JSESSIONID cookie 的值。

如果目标方法参数类型不是String,将自动应用类型转换。请参见类型转换spring-doc.cadn.net.cn

@ModelAttribute

您可以使用 @ModelAttribute 注解在方法参数上访问模型中的属性,或者在不存在时实例化它。模型属性还会被与字段名称匹配的 HTTP Servlet 请求参数值覆盖。这被称为数据绑定,它可以让你免于处理解析和转换单个查询参数和表单字段。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
1 绑定 Pet 的一个实例。
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
1 绑定 Pet 的一个实例。

The Pet 实例的解析如下:spring-doc.cadn.net.cn

虽然使用模型来填充模型属性是一种常见的做法,另一种选择是依赖于Converter<String, T>与URI路径变量约定结合使用。在以下示例中,模型属性名称account与URI路径变量account匹配,并通过传递String账户编号给注册的Converter<String, Account>来加载Accountspring-doc.cadn.net.cn

Java
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) {
    // ...
}
Kotlin
@PutMapping("/accounts/{account}")
fun save(@ModelAttribute("account") account: Account): String {
    // ...
}

在获取模型属性实例后,应用数据绑定。WebDataBinder类将Servlet请求参数名称(查询参数和表单字段)与目标Object上的字段名称进行匹配。在应用类型转换后,匹配的字段将被填充。有关数据绑定(和验证)的更多信息,请参见验证。有关自定义数据绑定的更多信息,请参见DataBinderspring-doc.cadn.net.cn

数据绑定可能会导致错误。默认情况下,会抛出一个BindException。但是,要在控制器方法中检查此类错误,您可以在@ModelAttribute的旁边立即添加一个BindingResult参数,如下例所示:spring-doc.cadn.net.cn

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1 @ModelAttribute 旁边添加一个 BindingResult
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
1 @ModelAttribute 旁边添加一个 BindingResult

在某些情况下,你可能需要访问没有数据绑定的模型属性。对于这种情况,你可以将Model注入到控制器中并直接访问它,或者,设置@ModelAttribute(binding=false),如下例所示:spring-doc.cadn.net.cn

Java
@ModelAttribute
public AccountForm setUpForm() {
    return new AccountForm();
}

@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
    return accountRepository.findOne(accountId);
}

@PostMapping("update")
public String update(@Valid AccountForm form, BindingResult result,
        @ModelAttribute(binding=false) Account account) { (1)
    // ...
}
1 设置 @ModelAttribute(binding=false).
Kotlin
@ModelAttribute
fun setUpForm(): AccountForm {
    return AccountForm()
}

@ModelAttribute
fun findAccount(@PathVariable accountId: String): Account {
    return accountRepository.findOne(accountId)
}

@PostMapping("update")
fun update(@Valid form: AccountForm, result: BindingResult,
           @ModelAttribute(binding = false) account: Account): String { (1)
    // ...
}
1 设置 @ModelAttribute(binding=false).

您可以在数据绑定后自动应用验证,方法是添加javax.validation.Valid注解或Spring的@Validated注解(Bean ValidationSpring 验证)。以下示例展示了如何操作:spring-doc.cadn.net.cn

Java
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
1 验证 Pet 实例。
Kotlin
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}

请注意,使用 @ModelAttribute 是可选的(例如,设置其属性)。 默认情况下,任何不是简单值类型(由 BeanUtils#isSimpleProperty 确定) 且未被任何其他参数解析器解析的参数将被视为带有 @ModelAttribute 注解。spring-doc.cadn.net.cn

@SessionAttributes

@SessionAttributes 用于在HTTP Servlet会话中存储模型属性。它是一种类型级别的注解,声明了特定控制器使用的会话属性。这通常列出了模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以便后续请求访问。spring-doc.cadn.net.cn

以下示例使用了@SessionAttributes注解:spring-doc.cadn.net.cn

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用 @SessionAttributes 注解。
Kotlin
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
1 使用 @SessionAttributes 注解。

在第一次请求时,当一个模型属性(名称为pet)被添加到模型中时,它会自动提升并保存在HTTP Servlet会话中。它将一直保留在那里,直到另一个控制器方法使用SessionStatus方法参数来清除存储,如下例所示:spring-doc.cadn.net.cn

Java
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) {
        if (errors.hasErrors) {
            // ...
        }
            status.setComplete(); (2)
            // ...
        }
    }
}
1 Pet 值存储在Servlet会话中。
2 清除Servlet会话中的Pet值。
Kotlin
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String {
        if (errors.hasErrors()) {
            // ...
        }
        status.setComplete() (2)
        // ...
    }
}
1 Pet 值存储在Servlet会话中。
2 清除Servlet会话中的Pet值。
@SessionAttribute

如果您需要访问预先存在的全局管理的会话属性(即,在控制器之外管理的,例如,通过过滤器)并且这些属性可能不存在,您可以使用@SessionAttributes注解在方法参数上,如下例所示:spring-doc.cadn.net.cn

Java
@RequestMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
1 使用 @SessionAttribute 注解。
Kotlin
@RequestMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}

对于需要添加或删除会话属性的使用场景,请考虑将 org.springframework.web.context.request.WebRequestjavax.servlet.http.HttpSession 注入控制器方法中。spring-doc.cadn.net.cn

用于在控制器工作流程中将模型属性临时存储在会话中,请考虑使用 @SessionAttributes,如在 @SessionAttributes 中所述。spring-doc.cadn.net.cn

@RequestAttribute

类似于@SessionAttribute,你可以使用@RequestAttribute注解来访问先前创建的请求属性(例如,由Servlet FilterHandlerInterceptor创建的):spring-doc.cadn.net.cn

Java
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
1 使用 @RequestAttribute 注解。
Kotlin
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
1 使用 @RequestAttribute 注解。
重定向属性

默认情况下,所有模型属性都被视为会在重定向URL中作为URI模板变量暴露。在剩余的属性中,那些原始类型或原始类型集合或数组的属性会自动作为查询参数附加。spring-doc.cadn.net.cn

将原始类型属性作为查询参数附加可能是期望的结果,如果模型实例是专门为重定向准备的。但是,在注解控制器中,模型可能包含为渲染目的添加的其他属性(例如,下拉字段值)。为了避免这些属性出现在URL中,可以声明一个类型为@RequestMapping的方法参数,并使用它来指定要提供给RedirectView的确切属性。如果方法确实重定向,则使用RedirectAttributes的内容。否则,使用模型的内容。spring-doc.cadn.net.cn

The RequestMappingHandlerAdapter 提供了一个名为 ignoreDefaultModelOnRedirect 的标志,你可以使用它来指示默认情况下 Model 的内容如果被控制器方法重定向则永远不应使用。相反,控制器 方法应声明一个类型为 RedirectAttributes 的属性,或者如果不这样做, 则不应将任何属性传递给 RedirectView。MVC 命名空间和 MVC Java 配置都保持此标志设置为 false,以保持向后兼容性。 但是,对于新应用程序,我们建议将其设置为 truespring-doc.cadn.net.cn

请注意,来自当前请求的URI模板变量在扩展重定向URL时会自动可用,您不需要显式地通过ModelRedirectAttributes添加它们。以下示例展示了如何定义重定向:spring-doc.cadn.net.cn

Java
@PostMapping("/files/{path}")
public String upload(...) {
    // ...
    return "redirect:files/{path}";
}
Kotlin
@PostMapping("/files/{path}")
fun upload(...): String {
    // ...
    return "redirect:files/{path}"
}

另一种传递数据到重定向目标的方法是使用闪存属性。与其它重定向属性不同,闪存属性保存在HTTP会话中(因此不会出现在URL中)。有关更多信息,请参见闪存属性spring-doc.cadn.net.cn

Flash 属性

闪存属性提供了一种方式,使得一个请求可以存储打算在另一个请求中使用的属性。这通常在重定向时需要——例如,在Post-Redirect-Get模式中。闪存属性在重定向之前临时保存(通常在会话中),以便在重定向后的请求中使用,并且会立即移除。spring-doc.cadn.net.cn

Spring MVC 有两个主要的抽象来支持闪现属性。FlashMap 用于保存闪现属性,而 FlashMapManager 用于存储、检索和管理 FlashMap 实例。spring-doc.cadn.net.cn

Flash attribute 支持始终是“开启”的,不需要显式启用。但是,如果未使用,它永远不会导致 HTTP 会话的创建。在每个请求中,都有一个“输入”FlashMap,其中包含从上一个请求传递过来的属性(如果有),以及一个“输出”FlashMap,其中包含要保存以供后续请求使用的属性。这两个FlashMap实例可以从 Spring MVC 中的任何地方通过RequestContextUtils中的静态方法访问。spring-doc.cadn.net.cn

注解控制器通常不需要直接与 FlashMap 交互。相反,一个 @RequestMapping 方法可以接受一个类型为 RedirectAttributes 的参数,并使用它来为重定向场景添加闪存属性。通过 RedirectAttributes 添加的闪存属性会自动传播到“输出”FlashMap。类似地,在重定向之后,“输入”FlashMap 中的属性会自动添加到目标 URL 所服务的控制器的 Model 中。spring-doc.cadn.net.cn

将请求与闪存属性匹配

闪存属性的概念在许多其他Web框架中都存在,并且有时被证明会暴露出并发问题。这是因为,按定义,闪存属性需要存储到下一个请求。然而,这个“下一个”请求可能并不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,闪存属性会被过早删除。spring-doc.cadn.net.cn

为了减少此类问题的可能性,RedirectView 会自动在 FlashMap 实例上“盖章”目标重定向URL的路径和查询参数。反过来,默认的 FlashMapManager 在查找“输入”FlashMap 时会匹配这些信息到传入的请求。spring-doc.cadn.net.cn

这并不能完全消除并发问题的可能性,但通过重定向URL中已有的信息大大减少了这种可能性。因此,我们建议您主要在重定向场景中使用闪现属性。spring-doc.cadn.net.cn

多部分

MultipartResolver启用 后,POST 请求中包含 multipart/form-data 的内容将被解析并可作为常规请求参数访问。以下示例访问了一个常规表单字段和一个上传文件:spring-doc.cadn.net.cn

Java
@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(@RequestParam("name") String name,
            @RequestParam("file") MultipartFile file) {

        if (!file.isEmpty()) {
            byte[] bytes = file.getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}
Kotlin
@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(@RequestParam("name") name: String,
                        @RequestParam("file") file: MultipartFile): String {

        if (!file.isEmpty) {
            val bytes = file.bytes
            // store the bytes somewhere
            return "redirect:uploadSuccess"
        }
        return "redirect:uploadFailure"
    }
}

将参数类型声明为List<MultipartFile>允许为同一个参数名称解析多个文件。spring-doc.cadn.net.cn

@RequestParam 注解被声明为 Map<String, MultipartFile>MultiValueMap<String, MultipartFile> 时,如果没有在注解中指定参数名称,则会使用每个给定参数名称的multipart文件填充映射。spring-doc.cadn.net.cn

使用Servlet 3.0的多部分解析,您还可以声明javax.servlet.http.Part,而不是Spring的MultipartFile,作为方法参数或集合值类型。

您也可以将multipart内容作为数据绑定到命令对象的一部分使用。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下例所示:spring-doc.cadn.net.cn

Java
class MyForm {

    private String name;

    private MultipartFile file;

    // ...
}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        if (!form.getFile().isEmpty()) {
            byte[] bytes = form.getFile().getBytes();
            // store the bytes somewhere
            return "redirect:uploadSuccess";
        }
        return "redirect:uploadFailure";
    }
}
Kotlin
class MyForm(val name: String, val file: MultipartFile, ...)

@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(form: MyForm, errors: BindingResult): String {
        if (!form.file.isEmpty) {
            val bytes = form.file.bytes
            // store the bytes somewhere
            return "redirect:uploadSuccess"
        }
        return "redirect:uploadFailure"
    }
}

多部分请求也可以在RESTful服务场景中从非浏览器客户端提交。以下示例展示了一个包含JSON的文件:spring-doc.cadn.net.cn

POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

你可以通过使用 @RequestParam 作为 String 来访问 "meta-data" 部分,但你可能希望它从 JSON 反序列化(类似于 @RequestBody)。使用 @RequestPart 注解来在将其与 HttpMessageConverter 转换后访问 multipart。spring-doc.cadn.net.cn

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") MultipartFile file) {
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData,
        @RequestPart("file-data") file: MultipartFile): String {
    // ...
}

您可以使用 @RequestPart 结合 javax.validation.Valid 或使用 Spring 的 @Validated 注解,这两种方式都会导致 Standard Bean Validation 被应用。 默认情况下,验证错误会导致 MethodArgumentNotValidException,这会被转换为 400 (BAD_REQUEST) 响应。或者,您可以在控制器中通过 ErrorsBindingResult 参数本地处理验证错误, 如下例所示:spring-doc.cadn.net.cn

Java
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
        BindingResult result) {
    // ...
}
Kotlin
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
        result: BindingResult): String {
    // ...
}
@RequestBody

您可以使用 @RequestBody 注解来读取和反序列化请求体为一个 对象 通过一个 转换器。 以下示例使用了一个 参数spring-doc.cadn.net.cn

Java
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}

您可以使用消息转换器选项来配置或自定义消息转换。spring-doc.cadn.net.cn

您可以使用 @RequestBody 结合 javax.validation.Valid 或 Spring 的 @Validated 注解,这两种方式都会导致 Standard Bean Validation 被应用。默认情况下,验证错误会导致 MethodArgumentNotValidException,这会被转换为 400 (BAD_REQUEST) 响应。或者,您也可以在控制器中通过 ErrorsBindingResult 参数本地处理验证错误,如下例所示:spring-doc.cadn.net.cn

Java
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, BindingResult result) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
    // ...
}
HttpEntity

HttpEntity 基本上等同于使用 @RequestBody,但它是基于一个暴露请求头和正文的容器对象。以下示例展示了这一点:spring-doc.cadn.net.cn

Java
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
Kotlin
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
@ResponseBody

您可以使用 @ResponseBody 注解在方法上,以将返回序列化到响应体中,通过一个 HttpMessageConverter。 以下示例显示了一个例子:spring-doc.cadn.net.cn

Java
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
Kotlin
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}

@ResponseBody 在类级别也受支持,在这种情况下,它将被所有控制器方法继承。这是 @RestController 的效果,它实际上只是一个用 @Controller@ResponseBody 标记的元注解。spring-doc.cadn.net.cn

您可以使用 @ResponseBody 与响应式类型。 请参阅 异步请求响应式类型 以获取更多详细信息。spring-doc.cadn.net.cn

您可以使用消息转换器选项来配置或自定义消息转换。spring-doc.cadn.net.cn

你可以结合使用@ResponseBody方法与JSON序列化视图。 有关详细信息,请参阅Jackson JSONspring-doc.cadn.net.cn

响应实体

ResponseEntity 类似于 @ResponseBody,但带有状态和头信息。例如:spring-doc.cadn.net.cn

Java
@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}
Kotlin
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body = ...
    val etag = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}

Spring MVC 支持使用单个值 响应式类型 来异步生成 ResponseEntity,和/或使用单值和多值响应式类型作为响应体。spring-doc.cadn.net.cn

Jackson JSON

Spring 提供对 Jackson JSON 库的支持。spring-doc.cadn.net.cn

JSON 视图

Spring MVC 提供了内置支持来使用 Jackson 的序列化视图, 这允许在 Object 中只渲染一部分字段。要与 @ResponseBodyResponseEntity 控制器方法一起使用,可以使用 Jackson 的 @JsonView 注解来激活一个序列化视图类,如下例所示:spring-doc.cadn.net.cn

Java
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    private String username;
    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @JsonView(WithoutPasswordView.class)
    public String getUsername() {
        return this.username;
    }

    @JsonView(WithPasswordView.class)
    public String getPassword() {
        return this.password;
    }
}
Kotlin
@RestController
class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView::class)
    fun getUser() = User("eric", "7!jd#h23")
}

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String) {

    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}
@JsonView 允许一个视图类数组,但每个控制器方法只能指定一个。如果你需要激活多个视图,可以使用复合接口。

如果你想通过编程方式执行上述操作,而不是声明一个@JsonView注解, 请将返回值包装在MappingJacksonValue中,并使用它来提供序列化视图:spring-doc.cadn.net.cn

Java
@RestController
public class UserController {

    @GetMapping("/user")
    public MappingJacksonValue getUser() {
        User user = new User("eric", "7!jd#h23");
        MappingJacksonValue value = new MappingJacksonValue(user);
        value.setSerializationView(User.WithoutPasswordView.class);
        return value;
    }
}
Kotlin
@RestController
class UserController {

    @GetMapping("/user")
    fun getUser(): MappingJacksonValue {
        val value = MappingJacksonValue(User("eric", "7!jd#h23"))
        value.serializationView = User.WithoutPasswordView::class.java
        return value
    }
}

对于依赖视图解析的控制器,您可以将序列化视图类添加到模型中,如下例所示:spring-doc.cadn.net.cn

Java
@Controller
public class UserController extends AbstractController {

    @GetMapping("/user")
    public String getUser(Model model) {
        model.addAttribute("user", new User("eric", "7!jd#h23"));
        model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);
        return "userView";
    }
}
Kotlin
import org.springframework.ui.set

@Controller
class UserController : AbstractController() {

    @GetMapping("/user")
    fun getUser(model: Model): String {
        model["user"] = User("eric", "7!jd#h23")
        model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java
        return "userView"
    }
}

1.3.4. 模型

您可以使用@ModelAttribute注解:spring-doc.cadn.net.cn

  • 方法参数 中,在 @RequestMapping 个方法 中创建或访问模型中的 Object 并通过 WebDataBinder 将其绑定到请求。spring-doc.cadn.net.cn

  • 作为方法级别的注解在@Controller@ControllerAdvice类中,这些类有助于在任何@RequestMapping方法调用之前初始化模型。spring-doc.cadn.net.cn

  • @RequestMapping 方法上标记其返回值为模型属性。spring-doc.cadn.net.cn

本节讨论 @ModelAttribute 方法——前述列表中的第二项。
控制器可以有任意数量的 @ModelAttribute 方法。所有这些方法都在同一控制器中的 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器之间共享。有关详细信息,请参阅控制器建议部分。spring-doc.cadn.net.cn

@ModelAttribute 方法具有灵活的方法签名。它们支持与 @RequestMapping 方法相同的许多参数,但不包括 @ModelAttribute 本身或任何与请求正文相关的内容。spring-doc.cadn.net.cn

以下示例展示了一个@ModelAttribute方法:spring-doc.cadn.net.cn

Java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}

以下示例仅添加了一个属性:spring-doc.cadn.net.cn

Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number)
}
当名称未明确指定时,将根据 Object 类型选择一个默认名称,如 Conventions 的 javadoc 中所述。您始终可以通过使用重载的 addAttribute 方法或通过 name 属性在 @ModelAttribute 上(对于返回值)来分配一个显式名称。

您还可以在@RequestMapping方法上使用@ModelAttribute作为方法级注解, 在这种情况下,@RequestMapping方法的返回值将被解释为模型属性。这通常不是必需的,因为这是HTML控制器中的默认行为, 除非@RequestMapping方法的返回值是String,否则会被解释为视图名称。@ModelAttribute还可以自定义模型属性名称,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}

1.3.5. DataBinder

@Controller@ControllerAdvice 个类可以有 @InitBinder 个方法来初始化 WebDataBinder 的实例,这些方法反过来可以:spring-doc.cadn.net.cn

  • 将请求参数(即表单或查询数据)绑定到模型对象。spring-doc.cadn.net.cn

  • 将基于字符串的请求值(例如请求参数、路径变量、标头、cookie 等)转换为目标类型的控制器方法参数。spring-doc.cadn.net.cn

  • Format model object values as String values when rendering HTML forms.spring-doc.cadn.net.cn

@InitBinder 方法可以注册控制器特定的 java.bean.PropertyEditor 或 Spring ConverterFormatter 组件。此外,您可以使用 MVC 配置 来在全局共享的 FormattingConversionService 中注册 ConverterFormatter 类型。spring-doc.cadn.net.cn

@InitBinder 方法支持许多与 @RequestMapping 方法相同的参数,但不包括 @ModelAttribute(命令对象)参数。通常,它们声明为具有 WebDataBinder 参数(用于注册)和 void 返回值。以下示例显示了一个示例:spring-doc.cadn.net.cn

Java
@Controller
public class FormController {

    @InitBinder (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}
1 定义一个@InitBinder方法。
Kotlin
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}
1 定义一个@InitBinder方法。

Alternatively, when you use a Formatter-based setup through a shared FormattingConversionService, you can re-use the same approach and register controller-specific Formatter implementations, as the following example shows:spring-doc.cadn.net.cn

Java
@Controller
public class FormController {

    @InitBinder (1)
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
    }

    // ...
}
1 在自定义格式化器上定义一个@InitBinder方法。
Kotlin
@Controller
class FormController {

    @InitBinder (1)
    protected fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))
    }

    // ...
}
1 在自定义格式化器上定义一个@InitBinder方法。
模型设计

在Web应用程序的上下文中,数据绑定涉及将HTTP请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象的属性。spring-doc.cadn.net.cn

public 个属性遵循 JavaBeans命名约定 并公开用于数据绑定 —— 例如,public String getFirstName()public void setFirstName(String) 方法用于 firstName 属性。spring-doc.cadn.net.cn

模型对象及其嵌套的对象图有时也被称为命令对象表单支持对象POJO(简单的Java对象)。

默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着你需要仔细考虑模型有哪些公共属性,因为客户端可能会针对任何公共属性路径进行操作,甚至包括那些在特定使用场景下不期望被操作的属性。spring-doc.cadn.net.cn

例如,给定一个HTTP表单数据端点,恶意客户端可能会提供模型对象图中存在的属性值,但这些属性并不包含在浏览器中显示的HTML表单中。这可能会导致数据被设置在模型对象及其任何嵌套对象上,而这些数据是不期望被更新的。spring-doc.cadn.net.cn

推荐的做法是使用一个专用的模型对象,该对象仅公开与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单中,模型对象应声明一组最小的属性,如以下ChangeEmailForm所示。spring-doc.cadn.net.cn

public class ChangeEmailForm {

    private String oldEmailAddress;
    private String newEmailAddress;

    public void setOldEmailAddress(String oldEmailAddress) {
        this.oldEmailAddress = oldEmailAddress;
    }

    public String getOldEmailAddress() {
        return this.oldEmailAddress;
    }

    public void setNewEmailAddress(String newEmailAddress) {
        this.newEmailAddress = newEmailAddress;
    }

    public String getNewEmailAddress() {
        return this.newEmailAddress;
    }

}

如果你不能或不想为每个数据绑定用例使用一个专用的模型对象,你必须限制允许进行数据绑定的属性。理想情况下,你可以通过在WebDataBinder上使用setAllowedFields()方法来注册允许的字段模式spring-doc.cadn.net.cn

例如,要在应用程序中注册允许的字段模式,您可以在@InitBinder方法中实现一个@Controller@ControllerAdvice组件,如下所示:spring-doc.cadn.net.cn

@Controller
public class ChangeEmailController {

    @InitBinder
    void initBinder(WebDataBinder binder) {
        binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
    }

    // @RequestMapping methods, etc.

}

除了注册允许的模式外,还可以通过setDisallowedFields()方法在DataBinder及其子类中注册不允许的字段模式。请注意,“白名单”比“黑名单”更安全。因此,建议优先使用setAllowedFields()而不是setDisallowedFields()spring-doc.cadn.net.cn

请注意,与允许的字段模式匹配是区分大小写的;而与不允许的字段模式匹配是不区分大小写的。此外,即使某个字段也恰好匹配允许列表中的模式,但如果它匹配了不允许的模式,该字段也不会被接受。spring-doc.cadn.net.cn

正确配置允许和不允许的字段模式非常重要,尤其是在直接暴露领域模型以进行数据绑定时。否则,这将是一个很大的安全风险。spring-doc.cadn.net.cn

此外,强烈建议您不要在数据绑定场景中使用来自领域模型(如JPA或Hibernate实体)的类型作为模型对象。spring-doc.cadn.net.cn

1.3.6. 异常

@Controller@ControllerAdvice 类可以包含 @ExceptionHandler 方法来处理控制器方法中的异常,如下例所示:spring-doc.cadn.net.cn

Java
@Controller
public class SimpleController {

    // ...

    @ExceptionHandler
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
Kotlin
@Controller
class SimpleController {

    // ...

    @ExceptionHandler
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}

异常可能与正在传播的顶级异常匹配(即,直接抛出的 IOException)或与顶级包装异常中的直接原因匹配(例如,IOException 被包装在 IllegalStateException 中)。spring-doc.cadn.net.cn

对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的例子所示。当多个异常方法匹配时,通常优先选择根异常匹配而不是原因异常匹配。更具体地说,ExceptionDepthComparator用于根据异常从抛出异常类型的深度对异常进行排序。spring-doc.cadn.net.cn

或者,注解声明可以缩小异常类型的范围以匹配,如下例所示:spring-doc.cadn.net.cn

Java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
    // ...
}
Kotlin
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: IOException): ResponseEntity<String> {
    // ...
}

你甚至可以使用一个特定异常类型的列表,并带有非常通用的参数签名,如下例所示:spring-doc.cadn.net.cn

Java
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
    // ...
}
Kotlin
@ExceptionHandler(FileSystemException::class, RemoteException::class)
fun handle(ex: Exception): ResponseEntity<String> {
    // ...
}

根异常和原因异常匹配之间的区别可能会令人惊讶。spring-doc.cadn.net.cn

在显示的IOException变体中,该方法通常以实际的FileSystemExceptionRemoteException实例作为参数调用, 因为它们都从IOException继承。但是,如果任何此类匹配的异常在包装异常中传播,而该包装异常本身是一个IOException, 则传递的异常实例是该包装异常。spring-doc.cadn.net.cn

行为在handle(Exception)变体中甚至更简单。这总是在包装场景中被调用,带有包装异常,在这种情况下,实际匹配的异常可以通过ex.getCause()找到。传递的异常只有在这些作为顶级异常抛出时才是实际的FileSystemExceptionRemoteException实例。spring-doc.cadn.net.cn

我们通常建议你在参数签名中尽可能具体,减少根异常类型和原因异常类型之间不匹配的潜在可能性。考虑将一个多匹配方法拆分为单独的@ExceptionHandler方法,每个方法通过其签名匹配一个特定的异常类型。spring-doc.cadn.net.cn

在多@ControllerAdvice配置中,我们建议您在@ControllerAdvice上声明您的主要根异常映射,并指定相应的顺序。虽然优先选择根异常匹配而不是原因,但这在给定控制器或@ControllerAdvice类的方法中定义。这意味着在优先级更高的@ControllerAdvice bean上的原因匹配优先于在优先级更低的@ControllerAdvice bean上的任何匹配(例如,根异常)。spring-doc.cadn.net.cn

最后但并非最不重要的是,一个@ExceptionHandler方法实现可以选择通过以原始形式重新抛出给定的异常实例来退出处理。这在你只对顶级匹配或在无法静态确定的具体上下文中的匹配感兴趣的情况下很有用。重新抛出的异常会继续通过剩余的解析链传播,就像给定的@ExceptionHandler方法根本没有匹配一样。spring-doc.cadn.net.cn

Spring MVC 中对 @ExceptionHandler 方法的支持是基于 DispatcherServlet 级别的 HandlerExceptionResolver 机制。spring-doc.cadn.net.cn

方法参数

@ExceptionHandler 个方法支持以下参数:spring-doc.cadn.net.cn

方法参数 描述

异常类型spring-doc.cadn.net.cn

获取引发的异常。spring-doc.cadn.net.cn

HandlerMethodspring-doc.cadn.net.cn

获取引发异常的控制器方法。spring-doc.cadn.net.cn

WebRequest, NativeWebRequestspring-doc.cadn.net.cn

通用访问请求参数以及请求和会话属性,无需直接使用Servlet API。spring-doc.cadn.net.cn

javax.servlet.ServletRequest, javax.servlet.ServletResponsespring-doc.cadn.net.cn

选择任何特定的请求或响应类型(例如,ServletRequestHttpServletRequest 或 Spring 的 MultipartRequestMultipartHttpServletRequest)。spring-doc.cadn.net.cn

javax.servlet.http.HttpSessionspring-doc.cadn.net.cn

强制要求存在会话。因此,此类参数永远不会为null
请注意,会话访问不是线程安全的。如果允许多个请求并发访问会话,请考虑将RequestMappingHandlerAdapter实例的synchronizeOnSession标志设置为truespring-doc.cadn.net.cn

java.security.Principalspring-doc.cadn.net.cn

当前已认证的用户——如果已知,可能是特定的Principal实现类。spring-doc.cadn.net.cn

HttpMethodspring-doc.cadn.net.cn

请求的HTTP方法。spring-doc.cadn.net.cn

java.util.Localespring-doc.cadn.net.cn

当前请求的本地化设置,由最具体的LocaleResolver确定——实际上,是配置的LocaleResolverLocaleContextResolverspring-doc.cadn.net.cn

java.util.TimeZone, java.time.ZoneIdspring-doc.cadn.net.cn

与当前请求相关联的时区,由LocaleContextResolver确定。spring-doc.cadn.net.cn

java.io.OutputStream, java.io.Writerspring-doc.cadn.net.cn

要访问由Servlet API暴露的原始响应体。spring-doc.cadn.net.cn

java.util.Map, org.springframework.ui.Model, org.springframework.ui.ModelMapspring-doc.cadn.net.cn

用于访问错误响应的模型。始终为空。spring-doc.cadn.net.cn

RedirectAttributesspring-doc.cadn.net.cn

指定重定向时要使用的属性(即附加到查询字符串的属性)和暂时存储直到重定向后请求的闪存属性。请参见重定向属性闪存属性spring-doc.cadn.net.cn

@SessionAttributespring-doc.cadn.net.cn

对于访问任何会话属性,与作为类级别@SessionAttributes声明的结果存储在会话中的模型属性不同。请参见 @SessionAttribute以获取更多详细信息。spring-doc.cadn.net.cn

@RequestAttributespring-doc.cadn.net.cn

用于访问请求属性。有关更多详细信息,请参见 @RequestAttributespring-doc.cadn.net.cn

返回值

@ExceptionHandler 方法支持以下返回值:spring-doc.cadn.net.cn

返回值 描述

@ResponseBodyspring-doc.cadn.net.cn

返回值通过 HttpMessageConverter 实例进行转换,并写入响应。请参见 @ResponseBodyspring-doc.cadn.net.cn

HttpEntity<B>, ResponseEntity<B>spring-doc.cadn.net.cn

返回值指定将完整的响应(包括HTTP头和正文)通过HttpMessageConverter实例进行转换并写入响应。 参见ResponseEntityspring-doc.cadn.net.cn

Stringspring-doc.cadn.net.cn

一个视图名称,将通过ViewResolver个实现进行解析,并与隐式模型一起使用——通过命令对象和@ModelAttribute方法确定。处理器方法还可以通过声明一个Model参数(如前所述)来程序化地丰富模型。spring-doc.cadn.net.cn

Viewspring-doc.cadn.net.cn

一个 View 实例用于与隐式模型一起渲染——通过命令对象和 @ModelAttribute 方法确定。处理器方法还可以通过声明一个 Model 参数(如前所述)来程序化地丰富模型。spring-doc.cadn.net.cn

java.util.Map, org.springframework.ui.Modelspring-doc.cadn.net.cn

Attributes to be added to the implicit model with the view name implicitly determined through a RequestToViewNameTranslator.spring-doc.cadn.net.cn

@ModelAttributespring-doc.cadn.net.cn

要添加到模型中的属性,视图名称通过RequestToViewNameTranslator隐式确定。spring-doc.cadn.net.cn

请注意,@ModelAttribute 是可选的。请参见本表末尾的“任何其他返回值”。spring-doc.cadn.net.cn

ModelAndView 对象spring-doc.cadn.net.cn

要使用的视图和模型属性,以及可选的响应状态。spring-doc.cadn.net.cn

voidspring-doc.cadn.net.cn

具有 void 返回类型(或 null 返回值)的方法被认为完全处理了响应,如果它还具有 ServletResponse 一个 OutputStream 参数,或 带有 @ResponseStatus 注解。同样,如果控制器进行了积极的 ETaglastModified 时间戳检查(详情请参见控制器)。spring-doc.cadn.net.cn

如果上述情况都不成立,返回类型为 void 也可以表示“无响应体”对于 REST 控制器或默认视图名称选择对于 HTML 控制器。spring-doc.cadn.net.cn

任何其他返回值spring-doc.cadn.net.cn

如果返回值不匹配上述任何一种情况,并且不是简单类型(由 BeanUtils#isSimpleProperty 确定), 默认情况下,它将被视为要添加到模型中的模型属性。如果是简单类型,则保持未解析。spring-doc.cadn.net.cn

REST API 异常

对于REST服务的一个常见需求是在响应体中包含错误详细信息。Spring框架不会自动执行此操作,因为响应体中的错误详细信息表示是特定于应用程序的。但是,可以使用具有@RestController返回值的方法来设置响应的状态和体。此类方法也可以在@ControllerAdvice类中声明,以便全局应用它们。spring-doc.cadn.net.cn

实现全局异常处理并在响应正文中包含错误详细信息的应用程序应考虑扩展 ResponseEntityExceptionHandler, 它提供了对Spring MVC抛出的异常的处理,并提供了自定义响应正文的挂钩。要使用此功能,请创建一个子类并覆盖必要的方法,然后将其声明为Spring bean。spring-doc.cadn.net.cn

1.3.7. 控制器建议

通常 @ExceptionHandler@InitBinder@ModelAttribute 方法在声明它们的 @Controller 类(或类层次结构)中应用。如果你想让这些方法更全局地应用(跨控制器),你可以将它们声明在一个标注有 @ControllerAdvice@RestControllerAdvice 的类中。spring-doc.cadn.net.cn

@ControllerAdvice 被注解为 @Component,这意味着此类可以通过 组件扫描 注册为 Spring beans。@RestControllerAdvice 是一个组合注解,它被注解为 @ControllerAdvice@ResponseBody,这基本上意味着 @ExceptionHandler 方法将通过消息转换将响应体渲染到响应中(而不是视图解析或模板渲染)。spring-doc.cadn.net.cn

在启动时,@RequestMapping@ExceptionHandler 基础架构类检测带有 @ControllerAdvice 注解的 Spring bean,并在运行时应用它们的方法。全局 @ExceptionHandler 方法(来自 @ControllerAdvice)在本地 @Controller 方法之后应用。相比之下,全局 @ModelAttribute@InitBinder 方法在本地方法之前应用。spring-doc.cadn.net.cn

默认情况下,@ControllerAdvice 方法适用于每个请求(即所有控制器), 但你可以通过在注解上使用属性将范围缩小到一组控制器子集,如下例所示:spring-doc.cadn.net.cn

Java
// 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 {}
Kotlin
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
class ExampleAdvice1

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
class ExampleAdvice2

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
class ExampleAdvice3

在前面的示例中,选择器在运行时进行评估,如果广泛使用可能会对性能产生负面影响。有关详细信息,请参阅 @ControllerAdvice Javadoc。spring-doc.cadn.net.cn

1.4. 功能端点

Spring Web MVC 包括 WebMvc.fn,这是一种轻量级的函数式编程模型,其中使用函数来路由和处理请求,并且契约设计为不可变性。它是基于注解的编程模型的替代方案,但同样运行在相同的 DispatcherServlet 上。spring-doc.cadn.net.cn

1.4.1. 概览

在WebMvc.fn中,一个HTTP请求是通过HandlerFunction来处理的:一个接受ServerRequest并返回ServerResponse的函数。 请求和响应对象都有不可变的契约,提供了对HTTP请求和响应的JDK 8友好的访问。 HandlerFunction相当于基于注解编程模型中的@RequestMapping方法的主体。spring-doc.cadn.net.cn

Incoming requests are routed to a handler function with a RouterFunction: a function that takes ServerRequest and returns an optional HandlerFunction (i.e. Optional<HandlerFunction>). When the router function matches, a handler function is returned; otherwise an empty Optional. RouterFunction is the equivalent of a @RequestMapping annotation, but with the major difference that router functions provide not just data, but also behavior。spring-doc.cadn.net.cn

RouterFunctions.route() 提供了一个路由器构建器,便于创建路由器, 如下例所示:spring-doc.cadn.net.cn

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();


public class PersonHandler {

    // ...

    public ServerResponse listPeople(ServerRequest request) {
        // ...
    }

    public ServerResponse createPerson(ServerRequest request) {
        // ...
    }

    public ServerResponse getPerson(ServerRequest request) {
        // ...
    }
}
Kotlin
import org.springframework.web.servlet.function.router

val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = router { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}


class PersonHandler(private val repository: PersonRepository) {

    // ...

    fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
1 使用路由器DSL创建路由器。

如果您将RouterFunction注册为一个bean,例如通过在@Configuration类中暴露它,它将被servlet自动检测到,如运行服务器中所述。spring-doc.cadn.net.cn

1.4.2. 处理函数

ServerRequestServerResponse 是不可变的接口,它们提供了 JDK 8 友好的访问 HTTP 请求和响应的功能,包括头信息、正文、方法和状态码。spring-doc.cadn.net.cn

服务器请求

ServerRequest 提供对 HTTP 方法、URI、标头和查询参数的访问, 而主体的访问则通过 body 方法提供。spring-doc.cadn.net.cn

以下示例将请求正文提取到 Stringspring-doc.cadn.net.cn

Java
String string = request.body(String.class);
Kotlin
val string = request.body<String>()

以下示例将正文提取到 List<Person>, 其中 Person 对象从序列化形式(如 JSON 或 XML)解码:spring-doc.cadn.net.cn

Java
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
Kotlin
val people = request.body<Person>()

以下示例展示了如何访问参数:spring-doc.cadn.net.cn

Java
MultiValueMap<String, String> params = request.params();
Kotlin
val map = request.params()
服务器响应

ServerResponse 提供对 HTTP 响应的访问,并且由于它是不可变的,您可以使用 build 方法来创建它。您可以使用构建器设置响应状态、添加响应头或提供正文。以下示例创建了一个 200 (OK) 响应,带有 JSON 内容:spring-doc.cadn.net.cn

Java
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
Kotlin
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)

以下示例展示了如何构建一个包含Location标头且没有正文的201(已创建)响应:spring-doc.cadn.net.cn

Java
URI location = ...
ServerResponse.created(location).build();
Kotlin
val location: URI = ...
ServerResponse.created(location).build()
处理器类

我们可以将处理器函数写成 lambda 表达式,如下例所示:spring-doc.cadn.net.cn

Java
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().body("Hello World");
Kotlin
val helloWorld: (ServerRequest) -> ServerResponse =
  { ServerResponse.ok().body("Hello World") }

这很方便,但在应用程序中我们需要多个函数,并且多个内联lambda可能会变得混乱。因此,将相关的处理器函数组合到一个处理器类中是有用的,这个处理器类的作用类似于注解应用中的 @Controller。 例如,以下类公开了一个响应式Person存储库:spring-doc.cadn.net.cn

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public ServerResponse listPeople(ServerRequest request) { (1)
        List<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people);
    }

    public ServerResponse createPerson(ServerRequest request) throws Exception { (2)
        Person person = request.body(Person.class);
        repository.savePerson(person);
        return ok().build();
    }

    public ServerResponse getPerson(ServerRequest request) { (3)
        int personId = Integer.parseInt(request.pathVariable("id"));
        Person person = repository.getPerson(personId);
        if (person != null) {
            return ok().contentType(APPLICATION_JSON).body(person);
        }
        else {
            return ServerResponse.notFound().build();
        }
    }

}
1 listPeople 是一个处理器函数,它返回存储库中找到的所有 Person 对象的 JSON。
2 createPerson 是一个处理器函数,用于存储包含在请求体中的新 Person
3 getPerson 是一个处理器函数,它根据 id 路径变量返回一个单独的人员信息。我们从仓库中检索该 Person 并创建 JSON 响应,如果找到的话。如果没有找到,则返回 404 Not Found 响应。
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: List<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).body(people);
    }

    fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.body<Person>()
        repository.savePerson(person)
        return ok().build()
    }

    fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }
                ?: ServerResponse.notFound().build()

    }
}
1 listPeople 是一个处理器函数,它返回存储库中找到的所有 Person 对象的 JSON。
2 createPerson 是一个处理器函数,用于存储包含在请求体中的新 Person
3 getPerson 是一个处理器函数,它根据 id 路径变量返回一个单独的人员信息。我们从仓库中检索该 Person 并创建 JSON 响应,如果找到的话。如果没有找到,则返回 404 Not Found 响应。
验证

一个功能端点可以使用Spring的验证功能来对请求体进行验证。例如,给定一个自定义的Spring验证器实现对于Personspring-doc.cadn.net.cn

Java
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

    public ServerResponse createPerson(ServerRequest request) {
        Person person = request.body(Person.class);
        validate(person); (2)
        repository.savePerson(person);
        return ok().build();
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
1 创建 Validator 实例。
2 应用验证。
3 对于400响应抛出异常。
Kotlin
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.body<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().build()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person")
        validator.validate(person, errors)
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
1 创建 Validator 实例。
2 应用验证。
3 对于400响应抛出异常。

Handlers can also use the standard bean validation API (JSR-303) by creating and injecting a global Validator instance based on LocalValidatorFactoryBean. See Spring Validation.spring-doc.cadn.net.cn

1.4.3. RouterFunction

Router functions are used to route the requests to the corresponding HandlerFunction. Typically, you do not write router functions yourself, but rather use a method on the RouterFunctions utility class to create one. RouterFunctions.route() (no parameters) provides you with a fluent builder for creating a router function, whereas RouterFunctions.route(RequestPredicate, HandlerFunction) offers a direct way to create a router.spring-doc.cadn.net.cn

通常,建议使用route()构建器,因为它为典型的映射场景提供了方便的快捷方式,而不需要难以发现的静态导入。 例如,路由器函数构建器提供了方法GET(String, HandlerFunction)来创建GET请求的映射;以及POST(String, HandlerFunction)用于POST请求。spring-doc.cadn.net.cn

除了基于HTTP方法的映射外,路由构建器还提供了一种在映射到请求时引入额外谓词的方法。对于每种HTTP方法,都有一个重载变体,该变体接受一个RequestPredicate作为参数,通过该参数可以表达额外的约束条件。spring-doc.cadn.net.cn

谓词

你可以编写自己的 RequestPredicate,但是RequestPredicates工具类提供了常用的实现,这些实现基于请求路径、HTTP方法、内容类型等。 以下示例使用请求谓词来根据Accept头创建约束:spring-doc.cadn.net.cn

Java
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().body("Hello World")).build();
Kotlin
import org.springframework.web.servlet.function.router

val route = router {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().body("Hello World")
    }
}

你可以通过以下方式组合多个请求谓词:spring-doc.cadn.net.cn

许多来自RequestPredicates的谓词是组合的。 例如,RequestPredicates.GET(String)是由RequestPredicates.method(HttpMethod)RequestPredicates.path(String)组合而成的。 上面显示的例子还使用了两个请求谓词,因为构建器内部使用 RequestPredicates.GET,并将其与accept谓词组合。spring-doc.cadn.net.cn

路由

路由函数按顺序进行评估:如果第一个路由不匹配,则评估第二个路由,依此类推。因此,建议将更具体的路由声明在更通用的路由之前。请注意,这种行为与基于注解的编程模型不同,在基于注解的编程模型中,会自动选择“最具体的”控制器方法。spring-doc.cadn.net.cn

在使用路由器功能构建器时,所有定义的路由都将组合成一个RouterFunction,该RouterFunctionbuild()返回。 还有其他方法可以将多个路由器功能组合在一起:spring-doc.cadn.net.cn

以下示例展示了四个路由的组成:spring-doc.cadn.net.cn

Java
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
1 GET /person/{id} 带有匹配JSON的 Accept 标头被路由到 PersonHandler.getPerson
2 GET /person 带有匹配JSON的 Accept 标头被路由到 PersonHandler.listPeople
3 POST /person 没有附加谓词时映射到 PersonHandler.createPerson,并且
4 otherRoute 是一个在其他地方创建并添加到路由构建中的路由器函数。
Kotlin
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.web.servlet.function.router

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute = router {  }

val route = router {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
1 GET /person/{id} 带有匹配JSON的 Accept 标头被路由到 PersonHandler.getPerson
2 GET /person 带有匹配JSON的 Accept 标头被路由到 PersonHandler.listPeople
3 POST /person 没有附加谓词时映射到 PersonHandler.createPerson,并且
4 otherRoute 是一个在其他地方创建并添加到路由构建中的路由器函数。
嵌套路由

一组路由器函数通常会有一个共享的谓词,例如一个共享的路径。 在上面的例子中,共享的谓词将是一个匹配/person的路径谓词,被三条路由使用。 当使用注解时,你可以通过使用类型级别的@RequestMapping注解来消除这种重复。 在WebMvc.fn中,可以通过路由器函数构建器上的path方法来共享路径谓词。 例如,上面的最后几行可以通过使用嵌套路由来改进:spring-doc.cadn.net.cn

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET("", accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .build();
1 请注意,第二个参数path是一个接受路由器构建器的消费者。
Kotlin
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET("", accept(APPLICATION_JSON), handler::listPeople)
        POST("/person", handler::createPerson)
    }
}

尽管基于路径的嵌套是最常见的,但你可以通过使用构建器上的nest方法来根据任何类型的谓词进行嵌套。 上述代码中仍然包含一些重复的部分,即共享的Accept-header谓词。 我们可以通过结合使用nest方法和accept来进行进一步优化:spring-doc.cadn.net.cn

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .build();
Kotlin
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
        }
    }
}

1.4.4. 运行服务器

您通常在基于DispatcherHandler的设置中通过MVC配置运行路由功能,该配置使用Spring配置来声明处理请求所需的组件。MVC Java配置声明以下基础设施组件以支持功能性端点:spring-doc.cadn.net.cn

  • RouterFunctionMapping: 检测一个或多个RouterFunction<?> beans 在 Spring 配置中,通过RouterFunction.andOther 将它们组合,并将请求路由到组合后的RouterFunctionspring-doc.cadn.net.cn

  • HandlerFunctionAdapter: 简单的适配器,允许DispatcherHandler调用 一个被映射到请求的HandlerFunctionspring-doc.cadn.net.cn

上述组件让功能端点适应DispatcherServlet请求处理生命周期,并且(潜在地)与注解控制器并行运行,如果声明了任何注解控制器的话。这也是Spring Boot WebStarters如何启用功能端点的方式。spring-doc.cadn.net.cn

以下示例展示了一个WebFlux Java配置:spring-doc.cadn.net.cn

Java
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
Kotlin
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}

1.4.5. 过滤处理器函数

你可以通过使用路由函数构建器上的 beforeafterfilter 方法来过滤处理器函数。通过注解,你可以使用 @ControllerAdvice、一个 ServletFilter 或两者结合来实现类似的功能。过滤器将应用于由构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不会应用到“顶级”路由上。 例如,考虑以下示例:spring-doc.cadn.net.cn

Java
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
1 添加自定义请求头的before过滤器仅应用于两个GET路由。
2 The after 过滤器会记录响应,它被应用于所有路由,包括嵌套的路由。
Kotlin
import org.springframework.web.servlet.function.router

val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
    }
    POST("/person", handler::createPerson)
    after { _, response -> (2)
        logResponse(response)
    }
}
1 添加自定义请求头的before过滤器仅应用于两个GET路由。
2 The after 过滤器会记录响应,它被应用于所有路由,包括嵌套的路由。

The filter 方法在路由构建器上接受一个 HandlerFilterFunction: 一个接受 ServerRequestHandlerFunction 并返回一个 ServerResponse 的函数。该处理器函数参数表示链中的下一个元素。这通常是被路由到的处理器,但也可能是应用了多个过滤器时的另一个过滤器。spring-doc.cadn.net.cn

现在我们可以为我们的路由添加一个简单的安全过滤器,假设我们有一个SecurityManager可以判断特定路径是否允许。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

Java
SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET("", handler::listPeople))
        .POST("/person", handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
Kotlin
import org.springframework.web.servlet.function.router

val securityManager: SecurityManager = ...

val route = router {
    ("/person" and accept(APPLICATION_JSON)).nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        POST("/person", handler::createPerson)
        filter { request, next ->
            if (securityManager.allowAccessTo(request.path())) {
                next(request)
            }
            else {
                status(UNAUTHORIZED).build();
            }
        }
    }
}

上一个示例演示了调用 next.handle(ServerRequest) 是可选的。 我们只在允许访问时才运行处理器函数。spring-doc.cadn.net.cn

除了使用路由器功能构建器上的 filter 方法,还可以通过 RouterFunction.filter(HandlerFilterFunction) 将过滤器应用于现有的路由器功能。spring-doc.cadn.net.cn

CORS支持通过专门的CorsFilter为功能性端点提供。

1.5. URI 链接

本节描述了Spring框架中处理URI的各种选项。spring-doc.cadn.net.cn

1.5.1. UriComponents

Spring MVC 和 Spring WebFluxspring-doc.cadn.net.cn

UriComponentsBuilder 帮助根据带有变量的URI模板构建URI,如下例所示:spring-doc.cadn.net.cn

Java
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
1 带有URI模板的静态工厂方法。
2 添加或替换 URI 组件。
3 请求对 URI 模板和 URI 变量进行编码。
4 构建一个UriComponents
5 展开变量并获取URI
Kotlin
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build() (4)

val uri = uriComponents.expand("Westin", "123").toUri()  (5)
1 带有URI模板的静态工厂方法。
2 添加或替换 URI 组件。
3 请求对 URI 模板和 URI 变量进行编码。
4 构建一个UriComponents
5 展开变量并获取URI

前一个示例可以合并为一个链并用 buildAndExpand 缩短, 如下例所示:spring-doc.cadn.net.cn

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri()

你可以进一步缩短它,直接使用 URI(这隐含了编码),如下例所示:spring-doc.cadn.net.cn

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")

你可以进一步使用完整的URI模板来缩短它,如下例所示:spring-doc.cadn.net.cn

Java
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");
Kotlin
val uri = UriComponentsBuilder
    .fromUriString("https://example.com/hotels/{hotel}?q={q}")
    .build("Westin", "123")

1.5.2. UriBuilder

Spring MVC 和 Spring WebFluxspring-doc.cadn.net.cn

UriComponentsBuilder 实现 UriBuilder. 您可以创建一个 UriBuilder,进而使用 UriBuilderFactory. 一起,UriBuilderFactoryUriBuilder 提供了一个可插拔的机制来根据 URI 模板构建 URI,基于共享配置,例如基础 URL、编码偏好和其他细节。spring-doc.cadn.net.cn

您可以使用 RestTemplateWebClient 配置 UriBuilderFactory 来自定义 URI 的准备。DefaultUriBuilderFactoryUriBuilderFactory 的默认实现,内部使用 UriComponentsBuilder 并公开共享的配置选项。spring-doc.cadn.net.cn

以下示例展示了如何配置一个RestTemplatespring-doc.cadn.net.cn

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory

以下示例配置了一个WebClientspring-doc.cadn.net.cn

Java
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val client = WebClient.builder().uriBuilderFactory(factory).build()

此外,您还可以直接使用 DefaultUriBuilderFactory。它类似于使用 UriComponentsBuilder,但不同的是,它是一个实际的实例,用于保存配置和偏好设置,如下例所示:spring-doc.cadn.net.cn

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
Kotlin
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")

1.5.3. URI编码

Spring MVC 和 Spring WebFluxspring-doc.cadn.net.cn

UriComponentsBuilder 在两个级别上提供了编码选项:spring-doc.cadn.net.cn

两种选项都会将非ASCII字符和非法字符替换为转义的八进制数。然而,第一个选项还会替换URI变量中具有保留意义的字符。spring-doc.cadn.net.cn

考虑 ";",它在路径中是合法的,但具有保留含义。第一个选项会在 URI 变量中将 ";" 替换为 "%3B",但在 URI 模板中不会替换。相比之下,第二个选项从不替换 ";",因为它在路径中是合法的字符。

在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量作为不透明数据进行完全编码,而第二个选项只有在 URI 变量故意包含保留字符时才有用。spring-doc.cadn.net.cn

以下示例使用了第一种选项:spring-doc.cadn.net.cn

Java
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"
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri()

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"

你可以通过直接访问 URI(这隐含了编码)来缩短前面的例子,如下例所示:spring-doc.cadn.net.cn

Java
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar");
Kotlin
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")

你可以进一步使用完整的URI模板来缩短它,如下例所示:spring-doc.cadn.net.cn

Java
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar");
Kotlin
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")

The WebClient and the RestTemplate expand and encode URI templates internally through the UriBuilderFactory strategy. Both can be configured with a custom strategy. as the following example shows:spring-doc.cadn.net.cn

Java
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
Kotlin
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()

The DefaultUriBuilderFactory implementation uses UriComponentsBuilder internally to expand and encode URI templates. As a factory, it provides a single place to configure the approach to encoding, based on one of the below encoding modes:spring-doc.cadn.net.cn

  • TEMPLATE_AND_VALUES: 使用 UriComponentsBuilder#encode(),对应于前面列表中的第一个选项,用于预编码URI模板,并在扩展时严格编码URI变量。spring-doc.cadn.net.cn

  • VALUES_ONLY: 不对URI模板进行编码,而是通过UriUtils#encodeUriUriVariables在将URI变量展开到模板之前应用严格的编码。spring-doc.cadn.net.cn

  • URI_COMPONENT: 使用 UriComponents#encode(),对应于前面列表中的第二个选项,用于在 URI 变量扩展后对 URI 组件值进行编码。spring-doc.cadn.net.cn

  • NONE: 未应用任何编码。spring-doc.cadn.net.cn

The RestTemplate is set to EncodingMode.URI_COMPONENT for historic reasons and for backwards compatibility. The WebClient relies on the default value in DefaultUriBuilderFactory, which was changed from EncodingMode.URI_COMPONENT in 5.0.x to EncodingMode.TEMPLATE_AND_VALUES in 5.1.spring-doc.cadn.net.cn

1.5.4. 相对Servlet请求

您可以使用 ServletUriComponentsBuilder 来创建相对于当前请求的URI, 如下例所示:spring-doc.cadn.net.cn

Java
HttpServletRequest request = ...

// Re-uses host, scheme, port, path and query string...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}").build()
        .expand("123")
        .encode();
Kotlin
val request: HttpServletRequest = ...

// Re-uses host, scheme, port, path and query string...

val ucb = ServletUriComponentsBuilder.fromRequest(request)
        .replaceQueryParam("accountId", "{id}").build()
        .expand("123")
        .encode()

您可以根据上下文路径创建相对URI,如下例所示:spring-doc.cadn.net.cn

Java
// Re-uses host, port and context path...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts").build()
Kotlin
// Re-uses host, port and context path...

val ucb = ServletUriComponentsBuilder.fromContextPath(request)
        .path("/accounts").build()

您可以相对于Servlet创建URI(例如,/main/*), 如下例所示:spring-doc.cadn.net.cn

Java
// Re-uses host, port, context path, and Servlet prefix...

ServletUriComponentsBuilder ucb = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts").build()
Kotlin
// Re-uses host, port, context path, and Servlet prefix...

val ucb = ServletUriComponentsBuilder.fromServletMapping(request)
        .path("/accounts").build()
从5.1版本开始,ServletUriComponentsBuilder 忽略来自 ForwardedX-Forwarded-* 标头的信息,这些标头指定了客户端发起的地址。考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃此类标头。

Spring MVC 提供了一种机制来准备指向控制器方法的链接。例如,以下的 MVC 控制器允许创建链接:spring-doc.cadn.net.cn

Java
@Controller
@RequestMapping("/hotels/{hotel}")
public class BookingController {

    @GetMapping("/bookings/{booking}")
    public ModelAndView getBooking(@PathVariable Long booking) {
        // ...
    }
}
Kotlin
@Controller
@RequestMapping("/hotels/{hotel}")
class BookingController {

    @GetMapping("/bookings/{booking}")
    fun getBooking(@PathVariable booking: Long): ModelAndView {
        // ...
    }
}

您可以按名称引用方法来准备一个链接,如下例所示:spring-doc.cadn.net.cn

Java
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
Kotlin
val uriComponents = MvcUriComponentsBuilder
    .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)

val uri = uriComponents.encode().toUri()

在前面的例子中,我们提供了实际的方法参数值(在这种情况下,是长整型值:21),用于作为路径变量并插入到URL中。此外,我们提供值42来填充任何剩余的URI变量,例如从类型级别的请求映射继承的hotel变量。如果方法有更多的参数,我们可以为不需要用于URL的参数提供null。一般来说,只有@PathVariable@RequestParam参数与构建URL相关。spring-doc.cadn.net.cn

有其他方法可以使用MvcUriComponentsBuilder。例如,你可以通过代理模拟测试的技术来避免通过名称引用控制器方法,如下例所示(该示例假设静态导入了MvcUriComponentsBuilder.on):spring-doc.cadn.net.cn

Java
UriComponents uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);

URI uri = uriComponents.encode().toUri();
Kotlin
val uriComponents = MvcUriComponentsBuilder
    .fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

val uri = uriComponents.encode().toUri()
Controller method signatures 在设计上有限制,当它们应该可用于使用 fromMethodCall 创建链接时。除了需要一个正确的参数签名外,还有一个技术限制在于返回类型(即,为链接构建器调用生成运行时代理),因此返回类型不能是 final。特别是,常见的用于视图名称的 String 返回类型在这里不起作用。您应该使用 ModelAndView 或者甚至是纯 Object(带有 String 返回值)。

早些时候的示例使用了MvcUriComponentsBuilder中的静态方法。内部,它们依赖于ServletUriComponentsBuilder来从当前请求的协议、主机、端口、上下文路径和Servlet路径准备一个基础URL。这在大多数情况下都能很好地工作。然而,有时这可能不够。例如,您可能在请求的上下文之外(如准备链接的批处理过程),或者您可能需要插入路径前缀(如从请求路径中移除并需要重新插入到链接中的本地化前缀)。spring-doc.cadn.net.cn

对于此类情况,您可以使用静态的fromXxx重载方法,这些方法接受一个UriComponentsBuilder以使用基础URL。或者,您可以创建一个带有基础URL的MvcUriComponentsBuilder实例,然后使用基于实例的withXxx方法。例如,以下示例使用withMethodCallspring-doc.cadn.net.cn

Java
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();
Kotlin
val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")
val builder = MvcUriComponentsBuilder.relativeTo(base)
builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)

val uri = uriComponents.encode().toUri()
从5.1版本开始,MvcUriComponentsBuilder 忽略来自 ForwardedX-Forwarded-* 标头的信息,这些标头指定了客户端源地址。考虑使用 ForwardedHeaderFilter 来提取和使用或丢弃此类标头。

在诸如 Thymeleaf、FreeMarker 或 JSP 的视图中,你可以通过引用每个请求映射的隐式或显式分配的名称来构建指向注解控制器的链接。spring-doc.cadn.net.cn

请考虑以下示例:spring-doc.cadn.net.cn

Java
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {

    @RequestMapping("/{country}")
    public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... }
}
Kotlin
@RequestMapping("/people/{id}/addresses")
class PersonAddressController {

    @RequestMapping("/{country}")
    fun getAddress(@PathVariable country: String): HttpEntity<PersonAddress> { ... }
}

给定上述控制器,你可以从JSP中准备一个链接,如下所示:spring-doc.cadn.net.cn

<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %>
...
<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a>

上一个示例依赖于在 Spring 标签库中声明的 mvcUrl 函数(即,META-INF/spring.tld),但定义您自己的函数或为其他模板技术准备类似的函数也很容易。spring-doc.cadn.net.cn

这里是它的工作原理。在启动时,每个@RequestMapping都被分配一个默认名称 通过HandlerMethodMappingNamingStrategy,其默认实现使用类和方法名的首字母大写(例如,getThing方法在 ThingController中变为“TC#getThing”)。如果有名称冲突,您可以使用 @RequestMapping(name="..")来分配一个显式名称或实现您自己的 HandlerMethodMappingNamingStrategyspring-doc.cadn.net.cn

1.6. 异步请求

Spring MVC 与 Servlet 3.0 异步请求处理有广泛集成:spring-doc.cadn.net.cn

1.6.1. DeferredResult

一旦在Servlet容器中启用了异步请求处理功能,控制器方法可以将任何受支持的控制器方法返回值包装在DeferredResult中,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
    DeferredResult<String> deferredResult = new DeferredResult<String>();
    // Save the deferredResult somewhere..
    return deferredResult;
}

// From some other thread...
deferredResult.setResult(result);
Kotlin
@GetMapping("/quotes")
@ResponseBody
fun quotes(): DeferredResult<String> {
    val deferredResult = DeferredResult<String>()
    // Save the deferredResult somewhere..
    return deferredResult
}

// From some other thread...
deferredResult.setResult(result)

控制器可以异步生成返回值,从不同的线程——例如,响应外部事件(JMS消息)、定时任务或其他事件。spring-doc.cadn.net.cn

1.6.2. Callable

一个控制器可以包装任何受支持的返回值,如下例所示:spring-doc.cadn.net.cn

Java
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };
}
Kotlin
@PostMapping
fun processUpload(file: MultipartFile) = Callable<String> {
    // ...
    "someView"
}

然后可以通过将给定的任务通过配置的 TaskExecutor来获得返回值。spring-doc.cadn.net.cn

1.6.3. 处理

这里是一个关于Servlet异步请求处理的非常简洁的概述:spring-doc.cadn.net.cn

  • 一个 ServletRequest 可以通过调用 request.startAsync() 切换为异步模式。 这样做的主要效果是Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。spring-doc.cadn.net.cn

  • 调用 request.startAsync() 返回 AsyncContext,你可以将其用于进一步控制异步处理。例如,它提供了 dispatch 方法,该方法类似于 Servlet API 中的转发,但允许应用程序在 Servlet 容器线程上恢复请求处理。spring-doc.cadn.net.cn

  • The ServletRequest 提供对当前 DispatcherType 的访问,您可以使用它来区分处理初始请求、异步调度、转发和其他调度器类型。spring-doc.cadn.net.cn

DeferredResult 处理如下:spring-doc.cadn.net.cn

  • 控制器返回一个 DeferredResult 并将其保存在某个内存队列或列表中,以便可以访问。spring-doc.cadn.net.cn

  • Spring MVC 调用 request.startAsync()spring-doc.cadn.net.cn

  • 与此同时,DispatcherServlet 和所有配置的过滤器退出请求处理线程,但响应仍然保持打开状态。spring-doc.cadn.net.cn

  • 应用程序在某个线程中设置 DeferredResult,然后Spring MVC将请求分发回Servlet容器。spring-doc.cadn.net.cn

  • The DispatcherServlet is invoked again, and processing resumes with the asynchronously produced return value.spring-doc.cadn.net.cn

Callable 处理如下:spring-doc.cadn.net.cn

  • 控制器返回一个 Callablespring-doc.cadn.net.cn

  • Spring MVC 调用 request.startAsync() 并将 Callable 提交到 一个 TaskExecutor 以在单独的线程中进行处理。spring-doc.cadn.net.cn

  • 与此同时,DispatcherServlet 和所有过滤器退出Servlet容器线程, 但响应仍然保持打开状态。spring-doc.cadn.net.cn

  • 最终 Callable 生成了结果,Spring MVC 将请求分发回 Servlet 容器以完成处理。spring-doc.cadn.net.cn

  • The DispatcherServlet 被再次调用,处理从 Callable 异步生成的返回值继续进行。spring-doc.cadn.net.cn

有关进一步的背景和上下文,你还可以阅读 介绍异步请求处理支持的博客文章,这些文章介绍了Spring MVC 3.2中的异步请求处理支持。spring-doc.cadn.net.cn

异常处理

当你使用 DeferredResult 时,你可以选择是否调用 setResultsetErrorResult 并抛出异常。在这两种情况下,Spring MVC 将请求重新分发回 Servlet 容器以完成处理。然后,它将被视为控制器方法返回给定值或产生给定异常。然后该异常将通过常规异常处理机制(例如,调用 @ExceptionHandler 方法)。spring-doc.cadn.net.cn

当你使用 Callable 时,类似的处理逻辑会发生,主要区别在于结果是从 Callable 返回的,或者它会抛出异常。spring-doc.cadn.net.cn

拦截

HandlerInterceptor 个实例可以是 AsyncHandlerInterceptor 类型,以在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted 回调(而不是 postHandleafterCompletion)。spring-doc.cadn.net.cn

HandlerInterceptor 实现也可以注册一个 CallableProcessingInterceptor 或一个 DeferredResultProcessingInterceptor, 以更深入地集成到异步请求的生命周期中(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptorspring-doc.cadn.net.cn

DeferredResult 提供了 onTimeout(Runnable)onCompletion(Runnable) 回调。 请参阅 DeferredResult 的 javadoc》 以获取更多详细信息。Callable 可以替换为 WebAsyncTask,后者提供了超时和完成回调的附加方法。spring-doc.cadn.net.cn

与WebFlux相比

Servlet API 最初是为单次通过 Filter-Servlet 链而构建的。异步请求处理在 Servlet 3.0 中引入,允许应用程序退出 Filter-Servlet 链但仍保留响应以供进一步处理。Spring MVC 的异步支持就是基于这种机制。当控制器返回 DeferredResult 时,Filter-Servlet 链被退出,并且 Servlet 容器线程被释放。稍后,当 DeferredResult 被设置时,将进行一次 ASYNC 分发(到相同的 URL),在此期间控制器再次被映射,但不调用它,而是使用 DeferredResult 值(就像控制器返回该值一样)来恢复处理。spring-doc.cadn.net.cn

相比之下,Spring WebFlux 并不是基于 Servlet API 构建的,也不需要这样的异步请求处理功能,因为它在设计上就是异步的。异步处理内置在所有框架合约中,并在整个请求处理过程中得到内在支持。spring-doc.cadn.net.cn

从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持在控制器方法中将异步和响应式类型作为返回值。Spring MVC 甚至支持流式传输,包括响应式背压。然而,与 WebFlux 不同的是,响应中的单个写操作仍然是阻塞的(并且是在单独的线程上执行的),而 WebFlux 依赖于非阻塞 I/O,并且不需要为每个写操作额外分配一个线程。spring-doc.cadn.net.cn

另一个基本的区别是,Spring MVC 不支持在控制器方法参数中使用异步或响应式类型(例如,@RequestBody@RequestPart 等),也不支持将异步和响应式类型作为模型属性。Spring WebFlux 支持所有这些功能。spring-doc.cadn.net.cn

1.6.4. HTTP流式传输

您可以使用 DeferredResultCallable 来获取单个异步返回值。 如果您想生成多个异步值并将它们写入响应,该怎么办?本节描述了如何实现这一点。spring-doc.cadn.net.cn

对象

您可以使用 ResponseBodyEmitter 返回值来生成一个对象流,其中每个对象都使用 HttpMessageConverter 序列化并写入响应,如下例所示:spring-doc.cadn.net.cn

Java
@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();
Kotlin
@GetMapping("/events")
fun handle() = ResponseBodyEmitter().apply {
    // Save the emitter somewhere..
}

// In some other thread
emitter.send("Hello once")

// and again later on
emitter.send("Hello again")

// and done at some point
emitter.complete()

您也可以使用 ResponseBodyEmitter 作为 ResponseEntity 中的正文,让您 自定义响应的状态和标头。spring-doc.cadn.net.cn

当一个emitter抛出一个IOException(例如,如果远程客户端断开连接),应用程序不需要负责清理连接,也不应该调用emitter.completeemitter.completeWithError。相反,servlet容器会自动触发一个AsyncListener错误通知,在这个过程中Spring MVC会进行一次completeWithError调用。这次调用反过来会对应用程序进行最后一次ASYNC调度,在此期间Spring MVC会调用配置的异常解析器并完成请求。spring-doc.cadn.net.cn

SSE

SseEmitter (一个 ResponseBodyEmitter 的子类) 提供了对 服务器发送事件 的支持,其中从服务器发送的事件按照 W3C SSE 规范进行格式化。要从控制器生成一个 SSE 流,请返回 SseEmitter,如下例所示:spring-doc.cadn.net.cn

Java
@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();
Kotlin
@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
fun handle() = SseEmitter().apply {
    // Save the emitter somewhere..
}

// In some other thread
emitter.send("Hello once")

// and again later on
emitter.send("Hello again")

// and done at some point
emitter.complete()

While SSE是浏览器流媒体的主要选项,请注意Internet Explorer不支持服务器发送事件。考虑使用Spring的WebSocket消息传递,并使用SockJS后备传输(包括SSE),以针对广泛的浏览器。spring-doc.cadn.net.cn

另见 前一节 有关异常处理的说明。spring-doc.cadn.net.cn

原始数据

有时,跳过消息转换并直接流式传输到响应可能是有用的 OutputStream(例如,用于文件下载)。您可以使用StreamingResponseBody 返回值类型来实现这一点,如下例所示:spring-doc.cadn.net.cn

Java
@GetMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // write...
        }
    };
}
Kotlin
@GetMapping("/download")
fun handle() = StreamingResponseBody {
    // write...
}

您可以使用 StreamingResponseBody 作为 ResponseEntity 的主体来 自定义响应的状态和标头。spring-doc.cadn.net.cn

1.6.5. 响应式类型

Spring MVC 支持在控制器中使用响应式客户端库(请参阅WebFlux部分的响应式库)。 这包括来自WebClientspring-webflux等,以及其他如Spring Data响应式数据存储库。在这些情况下,能够从控制器方法返回响应式类型非常方便。spring-doc.cadn.net.cn

响应式返回值的处理方式如下:spring-doc.cadn.net.cn

  • 单值承诺被适配,类似于使用 DeferredResult。示例包括 Mono (Reactor) 或 Single (RxJava)。spring-doc.cadn.net.cn

  • 一个多值流,具有流媒体类型(如application/stream+jsontext/event-stream),会被适配,类似于使用ResponseBodyEmitterSseEmitter。示例包括Flux(Reactor)或Observable(RxJava)。应用程序还可以返回Flux<ServerSentEvent>Observable<ServerSentEvent>spring-doc.cadn.net.cn

  • 一个多值流,带有任何其他媒体类型(如application/json)会被适应,类似于使用DeferredResult<List<?>>spring-doc.cadn.net.cn

Spring MVC 通过来自 spring-coreReactiveAdapterRegistry 支持 Reactor 和 RxJava,这使得它可以适应多种响应式库。

对于流式传输到响应,支持响应式背压,但写入响应仍然阻塞,并且在通过配置的 TaskExecutor上运行,以避免阻塞上游源(例如从WebClient返回的Flux)。默认情况下,使用SimpleAsyncTaskExecutor进行阻塞写入,但这在负载下并不合适。如果您计划使用响应式类型进行流式传输,应使用MVC配置来配置任务执行器。spring-doc.cadn.net.cn

1.6.6. 断开连接

Servlet API 没有提供任何通知来指示远程客户端何时断开连接。因此,在通过 SseEmitter响应式类型 流式传输响应时,定期发送数据非常重要,因为如果客户端已断开连接,写入会失败。可以发送空的(仅注释)SSE 事件或任何其他数据,对方端需要将其解释为心跳并忽略它。spring-doc.cadn.net.cn

Alternatively, consider using web messaging solutions (such as STOMP over WebSocket or WebSocket with SockJS) that have a built-in heartbeat mechanism.spring-doc.cadn.net.cn

1.6.7. 配置

异步请求处理功能必须在Servlet容器级别启用。 MVC配置还提供了几个用于异步请求的选项。spring-doc.cadn.net.cn

Servlet 容器

Filter 和 Servlet 声明有一个 asyncSupported 标志,需要设置为 true 以启用异步请求处理。此外,Filter 映射应声明为处理 ASYNC javax.servlet.DispatchTypespring-doc.cadn.net.cn

在Java配置中,当你使用AbstractAnnotationConfigDispatcherServletInitializer 来初始化Servlet容器时,这是自动完成的。spring-doc.cadn.net.cn

web.xml 配置中,您可以将 <async-supported>true</async-supported> 添加到 DispatcherServletFilter 声明中,并将 <dispatcher>ASYNC</dispatcher> 添加到过滤器映射中。spring-doc.cadn.net.cn

Spring MVC

MVC配置暴露了以下与异步请求处理相关的选项:spring-doc.cadn.net.cn

您可以配置以下内容:spring-doc.cadn.net.cn

  • 异步请求的默认超时值,如果未设置,则取决于底层的Servlet容器。spring-doc.cadn.net.cn

  • AsyncTaskExecutor 用于在使用 响应式类型进行流式传输时阻塞写入,以及执行从控制器方法返回的Callable实例。我们强烈建议配置此属性,如果您使用响应式类型进行流式传输或有返回Callable的控制器方法,默认情况下它是SimpleAsyncTaskExecutorspring-doc.cadn.net.cn

  • DeferredResultProcessingInterceptor 个实现和 CallableProcessingInterceptor 个实现。spring-doc.cadn.net.cn

请注意,您还可以为 DeferredResultResponseBodyEmitterSseEmitter 设置默认超时值。对于 Callable,您可以使用 WebAsyncTask 来提供超时值。spring-doc.cadn.net.cn

1.7. 跨域资源共享(CORS)

Spring MVC 允许你处理 CORS(跨域资源共享)。本节描述了如何进行处理。spring-doc.cadn.net.cn

1.7.1. 介绍

出于安全原因,浏览器禁止向当前源以外的资源发起 AJAX 调用。例如,你可以在一个标签页中打开你的银行账户,在另一个标签页中打开 evil.com。evil.com 中的脚本不应该能够使用你的凭证向你的银行 API 发起 AJAX 请求——例如从你的账户中取款!spring-doc.cadn.net.cn

跨源资源共享(CORS)是由W3C规范实现的,大多数浏览器都支持它,它允许你指定授权的跨域请求类型,而不是使用基于IFRAME或JSONP的不安全且功能较弱的变通方案。spring-doc.cadn.net.cn

1.7.2. 处理

CORS规范区分了预检请求、简单请求和实际请求。 要了解CORS的工作原理,你可以阅读 这篇文章,或者查看规范以获取更多详细信息。spring-doc.cadn.net.cn

Spring MVC HandlerMapping 实现提供了内置的 CORS 支持。在成功将请求映射到处理器后,HandlerMapping 实现会检查给定请求和处理器的 CORS 配置并采取进一步操作。预检请求会直接处理,而简单的和实际的 CORS 请求会被拦截、验证,并设置所需的 CORS 响应头。spring-doc.cadn.net.cn

为了启用跨域请求(即Origin标头存在且与请求的主机不同),你需要有一些明确声明的CORS配置。如果没有找到匹配的CORS配置,预检请求将被拒绝。不会向简单和实际的CORS请求的响应中添加任何CORS标头,因此浏览器会拒绝这些请求。spring-doc.cadn.net.cn

每个 HandlerMapping 可以通过 配置 基于 URL 模式的 CorsConfiguration 映射单独进行配置。在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明这些映射,这会导致所有 HandlerMappping 实例传递一个单一的全局映射。spring-doc.cadn.net.cn

你可以结合在 HandlerMapping 级别的全局CORS配置与更精细的、处理器级别的CORS配置。例如,注解控制器可以使用类级别或方法级别的 @CrossOrigin 注解(其他处理器可以实现 CorsConfigurationSource)。spring-doc.cadn.net.cn

全局配置和本地配置的组合规则通常是累加的——例如,所有全局和所有本地源。对于那些只能接受单个值的属性(例如allowCredentialsmaxAge),本地值会覆盖全局值。有关更多详细信息,请参见 CorsConfiguration#combine(CorsConfiguration)spring-doc.cadn.net.cn

要从源代码中学习更多或进行高级自定义,请查看背后的代码:spring-doc.cadn.net.cn

1.7.3. @CrossOrigin

The @CrossOrigin 注解允许在注解的控制器方法上启用跨域请求,如下例所示:spring-doc.cadn.net.cn

Java
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }
}

默认情况下,@CrossOrigin 允许:spring-doc.cadn.net.cn

allowedCredentials 默认情况下未启用,因为这会建立一个信任级别,该级别会暴露敏感的用户特定信息(例如 cookie 和 CSRF Tokens),并且仅应在适当的情况下使用。spring-doc.cadn.net.cn

maxAge 设置为 30 分钟。spring-doc.cadn.net.cn

@CrossOrigin 在类级别也是支持的,并且被所有方法继承,如下例所示:spring-doc.cadn.net.cn

Java
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }

您可以在类级别和方法级别使用 @CrossOrigin,如下例所示:spring-doc.cadn.net.cn

Java
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com")
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}
Kotlin
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com")
    @GetMapping("/{id}")
    fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    fun remove(@PathVariable id: Long) {
        // ...
    }
}

1.7.4. 全局配置

除了细粒度的控制器方法级别的配置,你可能还想定义一些全局的CORS配置。你可以为任何HandlerMapping单独设置基于URL的CorsConfiguration映射。然而,大多数应用程序使用MVC Java配置或MVC XML命名空间来实现这一点。spring-doc.cadn.net.cn

默认情况下,全局配置启用了以下功能:spring-doc.cadn.net.cn

allowedCredentials 默认情况下未启用,因为这会建立一个信任级别,该级别会暴露敏感的用户特定信息(例如 cookie 和 CSRF Tokens),并且仅应在适当的情况下使用。spring-doc.cadn.net.cn

maxAge 设置为 30 分钟。spring-doc.cadn.net.cn

Java配置

要启用MVC Java配置中的CORS,可以使用CorsRegistry回调, 如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

        registry.addMapping("/api/**")
                .allowedOrigins("https://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600)

        // Add more mappings...
    }
}
XML配置

要启用XML命名空间中的CORS,可以使用<mvc:cors>元素, 如下例所示:spring-doc.cadn.net.cn

<mvc:cors>

    <mvc:mapping path="/api/**"
        allowed-origins="https://domain1.com, https://domain2.com"
        allowed-methods="GET, PUT"
        allowed-headers="header1, header2, header3"
        exposed-headers="header1, header2" allow-credentials="true"
        max-age="123" />

    <mvc:mapping path="/resources/**"
        allowed-origins="https://domain1.com" />

</mvc:cors>

1.7.5. 跨域资源共享(CORS)过滤器

您可以通过内置的 CorsFilter应用CORS支持。spring-doc.cadn.net.cn

如果你尝试使用CorsFilter与Spring Security一起,需要注意的是Spring Security具有 内置支持 用于CORS。

要配置该过滤器,请在构造函数中传递一个CorsConfigurationSource,如下例所示:spring-doc.cadn.net.cn

Java
CorsConfiguration config = new CorsConfiguration();

// Possibly...
// config.applyPermitDefaultValues()

config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

CorsFilter filter = new CorsFilter(source);
Kotlin
val config = CorsConfiguration()

// Possibly...
// config.applyPermitDefaultValues()

config.allowCredentials = true
config.addAllowedOrigin("https://domain1.com")
config.addAllowedHeader("*")
config.addAllowedMethod("*")

val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", config)

val filter = CorsFilter(source)

1.8. Web 安全

The Spring Security 项目提供了支持,用于保护Web应用程序免受恶意攻击。请参阅Spring Security参考文档,包括:spring-doc.cadn.net.cn

HDIV 是另一个与 Spring MVC 集成的 Web 安全框架。spring-doc.cadn.net.cn

1.9. HTTP 缓存

HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕 Cache-Control 响应头以及随后的条件请求头(如 Last-ModifiedETag)。Cache-Control 指导私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。ETag 头用于发出条件请求,如果内容未更改,则可能导致 304(NOT_MODIFIED)响应而不带正文。ETag 可以被视为 Last-Modified 头的更高级继任者。spring-doc.cadn.net.cn

本节描述了Spring Web MVC中可用的与HTTP缓存相关的选项。spring-doc.cadn.net.cn

1.9.1. CacheControl

CacheControl 提供支持以配置与 Cache-Control 标头相关的设置,并且可以作为多个地方的参数接受:spring-doc.cadn.net.cn

虽然RFC 7234 描述了 Cache-Control 响应头的所有可能指令,CacheControl 类型采用了一种以使用场景为导向的方法,重点关注常见场景:spring-doc.cadn.net.cn

Java
// 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();
Kotlin
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)

// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()

WebContentGenerator 也接受一个更简单的 cachePeriod 属性(以秒为单位定义),其工作方式如下:spring-doc.cadn.net.cn

1.9.2. 控制器

Controllers 可以显式添加对 HTTP 缓存的支持。我们建议这样做,因为资源的 lastModifiedETag 值需要在与条件请求头进行比较之前计算出来。控制器可以向 5 添加一个 ETag 标头和 Cache-Control 设置,如下例所示:spring-doc.cadn.net.cn

Java
@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);
}
Kotlin
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {

    val book = findBook(id);
    val version = book.getVersion()

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book)
}

前一个示例会发送一个304 (NOT_MODIFIED)响应,如果与条件请求头的比较表明内容没有更改,则响应体为空。否则,将ETagCache-Control头添加到响应中。spring-doc.cadn.net.cn

你也可以在控制器中针对条件请求头进行检查,如下例所示:spring-doc.cadn.net.cn

Java
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {

    long eTag = ... (1)

    if (request.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
1 应用程序特定的计算。
2 响应已设置为 304 (NOT_MODIFIED) — 不再进行进一步处理。
3 继续处理请求。
Kotlin
@RequestMapping
fun myHandleMethod(request: WebRequest, model: Model): String? {

    val eTag: Long = ... (1)

    if (request.checkNotModified(eTag)) {
        return null (2)
    }

    model[...] = ... (3)
    return "myViewName"
}
1 应用程序特定的计算。
2 响应已设置为 304 (NOT_MODIFIED) — 不再进行进一步处理。
3 继续处理请求。

有三种变体用于检查针对eTag值、lastModified值或两者组合的条件请求。对于条件GETHEAD请求,您可以将响应设置为304(NOT_MODIFIED)。对于条件POSTPUTDELETE,您可以将响应设置为412(PRECONDITION_FAILED),以防止并发修改。spring-doc.cadn.net.cn

1.9.3. 静态资源

您应该使用 Cache-Control 和条件响应头来提供静态资源,以获得最佳性能。请参见配置静态资源的部分。spring-doc.cadn.net.cn

1.9.4. ETag 过滤器

您可以使用 ShallowEtagHeaderFilter 来添加“浅层”eTag 值,这些值是从响应内容中计算得出的,因此可以节省带宽但不能节省CPU时间。请参见浅层ETagspring-doc.cadn.net.cn

1.10. 视图技术

Spring MVC 中视图技术的使用是可插拔的,无论您决定使用 Thymeleaf、Groovy Markup Templates、JSP 还是其他技术,主要是一个配置更改的问题。本章涵盖了与 Spring MVC 集成的视图技术。我们假设您已经熟悉了视图解析spring-doc.cadn.net.cn

Spring MVC 应用程序的视图位于该应用程序的内部信任边界内。 视图可以访问应用程序上下文中的所有 bean。 因此,不建议在模板可由外部源编辑的应用程序中使用 Spring MVC 的模板支持,因为这可能会带来安全风险。

1.10.1. Thymeleaf

Thymeleaf 是一个现代的服务器端 Java 模板引擎,它强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于独立处理 UI 模板(例如,由设计师处理)非常有帮助,而无需运行服务器。如果你想替换 JSP,Thymeleaf 提供了最全面的功能集之一,使这种过渡更加容易。Thymeleaf 是活跃开发和维护的。有关更完整的介绍,请参阅 Thymeleaf 项目主页。spring-doc.cadn.net.cn

Thymeleaf与Spring MVC的集成由Thymeleaf项目管理。 配置涉及一些bean声明,如 ServletContextTemplateResolver, SpringTemplateEngine, 和 ThymeleafViewResolver。 有关更多详细信息,请参阅Thymeleaf+Springspring-doc.cadn.net.cn

1.10.2. FreeMarker

Apache FreeMarker 是一个用于生成从HTML到电子邮件等各种文本输出的模板引擎。Spring框架内置了使用FreeMarker模板与Spring MVC集成的支持。spring-doc.cadn.net.cn

查看配置

以下示例展示了如何将FreeMarker配置为视图技术:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("/WEB-INF/freemarker")
    }
}

以下示例展示了如何在XML中配置相同的内容:spring-doc.cadn.net.cn

<mvc:annotation-driven/>

<mvc:view-resolvers>
    <mvc:freemarker/>
</mvc:view-resolvers>

<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
    <mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>

Alternatively, you can also declare the FreeMarkerConfigurer bean for full control over all properties, as the following example shows:spring-doc.cadn.net.cn

<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模板。spring-doc.cadn.net.cn

FreeMarker 配置

您可以直接将FreeMarker的“设置”和“共享变量”传递给由Spring管理的FreeMarker Configuration 对象,方法是设置FreeMarkerConfigurer bean的相应bean属性。freemarkerSettings 属性需要一个java.util.Properties 对象,而freemarkerVariables 属性需要一个java.util.Map。以下示例演示了如何使用FreeMarkerConfigurerspring-doc.cadn.net.cn

<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
    <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
    <property name="freemarkerVariables">
        <map>
            <entry key="xml_escape" value-ref="fmXmlEscape"/>
        </map>
    </property>
</bean>

<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>

请参阅FreeMarker文档,了解适用于Configuration对象的设置和变量的详细信息。spring-doc.cadn.net.cn

表单处理

Spring 提供了一个标签库供 JSP 使用,其中包含一个 <spring:bind/> 元素。此元素主要用于让表单显示来自表单支持对象的值,并显示来自 web 或业务层中的 Validator 的验证失败结果。Spring 还支持在 FreeMarker 中使用相同的功能,并提供了额外的便利宏来生成表单输入元素。spring-doc.cadn.net.cn

绑定宏

一组标准的宏在spring-webmvc.jar文件中维护,用于FreeMarker,因此它们始终可用于配置适当的应用程序。spring-doc.cadn.net.cn

Spring模板库中定义的一些宏被认为是内部的(私有的),但宏定义中没有这样的作用域,使得所有宏对调用代码和用户模板都是可见的。以下部分仅集中于你需要直接在模板中调用的宏。如果你想直接查看宏代码,文件名为spring.ftl,位于org.springframework.web.servlet.view.freemarker包中。spring-doc.cadn.net.cn

简单绑定

在基于FreeMarker模板的HTML表单中,这些模板作为Spring MVC控制器的表单视图,你可以使用类似于以下示例的代码来绑定字段值并以类似于JSP的方式显示每个输入字段的错误消息。以下示例展示了一个personForm视图:spring-doc.cadn.net.cn

<!-- FreeMarker macros have to be imported into a namespace.
    We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
    ...
    <form action="" method="POST">
        Name:
        <@spring.bind "personForm.name"/>
        <input type="text"
            name="${spring.status.expression}"
            value="${spring.status.value?html}"/><br />
        <#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
        <br />
        ...
        <input type="submit" value="submit"/>
    </form>
    ...
</html>

<@spring.bind> 需要一个 'path' 参数,该参数由你的命令对象的名称(默认是 'command',除非你在控制器配置中更改了它)后跟一个点和你希望绑定的命令对象上的字段名称组成。你还可以使用嵌套字段,例如 command.address.streetbind 宏假设由 ServletContext 参数 defaultHtmlEscapeweb.xml 中指定的默认 HTML 转义行为。spring-doc.cadn.net.cn

一种名为<@spring.bindEscaped>的宏的替代形式接受第二个参数,该参数明确指定是否应在状态错误消息或值中使用HTML转义。您可以根据需要将其设置为truefalse。额外的表单处理宏简化了HTML转义的使用,您应该尽可能使用这些宏。它们在下一节中解释。spring-doc.cadn.net.cn

输入宏

FreeMarker的附加便利宏简化了绑定和表单生成(包括验证错误显示)。使用这些宏来生成表单输入字段从来不是必须的,你可以将它们与简单的HTML或直接调用我们之前强调的Spring绑定宏混合使用。spring-doc.cadn.net.cn

下表列出了可用的宏,显示了FreeMarker模板 (FTL) 定义以及每个宏所接受的参数列表:spring-doc.cadn.net.cn

表 6. 宏定义表
FTL 定义

message (根据代码参数从资源束中输出一个字符串)spring-doc.cadn.net.cn

<@spring.message code/>spring-doc.cadn.net.cn

messageText (根据代码参数从资源束中输出一个字符串,如果找不到则回退到默认参数的值)spring-doc.cadn.net.cn

<@spring.messageText code, text/>spring-doc.cadn.net.cn

url (在相对URL前加上应用程序的上下文根)spring-doc.cadn.net.cn

<@spring.url relativeUrl/>spring-doc.cadn.net.cn

formInput (标准输入字段,用于收集用户输入)spring-doc.cadn.net.cn

<@spring.formInput 路径, 属性, 字段类型/>spring-doc.cadn.net.cn

formHiddenInput (隐藏输入字段,用于提交非用户输入)spring-doc.cadn.net.cn

<@spring.formHiddenInput path, attributes/>spring-doc.cadn.net.cn

formPasswordInput (标准输入字段,用于收集密码。请注意,这种类型的字段中永远不会填充任何值。)spring-doc.cadn.net.cn

<@spring.formPasswordInput path, attributes/>spring-doc.cadn.net.cn

formTextarea (大型文本字段,用于收集长的、自由格式的文本输入)spring-doc.cadn.net.cn

<@spring.formTextarea path, attributes/>spring-doc.cadn.net.cn

formSingleSelect (下拉框选项,允许选择一个必填值)spring-doc.cadn.net.cn

<@spring.formSingleSelect path, options, attributes/>spring-doc.cadn.net.cn

formMultiSelect (一个列表框,允许用户选择0个或多个值)spring-doc.cadn.net.cn

<@spring.formMultiSelect path, options, attributes/>spring-doc.cadn.net.cn

formRadioButtons (一组单选按钮,允许从可用选项中进行单选)spring-doc.cadn.net.cn

<@spring.formRadioButtons 路径, 选项 分隔符, 属性/>spring-doc.cadn.net.cn

formCheckboxes (一组复选框,允许选择0个或多个值)spring-doc.cadn.net.cn

<@spring.formCheckboxes path, options, separator, attributes/>spring-doc.cadn.net.cn

formCheckbox (一个单选框)spring-doc.cadn.net.cn

<@spring.formCheckbox path, attributes/>spring-doc.cadn.net.cn

showErrors (简化绑定字段的验证错误显示)spring-doc.cadn.net.cn

<@spring.showErrors 分隔符, classOrStyle/>spring-doc.cadn.net.cn

In FreeMarker 模板中,formHiddenInputformPasswordInput 实际上并不是必需的,因为你可以使用正常的 formInput 宏,指定 hiddenpassword 作为 fieldType 参数的值。

上述任何宏的参数都有其一致的含义:spring-doc.cadn.net.cn

  • path: 要绑定的字段的名称(例如 "command.name")spring-doc.cadn.net.cn

  • options: A Map of all the available values that can be selected from in the input field. The keys to the map represent the values that are POSTed back from the form and bound to the command object. Map objects stored against the keys are the labels displayed on the form to the user and may be different from the corresponding values posted back by the form. Usually, such a map is supplied as reference data by the controller. You can use any Map implementation, depending on required behavior. For strictly sorted maps, you can use a SortedMap (such as a TreeMap) with a suitable Comparator and, for arbitrary Maps that should return values in insertion order, use a LinkedHashMap or a LinkedMap from commons-collectionsspring-doc.cadn.net.cn

  • separator: 当多个选项作为独立元素(单选按钮或复选框)可用时,用于分隔列表中每个选项的字符序列(例如 <br>)。spring-doc.cadn.net.cn

  • attributes: 在HTML标签内包含的额外字符串或任意标签。这个字符串由宏直接回显。例如,在一个 textarea 字段中,你可以提供属性(如 'rows="5" cols="60"'),或者你可以传递样式信息,如 'style="border:1px solid silver"'。spring-doc.cadn.net.cn

  • classOrStyle: 对于 showErrors 宏,包裹每个错误的元素使用的 CSS 类的名称。如果没有提供信息(或值为空),则错误将被包裹在 <b></b> 标签中。spring-doc.cadn.net.cn

以下部分概述了宏的示例。spring-doc.cadn.net.cn

输入字段

The formInput 宏接受 path 参数 (command.name) 和一个额外的 attributes 参数(在下面的例子中是空的)。该宏以及所有其他表单生成宏都会对路径参数执行隐式 Spring 绑定。绑定在新的绑定发生之前保持有效,因此 showErrors 宏不需要再次传递路径参数——它操作的是上次创建绑定的字段。spring-doc.cadn.net.cn

The showErrors 宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符)并且还接受第二个参数——这次是一个类名或样式属性。请注意,FreeMarker 可以为 attributes 参数指定默认值。以下示例展示了如何使用 formInputshowWErrors 宏:spring-doc.cadn.net.cn

<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>

下一个示例展示了表单片段的输出,生成名称字段并在表单提交时显示验证错误,因为字段中没有值。验证通过Spring的Validation框架进行。spring-doc.cadn.net.cn

生成的HTML类似于以下示例:spring-doc.cadn.net.cn

Name:
<input type="text" name="name" value="">
<br>
    <b>required</b>
<br>
<br>

The formTextarea 宏的工作方式与 formInput 宏相同,并接受相同的参数列表。通常,第二个参数 (attributes) 用于传递样式信息或 rowscols 属性给 textareaspring-doc.cadn.net.cn

选择字段

您可以使用四个选择字段宏来生成常见的 UI 值选择输入,用于您的 HTML 表单:spring-doc.cadn.net.cn

每个宏接受一个包含表单字段值和对应标签的选项Map。值和标签可以相同。spring-doc.cadn.net.cn

下一个示例是关于FTL中的单选按钮。表单支持对象为此字段指定了默认值 'London',因此不需要验证。当表单被渲染时,整个城市列表作为参考数据以 'cityMap' 的名称提供在模型中。以下列出了该示例:spring-doc.cadn.net.cn

...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>

上一个示例渲染了一行单选按钮,每个值对应一个 cityMap,并使用 "" 作为分隔符。没有提供额外的属性(宏的最后一个参数缺失)。cityMap 对映射中的每个键值对使用相同的 String。映射的键是表单实际提交为 POST 请求参数的内容。映射的值是用户看到的标签。在前面的例子中,给定三个著名的城市列表和表单支持对象中的默认值,HTML 看起来如下:spring-doc.cadn.net.cn

Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>

如果你的应用程序期望通过内部代码来处理城市(例如),你可以创建一个包含合适键的代码映射,如下例所示:spring-doc.cadn.net.cn

Java
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
    Map<String, String> cityMap = new LinkedHashMap<>();
    cityMap.put("LDN", "London");
    cityMap.put("PRS", "Paris");
    cityMap.put("NYC", "New York");

    Map<String, Object> model = new HashMap<>();
    model.put("cityMap", cityMap);
    return model;
}
Kotlin
protected fun referenceData(request: HttpServletRequest): Map<String, *> {
    val cityMap = linkedMapOf(
            "LDN" to "London",
            "PRS" to "Paris",
            "NYC" to "New York"
    )
    return hashMapOf("cityMap" to cityMap)
}

代码现在生成的输出中,单选按钮的值是相关的代码,但用户仍然可以看到更友好的城市名称,如下所示:spring-doc.cadn.net.cn

Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>
HTML 转义

默认情况下,前面描述的表单宏生成的HTML元素符合HTML 4.01标准,并使用在您的web.xml文件中定义的默认HTML转义值,如Spring绑定支持所使用的那样。要使元素符合XHTML标准或覆盖默认的HTML转义值,您可以在模板(或在模型中,它们对模板可见)中指定两个变量。在模板中指定它们的优点是可以在模板处理的后期将它们更改为不同的值,从而为表单中的不同字段提供不同的行为。spring-doc.cadn.net.cn

要将你的标签切换为XHTML兼容,请为名为xhtmlCompliant的模型或上下文变量指定值true,如下例所示:spring-doc.cadn.net.cn

<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>

处理这条指令后,由 Spring 宏生成的任何元素现在都符合 XHTML 标准。spring-doc.cadn.net.cn

同样地,你可以指定每个字段的 HTML 转义,如下例所示:spring-doc.cadn.net.cn

<#-- until this point, default HTML escaping is used -->

<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>

<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->

1.10.3. Groovy Markup

The Groovy Markup Template Engine is primarily aimed at generating XML-like markup (XML, XHTML, HTML5, and others), but you can use it to generate any text-based content. The Spring Framework has a built-in integration for using Spring MVC with Groovy Markup.spring-doc.cadn.net.cn

Groovy Markup Template 引擎需要 Groovy 2.3.1+。
配置

以下示例展示了如何配置Groovy Markup Template Engine:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.groovy()
    }

    // Configure the Groovy Markup Template Engine...

    @Bean
    fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
        resourceLoaderPath = "/WEB-INF/"
    }
}

以下示例展示了如何在XML中配置相同的内容:spring-doc.cadn.net.cn

<mvc:annotation-driven/>

<mvc:view-resolvers>
    <mvc:groovy/>
</mvc:view-resolvers>

<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
示例

与传统的模板引擎不同,Groovy Markup 依赖于使用构建器语法的 DSL。下面的示例展示了一个 HTML 页面的样本模板:spring-doc.cadn.net.cn

yieldUnescaped '<!DOCTYPE html>'
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')
    }
}

1.10.4. 脚本视图

Spring框架内置了使用Spring MVC与任何可以在JSR-223 Java脚本引擎上运行的模板库进行集成的功能。我们已经在不同的脚本引擎上测试了以下模板库:spring-doc.cadn.net.cn

脚本库 脚本引擎

汉德尔斯spring-doc.cadn.net.cn

纳什角spring-doc.cadn.net.cn

Mustachespring-doc.cadn.net.cn

纳什角spring-doc.cadn.net.cn

反应spring-doc.cadn.net.cn

纳什角spring-doc.cadn.net.cn

EJSspring-doc.cadn.net.cn

纳什角spring-doc.cadn.net.cn

ERBspring-doc.cadn.net.cn

JRubyspring-doc.cadn.net.cn

字符串模板spring-doc.cadn.net.cn

Jythonspring-doc.cadn.net.cn

Kotlin 脚本模板spring-doc.cadn.net.cn

Kotlinspring-doc.cadn.net.cn

集成任何其他脚本引擎的基本规则是,它必须实现ScriptEngineInvocable接口。
要求

你需要将脚本引擎添加到你的类路径中,具体细节因脚本引擎而异:spring-doc.cadn.net.cn

你需要拥有脚本模板库。对于Javascript,一种方法是通过WebJarsspring-doc.cadn.net.cn

脚本模板

您可以声明一个 ScriptTemplateConfigurer bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来渲染模板等。 以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}

以下示例展示了相同的XML安排:spring-doc.cadn.net.cn

<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配置看起来没有区别,如下例所示:spring-doc.cadn.net.cn

Java
@Controller
public class SampleController {

    @GetMapping("/sample")
    public String test(Model model) {
        model.addAttribute("title", "Sample title");
        model.addAttribute("body", "Sample body");
        return "template";
    }
}
Kotlin
@Controller
class SampleController {

    @GetMapping("/sample")
    fun test(model: Model): String {
        model["title"] = "Sample title"
        model["body"] = "Sample body"
        return "template"
    }
}

以下示例显示了Mustache模板:spring-doc.cadn.net.cn

<html>
    <head>
        <title>{{title}}</title>
    </head>
    <body>
        <p>{{body}}</p>
    </body>
</html>

渲染函数将被调用,并传入以下参数:spring-doc.cadn.net.cn

Mustache.render() 与该签名原生兼容,因此您可以直接调用它。spring-doc.cadn.net.cn

如果你的模板技术需要一些定制,你可以提供一个实现自定义渲染函数的脚本。例如,Handlebars在使用模板之前需要编译模板,并且需要一个polyfill来模拟一些在服务器端脚本引擎中不可用的浏览器功能。spring-doc.cadn.net.cn

以下示例展示了如何操作:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}
sharedEngine属性设置为false是在使用非线程安全的脚本引擎和未设计为支持并发的模板库(如Handlebars或在Nashorn上运行的React)时所必需的。在这种情况下,需要Java SE 8更新60,因为这个bug,但通常建议使用最近的Java SE补丁版本。

polyfill.js 定义了 Handlebars 正常运行所需的唯一 window 对象,如下所示:spring-doc.cadn.net.cn

var window = {};

这个基本的render.js实现会在使用模板之前对其进行编译。一个生产就绪的实现还应该存储任何重复使用的缓存模板或预编译模板。 你可以在脚本侧执行此操作(并处理你需要的任何自定义——例如管理模板引擎配置)。以下示例展示了如何操作:spring-doc.cadn.net.cn

function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}

查看Spring Framework的单元测试、Java资源,以获取更多配置示例。spring-doc.cadn.net.cn

1.10.5. JSP 和 JSTL

Spring框架内置了使用Spring MVC与JSP和JSTL集成的功能。spring-doc.cadn.net.cn

视图解析器

在使用JSP开发时,您可以声明一个InternalResourceViewResolver或一个ResourceBundleViewResolver bean。spring-doc.cadn.net.cn

ResourceBundleViewResolver 依赖于一个属性文件来定义视图名称,这些视图名称映射到一个类和一个URL。使用 ResourceBundleViewResolver,你可以通过仅使用一个解析器混合不同类型的视图,如下例所示:spring-doc.cadn.net.cn

<!-- the ResourceBundleViewResolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
</bean>

# And a sample properties file is used (views.properties in WEB-INF/classes):
welcome.(class)=org.springframework.web.servlet.view.JstlView
welcome.url=/WEB-INF/jsp/welcome.jsp

productList.(class)=org.springframework.web.servlet.view.JstlView
productList.url=/WEB-INF/jsp/productlist.jsp

InternalResourceViewResolver 也可以用于 JSP。作为最佳实践,我们强烈建议将您的 JSP 文件放置在 'WEB-INF' 目录下的一个目录中,以便客户端无法直接访问。spring-doc.cadn.net.cn

<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>
JSP与JSTL

当使用JSP标准标签库(JSTL)时,你必须使用一个特殊的视图类,即 JstlView,因为JSTL需要一些准备工作才能使诸如I18N等功能生效。spring-doc.cadn.net.cn

Spring的JSP标签库

Spring 提供了将请求参数绑定到命令对象的功能,如前几章所述。为了便于结合这些数据绑定功能开发 JSP 页面,Spring 提供了一些标签,使事情变得更加简单。所有 Spring 标签都具有 HTML 转义功能,可以启用或禁用字符转义。spring-doc.cadn.net.cn

The spring.tld 标签库描述符 (TLD) 已包含在 spring-webmvc.jar 中。 有关各个标签的综合参考,请浏览 API参考 或参阅标签库描述。spring-doc.cadn.net.cn

Spring的表单标签库

从2.0版本开始,Spring提供了一整套支持数据绑定的标签,用于处理使用JSP和Spring Web MVC时的表单元素。每个标签都支持其对应的HTML标签的一系列属性,使得这些标签易于使用且直观。生成的HTML符合HTML 4.01/XHTML 1.0标准。spring-doc.cadn.net.cn

与其它表单/输入标签库不同,Spring 的表单标签库是与 Spring Web MVC 集成的,这使得这些标签可以访问你的控制器处理的命令对象和参考数据。正如我们在下面的例子中所示,表单标签使得 JSP 更易于开发、阅读和维护。spring-doc.cadn.net.cn

我们逐一查看表单标签,并通过示例了解每个标签的使用方法。我们已经包含了某些标签需要进一步说明时生成的HTML片段。spring-doc.cadn.net.cn

配置

表单标签库包含在spring-webmvc.jar中。库描述符被称为spring-form.tldspring-doc.cadn.net.cn

要在JSP页面顶部使用此库中的标签,请添加以下指令:spring-doc.cadn.net.cn

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

其中 form 是您希望为此库中的标签使用的标签名称前缀。spring-doc.cadn.net.cn

表单标签

此标签渲染一个HTML 'form'元素,并为内部标签暴露绑定路径。它将命令对象放入PageContext中,以便内部标签可以访问命令对象。该库中的所有其他标签都是form标签的嵌套标签。spring-doc.cadn.net.cn

假设我们有一个名为User的领域对象。它是一个具有属性(如firstNamelastName)的JavaBean。我们可以将其用作表单控制器的表单支持对象,该控制器返回form.jsp。以下示例展示了form.jsp可能的样子:spring-doc.cadn.net.cn

<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>

The firstNamelastName 值是从页面控制器放置在 PageContext 中的命令对象中检索到的。继续阅读以查看更复杂的示例,了解如何使用 form 标签与内部标签结合使用。spring-doc.cadn.net.cn

以下列表显示了生成的HTML,它看起来像一个标准表单:spring-doc.cadn.net.cn

<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。如果你将表单支持对象以其他名称放入模型中(这绝对是一个最佳实践),你可以将表单绑定到命名变量,如下例所示:spring-doc.cadn.net.cn

<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>
0 标签

此标签渲染一个HTML input 元素,并默认绑定值和 type='text'。 有关此标签的示例,请参见 表单标签。您还可以使用 HTML5 特定类型,例如 emailteldate 等。spring-doc.cadn.net.cn

0 标签

此标签渲染一个HTML input 标签,并将 type 设置为 checkboxspring-doc.cadn.net.cn

假设我们的User有偏好设置,例如新闻订阅和爱好列表。以下示例显示了Preferences类:spring-doc.cadn.net.cn

Java
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;
    }
}
Kotlin
class Preferences(
        var receiveNewsletter: Boolean,
        var interests: StringArray,
        var favouriteWord: String
)

对应的 form.jsp 可以如下所示:spring-doc.cadn.net.cn

<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标签,应该能满足你所有的复选框需求。spring-doc.cadn.net.cn

  • 方法一:当绑定值为类型 java.lang.Boolean 时,如果绑定值为 true,则将 input(checkbox) 标记为 checkedvalue 属性对应于 setValue(Object) 值属性的解析值。spring-doc.cadn.net.cn

  • 方法二:当绑定的值为类型 arrayjava.util.Collection 时,如果配置的 setValue(Object) 值存在于绑定的 Collection 中,则将 input(checkbox) 标记为 checkedspring-doc.cadn.net.cn

  • 方法三:对于任何其他绑定值类型,如果配置的setValue(Object)等于绑定值,则将input(checkbox)标记为checkedspring-doc.cadn.net.cn

请注意,无论采用哪种方法,生成的 HTML 结构都是相同的。以下 HTML 代码片段定义了一些复选框:spring-doc.cadn.net.cn

<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表单数据绑定能够正常工作。checkbox 标签遵循现有的Spring约定,为每个复选框包含一个以下划线 (_) 开头的隐藏参数。通过这样做,实际上是告诉Spring“该复选框在表单提交时是可见的,我希望我的对象能够反映复选框的状态,无论它是否被选中。”spring-doc.cadn.net.cn

0 标签

此标签渲染多个HTML input 标签,并将 type 设置为 checkboxspring-doc.cadn.net.cn

本节基于前一个checkbox标签部分的示例。有时,你可能不希望在JSP页面中列出所有可能的兴趣爱好。你更愿意在运行时提供可用选项的列表,并将其传递给标签。这就是checkboxes标签的目的。你可以传递一个Array、一个List或一个包含可用选项的Mapitems属性中。通常,绑定属性是一个集合,以便它可以保存用户选择的多个值。以下示例展示了一个使用此标签的JSP:spring-doc.cadn.net.cn

<form:form>
    <table>
        <tr>
            <td>Interests:</td>
            <td>
                <%-- Property is of an array or of type java.util.Collection --%>
                <form:checkboxes path="preferences.interests" items="${interestList}"/>
            </td>
        </tr>
    </table>
</form:form>

此示例假设 interestList 是一个 List 作为模型属性可用,其中包含要从中选择的值的字符串。如果你使用 Map, 映射条目的键用作值,而映射条目的值用作显示的标签。你还可以使用自定义对象,其中你可以通过使用 itemValue 提供值的属性名称,并通过使用 itemLabel 提供标签的属性名称。spring-doc.cadn.net.cn

0 标签

此标签渲染一个HTML input 元素,并将 type 设置为 radiospring-doc.cadn.net.cn

一个典型的使用模式涉及多个标签实例绑定到同一个属性,但具有不同的值,如下例所示:spring-doc.cadn.net.cn

<tr>
    <td>Sex:</td>
    <td>
        Male: <form:radiobutton path="sex" value="M"/> <br/>
        Female: <form:radiobutton path="sex" value="F"/>
    </td>
</tr>
0 标签

此标签渲染多个HTML input 元素,并将 type 设置为 radiospring-doc.cadn.net.cn

checkboxes 标签 类似,你可能希望将可用选项作为运行时变量传递。对于这种用法,你可以使用 radiobuttons 标签。你可以传递一个 Array、一个 List 或一个包含可用选项的 Map。这些选项存储在 items 属性中。如果你使用 Map,则映射项的键用作值,而映射项的值用作显示的标签。你还可以使用自定义对象,并通过使用 itemValue 提供值属性名称,使用 itemLabel 提供标签属性名称,如下例所示:spring-doc.cadn.net.cn

<tr>
    <td>Sex:</td>
    <td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
0 标签

此标签渲染一个HTML input 标签,并将类型设置为 password,绑定值。spring-doc.cadn.net.cn

<tr>
    <td>Password:</td>
    <td>
        <form:password path="password"/>
    </td>
</tr>

请注意,默认情况下不会显示密码值。如果您确实希望显示密码值,可以将showPassword属性的值设置为true,如下例所示:spring-doc.cadn.net.cn

<tr>
    <td>Password:</td>
    <td>
        <form:password path="password" value="^76525bvHGq" showPassword="true"/>
    </td>
</tr>
0 标签

此标签渲染一个HTML 'select' 元素。它支持数据绑定到选中的选项以及使用嵌套的 optionoptions 标签。spring-doc.cadn.net.cn

假设一个User拥有一系列技能。对应的HTML可以如下所示:spring-doc.cadn.net.cn

<tr>
    <td>Skills:</td>
    <td><form:select path="skills" items="${skills}"/></td>
</tr>

如果User’s技能在草药学中,那么“技能”行的HTML源代码可能如下:spring-doc.cadn.net.cn

<tr>
    <td>Skills:</td>
    <td>
        <select name="skills" multiple="true">
            <option value="Potions">Potions</option>
            <option value="Herbology" selected="selected">Herbology</option>
            <option value="Quidditch">Quidditch</option>
        </select>
    </td>
</tr>
0 标签

此标签渲染一个HTML option 元素。它根据绑定的值设置 selected。以下HTML显示了它的典型输出:spring-doc.cadn.net.cn

<tr>
    <td>House:</td>
    <td>
        <form:select path="house">
            <form:option value="Gryffindor"/>
            <form:option value="Hufflepuff"/>
            <form:option value="Ravenclaw"/>
            <form:option value="Slytherin"/>
        </form:select>
    </td>
</tr>

如果User’s号房子在格兰芬多,那么“House”行的HTML源代码如下:spring-doc.cadn.net.cn

<tr>
    <td>House:</td>
    <td>
        <select name="house">
            <option value="Gryffindor" selected="selected">Gryffindor</option> (1)
            <option value="Hufflepuff">Hufflepuff</option>
            <option value="Ravenclaw">Ravenclaw</option>
            <option value="Slytherin">Slytherin</option>
        </select>
    </td>
</tr>
1 请注意添加了一个selected属性。
0 标签

此标签渲染一组HTML option 元素。它根据绑定值设置 selected 属性。 以下HTML显示了它的典型输出:spring-doc.cadn.net.cn

<tr>
    <td>Country:</td>
    <td>
        <form:select path="country">
            <form:option value="-" label="--Please Select"/>
            <form:options items="${countryList}" itemValue="code" itemLabel="name"/>
        </form:select>
    </td>
</tr>

如果User居住在英国,“Country”行的HTML源代码如下:spring-doc.cadn.net.cn

<tr>
    <td>Country:</td>
    <td>
        <select name="country">
            <option value="-">--Please Select</option>
            <option value="AT">Austria</option>
            <option value="UK" selected="selected">United Kingdom</option> (1)
            <option value="US">United States</option>
        </select>
    </td>
</tr>
1 请注意添加了一个selected属性。

如上例所示,option 标签与 options 标签的组合使用生成相同的标准 HTML,但允许你在 JSP 中显式指定一个仅用于显示的值(它应该位于此处),例如示例中的默认字符串:“-- 请选择”。spring-doc.cadn.net.cn

The items 属性通常填充一个项目对象的集合或数组。 itemValueitemLabel 指向这些项目对象的 bean 属性,如果指定了的话。否则,项目对象本身将被转换为字符串。或者,您可以指定一个 Map 项,这种情况下,映射键被视为选项值,而映射值对应于选项标签。如果指定了 itemValueitemLabel(或两者都指定了),则项目值属性适用于映射键,项目标签属性适用于映射值。spring-doc.cadn.net.cn

0 标签

此标签渲染一个HTML textarea 元素。以下HTML显示了它的典型输出:spring-doc.cadn.net.cn

<tr>
    <td>Notes:</td>
    <td><form:textarea path="notes" rows="3" cols="20"/></td>
    <td><form:errors path="notes"/></td>
</tr>
0 标签

此标签渲染一个HTML input 标签,并将 type 设置为 hidden 的绑定值。要提交未绑定的隐藏值,请使用 HTML input 标签并将 type 设置为 hidden。 以下HTML显示了它的典型输出:spring-doc.cadn.net.cn

<form:hidden path="house"/>

如果我们选择将house值作为隐藏值提交,HTML代码如下:spring-doc.cadn.net.cn

<input name="house" type="hidden" value="Gryffindor"/>
0 标签

此标签在HTML span 元素中渲染字段错误。它提供了访问在控制器中创建的错误或与控制器关联的任何验证器创建的错误的途径。spring-doc.cadn.net.cn

假设我们希望在提交表单后显示firstNamelastName字段的所有错误消息。我们有一个针对User类的验证器,名为UserValidator,如下例所示:spring-doc.cadn.net.cn

Java
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.");
    }
}
Kotlin
class UserValidator : Validator {

    override fun supports(candidate: Class<*>): Boolean {
        return User::class.java.isAssignableFrom(candidate)
    }

    override fun validate(obj: Any, errors: Errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")
    }
}

The form.jsp 可以如下所示:spring-doc.cadn.net.cn

<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>

如果我们提交一个表单,其中 firstNamelastName 字段的值为空, 则HTML代码如下:spring-doc.cadn.net.cn

<form method="POST">
    <table>
        <tr>
            <td>First Name:</td>
            <td><input name="firstName" type="text" value=""/></td>
            <%-- Associated errors to firstName field displayed --%>
            <td><span name="firstName.errors">Field is required.</span></td>
        </tr>

        <tr>
            <td>Last Name:</td>
            <td><input name="lastName" type="text" value=""/></td>
            <%-- Associated errors to lastName field displayed --%>
            <td><span name="lastName.errors">Field is required.</span></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes"/>
            </td>
        </tr>
    </table>
</form>

如果我们想显示给定页面的整个错误列表怎么办?下一个示例 显示了errors标签还支持一些基本的通配符功能。spring-doc.cadn.net.cn

以下示例在页面顶部显示一个错误列表,然后在字段旁边显示特定于字段的错误:spring-doc.cadn.net.cn

<form:form>
    <form:errors path="*" cssClass="errorBox"/>
    <table>
        <tr>
            <td>First Name:</td>
            <td><form:input path="firstName"/></td>
            <td><form:errors path="firstName"/></td>
        </tr>
        <tr>
            <td>Last Name:</td>
            <td><form:input path="lastName"/></td>
            <td><form:errors path="lastName"/></td>
        </tr>
        <tr>
            <td colspan="3">
                <input type="submit" value="Save Changes"/>
            </td>
        </tr>
    </table>
</form:form>

HTML代码如下:spring-doc.cadn.net.cn

<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>

The spring-form.tld 标签库描述符 (TLD) 已包含在 spring-webmvc.jar 中。 有关各个标签的综合参考,请浏览 API参考 或参阅标签库描述。spring-doc.cadn.net.cn

HTTP方法转换

REST 的一个关键原则是使用“统一接口”。这意味着所有资源(URL)都可以通过相同的四种 HTTP 方法进行操作:GET、PUT、POST 和 DELETE。对于每种方法,HTTP 规范定义了确切的语义。例如,GET 应该始终是一个安全的操作,意味着它没有副作用,而 PUT 或 DELETE 应该是幂等的,意味着你可以反复执行这些操作,但最终结果应该相同。虽然 HTTP 定义了这四种方法,但 HTML 只支持两种:GET 和 POST。幸运的是,有两种可能的变通方法:你可以使用 JavaScript 来执行 PUT 或 DELETE,或者在 HTML 表单中将“真实”的方法作为附加参数(以 HTML 表单中的隐藏输入字段的形式)来模拟 PUT 或 DELETE。Spring 的 HiddenHttpMethodFilter 使用了后一种技巧。这个过滤器是一个普通的 Servlet 过滤器,因此可以与任何 Web 框架结合使用(不仅仅是 Spring MVC)。将此过滤器添加到你的 web.xml 中,POST 请求会根据隐藏的 method 参数被转换为相应的 HTTP 方法请求。spring-doc.cadn.net.cn

为了支持HTTP方法转换,Spring MVC表单标签已更新以支持设置HTTP方法。例如,以下代码片段来自宠物诊所示例:spring-doc.cadn.net.cn

<form:form method="delete">
    <p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>

前一个示例执行了一个HTTP POST,其中“真实”的DELETE方法隐藏在一个请求参数后面。它被HiddenHttpMethodFilter捕获,该值在web.xml中定义,如下例所示:spring-doc.cadn.net.cn

<filter>
    <filter-name>httpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>httpMethodFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

以下示例显示了对应的@Controller方法:spring-doc.cadn.net.cn

Java
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
    this.clinic.deletePet(petId);
    return "redirect:/owners/" + ownerId;
}
Kotlin
@RequestMapping(method = [RequestMethod.DELETE])
fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {
    clinic.deletePet(petId)
    return "redirect:/owners/$ownerId"
}
HTML5标签

Spring 表单标签库允许输入动态属性,这意味着你可以输入任何 HTML5 特定的属性。spring-doc.cadn.net.cn

表单 input 标签支持输入除 text 之外的类型属性。这旨在允许渲染新的 HTML5 特定输入类型,例如 emaildaterange 等。请注意,输入 type='text' 并不是必需的,因为 text 是默认类型。spring-doc.cadn.net.cn

1.10.6. 模板

您可以在使用 Spring 的 Web 应用程序中集成 Tiles,就像集成其他任何视图技术一样。本节大致描述了如何进行集成。spring-doc.cadn.net.cn

本节重点介绍Spring对org.springframework.web.servlet.view.tiles3包中Tiles 3版本的支持。
依赖项

要能够使用Tiles,您需要在项目中添加对Tiles 3.0.1或更高版本的依赖项 以及其传递依赖项spring-doc.cadn.net.cn

配置

要能够使用Tiles,您需要通过包含定义的文件来配置它(有关定义和其他Tiles概念的基本信息,请参见 https://tiles.apache.org)。在Spring中,这是通过使用TilesConfigurer完成的。 以下示例ApplicationContext配置展示了如何进行配置:spring-doc.cadn.net.cn

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/defs/general.xml</value>
            <value>/WEB-INF/defs/widgets.xml</value>
            <value>/WEB-INF/defs/administrator.xml</value>
            <value>/WEB-INF/defs/customer.xml</value>
            <value>/WEB-INF/defs/templates.xml</value>
        </list>
    </property>
</bean>

上一个示例定义了五个包含定义的文件。这些文件都位于WEB-INF/defs目录中。在WebApplicationContext初始化时,这些文件会被加载,并且定义工厂会被初始化。完成之后,可以在你的Spring Web应用程序中将Tiles视图包含在定义文件中的视图使用。为了能够使用这些视图,你必须有一个ViewResolver,就像使用其他任何与Spring一起使用的视图技术一样。你可以使用两种实现中的任意一种,UrlBasedViewResolverResourceBundleViewResolverspring-doc.cadn.net.cn

您可以按特定于区域设置的方式指定 Tiles 定义,方法是在名称后添加一个下划线,然后是该区域设置,如下例所示:spring-doc.cadn.net.cn

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/defs/tiles.xml</value>
            <value>/WEB-INF/defs/tiles_fr_FR.xml</value>
        </list>
    </property>
</bean>

在上述配置中,tiles_fr_FR.xml 用于具有 fr_FR 语言环境的请求,tiles.xml 作为默认值使用。spring-doc.cadn.net.cn

由于下划线用于表示区域设置,我们建议不要在Tiles定义的文件名中以其他方式使用它们。
UrlBasedViewResolver

The UrlBasedViewResolver 为每个需要解析的视图实例化给定的 viewClass。以下 bean 定义了一个 UrlBasedViewResolverspring-doc.cadn.net.cn

<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.tiles3.TilesView"/>
</bean>
ResourceBundleViewResolver

ResourceBundleViewResolver 必须提供一个包含视图名称和视图类的属性文件,这些视图名称和视图类是解析器可以使用的。以下示例显示了一个 ResourceBundleViewResolver 的 bean 定义以及相应的视图名称和视图类(摘自 Pet Clinic 示例):spring-doc.cadn.net.cn

<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时,你可以轻松混合 不同的视图技术。spring-doc.cadn.net.cn

请注意,TilesView 类支持 JSTL(JSP 标准标签库)。spring-doc.cadn.net.cn

SimpleSpringPreparerFactorySpringBeanPreparerFactory

作为高级功能,Spring 还支持两种特殊的 Tiles PreparerFactory 实现。有关如何在你的 Tiles 定义文件中使用 ViewPreparer 引用的详细信息,请参阅 Tiles 文档。spring-doc.cadn.net.cn

你可以指定 SimpleSpringPreparerFactory 以自动装配 ViewPreparer 实例,基于指定的预处理类,应用 Spring 的容器回调以及应用配置的 Spring BeanPostProcessors。如果启用了 Spring 的上下文范围注解配置,则 ViewPreparer 类中的注解将被自动检测和应用。请注意,这需要在 Tiles 定义文件中指定预处理类,因为默认的 PreparerFactory 不会这样做。spring-doc.cadn.net.cn

你可以指定 SpringBeanPreparerFactory 来操作指定的预处理器名称(而不是类),从 DispatcherServlet 的应用程序上下文中获取相应的 Spring bean。在这种情况下,Spring 应用程序上下文完全控制了 bean 的创建过程,允许使用显式的依赖注入配置、作用域 bean 等等。请注意,你需要为每个预处理器名称(如在 Tiles 定义中使用的)定义一个 Spring bean 定义。以下示例展示了如何在 TilesConfigurer bean 上定义一个 SpringBeanPreparerFactory 属性:spring-doc.cadn.net.cn

<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles3.TilesConfigurer">
    <property name="definitions">
        <list>
            <value>/WEB-INF/defs/general.xml</value>
            <value>/WEB-INF/defs/widgets.xml</value>
            <value>/WEB-INF/defs/administrator.xml</value>
            <value>/WEB-INF/defs/customer.xml</value>
            <value>/WEB-INF/defs/templates.xml</value>
        </list>
    </property>

    <!-- resolving preparer names as Spring bean definition names -->
    <property name="preparerFactoryClass"
            value="org.springframework.web.servlet.view.tiles3.SpringBeanPreparerFactory"/>

</bean>

1.10.7. RSS 和 Atom

Both AbstractAtomFeedViewAbstractRssFeedView 继承自 AbstractFeedView 基类,并分别用于提供 Atom 和 RSS Feed 视图。它们 基于 ROME 项目,并位于 包 org.springframework.web.servlet.view.feed 中。spring-doc.cadn.net.cn

AbstractAtomFeedView 要求您实现 buildFeedEntries() 方法,并且可选地重写 buildFeedMetadata() 方法(默认实现为空)。以下示例展示了如何实现:spring-doc.cadn.net.cn

Java
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
    }
}
Kotlin
class SampleContentAtomView : AbstractAtomFeedView() {

    override fun buildFeedMetadata(model: Map<String, Any>,
            feed: Feed, request: HttpServletRequest) {
        // implementation omitted
    }

    override fun buildFeedEntries(model: Map<String, Any>,
            request: HttpServletRequest, response: HttpServletResponse): List<Entry> {
        // implementation omitted
    }
}

类似的实现要求也适用于AbstractRssFeedView,如下例所示:spring-doc.cadn.net.cn

Java
public class SampleContentRssView extends AbstractRssFeedView {

    @Override
    protected void buildFeedMetadata(Map<String, Object> model,
            Channel feed, HttpServletRequest request) {
        // implementation omitted
    }

    @Override
    protected List<Item> buildFeedItems(Map<String, Object> model,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        // implementation omitted
    }
}
Kotlin
class SampleContentRssView : AbstractRssFeedView() {

    override fun buildFeedMetadata(model: Map<String, Any>,
                                feed: Channel, request: HttpServletRequest) {
        // implementation omitted
    }

    override fun buildFeedItems(model: Map<String, Any>,
            request: HttpServletRequest, response: HttpServletResponse): List<Item> {
        // implementation omitted
    }
}

The buildFeedItems()buildFeedEntries() 方法传递了HTTP请求,以防你需要访问Locale。HTTP响应仅在设置cookie或其他HTTP头时传递。方法返回后,feed将自动写入响应对象。spring-doc.cadn.net.cn

有关创建Atom视图的示例,请参阅Alef Arendsen的Spring团队博客文章spring-doc.cadn.net.cn

1.10.8. PDF 和 Excel

Spring 提供了返回 HTML 以外的输出方式,包括 PDF 和 Excel 电子表格。本节描述了如何使用这些功能。spring-doc.cadn.net.cn

文档视图简介

HTML页面并不总是用户查看模型输出的最佳方式,而Spring使得从模型数据动态生成PDF文档或Excel电子表格变得简单。文档是视图,并以正确的内容类型从服务器流式传输,以便(希望)客户端PC能够响应地运行他们的电子表格或PDF查看器应用程序。spring-doc.cadn.net.cn

为了使用Excel视图,你需要将Apache POI库添加到类路径中。 对于PDF生成,你需要添加(最好)OpenPDF库。spring-doc.cadn.net.cn

你应该使用底层文档生成库的最新版本,如果可能的话。特别是,我们强烈推荐使用 OpenPDF(例如,OpenPDF 1.2.12),而不是过时的原始 iText 2.1.7,因为 OpenPDF 是活跃维护的,并且修复了不信任 PDF 内容的一个重要漏洞。
PDF视图

一个简单的单词列表PDF视图可以扩展 org.springframework.web.servlet.view.document.AbstractPdfView 并实现 buildPdfDocument() 方法,如下例所示:spring-doc.cadn.net.cn

Java
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));
        }
    }
}
Kotlin
class PdfWordList : AbstractPdfView() {

    override fun buildPdfDocument(model: Map<String, Any>, doc: Document, writer: PdfWriter,
            request: HttpServletRequest, response: HttpServletResponse) {

        val words = model["wordList"] as List<String>
        for (word in words) {
            doc.add(Paragraph(word))
        }
    }
}

控制器可以从外部视图定义返回这样的视图(通过名称引用它),或者作为View实例从处理器方法返回。spring-doc.cadn.net.cn

Excel视图

自 Spring Framework 4.2 起,org.springframework.web.servlet.view.document.AbstractXlsView 作为 Excel 视图的基础类提供。它基于 Apache POI,具有专门的子类 (AbstractXlsxViewAbstractXlsxStreamingView),这些子类取代了过时的 AbstractExcelView 类。spring-doc.cadn.net.cn

编程模型类似于AbstractPdfView,其中buildExcelDocument()作为中心模板方法,控制器能够从外部定义(通过名称)或作为View实例从处理器方法返回视图。spring-doc.cadn.net.cn

1.10.9. Jackson

Spring 提供对 Jackson JSON 库的支持。spring-doc.cadn.net.cn

基于Jackson的JSON MVC视图

The MappingJackson2JsonView 使用 Jackson 库的 ObjectMapper 来将响应内容渲染为 JSON。默认情况下,模型映射的全部内容(框架特定类除外)都会被编码为 JSON。对于需要过滤映射内容的情况,你可以通过使用 modelKeys 属性来指定要编码的具体模型属性集。你还可以使用 extractValueFromSingleKeyModel 属性,使得单键模型中的值被直接提取并序列化,而不是作为模型属性的映射。spring-doc.cadn.net.cn

你可以根据需要使用Jackson提供的注解来定制JSON映射。当你需要进一步控制时,可以通过ObjectMapper属性注入自定义的ObjectMapper,用于需要为特定类型提供自定义JSON序列化器和反序列化器的情况。spring-doc.cadn.net.cn

基于Jackson的XML视图

MappingJackson2XmlView 使用 Jackson XML 扩展的 XmlMapper 来将响应内容渲染为 XML。如果模型包含多个条目,您应该使用 modelKey bean 属性显式设置要序列化的对象。如果模型包含一个条目,它将自动进行序列化。spring-doc.cadn.net.cn

您可以根据需要使用JAXB或Jackson提供的注解自定义XML映射。当您需要进一步控制时,可以通过ObjectMapper属性注入自定义的XmlMapper,对于需要为特定类型提供序列化程序和反序列化程序的自定义XML情况。spring-doc.cadn.net.cn

1.10.10. XML序列化

The MarshallingView 使用一个 XML Marshaller(在 org.springframework.oxm 包中定义)将响应内容渲染为 XML。你可以通过使用 MarshallingView 实例的 modelKey bean 属性显式设置要序列化的对象。或者,视图会遍历所有模型属性并序列化第一个受支持的类型。 对于 Marshaller 的更多功能,请参见 使用 O/X 映射器进行 XML 序列化spring-doc.cadn.net.cn

1.10.11. XSLT视图

XSLT是一种用于XML的转换语言,并且在Web应用程序中作为视图技术非常流行。如果您的应用程序自然地处理XML,或者您的模型可以轻松转换为XML,那么XSLT可以是一个很好的视图技术选择。以下部分展示了如何在Spring Web MVC应用程序中生成XML文档作为模型数据,并使用XSLT进行转换。spring-doc.cadn.net.cn

这个示例是一个简单的Spring应用程序,它创建一个单词列表并将它们添加到模型映射中。返回该映射以及我们XSLT视图的视图名称。有关Spring Web MVC的Controller接口的详细信息,请参见注解控制器spring-doc.cadn.net.cn

Bean

配置对于简单的Spring Web应用程序是标准的:MVC配置必须定义一个XsltViewResolver bean和常规的MVC注解配置。以下示例展示了如何实现:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {

    @Bean
    fun xsltViewResolver() = XsltViewResolver().apply {
        setPrefix("/WEB-INF/xsl/")
        setSuffix(".xslt")
    }
}
控制器

我们还需要一个控制器来封装我们的单词生成逻辑。spring-doc.cadn.net.cn

控制器逻辑封装在@Controller类中,处理器方法定义如下:spring-doc.cadn.net.cn

Java
@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";
    }
}
Kotlin
import org.springframework.ui.set

@Controller
class XsltController {

    @RequestMapping("/")
    fun home(model: Model): String {
        val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
        val root = document.createElement("wordList")

        val words = listOf("Hello", "Spring", "Framework")
        for (word in words) {
            val wordNode = document.createElement("word")
            val textNode = document.createTextNode(word)
            wordNode.appendChild(textNode)
            root.appendChild(wordNode)
        }

        model["wordList"] = root
        return "home"
    }
}

到目前为止,我们只创建了一个DOM文档并将其添加到了Model映射中。请注意,您还可以加载一个XML文件作为Resource并使用它代替自定义的DOM文档。spring-doc.cadn.net.cn

有一些软件包可以自动将对象图“转换为DOM”,但在Spring中,你可以完全灵活地以任何方式从模型创建DOM。这防止了XML转换在模型数据结构中占据太大的比重,这是使用工具管理DOM化过程时的一个风险。spring-doc.cadn.net.cn

转换

最后,XsltViewResolver 解析 “home” XSLT 模板文件并将 DOM 文档合并到其中以生成我们的视图。如 XsltViewResolver 配置所示,XSLT 模板位于 WEB-INF/xsl 目录中的 war 文件中,并以 xslt 文件扩展名结尾。spring-doc.cadn.net.cn

以下示例展示了一个XSLT转换:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output method="html" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <html>
            <head><title>Hello!</title></head>
            <body>
                <h1>My First Words</h1>
                <ul>
                    <xsl:apply-templates/>
                </ul>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="word">
        <li><xsl:value-of select="."/></li>
    </xsl:template>

</xsl:stylesheet>

上述转换将呈现为以下HTML:spring-doc.cadn.net.cn

<html>
    <head>
        <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Hello!</title>
    </head>
    <body>
        <h1>My First Words</h1>
        <ul>
            <li>Hello</li>
            <li>Spring</li>
            <li>Framework</li>
        </ul>
    </body>
</html>

1.11. MVC 配置

MVC Java配置和MVC XML命名空间提供了适合大多数应用程序的默认配置,并提供了一个配置API来对其进行自定义。spring-doc.cadn.net.cn

对于更高级的自定义,这些在配置API中不可用,请参见高级Java配置高级XML配置spring-doc.cadn.net.cn

你不需要理解MVC Java配置和MVC命名空间创建的底层bean。如果你想了解更多,参见特殊Bean类型Web MVC配置spring-doc.cadn.net.cn

1.11.1. 启用MVC配置

在Java配置中,你可以使用@注解来启用MVC配置,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig {
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig

在XML配置中,你可以使用<mvc:annotation-driven>元素来启用MVC配置,如下例所示:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

上一个示例注册了多个Spring MVC 基础结构bean,并适应类路径上的依赖项(例如,用于JSON、XML和其他格式的有效载荷转换器)。spring-doc.cadn.net.cn

1.11.2. MVC配置API

在Java配置中,你可以实现WebMvcConfigurer接口,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    // Implement configuration methods...
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
    // Implement configuration methods...
}

在XML中,你可以检查<mvc:annotation-driven/>的属性和子元素。你可以查看Spring MVC XML模式或使用你的IDE的代码补全功能来发现可用的属性和子元素。spring-doc.cadn.net.cn

1.11.3. 类型转换

默认情况下,已安装各种数字和日期类型的格式化程序,并通过字段上的@NumberFormat@DateTimeFormat支持自定义。spring-doc.cadn.net.cn

要在Java配置中注册自定义格式化程序和转换器,请使用以下方法:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}

要在XML配置中执行相同的操作,请使用以下代码:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven conversion-service="conversionService"/>

    <bean id="conversionService"
            class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="org.example.MyConverter"/>
            </set>
        </property>
        <property name="formatters">
            <set>
                <bean class="org.example.MyFormatter"/>
                <bean class="org.example.MyAnnotationFormatterFactory"/>
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.example.MyFormatterRegistrar"/>
            </set>
        </property>
    </bean>

</beans>

默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求的区域设置。这对于日期以字符串形式表示的表单有效,这些字符串使用“input”表单字段。然而,对于“date”和“time”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于此类情况,可以如下自定义日期和时间格式:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
请参阅the FormatterRegistrar SPIFormattingConversionServiceFactoryBean以获取有关何时使用FormatterRegistrar实现的更多信息。

1.11.4. 验证

默认情况下,如果Bean Validation存在于类路径上(例如,Hibernate Validator),则LocalValidatorFactoryBean会被注册为全局验证器,用于控制器方法参数上的@ValidValidatedspring-doc.cadn.net.cn

在Java配置中,你可以自定义全局Validator实例,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public Validator getValidator() {
        // ...
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun getValidator(): Validator {
        // ...
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven validator="globalValidator"/>

</beans>

请注意,您还可以在本地注册 Validator 个实现, 如下例所示:spring-doc.cadn.net.cn

Java
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }
}
Kotlin
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}
如果你需要在某个地方注入一个LocalValidatorFactoryBean,创建一个bean并用@Primary标记它,以避免与MVC配置中声明的那个发生冲突。

1.11.5. 拦截器

在Java配置中,你可以注册拦截器以应用于传入的请求,如下例所示:spring-doc.cadn.net.cn

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/*");
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(LocaleChangeInterceptor())
        registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
        registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*")
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:interceptors>
    <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/admin/**"/>
        <bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/secure/*"/>
        <bean class="org.example.SecurityInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

1.11.6. 内容类型

您可以配置Spring MVC如何从请求中确定请求的媒体类型 (例如,Accept头、URL路径扩展、查询参数等)。spring-doc.cadn.net.cn

默认情况下,首先检查URL路径扩展名——jsonxmlrssatom 注册为已知扩展名(取决于类路径依赖)。其次检查Accept标头。spring-doc.cadn.net.cn

考虑将这些默认值更改为仅 Accept 标头,并且,如果您必须使用基于URL的内容类型解析,请考虑使用查询参数策略而不是路径扩展名。有关详细信息,请参阅后缀匹配后缀匹配和RFDspring-doc.cadn.net.cn

在Java配置中,你可以自定义请求的内容类型解析,如下例所示:spring-doc.cadn.net.cn

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);
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
        configurer.mediaType("json", MediaType.APPLICATION_JSON)
        configurer.mediaType("xml", MediaType.APPLICATION_XML)
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>

<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
    <property name="mediaTypes">
        <value>
            json=application/json
            xml=application/xml
        </value>
    </property>
</bean>

1.11.7. 消息转换器

您可以自定义 HttpMessageConverter 在Java配置中,通过覆盖 configureMessageConverters() (以替换Spring MVC创建的默认转换器)或通过覆盖 extendMessageConverters() (以定制默认转换器或向默认转换器添加额外的转换器)。spring-doc.cadn.net.cn

以下示例添加了XML和Jackson JSON转换器,并使用自定义的ObjectMapper代替默认的转换器:spring-doc.cadn.net.cn

Java
@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()));
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfiguration : WebMvcConfigurer {

    override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
        val builder = Jackson2ObjectMapperBuilder()
                .indentOutput(true)
                .dateFormat(SimpleDateFormat("yyyy-MM-dd"))
                .modulesToInstall(ParameterNamesModule())
        converters.add(MappingJackson2HttpMessageConverter(builder.build()))
        converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))

在前面的例子中, Jackson2ObjectMapperBuilder 用于为 MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter 创建一个通用配置,启用缩进、自定义日期格式,并注册 jackson-module-parameter-names, 这添加了对访问参数名称的支持(Java 8 中新增的功能)。spring-doc.cadn.net.cn

此构建器按如下方式自定义Jackson的默认属性:spring-doc.cadn.net.cn

它还会自动注册以下知名模块,如果在类路径上检测到它们:spring-doc.cadn.net.cn

启用Jackson XML支持的缩进需要 woodstox-core-asl 个依赖项,除了 jackson-dataformat-xml 之外。

还有其他有趣的 Jackson 模块可供使用:spring-doc.cadn.net.cn

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
        <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
            <property name="objectMapper" ref="xmlMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
      p:indentOutput="true"
      p:simpleDateFormat="yyyy-MM-dd"
      p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>

<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>

1.11.8. 视图控制器

这是定义一个ParameterizableViewController的快捷方式,当被调用时立即转发到视图。你可以在静态情况下使用它,当时没有Java控制器逻辑需要在视图生成响应之前运行。spring-doc.cadn.net.cn

以下Java配置示例将对/的请求转发到名为home的视图:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.addViewController("/").setViewName("home")
    }
}

以下示例通过使用XML和<mvc:view-controller>元素实现了与前面示例相同的效果:spring-doc.cadn.net.cn

<mvc:view-controller path="/" view-name="home"/>

如果一个@RequestMapping方法被映射到任何HTTP方法的URL,则不能使用视图控制器来处理相同的URL。这是因为通过URL匹配到注解控制器被认为是一个足够强烈的端点所有权指示,因此可以向客户端发送405(METHOD_NOT_ALLOWED)、415(UNSUPPORTED_MEDIA_TYPE)或类似的响应以帮助调试。因此,建议避免将URL处理拆分到注解控制器和视图控制器之间。spring-doc.cadn.net.cn

1.11.9. 视图解析器

MVC配置简化了视图解析器的注册。spring-doc.cadn.net.cn

以下Java配置示例通过使用JSP和Jackson作为默认的View进行内容协商视图解析JSON渲染:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.jsp();
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.enableContentNegotiation(MappingJackson2JsonView())
        registry.jsp()
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<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 和脚本模板也需要配置底层视图技术。spring-doc.cadn.net.cn

MVC名称空间提供了专用元素。以下示例与FreeMarker一起工作:spring-doc.cadn.net.cn

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:freemarker cache="false"/>
</mvc:view-resolvers>

<mvc:freemarker-configurer>
    <mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>

在Java配置中,你可以添加相应的Configurer bean, 如下例所示:spring-doc.cadn.net.cn

Java
@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;
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.enableContentNegotiation(MappingJackson2JsonView())
        registry.freeMarker().cache(false)
    }

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("/freemarker")
    }
}

1.11.10. 静态资源

此选项提供了一种方便的方式来从基于Resource的列表位置提供静态资源。spring-doc.cadn.net.cn

在下一个示例中,给定一个以 /resources 开始的请求,使用相对路径来查找并提供相对于 /public 的静态资源,这些资源位于 Web 应用程序根目录下或类路径下的 /static。这些资源将被设置为一年后过期,以确保最大限度地利用浏览器缓存并减少浏览器发出的 HTTP 请求。还会评估 Last-Modified 标头,如果存在,则返回 304 状态码。spring-doc.cadn.net.cn

以下示例展示了如何使用Java配置来实现:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
            .addResourceLocations("/public", "classpath:/static/")
            .setCachePeriod(31556926);
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCachePeriod(31556926)
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:resources mapping="/resources/**"
    location="/public, classpath:/static/"
    cache-period="31556926" />

资源处理器还支持一系列的 ResourceResolver 实现和 ResourceTransformer 实现, 您可以使用它们来创建一个用于处理优化资源的工具链。spring-doc.cadn.net.cn

您可以使用 VersionResourceResolver 用于基于 MD5 哈希计算的版本化资源 URL,该哈希从内容、固定的应用程序版本或其他内容中计算得出。一个 ContentVersionStrategy(MD5 哈希)是不错的选择——尽管有一些显著的例外,例如与模块加载器一起使用的 JavaScript 资源。spring-doc.cadn.net.cn

以下示例展示了如何在Java配置中使用VersionResourceResolverspring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:resources mapping="/resources/**" location="/public/">
    <mvc:resource-chain resource-cache="true">
        <mvc:resolvers>
            <mvc:version-resolver>
                <mvc:content-version-strategy patterns="/**"/>
            </mvc:version-resolver>
        </mvc:resolvers>
    </mvc:resource-chain>
</mvc:resources>

然后可以使用 ResourceUrlProvider 重写URL并应用完整的解析器和转换器链——例如,插入版本。MVC配置提供了一个 ResourceUrlProvider bean,以便它可以被注入到其他bean中。你还可以通过 ResourceUrlEncodingFilter 使重写对Thymeleaf、JSP、FreeMarker和其他依赖于 HttpServletResponse#encodeURL 的URL标签透明。spring-doc.cadn.net.cn

请注意,当同时使用 EncodedResourceResolver(例如,用于提供gzip或brotli编码的资源)和 VersionResourceResolver 时,您必须按此顺序注册它们。这确保了基于内容的版本始终根据未编码文件可靠地计算。spring-doc.cadn.net.cn

WebJars 也通过 WebJarsResourceResolver 得到支持,当 org.webjars:webjars-locator-core 库存在于类路径上时,该库会自动注册。解析器可以重写URL以包含jar的版本,并且还可以匹配不带版本的传入URL——例如,从 /jquery/jquery.min.js/jquery/1.2.0/jquery.min.jsspring-doc.cadn.net.cn

1.11.11. 默认Servlet

Spring MVC 允许将 DispatcherServlet 映射到 /(从而覆盖容器默认 Servlet 的映射),同时仍然允许静态资源请求由容器的默认 Servlet 处理。它配置了一个 DefaultServletHttpRequestHandler,具有 /** 的 URL 映射和相对于其他 URL 映射的最低优先级。spring-doc.cadn.net.cn

此处理器将所有请求转发到默认的Servlet。因此,它必须保持在所有其他URL HandlerMappings的最后。如果你使用<mvc:annotation-driven>,就是这种情况。或者,如果你设置了自己的自定义HandlerMapping实例,请确保将其order属性设置为低于DefaultServletHttpRequestHandler的值,DefaultServletHttpRequestHandler的值是Integer.MAX_VALUEspring-doc.cadn.net.cn

以下示例展示了如何通过默认设置启用该功能:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
        configurer.enable()
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:default-servlet-handler/>

覆盖/ Servlet映射的注意事项是,必须通过名称而不是路径来检索默认Servlet的RequestDispatcher。在启动时,DefaultServletHttpRequestHandler试图自动检测容器中的默认Servlet,使用大多数主要Servlet容器(包括Tomcat、Jetty、GlassFish、JBoss、Resin、WebLogic和WebSphere)的已知名称列表进行自动检测。如果默认Servlet已被自定义配置为不同的名称,或者正在使用一个未知默认Servlet名称的不同Servlet容器,则必须显式提供默认Servlet的名称,如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable("myCustomDefaultServlet");
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {
        configurer.enable("myCustomDefaultServlet")
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

1.11.12. 路径匹配

您可以自定义与路径匹配和URL处理相关的选项。 有关各个选项的详细信息,请参阅 PathMatchConfigurer javadoc。spring-doc.cadn.net.cn

以下示例展示了如何在Java配置中自定义路径匹配:spring-doc.cadn.net.cn

Java
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseTrailingSlashMatch(false)
            .setUseRegisteredSuffixPatternMatch(true)
            .setPathMatcher(antPathMatcher())
            .setUrlPathHelper(urlPathHelper())
            .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
    }

    @Bean
    public UrlPathHelper urlPathHelper() {
        //...
    }

    @Bean
    public PathMatcher antPathMatcher() {
        //...
    }
}
Kotlin
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {

    override fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseSuffixPatternMatch(true)
            .setUseTrailingSlashMatch(false)
            .setUseRegisteredSuffixPatternMatch(true)
            .setPathMatcher(antPathMatcher())
            .setUrlPathHelper(urlPathHelper())
            .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
    }

    @Bean
    fun urlPathHelper(): UrlPathHelper {
        //...
    }

    @Bean
    fun antPathMatcher(): PathMatcher {
        //...
    }
}

以下示例展示了如何在XML中实现相同的配置:spring-doc.cadn.net.cn

<mvc:annotation-driven>
    <mvc:path-matching
        trailing-slash="false"
        registered-suffixes-only="true"
        path-helper="pathHelper"
        path-matcher="pathMatcher"/>
</mvc:annotation-driven>

<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>

1.11.13. 高级Java配置

@EnableWebMvc 引入了 DelegatingWebMvcConfiguration,这:spring-doc.cadn.net.cn

对于高级模式,你可以移除 @EnableWebMvc 并直接从 DelegatingWebMvcConfiguration 扩展,而不是实现 WebMvcConfigurer, 如下例所示:spring-doc.cadn.net.cn

Java
@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    // ...
}
Kotlin
@Configuration
class WebConfig : DelegatingWebMvcConfiguration() {

    // ...
}

你可以保留现有的方法在WebConfig中,但现在你也可以覆盖基类中的bean声明,并且你仍然可以在类路径上有任何数量的其他WebMvcConfigurer实现。spring-doc.cadn.net.cn

1.11.14. 高级XML配置

MVC命名空间没有高级模式。如果你需要自定义一个无法通过其他方式更改的bean上的属性,可以使用Spring ApplicationContextBeanPostProcessor 生命周期钩子,如下例所示:spring-doc.cadn.net.cn

Java
@Component
public class MyPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
        // ...
    }
}
Kotlin
@Component
class MyPostProcessor : BeanPostProcessor {

    override fun postProcessBeforeInitialization(bean: Any, name: String): Any {
        // ...
    }
}

请注意,您需要将MyPostProcessor声明为一个bean,要么在XML中显式声明,要么通过<component-scan/>声明让其被检测到。spring-doc.cadn.net.cn

1.12. HTTP/2

Servlet 4 容器需要支持 HTTP/2,并且 Spring Framework 5 与 Servlet API 4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置相关的问题需要注意。更多详细信息,请参见 HTTP/2 维基页面spring-doc.cadn.net.cn

Servlet API 确实暴露了一个与 HTTP/2 相关的构造。你可以使用 javax.servlet.http.PushBuilder 主动向客户端推送资源,并且它作为 方法参数 支持在 @RequestMapping 方法中使用。spring-doc.cadn.net.cn

2. REST 客户端

本节描述了客户端访问 REST 端点的选项。spring-doc.cadn.net.cn

2.1. RestTemplate

RestTemplate 是一个同步客户端,用于执行 HTTP 请求。它是原始的 Spring REST 客户端,并在底层 HTTP 客户端库上提供了一个简单的模板方法 API。spring-doc.cadn.net.cn

从5.0版本开始,RestTemplate处于维护模式,仅接受少量更改请求和错误修复。请考虑使用WebClient,它提供了一个更现代化的API,并支持同步、异步和流式场景。

请参阅 REST端点 了解详细信息。spring-doc.cadn.net.cn

2.2. WebClient

WebClient 是一个非阻塞的、响应式的客户端,用于执行 HTTP 请求。它在 5.0 版本中引入,并提供了一个现代的替代方案来代替 RestTemplate,支持同步和异步以及流式场景下的高效操作。spring-doc.cadn.net.cn

RestTemplate不同,WebClient支持以下内容:spring-doc.cadn.net.cn

请参阅 WebClient 了解更多信息。spring-doc.cadn.net.cn

3. 测试

本节总结了spring-test中可用于Spring MVC应用程序的选项。spring-doc.cadn.net.cn

  • Servlet API Mocks: Servlet API 合同的模拟实现,用于单元测试控制器、过滤器和其他 Web 组件。有关详细信息,请参阅 Servlet API 模拟对象。spring-doc.cadn.net.cn

  • TestContext Framework: 支持在JUnit和TestNG测试中加载Spring配置,包括在测试方法之间高效缓存加载的配置以及支持加载WebApplicationContextMockServletContext。 有关更多详细信息,请参见TestContext Frameworkspring-doc.cadn.net.cn

  • Spring MVC 测试:一个框架,也称为MockMvc,用于通过DispatcherServlet(即支持注解)测试带有注解的控制器,包括 Spring MVC 基础设施但不包含 HTTP 服务器。 有关详细信息,请参见Spring MVC 测试spring-doc.cadn.net.cn

  • 客户端REST:spring-test 提供了一个 MockRestServiceServer,你可以将其用作模拟服务器,用于测试客户端代码,这些代码内部使用了 RestTemplate。有关详细信息,请参阅 客户端REST测试spring-doc.cadn.net.cn

  • WebTestClient: 专为测试WebFlux应用程序而构建,但也可以用于通过HTTP连接对任何服务器进行端到端集成测试。它是一个非阻塞的响应式客户端,非常适合测试异步和流式场景。spring-doc.cadn.net.cn

4. WebSockets

这部分参考文档涵盖了对Servlet堆栈的支持,WebSocket消息传递,包括原始的WebSocket交互,通过SockJS进行的WebSocket模拟,以及通过STOMP作为WebSocket上的子协议实现的发布-订阅消息传递。spring-doc.cadn.net.cn

4.1. WebSocket简介

WebSocket协议,RFC 6455,提供了一种标准化的方式,用于在客户端和服务器之间通过单个TCP连接建立全双工、双向通信通道。它与HTTP是不同的TCP协议,但设计为通过HTTP工作,使用端口80和443,并允许重用现有的防火墙规则。spring-doc.cadn.net.cn

WebSocket 交互从使用 HTTP Upgrade 头的 HTTP 请求开始,以升级或在这种情况下切换到 WebSocket 协议。以下示例显示了这样的交互:spring-doc.cadn.net.cn

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
1 核心 Upgrade 标头。
2 使用 Upgrade 连接。

与通常的200状态码不同,支持WebSocket的服务器返回的输出类似于以下内容:spring-doc.cadn.net.cn

HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
1 协议切换

成功握手后,HTTP升级请求底层的TCP套接字保持打开状态,客户端和服务器可以继续发送和接收消息。spring-doc.cadn.net.cn

关于WebSockets的工作原理的完整介绍超出了本文档的范围。请参阅RFC 6455、HTML5中的WebSocket章节,或网络上的众多介绍和教程。spring-doc.cadn.net.cn

请注意,如果WebSocket服务器运行在Web服务器(例如nginx)后面,您可能需要配置它以将WebSocket升级请求传递给WebSocket服务器。同样,如果应用程序在云环境中运行,请查看云提供商关于WebSocket支持的相关说明。spring-doc.cadn.net.cn

4.1.1. HTTP 与 WebSocket

尽管WebSocket被设计为与HTTP兼容并以HTTP请求开始,但重要的是要理解这两种协议会导致非常不同的架构和应用程序编程模型。spring-doc.cadn.net.cn

在HTTP和REST中,应用程序被建模为许多URL。要与应用程序交互,客户端访问这些URL,采用请求-响应的方式。服务器根据HTTP URL、方法和标头将请求路由到适当的处理器。spring-doc.cadn.net.cn

相比之下,在WebSockets中,通常只有一个URL用于初始连接。 随后,所有应用程序消息都在同一个TCP连接上流动。这指向了一种完全不同的异步、事件驱动的消息架构。spring-doc.cadn.net.cn

WebSocket 也是一种低级别的传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息。spring-doc.cadn.net.cn

WebSocket 客户端和服务器可以通过 HTTP 握手请求中的 Sec-WebSocket-Protocol 头部协商使用更高层级的消息协议(例如,STOMP)。如果没有这个头部,它们需要自己制定约定。spring-doc.cadn.net.cn

4.1.2. 何时使用WebSockets

WebSockets 可以使网页变得动态和互动。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供一个简单而有效的解决方案。spring-doc.cadn.net.cn

例如,新闻、邮件和社交动态需要动态更新,但每隔几分钟更新一次可能是完全可以接受的。另一方面,协作、游戏和金融应用程序需要更接近实时。spring-doc.cadn.net.cn

延迟本身并不是决定性因素。如果消息量相对较低(例如,监控网络故障),HTTP流或轮询可以提供有效的解决方案。正是低延迟、高频率和高容量的结合使得使用WebSocket成为最佳选择。spring-doc.cadn.net.cn

请记住,通过互联网,您无法控制的限制性代理可能会阻止WebSocket交互,要么是因为它们没有配置为传递Upgrade标头,要么是因为它们关闭了看起来空闲的长连接。这意味着在防火墙内的内部应用程序中使用WebSocket是一个更直接的决定,而不是在面向公众的应用程序中。spring-doc.cadn.net.cn

4.2. WebSocket API

Spring框架提供了一个WebSocket API,你可以使用它来编写处理WebSocket消息的客户端和服务器端应用程序。spring-doc.cadn.net.cn

4.2.1. WebSocketHandler

创建WebSocket服务器就像实现WebSocketHandler一样简单,或者更可能的是,扩展TextWebSocketHandlerBinaryWebSocketHandler。以下示例使用TextWebSocketHandlerspring-doc.cadn.net.cn

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处理器映射到特定URL,有专门的WebSocket Java配置和XML命名空间支持,如下例所示:spring-doc.cadn.net.cn

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配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

上一个示例适用于Spring MVC应用程序,并应包含在DispatcherServlet的配置中。但是,Spring的WebSocket支持并不依赖于Spring MVC。借助WebSocketHttpRequestHandler,相对简单地将WebSocketHandler集成到其他HTTP服务环境中。spring-doc.cadn.net.cn

在直接使用WebSocketHandler API与间接使用,例如通过STOMP消息传递时,应用程序必须同步消息的发送,因为底层的标准WebSocket会话(JSR-356)不允许并发发送。一个选项是将WebSocketSession包装为ConcurrentWebSocketSessionDecoratorspring-doc.cadn.net.cn

4.2.2. WebSocket 握手

定制初始HTTP WebSocket握手请求的最简单方法是通过一个HandshakeInterceptor,它提供了在握手“之前”和“之后”的方法。你可以使用此类拦截器来阻止握手或使任何属性对WebSocketSession可用。以下示例使用了一个内置拦截器将HTTP会话属性传递给WebSocket会话:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

一个更高级的选项是扩展DefaultHandshakeHandler,该组件执行WebSocket握手的步骤,包括验证客户端源、协商子协议和其他细节。如果应用程序需要配置自定义RequestUpgradeStrategy以适应尚未支持的WebSocket服务器引擎和版本,也可能需要使用此选项(有关此主题的更多信息,请参见部署)。Java配置和XML命名空间都允许配置自定义HandshakeHandlerspring-doc.cadn.net.cn

Spring 提供了一个 WebSocketHandlerDecorator 基类,你可以用来装饰一个 WebSocketHandler 以添加额外的行为。日志记录和异常处理实现已经提供,并且在使用 WebSocket Java 配置或 XML 命名空间时默认添加。ExceptionWebSocketHandlerDecorator 捕获任何 WebSocketHandler 方法中出现的所有未捕获的异常,并使用状态 1011 关闭 WebSocket 会话,该状态表示服务器错误。

4.2.3. 部署

Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,在那里 DispatcherServlet 同时处理 HTTP WebSocket 握手和其他 HTTP 请求。它也很容易通过调用 WebSocketHttpRequestHandler 集成到其他 HTTP 处理场景中。这很方便且易于理解。然而,对于 JSR-356 运行时有一些特殊注意事项。spring-doc.cadn.net.cn

Java WebSocket API (JSR-356) 提供了两种部署机制。第一种涉及在启动时对 Servlet 容器类路径进行扫描(Servlet 3 的特性)。另一种是在 Servlet 容器初始化时使用注册 API。这两种机制都无法实现使用单一的“前端控制器”来处理所有 HTTP 请求——包括 WebSocket 握手和所有其他 HTTP 请求——例如 Spring MVC 的 DispatcherServletspring-doc.cadn.net.cn

这是JSR-356的一个重要限制,Spring的WebSocket支持通过服务器特定的RequestUpgradeStrategy实现来解决,即使在JSR-356运行时也是如此。 目前这些策略适用于Tomcat、Jetty、GlassFish、WebLogic、WebSphere和Undertow(以及WildFly)。spring-doc.cadn.net.cn

关于克服Java WebSocket API上述限制的请求已创建,可以在此处关注: eclipse-ee4j/websocket-api#211。 Tomcat、Undertow 和 WebSphere 提供了它们自己的API替代方案,这使得这一点成为可能,并且Jetty也可以实现。我们希望更多的服务器也能这样做。

一个次要的考虑因素是,支持JSR-356的Servlet容器预计会执行ServletContainerInitializer(SCI)扫描,这可能会减慢应用程序启动速度——在某些情况下,这种影响非常明显。如果在升级到支持JSR-356的Servlet容器版本后观察到了显著的影响,应该可以通过在web.xml中使用<absolute-ordering />元素来选择性地启用或禁用Web片段(和SCI扫描),如下例所示:spring-doc.cadn.net.cn

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering/>

</web-app>

然后你可以通过名称有选择地启用web片段,例如Spring自己的SpringServletContainerInitializer,它提供了对Servlet 3 Java初始化API的支持。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <absolute-ordering>
        <name>spring_web</name>
    </absolute-ordering>

</web-app>

4.2.4. 服务器配置

每个底层WebSocket引擎都暴露了配置属性,这些属性控制运行时特性,例如消息缓冲区大小、空闲超时等。spring-doc.cadn.net.cn

对于Tomcat、WildFly和GlassFish,您可以在WebSocket Java配置中添加一个ServletServerContainerFactoryBean,如下例所示:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>

</beans>
对于客户端WebSocket配置,您应该使用WebSocketContainerFactoryBean (XML) 或 ContainerProvider.getWebSocketContainer() (Java配置)。

对于Jetty,你需要提供一个预配置的Jetty WebSocketServerFactory 并将其插入到Spring的DefaultHandshakeHandler中,通过你的WebSocket Java配置。 以下示例展示了如何操作:spring-doc.cadn.net.cn

@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配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/echo" handler="echoHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>

    <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>

    <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
        <constructor-arg ref="serverFactory"/>
    </bean>

    <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
        <constructor-arg>
            <bean class="org.eclipse.jetty...WebSocketPolicy">
                <constructor-arg value="SERVER"/>
                <property name="inputBufferSize" value="8092"/>
                <property name="idleTimeout" value="600000"/>
            </bean>
        </constructor-arg>
    </bean>

</beans>

4.2.5. 允许的源

从Spring Framework 4.1.5开始,默认行为是仅接受同源请求的WebSocket和SockJS。也可以允许所有或指定列表中的源。此检查主要是为浏览器客户端设计的。没有任何东西阻止其他类型的客户端修改Origin头值(有关详细信息,请参阅RFC 6454: Web Origin Concept)。spring-doc.cadn.net.cn

三种可能的行为是:spring-doc.cadn.net.cn

  • 仅允许同源请求(默认):在这种模式下,当SockJS启用时,Iframe HTTP响应头X-Frame-Options被设置为SAMEORIGIN,并且禁用JSONP传输,因为它不允许检查请求的来源。因此,当启用此模式时,IE6和IE7不受支持。spring-doc.cadn.net.cn

  • 允许指定的来源列表:每个允许的来源必须以 http://https:// 开头。在这种模式下,当 SockJS 启用时,IFrame 传输将被禁用。因此,启用此模式时,IE6 到 IE9 不受支持。spring-doc.cadn.net.cn

  • 允许所有源:要启用此模式,您应提供 * 作为允许的源值。在此模式下,所有传输方式都可用。spring-doc.cadn.net.cn

您可以配置WebSocket和SockJS允许的源,如下例所示:spring-doc.cadn.net.cn

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
    }

    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers allowed-origins="https://mydomain.com">
        <websocket:mapping path="/myHandler" handler="myHandler" />
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

4.3. SockJS 备选方案

通过公共互联网,您无法控制的限制性代理可能会阻止WebSocket交互,原因可能是它们未配置为传递Upgrade标头,或者因为它们会关闭看起来处于空闲状态的长连接。spring-doc.cadn.net.cn

这个问题的解决方案是WebSocket模拟——也就是说,首先尝试使用WebSocket,然后回退到基于HTTP的技术,这些技术模拟WebSocket交互并提供相同的应用程序级API。spring-doc.cadn.net.cn

在Servlet堆栈上,Spring框架提供了对SockJS协议的服务器(以及客户端)支持。spring-doc.cadn.net.cn

4.3.1. 概览

SockJS 的目标是让应用程序使用 WebSocket API,但在运行时必要时可以无缝切换到非 WebSocket 替代方案,而无需更改应用程序代码。spring-doc.cadn.net.cn

SockJS 包括:spring-doc.cadn.net.cn

SockJS 专为浏览器使用而设计。它使用多种技术来支持广泛版本的浏览器。 有关 SockJS 传输类型和浏览器的完整列表,请参见 SockJS 客户端 页面。传输 分为三个一般类别:WebSocket、HTTP 流式传输和 HTTP 长轮询。 有关这些类别的概述,请参见 这篇博客文章spring-doc.cadn.net.cn

SockJS客户端首先发送GET /info以从服务器获取基本信息。之后,它必须决定使用哪种传输方式。如果可能,将使用WebSocket。如果不可用,在大多数浏览器中,至少有一种HTTP流选项。如果没有,则使用HTTP(长)轮询。spring-doc.cadn.net.cn

所有传输请求具有以下URL结构:spring-doc.cadn.net.cn

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

WebSocket 传输只需要一个 HTTP 请求来进行 WebSocket 握手。 此后所有消息都在该套接字上交换。spring-doc.cadn.net.cn

HTTP传输需要更多的请求。例如,Ajax/XHR流依赖于一个长连接请求来处理服务器到客户端的消息,并且需要额外的HTTP POST请求来处理客户端到服务器的消息。长轮询与此类似,不同之处在于它在每次服务器到客户端发送消息后结束当前请求。spring-doc.cadn.net.cn

SockJS 添加了最小的消息帧。例如,服务器最初发送字母 o (“打开”帧),消息作为 a["message1","message2"] (JSON编码的数组) 发送,如果25秒内没有消息流动,则发送字母 h (“心跳”帧),并发送字母 c (“关闭”帧) 来关闭会话。spring-doc.cadn.net.cn

要了解更多,请在浏览器中运行一个示例并观察HTTP请求。 SockJS客户端允许固定传输列表,因此可以逐个查看每个传输。SockJS客户端还提供了一个调试标志, 这会在浏览器控制台启用有用的提示信息。在服务器端,您可以为org.springframework.web.socket启用TRACE日志记录。 有关更多详细信息,请参阅SockJS协议的叙述性测试spring-doc.cadn.net.cn

4.3.2. 启用SockJS

您可以通过Java配置启用SockJS,如下例所示:spring-doc.cadn.net.cn

@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配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:sockjs/>
    </websocket:handlers>

    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

上一个示例适用于Spring MVC应用程序,并应包含在DispatcherServlet的配置中。但是,Spring的WebSocket和SockJS支持并不依赖于Spring MVC。借助SockJsHttpRequestHandler,将其集成到其他HTTP服务环境中相对简单。spring-doc.cadn.net.cn

在浏览器端,应用程序可以使用 sockjs-client (版本 1.0.x)。它 模拟了W3C WebSocket API,并与服务器通信以选择最佳的 传输选项,具体取决于运行它的浏览器。请参阅 sockjs-client 页面和浏览器支持的 传输类型列表。客户端还提供了几个 配置选项——例如,指定要包含哪些传输。spring-doc.cadn.net.cn

4.3.3. IE 8 和 9

Internet Explorer 8 和 9 仍在使用。它们是 SockJS 存在的关键原因之一。本节涵盖了在这些浏览器中运行的重要注意事项。spring-doc.cadn.net.cn

SockJS客户端通过使用Microsoft的XDomainRequest在IE 8和9中支持Ajax/XHR流。这可以跨域工作,但不支持发送cookie。
Cookie对于Java应用程序通常至关重要。
但是,由于SockJS客户端可以与多种服务器类型(不仅仅是Java服务器)一起使用,因此需要知道cookie是否重要。
如果重要,SockJS客户端更倾向于使用Ajax/XHR进行流传输。否则,它依赖于基于iframe的技术。spring-doc.cadn.net.cn

The first /info request from the SockJS client is a request for information that can influence the client’s choice of transports. One of those details is whether the server application relies on cookies (for example, for authentication purposes or clustering with sticky sessions). Spring’s SockJS support includes a property called sessionCookieNeeded. It is enabled by default, since most Java applications rely on the JSESSIONID cookie. If your application does not need it, you can turn off this option, and SockJS client should then choose xdr-streaming in IE 8 and 9。spring-doc.cadn.net.cn

如果你确实使用了基于iframe的传输,请记住,浏览器可以通过将HTTP响应头X-Frame-Options设置为DENYSAMEORIGINALLOW-FROM <origin>来指示阻止在给定页面上使用IFrames。这是用来防止点击劫持spring-doc.cadn.net.cn

Spring Security 3.2+ 提供了将 X-Frame-Options 设置为每个响应的支持。默认情况下,Spring Security Java 配置将其设置为 DENY。在 3.2 版本中,Spring Security XML 命名空间不会默认设置该标头,但可以配置为这样做。将来,可能会默认设置该标头。spring-doc.cadn.net.cn

请参阅默认安全头 Spring Security文档中的详细信息,了解如何配置X-Frame-Options头的设置。你还可以参阅SEC-2501以获取更多背景信息。spring-doc.cadn.net.cn

如果你的应用程序添加了X-Frame-Options响应头(正如它应该做的那样!)并且依赖于基于iframe的传输,你需要将头值设置为SAMEORIGINALLOW-FROM <origin>。Spring SockJS支持还需要知道SockJS客户端的位置,因为它是从iframe加载的。默认情况下,iframe被设置为从CDN位置下载SockJS客户端。最好将此选项配置为与应用程序相同的源。spring-doc.cadn.net.cn

以下示例展示了如何在Java配置中实现这一点:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS()
                .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
    }

    // ...

}

XML命名空间通过<websocket:sockjs>元素提供了类似的选择。spring-doc.cadn.net.cn

在初始开发期间,请启用SockJS客户端devel模式,该模式防止浏览器缓存SockJS请求(如iframe),否则这些请求将被缓存。有关如何启用它的详细信息,请参阅SockJS客户端页面。

4.3.4. 心跳

SockJS协议要求服务器发送心跳消息以防止代理认为连接已挂起。Spring SockJS配置中有一个名为heartbeatTime的属性,您可以使用它来自定义频率。默认情况下,如果在此连接上没有发送其他消息,则在25秒后发送心跳消息。这个25秒的值符合以下IETF建议中的公共互联网应用程序。spring-doc.cadn.net.cn

在使用WebSocket和SockJS的STOMP时,如果STOMP客户端和服务器协商交换心跳,SockJS的心跳将被禁用。

Spring SockJS支持还允许您配置TaskScheduler以调度心跳任务。任务调度器由线程池支持,默认设置基于可用处理器的数量。您应该根据您的具体需求考虑自定义这些设置。spring-doc.cadn.net.cn

4.3.5. 客户端断开连接

HTTP流媒体和HTTP长轮询SockJS传输需要连接保持比通常更长的时间。有关这些技术的概述,请参阅这篇博客文章spring-doc.cadn.net.cn

在Servlet容器中,这是通过Servlet 3的异步支持来实现的,该支持允许退出Servlet容器线程,处理请求,并从另一个线程继续向响应写入。spring-doc.cadn.net.cn

一个特定的问题是Servlet API没有提供客户端断开连接的通知。参见eclipse-ee4j/servlet-api#44。 然而,Servlet容器在后续尝试向响应写入时会抛出异常。由于Spring的SockJS服务支持服务器发送的心跳(默认每25秒一次),这意味着通常在该时间段内可以检测到客户端断开连接(如果消息发送更频繁,则可以更早检测到)。spring-doc.cadn.net.cn

因此,可能会因为客户端断开连接而导致网络I/O失败,这可能会在日志中填充不必要的堆栈跟踪。Spring尽最大努力识别表示客户端断开连接的此类网络故障(针对每个服务器)并使用专用的日志类别DISCONNECTED_CLIENT_LOG_CATEGORY(在AbstractSockJsSession中定义)记录最小的消息。如果你需要查看堆栈跟踪,可以将该日志类别设置为TRACE。

4.3.6. SockJS 和 CORS

如果你允许跨域请求(参见允许的源),SockJS协议在XHR流式传输和轮询传输中使用CORS进行跨域支持。因此,除非检测到响应中存在CORS头,否则会自动添加CORS头。所以,如果一个应用程序已经配置为提供CORS支持(例如,通过Servlet Filter),Spring的SockJsService会跳过这一部分。spring-doc.cadn.net.cn

也可以通过在Spring的SockJsService中设置suppressCors属性来禁用添加这些CORS标头。spring-doc.cadn.net.cn

SockJS 需要以下头信息和值:spring-doc.cadn.net.cn

有关确切的实现,请参见源代码中的 addCorsHeadersAbstractSockJsServiceTransportType 枚举。spring-doc.cadn.net.cn

Alternatively, if the CORS configuration allows it, consider excluding URLs with the SockJS endpoint prefix, thus letting Spring’s SockJsService handle it.spring-doc.cadn.net.cn

4.3.7. SockJsClient

Spring 提供了一个 SockJS Java 客户端,用于在不使用浏览器的情况下连接到远程 SockJS 端点。这在需要通过公共网络进行两个服务器之间的双向通信时特别有用(即,在网络代理可能阻止使用 WebSocket 协议的情况下)。SockJS Java 客户端在测试中也非常有用(例如,模拟大量并发用户)。spring-doc.cadn.net.cn

SockJS Java客户端支持websocketxhr-streamingxhr-polling传输。其余的传输方式仅在浏览器中使用才有意义。spring-doc.cadn.net.cn

您可以使用以下方式配置WebSocketTransportspring-doc.cadn.net.cn

一个 XhrTransport,根据定义,同时支持 xhr-streamingxhr-polling,因为从客户端的角度来看,除了用于连接服务器的URL之外,没有其他区别。目前有两种实现:spring-doc.cadn.net.cn

以下示例展示了如何创建一个SockJS客户端并连接到SockJS端点:spring-doc.cadn.net.cn

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进行配置:spring-doc.cadn.net.cn

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

以下示例展示了服务器端与 SockJS 相关的属性(有关详细信息,请参阅 javadoc), 这些属性你也应该考虑进行自定义:spring-doc.cadn.net.cn

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/sockjs").withSockJS()
            .setStreamBytesLimit(512 * 1024) (1)
            .setHttpMessageCacheSize(1000) (2)
            .setDisconnectDelay(30 * 1000); (3)
    }

    // ...
}
1 streamBytesLimit 属性设置为 512KB(默认值为 128KB — 128 * 1024)。
2 httpMessageCacheSize 属性设置为 1,000(默认值为 100)。
3 disconnectDelay 属性设置为 30 秒(默认值为五秒 — 5 * 1000)。

4.4. STOMP

WebSocket协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,客户端和服务器可以通过该机制协商一个子协议(即更高层次的消息协议),以便在WebSocket之上使用,从而定义双方可以发送什么样的消息、消息的格式以及每条消息的内容等。使用子协议是可选的,但无论是否使用子协议,客户端和服务器都需要就一种定义消息内容的协议达成一致。spring-doc.cadn.net.cn

4.4.1. 概览

STOMP (简单文本定向消息协议) 最初是为脚本语言(如 Ruby、Python 和 Perl)连接到企业消息代理而创建的。它旨在解决常用消息模式的一个最小子集。STOMP 可以通过任何可靠的双向流网络协议使用,例如 TCP 和 WebSocket。尽管 STOMP 是一种文本定向协议,但消息负载可以是文本或二进制。spring-doc.cadn.net.cn

STOMP 是一种基于帧的协议,其帧的模型基于 HTTP。以下列出了 STOMP 帧的结构:spring-doc.cadn.net.cn

COMMAND
header1:value1
header2:value2

Body^@

Clients can use the SEND or SUBSCRIBE commands to send or subscribe for messages, along with a destination header that describes what the message is about and who should receive it. This enables a simple publish-subscribe mechanism that you can use to send messages through the broker to other connected clients or to send messages to the server to request that some work be performed.spring-doc.cadn.net.cn

当你使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理。消息被路由到@Controller个消息处理方法或一个简单的内存中代理,该代理跟踪订阅并将消息广播给已订阅的用户。你还可以配置Spring与专用的STOMP代理(如RabbitMQ、ActiveMQ等)一起工作,用于实际的消息广播。在这种情况下,Spring维护与代理的TCP连接,将消息转发给它,并将来自它的消息传递给已连接的WebSocket客户端。因此,Spring Web应用程序可以依赖统一的基于HTTP的安全性、通用验证和熟悉的消息处理编程模型。spring-doc.cadn.net.cn

以下示例展示了一个客户端订阅接收股票报价,服务器可能会周期性地发出这些报价(例如,通过一个定时任务发送消息到经纪人):spring-doc.cadn.net.cn

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

以下示例展示了一个发送交易请求的客户端,该请求可以通过服务器上的@MessageMapping方法处理:spring-doc.cadn.net.cn

SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

执行后,服务器可以 向客户端广播交易确认消息和详细信息。spring-doc.cadn.net.cn

目的地的含义在STOMP规范中故意保持不透明。它可以是任何字符串,完全由STOMP服务器定义它们支持的目的地的语义和语法。然而,目的地通常被定义为路径类似的字符串,其中/topic/..表示发布-订阅(一对多)消息交换,而/queue/表示点对点(一对一)消息交换。spring-doc.cadn.net.cn

STOMP服务器可以使用MESSAGE命令向所有订阅者广播消息。 以下示例展示了一个服务器向已订阅的客户端发送股票报价:spring-doc.cadn.net.cn

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

服务器不能发送未请求的消息。来自服务器的所有消息都必须是对特定客户端订阅的响应,并且服务器消息的subscription-id标头必须与客户端订阅的id标头匹配。spring-doc.cadn.net.cn

上述概述旨在提供对STOMP协议的基本理解。我们建议完整查看该协议的 规范spring-doc.cadn.net.cn

4.4.2. 好处

使用STOMP作为子协议可以让Spring框架和Spring Security提供比使用原始WebSockets更丰富的编程模型。同样的观点也可以应用于HTTP与原始TCP的对比,以及它如何让Spring MVC和其他Web框架提供丰富的功能。以下是好处列表:spring-doc.cadn.net.cn

  • 无需发明自定义的消息协议和消息格式。spring-doc.cadn.net.cn

  • STOMP客户端,包括一个Java客户端 在Spring Framework中,都是可用的。spring-doc.cadn.net.cn

  • 您可以(可选地)使用消息代理(如 RabbitMQ、ActiveMQ 等)来管理订阅和广播消息。spring-doc.cadn.net.cn

  • 应用程序逻辑可以组织在任意数量的@Controller实例中,并且可以根据STOMP目标头将消息路由到它们,而不是使用单个WebSocketHandler处理给定连接的原始WebSocket消息。spring-doc.cadn.net.cn

  • 您可以使用 Spring Security 根据 STOMP 目的地和消息类型来保护消息。spring-doc.cadn.net.cn

4.4.3. 启用STOMP

STOMP over WebSocket 支持在 spring-messagingspring-websocket 模块中可用。一旦你有了这些依赖项,你就可以通过 SockJS Fallback 以 WebSocket 的方式暴露 STOMP 端点,如下例所示:spring-doc.cadn.net.cn

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();  (1)
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app"); (2)
        config.enableSimpleBroker("/topic", "/queue"); (3)
    }
}
1 /portfolio 是WebSocket(或SockJS)客户端需要连接以进行WebSocket握手的端点的HTTP URL。
2 STOMP 消息的目标头以 /app 开头的将被路由到 @MessageMapping 方法中的 @Controller 类。
3 使用内置的消息代理进行订阅和广播,并将目标头以 /topic `or `/queue 开头的消息路由到代理。

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio">
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/topic, /queue"/>
    </websocket:message-broker>

</beans>
对于内置的简单代理,/topic/queue 前缀没有任何特殊含义。它们仅仅是一种约定,用于区分发布订阅与点对点消息传递(即多个订阅者与一个消费者)。当你使用外部代理时,请检查代理的STOMP页面,以了解它支持哪种类型的STOMP目标和前缀。

要从浏览器连接,对于SockJS,你可以使用 sockjs-client。对于STOMP,许多应用程序使用了 jmesnil/stomp-websocket 库(也称为 stomp.js),该库功能完整,并且已经在生产环境中使用多年,但已不再维护。目前 JSteunou/webstomp-client 是该库最活跃的维护和发展的继任者。以下示例代码基于它:spring-doc.cadn.net.cn

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

或者,如果你通过WebSocket(不使用SockJS)连接,可以使用以下代码:spring-doc.cadn.net.cn

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

请注意,在前面的例子中,stompClient 不需要指定 loginpasscode 标头。即使指定了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参见 连接到代理身份验证spring-doc.cadn.net.cn

有关更多示例代码,请参见:spring-doc.cadn.net.cn

4.4.4. WebSocket服务器

要配置底层的WebSocket服务器,服务器配置中的信息适用。但是对于Jetty,您需要通过StompEndpointRegistry设置HandshakeHandlerWebSocketPolicyspring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
    }

    @Bean
    public DefaultHandshakeHandler handshakeHandler() {

        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);

        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
}

4.4.5. 消息流

一旦暴露了STOMP端点,Spring应用程序就会成为连接客户端的STOMP代理。本节描述了服务器端的消息流。spring-doc.cadn.net.cn

The spring-messaging 模块包含最初源自Spring Integration的基础消息传递应用程序支持,并在后来被提取并纳入Spring Framework,以便在许多Spring项目和应用场景中更广泛地使用。 以下列表简要描述了可用的消息传递抽象:spring-doc.cadn.net.cn

Java配置(即,@EnableWebSocketMessageBroker)和XML命名空间配置(即,<websocket:message-broker>)都使用上述组件来组装消息流程。下图显示了启用简单内置消息代理时使用的组件:spring-doc.cadn.net.cn

message flow simple broker

前面的图表显示了三个消息通道:spring-doc.cadn.net.cn

下一个图显示了当配置外部代理(如 RabbitMQ)用于管理订阅和广播消息时所使用的组件:spring-doc.cadn.net.cn

message flow broker relay

前两个图的主要区别在于使用了“代理中继”来通过 TCP 将消息传递到外部 STOMP 代理,以及将消息从代理传递到已订阅的客户端。spring-doc.cadn.net.cn

当通过WebSocket连接接收到消息时,这些消息会被解码为STOMP帧, 转换为Spring Message表示形式,并发送到 clientInboundChannel进行进一步处理。例如,目标头以/app开头的STOMP消息 可能会被路由到带注解的控制器中的@MessageMapping方法,而/topic/queue消息则可能直接 路由到消息代理。spring-doc.cadn.net.cn

一个处理客户端STOMP消息的注解@Controller可以通过brokerChannel将消息发送到消息代理,代理通过clientOutboundChannel将消息广播给匹配的订阅者。同一个控制器也可以对HTTP请求做出相同的操作,因此客户端可以执行HTTP POST,然后@PostMapping方法可以将消息发送到消息代理以广播给已订阅的客户端。spring-doc.cadn.net.cn

我们可以通过一个简单的例子来跟踪流程。考虑以下示例,它设置了一个服务器:spring-doc.cadn.net.cn

@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;
    }
}

前面的示例支持以下流程:spring-doc.cadn.net.cn

  1. 客户端连接到 http://localhost:8080/portfolio,一旦建立WebSocket连接,STOMP帧就会开始在其上流动。spring-doc.cadn.net.cn

  2. 客户端发送一个带有目标头为 /topic/greeting 的 SUBSCRIBE 消息。一旦接收到并解码,该消息将被发送到 clientInboundChannel,然后被路由到消息代理,该代理存储客户端的订阅。spring-doc.cadn.net.cn

  3. 客户端向 /app/greeting 发送一个 aSEND 帧。/app 前缀用于将其路由到带注解的控制器。在剥离 /app 前缀后,目的地的剩余部分 /greeting 将映射到 GreetingController 中的 @MessageMapping 方法。spring-doc.cadn.net.cn

  4. GreetingController返回的值被转换为一个Spring Message,其负载基于返回值,且默认目标头为/topic/greeting(从输入目标中通过将/app替换为/topic得到)。生成的消息被发送到brokerChannel,并由消息代理处理。spring-doc.cadn.net.cn

  5. 消息代理会找到所有匹配的订阅者,并通过 clientOutboundChannel 向每个订阅者发送 MESSAGE 帧,从那里消息会被编码为 STOMP 帧并通过 WebSocket 连接发送。spring-doc.cadn.net.cn

下一节将提供更多有关带注解方法的详细信息,包括支持的参数类型和返回值类型。spring-doc.cadn.net.cn

4.4.6. 注解控制器

应用程序可以使用带注释的 @Controller 类来处理来自客户端的消息。 此类可以声明 @MessageMapping@SubscribeMapping@ExceptionHandler 方法,如以下主题所述:spring-doc.cadn.net.cn

@MessageMapping

您可以使用 @MessageMapping 来注解根据消息目标路由方法。它在方法级别以及类型级别都受支持。在类型级别,@MessageMapping 用于表达控制器中所有方法之间的共享映射。spring-doc.cadn.net.cn

默认情况下,映射值是 Ant 风格的路径模式(例如 /thing*, /thing/**), 包括对模板变量的支持(例如,/thing/{id})。可以通过 @DestinationVariable 方法参数引用这些值。应用程序还可以切换到以点分隔的目标约定来映射,如 点作为分隔符 中所述。spring-doc.cadn.net.cn

支持的方法参数

下表描述了方法参数:spring-doc.cadn.net.cn

方法参数 描述

Messagespring-doc.cadn.net.cn

要访问完整消息。spring-doc.cadn.net.cn

MessageHeadersspring-doc.cadn.net.cn

访问 Message 中的标头。spring-doc.cadn.net.cn

MessageHeaderAccessor, SimpMessageHeaderAccessor, 和 StompHeaderAccessorspring-doc.cadn.net.cn

通过类型化访问器方法访问标头。spring-doc.cadn.net.cn

@Payloadspring-doc.cadn.net.cn

要访问消息的有效负载,已通过配置的 MessageConverter 转换(例如,从 JSON)。spring-doc.cadn.net.cn

此注解的出现并非必需,因为如果没有其他参数匹配,默认情况下会将其视为已存在。spring-doc.cadn.net.cn

您可以使用 @javax.validation.Valid 或 Spring 的 @Validated 对有效负载参数进行注解, 以使有效负载参数自动进行验证。spring-doc.cadn.net.cn

@Headerspring-doc.cadn.net.cn

要访问特定的头值 — 如果需要,可以使用 org.springframework.core.convert.converter.Converter 进行类型转换。spring-doc.cadn.net.cn

@Headersspring-doc.cadn.net.cn

用于访问消息中的所有头信息。此参数必须可分配给 java.util.Mapspring-doc.cadn.net.cn

@DestinationVariablespring-doc.cadn.net.cn

对于访问从消息目标中提取的模板变量。 值会根据声明的方法参数类型进行转换。spring-doc.cadn.net.cn

java.security.Principalspring-doc.cadn.net.cn

在 WebSocket HTTP 握手时的已登录用户。spring-doc.cadn.net.cn

返回值

默认情况下,来自 @MessageMapping 方法的返回值会通过匹配的 MessageConverter 序列化为负载,并作为 Message 发送到 brokerChannel, 从那里它会被广播给订阅者。出站消息的目标与入站消息的目标相同,但前面带有 /topicspring-doc.cadn.net.cn

您可以使用 @SendTo@SendToUser 注解来自定义输出消息的目标。@SendTo 用于自定义目标目的地或指定多个目的地。@SendToUser 用于将输出消息仅定向到与输入消息关联的用户。参见 用户目标spring-doc.cadn.net.cn

您可以同时在同一个方法中使用 @SendTo@SendToUser,并且两者都在类级别上受支持,在这种情况下,它们作为该类中方法的默认值。但是,请注意,任何方法级别的 @SendTo@SendToUser 注解都会覆盖类级别的任何此类注解。spring-doc.cadn.net.cn

消息可以异步处理,并且 @MessageMapping 方法可以返回 ListenableFutureCompletableFutureCompletionStagespring-doc.cadn.net.cn

请注意,@SendTo@SendToUser 只是方便的用法,实际上相当于使用 SimpMessagingTemplate 发送消息。如果需要,对于更高级的场景,@MessageMapping 方法可以回退到直接使用 SimpMessagingTemplate。 这可以代替返回一个值,或者可能同时进行。 参见 发送消息spring-doc.cadn.net.cn

@SubscribeMapping

@SubscribeMapping@MessageMapping 类似,但将映射限制为仅订阅消息。它支持与 @MessageMapping 相同的 方法参数。然而,对于返回值,默认情况下,消息会直接发送给客户端(通过 clientOutboundChannel,作为对订阅的响应),而不是发送给代理服务器(通过 brokerChannel,作为匹配订阅的广播)。添加 @SendTo@SendToUser 会覆盖此行为,而是发送给代理服务器。spring-doc.cadn.net.cn

什么时候会用到这个?假设代理被映射到/topic/queue,而应用程序控制器被映射到/app。在这种设置下,代理将所有针对重复广播的订阅存储到/topic/queue,应用程序无需参与。客户端也可以订阅某些/app目标,控制器可以对此订阅返回一个值,而无需涉及代理,也不需要再次存储或使用该订阅(有效的一次性请求-响应交换)。此用例之一是在启动时用初始数据填充用户界面。spring-doc.cadn.net.cn

什么时候这种情况没有用?除非你希望代理和控制器独立处理消息(包括订阅),否则不要尝试将代理和控制器映射到相同的目录取决。传入的消息是并行处理的。不能保证代理或控制器先处理某条消息。如果目标是在订阅存储并准备好广播时收到通知,客户端应请求收据(如果服务器支持的话,简单代理不支持)。例如,使用 Java STOMP 客户端,你可以执行以下操作来添加收据:spring-doc.cadn.net.cn

@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...
});

服务器端选项是 注册 一个 ExecutorChannelInterceptorbrokerChannel 上并实现 afterMessageHandled 在消息(包括订阅)处理之后调用的方法。spring-doc.cadn.net.cn

@MessageExceptionHandler

应用程序可以使用 @MessageExceptionHandler 种方法来处理来自 @MessageMapping 方法的异常。您可以在注解本身中声明异常,或者通过方法参数声明,如果您想访问异常实例的话。 下面的示例通过方法参数声明了一个异常:spring-doc.cadn.net.cn

@Controller
public class MyController {

    // ...

    @MessageExceptionHandler
    public ApplicationError handleException(MyException exception) {
        // ...
        return appError;
    }
}

@MessageExceptionHandler 方法支持灵活的方法签名,并支持与 @MessageMapping 方法相同的方法参数类型和返回值。spring-doc.cadn.net.cn

通常,@MessageExceptionHandler 方法在其声明的 @Controller 类(或类层次结构)中适用。如果您希望这些方法更全局地应用(跨控制器),可以将它们声明在一个用 @ControllerAdvice 标记的类中。这类似于 Spring MVC 中提供的 类似支持spring-doc.cadn.net.cn

4.4.7. 发送消息

如果你希望从应用程序的任何部分向连接的客户端发送消息呢?任何应用程序组件都可以向 brokerChannel 发送消息。最简单的方法是注入一个 SimpMessagingTemplate 并使用它来发送消息。通常,你会通过类型进行注入,如下例所示:spring-doc.cadn.net.cn

@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)来限定它。spring-doc.cadn.net.cn

4.4.8. 简单代理

内置的简单消息代理处理来自客户端的订阅请求,将其存储在内存中,并向具有匹配目标的已连接客户端广播消息。该代理支持类似路径的目标,包括对 Ant 风格目标模式的订阅。spring-doc.cadn.net.cn

应用程序也可以使用点分隔(而不是斜杠分隔)的目标。 查看 使用点作为分隔符

如果配置了任务调度器,简单消息代理将支持 STOMP 心跳。 为此,您可以声明自己的调度器,或使用自动声明并在内部使用的调度器。以下示例展示了如何声明您自己的调度器:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private TaskScheduler messageBrokerTaskScheduler;

    @Autowired
    public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
        this.messageBrokerTaskScheduler = taskScheduler;
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.enableSimpleBroker("/queue/", "/topic/")
                .setHeartbeatValue(new long[] {10000, 20000})
                .setTaskScheduler(this.messageBrokerTaskScheduler);

        // ...
    }
}

4.4.9. 外部代理

简单代理非常适合入门,但仅支持STOMP命令的一个子集(它不支持确认、回执和其他一些功能),依赖于一个简单的消息发送循环,并且不适合集群。 作为替代方案,您可以升级您的应用程序以使用功能齐全的消息代理。spring-doc.cadn.net.cn

请参阅您选择的消息代理的STOMP文档(例如 RabbitMQ, ActiveMQ和其他),安装代理, 并以启用STOMP支持的方式运行它。然后您可以在Spring配置中启用STOMP代理中继 (而不是简单代理)。spring-doc.cadn.net.cn

以下示例配置启用了功能齐全的消息代理:spring-doc.cadn.net.cn

@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配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio" />
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

</beans>

前面配置中的STOMP代理中继器是一个Spring MessageHandler ,它通过将消息转发到外部消息代理来处理消息。 为此,它会建立与代理的TCP连接,将所有消息转发给代理, 然后通过WebSocket会话将从代理接收到的所有消息转发给客户端。本质上,它作为一个“中继”在两个方向上转发消息。spring-doc.cadn.net.cn

添加 io.projectreactor.netty:reactor-nettyio.netty:netty-all 依赖项以管理TCP连接。

此外,应用程序组件(如HTTP请求处理方法、业务服务等)也可以按照 发送消息 中所述,向代理中继发送消息,以向订阅的WebSocket客户端广播消息。spring-doc.cadn.net.cn

实际上,代理中继实现了健壮且可扩展的消息广播。spring-doc.cadn.net.cn

4.4.10. 连接到代理服务器

STOMP 代理中继与代理保持一个“系统”TCP 连接。 此连接仅用于来自服务器端应用程序的消息, 不用于接收消息。您可以为该连接配置 STOMP 凭据(即, STOMP 帧 loginpasscode 头部)。这在 XML 命名空间和 Java 配置中均以 systemLoginsystemPasscode 属性形式暴露,其默认值为 guestguestspring-doc.cadn.net.cn

STOMP 代理中继为每个连接的 WebSocket 客户端创建单独的 TCP 连接。您可以配置用于所有代表客户端创建的 TCP 连接的 STOMP 凭据。这在 XML 命名空间和 Java 配置中均以 clientLoginclientPasscode 属性形式提供,其默认值为 guestguestspring-doc.cadn.net.cn

STOMP 代理始终在它代表客户端转发到代理的每个 CONNECT 帧上设置 loginpasscode 头。因此,WebSocket 客户端不需要设置这些头。它们将被忽略。如 身份验证 部分所述,WebSocket 客户端应改用 HTTP 身份验证来保护 WebSocket 端点并建立客户端身份。

STOMP 代理中继通过“系统”TCP 连接向消息代理发送和接收心跳。您可以配置发送和接收心跳的间隔时间(默认各为 10 秒)。如果与代理的连接丢失,代理中继会继续每 5 秒尝试重新连接,直到成功为止。spring-doc.cadn.net.cn

任何Spring Bean都可以实现 ApplicationListener<BrokerAvailabilityEvent> 在与代理的“系统”连接丢失并重新建立时接收通知。例如,一个广播股票报价的股票报价服务在没有活动的“系统”连接时可以停止尝试发送消息。spring-doc.cadn.net.cn

默认情况下,STOMP 代理中继总是连接到相同的主机和端口,并在连接丢失时重新连接。如果您希望在每次尝试连接时提供多个地址,可以配置一个地址提供程序,而不是固定的主机和端口。下面的示例展示了如何做到这一点:spring-doc.cadn.net.cn

@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 代理。 此属性的值将作为每个 CONNECT 帧的 host 头部设置, 在云环境中可能很有用(例如,当实际建立 TCP 连接的主机与提供基于云的 STOMP 服务的主机不同时)。spring-doc.cadn.net.cn

4.4.11. 使用句点作为分隔符

当消息被路由到 @MessageMapping 方法时,它们会与 AntPathMatcher 匹配。默认情况下,模式应使用斜杠 (/) 作为分隔符。 这在Web应用程序中是一个很好的惯例,类似于HTTP URL。然而,如果您更习惯于消息传递约定,可以切换为使用点 (.) 作为分隔符。spring-doc.cadn.net.cn

以下示例展示了如何在Java配置中实现这一点:spring-doc.cadn.net.cn

@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配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:websocket="http://www.springframework.org/schema/websocket"
        xsi:schemaLocation="
                http://www.springframework.org/schema/beans
                https://www.springframework.org/schema/beans/spring-beans.xsd
                http://www.springframework.org/schema/websocket
                https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker application-destination-prefix="/app" path-matcher="pathMatcher">
        <websocket:stomp-endpoint path="/stomp"/>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>

    <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher">
        <constructor-arg index="0" value="."/>
    </bean>

</beans>

之后,控制器可以在 @MessageMapping 方法中使用点(.)作为分隔符, 如下面的示例所示:spring-doc.cadn.net.cn

@Controller
@MessageMapping("red")
public class RedController {

    @MessageMapping("blue.{green}")
    public void handleGreen(@DestinationVariable String green) {
        // ...
    }
}

客户端现在可以向 /app/red.blue.green123 发送消息。spring-doc.cadn.net.cn

在前面的示例中,我们没有更改“broker relay”的前缀,因为这些前缀完全取决于外部消息代理。请参阅您使用的代理的 STOMP 文档页面,以查看目标头支持哪些约定。spring-doc.cadn.net.cn

另一方面,“简单代理”依赖于配置的 PathMatcher,因此,如果您更改了分隔符,该更改也会应用到代理,并且代理匹配消息到订阅中的模式的方式也会改变。spring-doc.cadn.net.cn

4.4.12. 身份验证

每个基于 WebSocket 的 STOMP 消息传输会话都从 HTTP 请求开始。 这可以是升级到 WebSocket 的请求(即 WebSocket 握手) 或者,在 SockJS 回退的情况下,是一系列 SockJS HTTP 传输请求。spring-doc.cadn.net.cn

许多网络应用程序已经具备了认证和授权机制,以保护HTTP请求。通常,用户通过使用登录页面、HTTP基本认证或其他方式,由Spring Security进行认证。已认证用户的安全上下文会保存在HTTP会话中,并与同一基于Cookie的会话中的后续请求相关联。spring-doc.cadn.net.cn

因此,对于 WebSocket 握手或 SockJS HTTP 传输请求,通常已经可以通过 HttpServletRequest#getUserPrincipal() 访问到已认证的用户。Spring 会自动将该用户与为他们创建的 WebSocket 或 SockJS 会话关联,并且随后通过该会话传输的所有 STOMP 消息也会通过用户头进行关联。spring-doc.cadn.net.cn

简而言之,典型的Web应用程序除了已经做的安全工作外,不需要做其他事情。用户在HTTP请求级别进行身份验证,安全上下文通过基于Cookie的HTTP会话保持(该会话随后与为该用户创建的WebSocket或SockJS会话相关联),并且会导致每个通过应用程序的Message都带有用户头信息。spring-doc.cadn.net.cn

请注意,STOMP 协议在 CONNECT 帧上确实有 loginpasscode 头信息。这些头信息最初是为此设计的,并且在某些情况下仍然需要,例如通过 TCP 使用 STOMP 时。然而,对于通过 WebSocket 使用的 STOMP,默认情况下,Spring 会忽略 STOMP 协议级别的授权头信息,假定用户已经在 HTTP 传输级别完成身份验证,并期望 WebSocket 或 SockJS 会话中包含已认证的用户。spring-doc.cadn.net.cn

Spring Security 提供了 WebSocket 子协议授权 ,该功能使用 ChannelInterceptor 根据消息中的用户头信息对消息进行授权。 此外,Spring Session 提供了 WebSocket 集成 ,确保在 WebSocket 会话仍然处于活动状态时,用户的 HTTP 会话不会过期。

4.4.13. Tokens认证

Spring Security OAuth 提供基于Tokens的安全支持,包括JSON Web Token(JWT)。 您可以将其用作Web应用程序中的身份验证机制, 包括WebSocket上的STOMP交互,如上一节所述 (即通过基于cookie的会话来维持身份)。spring-doc.cadn.net.cn

同时,基于 cookie 的会话并不总是最佳选择(例如,在不维护服务器端会话的应用程序中,或者在移动应用程序中,通常使用头信息进行身份验证)。spring-doc.cadn.net.cn

The WebSocket协议,RFC 6455 “并未规定服务器在WebSocket握手期间对客户端进行身份验证的特定方式。” 实际上,浏览器客户端只能使用标准的身份验证头(即基本HTTP身份验证)或cookies,而不能(例如)提供自定义头。同样,SockJS JavaScript客户端也无法在SockJS传输请求中发送HTTP头。请参见 sockjs-client问题196。 相反,它允许发送查询参数,您可以使用这些参数发送Tokens,但这也有其自身的缺点(例如,Tokens可能会意外地与URL一起记录在服务器日志中)。spring-doc.cadn.net.cn

前面的限制适用于基于浏览器的客户端,不适用于基于 Spring 的 Java STOMP 客户端,该客户端支持在 WebSocket 和 SockJS 请求中发送头信息。

因此,希望避免使用 Cookie 的应用程序在 HTTP 协议级别可能没有任何好的替代认证方案。它们可以选择在 STOMP 消息协议级别通过消息头进行认证。这样做需要两个简单的步骤:spring-doc.cadn.net.cn

  1. 使用STOMP客户端在连接时传递身份验证头。spring-doc.cadn.net.cn

  2. 使用 ChannelInterceptor 处理身份验证头信息。spring-doc.cadn.net.cn

下一个示例使用服务器端配置来注册一个自定义的身份验证拦截器。请注意,拦截器只需对 CONNECT Message 进行身份验证并设置用户头信息即可。Spring 会记录并保存已验证的用户,并将其与同一会话上的后续 STOMP 消息关联。下面的示例展示了如何注册一个自定义的身份验证拦截器:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    Authentication user = ... ; // access authentication header(s)
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}

此外,请注意,当您使用 Spring Security 的消息授权时,目前需要确保认证 ChannelInterceptor 配置的顺序在 Spring Security 之前。这可以通过在自己的 WebSocketMessageBrokerConfigurer 实现中声明自定义拦截器来完成,并用 @Order(Ordered.HIGHEST_PRECEDENCE + 99) 标记。spring-doc.cadn.net.cn

4.4.14. 用户目的地

应用程序可以发送针对特定用户的消息,Spring 的 STOMP 支持为此识别以 /user/ 为前缀的目的地。 例如,客户端可能会订阅 /user/queue/position-updates 目的地。 该目的地由 UserDestinationMessageHandler 处理, 并转换为特定于用户会话的目的地 (例如 /queue/position-updates-user123)。这提供了订阅通用名称目的地的便利性, 同时确保与其他订阅相同目的地的用户之间不会发生冲突,从而使每个用户都能收到独立的股票持仓更新。spring-doc.cadn.net.cn

在发送端,消息可以发送到如/user/{username}/queue/position-updates这样的目标,该目标又通过UserDestinationMessageHandler转换为一个或多个目标,每个目标对应与用户相关联的一个会话。这使得应用程序中的任何组件都可以发送针对特定用户的消息,而无需知道除了用户名和通用目标之外的其他信息。这也通过注解和消息模板得到支持。spring-doc.cadn.net.cn

一个消息处理方法可以通过<code>0</code>注解(也支持在类级别上使用,以共享一个共同的目标)将消息发送给与正在处理的消息相关联的用户,如下例所示:spring-doc.cadn.net.cn

@Controller
public class PortfolioController {

    @MessageMapping("/trade")
    @SendToUser("/queue/position-updates")
    public TradeResult executeTrade(Trade trade, Principal principal) {
        // ...
        return tradeResult;
    }
}

如果用户有多个会话,默认情况下,所有订阅到给定目标的会话都会被定位。但是,有时可能需要仅定位正在处理的消息发送的会话。你可以通过将 broadcast 属性设置为 false 来实现,如下例所示:spring-doc.cadn.net.cn

@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命名空间创建的<code>0</code>,将消息发送到用户目标。 (如果需要与<code>2</code>进行限定,bean名称为<code>1</code>。)下面的示例显示了如何操作:spring-doc.cadn.net.cn

@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配置中的userDestinationBroadcast属性和XML中的message-broker元素的user-destination-broadcast属性来完成。spring-doc.cadn.net.cn

4.4.15. 消息的顺序

来自代理的消息被发布到 clientOutboundChannel,从那里它们被写入WebSocket会话。由于该通道由 ThreadPoolExecutor 支持,消息会在不同的线程中处理,客户端接收到的结果顺序可能与发布的顺序不完全一致。spring-doc.cadn.net.cn

如果这是个问题,请启用 setPreservePublishOrder 标志,如下例所示:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    protected void configureMessageBroker(MessageBrokerRegistry registry) {
        // ...
        registry.setPreservePublishOrder(true);
    }

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker preserve-publish-order="true">
        <!-- ... -->
    </websocket:message-broker>

</beans>

当设置该标志时,同一客户端会话中的消息将按顺序依次发布到 clientOutboundChannel,从而保证发布的顺序。 请注意,这会带来一定的性能开销,因此只有在需要时才应启用它。spring-doc.cadn.net.cn

4.4.16. 事件

多个 ApplicationContext 事件会被发布,并可以通过实现 Spring 的 ApplicationListener 接口来接收:spring-doc.cadn.net.cn

  • BrokerAvailabilityEvent: 表示代理何时可用或不可用。 当“简单”代理在启动时立即可用并在应用程序运行期间保持可用时,STOMP“代理中继”可能会失去与完整功能代理的连接(例如,如果代理重新启动)。代理中继具有重新连接逻辑,并在代理恢复时重新建立“系统”连接。因此,每当状态从已连接变为断开或反之,都会发布此事件。使用 SimpMessagingTemplate 的组件应订阅此事件,并在代理不可用时避免发送消息。无论如何,它们应准备好在发送消息时处理 MessageDeliveryExceptionspring-doc.cadn.net.cn

  • SessionConnectEvent: 当接收到新的 STOMP CONNECT 时发布,表示新客户端会话的开始。该事件包含表示连接的消息,包括会话 ID、用户信息(如果有)以及客户端发送的任何自定义标头。这对于跟踪客户端会话很有用。订阅此事件的组件可以使用 SimpMessageHeaderAccessorStompMessageHeaderAccessor 包装其中的消息。spring-doc.cadn.net.cn

  • SessionConnectedEvent: 在代理发送了对 CONNECT 的 STOMP CONNECTED 帧响应后不久发布。此时,STOMP 会话可以被视为已完全建立。spring-doc.cadn.net.cn

  • SessionSubscribeEvent: 在接收到新的 STOMP SUBSCRIBE 时发布。spring-doc.cadn.net.cn

  • SessionUnsubscribeEvent: 在接收到新的 STOMP UNSUBSCRIBE 时触发。spring-doc.cadn.net.cn

  • SessionDisconnectEvent: 当STOMP会话结束时发布。DISCONNECT可能由客户端发送,也可能在WebSocket会话关闭时自动生成。在某些情况下,每个会话可能会多次发布此事件。组件应针对多个断开事件具有幂等性。spring-doc.cadn.net.cn

当使用功能齐全的消息代理时,如果消息代理暂时不可用,STOMP“消息代理中继”会自动重新连接“系统”连接。但是,客户端连接不会自动重新连接。假设启用了心跳机制,客户端通常会在10秒内发现消息代理无响应。客户端需要实现自己的重新连接逻辑。

4.4.17. 拦截

事件 为 STOMP 连接的生命周期提供通知,但不针对每个客户端消息。应用程序还可以注册一个 ChannelInterceptor 来拦截任何消息以及处理链中的任何部分。 以下示例显示了如何拦截来自客户端的入站消息:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new MyChannelInterceptor());
    }
}

自定义 ChannelInterceptor 可以使用 StompHeaderAccessorSimpMessageHeaderAccessor 来访问有关消息的信息,如下例所示:spring-doc.cadn.net.cn

public class MyChannelInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        StompCommand command = accessor.getStompCommand();
        // ...
        return message;
    }
}

应用程序还可以实现 ExecutorChannelInterceptor,它是 ChannelInterceptor 的一个子接口,在处理消息的线程中具有回调函数。 当向通道发送消息时,每个消息都会调用一次 ChannelInterceptor,而 ExecutorChannelInterceptor 则在每个订阅了该通道消息的 MessageHandler 的线程中提供钩子函数。spring-doc.cadn.net.cn

请注意,与前面所述的 SesionDisconnectEvent 类似,断开消息可以来自客户端,也可以在 WebSocket 会话关闭时自动生成。在某些情况下,拦截器可能会对每个会话多次拦截此消息。组件应针对多个断开事件具有幂等性。spring-doc.cadn.net.cn

4.4.18. STOMP 客户端

Spring 提供了一个基于 WebSocket 的 STOMP 客户端和一个基于 TCP 的 STOMP 客户端。spring-doc.cadn.net.cn

首先,您可以创建并配置 WebSocketStompClient,如下例所示:spring-doc.cadn.net.cn

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在前面的例子中,您可以将 StandardWebSocketClient 替换为 SockJsClient, 因为这也是 WebSocketClient 的一种实现。 SockJsClient 可以使用 WebSocket 或基于 HTTP 的传输作为回退方案。 有关详细信息,请参见 SockJsClientspring-doc.cadn.net.cn

接下来,你可以建立连接并为STOMP会话提供一个处理程序,如下例所示:spring-doc.cadn.net.cn

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

当会话准备好使用时,会通知处理程序,如下例所示:spring-doc.cadn.net.cn

public class MyStompSessionHandler extends StompSessionHandlerAdapter {

    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        // ...
    }
}

会话建立后,可以发送任何有效负载,并使用配置的 MessageConverter 进行序列化,如下例所示:spring-doc.cadn.net.cn

session.send("/topic/something", "payload");

您还可以订阅目标。 subscribe 方法需要一个用于订阅消息的处理程序,并返回一个 Subscription 句柄,您可以使用该句柄取消订阅。对于每个接收到的消息,处理程序可以指定应将有效负载反序列化的目标 Object 类型,如下例所示:spring-doc.cadn.net.cn

session.subscribe("/topic/something", new StompFrameHandler() {

    @Override
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        // ...
    }

});

要启用STOMP心跳,可以将WebSocketStompClient配置为TaskScheduler 并可选地自定义心跳间隔(10秒的写入不活动时间, 这会导致发送心跳,以及10秒的读取不活动时间, 这会关闭连接)。spring-doc.cadn.net.cn

当您在性能测试中使用 WebSocketStompClient 来从同一台机器模拟数千个客户端时,建议关闭心跳功能,因为每个连接都会安排自己的心跳任务,这在同一台机器上运行大量客户端时并不是最优的。

STOMP协议还支持确认,其中客户端必须添加一个receipt头,服务器在发送或订阅处理后会通过RECEIPT帧进行响应。为了支持此功能,StompSession提供了setAutoReceipt(boolean),这会导致在每次后续的发送或订阅事件中添加一个receipt头。 或者,您也可以手动将确认头添加到StompHeaders中。 发送和订阅都会返回一个Receiptable的实例,您可以使用它来注册确认成功和失败的回调。 为此功能,您必须将客户端配置为TaskScheduler,以及确认过期前的时间(默认为15秒)。spring-doc.cadn.net.cn

请注意,StompSessionHandler本身是一个StompFrameHandler,这使得它除了可以处理消息处理中的异常的handleException回调外,还可以处理错误帧,以及handleTransportError用于传输层错误,包括ConnectionLostExceptionspring-doc.cadn.net.cn

4.4.19. WebSocket 作用域

每个 WebSocket 会话都有一个属性映射。该映射作为报头附加到传入的客户端消息,并可以从控制器方法中访问,如下例所示:spring-doc.cadn.net.cn

@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 使用范围代理模式,如下例所示:spring-doc.cadn.net.cn

@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 生命周期方法,如前面的示例所示。spring-doc.cadn.net.cn

4.4.20. 性能

在性能方面没有万能的解决方案。许多因素都会影响性能,包括消息的大小和数量、应用程序方法是否执行需要阻塞的操作,以及外部因素(如网络速度和其他问题)。本节的目标是提供可用配置选项的概述,并就如何考虑扩展性提供一些思路。spring-doc.cadn.net.cn

在消息应用程序中,消息通过通道进行异步执行,这些执行由线程池支持。配置此类应用程序需要了解通道和消息的流动。因此,建议回顾 消息的流动spring-doc.cadn.net.cn

显然,开始的地方是配置支持 clientInboundChannelclientOutboundChannel 的线程池。默认情况下,两者的配置都是可用处理器数量的两倍。spring-doc.cadn.net.cn

如果带注解的方法中消息的处理主要是CPU密集型的,那么clientInboundChannel的线程数应尽量接近处理器的数量。如果它们所做的工作更多是IO密集型的,并且需要在数据库或其他外部系统上进行阻塞或等待,线程池的大小可能需要增加。spring-doc.cadn.net.cn

ThreadPoolExecutor 有三个重要的属性:核心线程池大小、最大线程池大小以及用于存储没有可用线程的任务的队列容量。spring-doc.cadn.net.cn

一个常见的误解是,配置核心线程数(例如 10)和最大线程数(例如 20)会导致一个拥有 10 到 20 个线程的线程池。实际上,如果容量保持默认值 Integer.MAX_VALUE 不变,线程池永远不会超过核心线程数,因为所有额外的任务都会被排队。spring-doc.cadn.net.cn

查看 ThreadPoolExecutor 的 Javadoc 了解这些属性的工作方式并理解各种排队策略。spring-doc.cadn.net.cn

clientOutboundChannel 端,主要是向 WebSocket 客户端发送消息。如果客户端处于快速网络中,线程数量应保持接近可用处理器的数量。如果它们速度较慢或带宽较低,消耗消息所需的时间更长,并会给线程池带来负担。因此,增加线程池大小变得必要。spring-doc.cadn.net.cn

虽然clientInboundChannel的工作量可能可以预测 — 毕竟,它是基于应用程序所做的操作 — 但如何配置“clientOutboundChannel”则更难,因为它基于应用程序无法控制的因素。因此,还有两个额外的属性与消息的发送有关: sendTimeLimitsendBufferSizeLimit。您可以使用这些方法来配置发送操作允许花费的时间以及在向客户端发送消息时可以缓冲的数据量。spring-doc.cadn.net.cn

一般来说,任何时候只能使用一个线程向客户端发送消息。而所有其他消息则会被缓冲,你可以使用这些属性来决定发送消息允许花费多长时间以及在此期间可以缓冲多少数据。有关重要的额外细节,请参阅XML模式的javadoc和文档。spring-doc.cadn.net.cn

以下示例显示了一个可能的配置:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
    }

    // ...

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

您也可以使用前面所示的WebSocket传输配置来配置传入STOMP消息的最大允许大小。理论上,WebSocket消息的大小几乎是无限的。但实际上,WebSocket服务器会施加限制——例如,Tomcat上的8K和Jetty上的64K。因此,STOMP客户端(如JavaScript webstomp-client 和其他客户端)会在16K边界处拆分较大的STOMP消息,并作为多个WebSocket消息发送,这要求服务器进行缓冲和重新组合。spring-doc.cadn.net.cn

Spring 的 STOMP-over-WebSocket 支持实现了这一点,因此应用程序可以配置 STOMP 消息的最大大小,而无需考虑特定于 WebSocket 服务器的消息大小。请记住,如果需要的话,WebSocket 消息大小会自动进行调整,以确保它们至少可以承载 16K 的 WebSocket 消息。spring-doc.cadn.net.cn

以下示例显示了一种可能的配置:spring-doc.cadn.net.cn

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(128 * 1024);
    }

    // ...

}

以下示例展示了前面示例的XML配置等价写法:spring-doc.cadn.net.cn

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">

    <websocket:message-broker>
        <websocket:transport message-size="131072" />
        <!-- ... -->
    </websocket:message-broker>

</beans>

关于扩展的一个重要点是使用多个应用程序实例。 目前,你无法通过简单的代理程序实现这一点。 但是,当你使用一个功能齐全的代理程序(例如 RabbitMQ)时,每个应用程序实例都会连接到代理程序,一个应用程序实例广播的消息可以通过代理程序传递给通过其他任何应用程序实例连接的 WebSocket 客户端。spring-doc.cadn.net.cn

4.4.21. 监控

当使用 @EnableWebSocketMessageBroker<websocket:message-broker> 时,关键基础设施组件会自动收集统计信息和计数器,从而提供有关应用程序内部状态的重要洞察。该配置还声明了一个类型为 WebSocketMessageBrokerStats 的 Bean,用于将所有可用信息集中在一起,并默认每 30 分钟在 INFO 级别记录一次。此 Bean 可通过 Spring 的 MBeanExporter 导出到 JMX,以便在运行时查看(例如,通过 JDK 的 jconsole)。以下列表总结了可用的信息:spring-doc.cadn.net.cn

客户端WebSocket会话
当前

表示当前有多少个客户端会话,进一步按 WebSocket 与 HTTP 流式传输和轮询 SockJS 会话进行细分。spring-doc.cadn.net.cn

总数

表示已建立的总会话数。spring-doc.cadn.net.cn

异常关闭
连接失败

会话在建立后但未在60秒内收到任何消息就被关闭。这通常是代理或网络问题的迹象。spring-doc.cadn.net.cn

发送限制已超出

会话在超过配置的发送超时时间或发送缓冲区限制后关闭,这可能发生在慢速客户端上(参见上一节)。spring-doc.cadn.net.cn

传输错误

会话在传输错误后关闭,例如无法读取或写入 WebSocket 连接或 HTTP 请求或响应。spring-doc.cadn.net.cn

STOMP 帧

已处理的 CONNECT、CONNECTED 和 DISCONNECT 帧的总数,表示在 STOMP 层连接的客户端数量。请注意,当会话异常关闭或客户端未发送 DISCONNECT 帧就关闭时,DISCONNECT 的计数可能会较低。spring-doc.cadn.net.cn

STOMP代理中继
TCP 连接

指示通过客户端的WebSocket会话在代理上建立的TCP连接数量。这应该等于客户端WebSocket会话的数量加上一个额外的共享“系统”连接,用于从应用程序内部发送消息。spring-doc.cadn.net.cn

STOMP 帧

客户端向代理发送或接收的CONNECT、CONNECTED和DISCONNECT帧的总数。请注意,无论客户端WebSocket会话如何关闭,都会向代理发送一个DISCONNECT帧。因此,DISCONNECT帧数量较低表明代理正在主动关闭连接(可能是由于心跳未及时到达、无效的输入帧或其他问题)。spring-doc.cadn.net.cn

客户端入站通道

来自支持 clientInboundChannel 的线程池的统计数据,提供了有关传入消息处理健康状况的见解。在此排队的任务表明应用程序可能无法及时处理消息。如果存在 I/O 绑定的任务(例如,缓慢的数据库查询、对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。spring-doc.cadn.net.cn

客户端出站通道

来自支持 clientOutboundChannel 的线程池的统计信息,可提供有关向客户端广播消息健康状况的见解。在此排队的任务表明客户端消费消息的速度过慢。一种解决方法是增加线程池大小以适应预期的并发慢速客户端数量。另一种选择是减少发送超时时间和发送缓冲区大小限制(参见上一节)。spring-doc.cadn.net.cn

SockJS 任务调度器

线程池的 SockJS 任务调度器生成的统计数据,用于发送心跳。请注意,当在 STOMP 层协商心跳时,SockJS 心跳将被禁用。spring-doc.cadn.net.cn

4.4.22. 测试

当使用 Spring 的 STOMP-over-WebSocket 支持测试应用程序时,有两种主要方法。第一种是编写服务器端测试,以验证控制器及其注解的消息处理方法的功能。第二种是编写完整的端到端测试,涉及运行客户端和服务器。spring-doc.cadn.net.cn

两种方法并不互相排斥。相反,它们在整体测试策略中都有其位置。服务器端测试更专注,也更容易编写和维护。另一方面,端到端的集成测试更加全面,测试的内容也更多,但它们的编写和维护也更加复杂。spring-doc.cadn.net.cn

服务器端测试最简单的方式是编写控制器的单元测试。然而,这种方式并不足够有用,因为控制器的很多功能都依赖于其注解。纯单元测试无法测试这些内容。spring-doc.cadn.net.cn

理想情况下,被测试的控制器应该像运行时那样被调用,就像使用 Spring MVC 测试框架测试处理 HTTP 请求的控制器的方法一样——也就是说,不运行 Servlet 容器,而是依靠 Spring 框架来调用带注解的控制器。与 Spring MVC 测试类似,这里你有两种可能的替代方案,要么使用“基于上下文”的设置,要么使用“独立”设置:spring-doc.cadn.net.cn

  • 使用 Spring TestContext 框架加载实际的 Spring 配置,将 clientInboundChannel 注入为测试字段,并使用它向控制器方法发送要处理的消息。spring-doc.cadn.net.cn

  • 手动设置调用控制器(即 SimpAnnotationMethodMessageHandler)所需的最小 Spring 框架基础设施,并将消息直接传递给控制器。spring-doc.cadn.net.cn

这两种设置场景都在 股票投资组合的测试 示例应用程序中进行了演示。spring-doc.cadn.net.cn

第二种方法是创建端到端的集成测试。为此,你需要以嵌入模式运行一个WebSocket服务器,并作为WebSocket客户端连接到它,发送包含STOMP帧的WebSocket消息。 股票组合 示例应用程序的测试也通过使用Tomcat作为嵌入式WebSocket服务器和一个简单的STOMP客户端来演示这种方法。spring-doc.cadn.net.cn

5. 其他 Web 框架

本章详细介绍了Spring与第三方Web框架的集成。spring-doc.cadn.net.cn

Spring框架的核心价值主张之一就是提供选择。从一般意义上讲,Spring不会强制你使用或接受任何特定的架构、技术或方法(尽管它当然会推荐一些而不是其他)。开发人员及其开发团队可以选择最相关的架构、技术和方法,这种自由在Web领域尤为明显,Spring提供了自己的Web框架(Spring MVCSpring WebFlux),同时支持与许多流行的第三方Web框架集成。spring-doc.cadn.net.cn

5.1. 常见配置

在深入探讨每个支持的Web框架的集成细节之前,让我们先看一下不特定于任何一个Web框架的常见Spring配置。(本节同样适用于Spring自身的Web框架变种。)spring-doc.cadn.net.cn

Spring轻量级应用模型所提倡的一个概念(暂且用这个说法)是分层架构。请记住,在“经典”的分层架构中,Web层只是众多层中的一层。它作为进入服务器端应用的入口之一,并将任务委托给服务层中定义的服务对象(门面对象),以满足特定于业务(与表现技术无关)的用例。在Spring中,这些服务对象、任何其他业务相关的对象、数据访问对象等都存在于一个独立的“业务上下文”中,该上下文不包含Web或表现层的对象(表现对象,如Spring MVC控制器,通常在独立的“表现上下文”中进行配置)。本节将详细介绍如何配置一个包含应用程序中所有“业务Bean”的Spring容器(WebApplicationContext)。spring-doc.cadn.net.cn

接下来具体说明,您只需在Web应用程序的标准Java EE Servlet web.xml 文件中声明一个 ContextLoaderListener ,并添加一个 contextConfigLocation<context-param/> 部分(在同一文件中),以定义要加载的Spring XML配置文件集。spring-doc.cadn.net.cn

考虑以下 <listener/> 配置:spring-doc.cadn.net.cn

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

进一步考虑以下 <context-param/> 配置:spring-doc.cadn.net.cn

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>

如果您不指定 contextConfigLocation 上下文参数,ContextLoaderListener 会查找名为 /WEB-INF/applicationContext.xml 的文件来加载。上下文文件加载完成后,Spring 会根据 bean 定义创建一个 WebApplicationContext 对象,并将其存储在 Web 应用程序的 ServletContext 中。spring-doc.cadn.net.cn

所有 Java Web 框架都建立在 Servlet API 之上,因此您可以使用以下代码片段来访问由 ContextLoaderListener 创建的这个“业务上下文”ApplicationContextspring-doc.cadn.net.cn

以下示例显示了如何获取 WebApplicationContextspring-doc.cadn.net.cn

WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);

WebApplicationContextUtils 类是为了方便,因此您不需要记住 ServletContext 属性的名称。它的 getWebApplicationContext() 方法在对象不存在于 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 键下时返回 null。为了避免在应用程序中出现 NullPointerExceptions,最好使用 getRequiredWebApplicationContext() 方法。当 ApplicationContext 缺失时,此方法会抛出异常。spring-doc.cadn.net.cn

一旦你有了对 WebApplicationContext 的引用,就可以通过名称或类型获取 bean。大多数开发人员通过名称获取 bean,然后将其转换为其中一个实现的接口。spring-doc.cadn.net.cn

幸运的是,本节中的大多数框架都有更简单的方式来查找Bean。 它们不仅便于从Spring容器中获取Bean,还允许你在控制器中使用依赖注入。每个Web框架部分都对其特定的集成策略有更详细的说明。spring-doc.cadn.net.cn

5.2. JSF

JavaServer Faces (JSF) 是 JCP 的标准基于组件、事件驱动的 Web 用户界面框架。它是 Java EE 体系结构的一部分,也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。spring-doc.cadn.net.cn

请注意,JSF的较新版本在应用服务器中与CDI基础设施紧密关联,一些新的JSF功能仅在此类环境中才能正常工作。Spring的JSF支持不再积极发展,主要是在现代化旧的基于JSF的应用程序时用于迁移目的。spring-doc.cadn.net.cn

Spring 的 JSF 集成的关键元素是 JSF ELResolver 机制。spring-doc.cadn.net.cn

5.2.1. Spring Bean 解析器

SpringBeanFacesELResolver 是一个符合 JSF 的 ELResolver 实现, 与 JSF 和 JSP 中使用的标准统一 EL 集成。它首先委托给 Spring 的“业务上下文” WebApplicationContext,然后委托给底层 JSF 实现的 默认解析器。spring-doc.cadn.net.cn

在配置方面,您可以在JSF的SpringBeanFacesELResolver文件中定义faces-context.xml,如下例所示:spring-doc.cadn.net.cn

<faces-config>
    <application>
        <el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
        ...
    </application>
</faces-config>

5.2.2. 使用 FacesContextUtils

一个自定义的 ELResolver 在将您的属性映射到 faces-config.xml 中的 bean 时效果很好,但有时您可能需要显式获取一个 bean。 FacesContextUtils 类使这变得容易。它类似于 WebApplicationContextUtils,只是它接受一个 FacesContext 参数而不是 ServletContext 参数。spring-doc.cadn.net.cn

以下示例展示了如何使用FacesContextUtilsspring-doc.cadn.net.cn

ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());

5.3. Apache Struts 2.x

由Craig McClanahan发明,Struts 是一个由 Apache 软件基金会托管的开源项目。当时,它极大地简化了 JSP/Servlet 编程范式,并赢得了使用专有框架的许多开发者的青睐。它简化了编程模型,它是开源的(因此像啤酒一样免费),并且有一个庞大的社区,使该项目得以发展并在 Java 网络开发人员中变得流行。spring-doc.cadn.net.cn

作为原始 Struts 1.x 的后续版本,请查看 Struts 2.x 和 Struts 提供的 Spring 插件,以实现内置的 Spring 集成。spring-doc.cadn.net.cn

5.4. Apache Tapestry 5.x

Tapestry 是一个“面向组件的框架,用于在 Java 中创建动态、健壮、高度可扩展的 Web 应用程序。”spring-doc.cadn.net.cn

虽然Spring有自己的强大的Web层,但结合使用Tapestry作为Web用户界面和Spring容器作为底层层来构建企业Java应用程序有许多独特的优势。spring-doc.cadn.net.cn

有关更多信息,请参阅 Tapestry 的专用 Spring 集成模块spring-doc.cadn.net.cn

5.5. 其他资源

以下链接指向本章中描述的各种Web框架的其他资源。spring-doc.cadn.net.cn