Spring中的Web访问:Servlet API支持
本文的内容涵盖了对构建在 Servlet API 之上并部署到 Servlet 容器的 Servlet 栈 Web 应用程序的支持。
章节包括 Spring MVC、视图技术、CORS 支持等内容。
# 一、Web MVC
Spring Web MVC 是最初构建于 Servlet API 之上的 Web 框架,从 Spring Framework 的最初版本就已包含在内。正式名称 "Spring Web MVC" 源于其源代码模块(spring-webmvc
(opens new window))的名称,但它更常被称为 "Spring MVC"。
与 Spring Web MVC 并行,Spring Framework 5.0 引入了一个响应式堆栈 Web 框架,其名称 "Spring WebFlux" 也基于其源代码模块(spring-webflux
(opens new window))。
有关基线信息以及与 Servlet 容器和 Jakarta EE 版本范围的兼容性,请参阅 Spring Framework Wiki (opens new window)。
# 1、DispatcherServlet
Spring MVC 和许多其他 Web 框架一样,都围绕着前端控制器模式设计。在这种模式中,一个中央 Servlet
,即 DispatcherServlet
,提供了一个共享的请求处理算法,而实际的工作则由可配置的委托组件执行。这种模型非常灵活,并支持多样化的工作流程。
DispatcherServlet
,需要根据 Servlet 规范通过 Java 配置或在 web.xml
中声明和映射。反过来,DispatcherServlet
使用 Spring 配置来发现它需要的委托组件,用于请求映射、视图解析、异常处理以及更多。
以下 Java 配置示例注册并初始化了 DispatcherServlet
,Servlet 容器会自动检测到它:
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 加载 Spring Web 应用程序配置
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// 创建并注册 DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
注意: 除了直接使用 ServletContext API,你还可以扩展 AbstractAnnotationConfigDispatcherServletInitializer
并覆盖特定的方法。
注意: 对于编程式用例,可以使用 GenericWebApplicationContext
代替 AnnotationConfigWebApplicationContext
。详情请参见 GenericWebApplicationContext (opens new window) 的 Javadoc。
以下 web.xml
配置示例注册并初始化了 DispatcherServlet
:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
注意: Spring Boot 遵循不同的初始化顺序。Spring Boot 不是挂钩到 Servlet 容器的生命周期,而是使用 Spring 配置来引导自身和嵌入式 Servlet 容器。Filter
和 Servlet
的声明在 Spring 配置中被检测到,并注册到 Servlet 容器中。更多细节请参见 Spring Boot 文档 (opens new window)。
# 1.1、上下文层次结构
DispatcherServlet
期望使用 WebApplicationContext
(一个普通 ApplicationContext
的扩展)来进行自身的配置。WebApplicationContext
拥有一个到 ServletContext
的连接以及与其关联的 Servlet
。它也被绑定到 ServletContext
,这样应用程序就可以使用 RequestContextUtils
上的静态方法来查找 WebApplicationContext
(如果他们需要访问它)。
对于许多应用程序来说,拥有一个单独的 WebApplicationContext
是简单且足够的。也可以拥有一个上下文层次结构,其中一个根 WebApplicationContext
在多个 DispatcherServlet
(或其他 Servlet
) 实例之间共享,每个实例都有自己的子 WebApplicationContext
配置。
根 WebApplicationContext
通常包含基础设施 bean,例如需要在多个 Servlet
实例之间共享的数据仓库和业务服务。这些 bean 可以被有效地继承,并且可以在特定于 Servlet 的子 WebApplicationContext
中被覆盖,该子 WebApplicationContext
通常包含给定 Servlet
本地的 bean。下图显示了这种关系:
以下示例配置了一个 WebApplicationContext
层次结构:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
提示: 如果不需要应用程序上下文层次结构,应用程序可以通过 getRootConfigClasses()
返回所有配置,并通过 getServletConfigClasses()
返回 null
。
以下示例显示了等效的 web.xml
:
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
提示: 如果不需要应用程序上下文层次结构,应用程序可以仅配置一个“根”上下文,并使 contextConfigLocation
Servlet 参数为空。
# 1.2、特殊Bean类型
DispatcherServlet
委托特殊 bean 来处理请求并呈现适当的响应。“特殊 bean”是指由 Spring 管理的、实现了框架契约的 Object
实例。 这些通常带有内置契约,但您可以自定义它们的属性并扩展或替换它们。
下表列出了 DispatcherServlet
检测到的特殊 bean:
Bean类型 | 说明 |
---|---|
HandlerMapping | 将请求映射到处理程序,以及用于预处理和后处理的拦截器列表。 映射基于某些标准,这些标准的详细信息因 HandlerMapping 实现而异。主要的 HandlerMapping 实现有两种:RequestMappingHandlerMapping (支持使用 @RequestMapping 注释的方法)和 SimpleUrlHandlerMapping (维护 URI 路径模式到处理程序的显式注册)。 |
HandlerAdapter | 帮助 DispatcherServlet 调用映射到请求的处理程序,而不管处理程序的实际调用方式如何。 例如,调用带注释的控制器需要解析注释。 HandlerAdapter 的主要目的是使 DispatcherServlet 免受此类细节的影响。 |
HandlerExceptionResolver | 解析异常的策略,可能会将异常映射到处理程序、HTML 错误视图或其他目标。 |
ViewResolver | 将从处理程序返回的基于逻辑 String 的视图名称解析为实际的 View ,以便呈现到响应。 |
LocaleResolver , LocaleContextResolver | 解析客户端正在使用的 Locale 以及可能的时区,以便能够提供国际化的视图。 |
ThemeResolver | 解析您的 Web 应用程序可以使用的主题 — 例如,提供个性化布局。 |
MultipartResolver | 抽象,用于在某些多部分解析库的帮助下解析多部分请求(例如,浏览器表单文件上载)。 |
FlashMapManager | 存储和检索“输入”和“输出” FlashMap ,这些 FlashMap 可用于将属性从一个请求传递到另一个请求,通常跨重定向传递。 |
# 1.3、MVC 配置
应用程序可以声明特殊 Bean 类型中列出的、处理请求所需的基础设施 Bean。DispatcherServlet
检查 WebApplicationContext
中是否存在每个特殊的 Bean。如果没有匹配的 Bean 类型,它会回退到 DispatcherServlet.properties
中列出的默认类型。
大多数情况下,MVC 配置是最佳的起点。它使用 Java 或 XML 声明所需的 Bean,并提供更高级别的配置回调 API 以进行自定义。
注意: Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外的便捷选项。
# 1.4、Servlet配置
在 Servlet 环境中,你可以选择以编程方式配置 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 容器。WebApplicationInitializer
的一个抽象基类实现,名为 AbstractDispatcherServletInitializer
,通过重写指定 servlet 映射和 DispatcherServlet
配置位置的方法,可以更轻松地注册 DispatcherServlet
。
对于使用基于 Java 的 Spring 配置的应用程序,建议使用 AbstractDispatcherServletInitializer
,如下例所示:
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
如果你使用基于 XML 的 Spring 配置,你应该直接继承 AbstractDispatcherServletInitializer
,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
AbstractDispatcherServletInitializer
还提供了一种便捷的方式来添加 Filter
实例,并将其自动映射到 DispatcherServlet
,如下例所示:
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
每个过滤器都根据其具体类型添加一个默认名称,并自动映射到 DispatcherServlet
。
AbstractDispatcherServletInitializer
的 isAsyncSupported
受保护方法提供了一个地方来为 DispatcherServlet
和映射到它的所有过滤器启用异步支持。默认情况下,此标志设置为 true
。
最后,如果你需要进一步自定义 DispatcherServlet
本身,你可以重写 createDispatcherServlet
方法。
# 1.5、Processing(处理流程)
DispatcherServlet
按照以下步骤处理请求:
在
WebApplicationContext
中查找,并将其作为属性绑定到请求中,以便控制器和流程中的其他元素可以使用。默认情况下,它绑定在DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE
键下。将区域解析器绑定到请求,以便流程中的元素在处理请求时(呈现视图、准备数据等)可以解析要使用的区域。 如果不需要区域解析,则不需要区域解析器。
将主题解析器绑定到请求,以便视图等元素可以确定要使用的主题。 如果不使用主题,则可以忽略它。
如果指定多部分文件解析器,则检查请求中是否存在多部分。 如果找到多部分,则将请求包装在
MultipartHttpServletRequest
中,以供流程中的其他元素进一步处理。搜索适当的处理器。如果找到处理器,则运行与该处理器关联的执行链(预处理器、后处理器和控制器),以准备用于渲染的模型。 另外,对于带注解的控制器,可以呈现响应(在
HandlerAdapter
中),而不是返回视图。如果返回模型,则渲染视图。 如果未返回模型(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不渲染任何视图,因为可能已经满足了请求。
在 WebApplicationContext
中声明的 HandlerExceptionResolver
bean 用于解析在请求处理期间引发的异常。 这些异常解析器允许自定义用于处理异常的逻辑。
对于 HTTP 缓存支持,处理程序可以使用 WebRequest
的 checkNotModified
方法,以及带注解控制器的更多选项。
您可以通过将 Servlet 初始化参数(init-param
元素)添加到 web.xml
文件中的 Servlet 声明来定制各个 DispatcherServlet
实例。 下表列出了支持的参数:
Parameter | Explanation |
---|---|
contextClass | 实现 ConfigurableWebApplicationContext 的类,由该 Servlet 实例化并在本地配置。默认情况下,使用 XmlWebApplicationContext 。 |
contextConfigLocation | 传递给上下文实例(由 contextClass 指定)的字符串,用于指示可以在哪里找到上下文。 该字符串可能由多个字符串(使用逗号作为分隔符)组成,以支持多个上下文。 如果多个上下文位置包含定义了两次的 bean,则以最新的位置为准。 |
namespace | WebApplicationContext 的命名空间。 默认为 [servlet-name]-servlet 。 |
throwExceptionIfNoHandlerFound | 是否在找不到请求的处理程序时引发 NoHandlerFoundException 。 然后可以使用 HandlerExceptionResolver 捕获异常(例如,通过使用 @ExceptionHandler 控制器方法)并像其他异常一样处理。 从 6.1 开始,此属性设置为 true 并已弃用。 请注意,如果还配置了默认 servlet 处理,则未解析的请求始终转发到默认 servlet,并且永远不会引发 404 错误。 |
# 1.6、路径匹配
Servlet API 将完整的请求路径暴露为 requestURI
,并将其进一步细分为 contextPath
、servletPath
和 pathInfo
,它们的值取决于 Servlet 的映射方式。Spring MVC 需要从这些输入中确定用于映射处理程序的查找路径,该路径应排除 contextPath
和任何 servletMapping
前缀(如果适用)。
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 配置的 Path Matching 中提供具有 alwaysUseFullPath=true
的 UrlPathHelper
来实现相同的效果。
幸运的是,默认的 Servlet 映射 "/"
是一个不错的选择。但是,仍然存在一个问题,即需要解码 requestURI
才能与控制器映射进行比较。由于存在解码可能改变路径结构的保留字符的风险,这仍然是不希望的。如果预期不存在此类字符,则可以拒绝它们(如 Spring Security HTTP 防火墙),或者可以配置 UrlPathHelper
,使其 urlDecode=false
,但控制器映射需要匹配编码的路径,这可能并不总是有效。此外,有时 DispatcherServlet
需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。
当使用 PathPatternParser
和解析后的模式时,这些问题都得到了解决,它是使用带有 AntPathMatcher
的字符串路径匹配的替代方法。从 5.3 版本开始,PathPatternParser
已可在 Spring MVC 中使用,并且从 6.0 版本开始默认启用。与需要解码查找路径或编码控制器映射的 AntPathMatcher
不同,解析后的 PathPattern
一次匹配一个路径段的路径的解析表示形式,称为 RequestPath
。这允许单独解码和清理路径段值,而不会有改变路径结构的风险。只要使用 Servlet 路径映射并且前缀保持简单,即没有编码字符,解析后的 PathPattern
还支持使用 servletPath
前缀映射。
# 1.7、拦截器
所有的 HandlerMapping
实现都支持处理器拦截,这在你想要在所有请求中应用一些功能时非常有用。一个 HandlerInterceptor
可以实现以下方法:
preHandle(..)
— 在实际的处理器运行之前执行的回调函数,返回一个布尔值。如果该方法返回true
,则执行继续;如果返回false
,则执行链的其余部分将被绕过,并且处理器不会被调用。postHandle(..)
— 在处理器运行之后执行的回调函数。afterCompletion(..)
— 在完整请求完成之后执行的回调函数。
注意: 对于
@ResponseBody
和ResponseEntity
控制器方法,响应在HandlerAdapter
中被写入和提交,之后才会调用postHandle
。这意味着修改响应(例如添加额外的header)为时已晚。你可以实现ResponseBodyAdvice
并将其声明为一个 Controller Advice bean,或者直接在RequestMappingHandlerAdapter
上配置它。
请参考 MVC 配置中的 拦截器 部分,查看如何配置拦截器的示例。你也可以通过在各个 HandlerMapping
实现上使用 setter 方法直接注册它们。
警告: 由于与注解控制器路径匹配可能存在不匹配,拦截器并不理想地适用于作为安全层。通常,我们建议使用 Spring Security,或者类似的方法,集成到 Servlet 过滤器链中,并尽早应用。
# 1.8、异常
如果请求映射期间发生异常,或者从请求处理程序(例如 @Controller
)中抛出异常,DispatcherServlet
会委托给 HandlerExceptionResolver
bean 链来解决异常并提供替代处理,这通常是错误响应。
下表列出了可用的 HandlerExceptionResolver
实现:
HandlerExceptionResolver | Description |
---|---|
SimpleMappingExceptionResolver | 异常类名与错误视图名称之间的映射。适用于在浏览器应用程序中渲染错误页面。 |
DefaultHandlerExceptionResolver | 解决 Spring MVC 引发的异常,并将它们映射到 HTTP 状态码。另请参阅替代方案 ResponseEntityExceptionHandler 和 Error Responses。 |
ResponseStatusExceptionResolver | 解决带有 @ResponseStatus 注解的异常,并根据注解中的值将其映射到 HTTP 状态码。 |
ExceptionHandlerExceptionResolver | 通过调用 @Controller 或 @ControllerAdvice 类中的 @ExceptionHandler 方法来解决异常。请参阅 @ExceptionHandler methods。 |
# a、解析器链
您可以通过在 Spring 配置中声明多个 HandlerExceptionResolver
bean 并根据需要设置它们的 order
属性来形成异常解析器链。 order
属性越高,异常解析器的位置就越靠后。
HandlerExceptionResolver
的契约指定它可以返回:
- 指向错误视图的
ModelAndView
。 - 如果异常在解析器内部处理,则返回一个空的
ModelAndView
。 - 如果异常仍未解决,则返回
null
,以便后续解析器尝试,如果异常最终都未解决,则允许其冒泡到 Servlet 容器。
MVC Config 会自动声明内置的解析器,用于处理默认的 Spring MVC 异常、带有 @ResponseStatus
注解的异常以及对 @ExceptionHandler
方法的支持。您可以自定义该列表或将其替换。
# b、容器错误页面
如果任何 HandlerExceptionResolver
都没有解决异常,因此,异常会继续传播,或者如果响应状态被设置为错误状态(即 4xx、5xx),Servlet 容器可以在 HTML 中呈现默认错误页面。 要自定义容器的默认错误页面,您可以在 web.xml
中声明错误页面映射。 以下示例显示了如何执行此操作:
<error-page>
<location>/error</location>
</error-page>
鉴于前面的示例,当异常冒泡或响应具有错误状态时,Servlet 容器会在容器内对配置的 URL(例如,/error
)进行 ERROR 调度。 然后,DispatcherServlet
会处理它,可能会将其映射到 @Controller
,可以实现该 @Controller
以返回带有模型的错误视图名称或呈现 JSON 响应,如以下示例所示:
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
// 获取 Servlet 容器设置的状态码
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
// 获取 Servlet 容器描述的错误信息
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
Tip: Servlet API 没有提供在 Java 中创建错误页面映射的方法。 但是,您可以同时使用 WebApplicationInitializer
和最小的 web.xml
。
# 1.9、渲染模型
Spring MVC 定义了 ViewResolver
和 View
接口,允许你在浏览器中渲染模型,而无需将你绑定到特定的视图技术。ViewResolver
提供了视图名称和实际视图之间的映射。View
负责在交给特定视图技术之前准备数据。
下表提供了关于 ViewResolver
层次结构的更多详细信息:
ViewResolver | Description |
---|---|
AbstractCachingViewResolver | AbstractCachingViewResolver 的子类缓存它们解析的视图实例。缓存提高了某些视图技术的性能。你可以通过将 cache 属性设置为 false 来关闭缓存。此外,如果必须在运行时刷新某个视图(例如,当 FreeMarker 模板被修改时),可以使用 removeFromCache(String viewName, Locale loc) 方法。 |
UrlBasedViewResolver | ViewResolver 接口的简单实现,它直接将逻辑视图名称解析为 URL,而无需显式的映射定义。如果你的逻辑名称以直接的方式与视图资源的名称匹配,而不需要任意映射,则这是合适的。 |
InternalResourceViewResolver | UrlBasedViewResolver 的便捷子类,支持 InternalResourceView (实际上是 Servlet 和 JSP)以及诸如 JstlView 的子类。你可以通过使用 setViewClass(..) 为此解析器生成的所有视图指定视图类。有关详细信息,请参阅 [ UrlBasedViewResolver``][https://docs.spring.io/spring-framework/docs/6.2.3/javadoc-api/org/springframework/web/reactive/result/view/UrlBasedViewResolver.html] javadoc。 |
FreeMarkerViewResolver | UrlBasedViewResolver 的便捷子类,支持 FreeMarkerView 和它们的自定义子类。 |
ContentNegotiatingViewResolver | ViewResolver 接口的实现,它基于请求文件名或 Accept 头部解析视图。 |
BeanNameViewResolver | ViewResolver 接口的实现,它将视图名称解释为当前应用程序上下文中的 bean 名称。这是一种非常灵活的变体,允许基于不同的视图名称混合和匹配不同的视图类型。每个此类 View 可以定义为一个 bean,例如,在 XML 或配置类中。 |
# 1.10、处理
你可以通过声明多个解析器 bean 并根据需要设置 order
属性来指定顺序,从而链接视图解析器。记住,order 属性越高,视图解析器在链中的位置就越靠后。
ViewResolver
的约定指定它可以返回 null,以指示未找到视图。但是,对于 JSP 和 InternalResourceViewResolver
,确定 JSP 是否存在的唯一方法是通过 RequestDispatcher
执行分派。因此,你必须始终将 InternalResourceViewResolver
配置为视图解析器的总体顺序中的最后一个。
配置视图解析就像将 ViewResolver
bean 添加到你的 Spring 配置一样简单。MVC Config
提供了一个专用的配置 API,用于 View Resolvers
和添加无逻辑的 View Controllers
,这对于 HTML 模板渲染而无需控制器逻辑非常有用。
# 1.11、重定向
视图名称中的特殊 redirect:
前缀允许你执行重定向。UrlBasedViewResolver
(及其子类)将其识别为需要重定向的指令。视图名称的其余部分是重定向 URL。
最终效果与控制器返回 RedirectView
相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如 redirect:/myapp/some/resource
)相对于当前 Servlet 上下文重定向,而诸如 redirect:https://myhost.com/some/arbitrary/path
的名称则重定向到绝对 URL。
# 1.12、转发
你也可以对最终由 UrlBasedViewResolver
及其子类解析的视图名称使用特殊的 forward:
前缀。这将创建一个 InternalResourceView
,它执行 RequestDispatcher.forward()
。因此,此前缀对于 InternalResourceViewResolver
和 InternalResourceView
(对于 JSP)没有用,但如果你使用另一种视图技术,但仍然希望强制转发资源以由 Servlet/JSP 引擎处理,则它可能很有用。请注意,你也可以链接多个视图解析器。
# 1.13、内容协商
ContentNegotiatingViewResolver
本身不解析视图,而是委托给其他视图解析器,并选择与客户端请求的表示形式相似的视图。可以从 Accept
标头或查询参数(例如 "/path?format=pdf"
)确定表示形式。
ContentNegotiatingViewResolver
通过将请求媒体类型与每个 ViewResolvers
的 View
支持的媒体类型(也称为 Content-Type
)进行比较,来选择适当的 View
以处理请求。列表中第一个具有兼容 Content-Type
的 View
将表示形式返回给客户端。如果 ViewResolver
链无法提供兼容的视图,则会查阅通过 DefaultViews
属性指定的视图列表。对于可以呈现当前资源的适当表示形式的单例 Views
来说,后一个选项是合适的,而不管逻辑视图名称如何。Accept
标头可以包括通配符(例如,text/*
),在这种情况下,Content-Type
为 text/xml
的 View
是兼容的匹配项。
# 1.14、区域(国际化)
大多数 Spring 架构的组件都支持国际化,Spring Web MVC 框架也是如此。DispatcherServlet
允许你使用客户端的区域设置自动解析消息。这是通过 LocaleResolver
对象完成的。
当请求进入时,DispatcherServlet
查找区域设置解析器,如果找到一个,它会尝试使用它来设置区域设置。通过使用 RequestContext.getLocale()
方法,你始终可以检索由区域设置解析器解析的区域设置。
除了自动区域设置解析之外,你还可以将拦截器附加到处理程序映射,以便在特定情况下更改区域设置(例如,基于请求中的参数)。
区域设置解析器和拦截器在 org.springframework.web.servlet.i18n
包中定义,并以通常的方式在你的应用程序上下文中配置。以下是 Spring 中包含的区域设置解析器的选择。
# a、时区
除了获取客户端的区域设置之外,了解其时区通常也很有用。LocaleContextResolver
接口提供了 LocaleResolver
的扩展,允许解析器提供更丰富的 LocaleContext
,其中可能包含时区信息。
如果可用,可以使用 RequestContext.getTimeZone()
方法获取用户的 TimeZone
。时区信息由注册到 Spring 的 ConversionService
的任何 Date/Time Converter
和 Formatter
对象自动使用。
# b、解析器
此区域设置解析器检查客户端发送的请求中的 accept-language
标头(例如,Web 浏览器)。通常,此标头字段包含客户端操作系统的区域设置。请注意,此解析器不支持时区信息。
# c、解析器
此区域设置解析器检查客户端上可能存在的 Cookie
,以查看是否指定了 Locale
或 TimeZone
。 如果是这样,它使用指定的详细信息。 通过使用此区域设置解析器的属性,你可以指定 cookie 的名称以及最长使用期限。 以下示例定义了一个 CookieLocaleResolver
:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<!-- cookie 的名称 -->
<property name="cookieName" value="clientlanguage"/>
<!-- 以秒为单位。 如果设置为 -1,则 cookie 不会持久保存(在浏览器关闭时删除) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
下表描述了 CookieLocaleResolver
的属性:
属性 | 默认值 | 描述 |
---|---|---|
cookieName | 类名 + LOCALE | Cookie 的名称 |
cookieMaxAge | Servlet 容器默认值 | Cookie 在客户端上保留的最长时间。 如果指定 -1 ,则 cookie 将不会持久保存。 仅在客户端关闭浏览器之前可用。 |
cookiePath | / | 将 Cookie 的可见性限制到站点的某个部分。 当指定 cookiePath 时,Cookie 仅对该路径及其下面的路径可见。 |
# d、解析器
SessionLocaleResolver
允许你从可能与用户请求关联的会话中检索 Locale
和 TimeZone
。 与 CookieLocaleResolver
相比,此策略将本地选择的区域设置设置存储在 Servlet 容器的 HttpSession
中。 因此,这些设置对于每个会话都是临时的,因此在每个会话结束时都会丢失。
请注意,与外部会话管理机制(例如 Spring Session 项目)没有直接关系。 此 SessionLocaleResolver
评估并修改针对当前 HttpServletRequest
的相应 HttpSession
属性。
# e、拦截器
你可以通过将 LocaleChangeInterceptor
添加到 HandlerMapping
定义之一来启用区域设置的更改。 它会检测请求中的参数并相应地更改区域设置,从而在 dispatcher 的应用程序上下文中调用 LocaleResolver
上的 setLocale
方法。 下一个示例显示,现在对包含名为 siteLanguage
的参数的所有 *.view
资源的调用都会更改区域设置。 因此,例如,URL www.sf.net/home.view?siteLanguage=nl
的请求会将站点语言更改为荷兰语。 以下示例显示了如何拦截区域设置:
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
# 1.15、主题
你可以应用 Spring Web MVC 框架的主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是影响应用程序视觉样式的样式表和图像。
警告: 从 6.0 版本开始,对主题的支持已被弃用,有利于使用 CSS,而无需服务器端的任何特殊支持。
# a、定义主题
要在 Web 应用程序中使用主题,必须设置 org.springframework.ui.context.ThemeSource
接口的实现。WebApplicationContext
接口扩展了 ThemeSource
,但将其职责委托给专门的实现。默认情况下,委托是一个 org.springframework.ui.context.support.ResourceBundleThemeSource
实现,它从类路径的根目录加载属性文件。要使用自定义 ThemeSource
实现或配置 ResourceBundleThemeSource
的基本名称前缀,可以在应用程序上下文中注册一个保留名称为 themeSource
的 bean。Web 应用程序上下文会自动检测具有该名称的 bean 并使用它。
当使用 ResourceBundleThemeSource
时,主题在简单的属性文件中定义。属性文件列出了组成主题的资源,如下例所示:
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
属性的键是从视图代码引用主题元素的名称。对于 JSP,通常使用 spring:theme
自定义标签来完成此操作,该标签与 spring:message
标签非常相似。以下 JSP 片段使用上一个示例中定义的主题来自定义外观:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
默认情况下,ResourceBundleThemeSource
使用空的基本名称前缀。因此,属性文件从类路径的根目录加载。因此,你需要将 cool.properties
主题定义放在类路径根目录下的一个目录中(例如,在 /WEB-INF/classes
中)。ResourceBundleThemeSource
使用标准的 Java 资源包加载机制,从而可以完全国际化主题。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties
,它引用一个带有荷兰语文本的特殊背景图像。
# b、解析主题
在定义主题之后(如前一节所述),你需要决定使用哪个主题。DispatcherServlet
查找名为 themeResolver
的 bean,以确定要使用哪个 ThemeResolver
实现。主题解析器的工作方式与 LocaleResolver
非常相似。它检测要用于特定请求的主题,并且还可以更改请求的主题。下表描述了 Spring 提供的各种主题解析器:
表 1. ThemeResolver 实现
类 | 描述 |
---|---|
FixedThemeResolver | 选择一个固定的主题,通过使用 defaultThemeName 属性进行设置。 |
SessionThemeResolver | 主题保存在用户的 HTTP 会话中。每个会话仅需设置一次,但不会在会话之间持久保存。 |
CookieThemeResolver | 所选主题存储在客户端的 Cookie 中。 |
Spring 还提供了一个 ThemeChangeInterceptor
,它允许使用简单的请求参数在每个请求上更改主题。
# 1.16、多部分请求(文件上传) 解析
MultipartResolver
来自 org.springframework.web.multipart
包,是一种用于解析包含文件上传的多部分请求的策略。 对于 Servlet 多部分请求解析,有一个基于容器的 StandardServletMultipartResolver
实现。 请注意,基于 Apache Commons FileUpload 的过时的 CommonsMultipartResolver
已不再可用,因为 Spring Framework 6.0 及其新的 Servlet 5.0+ 基线。
要启用多部分处理,需要在 DispatcherServlet
Spring 配置中声明一个名为 multipartResolver
的 MultipartResolver
bean。 DispatcherServlet
检测到它并将其应用于传入的请求。 当收到内容类型为 multipart/form-data
的 POST 请求时,解析器会解析内容并将当前的 HttpServletRequest
包装为 MultipartHttpServletRequest
,以便除了将各个部分作为请求参数公开之外,还可以访问已解析的文件。
# a、Multipart 解析
Servlet 多部分解析需要通过 Servlet 容器配置启用。 为此:
- 在 Java 中,在 Servlet 注册上设置
MultipartConfigElement
。 - 在
web.xml
中,将"<multipart-config>"
部分添加到 servlet 声明中。
以下示例展示了如何在 Servlet 注册上设置 MultipartConfigElement
:
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
// 可选地设置 maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
完成 Servlet 多部分配置后,可以添加一个类型为 StandardServletMultipartResolver
且名称为 multipartResolver
的 bean。
注意:
此解析器变体按原样使用 Servlet 容器的多部分解析器,可能会使应用程序暴露于容器实现差异。 默认情况下,它将尝试解析具有任何 HTTP 方法的任何 multipart/
内容类型,但这可能并非所有 Servlet 容器都支持。 有关详细信息和配置选项,请参阅 StandardServletMultipartResolver (opens new window) javadoc。
# 1.17、日志
DEBUG 级别的 Spring MVC 日志被设计成紧凑、最小化且人性化。它侧重于高价值的信息点,这些信息点可以反复使用,而不是仅在调试特定问题时才有用。
TRACE 级别的日志通常遵循与 DEBUG 相同的原则(例如,也不应该是信息洪流),但可以用于调试任何问题。此外,与 DEBUG 相比,某些日志消息可能会在 TRACE 级别显示不同的详细程度。
# a、敏感数据
DEBUG 和 TRACE 日志可能会记录敏感信息。这就是默认情况下请求参数和 header 会被屏蔽的原因,必须通过 DispatcherServlet
上的 enableLoggingRequestDetails
属性显式启用它们的完整记录。
以下示例展示了如何使用 Java 配置来实现:
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
# 2、过滤器
The spring-web
模块提供了一些有用的过滤器。
Servlet 过滤器可以在 web.xml
配置文件中配置,也可以使用 Servlet 注解进行配置。如果你正在使用 Spring Boot,你可以将它们声明为 Bean,并将它们配置为应用的一部分。
# 2.1、表单数据
浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、PATCH 和 DELETE。Servlet API 要求 ServletRequest.getParameter*()
方法仅支持 HTTP POST 的表单字段访问。
spring-web
模块提供了 FormContentFilter
来拦截 Content-Type 为 application/x-www-form-urlencoded
的 HTTP PUT、PATCH 和 DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest
,使表单数据可以通过 ServletRequest.getParameter*()
系列方法访问。
# 2.2、转发的 Headers
当请求通过负载均衡器等代理时,主机、端口和协议可能会发生变化,这使得从客户端的角度创建指向正确的主机、端口和协议的链接成为一项挑战。
RFC 7239 (opens new window) 定义了 Forwarded
HTTP header,代理可以使用它来提供关于原始请求的信息。
# a、非标准Headers
还有其他非标准 Header,包括 X-Forwarded-Host
、X-Forwarded-Port
、X-Forwarded-Proto
、X-Forwarded-Ssl
和 X-Forwarded-Prefix
。
# X-Forwarded-Host
虽然不是标准,但 X-Forwarded-Host: <host>
(opens new window) 是一个事实上的标准Header,用于将原始主机通信到下游服务器。例如,如果将 example.com/resource
的请求发送到将请求转发到 http://localhost:8080/resource
的代理,则可以发送 X-Forwarded-Host: example.com
的Header,以通知服务器原始主机为 example.com
。
# X-Forwarded-Port
虽然不是标准,但 X-Forwarded-Port: <port>
是一个事实上的标准Header,用于将原始端口通信到下游服务器。例如,如果将 example.com/resource
的请求发送到将请求转发到 http://localhost:8080/resource
的代理,则可以发送 X-Forwarded-Port: 443
的Header,以通知服务器原始端口为 443
。
# X-Forwarded-Proto
虽然不是标准,但 X-Forwarded-Proto: (https|http)
(opens new window) 是一个事实上的标准Header,用于将原始协议(例如,https/http)通信到下游服务器。例如,如果将 example.com/resource
的请求发送到将请求转发到 http://localhost:8080/resource
的代理,则可以发送 X-Forwarded-Proto: https
的Header,以通知服务器原始协议为 https
。
# X-Forwarded-Ssl
虽然不是标准,但 X-Forwarded-Ssl: (on|off)
是一个事实上的标准Header,用于将原始协议(例如,https/http)通信到下游服务器。例如,如果将 example.com/resource
的请求发送到将请求转发到 http://localhost:8080/resource
的代理,则可以发送 X-Forwarded-Ssl: on
以通知服务器原始协议为 https
。
# X-Forwarded-Prefix
虽然不是标准,但 X-Forwarded-Prefix: <prefix>
(opens new window) 是一个事实上的标准Headers,用于将原始URL路径前缀通信到下游服务器。
X-Forwarded-Prefix
的使用可能因部署场景而异,并且需要灵活以允许替换、删除或在目标服务器的路径前缀前面添加前缀。
场景 1:覆盖路径前缀
https://example.com/api/{path} -> http://localhost:8080/app1/{path}
前缀是在捕获组 {path}
之前的路径的开头。对于代理,前缀是 /api
,而对于服务器,前缀是 /app1
。在这种情况下,代理可以发送 X-Forwarded-Prefix: /api
,以使原始前缀 /api
覆盖服务器前缀 /app1
。
场景 2:删除路径前缀
有时,应用程序可能希望删除前缀。例如,考虑以下代理到服务器的映射:
https://app1.example.com/{path} -> http://localhost:8080/app1/{path}
https://app2.example.com/{path} -> http://localhost:8080/app2/{path}
代理没有前缀,而应用程序 app1
和 app2
分别具有路径前缀 /app1
和 /app2
。代理可以发送 X-Forwarded-Prefix:
以使空前缀覆盖服务器前缀 /app1
和 /app2
。
注意: 这种部署场景的一个常见情况是,每个生产应用服务器都支付许可证费用,并且最好在每个服务器上部署多个应用程序以减少费用。另一个原因是,在同一服务器上运行更多应用程序,以便共享服务器运行所需的资源。
在这些场景中,应用程序需要一个非空的上下文根,因为同一服务器上有多个应用程序。但是,这不应在公共 API 的 URL 路径中可见,在这些路径中,应用程序可以使用不同的子域,这些子域提供以下好处:
- 提高安全性,例如,同源策略
- 独立扩展应用程序(不同的域指向不同的 IP 地址)
场景 3:插入路径前缀
在其他情况下,可能需要添加前缀。例如,考虑以下代理到服务器的映射:
https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
在这种情况下,代理具有前缀 /api/app1
,服务器具有前缀 /app1
。代理可以发送 X-Forwarded-Prefix: /api/app1
以使原始前缀 /api/app1
覆盖服务器前缀 /app1
。
# 2.3、ForwardedHeaderFilter
ForwardedHeaderFilter
是一个 Servlet 过滤器,可修改请求,以便 a) 基于 Forwarded
header更改主机、端口和协议,以及 b) 删除这些header以消除进一步的影响。该过滤器依赖于包装请求,因此必须将其排序在其他过滤器(例如 RequestContextFilter
)之前,这些过滤器应使用修改后的请求而不是原始请求。
# 2.4、安全考虑
转发header存在安全隐患,因为应用程序无法知道header是由代理(如预期)还是由恶意客户端添加的。这就是为什么应将信任边界处的代理配置为删除来自外部的不受信任的 Forwarded
header。您还可以使用 removeOnly=true
配置 ForwardedHeaderFilter
,在这种情况下,它将删除header但不使用它们。
# 2.5、类型
为了支持异步请求和错误分派,应使用 DispatcherType.ASYNC
以及 DispatcherType.ERROR
映射此过滤器。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer
,则会自动为所有分派类型注册所有过滤器。但是,如果通过 web.xml
或在 Spring Boot 中通过 FilterRegistrationBean
注册过滤器,请确保除了 DispatcherType.REQUEST
之外,还包括 DispatcherType.ASYNC
和 DispatcherType.ERROR
。
# 2.6、ETag
ShallowEtagHeaderFilter
过滤器通过缓存写入响应的内容并从中计算 MD5 哈希来创建“浅”ETag。下次客户端发送时,它会执行相同的操作,但也会将计算出的值与 If-None-Match
请求header进行比较,如果两者相等,则返回 304 (NOT_MODIFIED)。
此策略节省了网络带宽,但不节省 CPU,因为必须为每个请求计算完整的响应。状态更改 HTTP 方法和其他 HTTP 条件请求header(例如 If-Match
和 If-Unmodified-Since
)不在此过滤器的范围内。控制器级别的其他策略可以避免计算,并且对 HTTP 条件请求具有更广泛的支持。
此过滤器具有一个 writeWeakETag
参数,该参数配置过滤器以写入类似于以下内容的弱 ETag:W/"02a2d595e6ed9a0b24f027f2b63b134d6"
(如 RFC 7232 第 2.3 节 (opens new window) 中定义)。
为了支持异步请求,必须使用 DispatcherType.ASYNC
映射此过滤器,以便过滤器可以延迟并成功生成 ETag 到最后一个异步分派的末尾。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer
,则会自动为所有分派类型注册所有过滤器。但是,如果通过 web.xml
或在 Spring Boot 中通过 FilterRegistrationBean
注册过滤器,请确保包括 DispatcherType.ASYNC
。
# 2.7、CORS
Spring MVC 通过控制器上的注解提供对 CORS 配置的细粒度支持。但是,当与 Spring Security 一起使用时,我们建议依赖内置的 CorsFilter
,该过滤器必须在 Spring Security 的过滤器链之前排序。
有关更多详细信息,请参阅关于 CORS 和 CORS 过滤器的章节。
# 2.8、Handler
在之前的 Spring Framework 版本中,可以将 Spring MVC 配置为在将传入请求映射到控制器方法时忽略 URL 路径中的尾部斜杠。这可以通过启用 PathMatchConfigurer
上的 setUseTrailingSlashMatch
选项来完成。这意味着发送 "GET /home/" 请求将由使用 @GetMapping("/home")
注释的控制器方法处理。
此选项已停用,但仍希望应用程序以安全的方式处理此类请求。UrlHandlerFilter
Servlet 过滤器专为此目的而设计。可以将其配置为:
- 在收到带有尾部斜杠的 URL 时,使用 HTTP 重定向状态进行响应,将浏览器发送到非尾部斜杠URL变体。
- 包装请求,使其表现得就像已发送没有尾部斜杠的请求一样,并继续处理该请求。
以下是如何实例化和配置博客应用程序的 UrlHandlerFilter
:
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
.trailingSlashHandler("/admin/**").wrapRequest()
.build();
# 3、消息转换
spring-web
模块包含 HttpMessageConverter
接口,用于通过 InputStream
和 OutputStream
读取和写入 HTTP 请求和响应的主体。HttpMessageConverter
实例用在客户端(例如,在 RestClient
中)和服务器端(例如,在 Spring MVC REST 控制器中)。
框架中提供了主要媒体(MIME)类型的具体实现,默认情况下,这些实现已在客户端注册到 RestClient
和 RestTemplate
,并在服务器端注册到 RequestMappingHandlerAdapter
。
下面介绍 HttpMessageConverter
的几个实现。有关完整列表,请参阅 HttpMessageConverter
Javadoc (opens new window)。对于所有转换器,都使用默认媒体类型,但可以通过设置 supportedMediaTypes
属性来覆盖它。
MessageConverter | Description |
---|---|
StringHttpMessageConverter | HttpMessageConverter 的一个实现,可以从 HTTP 请求和响应中读取和写入 String 实例。默认情况下,此转换器支持所有文本媒体类型(text/* ),并使用 Content-Type text/plain 写入。 |
FormHttpMessageConverter | HttpMessageConverter 的一个实现,可以从 HTTP 请求和响应中读取和写入表单数据。默认情况下,此转换器读取和写入 application/x-www-form-urlencoded 媒体类型。 表单数据从 MultiValueMap<String, String> 读取并写入其中。 该转换器还可以写入(但不能读取)从 MultiValueMap<String, Object> 读取的多部分数据。 默认情况下,支持 multipart/form-data 。 可以支持其他多部分子类型以写入表单数据。 有关更多详细信息,请查阅 FormHttpMessageConverter 的 javadoc。 |
ByteArrayHttpMessageConverter | HttpMessageConverter 的一个实现,可以从 HTTP 请求和响应中读取和写入字节数组。 默认情况下,此转换器支持所有媒体类型(*/* ),并使用 Content-Type application/octet-stream 写入。 你可以通过设置 supportedMediaTypes 属性并重写 getContentType(byte[]) 来覆盖它。 |
MarshallingHttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 Spring 的 org.springframework.oxm 包中的 Marshaller 和 Unmarshaller 抽象来读取和写入 XML。 此转换器需要一个 Marshaller 和 Unmarshaller 才能使用。 你可以通过构造函数或 bean 属性注入它们。 默认情况下,此转换器支持 text/xml 和 application/xml 。 |
MappingJackson2HttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 Jackson 的 ObjectMapper 读取和写入 JSON。 你可以使用 Jackson 提供的注解根据需要自定义 JSON 映射。 当你需要进一步的控制时(例如,需要为特定类型提供自定义 JSON 序列化器/反序列化器的情况),你可以通过 ObjectMapper 属性注入自定义 ObjectMapper 。 默认情况下,此转换器支持 application/json 。 这需要 com.fasterxml.jackson.core:jackson-databind 依赖项。 |
MappingJackson2XmlHttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 [Jackson XML][https://github.com/FasterXML/jackson-dataformat-xml] 扩展的 XmlMapper 读取和写入 XML。 你可以使用 JAXB 或 Jackson 提供的注解根据需要自定义 XML 映射。 当你需要进一步的控制时(例如,需要为特定类型提供自定义 XML 序列化器/反序列化器的情况),你可以通过 ObjectMapper 属性注入自定义 XmlMapper 。 默认情况下,此转换器支持 application/xml 。 这需要 com.fasterxml.jackson.dataformat:jackson-dataformat-xml 依赖项。 |
MappingJackson2CborHttpMessageConverter | com.fasterxml.jackson.dataformat:jackson-dataformat-cbor |
SourceHttpMessageConverter | HttpMessageConverter 的一个实现,可以从 HTTP 请求和响应中读取和写入 javax.xml.transform.Source 。 仅支持 DOMSource 、SAXSource 和 StreamSource 。 默认情况下,此转换器支持 text/xml 和 application/xml 。 |
GsonHttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 "Google Gson" 读取和写入 JSON。 这需要 com.google.code.gson:gson 依赖项。 |
JsonbHttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 Jakarta Json Bind API 读取和写入 JSON。 这需要 jakarta.json.bind:jakarta.json.bind-api 依赖项和一个可用的实现。 |
ProtobufHttpMessageConverter | HttpMessageConverter 的一个实现,可以使用 application/x-protobuf 内容类型以二进制格式读取和写入 Protobuf 消息。 这需要 com.google.protobuf:protobuf-java 依赖项。 |
ProtobufJsonFormatHttpMessageConverter | HttpMessageConverter 的一个实现,可以读写 JSON 文档到 Protobuf 消息以及从 Protobuf 消息读写 JSON 文档。 这需要 com.google.protobuf:protobuf-java-util 依赖项。 |
# 4、注解式控制器
Spring MVC 提供了一个基于注解的编程模型,其中 Controller
和 RestController
组件使用注解来表达请求映射、请求输入、异常处理等。注解式控制器具有灵活的方法签名,不必扩展基类或实现特定的接口。以下示例展示了一个通过注解定义的控制器:
@Controller
public class HelloController {
@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}
在上面的例子中,该方法接受一个 Model
并返回一个视图名称作为 String
,但还存在许多其他的选项,将在本章稍后进行解释。
# 4.1、声明
你可以通过在Servlet的WebApplicationContext
中使用标准的Spring bean定义来定义控制器bean。@Controller
注解可以自动检测,与Spring对检测classpath中的@Component
类并自动注册bean定义的一般支持保持一致。它也充当带注解的类的构造型,表明其作为Web组件的角色。
要启用对此类@Controller
bean的自动检测,你可以将组件扫描添加到Java配置中,如以下示例所示:
@Configuration
@ComponentScan("org.example.web")
public class WebConfiguration {
// ...
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example.web"/>
<!-- ... -->
</beans>
@RestController
是一个组合注解,它本身使用@Controller
和@ResponseBody
进行元注解,以指示控制器中的每个方法都继承类型级别的@ResponseBody
注解,因此直接写入响应体,而不是通过视图解析和使用HTML模板进行渲染。
# a、AOP代理
在某些情况下,你可能需要在运行时使用AOP代理来装饰控制器。一个例子是你选择直接在控制器上使用@Transactional
注解。在这种情况下,对于控制器,我们建议使用基于类的代理。直接在控制器上使用此类注解时,会自动启用基于类的代理。
如果控制器实现了一个接口,并且需要AOP代理,你可能需要显式配置基于类的代理。例如,使用@EnableTransactionManagement
,你可以更改为@EnableTransactionManagement(proxyTargetClass = true)
,使用<tx:annotation-driven/>
,你可以更改为<tx:annotation-driven proxy-target-class="true"/>
。
注意: 请记住,从6.0开始,使用接口代理时,Spring MVC不再仅仅基于接口上的类型级别
@RequestMapping
注解来检测控制器。请启用基于类的代理,否则接口还必须具有@Controller
注解。
# 4.2、映射请求
本章节讨论了带注解的控制器的请求映射。
# a、@RequestMapping
你可以使用 @RequestMapping
注解将请求映射到控制器方法。它有各种属性,可以通过 URL、HTTP 方法、请求参数、header 和媒体类型进行匹配。你可以在类级别使用它来表达共享的映射,或者在方法级别使用它来缩小到特定的 endpoint 映射。
还有一些特定 HTTP 方法的 @RequestMapping
快捷变体:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping
这些快捷方式是自定义注解,之所以提供这些快捷方式是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用默认匹配所有 HTTP 方法的 @RequestMapping
。在类级别仍然需要 @RequestMapping
来表达共享映射。
Note:
@RequestMapping
不能与在同一元素(类、接口或方法)上声明的其他@RequestMapping
注解一起使用。如果在同一元素上检测到多个@RequestMapping
注解,将记录一条警告,并且只使用第一个映射。这也适用于组合的@RequestMapping
注解,例如@GetMapping
、@PostMapping
等。
以下示例具有类型和方法级别的映射:
@RestController
@RequestMapping("/persons")
class PersonController {
@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
# b、模式
@RequestMapping
方法可以使用 URL 模式进行映射。有两种选择:
PathPattern
- 一个预先解析的模式,与 URL 路径匹配,URL 路径也被预解析为PathContainer
。专为 Web 使用而设计,此解决方案有效地处理编码和路径参数,并能高效地匹配。AntPathMatcher
- 将字符串模式与字符串路径匹配。这是最初的解决方案,也用于 Spring 配置中,用于在类路径、文件系统和其他位置选择资源。它的效率较低,并且字符串路径输入对于有效处理编码以及 URL 的其他问题是一个挑战。
PathPattern
是 Web 应用程序的推荐解决方案,并且是 Spring WebFlux 中唯一的选择。从 5.3 版本开始,它已启用在 Spring MVC 中使用,并且从 6.0 版本开始默认启用。
PathPattern
支持与 AntPathMatcher
相同的模式语法。此外,它还支持捕获模式,例如,{*spring}
,用于匹配路径末尾的 0 个或多个路径段。PathPattern
还限制了使用 **
匹配多个路径段,仅允许在模式末尾使用。这消除了在为给定请求选择最佳匹配模式时的许多歧义情况。有关完整的模式语法,请参阅 PathPattern (opens new window) 和 AntPathMatcher (opens new window)。
一些示例模式:
"/resources/ima?e.png"
- 匹配路径段中的一个字符"/resources/*.png"
- 匹配路径段中的零个或多个字符"/resources/**"
- 匹配多个路径段"/projects/{project}/versions"
- 匹配一个路径段并将其捕获为变量"/projects/{project:[a-z]+}/versions"
- 匹配并捕获带有正则表达式的变量
捕获的 URI 变量可以使用 @PathVariable
访问。例如:
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
你可以在类和方法级别声明 URI 变量,如以下示例所示:
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {
@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}
URI 变量会自动转换为适当的类型,否则会引发 TypeMismatchException
。默认情况下支持简单类型(int
、long
、Date
等),你可以注册对任何其他数据类型的支持。
你可以显式命名 URI 变量(例如,@PathVariable("customId")
),但是如果名称相同并且你的代码是用 -parameters
编译器标志编译的,则可以省略该细节。
语法 {varName:regex}
声明一个带有正则表达式的 URI 变量,其语法为 {varName:regex}
。 例如,给定 URL "/spring-web-3.0.5.jar"
,以下方法提取名称,版本和文件扩展名:
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}
URI 路径模式也可以具有嵌入的${…}
占位符,这些占位符在启动时通过使用PropertySourcesPlaceholderConfigurer
针对本地、系统、环境和其他属性源来解析。 例如,你可以使用它基于某些外部配置来参数化基本URL。
# c、形态比较
当多个模式匹配一个URL时,必须选择最佳匹配。 这是通过以下方式之一完成的,具体取决于是否为使用启用了已解析的PathPattern
:
PathPattern.SPECIFICITY_COMPARATOR
(opens new window)AntPathMatcher.getPatternComparator(String path)
(opens new window)
两者都有助于对具有更多特定模式的模式进行排序。 如果模式的URI变量数量(计为1),单个通配符(计为1)和双通配符(计为2)的计数较低,则模式更具体。 在给定相等分数的情况下,选择较长的模式。 在给定相同分数和长度的情况下,选择具有比通配符更多的URI变量的模式。
默认映射模式(/**
)不包括在评分中,并且始终最后排序。 此外,前缀模式(例如/public/**
)被认为不如没有双通配符的其他模式具体。
# d、后缀匹配
从 5.3 开始,默认情况下 Spring MVC 不再执行 .*
后缀模式匹配,其中映射到 /person
的控制器也隐式映射到 /person.*
。 因此,路径扩展名不再用于解释响应的请求内容类型 - 例如,/person.pdf
、/person.xml
等。
当浏览器过去发送难以一致解释的 Accept
标头时,以这种方式使用文件扩展名是必要的。 目前,这不再是必需的,使用 Accept
标头应该是首选。
随着时间的推移,文件名称扩展名的使用已被证明在多种方式上存在问题。 当与 URI 变量、路径参数和 URI 编码的使用重叠时,它可能导致歧义。 基于 URL 的授权和安全推理(有关更多详细信息,请参见下一节)也变得更加困难。
要在 5.3 之前的版本中完全禁用路径扩展名的使用,请设置以下内容:
useSuffixPatternMatching(false)
,请参阅下面章节的 PathMatchConfigurerfavorPathExtension(false)
,请参阅下面章节的 ContentNegotiationConfigurer
除了通过 "Accept"
标头以外,仍然可以通过其他方式请求内容类型,例如在浏览器中键入 URL 时。 路径扩展名的安全替代方法是使用查询参数策略。 如果必须使用文件扩展名,请考虑通过 ContentNegotiationConfigurer 的 mediaTypes
属性将它们限制为显式注册的扩展名列表。
# e、后缀匹配和 RFD
反射文件下载(RFD)攻击类似于 XSS,因为它依赖于在响应中反映的请求输入(例如,查询参数和URI变量)。 但是,RFD 攻击不是将 JavaScript 插入 HTML 中,而是依赖于浏览器切换以执行下载,并在稍后双击时将响应视为可执行脚本。
在 Spring MVC 中,@ResponseBody
和 ResponseEntity
方法存在风险,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展名来请求这些内容类型。 禁用后缀模式匹配并使用路径扩展名进行内容协商会降低风险,但不足以阻止 RFD 攻击。
为了防止 RFD 攻击,在呈现响应正文之前,Spring MVC 会添加一个 Content-Disposition:inline;filename=f.txt
标头,以建议固定且安全的下载文件。 仅当 URL 路径包含既不允许作为安全扩展也不明确注册用于内容协商的文件扩展名时,才会执行此操作。 但是,当 URL 直接键入浏览器时,它可能会产生副作用。
默认情况下,允许许多常见的路径扩展名作为安全扩展。 具有自定义 HttpMessageConverter
实现的应用程序可以显式注册用于内容协商的文件扩展名,以避免为这些扩展名添加 Content-Disposition
标头。
有关与 RFD 相关的其他建议,请参阅 CVE-2015-5211 (opens new window)。
# f、消费者 Media Types
你可以根据请求的 Content-Type
来缩小请求映射,如以下示例所示:
@PostMapping(path = "/pets", consumes = "application/json") // 使用 consumes 属性按内容类型缩小映射。
public void addPet(@RequestBody Pet pet) {
// ...
}
consumes
属性还支持否定表达式 - 例如,!text/plain
表示除 text/plain
以外的任何内容类型。
你可以在类级别声明一个共享的 consumes
属性。 但是,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 consumes
属性会覆盖而不是扩展类级别的声明。
Tip:
MediaType
为常用的 media 类型提供了常量,例如APPLICATION_JSON_VALUE
和APPLICATION_XML_VALUE
。
# g、生产者 Media Types
你可以根据 Accept
请求头和控制器方法生成的 content type 列表来缩小请求映射,如下例所示:
@GetMapping(path = "/pets/{petId}", produces = "application/json") // 使用 produces` 属性按 content type 缩小映射。
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}
媒体类型可以指定字符集。支持否定表达式 — 例如,'!text/plain' 表示除 “text/plain” 之外的任何内容类型。
你可以在类级别声明一个共享的 produces
属性。 但是,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 produces
属性会覆盖而不是扩展类级别的声明。
Tip:
MediaType
为常用的 media 类型提供了常量,例如APPLICATION_JSON_VALUE
和APPLICATION_XML_VALUE
。
# h、参数、标头
你可以根据请求参数条件来缩小请求映射。你可以测试请求参数是否存在 ( myParam
),测试请求参数是否缺失 (!myParam
),或者测试请求参数值是否为指定值( myParam=myValue
)。 下面的例子展示了如何测试指定的参数值:
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") // 测试 `myParam` 是否等于 `myValue`
public void findPet(@PathVariable String petId) {
// ...
}
你也可以对请求header使用相同的设置,如下例所示:
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // 测试 `myHeader` 是否等于 `myValue`
public void findPet(@PathVariable String petId) {
// ...
}
Tip: 你可以用headers的条件来匹配 HTTP 的
Content-Type
和Accept
,但是最好还是用 consumes 和 produces 来代替。
# i、HEAD, OPTIONS
@GetMapping
(和@RequestMapping(method=HttpMethod.GET)
)透明地支持HTTP HEAD以进行请求映射。 控制器方法不需要更改。 应用于jakarta.servlet.http.HttpServlet
的响应包装器可确保将Content-Length
标头设置为写入的字节数(而实际上不写入响应)。
默认情况下,HTTP OPTIONS是通过将Allow
响应标头设置为所有与URL模式匹配的@RequestMapping
方法中列出的HTTP方法列表来处理的。
对于没有HTTP方法声明的@RequestMapping
,Allow
标头设置为GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS
。 控制器方法应始终声明支持的HTTP方法(例如,通过使用特定的HTTP方法变体:@GetMapping
,@PostMapping
等)。
你可以显式地将@RequestMapping
方法映射到HTTP HEAD和HTTP OPTIONS,但在通常情况下没有必要这样做。
# j、自定义注释
Spring MVC 支持使用组合注解 (opens new window) 进行请求映射。 这些注解本身用 @RequestMapping
进行元注解,并组合起来以更窄、更具体的目的重新声明 @RequestMapping
属性的子集(或全部)。
@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
和 @PatchMapping
是组合注解的例子。 提供这些注解是因为,可以说,大多数控制器应映射到特定的 HTTP 方法,而不是使用默认可以匹配所有 HTTP 方法的 @RequestMapping
。 如果你需要如何实现组合注解的例子,可以查看这些注解是如何声明的。
Note:
@RequestMapping
不能与在同一元素(类、接口或方法)上声明的其他@RequestMapping
注解一起使用。 如果在同一元素上检测到多个@RequestMapping
注解,将会记录一个警告,并且只会使用第一个映射。 这也适用于 @GetMapping、@PostMapping 等组成的@RequestMapping
注解。
Spring MVC 还支持具有自定义请求匹配逻辑的自定义请求映射属性。 这是一个更高级的选项,需要继承 RequestMappingHandlerMapping
类并重写 getCustomMethodCondition
方法,你可以在该方法中检查自定义属性并返回你自己的 RequestCondition
。
# k、显式注册
你可以通过编程方式注册处理程序方法,你可以将它用于动态注册或者用于高级的使用场景,例如在不同的 URL 下注册同一个处理程序的不同的实例。 下面的示例程序注册了一个处理程序方法。
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) // 注入目标处理器和控制器的处理器映射。
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); // 准备请求映射元数据
Method method = UserHandler.class.getMethod("getUser", Long.class); // 获取处理程序方法。
mapping.registerMapping(info, handler, method); // 添加注册。
}
}
# l、@HttpExchange
@HttpExchange
的主要目的是用生成的代理抽象 HTTP 客户端代码,放置此类注解的 HTTP 接口 (opens new window) 是一个对客户端和服务器用例都中立的约定。 除了简化客户端代码外,在某些情况下,HTTP 接口可能是一种方便的服务器公开其 API 以供客户端访问的方式。 这会导致客户端和服务器之间耦合的增加,通常不是一个好的选择,特别是对于公共 API 而言,但对于内部 API 而言,可能正是目标。 这是 Spring Cloud 中常用的一种方法,也是为什么 @HttpExchange
被支持作为 @RequestMapping
的替代方案,用于控制器类中的服务器端处理。
例如:
@HttpExchange("/persons")
interface PersonService {
@GetExchange("/{id}")
Person getPerson(@PathVariable Long id);
@PostExchange
void add(@RequestBody Person person);
}
@RestController
class PersonController implements PersonService {
public Person getPerson(@PathVariable Long id) {
// ...
}
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}
@HttpExchange
和 @RequestMapping
有所不同。 @RequestMapping
可以通过路径模式、HTTP 方法等映射到任意数量的请求,而 @HttpExchange
声明一个具有具体 HTTP 方法、路径和内容类型的单个 endpoint。
对于方法参数和返回值,通常,@HttpExchange
支持 @RequestMapping
支持的方法参数的一个子集。 值得注意的是,它排除了任何服务器端特定的参数类型。 有关详细信息,请参阅 @HttpExchange
(opens new window)和@RequestMapping
的列表。
@HttpExchange
还支持一个 headers()
参数,该参数接受类似 "name=value"
的键值对,就像客户端的 @RequestMapping(headers={})
中一样。 在服务器端,这将扩展到RequestMapping支持的完整语法。
# 4.3、控制器方法
@RequestMapping
处理方法具有灵活的签名,可以从一系列支持的控制器方法参数和返回值中进行选择。
# a、方法参数
本文介绍了受支持的控制器方法参数。所有参数均不支持响应式类型。
JDK 8 的 java.util.Optional
支持作为方法参数,可以与具有 required
属性的注解(例如,@RequestParam
、@RequestHeader
等)结合使用,等同于 required=false
。
控制器方法参数 | 描述 |
---|---|
WebRequest , NativeWebRequest | 对请求参数以及请求和会话属性的通用访问,无需直接使用 Servlet API。 |
jakarta.servlet.ServletRequest , jakarta.servlet.ServletResponse | 选择任何特定的请求或响应类型,例如,ServletRequest 、HttpServletRequest 或 Spring 的 MultipartRequest 、MultipartHttpServletRequest 。 |
jakarta.servlet.http.HttpSession | 强制存在会话。因此,此类参数永远不会为 null 。请注意,会话访问不是线程安全的。如果允许多个请求同时访问会话,请考虑将 RequestMappingHandlerAdapter 实例的 synchronizeOnSession 标志设置为 true 。 |
jakarta.servlet.http.PushBuilder | Servlet 4.0 推送构建器 API,用于编程式 HTTP/2 资源推送。请注意,根据 Servlet 规范,如果客户端不支持该 HTTP/2 功能,则注入的 PushBuilder 实例可能为 null。 |
java.security.Principal | 当前经过身份验证的用户,如果已知,则可能是特定的 Principal 实现类。请注意,此参数不会被立即解析,如果它被注解修饰,以便允许自定义解析器在回退到通过 HttpServletRequest#getUserPrincipal 进行默认解析之前解析它。例如,Spring Security 的 Authentication 实现了 Principal ,并且将通过 HttpServletRequest#getUserPrincipal 注入为 Principal ,除非它也被 Authentication#getPrincipal 通过自定义Spring Security解析器解决的@AuthenticationPrincipal 注解 |
HttpMethod | 请求的 HTTP 方法。 |
java.util.Locale | 当前请求的区域设置,由最具体的 LocaleResolver 提供(实际上,是配置的 LocaleResolver 或 LocaleContextResolver )。 |
java.util.TimeZone + java.time.ZoneId | 与当前请求关联的时区,由 LocaleContextResolver 确定。 |
java.io.InputStream , java.io.Reader | 用于访问 Servlet API 公开的原始请求体。 |
java.io.OutputStream , java.io.Writer | 用于访问 Servlet API 公开的原始响应体。 |
@PathVariable | 用于访问 URI 模板变量。 |
@MatrixVariable | 用于访问 URI 路径段中的名称-值对。 |
@RequestParam | 用于访问 Servlet 请求参数,包括 multipart 文件。参数值将转换为声明的方法参数类型。请注意,对于简单参数值,@RequestParam 的使用是可选的。 |
@RequestHeader | 用于访问请求标头。标头值将转换为声明的方法参数类型。 |
@CookieValue | 用于访问 Cookie。Cookie 值将转换为声明的方法参数类型。 |
@RequestBody | 用于访问 HTTP 请求体。正文内容使用 HttpMessageConverter 实现转换为声明的方法参数类型。 |
HttpEntity<B> | 用于访问请求标头和正文。正文通过 HttpMessageConverter 转换。 |
@RequestPart | 用于访问 multipart/form-data 请求中的一部分,使用 HttpMessageConverter 转换该部分的正文。 |
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap | 用于访问 HTML 控制器中使用的模型,并作为视图呈现的一部分公开给模板。 |
RedirectAttributes | 指定在重定向的情况下使用的属性(即,附加到查询字符串)和要临时存储的 flash 属性,直到重定向后的请求。 |
@ModelAttribute | 用于访问模型中现有的属性(如果不存在,则实例化),并应用数据绑定和验证。请注意,@ModelAttribute 的使用是可选的(例如,设置其属性)。 |
Errors , BindingResult | 用于访问命令对象(即,@ModelAttribute 参数)的验证和数据绑定中的错误,或者来自验证 @RequestBody 或 @RequestPart 参数的错误。必须在验证的方法参数之后立即声明 Errors 或 BindingResult 参数。 |
SessionStatus + 类级别的 @SessionAttributes | 用于标记表单处理完成,这将触发通过类级别的 @SessionAttributes 注解声明的会话属性的清理。 |
UriComponentsBuilder | 用于准备相对于当前请求的主机、端口、方案、上下文路径和 servlet 映射的文字部分的 URL。 |
@SessionAttribute | 用于访问任何会话属性,与存储在会话中的模型属性(作为类级别 @SessionAttributes 声明的结果)相反。 |
@RequestAttribute | 用于访问请求属性。 |
任何其他参数 | 如果方法参数与此表中的任何早期值都不匹配,并且它是一个简单类型(由 BeanUtils#isSimpleProperty 确定),则将其解析为 @RequestParam 。否则,将其解析为 @ModelAttribute 。 |
# b、返回值
本节描述了controller方法支持的返回值类型。所有返回值类型都支持响应式类型。
Controller方法返回值 | 描述 |
---|---|
@ResponseBody | 返回值通过 HttpMessageConverter 的实现进行转换,并写入响应中。 |
HttpEntity<B> , ResponseEntity<B> | 返回值指定完整的响应(包括HTTP头和body),通过 HttpMessageConverter 的实现进行转换,并写入响应中。 |
HttpHeaders | 用于返回仅包含header而没有body的响应。 |
ErrorResponse | 要使用body中的详细信息呈现 RFC 9457 错误响应,请参阅前面章节的错误响应。 |
ProblemDetail | 要使用body中的详细信息呈现 RFC 9457 错误响应,请参阅前面章节的错误响应。 |
String | 视图名称,通过 ViewResolver 的实现进行解析,并与隐式模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。handler方法还可以通过声明一个 Model 参数来以编程方式丰富模型。 |
View | 一个 View 实例,用于与隐式模型(通过命令对象和 @ModelAttribute 方法确定)一起进行渲染。handler方法还可以通过声明一个 Model 参数来以编程方式丰富模型。 |
java.util.Map , org.springframework.ui.Model | 要添加到隐式模型的属性,视图名称通过 RequestToViewNameTranslator 隐式确定。 |
@ModelAttribute | 要添加到模型的属性,视图名称通过 RequestToViewNameTranslator 隐式确定。注意:@ModelAttribute 是可选的。 |
ModelAndView object | 要使用的视图和模型属性,以及可选的响应状态。 |
FragmentsRendering , Collection<ModelAndView> | 用于渲染一个或多个片段,每个片段都有自己的视图和模型。 |
void | 如果一个 void 返回类型(或 null 返回值)的方法同时拥有一个 ServletResponse 、一个 OutputStream 参数或一个 @ResponseStatus 注解,则认为它已经完全处理了该响应。如果controller已经做过积极的 ETag 或 lastModified 时间戳检查,情况也是如此。如果以上都不是真的,void 返回类型也可以表示REST controller的“没有响应体”,或HTML controller的默认视图名称选择。 |
DeferredResult<V> | 从任何线程异步生成任何前面的返回值 — 例如,作为某个事件或回调的结果。 |
Callable<V> | 在Spring MVC管理的线程中异步生成任何上述返回值。 |
ListenableFuture<V> , java.util.concurrent.CompletionStage<V> , java.util.concurrent.CompletableFuture<V> | 作为 DeferredResult 的替代方案,为了方便起见(例如,当底层服务返回其中一个时)。 |
ResponseBodyEmitter , SseEmitter | 异步发送一个对象流,通过 HttpMessageConverter 的实现写入响应。也支持作为 ResponseEntity 的body。 |
StreamingResponseBody | 异步写入响应 OutputStream 。也支持作为 ResponseEntity 的body。 |
通过 ReactiveAdapterRegistry 注册的Reactor和其他响应式类型 | 单值类型(例如 Mono )相当于返回 DeferredResult 。多值类型(例如 Flux )可以被视为流,具体取决于请求的媒体类型(例如 "text/event-stream"、"application/json+stream"),否则会被收集到一个List中并作为单个值呈现。 |
其他返回值 | 如果返回值以任何其他方式保持未解析的状态,则它被视为模型属性,除非它是由 BeanUtils#isSimpleProperty 确定的简单类型,在这种情况下,它保持未解析的状态。 |
# c、类型转换
本文档介绍了Spring MVC中,控制器方法参数的类型转换机制,特别是在处理基于 String
类型的请求输入时。
如果控制器方法中的某些注解参数,代表的是基于 String
类型的请求输入(例如 RequestParam
、RequestHeader
、PathVariable
、MatrixVariable
和 CookieValue
),而该参数声明的类型不是 String
,则需要进行类型转换。
在这种情况下,系统会自动根据配置的转换器进行类型转换。默认情况下,支持简单类型(int
、long
、Date
等)。你可以通过 WebDataBinder
或向 FormattingConversionService
注册 Formatters
来自定义类型转换。
类型转换中一个实际的问题是,如何处理空的 String
源值。如果空值经过类型转换后变为 null
,则将其视作缺失值。对于 Long
、UUID
以及其他目标类型,可能会出现这种情况。如果希望注入 null
值,可以使用参数注解上的 required
标志,或者将参数声明为 @Nullable
。
注意: 从 5.3 版本开始,即使在类型转换之后,也会强制执行非空参数。如果你的处理方法需要接受 null
值,可以将参数声明为 @Nullable
,或者在对应的 @RequestParam
等注解中将其标记为 required=false
。这是一个最佳实践,也是解决在 5.3 版本升级中遇到的回归问题的推荐方案。
或者,你可以专门处理因为标记为required的@PathVariable
而产生的 MissingPathVariableException
异常。类型转换后的 null
值会被视作空的原始值,因此会抛出对应的 Missing…Exception
异常变体。
# d、变量
RFC 3986 讨论了路径段中的名称-值对。在 Spring MVC 中,我们基于 Tim Berners-Lee 的一篇“旧帖子 (opens new window)” 将其称为“矩阵变量”,但它们也可以被称为 URI 路径参数。
矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,/cars;color=red,green;year=2012
)。也可以通过重复的变量名来指定多个值(例如,color=red;color=green;color=blue
)。
如果 URL 期望包含矩阵变量,则控制器方法的请求映射必须使用 URI 变量来屏蔽该变量内容,并确保请求可以成功匹配,而与矩阵变量的顺序和存在无关。以下示例使用矩阵变量:
// GET /pets/42;q=11;r=22
@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {
// petId == 42
// q == 11
}
鉴于所有路径段都可能包含矩阵变量,您有时可能需要消除矩阵变量预期存在的路径变量的歧义。以下示例展示了如何做到这一点:
// GET /owners/42;q=11/pets/21;q=22
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable(name="q", pathVar="ownerId") int q1,
@MatrixVariable(name="q", pathVar="petId") int q2) {
// q1 == 11
// q2 == 22
}
可以将矩阵变量定义为可选的,并指定一个默认值,如下例所示:
// GET /pets/42
@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {
// q == 1
}
要获取所有矩阵变量,可以使用 MultiValueMap
,如下例所示:
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23
@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
@MatrixVariable MultiValueMap<String, String> matrixVars,
@MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {
// matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
// petMatrixVars: ["q" : 22, "s" : 23]
}
请注意,您需要启用矩阵变量的使用。在 MVC Java 配置中,您需要通过 路径匹配 设置一个 UrlPathHelper
,其中 removeSemicolonContent=false
。在 MVC XML 命名空间中,您可以设置 <mvc:annotation-driven enable-matrix-variables="true"/>
。
# e、@RequestParam
你可以使用 @RequestParam
注解将 Servlet 请求参数(即,查询参数或表单数据)绑定到控制器中的方法参数。
以下示例展示了如何使用:
@Controller
@RequestMapping("/pets")
public class EditPetForm {
// ...
@GetMapping
public String setupForm(@RequestParam("petId") int petId, Model model) { // (1)
Pet pet = this.clinic.loadPet(petId);
model.addAttribute("pet", pet);
return "petForm";
}
// ...
}
- 使用
@RequestParam
绑定petId
。
默认情况下,使用此注解的方法参数是必需的,但是你可以通过将 @RequestParam
注解的 required
标志设置为 false
或使用 java.util.Optional
包装器声明参数来指定方法参数是可选的。
如果目标方法参数类型不是 String
,则会自动应用类型转换。
将参数类型声明为数组或列表允许解析同一参数名称的多个参数值。
当 @RequestParam
注解声明为 Map<String, String>
或 MultiValueMap<String, String>
时,如果未在注解中指定参数名称,则该映射将填充每个给定参数名称的请求参数值。 以下示例展示了如何使用表单数据处理来执行此操作:
@Controller
@RequestMapping("/pets")
class EditPetForm {
// ...
@PostMapping(path = "/process", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String processForm(@RequestParam MultiValueMap<String, String> params) {
// ...
}
// ...
}
请注意,使用 @RequestParam
是可选的(例如,设置其属性)。 默认情况下,任何属于简单值类型的参数(由 BeanUtils#isSimpleProperty (opens new window) 确定)并且未被任何其他参数解析器解析,都将被视为已使用 @RequestParam
注解。
# f、@RequestHeader
你可以使用 @RequestHeader
注解将请求头绑定到控制器中的方法参数。
考虑以下带 header 的请求:
Host localhost:8080
Accept text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding gzip,deflate
Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive 300
以下示例获取 Accept-Encoding
和 Keep-Alive
header 的值:
@GetMapping("/demo")
public void handle(
@RequestHeader("Accept-Encoding") String encoding, // (1) 获取 `Accept-Encoding` header 的值
@RequestHeader("Keep-Alive") long keepAlive) { // (2) 获取 `Keep-Alive` header 的值
//...
}
如果目标方法参数类型不是 String
,则会自动应用类型转换。
当在 Map<String, String>
、MultiValueMap<String, String>
或 HttpHeaders
参数上使用 @RequestHeader
注解时,该 map 将填充所有 header 值。
提示: 内置支持可用于将逗号分隔的字符串转换为字符串数组或集合,或类型转换系统已知的其他类型。例如,使用 @RequestHeader("Accept")
注释的方法参数可以是 String
类型,也可以是 String[]
或 List<String>
类型。
# g、@CookieValue
你可以使用 @CookieValue
注解将 HTTP Cookie 的值绑定到 Controller 方法的参数上。
考虑如下包含 Cookie 的请求:
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
下面的示例展示了如何获取 Cookie 的值:
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { // (1)
//...
}
- 获取
JSESSIONID
Cookie 的值。
如果目标方法参数的类型不是 String
,则会自动进行类型转换。
# h、@ModelAttribute
@ModelAttribute
方法参数注解将请求参数、URI 路径变量和请求头绑定到模型对象上。例如:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { // (1)
// 方法逻辑...
}
- 绑定到一个
Pet
实例。
请求参数是一个 Servlet API 概念,包括来自请求体的表单数据和查询参数。URI 变量和请求头也包含在内,但前提是它们不覆盖具有相同名称的请求参数。破折号会从请求头名称中去除。
上面的 Pet
实例可能是:
- 从模型中访问,它可能已经被
@ModelAttribute
方法添加。 - 如果模型属性在类级别的
@SessionAttributes
注解中列出,则从 HTTP 会话中访问。 - 如果模型属性名称与请求值的名称(如路径变量或请求参数)匹配,则通过
Converter
获取(示例如下)。 - 通过默认构造函数实例化。
- 通过“主构造函数”实例化,其参数与 Servlet 请求参数匹配。参数名称通过字节码中的运行时保留参数名称确定。
如上所述,如果模型属性名称与请求值的名称(如路径变量或请求参数)匹配,并且存在兼容的 Converter<String, T>
,则可以使用 Converter<String, T>
获取模型对象。在下面的示例中,模型属性名称 account
与 URI 路径变量 account
匹配,并且注册了一个 Converter<String, Account>
,它可能从持久化存储中检索它:
@PutMapping("/accounts/{account}")
public String save(@ModelAttribute("account") Account account) { // (1)
// ...
}
默认情况下,构造函数和属性数据绑定都会应用。但是,模型对象设计需要仔细考虑,并且出于安全原因,建议使用专门为 Web 绑定定制的对象,或者仅应用构造函数绑定。如果仍然必须使用属性绑定,则应设置 allowedFields 模式以限制可以设置哪些属性。
使用构造函数绑定时,您可以通过 @BindParam
注解自定义请求参数名称。例如:
class Account {
private final String firstName;
public Account(@BindParam("first-name") String firstName) {
this.firstName = firstName;
}
}
注意:
@BindParam
也可以放在与构造函数参数对应的字段上。虽然@BindParam
开箱即用,但您也可以通过在DataBinder
上设置DataBinder.NameResolver
来使用不同的注解。
构造函数绑定支持 List
、Map
和数组参数,这些参数可以从单个字符串转换(例如,逗号分隔的列表),或者基于索引键(例如 accounts[2].name
或 account[KEY].name
)。
在某些情况下,您可能希望访问模型属性而无需数据绑定。对于这种情况,您可以将 Model
注入到控制器中并直接访问它,或者,也可以设置 @ModelAttribute(binding=false)
,如以下示例所示:
@ModelAttribute
public AccountForm setUpForm() {
return new AccountForm();
}
@ModelAttribute
public Account findAccount(@PathVariable String accountId) {
return accountRepository.findOne(accountId);
}
@PostMapping("update")
public String update(AccountForm form, BindingResult result,
@ModelAttribute(binding=false) Account account) { // (1)
// ...
}
- 设置
@ModelAttribute(binding=false)
。
如果数据绑定导致错误,默认情况下会引发 MethodArgumentNotValidException
,但是您也可以在 @ModelAttribute
旁边立即添加一个 BindingResult
参数,以便在控制器方法中处理此类错误。例如:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
- 在
@ModelAttribute
旁边添加BindingResult
。
您可以通过添加 jakarta.validation.Valid
注解或 Spring 的 @Validated
注解,在数据绑定后自动应用验证。例如:
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // (1)
if (result.hasErrors()) {
return "petForm";
}
// ...
}
- 验证
Pet
实例。
如果在 @ModelAttribute
之后没有 BindingResult
参数,则会引发带有验证错误的 MethodArgumentNotValueException
。但是,如果方法验证适用,因为其他参数具有 @jakarta.validation.Constraint
注解,则会引发 HandlerMethodValidationException
。
提示:
使用
@ModelAttribute
是可选的。默认情况下,任何不由任何其他参数解析器解析 并且 不是简单值类型的参数(由BeanUtils#isSimpleProperty
确定)都被视为隐式的@ModelAttribute
。
警告:
使用 GraalVM 编译为本机映像时,上述隐式
@ModelAttribute
支持不允许对相关数据绑定反射提示进行适当的提前推断。因此,建议使用@ModelAttribute
显式注解方法参数以用于 GraalVM 本机映像。
# i、@SessionAttributes
@SessionAttributes
用于在请求之间将模型属性存储在 HTTP Servlet 会话中。它是一个类型级别的注解,用于声明特定控制器使用的会话属性。 通常,它会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以便后续的请求可以访问。
以下示例使用了 @SessionAttributes
注解:
@Controller
@SessionAttributes("pet") // (1)
public class EditPetForm {
// ...
}
- 使用
@SessionAttributes
注解。
在第一个请求中,当名为 pet
的模型属性添加到模型中时,它会自动提升并保存在 HTTP Servlet 会话中。它会一直保存在那里,直到另一个控制器方法使用 SessionStatus
方法参数来清除存储,如下面的示例所示:
@Controller
@SessionAttributes("pet") // (1)
public class EditPetForm {
// ...
@PostMapping("/pets/{id}")
public String handle(Pet pet, BindingResult errors, SessionStatus status) {
if (errors.hasErrors) {
// ...
}
status.setComplete(); // (2)
// ...
}
}
- 将
Pet
值存储在 Servlet 会话中。 - 从 Servlet 会话中清除
Pet
值。
# j、@SessionAttribute
如果需要访问由全局管理的(即在控制器之外,例如由过滤器管理的)且可能存在也可能不存在的预先存在的会话属性,则可以在方法参数上使用@SessionAttribute
注解,如以下示例所示:
@RequestMapping("/")
public String handle(@SessionAttribute User user) { // (1)
// ...
}
- 使用
@SessionAttribute
注解。
对于需要添加或删除会话属性的用例,请考虑将org.springframework.web.context.request.WebRequest
或jakarta.servlet.http.HttpSession
注入到控制器方法中。
对于在会话中临时存储模型属性以作为控制器工作流的一部分,请考虑使用@SessionAttributes
,如@SessionAttributes
中所述。
# k、@RequestAttribute
@RequestAttribute
注解类似于 @SessionAttribute
,可以用来访问之前创建的请求属性(例如,通过 Servlet Filter
或 HandlerInterceptor
)。
@GetMapping("/")
public String handle(@RequestAttribute Client client) { // (1)
// ...
}
- 使用
@RequestAttribute
注解。
# l、重定向属性
默认情况下,所有模型属性都被视为在重定向 URL 中公开为 URI 模板变量。在剩余的属性中,那些属于基本类型、基本类型集合或基本类型数组的属性会自动附加为查询参数。
如果模型实例是专门为重定向准备的,那么将基本类型属性作为查询参数附加可能是所期望的结果。然而,在注解的控制器中,模型可能包含为了渲染目的而添加的额外属性(例如,下拉字段的值)。为了避免这些属性出现在 URL 中,@RequestMapping
方法可以声明一个 RedirectAttributes
类型的参数,并使用它来指定要提供给 RedirectView
的确切属性。如果该方法确实发生了重定向,那么会使用 RedirectAttributes
的内容。否则,会使用模型的内容。
RequestMappingHandlerAdapter
提供了一个名为 ignoreDefaultModelOnRedirect
的标志,您可以使用它来指示如果控制器方法重定向,则永远不应使用默认 Model
的内容。相反,控制器方法应该声明一个 RedirectAttributes
类型的属性,或者,如果没有这样做,则不应将任何属性传递给 RedirectView
。MVC 命名空间和 MVC Java 配置都将此标志设置为 false
,以保持向后兼容性。但是,对于新的应用程序,我们建议将其设置为 true
。
请注意,当前请求中的 URI 模板变量在扩展重定向 URL 时会自动可用,您无需通过 Model
或 RedirectAttributes
显式添加它们。以下示例展示了如何定义重定向:
@PostMapping("/files/{path}")
public String upload(...) {
// ...
return "redirect:files/{path}";
}
另一种将数据传递到重定向目标的方法是使用 flash 属性。与其他重定向属性不同,flash 属性保存在 HTTP 会话中(因此,不会出现在 URL 中)。
# m、属性
Flash 属性提供了一种方式,让一个请求存储一些属性,以供另一个请求使用。这在重定向时最常见,例如,Post-Redirect-Get 模式。 Flash 属性在重定向之前被临时保存(通常在会话中),以便在重定向之后的请求中可用,并立即被移除。
Spring MVC 提供了两个主要的抽象来支持 flash 属性。 FlashMap
用于保存 flash 属性,而 FlashMapManager
用于存储、检索和管理 FlashMap
实例。
Flash 属性支持总是“开启”的,不需要显式启用。 但是,如果没有使用,它永远不会导致 HTTP 会话创建。 在每个请求上,都有一个“输入”FlashMap
,其中包含从先前请求传递的属性(如果有),以及一个“输出”FlashMap
,其中包含要为后续请求保存的属性。 可以通过 RequestContextUtils
中的静态方法从 Spring MVC 中的任何位置访问这两个 FlashMap
实例。
注解控制器通常不需要直接使用 FlashMap
。 相反,@RequestMapping
方法可以接受 RedirectAttributes
类型的参数,并使用它来为重定向场景添加 flash 属性。 通过 RedirectAttributes
添加的 Flash 属性会自动传播到“输出”FlashMap。 类似地,在重定向之后,来自“输入”FlashMap
的属性会自动添加到服务目标 URL 的控制器的 Model
中。
匹配请求到 Flash 属性
Flash 属性的概念存在于许多其他 Web 框架中,并且已被证明有时会暴露于并发问题。 这是因为,根据定义,flash 属性将被存储到下一个请求。 然而,“下一个”请求可能不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,flash 属性会被过早地删除。
为了减少这种问题的可能性,
RedirectView
会自动使用目标重定向 URL 的路径和查询参数来“标记”FlashMap
实例。 反过来,默认的FlashMapManager
在查找“输入”FlashMap
时,会将该信息与传入的请求进行匹配。这并不能完全消除并发问题的可能性,但通过重定向 URL 中已有的信息大大降低了这种可能性。 因此,我们建议主要将 flash 属性用于重定向场景。
# n、Multipart
在启用了 MultipartResolver
之后,带有 multipart/form-data
的 POST 请求的内容会被解析,并且可以像常规请求参数一样访问。以下示例访问了一个常规表单字段和一个上传的文件:
@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";
}
}
声明参数类型为 List<MultipartFile>
可以解析同一参数名称的多个文件。
当 @RequestParam
注解声明为 Map<String, MultipartFile>
或 MultiValueMap<String, MultipartFile>
,且注解中没有指定参数名称时,该 Map 将填充每个给定参数名称的 multipart 文件。
注意:
使用 Servlet multipart 解析,你也可以声明
jakarta.servlet.http.Part
而不是 Spring 的MultipartFile
,作为方法参数或集合值类型。
你还可以使用 multipart 内容作为数据绑定的一部分,绑定到一个命令对象。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下例所示:
class MyForm {
private String name;
private MultipartFile file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
if (!form.getFile().isEmpty()) {
byte[] bytes = form.getFile().getBytes();
// store the bytes somewhere
// 将字节存储到某个地方
return "redirect:uploadSuccess";
}
return "redirect:uploadFailure";
}
}
Multipart 请求也可以从非浏览器客户端在 RESTful 服务场景中提交。以下示例显示了一个带有 JSON 的文件:
POST /someUrl
Content-Type: multipart/mixed
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
{
"name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
你可以使用 @RequestParam
将 "meta-data" 部分作为 String
访问,但你可能希望从 JSON 中反序列化它(类似于 @RequestBody
)。 使用 @RequestPart
注解在使用 HttpMessageConverter
转换 multipart 后访问它:
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata,
@RequestPart("file-data") MultipartFile file) {
// ...
}
你可以将 @RequestPart
与 jakarta.validation.Valid
结合使用,或者使用 Spring 的 @Validated
注解,这两个注解都会导致应用标准的 Bean 验证。默认情况下,验证错误会导致 MethodArgumentNotValidException
异常,该异常会转换为 400 (BAD_REQUEST) 响应。或者,你可以在控制器内通过 Errors
或 BindingResult
参数在本地处理验证错误,如下例所示:
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) {
// ...
}
如果由于其他参数具有 @Constraint
注解而应用了方法验证,则会引发 HandlerMethodValidationException
。
# o、@RequestBody
你可以使用 @RequestBody
注解来读取请求体,并通过 HttpMessageConverter
将其反序列化为一个 Object
。以下示例展示了如何使用 @RequestBody
参数:
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
// ...
}
你可以使用 MVC 配置的 消息转换器选项来配置或自定义消息转换。
注意: 应该使用 @RequestParam 读取表单数据,而不是
@RequestBody
。 因为在Servlet API中,请求参数的访问会导致请求体被解析,从而导致无法再次读取请求体,所以@RequestBody
并非总是可靠。
你可以将 @RequestBody
与 jakarta.validation.Valid
或 Spring 的 @Validated
注解结合使用,它们都会触发标准的 Bean 验证。默认情况下,验证错误会抛出 MethodArgumentNotValidException
,并将其转换为 400 (BAD_REQUEST) 响应。或者,你也可以通过 Errors
或 BindingResult
参数在控制器本地处理验证错误,如下例所示:
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Account account, Errors errors) {
// ...
}
如果由于其他参数具有 @Constraint
注解而应用方法验证,则会引发 HandlerMethodValidationException
。
# p、HttpEntity
HttpEntity
或多或少等同于使用 @RequestBody
,但它是基于一个容器对象,该对象公开请求头和正文。以下示例展示了如何使用:
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
// ...
}
# q、@ResponseBody
你可以使用方法上的 @ResponseBody
注解,通过 HttpMessageConverter
将返回结果序列化到响应体中。以下示例展示了如何使用:
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
// ...
}
@ResponseBody
也支持在类级别使用,此时它会被该控制器中的所有方法继承。@RestController
实际上就是这样使用的,它是一个用 @Controller
和 @ResponseBody
标记的组合注解(meta-annotation)。
可以返回 Resource
对象来提供文件内容,将提供的 resource 的 InputStream
内容复制到 response 的 OutputStream
。注意,InputStream
应该由 Resource
句柄延迟检索,以便在复制到响应后可靠地关闭它。如果为此使用 InputStreamResource
,请确保使用按需 InputStreamSource
(例如,通过检索实际 InputStream
的 lambda 表达式)构造它。
你可以将 @ResponseBody
方法与 JSON 序列化视图结合使用。
# r、ResponseEntity
ResponseEntity
类似于 @ResponseBody
,但增加了状态码和头部信息。例如:
@GetMapping("/something")
public ResponseEntity<String> handle() {
String body = ... ;
String etag = ... ;
return ResponseEntity.ok().eTag(etag).body(body);
}
通常,body 会作为一个 value object 提供,并通过注册的 HttpMessageConverters
渲染为相应的响应表示形式(例如 JSON)。
对于文件内容,可以返回 ResponseEntity<Resource>
,将提供的 resource 的 InputStream
内容复制到响应的 OutputStream
。请注意,为了在复制到响应后可靠地关闭 InputStream
,应该通过 Resource
句柄延迟检索 InputStream
。如果为此目的使用 InputStreamResource
,请确保使用按需 InputStreamSource
(例如,通过检索实际 InputStream
的 lambda 表达式)构造它。此外,只有结合自定义 contentLength()
实现才支持 InputStreamResource
的自定义子类,该实现避免为此目的消耗流。
Spring MVC 支持使用单个 value 异步生成 ResponseEntity
,以及用于 body 的单值和多值 reactive type。 这允许以下类型的异步响应:
ResponseEntity<Mono<T>>
或ResponseEntity<Flux<T>>
立即告知响应状态和头部信息,而 body 将在稍后异步提供。如果 body 由 0..1 个值组成,则使用Mono
;如果它可以生成多个值,则使用Flux
。Mono<ResponseEntity<T>>
异步地在稍后提供所有三个内容:响应状态、头部信息和 body。这允许响应状态和头部根据异步请求处理的结果而变化。
# s、JSON
Spring 提供了对 Jackson JSON 库的支持。
# 视图
Spring MVC 提供了对 Jackson 的序列化视图 (opens new window) 的内置支持,允许只渲染 Object
中的一部分字段。 要将其与 @ResponseBody
或 ResponseEntity
控制器方法一起使用,可以使用 Jackson 的 @JsonView
注解来激活序列化视图类,如下例所示:
@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
允许使用视图类数组,但每个控制器方法只能指定一个。 如果需要激活多个视图,可以使用组合接口。
如果希望以编程方式执行上述操作,而不是声明 @JsonView
注解,请使用 MappingJacksonValue
包装返回值,并使用它来提供序列化视图:
@RestController
public class UserController {
@GetMapping("/user")
public MappingJacksonValue getUser() {
User user = new User("eric", "7!jd#h23");
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(User.WithoutPasswordView.class);
return value;
}
}
对于依赖视图解析的控制器,可以将序列化视图类添加到模型中,如下例所示:
@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";
}
}
# 4.4、Model
你可以使用@ModelAttribute
注解:
- 在
@RequestMapping
方法中的方法参数上,用于创建或访问模型中的Object
,并通过WebDataBinder
将其绑定到请求。 - 作为
@Controller
或@ControllerAdvice
类中的方法级别注解,用于在任何@RequestMapping
方法调用之前初始化模型。 - 在
@RequestMapping
方法上,用于标记其返回值是一个模型属性。
本节讨论@ModelAttribute
方法,即上述列表中的第二项。一个控制器可以有任意数量的@ModelAttribute
方法。所有这些方法都会在同一个控制器中的@RequestMapping
方法之前被调用。@ModelAttribute
方法也可以通过@ControllerAdvice
在多个控制器之间共享。
@ModelAttribute
方法具有灵活的方法签名。它们支持与@RequestMapping
方法相同的许多参数,但@ModelAttribute
本身或与请求体相关的任何内容除外。
以下示例展示了一个@ModelAttribute
方法:
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// 添加更多 ...
}
以下示例仅添加一个属性:
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
注意: 如果未显式指定名称,则会根据
Object
类型选择默认名称,如Conventions (opens new window)的javadoc中所述。 你始终可以通过使用重载的addAttribute
方法或通过@ModelAttribute
(对于返回值)上的name
属性来分配显式名称。
你还可以将@ModelAttribute
用作@RequestMapping
方法上的方法级别注解,在这种情况下,@RequestMapping
方法的返回值被解释为模型属性。 这通常不是必需的,因为这是HTML控制器中的默认行为,除非返回值是一个String
,否则该String
将被解释为视图名称。@ModelAttribute
还可以自定义模型属性名称,如以下示例所示:
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
# 4.5、@InitBinder
@Controller
或 @ControllerAdvice
类可以拥有 @InitBinder
方法来初始化 WebDataBinder
实例,进而可以实现以下功能:
- 将请求参数绑定到模型对象。
- 将请求值从字符串转换为对象属性类型。
- 在渲染 HTML 表单时,将模型对象属性格式化为字符串。
在 @Controller
中,DataBinder
的自定义设置在控制器内部局部生效,或者甚至可以通过注解按名称引用特定的模型属性。在 @ControllerAdvice
中,自定义设置可以应用于全部或部分控制器。
你可以在 DataBinder
中注册 PropertyEditor
、Converter
和 Formatter
组件来进行类型转换。或者,你可以使用 MVC 配置 在全局共享的 FormattingConversionService
中注册 Converter
和 Formatter
组件。
@InitBinder
方法可以拥有与 @RequestMapping
方法相同的参数,但有一个显著的例外:@ModelAttribute
。通常,这些方法会有一个 WebDataBinder
参数(用于注册)和一个 void
返回值,例如:
@Controller
public class FormController {
@InitBinder // (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
- 定义一个
@InitBinder
方法。
或者,当你通过共享的 FormattingConversionService
使用基于 Formatter
的设置时,你可以重用相同的方法并注册特定于控制器的 Formatter
实现,如以下示例所示:
@Controller
public class FormController {
@InitBinder // (1)
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}
// ...
}
- 在自定义格式化器上定义
@InitBinder
方法。
# a、模型设计
Web 请求的数据绑定 (opens new window)涉及将请求参数绑定到模型对象。默认情况下,请求参数可以绑定到模型对象的任何公共属性,这意味着恶意客户端可以为模型对象图中存在的属性提供额外的值,但这些属性不应被设置。这就是为什么模型对象设计需要仔细考虑的原因。
一个好的做法是使用专用模型对象,而不是暴露你的域模型,例如 JPA 或 Hibernate 实体,用于 Web 数据绑定。例如,在用于更改电子邮件地址的表单上,创建一个 ChangeEmailForm
模型对象,该对象仅声明输入所需的属性:
public class ChangeEmailForm {
private String oldEmailAddress;
private String newEmailAddress;
public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}
public String getOldEmailAddress() {
return this.oldEmailAddress;
}
public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}
public String getNewEmailAddress() {
return this.newEmailAddress;
}
}
另一个好的做法是应用构造器绑定 (opens new window),该绑定仅使用构造器参数所需的请求参数,并忽略任何其他输入。这与属性绑定形成对比,后者默认绑定每个存在匹配属性的请求参数。
如果专用模型对象或构造器绑定都不够,并且你必须使用属性绑定,我们强烈建议在 WebDataBinder
上注册 allowedFields
模式(区分大小写),以防止设置意外的属性。 例如:
@Controller
public class ChangeEmailController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}
// @RequestMapping methods, etc.
}
你还可以注册 disallowedFields
模式(不区分大小写)。但是,与“禁止”配置相比,“允许”配置更受欢迎,因为它更明确且不易出错。
默认情况下,构造器绑定和属性绑定都使用。 如果只想使用构造器绑定,则可以通过 @InitBinder
方法(在控制器本地或通过 @ControllerAdvice
全局设置)在 WebDataBinder
上设置 declarativeBinding
标志。 启用此标志可确保仅使用构造器绑定,并且除非配置了 allowedFields
模式,否则不使用属性绑定。 例如:
@Controller
public class MyController {
@InitBinder
void initBinder(WebDataBinder binder) {
binder.setDeclarativeBinding(true);
}
// @RequestMapping methods, etc.
}
# 4.6、Validation
Spring MVC 内置了对 @RequestMapping
方法的验证,包括 Java Bean Validation。验证可以在以下两个级别应用:
@ModelAttribute
,@RequestBody
和@RequestPart
参数解析器会单独验证方法参数,如果方法参数使用 Jakarta@Valid
或 Spring 的@Validated
注解,并且 紧随其后没有Errors
或BindingResult
参数,并且 不需要方法验证 (接下来会讨论) 。 在这种情况下引发的异常是MethodArgumentNotValidException
。当
@Constraint
注解 (例如@Min
,@NotBlank
等) 直接在方法参数或方法 (对于返回值) 上声明时,则必须应用方法验证,并且它取代方法参数级别的验证,因为方法验证涵盖了方法参数约束和通过@Valid
的嵌套约束。 在这种情况下引发的异常是HandlerMethodValidationException
。
应用程序必须同时处理 MethodArgumentNotValidException
和 HandlerMethodValidationException
,因为根据控制器方法签名可能会引发其中任何一个。 但是,这两个异常的设计非常相似,并且可以使用几乎相同的代码来处理。 主要区别在于前者用于单个对象,而后者用于方法参数列表。
Note:
@Valid
不是约束注解,而是用于 Object 内的嵌套约束。 因此,@Valid
本身不会导致方法验证。 另一方面,@NotNull
是一个约束,将其添加到@Valid
参数会导致方法验证。 对于专门的 nullability,你也可以使用@RequestBody
或@ModelAttribute
的required
标志。
方法验证可以与 Errors
或 BindingResult
方法参数结合使用。 但是,仅当所有验证错误都发生在方法参数上且紧跟其后有 Errors
时,才会调用控制器方法。 如果任何其他方法参数存在验证错误,则会引发 HandlerMethodValidationException
。
你可以通过 WebMvc config 全局配置 Validator
,也可以通过 @Controller
或 @ControllerAdvice
中的 @InitBinder 方法在本地进行配置。 你也可以使用多个验证器。
Note: 如果控制器具有类级别的
@Validated
,则方法验证通过 AOP 代理应用。 为了利用 Spring Framework 6.1 中添加的 Spring MVC 内置方法验证支持,你需要从控制器中删除类级别的@Validated
注解。
错误响应 部分提供了有关如何处理 MethodArgumentNotValidException
和 HandlerMethodValidationException
的更多详细信息,以及如何通过 MessageSource
以及特定于语言环境和语言的资源包来定制其呈现。
为了进一步自定义处理方法验证错误,你可以扩展 ResponseEntityExceptionHandler
或在控制器或 @ControllerAdvice
中使用 @ExceptionHandler
方法,并直接处理 HandlerMethodValidationException
。 该异常包含一个 ParameterValidationResult
列表,该列表按方法参数对验证错误进行分组。 你可以遍历这些错误,也可以通过控制器方法参数类型提供具有回调方法的访问器:
HandlerMethodValidationException ex = ... ;
ex.visitResults(new HandlerMethodValidationException.Visitor() {
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
}
@Override
public void requestParam(RequestParam requestParam, ParameterValidationResult result) {
// ...
}
@Override
public void modelAttribute(ModelAttribute modelAttribute, ParameterErrors errors) {
// ...
}
@Override
public void other(ParameterValidationResult result) {
// ...
}
});
# 4.7、异常
@Controller
和 @ControllerAdvice
类可以拥有 @ExceptionHandler
方法来处理 controller 方法中的异常,如下面的示例所示:
import java.io.IOException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
@Controller
public class SimpleController {
@ExceptionHandler(IOException.class)
public ResponseEntity<String> handle() {
return ResponseEntity.internalServerError().body("Could not read file storage"); // 无法读取文件存储
}
}
# a、异常映射
异常可以匹配被传播的顶层异常(例如,直接抛出的 IOException
)或包装器异常中的嵌套原因(例如,包装在 IllegalStateException
中的 IOException
)。从 5.3 开始,这可以在任意原因级别进行匹配,而以前只考虑直接原因。
对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,通常首选根异常匹配而不是原因异常匹配。更具体地说,ExceptionDepthComparator
用于根据异常类型与抛出异常的深度对异常进行排序。
或者,注解声明可以缩小要匹配的异常类型,如下面的示例所示:
import java.io.IOException;
import java.nio.file.FileSystemException;
import java.rmi.RemoteException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleIoException(IOException ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
你甚至可以使用具有非常通用参数签名的特定异常类型列表,如下例所示:
import java.io.IOException;
import java.nio.file.FileSystemException;
import java.rmi.RemoteException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handleExceptions(Exception ex) {
return ResponseEntity.internalServerError().body(ex.getMessage());
}
注意: 根异常匹配和原因异常匹配之间的区别可能令人惊讶。
在前面显示的
IOException
变体中,该方法通常使用实际的FileSystemException
或RemoteException
实例作为参数来调用,因为它们都扩展自IOException
。但是,如果在包装异常(本身就是IOException
)中传播任何此类匹配异常,则传入的异常实例就是该包装异常。在
handle(Exception)
变体中,行为甚至更简单。在包装场景中,这始终使用包装异常调用,在这种情况下,可以通过ex.getCause()
找到实际匹配的异常。仅当这些异常作为顶级异常抛出时,传入的异常才是实际的FileSystemException
或RemoteException
实例。
我们通常建议你在参数签名中尽可能具体,从而减少根异常类型和原因异常类型之间不匹配的可能性。考虑将多重匹配的方法分解为单独的 @ExceptionHandler
方法,每个方法通过其签名匹配单个特定异常类型。
在 multi-@ControllerAdvice
配置中,我们建议在以对应顺序优先化的 @ControllerAdvice
上声明你的主要根异常映射。虽然根异常匹配优先于原因,但这定义在给定的 controller 或 @ControllerAdvice
类的方法之间。这意味着,与较低优先级 @ControllerAdvice
bean 上的任何匹配项(例如根)相比,更高优先级的 @ControllerAdvice
bean 上的原因匹配项是首选的。
最后但并非最不重要的一点是,@ExceptionHandler
方法实现可以选择通过以其原始形式重新抛出给定的异常实例来退出处理。这在仅对根级别匹配项或无法静态确定的特定上下文中的匹配项感兴趣的场景中非常有用。重新抛出的异常将通过剩余的解析链传播,就好像给定的 @ExceptionHandler
方法首先没有匹配一样。
Spring MVC 中对 @ExceptionHandler
方法的支持建立在 DispatcherServlet
级别,HandlerExceptionResolver
机制。
# b、Type 映射
除了异常类型,@ExceptionHandler
方法还可以声明生产的媒体类型。这允许根据 HTTP 客户端请求的媒体类型(通常在 "Accept" HTTP 请求标头中)来优化错误响应。
应用程序可以直接在注解上声明生产的媒体类型,对于相同的异常类型:
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}
@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
在这里,方法处理相同的异常类型,但不会被拒绝为重复项。相反,请求 "application/json" 的 API 客户端将收到 JSON 错误,浏览器将获得 HTML 错误视图。 每个 @ExceptionHandler
注解都可以声明多个可生成的媒体类型,错误处理阶段的内容协商将决定使用哪种内容类型。
# c、方法参数
@ExceptionHandler
方法支持以下参数:
方法参数 | 描述 |
---|---|
Exception 类型 | 用于访问引发的异常。 |
HandlerMethod | 用于访问引发异常的 controller 方法。 |
WebRequest , NativeWebRequest | 无需直接使用 Servlet API 即可通用地访问请求参数、请求和会话属性。 |
jakarta.servlet.ServletRequest , jakarta.servlet.ServletResponse | 选择任何特定的请求或响应类型(例如,ServletRequest 或 HttpServletRequest 或 Spring 的 MultipartRequest 或 MultipartHttpServletRequest )。 |
jakarta.servlet.http.HttpSession | 强制存在会话。 因此,这样的参数永远不会为 null 。请注意,会话访问不是线程安全的。 如果允许多个请求同时访问会话,请考虑将 RequestMappingHandlerAdapter 实例的 synchronizeOnSession 标志设置为 true 。 |
java.security.Principal | 当前经过身份验证的用户 - 如果已知的话,可能是特定的 Principal 实现类。 |
HttpMethod | 请求的 HTTP 方法。 |
java.util.Locale | 当前请求区域设置,由最具体的 LocaleResolver 确定 - 实际上,是配置的 LocaleResolver 或 LocaleContextResolver 。 |
java.util.TimeZone , java.time.ZoneId | 与当前请求关联的时区,由 LocaleContextResolver 确定。 |
java.io.OutputStream , java.io.Writer | 用于访问 Servlet API 公开的原始响应主体。 |
java.util.Map , org.springframework.ui.Model , org.springframework.ui.ModelMap | 用于访问错误响应的模型。 始终为空 |
RedirectAttributes | 指定在重定向时使用的属性 - (即附加到查询字符串)和要临时存储的 flash 属性,直到重定向后的请求。 请参阅重定向属性和 Flash 属性。 |
@SessionAttribute | 用于访问任何会话属性,而不是由于类级别 @SessionAttributes 声明而存储在会话中的模型属性。 有关更多详细信息,请参见@SessionAttribute 。 |
@RequestAttribute | 用于访问请求属性。 有关更多详细信息,请参见@RequestAttribute 。 |
# d、返回值
@ExceptionHandler
方法支持以下返回值:
返回值 | 描述 |
---|---|
@ResponseBody | 返回值通过 HttpMessageConverter 实例进行转换并写入响应。 请参见@ResponseBody 。 |
HttpEntity<B> , ResponseEntity<B> | 返回值指定应通过 HttpMessageConverter 实例转换完整的响应(包括 HTTP 标头和正文)并写入响应。 请参见ResponseEntity。 |
ErrorResponse | 要呈现正文中带有详细信息的 RFC 9457 错误响应,请参阅错误响应 |
ProblemDetail | 要呈现正文中带有详细信息的 RFC 9457 错误响应,请参阅错误响应 |
String | 要使用 ViewResolver 实现解析的视图名称,并与隐式模型(通过命令对象和 @ModelAttribute 方法确定)一起使用。 处理程序方法还可以通过声明 Model 参数(如前所述)以编程方式丰富模型。 |
View | 要与隐式模型(通过命令对象和 @ModelAttribute 方法确定)一起使用的 View 实例。 处理程序方法还可以通过声明 Model 参数(如前所述)以编程方式丰富模型。 |
java.util.Map , org.springframework.ui.Model | 要添加到隐式模型的属性,视图名称通过 RequestToViewNameTranslator 隐式确定。 |
@ModelAttribute | 要添加到模型的属性,视图名称通过 RequestToViewNameTranslator 隐式确定。请注意,@ModelAttribute 是可选的。 请参阅本表末尾的“任何其他返回值”。 |
ModelAndView 对象 | 要使用的视图和模型属性,以及可选的响应状态。 |
void | 如果具有 ServletResponse 、OutputStream 参数或 @ResponseStatus 注解,则具有 void 返回类型(或 null 返回值)的方法被认为已完全处理响应。 如果 controller 已进行正面的 ETag 或 lastModified 时间戳检查,则同样适用(有关详细信息,请参见Controllers)。如果以上都不是真的,则 void 返回类型也可以指示 REST controller 的“无响应正文”,或 HTML controller 的默认视图名称选择。 |
任何其他返回值 | 如果返回值与上述任何一项都不匹配,并且不是简单类型(由BeanUtils#isSimpleProperty (opens new window)确定),则默认情况下,它被视为要添加到模型的模型属性。 如果它是简单类型,则保持未解析状态。 |
# 4.8、控制器建议
@ExceptionHandler
、@InitBinder
和 @ModelAttribute
方法仅应用于声明它们的 @Controller
类或类层级结构中。 如果它们在 @ControllerAdvice
或 @RestControllerAdvice
类中声明,那么它们将应用于任何控制器。 此外,从 5.3 版本开始,@ControllerAdvice
中的 @ExceptionHandler
方法可用于处理来自任何 @Controller
或任何其他处理程序的异常。
@ControllerAdvice
使用 @Component
进行元注解,因此可以通过组件扫描注册为 Spring Bean。@RestControllerAdvice
使用 @ControllerAdvice
和 @ResponseBody
进行元注解,这意味着 @ExceptionHandler
方法的返回值将通过响应体消息转换进行呈现,而不是通过 HTML 视图进行呈现。
在启动时,RequestMappingHandlerMapping
和 ExceptionHandlerExceptionResolver
检测 controller advice beans 并在运行时应用它们。 来自 @ControllerAdvice
的全局 @ExceptionHandler
方法在来自 @Controller
的本地方法之后应用。 相比之下,全局 @ModelAttribute
和 @InitBinder
方法在本地方法之前应用。
@ControllerAdvice
注解具有一些属性,可以用来缩小它们所应用的控制器和处理程序的范围。 例如:
// 目标是所有使用 @RestController 注解的 Controller
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 目标是特定包中的所有 Controller
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 目标是可分配给特定类的所有 Controller
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
前面的示例中的选择器在运行时进行评估,如果大量使用,可能会对性能产生负面影响。 有关更多详细信息,请参见 @ControllerAdvice
(opens new window) 的 Java 文档。
# 5、功能端点
Spring Web MVC 包含了 WebMvc.fn,一种轻量级的函数式编程模型,其中使用函数来路由和处理请求,并且合约被设计为不可变的。它是基于注解的编程模型的替代方案,但在相同的 DispatcherServlet
上运行。
# 5.1、概述
在 WebMvc.fn 中,HTTP 请求由 HandlerFunction
处理:该函数接收 ServerRequest
并返回 ServerResponse
。请求和响应对象都具有不可变的合约,可以方便地以 JDK 8 友好的方式访问 HTTP 请求和响应。HandlerFunction
相当于基于注解的编程模型中 @RequestMapping
方法的方法体内容。
传入的请求通过 RouterFunction
路由到处理函数。RouterFunction
定义了接收 ServerRequest
并有选择地返回 HandlerFunction
的函数(例如 Optional<HandlerFunction>
)。当路由器函数匹配时,会返回一个处理函数;否则会返回一个空的 Optional。RouterFunction
相当于 @RequestMapping
注解,但主要区别在于路由器函数不仅提供数据,还提供行为。
RouterFunctions.route()
提供了一个路由器构建器,方便创建路由器,如下例所示:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
import static org.springframework.web.servlet.function.RouterFunctions.route;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> route = RouterFunctions.route() // (1)
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();
public class PersonHandler {
// ...
public ServerResponse listPeople(ServerRequest request) {
// ...
}
public ServerResponse createPerson(ServerRequest request) {
// ...
}
public ServerResponse getPerson(ServerRequest request) {
// ...
}
}
- 使用
route()
创建路由器。
如果将 RouterFunction
注册为 bean,例如通过在 @Configuration
类中暴露它,它将被 Servlet 自动检测到,如 "运行服务器" 中所述。
# 5.2、HandlerFunction
ServerRequest
和 ServerResponse
是不可变的接口,它们提供了对 HTTP 请求和响应(包括标头、正文、方法和状态代码)的 JDK 8 友好的访问。
# a、ServerRequest
ServerRequest
提供了对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问通过 body
方法提供。
以下示例将请求正文提取为 String
:
String string = request.body(String.class);
以下示例将正文提取为 List<Person>
,其中 Person
对象从序列化形式(如 JSON 或 XML)解码:
List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {});
以下示例显示如何访问参数:
MultiValueMap<String, String> params = request.params();
# b、ServerResponse
ServerResponse
提供了对 HTTP 响应的访问,并且由于它是不可变的,因此可以使用 build
方法来创建它。 可以使用构建器来设置响应状态、添加响应标头或提供正文。以下示例创建一个状态码为 200 (OK) 的 JSON 内容的响应:
Person person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
以下示例展示了如何构建一个 201 (CREATED) 的响应,其中包含一个 Location
头且没有 body:
URI location = ...
ServerResponse.created(location).build();
您还可以使用异步结果作为主体,其形式为 CompletableFuture
、Publisher
或 ReactiveAdapterRegistry
支持的任何其他类型。例如:
Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class);
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);
如果不仅是正文,而且状态或标头也基于异步类型,则可以使用 ServerResponse
上的静态 async
方法,该方法接受 CompletableFuture<ServerResponse>
、Publisher<ServerResponse>
或 ReactiveAdapterRegistry
支持的任何其他异步类型。例如:
Mono<ServerResponse> asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)
.map(p -> ServerResponse.ok().header("Name", p.getName()).body(p));
ServerResponse.async(asyncResponse);
可以通过 ServerResponse
上的静态 sse
方法提供 Server-Sent Events(服务器发送事件) (opens new window)。该方法提供的构建器允许你发送字符串或其他对象作为 JSON。例如:
public RouterFunction<ServerResponse> sse() {
return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {
// Save the sseBuilder object somewhere.. 保存 sseBuilder 对象到某处
}));
}
// In some other thread, sending a String 在另一个线程中,发送 String
sseBuilder.send("Hello world");
// Or an object, which will be transformed into JSON 或者一个将被转换为 JSON 的对象
Person person = ...
sseBuilder.send(person);
// Customize the event by using the other methods 使用其他方法自定义事件
sseBuilder.id("42")
.event("sse event")
.data(person);
// and done at some point 在某个时刻结束
sseBuilder.complete();
# c、Classes
我们可以将处理函数编写为一个 lambda 表达式,如下例所示:
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().body("Hello World");
这很方便,但在一个应用程序中我们需要多个函数,多个内联 lambda 表达式可能会变得混乱。因此,将相关的处理函数分组到一个处理类中非常有用,这与基于注解的应用程序中的 @Controller
具有相似的作用。例如,以下类公开了一个响应式的 Person
存储库:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public ServerResponse listPeople(ServerRequest request) { // (1)
List<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people);
}
public ServerResponse createPerson(ServerRequest request) throws Exception { // (2)
Person person = request.body(Person.class);
repository.savePerson(person);
return ok().build();
}
public ServerResponse getPerson(ServerRequest request) { // (3)
int personId = Integer.parseInt(request.pathVariable("id"));
Person person = repository.getPerson(personId);
if (person != null) {
return ok().contentType(APPLICATION_JSON).body(person);
}
else {
return ServerResponse.notFound().build();
}
}
}
listPeople
是一个处理函数,它将存储库中找到的所有Person
对象作为 JSON 返回。createPerson
是一个处理函数,用于存储请求正文中包含的新的Person
。getPerson
是一个处理函数,它返回由id
路径变量标识的一个人。 如果找到了该Person
,我们从存储库中检索该Person
并创建一个 JSON 响应。 如果没有找到,我们返回 404 Not Found 响应。
# d、Validation
函数式端点可以使用 Spring 的校验工具 对请求体应用校验。例如,给定一个自定义 Spring Validator 实现,用于 Person
:
public class PersonHandler {
private final Validator validator = new PersonValidator(); // (1)
// ...
public ServerResponse createPerson(ServerRequest request) {
Person person = request.body(Person.class);
validate(person); // (2)
repository.savePerson(person);
return ok().build();
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); // (3)
}
}
}
- 创建
Validator
实例。 - 应用校验。
- 抛出异常以获得 400 响应。
Handlers 也可以使用标准的 bean 校验 API (JSR-303),通过创建和注入一个全局的基于 LocalValidatorFactoryBean
的 Validator (opens new window) 实例。
# 5.3、RouterFunction
路由器函数用于将请求路由到相应的 HandlerFunction
。 通常,我们不自己编写路由器函数,而是使用 RouterFunctions
实用程序类中的方法来创建一个。 RouterFunctions.route()
(无参数) 为您提供了一个流式构建器来创建一个路由器函数,而 RouterFunctions.route(RequestPredicate, HandlerFunction)
提供了一种直接创建路由器的方法。
通常,建议使用 route()
构建器,因为它为典型的映射场景提供了便捷的快捷方式,而无需难以发现的静态导入。 例如,路由器函数构建器提供了方法 GET(String, HandlerFunction)
来为 GET 请求创建映射;POST(String, HandlerFunction)
用于 POST 请求。
除了基于 HTTP 方法的映射之外,路由构建器还提供了一种在映射到请求时引入其他谓词的方法。 对于每种 HTTP 方法,都有一个重载变体,该变体将 RequestPredicate
作为参数,通过该参数可以表示其他约束。
# a、谓语
你可以编写自己的 RequestPredicate
,但是 RequestPredicates
实用程序类提供了常用的实现,这些实现基于请求路径、HTTP 方法、内容类型等等。以下示例使用请求谓词基于 Accept
标头创建约束:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().body("Hello World")).build();
您可以使用以下方法将多个请求谓词组合在一起:
RequestPredicate.and(RequestPredicate)
— 两者都必须匹配。RequestPredicate.or(RequestPredicate)
— 两者中任一个可以匹配。
RequestPredicates
中的许多谓词都是组合而成的。 例如,RequestPredicates.GET(String)
由 RequestPredicates.method(HttpMethod)
和 RequestPredicates.path(String)
组成。 上面显示的示例也使用了两个请求谓词,因为构建器在内部使用 RequestPredicates.GET
,并将其与 accept
谓词组合在一起。
# b、路由器函数
路由器函数按顺序进行求值:如果第一个路由不匹配,则对第二个路由进行求值,依此类推。 因此,先声明更具体的路由是有意义的,然后再声明常规路由。 在将路由器函数注册为 Spring bean 时,这一点也很重要,如稍后所述。 请注意,此行为与基于注解的编程模型不同,在基于注解的编程模型中,会自动选择“最具体”的控制器方法。
使用路由器函数构建器时,所有定义的路由都组合到一个 RouterFunction
中,该 RouterFunction
从 build()
返回。 还有其他方法可以将多个路由器函数组合在一起:
- 在
RouterFunctions.route()
构建器上使用add(RouterFunction)
RouterFunction.and(RouterFunction)
RouterFunction.andRoute(RequestPredicate, HandlerFunction)
— 使用嵌套的RouterFunctions.route()
的RouterFunction.and()
的快捷方式。
以下示例显示了四个路由的组成:
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.servlet.function.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // (1)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // (2)
.POST("/person", handler::createPerson) // (3)
.add(otherRoute) // (4)
.build();
GET /person/{id}
且Accept
标头与 JSON 匹配的路由到PersonHandler.getPerson
GET /person
且Accept
标头与 JSON 匹配的路由到PersonHandler.listPeople
- 没有附加谓词的
POST /person
映射到 `PersonHandler.createPerson otherRoute
是在其他位置创建并添加到已构建的路由的路由器函数。
# c、嵌套路由
通常,一组路由器函数具有共享的谓词,例如共享路径。 在上面的示例中,共享谓词将是与 /person
匹配的路径谓词,由三个路由使用。 使用注解时,可以使用映射到 /person
的类型级别 @RequestMapping
注解来消除这种重复。 在 WebMvc.fn 中,可以通过路由器函数构建器上的 path
方法共享路径谓词。 例如,可以使用嵌套路由通过以下方式改进以上示例的最后几行:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", builder -> builder // (1)
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();
- 请注意,
path
的第二个参数是使用路由器构建器的使用者。
尽管基于路径的嵌套是最常见的,但是可以通过使用构建器上的 nest
方法对任何类型的谓词进行嵌套。 上面仍然包含一些重复的形式,即共享的 Accept
标头谓词。 我们还可以通过将 nest
方法与 accept
一起使用来进一步改进:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();
# 5.4、服务资源
WebMvc.fn 提供了内置的资源服务支持。
注意:除了下面描述的功能之外,还可以通过 RouterFunctions#resource(java.util.function.Function)
实现更加灵活的资源处理。
# a、重定向到资源
可以将与指定谓词匹配的请求重定向到资源。 例如,这对于处理单页应用程序中的重定向非常有用。
ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = RouterFunctions.route()
.resource(spaPredicate, index)
.build();
# b、从根位置提供资源
也可以将与给定模式匹配的请求路由到相对于给定根位置的资源。
Resource location = new FileUrlResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
# 5.5、运行 Server
通常,通过 DispatcherHandler
设置中的 MVC 配置 来运行路由器函数。DispatcherHandler
是基于 DispatcherServlet
的设置,它使用 Spring 配置来声明处理请求所需的组件。 MVC Java 配置声明了以下基础组件来支持函数式端点:
RouterFunctionMapping
:检测 Spring 配置中的一个或多个RouterFunction<?>
bean,对它们进行排序,通过RouterFunction.andOther
组合它们,并将请求路由到由此产生的合成RouterFunction
。HandlerFunctionAdapter
:简单的适配器,使DispatcherHandler
可以调用映射到请求的HandlerFunction
。
以上组件使函数式端点适合 DispatcherServlet
请求处理生命周期,并且(可能)与任何已声明的带注解的控制器Controller并行运行。 这也是 Spring Boot Web starter 启用函数式端点的方式。
以下示例显示了一个 WebFlux Java 配置:
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
# 5.6、过滤处理函数
您可以使用路由函数构建器上的 before
、after
或 filter
方法来过滤处理函数。通过注解,您可以通过使用 @ControllerAdvice
,ServletFilter
或两者来实现类似的功能。过滤器将应用于由构建器构建的所有路由。这意味着在嵌套路由中定义的过滤器不适用于“顶级”路由。例如,考虑以下示例:
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople)
.before(request -> ServerRequest.from(request) // (1)
.header("X-RequestHeader", "Value")
.build()))
.POST(handler::createPerson))
.after((request, response) -> logResponse(response)) // (2)
.build();
- 添加自定义请求标头的
before
过滤器仅应用于两个 GET 路由。 - 记录响应的 "after" 过滤器将应用于所有路由,包括嵌套路由。
路由器构建器上的 filter
方法采用 HandlerFilterFunction
:该函数采用 ServerRequest
和 HandlerFunction
并返回 ServerResponse
。 处理函数参数表示链中的下一个元素。 这通常是要路由到的处理程序,但如果应用了多个过滤器,它也可以是另一个过滤器。
现在,我们可以向我们的路由添加一个简单的安全过滤器,假设我们有一个 SecurityManager
,它可以确定是否允许特定的路径。 以下示例展示了如何执行此操作:
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
上面的示例演示了调用 next.handle(ServerRequest)
是可选的。 仅在允许访问时,才允许运行处理函数。
除了使用路由器函数构建器上的 filter
方法之外,还可以通过 RouterFunction.filter(HandlerFilterFunction)
将过滤器应用于现有的路由器函数。
CORS 对函数式端点的支持通过专用的 CorsFilter
提供。
# 6、Links
此部分描述了 Spring Framework 中可用于处理 URI 的各种选项。
# 6.1、UriComponents
(Spring MVC 和 Spring WebFlux)
UriComponentsBuilder
帮助从带有变量的 URI 模板构建 URI,如下例所示:
UriComponents uriComponents = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}") // (1) 静态工厂方法,使用 URI 模板。
.queryParam("q", "{q}") // (2) 添加或替换 URI 组件。
.encode() // (3) 请求对 URI 模板和 URI 变量进行编码。
.build(); // (4) 构建一个 UriComponents。
URI uri = uriComponents.expand("Westin", "123").toUri(); // (5) 展开变量并获得 URI。
- 1 静态工厂方法,使用 URI 模板。
- 2 添加或替换 URI 组件。
- 3 请求对 URI 模板和 URI 变量进行编码。
- 4 构建一个
UriComponents
。 - 5 展开变量并获得
URI
。
前面的例子可以合并成一个链式调用,并使用 buildAndExpand
缩短,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("Westin", "123")
.toUri();
您可以通过直接获取 URI (这意味着编码) 来进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
您还可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder
.fromUriString("https://example.com/hotels/{hotel}?q={q}")
.build("Westin", "123");
# 6.2、UriBuilder
UriComponentsBuilder
实现了 UriBuilder
。反过来,您可以使用 UriBuilderFactory
创建一个 UriBuilder
。UriBuilderFactory
和 UriBuilder
一起提供了一种可插拔的机制,可以基于共享配置(如基本 URL、编码首选项和其他详细信息)从 URI 模板构建 URI。
您可以配置 RestTemplate
和 WebClient
以使用 UriBuilderFactory
来定制 URI 的准备工作。DefaultUriBuilderFactory
是 UriBuilderFactory
的默认实现,它在内部使用 UriComponentsBuilder
并公开共享的配置选项。
以下示例展示如何配置 RestTemplate
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
以下示例配置 WebClient
:
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;
String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
此外,您也可以直接使用 DefaultUriBuilderFactory
。它类似于使用 UriComponentsBuilder
,但它不是静态工厂方法,而是一个实际的实例,它保存配置和首选项,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);
URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
.queryParam("q", "{q}")
.build("Westin", "123");
# 6.3、Parsing
UriComponentsBuilder
支持两种 URI 解析器类型:
- RFC 解析器:此解析器类型期望 URI 字符串符合 RFC 3986 语法,并将与语法的偏差视为非法。
- WhatWG 解析器:此解析器基于 WhatWG URL Living Standard (opens new window) 中的 URL 解析算法 (opens new window)。它提供了对各种意外输入情况的宽松处理。浏览器实现此功能是为了宽松地处理用户键入的 URL。有关更多详细信息,请参阅 URL Living Standard 和 URL 解析 测试用例 (opens new window)。
默认情况下,RestClient
、WebClient
和 RestTemplate
使用 RFC 解析器类型,并期望应用程序提供符合 RFC 语法的 URL 模板。要更改此设置,您可以在任何客户端上自定义 UriBuilderFactory
。
应用程序和框架可能进一步依赖 UriComponentsBuilder
来满足自身的需求,以解析用户提供的 URL,以便检查并可能验证 URI 组件,如 scheme、host、port、path 和 query。这些组件可以决定使用 WhatWG 解析器类型,以便更宽松地处理 URL,并在重定向到输入 URL 或将其包含在对浏览器的响应中时,与浏览器解析 URI 的方式保持一致。
# 6.4、Encoding
UriComponentsBuilder
在两个级别公开编码选项:
UriComponentsBuilder#encode()
(opens new window): 首先预编码 URI 模板,然后在展开时严格编码 URI 变量。UriComponents#encode()
(opens new window): 在 URI 变量展开后 编码 URI 组件。
这两个选项都用转义的八位字节替换非 ASCII 和非法字符。但是,第一个选项还会替换在 URI 变量中出现的具有保留含义的字符。
考虑 ";",它在路径中是合法的,但具有保留含义。第一个选项将 URI 变量中的 ";" 替换为 "%3B",但不替换 URI 模板中的 ";"。相比之下,第二个选项永远不会替换 ";",因为它是路径中的合法字符。
在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为要完全编码的不透明数据,而如果 URI 变量确实包含保留字符,则第二个选项很有用。当根本不展开 URI 变量时,第二个选项也很有用,因为这也会对偶然看起来像 URI 变量的任何内容进行编码。
以下示例使用第一个选项:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.encode()
.buildAndExpand("New York", "foo+bar")
.toUri();
// 结果是 "/hotel%20list/New%20York?q=foo%2Bbar"
您可以通过直接获取 URI (这意味着编码) 来缩短前面的示例,如下例所示:
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
.queryParam("q", "{q}")
.build("New York", "foo+bar");
您还可以使用完整的 URI 模板进一步缩短它,如下例所示:
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
.build("New York", "foo+bar");
WebClient
和 RestTemplate
在内部通过 UriBuilderFactory
策略展开和编码 URI 模板。两者都可以配置为使用自定义策略,如下例所示:
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// 定制 RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// 定制 WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
DefaultUriBuilderFactory
实现使用 UriComponentsBuilder
在内部展开和编码 URI 模板。作为工厂,它提供了一个配置编码方法的单一位置,基于以下编码模式之一:
TEMPLATE_AND_VALUES
: 使用UriComponentsBuilder#encode()
,对应于前面列表中的第一个选项,以预编码 URI 模板并在展开时严格编码 URI 变量。VALUES_ONLY
: 不编码 URI 模板,而是通过UriUtils#encodeUriVariables
在将 URI 变量展开到模板之前,对其应用严格编码。URI_COMPONENT
: 使用UriComponents#encode()
,对应于前面列表中的第二个选项,在 URI 变量展开 后 编码 URI 组件值。NONE
: 不应用任何编码。
由于历史原因和向后兼容性,RestTemplate
设置为 EncodingMode.URI_COMPONENT
。WebClient
依赖于 DefaultUriBuilderFactory
中的默认值,该值从 5.0.x 中的 EncodingMode.URI_COMPONENT
更改为 5.1 中的 EncodingMode.TEMPLATE_AND_VALUES
。
# 6.5、相对的 Servlet 请求
您可以使用 ServletUriComponentsBuilder
创建相对于当前请求的 URI,如下例所示:
HttpServletRequest request = ...
// 重用 scheme, host, port, path 和 query string...
URI uri = ServletUriComponentsBuilder.fromRequest(request)
.replaceQueryParam("accountId", "{id}")
.build("123");
您可以创建相对于上下文路径的 URI,如下例所示:
HttpServletRequest request = ...
// 重用 scheme, host, port 和 context path...
URI uri = ServletUriComponentsBuilder.fromContextPath(request)
.path("/accounts")
.build()
.toUri();
您可以创建相对于 Servlet (例如, /main/*
) 的URI,如下例所示:
HttpServletRequest request = ...
// 重用 scheme, host, port, context path 和 Servlet 映射前缀...
URI uri = ServletUriComponentsBuilder.fromServletMapping(request)
.path("/accounts")
.build()
.toUri();
从 5.1 开始,
ServletUriComponentsBuilder
忽略来自指定客户端发起地址的Forwarded
和X-Forwarded-*
header 的信息。 考虑使用 ForwardedHeaderFilter 提取和使用或者丢弃此类 header。
# 6.6、控制器链接
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 变量,例如从类型级别的请求映射继承的 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();
当控制器方法签名应该可用于通过
fromMethodCall
创建链接时,它们的设计受到限制。 除了需要正确的参数签名外,对返回类型还存在技术限制 (即,为链接构建器调用生成运行时代理),因此返回类型不能是final
。 特别是,视图名称的常见String
返回类型在此处不起作用。 您应该使用ModelAndView
甚至纯Object
(带有一个String
返回值) 代替。
前面的示例使用 MvcUriComponentsBuilder
中的静态方法。 在内部,它们依赖于 ServletUriComponentsBuilder
从当前请求的 scheme, host, port, context path 和 servlet path 准备一个基本 URL。 这在大多数情况下都有效。 但是,有时它可能不足。 例如,您可能在请求的上下文之外 (例如,准备链接的批处理过程),或者您可能需要插入一个路径前缀 (例如,从请求路径中删除并需要重新插入到链接中的区域设置前缀)。
对于这种情况,您可以使用静态 fromXxx
重载方法,这些方法接受一个 UriComponentsBuilder
以使用一个基本 URL。 或者,您可以创建一个带有基本 URL 的 MvcUriComponentsBuilder
实例,然后使用基于实例的 withXxx
方法。 例如,以下列表使用 withMethodCall
:
UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");
MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);
builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);
URI uri = uriComponents.encode().toUri();
从 5.1 开始,
MvcUriComponentsBuilder
忽略了Forwarded
和X-Forwarded-*
header 中的信息, 这些 header 指定了客户端发起的地址。 可以考虑使用 ForwardedHeaderFilter 提取和使用或者丢弃这些 header。
# 6.7、视图中的链接
在诸如 Thymeleaf, FreeMarker, 或者 JSP 这样的视图中, 您可以通过引用为每个请求映射隐式或显式分配的名称来构建到注解控制器的链接。
考虑以下示例:
@RequestMapping("/people/{id}/addresses")
public class PersonAddressController {
@RequestMapping("/{country}")
public HttpEntity<PersonAddress> 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
函数,但很容易定义您自己的函数或者为其他模板技术准备一个类似的函数。
以下是它的工作原理。 在启动时, 每个 @RequestMapping
通过 HandlerMethodMappingNamingStrategy
被分配一个默认名称, 其默认实现使用类和方法名称的大写字母 (例如, ThingController
中的 getThing
方法变成 "TC#getThing")。 如果存在名称冲突, 您可以使用 @RequestMapping(name="..")
来分配显式名称,或者实现您自己的 HandlerMethodMappingNamingStrategy
。
# 7、异步请求
Spring MVC 与 Servlet 异步请求处理有着广泛的集成:
- 控制器方法中的
DeferredResult
和Callable
返回值,为单个异步返回值提供基本支持。 - 控制器可以
流式传输
多个值,包括SSE
和原始数据
。 - 控制器可以使用响应式客户端,并返回
响应式类型
以进行响应处理。
有关这与 Spring WebFlux 的不同之处的概述,请参见下面的 Async Spring MVC 与 WebFlux 的比较 部分。
# 7.1、DeferredResult
一旦在 Servlet 容器中 启用 异步请求处理功能,控制器方法可以使用 DeferredResult
包装任何支持的控制器方法返回值,如下例所示:
@GetMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
DeferredResult<String> deferredResult = new DeferredResult<>();
// Save the deferredResult somewhere..
// 将 deferredResult 保存在某个地方
return deferredResult;
}
// From some other thread...
// 从其他线程
deferredResult.setResult(result);
控制器可以从不同的线程异步生成返回值 - 例如,响应外部事件 (JMS 消息)、计划任务或其他事件。
# 7.2、Callable
控制器可以使用 java.util.concurrent.Callable
包装任何支持的返回值,如下例所示:
@PostMapping
public Callable<String> processUpload(final MultipartFile file) {
return () -> "someView";
}
然后,可以通过运行通过配置的 AsyncTaskExecutor
来获得返回值。
# 7.3、处理
以下是 Servlet 异步请求处理的简明概述:
- 通过调用
request.startAsync()
可以将ServletRequest
置于异步模式。这样做的主要效果是 Servlet(以及任何过滤器)可以退出,但响应保持打开状态,以便稍后完成处理。 - 对
request.startAsync()
的调用返回AsyncContext
,你可以使用它来进一步控制异步处理。例如,它提供dispatch
方法,该方法类似于 Servlet API 中的 forward,除了它允许应用程序在 Servlet 容器线程上恢复请求处理。 ServletRequest
提供对当前DispatcherType
的访问,你可以使用它来区分处理初始请求、异步派发、转发和其他派发类型。
DeferredResult
处理的工作方式如下:
- 控制器返回
DeferredResult
并将其保存在某个内存队列或列表中,以便可以访问它。 - Spring MVC 调用
request.startAsync()
。 - 与此同时,
DispatcherServlet
和所有配置的过滤器退出请求处理线程,但响应保持打开状态。 - 应用程序从某个线程设置
DeferredResult
,Spring MVC 将请求派发回 Servlet 容器。 - 再次调用
DispatcherServlet
,并使用异步生成的返回值恢复处理。
Callable
处理的工作方式如下:
- 控制器返回
Callable
。 - Spring MVC 调用
request.startAsync()
并将Callable
提交给AsyncTaskExecutor
以在单独的线程中进行处理。 - 与此同时,
DispatcherServlet
和所有过滤器退出 Servlet 容器线程,但响应保持打开状态。 - 最终,
Callable
产生结果,Spring MVC 将请求派发回 Servlet 容器以完成处理。 - 再次调用
DispatcherServlet
,并使用从Callable
异步生成的返回值恢复处理。
有关更多背景和上下文,你还可以阅读 博客文章 (opens new window),其中介绍了 Spring MVC 3.2 中的异步请求处理支持。
# a、异常处理
当你使用 DeferredResult
时,你可以选择调用 setResult
或使用异常调用 setErrorResult
。在这两种情况下,Spring MVC 都会将请求派发回 Servlet 容器以完成处理。然后,它被视为就像控制器方法返回了给定值,或者就像它产生了给定的异常一样。然后,该异常通过常规异常处理机制(例如,调用 @ExceptionHandler
方法)。
当你使用 Callable
时,会发生类似的处理逻辑,主要区别在于结果从 Callable
返回或由它引发异常。
# b、拦截
HandlerInterceptor
实例可以是 AsyncHandlerInterceptor
类型,以便在启动异步处理的初始请求上接收 afterConcurrentHandlingStarted
回调(而不是 postHandle
和 afterCompletion
)。
HandlerInterceptor
实现还可以注册 CallableProcessingInterceptor
或 DeferredResultProcessingInterceptor
,以更深入地与异步请求的生命周期集成(例如,处理超时事件)。有关更多详细信息,请参见 AsyncHandlerInterceptor
(opens new window)。
DeferredResult
提供 onTimeout(Runnable)
和 onCompletion(Runnable)
回调。有关更多详细信息,请参见 DeferredResult
的 javadoc (opens new window)。Callable
可以替换为暴露用于超时和完成回调的附加方法的 WebAsyncTask
。
# c、Spring MVC 与 WebFlux 的比较
Servlet API 最初是为通过 Filter-Servlet 链进行单次传递而构建的。异步请求处理允许应用程序退出 Filter-Servlet 链,但保持响应开放以供进一步处理。Spring MVC 异步支持围绕该机制构建。当控制器返回 DeferredResult
时,Filter-Servlet 链退出,并且 Servlet 容器线程被释放。稍后,当设置 DeferredResult
时,会进行 ASYNC
派发(到相同的 URL),在此期间再次映射控制器,但不是调用它,而是使用 DeferredResult
值(就像控制器返回它一样)恢复处理。
相比之下,Spring WebFlux 既不是基于 Servlet API 构建的,也不需要这样的异步请求处理功能,因为它在设计上是异步的。异步处理已构建到所有框架合同中,并在请求处理的所有阶段中得到内在支持。
从编程模型的角度来看,Spring MVC 和 Spring WebFlux 都支持异步和 响应式类型 作为控制器方法中的返回值。Spring MVC 甚至支持流式传输,包括响应式背压。但是,对响应的单个写入仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同,WebFlux 依赖于非阻塞 I/O 并且不需要额外的线程来执行每次写入。
另一个根本区别是 Spring MVC 不支持控制器方法参数中的异步或响应式类型(例如,@RequestBody
、@RequestPart
等),也没有对异步和响应式类型作为模型属性的任何显式支持。Spring WebFlux 支持所有这些。
最后,从配置的角度来看,必须在 Servlet 容器级别启用 异步请求处理功能。
# 7.4、流式传输
你可以将 DeferredResult
和 Callable
用于单个异步返回值。如果你想生成多个异步值并将它们写入响应怎么办?本节介绍如何执行此操作。
# a、对象
你可以使用 ResponseBodyEmitter
返回值生成对象流,其中每个对象都使用 HttpMessageConverter
序列化并写入响应,如下例所示:
@GetMapping("/events")
public ResponseBodyEmitter handle() {
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
// Save the emitter somewhere..
// 将 emitter 保存在某个地方
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
中的 body,从而可以自定义响应的状态和标头。
当 emitter
抛出 IOException
(例如,如果远程客户端消失)时,应用程序不负责清理连接,也不应调用 emitter.complete
或 emitter.completeWithError
。相反,servlet 容器自动启动 AsyncListener
错误通知,其中 Spring MVC 进行 completeWithError
调用。反过来,此调用会执行一次最终的 ASYNC
派发到应用程序,在此期间,Spring MVC 调用配置的异常解析器并完成请求。
# b、SSE
SseEmitter
(ResponseBodyEmitter
的子类)提供对 服务器发送事件 (opens new window) 的支持,其中从服务器发送的事件根据 W3C SSE 规范进行格式化。要从控制器生成 SSE 流,请返回 SseEmitter
,如下例所示:
@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
// Save the emitter somewhere..
// 将 emitter 保存在某个地方
return emitter;
}
// In some other thread
// 在其他线程中
emitter.send("Hello once");
// and again later on
// 稍后再次
emitter.send("Hello again");
// and done at some point
// 并在某个时候完成
emitter.complete();
虽然 SSE 是流式传输到浏览器的主要选项,但请注意 Internet Explorer 不支持服务器发送事件。考虑使用 Spring 的 WebSocket 消息传递,其中包含针对各种浏览器的 SockJS 回退 传输(包括 SSE)。
另请参见 上一节 以了解有关异常处理的说明。
# c、原始数据
有时,绕过消息转换并直接流式传输到响应 OutputStream
(例如,用于文件下载)很有用。你可以使用 StreamingResponseBody
返回值类型来执行此操作,如下例所示:
@GetMapping("/download")
public StreamingResponseBody handle() {
return new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// write...
// 写入
}
};
}
你可以将 StreamingResponseBody
用作 ResponseEntity
中的 body,以自定义响应的状态和标头。
# 7.5、响应式类型
Spring MVC 支持在控制器中使用响应式客户端库(另请阅读 WebFlux 部分中的 响应式库)。这包括来自 spring-webflux
的 WebClient
和其他库,例如 Spring Data 响应式数据存储库。在这种情况下,能够从控制器方法返回响应式类型很方便。
响应式返回值的处理方式如下:
- 单值 Promise 类似于使用
DeferredResult
进行调整。示例包括Mono
(Reactor) 或Single
(RxJava)。 - 具有流式媒体类型(例如
application/x-ndjson
或text/event-stream
)的多值流类似于使用ResponseBodyEmitter
或SseEmitter
进行调整。示例包括Flux
(Reactor) 或Observable
(RxJava)。应用程序还可以返回Flux<ServerSentEvent>
或Observable<ServerSentEvent>
。 - 具有任何其他媒体类型(例如
application/json
)的多值流类似于使用DeferredResult<List<?>>
进行调整。
Spring MVC 通过来自
spring-core
的ReactiveAdapterRegistry
(opens new window) 支持 Reactor 和 RxJava,这使其能够从多个响应式库进行调整。
对于流式传输到响应,支持响应式背压,但对响应的写入仍然是阻塞的,并通过 配置的 AsyncTaskExecutor
在单独的线程上运行,以避免阻塞上游源,例如从 WebClient
返回的 Flux
。
# 7.6、上下文传播
通常通过 java.lang.ThreadLocal
传播上下文。这对于在同一线程上进行处理是透明地工作的,但需要在多个线程上进行异步处理的额外工作。Micrometer 上下文传播 (opens new window)库简化了跨线程并在ThreadLocal
值、Reactor context (opens new window)、GraphQL Java context (opens new window)等上下文机制之上进行上下文传播。
如果在类路径上存在“Micrometer 上下文传播”,当控制器方法返回响应式类型(如Flux
或Mono
)时,所有ThreadLocal
如果存在注册的io.micrometer.ThreadLocalAccessor
,则使用ThreadLocalAccessor
分配的键,作为键值对写入 Reactor Context
。
对于其他异步处理方案,您可以直接使用上下文传播库。例如:
// Capture ThreadLocal values from the main thread ...
// 从主线程捕获 ThreadLocal 值
ContextSnapshot snapshot = ContextSnapshot.captureAll();
// On a different thread: restore ThreadLocal values
// 在不同的线程上:恢复 ThreadLocal 值
try (ContextSnapshot.Scope scope = snapshot.setThreadLocals()) {
// ...
}
以下ThreadLocalAccessor
实现开箱即用:
LocaleContextThreadLocalAccessor
— 通过LocaleContextHolder
传播LocaleContext
RequestAttributesThreadLocalAccessor
— 通过RequestContextHolder
传播RequestAttributes
以上这些不会自动注册。您需要在启动时通过ContextRegistry.getInstance()
注册它们。
有关更多详细信息,请参阅“Micrometer 上下文传播”库的 文档 (opens new window)。
# 7.7、断开连接
当远程客户端消失时,Servlet API 不提供任何通知。因此,在通过“SseEmitter”或响应式类型流式传输到响应时,定期发送数据非常重要,因为如果客户端已断开连接,则写入将失败。发送可以采用空(仅注释)“SSE”事件的形式,或者另一端必须将其解释为心跳而忽略的任何其他数据。
或者,考虑使用具有内置心跳机制的 Web 消息解决方案(例如“通过 WebSocket 的 STOMP”或具有“SockJS”的“WebSocket”)。
# 7.8、配置
必须在 Servlet 容器级别启用异步请求处理功能。MVC 配置还公开异步请求的多个选项。
# a、容器
Filter 和 Servlet 声明具有一个asyncSupported
标志,需要将其设置为true
才能启用异步请求处理。此外,应声明 Filter 映射以处理ASYNC
jakarta.servlet.DispatchType
。
在 Java 配置中,当使用AbstractAnnotationConfigDispatcherServletInitializer
初始化 Servlet 容器时,会自动完成此操作。
在web.xml
配置中,您可以将<async-supported>true</async-supported>
添加到DispatcherServlet
和Filter
声明,并将<dispatcher>ASYNC</dispatcher>
添加到过滤器映射。
# b、MVC
MVC 配置公开了以下异步请求处理选项:
- Java 配置:在
WebMvcConfigurer
上使用configureAsyncSupport
回调。 - XML 命名空间:在
<mvc:annotation-driven>
下使用<async-support>
元素。
您可以配置以下内容:
- 异步请求的默认超时值取决于底层 Servlet 容器,除非显式设置它。
AsyncTaskExecutor
用于在使用 响应式类型 进行流式传输时阻塞写入,并用于执行从控制器方法返回的Callable
实例。默认情况下使用的那个不适合在负载下进行生产。DeferredResultProcessingInterceptor
实现和CallableProcessingInterceptor
实现。
请注意,您还可以在DeferredResult
、ResponseBodyEmitter
和SseEmitter
上设置默认超时值。对于Callable
,您可以使用WebAsyncTask
提供超时值。
# 8、CORS
Spring MVC 允许你处理 CORS (跨域资源共享)。本节将介绍如何进行配置。
# 8.1、简介
出于安全原因,浏览器禁止 AJAX 调用当前域之外的资源。例如,你可能在一个标签页中打开你的银行账户,在另一个标签页中打开 evil.com。来自 evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发起 AJAX 请求——例如,从你的账户中取款!
跨域资源共享 (CORS) 是一个 W3C 规范,已被大多数浏览器实现。它允许你指定授权哪些类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的安全性较低且功能较弱的解决方法。
# 8.2、凭据请求
将 CORS 用于凭据请求需要启用 allowedCredentials
。请注意,此选项与配置的域建立高信任级别,并且还会通过暴露敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌)来增加 Web 应用程序的攻击面。
启用凭据还会影响如何处理配置的 " * "
CORS 通配符:
- 不允许在
allowOrigins
中使用通配符,但可以选择使用allowOriginPatterns
属性来匹配动态的 origin 集合。 - 当在
allowedHeaders
或allowedMethods
上设置时,Access-Control-Allow-Headers
和Access-Control-Allow-Methods
响应头通过复制 CORS 预检请求中指定的 headers 和 method 来处理。 - 当在
exposedHeaders
上设置时,Access-Control-Expose-Headers
响应头设置为配置的 header 列表或通配符。虽然 CORS 规范不允许在将Access-Control-Allow-Credentials
设置为true
时使用通配符,但大多数浏览器都支持它,并且响应 header 在 CORS 处理期间并非全部可用,因此,无论allowCredentials
属性的值如何,通配符都是在指定时使用的 header 值。
虽然这种通配符配置可能很方便,但建议尽可能配置一组有限的值,以提供更高的安全性。
# 8.3、处理
CORS 规范区分了预检请求(preflight),简单请求(simple),和实际请求(actual requests)。要了解 CORS 的工作原理,你可以阅读这篇文章 (opens new window),或查阅规范以获取更多详细信息。
Spring MVC HandlerMapping
的实现提供了对 CORS 的内置支持。在成功将请求映射到处理程序后,HandlerMapping
的实现会检查给定请求和处理程序的 CORS 配置,并采取进一步的操作。预检请求会被直接处理,而简单和实际的 CORS 请求会被拦截、验证,并设置所需的 CORS 响应头。
为了启用跨域请求(即,存在 Origin
头,并且与请求的主机不同),你需要显式声明一些 CORS 配置。如果未找到匹配的 CORS 配置,则会拒绝预检请求。不会将 CORS header 添加到简单和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。
每个 HandlerMapping
都可以通过基于 URL 模式的 CorsConfiguration
映射进行单独配置 (opens new window)。在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 HandlerMapping
实例。
你可以将 HandlerMapping
级别的全局 CORS 配置与更细粒度的处理程序级别的 CORS 配置相结合。例如,注解控制器可以使用类级别或方法级别的 @CrossOrigin
注解(其他处理程序可以实现 CorsConfigurationSource
)。
全局和本地配置的组合规则通常是加法的——例如,所有全局和所有本地的 origin。对于那些只能接受单个值的属性,例如 allowCredentials
和 maxAge
,本地配置将覆盖全局值。有关更多详细信息,请参见 CorsConfiguration#combine(CorsConfiguration)
(opens new window)。
提示
要从源代码中了解更多信息或进行高级自定义,请检查以下代码:
CorsConfiguration
CorsProcessor
,DefaultCorsProcessor
AbstractHandlerMapping
# 9、@CrossOrigin
@CrossOrigin
注解使带注释的控制器方法能够进行跨域请求,如以下示例所示:
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
默认情况下,@CrossOrigin
允许:
- 所有 origin。
- 所有 header。
- 控制器方法映射到的所有 HTTP 方法。
默认情况下不启用 allowCredentials
,因为它建立了一个信任级别,会暴露特定于用户的敏感信息(例如 cookie 和 CSRF 令牌),因此只应在适当的情况下使用。启用后,必须将 allowOrigins
设置为一个或多个特定域(但不是特殊值 " * "
),或者可以使用 allowOriginPatterns
属性来匹配动态的 origin 集合。
maxAge
设置为 30 分钟。
@CrossOrigin
在类级别也受支持,并且由所有方法继承,如以下示例所示:
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
你可以在类级别和方法级别同时使用 @CrossOrigin
,如以下示例所示:
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}
# 9.1、全局配置
除了细粒度的控制器方法级别的配置之外,你可能还需要定义一些全局 CORS 配置。你可以在任何 HandlerMapping
上单独设置基于 URL 的 CorsConfiguration
映射。但是,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来执行此操作。
默认情况下,全局配置启用以下内容:
- 所有 origin。
- 所有 header。
GET
、HEAD
和POST
方法。
默认情况下不启用 allowCredentials
,因为它建立了一个信任级别,会暴露特定于用户的敏感信息(例如 cookie 和 CSRF 令牌),因此只应在适当的情况下使用。启用后,必须将 allowOrigins
设置为一个或多个特定域(但不是特殊值 " * "
),或者可以使用 allowOriginPatterns
属性来匹配动态的 origin 集合。
maxAge
设置为 30 分钟。
# a、配置
要在 MVC Java 配置中启用 CORS,你可以使用 CorsRegistry
回调,如以下示例所示:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// 添加更多映射...
}
}
# b、配置
要在 XML 命名空间中启用 CORS,你可以使用 <mvc:cors>
元素,如以下示例所示:
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
# 9.2、过滤器
你可以通过内置的 CorsFilter
应用 CORS 支持。
请记住,如果你尝试将 CorsFilter
与 Spring Security 结合使用,则 Spring Security 对 CORS 具有内置支持 (opens new window)。
要配置过滤器,请将 CorsConfigurationSource
传递给其构造函数,如以下示例所示:
CorsConfiguration config = new CorsConfiguration();
// 可能...
// config.applyPermitDefaultValues()
config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
CorsFilter filter = new CorsFilter(source);
# 10、错误响应
一个常见的 REST 服务需求是在错误响应的正文中包含详细信息。Spring Framework 支持 "HTTP API 的问题详情" 规范,RFC 9457 (opens new window)。
以下是对此支持的主要抽象:
ProblemDetail
—— RFC 9457 问题详情的表示;一个简单的容器,用于存放规范中定义的标准字段和非标准字段。ErrorResponse
—— 暴露 HTTP 错误响应详情的契约,包括 HTTP 状态、响应头以及 RFC 9457 格式的正文;这允许异常封装并暴露它们如何映射到 HTTP 响应的详细信息。所有 Spring MVC 异常都实现了这个接口。ErrorResponseException
—— 基本的ErrorResponse
实现,其他类可以将其作为方便的基类使用。ResponseEntityExceptionHandler
—— 一个方便的基类,用于@ControllerAdvice
,它可以处理所有 Spring MVC 异常和任何ErrorResponseException
,并呈现带有正文的错误响应。
# 10.1、渲染
你可以从任何 @ExceptionHandler
或任何 @RequestMapping
方法返回 ProblemDetail
或 ErrorResponse
,以呈现 RFC 9457 响应。处理方式如下:
ProblemDetail
的status
属性决定了 HTTP 状态。- 如果尚未设置,则从当前 URL 路径设置
ProblemDetail
的instance
属性。 - 对于内容协商,当渲染
ProblemDetail
时,JacksonHttpMessageConverter
优先选择 "application/problem+json" 而不是 "application/json",并且如果没有找到兼容的媒体类型,也会回退到 "application/problem+json"。
要为 Spring WebFlux 异常和任何 ErrorResponseException
启用 RFC 9457 响应,请扩展 ResponseEntityExceptionHandler
并在 Spring 配置中将其声明为 @ControllerAdvice
。该处理器有一个 @ExceptionHandler
方法,用于处理任何 ErrorResponse
异常,其中包括所有内置的 Web 异常。你可以添加更多异常处理方法,并使用受保护的方法将任何异常映射到 ProblemDetail
。
你可以通过 WebMvcConfigurer
使用 MVC 配置 注册 ErrorResponse
拦截器。使用它来拦截任何 RFC 9457 响应并采取一些操作。
# 10.2、非标准字段
你可以通过以下两种方式之一扩展 RFC 9457 响应,添加非标准字段。
- 插入到
ProblemDetail
的 "properties"Map
中。当使用 Jackson 库时,Spring Framework 注册ProblemDetailJacksonMixin
,以确保 "properties"Map
被解包并呈现为响应中的顶级 JSON 属性,同样,反序列化期间的任何未知属性都将插入到此Map
中。 - 您还可以扩展
ProblemDetail
以添加专用的非标准属性。ProblemDetail
中的复制构造函数允许子类轻松地从现有的ProblemDetail
创建。 例如,可以从@ControllerAdvice
(如ResponseEntityExceptionHandler
)集中完成此操作,该建议将异常的ProblemDetail
重新创建为具有附加非标准字段的子类。
# 10.3、自定义和国际化 (i18n)
自定义和国际化错误响应详细信息是一个常见的需求。自定义 Spring MVC 异常的问题详情以避免泄露实现细节也是一个好习惯。本节介绍对此的支持。
ErrorResponse
公开了 "type"、"title" 和 "detail" 的消息代码,以及 "detail" 字段的消息代码参数。ResponseEntityExceptionHandler
通过 MessageSource 解析这些代码,并相应地更新相应的 ProblemDetail
字段。
消息代码的默认策略如下:
- "type":
problemDetail.type.[fully qualified exception class name]
(完全限定的异常类名) - "title":
problemDetail.title.[fully qualified exception class name]
(完全限定的异常类名) - "detail":
problemDetail.[fully qualified exception class name][suffix]
(完全限定的异常类名 + 后缀)
ErrorResponse
可能公开多个消息代码,通常在默认消息代码中添加后缀。下表列出了 Spring MVC 异常的消息代码和参数:
异常 | 消息代码 | 消息代码参数 |
---|---|---|
AsyncRequestTimeoutException | (默认) | |
ConversionNotSupportedException | (默认) | {0} 属性名称, {1} 属性值 |
HandlerMethodValidationException | (默认) | {0} 列出所有验证错误。每个错误的消息代码和参数也通过 MessageSource 解析。 |
HttpMediaTypeNotAcceptableException | (默认) | {0} 支持的媒体类型列表 |
HttpMediaTypeNotAcceptableException | (默认) + ".parseError" | |
HttpMediaTypeNotSupportedException | (默认) | {0} 不支持的媒体类型, {1} 支持的媒体类型列表 |
HttpMediaTypeNotSupportedException | (默认) + ".parseError" | |
HttpMessageNotReadableException | (默认) | |
HttpMessageNotWritableException | (默认) | |
HttpRequestMethodNotSupportedException | (默认) | {0} 当前 HTTP 方法, {1} 支持的 HTTP 方法列表 |
MethodArgumentNotValidException | (默认) | {0} 全局错误列表, {1} 字段错误列表。每个错误的消息代码和参数也通过 MessageSource 解析。 |
MissingRequestHeaderException | (默认) | {0} 标头名称 |
MissingServletRequestParameterException | (默认) | {0} 请求参数名称 |
MissingMatrixVariableException | (默认) | {0} 矩阵变量名称 |
MissingPathVariableException | (默认) | {0} 路径变量名称 |
MissingRequestCookieException | (默认) | {0} Cookie 名称 |
MissingServletRequestPartException | (默认) | {0} 部分名称 |
NoHandlerFoundException | (默认) | |
NoResourceFoundException | (默认) | |
TypeMismatchException | (默认) | {0} 属性名称, {1} 属性值 |
UnsatisfiedServletRequestParameterException | (默认) | {0} 参数条件列表 |
注意:与其他异常不同,
MethodArgumentValidException
和HandlerMethodValidationException
的消息参数基于MessageSourceResolvable
错误列表,这些错误也可以通过 MessageSource 资源束进行定制。有关详细信息,请参见 Customize Validation Errors。
# a、客户端处理
当使用 WebClient
时,客户端应用程序可以捕获 WebClientResponseException
,或者当使用 RestTemplate
时,可以捕获 RestClientResponseException
,并使用它们的 getResponseBodyAs
方法将错误响应正文解码为任何目标类型,例如 ProblemDetail
或 ProblemDetail
的子类。
# 11、安全
Spring Security (opens new window) 项目为保护 Web 应用程序免受恶意利用提供了支持。请参阅 Spring Security 参考文档,包括:
- Spring MVC Security (opens new window)
- Spring MVC Test Support (opens new window)
- CSRF protection (opens new window) (CSRF 保护)
- Security Response Headers (opens new window) (安全响应头)
HDIV (opens new window) 是另一个与 Spring MVC 集成的 Web 安全框架。
# 12、缓存
HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕 Cache-Control
响应头以及后续的条件请求头(例如 Last-Modified
和 ETag
)展开。Cache-Control
建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。如果内容没有更改,则可以使用 ETag
标头发出条件请求,该请求可能会导致 304 (NOT_MODIFIED) 而没有正文。ETag
可以被看作是 Last-Modified
标头的更高级的继任者。
本节介绍 Spring Web MVC 中可用的与 HTTP 缓存相关的选项。
# 12.1、CacheControl
CacheControl
(opens new window) 提供了对配置与 Cache-Control
标头相关的设置的支持,并在多个地方被接受为参数:
WebContentInterceptor
(opens new window)WebContentGenerator
(opens new window)- Controllers
- Static Resources
虽然 RFC 7234 (opens new window) 描述了 Cache-Control
响应头的所有可能指令,但 CacheControl
类型采用面向用例的方法,侧重于常见场景:
// 缓存一小时 - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);
// 防止缓存 - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();
// 在公共和私有缓存中缓存十天,
// 公共缓存不应转换响应
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
WebContentGenerator
还可以接受更简单的 cachePeriod
属性(以秒为单位定义),其工作方式如下:
-1
值不生成Cache-Control
响应头。0
值通过使用'Cache-Control: no-store'
指令来防止缓存。n > 0
值通过使用'Cache-Control: max-age=n'
指令将给定的响应缓存n
秒。
# 12.2、Controllers
Controllers 可以为 HTTP 缓存添加显式支持。我们建议这样做,因为需要在将资源的 lastModified
或 ETag
值与条件请求头进行比较之前对其进行计算。Controller 可以将 ETag
标头和 Cache-Control
设置添加到 ResponseEntity
,如以下示例所示:
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {
Book book = findBook(id);
String version = book.getVersion();
return ResponseEntity
.ok()
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
.eTag(version) // lastModified is also available
.body(book);
}
如果与条件请求头的比较表明内容未更改,则前面的示例会发送一个 304 (NOT_MODIFIED) 响应,其中包含一个空正文。否则,ETag
和 Cache-Control
标头将添加到响应中。
你也可以在 controller 中检查条件请求头,如以下示例所示:
@RequestMapping
public String myHandleMethod(WebRequest request, Model model) {
long eTag = ... // (1) 应用特定的计算。
if (request.checkNotModified(eTag)) {
return null; // (2) 响应已设置为 304 (NOT_MODIFIED) — 无需进一步处理。
}
model.addAttribute(...); // (3) 继续处理请求。
return "myViewName";
}
有三种变体可以检查针对 eTag
值、lastModified
值或两者的条件请求。对于有条件的 GET
和 HEAD
请求,你可以将响应设置为 304 (NOT_MODIFIED)。对于有条件的 POST
、PUT
和 DELETE
,你可以将响应设置为 412 (PRECONDITION_FAILED),以防止并发修改。
# 12.3、静态资源
你应该使用 Cache-Control
和条件响应头提供静态资源,以获得最佳性能。请参阅有关配置静态资源的部分。
# 13、ETag
过滤器
你可以使用 ShallowEtagHeaderFilter
添加从响应内容计算出的“浅” eTag
值,从而节省带宽,但不节省 CPU 时间。请参Shallow ETag。
# 14、Technologies
Spring MVC中视图的渲染是可插拔的。无论你决定使用Thymeleaf、Groovy Markup Templates、JSP或其他技术,主要取决于配置的更改。本章介绍与Spring MVC集成的视图技术。
警告: Spring MVC应用程序的视图位于该应用程序的内部信任边界内。视图可以访问应用程序上下文中所有bean。因此,不建议在模板可由外部源编辑的应用程序中使用Spring MVC的模板支持,因为这可能存在安全隐患。
# 14.1、内容概要
- Thymeleaf
- FreeMarker
- Groovy Markup
- Script Views
- HTML Fragments
- JSP and JSTL
- RSS and Atom
- PDF and Excel
- Jackson
- XML Marshalling
- XSLT Views
# 14.2、Thymeleaf
Thymeleaf 是一种现代化的服务器端 Java 模板引擎,它强调自然 HTML 模板,这些模板可以通过双击在浏览器中预览。这对于独立进行 UI 模板的工作(例如,由设计师进行)非常有用,而无需运行服务器。 如果你想替换 JSP,Thymeleaf 提供了最广泛的功能集合之一,以使这种转换更容易。 Thymeleaf 正在被积极地开发和维护。 有关更完整的介绍,请参见 Thymeleaf (opens new window) 项目主页。
Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。 该配置涉及几个 bean 声明,例如 ServletContextTemplateResolver
、SpringTemplateEngine
和 ThymeleafViewResolver
。 有关更多详细信息,请参见 Thymeleaf+Spring (opens new window)。
# 14.3、FreeMarker
Apache FreeMarker 是一个模板引擎,可以从 HTML 到电子邮件等任何类型的文本输出生成。Spring Framework 内置了将 Spring MVC 与 FreeMarker 模板集成。
# a、配置
以下示例展示了如何配置 FreeMarker 作为视图技术:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// 配置 FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
configurer.setDefaultCharset(StandardCharsets.UTF_8);
return configurer;
}
}
以下示例展示了如何在 XML 中配置相同的内容:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- 配置 FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
或者,您也可以声明 FreeMarkerConfigurer
bean 以完全控制所有属性,如以下示例所示:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
您的模板需要存储在前面示例中 FreeMarkerConfigurer
指定的目录中。给定前面的配置,如果您的控制器返回的视图名称为 welcome
,则解析器将查找 /WEB-INF/freemarker/welcome.ftl
模板。
# b、配置
您可以通过在 FreeMarkerConfigurer
bean 上设置适当的 bean 属性,将 FreeMarker “设置”和“共享变量”直接传递到 FreeMarker Configuration
对象 (由 Spring 管理)。freemarkerSettings
属性需要一个 java.util.Properties
对象,而 freemarkerVariables
属性需要一个 java.util.Map
。以下示例展示了如何使用 FreeMarkerConfigurer
:
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
有关设置和变量如何应用于 Configuration
对象的详细信息,请参阅 FreeMarker 文档。
# c、表单处理
Spring 提供了一个标签库,用于 JSP 中,其中包含一个 <spring:bind/>
元素。 此元素主要允许表单显示来自表单支持对象的值,并显示来自 web 或业务层的 Validator
中失败验证的结果。 Spring 还支持 FreeMarker 中的相同功能,并为生成表单输入元素本身提供了额外的便利宏。
# 宏
一组标准的宏维护在 FreeMarker 的 spring-webmvc.jar
文件中,因此它们始终可用于适当配置的应用程序。
Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在此类范围,这使得所有宏对调用代码和用户模板可见。 以下各节仅关注您需要直接从模板中调用的宏。 如果您想直接查看宏代码,该文件名为 spring.ftl
,位于 org.springframework.web.servlet.view.freemarker
包中。
# 简单绑定
在基于 FreeMarker 模板的 HTML 表单中,这些模板充当 Spring MVC 控制器的表单视图,您可以使用类似于下一个示例的代码绑定到字段值,并以类似于 JSP 等效的方式显示每个输入字段的错误消息。 以下示例显示了一个 personForm
视图:
<!-- FreeMarker 宏必须导入到命名空间中。
我们强烈建议坚持使用“spring”。 -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
姓名:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>
<@spring.bind>
需要一个 'path' 参数,该参数由您的命令对象的名称(除非您在控制器配置中更改了它,否则为“command”)后跟一个句点以及您要绑定到的命令对象上的字段的名称组成。 您还可以使用嵌套字段,例如 command.address.street
。 bind
宏假定由 ServletContext
参数 defaultHtmlEscape
在 web.xml
中指定的默认 HTML 转义行为。
名为 <@spring.bindEscaped>
的宏的另一种形式采用第二个参数,该参数明确指定是否应在状态错误消息或值中使用 HTML 转义。 您可以根据需要将其设置为 true
或 false
。 其他表单处理宏简化了 HTML 转义的使用,您应该尽可能使用这些宏。 它们将在下一节中进行说明。
# 输入宏
用于 FreeMarker 的其他便利宏简化了绑定和表单生成(包括验证错误显示)。 从来没有必要使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 或直接调用我们之前突出显示的 Spring bind 宏混合和匹配。
下表提供了可用宏,显示了 FreeMarker 模板 (FTL) 定义以及每个宏采用的参数列表:
宏 | FTL 定义 |
---|---|
message (从基于 code 参数的资源束输出字符串) | <@spring.message code/> |
messageText (从基于 code 参数的资源束输出字符串,回退到 default 参数的值) | <@spring.messageText code, text/> |
url (用应用程序的上下文根作为相对 URL 的前缀) | <@spring.url relativeUrl/> |
formInput (用于收集用户输入的标准输入字段) | <@spring.formInput path, attributes, fieldType/> |
formHiddenInput (用于提交非用户输入的隐藏输入字段) | <@spring.formHiddenInput path, attributes/> |
formPasswordInput (收集密码的标准输入字段。 请注意,此类型的字段中永远不会填充任何值。) | <@spring.formPasswordInput path, attributes/> |
formTextarea (用于收集长文本、自由格式文本输入的大文本字段) | <@spring.formTextarea path, attributes/> |
formSingleSelect (下拉选项框,允许选择单个所需值) | <@spring.formSingleSelect path, options, attributes/> |
formMultiSelect (一个选项列表框,允许用户选择 0 个或更多值) | <@spring.formMultiSelect path, options, attributes/> |
formRadioButtons (一组单选按钮,允许从可用选项中进行单项选择) | <@spring.formRadioButtons path, options separator, attributes/> |
formCheckboxes (一组复选框,允许选择 0 个或更多值) | <@spring.formCheckboxes path, options, separator, attributes/> |
formCheckbox (一个复选框) | <@spring.formCheckbox path, attributes/> |
showErrors (简化绑定字段的验证错误的显示) | <@spring.showErrors separator, classOrStyle/> |
在 FreeMarker 模板中,实际上不需要 formHiddenInput
和 formPasswordInput
,因为您可以使用普通的 formInput
宏,指定 hidden
或 password
作为 fieldType
参数的值。
任何上述宏的参数都具有一致的含义:
path
: 要绑定的字段的名称(例如,“command.name”)options
: 可以在输入字段中选择的所有可用值的 Map。 map 的键表示从表单 POST 回并绑定到命令对象的值。 存储在键中的 Map 对象是在表单上向用户显示的标签,并且可能与表单 POST 回的相应值不同。 通常,此类 map 由控制器作为参考数据提供。 您可以使用任何 Map 实现,具体取决于所需的行为。 对于严格排序的 map,您可以使用具有合适的 Comparator 的 SortedMap(例如 TreeMap),对于应按插入顺序返回值的任意 Map,请使用来自 commons-collections 的 LinkedHashMap 或 LinkedMap。separator
: 在多个选项作为离散元素(单选按钮或复选框)可用的情况下,用于分隔列表中每个选项的字符序列(例如<br>
)。attributes
: 要包含在 HTML 标签本身中的任意标签或文本的附加字符串。 此字符串由宏按字面输出。 例如,在 textarea 字段中,您可以提供 attributes(例如 'rows="5" cols="60"'),或者您可以传递样式信息,例如 'style="border:1px solid silver"'。classOrStyle
: 对于 showErrors 宏,包装每个错误的 span 元素使用的 CSS 类的名称。 如果未提供任何信息(或该值为空),则错误将包装在<b></b>
标签中。
以下各节概述了宏的示例。
输入字段:
formInput
宏采用 path
参数 (command.name
) 和一个附加的 attributes
参数(在即将到来的示例中为空)。 该宏,连同所有其他表单生成宏,对路径参数执行隐式 Spring bind。 绑定保持有效,直到发生新的 bind,因此 showErrors
宏不需要再次传递 path 参数 — 它对上次创建绑定的字段进行操作。
showErrors
宏采用一个分隔符参数(用于分隔给定字段上的多个错误的字符),并且还接受第二个参数 - 这次是一个类名或样式属性。 请注意,FreeMarker 可以为 attributes 参数指定默认值。 以下示例显示了如何使用 formInput
和 showErrors
宏:
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>
下一个示例显示了表单片段的输出,生成了 name 字段并在表单在字段中没有值的情况下提交后显示验证错误。 验证通过 Spring 的 Validation 框架进行。
生成的 HTML 类似于以下示例:
姓名:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>
formTextarea
宏的工作方式与 formInput
宏相同,并接受相同的参数列表。 通常,第二个参数 (attributes
) 用于传递样式信息或 rows
和 cols
属性用于 textarea
。
选择字段:
您可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入:
formSingleSelect
formMultiSelect
formRadioButtons
formCheckboxes
四个宏中的每一个都接受一个选项 Map,其中包含表单字段的值和与该值对应的标签。 值和标签可以相同。
下一个示例用于 FTL 中的单选按钮。 表单后备对象为此字段指定了默认值“London”,因此无需验证。 渲染表单时,要选择的整个城市列表作为引用数据在“cityMap”名称下的模型中提供。 以下清单显示了该示例:
...
镇:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>
前面的清单呈现一行单选按钮,每个单选按钮对应于 cityMap
中的一个值,并使用分隔符 ""
。 没有提供其他属性(宏的最后一个参数缺失)。 cityMap
对 map 中的每个键值对使用相同的 String。 map 的键是表单实际提交为 POST 请求参数的内容。 map 值是用户看到的标签。 在前面的示例中,给定一个由三个众所周知的城市组成的列表以及表单后备对象中的默认值,则 HTML 类似于以下内容:
镇:
<input type="radio" name="address.town" value="London">伦敦</input>
<input type="radio" name="address.town" value="Paris" checked="checked">巴黎</input>
<input type="radio" name="address.town" value="New York">纽约</input>
如果您的应用程序希望通过内部代码(例如)处理城市,则可以创建具有适当键的代码 map,如以下示例所示:
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");
Map<String, Object> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}
该代码现在生成的输出是单选按钮的值是相关代码,但用户仍然会看到更用户友好的城市名称,如下所示:
镇:
<input type="radio" name="address.town" value="LDN">伦敦</input>
<input type="radio" name="address.town" value="PRS" checked="checked">巴黎</input>
<input type="radio" name="address.town" value="NYC">纽约</input>
# 转义
前面介绍的表单宏的默认用法会生成符合 HTML 4.01 规范的 HTML 元素,并使用在 web.xml
文件中定义的 HTML 转义的默认值,Spring 的 bind 支持使用该值。 要使这些元素符合 XHTML 规范或覆盖默认的 HTML 转义值,您可以在模板(或模型,模板可以在其中看到它们)中指定两个变量。 在模板中指定它们的优点是可以在模板处理中的稍后将其更改为不同的值,以为表单中的不同字段提供不同的行为。
要将标签切换为符合 XHTML 规范,请为名为 xhtmlCompliant
的模型或上下文变量指定值 true
,如以下示例所示:
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>
处理此指令后,Spring 宏生成的任何元素现在都符合 XHTML 规范。
以类似的方式,您可以为每个字段指定 HTML 转义,如以下示例所示:
<#-- 在此之前,使用默认的 HTML 转义 -->
<#assign htmlEscape = true>
<#-- 下一个字段将使用 HTML 转义 -->
<@spring.formInput "command.name"/>
<#assign htmlEscape = false in spring>
<#-- 所有将来的字段都将在 HTML 转义关闭的情况下绑定 -->
# 14.4、Markup
Groovy Markup 模板引擎主要用于生成类 XML 标记(XML、XHTML、HTML5 等),但你也可以用它来生成任何基于文本的内容。Spring Framework 内置了将 Spring MVC 与 Groovy Markup 结合使用的集成功能。
注意: Groovy Markup 模板引擎需要 Groovy 2.3.1+。
# a、配置
下面的示例展示了如何配置 Groovy Markup 模板引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// 配置 Groovy Markup 模板引擎...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
下面的示例展示了如何在 XML 中进行相同的配置:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- 配置 Groovy Markup 模板引擎... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
# b、示例
与传统的模板引擎不同,Groovy Markup 依赖于使用构建器语法的 DSL。下面的示例展示了一个 HTML 页面的示例模板:
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page') // 我的页面
}
body {
p('This is an example of HTML contents') // 这是一个 HTML 内容的示例
}
}
# 14.5、Views
The Spring Framework 内置了对 Spring MVC 的集成,可以与任何能够在 JSR-223 Java 脚本引擎上运行的模板库一起使用。我们已经在不同的脚本引擎上测试了以下模板库:
集成任何其他脚本引擎的基本规则是它必须实现
ScriptEngine
和Invocable
接口。
# a、Requirements
您需要在类路径中包含脚本引擎,具体取决于脚本引擎:
- Nashorn (opens new window) JavaScript 引擎随 Java 8+ 提供。强烈建议使用最新的可用更新版本。
- 应添加 JRuby (opens new window) 作为 Ruby 支持的依赖项。
- 应添加 Jython (opens new window) 作为 Python 支持的依赖项。
您需要拥有脚本模板库。一种方法是通过 WebJars (opens new window) 获得 JavaScript 模板库。
# b、Templates
您可以声明一个 ScriptTemplateConfigurer
Bean,以指定要使用的脚本引擎、要加载的脚本文件、要调用的渲染模版函数等等。以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
以下示例显示了 XML 中相同的安排:
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>
对于 Java 和 XML 配置,Controller 看起来没有什么不同,如以下示例所示:
@Controller
public class SampleController {
@GetMapping("/sample")
public String test(Model model) {
model.addAttribute("title", "Sample title");
model.addAttribute("body", "Sample body");
return "template";
}
}
以下示例显示了 Mustache 模板:
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
使用以下参数调用渲染函数:
String template
: 模板内容Map model
: 视图模型RenderingContext renderingContext
:RenderingContext
,提供对应用程序上下文、区域设置、模板加载器和 URL 的访问(自 5.0 起)
Mustache.render()
在本质上与此签名兼容,因此您可以直接调用它。
如果您的模版技术需要一些自定义,您可以提供一个脚本来实现自定义渲染函数。例如,Handlerbars (opens new window) 需要在使用模板之前对其进行编译,并且需要一个 polyfill (opens new window) 来模拟某些在服务器端脚本引擎中不可用的浏览器功能。
以下示例显示了如何做到这一点:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
当将非线程安全的脚本引擎与并非为并发设计的模版库(例如在 Nashorn 上运行的 Handlebars 或 React)一起使用时,需要将
sharedEngine
属性设置为false
。在这种情况下,由于 此错误 (opens new window) ,需要 Java SE 8 update 60,但通常建议在任何情况下都使用最新的 Java SE 补丁版本。
polyfill.js
仅定义 Handlebars 正常运行所需的 window
对象,如下所示:
var window = {};
这个基本的 render.js
实现会在使用模板之前对其进行编译。生产就绪的实现还应存储任何重用的缓存模板或预编译模板。您可以在脚本端执行此操作(并处理所需的任何自定义 - 例如,管理模板引擎配置)。以下示例显示了如何做到这一点:
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
查看 Spring Framework 单元测试、Java (opens new window) 和 resources (opens new window),了解更多配置示例。
# 14.6、片段
HTMX (opens new window) 和 Hotwire Turbo (opens new window) 强调一种 “HTML over the wire” 的方法,即客户端接收服务器更新的内容是 HTML,而不是 JSON。 这样可以在不需要编写大量 JavaScript 代码(甚至不需要编写任何 JavaScript 代码)的情况下,获得 SPA(单页应用)的优点。 想要了解更多信息,请访问它们的官方网站。
在 Spring MVC 中,视图渲染通常需要指定一个视图和一个模型。但是,在 “HTML over the wire” 的方法中,一个常见的功能是发送多个 HTML 片段,浏览器可以使用这些片段来更新页面的不同部分。 为此,控制器方法可以返回 Collection<ModelAndView>
。 例如:
@GetMapping
List<ModelAndView> handle() {
return List.of(new ModelAndView("posts"), new ModelAndView("comments"));
}
也可以通过返回专门的类型 FragmentsRendering
来实现相同的效果:
@GetMapping
FragmentsRendering handle() {
return FragmentsRendering.with("posts").fragment("comments").build();
}
每个片段都可以有独立的模型,并且该模型会继承请求的共享模型的属性。
HTMX 和 Hotwire Turbo 支持通过 SSE(服务器发送事件)进行流式更新。 控制器可以使用 SseEmitter
发送 ModelAndView
,以便为每个事件渲染片段:
@GetMapping
SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
startWorkerThread(() -> {
try {
emitter.send(SseEmitter.event().data(new ModelAndView("posts")));
emitter.send(SseEmitter.event().data(new ModelAndView("comments")));
// ...
}
catch (IOException ex) {
// 取消发送
}
});
return emitter;
}
同样,也可以通过返回 Flux<ModelAndView>
或任何可以通过 ReactiveAdapterRegistry
适配到 Reactive Streams Publisher
的其他类型来实现。
# 14.7、和 JSTL
Spring Framework 内置了将 Spring MVC 与 JSP 和 JSTL 结合使用的集成。
# a、Resolvers
使用 JSP 进行开发时,通常需要声明一个 InternalResourceViewResolver
bean。
InternalResourceViewResolver
可以用于分发到任何 Servlet 资源,特别是 JSP。作为最佳实践,我们强烈建议将 JSP 文件放在 'WEB-INF'
目录下的一个目录中,这样客户端就无法直接访问。
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
# b、versus JSTL
当使用 JSP 标准标签库(JSTL)时,您必须使用一个特殊的视图类 JstlView
,因为 JSTL 需要一些准备工作,例如 I18N 功能才能工作。
# c、的 JSP 标签库
Spring 提供了请求参数与命令对象的数据绑定,如前面的章节所述。为了方便 JSP 页面的开发并结合这些数据绑定功能,Spring 提供了一些标签,使事情变得更加容易。所有 Spring 标签都具有 HTML 转义功能,可以启用或禁用字符的转义。
spring.tld
标签库描述符(TLD)包含在 spring-webmvc.jar
中。 关于各个标签的完整参考,请浏览 API 参考 (opens new window) 或查看标签库描述。
# d、的 form 标签库
从 2.0 版本开始,Spring 提供了一套完整的数据绑定感知标签,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标签都支持其对应的 HTML 标签的属性集,使这些标签使用起来既熟悉又直观。标签生成的 HTML 符合 HTML 4.01/XHTML 1.0 标准。
与其他表单/输入标签库不同,Spring 的表单标签库与 Spring Web MVC 集成,使标签可以访问控制器处理的命令对象和参考数据。正如我们在以下示例中展示的那样,表单标签使 JSP 更易于开发、阅读和维护。
我们将介绍表单标签,并查看每个标签如何使用的示例。在某些标签需要进一步说明的地方,我们包含已生成的 HTML 代码段。
# 配置
表单标签库捆绑在 spring-webmvc.jar
中。库描述符名为 spring-form.tld
。
要使用此库中的标签,请将以下指令添加到 JSP 页面的顶部:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
其中 form
是您要用于此库中标签的标签名称前缀。
# 标签
此标签呈现一个 HTML 'form' 元素,并公开一个绑定路径到内部标签以进行绑定。 它将命令对象放入 PageContext
中,以便内部标签可以访问命令对象。 此库中的所有其他标签都是 form
标签的嵌套标签。
假设我们有一个名为 User
的领域对象。 它是一个具有属性(例如 firstName
和 lastName
)的 JavaBean。 我们可以将其用作 form 控制器的表单支持对象,该控制器返回 form.jsp
。 以下示例显示了 form.jsp
的外观:
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
firstName
和 lastName
值是从页面控制器放置在 PageContext
中的命令对象中检索的。 继续阅读以查看有关如何在 form
标签中使用内部标签的更复杂的示例。
以下列表显示了生成的 HTML,它看起来像一个标准表单:
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value="Harry"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value="Potter"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
前面的 JSP 假定表单支持对象的变量名为 command
。 如果您已将表单支持对象以另一个名称放入模型中(绝对是最佳实践),则可以将表单绑定到命名的变量,如以下示例所示:
<form:form modelAttribute="user">
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
# input
标签
此标签呈现一个 HTML input
元素,其绑定值默认为 type='text'
。 有关此标签的示例,请参见 Form 标签。 您也可以使用特定于 HTML5 的类型,例如 email
、tel
、date
等。
# checkbox
标签
此标签呈现一个 HTML input
标签,并将 type
设置为 checkbox
。
假设我们的 User
具有一些偏好设置,例如新闻通讯订阅和一些兴趣爱好。 以下示例显示了 Preferences
类:
public class Preferences {
private boolean receiveNewsletter;
private String[] interests;
private String favouriteWord;
public boolean isReceiveNewsletter() {
return receiveNewsletter;
}
public void setReceiveNewsletter(boolean receiveNewsletter) {
this.receiveNewsletter = receiveNewsletter;
}
public String[] getInterests() {
return interests;
}
public void setInterests(String[] interests) {
this.interests = interests;
}
public String getFavouriteWord() {
return favouriteWord;
}
public void setFavouriteWord(String favouriteWord) {
this.favouriteWord = favouriteWord;
}
}
相应的 form.jsp
可能类似于以下内容:
<form:form>
<table>
<tr>
<td>Subscribe to newsletter?:</td>
<%-- Approach 1: Property is of type java.lang.Boolean --%>
<td><form:checkbox path="preferences.receiveNewsletter"/></td>
</tr>
<tr>
<td>Interests:</td>
<%-- Approach 2: Property is of an array or of type java.util.Collection --%>
<td>
Quidditch: <form:checkbox path="preferences.interests" value="Quidditch"/>
Herbology: <form:checkbox path="preferences.interests" value="Herbology"/>
Defence Against the Dark Arts: <form:checkbox path="preferences.interests" value="Defence Against the Dark Arts"/>
</td>
</tr>
<tr>
<td>Favourite Word:</td>
<%-- Approach 3: Property is of type java.lang.Object --%>
<td>
Magic: <form:checkbox path="preferences.favouriteWord" value="Magic"/>
</td>
</tr>
</table>
</form:form>
checkbox
标签有三种方法,应该满足所有复选框的需求。
- 方法 1:当绑定值的类型为
java.lang.Boolean
时,如果绑定值为true
,则将input(checkbox)
标记为checked
。value
属性对应于setValue(Object)
值属性的已解析值。 - 方法 2:当绑定值的类型为
array
或java.util.Collection
时,如果配置的setValue(Object)
值存在于绑定的Collection
中,则将input(checkbox)
标记为checked
。 - 方法 3:对于任何其他绑定值类型,如果配置的
setValue(Object)
等于绑定值,则将input(checkbox)
标记为checked
。
请注意,无论采用哪种方法,都会生成相同的 HTML 结构。 以下 HTML 代码段定义了一些复选框:
<tr>
<td>Interests:</td>
<td>
Quidditch: <input name="preferences.interests" type="checkbox" value="Quidditch"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Herbology: <input name="preferences.interests" type="checkbox" value="Herbology"/>
<input type="hidden" value="1" name="_preferences.interests"/>
Defence Against the Dark Arts: <input name="preferences.interests" type="checkbox" value="Defence Against the Dark Arts"/>
<input type="hidden" value="1" name="_preferences.interests"/>
</td>
</tr>
您可能不希望在每个复选框之后看到其他隐藏字段。 当 HTML 页面中的复选框未选中时,其值不会作为 HTTP 请求参数的一部分发送到服务器,因此我们需要一种解决方法来使 Spring 表单数据绑定能够正常工作。 checkbox
标签遵循现有的 Spring 约定,即为每个复选框包含一个带有下划线 ( _
) 前缀的隐藏参数。 这样,您实际上是在告诉 Spring:“复选框在表单中可见,并且我希望我的表单数据绑定到的对象反映复选框的状态,无论如何。”
# checkboxes
标签
此标签呈现多个 HTML input
标签,并将 type
设置为 checkbox
。
本节建立在上一节有关 checkbox
标签的示例之上。 有时,您可能不想在 JSP 页面中列出所有可能的兴趣爱好。 您宁愿在运行时提供可用选项的列表,并将其传递到标签中。 这就是 checkboxes
标签的目的。 您可以传入一个 Array
、一个 List
或一个 Map
,其中包含 items
属性中的可用选项。 通常,绑定属性是一个集合,以便它可以容纳用户选择的多个值。 以下示例显示了一个使用此标签的 JSP:
<form:form>
<table>
<tr>
<td>Interests:</td>
<td>
<%-- Property is of an array or of type java.util.Collection --%>
<form:checkboxes path="preferences.interests" items="${interestList}"/>
</td>
</tr>
</table>
</form:form>
此示例假定 interestList
是一个 List
,可用作模型属性,其中包含要从中选择的值的字符串。 如果使用 Map
,则 map 条目键将用作值,而 map 条目的值将用作要显示的标签。 您还可以使用自定义对象,在其中可以使用 itemValue
提供值的属性名称,并使用 itemLabel
提供标签。
# radiobutton
标签
此标签呈现一个 HTML input
元素,并将 type
设置为 radio
。
一个典型的用法模式涉及绑定到同一属性但具有不同值的多个标签实例,如以下示例所示:
<tr>
<td>Sex:</td>
<td>
Male: <form:radiobutton path="sex" value="M"/> <br/>
Female: <form:radiobutton path="sex" value="F"/>
</td>
</tr>
# radiobuttons
标签
此标签呈现多个 HTML input
元素,并将 type
设置为 radio
。
与 checkboxes
标签 一样,您可能希望将可用选项作为运行时变量传入。 对于此用法,可以使用 radiobuttons
标签。 您传入一个 Array
、一个 List
或一个 Map
,其中包含 items
属性中的可用选项。 如果您使用 Map
,则 map 条目键将用作值,并且 map 条目的值将用作要显示的标签。 您还可以使用自定义对象,在其中可以使用 itemValue
提供值的属性名称,并使用 itemLabel
提供标签,如以下示例所示:
<tr>
<td>Sex:</td>
<td><form:radiobuttons path="sex" items="${sexOptions}"/></td>
</tr>
# password
标签
此标签呈现一个 HTML input
标签,其类型设置为 password
,并带有绑定值。
<tr>
<td>Password:</td>
<td>
<form:password path="password"/>
</td>
</tr>
请注意,默认情况下,密码值不会显示。 如果确实要显示密码值,则可以将 showPassword
属性的值设置为 true
,如以下示例所示:
<tr>
<td>Password:</td>
<td>
<form:password path="password" value="^76525bvHGq" showPassword="true"/>
</td>
</tr>
# select
标签
此标签呈现一个 HTML 'select' 元素。 它支持数据绑定到所选选项,以及使用嵌套的 option
和 options
标签。
假设一个 User
具有一个技能列表。 相应的 HTML 可能如下所示:
<tr>
<td>Skills:</td>
<td><form:select path="skills" items="${skills}"/></td>
</tr>
如果 User
的技能在 Herbology 中,则“Skills”行的 HTML 源代码可能如下所示:
<tr>
<td>Skills:</td>
<td>
<select name="skills" multiple="true">
<option value="Potions">Potions</option>
<option value="Herbology" selected="selected">Herbology</option>
<option value="Quidditch">Quidditch</option>
</select>
</td>
</tr>
# option
标签
此标签呈现一个 HTML option
元素。 它根据绑定值设置 selected
。 以下 HTML 显示了它的典型输出:
<tr>
<td>House:</td>
<td>
<form:select path="house">
<form:option value="Gryffindor"/>
<form:option value="Hufflepuff"/>
<form:option value="Ravenclaw"/>
<form:option value="Slytherin"/>
</form:select>
</td>
</tr>
如果 User
的房子在格兰芬多,则“House”行的 HTML 源代码将如下所示:
<tr>
<td>House:</td>
<td>
<select name="house">
<option value="Gryffindor" selected="selected">Gryffindor</option> <i class="conum" data-value="1"></i><b>(1)</b>
<option value="Hufflepuff">Hufflepuff</option>
<option value="Ravenclaw">Ravenclaw</option>
<option value="Slytherin">Slytherin</option>
</select>
</td>
</tr>
(1): 注意添加了一个 selected
属性
# options
标签
此标签呈现一个 HTML option
元素的列表。 它根据绑定值设置 selected
属性。 以下 HTML 显示了它的典型输出:
<tr>
<td>Country:</td>
<td>
<form:select path="country">
<form:option value="-" label="--Please Select"/>
<form:options items="${countryList}" itemValue="code" itemLabel="name"/>
</form:select>
</td>
</tr>
如果 User
居住在英国,则“Country”行的 HTML 源代码将如下所示:
<tr>
<td>Country:</td>
<td>
<select name="country">
<option value="-">--Please Select</option>
<option value="AT">Austria</option>
<option value="UK" selected="selected">United Kingdom</option> <i class="conum" data-value="1"></i><b>(1)</b>
<option value="US">United States</option>
</select>
</td>
</tr>
(1): 注意添加了一个 selected
属性
正如前面的示例所示,option
标签与 options
标签的组合使用会生成相同的标准 HTML,但允许您在 JSP 中显式指定一个仅用于显示的值(它所属的位置),例如示例中的默认字符串:“-- Please Select”。
通常使用项目对象的集合或数组填充 items
属性。 如果指定,itemValue
和 itemLabel
指的是这些项目对象的 bean 属性。 否则,项目对象本身将转换为字符串。 或者,您可以指定一个 Map
的项目,在这种情况下,map 键将被解释为选项值,而 map 值对应于选项标签。 如果还指定了 itemValue
或 itemLabel
(或两者都指定了),则项目值属性将应用于 map 键,而项目标签属性将应用于 map 值。
# textarea
标签
此标签呈现一个 HTML textarea
元素。 以下 HTML 显示了它的典型输出:
<tr>
<td>Notes:</td>
<td><form:textarea path="notes" rows="3" cols="20"/></td>
<td><form:errors path="notes"/></td>
</tr>
# hidden
标签
此标签呈现一个 HTML input
标签,并把 type
设置为 hidden
且包含绑定值。 要提交未绑定的隐藏值,请使用 HTML input
标签,并将 type
设置为 hidden
。 以下 HTML 显示了它的典型输出:
<form:hidden path="house"/>
如果我们选择将 house
值作为隐藏值提交,则 HTML 将如下所示:
<input name="house" type="hidden" value="Gryffindor"/>
# errors
标签
此标签在 HTML span
元素中呈现字段错误。 它提供对控制器中创建的错误或与控制器关联的任何验证器创建的错误的访问。
假设我们要在提交表单后显示 firstName
和 lastName
字段的所有错误消息。 我们有一个用于 User
类实例的验证器,名为 UserValidator
,如以下示例所示:
public class UserValidator implements Validator {
public boolean supports(Class candidate) {
return User.class.isAssignableFrom(candidate);
}
public void validate(Object obj, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");
}
}
form.jsp
可以如下所示:
<form:form>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<%-- Show errors for firstName field --%>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<%-- Show errors for lastName field --%>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
如果我们在 firstName
和 lastName
字段中提交一个带有空值的表单,则 HTML 将如下所示:
<form method="POST">
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<%-- Associated errors to firstName field displayed --%>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<%-- Associated errors to lastName field displayed --%>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
如果我们想要显示给定页面的整个错误列表怎么办? 下一个示例表明 errors
标签还支持一些基本的通配符功能。
path="*"
: 显示所有错误。path="lastName"
: 显示与lastName
字段关联的所有错误。- 如果省略
path
,则仅显示对象错误。
以下示例显示了页面顶部的错误列表,然后在字段旁边显示了特定于字段的错误:
<form:form>
<form:errors path="*" cssClass="errorBox"/>
<table>
<tr>
<td>First Name:</td>
<td><form:input path="firstName"/></td>
<td><form:errors path="firstName"/></td>
</tr>
<tr>
<td>Last Name:</td>
<td><form:input path="lastName"/></td>
<td><form:errors path="lastName"/></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form:form>
HTML 将如下所示:
<form method="POST">
<span name="*.errors" class="errorBox">Field is required.<br/>Field is required.</span>
<table>
<tr>
<td>First Name:</td>
<td><input name="firstName" type="text" value=""/></td>
<td><span name="firstName.errors">Field is required.</span></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="lastName" type="text" value=""/></td>
<td><span name="lastName.errors">Field is required.</span></td>
</tr>
<tr>
<td colspan="3">
<input type="submit" value="Save Changes"/>
</td>
</tr>
</table>
</form>
spring-form.tld
标签库描述符(TLD)包含在 spring-webmvc.jar
中。 有关各个标签的完整参考,请浏览 API 参考 (opens new window) 或查看标签库描述。
# 方法转换
REST 的一个关键原则是使用“统一接口”。 这意味着所有资源(URL)都可以通过使用相同的四种 HTTP 方法(GET,PUT,POST 和 DELETE)进行操作。 对于每种方法,HTTP 规范定义了确切的语义。 例如,GET 应该始终是一个安全的操作,这意味着它没有副作用,并且 PUT 或 DELETE 应该是幂等的,这意味着您可以一遍又一遍地重复这些操作,但是最终结果应该是相同的。 虽然 HTTP 定义了这四种方法,但 HTML 仅支持两种:GET 和 POST。 幸运的是,有两种可能的解决方法:您可以使用 JavaScript 执行 PUT 或 DELETE,也可以使用 POST,并将“真实”方法作为附加参数(在 HTML 表单中建模为隐藏的输入字段)。 Spring 的 HiddenHttpMethodFilter
使用后一种技巧。 此过滤器是一个普通的 Servlet 过滤器,因此,它可以与任何 Web 框架(而不仅仅是 Spring MVC)结合使用。 将此过滤器添加到您的 web.xml,带有隐藏 method
参数的 POST 将转换为相应的 HTTP 方法请求。
为了支持 HTTP 方法转换,Spring MVC 表单标签已更新为支持设置 HTTP 方法。 例如,以下代码段来自宠物诊所示例:
<form:form method="delete">
<p class="submit"><input type="submit" value="Delete Pet"/></p>
</form:form>
前面的示例执行 HTTP POST,并将“真实” DELETE 方法隐藏在请求参数后面。 它由在 web.xml 中定义的 HiddenHttpMethodFilter
拾取,如以下示例所示:
<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<servlet-name>petclinic</servlet-name>
</filter-mapping>
以下示例显示了相应的 @Controller
方法:
@RequestMapping(method = RequestMethod.DELETE)
public String deletePet(@PathVariable int ownerId, @PathVariable int petId) {
this.clinic.deletePet(petId);
return "redirect:/owners/" + ownerId;
}
# HTML5 标签
Spring 表单标签库允许输入动态属性,这意味着您可以输入任何 HTML5 特定的属性。
表单 input
标签支持输入 text
以外的 type 属性。 这旨在允许呈现新的 HTML5 特定输入类型,例如 email
,date
,range
等。 请注意,不需要输入 type='text'
,因为 text
是默认类型。
# 14.8、and Atom
AbstractAtomFeedView
和 AbstractRssFeedView
都继承自 AbstractFeedView
基类,分别用于提供 Atom 和 RSS Feed 视图。它们基于 ROME (opens new window) 项目,并位于 org.springframework.web.servlet.view.feed
包中。
AbstractAtomFeedView
要求你实现 buildFeedEntries()
方法,并可以选择性地重写 buildFeedMetadata()
方法(默认实现为空)。以下示例展示了如何做到这一点:
public class SampleContentAtomView extends AbstractAtomFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// 忽略具体实现
}
@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 忽略具体实现
}
}
实现 AbstractRssFeedView
也有类似的要求,如下例所示:
public class SampleContentRssView extends AbstractRssFeedView {
@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// 忽略具体实现
}
@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// 忽略具体实现
}
}
buildFeedItems()
和 buildFeedEntries()
方法传入了 HTTP 请求,以便在你需要访问 Locale 时使用。HTTP 响应仅用于设置 cookie 或其他 HTTP 标头。方法返回后,feed 会自动写入响应对象。
有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team Blog 文章 (opens new window)。
# 14.9、and Excel
Spring 提供了返回 HTML 之外的其他输出的方法,包括 PDF 和 Excel 电子表格。本节介绍如何使用这些功能。
# a、文档视图简介
HTML 页面并非总是用户查看模型输出的最佳方式,Spring 使从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是一个视图,并以正确的内容类型从服务器流式传输,(希望)使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序以做出响应。
为了使用 Excel 视图,你需要将 Apache POI 库添加到你的类路径中。对于 PDF 生成,你需要添加(最好是)OpenPDF 库。
注意
你应该尽可能使用底层文档生成库的最新版本。 特别是,我们强烈建议使用 OpenPDF(例如,OpenPDF 1.2.12)而不是过时的原始 iText 2.1.7,因为 OpenPDF 得到了积极维护,并修复了针对不受信任的 PDF 内容的重要漏洞。
# b、Views
一个用于单词列表的简单 PDF 视图可以扩展 org.springframework.web.servlet.view.document.AbstractPdfView
并实现 buildPdfDocument()
方法,如下例所示:
import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.springframework.web.servlet.view.document.AbstractPdfView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public class PdfWordList extends AbstractPdfView {
@Override
protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {
List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}
控制器可以从外部视图定义(通过名称引用它)或作为处理程序方法中的 View
实例返回这样的视图。
# c、Views
自 Spring Framework 4.2 起,org.springframework.web.servlet.view.document.AbstractXlsView
被提供作为 Excel 视图的基类。它基于 Apache POI,具有专门的子类(AbstractXlsxView
和 AbstractXlsxStreamingView
),它们取代了过时的 AbstractExcelView
类。
编程模型类似于 AbstractPdfView
,其中 buildExcelDocument()
作为中心模板方法,控制器能够从外部定义(按名称)或作为处理程序方法中的 View
实例返回这样的视图。
# 14.10、Jackson
Spring提供了对Jackson JSON库的支持。
# a、Jackson-based JSON MVC Views
MappingJackson2JsonView
使用Jackson库的 ObjectMapper
将响应内容呈现为JSON。 默认情况下,模型映射的整个内容(框架特定的类除外)被编码为JSON。 如果需要过滤映射的内容,您可以通过使用 modelKeys
属性来指定要编码的特定模型属性集。 您还可以使用 extractValueFromSingleKeyModel
属性来提取单键模型中的值并直接序列化,而不是作为模型属性的映射。
您可以根据需要使用Jackson提供的注解来自定义JSON映射。 如果需要进一步的控制,您可以通过 ObjectMapper
属性注入自定义的 ObjectMapper
,以便为特定类型提供自定义的JSON序列化器和反序列化器。
# b、Jackson-based XML Views
MappingJackson2XmlView
使用 Jackson XML extension (opens new window) 的 XmlMapper
将响应内容呈现为XML。 如果模型包含多个条目,则应使用 modelKey
bean属性显式设置要序列化的对象。 如果模型包含单个条目,则会自动序列化。
您可以根据需要使用JAXB或Jackson提供的注解来自定义XML映射。 如果需要进一步的控制,您可以通过 ObjectMapper
属性注入自定义的 XmlMapper
,以便为特定类型提供自定义的XML序列化器和反序列化器。
# 14.11、Marshalling
MarshallingView
使用 XML Marshaller
(定义在 org.springframework.oxm
包中)将响应内容渲染为 XML。你可以通过使用 MarshallingView
实例的 modelKey
bean 属性显式设置要被 Marshalling 的对象。或者,视图会遍历所有模型属性,并 Marshalling Marshaller
支持的第一个类型。有关 org.springframework.oxm
包中功能的更多信息,请参见 使用 O/X 映射器 Marshalling XML (opens new window)。
# 14.12、Views
XSLT 是一种用于 XML 的转换语言,在 Web 应用程序中作为视图技术非常流行。如果您的应用程序本身就处理 XML,或者您的模型可以轻松地转换为 XML,那么 XSLT 可以作为一种不错的视图技术选择。以下部分展示了如何在 Spring Web MVC 应用程序中生成 XML 文档作为模型数据,并使用 XSLT 对其进行转换。
本示例是一个简单的 Spring 应用程序,它在 Controller
中创建一个单词列表,并将它们添加到模型映射中。该映射与 XSLT 视图的视图名称一起返回。有关 Spring Web MVC 的 Controller
接口的详细信息,请参阅 Annotated Controllers。XSLT 控制器将单词列表转换为一个简单的 XML 文档,以便进行转换。
# a、Beans
对于一个简单的 Spring Web 应用程序来说,配置是标准的:MVC 配置必须定义一个 XsltViewResolver
bean 和常规的 MVC 注解配置。以下示例展示了如何做到这一点:
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
# b、Controller
我们还需要一个 Controller 来封装我们的单词生成逻辑。
Controller 的逻辑封装在一个 @Controller
类中,处理方法定义如下:
@Controller
public class XsltController {
@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");
List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}
model.addAttribute("wordList", root);
return "home";
}
}
到目前为止,我们只创建了一个 DOM 文档并将其添加到 Model 映射中。请注意,您也可以加载 XML 文件作为 Resource
,并使用它来代替自定义 DOM 文档。
有一些软件包可以自动“domify”一个对象图,但是在 Spring 中,您可以完全灵活地以任何您选择的方式从您的模型创建 DOM。这可以防止 XML 转换在您的模型数据的结构中发挥过大的作用,而当使用工具管理 domification 过程时,这是一种危险。
# c、Transformation
最后,XsltViewResolver
解析 “home” XSLT 模板文件,并将 DOM 文档合并到其中以生成我们的视图。如 XsltViewResolver
配置中所示,XSLT 模板位于 war
文件中的 WEB-INF/xsl
目录中,并以 xslt
文件扩展名结尾。
以下示例展示了一个 XSLT 转换:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes"/>
<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>
<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>
</xsl:stylesheet>
上述转换呈现为以下 HTML:
<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>
# 15、MVC配置
MVC Java配置和MVC XML命名空间为大多数应用程序提供了合适的默认配置,并提供了一个配置API来自定义它。
对于更高级的自定义,这些自定义在配置API中不可用,请参见高级Java配置和高级XML配置。
你不需要理解MVC Java配置和MVC命名空间创建的底层bean。如果你想了解更多,请参见特殊Bean类型和Web MVC配置。
# 15.1、启用 MVC 配置
你可以使用 @EnableWebMvc
注解通过编程方式启用 MVC 配置,或者使用 XML 配置中的 <mvc:annotation-driven>
,如下例所示:
@Configuration
@EnableWebMvc
public class WebConfiguration {
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<mvc:annotation-driven/>
</beans>
注意: 当使用 Spring Boot 时,你可能希望使用类型为 WebMvcConfigurer
的 @Configuration
类,但不要使用 @EnableWebMvc
,以保留 Spring Boot MVC 的自定义配置。有关更多详细信息,请参阅 MVC 配置 API 部分 和 Spring Boot 专用文档 (opens new window)。
上面的例子注册了一些 Spring MVC 的基础设施 Bean,并且可以根据类路径上可用的依赖进行调整(例如,JSON、XML 和其他格式的有效载荷转换器)。
# 15.2、配置 API
你可以通过实现 WebMvcConfigurer
接口在 Java 配置中自定义 MVC,如下例所示:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
// 实现配置方法...
}
在 XML 中,你可以查看 <mvc:annotation-driven/>
的属性和子元素。 你可以查看 Spring MVC XML schema (opens new window) 或使用 IDE 的代码完成功能来发现可用的属性和子元素。
# 15.3、类型转换
默认情况下,会安装各种数字和日期类型的格式化器,并支持通过字段和参数上的 @NumberFormat
、@DurationFormat
和 @DateTimeFormat
进行自定义。
要注册自定义格式化器和转换器,请使用以下方法:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// ...
}
}
<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>
默认情况下,Spring MVC 在解析和格式化日期值时会考虑请求的区域设置(Locale)。这适用于日期表示为带有 "input" 表单字段的字符串的表单。但是,对于 "date" 和 "time" 表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,可以按如下方式自定义日期和时间格式:
@Configuration
public class DateTimeWebConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
注意: 有关何时使用 FormatterRegistrar 实现的更多详细信息,请参阅 FormatterRegistrar SPI 和
FormattingConversionServiceFactoryBean
。
# 15.4、Validation
默认情况下,如果类路径中存在 Bean Validation (例如,Hibernate Validator),LocalValidatorFactoryBean
会被注册为一个全局的 Validator
,以便在控制器方法参数上使用 @Valid
和 @Validated
。
你可以自定义全局的 Validator
实例,如下例所示:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public Validator getValidator() {
Validator validator = new OptionalValidatorFactoryBean();
// ...
return validator;
}
}
注意,你也可以注册本地的 Validator
实现,如下例所示:
@Controller
public class MyController {
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.addValidators(new FooValidator());
}
}
提示: 如果你需要将 LocalValidatorFactoryBean
注入到某个地方,创建一个 bean 并用 @Primary
注解标记它,以避免与 MVC 配置中声明的 bean 冲突。
# a、Interceptors
你可以注册拦截器来应用于传入的请求,如下面的例子所示:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor());
}
}
<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:interceptors>
警告: 拦截器并不理想地适合作为安全层,因为可能与注解控制器路径匹配不一致。通常,我们建议使用Spring Security,或者使用与Servlet过滤器链集成的类似方法,并尽早应用它。
注意: XML配置将拦截器声明为MappedInterceptor
beans,这些beans依次被任何HandlerMapping
bean检测到,包括来自其他框架的beans。 相比之下,Java配置仅将拦截器传递给它管理的HandlerMapping
bean。为了在Spring MVC和其他框架的带有MVC Java配置的HandlerMapping
bean之间重用相同的拦截器,可以声明MappedInterceptor
bean(并且不要在Java配置中手动添加它们),或者在Java配置和其他HandlerMapping
bean中配置相同的拦截器。
# 15.5、Types
你可以配置 Spring MVC 如何从请求中确定请求的媒体类型(例如,Accept
header,URL 路径扩展,查询参数等)。
默认情况下,只会检查 Accept
header。
如果必须使用基于 URL 的内容类型解析,请考虑使用查询参数策略而不是路径扩展。 有关更多详细信息, 请参见 后缀匹配 和 后缀匹配和 RFD。
你可以自定义请求的内容类型解析,如下例所示:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.mediaType("json", MediaType.APPLICATION_JSON);
configurer.mediaType("xml", MediaType.APPLICATION_XML);
}
}
<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"/>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="mediaTypes">
<value>
json=application/json
xml=application/xml
</value>
</property>
</bean>
# 15.6、消息转换器
你可以通过覆盖 configureMessageConverters()
方法来设置Java配置中使用的 HttpMessageConverter
实例,替换默认使用的实例。你也可以通过覆盖 extendMessageConverters()
方法来自定义已配置的消息转换器列表。
提示: 在 Spring Boot 应用程序中, 除了默认的转换器之外,
WebMvcAutoConfiguration
还会添加它检测到的任何HttpMessageConverter
的 Bean. 因此, 在 Boot 应用程序中, 最好使用 HttpMessageConverters (opens new window) 机制. 或者, 也可以使用extendMessageConverters
在末尾修改消息转换器.
以下示例添加了 XML 和 Jackson JSON 转换器,并使用自定义的 ObjectMapper
代替默认的转换器:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
<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"/>
在上述示例中,Jackson2ObjectMapperBuilder
用于为 MappingJackson2HttpMessageConverter
和 MappingJackson2XmlHttpMessageConverter
创建一个通用配置,启用缩进、自定义日期格式并注册 jackson-module-parameter-names
(opens new window),它增加了对访问参数名称的支持(Java 8 中添加的功能)。
此构建器按如下方式自定义 Jackson 的默认属性:
禁用
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
(反序列化时,遇到未知属性时失败)。禁用
MapperFeature.DEFAULT_VIEW_INCLUSION
(禁用默认视图包含)。
如果检测到以下已知的模块在类路径中,它也会自动注册它们:
jackson-datatype-jsr310
(opens new window): 支持 Java 8 的日期和时间 API 类型。jackson-datatype-jdk8
(opens new window): 支持其他 Java 8 类型,例如Optional
。
注意: 要使用 Jackson XML 支持启用缩进, 除了
jackson-dataformat-xml
(opens new window) 之外, 还需要woodstox-core-asl
(opens new window) 依赖项。
还有其他有趣的 Jackson 模块可用:
jackson-datatype-money
(opens new window): 支持javax.money
类型(非官方模块)。jackson-datatype-hibernate
(opens new window): 支持 Hibernate 特定的类型和属性(包括延迟加载方面)。
# 15.7、Controllers (视图控制器)
这是一个用于定义 ParameterizableViewController
的快捷方式,该控制器在调用时立即转发到视图。当在视图生成响应之前不需要运行 Java 控制器逻辑的静态情况下,可以使用它。
下面的例子将对 /
的请求转发到一个名为 home
的视图:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("home");
}
}
如果一个 @RequestMapping
方法被映射到任何 HTTP 方法的 URL,那么视图控制器就不能用于处理相同的 URL。这是因为通过 URL 匹配到一个注解的控制器被认为是足够强的端点所有权指示,以便可以向客户端发送 405 (METHOD_NOT_ALLOWED)、415 (UNSUPPORTED_MEDIA_TYPE) 或类似的响应,以帮助调试。因此,建议避免在一个注解的控制器和一个视图控制器之间分割 URL 处理。
# 15.8、Resolvers
MVC 配置简化了视图解析器的注册。
以下示例通过使用 JSP 和 Jackson 作为 JSON 渲染的默认 View
来配置内容协商视图解析:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
<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、Groovy Markup 和脚本模板也需要配置底层视图技术。以下示例使用 FreeMarker:
@Configuration
public class FreeMarkerConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.freeMarker().cache(false);
}
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/freemarker");
return configurer;
}
}
<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-views="false"/>
</mvc:view-resolvers>
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/freemarker"/>
</mvc:freemarker-configurer>
# 15.9、静态资源
此选项提供了一种便捷的方式,可以从基于 Resource
的位置列表中提供静态资源。
在下面的示例中,如果请求以 /resources
开头,则相对路径用于查找和提供相对于 Web 应用程序根目录下的 /public
或类路径下的 /static
的静态资源。 这些资源具有一年的过期时间,以确保最大限度地利用浏览器缓存并减少浏览器发出的 HTTP 请求。 Last-Modified
信息是从 Resource#lastModified
推断出来的,因此 HTTP 条件请求支持 "Last-Modified"
标头。
以下清单显示了如何执行此操作:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
<mvc:resources mapping="/resources/**" location="/public, classpath:/static/"
cache-period="31556926" />
另请参阅 静态资源的 HTTP 缓存支持。
资源处理器还支持 ResourceResolver
实现和 ResourceTransformer
实现的链,您可以使用它们来创建工具链,以便处理优化的资源。
您可以使用 VersionResourceResolver
获取基于从内容计算出的 MD5 哈希、固定的应用程序版本或其他信息的版本化资源 URL 。ContentVersionStrategy
(MD5 哈希)是一个不错的选择 - 但也有一些值得注意的例外,例如与模块加载器一起使用的 JavaScript 资源。
以下示例显示了如何使用 VersionResourceResolver
:
@Configuration
public class VersionedConfiguration implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public/")
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
}
}
<mvc:resources mapping="/resources/**" location="/public/">
<mvc:resource-chain resource-cache="true">
<mvc:resolvers>
<mvc:version-resolver>
<mvc:content-version-strategy patterns="/**"/>
</mvc:version-resolver>
</mvc:resolvers>
</mvc:resource-chain>
</mvc:resources>
然后,您可以使用 ResourceUrlProvider
重写 URL 并应用解析器和转换器的完整链 - 例如,插入版本。 MVC 配置提供了一个 ResourceUrlProvider
bean,以便可以将其注入到其他 bean 中。您还可以使用 ResourceUrlEncodingFilter
为 Thymeleaf,JSP,FreeMarker 和其他依赖于 HttpServletResponse#encodeURL
的 URL 标签提供透明的重写。
请注意,当同时使用 EncodedResourceResolver
(例如,用于提供 gzip 或 brotli 编码的资源)和 VersionResourceResolver
时,您必须按此顺序注册它们。 这样可确保始终根据未编码的文件可靠地计算基于内容的版本。
对于 WebJars (opens new window),建议使用版本化的 URL,例如 /webjars/jquery/1.2.0/jquery.min.js
,并且是最有效的使用方式。 相关的资源位置已通过 Spring Boot 开箱即用进行配置(或者可以通过 ResourceHandlerRegistry
手动配置),并且不需要添加 org.webjars:webjars-locator-core
依赖项。
像 /webjars/jquery/jquery.min.js
这样的无版本 URL 通过 WebJarsResourceResolver
受支持,该解析器在类路径上存在 org.webjars:webjars-locator-core
库时会自动注册,但代价是类路径扫描可能会减慢应用程序的启动速度。 解析器可以重写 URL 以包含 jar 的版本,并且还可以匹配没有传入版本的 URL - 例如,从 /webjars/jquery/jquery.min.js
到 /webjars/jquery/1.2.0/jquery.min.js
。
Tip:
基于
ResourceHandlerRegistry
的 Java 配置提供了更多选项,可以进行细粒度控制,例如,上次修改行为和优化的资源解析。
# 15.10、默认Servlet
Spring MVC 允许将 DispatcherServlet
映射到 /
(从而覆盖容器默认 Servlet 的映射),同时仍然允许静态资源请求由容器的默认 Servlet 处理。它配置了一个 DefaultServletHttpRequestHandler
,其 URL 映射为 /**
,并且相对于其他 URL 映射具有最低的优先级。
这个处理器会将所有请求转发到默认 Servlet。因此,它必须在所有其他 URL HandlerMappings
中保持最后。如果您使用 <mvc:annotation-driven>
,情况就是这样。或者,如果您设置了自己的自定义 HandlerMapping
实例,请确保将其 order
属性设置为低于 DefaultServletHttpRequestHandler
的值,该值为 Integer.MAX_VALUE
。
以下示例展示了如何使用默认设置启用该功能:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
}
<mvc:default-servlet-handler/>
覆盖 /
Servlet 映射的注意事项是,必须按名称而不是按路径检索默认 Servlet 的 RequestDispatcher
。DefaultServletHttpRequestHandler
尝试在启动时自动检测容器的默认 Servlet,它使用一个已知名称列表,适用于大多数主要的 Servlet 容器(包括 Tomcat、Jetty、GlassFish、JBoss、WebLogic 和 WebSphere)。如果默认 Servlet 已通过不同的名称进行了自定义配置,或者如果正在使用不同的 Servlet 容器(其中默认 Servlet 名称未知),则必须显式提供默认 Servlet 的名称,如以下示例所示:
@Configuration
public class CustomDefaultServletConfiguration implements WebMvcConfigurer {
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable("myCustomDefaultServlet");
}
}
<mvc:default-servlet-handler default-servlet-name="myCustomDefaultServlet"/>
# 15.11、Matching
你可以自定义与路径匹配和 URL 处理相关的选项。有关各个选项的详细信息,请参阅 PathMatchConfigurer
(opens new window) 的 Javadoc。
以下示例展示了如何自定义路径匹配:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
}
private PathPatternParser patternParser() {
PathPatternParser pathPatternParser = new PathPatternParser();
// ...
return pathPatternParser;
}
}
<mvc:annotation-driven>
<mvc:path-matching
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"/>
# 15.12、高级 Java 配置
@EnableWebMvc
引入了 DelegatingWebMvcConfiguration
,它具有以下作用:
- 为 Spring MVC 应用程序提供默认的 Spring 配置。
- 检测并委托给
WebMvcConfigurer
实现来定制该配置。
对于高级模式,您可以移除 @EnableWebMvc
,并直接从 DelegatingWebMvcConfiguration
扩展,而不是实现 WebMvcConfigurer
,如下例所示:
@Configuration
public class WebConfiguration extends DelegatingWebMvcConfiguration {
// ...
}
您可以保留 WebConfig
中的现有方法,但现在也可以覆盖基类中的 bean 声明。同时,类路径上仍然可以存在任意数量的其他 WebMvcConfigurer
实现。
# 15.13、高级XML配置
MVC命名空间没有高级模式。 如果你需要自定义bean上的某个属性,但又无法通过其他方式更改,你可以使用Spring ApplicationContext
的BeanPostProcessor
生命周期钩子,如下例所示:
@Component
public class MyPostProcessor implements BeanPostProcessor {
public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {
// ...
return bean;
}
}
请注意,你需要将MyPostProcessor
声明为一个bean,可以直接在XML中显式声明,也可以通过<component-scan/>
声明让它被自动检测到。
# 16、HTTP/2
Servlet 4 容器需要支持 HTTP/2,并且 Spring Framework 5 兼容 Servlet API 4。从编程模型的角度来看,应用程序不需要做任何特定的事情。然而,有一些与服务器配置相关的注意事项。更多详细信息,请参考 HTTP/2 wiki 页面 (opens new window)。
Servlet API 确实公开了一个与 HTTP/2 相关的构造。你可以使用 jakarta.servlet.http.PushBuilder
主动将资源推送到客户端,它被支持作为 @RequestMapping
方法的方法参数。
# 二、客户端
本节介绍客户端访问 REST 接口的几种方式。
# 1、RestClient
RestClient
是一个同步 HTTP 客户端,它暴露了一个现代化的、流畅的 API。
更多细节请参考RestClient (opens new window)。
# 2、WebClient
WebClient
是一个响应式客户端,用于执行 HTTP 请求,它也提供了一个流畅的 API。
更多细节请参考 WebClient (opens new window)。
# 3、RestTemplate
RestTemplate
是一个同步客户端,用于执行 HTTP 请求。 它是 Spring 最初的 REST 客户端,通过底层的 HTTP 客户端库暴露了一个简单的、模板方法的 API。
更多细节请参考 REST 接口 (opens new window)。
# 4、接口
Spring Framework 允许你将 HTTP 服务定义为一个带有 HTTP 交换方法的 Java 接口。 然后,你可以生成一个代理来实现这个接口并执行交换。 这有助于简化 HTTP 远程访问,并为选择同步或响应式等 API 风格提供额外的灵活性。
更多细节请参考 REST 接口 (opens new window)。
# 三、其他 Web 框架
本文档详细介绍了 Spring 与第三方 Web 框架的集成。
Spring 框架的核心价值主张之一就是赋予开发者自主选择的权利。一般来说,Spring 不会强迫你使用或接受任何特定的架构、技术或方法(尽管它肯定会推荐一些而不是其他的)。这种选择对于开发者及其开发团队来说,最相关的架构、技术或方法的自由,在 Web 领域最为明显,Spring 提供了自己的 Web框架(Spring MVC 和 Spring WebFlux),同时,支持与许多流行的第三方 Web 框架集成。
# 1、通用配置
在深入了解每个受支持 Web框架的集成细节之前,我们首先来看一下通用的 Spring 配置,这些配置不特定于任何一个 Web 框架。(本节同样适用于 Spring 自己的 Web 框架变体。)
Spring 轻量级应用程序模型所支持的概念之一是分层架构。记住,在“经典”分层架构中,Web 层只是众多层中的一层。它充当服务器端应用程序的入口点之一,并将请求委托给服务层中定义的服务对象(外观),以满足特定于业务(并且与呈现技术无关)的用例。在 Spring 中,这些服务对象、任何其他特定于业务的对象、数据访问对象等都存在于一个独立的“业务上下文”中,该上下文不包含 Web 或表示层对象(表示对象,例如 Spring MVC控制器,通常在不同的“表示上下文”中配置)。本节详细介绍了如何配置一个 Spring 容器(WebApplicationContext
),该容器包含应用程序中的所有“业务 Bean”。
具体来说,你需要做的就是在 Web应用程序的标准 Jakarta EE servlet web.xml
文件中声明一个 ContextLoaderListener (opens new window) ,并添加一个 contextConfigLocation
<context-param/>
部分(在同一个文件中),该部分定义要加载的 Spring XML 配置文件集。
考虑以下 <listener/>
配置:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
进一步考虑以下 <context-param/>
配置:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
如果你没有指定 contextConfigLocation
上下文参数,ContextLoaderListener
将查找一个名为 /WEB-INF/applicationContext.xml
的文件来加载。一旦上下文文件被加载,Spring 创建一个 WebApplicationContext (opens new window) 对象,该对象基于 Bean 定义,并将其存储在 Web应用程序的 ServletContext
中。
所有 Java Web 框架都构建在 Servlet API 之上,因此你可以使用以下代码片段来访问由 ContextLoaderListener
创建的这个“业务上下文” ApplicationContext
。
以下示例展示了如何获取 WebApplicationContext
:
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
WebApplicationContextUtils (opens new window) 类是为了方便起见,因此你无需记住 ServletContext
属性的名称。 如果在 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
键下不存在对象,则其 getWebApplicationContext()
方法返回 null
。 与在应用程序中冒着获得 NullPointerExceptions
的风险相比,最好使用 getRequiredWebApplicationContext()
方法。 当 ApplicationContext
丢失时,此方法会引发异常。
一旦你获得了对 WebApplicationContext
的引用,你就可以通过它们的名称或类型检索 Bean。 大多数开发人员通过名称检索 Bean,然后将它们强制转换为它们实现的接口之一。
幸运的是,本节中的大多数框架都具有更简单的查找 Bean 的方法。 它们不仅可以轻松地从 Spring 容器中获取 Bean,而且还可以让你在其控制器上使用依赖注入。 每个 Web 框架部分都详细介绍了其特定的集成策略。
# 2、JSF
JavaServer Faces (JSF) 是 JCP 的标准组件化的、事件驱动的 Web 用户界面框架。它是 Jakarta EE 保护伞的正式组成部分,但也可以单独使用,例如,通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。
请注意,最新版本的 JSF 在应用服务器中与 CDI 基础设施紧密相连,一些新的 JSF 功能只能在这种环境中工作。 Spring 的 JSF 支持不再积极发展,主要用于在现代化基于旧 JSF 的应用程序时的迁移目的。
Spring 的 JSF 集成的关键要素是 JSF ELResolver
机制。
# 2.1、Bean 解析器
SpringBeanFacesELResolver
是一个 JSF 兼容的 ELResolver
实现,它与 JSF 和 JSP 使用的标准 Unified EL 集成。它首先委托给 Spring 的“业务上下文” WebApplicationContext
,然后委托给底层 JSF 实现的默认解析器。
从配置方面来说,你可以在 JSF faces-context.xml
文件中定义 SpringBeanFacesELResolver
,如以下示例所示:
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
# 2.2、使用 FacesContextUtils
当将属性映射到 faces-config.xml
中的 Bean 时,自定义 ELResolver
效果很好,但有时你可能需要显式地获取一个 Bean。 FacesContextUtils (opens new window) 类使这变得容易。 它类似于 WebApplicationContextUtils
,不同之处在于它接受 FacesContext
参数而不是 ServletContext
参数。
以下示例展示了如何使用 FacesContextUtils
:
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
# 3、Struts
Struts 由 Craig McClanahan 发明,是由 Apache 软件基金会托管的一个开源项目。Struts (opens new window) 1.x 极大地简化了 JSP/Servlet 编程范例,并赢得了许多使用专有框架的开发人员的青睐。 它简化了编程模型; 它是开源的; 并且它拥有一个庞大的社区,这让该项目得以发展并在 Java Web 开发人员中广受欢迎。
作为原始 Struts 1.x 的继承者,请查看 Struts 2.x 或更新的版本,以及 Struts 提供的 Spring Plugin (opens new window),以获得内置的 Spring 集成。
# 4、Tapestry
Tapestry (opens new window) 是一个“面向组件的框架,用于在 Java 中创建动态、健壮、高度可扩展的 Web 应用程序”。
虽然 Spring 有它自己的 强大的 Web 层, 通过结合使用 Tapestry 作为 Web 用户界面和 Spring 容器作为较低层来构建企业 Java 应用程序,有很多独特的优势。
有关更多信息,请参阅 Tapestry 的专用 Spring 集成模块 (opens new window)。
# 5、更多资源
以下链接指向有关本章中描述的各种 Web框架的更多资源。
祝你变得更强!