22. Web MVC 框架

22.1 Spring Web MVC 框架简介

Spring Web 模型视图控制器(MVC)框架是围绕DispatcherServlet设计的,该框架将请求分配给处理程序,并具有可配置的处理程序 Map,视图分辨率,语言环境,时区和主题分辨率,以及对文件上传的支持。默认处理程序基于@Controller@RequestMapping注解,提供了多种灵活的处理方法。随着 Spring 3.0 的引入,@Controller机制还允许您通过@PathVariable注解和其他功能来创建 RESTful 网站和应用程序。

Note

“开放扩展……” Spring Web MVC 和 Spring 通常的一个关键设计原则是“开放扩展,封闭修改”原则。

Spring Web MVC 核心类中的某些方法标记为final。作为开发人员,您不能覆盖这些方法来提供自己的行为。这不是随意进行的,而是要牢记这一原则。

有关此原理的解释,请参考 Seth Ladd 等人的* Expert Spring Web MVC 和 Web Flow *。具体请参见第一版第 117 页的“外观设计”部分。或者,请参阅

当您使用 Spring MVC 时,您不能在最终方法中添加建议。例如,您不能将建议添加到AbstractController.setSynchronizeOnSession()方法。有关 AOP 代理以及为什么不能在最终方法中添加建议的更多信息,请参考第 11.6.1 节“了解 AOP 代理”

在 Spring Web MVC 中,您可以将任何对象用作命令或表单支持对象。您不需要实现特定于框架的接口或 Base Class。 Spring 的数据绑定非常灵活:例如,它将类型不匹配视为可以由应用程序评估的验证错误,而不是系统错误。因此,您无需在表单对象中将业务对象的属性复制为简单的,无类型的字符串,而只需处理无效的提交或正确转换字符串即可。相反,通常更可取的是直接绑定到您的业务对象。

Spring 的视图分辨率非常灵活。 Controller通常负责使用数据准备模型Map并选择视图名称,但它也可以直接写入响应流并完成请求。视图名称解析可以通过文件 extensions 或 AcceptHeadersContent Type 协商,Bean 名称,属性文件甚至是自定义ViewResolver实现来高度配置。该模型(MVC 中的 M)是Map接口,它允许对视图技术进行完整的抽象。您可以直接与基于模板的渲染技术(例如 JSP,Velocity 和 Freemarker)集成,也可以直接生成 XML,JSON,Atom 和许多其他类型的内容。模型Map可以简单地转换为适当的格式,例如 JSP 请求属性,Velocity 模板模型。

22.1.1 Spring Web MVC 的功能

Spring Web Flow

Spring Web Flow(SWF)旨在成为 ManagementWeb 应用程序页面流的最佳解决方案。

SWF 与 Servlet 和 Portlet 环境中的现有框架(如 Spring MVC 和 JSF)集成。如果您的业务流程(一个或多个流程)将从会话模型(而不是纯粹的请求模型)中受益,那么 SWF 可能是解决方案。

SWF 允许您将逻辑页面流捕获为可在不同情况下重用的自包含模块,因此非常适合构建 Web 应用程序模块,以通过驱动业务流程的受控导航来指导用户。

有关 SWF 的更多信息,请查阅Spring Web Flow 网站

Spring 的 Web 模块包括许多独特的 Web 支持功能:

  • 清晰的角色分离。每个角色(例如控制器,验证器,命令对象,表单对象,模型对象,DispatcherServlet,处理程序 Map,视图解析器等)都可以由专门的对象来实现。

  • *将框架和应用程序类强大而直接地配置为 JavaBeans *。此配置功能包括跨上下文的轻松引用,例如从 Web 控制器到业务对象和验证器。

  • *适应性,非侵入性和灵 Active.*可以针对给定场景使用参数 Comments 之一(例如,@ RequestParam,@ RequestHeader,@ PathVariable 等)定义所需的任何控制器方法签名。

  • 可重用的业务代码,无需重复。使用现有的业务对象作为命令或表单对象,而不是对其进行镜像以扩展特定的框架 Base Class。

  • 可定制的绑定和验证。类型不匹配是应用程序级验证错误,它保留有问题的值,本地化的日期和数字绑定等,而不是通过手动解析和转换为业务对象的纯字符串形式对象。

  • 可定制的处理程序 Map 和视图分辨率。处理程序 Map 和视图解析策略的范围从简单的基于 URL 的配置到复杂的,专门构建的解析策略。 Spring 比要求特定技术的 Web MVC 框架更加灵活。

  • 灵活的模型传输。名称/值Map的模型传输支持与任何视图技术的轻松集成。

  • *可自定义的语言环境,时区和主题分辨率,对带有或不带有 Spring 标记库的 JSP 的支持,对 JSTL 的支持,对 Velocity 的支持而无需额外的 bridge 等等。

  • 一个简单而强大的 JSP 标签库,称为 Spring 标签库,它提供对诸如数据绑定和主题之类的功能的支持。自定义标签在标记代码方面提供了最大的灵 Active。有关标签库 Descriptors 的信息,请参见标题为第 43 章,Spring JSP 标记库的附录。

    • Spring 2.0 中引入了一个 JSP 表单标签库,使在 JSP 页面中编写表单变得更加容易.*有关标签库 Descriptors 的信息,请参阅标题为第 44 章,Spring 形式的 JSP 标签库的附录。
  • *其生命周期仅限于当前 HTTP 请求或 HTTP Session的 bean.*这不是 Spring MVC 本身的特定功能,而是 Spring MVC 使用的WebApplicationContext容器。 第 7.5.4 节“请求,会话,全局会话,应用程序和 WebSocket 范围”中描述了这些 bean 范围

22.1.2 其他 MVC 实现的可插入性

对于某些项目,最好使用非 Spring MVC 实现。许多团队希望利用现有的技能和工具投资,例如使用 JSF。

如果您不想使用 Spring 的 Web MVC,但打算利用 Spring 提供的其他解决方案,则可以轻松地将自己选择的 Web MVC 框架与 Spring 集成在一起。只需通过其ContextLoaderListener启动 Spring 根应用程序上下文,并通过其ServletContext属性(或 Spring 的相应辅助方法)从任何操作对象中访问它。不涉及“插件”,因此不需要专门的集成。从 Web 层的角度来看,您只需将 Spring 用作库,而将根应用程序上下文实例作为入口点。

即使没有 Spring 的 Web MVC,您注册的 Bean 和 Spring 的服务也可以唾手可得。在这种情况下,Spring 不会与其他 Web 框架竞争。它只是解决了纯 Web MVC 框架所没有的许多领域,从 bean 配置到数据访问和事务处理。因此,即使您只想使用 JDBC 或 Hibernate 等事务抽象,您也可以使用 Spring 中间层和/或数据访问层来丰富您的应用程序。

22.2 DispatcherServlet

与许多其他 Web MVC 框架一样,Spring 的 Web MVC 框架是由请求驱动的,围绕中央 Servlet 设计,该 Servlet 将请求分发给控制器并提供其他功能来促进 Web 应用程序的开发。 Spring 的DispatcherServlet不仅能做到这一点。它与 Spring IoC 容器完全集成在一起,因此,您可以使用 Spring 拥有的所有其他功能。

下图说明了 Spring Web MVC DispatcherServlet的请求处理工作流程。精通模式的 Reader 将认识到DispatcherServlet是“ Front Controller”设计模式的表达(这是 Spring Web MVC 与许多其他领先的 Web 框架共享的模式)。

图 22.1 Spring Web MVC(高级)中的请求处理工作流

mvc

DispatcherServlet是实际的Servlet(它继承自HttpServletBase Class),因此是在 Web 应用程序中声明的。您需要使用 URLMap 来 Map 希望DispatcherServlet处理的请求。这是 Servlet 3.0 环境中的标准 Java EE Servlet 配置:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        ServletRegistration.Dynamic registration = container.addServlet("example", new DispatcherServlet());
        registration.setLoadOnStartup(1);
        registration.addMapping("/example/*");
    }
}

在前面的示例中,所有以/example开头的请求都将由名为exampleDispatcherServlet实例处理。

WebApplicationInitializer是 Spring MVC 提供的接口,可确保检测到基于代码的配置并将其自动用于初始化任何 Servlet 3 容器。此接口名为AbstractAnnotationConfigDispatcherServletInitializer的抽象 Base Class 实现,只需指定 ServletMap 并列出配置类,就可以更轻松地注册DispatcherServlet,甚至是建议的方式来设置 Spring MVC 应用程序。有关更多详细信息,请参见基于代码的 Servlet 容器初始化

DispatcherServlet是实际的Servlet(它继承自HttpServletBase Class),因此是在 Web 应用程序的web.xml中声明的。您需要通过在同一web.xml文件中使用 URLMap 来 Map 希望DispatcherServlet处理的请求。这是标准的 Java EE Servlet 配置。以下示例显示了这样的DispatcherServlet声明和 Map:

以下是上述基于代码的示例的web.xml等效项:

<web-app>
    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

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

</web-app>

第 7.15 节“ ApplicationContext 的附加功能”中详细说明的那样,可以确定 Spring 中的ApplicationContext个实例。在 Web MVC 框架中,每个DispatcherServlet都有自己的WebApplicationContext,它继承了根WebApplicationContext中已经定义的所有 bean。根WebApplicationContext应该包含所有在其他上下文和 Servlet 实例之间共享的基础结构 Bean。这些继承的 bean 可以在 servlet 特定的作用域中被覆盖,并且您可以在给定 Servlet 实例本地定义新的特定于作用域的 bean。

图 22.2 Spring Web MVC 中的典型上下文层次结构

mvc 上下文层次结构

初始化DispatcherServlet时,Spring MVC 在 Web 应用程序的WEB-INF目录中查找名为* [servlet-name] -servlet.xml *的文件,并在该目录中创建定义的 bean,从而覆盖任何用相同名称定义的 bean 的定义。在 Global 范围内。

考虑以下DispatcherServlet Servlet 配置(在web.xml文件中):

<web-app>
    <servlet>
        <servlet-name>golfing</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>golfing</servlet-name>
        <url-pattern>/golfing/*</url-pattern>
    </servlet-mapping>
</web-app>

完成上述 Servlet 配置后,您将需要在应用程序中拥有一个名为/WEB-INF/golfing-servlet.xml的文件。该文件将包含所有特定于 Spring Web MVC 的组件(bean)。您可以通过 Servlet 初始化参数更改此配置文件的确切位置(有关详细信息,请参见下文)。

对于单个 DispatcherServlet 场景,也可能只有一个根上下文。

图 22.3 Spring Web MVC 中的单根上下文

mvc 根上下文

可以通过设置一个空的 contextConfigLocation servlet init 参数来配置它,如下所示:

<web-app>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>dispatcher</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>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

WebApplicationContext是普通ApplicationContext的扩展,具有 Web 应用程序必需的一些额外功能。它与普通ApplicationContext的不同之处在于它能够解析主题(请参见第 22.9 节“使用主题”),并且知道与之关联的 Servlet(通过链接到ServletContext)。 WebApplicationContext绑定在ServletContext中,并且通过使用RequestContextUtils类上的静态方法,如果需要访问WebApplicationContext,则始终可以查找它。

注意,我们可以使用基于 Java 的配置实现相同的目的:

public class GolfingWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // GolfingAppConfig defines beans that would be in root-context.xml
        return new Class<?>[] { GolfingAppConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // GolfingWebConfig defines beans that would be in golfing-servlet.xml
        return new Class<?>[] { GolfingWebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/golfing/*" };
    }
}

22.2.1 WebApplicationContext 中的特殊 Bean 类型

Spring DispatcherServlet使用特殊的 bean 处理请求并呈现适当的视图。这些 bean 是 Spring MVC 的一部分。您可以通过在WebApplicationContext中简单配置一个或多个特殊 bean 来选择要使用的特殊 bean。但是,您不需要一开始就这样做,因为 Spring MVC 会维护一个默认 Bean 列表,以供您在未配置任何默认 Bean 时使用。下一节将对此进行更多介绍。首先,请参见下表,列出DispatcherServlet所依赖的特殊 bean 类型。

表 22.1 WebApplicationContext 中的特殊 bean 类型

Bean typeExplanation
HandlerMapping根据某些条件将传入的请求 Map 到处理程序以及一系列预处理程序和后处理程序(处理程序拦截器),这些标准的详细信息因HandlerMapping实现而异。最受欢迎的实现支持带 Comments 的控制器,但也存在其他实现。
HandlerAdapter帮助DispatcherServlet调用 Map 到请求的处理程序,而不考虑实际调用的处理程序是什么。例如,调用带 Comments 的控制器需要解析各种 Comments。因此,HandlerAdapter的主要目的是保护DispatcherServlet免受此类细节的影响。
HandlerExceptionResolver将异常 Map 到视图也可以使用更复杂的异常处理代码。
ViewResolver将基于字符串的逻辑视图名称解析为实际的View类型。
LocaleResolver & LocaleContextResolver解决 Client 正在使用的语言环境以及可能的时区,以便能够提供国际化的视图
ThemeResolver解决您的 Web 应用程序可以使用的主题,例如,提供个性化的布局
MultipartResolver解析 Multipart 请求,例如以支持处理从 HTML 表单上传的文件。
FlashMapManager存储和检索“Importing”和“输出” FlashMap,它们通常可用于通过重定向将属性从一个请求传递到另一个请求。

22.2.2 默认的 DispatcherServlet 配置

如上一节中针对每个特殊 bean 所述,DispatcherServlet维护默认情况下要使用的实现列表。此信息保存在包org.springframework.web.servlet的文件DispatcherServlet.properties中。

所有特殊 bean 都有一些合理的默认值。尽管您迟早需要自定义这些 bean 提供的一个或多个属性,但迟早要这么做。例如,将InternalResourceViewResolverprefix属性配置为视图文件的父位置是很常见的。

不管细节如何,在这里要理解的重要概念都是:一旦在WebApplicationContext中配置了特殊的 bean(例如InternalResourceViewResolver),就可以有效地覆盖默认实现的列表,否则该默认实现将用于该特殊的 bean 类型。例如,如果您配置InternalResourceViewResolver,则将忽略ViewResolver实现的默认列表。

第 22.16 节“配置 Spring MVC”中,您将学习配置 Spring MVC 的其他选项,包括 MVC Java 配置和 MVC XML 名称空间,这两个选项都提供了一个简单的起点,并且几乎不了解 Spring MVC 的工作原理。无论您选择如何配置应用程序,本节中介绍的概念都是基础知识,对您有帮助。

22.2.3 DispatcherServlet 处理序列

设置DispatcherServlet并收到针对特定DispatcherServlet的请求后,DispatcherServlet开始按以下方式处理请求:

  • 搜索WebApplicationContext并将其绑定在请求中,作为控制器和流程中其他元素可以使用的属性。默认情况下,它绑定在键DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE下。

  • 语言环境解析器绑定到请求,以使流程中的元素能够解析在处理请求(渲染视图,准备数据等)时要使用的语言环境。如果不需要语言环境解析,则不需要它。

  • 主题解析器绑定到请求,以使诸如视图之类的元素确定要使用的主题。如果不使用主题,则可以忽略它。

  • 如果指定 Multipart 文件解析器,则将检查请求中是否有 Multipart;否则,将检查请求。如果找到 Multipart,则将请求包装在MultipartHttpServletRequest中,以供流程中的其他元素进一步处理。有关 Multipart 处理的更多信息,请参见第 22.10 节“ Spring 的 Multipart(文件上传)支持”

  • 搜索适当的处理程序。如果找到处理程序,则执行与处理程序(预处理器,后处理器和控制器)关联的执行链,以准备模型或渲染。

  • 如果返回模型,则呈现视图。如果未返回任何模型(可能是由于安全原因,预处理程序或后处理器可能会拦截该请求),则不会呈现任何视图,因为该请求可能已经被满足。

WebApplicationContext中声明的处理程序异常解析器将拾取在处理请求期间引发的异常。使用这些异常解析器可以使您定义自定义行为以解决异常。

Spring DispatcherServlet还支持 Servlet API 指定的* last-modification-date 的返回。确定特定请求的最后修改日期的过程非常简单:DispatcherServlet查找适当的处理程序 Map,并测试找到的处理程序是否实现了 LastModified *接口。如果是这样,则LastModified接口的long getLastModified(request)方法的值返回给 Client 端。

您可以通过向web.xml文件中的 Servlet 声明中添加 Servlet 初始化参数(init-param元素)来自定义DispatcherServlet实例。有关支持的参数列表,请参见下表。

表 22.2 DispatcherServlet 初始化参数

ParameterExplanation
contextClass实现ConfigurableWebApplicationContext的类,将由此 Servlet 实例化并在本地配置。默认情况下,使用XmlWebApplicationContext
contextConfigLocation传递给上下文实例的字符串(由contextClass指定),指示可以在哪里找到上下文。该字符串可能包含多个字符串(使用逗号作为分隔符)以支持多个上下文。如果多个上下文位置的 bean 定义了两次,则以最新位置为准。
namespaceWebApplicationContext的命名空间。默认为[servlet-name]-servlet

22.3 实施控制器

控制器提供对您通常通过服务接口定义的应用程序行为的访问。控制器解释用户 Importing 并将其转换为由视图表示给用户的模型。 Spring 以非常抽象的方式实现了一个控制器,使您能够创建各种各样的控制器。

Spring 2.5 为 MVC 控制器引入了一个基于 Comments 的编程模型,该模型使用诸如@RequestMapping@RequestParam@ModelAttribute等 Comments。 Servlet MVC 和 Portlet MVC 均可使用此 Comments 支持。以这种方式实现的控制器不必扩展特定的 Base Class 或实现特定的接口。此外,它们通常不直接依赖 Servlet 或 Portlet API,尽管您可以轻松配置对 Servlet 或 Portlet 设施的访问。

Tip

Github 上的 Spring 项目 Org中可用,许多 Web 应用程序利用本节中描述的 Comments 支持,包括* MvcShowcase MvcAjax MvcBasic PetClinic PetCare *以及其他。

@Controller
public class HelloWorldController {

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World!");
        return "helloWorld";
    }
}

如您所见,@Controller@RequestMapping注解允许灵活的方法名称和签名。在此特定示例中,该方法接受Model并将视图名称返回为String,但是可以使用各种其他方法参数和返回值,如本节稍后说明。 @Controller@RequestMapping以及许多其他 Comments 构成了 Spring MVC 实现的基础。本节记录了这些 Comments 以及它们在 Servlet 环境中的最常用用法。

22.3.1 使用@Controller 定义控制器

@ControllerComments 指示特定的类充当* controller *的角色。 Spring 不需要您扩展任何控制器 Base Class 或引用 Servlet API。但是,如果需要,您仍然可以引用特定于 Servlet 的功能。

@ControllerComments 充当带 Comments 的类的构造型,指示其作用。调度程序扫描此类带 Comments 的类以查找 Map 的方法,并检测@RequestMappingComments(请参阅下一节)。

您可以在调度程序的上下文中使用标准的 Spring bean 定义来显式定义带 Comments 的 controller bean。但是,@Controller构造型还允许自动检测,与 Spring 常规支持一致,以支持在 Classpath 中检测组件类并为其自动注册 Bean 定义。

要启用对这些带 Comments 的控制器的自动检测,可以将组件扫描添加到配置中。使用下面的 XML 代码段所示的* spring-context *模式:

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.springframework.samples.petclinic.web"/>

    <!-- ... -->

</beans>

22.3.2 使用@RequestMappingMap 请求

您可以使用@RequestMappingComments 将 URL(例如/appointments)Map 到整个类或特定的处理程序方法上。通常,类级别的 Comments 将特定的请求路径(或路径模式)Map 到表单控制器上,其他方法级别的 Comments 使特定的 HTTP 方法请求方法(“ GET”,“ POST”等)的主 Map 范围变窄。或 HTTP 请求参数条件。

  • Petcare *示例的以下示例显示了使用该 Comments 的 Spring MVC 应用程序中的控制器:
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

    private final AppointmentBook appointmentBook;

    @Autowired
    public AppointmentsController(AppointmentBook appointmentBook) {
        this.appointmentBook = appointmentBook;
    }

    @RequestMapping(method = RequestMethod.GET)
    public Map<String, Appointment> get() {
        return appointmentBook.getAppointmentsForToday();
    }

    @RequestMapping(path = "/{day}", method = RequestMethod.GET)
    public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
        return appointmentBook.getAppointmentsForDay(day);
    }

    @RequestMapping(path = "/new", method = RequestMethod.GET)
    public AppointmentForm getNewForm() {
        return new AppointmentForm();
    }

    @RequestMapping(method = RequestMethod.POST)
    public String add(@Valid AppointmentForm appointment, BindingResult result) {
        if (result.hasErrors()) {
            return "appointments/new";
        }
        appointmentBook.addAppointment(appointment);
        return "redirect:/appointments";
    }
}

在上面的示例中,在许多地方都使用了@RequestMapping。第一次使用是在类型(类)级别上,这表明该控制器中的所有处理程序方法都相对于/appointments路径。 get()方法还有一个@RequestMapping改进:它仅接受GET请求,这意味着用于/appointments的 HTTP GET会调用此方法。 add()具有类似的改进,而getNewForm()将 HTTP 方法和路径的定义合并为一个,因此appointments/newGET请求由该方法处理。

getForDay()方法显示@RequestMapping的另一种用法:URI 模板。 (请参见称为“ URI 模板模式”的部分)。

不需要类级别的@RequestMapping。没有它,所有路径都是绝对的,而不是相对的。 * PetClinic *示例应用程序的以下示例显示了使用@RequestMapping的多动作控制器:

@Controller
public class ClinicController {

    private final Clinic clinic;

    @Autowired
    public ClinicController(Clinic clinic) {
        this.clinic = clinic;
    }

    @RequestMapping("/")
    public void welcomeHandler() {
    }

    @RequestMapping("/vets")
    public ModelMap vetsHandler() {
        return new ModelMap(this.clinic.getVets());
    }

}

上面的示例未指定GETPUTPOST等等,因为@RequestMapping默认情况下 Map 了所有 HTTP 方法。使用@RequestMapping(method=GET)@GetMapping缩小 Map 范围。

由@RequestMapping 变体组成

Spring Framework 4.3 引入了@RequestMappingComments 的以下方法级别的* composed *变体,有助于简化常见 HTTP 方法的 Map 并更好地表达带 Comments 的处理程序方法的语义。例如,@GetMapping可以读取为GET @RequestMapping

  • @GetMapping

  • @PostMapping

  • @PutMapping

  • @DeleteMapping

  • @PatchMapping

以下示例显示了上一节中AppointmentsController的修改后的版本,该版本已通过* composed * @RequestMappingComments 进行了简化。

@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

    private final AppointmentBook appointmentBook;

    @Autowired
    public AppointmentsController(AppointmentBook appointmentBook) {
        this.appointmentBook = appointmentBook;
    }

    @GetMapping
    public Map<String, Appointment> get() {
        return appointmentBook.getAppointmentsForToday();
    }

    @GetMapping("/{day}")
    public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
        return appointmentBook.getAppointmentsForDay(day);
    }

    @GetMapping("/new")
    public AppointmentForm getNewForm() {
        return new AppointmentForm();
    }

    @PostMapping
    public String add(@Valid AppointmentForm appointment, BindingResult result) {
        if (result.hasErrors()) {
            return "appointments/new";
        }
        appointmentBook.addAppointment(appointment);
        return "redirect:/appointments";
    }
}

@Controller 和 AOP 代理

在某些情况下,可能需要在运行时为控制器装饰 AOP 代理。一个示例是,如果您选择直接在控制器上具有@Transactional注解。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。这通常是控制器的默认选择。但是,如果控制器必须实现不是 Spring Context 回调的接口(例如InitializingBean*Aware等),则可能需要显式配置基于类的代理。例如对于<tx:annotation-driven/>,更改为<tx:annotation-driven proxy-target-class="true"/>

Spring MVC 3.1 中@RequestMapping 方法的新支持类

Spring 3.1 为分别称为RequestMappingHandlerMappingRequestMappingHandlerAdapter@RequestMapping方法引入了一组新的支持类。建议使用它们,甚至要求它们使用 Spring MVC 3.1 以及以后的功能。默认情况下,新的支持类由 MVC 命名空间和 MVC Java 配置启用,但如果不使用,则必须显式配置。本节描述了新旧支持类之间的一些重要区别。

在 Spring 3.1 之前,类型和方法级别的请求 Map 在两个单独的阶段中进行了检查:首先由DefaultAnnotationHandlerMapping选择一个控制器,然后由AnnotationMethodHandlerAdapter缩小要调用的实际方法的范围。

对于 Spring 3.1 中的新支持类,RequestMappingHandlerMapping是唯一决定应使用哪种方法处理请求的地方。将控制器方法视为唯一端点的集合,这些端点具有从类型和方法级别@RequestMapping信息派生的每个方法的 Map。

这带来了一些新的可能性。现在,HandlerInterceptorHandlerExceptionResolver现在可以期望基于对象的处理程序为HandlerMethod,这使他们可以检查确切的方法,其参数和关联的 Comments。 URL 的处理不再需要在不同的控制器之间进行拆分。

还有几件事不再可能:

  • 首先选择带有SimpleUrlHandlerMappingBeanNameUrlHandlerMapping的控制器,然后根据@RequestMappingComments 缩小方法范围。

  • 依靠方法名称作为后备机制来消除两个@RequestMapping方法之间的歧义,这些方法没有显式的路径 MapURL 路径,但在其他方面均相等,例如通过 HTTP 方法。在新的支持类中,必须唯一 Map@RequestMapping方法。

  • 如果没有其他控制器方法更具体地匹配,则使用一个默认方法(无显式路径 Map)处理请求。在新的支持类中,如果找不到匹配的方法,则会引发 404 错误。

现有支持类仍支持上述功能。但是,要利用 Spring MVC 3.1 的新功能,您需要使用新的支持类。

URI 模板模式

  • URI 模板*可用于以@RequestMapping方法方便地访问 URL 的选定部分。

URI 模板是类似于 URI 的字符串,包含一个或多个变量名。当您用值代替这些变量时,模板将成为 URI。 URI 模板的proposed RFC定义如何参数化 URI。例如,URI 模板http://www.example.com/users/{userId}包含变量* userId 。将值 fred *分配给变量会产生http://www.example.com/users/fred

在 Spring MVC 中,您可以在方法参数上使用@PathVariable注解将其绑定到 URI 模板变量的值:

@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable String ownerId, Model model) {
    Owner owner = ownerService.findOwner(ownerId);
    model.addAttribute("owner", owner);
    return "displayOwner";
}

URI 模板“ /owners/{ownerId}" specifies the variable name ownerId”。当控制器处理此请求时,ownerId的值将设置为 URI 相应部分中的值。例如,当请求/owners/fred时,ownerId的值为fred

Tip

为了处理@PathVariable 注解,Spring MVC 需要按名称查找匹配的 URI 模板变量。您可以在注解中指定它:

@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable("ownerId") String theOwner, Model model) {
// implementation omitted
}

或者,如果 URI 模板变量名称与方法参数名称匹配,则可以省略该详细信息。只要使用调试信息或 Java 8 上的-parameters编译器标志编译代码,Spring MVC 就会将方法参数名称与 URI 模板变量名称匹配:

@GetMapping("/owners/{ownerId}")
public String findOwner(@PathVariable String ownerId, Model model) {
// implementation omitted
}

一个方法可以具有任意数量的@PathVariable注解:

@GetMapping("/owners/{ownerId}/pets/{petId}")
public String findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    Owner owner = ownerService.findOwner(ownerId);
    Pet pet = owner.getPet(petId);
    model.addAttribute("pet", pet);
    return "displayPet";
}

Map<String, String>参数上使用@PathVariableComments 时,将使用所有 URI 模板变量填充 Map。

URI 模板可以从类型和方法级别的* @ RequestMapping *Comments 中组装。结果,可以使用诸如/owners/42/pets/21之类的 URL 调用findPet()方法。

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

    @RequestMapping("/pets/{petId}")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // implementation omitted
    }

}

@PathVariable参数可以是任何简单类型,例如intlongDate等。Spring 会自动将其转换为适当的类型,否则会抛出TypeMismatchException。您还可以注册对解析其他数据类型的支持。参见称为“方法参数和类型转换”的部分名为“自定义 WebDataBinder 初始化”的部分

具有正则表达式的 URI 模板模式

有时,在定义 URI 模板变量时需要更高的精度。考虑 URL "/spring-web/spring-web-3.0.5.jar"。您如何将其分解为多个部分?

@RequestMappingComments 支持在 URI 模板变量中使用正则表达式。语法为{varName:regex},其中第一部分定义变量名称,第二部分定义正则表达式。例如:

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {
    // ...
}

Path Patterns

除了 URI 模板之外,@RequestMappingComments 和所有* composed * @RequestMapping变体还支持 Ant 样式的路径模式(例如/myPath/*.do)。还支持 URI 模板变量和 Ant 样式的 glob 的组合(例如/owners/*/pets/{petId})。

路径模式比较

当 URL 匹配多个模式时,将使用排序来查找最特定的匹配项。

URI 变量和通配符数量较少的模式被认为更具体。例如/hotels/{hotel}/*具有 1 个 URI 变量和 1 个通配符,并且被认为比/hotels/{hotel}/**更具体,而/hotels/{hotel}/**作为 1 个 URI 变量和 2 个通配符。

如果两个模式的计数相同,则更长的模式被认为更具体。例如/foo/bar*/foo/*更长,被认为更具体。

当两个模式的计数和长度相同时,通配符较少的模式被认为更具体。例如/hotels/{hotel}/hotels/*更具体。

还有一些其他特殊规则:

  • 默认 Map 模式 /**比其他任何模式都不具体。例如/api/{a}/{b}/{c}更具体。

  • 诸如/public/**之类的 前缀模式 比不包含双通配符的其他任何模式都没有那么具体。例如/public/path3/{a}/{b}/{c}更具体。

有关详细信息,请参见AntPathMatcher中的AntPatternComparator。请注意,可以自定义 PathMatcher(请参阅配置 Spring MVC 的部分中的第 22.16.11 节,“路径匹配”)。

占位符的路径模式

@RequestMapping注解中的模式针对本地属性和/或系统属性以及环境变量支持${…}占位符。在可能需要通过配置自定义控制器 Map 到的路径的情况下,这可能很有用。有关占位符的更多信息,请参见PropertyPlaceholderConfigurer类的 javadocs。

后缀模式匹配

默认情况下,Spring MVC 执行".*"后缀模式匹配,因此 Map 到/person的控制器也隐式 Map 到/person.*。这使得通过 URL 路径(例如/person.pdf/person.xml)请求资源的不同表示形式变得容易。

可以关闭后缀模式匹配,也可以将其限制为明确注册用于内容协商目的的一组路径扩展。通常建议这样做,以最大程度地减少常见请求 Map(例如/person/{id})的歧义,在这种情况下,点可能不表示文件 extensions,例如/person/[email protected]/person/[email protected]。此外,如以下 Comments 中所述,在某些情况下可以使用后缀模式匹配和内容协商来尝试恶意攻击,并且有充分的理由对其进行有意义的限制。

有关后缀模式匹配的配置,请参见第 22.16.11 节,“路径匹配”;对于内容协商的配置,请参见第 22.16.6 节“内容协商”

后缀模式匹配和 RFD

反射文件下载(RFD)攻击最早是在 2014 年的Trustwave 发表的论文中描述的。该攻击与 XSS 类似,因为它依赖于响应中反映的 Importing(例如查询参数,URI 变量)。但是,RFD 攻击不是将 JavaScript 插入 HTML,而是依靠浏览器切换来执行下载,并且如果双击基于文件 extensions(例如.bat,.cmd)的响应,则将其视为可执行脚本。

在 Spring MVC 中,@ResponseBodyResponseEntity方法存在风险,因为它们可以呈现 Client 端可以请求的不同 Content Type,包括通过 URL 路径扩展。但是请注意,仅禁用后缀模式匹配或仅出于内容协商目的而禁用路径扩展都不能有效地防止 RFD 攻击。

为了全面防御 RFD,Spring MVC 在呈现响应主体之前添加了Content-Disposition:inline;filename=f.txtHeaders,以建议固定且安全的下载文件名。仅当 URL 路径包含既未列入白名单也未明确注册用于内容协商目的的文件 extensions 时,才执行此操作。但是,当直接在浏览器中键入 URL 时,可能会产生副作用。

默认情况下,许多常见的路径 extensions 都被列入白名单。此外,REST API 调用通常不打算直接在浏览器中用作 URL。但是,使用自定义HttpMessageConverter实现的应用程序可以显式注册文件 extensions 以进行内容协商,并且不会为此类 extensions 添加 Content-DispositionHeaders。参见第 22.16.6 节“内容协商”

Note

最初是在CVE-2015-5211的工作中引入的。以下是该报告的其他建议:

  • 编码而不是转义 JSON 响应。这也是 OWASP XSS 的建议。有关如何使用 Spring 进行操作的示例,请参见spring-jackson-owasp

  • 将后缀模式匹配配置为关闭或仅限于显式注册的后缀。

  • 使用“ useJaf”和“ ignoreUnknownPathExtensions”属性设置为 false 来配置内容协商,这将导致 extensions 未知的 URL 产生 406 响应。但是请注意,如果自然希望 URL 的末尾有一个点,那么这可能不是一个选择。

  • 在响应中添加X-Content-Type-Options: nosniffHeaders。 Spring Security 4 默认情况下会这样做。

Matrix Variables

URI 规范RFC 3986定义了在路径段中包括名称/值对的可能性。规范中没有使用特定术语。通用的“ URI 路径参数”也可以应用,尽管更独特的"Matrix URIs"源自 Tim Berners-Lee 的旧post,也经常使用并且众所周知。在 Spring MVC 中,这些被称为矩阵变量。

矩阵变量可以出现在任何路径段中,每个矩阵变量用“;”分隔(分号)。例如:"/cars;color=red;year=2012"。多个值可以是“,”(逗号)分隔"color=red,green,blue",也可以是变量名重复"color=red;color=green;color=blue"

如果期望 URL 包含矩阵变量,则请求 Map 模式必须使用 URI 模板表示它们。这确保了无论矩阵变量是否存在以及以什么 Sequences 提供,都可以正确匹配请求。

下面是提取矩阵变量“ q”的示例:

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

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

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

}

由于所有路径段都可能包含矩阵变量,因此在某些情况下,您需要更具体地确定变量的预期位置:

// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

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

}

矩阵变量可以定义为可选,并指定默认值:

// GET /pets/42

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

    // q == 1

}

所有矩阵变量都可以在 Map 中获得:

// 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" : 11, "s" : 23]

}

请注意,要启用矩阵变量的使用,必须将RequestMappingHandlerMappingremoveSemicolonContent属性设置为false。默认情况下,它设置为true

Tip

MVC Java 配置和 MVC 命名空间都提供了启用矩阵变量使用的选项。

如果您使用的是 Java config,则使用 MVC Java Config 进行高级自定义部分介绍了如何自定义RequestMappingHandlerMapping

在 MVC 命名空间中,<mvc:annotation-driven>元素具有enable-matrix-variables属性,应将其设置为true。默认情况下,它设置为false

<?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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">

<mvc:annotation-driven enable-matrix-variables="true"/>

</beans>

消耗媒体类型

您可以通过指定消耗性媒体类型的列表来缩小主 Map 的范围。仅当Content-Type请求 Headers 与指定的媒体类型匹配时,该请求才会被匹配。例如:

@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // implementation omitted
}

消耗性媒体类型表达式也可以像!text/plain一样取反,以匹配除text/plainContent-Type之外的所有请求。还应考虑使用MediaType中提供的常量,例如APPLICATION_JSON_VALUEAPPLICATION_JSON_UTF8_VALUE

Tip

  • consumes *条件在类型和方法级别上受支持。与大多数其他条件不同,在类型级别使用时,方法级别的消耗类型将覆盖而不是扩展类型级别的消耗类型。

可生产的媒体类型

您可以通过指定可生产媒体类型的列表来缩小主 Map 的范围。仅当Accept请求 Headers 与这些值之一匹配时,该请求才会被匹配。此外,使用* produces 条件可确保用于生成响应的实际 Content Type 遵守 produces *条件中指定的媒体类型。例如:

@GetMapping(path = "/pets/{petId}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {
    // implementation omitted
}

Note

请注意,在* produces *条件中指定的媒体类型还可以选择指定字符集。例如,在上面的代码段中,我们指定的媒体类型与在MappingJackson2HttpMessageConverter中配置的默认媒体类型相同,包括UTF-8字符集。

就像使用* consumes *一样,可以否定可生产的媒体类型表达式,如!text/plain,以匹配除Accept头值text/plain之外的所有请求。还应考虑使用MediaType中提供的常量,例如APPLICATION_JSON_VALUEAPPLICATION_JSON_UTF8_VALUE

Tip

  • produces *条件在类型和方法级别上受支持。与大多数其他条件不同,在类型级别使用时,方法级别的可生产类型将覆盖而不是扩展类型级别的可生产类型。

请求参数和 Headers 值

您可以通过"myParam""!myParam""myParam=myValue"之类的请求参数条件来缩小请求匹配范围。前两个测试用于请求参数是否存在,而第三测试用于特定参数值。这是带有请求参数值条件的示例:

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

    @GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // implementation omitted
    }

}

可以执行相同的操作来测试是否存在请求 Headers 或根据特定的请求 Headers 值进行匹配:

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

    @GetMapping(path = "/pets", headers = "myHeader=myValue")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
        // implementation omitted
    }

}

Tip

尽管您可以使用媒体类型通配符来匹配* Content-Type Accept Headers 值(例如“ content-type = text/将匹配 text/plain“ ” text/html“ ),建议分别使用 consumes produces *条件。它们专门用于此目的。

HTTP HEAD 和 HTTP OPTIONS

Map 到“ GET”的@RequestMapping方法也隐式 Map 到“ HEAD”,即无需显式声明“ HEAD”。像处理 HTTP GET 一样处理 HTTP HEAD 请求,除了只写字节数并设置“ Content-Length”Headers,而不是写正文。

@RequestMapping方法对 HTTP OPTIONS 具有内置支持。默认情况下,通过将“允许”响应 Headers 设置为在所有具有匹配 URL 模式的@RequestMapping方法上显式声明的 HTTP 方法,来处理 HTTP OPTIONS 请求。如果未明确声明任何 HTTP 方法,则将“允许”Headers 设置为“ GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS”。理想情况下,始终声明@RequestMapping方法旨在处理的 HTTP 方法,或者使用专用的“组成” @RequestMapping变体之一(请参见称为“ Composition @RequestMapping Variants”的部分)。

尽管不是必需的,但可以将@RequestMapping方法 Map 到 HTTP HEAD 或 HTTP OPTIONS 或两者。

22.3.3 定义@RequestMapping 处理程序方法

@RequestMapping处理程序方法可以具有非常灵活的签名。下一节介绍了受支持的方法参数和返回值。可以按任意 Sequences 使用大多数参数,唯一的 exception 是BindingResult参数。下一节将对此进行描述。

Note

Spring 3.1 为分别称为RequestMappingHandlerMappingRequestMappingHandlerAdapter@RequestMapping方法引入了一组新的支持类。建议使用它们,甚至要求它们使用 Spring MVC 3.1 以及以后的功能。默认情况下,新的支持类从 MVC 命名空间启用,并使用 MVC Java 配置启用,但如果不使用,则必须显式配置。

支持的方法参数类型

以下是受支持的方法参数:

  • 请求或响应对象(Servlet API)。选择任何特定的请求或响应类型,例如ServletRequestHttpServletRequest

  • 会话对象(Servlet API):类型为HttpSession。此类型的参数强制存在相应的会话。结果,这样的参数永远不会是null

Note

会话访问可能不是线程安全的,尤其是在 Servlet 环境中。如果允许多个请求同时访问会话,请考虑将RequestMappingHandlerAdapter的“ synchronizeOnSession”标志设置为“ true”。

  • org.springframework.web.context.request.WebRequestorg.springframework.web.context.request.NativeWebRequest。允许通用请求参数访问以及请求/会话属性访问,而无需与本机 Servlet/Portlet API 绑定。

  • 由当前可用的最特定的语言环境解析器确定的当前请求语言环境的java.util.Locale实际上是 MVC 环境中已配置的LocaleResolver/LocaleContextResolver

  • java.util.TimeZone(Java 6)/ java.time.ZoneId(Java 8 上)表示与当前请求关联的时区,由LocaleContextResolver确定。

  • java.io.InputStream/java.io.Reader用于访问请求的内容。此值是 Servlet API 公开的原始 InputStream/Reader。

  • java.io.OutputStream/java.io.Writer用于生成响应的内容。此值是 Servlet API 公开的原始 OutputStream/Writer。

  • org.springframework.http.HttpMethod用于 HTTP 请求方法。

  • java.security.Principal包含当前已认证的用户。

  • @PathVariable个带 Comments 的参数,用于访问 URI 模板变量。参见称为“ URI 模板模式”的部分

  • @MatrixVariable带 Comments 的参数,用于访问 URI 路径段中的名称/值对。参见称为“矩阵变量”的部分

  • @RequestParam带 Comments 的参数,用于访问特定的 Servlet 请求参数。参数值将转换为声明的方法参数类型。参见名为“使用@RequestParam 将请求参数绑定到方法参数”的部分

  • @RequestHeader带 Comments 的参数,用于访问特定的 Servlet 请求 HTTPHeaders。参数值将转换为声明的方法参数类型。参见名为“使用@RequestHeaderCommentsMap 请求 Headers 属性”的部分

  • @RequestBody带 Comments 的参数,用于访问 HTTP 请求正文。使用HttpMessageConverter s 将参数值转换为声明的方法参数类型。参见名为“使用@RequestBodyCommentsMap 请求正文”的部分

  • @RequestPart带 Comments 的参数,用于访问“Multipart/表单数据”请求部分的内容。参见第 22.10.5 节“处理来自编程 Client 端的文件上传请求”第 22.10 节“ Spring 的 Multipart(文件上传)支持”

  • @SessionAttribute带 Comments 的参数,用于访问现有的永久会话属性(例如,用户身份验证对象),而不是通过@SessionAttributes作为控制器工作流一部分临时存储在会话中的模型属性。

  • @RequestAttribute带 Comments 的参数,用于访问请求属性。

  • HttpEntity<?>参数,用于访问 Servlet 请求的 HTTPHeaders 和内容。请求流将使用HttpMessageConverter转换为实体。参见名为“使用 HttpEntity”的部分

  • java.util.Map/org.springframework.ui.Model/org.springframework.ui.ModelMap用于丰富公开给 Web 视图的隐式模型。

  • org.springframework.web.servlet.mvc.support.RedirectAttributes指定要在重定向的情况下使用的确切属性集,并添加 Flash 属性(临时存储在服务器端的属性,以使其在重定向后可用于请求)。参见称为“将数据传递到重定向目标”的部分第 22.6 节“使用闪存属性”

  • 通过@InitBinder方法和/或 HandlerAdapter 配置,可自定义类型转换的命令或表单对象将请求参数绑定到 bean 属性(通过设置器)或直接绑定到字段。请参见RequestMappingHandlerAdapterwebBindingInitializer属性。默认情况下,此类命令对象及其验证结果将使用命令类名称(例如,类型为“ some.package.OrderAddress”的命令对象的模型属性“ orderAddress”。 ModelAttribute注解可用于方法参数,以自定义所使用的模型属性名称。

  • 上一个命令或表单对象(紧接在前的方法参数)的org.springframework.validation.Errors/org.springframework.validation.BindingResult验证结果。

  • org.springframework.web.bind.support.SessionStatus状态句柄,用于将表单处理标记为已完成,这将触发由处理程序类型级别的@SessionAttributes注解指示的会话属性的清除。

  • org.springframework.web.util.UriComponentsBuilder一个构建器,用于准备相对于当前请求的主机,端口,方案,上下文路径和 servletMap 的 Literals 部分的 URL。

ErrorsBindingResult参数必须跟随立即绑定的模型对象,因为方法签名可能具有多个模型对象,并且 Spring 将为它们各自创建一个单独的BindingResult实例,因此以下示例将不起作用:

BindingResult 和@ModelAttribute 的无效 Sequences.

@PostMapping
public String processSubmit(@ModelAttribute("pet") Pet pet, Model model, BindingResult result) { ... }

请注意,在PetBindingResult之间有一个Model参数。要使此工作正常进行,您必须按如下所示重新排列参数:

@PostMapping
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result, Model model) { ... }

Note

支持 JDK 1.8 的java.util.Optional作为方法参数类型,并带有具有required属性的 Comments(例如@RequestParam@RequestHeader等)。在这些情况下,使用java.util.Optional等同于使用required=false

支持的方法返回类型

以下是受支持的返回类型:

  • 一个ModelAndView对象,该模型隐含了命令对象和@ModelAttributeComments 的参考数据访问器方法的结果。

  • Model对象,其视图名称是通过RequestToViewNameTranslator隐式确定的,模型隐含了命令对象和@ModelAttributeComments 的参考数据访问器方法的结果。

  • 一个用于公开模型的Map对象,其视图名称是通过RequestToViewNameTranslator隐式确定的,并且该模型隐含了命令对象和@ModelAttributeComments 的参考数据访问器方法的结果。

  • View对象,其模型是通过命令对象和带有@ModelAttributeComments 的参考数据访问器方法隐式确定的。处理程序方法还可以pass 语句Model参数来以编程方式丰富模型(请参见上文)。

  • String值解释为逻辑视图名称,并通过命令对象和带有@ModelAttributeComments 的参考数据访问器方法隐式确定模型。处理程序方法还可以pass 语句Model参数来以编程方式丰富模型(请参见上文)。

  • void,如果方法本身处理响应(通过直接编写响应内容,为此声明一个类型为ServletResponse/HttpServletResponse的参数),或者应该通过RequestToViewNameTranslator隐式确定视图名称(而不是在RequestToViewNameTranslator中声明响应参数)处理程序方法签名)。

  • 如果该方法以@ResponseBodyComments,则将返回类型写入响应 HTTP 正文。返回值将使用HttpMessageConverter转换为声明的方法参数类型。参见名为“使用@ResponseBodyCommentsMap 响应正文”的部分

  • HttpEntity<?>ResponseEntity<?>对象,用于访问 Servlet 响应 HTTPHeaders 和内容。实体主体将使用HttpMessageConverter转换为响应流。参见名为“使用 HttpEntity”的部分

  • HttpHeaders对象,不带任何正文返回响应。

  • 当应用程序希望在 Spring MVCManagement 的线程中异步产生返回值时,可以返回Callable<?>

  • 当应用程序希望从自己选择的线程中产生返回值时,可以返回DeferredResult<?>

  • 当应用程序要从线程池提交中产生值时,可以返回ListenableFuture<?>CompletableFuture<?>/CompletionStage<?>

  • 可以返回ResponseBodyEmitter以异步地将多个对象写入响应;也支持作为ResponseEntity内的主体。

  • 可以返回SseEmitter以异步将服务器发送的事件写入响应;也支持作为ResponseEntity内的主体。

  • 可以返回StreamingResponseBody以异步写入响应 OutputStream;也支持作为ResponseEntity内的主体。

  • 在方法级别使用通过@ModelAttribute指定的属性名称(或基于返回类型类名称的默认属性名称),将任何其他返回类型视为要公开给视图的单个模型属性。该模型隐含了命令对象和带有@ModelAttributeComments 的参考数据访问器方法的结果。

使用@RequestParam 将请求参数绑定到方法参数

使用@RequestParam注解将请求参数绑定到控制器中的方法参数。

以下代码段显示了用法:

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

    // ...

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

    // ...

}

默认情况下需要使用此注解的参数,但是您可以通过将@RequestParamrequired属性设置为false(例如@RequestParam(name="id", required=false))来指定参数是可选的。

如果目标方法参数类型不是String,则会自动应用类型转换。参见称为“方法参数和类型转换”的部分

Map<String, String>MultiValueMap<String, String>参数上使用@RequestParam注解时,将使用所有请求参数填充 Map。

使用@RequestBody 注解 Map 请求正文

@RequestBody方法参数 Comments 指示方法参数应绑定到 HTTP 请求正文的值。例如:

@PutMapping("/something")
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

您可以使用HttpMessageConverter将请求正文转换为方法参数。 HttpMessageConverter负责从 HTTP 请求消息转换为对象,并从对象转换为 HTTP 响应主体。 RequestMappingHandlerAdapter支持@RequestBodyComments 以及以下默认HttpMessageConverters

  • ByteArrayHttpMessageConverter转换字节数组。

  • StringHttpMessageConverter转换字符串。

  • FormHttpMessageConverter将表单数据与 MultiValueMap<String, String>相互转换。

  • SourceHttpMessageConverter往返于 javax.xml.transform.Source 进行转换。

有关这些转换器的更多信息,请参见Message Converters。还要注意,如果使用 MVC 名称空间或 MVC Java 配置,则默认情况下会注册更多的消息转换器。有关更多信息,请参见第 22.16.1 节“启用 MVC Java Config 或 MVC XML 命名空间”

如果打算读写 XML,则需要使用org.springframework.oxm包中的MarshallerUnmarshaller实现配置MarshallingHttpMessageConverter。下面的示例显示了如何直接在您的配置中执行此操作,但是如果您的应用程序是通过 MVC 名称空间或 MVC Java 配置配置的,请参见第 22.16.1 节“启用 MVC Java Config 或 MVC XML 命名空间”

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="messageConverters">
        <util:list id="beanList">
            <ref bean="stringHttpMessageConverter"/>
            <ref bean="marshallingHttpMessageConverter"/>
        </util:list>
    </property
</bean>

<bean id="stringHttpMessageConverter"
        class="org.springframework.http.converter.StringHttpMessageConverter"/>

<bean id="marshallingHttpMessageConverter"
        class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
    <property name="marshaller" ref="castorMarshaller"/>
    <property name="unmarshaller" ref="castorMarshaller"/>
</bean>

<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>

@RequestBody方法参数可以用@ValidComments,在这种情况下,将使用配置的Validator实例对其进行验证。当使用 MVC 名称空间或 MVC Java 配置时,假定在 Classpath 上有 JSR-303 实现可用,则会自动配置 JSR-303 验证器。

就像@ModelAttribute参数一样,Errors参数可用于检查错误。如果未声明此类参数,则会引发MethodArgumentNotValidExceptionDefaultHandlerExceptionResolver中处理了异常,该异常将400错误发送回 Client 端。

Note

另请参阅第 22.16.1 节“启用 MVC Java Config 或 MVC XML 命名空间”,以获取有关通过 MVC 名称空间或 MVC Java 配置配置消息转换器和验证器的信息。

使用@ResponseBody 注解 Map 响应正文

@ResponseBodyComments 类似于@RequestBody。该 Comments 可以放在方法上,并指示返回类型应直接写到 HTTP 响应主体(而不是放置在模型中,或解释为视图名称)。例如:

@GetMapping("/something")
@ResponseBody
public String helloWorld() {
    return "Hello World";
}

上面的示例将导致文本Hello World被写入 HTTP 响应流。

@RequestBody一样,Spring 通过使用HttpMessageConverter将返回的对象转换为响应主体。有关这些转换器的更多信息,请参见上一节和Message Converters

使用@RestController 注解创建 REST 控制器

让 Controllers 实现 REST API 是一个非常常见的用例,因此仅提供 JSON,XML 或自定义 MediaType 内容。为了方便起见,您可以使用@RestController来 Comments 控制器 Class,而不是使用@ResponseBody来 Comments 所有的@RequestMapping方法。

@RestController是结合@ResponseBody@Controller的构造型 Comments。不仅如此,它还为 Controller 赋予了更多含义,并且在将来的框架发行版中可能还会包含其他语义。

与常规@Controller一样,@RestController可以由@ControllerAdvice@RestControllerAdvice bean 辅助。有关更多详细信息,请参见名为“使用@ControllerAdvice 和@RestControllerAdvice 为控制器提供建议”的部分部分。

Using HttpEntity

HttpEntity@RequestBody@ResponseBody类似。除了可以访问请求和响应主体之外,HttpEntity(以及响应特定的子类ResponseEntity)还允许访问请求和响应头,如下所示:

@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
    String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader"));
    byte[] requestBody = requestEntity.getBody();

    // do something with request header and body

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

上面的示例获取MyRequestHeader请求 Headers 的值,并将主体读取为字节数组。它将MyResponseHeader添加到响应中,将Hello World写入响应流中,并将响应状态代码设置为 201(已创建)。

@RequestBody@ResponseBody一样,Spring 使用HttpMessageConverter在请求流和响应流之间进行转换。有关这些转换器的更多信息,请参见上一节和Message Converters

在方法上使用@ModelAttribute

@ModelAttribute注解可用于方法或方法参数。本节说明其在方法上的用法,而下一节说明其在方法参数上的用法。

方法上的@ModelAttribute表示该方法的目的是添加一个或多个模型属性。此类方法支持与@RequestMapping方法相同的参数类型,但不能直接 Map 到请求。相反,在同一控制器中,控制器中的@ModelAttribute方法在@RequestMapping方法之前被调用。几个例子:

// Add one attribute
// The return value of the method is added to the model under the name "account"
// You can customize the name via @ModelAttribute("myAccount")

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountManager.findAccount(number);
}

// Add multiple attributes

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // add more ...
}

@ModelAttribute方法用于用常用属性填充模型,例如,用状态或宠物类型填充下拉列表,或检索诸如 Account 之类的命令对象,以便使用它来表示 HTML 表单上的数据。下一种情况将在后面的部分中进一步讨论。

请注意@ModelAttribute方法的两种样式。在第一种方法中,该方法通过返回来隐式添加属性。在第二种方法中,该方法接受Model并向其添加任意数量的模型属性。您可以根据需要在两种样式之间进行选择。

控制器可以具有任意数量的@ModelAttribute方法。所有此类方法均在同一控制器的@RequestMapping个方法之前调用。

@ModelAttribute方法也可以在@ControllerAdviceComments 的类中定义,并且此类方法适用于许多控制器。有关更多详细信息,请参见名为“使用@ControllerAdvice 和@RestControllerAdvice 为控制器提供建议”的部分部分。

Tip

如果未明确指定模型属性名称,会发生什么?在这种情况下,会根据模型属性的类型为它分配默认名称。例如,如果该方法返回类型为Account的对象,则使用的默认名称为“帐户”。您可以通过@ModelAttribute注解的值进行更改。如果将属性直接添加到Model,请使用适当的重载addAttribute(..)方法-即具有或不具有属性名称。

@ModelAttributeComments 也可以用于@RequestMapping方法。在那种情况下,@RequestMapping方法的返回值被解释为模型属性而不是视图名称。然后,根据视图名称约定派生视图名称,这与返回void的方法非常相似,请参见第 22.13.3 节“默认视图名称”

在方法参数上使用@ModelAttribute

如上一节所述,@ModelAttribute可以在方法或方法参数上使用。本节说明其在方法参数上的用法。

方法参数上的@ModelAttribute表示应从模型中检索参数。如果模型中不存在该参数,则应首先实例化该参数,然后将其添加到模型中。一旦出现在模型中,则应从具有匹配名称的所有请求参数中填充参数的字段。这在 Spring MVC 中称为数据绑定,这是一种非常有用的机制,使您不必分别解析每个表单字段。

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { }

给定以上示例,Pet 实例可以从哪里来?有几种选择:

@ModelAttribute方法是从数据库检索属性的常用方法,可以选择使用@SessionAttributes在请求之间存储该属性。在某些情况下,使用 URI 模板变量和类型转换器来检索属性可能很方便。这是一个例子:

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

在此示例中,模型属性的名称(即“帐户”)与 URI 模板变量的名称匹配。如果您注册的Converter<String, Account>可以将String帐户值转换为Account实例,那么上面的示例将可以使用而无需@ModelAttribute方法。

下一步是数据绑定。 WebDataBinder类匹配请求参数名称(包括查询字符串参数和表单字段)以按名称对属性字段建模。在必要时应用了类型转换(从 String 到目标字段类型)后,将填充匹配字段。 第 9 章,验证,数据绑定和类型转换中介绍了数据绑定和验证。 名为“自定义 WebDataBinder 初始化”的部分中介绍了为控制器级别自定义数据绑定过程。

数据绑定的结果可能是错误,例如缺少必填字段或类型转换错误。要检查此类错误,请在@ModelAttribute参数后紧接添加BindingResult参数:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

使用BindingResult,您可以检查是否发现了错误,在这种情况下,通常呈现相同的表单,借助 Spring 的<errors> form 标记可以显示错误。

请注意,在某些情况下,无需数据绑定即可访问模型中的属性可能很有用。在这种情况下,您可以将Model注入到控制器中,或者使用注解上的binding标志:

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

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

@PostMapping("update")
public String update(@Valid AccountUpdateForm form, BindingResult result,
        @ModelAttribute(binding=false) Account account) {

    // ...
}

除了数据绑定外,您还可以使用自己的自定义验证程序调用验证,并传递与记录数据绑定错误相同的BindingResult。这样一来,就可以将数据绑定和验证错误汇总到一个地方,然后再报告给用户:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

    new PetValidator().validate(pet, result);
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

或者,您可以通过添加 JSR-303 @ValidComments 来自动调用验证:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {

    if (result.hasErrors()) {
        return "petForm";
    }

    // ...

}

有关如何配置和使用验证的详细信息,请参见第 9.8 节“Spring 验证”第 9 章,验证,数据绑定和类型转换

使用@SessionAttributes 在请求之间的 HTTP 会话中存储模型属性

类型级别的@SessionAttributesComments 声明特定处理程序使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些名称应透明地存储在会话或某些会话存储中,用作后续请求之间的表单支持 Bean。

以下代码段显示了此 Comments 的用法,并指定了模型属性名称:

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {
    // ...
}

使用@SessionAttribute 访问预先存在的全局会话属性

如果您需要访问全局 Management 的预先存在的会话属性,即在控制器外部(例如通过过滤器),并且可能存在或可能不存在,请在方法参数上使用@SessionAttributeComments:

@RequestMapping("/")
public String handle(@SessionAttribute User user) {
    // ...
}

对于需要添加或删除会话属性的用例,请考虑将org.springframework.web.context.request.WebRequestjavax.servlet.http.HttpSession注入控制器方法。

对于作为控制器工作流一部分在会话中临时存储模型属性,请考虑使用名为“使用@SessionAttributes 在请求之间的 HTTP 会话中存储模型属性”的部分中所述的SessionAttributes

使用@RequestAttribute 访问请求属性

类似于@SessionAttribute@RequestAttribute注解可用于访问由过滤器或拦截器创建的预先存在的请求属性:

@RequestMapping("/")
public String handle(@RequestAttribute Client client) {
    // ...
}

处理“应用程序/ x-www-form-urlencoded”数据

前面的部分介绍了使用@ModelAttribute来支持来自浏览器 Client 端的表单提交请求。对于非浏览器 Client 端的请求,也建议使用相同的 Comments。但是,在处理 HTTP PUT 请求方面有一个明显的不同。浏览器可以通过 HTTP GET 或 HTTP POST 提交表单数据。非浏览器 Client 端也可以通过 HTTP PUT 提交表单。这提出了一个挑战,因为 Servlet 规范要求ServletRequest.getParameter*()系列方法仅支持 HTTP POST 而不支持 HTTP PUT 的表单字段访问。

为了支持 HTTP PUT 和 PATCH 请求,spring-web模块提供了过滤器HttpPutFormContentFilter,可以在web.xml中对其进行配置:

<filter>
    <filter-name>httpPutFormFilter</filter-name>
    <filter-class>org.springframework.web.filter.HttpPutFormContentFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>httpPutFormFilter</filter-name>
    <servlet-name>dispatcherServlet</servlet-name>
</filter-mapping>

<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>

上面的过滤器拦截 Content Type 为application/x-www-form-urlencoded的 HTTP PUT 和 PATCH 请求,从请求的正文中读取表单数据,并包装ServletRequest以使表单数据可通过ServletRequest.getParameter*()系列方法使用。

Note

由于HttpPutFormContentFilter占用了请求的主体,因此不应为依赖其他转换器application/x-www-form-urlencoded的 PUT 或 PATCH URL 进行配置。这包括@RequestBody MultiValueMap<String, String>HttpEntity<MultiValueMap<String, String>>

使用@CookieValue 注解 MapCookie 值

@CookieValueComments 允许将方法参数绑定到 HTTP cookie 的值。

让我们考虑以下 HTTP 请求已收到以下 cookie:

JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84

下面的代码示例演示了如何获取JSESSIONID cookie 的值:

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
    //...
}

如果目标方法参数类型不是String,则会自动应用类型转换。参见称为“方法参数和类型转换”的部分

Servlet 和 Portlet 环境中的 Comments 处理程序方法支持此 Comments。

使用@RequestHeader 注解 Map 请求 Headers 属性

@RequestHeader注解允许将方法参数绑定到请求 Headers。

这是一个示例请求 Headers:

Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300

下面的代码示例演示如何获取Accept-EncodingKeep-AliveHeaders 的值:

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
        @RequestHeader("Keep-Alive") long keepAlive) {
    //...
}

如果方法参数不是String,则会自动应用类型转换。参见称为“方法参数和类型转换”的部分

Map<String, String>MultiValueMap<String, String>HttpHeaders参数上使用@RequestHeaderComments 时,将使用所有 Headers 值填充 Map。

Tip

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

Servlet 和 Portlet 环境中的 Comments 处理程序方法支持此 Comments。

方法参数和类型转换

从请求中提取的基于字符串的值(包括请求参数,路径变量,请求 Headers 和 cookie 值)可能需要转换为方法参数或字段的目标类型(例如,将请求参数绑定到@ModelAttribute参数中的字段)他们必须遵守。如果目标类型不是String,Spring 会自动转换为适当的类型。支持所有简单类型,例如 int,long,Date 等。您可以通过WebDataBinder(请参见名为“自定义 WebDataBinder 初始化”的部分)或通过向FormattingConversionService注册Formatters(请参见第 9.6 节“ Spring 字段格式化”)来进一步自定义转换过程。

自定义 WebDataBinder 初始化

要通过 Spring 的WebDataBinder自定义与 PropertyEditor 的请求参数绑定,可以在控制器中使用@InitBinderComments 的方法,在@ControllerAdvice类中使用@InitBinder的方法,或者提供自定义WebBindingInitializer。有关更多详细信息,请参见名为“使用@ControllerAdvice 和@RestControllerAdvice 为控制器提供建议”的部分部分。

使用@InitBinder 自定义数据绑定

使用@InitBinderComments 控制器方法,可以直接在控制器类中配置 Web 数据绑定。 @InitBinder标识用于初始化WebDataBinder的方法,该方法将用于填充命令并形成带 Comments 的处理程序方法的对象参数。

此类 init-binder 方法支持@RequestMapping方法支持的所有参数,但命令/表单对象和相应的验证结果对象除外。初始化绑定器方法不能具有返回值。因此,它们通常被声明为void。典型的参数包括WebDataBinderWebRequestjava.util.Locale结合使用,从而允许代码注册特定于上下文的编辑器。

下面的示例演示如何使用@InitBinder为所有java.util.Date表单属性配置CustomDateEditor

@Controller
public class MyFormController {

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

    // ...
}

另外,从 Spring 4.2 开始,请考虑使用addCustomFormatter而不是PropertyEditor实例来指定Formatter实现。如果您碰巧在共享的FormattingConversionService中也有基于Formatter的设置,并且该方法可用于控制器的特定于绑定规则的调整,则此方法特别有用。

@Controller
public class MyFormController {

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

    // ...
}
配置自定义 WebBindingInitializer

要外部化数据绑定初始化,可以提供WebBindingInitializer接口的自定义实现,然后通过为AnnotationMethodHandlerAdapter提供自定义 bean 配置来启用该接口,从而覆盖默认配置。

PetClinic 应用程序的以下示例显示了使用WebBindingInitializer接口org.springframework.samples.petclinic.web.ClinicBindingInitializer的自定义实现的配置,该配置可配置多个 PetClinic 控制器所需的 PropertyEditor。

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="cacheSeconds" value="0"/>
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer"/>
    </property>
</bean>

@InitBinder方法也可以在@ControllerAdviceComments 的类中定义,在这种情况下,它们适用于匹配的控制器。这提供了使用WebBindingInitializer的替代方法。有关更多详细信息,请参见名为“使用@ControllerAdvice 和@RestControllerAdvice 为控制器提供建议”的部分部分。

使用@ControllerAdvice 和@RestControllerAdvice 为控制器提供建议

@ControllerAdviceComments 是组件 Comments,允许通过 Classpath 扫描自动检测实现类。使用 MVC 名称空间或 MVC Java 配置时会自动启用它。

@ControllerAdviceComments 的类可以包含@ExceptionHandler@InitBinder@ModelAttributeComments 的方法,这些方法将适用于所有控制器层次结构中的@RequestMapping方法,而不是声明它们的控制器层次结构。

@RestControllerAdvice是替代方法,其中@ExceptionHandler方法默认情况下采用@ResponseBody语义。

@ControllerAdvice@RestControllerAdvice都可以定位控制器的子集:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class AnnotationAdvice {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class BasePackageAdvice {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class AssignableTypesAdvice {}

请查看@ControllerAdvice documentation以获取更多详细信息。

Jackson 序列化视图支持

有时,根据上下文筛选将序列化到 HTTP 响应正文的对象可能会很有用。为了提供这种功能,Spring MVC 内置了对使用Jackson 的序列化视图进行渲染的支持。

要将它与@ResponseBody控制器方法或返回ResponseEntity的控制器方法一起使用,只需添加@JsonViewComments 以及一个类参数,该参数指定要使用的视图类或接口:

@RestController
public class UserController {

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

public class User {

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

    private String username;
    private String password;

    public User() {
    }

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

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

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

Note

请注意,尽管@JsonView允许指定多个类,但是仅在一个类参数的情况下才支持在控制器方法上使用。如果需要启用多个视图,请考虑使用复合接口。

对于依赖视图分辨率的控制器,只需将序列化视图类添加到模型中:

@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";
    }
}

Jackson JSONP 支持

为了启用对@ResponseBodyResponseEntity方法的JSONP支持,请声明一个扩展AbstractJsonpResponseBodyAdvice@ControllerAdvice bean,如下所示,其中构造函数参数表示 JSONP 查询参数名称:

@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice {

    public JsonpAdvice() {
        super("callback");
    }
}

对于依赖视图分辨率的控制器,当请求具有名为jsonpcallback的查询参数时,会自动启用 JSONP。这些名称可以通过jsonpParameterNames属性进行自定义。

Note

从 Spring Framework 4.3.18 开始,不推荐使用 JSONP 支持,从 Spring Framework 5.1 开始将删除该支持,而应使用CORS

22.3.4 异步请求处理

Spring MVC 3.2 引入了基于 Servlet 3 的异步请求处理。现在,控制器方法可以像往常一样返回值,而不是返回值java.util.concurrent.Callable并从 Spring MVC 托管线程产生返回值。同时,退出并释放主要的 Servlet 容器线程,并允许其处理其他请求。 Spring MVC 借助TaskExecutor在单独的线程中调用Callable,当Callable返回时,该请求被分派回 Servlet 容器以使用Callable返回的值恢复处理。这是这种控制器方法的示例:

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

    return new Callable<String>() {
        public String call() throws Exception {
            // ...
            return "someView";
        }
    };

}

控制器方法的另一个选项是返回DeferredResult的实例。在这种情况下,返回值也将从任何线程产生,即不受 Spring MVCManagement 的线程。例如,可以响应于某些外部事件(例如 JMS 消息,计划任务等)来产生结果。这是这种控制器方法的示例:

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

// In some other thread...
deferredResult.setResult(data);

如果不了解 Servlet 3.0 异步请求处理功能,可能很难理解这一点。continue 阅读肯定会有所帮助。以下是有关底层机制的一些基本事实:

  • 可以通过调用request.startAsync()ServletRequest置于异步模式。这样做的主要效果是 Servlet 以及所有过滤器都可以退出,但响应将保持打开状态,以允许稍后完成处理。

  • request.startAsync()的调用返回AsyncContext,该AsyncContext可用于进一步控制异步处理。例如,它提供了方法dispatch,它类似于 Servlet API 的转发,但它允许应用程序恢复 Servlet 容器线程上的请求处理。

  • ServletRequest提供对当前DispatcherType的访问,可用于区分处理初始请求,异步分派,转发和其他分派器类型。

考虑到上述内容,以下是使用Callable处理异步请求的事件序列:

  • 控制器返回Callable

  • Spring MVC 开始异步处理,并将Callable提交给TaskExecutor以便在单独的线程中进行处理。

  • DispatcherServlet和所有 Filter 退出 Servlet 容器线程,但响应保持打开状态。

  • Callable产生结果,Spring MVC 将请求分派回 Servlet 容器以恢复处理。

  • 再次调用DispatcherServlet,并以Callable异步产生的结果恢复处理。

DeferredResult的序列非常相似,只是它取决于应用程序从任何线程产生异步结果:

  • 控制器返回一个DeferredResult并将其保存在一些可以访问它的内存队列或列表中。

  • Spring MVC 开始异步处理。

  • DispatcherServlet和所有已配置的过滤器退出请求处理线程,但响应保持打开状态。

  • 应用程序从某个线程设置DeferredResult,Spring MVC 将请求分派回 Servlet 容器。

  • DispatcherServlet再次被调用,并以异步产生的结果 continue 处理。

要进一步了解异步请求处理的动机以及何时或为何使用它,请阅读本博客文章系列

异步请求的异常处理

如果从控制器方法返回的Callable在执行时引发异常怎么办?简短的答案与控制器方法引发异常时发生的情况相同。它通过常规的异常处理机制进行处理。更长的解释是,当Callable引发异常时,Spring MVC 将Exception作为结果分派到 Servlet 容器,并导致使用Exception而不是控制器方法返回值来恢复请求处理。使用DeferredResult时,可以选择使用Exception实例调用setResult还是setErrorResult

拦截异步请求

HandlerInterceptor还可以实现AsyncHandlerInterceptor,以实现afterConcurrentHandlingStarted回调,当异步处理开始时,该回调将代替postHandleafterCompletion进行调用。

HandlerInterceptor还可以注册CallableProcessingInterceptorDeferredResultProcessingInterceptor,以便与异步请求的生命周期更深入地集成,例如处理超时事件。有关更多详细信息,请参见AsyncHandlerInterceptor的 Javadoc。

DeferredResult类型还提供了onTimeout(Runnable)onCompletion(Runnable)之类的方法。有关更多详细信息,请参见DeferredResult的 Javadoc。

使用Callable时,您可以使用WebAsyncTask实例包装它,该实例还提供超时和完成的注册方法。

HTTP Streaming

控制器方法可以使用DeferredResultCallable异步生成其返回值,并且可以用于实现诸如long polling之类的技术,其中服务器可以将事件尽快推送给 Client 端。

如果要在单个 HTTP 响应上推送多个事件该怎么办?这是一种与“长轮询”有关的技术,称为“ HTTP 流”。 Spring MVC 通过ResponseBodyEmitter返回值类型使之成为可能,该返回值类型可用于发送多个对象,而不是通常使用@ResponseBody的情况,其中返回的每个对象都使用HttpMessageConverter写入响应中。

这是一个例子:

@RequestMapping("/events")
public ResponseBodyEmitter handle() {
    ResponseBodyEmitter emitter = new ResponseBodyEmitter();
    // Save the emitter somewhere..
    return emitter;
}

// In some other thread
emitter.send("Hello once");

// and again later on
emitter.send("Hello again");

// and done at some point
emitter.complete();

请注意,ResponseBodyEmitter也可以用作ResponseEntity中的正文,以自定义响应的状态和标题。

HTTP 流与服务器发送的事件

SseEmitterResponseBodyEmitter的子类,为Server-Sent Events提供支持。服务器发送的事件只是同一“ HTTP 流”技术的另一种变体,只是从服务器推送的事件是根据 W3C 服务器发送的事件规范进行格式化的。

服务器发送的事件可用于其预期目的,即将事件从服务器推送到 Client 端。在 Spring MVC 中,这很容易做到,只需返回一个类型为SseEmitter的值。

但是请注意,Internet Explorer 不支持服务器发送事件,对于更高级的 Web 应用程序消息传递场景(例如在线游戏,协作,财务应用程序等),最好考虑使用 Spring 的 WebSocket 支持,其中包括 SockJS 风格的 WebSocket 仿真。种类繁多的浏览器(包括 Internet Explorer)以及更高级别的消息传递模式,这些消息传递模式可在以消息传递为中心的体系结构中通过发布-订阅模型与 Client 端进行交互。有关此背景的更多信息,请参见以下博客文章

HTTP 直接流到 OutputStream

ResponseBodyEmitter允许通过HttpMessageConverter将对象写入响应来发送事件。这可能是最常见的情况,例如在写入 JSON 数据时。但是,有时绕过消息转换并直接写入响应OutputStream很有用,例如用于文件下载。这可以借助StreamingResponseBody返回值类型来完成。

这是一个例子:

@RequestMapping("/download")
public StreamingResponseBody handle() {
    return new StreamingResponseBody() {
        @Override
        public void writeTo(OutputStream outputStream) throws IOException {
            // write...
        }
    };
}

请注意,StreamingResponseBody也可以用作ResponseEntity中的正文,以自定义响应的状态和标题。

配置异步请求处理

Servlet 容器配置

对于配置了web.xml的应用程序,请确保更新到版本 3.0:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            http://java.sun.com/xml/ns/javaee
            http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    ...

</web-app>

必须通过web.xml中的<async-supported>true</async-supported>子元素在DispatcherServlet上启用异步支持。另外,必须将参与异步请求处理的任何Filter配置为支持 ASYNC 调度程序类型。为 Spring 框架提供的所有过滤器启用 ASYNC 调度程序类型应该是安全的,因为它们通常会扩展OncePerRequestFilter,并且可以在运行时检查是否需要将过滤器包含在异步调度中。

以下是一些示例 web.xml 配置:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
            http://java.sun.com/xml/ns/javaee
            http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <filter>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <filter-class>org.springframework.~.OpenEntityManagerInViewFilter</filter-class>
        <async-supported>true</async-supported>
    </filter>

    <filter-mapping>
        <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>ASYNC</dispatcher>
    </filter-mapping>

</web-app>

如果使用 Servlet 3,例如通过WebApplicationInitializer进行基于 Java 的配置,则还需要像web.xml一样设置“ asyncSupported”标志以及 ASYNC 调度程序类型。为了简化所有这些配置,请考虑扩展AbstractDispatcherServletInitializer或更好的AbstractAnnotationConfigDispatcherServletInitializer,它们会自动设置这些选项并使注册Filter实例非常容易。

Spring MVC 配置

MVC Java 配置和 MVC 名称空间提供用于配置异步请求处理的选项。 WebMvcConfigurer具有方法configureAsyncSupport,而<mvc:annotation-driven>具有<async-support>子元素。

这些允许您配置用于异步请求的默认超时值,如果未设置,则取决于基础 Servlet 容器(例如,在 Tomcat 上为 10 秒)。您还可以配置AsyncTaskExecutor以执行从控制器方法返回的Callable实例。强烈建议配置此属性,因为默认情况下,Spring MVC 使用SimpleAsyncTaskExecutor。 MVC Java 配置和 MVC 命名空间还允许您注册CallableProcessingInterceptorDeferredResultProcessingInterceptor实例。

如果需要覆盖特定DeferredResult的默认超时值,则可以使用适当的类构造函数来实现。同样,对于Callable,您可以将其包装在WebAsyncTask中,并使用适当的类构造函数自定义超时值。 WebAsyncTask的类构造函数还允许提供AsyncTaskExecutor

22.3.5 测试控制器

spring-test模块为测试带 Comments 的控制器提供了一流的支持。参见第 15.6 节“ Spring MVC 测试框架”

22.4 处理程序 Map

在 Spring 的早期版本中,要求用户在 Web 应用程序上下文中定义一个或多个HandlerMapping bean,以将传入的 Web 请求 Map 到适当的处理程序。随着带 Comments 的控制器的引入,您通常不需要这样做,因为RequestMappingHandlerMapping自动在所有@Controller bean 上查找@RequestMappingComments。但是,请记住,从AbstractHandlerMapping扩展的所有HandlerMapping类都具有以下属性,您可以使用这些属性来自定义其行为:

  • interceptors要使用的拦截器列表。 HandlerInterceptor第 22.4.1 节“使用 HandlerInterceptor 拦截请求”中讨论。

  • defaultHandler要使用的默认处理程序,如果此处理程序 Map 未导致匹配的处理程序。

  • order根据 order 属性的值(请参见org.springframework.core.Ordered接口),Spring 对上下文中可用的所有处理程序 Map 进行排序,并应用第一个匹配的处理程序。

  • alwaysUseFullPath如果是true,Spring 将使用当前 Servlet 上下文中的完整路径来查找适当的处理程序。如果为false(默认值),则使用当前 ServletMap 中的路径。例如,如果使用/testing/*MapServlet,并且alwaysUseFullPath属性设置为 true,则使用/testing/viewPage.html,而如果属性设置为 false,则使用/viewPage.html

  • 从 Spring 2.5 开始,urlDecode默认为true。如果您希望比较编码路径,请将此标志设置为false。但是,HttpServletRequest始终以解码形式公开 Servlet 路径。请注意,与编码路径相比,Servlet 路径将不匹配。

以下示例显示如何配置拦截器:

<beans>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="interceptors">
            <bean class="example.MyInterceptor"/>
        </property>
    </bean>
<beans>

22.4.1 使用 HandlerInterceptor 拦截请求

Spring 的处理程序 Map 机制包括处理程序拦截器,当您要将特定功能应用于某些请求(例如,检查主体)时,该处理程序很有用。

位于处理程序 Map 中的拦截器必须实现org.springframework.web.servlet包中的HandlerInterceptor。该接口定义了三种方法:preHandle(..)在执行实际处理程序之前被调用; postHandle(..)在处理程序执行后被调用;并且afterCompletion(..)在完成完整请求之后被调用*。这三种方法应提供足够的灵 Active 来执行各种预处理和后处理。

preHandle(..)方法返回布尔值。您可以使用此方法来中断或 continue 执行链的处理。当此方法返回true时,处理程序执行链将 continue;当它返回 false 时,DispatcherServlet假定拦截器本身已经处理了请求(例如,渲染了适当的视图),并且没有 continue 执行其他拦截器和执行链中的实际处理程序。

可以使用interceptors属性配置拦截器,该属性存在于从AbstractHandlerMapping扩展的所有HandlerMapping类中。在下面的示例中显示:

<beans>
    <bean id="handlerMapping"
            class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
    </bean>

    <bean id="officeHoursInterceptor"
            class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime" value="9"/>
        <property name="closingTime" value="18"/>
    </bean>
</beans>
package samples;

public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {

    private int openingTime;
    private int closingTime;

    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }

    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
            Object handler) throws Exception {
        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour && hour < closingTime) {
            return true;
        }
        response.sendRedirect("http://host.com/outsideOfficeHours.html");
        return false;
    }
}

该 Map 处理的所有请求都被TimeBasedAccessInterceptor拦截。如果当前时间不在办公时间,则将用户重定向到静态 HTML 文件,例如,您只能在办公时间访问该网站。

Note

使用RequestMappingHandlerMapping时,实际的处理程序是HandlerMethod的实例,该实例标识将要调用的特定控制器方法。

如您所见,Spring 适配器类HandlerInterceptorAdapter使扩展HandlerInterceptor接口更加容易。

Tip

在上面的示例中,配置的拦截器将应用于使用带 Comments 的控制器方法处理的所有请求。如果要缩小应用拦截器的 URL 路径,可以使用 MVC 命名空间或 MVC Java 配置,或者声明MappedInterceptor类型的 bean 实例来执行此操作。参见第 22.16.1 节“启用 MVC Java Config 或 MVC XML 命名空间”

请注意,HandlerInterceptorpostHandle方法并不总是非常适合与@ResponseBodyResponseEntity方法一起使用。在这种情况下,HttpMessageConverter在调用postHandle之前写入并提交响应,这使得无法更改响应,例如添加 Headers。相反,应用程序可以实现ResponseBodyAdvice并将其声明为@ControllerAdvice bean 或直接在RequestMappingHandlerAdapter上对其进行配置。

22.5 解决视图

用于 Web 应用程序的所有 MVC 框架都提供了一种解决视图的方法。 Spring 提供了视图解析器,使您可以在浏览器中呈现模型,而无需将您与特定的视图技术联系在一起。使用 Spring,您可以立即使用 JSP,Velocity 模板和 XSLT 视图。有关如何集成和使用多种不同的视图技术的讨论,请参见第 23 章,查看技术

对于 Spring 处理视图的方式而言,重要的两个接口是ViewResolverViewViewResolver提供视图名称和实际视图之间的 Map。 View接口处理请求的准备并将请求移交给一种视图技术。

22.5.1 使用 ViewResolver 界面解析视图

第 22.3 节“实现控制器”中所述,Spring Web MVC 控制器中的所有处理程序方法都必须显式(例如,通过返回StringViewModelAndView)或隐式(即基于约定)解析为逻辑视图名称。 Spring 中的视图由逻辑视图名称寻址,并由视图解析器解析。 Spring 附带了很多视图解析器。该表列出了其中的大多数。以下是几个例子。

表 22.3 查看解析器

ViewResolverDescription
AbstractCachingViewResolver缓存视图的抽象视图解析器。通常,视图需要先准备才能使用。扩展此视图解析器可提供缓存。
XmlViewResolverViewResolver的实现,该实现接受用 XML 编写的配置文件,该配置文件的 DTD 与 Spring 的 XML bean 工厂相同。默认配置文件为/WEB-INF/views.xml
ResourceBundleViewResolverViewResolver的实现,该实现使用由包基本名称指定的ResourceBundle中的 bean 定义。通常,您可以在类文件中的属性文件中定义 Binding 软件。默认文件名为views.properties
UrlBasedViewResolverViewResolver接口的简单实现,可以直接将逻辑视图名称解析为 URL,而无需显式的 Map 定义。如果您的逻辑名称以直接的方式与视图资源的名称匹配,而不需要任意 Map,则这是适当的。
InternalResourceViewResolver方便的UrlBasedViewResolver子类,支持InternalResourceView(实际上是 Servlet 和 JSP)以及JstlViewTilesView之类的子类。您可以使用setViewClass(..)为该解析器生成的所有视图指定视图类。有关详细信息,请参见UrlBasedViewResolver javadocs。
VelocityViewResolver / FreeMarkerViewResolver方便的UrlBasedViewResolver子类,分别支持VelocityView(实际上是 Velocity 模板)或FreeMarkerView,以及它们的自定义子类。
ContentNegotiatingViewResolverViewResolver接口的实现,该接口根据请求文件名或AcceptHeaders 解析视图。参见第 22.5.4 节“ ContentNegotiatingViewResolver”

例如,将 JSP 作为视图技术,可以使用UrlBasedViewResolver。该视图解析器将视图名称转换为 URL,并将请求移交给 RequestDispatcher 来呈现视图。

<bean id="viewResolver"
        class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

当返回test作为逻辑视图名称时,此视图解析器将请求转发到RequestDispatcher,该请求会将请求发送到/WEB-INF/jsp/test.jsp

在 Web 应用程序中结合使用不同的视图技术时,可以使用ResourceBundleViewResolver

<bean id="viewResolver"
        class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
    <property name="defaultParentView" value="parentView"/>
</bean>

ResourceBundleViewResolver检查由基名标识的ResourceBundle,对于应该解析的每个视图,它都使用[viewname].(class)属性的值作为视图类,并使用[viewname].url属性的值作为视图 URL。可以在下一章中找到示例,其中涵盖了视图技术。如您所见,您可以标识一个父视图,属性文件中的所有视图都将从该父视图“扩展”。例如,通过这种方式,您可以指定默认视图类。

Note

AbstractCachingViewResolver缓存视图的实例实例的子类。缓存可以提高某些视图技术的性能。通过将cache属性设置为false可以关闭缓存。此外,如果必须在运行时刷新某个视图(例如,修改 Velocity 模板时),则可以使用removeFromCache(String viewName, Locale loc)方法。

22.5.2 链接 ViewResolvers

Spring 支持多个视图解析器。因此,您可以链接解析器,例如在某些情况下覆盖特定视图。通过将多个解析器添加到您的应用程序上下文中,并在必要时通过设置order属性以指定 Sequences 来链接视图解析器。请记住,order 属性越高,视图解析器在链中的定位就越晚。

在以下示例中,视图解析器链由两个解析器组成,一个InternalResourceViewResolver始终自动定位为链中的最后一个解析器,另一个XmlViewResolver用于指定 Excel 视图。 InternalResourceViewResolver不支持 Excel 视图。

<bean id="jspViewResolver" 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>

<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
    <property name="order" value="1"/>
    <property name="location" value="/WEB-INF/views.xml"/>
</bean>

<!-- in views.xml -->

<beans>
    <bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>

如果特定的视图解析器未生成视图,则 Spring 会检查其他视图解析器的上下文。如果存在其他视图解析器,Spring 将 continue 检查它们,直到解析视图为止。如果没有视图解析器返回视图,则 Spring 抛出ServletException

视图解析器的协定指定视图解析器可以返回 null 以指示找不到该视图。但是,并非所有视图解析器都执行此操作,因为在某些情况下,解析器根本无法检测视图是否存在。例如,InternalResourceViewResolver在内部使用RequestDispatcher,而调度是确定 JSP 是否存在的唯一方法,但是此操作只能执行一次。 VelocityViewResolver和其他一些也是如此。检查特定视图解析器的 javadocs,以查看它是否报告不存在的视图。因此,将InternalResourceViewResolver放置在链中的最后一个位置以外的位置会导致未完全检查该链,因为InternalResourceViewResolver总是会返回视图!

22.5.3 重定向到视图

如前所述,控制器通常返回逻辑视图名称,视图解析器将其解析为特定的视图技术。对于通过 Servlet 或 JSP 引擎处理的视图技术(例如 JSP),通常通过InternalResourceViewResolverInternalResourceView的组合来处理此分辨率,该组合会发出内部转发或通过 Servlet API 的RequestDispatcher.forward(..)方法或RequestDispatcher.include()方法进行包含。对于其他视图技术,例如 Velocity,XSLT 等,视图本身将内容直接写入响应流。

有时需要在呈现视图之前向 Client 端发出 HTTP 重定向。例如,当已经用POST数据调用一个控制器,并且响应实际上是委派给另一个控制器时(例如,成功提交表单时),这是理想的。在这种情况下,正常的内部转发将意味着另一个控制器也将看到相同的POST数据,如果将其与其他预期数据混淆,则可能会出现问题。在显示结果之前执行重定向的另一个原因是消除了用户多次提交表单数据的可能性。在这种情况下,浏览器将首先发送初始POST;然后它将收到响应以重定向到另一个 URL;最后,浏览器将对重定向响应中命名的 URL 执行后续的GET。因此,从浏览器的角度来看,当前页面不反映POST的结果,而是GET的结果。最终结果是用户无法通过执行刷新来意外地重新 Importing 相同的数据。刷新将强制结果页面的GET,而不是重新发送初始POST的数据。

RedirectView

作为控制器响应的结果强制重定向的一种方法是,控制器创建并返回 Spring 的RedirectView的实例。在这种情况下,DispatcherServlet不使用常规视图解析机制。而是因为已经给了它(重定向)视图,所以DispatcherServlet只是指示该视图执行其工作。 RedirectView依次调用HttpServletResponse.sendRedirect()将 HTTP 重定向发送到 Client 端浏览器。

如果使用RedirectView并且视图是由控制器本身创建的,则建议您配置重定向 URL,以将其注入到控制器中,这样它就不会烘焙到控制器中,而是在上下文中与视图名称一起配置。 名为“重定向:前缀”的部分促进了这种去耦。

将数据传递到重定向目标

默认情况下,所有模型属性均被视为在重定向 URL 中作为 URI 模板变量公开。在其余属性中,那些属于原始类型或原始类型的集合/数组的属性会自动附加为查询参数。

如果专门为重定向准备了模型实例,则将原始类型属性附加为查询参数可能是理想的结果。但是,在带 Comments 的控制器中,模型可能包含为渲染目的添加的其他属性(例如,下拉字段值)。为避免此类属性出现在 URL 中的可能性,@RequestMapping方法可以声明RedirectAttributes类型的参数,并使用它来指定确切的属性以供RedirectView使用。如果该方法确实重定向,则使用RedirectAttributes的内容。否则,将使用模型的内容。

RequestMappingHandlerAdapter提供了一个名为"ignoreDefaultModelOnRedirect"的标志,如果控制器方法重定向,则该标志可用于指示默认Model的内容永远不要使用。相反,控制器方法应声明RedirectAttributes类型的属性,或者如果没有声明,则不应将任何属性传递给RedirectView。 MVC 命名空间和 MVC Java 配置都将此标志设置为false,以保持向后兼容性。但是,对于新应用程序,我们建议将其设置为true

请注意,扩展重定向 URL 时,来自当前请求的 URI 模板变量将自动变为可用,并且不需要通过ModelRedirectAttributes显式添加。例如:

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

将数据传递到重定向目标的另一种方法是通过* Flash Attributes *。与其他重定向属性不同,闪存属性保存在 HTTP 会话中(因此不会出现在 URL 中)。有关更多信息,请参见第 22.6 节“使用闪存属性”

重定向:前缀

尽管使用RedirectView可以很好地工作,但是如果控制器本身创建了RedirectView,则无法避免这样的事实,即控制器知道正在发生重定向。这确实是次优的,而且太紧密了。控制器不应该 true 在乎响应的处理方式。通常,它仅应根据已注入其中的视图名称进行操作。

特殊的redirect:前缀使您可以完成此操作。如果返回的视图名称带有前缀redirect:,则UrlBasedViewResolver(以及所有子类)将识别出这是需要重定向的特殊指示。视图名称的其余部分将被视为重定向 URL。

最终效果与控制器返回RedirectView的效果相同,但是现在控制器本身可以简单地根据逻辑视图名称进行操作。逻辑视图名称(例如redirect:/myapp/some/resource)将相对于当前 Servlet 上下文重定向,而逻辑名称(例如redirect:http://myhost.com/some/arbitrary/path)将重定向到绝对 URL。

请注意,控制器处理程序以@ResponseStatusComments,Comments 值优先于RedirectView设置的响应状态。

转发:前缀

对于最终由UrlBasedViewResolver和子类解析的视图名称,也可以使用特殊的forward:前缀。这将在其余视图名称(被视为 URL)周围创建一个InternalResourceView(最终将完成RequestDispatcher.forward())。因此,此前缀对于InternalResourceViewResolverInternalResourceView无效(例如,对于 JSP)。但是,当您主要使用另一种视图技术,但仍希望强制转发由 Servlet/JSP 引擎处理的资源时,前缀可能会很有用。 (请注意,您也可以链接多个视图解析器.)

redirect:前缀一样,如果将带有forward:前缀的视图名称注入到控制器中,则控制器在处理响应方面不会检测到任何特殊情况。

22.5.4 ContentNegotiatingViewResolver

ContentNegotiatingViewResolver不会解析视图本身,而是委派给其他视图解析器,选择与 Client 端请求的表示类似的视图。Client 端有两种策略可以请求服务器表示:

  • 为每个资源使用不同的 URI,通常在 URI 中使用不同的文件 extensions。例如,URI http://www.example.com/users/fred.pdf请求用户 fred 的 PDF 表示,而http://www.example.com/users/fred.xml请求 XML 表示。

  • 对 Client 端使用相同的 URI 来定位资源,但是设置Accept HTTP 请求 Headers 以列出其可以理解的media types。例如,对于将AcceptHeaders 设置为application/pdfhttp://www.example.com/users/fred的 HTTP 请求,请求用户 fred 的 PDF 表示,而对AcceptHeaders 设置为text/xmlhttp://www.example.com/users/fred请求 XML 表示。此策略称为content negotiation

Note

AcceptHeaders 的一个问题是无法在 HTML 的网络浏览器中进行设置。例如,在 Firefox 中,它固定为:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

因此,在开发基于浏览器的 Web 应用程序时,通常会为每个表示形式使用不同的 URI。

为了支持资源的多种表示形式,Spring 提供了ContentNegotiatingViewResolver来基于 HTTP 请求的文件 extensions 或Accept头解析视图。 ContentNegotiatingViewResolver本身并不执行视图解析,而是委托给您通过 bean 属性ViewResolvers指定的视图解析器列表。

ContentNegotiatingViewResolver通过将请求媒体类型与与每个ViewResolvers关联的View支持的媒体类型(也称为Content-Type)进行比较,从而选择合适的View来处理请求。列表中具有兼容Content-Type的第一个View将表示形式返回给 Client 端。如果ViewResolver链无法提供兼容的视图,则将查询通过DefaultViews属性指定的视图列表。后一个选项适用于单例Views,该单例Views可以呈现当前资源的适当表示形式,而与逻辑视图名称无关。 AcceptHeaders 可以包含通配符,例如text/*,在这种情况下,Content-Type 为text/xmlView是兼容的匹配。

要支持基于文件 extensions 的视图的自定义分辨率,请使用ContentNegotiationManager:请参见第 22.16.6 节“内容协商”

这是ContentNegotiatingViewResolver的示例配置:

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="viewResolvers">
        <list>
            <bean class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
            <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
                <property name="prefix" value="/WEB-INF/jsp/"/>
                <property name="suffix" value=".jsp"/>
            </bean>
        </list>
    </property>
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </list>
    </property>
</bean>

<bean id="content" class="com.foo.samples.rest.SampleContentAtomView"/>

InternalResourceViewResolver处理视图名称和 JSP 页面的翻译,而BeanNameViewResolver返回基于 bean 名称的视图。 (有关 Spring 如何查找和实例化视图的更多详细信息,请参见“ 使用 ViewResolver 界面解析视图”。)在此示例中,content bean 是从AbstractAtomFeedView继承的类,该类返回 Atom RSS feed。有关创建 Atom Feed 表示的更多信息,请参见 Atom 视图部分。

在上述配置中,如果发出带有.htmlextensions 的请求,则视图解析器将查找与text/html媒体类型匹配的视图。 InternalResourceViewResolver提供text/html的匹配视图。如果请求的文件 extensions 为.atom,则视图解析器将查找与application/atom+xml媒体类型匹配的视图。如果返回的视图名称是content,则此视图由BeanNameViewResolverMap 到SampleContentAtomView。如果使用文件 extensions.json发出请求,则无论视图名称如何,都将从DefaultViews列表中选择MappingJackson2JsonView实例。或者,可以在没有文件 extensions 但将AcceptHeaders 设置为首选媒体类型的情况下发出 Client 端请求,并且将发生对视图的相同请求分辨率。

Note

如果未明确配置`ContentNegotiatingViewResolver 的 ViewResolvers 列表,它将自动使用在应用程序上下文中定义的任何 ViewResolvers。

下面显示了相应的控制器代码,该代码为应用程序/ atom xml 的AcceptHeaders 返回格式为http://localhost/content.atomhttp://localhost/content的 URI 的 Atom RSS feed。

@Controller
public class ContentController {

    private List<SampleContent> contentList = new ArrayList<SampleContent>();

    @GetMapping("/content")
    public ModelAndView getContent() {
        ModelAndView mav = new ModelAndView();
        mav.setViewName("content");
        mav.addObject("sampleContentList", contentList);
        return mav;
    }

}

22.6 使用 Flash 属性

Flash 属性为一个请求提供了一种存储打算在另一个请求中使用的属性的方式。重定向时最常需要此功能,例如* Post/Redirect/Get *模式。 Flash 属性会在重定向之前(通常在会话中)临时保存,以便在重定向之后可供请求使用,并立即删除。

Spring MVC 有两个主要抽象来支持 Flash 属性。 FlashMap用于保存 Flash 属性,而FlashMapManager用于存储,检索和 ManagementFlashMap实例。

Flash 属性支持始终为“ on”,并且不需要显式启用,即使不使用它也不会导致 HTTP 会话创建。在每个请求上,都有一个具有从上一个请求(如果有)传递的属性的“Importing” FlashMap,和一个具有为后续请求保存的属性的“输出” FlashMap。通过RequestContextUtils中的静态方法,可以从 Spring MVC 中的任何位置访问这两个FlashMap实例。

带 Comments 的控制器通常不需要直接与FlashMap一起使用。而是@RequestMapping方法可以接受RedirectAttributes类型的参数,并使用它为重定向方案添加 Flash 属性。通过RedirectAttributes添加的 Flash 属性会自动传播到“输出” FlashMap。同样,在重定向之后,来自“Importing” FlashMap的属性会自动添加到服务于目标 URL 的控制器的Model中。

Matching requests to flash attributes

闪存属性的概念存在于许多其他 Web 框架中,并被证明有时会遇到并发问题。这是因为根据定义,闪存属性将存储到下一个请求。但是,非常“下一个”请求可能不是预期的接收者,而是另一个异步请求(例如轮询或资源请求),在这种情况下,过早删除了闪存属性。

为了减少发生此类问题的可能性,RedirectView自动使用目标重定向 URL 的路径和查询参数“标记” FlashMap实例。当查询“Importing” FlashMap时,默认值FlashMapManager会将信息与传入请求匹配。

这不能完全消除并发问题的可能性,但是通过重定向 URL 中已经可用的信息可以大大减少并发问题。因此,建议主要在重定向方案中使用 flash 属性。

22.7 构建 URI

Spring MVC 提供了一种使用UriComponentsBuilderUriComponents构建和编码 URI 的机制。

例如,您可以扩展和编码 URI 模板字符串:

UriComponents uriComponents = UriComponentsBuilder.fromUriString(
        "http://example.com/hotels/{hotel}/bookings/{booking}").build();

URI uri = uriComponents.expand("42", "21").encode().toUri();

请注意,UriComponents是不可变的,并且必要时expand()encode()操作将返回新实例。

您还可以使用单个 URI 组件进行扩展和编码:

UriComponents uriComponents = UriComponentsBuilder.newInstance()
        .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
        .expand("42", "21")
        .encode();

在 Servlet 环境中,ServletUriComponentsBuilder子类提供静态工厂方法来从 Servlet 请求中复制可用的 URL 信息:

HttpServletRequest request = ...

// Re-use host, scheme, port, path and query string
// Replace the "accountId" query param

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

或者,您可以选择将可用信息的子集复制到上下文路径,包括上下文路径:

// Re-use host, port and context path
// Append "/accounts" to the path

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

或者,如果DispatcherServlet是按名称 Map 的(例如/main/*),则还可以包含 servletMap 的 Literals 部分:

// Re-use host, port, context path
// Append the literal part of the servlet mapping to the path
// Append "/accounts" to the path

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

22.7.1 构建到控制器和方法的 URI

Spring MVC 提供了一种准备到控制器方法的链接的机制。例如,以下 MVC 控制器可以轻松地创建链接:

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

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

您可以通过按名称引用方法来准备链接:

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

URI uri = uriComponents.encode().toUri();

在上面的示例中,我们提供了实际的方法参数值,在这种情况下为 long 值 21,将用作路径变量并插入到 URL 中。此外,我们提供了值 42,以填充所有剩余的 URI 变量,例如从类型级别请求 Map 继承的“ hotel”变量。如果该方法具有更多参数,则可以为 URL 不需要的参数提供 null。通常,只有@PathVariable@RequestParam参数与构造 URL 有关。

还有其他使用MvcUriComponentsBuilder的方法。例如,您可以使用类似于代理的测试模拟技术,以避免通过名称引用控制器方法(该示例假定MvcUriComponentsBuilder.on静态导入):

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

URI uri = uriComponents.encode().toUri();

Note

当控制器方法签名可用于fromMethodCall链接创建时,其设计受到限制。除了需要适当的参数签名外,返回类型还有一个技术限制:即为链接生成器调用生成运行时代理,因此返回类型不得为final。特别是,视图名称的通用String返回类型在这里不起作用。而是使用ModelAndView或什至是普通Object(返回值为String)。

上面的示例在MvcUriComponentsBuilder中使用静态方法。在内部,它们依靠ServletUriComponentsBuilder从当前请求的方案,主机,端口,上下文路径和 servlet 路径准备基本 URL。在大多数情况下,此方法效果很好,但是有时可能不足。例如,您可能不在请求的上下文之内(例如,准备链接的批处理过程),或者您可能需要插入路径前缀(例如,从请求路径中删除并需要重新插入链接的语言环境前缀) )。

在这种情况下,您可以使用接受UriComponentsBuilder来使用基本 URL 的静态“ fromXxx”重载方法。或者,您可以使用基本 URL 创建MvcUriComponentsBuilder的实例,然后使用基于实例的“ withXxx”方法。例如:

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();

22.7.2 使用“ Forwarded”和“ X-Forwarded- *”标题

当请求通过诸如负载平衡器之类的代理进行传输时,主机,端口和方案可能会发生变化,这为需要创建到资源的链接的应用程序带来了挑战,因为链接应反映原始请求的主机,端口和方案,如 Client 角度。

RFC 7239为代理定义“转发” HTTPHeaders,以用于提供有关原始请求的信息。还使用其他非标准 Headers,例如“ X-Forwarded-Host”,“ X-Forwarded-Port”和“ X-Forwarded-Proto”。

ServletUriComponentsBuilderMvcUriComponentsBuilder都从“ Forwarded”Headers 或从“ X-Forwarded-Host”,“ X-Forwarded-Port”和“ X-Forwarded-Proto”(如果未提供)中检测,提取和使用信息当前,这样生成的链接将反映原始请求。

ForwardedHeaderFilter为整个应用程序提供了一次在全局范围内执行相同操作的替代方法。筛选器包装请求以覆盖主机,端口和方案信息,还“隐藏”任何转发的 Headers 以进行后续处理。

请注意,如 RFC 7239 的第 8 节中所述,在使用转发的 Headers 时要考虑安全性。在应用程序级别,很难确定转发的 Headers 是否可以信任。这就是为什么应正确配置上游网络以从外部过滤掉不受信任的转发报头的原因。

没有代理并且不需要使用转发 Headers 的应用程序可以将ForwardedHeaderFilter配置为删除并忽略此类 Headers。

22.7.3 从视图向控制器和方法构建 URI

您也可以从诸如 JSP,Thymeleaf,FreeMarker 之类的视图中构建到带 Comments 的控制器的链接。可以使用MvcUriComponentsBuilder中的fromMappingName方法(通过名称引用 Map)来完成此操作。

根据该类的大写字母和完整的方法名称,为每个@RequestMapping分配一个默认名称。例如,类FooController中的方法getFoo被分配了名称“ FC#getFoo”。通过创建HandlerMethodMappingNamingStrategy的实例并将其插入RequestMappingHandlerMapping,可以替换或自定义此策略。默认策略实现还会查看@RequestMapping上的 name 属性,并使用该属性(如果存在)。这意味着,如果分配的默认 Map 名称与另一个名称冲突(例如,重载方法),则可以在@RequestMapping上显式分配名称。

Note

分配的请求 Map 名称在启动时记录在 TRACE 级别。

Spring JSP 标记库提供了一个名为mvcUrl的函数,该函数可用于基于该机制准备到控制器方法的链接。

例如给出:

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

    @RequestMapping("/{country}")
    public HttpEntity getAddress(@PathVariable String country) { ... }
}

您可以按照以下步骤准备来自 JSP 的链接:

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

上面的示例依赖于 Spring 标记库(即 META-INF/spring.tld)中声明的mvcUrl JSP 函数。对于更高级的情况(例如,上一节中介绍的自定义基本 URL),可以轻松定义自己的函数或使用自定义标记文件,以便将MvcUriComponentsBuilder的特定实例与自定义基本 URL 结合使用。

22.8 使用语言环境

就像 Spring Web MVC 框架一样,Spring 体系结构的大多数部分都支持国际化。 DispatcherServlet使您可以使用 Client 端的语言环境自动解析邮件。这是通过LocaleResolver个对象完成的。

收到请求时,DispatcherServlet查找语言环境解析器,如果找到一个解析器,它将尝试使用它来设置语言环境。使用RequestContext.getLocale()方法,您始终可以检索由语言环境解析器解析的语言环境。

除了自动的语言环境解析之外,您还可以在处理程序 Map 上附加一个拦截器(有关处理程序 Map 拦截器的更多信息,请参见第 22.4.1 节“使用 HandlerInterceptor 拦截请求”)以在特定情况下(例如,基于请求中的参数)更改语言环境。

语言环境解析器和拦截器在org.springframework.web.servlet.i18n包中定义,并以常规方式在您的应用程序上下文中配置。这是 Spring 中包含的语言环境解析器的一部分。

22.8.1 获取时区信息

除了获取 Client 的语言环境外,了解他们的时区通常也很有用。 LocaleContextResolver界面提供了对LocaleResolver的扩展,该扩展允许解析程序提供更丰富的LocaleContext,其中可能包含时区信息。

如果可用,可以使用RequestContext.getTimeZone()方法获取用户的TimeZone。使用 Spring 的ConversionService注册的 Date/Time ConverterFormatter对象将自动使用时区信息。

22.8.2 AcceptHeaderLocaleResolver

此语言环境解析器检查 Client 端(例如,网络浏览器)发送的请求中的accept-languageHeaders。通常,此头字段包含 Client 端 os 的语言环境。 请注意,此解析器不支持时区信息.

22.8.3 CookieLocaleResolver

此语言环境解析器检查 Client 端上可能存在的Cookie,以查看是否指定了LocaleTimeZone。如果是这样,它将使用指定的详细信息。使用此语言环境解析器的属性,可以指定 cookie 的名称以及最长期限。在下面找到定义CookieLocaleResolver的示例。

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

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

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

</bean>

表 22.4. CookieLocaleResolver 属性

PropertyDefaultDescription
cookieName类别名称 LOCALECookie 的名称
cookieMaxAgeServlet 容器默认Cookie 在 Client 端上保持持久的最长时间。如果指定-1,则 cookie 不会保留;它仅在 Client 端关闭其浏览器之前可用。
cookiePath/将 Cookie 的可见性限制在您网站的特定部分。当指定 cookiePath 时,该 cookie 仅在该路径及其下方的路径中可见。

22.8.4 SessionLocaleResolver

SessionLocaleResolver允许您从可能与用户请求关联的会话中检索LocaleTimeZone。与CookieLocaleResolver相反,此策略将本地选择的语言环境设置存储在 Servlet 容器的HttpSession中。因此,这些设置对于每个会话来说都是临时的,因此在每个会话终止时都会丢失。

请注意,与外部会话 Management 机制(例如 Spring Session 项目)没有直接关系。该SessionLocaleResolver将仅针对当前HttpServletRequest评估并修改相应的HttpSession属性。

22.8.5 LocaleChangeInterceptor

您可以通过将LocaleChangeInterceptor添加到一个处理程序 Map 中来启用语言环境更改(请参阅第 22.4 节“处理程序 Map”)。它将检测请求中的参数并更改语言环境。它在上下文中也存在的LocaleResolver上调用setLocale()。下面的示例显示对包含参数siteLanguage的所有*.view资源的调用现在将更改语言环境。因此,例如,请求以下 URL http://www.sf.net/home.view?siteLanguage=nl会将站点语言更改为荷兰语。

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

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

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

22.9 使用主题

22.9.1 主题概述

您可以应用 Spring Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源(通常是样式表和图像)的集合,这些资源会影响应用程序的视觉样式。

22.9.2 定义主题

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

使用ResourceBundleThemeSource时,将在一个简单的属性文件中定义一个主题。属性文件列出了组成主题的资源。这是一个例子:

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

属性的键是引用视图代码中主题元素的名称。对于 JSP,通常使用spring:theme自定义标签来完成此操作,该标签与spring:message标签非常相似。以下 JSP 片段使用上一示例中定义的主题来自定义外观:

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

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

22.9.3 主题解析器

在定义主题之后,如上一节所述,您可以决定使用哪个主题。 DispatcherServlet将查找名为themeResolver的 bean,以找出要使用的ThemeResolver实现。主题解析器的工作方式与LocaleResolver大致相同。它可以检测用于特定请求的主题,还可以更改请求的主题。 Spring 提供了以下主题解析器:

表 22.5. ThemeResolver 实现

ClassDescription
FixedThemeResolver选择一个固定的主题,使用defaultThemeName属性进行设置。
SessionThemeResolver主题在用户的 HTTP 会话中维护。每个会话只需要设置一次,而在会话之间不必保留。
CookieThemeResolver所选主题存储在 Client 端的 cookie 中。

Spring 还提供了一个ThemeChangeInterceptor,它允许使用简单的请求参数对每个请求进行主题更改。

22.10 Spring 的 Multipart(文件上传)支持

22.10.1 Introduction

Spring 的内置 Multipart 支持处理 Web 应用程序中的文件上传。您可以通过org.springframework.web.multipart软件包中定义的可插入MultipartResolver对象启用此 Multipart 支持。 Spring 提供了一种MultipartResolver实现与Commons FileUpload一起使用,另一种实现与 Servlet 3.0Multipart 请求解析一起使用。

默认情况下,Spring 不执行 Multipart 处理,因为某些开发人员希望自己处理 Multipart。您可以通过将 Multipart 解析器添加到 Web 应用程序的上下文中来启用 SpringMultipart 处理。检查每个请求以查看其是否包含 Multipart。如果未找到 Multipart,则请求将按预期 continue。如果在请求中找到 Multipart,则使用在上下文中声明的MultipartResolver。之后,您请求中的 multipart 属性将被视为其他任何属性。

22.10.2 将 MultipartResolver 与 Commons FileUpload 一起使用

以下示例显示了如何使用CommonsMultipartResolver

<bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">

    <!-- one of the properties available; the maximum file size in bytes -->
    <property name="maxUploadSize" value="100000"/>

</bean>

当然,您还需要在 Classpath 中放入适当的 jar,以使 Multipart 解析器正常工作。如果是CommonsMultipartResolver,则需要使用commons-fileupload.jar

当 Spring DispatcherServlet检测到一个 Multipart 请求时,它将激活在您的上下文中声明的解析器并移交该请求。然后,解析程序将当前的HttpServletRequest包装为支持分段文件上传的MultipartHttpServletRequest。使用MultipartHttpServletRequest,您可以获得有关此请求包含的 Multipart 的信息,并实际上可以访问控制器中的 Multipart 文件本身。

22.10.3 在 Servlet 3.0 中使用 MultipartResolver

为了使用基于 Servlet 3.0 的 Multipart 解析,您需要在web.xml中用"multipart-config"部分标记DispatcherServlet,或者在编程 Servlet 注册中用javax.servlet.MultipartConfigElement标记DispatcherServlet,或者在自定义 Servlet 类的情况下,可以在 Servlet 类上标记javax.servlet.annotation.MultipartConfigComments。需要在该 Servlet 注册级别应用配置设置(例如最大大小或存储位置),因为 Servlet 3.0 不允许通过 MultipartResolver 进行这些设置。

以上述方法之一启用 Servlet 3.0Multipart 解析后,您可以将StandardServletMultipartResolver添加到 Spring 配置中:

<bean id="multipartResolver"
        class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
</bean>

22.10.4 处理表单中的文件上传

MultipartResolver完成其工作后,将像处理其他请求一样处理该请求。首先,使用文件 Importing 创建一个表单,该文件 Importing 将允许用户上载表单。编码属性(enctype="multipart/form-data")使浏览器知道如何将表单编码为 Multipart 请求:

<html>
    <head>
        <title>Upload a file please</title>
    </head>
    <body>
        <h1>Please upload a file</h1>
        <form method="post" action="/form" enctype="multipart/form-data">
            <input type="text" name="name"/>
            <input type="file" name="file"/>
            <input type="submit"/>
        </form>
    </body>
</html>

下一步是创建一个处理文件上传的控制器。该控制器与普通带 Comments 的@Controller非常相似,除了我们在方法参数中使用MultipartHttpServletRequestMultipartFile之外:

@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";
    }

}

请注意@RequestParam方法参数如何 Map 到表单中声明的 Importing 元素。在此示例中,byte[]不会执行任何操作,但是实际上您可以将其保存在数据库中,将其存储在文件系统上,依此类推。

使用 Servlet 3.0Multipart 解析时,您还可以将javax.servlet.http.Part用作 method 参数:

@Controller
public class FileUploadController {

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

        InputStream inputStream = file.getInputStream();
        // store bytes from uploaded file somewhere

        return "redirect:uploadSuccess";
    }

}

22.10.5 处理来自编程 Client 端的文件上传请求

在 RESTful 服务方案中,也可以从非浏览器 Client 端提交 Multipart 请求。以上所有示例和配置同样适用于此。但是,与通常提交文件和简单表单字段的浏览器不同,编程 Client 端还可以发送特定 Content Type 的更复杂的数据,例如带有文件的 Multipart 请求和带有 JSON 格式数据的第二部分:

POST /someUrl
Content-Type: multipart/mixed

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

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

您可以使用@RequestParam("meta-data") String metadata控制器方法参数访问名为“元数据”的部分。但是,您可能更希望在请求部分的主体中接受从 JSON 格式的数据初始化的强类型对象,这与@RequestBody借助_将非 Multipart 请求的主体转换为目标对象的方式非常相似HttpMessageConverter

为此,您可以使用@RequestPartComments 代替@RequestParamComments。考虑到 Multipart 的'Content-Type'Headers,它使您可以通过HttpMessageConverter传递特定 Multipart 的内容:

@PostMapping("/someUrl")
public String onSubmit(@RequestPart("meta-data") MetaData metadata,
        @RequestPart("file-data") MultipartFile file) {

    // ...

}

注意如何用@RequestParam@RequestPart交替访问MultipartFile方法参数。但是,在这种情况下,@RequestPart("meta-data") MetaData方法参数根据其'Content-Type'Headers 读取为 JSON 内容,并在MappingJackson2HttpMessageConverter的帮助下进行转换。

22.11 处理异常

22.11.1 HandlerExceptionResolver

Spring HandlerExceptionResolver实现处理在控制器执行期间发生的意外异常。 HandlerExceptionResolver有点类似于您可以在 Web 应用程序 Descriptorsweb.xml中定义的异常 Map。但是,它们提供了一种更灵活的方法。例如,它们提供有关引发异常时正在执行哪个处理程序的信息。此外,以编程方式处理异常的方法为您提供了更多选项,可以在请求转发到另一个 URL 之前(与使用 Servlet 特定的异常 Map 相同的最终结果)进行适当响应。

除了实现HandlerExceptionResolver接口(这仅是实现resolveException(Exception, Handler)方法并返回ModelAndView的问题)之外,您还可以使用提供的SimpleMappingExceptionResolver或创建@ExceptionHandler方法。 SimpleMappingExceptionResolver使您可以获取可能引发的任何异常的类名称,并将其 Map 到视图名称。这在功能上等效于 Servlet API 中的异常 Map 功能,但是也可以实现来自不同处理程序的异常的更细粒度的 Map。另一方面,@ExceptionHandler注解可用于应调用以处理异常的方法。这样的方法可以在@Controller类中本地定义,或者在@ControllerAdvice类中定义时可以应用于许多@Controller类。以下各节将对此进行更详细的说明。

22.11.2 @ExceptionHandler

HandlerExceptionResolver接口和SimpleMappingExceptionResolver实现使您可以声明性地将 Exception 与某些可选的 Java 逻辑一起 Map 到特定视图,然后再转发到这些视图。但是,在某些情况下,尤其是在依靠@ResponseBody方法而不是视图分辨率时,直接设置响应的状态并将可选的错误内容写入响应的主体可能更方便。

您可以使用@ExceptionHandler个方法来实现。当在控制器中声明时,此类方法适用于该控制器(或其任何子类)的@RequestMapping方法引发的异常。您还可以在@ControllerAdvice类中声明@ExceptionHandler方法,在这种情况下,它处理来自许多控制器的@RequestMapping方法的异常。以下是控制器本地@ExceptionHandler方法的示例:

@Controller
public class SimpleController {

    // ...

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

该异常可能与正在传播的顶级异常(即直接抛出IOException)相匹配,也可能与顶级包装程序异常中的直接原因(例如IllegalStateException中包装的IOException)相匹配。

对于匹配的异常类型,最好将目标异常声明为方法参数,如上所示。当多个异常方法匹配时,根源异常匹配通常比原因异常匹配更可取。更具体地说,ExceptionDepthComparator用于根据异常对引发的异常类型的深度进行排序。

或者,可以将@ExceptionHandler值设置为异常类型的数组。如果抛出与列表中的一种类型匹配的异常,则将调用以匹配的@ExceptionHandlerComments 的方法。如果未设置 Comments 值,则将使用声明的方法参数类型进行匹配。

Tip

对于@ExceptionHandler方法,在特定控制器或建议 Bean 的处理程序方法中,与仅匹配导致当前异常的原因相比,将首选与根异常匹配。但是,优先级较高的@ControllerAdvice上的原因匹配仍将优先于优先级较低的通知 Bean 上的任何匹配(无论是根源还是原因级别)。因此,在使用多建议方案时,请在具有相应 Sequences 的优先建议 Bean 上声明您的主根异常 Map!

与标有@RequestMappingComments 的标准控制器方法非常相似,方法参数和@ExceptionHandler方法的返回值可以很灵活。例如,可以在 Servlet 环境中访问HttpServletRequest,而在 Portlet 环境中可以访问PortletRequest。返回类型可以是String,它被解释为视图名称,ModelAndView对象,ResponseEntity,或者您也可以添加@ResponseBody以使方法返回值由消息转换器转换并写入响应流。

最后但并非最不重要的一点是,@ExceptionHandler方法实现可以选择通过以原始形式重新抛出异常来退出处理给定异常实例。在仅对根级别匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下,这很有用。重新抛出的异常将在其余的解析链中传播,就像给定的@ExceptionHandler方法最初没有匹配一样。

22.11.3 处理标准 Spring MVC 异常

Spring MVC 在处理请求时可能会引发许多异常。 SimpleMappingExceptionResolver可以根据需要轻松地将任何异常 Map 到默认错误视图。但是,当与以自动方式解释响应的 Client 端一起使用时,您将需要在响应上设置特定的状态代码。根据引发的异常,状态码可能指示 Client 端错误(4xx)或服务器错误(5xx)。

DefaultHandlerExceptionResolver将 Spring MVC 异常转换为特定的错误状态代码。默认情况下,它是通过 MVC 名称空间,MVC Java 配置以及DispatcherServlet(即,当不使用 MVC 名称空间或 Java 配置时)注册的。下面列出了此解析器处理的一些异常以及相应的状态码:

ExceptionHTTP 状态码
BindException400 (错误请求)
ConversionNotSupportedException500 (内部服务器错误)
HttpMediaTypeNotAcceptableException406 (不接受)
HttpMediaTypeNotSupportedException415 (不受支持的媒体类型)
HttpMessageNotReadableException400 (错误请求)
HttpMessageNotWritableException500 (内部服务器错误)
HttpRequestMethodNotSupportedException405 (不允许使用方法)
MethodArgumentNotValidException400 (错误请求)
MissingPathVariableException500 (内部服务器错误)
MissingServletRequestParameterException400 (错误请求)
MissingServletRequestPartException400 (错误请求)
NoHandlerFoundException404 (未找到)
NoSuchRequestHandlingMethodException404 (未找到)
TypeMismatchException400 (错误请求)

DefaultHandlerExceptionResolver通过设置响应状态透明地工作。但是,它会停止将任何错误内容写入响应的主体,而您的应用程序可能需要向每个错误响应添加开发人员友好的内容,例如在提供 REST API 时。您可以准备ModelAndView并通过视图分辨率来渲染错误内容,即配置ContentNegotiatingViewResolverMappingJackson2JsonView等。但是,您可能更喜欢使用@ExceptionHandler方法。

如果您更喜欢通过@ExceptionHandler方法编写错误内容,则可以扩展ResponseEntityExceptionHandler。这是@ControllerAdvice类的便利基础,提供了@ExceptionHandler方法来处理标准 Spring MVC 异常并返回ResponseEntity。这样,您就可以使用消息转换器自定义响应并写入错误内容。有关更多详细信息,请参见ResponseEntityExceptionHandler javadocs。

22.11.4 使用@ResponseStatusComments 业务异常

可以使用@ResponseStatusComments 业务异常。引发异常时,ResponseStatusExceptionResolver通过相应地设置响应的状态来处理它。默认情况下,DispatcherServlet注册ResponseStatusExceptionResolver并可供使用。

22.11.5 自定义默认的 Servlet 容器错误页面

当响应的状态设置为错误状态代码并且响应的主体为空时,Servlet 容器通常呈现 HTML 格式的错误页面。要自定义容器的默认错误页面,可以在web.xml中声明一个<error-page>元素。直到 Servlet 3,该元素都必须 Map 到特定的状态代码或异常类型。从 Servlet 3 开始,不需要 Map 错误页面,这实际上意味着指定的位置可自定义默认的 Servlet 容器错误页面。

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

请注意,错误页面的实际位置可以是 JSP 页面或容器内的其他 URL,包括通过@Controller方法处理的 URL:

写入错误信息时,可以通过控制器中的请求属性访问HttpServletResponse上设置的状态代码和错误消息:

@Controller
public class ErrorController {

    @RequestMapping(path = "/error", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    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;
    }

}

或在 JSP 中:

<%@ page contentType="application/json" pageEncoding="UTF-8"%>
{
    status:<%=request.getAttribute("javax.servlet.error.status_code") %>,
    reason:<%=request.getAttribute("javax.servlet.error.message") %>
}

22.12 网络安全

Spring Security项目提供的功能可保护 Web 应用程序免受恶意攻击。在"CSRF protection"“安全响应标题”以及“ Spring MVC 集成”的章节中查阅参考文档。请注意,并非所有功能都必需使用 Spring Security 保护应用程序安全。例如,可以通过在配置中添加CsrfFilterCsrfRequestDataValueProcessor来添加 CSRF 保护。有关示例,请参见Spring MVC 展示

另一种选择是使用专用于 Web 安全的框架。 HDIV是这样的框架,并且与 Spring MVC 集成。

22.13 关于配置支持的约定

对于许多项目,坚持既定的约定并具有合理的默认值正是他们(这些项目)所需要的,并且 Spring Web MVC 现在已经明确支持“约定之上的配置”。这意味着,如果您构建了一组命名约定等,则可以实质上减少设置处理程序 Map,视图解析器,ModelAndView实例等所需的配置量。这是一个很大的福音如果要选择快速原型,还可以在整个代码库中提供一定程度的(始终是良好的)一致性。

配置约定约定支持解决 MVC 的三个核心领域:模型,视图和控制器。

22.13.1 控制器 ControllerClassNameHandlerMapping

ControllerClassNameHandlerMapping类是HandlerMapping实现,它使用约定来确定请求 URL 和要处理这些请求的Controller实例之间的 Map。

考虑以下简单的Controller实现。请特别注意类的“名称”。

public class ViewShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // the implementation is not hugely important for this example...
    }

}

以下是相应的 Spring Web MVC 配置文件的摘录:

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

<bean id="viewShoppingCart" class="x.y.z.ViewShoppingCartController">
    <!-- inject dependencies as required... -->
</bean>

ControllerClassNameHandlerMapping查找在其应用程序上下文中定义的所有各种处理程序(或Controller)bean,并删除名称中的Controller以定义其处理程序 Map。因此,ViewShoppingCartControllerMap 到/viewshoppingcart*请求 URL。

让我们看看更多示例,以便使中心思想立即变得熟悉。 (请注意,URL 中的所有小写字母与驼峰式的Controller类名相反.)

  • WelcomeControllerMap 到/welcome*请求网址

  • HomeControllerMap 到/home*请求网址

  • IndexControllerMap 到/index*请求网址

  • RegisterControllerMap 到/register*请求网址

对于MultiActionController处理程序类,生成的 Map 稍微复杂一些。以下示例中的Controller名称假定为MultiActionController实现:

  • AdminControllerMap 到/admin/*请求网址

  • CatalogControllerMap 到/catalog/*请求网址

如果遵循将Controller实现命名为xxxController的约定,则ControllerClassNameHandlerMapping可以节省繁琐的定义和维护潜在的* looooong * SimpleUrlHandlerMapping(或类似操作)的繁琐工作。

ControllerClassNameHandlerMapping类扩展了AbstractHandlerMappingBase Class,因此您可以定义HandlerInterceptor实例以及其他所有内容,就像使用许多其他HandlerMapping实现一样。

22.13.2 模型 ModelMap(ModelAndView)

ModelMap类本质上是美化的Map,可以使要显示在View中(或上方)显示的对象遵守通用的命名约定。考虑下面的Controller实现;请注意,没有指定任何关联名称就将对象添加到ModelAndView

public class DisplayShoppingCartController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {

        List cartItems = // get a List of CartItem objects
        User user = // get the User doing the shopping

        ModelAndView mav = new ModelAndView("displayShoppingCart"); <-- the logical view name

        mav.addObject(cartItems); <-- look ma, no name, just the object
        mav.addObject(user); <-- and again ma!

        return mav;
    }
}

ModelAndView类使用ModelMap类,该类是自定义Map的实现,当将对象添加到对象时,该实现会自动为该对象生成密钥。对于标量对象(例如User),确定添加对象名称的策略是使用对象类的简短类名。以下示例是为放入ModelMap实例中的标量对象生成的名称。

  • 添加的x.y.User实例将生成名称user

  • 添加的x.y.Registration实例将生成名称registration

  • 添加的x.y.Foo实例将生成名称foo

  • 添加的java.util.HashMap实例将生成名称hashMap。在这种情况下,您可能希望明确显示名称,因为hashMap不够直观。

  • 添加null将导致抛出IllegalArgumentException。如果您要添加的一个或多个对象可能是null,那么您也将希望明确显示该名称。

What, no automatic pluralization?

Spring Web MVC 的配置约定约定不支持自动复数。也就是说,您不能将ListPerson对象添加到ModelAndView并将生成的名称为people

这个决定是经过一番辩论后做出的,最后以“最不惊奇的原则”获胜。

在添加SetList之后生成名称的策略是偷看集合,获取集合中第一个对象的简短类名,并在名称后加上List。数组也是如此,尽管对于数组而言,不必窥视数组的内容。一些示例将使集合的名称生成的语义更加清晰:

  • 添加了零个或多个x.y.User元素的x.y.User[]数组将生成名称userList

  • 添加了零个或多个x.y.User元素的x.y.Foo[]数组将生成名称fooList

  • 添加了一个或多个x.y.User元素的java.util.ArrayList将生成名称userList

  • 添加了一个或多个x.y.Foo元素的java.util.HashSet将生成名称fooList

    • empty * java.util.ArrayList根本不会添加(实际上,addObject(..)呼叫本质上是空操作)。

22.13.3 默认视图名称

没有明确提供逻辑视图名称时,RequestToViewNameTranslator接口将确定逻辑View名称。它只有一个实现,即DefaultRequestToViewNameTranslator类。

DefaultRequestToViewNameTranslator将请求 URLMap 到逻辑视图名称,如以下示例所示:

public class RegistrationController implements Controller {

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
        // process the request...
        ModelAndView mav = new ModelAndView();
        // add data as necessary to the model...
        return mav;
        // notice that no View or logical view name has been set
    }

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

    <!-- this bean with the well known name generates view names for us -->
    <bean id="viewNameTranslator"
            class="org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator"/>

    <bean class="x.y.RegistrationController">
        <!-- inject dependencies as necessary -->
    </bean>

    <!-- maps request URLs to Controller names -->
    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

请注意,在handleRequest(..)方法的实现中,如何在返回的ModelAndView上未设置View或逻辑视图名称。 DefaultRequestToViewNameTranslator的任务是根据请求的网址生成逻辑视图名称。在与ControllerClassNameHandlerMapping结合使用的上述RegistrationController的情况下,http://localhost/registration.html的请求 URL 导致DefaultRequestToViewNameTranslator生成逻辑视图名称registration。然后,该逻辑视图名称由InternalResourceViewResolver bean 解析为/WEB-INF/jsp/registration.jsp视图。

Tip

您不需要显式定义DefaultRequestToViewNameTranslator bean。如果您喜欢DefaultRequestToViewNameTranslator的默认设置,则可以使用 Spring Web MVC DispatcherServlet实例化此类的实例(如果未明确配置)。

当然,如果需要更改默认设置,则需要显式配置自己的DefaultRequestToViewNameTranslator bean。有关可配置的各种属性的详细信息,请查阅全面的DefaultRequestToViewNameTranslator javadocs。

22.14 HTTP 缓存支持

好的 HTTP 缓存策略可以显着提高 Web 应用程序的性能及其 Client 端的体验。 'Cache-Control' HTTP 响应 Headers 和诸如'Last-Modified''ETag'之类的条件 Headers 主要负责此工作。

'Cache-Control' HTTP 响应 Headers 建议私有缓存(例如浏览器)和公共缓存(例如代理)如何缓存 HTTP 响应以供进一步重用。

ETag(实体标记)是由 HTTP/1.1 兼容的 Web 服务器返回的 HTTP 响应 Headers,用于确定给定 URL 的内容更改。可以认为它是Last-ModifiedHeaders 的更复杂的后继者。当服务器返回带有 ETag 头的表示形式时,Client 端可以在If-None-Match头的后续 GET 中使用此头。如果内容未更改,则服务器返回304: Not Modified

本节描述了可用于在 Spring Web MVC 应用程序中配置 HTTP 缓存的不同选择。

22.14.1 缓存控制 HTTPHeaders

Spring Web MVC 支持许多用例和为应用程序配置“缓存控制”Headers 的方法。 RFC 7234 第 5.2.2 节完全描述了该 Headers 及其可能的指令,但是有几种方法可以解决最常见的情况。

Spring Web MVC 在其多个 API 中使用配置约定:setCachePeriod(int seconds)

  • -1的值不会生成'Cache-Control'响应头。

  • 0值将阻止使用'Cache-Control: no-store'指令进行缓存。

  • n > 0值将使用'Cache-Control: max-age=n'指令将给定的响应缓存n秒。

CacheControl构建器类仅描述了可用的“ Cache-Control”指令,并使构建自己的 HTTP 缓存策略更加容易。一旦构建,就可以在多个 Spring Web MVC API 中将CacheControl实例作为参数接受。

// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS)
                                    .noTransform().cachePublic();

22.14.2 对静态资源的 HTTP 缓存支持

静态资源应使用适当的'Cache-Control'和条件 Headers 提供服务,以实现最佳性能。 配置 ResourceHttpRequestHandler用于提供静态资源,不仅可以通过读取文件的元数据来本地写入'Last-Modified'头,而且如果配置正确也可以写入'Cache-Control'头。

您可以在ResourceHttpRequestHandler上设置cachePeriod属性或使用CacheControl实例,该实例支持更具体的指令:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public-resources/")
                .setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic());
    }

}

在 XML 中:

<mvc:resources mapping="/resources/**" location="/public-resources/">
    <mvc:cache-control max-age="3600" cache-public="true"/>
</mvc:resources>

22.14.3 支持控制器中的 Cache-Control,ETag 和 Last-Modified 响应 Headers

控制器可以支持'Cache-Control''ETag'和/或'If-Modified-Since' HTTP 请求;如果要在响应上设置'Cache-Control'Headers,则确实建议这样做。这涉及计算给定请求的 lastModified long和/或 Etag 值,将其与'If-Modified-Since'请求 Headers 值进行比较,并可能返回状态码为 304(未修改)的响应。

名为“使用 HttpEntity”的部分中所述,控制器可以使用HttpEntity类型与请求/响应进行交互。返回ResponseEntity的控制器可以在响应中包括 HTTP 缓存信息,如下所示:

@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);
}

这样做不仅会在响应中包含'ETag''Cache-Control'Headers,而且还会 将响应转换为空主体的HTTP 304 Not Modified响应 ,如果 Client 端发送的条件 Headers 与 Controller 设置的缓存信息匹配。

@RequestMapping方法也可能希望支持相同的行为。这可以通过以下方式实现:

@RequestMapping
public String myHandleMethod(WebRequest webRequest, Model model) {

    long lastModified = // 1. application-specific calculation

    if (request.checkNotModified(lastModified)) {
        // 2. shortcut exit - no further processing necessary
        return null;
    }

    // 3. or otherwise further request processing, actually preparing content
    model.addAttribute(...);
    return "myViewName";
}

这里有两个关键元素:调用request.checkNotModified(lastModified)和返回null。前者在返回true之前设置适当的响应状态和 Headers。后者与前者的结合使 Spring MVC 不对请求进行进一步处理。

请注意,有 3 种变体:

  • request.checkNotModified(lastModified)将 lastModified 与'If-Modified-Since''If-Unmodified-Since'请求 Headers 进行比较

  • request.checkNotModified(eTag)将 eTag 与'If-None-Match'请求 Headers 进行比较

  • request.checkNotModified(eTag, lastModified)都执行,这意味着两个条件都应有效

收到有条件的'GET'/'HEAD'请求时,checkNotModified将检查资源是否尚未修改,如果已修改,则将导致HTTP 304 Not Modified响应。在有条件'POST'/'PUT'/'DELETE'请求的情况下,checkNotModified将检查该资源是否尚未修改,如果已经修改,它将导致HTTP 409 Precondition Failed响应以防止并发修改。

22.14.4 浅 ETag 支持

Servlet 过滤器ShallowEtagHeaderFilter提供对 ETag 的支持。它是一个普通的 Servlet 过滤器,因此可以与任何 Web 框架结合使用。 ShallowEtagHeaderFilter过滤器创建所谓的浅 ETag(与深 ETag 相对,稍后再详细介绍)。该过滤器缓存呈现的 JSP 的内容(或其他内容),在其之上生成 MD5 哈希值,并将其作为 ETag 返回响应中的 Headers。Client 端下一次发送对相同资源的请求时,它将使用该哈希作为If-None-Match值。筛选器检测到此情况,再次渲染视图,然后比较两个哈希。如果它们相等,则返回304

请注意,此策略可节省网络带宽,但不能节省 CPU,因为必须为每个请求计算完整响应。控制器级别的其他策略(如上所述)可以节省网络带宽并避免计算。

该过滤器具有writeWeakETag参数,该参数将过滤器配置为写入弱 ETag,例如W/"02a2d595e6ed9a0b24f027f2b63b134d6",如RFC 7232 第 2.3 节中所定义。

您在web.xml中配置ShallowEtagHeaderFilter

<filter>
    <filter-name>etagFilter</filter-name>
    <filter-class>org.springframework.web.filter.ShallowEtagHeaderFilter</filter-class>
    <!-- Optional parameter that configures the filter to write weak ETags
    <init-param>
        <param-name>writeWeakETag</param-name>
        <param-value>true</param-value>
    </init-param>
    -->
</filter>

<filter-mapping>
    <filter-name>etagFilter</filter-name>
    <servlet-name>petclinic</servlet-name>
</filter-mapping>

或在 Servlet 3.0 环境中,

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new ShallowEtagHeaderFilter() };
    }

}

有关更多详细信息,请参见第 22.15 节“基于代码的 Servlet 容器初始化”

22.15 基于代码的 Servlet 容器初始化

在 Servlet 3.0 环境中,您可以选择以编程方式配置 Servlet 容器,以替代方式或与web.xml文件结合使用。以下是注册DispatcherServlet的示例:

import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
        registration.setLoadOnStartup(1);
        registration.addMapping("/");
    }

}

WebApplicationInitializer是 Spring MVC 提供的接口,可确保检测到您的实现并将其自动用于初始化任何 Servlet 3 容器。名为AbstractDispatcherServletInitializerWebApplicationInitializer的抽象 Base Class 实现通过简单地重写指定 servletMap 和DispatcherServlet配置的位置的方法,使DispatcherServlet的注册更加容易。

对于使用基于 Java 的 Spring 配置的应用程序,建议这样做:

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

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

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

如果使用基于 XML 的 Spring 配置,则应直接从AbstractDispatcherServletInitializer扩展:

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    @Override
    protected WebApplicationContext createServletApplicationContext() {
        XmlWebApplicationContext cxt = new XmlWebApplicationContext();
        cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
        return cxt;
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

}

AbstractDispatcherServletInitializer还提供了一种方便的方法来添加Filter个实例,并将它们自动 Map 到DispatcherServlet

public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

    // ...

    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
    }

}

每个过滤器都会根据其具体类型添加一个默认名称,并自动 Map 到DispatcherServlet

AbstractDispatcherServletInitializer的受isAsyncSupported保护的方法提供了一个位置,以对DispatcherServlet及其 Map 的所有过滤器启用异步支持。默认情况下,此标志设置为true

最后,如果您需要进一步自定义DispatcherServlet本身,则可以覆盖createDispatcherServlet方法。

22.16 配置 Spring MVC

第 22.2.1 节“ WebApplicationContext 中的特殊 Bean 类型”第 22.2.2 节“默认的 DispatcherServlet 配置”解释了有关 Spring MVC 的特殊 bean 以及DispatcherServlet使用的默认实现。在本节中,您将学习配置 Spring MVC 的两种其他方法。即 MVC Java 配置和 MVC XML 名称空间。

MVC Java 配置和 MVC 名称空间提供了类似的默认配置,该配置覆盖了DispatcherServlet默认值。目标是使大多数应用程序不必创建相同的配置,并且还提供了用于配置 Spring MVC 的更高级别的构造,这些构造只是一个简单的起点,几乎不需要或不需要基础配置。

您可以根据自己的喜好选择 MVC Java 配置或 MVC 名称空间。另外,正如您将在下面看到的那样,使用 MVC Java 配置,可以更轻松地查看基础配置以及直接对创建的 Spring MVC Bean 进行细粒度的自定义。但是,让我们从头开始。

22.16.1 启用 MVC Java Config 或 MVC XML 命名空间

要启用 MVC Java 配置,请将 Comments@EnableWebMvc添加到您的@Configuration类之一:

@Configuration
@EnableWebMvc
public class WebConfig {
}

要在 XML 中实现相同目的,请在 DispatcherServlet 上下文中(如果未定义 DispatcherServlet 上下文,则在根上下文中)使用mvc:annotation-driven元素:

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

</beans>

上面注册了RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver(以及其他),以支持使用带有 Comments 的控制器方法(例如@RequestMapping@ExceptionHandler等)处理请求。

它还启用以下功能:

  • 除了用于数据绑定的 JavaBeans PropertyEditor 之外,还通过ConversionService实例进行 Spring 3 样式类型转换。

  • 通过ConversionService使用@NumberFormatComments 支持formatting数字字段。

  • 使用@DateTimeFormat注解支持formatting DateCalendarLong和 Joda-Time 字段。

  • 如果 Classpath 中存在 JSR-303 提供程序,则支持@Valid validating @ControllerImporting。

  • HttpMessageConverter支持@RequestBody方法参数和@ResponseBody方法从@RequestMapping@ExceptionHandler方法返回的值。

这是由 mvc:annotation-driven 驱动设置的 HttpMessageConverters 的完整列表:

  • ByteArrayHttpMessageConverter转换字节数组。

    • StringHttpMessageConverter转换字符串。

    • 对于所有媒体类型,ResourceHttpMessageConverterorg.springframework.core.io.Resource相互转换。

    • SourceHttpMessageConverterjavax.xml.transform.Source相互转换。

    • FormHttpMessageConverter将表单数据与MultiValueMap<String, String>相互转换。

    • Jaxb2RootElementHttpMessageConverter将 Java 对象与 XML 对象进行 XML 转换,如果存在 JAXB2 且 Classpath 中不存在 Jackson 2 XML 扩展,则添加。

    • MappingJackson2HttpMessageConverter转换为 JSON 或从 JSON 转换为 JSON-如果 Classpath 中存在 Jackson 2,则添加。

    • MappingJackson2XmlHttpMessageConverter转换为 XML 或从 XML 转换为 XML-如果 Classpath 中存在Jackson 2 XML 扩展,则添加。

    • AtomFeedHttpMessageConverter转换 Atom 供稿-如果 Classpath 中存在罗马,则将其添加。

    • RssChannelHttpMessageConverter转换 RSS 源-如果 Classpath 中存在罗马,则添加。

有关如何自定义这些默认转换器的更多信息,请参见第 22.16.12 节,“消息转换器”

Note

使用Jackson2ObjectMapperBuilder创建的ObjectMapper实例创建 Jackson JSON 和 XML 转换器,以提供更好的默认配置。

该构建器使用以下属性自定义 Jackson 的默认属性:

如果在 Classpath 中检测到以下知名模块,它还将自动注册以下知名模块:

22.16.2 定制提供的配置

要自定义 Java 中的默认配置,您只需实现WebMvcConfigurer接口,或者更可能扩展WebMvcConfigurerAdapter类并覆盖所需的方法:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // Override configuration methods...
}

要自定义<mvc:annotation-driven/>的默认配置,请检查其支持哪些属性和子元素。您可以查看Spring MVC XML 模式或使用 IDE 的代码完成功能来发现可用的属性和子元素。

22.16.3 转换和格式化

默认情况下,会安装NumberDate类型的格式化程序,包括对@NumberFormat@DateTimeFormat注解的支持。如果 Classpath 中存在 Joda-Time,则还将安装对 Joda-Time 格式库的完全支持。要注册自定义格式器和转换器,请覆盖addFormatters方法:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }
}

在 MVC 名称空间中,添加<mvc:annotation-driven>时将应用相同的默认值。要注册自定义格式器和转换器,只需提供ConversionService

<?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
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven conversion-service="conversionService"/>

    <bean id="conversionService"
            class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="org.example.MyConverter"/>
            </set>
        </property>
        <property name="formatters">
            <set>
                <bean class="org.example.MyFormatter"/>
                <bean class="org.example.MyAnnotationFormatterFactory"/>
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.example.MyFormatterRegistrar"/>
            </set>
        </property>
    </bean>

</beans>

Note

有关何时使用 FormatterRegistrars 的更多信息,请参见第 9.6.4 节“ FormatterRegistrar SPI”FormattingConversionServiceFactoryBean

22.16.4 Validation

Spring 提供了一个Validator interface,可用于在应用程序的所有层中进行验证。在 Spring MVC 中,您可以将其配置为用作全局Validator实例,以便在遇到@Valid@Validated控制器方法参数时使用,和/或通过@InitBinder方法在控制器中用作本地Validator。可以将全局验证器实例和本地验证器实例组合在一起以提供复合验证。

Spring 还通过LocalValidatorFactoryBean supports JSR-303/JSR-349 Bean 验证,该修改使 Spring org.springframework.validation.Validator接口适应 Bean Validation javax.validation.ValidatorContract。可以将此类作为全局验证器插入 Spring MVC 中,如下所述。

默认情况下,当在 Classpath 中检测到诸如 Hibernate Validator 之类的 Bean 验证提供程序时,使用@EnableWebMvc<mvc:annotation-driven>会通过LocalValidatorFactoryBean在 Spring MVC 中自动注册 Bean 验证支持。

Note

有时将LocalValidatorFactoryBean注入到控制器或其他类中很方便。最简单的方法是声明自己的@Bean并用@Primary标记,以避免与 MVC Java 配置提供的冲突。

如果您更喜欢使用 MVC Java 配置中的那个,则需要从WebMvcConfigurationSupport覆盖mvcValidator方法,并声明该方法以显式返回LocalValidatorFactory而不是Validator。有关如何切换以扩展提供的配置的信息,请参见第 22.16.13 节,“使用 MVC Java Config 进行高级自定义”

或者,您可以配置自己的全局Validator实例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public Validator getValidator(); {
        // return "global" validator
    }
}

并使用 XML:

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

    <mvc:annotation-driven validator="globalValidator"/>

</beans>

要将全局验证与本地验证结合起来,只需添加一个或多个本地验证器即可:

@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}

通过这种最小配置,每当遇到@Valid@Validated方法参数时,配置的验证器将对其进行验证。任何验证冲突都会自动在BindingResult中显示为错误,可以作为方法参数访问,也可以在 Spring MVC HTML 视图中呈现。

22.16.5 Interceptors

您可以将HandlerInterceptorsWebRequestInterceptors配置为应用于所有传入请求或限制为特定的 URL 路径模式。

在 Java 中注册拦截器的示例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LocaleInterceptor());
        registry.addInterceptor(new ThemeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");
        registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");
    }

}

在 XML 中,使用<mvc:interceptors>元素:

<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>

22.16.6 内容协商

您可以配置 Spring MVC 如何根据请求确定请求的媒体类型。可用选项包括检查文件 extensions 的 URL 路径,检查“ Accept”Headers,特定的查询参数,或在什么都不要求的情况下使用默认 Content Type。默认情况下,首先检查请求 URI 中的路径扩展,然后检查“ Accept”Headers。

如果相应的依赖项在 Classpath 上,则默认情况下,MVC Java 配置和 MVC 名称空间寄存器jsonxmlrssatom。附加的路径扩展到媒体类型的 Map 也可以显式注册,并且也可以将它们白名单作为安全扩展,以进行 RFD 攻击检测(请参阅“后缀模式匹配和 RFD”部分)。

以下是通过 MVC Java 配置自定义内容协商选项的示例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.mediaType("json", MediaType.APPLICATION_JSON);
    }
}

在 MVC 命名空间中,<mvc:annotation-driven>元素具有content-negotiation-manager属性,该属性期望ContentNegotiationManager可以依次使用ContentNegotiationManagerFactoryBean创建:

<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>

如果未使用 MVC Java 配置或 MVC 名称空间,则需要创建ContentNegotiationManager的实例,并使用它来配置RequestMappingHandlerMapping以便进行请求 Map,并使用RequestMappingHandlerAdapterExceptionHandlerExceptionResolver进行内容协商。

请注意,现在ContentNegotiatingViewResolver也可以使用ContentNegotiationManager进行配置,因此您可以在整个 Spring MVC 中使用一个共享实例。

在更高级的情况下,配置多个ContentNegotiationManager实例可能又会有用,这些实例又可能包含自定义ContentNegotiationStrategy实现。例如,您可以将ExceptionHandlerExceptionResolverContentNegotiationManager配置为始终将请求的媒体类型解析为"application/json"。或者,您可能希望插入自定义策略,该策略具有某种逻辑,可以在不需要任何 Content Type 的情况下选择默认 Content Type(例如 XML 或 JSON)。

22.16.7 视图控制器

这是定义被调用时立即转发到视图的ParameterizableViewController的快捷方式。在视图生成响应之前没有 Java 控制器逻辑要执行的静态情况下,请使用它。

将对"/"的请求转发到 Java 中称为"home"的视图的示例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("home");
    }
}

在 XML 中也使用<mvc:view-controller>元素:

<mvc:view-controller path="/" view-name="home"/>

22.16.8 视图解析器

MVC 配置简化了视图解析器的注册。

以下是一个 Java 配置示例,该示例使用 FreeMarker HTML 模板和 Jackson 作为 JSON 呈现的默认View配置内容协商视图的分辨率:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.jsp();
    }
}

与 XML 相同:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:jsp/>
</mvc:view-resolvers>

但是请注意,FreeMarker,Velocity,Tiles,Groovy 标记和脚本模板也需要配置基础视图技术。

MVC 命名空间提供了专用元素。例如,使用 FreeMarker:

<mvc:view-resolvers>
    <mvc:content-negotiation>
        <mvc:default-views>
            <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
        </mvc:default-views>
    </mvc:content-negotiation>
    <mvc:freemarker cache="false"/>
</mvc:view-resolvers>

<mvc:freemarker-configurer>
    <mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>

在 Java config 中,只需添加相应的“ Configurer” Bean:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.enableContentNegotiation(new MappingJackson2JsonView());
        registry.freeMarker().cache(false);
    }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("/WEB-INF/");
        return configurer;
    }
}

22.16.9 服务资源

此选项允许ResourceHttpRequestHandlerResource位置列表中的任何一个满足特定 URL 模式的静态资源请求。这提供了一种从 Web 应用程序根目录以外的位置(包括 Classpath 上的位置)提供静态资源的便捷方法。 cache-period属性可用于设置远期到期 Headers(1 年是诸如 Page Speed 和 YSlow 之类的优化工具的建议),以便 Client 端可以更有效地利用它们。处理程序还正确评估Last-ModifiedHeaders(如果存在),以便适当地返回304状态代码,从而避免了 Client 端已经缓存的资源的不必要开销。例如,要从 Web 应用程序根目录中的public-resources目录提供 URL 模式为/resources/**的资源请求,请使用:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/");
    }

}

与 XML 相同:

<mvc:resources mapping="/resources/**" location="/public-resources/"/>

要为这些资源提供 1 年的到期期限,以确保最大程度地利用浏览器缓存并减少浏览器发出的 HTTP 请求,请执行以下操作:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations("/public-resources/").setCachePeriod(31556926);
    }

}

在 XML 中:

<mvc:resources mapping="/resources/**" location="/public-resources/" cache-period="31556926"/>

有关更多详细信息,请参见对静态资源的 HTTP 缓存支持

mapping属性必须是SimpleUrlHandlerMapping可以使用的 Ant 模式,而location属性必须指定一个或多个有效资源目录位置。可以使用逗号分隔的值列表来指定多个资源位置。将以指定的 Sequences 检查指定的位置,以确定是否存在任何给定请求的资源。例如,要启用从 Web 应用程序根目录和 Classpath 上任何 jar 中的/META-INF/public-web-resources/的已知路径提供资源,请使用:

@EnableWebMvc
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/", "classpath:/META-INF/public-web-resources/");
    }

}

在 XML 中:

<mvc:resources mapping="/resources/**" location="/, classpath:/META-INF/public-web-resources/"/>

当提供在部署新版本的应用程序时可能会更改的资源时,建议您将版本字符串合并到用于请求资源的 Map 模式中,以便您可以强制 Client 端请求新部署的应用程序资源版本。框架内置了对版本化 URL 的支持,可以通过在资源处理程序上配置资源链来启用。该链包含一个以上的ResourceResolver个实例,然后是一个或多个的ResourceTransformer个实例。它们一起可以提供任意的分辨率和资源转换。

内置的VersionResourceResolver可以配置为不同的策略。例如FixedVersionStrategy可以使用属性,日期或其他作为版本。 ContentVersionStrategy使用从资源的内容(称为“指纹” URL)计算出的 MD5 哈希值。请注意,VersionResourceResolver在提供资源时将自动使用解析的版本字符串作为 HTTP ETagHeaders 值。

ContentVersionStrategy是一个很好的默认选择,除非无法使用(例如,使用 JavaScript 模块加载程序)。您可以针对不同的模式配置不同的版本策略,如下所示。还请记住,计算基于内容的版本非常昂贵,因此应在 Producing 启用资源链缓存。

Java 配置示例;

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public-resources/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
}

XML example:

<mvc:resources mapping="/resources/**" location="/public-resources/">
	<mvc:resource-chain>
		<mvc:resource-cache/>
		<mvc:resolvers>
			<mvc:version-resolver>
				<mvc:content-version-strategy patterns="/**"/>
			</mvc:version-resolver>
		</mvc:resolvers>
	</mvc:resource-chain>
</mvc:resources>

为了使上述功能起作用,应用程序还必须使用版本来呈现 URL。最简单的方法是配置ResourceUrlEncodingFilter,它包装响应并覆盖其encodeURL方法。这将在 JSP,FreeMarker,Velocity 和调用 responseencodeURL方法的任何其他视图技术中运行。或者,应用程序也可以直接注入和使用ResourceUrlProvider bean,该 bean 是通过 MVC Java 配置和 MVC 名称空间自动声明的。

WebJarsResourceResolver也支持 Webjar,当"org.webjars:webjars-locator"库位于 Classpath 上时,它将自动注册。此解析器允许资源链从 HTTP GET 请求解析版本不可知库"GET /jquery/jquery.min.js"将返回资源"/jquery/1.2.0/jquery.min.js"。它还可以通过重写模板<script src="/jquery/jquery.min.js"/> → <script src="/jquery/1.2.0/jquery.min.js"/>中的资源 URL 来工作。

22.16.10 默认 Servlet

这允许将DispatcherServletMap 到“ /”(从而覆盖了容器默认 Servlet 的 Map),同时仍允许容器默认 Servlet 处理静态资源请求。它将DefaultServletHttpRequestHandler配置为具有“/**”的 URLMap,并且相对于其他 URLMap 具有最低的优先级。

该处理程序会将所有请求转发到默认 Servlet。因此,重要的是,它应以所有其他 URL HandlerMappings的 Sequences 保留在最后。如果您使用<mvc:annotation-driven>,或者您要设置自己的自定义HandlerMapping实例,情况一定会如此,请确保将其order属性设置为小于DefaultServletHttpRequestHandler的值Integer.MAX_VALUE

要使用默认设置启用功能,请使用:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

或使用 XML:

<mvc:default-servlet-handler/>

覆盖“ /” ServletMap 的警告是,必须通过名称而不是通过路径来检索默认 Servlet 的RequestDispatcherDefaultServletHttpRequestHandler将使用大多数主要 Servlet 容器(包括 Tomcat,Jetty,GlassFish,JBoss,Resin,WebLogic 和 WebSphere)的已知名称列表,尝试在启动时自动检测容器的默认 Servlet。如果已使用其他名称自定义配置了默认 Servlet,或者在默认 Servlet 名称未知的情况下使用了不同的 Servlet 容器,则必须明确提供默认 Servlet 的名称,如以下示例所示:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable("myCustomDefaultServlet");
    }

}

或使用 XML:

<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>

22.16.11 路径匹配

这允许自定义与 URLMap 和路径匹配有关的各种设置。有关各个选项的详细信息,请查看PathMatchConfigurer API。

以下是 Java 配置中的示例:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseSuffixPatternMatch(true)
            .setUseTrailingSlashMatch(false)
            .setUseRegisteredSuffixPatternMatch(true)
            .setPathMatcher(antPathMatcher())
            .setUrlPathHelper(urlPathHelper());
    }

    @Bean
    public UrlPathHelper urlPathHelper() {
        //...
    }

    @Bean
    public PathMatcher antPathMatcher() {
        //...
    }

}

与 XML 相同,请使用<mvc:path-matching>元素:

<mvc:annotation-driven>
    <mvc:path-matching
        suffix-pattern="true"
        trailing-slash="false"
        registered-suffixes-only="true"
        path-helper="pathHelper"
        path-matcher="pathMatcher"/>
</mvc:annotation-driven>

<bean id="pathHelper" class="org.example.app.MyPathHelper"/>
<bean id="pathMatcher" class="org.example.app.MyPathMatcher"/>

22.16.12 消息转换器

可以在 Java 配置中通过覆盖configureMessageConverters()来实现HttpMessageConverter的自定义,如果要替换 Spring MVC 创建的默认转换器,或者通过覆盖extendMessageConverters()如果只想对其进行自定义或向默认转换器添加其他转换器,则可以通过自定义HttpMessageConverter实现。

以下是添加自定义ObjectMapper而不是默认ObjectMapper的 Jackson JSON 和 XML 转换器的示例:

@Configuration
@EnableWebMvc
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @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.xml().build()));
    }

}

在此示例中,使用Jackson2ObjectMapperBuilder为启用了缩进的MappingJackson2HttpMessageConverterMappingJackson2XmlHttpMessageConverter创建通用配置,自定义的日期格式和jackson-module-parameter-names的注册,从而增加了对访问参数名称的支持(在 Java 8 中添加了功能)。

Note

启用具有 Jackson XML 支持的缩进除了jackson-dataformat-xml个之外还需要woodstox-core-asl个依赖。

其他有趣的 Jackson 模块也可用:

也可以在 XML 中执行相同的操作:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper" ref="objectMapper"/>
        </bean>
        <bean class="org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter">
            <property name="objectMapper" ref="xmlMapper"/>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"
      p:indentOutput="true"
      p:simpleDateFormat="yyyy-MM-dd"
      p:modulesToInstall="com.fasterxml.jackson.module.paramnames.ParameterNamesModule"/>

<bean id="xmlMapper" parent="objectMapper" p:createXmlMapper="true"/>

22.16.13 使用 MVC Java Config 进行高级自定义

从上面的示例中可以看到,MVC Java 配置和 MVC 名称空间提供了更高级别的构造,这些构造不需要对为您创建的基础 bean 的深入了解。相反,它可以帮助您专注于您的应用程序需求。但是,有时您可能需要更细粒度的控制,或者您可能只是希望了解底层配置。

进行更细粒度控制的第一步是查看为您创建的基础 bean。在 MVC Java 配置中,您可以在WebMvcConfigurationSupport中看到 javadocs 和@Bean方法。此类中的配置通过@EnableWebMvcComments 自动导入。实际上,如果您打开@EnableWebMvc,则可以看到@Import语句。

进行更细粒度控制的下一步是在WebMvcConfigurationSupport中创建的一个 bean 上自定义属性,或者可能提供您自己的实例。这需要两件事-删除@EnableWebMvcComments,以防止导入,然后从WebMvcConfigurationSupport的子类DelegatingWebMvcConfiguration扩展。这是一个例子:

@Configuration
public class WebConfig extends DelegatingWebMvcConfiguration {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        // ...
    }

    @Override
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        // Create or let "super" create the adapter
        // Then customize one of its properties
    }

}

Note

一个应用程序应该只有一个扩展DelegatingWebMvcConfiguration或单个@EnableWebMvc带 Comments 类的配置,因为它们都注册了相同的基础 bean。

以这种方式修改 bean 不会阻止您使用本节前面显示的任何更高级别的构造。 WebMvcConfigurerAdapter子类和WebMvcConfigurer实现仍在使用。

MVC 命名空间的高级自定义 22.16.14

使用 MVC 名称空间,对为您创建的配置进行细粒度的控制会比较困难。

如果确实需要这样做,而不是复制它提供的配置,请考虑配置一个BeanPostProcessor来检测要按类型定制的 bean,然后根据需要修改其属性。例如:

@Component
public class MyPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
        if (bean instanceof RequestMappingHandlerAdapter) {
            // Modify properties of the adapter
        }
    }

}

请注意,MyPostProcessor必须包含在<component scan/>中才能被检测到,或者,如果您愿意,可以使用 XML bean 声明显式声明它。