Appearance
Spring Web Servlet:MVC - Part1
Tip:基于 Spring Core 5.3.30 版本。
Spring Web MVC 是最初基于 Servlet API 构建的 Web 框架,从 Spring Framework 的一开始就已经包含在内。正式的名称,“Spring Web MVC”,来源于它的源模块(spring-webmvc),但它更常被称为 “Spring MVC”。
与 Spring Web MVC 平行,Spring Framework 5.0 引入了一个响应式堆栈 Web 框架,其名称,“Spring WebFlux”,也是基于其源模块(spring-webflux)。本章介绍 Spring Web MVC。下一章将介绍 Spring WebFlux。
有关 Servlet 容器和 Java EE 版本范围的基线信息和兼容性,请参阅 Spring Framework Wiki。
1. DispatcherServlet
和许多其他 Web 框架一样,Spring MVC 是围绕前端控制器模式设计的,其中一个中心 Servlet,即 DispatcherServlet,为请求处理提供了共享的算法,而实际的工作则由可配置的委托组件执行。这种模型灵活且支持多种工作流。
DispatcherServlet,就像任何 Servlet 一样,需要根据 Servlet 规范使用 Java 配置或在 web.xml 中声明和映射。反过来,DispatcherServlet 使用 Spring 配置来发现它需要的委托组件,用于请求映射、视图解析、异常处理等。
以下 Java 配置示例注册并初始化了 DispatcherServlet,它会被 Servlet 容器自动检测(参见 Servlet 配置):
Java
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Note:除了直接使用
ServletContextAPI,你还可以扩展AbstractAnnotationConfigDispatcherServletInitializer并重写特定的方法(参见上下文层次结构下的示例)。
Note:对于编程用例,
GenericWebApplicationContext可以作为AnnotationConfigWebApplicationContext的替代品。详细信息请参见GenericWebApplicationContextJavaDoc。
以下 web.xml 配置示例注册并初始化了 DispatcherServlet:
XML
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Note:Spring Boot 遵循不同的初始化顺序。Spring Boot 使用 Spring 配置来引导自身和嵌入式 Servlet 容器,而不是挂钩到 Servlet 容器的生命周期。在 Spring 配置中检测到的 Filter 和 Servlet 声明会被注册到 Servlet 容器中。更多详细信息,请参阅 Spring Boot 文档。
1.1. 上下文层次结构
DispatcherServlet 期望一个 WebApplicationContext(一个普通 ApplicationContext 的扩展)来进行自身的配置。WebApplicationContext 有一个与 ServletContext 和与之相关的 Servlet 的链接。它也绑定到 ServletContext,这样应用程序可以在需要访问它时使用 RequestContextUtils 上的静态方法来查找 WebApplicationContext。
对于许多应用程序,拥有一个单一的 WebApplicationContext 是简单且足够的。也可以有一个上下文层次结构,其中一个根 WebApplicationContext 被多个 DispatcherServlet(或其他 Servlet)实例共享,每个实例都有自己的子 WebApplicationContext 配置。有关上下文层次结构特性的更多信息,请参见 ApplicationContext 的附加功能。
根 WebApplicationContext 通常包含基础设施 Bean,如需要在多个 Servlet 实例之间共享的数据仓库和业务服务。这些 Bean 实际上是被继承的,并可以在特定 Servlet 的子 WebApplicationContext 中被覆盖(即,重新声明),后者通常包含给定 Servlet 的本地 Bean。以下图片显示了这种关系:

WebApplicationContext 与特定 Servlet 的子 WebApplicationContext 之间的关系以下示例配置了一个 WebApplicationContext 的层次结构:
Java
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Note:如果不需要应用程序上下文层次结构,应用程序可以通过
getRootConfigClasses()返回所有配置,并从getServletConfigClasses()返回null。
以下示例显示了等效的 web.xml:
XML
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Note:如果不需要应用程序上下文层次结构,应用程序可以只配置一个 “根” 上下文,并将 Servlet 参数
contextConfigLocation留空。
1.2. 特殊的 Bean 类型
DispatcherServlet 委托给特殊的 Bean 来处理请求并呈现适当的响应。所谓的 “特殊 Bean”,我们是指实现了框架契约的 Spring 管理的对象实例。这些通常带有内置契约,但你可以自定义它们的属性,并扩展或替换它们。
下表列出了 DispatcherServlet 能够检测到的特殊 Bean:
| Bean 类型 | 说明 |
|---|---|
HandlerMapping | 将请求映射到处理器,并附带一系列用于预处理和后处理的拦截器。映射基于一些标准,具体细节根据 HandlerMapping 的实现有所不同。两个主要的 HandlerMapping 实现是 RequestMappingHandlerMapping(支持 @RequestMapping 注解的方法)和 SimpleUrlHandlerMapping(维护 URI 路径模式到处理器的明确注册)。 |
HandlerAdapter | 帮助 DispatcherServlet 调用映射到请求的处理器,无论处理器实际如何被调用。例如,调用一个注解控制器需要解析注解。HandlerAdapter 的主要目的是屏蔽 DispatcherServlet 的这些细节。 |
HandlerExceptionResolver | 解决异常的策略,可能将它们映射到处理器,映射到 HTML 错误视图,或者其他目标。参见异常。 |
ViewResolver | 将处理器返回的基于字符串的逻辑视图名称解析为实际的 View,以便渲染到响应。参见视图解析和视图技术。 |
LocaleResolver、LocaleContextResolver | 解析客户端正在使用的 Locale,可能还有他们的时区,以便能够提供国际化的视图。参见 Locale。 |
ThemeResolver | 解析你的 Web 应用程序可以使用的主题 —— 例如,提供个性化布局。参见主题。 |
MultipartResolver | 用于解析 Multipart 请求(例如,浏览器表单文件上传)的抽象,借助某些 Multipart 解析库。参见 Multipart 解析器。 |
FlashMapManager | 存储和检索 “输入” 和 “输出” FlashMap,它们可用于在一个请求和另一个请求之间传递属性,通常是在重定向期间使用。请参阅 Flash 属性。 |
1.3. Web MVC 配置
应用程序可以声明在特殊的 Bean 类型中列出的处理请求所需的基础设施 Bean。DispatcherServlet 会检查 WebApplicationContext 中的每一个特殊 Bean。如果没有匹配的 Bean 类型,它会回退到在 DispatcherServlet.properties 中列出的默认类型。
在大多数情况下,MVC 配置是最好的起点。它用 Java 或 XML 声明所需的 Bean,并提供了一个更高级别的配置回调 API 来自定义它。
Note:Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外的便利选项。
1.4. Servlet 配置
在 Servlet 3.0+ 的环境中,你可以选择以编程方式配置 Servlet 容器,作为替代方案或与 web.xml 文件结合使用。以下示例注册了一个 DispatcherServlet:
Java
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
WebApplicationInitializer 是 Spring MVC 提供的一个接口,确保你的实现被检测到并自动用于初始化任何 Servlet 3 容器。一个名为 AbstractDispatcherServletInitializer 的 WebApplicationInitializer 的抽象基类实现,通过覆盖方法来指定 Servlet 映射和 DispatcherServlet 配置的位置,使注册 DispatcherServlet 变得更加容易。
这对使用基于 Java 的 Spring 配置的应用程序是推荐的,如下面的示例所示:
Java
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果你使用基于 XML 的 Spring 配置,你应该直接从 AbstractDispatcherServletInitializer 扩展,如下面的示例所示:
Java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
AbstractDispatcherServletInitializer 也提供了一种方便的方式来添加 Filter 实例,并自动将它们映射到 DispatcherServlet,如下面的示例所示:
Java
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter()
};
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
每个过滤器都根据其具体类型添加了一个默认名称,并自动映射到 DispatcherServlet。
AbstractDispatcherServletInitializer 的 isAsyncSupported 受保护方法提供了一个单一的位置来在 DispatcherServlet 和所有映射到它的过滤器上启用异步支持。默认情况下,此标志设置为 true。
最后,如果你需要进一步自定义 DispatcherServlet 本身,你可以覆盖 createDispatcherServlet 方法。
1.5. 流程(Processing)
DispatcherServlet 处理请求的过程如下:
搜索
WebApplicationContext并将其绑定在请求中作为控制器和处理过程中的其他元素可以使用的属性。默认情况下,它绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE键下。将地区解析器绑定到请求,以便处理过程中的元素在处理请求(渲染视图,准备数据等)时解析要使用的地区。如果你不需要地区解析,你不需要地区解析器。
将主题解析器绑定到请求,以便视图等元素确定要使用的主题。如果你不使用主题,你可以忽略它。
如果你指定了一个 Multipart 文件解析器,请求会被检查是否有 Multipart。如果找到 Multipart,请求将被包装在
MultipartHttpServletRequest中,以供处理过程中的其他元素进一步处理。有关 Multipart 处理的更多信息,请参见 Multipart 解析器。搜索适当的处理器。如果找到一个处理器,与处理器关联的执行链(预处理器,后处理器和控制器)将运行以准备渲染的 Model。或者,对于带注解的控制器,响应可以在
HandlerAdapter中渲染,而不是返回视图。如果返回了一个 Model,视图将被渲染。如果没有返回 Model(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不会渲染视图,因为请求可能已经被满足了。
在 WebApplicationContext 中声明的 HandlerExceptionResolver Bean 用于解决在请求处理过程中抛出的异常。这些异常解析器允许自定义处理异常的逻辑。有关更多详细信息,请参见异常。
对于 HTTP 缓存支持,处理器可以使用 WebRequest 的 checkNotModified 方法,以及带注解的控制器的更多选项,如在控制器的 HTTP 缓存中所述。
你可以通过在 web.xml 文件中的 Servlet 声明中添加 Servlet 初始化参数(init-param 元素)来自定义单个 DispatcherServlet 实例。以下表格列出了支持的参数:
| 参数 | 说明 |
|---|---|
contextClass | 实现 ConfigurableWebApplicationContext 的类,由此 Servlet 实例化并在本地配置。默认情况下,使用 XmlWebApplicationContext。 |
contextConfigLocation | 传递给上下文实例(由 contextClass 指定)的字符串,用于指示可以在哪里找到上下文。该字符串可能由多个字符串组成(使用逗号作为分隔符)以支持多个上下文。在多个上下文位置中,如果有两次定义的 Bean,最后的位置优先。 |
namespace | WebApplicationContext 的命名空间。默认为 [servlet-name]-servlet。 |
throwExceptionIfNoHandlerFound | 当没有找到处理请求的处理器时,是否抛出 NoHandlerFoundException。然后可以使用 HandlerExceptionResolver(例如,使用 @ExceptionHandler 控制器方法)捕获异常并像处理任何其他异常一样处理。默认情况下,此设置为 false,此时 DispatcherServlet 将响应状态设置为 404(NOT_FOUND),而不引发异常。请注意,如果还配置了默认的 Servlet 处理,未解决的请求总是转发到默认的 Servlet,永远不会引发 404。 |
DispatcherServlet 初始化参数1.6. 路径匹配
Servlet API 将完整的请求路径公开为 requestURI,并进一步将其细分为 contextPath、servletPath 和 pathInfo,这些值会根据 Servlet 的映射方式而变化。从这些输入中,Spring MVC 需要确定用于处理器映射的查找路径,该路径位于 DispatcherServlet 自身的映射内,排除 contextPath 和任何存在的 servletMapping 前缀。
requestURI、contextPath、servletPath 及 pathInfo 说明
requestURI:是客户端发出的完整请求路径,包括路径和查询字符串,但不包括协议(http/https)和主机名。例如,访问http://example.com/app/servlet/info/details?name=John&age=30时,requestURI是"/app/servlet/info/details?name=John&age=30"。contextPath:是 Web 应用的根路径,通常在部署应用时指定。假设你的网站是http://example.com/app,那么"/app"就是你的contextPath。servletPath:是 Servlet 映射的路径。假设你有一个 Servlet 映射到"/servlet",那么当你访问http://example.com/app/servlet时,"/servlet"就是你的servletPath。pathInfo:是映射到 Servlet 后的剩余路径信息。假设你访问http://example.com/app/servlet/info/details,那么"/info/details"就是pathInfo。
例如,访问 URL http://example.com/app/servlet/info/details 时:
requestURI=/app/servlet/info/detailscontextPath=/appservletPath=/servletpathInfo=/info/details
servletPath 和 pathInfo 被解码,这使得它们无法直接与完整的 requestURI 进行比较,以便推导出 lookupPath,这就需要解码 requestURI。然而,这引入了自身的问题,因为路径可能包含编码的保留字符,如 "/" 或 ";",这些字符在解码后可能会改变路径的结构,也可能导致安全问题。此外,Servlet 容器可能会以不同程度规范化 servletPath,这使得对 requestURI 进行 startsWith 比较进一步变得不可能。
这就是为什么最好避免依赖 servletPath,它带有基于前缀的 servletPath 映射类型。如果 DispatcherServlet 被映射为默认的 Servlet,即 "/",或者没有前缀,即 "/*",并且 Servlet 容器是 4.0+,那么 Spring MVC 就能够检测到 Servlet 的映射类型,并完全避免使用 servletPath 和 pathInfo。在 3.1 的 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过在 MVC 配置中的路径匹配提供一个 alwaysUseFullPath=true 的 UrlPathHelper 来实现等效的效果。
幸运的是,默认的 Servlet 映射 "/" 是一个好选择。然而,仍然存在一个问题,即需要解码 requestURI 以使其可以与控制器映射进行比较。这再次是不可取的,因为有可能解码改变路径结构的保留字符。如果不期望这样的字符,那么你可以拒绝它们(就像 Spring Security HTTP 防火墙那样),或者你可以配置 UrlPathHelper 为 urlDecode=false,但控制器映射将需要匹配到编码的路径,这可能并不总是工作得很好。此外,有时 DispatcherServlet 需要与另一个 Servlet 共享 URL 空间,并可能需要通过前缀进行映射。
上述问题可以通过从 PathMatcher 切换到 5.3 或更高版本中可用的解析 PathPattern 来更全面地解决,参见 Pattern 比较。与需要解码查找路径或编码控制器映射的 AntPathMatcher 不同,解析的 PathPattern 匹配到称为 RequestPath 的路径的解析表示,一次一个路径段。这允许单独解码和清理路径段值,而不会改变路径的结构。解析的 PathPattern 也支持使用 servletPath 前缀映射,只要前缀保持简单,不包含任何需要编码的字符。
1.7. 拦截器
所有的 HandlerMapping 实现都支持处理器拦截器,当你想要对某些请求应用特定功能时,这是非常有用的 —— 例如,检查主体。拦截器必须实现来自 org.springframework.web.servlet 包的 HandlerInterceptor,它有三种方法,这应该可以提供足够的灵活性来进行各种预处理和后处理:
preHandle(..):在实际的处理器运行之前postHandle(..):在处理器运行之后afterCompletion(..):在完成整个请求之后
preHandle(..) 方法返回一个布尔值。你可以使用此方法来中断或继续执行链的处理。当此方法返回 true 时,处理器执行链将继续。当它返回 false 时,DispatcherServlet 认为拦截器本身已经处理了请求(例如,渲染了适当的视图),并且不会继续执行执行链中的其他拦截器和实际的处理器。
请参阅 MVC 配置部分中的拦截器,以获取如何配置拦截器的示例。你也可以直接使用 HandlerMapping 实现上的设置器来注册它们。
postHandle 方法在 @ResponseBody 和 ResponseEntity 方法中用处不大,因为这些方法的响应是在 HandlerAdapter 中和 postHandle 之前写入和提交的。这意味着对响应进行任何更改(例如,添加额外的 Header)已经太晚了。对于这种情况,你可以实现 ResponseBodyAdvice,并将其声明为控制器增强 Bean,或者直接在 RequestMappingHandlerAdapter 上配置它。
1.8. 异常
如果在请求映射过程中发生异常,或者从请求处理器(如 @Controller)中抛出异常,DispatcherServlet 会委托一系列的 HandlerExceptionResolver Bean 来解决这个异常,并提供替代处理方式,通常是一个错误响应。
下表列出了可用的 HandlerExceptionResolver 实现:
HandlerExceptionResolver | 描述 |
|---|---|
SimpleMappingExceptionResolver | 异常类名与错误视图名之间的映射。在浏览器应用中渲染错误页面非常有用。 |
DefaultHandlerExceptionResolver | 解决由 Spring MVC 引发的异常,并将它们映射到 HTTP 状态码。另请参见替代的 ResponseEntityExceptionHandler 和 REST API 异常。 |
ResponseStatusExceptionResolver | 解决带有 @ResponseStatus 注解的异常,并根据注解中的值将它们映射到 HTTP 状态码。 |
ExceptionHandlerExceptionResolver | 通过在 @Controller 或 @ControllerAdvice 类中调用 @ExceptionHandler 方法来解决异常。参见 @ExceptionHandler 方法。 |
HandlerExceptionResolver 实现1.8.1. 解析器链
你可以通过在你的 Spring 配置中声明多个 HandlerExceptionResolver Bean,并根据需要设置它们的 order 属性,来形成一个异常解析器链。order 属性越高,异常解析器的位置就越后。
HandlerExceptionResolver 的契约规定它可以返回:
指向错误视图的
ModelAndView。如果异常在解析器内部被处理,则返回一个空的
ModelAndView。如果异常仍未解决,则返回
null,以供后续的解析器尝试,如果最后异常仍然存在,那么允许它冒泡到 Servlet 容器。
MVC 配置会自动声明内置的解析器,用于默认的 Spring MVC 异常、用于带有 @ResponseStatus 注解的异常,以及用于支持 @ExceptionHandler 方法。你可以自定义或替换这个列表。
1.8.2. 容器错误页面
如果任何 HandlerExceptionResolver 都未能解决异常,因此,异常被留下来传播,或者如果响应状态被设置为错误状态(即,4xx,5xx),Servlet 容器可以在 HTML 中渲染一个默认的错误页面。要自定义容器的默认错误页面,你可以在 web.xml 中声明一个错误页面映射。以下示例展示了如何操作:
XML
<error-page>
<location>/error</location>
</error-page>1
2
3
2
3
根据前面的示例,当异常冒泡上来或响应具有错误状态时,Servlet 容器会在容器内部对配置的 URL(例如,/error)进行 ERROR 调度。然后,这将由 DispatcherServlet 处理,可能将其映射到一个 @Controller,该 @Controller 可以实现返回带有 Model 的错误视图名称,或者渲染一个 JSON 响应,如下面的示例所示:
Java
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("javax.servlet.error.status_code"));
map.put("reason", request.getAttribute("javax.servlet.error.message"));
return map;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Note:Servlet API 并未提供在 Java 中创建错误页面映射的方式。但是,你可以同时使用
WebApplicationInitializer和一个最小的web.xml。
1.9. 视图解析
Spring MVC 定义了 ViewResolver 和 View 接口,这让你可以在浏览器中渲染 Model,而不需要绑定到特定的视图技术。ViewResolver 提供了视图名称和实际视图之间的映射。View 负责在将数据交给特定的视图技术之前进行准备。
下表提供了更多关于 ViewResolver 层次结构的详细信息:
ViewResolver | 描述 |
|---|---|
AbstractCachingViewResolver | AbstractCachingViewResolver 的子类会缓存它们解析的视图实例。缓存可以提高某些视图技术的性能。你可以通过设置 cache 属性为 false 来关闭缓存。此外,如果你必须在运行时刷新某个视图(例如,当 FreeMarker 模板被修改时),你可以使用 removeFromCache(String viewName, Locale loc) 方法。 |
UrlBasedViewResolver | ViewResolver 接口的简单实现,它直接将逻辑视图名称解析为 URL,无需显式的映射定义。如果你的逻辑名称与你的视图资源的名称直接匹配,而无需任意映射,那么这是合适的。 |
InternalResourceViewResolver | UrlBasedViewResolver 的方便子类,支持 InternalResourceView(实际上是 Servlet 和 JSP)以及诸如 JstlView 和 TilesView 的子类。你可以使用 setViewClass(..) 来为此解析器生成的所有视图指定视图类。详情请参见 UrlBasedViewResolver 的 JavaDoc。 |
FreeMarkerViewResolver | UrlBasedViewResolver 的方便子类,支持 FreeMarkerView 和它们的自定义子类。 |
ContentNegotiatingViewResolver | ViewResolver 接口的实现,根据请求文件名或 Accept Header 解析视图。参见内容协商。 |
BeanNameViewResolver | ViewResolver 接口的实现,将视图名称解释为当前应用上下文中的 Bean 名称。这是一个非常灵活的变体,允许你根据不同的视图名称混合和匹配不同的视图类型。每个这样的 View 可以被定义为一个 Bean,例如在 XML 或配置类中。 |
ViewResolver 实现1.9.1. 处理
你可以通过声明多个解析器 Bean,并在必要时设置 order 属性来指定排序,来形成视图解析器链。请记住,order 属性越高,视图解析器在链中的位置就越后。
ViewResolver 的契约规定它可以返回 null 来表示找不到视图。然而,在 JSP 和 InternalResourceViewResolver 的情况下,判断 JSP 是否存在的唯一方法是通过 RequestDispatcher 进行调度。因此,你必须始终将 InternalResourceViewResolver 配置为视图解析器总体顺序的最后一个。
配置视图解析就像在你的 Spring 配置中添加 ViewResolver Bean 一样简单。MVC 配置为视图解析器和添加无逻辑的视图控制器提供了一个专用的配置 API,这对于没有控制器逻辑的 HTML 模板渲染非常有用。
1.9.2. 重定向
在视图名称中,特殊的 redirect: 前缀让你可以执行重定向。UrlBasedViewResolver(及其子类)将其识别为需要重定向的指令。视图名称的其余部分是重定向 URL。
这与控制器返回 RedirectView 的效果相同,但现在控制器本身可以以逻辑视图名称进行操作。一个逻辑视图名称(如 redirect:/myapp/some/resource)相对于当前的 Servlet 上下文进行重定向,而像 redirect:https://myhost.com/some/arbitrary/path 这样的名称则重定向到绝对 URL。
请注意,如果一个控制器方法被 @ResponseStatus 注解标注,那么注解的值将优先于 RedirectView 设置的响应状态。
1.9.3. 转发
你也可以对最终由 UrlBasedViewResolver 及其子类解析的视图名称使用一个特殊的 forward: 前缀。这将创建一个 InternalResourceView,它会执行 RequestDispatcher.forward()。因此,这个前缀对 InternalResourceViewResolver 和 InternalResourceView(用于 JSP)并不有用,但如果你使用了其他的视图技术,却仍然希望强制将资源的转发交给 Servlet/JSP 引擎处理,那么这个前缀就会很有帮助。请注意,你也可以用链式的多个视图解析器来代替。
1.9.4. 内容协商
ContentNegotiatingViewResolver 并不自己解析视图,而是委托给其他视图解析器,并选择一个与客户端请求的表示相似的视图。这种表示可以从 Accept Header 或者查询参数(例如 "/path?format=pdf")中确定。
ContentNegotiatingViewResolver 通过比较请求的媒体类型和每个 ViewResolver 关联的 View 所支持的媒体类型(也被称为 Content-Type)来选择一个适当的 View 来处理请求。列表中第一个具有兼容 Content-Type 的 View 将返回表示给客户端。如果 ViewResolver 链不能提供一个兼容的视图,那么将会参考通过 DefaultViews 属性指定的视图列表。这个后者的选项适用于单例 View,它们可以渲染出当前资源的适当表示,而不考虑逻辑视图名称。Accept Header 可以包含通配符(例如 text/*),在这种情况下,Content-Type 为 text/xml 的 View 就是一个兼容的匹配。
1.10. 地区(Locale)
Spring 的大部分架构都支持国际化,就像 Spring Web MVC 框架一样。DispatcherServlet 允许你通过使用客户端的地区设置自动解析消息,这是通过 LocaleResolver 对象完成的。
当一个请求进来时,DispatcherServlet 会寻找一个地区解析器,如果找到了,它会尝试使用它来设置地区。通过使用 RequestContext.getLocale() 方法,你可以始终检索到由地区解析器解析的地区。
除了自动地区解析,你还可以将一个拦截器附加到处理器映射(参见拦截器以获取有关处理器映射拦截器的更多信息)以在特定情况下更改地区(例如,基于请求中的一个参数)。
地区解析器和拦截器在 org.springframework.web.servlet.i18n 包中定义,并以正常方式在你的应用程序上下文中配置。Spring 包含了以下的地区解析器选项:
1.10.1. 时区
除了获取客户端的地区信息外,通常还需要知道其时区。LocaleContextResolver 接口提供了一个扩展到 LocaleResolver 的功能,让解析器可以提供一个更丰富的 LocaleContext,其中可能包含时区信息。
当可用时,可以通过使用 RequestContext.getTimeZone() 方法获取用户的 TimeZone。时区信息会自动被任何已经在 Spring 的 ConversionService 中注册的 Date/Time Converter 和 Formatter 对象使用。
1.10.2. Header 解析器
这个地区解析器会检查客户端(例如,一个网络浏览器)发送的请求中的 accept-language Header。通常,这个 Header 字段包含了客户端操作系统的地区。请注意,这个解析器不支持时区信息。
1.10.3. Cookie 解析器
这个地区解析器会检查客户端上可能存在的 Cookie,以查看是否指定了 Locale 或 TimeZone。如果是,它会使用指定的详细信息。通过使用这个地区解析器的属性,你可以指定 Cookie 的名称以及最大寿命。以下是定义 CookieLocaleResolver 的示例:
XML
<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>1
2
3
4
5
2
3
4
5
以下表格描述了 CookieLocaleResolver 的属性:
| 属性 | 默认值 | 描述 |
|---|---|---|
cookieName | classname + LOCALE | Cookie 的名称。 |
cookieMaxAge | Servlet 容器默认 | Cookie 在客户端持续的最大时间。如果指定为 -1,Cookie 将不会持久化。它只在客户端关闭浏览器之前可用。 |
cookiePath | / | 限制 Cookie 在你的站点的某个部分的可见性。当指定了 cookiePath,Cookie 只对该路径及其下面的路径可见。 |
CookieLocaleResolver 的属性1.10.4. Session 解析器
SessionLocaleResolver 允许你从可能与用户请求关联的会话中检索 Locale 和 TimeZone。与 CookieLocaleResolver 相比,此策略将本地选择的地区设置存储在 Servlet 容器的 HttpSession 中。因此,这些设置对于每个会话都是临时的,因此,当每个会话结束时,这些设置就会丢失。
请注意,与外部会话管理机制(如 Spring Session 项目)没有直接关系。此 SessionLocaleResolver 根据当前的 HttpServletRequest 评估并修改相应的 HttpSession 属性。
1.10.5. Locale 拦截器
你可以通过将 LocaleChangeInterceptor 添加到 HandlerMapping 定义之一来启用更改地区设置。它检测请求中的参数,并相应地更改地区设置,调用调度器的应用程序上下文中的 LocaleResolver 上的 setLocale 方法。下一个示例显示了对包含名为 siteLanguage 的参数的所有 *.view 资源的调用现在会更改地区设置。所以,例如,对 URL https://www.sf.net/home.view?siteLanguage=nl 的请求,将站点语言更改为荷兰语。以下示例显示了如何拦截地区设置:
XML
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.11. 主题
你可以应用 Spring Web MVC 框架的主题来设置你的应用程序的整体外观和感觉,从而提升用户体验。一个主题是一组静态资源的集合,通常是样式表和图片,这些资源影响了应用程序的视觉风格。
1.11.1. 定义主题
要在你的 Web 应用程序中使用主题,你必须设置 org.springframework.ui.context.ThemeSource 接口的实现。WebApplicationContext 接口扩展了 ThemeSource,但将其职责委托给了一个专用的实现。默认情况下,委托是一个 org.springframework.ui.context.support.ResourceBundleThemeSource 实现,它从类路径的根部加载属性文件。要使用自定义的 ThemeSource 实现或配置 ResourceBundleThemeSource 的基本名称前缀,你可以在应用程序上下文中注册一个名为 themeSource 的 Bean。Web 应用程序上下文会自动检测到该名称的 Bean 并使用它。
当你使用 ResourceBundleThemeSource 时,主题在一个简单的属性文件中定义。属性文件列出了构成主题的资源,如下例所示:
Text
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg1
2
2
属性的键是从视图代码引用的主题元素的名称。对于 JSP,你通常使用 spring:theme 自定义标签来实现这一点,这与 spring:message 标签非常相似。以下 JSP 片段使用前面示例中定义的主题来定制外观和感觉:
XML
<%@ 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>1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
默认情况下,ResourceBundleThemeSource 使用一个空的基本名称前缀。因此,属性文件从类路径的根部加载。因此,你会将 cool.properties 主题定义放在类路径根目录的一个目录中(例如,在 /WEB-INF/classes 中)。ResourceBundleThemeSource 使用标准的 Java 资源包加载机制,允许主题的完全国际化。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties,它引用了一个带有荷兰文本的特殊背景图像。
1.11.2. 解析主题
在你定义了主题之后,如前一节所述,你需要决定使用哪个主题。DispatcherServlet 会查找名为 themeResolver 的 Bean,以找出要使用哪个 ThemeResolver 实现。主题解析器的工作方式与 LocaleResolver 非常相似。它检测用于特定请求的主题,并且也可以改变请求的主题。以下表格描述了 Spring 提供的主题解析器:
| 类 | 描述 |
|---|---|
FixedThemeResolver | 选择一个固定的主题,通过使用 defaultThemeName 属性来设置。 |
SessionThemeResolver | 主题在用户的 HTTP 会话中维护。每个会话只需要设置一次,但不会在会话之间持久化。 |
CookieThemeResolver | 选定的主题存储在客户端的 Cookie 中。 |
ThemeResolver 的实现Spring 也提供了一个 ThemeChangeInterceptor,它允许在每个请求上通过一个简单的请求参数来更改主题。
1.12. Multipart 解析器
来自 org.springframework.web.multipart 包的 MultipartResolver 是一种解析包括文件上传在内的 Multipart 请求的策略。有一个基于 Commons FileUpload 的实现,还有一个基于 Servlet 3.0 Multipart 请求解析的实现。
要启用 Multipart 处理,你需要在你的 DispatcherServlet Spring 配置中声明一个名为 multipartResolver 的 MultipartResolver Bean。DispatcherServlet 会检测到它并将其应用到传入的请求。当接收到一个内容类型为 multipart/form-data 的 POST 请求时,解析器会解析内容,将当前的 HttpServletRequest 包装为 MultipartHttpServletRequest,以便除了将部分内容作为请求参数外,还可以访问已解析的文件。
1.12.1. Apache Commons FileUpload
要使用 Apache Commons FileUpload,你可以配置一个类型为 CommonsMultipartResolver,名称为 multipartResolver 的 Bean。你还需要将 commons-fileupload Jar 作为你的类路径的依赖。
这种解析器变体委托给应用程序内的本地库,提供了跨 Servlet 容器的最大可移植性。作为替代方案,可以考虑通过容器自己的解析器进行标准的 Servlet Multipart 解析,如下所述。
Note:Commons FileUpload 传统上只适用于 POST 请求,但接受任何
multipart/内容类型。请参阅CommonsMultipartResolverJavaDoc 以获取详细信息和配置选项。
1.12.2. Servlet 3.0
Servlet 3.0 Multipart 解析需要通过 Servlet 容器配置来启用。要做到这一点:
在 Java 中,将
MultipartConfigElement设置在 Servlet 注册上。在
web.xml中,向 Servlet 声明添加一个"<multipart-config>"部分。
以下示例显示了如何在 Servlet 注册上设置 MultipartConfigElement:
Java
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
一旦 Servlet 3.0 配置就绪,你可以添加一个类型为 StandardServletMultipartResolver,名称为 multipartResolver 的 Bean。
Note:这种解析器变体直接使用你的 Servlet 容器的 Multipart 解析器,可能会将应用程序暴露给容器实现的差异中。默认情况下,它会尝试解析任何 HTTP 方法的
multipart/内容类型,但这可能不会在所有的 Servlet 容器中得到支持。请参阅StandardServletMultipartResolverJavaDoc 以获取详细信息和配置选项。
1.13. 日志
在 Spring MVC 中,DEBUG 级别的日志设计得紧凑、最小化且人性化。它关注的是那些反复有用的高价值信息,而不是那些只在调试特定问题时有用的信息。
TRACE 级别的日志通常遵循与 DEBUG 相同的原则(例如,也不应该是一个信息的大量输出),但可以用于调试任何问题。此外,一些日志消息在 TRACE 和 DEBUG 之间可能会显示不同级别的详细信息。
良好的日志来自于使用日志的经验。如果你发现任何不符合所述目标的内容,请告诉我们。
1.13.1. 敏感数据
DEBUG 和 TRACE 日志可能会记录敏感信息。这就是为什么请求参数和 Header 默认被掩码,而它们的完整日志必须通过在 DispatcherServlet 上显式启用 enableLoggingRequestDetails 属性来启用。
以下示例显示了如何使用 Java 配置来实现这一点:
Java
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
2. 过滤器
spring-web 模块提供了一些有用的过滤器:
2.1. Form Data
浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、PATCH 和 DELETE。Servlet API 要求 ServletRequest.getParameter*() 方法只支持 HTTP POST 的表单字段访问。
spring-web 模块提供了 FormContentFilter,用于拦截内容类型为 application/x-www-form-urlencoded 的 HTTP PUT、PATCH 和 DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest,使表单数据通过 ServletRequest.getParameter*() 方法族可用。
2.2. Forwarded Header
当请求通过代理(如负载均衡器)时,Host、Port 和 Scheme 可能会改变,这使得从客户端的角度创建指向正确的 Host、Port 和 Scheme 的链接成为了挑战。
RFC 7239 定义了代理可以用来提供关于原始请求的信息的 Forwarded HTTP Header。还有其他非标准 Header,包括 X-Forwarded-Host、X-Forwarded-Port、X-Forwarded-Proto、X-Forwarded-Ssl 和 X-Forwarded-Prefix。
ForwardedHeaderFilter 是一个 Servlet 过滤器,它修改请求以:
- 根据
ForwardedHeader 改变 Host、Port 和 Scheme; - 删除这些 Header 以消除进一步的影响;
过滤器依赖于包装请求,因此它必须排在其他过滤器(如 RequestContextFilter)之前,这些过滤器应该使用修改后的请求而不是原始请求。
对于 Forwarded Header 有安全考虑,因为应用程序无法知道 Header 是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么在信任边界的代理应该被配置为删除来自外部的不受信任的 Forwarded Header。你也可以将 ForwardedHeaderFilter 配置为 removeOnly=true,在这种情况下,它会删除但不使用 Header。
为了支持异步请求和 Error 调度,这个过滤器应该与 DispatcherType.ASYNC 和 DispatcherType.ERROR 一起映射。如果使用 Spring 框架的 AbstractAnnotationConfigDispatcherServletInitializer(参见 Servlet 配置),所有过滤器都会自动注册所有的调度类型。然而,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,一定要在 DispatcherType.REQUEST 之外,还包括 DispatcherType.ASYNC 和 DispatcherType.ERROR。
2.3. Shallow ETag
ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希来创建一个 “Shallow” ETag。下次客户端发送时,它会做同样的事情,但它还会将计算出的值与 If-None-Match 请求 Header 进行比较,如果两者相等,就返回 304(NOT_MODIFIED)。
这种策略节省了网络带宽,但没有节省 CPU,因为每个请求都必须计算完整的响应。早些时候描述的控制器级别的其他策略可以避免计算。参见 HTTP 缓存。
这个过滤器有一个 writeWeakETag 参数,可以配置过滤器写入类似于以下的弱 ETag:W/"02a2d595e6ed9a0b24f027f2b63b134d6"(如 RFC 7232 第 2.3 节所定义)。
为了支持异步请求,这个过滤器必须与 DispatcherType.ASYNC 映射,以便过滤器可以延迟并成功地在最后一个异步调度结束时生成 ETag。如果使用 Spring 框架的 AbstractAnnotationConfigDispatcherServletInitializer(参见 Servlet 配置),所有过滤器都会自动注册所有的调度类型。然而,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,一定要包括 DispatcherType.ASYNC。
2.4. CORS
Spring MVC 通过控制器上的注解提供了对 CORS 配置的细粒度支持。然而,当与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter,它必须排在 Spring Security 的过滤器链之前。