(window.webpackJsonp=window.webpackJsonp||[]).push([[452],{893:function(e,t,r){"use strict";r.r(t);var a=r(56),n=Object(a.a)({},(function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[r("h1",{attrs:{id:"servlet-堆栈上的-web"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#servlet-堆栈上的-web"}},[e._v("#")]),e._v(" Servlet 堆栈上的 Web")]),e._v(" "),r("p",[e._v("文档的这一部分涵盖了对构建在 Servlet API 上并部署到 Servlet 容器上的 Servlet 堆栈 Web 应用程序的支持。个别章节包括"),r("a",{attrs:{href:"#mvc"}},[e._v("Spring MVC")]),e._v("、"),r("a",{attrs:{href:"#mvc-view"}},[e._v("查看技术")]),e._v("、"),r("a",{attrs:{href:"#mvc-cors"}},[e._v("CORS 支持")]),e._v("和"),r("a",{attrs:{href:"#websocket"}},[e._v("WebSocket Support")]),e._v("。有关反应式堆栈 Web 应用程序,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#spring-web-reactive"}},[e._v("反应式堆栈上的 Web")]),e._v("。")],1),e._v(" "),r("h2",{attrs:{id:"_1-spring-web-mvc"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-spring-web-mvc"}},[e._v("#")]),e._v(" 1. Spring Web MVC")]),e._v(" "),r("p",[e._v("Spring Web MVC 是建立在 Servlet API 上的原始 Web 框架,并且从一开始就包含在 Spring 框架中。正式名称“ Spring Web MVC”来自其源模块的名称(["),r("code",[e._v("spring-webmvc")]),e._v("](https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-webmvc)),但更常见的名称是“ Spring MVC”。")]),e._v(" "),r("p",[e._v("与 Spring Web MVC 并行, Spring Framework5.0 引入了一种反应式堆栈 Web 框架,其名称为“ Spring WebFlux”,也基于其源模块(["),r("code",[e._v("spring-webflux")]),e._v("](https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-WebFlux))。本节涵盖 Spring Web MVC。"),r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#spring-web-reactive"}},[e._v("下一节")]),e._v("覆盖了 Spring WebFlux。")],1),e._v(" "),r("p",[e._v("有关基线信息以及与 Servlet 容器和 爪哇 EE 版本范围的兼容性,请参见 Spring 框架"),r("a",{attrs:{href:"https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions",target:"_blank",rel:"noopener noreferrer"}},[e._v("Wiki"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h3",{attrs:{id:"_1-1-dispatcherservlet"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-dispatcherservlet"}},[e._v("#")]),e._v(" 1.1.DispatcherServlet")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-dispatcher-handler"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring 与许多其他 Web 框架一样,MVC 是围绕前控制器模式设计的,其中中央、,提供用于请求处理的共享算法,而实际工作是通过可配置的委托组件来执行的。这个模型是灵活的,并支持不同的工作流程。")]),e._v(" "),r("p",[r("code",[e._v("DispatcherServlet")]),e._v(",就像任何"),r("code",[e._v("Servlet")]),e._v("一样,需要通过使用 爪哇 配置或在"),r("code",[e._v("web.xml")]),e._v("中根据 Servlet 规范进行声明和映射。反过来,"),r("code",[e._v("DispatcherServlet")]),e._v("使用 Spring 配置来发现它在请求映射、视图解析、异常处理、"),r("a",{attrs:{href:"#mvc-servlet-special-bean-types"}},[e._v("and more")]),e._v("中需要的委托组件。")]),e._v(" "),r("p",[e._v("下面的 爪哇 配置示例注册并初始化了"),r("code",[e._v("DispatcherServlet")]),e._v(",这是由 Servlet 容器自动检测的(参见"),r("a",{attrs:{href:"#mvc-container-config"}},[e._v("Servlet Config")]),e._v("):")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class MyWebApplicationInitializer implements WebApplicationInitializer {\n\n @Override\n public void onStartup(ServletContext servletContext) {\n\n // Load Spring web application configuration\n AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();\n context.register(AppConfig.class);\n\n // Create and register the DispatcherServlet\n DispatcherServlet servlet = new DispatcherServlet(context);\n ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);\n registration.setLoadOnStartup(1);\n registration.addMapping("/app/*");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyWebApplicationInitializer : WebApplicationInitializer {\n\n override fun onStartup(servletContext: ServletContext) {\n\n // Load Spring web application configuration\n val context = AnnotationConfigWebApplicationContext()\n context.register(AppConfig::class.java)\n\n // Create and register the DispatcherServlet\n val servlet = DispatcherServlet(context)\n val registration = servletContext.addServlet("app", servlet)\n registration.setLoadOnStartup(1)\n registration.addMapping("/app/*")\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("除了直接使用 ServletContext API 之外,你还可以扩展"),r("code",[e._v("AbstractAnnotationConfigDispatcherServletInitializer")]),e._v("并覆盖特定的方法"),r("br"),e._v("(请参见"),r("a",{attrs:{href:"#mvc-servlet-context-hierarchy"}},[e._v("上下文层次结构")]),e._v("下的示例)。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("对于编程用例,"),r("code",[e._v("GenericWebApplicationContext")]),e._v("可以用作"),r("br"),e._v("的"),r("code",[e._v("AnnotationConfigWebApplicationContext")]),e._v("的替代。详情见["),r("code",[e._v("GenericWebApplicationContext")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/support/genericwebapplicationcontext.html)爪哇doc。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("以下"),r("code",[e._v("web.xml")]),e._v("配置寄存器和初始化"),r("code",[e._v("DispatcherServlet")]),e._v("的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n\n \n org.springframework.web.context.ContextLoaderListener\n \n\n \n contextConfigLocation\n /WEB-INF/app-context.xml\n \n\n \n app\n org.springframework.web.servlet.DispatcherServlet\n \n contextConfigLocation\n \n \n 1\n \n\n \n app\n /app/*\n \n\n\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring 启动遵循不同的初始化顺序。 Spring boot 使用 Spring 配置来"),r("br"),e._v("bootstrap 本身和嵌入的 Servlet 容器,而不是连接到 Servlet 容器的生命周期"),r("br"),e._v("。"),r("code",[e._v("Filter")]),e._v("和"),r("code",[e._v("Servlet")]),e._v("声明"),r("br"),e._v("在 Spring 配置中检测到并在 Servlet 容器中注册。"),r("br"),e._v("有关更多详细信息,请参见"),r("a",{attrs:{href:"https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-embedded-container",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Boot documentation"),r("OutboundLink")],1),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-1-1-上下文层次结构"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-1-上下文层次结构"}},[e._v("#")]),e._v(" 1.1.1.上下文层次结构")]),e._v(" "),r("p",[r("code",[e._v("DispatcherServlet")]),e._v("对其自身的配置期望"),r("code",[e._v("WebApplicationContext")]),e._v("(普通"),r("code",[e._v("ApplicationContext")]),e._v("的扩展)。"),r("code",[e._v("WebApplicationContext")]),e._v("具有指向"),r("code",[e._v("ServletContext")]),e._v("和与其关联的"),r("code",[e._v("Servlet")]),e._v("的链接。它还绑定到"),r("code",[e._v("ServletContext")]),e._v(",使得应用程序可以在"),r("code",[e._v("RequestContextUtils")]),e._v("上使用静态方法来查找"),r("code",[e._v("WebApplicationContext")]),e._v(",如果他们需要访问它。")]),e._v(" "),r("p",[e._v("对于许多应用程序,具有一个"),r("code",[e._v("WebApplicationContext")]),e._v("是简单且足够的。还可以有一个上下文层次结构,其中一个根"),r("code",[e._v("WebApplicationContext")]),e._v("在多个"),r("code",[e._v("DispatcherServlet")]),e._v("(或其他"),r("code",[e._v("Servlet")]),e._v(")实例之间共享,每个实例都有自己的子实例"),r("code",[e._v("WebApplicationContext")]),e._v("配置。有关上下文层次结构特性的更多信息,请参见["),r("code",[e._v("ApplicationContext")]),e._v("](core.html#context-introduction)。")]),e._v(" "),r("p",[e._v("根"),r("code",[e._v("WebApplicationContext")]),e._v("通常包含基础设施 bean,例如需要跨多个"),r("code",[e._v("Servlet")]),e._v("实例共享的数据存储库和业务服务。这些 bean 被有效地继承,并且可以在 Servlet 特定的子"),r("code",[e._v("WebApplicationContext")]),e._v("中被重写(即重新声明),该子"),r("code",[e._v("Servlet")]),e._v("通常包含给定的"),r("code",[e._v("Servlet")]),e._v("本地的 bean。下图显示了这种关系:")]),e._v(" "),r("p",[r("img",{attrs:{src:"images/mvc-context-hierarchy.png",alt:"MVC 上下文层次结构"}})]),e._v(" "),r("p",[e._v("下面的示例配置了"),r("code",[e._v("WebApplicationContext")]),e._v("层次结构:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n @Override\n protected Class[] getRootConfigClasses() {\n return new Class[] { RootConfig.class };\n }\n\n @Override\n protected Class[] getServletConfigClasses() {\n return new Class[] { App1Config.class };\n }\n\n @Override\n protected String[] getServletMappings() {\n return new String[] { "/app1/*" };\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {\n\n override fun getRootConfigClasses(): Array> {\n return arrayOf(RootConfig::class.java)\n }\n\n override fun getServletConfigClasses(): Array> {\n return arrayOf(App1Config::class.java)\n }\n\n override fun getServletMappings(): Array {\n return arrayOf("/app1/*")\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果不需要应用程序上下文层次结构,则应用程序可以通过"),r("code",[e._v("getRootConfigClasses()")]),e._v("和"),r("code",[e._v("null")]),e._v("从"),r("code",[e._v("getServletConfigClasses()")]),e._v("返回所有"),r("br"),e._v("配置。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("下面的示例显示了"),r("code",[e._v("web.xml")]),e._v("的等价值:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n\n \n org.springframework.web.context.ContextLoaderListener\n \n\n \n contextConfigLocation\n /WEB-INF/root-context.xml\n \n\n \n app1\n org.springframework.web.servlet.DispatcherServlet\n \n contextConfigLocation\n /WEB-INF/app1-context.xml\n \n 1\n \n\n \n app1\n /app1/*\n \n\n\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果不需要应用程序上下文层次结构,则应用程序可以仅配置"),r("br"),e._v("“root”上下文,并将"),r("code",[e._v("contextConfigLocation")]),e._v(" Servlet 参数保留为空。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-1-2-特殊-bean-型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-2-特殊-bean-型"}},[e._v("#")]),e._v(" 1.1.2.特殊 Bean 型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-special-bean-types"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("DispatcherServlet")]),e._v("将委托给特殊的 bean 来处理请求并呈现适当的响应。“特殊 bean”指的是实现框架契约的 Spring-managed"),r("code",[e._v("Object")]),e._v("实例。这些通常带有内置契约,但你可以自定义它们的属性并扩展或替换它们。")]),e._v(" "),r("p",[e._v("下表列出了"),r("code",[e._v("DispatcherServlet")]),e._v("检测到的特殊 bean:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Bean type")]),e._v(" "),r("th",[e._v("解释")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("HandlerMapping")])]),e._v(" "),r("td",[e._v("将请求与"),r("a",{attrs:{href:"#mvc-handlermapping-interceptor"}},[e._v("拦截器")]),e._v("的列表一起映射到处理程序,以进行前处理和后处理。"),r("br"),e._v("映射基于某些条件,其中的细节取决于"),r("code",[e._v("HandlerMapping")]),e._v("实现。"),r("br"),r("br"),e._v("两个主要的"),r("code",[e._v("HandlerMapping")]),e._v("实现是"),r("code",[e._v("RequestMappingHandlerMapping")]),e._v("(它支持"),r("code",[e._v("@RequestMapping")]),e._v("注释的方法)和"),r("code",[e._v("SimpleUrlHandlerMapping")]),e._v("(它维护对处理程序的 URI 路径模式的显式注册)。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HandlerAdapter")])]),e._v(" "),r("td",[e._v("帮助"),r("code",[e._v("DispatcherServlet")]),e._v("调用映射到请求的处理程序,而不考虑"),r("br"),e._v("实际调用处理程序的方式。例如,调用带注释的控制器"),r("br"),e._v("需要解析注释。a"),r("code",[e._v("HandlerAdapter")]),e._v("的主要目的是"),r("br"),e._v("使"),r("code",[e._v("DispatcherServlet")]),e._v("不受这些细节的影响。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-exceptionhandlers"}},[r("code",[e._v("HandlerExceptionResolver")])])]),e._v(" "),r("td",[e._v("解决异常的策略,可能将异常映射到处理程序、HTML 错误"),r("br"),e._v("视图或其他目标。见"),r("a",{attrs:{href:"#mvc-exceptionhandlers"}},[e._v("Exceptions")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-viewresolver"}},[r("code",[e._v("ViewResolver")])])]),e._v(" "),r("td",[e._v("将从处理程序返回的基于逻辑"),r("code",[e._v("String")]),e._v("的视图名称解析为实际的"),r("code",[e._v("View")]),e._v(",并使用该视图将其呈现给响应。见"),r("a",{attrs:{href:"#mvc-viewresolver"}},[e._v("视图分辨率")]),e._v("和"),r("a",{attrs:{href:"#mvc-view"}},[e._v("查看技术")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-localeresolver"}},[r("code",[e._v("LocaleResolver")])]),e._v(", "),r("a",{attrs:{href:"#mvc-timezone"}},[e._v("LocaleContextResolver")])]),e._v(" "),r("td",[e._v("解析客户端正在使用的"),r("code",[e._v("Locale")]),e._v("以及可能的时区,以便能够"),r("br"),e._v("提供国际化的视图。见"),r("a",{attrs:{href:"#mvc-localeresolver"}},[e._v("Locale")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-themeresolver"}},[r("code",[e._v("ThemeResolver")])])]),e._v(" "),r("td",[e._v("解析 Web 应用程序可以使用的主题——例如,提供个性化的布局。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-themeresolver"}},[e._v("Themes")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-multipart"}},[r("code",[e._v("MultipartResolver")])])]),e._v(" "),r("td",[e._v("用于解析具有"),r("br"),e._v("的多部分请求(例如,浏览器表单文件上传)的抽象,需要借助一些多部分解析库。见"),r("a",{attrs:{href:"#mvc-multipart"}},[e._v("多部分旋转变压器")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"#mvc-flash-attributes"}},[r("code",[e._v("FlashMapManager")])])]),e._v(" "),r("td",[e._v("存储和检索“输入”和“输出”"),r("code",[e._v("FlashMap")]),e._v(",它们可以用于将"),r("br"),e._v("属性从一个请求传递到另一个请求,通常是通过重定向。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-flash-attributes"}},[e._v("flash 属性")]),e._v("。")])])])]),e._v(" "),r("h4",{attrs:{id:"_1-1-3-web-mvc-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-3-web-mvc-配置"}},[e._v("#")]),e._v(" 1.1.3.Web MVC 配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-framework-config"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("应用程序可以声明"),r("a",{attrs:{href:"#mvc-servlet-special-bean-types"}},[e._v("Special Bean Types")]),e._v("中列出的处理请求所需的基础设施 bean。"),r("code",[e._v("DispatcherServlet")]),e._v("检查每个特殊 Bean 的"),r("code",[e._v("WebApplicationContext")]),e._v("。如果没有匹配的 Bean 类型,它将返回到["),r("code",[e._v("DispatcherServlet.properties")]),e._v("](https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-webmvc/SRC/main/resources/org/springframework/web/ Servlet/dispatcherservlet.properties)中列出的默认类型。")]),e._v(" "),r("p",[e._v("在大多数情况下,"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("是最好的起点。它用 爪哇 或 XML 声明所需的 bean,并提供一个更高级别的配置回调 API 来定制它。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring Boot 依赖于 MVC 爪哇 配置来配置 Spring MVC,并且"),r("br"),e._v("提供了许多额外的方便选项。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-1-4-servlet-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-4-servlet-配置"}},[e._v("#")]),e._v(" 1.1.4. Servlet 配置")]),e._v(" "),r("p",[e._v("在 Servlet 3.0+ 环境中,你可以选择以编程方式配置 Servlet 容器作为替代方案,或者与"),r("code",[e._v("web.xml")]),e._v("文件组合。下面的示例注册了"),r("code",[e._v("DispatcherServlet")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.WebApplicationInitializer;\n\npublic class MyWebApplicationInitializer implements WebApplicationInitializer {\n\n @Override\n public void onStartup(ServletContext container) {\n XmlWebApplicationContext appContext = new XmlWebApplicationContext();\n appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");\n\n ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));\n registration.setLoadOnStartup(1);\n registration.addMapping("/");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.WebApplicationInitializer\n\nclass MyWebApplicationInitializer : WebApplicationInitializer {\n\n override fun onStartup(container: ServletContext) {\n val appContext = XmlWebApplicationContext()\n appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")\n\n val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))\n registration.setLoadOnStartup(1)\n registration.addMapping("/")\n }\n}\n')])])]),r("p",[r("code",[e._v("WebApplicationInitializer")]),e._v("是由 Spring MVC 提供的一个接口,该接口确保检测到你的实现并自动用于初始化任何 Servlet 3 容器。名为"),r("code",[e._v("AbstractDispatcherServletInitializer")]),e._v("的"),r("code",[e._v("WebApplicationInitializer")]),e._v("的抽象基类实现使得通过覆盖方法来指定 Servlet 映射和"),r("code",[e._v("DispatcherServlet")]),e._v("配置的位置来注册"),r("code",[e._v("DispatcherServlet")]),e._v("变得更加容易。")]),e._v(" "),r("p",[e._v("对于使用基于 爪哇 的 Spring 配置的应用程序,推荐这样做,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n @Override\n protected Class[] getRootConfigClasses() {\n return null;\n }\n\n @Override\n protected Class[] getServletConfigClasses() {\n return new Class[] { MyWebConfig.class };\n }\n\n @Override\n protected String[] getServletMappings() {\n return new String[] { "/" };\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {\n\n override fun getRootConfigClasses(): Array>? {\n return null\n }\n\n override fun getServletConfigClasses(): Array>? {\n return arrayOf(MyWebConfig::class.java)\n }\n\n override fun getServletMappings(): Array {\n return arrayOf("/")\n }\n}\n')])])]),r("p",[e._v("如果使用基于 XML 的 Spring 配置,则应该从"),r("code",[e._v("AbstractDispatcherServletInitializer")]),e._v("直接扩展,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {\n\n @Override\n protected WebApplicationContext createRootApplicationContext() {\n return null;\n }\n\n @Override\n protected WebApplicationContext createServletApplicationContext() {\n XmlWebApplicationContext cxt = new XmlWebApplicationContext();\n cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");\n return cxt;\n }\n\n @Override\n protected String[] getServletMappings() {\n return new String[] { "/" };\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyWebAppInitializer : AbstractDispatcherServletInitializer() {\n\n override fun createRootApplicationContext(): WebApplicationContext? {\n return null\n }\n\n override fun createServletApplicationContext(): WebApplicationContext {\n return XmlWebApplicationContext().apply {\n setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")\n }\n }\n\n override fun getServletMappings(): Array {\n return arrayOf("/")\n }\n}\n')])])]),r("p",[r("code",[e._v("AbstractDispatcherServletInitializer")]),e._v("还提供了一种方便的方式来添加"),r("code",[e._v("Filter")]),e._v("实例,并将它们自动映射到"),r("code",[e._v("DispatcherServlet")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {\n\n // ...\n\n @Override\n protected Filter[] getServletFilters() {\n return new Filter[] {\n new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("class MyWebAppInitializer : AbstractDispatcherServletInitializer() {\n\n // ...\n\n override fun getServletFilters(): Array {\n return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())\n }\n}\n")])])]),r("p",[e._v("每个过滤器都会根据其具体类型添加一个默认名称,并自动映射到"),r("code",[e._v("DispatcherServlet")]),e._v("。")]),e._v(" "),r("p",[r("code",[e._v("isAsyncSupported")]),e._v("的"),r("code",[e._v("AbstractDispatcherServletInitializer")]),e._v("保护方法提供了一个位置,可以在"),r("code",[e._v("DispatcherServlet")]),e._v("和映射到它的所有过滤器上启用异步支持。默认情况下,此标志设置为"),r("code",[e._v("true")]),e._v("。")]),e._v(" "),r("p",[e._v("最后,如果需要进一步自定义"),r("code",[e._v("DispatcherServlet")]),e._v("本身,则可以重写"),r("code",[e._v("createDispatcherServlet")]),e._v("方法。")]),e._v(" "),r("h4",{attrs:{id:"_1-1-5-处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-5-处理"}},[e._v("#")]),e._v(" 1.1.5.处理")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-dispatcher-handler-sequence"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("DispatcherServlet")]),e._v("按以下方式处理请求:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("在请求中搜索并绑定"),r("code",[e._v("WebApplicationContext")]),e._v(",将其作为控制器和流程中的其他元素可以使用的属性。默认情况下,它在"),r("code",[e._v("DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE")]),e._v("键下绑定。")])]),e._v(" "),r("li",[r("p",[e._v("Locale 解析器绑定到请求,以让流程中的元素解析在处理请求(呈现视图、准备数据等)时使用的 Locale。如果不需要区域设置解析,则不需要区域设置解析程序。")])]),e._v(" "),r("li",[r("p",[e._v("主题解析程序绑定到请求,以便让视图等元素决定使用哪个主题。如果你不使用主题,你可以忽略它。")])]),e._v(" "),r("li",[r("p",[e._v("如果指定了多部分文件解析器,则会检查请求的多部分。如果发现了多个部分,则将请求包装在"),r("code",[e._v("MultipartHttpServletRequest")]),e._v("中,以便由流程中的其他元素进行进一步处理。有关多部件处理的更多信息,请参见"),r("a",{attrs:{href:"#mvc-multipart"}},[e._v("多部分旋转变压器")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("将搜索一个合适的处理程序。如果找到了一个处理程序,则运行与该处理程序(预处理器、后处理器和控制器)相关联的执行链,以便为呈现准备一个模型。或者,对于带注释的控制器,可以呈现响应(在"),r("code",[e._v("HandlerAdapter")]),e._v("内),而不是返回视图。")])]),e._v(" "),r("li",[r("p",[e._v("如果返回了一个模型,则呈现该视图。如果没有返回模型(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不呈现视图,因为该请求可能已经满足。")])])]),e._v(" "),r("p",[e._v("在"),r("code",[e._v("WebApplicationContext")]),e._v("中声明的"),r("code",[e._v("HandlerExceptionResolver")]),e._v("bean 用于解决请求处理过程中抛出的异常。这些异常解析器允许定制逻辑来处理异常。有关更多详细信息,请参见"),r("a",{attrs:{href:"#mvc-exceptionhandlers"}},[e._v("Exceptions")]),e._v("。")]),e._v(" "),r("p",[e._v("对于 HTTP 缓存支持,处理程序可以使用"),r("code",[e._v("checkNotModified")]),e._v("的"),r("code",[e._v("WebRequest")]),e._v("方法,以及"),r("a",{attrs:{href:"#mvc-caching-etag-lastmodified"}},[e._v("控制器的 HTTP 缓存")]),e._v("中描述的注释控制器的其他选项。")]),e._v(" "),r("p",[e._v("可以通过在"),r("code",[e._v("web.xml")]),e._v("文件中的 Servlet 声明中添加 Servlet 初始化参数("),r("code",[e._v("init-param")]),e._v("元素)来定制单个"),r("code",[e._v("DispatcherServlet")]),e._v("实例。下表列出了支持的参数:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Parameter")]),e._v(" "),r("th",[e._v("解释")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("contextClass")])]),e._v(" "),r("td",[e._v("实现"),r("code",[e._v("ConfigurableWebApplicationContext")]),e._v("的类,将其实例化并由此 Servlet 本地配置"),r("br"),e._v("。默认情况下,使用"),r("code",[e._v("XmlWebApplicationContext")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("contextConfigLocation")])]),e._v(" "),r("td",[e._v("传递到上下文实例(由"),r("code",[e._v("contextClass")]),e._v("指定)到"),r("br"),e._v("的字符串表示可以在哪里找到上下文。字符串可能由多个"),r("br"),e._v("字符串组成(使用逗号作为分隔符),以支持多个上下文。在"),r("br"),e._v("具有两次定义的 bean 的多个上下文位置的情况下,最新的位置"),r("br"),e._v("优先。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("namespace")])]),e._v(" "),r("td",[r("code",[e._v("WebApplicationContext")]),e._v("的命名空间。默认值为"),r("code",[e._v("[servlet-name]-servlet")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("throwExceptionIfNoHandlerFound")])]),e._v(" "),r("td",[e._v("当没有为请求找到处理程序时,是否抛出"),r("code",[e._v("NoHandlerFoundException")]),e._v("。"),r("br"),e._v("然后可以使用"),r("code",[e._v("HandlerExceptionResolver")]),e._v("(例如,通过使用"),r("code",[e._v("@ExceptionHandler")]),e._v("控制器方法)捕获异常,并将其作为任何其他方法处理。"),r("br"),r("br"),e._v("默认情况下,此异常被设置为"),r("code",[e._v("false")]),e._v(",在这种情况下,"),r("code",[e._v("DispatcherServlet")]),e._v("将"),r("br"),e._v("响应状态设置为 404(不 _found),而不会引发异常。"),r("br"),r("br"),e._v("注意,如果"),r("a",{attrs:{href:"#mvc-default-servlet-handler"}},[e._v("default servlet handling")]),e._v("也配置了"),r("br"),e._v(",则未解决的请求总是被转发到默认的 Servlet "),r("br"),e._v(",并且永远不会引发 404。")])])])]),e._v(" "),r("h4",{attrs:{id:"_1-1-6-路径匹配"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-6-路径匹配"}},[e._v("#")]),e._v(" 1.1.6.路径匹配")]),e._v(" "),r("p",[e._v("Servlet API 将完整的请求路径公开为"),r("code",[e._v("requestURI")]),e._v(",并进一步将其细分为"),r("code",[e._v("contextPath")]),e._v("、"),r("code",[e._v("servletPath")]),e._v("和"),r("code",[e._v("pathInfo")]),e._v(",其值随 Servlet 映射的方式而变化。 Spring MVC 需要从这些输入中确定要用于处理程序映射的查找路径,这是"),r("code",[e._v("DispatcherServlet")]),e._v("本身的映射内的路径,不包括"),r("code",[e._v("contextPath")]),e._v("和任何"),r("code",[e._v("servletMapping")]),e._v("前缀(如果存在的话)。")]),e._v(" "),r("p",[e._v("对"),r("code",[e._v("servletPath")]),e._v("和"),r("code",[e._v("pathInfo")]),e._v("进行了解码,这使得它们不可能直接与完整的"),r("code",[e._v("requestURI")]),e._v("进行比较,从而得出查找路径,这使得有必要对"),r("code",[e._v("requestURI")]),e._v("进行解码。然而,这引入了它自己的问题,因为路径可能包含编码的保留字符,例如"),r("code",[e._v('"/"')]),e._v("或"),r("code",[e._v('";"')]),e._v(",这些字符在解码后可能会反过来改变路径的结构,这也可能导致安全问题。此外, Servlet 容器可以在不同程度上归一化"),r("code",[e._v("servletPath")]),e._v(",这使得进一步不可能针对"),r("code",[e._v("startsWith")]),e._v("执行"),r("code",[e._v("requestURI")]),e._v("的比较。")]),e._v(" "),r("p",[e._v("这就是为什么最好避免依赖基于前缀的"),r("code",[e._v("servletPath")]),e._v("映射类型所附带的"),r("code",[e._v("servletPath")]),e._v("。如果"),r("code",[e._v("DispatcherServlet")]),e._v("被映射为带有"),r("code",[e._v('"/"')]),e._v("的缺省 Servlet,或者以其他方式不使用带有"),r("code",[e._v('"/*"')]),e._v("的前缀,并且 Servlet 容器是 4.0+,那么 Spring MVC 能够检测 Servlet 映射类型并完全避免使用"),r("code",[e._v("servletPath")]),e._v("和"),r("code",[e._v("pathInfo")]),e._v("。在 3.1 Servlet 容器上,假设具有相同的 Servlet 映射类型,可以通过在 MVC 配置中通过"),r("a",{attrs:{href:"#mvc-config-path-matching"}},[e._v("路径匹配")]),e._v("提供带有"),r("code",[e._v("alwaysUseFullPath=true")]),e._v("的"),r("code",[e._v("UrlPathHelper")]),e._v("来实现等效。")]),e._v(" "),r("p",[e._v("幸运的是,默认的 Servlet 映射"),r("code",[e._v('"/"')]),e._v("是一个不错的选择。然而,仍然存在一个问题,即需要对"),r("code",[e._v("requestURI")]),e._v("进行解码,以便能够与控制器映射进行比较。这也是不希望的,因为有可能对保留的字符进行解码,从而改变路径结构。如果这样的字符不是预期的,那么你可以拒绝它们(如 Spring Security HTTP 防火墙),或者你可以配置"),r("code",[e._v("UrlPathHelper")]),e._v("和"),r("code",[e._v("urlDecode=false")]),e._v(",但是控制器映射将需要与编码路径匹配,这可能并不总是很好地工作。此外,有时"),r("code",[e._v("DispatcherServlet")]),e._v("需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。")]),e._v(" "),r("p",[e._v("上述问题可以通过从"),r("code",[e._v("PathMatcher")]),e._v("切换到解析的"),r("code",[e._v("PathPattern")]),e._v("在 5.3 或更高版本中可用,来更全面地解决,请参见"),r("a",{attrs:{href:"#mvc-ann-requestmapping-pattern-comparison"}},[e._v("模式比较")]),e._v("。与"),r("code",[e._v("AntPathMatcher")]),e._v("(需要对查找路径进行解码或对控制器映射进行编码)不同,解析的"),r("code",[e._v("PathPattern")]),e._v("与解析的路径表示(称为"),r("code",[e._v("RequestPath")]),e._v(")匹配,一次只有一个路径段。这允许单独地对路径段值进行解码和消毒,而不存在更改路径结构的风险。解析的"),r("code",[e._v("PathPattern")]),e._v("还支持使用"),r("code",[e._v("servletPath")]),e._v("前缀映射,只要前缀保持简单,并且没有任何需要编码的字符。")]),e._v(" "),r("h4",{attrs:{id:"_1-1-7-拦截"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-7-拦截"}},[e._v("#")]),e._v(" 1.1.7.拦截")]),e._v(" "),r("p",[e._v("所有"),r("code",[e._v("HandlerMapping")]),e._v("实现都支持处理程序拦截器,当你想要将特定功能应用于某些请求时,这些功能非常有用——例如,检查主体。拦截器必须使用三种方法从"),r("code",[e._v("org.springframework.web.servlet")]),e._v("包中实现"),r("code",[e._v("HandlerInterceptor")]),e._v(",这些方法应该提供足够的灵活性来执行各种预处理和后处理:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("preHandle(..)")]),e._v(":在实际处理程序运行之前")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("postHandle(..)")]),e._v(":运行处理程序之后")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("afterCompletion(..)")]),e._v(":在完成完整的请求之后")])])]),e._v(" "),r("p",[r("code",[e._v("preHandle(..)")]),e._v("方法返回一个布尔值。你可以使用此方法来中断或继续执行链的处理。当此方法返回"),r("code",[e._v("true")]),e._v("时,处理程序执行链将继续。当返回 false 时,"),r("code",[e._v("DispatcherServlet")]),e._v("假定拦截器本身已经处理了请求(并且,例如,呈现了一个适当的视图),并且不继续执行执行链中的其他拦截器和实际处理程序。")]),e._v(" "),r("p",[e._v("有关如何配置拦截器的示例,请参见 MVC 配置一节中的"),r("a",{attrs:{href:"#mvc-config-interceptors"}},[e._v("拦截器")]),e._v("。你还可以在单独的"),r("code",[e._v("HandlerMapping")]),e._v("实现上使用 setter 直接注册它们。")]),e._v(" "),r("p",[e._v("请注意,"),r("code",[e._v("postHandle")]),e._v("对于"),r("code",[e._v("@ResponseBody")]),e._v("和"),r("code",[e._v("ResponseEntity")]),e._v("方法不那么有用,因为它们的响应是在"),r("code",[e._v("HandlerAdapter")]),e._v("和"),r("code",[e._v("postHandle")]),e._v("之前编写和提交的。这意味着对响应进行任何更改都为时已晚,例如添加一个额外的标头。对于这样的场景,你可以实现"),r("code",[e._v("ResponseBodyAdvice")]),e._v(",并将其声明为"),r("a",{attrs:{href:"#mvc-ann-controller-advice"}},[e._v("财务总监建议")]),e._v(" Bean,或者直接在"),r("code",[e._v("RequestMappingHandlerAdapter")]),e._v("上配置它。")]),e._v(" "),r("h4",{attrs:{id:"_1-1-8-例外"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-8-例外"}},[e._v("#")]),e._v(" 1.1.8.例外")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-dispatcher-exceptions"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("如果在请求映射期间发生异常,或者从请求处理程序(例如"),r("code",[e._v("@Controller")]),e._v(")抛出异常,则"),r("code",[e._v("DispatcherServlet")]),e._v("将委托给"),r("code",[e._v("HandlerExceptionResolver")]),e._v("bean 的链来解决异常并提供替代处理,这通常是错误响应。")]),e._v(" "),r("p",[e._v("下表列出了可用的"),r("code",[e._v("HandlerExceptionResolver")]),e._v("实现:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[r("code",[e._v("HandlerExceptionResolver")])]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("SimpleMappingExceptionResolver")])]),e._v(" "),r("td",[e._v("异常类名和错误视图名之间的映射。用于在浏览器应用程序中呈现"),r("br"),e._v("错误页面。")])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html",target:"_blank",rel:"noopener noreferrer"}},[r("code",[e._v("DefaultHandlerExceptionResolver")]),r("OutboundLink")],1)]),e._v(" "),r("td",[e._v("解析由 Spring MVC 引发的异常,并将它们映射到 HTTP 状态代码。"),r("br"),e._v("另请参见备选方案"),r("code",[e._v("ResponseEntityExceptionHandler")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-rest-exceptions"}},[e._v("REST API 异常")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ResponseStatusExceptionResolver")])]),e._v(" "),r("td",[e._v("用"),r("code",[e._v("@ResponseStatus")]),e._v("注释解决异常,并根据注释中的值将它们映射到 HTTP 状态"),r("br"),e._v("代码。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ExceptionHandlerExceptionResolver")])]),e._v(" "),r("td",[e._v("通过在"),r("code",[e._v("@Controller")]),e._v("或"),r("code",[e._v("@ControllerAdvice")]),e._v("类中调用"),r("code",[e._v("@ExceptionHandler")]),e._v("方法来解决异常。见"),r("a",{attrs:{href:"#mvc-ann-exceptionhandler"}},[e._v("@ExceptionHandler 方法")]),e._v("。")])])])]),e._v(" "),r("h5",{attrs:{id:"解析器链"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#解析器链"}},[e._v("#")]),e._v(" 解析器链")]),e._v(" "),r("p",[e._v("通过在 Spring 配置中声明多个"),r("code",[e._v("HandlerExceptionResolver")]),e._v("bean 并根据需要设置它们的"),r("code",[e._v("order")]),e._v("属性,可以形成异常解决器链。Order 属性越高,异常解析器的定位就越晚。")]),e._v(" "),r("p",[r("code",[e._v("HandlerExceptionResolver")]),e._v("的契约指定它可以返回:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("指向错误视图的"),r("code",[e._v("ModelAndView")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("如果异常是在解析程序中处理的,则为空"),r("code",[e._v("ModelAndView")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("null")]),e._v("如果异常仍然未解决,则供后续的解析器尝试,并且,如果异常仍然在最后,则允许将其冒泡到 Servlet 容器。")])])]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("自动声明用于默认 Spring MVC 异常、用于"),r("code",[e._v("@ResponseStatus")]),e._v("注释异常和用于支持"),r("code",[e._v("@ExceptionHandler")]),e._v("方法的内置解析器。你可以自定义该列表或替换它。")]),e._v(" "),r("h5",{attrs:{id:"容器错误页"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#容器错误页"}},[e._v("#")]),e._v(" 容器错误页")]),e._v(" "),r("p",[e._v("如果异常仍然被任何"),r("code",[e._v("HandlerExceptionResolver")]),e._v("未解决,并且因此被留给传播,或者如果响应状态被设置为错误状态(即,4xx,5xx), Servlet 容器可以在 HTML 中呈现默认的错误页。要定制容器的默认错误页,可以在"),r("code",[e._v("web.xml")]),e._v("中声明一个错误页映射。下面的示例展示了如何做到这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n /error\n\n")])])]),r("p",[e._v("给出了前面的示例,当出现异常或者响应具有错误状态时, Servlet 容器在容器内对配置的 URL 进行错误分派(例如,"),r("code",[e._v("/error")]),e._v(")。然后由"),r("code",[e._v("DispatcherServlet")]),e._v("对此进行处理,可能将其映射到"),r("code",[e._v("@Controller")]),e._v(",该实现可用于返回带有模型的错误视图名称或呈现 JSON 响应,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\npublic class ErrorController {\n\n @RequestMapping(path = "/error")\n public Map handle(HttpServletRequest request) {\n Map map = new HashMap();\n map.put("status", request.getAttribute("javax.servlet.error.status_code"));\n map.put("reason", request.getAttribute("javax.servlet.error.message"));\n return map;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\nclass ErrorController {\n\n @RequestMapping(path = ["/error"])\n fun handle(request: HttpServletRequest): Map {\n val map = HashMap()\n map["status"] = request.getAttribute("javax.servlet.error.status_code")\n map["reason"] = request.getAttribute("javax.servlet.error.message")\n return map\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Servlet API 不提供在 爪哇 中创建错误页映射的方法。但是,你可以同时使用"),r("br"),e._v("和极小值"),r("code",[e._v("web.xml")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-1-9-视图分辨率"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-9-视图分辨率"}},[e._v("#")]),e._v(" 1.1.9.视图分辨率")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-viewresolution"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 定义了"),r("code",[e._v("ViewResolver")]),e._v("和"),r("code",[e._v("View")]),e._v("接口,这些接口允许你在浏览器中呈现模型,而无需将你绑定到特定的视图技术。"),r("code",[e._v("ViewResolver")]),e._v("提供了视图名称和实际视图之间的映射。"),r("code",[e._v("View")]),e._v("处理在将数据移交给特定视图技术之前的准备工作。")]),e._v(" "),r("p",[e._v("下表提供了关于"),r("code",[e._v("ViewResolver")]),e._v("层次结构的更多详细信息:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("ViewResolver")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("AbstractCachingViewResolver")])]),e._v(" "),r("td",[e._v("它们解析的"),r("code",[e._v("AbstractCachingViewResolver")]),e._v("缓存视图实例的子类。"),r("br"),e._v("缓存提高了某些视图技术的性能。通过将"),r("code",[e._v("cache")]),e._v("属性设置为"),r("code",[e._v("false")]),e._v(",可以关闭"),r("br"),e._v("缓存。此外,如果你必须在运行时刷新"),r("br"),e._v("某个视图(例如,当修改自由标记模板时),"),r("br"),e._v("你可以使用"),r("code",[e._v("removeFromCache(String viewName, Locale loc)")]),e._v("方法。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("UrlBasedViewResolver")])]),e._v(" "),r("td",[r("code",[e._v("ViewResolver")]),e._v("接口的简单实现,它在没有显式映射定义的情况下实现逻辑视图名称到 URL 的直接"),r("br"),e._v("解析。"),r("br"),e._v("如果你的逻辑名称以一种简单的方式匹配视图资源的名称"),r("br"),e._v(",而不需要任意映射,那么这是合适的。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("InternalResourceViewResolver")])]),e._v(" "),r("td",[e._v("方便的"),r("code",[e._v("UrlBasedViewResolver")]),e._v("子类,支持"),r("code",[e._v("InternalResourceView")]),e._v("(在"),r("br"),e._v("effect 中,servlet 和 JSP)和"),r("code",[e._v("JstlView")]),e._v("和"),r("code",[e._v("TilesView")]),e._v("等子类。通过使用"),r("code",[e._v("setViewClass(..)")]),e._v(",可以"),r("br"),e._v("为这个解析器生成的所有视图指定视图类。"),r("br"),e._v("参见["),r("code",[e._v("UrlBasedViewResolver")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/SpringFramework/web/reactive/result/view/urlbasedviewresolver.html)爪哇doc 获取详细信息。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("FreeMarkerViewResolver")])]),e._v(" "),r("td",[r("code",[e._v("UrlBasedViewResolver")]),e._v("的方便的子类,它支持"),r("code",[e._v("FreeMarkerView")]),e._v("和"),r("br"),e._v("它们的自定义子类。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ContentNegotiatingViewResolver")])]),e._v(" "),r("td",[e._v("实现"),r("code",[e._v("ViewResolver")]),e._v("接口,该接口根据"),r("br"),e._v("请求文件名或"),r("code",[e._v("Accept")]),e._v("报头解析视图。见"),r("a",{attrs:{href:"#mvc-multiple-representations"}},[e._v("内容协商")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("BeanNameViewResolver")])]),e._v(" "),r("td",[r("code",[e._v("ViewResolver")]),e._v("接口的实现,该接口在当前应用程序上下文中将视图名称解释为"),r("br"),e._v(" Bean 名称。这是一个非常灵活的变体,其"),r("br"),e._v("允许基于不同的视图名称混合和匹配不同的视图类型。"),r("br"),e._v("每个这样的"),r("code",[e._v("View")]),e._v("都可以被定义为 Bean,例如在 XML 中或在配置类中。")])])])]),e._v(" "),r("h5",{attrs:{id:"处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#处理"}},[e._v("#")]),e._v(" 处理")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-viewresolution-handling"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以通过声明多个解析器 Bean 并在必要时通过设置"),r("code",[e._v("order")]),e._v("属性来指定顺序来链接视图解析器 Bean。请记住,Order 属性越高,视图解析器在链中的定位就越晚。")]),e._v(" "),r("p",[r("code",[e._v("ViewResolver")]),e._v("的契约指定它可以返回 null 来表示找不到视图。但是,在 JSP 和"),r("code",[e._v("InternalResourceViewResolver")]),e._v("的情况下,要确定 JSP 是否存在,唯一的方法是通过"),r("code",[e._v("RequestDispatcher")]),e._v("执行分派。因此,你必须始终将"),r("code",[e._v("InternalResourceViewResolver")]),e._v("配置为在视图解析程序的总体顺序中的最后一个。")]),e._v(" "),r("p",[e._v("配置视图分辨率就像在 Spring 配置中添加"),r("code",[e._v("ViewResolver")]),e._v("bean 一样简单。"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("为"),r("a",{attrs:{href:"#mvc-config-view-resolvers"}},[e._v("视图解析器")]),e._v("和添加无逻辑"),r("a",{attrs:{href:"#mvc-config-view-controller"}},[e._v("视图控制器")]),e._v("提供了专用的配置 API,这些 API 对于没有控制器逻辑的 HTML 模板呈现非常有用。")]),e._v(" "),r("h5",{attrs:{id:"重定向"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#重定向"}},[e._v("#")]),e._v(" 重定向")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-redirecting-redirect-prefix"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("视图名称中的特殊"),r("code",[e._v("redirect:")]),e._v("前缀允许你执行重定向。"),r("code",[e._v("UrlBasedViewResolver")]),e._v("(及其子类)将其视为需要重定向的指令。视图名称的其余部分是重定向 URL。")]),e._v(" "),r("p",[e._v("净效果与控制器返回"),r("code",[e._v("RedirectView")]),e._v("相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如"),r("code",[e._v("redirect:/myapp/some/resource")]),e._v(")相对于当前 Servlet 上下文重定向,而名称(例如"),r("code",[e._v("redirect:https://myhost.com/some/arbitrary/path")]),e._v(")重定向到绝对 URL。")]),e._v(" "),r("p",[e._v("请注意,如果控制器方法使用"),r("code",[e._v("@ResponseStatus")]),e._v("进行注释,则注释值优先于"),r("code",[e._v("RedirectView")]),e._v("设置的响应状态。")]),e._v(" "),r("h5",{attrs:{id:"转发"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#转发"}},[e._v("#")]),e._v(" 转发")]),e._v(" "),r("p",[e._v("你还可以使用一个特殊的"),r("code",[e._v("forward:")]),e._v("前缀来表示最终由"),r("code",[e._v("UrlBasedViewResolver")]),e._v("和子类解析的视图名称。这将创建一个"),r("code",[e._v("InternalResourceView")]),e._v(",它执行一个"),r("code",[e._v("RequestDispatcher.forward()")]),e._v("。因此,对于"),r("code",[e._v("InternalResourceViewResolver")]),e._v("和"),r("code",[e._v("InternalResourceView")]),e._v("(对于 JSP),这个前缀是没有用的,但是如果你使用另一种视图技术,但是仍然希望强制由 Servlet/JSP 引擎处理一个资源的转发,那么这个前缀是有帮助的。请注意,你也可以链接多个视图解析程序。")]),e._v(" "),r("h5",{attrs:{id:"内容协商"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#内容协商"}},[e._v("#")]),e._v(" 内容协商")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-multiple-representations"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("["),r("code",[e._v("ContentNegotiatingViewResolver")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/contentlegatingviewresolver.html)并不解析视图本身,而是将视图委托给其他视图解析器,并选择与客户机请求的表示类似的视图。表示可以从"),r("code",[e._v("Accept")]),e._v("头或从查询参数(例如,"),r("code",[e._v('"/path?format=pdf"')]),e._v(")确定。")]),e._v(" "),r("p",[e._v("将"),r("code",[e._v("ContentNegotiatingViewResolver")]),e._v("选择适当的"),r("code",[e._v("View")]),e._v("来处理请求,方法是将请求媒体类型与"),r("code",[e._v("Content-Type")]),e._v("所支持的媒体类型(也称为"),r("code",[e._v("Content-Type")]),e._v(")与其"),r("code",[e._v("ViewResolvers")]),e._v("中的每个相关联。列表中具有兼容的"),r("code",[e._v("Content-Type")]),e._v("的第一个"),r("code",[e._v("View")]),e._v("将表示形式返回给客户机。如果"),r("code",[e._v("ViewResolver")]),e._v("链不能提供兼容的视图,则会查阅通过"),r("code",[e._v("DefaultViews")]),e._v("属性指定的视图列表。后一种选项适用于单例"),r("code",[e._v("Views")]),e._v(",它可以呈现当前资源的适当表示,而不管逻辑视图名称是什么。"),r("code",[e._v("Accept")]),e._v("头可以包括通配符(例如"),r("code",[e._v("text/*")]),e._v("),在这种情况下,其"),r("code",[e._v("View")]),e._v("的"),r("code",[e._v("Content-Type")]),e._v("是"),r("code",[e._v("text/xml")]),e._v("是一个兼容的匹配。")]),e._v(" "),r("p",[e._v("有关配置细节,请参见"),r("a",{attrs:{href:"#mvc-config-view-resolvers"}},[e._v("视图解析器")]),e._v("下的"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-1-10-场所"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-10-场所"}},[e._v("#")]),e._v(" 1.1.10.场所")]),e._v(" "),r("p",[e._v("Spring 体系结构的大多数部分都支持国际化,就像 Spring Web MVC 框架所做的那样。"),r("code",[e._v("DispatcherServlet")]),e._v("允许你通过使用客户机的区域设置自动解析消息。这是用"),r("code",[e._v("LocaleResolver")]),e._v("对象完成的。")]),e._v(" "),r("p",[e._v("当收到请求时,"),r("code",[e._v("DispatcherServlet")]),e._v("会查找一个区域设置解析器,如果找到一个,它会尝试使用它来设置区域设置。通过使用"),r("code",[e._v("RequestContext.getLocale()")]),e._v("方法,你始终可以检索由区域解析程序解析的区域设置。")]),e._v(" "),r("p",[e._v("除了自动解析语言环境外,还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的更多信息,请参见"),r("a",{attrs:{href:"#mvc-handlermapping-interceptor"}},[e._v("拦截")]),e._v("),以在特定情况下(例如,基于请求中的参数)更改语言环境。")]),e._v(" "),r("p",[e._v("Locale 解析器和拦截器是在"),r("code",[e._v("org.springframework.web.servlet.i18n")]),e._v("包中定义的,并以正常的方式在应用程序上下文中进行配置。 Spring 中包括了对区域设置解析器的以下选择。")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"#mvc-timezone"}},[e._v("时区")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-localeresolver-acceptheader"}},[e._v("报头解析器")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-localeresolver-cookie"}},[e._v("Cookie 解析器")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-localeresolver-session"}},[e._v("会话解析器")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-localeresolver-interceptor"}},[e._v("Locale 拦截器")])])])]),e._v(" "),r("h5",{attrs:{id:"time-zone"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#time-zone"}},[e._v("#")]),e._v(" Time Zone")]),e._v(" "),r("p",[e._v("除了获取客户端的语言环境,了解它的时区通常是有用的。"),r("code",[e._v("LocaleContextResolver")]),e._v("接口提供了"),r("code",[e._v("LocaleResolver")]),e._v("的扩展,使解析器提供更丰富的"),r("code",[e._v("LocaleContext")]),e._v(",其中可能包括时区信息。")]),e._v(" "),r("p",[e._v("当可用时,可以使用"),r("code",[e._v("RequestContext.getTimeZone()")]),e._v("方法获得用户的"),r("code",[e._v("TimeZone")]),e._v("。时区信息被注册在 Spring 的"),r("code",[e._v("ConversionService")]),e._v("中的任何日期/时间"),r("code",[e._v("Converter")]),e._v("和"),r("code",[e._v("Formatter")]),e._v("对象自动使用。")]),e._v(" "),r("h5",{attrs:{id:"报头解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#报头解析器"}},[e._v("#")]),e._v(" 报头解析器")]),e._v(" "),r("p",[e._v("此 Locale 解析器检查客户机(例如,Web 浏览器)发送的请求中的"),r("code",[e._v("accept-language")]),e._v("头。通常,这个头字段包含客户端操作系统的区域设置。请注意,此解析器不支持时区信息。")]),e._v(" "),r("h5",{attrs:{id:"cookie-解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#cookie-解析器"}},[e._v("#")]),e._v(" Cookie 解析器")]),e._v(" "),r("p",[e._v("此区域解析程序检查客户端上可能存在的"),r("code",[e._v("Cookie")]),e._v(",以查看是否指定了"),r("code",[e._v("Locale")]),e._v("或"),r("code",[e._v("TimeZone")]),e._v("。如果是,则使用指定的细节。通过使用此区域设置解析程序的属性,你可以指定 cookie 的名称以及最长期限。下面的示例定义了"),r("code",[e._v("CookieLocaleResolver")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n\n \x3c!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) --\x3e\n \n\n\n')])])]),r("p",[e._v("下表描述了属性"),r("code",[e._v("CookieLocaleResolver")]),e._v(":")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Property")]),e._v(" "),r("th",[e._v("Default")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("cookieName")])]),e._v(" "),r("td",[e._v("classname + LOCALE")]),e._v(" "),r("td",[e._v("饼干的名字")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("cookieMaxAge")])]),e._v(" "),r("td",[e._v("Servlet container default")]),e._v(" "),r("td",[e._v("cookie 在客户机上持续存在的最长时间。如果指定了"),r("code",[e._v("-1")]),e._v(",则不会持久保存"),r("br"),e._v("cookie。它仅在客户端关闭"),r("br"),e._v("浏览器之前可用。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("cookiePath")])]),e._v(" "),r("td",[e._v("/")]),e._v(" "),r("td",[e._v("将 cookie 的可见性限制在网站的特定部分。当"),r("code",[e._v("cookiePath")]),e._v("被指定为"),r("br"),e._v("时,cookie 仅对该路径及其下方的路径可见。")])])])]),e._v(" "),r("h5",{attrs:{id:"会话解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#会话解析器"}},[e._v("#")]),e._v(" 会话解析器")]),e._v(" "),r("p",[r("code",[e._v("SessionLocaleResolver")]),e._v("允许你从可能与用户请求关联的会话中检索"),r("code",[e._v("Locale")]),e._v("和"),r("code",[e._v("TimeZone")]),e._v("。与"),r("code",[e._v("CookieLocaleResolver")]),e._v("相反,该策略将本地选择的语言环境设置存储在 Servlet 容器的"),r("code",[e._v("HttpSession")]),e._v("中。因此,这些设置是每个会话的临时设置,因此在每个会话结束时都会丢失。")]),e._v(" "),r("p",[e._v("注意,与外部会话管理机制没有直接关系,例如 Spring 会话项目。这个"),r("code",[e._v("SessionLocaleResolver")]),e._v("针对当前的"),r("code",[e._v("HttpServletRequest")]),e._v("计算并修改相应的"),r("code",[e._v("HttpSession")]),e._v("属性。")]),e._v(" "),r("h5",{attrs:{id:"locale-拦截器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#locale-拦截器"}},[e._v("#")]),e._v(" Locale 拦截器")]),e._v(" "),r("p",[e._v("你可以通过将"),r("code",[e._v("LocaleChangeInterceptor")]),e._v("添加到"),r("code",[e._v("HandlerMapping")]),e._v("定义中的一个来启用更改区域设置。它在请求中检测一个参数,并相应地更改区域设置,在 Dispatcher 的应用程序上下文中调用"),r("code",[e._v("LocaleResolver")]),e._v("上的"),r("code",[e._v("setLocale")]),e._v("方法。下一个示例显示,对所有包含名为"),r("code",[e._v("siteLanguage")]),e._v("的参数的"),r("code",[e._v("*.view")]),e._v("资源的调用现在会更改区域设置。因此,例如,对 URL 的请求"),r("code",[e._v("[https://www.sf.net/home.view?siteLanguage=nl](https://www.sf.net/home.view?siteLanguage=nl)")]),e._v("将站点语言更改为荷兰语。下面的示例展示了如何截取区域设置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n\n\n\n\n \n \n \n \n \n \n /**/*.view=someController\n \n\n')])])]),r("h4",{attrs:{id:"_1-1-11-主题"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-11-主题"}},[e._v("#")]),e._v(" 1.1.11.主题")]),e._v(" "),r("p",[e._v("Spring 可以应用 Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响应用程序的视觉风格。")]),e._v(" "),r("h5",{attrs:{id:"定义主题"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#定义主题"}},[e._v("#")]),e._v(" 定义主题")]),e._v(" "),r("p",[e._v("要在 Web 应用程序中使用主题,你必须设置"),r("code",[e._v("org.springframework.ui.context.ThemeSource")]),e._v("接口的实现。"),r("code",[e._v("WebApplicationContext")]),e._v("接口扩展了"),r("code",[e._v("ThemeSource")]),e._v(",但将其职责委托给一个专用的实现。默认情况下,委托是一个"),r("code",[e._v("org.springframework.ui.context.support.ResourceBundleThemeSource")]),e._v("实现,它从 Classpath 的根目录加载属性文件。要使用自定义的"),r("code",[e._v("ThemeSource")]),e._v("实现或配置"),r("code",[e._v("ResourceBundleThemeSource")]),e._v("的基名前缀,你可以在应用程序上下文中使用保留的名称"),r("code",[e._v("themeSource")]),e._v("注册 Bean。Web 应用程序上下文自动检测具有该名称的 Bean 并使用它。")]),e._v(" "),r("p",[e._v("当你使用"),r("code",[e._v("ResourceBundleThemeSource")]),e._v("时,将在一个简单的属性文件中定义一个主题。Properties 文件列出了构成主题的资源,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("styleSheet=/themes/cool/style.css\nbackground=/themes/cool/img/coolBg.jpg\n")])])]),r("p",[e._v("属性的键是从视图代码中引用主题元素的名称。对于 JSP,你通常使用"),r("code",[e._v("spring:theme")]),e._v("自定义标记来执行此操作,该标记与"),r("code",[e._v("spring:message")]),e._v("标记非常相似。下面的 JSP 片段使用上一个示例中定义的主题来定制外观和感觉:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>\n\n \n \n \n \n ...\n \n\n')])])]),r("p",[e._v("默认情况下,"),r("code",[e._v("ResourceBundleThemeSource")]),e._v("使用一个空的基名前缀。因此,属性文件是从 Classpath 的根目录加载的。因此,你将把"),r("code",[e._v("cool.properties")]),e._v("主题定义放在 Classpath 根目录中(例如,在"),r("code",[e._v("/WEB-INF/classes")]),e._v("中)。"),r("code",[e._v("ResourceBundleThemeSource")]),e._v("使用标准的 爪哇 资源包加载机制,允许主题的完全国际化。例如,我们可以有一个"),r("code",[e._v("/WEB-INF/classes/cool_nl.properties")]),e._v(",它引用了一个特殊的背景图像,上面有荷兰语文本。")]),e._v(" "),r("h5",{attrs:{id:"解决主题"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#解决主题"}},[e._v("#")]),e._v(" 解决主题")]),e._v(" "),r("p",[e._v("在定义主题(如"),r("a",{attrs:{href:"#mvc-themeresolver-defining"}},[e._v("前一节")]),e._v("中所述)之后,你将决定使用哪个主题。"),r("code",[e._v("DispatcherServlet")]),e._v("查找名为"),r("code",[e._v("themeResolver")]),e._v("的 Bean,以找出要使用的"),r("code",[e._v("ThemeResolver")]),e._v("实现。主题解析器的工作方式与"),r("code",[e._v("LocaleResolver")]),e._v("几乎相同。它会检测用于特定请求的主题,还可以更改请求的主题。下表描述了 Spring 提供的主题解析器:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Class")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("FixedThemeResolver")])]),e._v(" "),r("td",[e._v("选择一个固定的主题,通过使用"),r("code",[e._v("defaultThemeName")]),e._v("属性进行设置。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("SessionThemeResolver")])]),e._v(" "),r("td",[e._v("该主题在用户的 HTTP 会话中进行维护。对于"),r("br"),e._v("每个会话只需要设置一次,但在会话之间不会持久化。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("CookieThemeResolver")])]),e._v(" "),r("td",[e._v("选定的主题存储在客户端的 cookie 中。")])])])]),e._v(" "),r("p",[e._v("Spring 还提供了一个"),r("code",[e._v("ThemeChangeInterceptor")]),e._v(",它允许使用一个简单的请求参数在每个请求上更改主题。")]),e._v(" "),r("h4",{attrs:{id:"_1-1-12-多部分旋转变压器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-12-多部分旋转变压器"}},[e._v("#")]),e._v(" 1.1.12.多部分旋转变压器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-multipart"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("来自"),r("code",[e._v("org.springframework.web.multipart")]),e._v("包的"),r("code",[e._v("MultipartResolver")]),e._v("是一种解析包括文件上传在内的多部分请求的策略。存在一种基于"),r("a",{attrs:{href:"https://commons.apache.org/proper/commons-fileupload",target:"_blank",rel:"noopener noreferrer"}},[e._v("Commons 文件上传"),r("OutboundLink")],1),e._v("的实现方式和另一种基于 Servlet 3.0 的多部分请求解析的实现方式。")]),e._v(" "),r("p",[e._v("要启用多部分处理,你需要在你的"),r("code",[e._v("DispatcherServlet")]),e._v(" Bean 配置中声明一个名为"),r("code",[e._v("multipartResolver")]),e._v("的"),r("code",[e._v("MultipartResolver")]),e._v(" Bean 配置。"),r("code",[e._v("DispatcherServlet")]),e._v("检测到它并将其应用于传入请求。当接收到内容类型为"),r("code",[e._v("multipart/form-data")]),e._v("的帖子时,解析器将当前"),r("code",[e._v("HttpServletRequest")]),e._v("的内容包装解析为"),r("code",[e._v("MultipartHttpServletRequest")]),e._v(",以提供对已解析文件的访问,此外还将部分作为请求参数公开。")]),e._v(" "),r("h5",{attrs:{id:"apache-commonsfileupload"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#apache-commonsfileupload"}},[e._v("#")]),e._v(" Apache Commons"),r("code",[e._v("FileUpload")])]),e._v(" "),r("p",[e._v("要使用 Apache Commons"),r("code",[e._v("FileUpload")]),e._v(",你可以配置类型为"),r("code",[e._v("CommonsMultipartResolver")]),e._v("的 Bean,名称为"),r("code",[e._v("multipartResolver")]),e._v("。你还需要将"),r("code",[e._v("commons-fileupload")]),e._v(" jar 作为 Classpath 的依赖项。")]),e._v(" "),r("p",[e._v("这个解析器变体将委托给应用程序中的一个本地库,从而在 Servlet 容器中提供了最大的可移植性。作为一种替代方案,考虑通过容器自己的解析器实现标准的 Servlet 多部分解析,如下所述。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Commons FileUpload 传统上只适用于 POST 请求,但接受任何"),r("code",[e._v("multipart/")]),e._v("内容类型。有关详细信息和配置选项,请参见["),r("code",[e._v("CommonsMultipartResolver")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/multipart/commons/commonsmultipartresolver.html)爪哇doc。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"servlet-3-0"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#servlet-3-0"}},[e._v("#")]),e._v(" Servlet 3.0")]),e._v(" "),r("p",[e._v("Servlet 3.0 需要通过 Servlet 容器配置来启用多部分解析。这样做:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("在 爪哇 中,在 Servlet 注册上设置"),r("code",[e._v("MultipartConfigElement")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("在"),r("code",[e._v("web.xml")]),e._v("中,在 Servlet 声明中添加"),r("code",[e._v('""')]),e._v("部分。")])])]),e._v(" "),r("p",[e._v("下面的示例显示了如何在 Servlet 注册上设置"),r("code",[e._v("MultipartConfigElement")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n // ...\n\n @Override\n protected void customizeRegistration(ServletRegistration.Dynamic registration) {\n\n // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold\n registration.setMultipartConfig(new MultipartConfigElement("/tmp"));\n }\n\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {\n\n // ...\n\n override fun customizeRegistration(registration: ServletRegistration.Dynamic) {\n\n // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold\n registration.setMultipartConfig(MultipartConfigElement("/tmp"))\n }\n\n}\n')])])]),r("p",[e._v("一旦 Servlet 3.0 配置到位,就可以添加 Bean 类型的"),r("code",[e._v("StandardServletMultipartResolver")]),e._v(",其名称为"),r("code",[e._v("multipartResolver")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("这个解析器变体按原样使用 Servlet 容器的多部分解析器,"),r("br"),e._v("可能会使应用程序暴露于容器实现的差异中。"),r("br"),e._v("默认情况下,它将尝试使用任何 HTTP"),r("code",[e._v("multipart/")]),e._v("方法解析任何"),r("br"),e._v("内容类型,但这可能不会在所有 Servlet 容器中得到支持。有关详细信息和配置选项,请参见["),r("code",[e._v("StandardServletMultipartResolver")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/multipart/support/standardservletmultipartresolver.html)爪哇doc。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-1-13-伐木"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-13-伐木"}},[e._v("#")]),e._v(" 1.1.13.伐木")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-logging"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 中的调试级日志被设计为紧凑、最小且对人友好的。它关注的是一次又一次有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。")]),e._v(" "),r("p",[e._v("跟踪级日志记录通常遵循与调试相同的原则(例如,也不应该是消防水龙带),但可以用于调试任何问题。此外,一些日志消息在跟踪和调试时可能会显示不同级别的详细信息。")]),e._v(" "),r("p",[e._v("良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标,请告诉我们。")]),e._v(" "),r("h5",{attrs:{id:"敏感数据"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#敏感数据"}},[e._v("#")]),e._v(" 敏感数据")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-logging-sensitive-data"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("调试和跟踪日志记录可能会记录敏感信息。这就是为什么请求参数和头在默认情况下被屏蔽,并且必须通过"),r("code",[e._v("DispatcherServlet")]),e._v("上的"),r("code",[e._v("enableLoggingRequestDetails")]),e._v("属性显式地启用它们的完整日志记录。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何通过使用 爪哇 配置来实现这一点:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class MyInitializer\n extends AbstractAnnotationConfigDispatcherServletInitializer {\n\n @Override\n protected Class[] getRootConfigClasses() {\n return ... ;\n }\n\n @Override\n protected Class[] getServletConfigClasses() {\n return ... ;\n }\n\n @Override\n protected String[] getServletMappings() {\n return ... ;\n }\n\n @Override\n protected void customizeRegistration(ServletRegistration.Dynamic registration) {\n registration.setInitParameter("enableLoggingRequestDetails", "true");\n }\n\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {\n\n override fun getRootConfigClasses(): Array>? {\n return ...\n }\n\n override fun getServletConfigClasses(): Array>? {\n return ...\n }\n\n override fun getServletMappings(): Array {\n return ...\n }\n\n override fun customizeRegistration(registration: ServletRegistration.Dynamic) {\n registration.setInitParameter("enableLoggingRequestDetails", "true")\n }\n}\n')])])]),r("h3",{attrs:{id:"_1-2-过滤器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-过滤器"}},[e._v("#")]),e._v(" 1.2.过滤器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-filters"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("spring-web")]),e._v("模块提供了一些有用的过滤器:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"#filters-http-put"}},[e._v("Form Data")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#filters-forwarded-headers"}},[e._v("转发头")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#filters-shallow-etag"}},[e._v("浅层 ETAG")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#filters-cors"}},[e._v("CORS")])])])]),e._v(" "),r("h4",{attrs:{id:"_1-2-1-表单数据"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-1-表单数据"}},[e._v("#")]),e._v(" 1.2.1.表单数据")]),e._v(" "),r("p",[e._v("浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、补丁和 DELETE。 Servlet API 要求"),r("code",[e._v("ServletRequest.getParameter*()")]),e._v("方法只支持用于 HTTP POST 的表单字段访问。")]),e._v(" "),r("p",[r("code",[e._v("spring-web")]),e._v("模块提供"),r("code",[e._v("FormContentFilter")]),e._v("来拦截内容类型为"),r("code",[e._v("application/x-www-form-urlencoded")]),e._v("的 HTTP PUT、修补和删除请求,从请求的主体中读取表单数据,并包装"),r("code",[e._v("ServletRequest")]),e._v("以使表单数据通过"),r("code",[e._v("ServletRequest.getParameter*()")]),e._v("系列方法可用。")]),e._v(" "),r("h4",{attrs:{id:"_1-2-2-转发头"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-2-转发头"}},[e._v("#")]),e._v(" 1.2.2.转发头")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-forwarded-headers"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化,这使得创建从客户端角度指向正确的主机、端口和方案的链接成为一个挑战。")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://tools.ietf.org/html/rfc7239",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC 7239"),r("OutboundLink")],1),e._v("定义了"),r("code",[e._v("Forwarded")]),e._v("HTTP 报头,代理可以使用该报头来提供有关原始请求的信息。还有其他非标准标题,包括"),r("code",[e._v("X-Forwarded-Host")]),e._v(","),r("code",[e._v("X-Forwarded-Port")]),e._v(","),r("code",[e._v("X-Forwarded-Proto")]),e._v(","),r("code",[e._v("X-Forwarded-Ssl")]),e._v(",和"),r("code",[e._v("X-Forwarded-Prefix")]),e._v("。")]),e._v(" "),r("p",[r("code",[e._v("ForwardedHeaderFilter")]),e._v("是一个 Servlet 过滤器,它修改请求,以便 a)基于"),r("code",[e._v("Forwarded")]),e._v("头来更改主机、端口和方案,以及 b)删除那些头来消除进一步的影响。过滤器依赖于包装请求,因此它必须在其他过滤器(例如"),r("code",[e._v("RequestContextFilter")]),e._v(")之前进行排序,这些过滤器应该与修改后的请求(而不是原始请求)一起工作。")]),e._v(" "),r("p",[e._v("转发头的安全性需要考虑,因为应用程序不能知道头是由代理添加的,还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的"),r("code",[e._v("Forwarded")]),e._v("头。你还可以将"),r("code",[e._v("ForwardedHeaderFilter")]),e._v("配置为"),r("code",[e._v("removeOnly=true")]),e._v(",在这种情况下,它会删除但不使用头。")]),e._v(" "),r("p",[e._v("为了支持"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和错误分派,这个过滤器应该映射为"),r("code",[e._v("DispatcherType.ASYNC")]),e._v("和"),r("code",[e._v("DispatcherType.ERROR")]),e._v("。如果使用 Spring Framework 的"),r("code",[e._v("AbstractAnnotationConfigDispatcherServletInitializer")]),e._v("(请参见"),r("a",{attrs:{href:"#mvc-container-config"}},[e._v("Servlet Config")]),e._v("),所有过滤器都会自动为所有分派类型注册。但是,如果通过"),r("code",[e._v("web.xml")]),e._v("或在 Spring 引导中通过"),r("code",[e._v("FilterRegistrationBean")]),e._v("注册过滤器,请确保除"),r("code",[e._v("DispatcherType.ASYNC")]),e._v("和"),r("code",[e._v("DispatcherType.ERROR")]),e._v("外还包括"),r("code",[e._v("DispatcherType.REQUEST")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-2-3-浅层-etag"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-3-浅层-etag"}},[e._v("#")]),e._v(" 1.2.3.浅层 ETAG")]),e._v(" "),r("p",[r("code",[e._v("ShallowEtagHeaderFilter")]),e._v("过滤器通过缓存写到响应的内容并从中计算 MD5 散列来创建一个“浅”ETag。下一次客户端发送时,它也会执行相同的操作,但是它也会将计算的值与"),r("code",[e._v("If-None-Match")]),e._v("请求头进行比较,如果两者相等,则返回 304(不是 _modified)。")]),e._v(" "),r("p",[e._v("这种策略节省了网络带宽,但不节省 CPU,因为必须为每个请求计算完整的响应。前面描述的控制器级别的其他策略可以避免计算。见"),r("a",{attrs:{href:"#mvc-caching"}},[e._v("HTTP 缓存")]),e._v("。")]),e._v(" "),r("p",[e._v("这个过滤器有一个"),r("code",[e._v("writeWeakETag")]),e._v("参数,该参数将过滤器配置为写入类似于以下内容的弱 ETags:"),r("code",[e._v('W/"02a2d595e6ed9a0b24f027f2b63b134d6"')]),e._v("(在"),r("a",{attrs:{href:"https://tools.ietf.org/html/rfc7232#section-2.3",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC7232 第 2.3 节"),r("OutboundLink")],1),e._v("中定义)。")]),e._v(" "),r("p",[e._v("为了支持"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v(",这个过滤器必须与"),r("code",[e._v("DispatcherType.ASYNC")]),e._v("映射,这样过滤器就可以延迟并成功地生成一个 ETag 到最后一个异步调度的结束。如果使用 Spring Framework 的"),r("code",[e._v("AbstractAnnotationConfigDispatcherServletInitializer")]),e._v("(请参见"),r("a",{attrs:{href:"#mvc-container-config"}},[e._v("Servlet Config")]),e._v("),所有过滤器都会自动为所有分派类型注册。但是,如果通过"),r("code",[e._v("web.xml")]),e._v("或在 Spring 引导中通过"),r("code",[e._v("FilterRegistrationBean")]),e._v("注册过滤器,请确保包含"),r("code",[e._v("DispatcherType.ASYNC")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-2-4-科尔斯"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-4-科尔斯"}},[e._v("#")]),e._v(" 1.2.4.科尔斯")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-filters-cors"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而,当与 Spring Security 一起使用时,我们建议依赖于内置的"),r("code",[e._v("CorsFilter")]),e._v(",这必须在 Spring Security 的过滤器链之前订购。")]),e._v(" "),r("p",[e._v("有关更多详细信息,请参见"),r("a",{attrs:{href:"#mvc-cors"}},[e._v("CORS")]),e._v("和"),r("a",{attrs:{href:"#mvc-cors-filter"}},[e._v("CORS 过滤器")]),e._v("部分。")]),e._v(" "),r("h3",{attrs:{id:"_1-3-带注释的控制器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-带注释的控制器"}},[e._v("#")]),e._v(" 1.3.带注释的控制器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-controller"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 提供了一种基于注释的编程模型,其中"),r("code",[e._v("@Controller")]),e._v("和"),r("code",[e._v("@RestController")]),e._v("组件使用注释来表示请求映射、请求输入、异常处理等。带注释的控制器具有灵活的方法签名,不需要扩展基类,也不需要实现特定的接口。下面的示例显示了由注释定义的控制器:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class HelloController {\n\n @GetMapping("/hello")\n public String handle(Model model) {\n model.addAttribute("message", "Hello World!");\n return "index";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.ui.set\n\n@Controller\nclass HelloController {\n\n @GetMapping("/hello")\n fun handle(model: Model): String {\n model["message"] = "Hello World!"\n return "index"\n }\n}\n')])])]),r("p",[e._v("在前面的示例中,该方法接受"),r("code",[e._v("Model")]),e._v("并以"),r("code",[e._v("String")]),e._v("的形式返回视图名称,但是存在许多其他选项,并在本章后面进行解释。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("a",{attrs:{href:"https://spring.io/guides",target:"_blank",rel:"noopener noreferrer"}},[e._v("spring.io"),r("OutboundLink")],1),e._v("上的指南和教程使用本节中描述的基于注释的"),r("br"),e._v("编程模型。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-3-1-声明"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-1-声明"}},[e._v("#")]),e._v(" 1.3.1.声明")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-controller"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以使用 Servlet 的"),r("code",[e._v("WebApplicationContext")]),e._v("中的标准 Spring Bean 定义来定义控制器 bean。该原型允许自动检测,与 Spring 用于检测 Classpath 中的类的通用支持保持一致并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 Web 组件的角色。")]),e._v(" "),r("p",[e._v("要启用对此类"),r("code",[e._v("@Controller")]),e._v("bean 的自动检测,可以将组件扫描添加到 爪哇 配置中,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@ComponentScan("org.example.web")\npublic class WebConfig {\n\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@ComponentScan("org.example.web")\nclass WebConfig {\n\n // ...\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n \x3c!-- ... --\x3e\n\n\n')])])]),r("p",[r("code",[e._v("@RestController")]),e._v("是一个"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#beans-meta-annotations"}},[e._v("组合注释")]),e._v(",它本身用"),r("code",[e._v("@Controller")]),e._v("和"),r("code",[e._v("@ResponseBody")]),e._v("进行了元注释,以指示控制器,其每个方法都继承了类型级"),r("code",[e._v("@ResponseBody")]),e._v("注释,因此,直接写到响应主体与视图解析之间,并使用 HTML 模板进行呈现。")],1),e._v(" "),r("h5",{attrs:{id:"aop-代理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#aop-代理"}},[e._v("#")]),e._v(" AOP 代理")]),e._v(" "),r("p",[e._v("在某些情况下,你可能需要在运行时使用 AOP 代理来装饰控制器。一个例子是,如果你选择在控制器上直接使用"),r("code",[e._v("@Transactional")]),e._v("注释。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。这通常是控制器的默认选择。但是,如果控制器必须实现不是 Spring 上下文回调的接口(例如"),r("code",[e._v("InitializingBean")]),e._v("、"),r("code",[e._v("*Aware")]),e._v("等),则可能需要显式地配置基于类的代理。例如,使用"),r("code",[e._v("")]),e._v("可以更改为"),r("code",[e._v('')]),e._v(",使用"),r("code",[e._v("@EnableTransactionManagement")]),e._v("可以更改为"),r("code",[e._v("@EnableTransactionManagement(proxyTargetClass = true)")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-3-2-请求映射"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-2-请求映射"}},[e._v("#")]),e._v(" 1.3.2.请求映射")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("@RequestMapping")]),e._v("注释将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射,或者在方法级别上使用它来缩小到特定的端点映射。")]),e._v(" "),r("p",[e._v("还有"),r("code",[e._v("@RequestMapping")]),e._v("的特定于 HTTP 方法的快捷方式变体:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("@GetMapping")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("@PostMapping")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("@PutMapping")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("@DeleteMapping")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("@PatchMapping")])])])]),e._v(" "),r("p",[e._v("提供的快捷方式是"),r("a",{attrs:{href:"#mvc-ann-requestmapping-composed"}},[e._v("自定义注释")]),e._v(",因为可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用"),r("code",[e._v("@RequestMapping")]),e._v(",后者默认情况下与所有 HTTP 方法匹配。在类级别上仍然需要一个"),r("code",[e._v("@RequestMapping")]),e._v("来表示共享映射。")]),e._v(" "),r("p",[e._v("下面的示例具有类型和方法级别的映射:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\n@RequestMapping("/persons")\nclass PersonController {\n\n @GetMapping("/{id}")\n public Person getPerson(@PathVariable Long id) {\n // ...\n }\n\n @PostMapping\n @ResponseStatus(HttpStatus.CREATED)\n public void add(@RequestBody Person person) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\n@RequestMapping("/persons")\nclass PersonController {\n\n @GetMapping("/{id}")\n fun getPerson(@PathVariable id: Long): Person {\n // ...\n }\n\n @PostMapping\n @ResponseStatus(HttpStatus.CREATED)\n fun add(@RequestBody person: Person) {\n // ...\n }\n}\n')])])]),r("h5",{attrs:{id:"uri-模式"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#uri-模式"}},[e._v("#")]),e._v(" URI 模式")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-uri-templates"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@RequestMapping")]),e._v("方法可以使用 URL 模式进行映射。有两种选择:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("PathPattern")]),e._v("——与 URL 路径匹配的预解析模式也预解析为"),r("code",[e._v("PathContainer")]),e._v("。该解决方案是为网络应用而设计的,能够有效地处理编码和路径参数,并能有效地进行匹配。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("{varName:regex}")]),e._v("—将字符串模式与字符串路径匹配。这也是在 Spring 配置中使用的原始解决方案,用于在 Classpath、文件系统上和其他位置上选择资源。它的效率较低,并且字符串路径输入对于有效地处理编码和 URL 的其他问题是一个挑战。")])])]),e._v(" "),r("p",[r("code",[e._v("PathPattern")]),e._v("是 Web 应用程序的推荐解决方案,也是 Spring WebFlux 中的唯一选择。在版本 5.3 之前,"),r("code",[e._v("AntPathMatcher")]),e._v("是 Spring MVC 中的唯一选择,并且仍然是默认的。但是"),r("code",[e._v("PathPattern")]),e._v("可以在"),r("a",{attrs:{href:"#mvc-config-path-matching"}},[e._v("MVC config")]),e._v("中启用。")]),e._v(" "),r("p",[r("code",[e._v("PathPattern")]),e._v("支持与"),r("code",[e._v("AntPathMatcher")]),e._v("相同的模式语法。此外,它还支持捕获模式,例如"),r("code",[e._v("{*spring}")]),e._v(",用于在路径的末端匹配 0 个或更多个路径段。"),r("code",[e._v("PathPattern")]),e._v("还限制了"),r("code",[e._v("**")]),e._v("用于匹配多个路径段的使用,因此只允许在模式的末尾使用。在为给定的请求选择最佳匹配模式时,这消除了许多模棱两可的情况。有关完整模式语法,请参阅"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/pattern/PathPattern.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("路径模式"),r("OutboundLink")],1),e._v("和"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/util/AntPathMatcher.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Antpathmatcher"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("p",[e._v("一些示例模式:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v('"/resources/ima?e.png"')]),e._v("-匹配路径段中的一个字符")])]),e._v(" "),r("li",[r("p",[r("code",[e._v('"/resources/*.png"')]),e._v("-匹配路径段中的零个或多个字符")])]),e._v(" "),r("li",[r("p",[r("code",[e._v('"/resources/**"')]),e._v("-匹配多个路径段")])]),e._v(" "),r("li",[r("p",[r("code",[e._v('"/projects/{project}/versions"')]),e._v("-匹配路径段并将其捕获为变量")])]),e._v(" "),r("li",[r("p",[r("code",[e._v('"/projects/{project:[a-z]+}/versions"')]),e._v("-匹配并捕获带有正则表达式的变量")])])]),e._v(" "),r("p",[e._v("捕获的 URI 变量可以通过"),r("code",[e._v("@PathVariable")]),e._v("访问。例如:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/owners/{ownerId}/pets/{petId}")\npublic Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/owners/{ownerId}/pets/{petId}")\nfun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {\n // ...\n}\n')])])]),r("p",[e._v("可以在类和方法级别声明 URI 变量,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@RequestMapping("/owners/{ownerId}")\npublic class OwnerController {\n\n @GetMapping("/pets/{petId}")\n public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@RequestMapping("/owners/{ownerId}")\nclass OwnerController {\n\n @GetMapping("/pets/{petId}")\n fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {\n // ...\n }\n}\n')])])]),r("p",[e._v("URI 变量将自动转换为适当的类型,或者生成"),r("code",[e._v("TypeMismatchException")]),e._v("。默认情况下支持简单类型("),r("code",[e._v("int")]),e._v(","),r("code",[e._v("long")]),e._v(","),r("code",[e._v("Date")]),e._v(",等等),你可以注册对任何其他数据类型的支持。参见"),r("a",{attrs:{href:"#mvc-ann-typeconversion"}},[e._v("类型转换")]),e._v("和["),r("code",[e._v("DataBinder")]),e._v("]。")]),e._v(" "),r("p",[e._v("你可以显式地命名 URI 变量(例如,"),r("code",[e._v('@PathVariable("customId")')]),e._v("),但是如果名称相同,并且你的代码是使用调试信息或 Java8 上的"),r("code",[e._v("-parameters")]),e._v("编译器标志编译的,则可以忽略该详细信息。")]),e._v(" "),r("p",[e._v("语法"),r("code",[e._v("{varName:regex}")]),e._v("声明一个 URI 变量,其正则表达式的语法为"),r("code",[e._v("{varName:regex}")]),e._v("。例如,给定的 URL"),r("code",[e._v('"/spring-web-3.0.5.jar"')]),e._v(",下面的方法会提取名称、版本和文件扩展名:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/{name:[a-z-]+}-{version:\\\\d\\\\.\\\\d\\\\.\\\\d}{ext:\\\\.[a-z]+}")\npublic void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/{name:[a-z-]+}-{version:\\\\d\\\\.\\\\d\\\\.\\\\d}{ext:\\\\.[a-z]+}")\nfun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) {\n // ...\n}\n')])])]),r("p",[e._v("URI 路径模式还可以嵌入"),r("code",[e._v("${…​}")]),e._v("占位符,这些占位符在启动时通过针对本地、系统、环境和其他属性源使用"),r("code",[e._v("PropertyPlaceHolderConfigurer")]),e._v("进行解析。例如,你可以使用它来基于某些外部配置参数化基本 URL。")]),e._v(" "),r("h5",{attrs:{id:"模式比较"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#模式比较"}},[e._v("#")]),e._v(" 模式比较")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-pattern-comparison"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("当多个模式匹配一个 URL 时,必须选择最佳匹配。根据解析的"),r("code",[e._v("PathPattern")]),e._v("的使用是否被启用,可以使用以下方法之一来完成此操作:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("["),r("code",[e._v("PathPattern.SPECIFICITY_COMPARATOR")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/util/pattern/pathpattern.html#specificity_comparator)")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("AntPathMatcher.getPatternComparator(String path)")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/util/antpathmatcher.html#getpatterncomparator-java.lang.string-)")])])]),e._v(" "),r("p",[e._v("这两种方法都有助于在顶部使用更具体的模式对模式进行排序。如果一个模式的 URI 变量(计为 1)、单通配符(计为 1)和双通配符(计为 2)的数量较少,那么它就不那么具体。给定一个相等的分数,选择较长的模式。在相同的分数和长度下,将选择 URI 变量多于通配符的模式。")]),e._v(" "),r("p",[e._v("默认的映射模式("),r("code",[e._v("/**")]),e._v(")被排除在评分之外,并且总是排在最后。另外,前缀模式(如"),r("code",[e._v("/public/**")]),e._v(")被认为不如其他没有双通配符的模式具体。")]),e._v(" "),r("p",[e._v("有关详细信息,请按照上面的链接查看模式比较器。")]),e._v(" "),r("h5",{attrs:{id:"后缀匹配"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#后缀匹配"}},[e._v("#")]),e._v(" 后缀匹配")]),e._v(" "),r("p",[e._v("从 5.3 开始,默认情况下 Spring MVC 不再执行"),r("code",[e._v(".*")]),e._v("后缀模式匹配,其中映射到"),r("code",[e._v("/person")]),e._v("的控制器也隐式映射到"),r("code",[e._v("/person.*")]),e._v("。因此,路径扩展不再用于解释响应所请求的内容类型——例如,"),r("code",[e._v("/person.pdf")]),e._v(","),r("code",[e._v("/person.xml")]),e._v(",以此类推。")]),e._v(" "),r("p",[e._v("在浏览器发送"),r("code",[e._v("Accept")]),e._v("头文件时,以这种方式使用文件扩展名是必要的,因为这种头文件很难一致地进行解释。目前,这已不再是必要的,使用"),r("code",[e._v("Accept")]),e._v("头应该是首选的选择。")]),e._v(" "),r("p",[e._v("随着时间的推移,文件扩展名的使用在很多方面都被证明是有问题的。当使用 URI 变量、路径参数和 URI 编码时,它可能会造成歧义。关于基于 URL 的授权和安全性的推理(更多详细信息请参见下一节)也变得更加困难。")]),e._v(" "),r("p",[e._v("要在 5.3 之前的版本中完全禁用路径扩展的使用,请设置以下内容:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("useSuffixPatternMatching(false)")]),e._v(",见"),r("a",{attrs:{href:"#mvc-config-path-matching"}},[e._v("路径匹配配置器")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("favorPathExtension(false)")]),e._v(",见"),r("a",{attrs:{href:"#mvc-config-content-negotiation"}},[e._v("内容协商配置器")])])])]),e._v(" "),r("p",[e._v("有一种方法来请求内容类型,而不是通过"),r("code",[e._v("HttpServletRequest#getUserPrincipal")]),e._v("头仍然是有用的,例如,当在浏览器中输入 URL 时。路径扩展的一种安全替代方法是使用查询参数策略。如果必须使用文件扩展名,请考虑通过"),r("a",{attrs:{href:"#mvc-config-content-negotiation"}},[e._v("内容协商配置器")]),e._v("的"),r("code",[e._v("mediaTypes")]),e._v("属性将它们限制为显式注册的扩展名列表。")]),e._v(" "),r("h5",{attrs:{id:"后缀匹配和-rfd"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#后缀匹配和-rfd"}},[e._v("#")]),e._v(" 后缀匹配和 RFD")]),e._v(" "),r("p",[e._v("反射文件下载攻击类似于 XSS,因为它依赖于响应中反映的请求输入(例如,查询参数和 URI 变量)。然而,RFD 攻击不是将 JavaScript 插入 HTML,而是依靠浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。")]),e._v(" "),r("p",[e._v("Spring MVC 中,"),r("code",[e._v("@ResponseBody")]),e._v("和"),r("code",[e._v("ResponseEntity")]),e._v("方法存在风险,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展来请求这些内容类型。禁用后缀模式匹配和使用路径扩展进行内容协商降低了风险,但不足以防止 RFD 攻击。")]),e._v(" "),r("p",[e._v("为了防止 RFD 攻击,在呈现响应体之前, Spring MVC 添加了一个"),r("code",[e._v("Content-Disposition:inline;filename=f.txt")]),e._v("头,以建议一个固定且安全的下载文件。只有当 URL 路径包含一个文件扩展名,而该文件扩展名既不被允许为安全的,也没有显式地注册用于内容协商时,才会执行此操作。然而,当 URL 被直接输入到浏览器中时,它可能会产生副作用。")]),e._v(" "),r("p",[e._v("默认情况下,许多常见的路径扩展都是安全的。具有自定义"),r("code",[e._v("HttpMessageConverter")]),e._v("实现的应用程序可以显式地注册用于内容协商的文件扩展名,以避免为这些扩展名添加"),r("code",[e._v("Content-Disposition")]),e._v("头。见"),r("a",{attrs:{href:"#mvc-config-content-negotiation"}},[e._v("内容类型")]),e._v("。")]),e._v(" "),r("p",[e._v("有关 RFD 的其他建议,请参见"),r("a",{attrs:{href:"https://pivotal.io/security/cve-2015-5211",target:"_blank",rel:"noopener noreferrer"}},[e._v("CVE-2015-5211"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"可消费媒体类型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#可消费媒体类型"}},[e._v("#")]),e._v(" 可消费媒体类型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-consumes"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以基于请求的"),r("code",[e._v("Content-Type")]),e._v("缩小请求映射范围,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping(path = "/pets", consumes = "application/json") (1)\npublic void addPet(@RequestBody Pet pet) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("consumes")]),e._v("属性通过内容类型缩小映射范围。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/pets", consumes = ["application/json"]) (1)\nfun addPet(@RequestBody pet: Pet) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("consumes")]),e._v("属性通过内容类型缩小映射范围。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[r("code",[e._v("consumes")]),e._v("属性还支持否定表达式——例如,"),r("code",[e._v("!text/plain")]),e._v("表示除"),r("code",[e._v("text/plain")]),e._v("以外的任何内容类型。")]),e._v(" "),r("p",[e._v("你可以在类级别声明一个共享的"),r("code",[e._v("consumes")]),e._v("属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别"),r("code",[e._v("consumes")]),e._v("属性覆盖而不是扩展类级声明。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("code",[e._v("MediaType")]),e._v("为常用的媒体类型提供常量,例如"),r("code",[e._v("APPLICATION_JSON_VALUE")]),e._v("和"),r("code",[e._v("APPLICATION_XML_VALUE")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"可生产媒体类型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#可生产媒体类型"}},[e._v("#")]),e._v(" 可生产媒体类型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-produces"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以基于"),r("code",[e._v("Accept")]),e._v("请求头和控制器方法产生的内容类型列表来缩小请求映射的范围,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping(path = "/pets/{petId}", produces = "application/json") (1)\n@ResponseBody\npublic Pet getPet(@PathVariable String petId) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("produces")]),e._v("属性通过内容类型缩小映射范围。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/pets/{petId}", produces = ["application/json"]) (1)\n@ResponseBody\nfun getPet(@PathVariable petId: String): Pet {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("produces")]),e._v("属性通过内容类型缩小映射范围。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("媒体类型可以指定字符集。支持否定表达式——例如,"),r("code",[e._v("!text/plain")]),e._v("表示除“text/plain”以外的任何内容类型。")]),e._v(" "),r("p",[e._v("你可以在类级别声明一个共享的"),r("code",[e._v("produces")]),e._v("属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别"),r("code",[e._v("removeSemicolonContent=false")]),e._v("属性覆盖而不是扩展类级声明。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("code",[e._v("MediaType")]),e._v("为常用的媒体类型提供常量,例如"),r("code",[e._v("APPLICATION_JSON_VALUE")]),e._v("和"),r("code",[e._v("APPLICATION_XML_VALUE")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"参数、标头"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#参数、标头"}},[e._v("#")]),e._v(" 参数、标头")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-params-and-headers"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以根据请求参数条件缩小请求映射的范围。你可以测试是否存在请求参数("),r("code",[e._v("myParam")]),e._v("),是否没有请求参数("),r("code",[e._v("!myParam")]),e._v("),或者是否存在特定值("),r("a",{attrs:{href:"#mvc-config-content-negotiation"}},[e._v("内容类型")]),e._v(")。下面的示例展示了如何测试一个特定值:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)\npublic void findPet(@PathVariable String petId) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("测试"),r("code",[e._v("myParam")]),e._v("是否等于"),r("code",[e._v("myValue")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)\nfun findPet(@PathVariable petId: String) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("测试"),r("code",[e._v("myParam")]),e._v("是否等于"),r("code",[e._v("myValue")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("你也可以在请求头条件中使用相同的方法,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)\npublic void findPet(@PathVariable String petId) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("测试"),r("code",[e._v("myHeader")]),e._v("是否等于"),r("code",[e._v("myValue")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)\nfun findPet(@PathVariable petId: String) {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("你可以将"),r("code",[e._v("Content-Type")]),e._v("和"),r("code",[e._v("Accept")]),e._v("与 headers 条件匹配,但最好使用"),r("a",{attrs:{href:"#mvc-ann-requestmapping-consumes"}},[e._v("consumes")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-requestmapping-produces"}},[e._v("produces")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"http-头-选项"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#http-头-选项"}},[e._v("#")]),e._v(" HTTP 头,选项")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-head-options"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@GetMapping")]),e._v("(和"),r("code",[e._v("@RequestMapping(method=HttpMethod.GET)")]),e._v(")透明地支持用于请求映射的 HTTP head。控制器的方法不需要更改。在"),r("code",[e._v("javax.servlet.http.HttpServlet")]),e._v("中应用的响应包装程序确保将"),r("code",[e._v("Content-Length")]),e._v("头设置为写入的字节数(而不实际写入响应)。")]),e._v(" "),r("p",[r("code",[e._v("@GetMapping")]),e._v("(和"),r("code",[e._v("@RequestMapping(method=HttpMethod.GET)")]),e._v(")隐式映射到并支持 HTTPHEAD。HTTP head 请求的处理方式就像 HTTP GET 一样,除了不写正文,而是计算字节数并设置"),r("code",[e._v("Content-Length")]),e._v("头。")]),e._v(" "),r("p",[e._v("默认情况下,HTTP 选项是通过将"),r("code",[e._v("Allow")]),e._v("响应头设置为具有匹配的 URL 模式的所有"),r("code",[e._v("@RequestMapping")]),e._v("方法中列出的 HTTP 方法列表来处理的。")]),e._v(" "),r("p",[e._v("对于不带 HTTP 方法声明的"),r("code",[e._v("@RequestMapping")]),e._v(","),r("code",[e._v("Allow")]),e._v("头被设置为"),r("code",[e._v("@RequestMapping")]),e._v("。控制器方法应该总是声明支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体:"),r("code",[e._v("@GetMapping")]),e._v(","),r("code",[e._v("@PostMapping")]),e._v(",以及其他)。")]),e._v(" "),r("p",[e._v("你可以显式地将"),r("code",[e._v("@RequestMapping")]),e._v("方法映射到 HTTPHead 和 HTTPOptions,但在常见的情况下,这是不必要的。")]),e._v(" "),r("h5",{attrs:{id:"自定义注释"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#自定义注释"}},[e._v("#")]),e._v(" 自定义注释")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#mvc-ann-requestmapping-head-options"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 支持使用"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#beans-meta-annotations"}},[e._v("组合注释")]),e._v("进行请求映射。这些注释本身是用"),r("code",[e._v("@RequestMapping")]),e._v("进行元注释的,并且是为了重新声明"),r("code",[e._v("@RequestMapping")]),e._v("属性的子集(或全部)而组成的,具有更窄、更具体的目的。")],1),e._v(" "),r("p",[r("code",[e._v("@GetMapping")]),e._v(","),r("code",[e._v("@PostMapping")]),e._v(","),r("code",[e._v("@PutMapping")]),e._v(","),r("code",[e._v("@DeleteMapping")]),e._v("和"),r("code",[e._v("@PatchMapping")]),e._v("是复合注释的例子。提供它们是因为,可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用"),r("code",[e._v("@RequestMapping")]),e._v(",后者默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例,请查看这些注释是如何声明的。")]),e._v(" "),r("p",[e._v("Spring MVC 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,它需要子类化"),r("code",[e._v("RequestMappingHandlerMapping")]),e._v("并覆盖"),r("code",[e._v("getCustomMethodCondition")]),e._v("方法,在该方法中,你可以检查自定义属性并返回你自己的"),r("code",[e._v("RequestCondition")]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"显式注册"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#显式注册"}},[e._v("#")]),e._v(" 显式注册")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestmapping-registration"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以以编程方式注册处理程序方法,你可以将其用于动态注册或高级情况,例如同一处理程序在不同 URL 下的不同实例。下面的示例注册了一个处理程序方法:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\npublic class MyConfig {\n\n @Autowired\n public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)\n throws NoSuchMethodException {\n\n RequestMappingInfo info = RequestMappingInfo\n .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)\n\n Method method = UserHandler.class.getMethod("getUser", Long.class); (3)\n\n mapping.registerMapping(info, handler, method); (4)\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("为控制器注入目标处理程序和处理程序映射。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("准备请求映射元数据。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("获取 handler 方法。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[e._v("添加注册。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\nclass MyConfig {\n\n @Autowired\n fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)\n val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)\n val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)\n mapping.registerMapping(info, handler, method) (4)\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("为控制器注入目标处理程序和处理程序映射。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("准备请求映射元数据。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("获取 handler 方法。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[e._v("添加注册。")])])])]),e._v(" "),r("h4",{attrs:{id:"_1-3-3-处理程序方法"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-3-处理程序方法"}},[e._v("#")]),e._v(" 1.3.3.处理程序方法")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-methods"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@RequestMapping")]),e._v("处理程序方法具有灵活的签名,并且可以从受支持的控制器方法参数和返回值的范围中进行选择。")]),e._v(" "),r("h5",{attrs:{id:"方法参数"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#方法参数"}},[e._v("#")]),e._v(" 方法参数")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-arguments"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("下一个表描述了受支持的控制器方法参数。任何参数都不支持反应式类型。")]),e._v(" "),r("p",[e._v("JDK8 的"),r("code",[e._v("java.util.Optional")]),e._v("作为方法参数与具有"),r("code",[e._v("required")]),e._v("属性(例如,"),r("code",[e._v("@RequestParam")]),e._v(","),r("code",[e._v("@RequestParam")]),e._v(",以及其他)并且与"),r("code",[e._v("required=false")]),e._v("等价的注释组合在一起,并被支持。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Controller method argument")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("WebRequest")]),e._v(", "),r("code",[e._v("NativeWebRequest")])]),e._v(" "),r("td",[e._v("对请求参数以及请求和会话属性的通用访问,而不需要直接使用"),r("br"),e._v("的 Servlet API。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("javax.servlet.ServletRequest")]),e._v(", "),r("code",[e._v("javax.servlet.ServletResponse")])]),e._v(" "),r("td",[e._v("选择任何特定的请求或响应类型——例如,"),r("code",[e._v("ServletRequest")]),e._v(","),r("code",[e._v("HttpServletRequest")]),e._v(","),r("br"),e._v("或 Spring 的"),r("code",[e._v("ServletRequest")]),e._v(","),r("code",[e._v("MultipartHttpServletRequest")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("javax.servlet.http.HttpSession")])]),e._v(" "),r("td",[e._v("强制执行会话的存在。因此,这样的参数永远不是"),r("code",[e._v("null")]),e._v("。"),r("br"),e._v("注意,会话访问不是线程安全的。考虑将"),r("code",[e._v("RequestMappingHandlerAdapter")]),e._v("实例的"),r("code",[e._v("synchronizeOnSession")]),e._v("标志设置为"),r("code",[e._v("true")]),e._v(",如果允许多个"),r("br"),e._v("请求并发访问会话。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("javax.servlet.http.PushBuilder")])]),e._v(" "),r("td",[e._v("Servlet 4.0Push Builder API 用于程序化的 HTTP/2 资源推送。注意,根据 Servlet 规范,如果客户机不支持该 HTTP/2 功能,则注入的实例可以为空。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.security.Principal")])]),e._v(" "),r("td",[e._v("当前经过身份验证的用户——如果已知的话,可能是特定的"),r("code",[e._v("Principal")]),e._v("实现类。"),r("br"),r("br"),e._v("注意,如果为了允许自定义解析器在通过"),r("code",[e._v("HttpServletRequest#getUserPrincipal")]),e._v("恢复默认解析之前对该参数进行注释,则该参数不会急于解析,例如,"),r("br"),e._v(", Spring security"),r("code",[e._v("Principal")]),e._v("实现了"),r("code",[e._v("Principal")]),e._v(",并且将通过"),r("code",[e._v("HttpServletRequest#getUserPrincipal")]),e._v("这样注入,除非它也被注释为"),r("code",[e._v("@AuthenticationPrincipal")]),e._v(",在这种情况下,它"),r("code",[e._v("LocaleResolver")]),e._v("由自定义 Spring security resolver 通过"),r("code",[e._v("Authentication#getPrincipal")]),e._v("解析。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpMethod")])]),e._v(" "),r("td",[e._v("请求的 HTTP 方法。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Locale")])]),e._v(" "),r("td",[e._v("当前的请求区域设置,由最具体的"),r("code",[e._v("LocaleResolver")]),e._v("确定(在"),r("br"),e._v("效果中,配置的"),r("code",[e._v("LocaleResolver")]),e._v("或"),r("code",[e._v("LocaleContextResolver")]),e._v(")。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.TimeZone")]),e._v(" + "),r("code",[e._v("java.time.ZoneId")])]),e._v(" "),r("td",[e._v("与当前请求相关联的时区,由"),r("code",[e._v("LocaleContextResolver")]),e._v("确定。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.io.InputStream")]),e._v(", "),r("code",[e._v("java.io.Reader")])]),e._v(" "),r("td",[e._v("用于访问由 Servlet API 公开的原始请求主体。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.io.OutputStream")]),e._v(", "),r("code",[e._v("java.io.Writer")])]),e._v(" "),r("td",[e._v("用于访问由 Servlet API 公开的原始响应体。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@PathVariable")])]),e._v(" "),r("td",[e._v("用于访问 URI 模板变量。见"),r("a",{attrs:{href:"#mvc-ann-requestmapping-uri-templates"}},[e._v("URI 模式")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@MatrixVariable")])]),e._v(" "),r("td",[e._v("用于访问 URI 路径段中的名称-值对。见"),r("br"),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestParam")])]),e._v(" "),r("td",[e._v("用于访问 Servlet 请求参数,包括多部分文件。参数值"),r("br"),e._v("被转换为声明的方法参数类型。参见["),r("code",[e._v("@RequestParam")]),e._v("](#mvc-ann-requestparam)以及"),r("br"),e._v("as"),r("a",{attrs:{href:"#mvc-multipart-forms"}},[e._v("Multipart")]),e._v("。"),r("br"),r("br"),e._v("注意,对于简单参数值,"),r("code",[e._v("@RequestParam")]),e._v("的使用是可选的。"),r("br"),e._v("参见本表末尾的“任何其他参数”。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestHeader")])]),e._v(" "),r("td",[e._v("用于访问请求头。标头值被转换为声明的方法参数"),r("br"),e._v("type。参见["),r("code",[e._v("@RequestHeader")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@CookieValue")])]),e._v(" "),r("td",[e._v("获取 cookie 的权限。Cookie 值被转换为声明的方法参数"),r("br"),e._v("type。参见["),r("code",[e._v("@CookieValue")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestBody")])]),e._v(" "),r("td",[e._v("用于访问 HTTP 请求主体。通过使用"),r("code",[e._v("HttpMessageConverter")]),e._v("实现,主体内容被转换为声明的方法"),r("br"),e._v("参数类型。参见["),r("code",[e._v("@RequestBody")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpEntity")])]),e._v(" "),r("td",[e._v("用于访问请求头和主体。体被转换为"),r("code",[e._v("HttpMessageConverter")]),e._v("。"),r("br"),e._v("见"),r("a",{attrs:{href:"#mvc-ann-httpentity"}},[e._v("HttpEntity")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestPart")])]),e._v(" "),r("td",[e._v("为了访问"),r("code",[e._v("multipart/form-data")]),e._v("请求中的一个部件,将该部件的主体"),r("br"),e._v("转换为"),r("code",[e._v("HttpMessageConverter")]),e._v("。见"),r("a",{attrs:{href:"#mvc-multipart-forms"}},[e._v("Multipart")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Map")]),e._v(", "),r("code",[e._v("org.springframework.ui.Model")]),e._v(", "),r("code",[e._v("org.springframework.ui.ModelMap")])]),e._v(" "),r("td",[e._v("用于访问 HTML 控制器中使用的模型,并将其作为"),r("br"),e._v("视图呈现的一部分暴露于模板中。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("RedirectAttributes")])]),e._v(" "),r("td",[e._v("指定在重定向情况下使用的属性(即要追加到查询"),r("br"),e._v("字符串中)和要临时存储的 flash 属性,直到重定向后的请求。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-redirecting-passing-data"}},[e._v("重定向属性")]),e._v("和"),r("a",{attrs:{href:"#mvc-flash-attributes"}},[e._v("flash 属性")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@ModelAttribute")])]),e._v(" "),r("td",[e._v("用于访问模型中的现有属性(如果不存在则实例化),并应用"),r("br"),e._v("数据绑定和验证。参见["),r("code",[e._v("@ModelAttribute")]),e._v("](#mvc-ann-modelattrib-method-args)以及"),r("a",{attrs:{href:"#mvc-ann-modelattrib-methods"}},[e._v("Model")]),e._v("和["),r("code",[e._v("DataBinder")]),e._v("](#mvc-ann-initbinder)。"),r("br"),r("br"),e._v("注意,"),r("code",[e._v("@ModelAttribute")]),e._v("的使用是可选的(例如,设置其属性)。"),r("br"),e._v("参见本表末尾的“任何其他参数”。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("Errors")]),e._v(", "),r("code",[e._v("BindingResult")])]),e._v(" "),r("td",[e._v("用于访问来自命令对象"),r("br"),e._v("的验证和数据绑定的错误(即"),r("code",[e._v("@ModelAttribute")]),e._v("参数)或来自"),r("code",[e._v("@RequestBody")]),e._v("或"),r("code",[e._v("@RequestPart")]),e._v("参数的验证的错误。你必须在验证方法参数之后立即声明"),r("code",[e._v("Errors")]),e._v("或"),r("code",[e._v("BindingResult")]),e._v("参数"),r("br"),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("SessionStatus")]),e._v(" + class-level "),r("code",[e._v("@SessionAttributes")])]),e._v(" "),r("td",[e._v("用于标记表单处理完成,这将触发清除通过类级"),r("code",[e._v("@SessionAttributes")]),e._v("注释声明的会话属性"),r("br"),e._v("。有关更多详细信息,请参见["),r("code",[e._v("@SessionAttributes")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("UriComponentsBuilder")])]),e._v(" "),r("td",[e._v("用于准备相对于当前请求的主机、端口、方案、上下文路径的 URL,以及 Servlet 映射的文字部分"),r("br"),e._v("。见"),r("a",{attrs:{href:"#mvc-uri-building"}},[e._v("URI Links")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@SessionAttribute")])]),e._v(" "),r("td",[e._v("对于任何会话属性的访问,与存储在会话"),r("br"),e._v("中的模型属性形成对比的是类级别"),r("code",[e._v("@SessionAttributes")]),e._v("声明的结果。有关更多详细信息,请参见["),r("code",[e._v("@SessionAttribute")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestAttribute")])]),e._v(" "),r("td",[e._v("用于访问请求属性。有关更多详细信息,请参见["),r("code",[e._v("@RequestAttribute")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[e._v("Any other argument")]),e._v(" "),r("td",[e._v("如果方法参数不匹配此表中的任何早期值,并且它是"),r("br"),e._v("一个简单的类型(由"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Beanutils#IsSimpleProperty"),r("OutboundLink")],1),e._v("确定,"),r("br"),e._v(",则将其解析为"),r("code",[e._v("@RequestParam")]),e._v("。否则,它被解析为"),r("code",[e._v("@ModelAttribute")]),e._v("。")])])])]),e._v(" "),r("h5",{attrs:{id:"返回值"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#返回值"}},[e._v("#")]),e._v(" 返回值")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-return-types"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("下一个表描述了受支持的控制器方法的返回值。所有返回值都支持反应性类型。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Controller method return value")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("@ResponseBody")])]),e._v(" "),r("td",[e._v("返回值通过"),r("code",[e._v("HttpMessageConverter")]),e._v("实现进行转换,并写入"),r("br"),e._v("响应。参见["),r("code",[e._v("@ResponseBody")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpEntity")]),e._v(", "),r("code",[e._v("ResponseEntity")])]),e._v(" "),r("td",[e._v("指定完整响应(包括 HTTP 头和主体)的返回值将通过"),r("code",[e._v("HttpMessageConverter")]),e._v("实现被转换"),r("br"),e._v("并写入响应。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-ann-responseentity"}},[e._v("负责实体")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpHeaders")])]),e._v(" "),r("td",[e._v("返回带有标题而没有正文的响应。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("String")])]),e._v(" "),r("td",[e._v("要用"),r("code",[e._v("ViewResolver")]),e._v("实现来解析的视图名称,并与隐式"),r("br"),e._v("模型一起使用——通过命令对象和"),r("code",[e._v("@ModelAttribute")]),e._v("方法确定。处理程序"),r("br"),e._v("方法还可以通过声明"),r("code",[e._v("Model")]),e._v("参数"),r("br"),e._v("(参见"),r("a",{attrs:{href:"#mvc-ann-requestmapping-registration"}},[e._v("显式注册")]),e._v(")以编程方式丰富模型。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("View")])]),e._v(" "),r("td",[e._v("通过命令对象和"),r("code",[e._v("@ModelAttribute")]),e._v("方法确定用于与隐式模型一起呈现的"),r("br"),e._v("实例。处理程序方法还可以通过声明一个"),r("code",[e._v("Model")]),e._v("参数"),r("br"),e._v("(参见"),r("code",[e._v("Model")]),e._v(")以编程方式丰富模型。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Map")]),e._v(", "),r("code",[e._v("org.springframework.ui.Model")])]),e._v(" "),r("td",[e._v("要添加到隐式模型的属性,通过"),r("code",[e._v("RequestToViewNameTranslator")]),e._v("隐式确定视图名称"),r("br"),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@ModelAttribute")])]),e._v(" "),r("td",[e._v("要添加到模型中的一个属性,其视图名称通过"),r("br"),e._v("a"),r("code",[e._v("RequestToViewNameTranslator")]),e._v("隐式确定。"),r("br"),r("br"),e._v("注意,"),r("code",[e._v("@ModelAttribute")]),e._v("是可选的。请参阅"),r("br"),e._v("本表末尾的“任何其他返回值”。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ModelAndView")]),e._v(" object")]),e._v(" "),r("td",[e._v("要使用的视图和模型属性,以及可选的响应状态。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("void")])]),e._v(" "),r("td",[e._v("具有"),r("code",[e._v("void")]),e._v("返回类型(或"),r("code",[e._v("null")]),e._v("返回值)的方法被认为具有完全的"),r("br"),e._v("处理响应,如果它还具有"),r("code",[e._v("ServletResponse")]),e._v(","),r("code",[e._v("OutputStream")]),e._v("参数,或"),r("br"),r("code",[e._v("@ResponseStatus")]),e._v("注释。如果控制器进行了正的"),r("code",[e._v("ETag")]),e._v("或"),r("code",[e._v("lastModified")]),e._v("时间戳检查(详见"),r("a",{attrs:{href:"#mvc-caching-etag-lastmodified"}},[e._v("控制器")]),e._v("),也是如此。"),r("code",[e._v("ServletResponse")]),e._v("如果以上都不是真的,"),r("code",[e._v("void")]),e._v("返回类型还可以指示"),r("br"),e._v("REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("DeferredResult")])]),e._v(" "),r("td",[e._v("从任何线程异步生成前面的任何返回值——例如,作为某个事件或回调的结果"),r("br"),e._v("。参见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和["),r("code",[e._v("DeferredResult")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("Callable")])]),e._v(" "),r("td",[e._v("在 Spring MVC 管理的线程中异步地产生上述任一返回值。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和["),r("code",[e._v("Callable")]),e._v("](#mvc-ann-async-callable)。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ListenableFuture")]),e._v(","),r("code",[e._v("java.util.concurrent.CompletionStage")]),e._v(","),r("code",[e._v("java.util.concurrent.CompletableFuture")])]),e._v(" "),r("td",[e._v("替代"),r("code",[e._v("DeferredResult")]),e._v(",作为一种方便(例如,当底层服务"),r("br"),e._v("返回其中之一时)。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ResponseBodyEmitter")]),e._v(", "),r("code",[e._v("SseEmitter")])]),e._v(" "),r("td",[e._v("用"),r("code",[e._v("MultiValueMap")]),e._v("实现异步地发出对象流以写入响应。也支持作为"),r("code",[e._v("ResponseEntity")]),e._v("的主体。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-async-http-streaming"}},[e._v("HTTP 流媒体")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("StreamingResponseBody")])]),e._v(" "),r("td",[e._v("异步写入响应"),r("code",[e._v("OutputStream")]),e._v("。也支持作为"),r("code",[e._v("ResponseEntity")]),e._v("的主体。见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-async-http-streaming"}},[e._v("HTTP 流媒体")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[e._v("Reactive types — Reactor, RxJava, or others through "),r("code",[e._v("ReactiveAdapterRegistry")])]),e._v(" "),r("td",[e._v("对于具有多值流的"),r("code",[e._v("DeferredResult")]),e._v("(例如,"),r("code",[e._v("Flux")]),e._v(","),r("code",[e._v("Observable")]),e._v(")"),r("br"),e._v("收集到"),r("code",[e._v("List")]),e._v("。"),r("br"),e._v("用于流场景(例如,使用"),r("code",[e._v("text/event-stream")]),e._v(","),r("code",[e._v("application/json+stream")]),e._v("和"),r("code",[e._v("ResponseBodyEmitter")]),e._v(",其中"),r("code",[e._v("SseEmitter")]),e._v("在 Spring MVC 管理的线程上执行阻塞 I/O,并且在每次写操作完成时施加背压"),r("br"),e._v("。"),r("br"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应类型")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[e._v("Any other return value")]),e._v(" "),r("td",[e._v("如果返回值与此表中的任何早期值不匹配,并且"),r("br"),e._v("是"),r("code",[e._v("String")]),e._v("或"),r("code",[e._v("void")]),e._v(",则将其作为视图名处理(如果不是简单的类型,则通过"),r("code",[e._v("RequestToViewNameTranslator")]),e._v("进行默认视图名选择),由"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Beanutils#IsSimpleProperty"),r("OutboundLink")],1),e._v("确定。"),r("br"),e._v("是简单类型的值仍未解决。")])])])]),e._v(" "),r("h5",{attrs:{id:"类型转换"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#类型转换"}},[e._v("#")]),e._v(" 类型转换")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-typeconversion"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("一些表示"),r("code",[e._v("String")]),e._v("的基于请求输入的带注释的控制器方法参数(例如"),r("code",[e._v("@RequestParam")]),e._v(","),r("code",[e._v("@RequestHeader")]),e._v(","),r("code",[e._v("@PathVariable")]),e._v(","),r("code",[e._v("@MatrixVariable")]),e._v(",以及"),r("code",[e._v("@CookieValue")]),e._v(")可以要求类型转换,如果参数被声明为"),r("code",[e._v("String")]),e._v("以外的内容。")]),e._v(" "),r("p",[e._v("对于这样的情况,类型转换是基于配置的转换器自动应用的。默认情况下,支持简单类型("),r("code",[e._v("int")]),e._v("、"),r("code",[e._v("long")]),e._v("、"),r("code",[e._v("Date")]),e._v("等)。你可以通过"),r("code",[e._v("WebDataBinder")]),e._v("(参见["),r("code",[e._v("DataBinder")]),e._v("](#mvc-ann-initbinder))或通过使用"),r("code",[e._v("FormattingConversionService")]),e._v("注册"),r("code",[e._v("Formatters")]),e._v("来定制类型转换。见"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#format"}},[e._v("Spring Field Formatting")]),e._v("。")],1),e._v(" "),r("p",[e._v("类型转换中的一个实际问题是空字符串源值的处理。如果类型转换的结果是"),r("code",[e._v("null")]),e._v(",那么这样的值将被视为丢失。这可能是"),r("code",[e._v("Long")]),e._v("、"),r("code",[e._v("UUID")]),e._v("和其他目标类型的情况。如果要允许注入"),r("code",[e._v("null")]),e._v(",可以在参数注释上使用"),r("code",[e._v("required")]),e._v("标志,或者将参数声明为"),r("code",[e._v("@Nullable")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("从 5.3 开始,即使在类型转换之后,也将强制执行非空参数。如果你的处理程序"),r("br"),e._v("方法也打算接受空值,那么可以将参数声明为"),r("code",[e._v("@Nullable")]),e._v(",或者在相应的"),r("code",[e._v("@RequestParam")]),e._v("注释中将其标记为"),r("code",[e._v("required=false")]),e._v("。这是"),r("br"),e._v("在 5.3 升级中遇到的回归的最佳实践和推荐解决方案。"),r("br"),r("br"),e._v("或者,你可以具体地处理例如在所需"),r("a",{attrs:{href:"https://www.w3.org/DesignIssues/MatrixURIs.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("“old post”"),r("OutboundLink")],1),e._v("的情况下产生的"),r("code",[e._v("@PathVariable")]),e._v("。转换后的空值将被处理为像"),r("br"),e._v("一个空的原始值一样,因此相应的"),r("code",[e._v("Missing…​Exception")]),e._v("变量将被抛出。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"矩阵变量"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#矩阵变量"}},[e._v("#")]),e._v(" 矩阵变量")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-matrix-variables"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("a",{attrs:{href:"https://tools.ietf.org/html/rfc3986#section-3.3",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC 3986"),r("OutboundLink")],1),e._v("讨论路径段中的名称-值对。在 Spring MVC 中,我们将那些称为基于 Tim Berners-Lee 的"),r("a",{attrs:{href:"https://www.w3.org/DesignIssues/MatrixURIs.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("“old post”"),r("OutboundLink")],1),e._v("的“矩阵变量”,但它们也可以称为 URI 路径参数。")]),e._v(" "),r("p",[e._v("矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,"),r("code",[e._v("/cars;color=red,green;year=2012")]),e._v(")。还可以通过重复的变量名指定多个值(例如,"),r("code",[e._v("@RequestParam")]),e._v(")。")]),e._v(" "),r("p",[e._v("如果一个 URL 预期包含矩阵变量,那么控制器方法的请求映射必须使用一个 URI 变量来掩盖该变量内容,并确保请求可以独立于矩阵变量的顺序和存在而成功匹配。下面的示例使用了一个矩阵变量:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /pets/42;q=11;r=22\n\n@GetMapping("/pets/{petId}")\npublic void findPet(@PathVariable String petId, @MatrixVariable int q) {\n\n // petId == 42\n // q == 11\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /pets/42;q=11;r=22\n\n@GetMapping("/pets/{petId}")\nfun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {\n\n // petId == 42\n // q == 11\n}\n')])])]),r("p",[e._v("考虑到所有的路径段都可能包含矩阵变量,你有时可能需要消除矩阵变量预期在哪个路径变量中的歧义。下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /owners/42;q=11/pets/21;q=22\n\n@GetMapping("/owners/{ownerId}/pets/{petId}")\npublic void findPet(\n @MatrixVariable(name="q", pathVar="ownerId") int q1,\n @MatrixVariable(name="q", pathVar="petId") int q2) {\n\n // q1 == 11\n // q2 == 22\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /owners/42;q=11/pets/21;q=22\n\n@GetMapping("/owners/{ownerId}/pets/{petId}")\nfun findPet(\n @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,\n @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {\n\n // q1 == 11\n // q2 == 22\n}\n')])])]),r("p",[e._v("矩阵变量可以定义为可选的,并指定默认值,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /pets/42\n\n@GetMapping("/pets/{petId}")\npublic void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {\n\n // q == 1\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /pets/42\n\n@GetMapping("/pets/{petId}")\nfun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {\n\n // q == 1\n}\n')])])]),r("p",[e._v("要获得所有矩阵变量,可以使用"),r("code",[e._v("MultiValueMap")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /owners/42;q=11;r=12/pets/21;q=22;s=23\n\n@GetMapping("/owners/{ownerId}/pets/{petId}")\npublic void findPet(\n @MatrixVariable MultiValueMap matrixVars,\n @MatrixVariable(pathVar="petId") MultiValueMap petMatrixVars) {\n\n // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]\n // petMatrixVars: ["q" : 22, "s" : 23]\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// GET /owners/42;q=11;r=12/pets/21;q=22;s=23\n\n@GetMapping("/owners/{ownerId}/pets/{petId}")\nfun findPet(\n @MatrixVariable matrixVars: MultiValueMap,\n @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap) {\n\n // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]\n // petMatrixVars: ["q" : 22, "s" : 23]\n}\n')])])]),r("p",[e._v("请注意,你需要启用矩阵变量的使用。在 MVC Java 配置中,你需要通过"),r("a",{attrs:{href:"#mvc-config-path-matching"}},[e._v("路径匹配")]),e._v("设置"),r("code",[e._v("UrlPathHelper")]),e._v("和"),r("code",[e._v("removeSemicolonContent=false")]),e._v("。在 MVC XML 命名空间中,可以设置"),r("code",[e._v('')]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"requestparam"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#requestparam"}},[e._v("#")]),e._v(" "),r("code",[e._v("@RequestParam")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestparam"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("@RequestParam")]),e._v("注释将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@RequestMapping("/pets")\npublic class EditPetForm {\n\n // ...\n\n @GetMapping\n public String setupForm(@RequestParam("petId") int petId, Model model) { (1)\n Pet pet = this.clinic.loadPet(petId);\n model.addAttribute("pet", pet);\n return "petForm";\n }\n\n // ...\n\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@RequestParam")]),e._v("绑定"),r("code",[e._v("petId")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.ui.set\n\n@Controller\n@RequestMapping("/pets")\nclass EditPetForm {\n\n // ...\n\n @GetMapping\n fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)\n val pet = this.clinic.loadPet(petId);\n model["pet"] = pet\n return "petForm"\n }\n\n // ...\n\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@RequestParam")]),e._v("绑定"),r("code",[e._v("petId")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("默认情况下,使用该注释的方法参数是必需的,但是你可以通过将"),r("code",[e._v("@RequestParam")]),e._v("注释的"),r("code",[e._v("required")]),e._v("标志设置为"),r("code",[e._v("false")]),e._v("或使用"),r("code",[e._v("java.util.Optional")]),e._v("包装器声明参数来指定方法参数是可选的。")]),e._v(" "),r("p",[e._v("如果目标方法参数类型不是"),r("code",[e._v("String")]),e._v(",则自动应用类型转换。见"),r("a",{attrs:{href:"#mvc-ann-typeconversion"}},[e._v("类型转换")]),e._v("。")]),e._v(" "),r("p",[e._v("将参数类型声明为数组或列表,可以为相同的参数名称解析多个参数值。")]),e._v(" "),r("p",[e._v("当"),r("code",[e._v("@RequestParam")]),e._v("注释被声明为"),r("code",[e._v("Map")]),e._v("或"),r("code",[e._v("MultiValueMap")]),e._v("时,如果注释中没有指定参数名称,那么映射将被填充为每个给定参数名称的请求参数值。")]),e._v(" "),r("p",[e._v("请注意,"),r("code",[e._v("@RequestParam")]),e._v("的使用是可选的(例如,用于设置其属性)。默认情况下,任何参数是一个简单的值类型(由"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Beanutils#IsSimpleProperty"),r("OutboundLink")],1),e._v("确定)且不是由任何其他参数解析器解析的,都将被视为用"),r("code",[e._v("@RequestParam")]),e._v("进行了注释。")]),e._v(" "),r("h5",{attrs:{id:"requestheader"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#requestheader"}},[e._v("#")]),e._v(" "),r("code",[e._v("@RequestHeader")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestheader"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("ResponseEntity")]),e._v("注释将请求头绑定到控制器中的方法参数。")]),e._v(" "),r("p",[e._v("考虑以下带有标题的请求:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("Host localhost:8080\nAccept text/html,application/xhtml+xml,application/xml;q=0.9\nAccept-Language fr,en-gb;q=0.7,en;q=0.3\nAccept-Encoding gzip,deflate\nAccept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7\nKeep-Alive 300\n")])])]),r("p",[e._v("下面的示例获取"),r("code",[e._v("Accept-Encoding")]),e._v("和"),r("code",[e._v("Keep-Alive")]),e._v("标题的值:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/demo")\npublic void handle(\n @RequestHeader("Accept-Encoding") String encoding, (1)\n @RequestHeader("Keep-Alive") long keepAlive) { (2)\n //...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("获取"),r("code",[e._v("Accept-Encoding")]),e._v("标头的值。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("获取"),r("code",[e._v("Keep-Alive")]),e._v("标头的值。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/demo")\nfun handle(\n @RequestHeader("Accept-Encoding") encoding: String, (1)\n @RequestHeader("Keep-Alive") keepAlive: Long) { (2)\n //...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("获取"),r("code",[e._v("Accept-Encoding")]),e._v("标头的值。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("获取"),r("code",[e._v("Keep-Alive")]),e._v("标头的值。")])])])]),e._v(" "),r("p",[e._v("如果目标方法参数类型不是"),r("code",[e._v("String")]),e._v(",则自动应用类型转换。见"),r("a",{attrs:{href:"#mvc-ann-typeconversion"}},[e._v("类型转换")]),e._v("。")]),e._v(" "),r("p",[e._v("当在"),r("code",[e._v("@RequestHeader")]),e._v("、"),r("code",[e._v("MultiValueMap")]),e._v("或"),r("code",[e._v("HttpHeaders")]),e._v("参数上使用"),r("code",[e._v("@RequestHeader")]),e._v("注释时,映射将填充所有头值。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("内置支持用于将逗号分隔的字符串转换为"),r("br"),e._v("数组或字符串集合或类型转换系统已知的其他类型。对于"),r("br"),e._v("示例,用"),r("code",[e._v('@RequestHeader("Accept")')]),e._v("注释的方法参数可以是类型"),r("code",[e._v("String")]),e._v("但也可以是"),r("code",[e._v("String[]")]),e._v("或"),r("code",[e._v("List")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"cookievalue"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#cookievalue"}},[e._v("#")]),e._v(" "),r("code",[e._v("@CookieValue")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-cookievalue"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("@CookieValue")]),e._v("注释将 HTTP cookie 的值绑定到控制器中的方法参数。")]),e._v(" "),r("p",[e._v("考虑使用以下 cookie 的请求:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84\n")])])]),r("p",[e._v("下面的示例展示了如何获得 Cookie 值:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/demo")\npublic void handle(@CookieValue("JSESSIONID") String cookie) { (1)\n //...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("获取"),r("code",[e._v("JSESSIONID")]),e._v("cookie 的值。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/demo")\nfun handle(@CookieValue("JSESSIONID") cookie: String) { (1)\n //...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("获取"),r("code",[e._v("JSESSIONID")]),e._v("cookie 的值。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果目标方法参数类型不是"),r("code",[e._v("String")]),e._v(",则自动应用类型转换。见"),r("a",{attrs:{href:"#mvc-ann-typeconversion"}},[e._v("类型转换")]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"modelattribute"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#modelattribute"}},[e._v("#")]),e._v(" "),r("code",[e._v("@ModelAttribute")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-modelattrib-method-args"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以在方法参数上使用"),r("code",[e._v("@ModelAttribute")]),e._v("注释来访问模型中的一个属性,或者如果不存在该属性,则将其实例化。模型属性还覆盖了来自 HTTP Servlet 请求参数的值,这些参数的名称与字段名匹配。这被称为数据绑定,它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\npublic String processSubmit(@ModelAttribute Pet pet) {\n // method logic...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\nfun processSubmit(@ModelAttribute pet: Pet): String {\n // method logic...\n}\n')])])]),r("p",[e._v("上面的"),r("code",[e._v("Pet")]),e._v("实例的来源如下:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("从可能由"),r("a",{attrs:{href:"#mvc-ann-modelattrib-methods"}},[e._v("@ModelAttribute 方法")]),e._v("添加的模型中检索。")])]),e._v(" "),r("li",[r("p",[e._v("如果模型属性在类级["),r("code",[e._v("@SessionAttributes")]),e._v("](#mvc-ann-sessionAttributes)注释中列出,则从 HTTP 会话中检索。")])]),e._v(" "),r("li",[r("p",[e._v("通过"),r("code",[e._v("Converter")]),e._v("获得,其中模型属性名与请求值(如路径变量或请求参数)的名称匹配(参见下一个示例)。")])]),e._v(" "),r("li",[r("p",[e._v("使用其默认构造函数实例化。")])]),e._v(" "),r("li",[r("p",[e._v("通过具有与 Servlet 请求参数匹配的参数的“主构造函数”实例化。参数名称是通过 爪哇Beans"),r("code",[e._v("@ConstructorProperties")]),e._v("或通过字节码中的运行时保留参数名称确定的。")])])]),e._v(" "),r("p",[e._v("除了使用"),r("a",{attrs:{href:"#mvc-ann-modelattrib-methods"}},[e._v("@ModelAttribute 方法")]),e._v("来提供它,或者依靠框架来创建模型属性之外,另一种选择是使用"),r("code",[e._v("Converter")]),e._v("来提供实例。当模型属性名称与请求值(例如路径变量或请求参数)的名称匹配时,并且存在"),r("code",[e._v("Converter")]),e._v("到模型属性类型的"),r("code",[e._v("String")]),e._v("时,将应用此方法。在下面的示例中,模型属性名为"),r("code",[e._v("account")]),e._v(",它与 URI 路径变量"),r("code",[e._v("account")]),e._v("匹配,并且有一个已注册的"),r("code",[e._v("Converter")]),e._v(",它可以从数据存储加载"),r("code",[e._v("Account")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PutMapping("/accounts/{account}")\npublic String save(@ModelAttribute("account") Account account) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PutMapping("/accounts/{account}")\nfun save(@ModelAttribute("account") account: Account): String {\n // ...\n}\n')])])]),r("p",[e._v("在获得模型属性实例之后,再进行数据绑定。"),r("code",[e._v("WebDataBinder")]),e._v("类将 Servlet 请求参数名称(查询参数和窗体字段)与目标"),r("code",[e._v("Object")]),e._v("上的字段名称匹配。在应用了类型转换之后(如有必要)填充匹配字段。有关数据绑定(和验证)的更多信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation"}},[e._v("验证")]),e._v("。有关自定义数据绑定的更多信息,请参见["),r("code",[e._v("DataBinder")]),e._v("]。")],1),e._v(" "),r("p",[e._v("数据绑定可能会导致错误。默认情况下,将引发"),r("code",[e._v("BindException")]),e._v("。但是,要检查控制器方法中的此类错误,可以在"),r("code",[e._v("@ModelAttribute")]),e._v("旁边立即添加一个"),r("code",[e._v("BindingResult")]),e._v("参数,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\npublic String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)\n if (result.hasErrors()) {\n return "petForm";\n }\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在"),r("code",[e._v("@ModelAttribute")]),e._v("旁边添加一个"),r("code",[e._v("BindingResult")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\nfun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)\n if (result.hasErrors()) {\n return "petForm"\n }\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在"),r("code",[e._v("@ModelAttribute")]),e._v("旁边添加一个"),r("code",[e._v("BindingResult")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在某些情况下,你可能希望访问没有数据绑定的模型属性。对于这种情况,你可以将"),r("code",[e._v("Model")]),e._v("注入控制器并直接访问它,或者设置"),r("code",[e._v("@ModelAttribute(binding=false)")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@ModelAttribute\npublic AccountForm setUpForm() {\n return new AccountForm();\n}\n\n@ModelAttribute\npublic Account findAccount(@PathVariable String accountId) {\n return accountRepository.findOne(accountId);\n}\n\n@PostMapping("update")\npublic String update(@Valid AccountForm form, BindingResult result,\n @ModelAttribute(binding=false) Account account) { (1)\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("设置"),r("code",[e._v("@ModelAttribute(binding=false)")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@ModelAttribute\nfun setUpForm(): AccountForm {\n return AccountForm()\n}\n\n@ModelAttribute\nfun findAccount(@PathVariable accountId: String): Account {\n return accountRepository.findOne(accountId)\n}\n\n@PostMapping("update")\nfun update(@Valid form: AccountForm, result: BindingResult,\n @ModelAttribute(binding = false) account: Account): String { (1)\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("设置"),r("code",[e._v("@ModelAttribute(binding=false)")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("通过添加"),r("code",[e._v("javax.validation.Valid")]),e._v("注释或 Spring 的"),r("code",[e._v("@Validated")]),e._v("注释("),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation-beanvalidation"}},[e._v("Bean Validation")]),e._v("和"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation"}},[e._v("Spring validation")]),e._v("),可以在数据绑定后自动应用验证。下面的示例展示了如何做到这一点:")],1),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\npublic String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)\n if (result.hasErrors()) {\n return "petForm";\n }\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("验证"),r("code",[e._v("Pet")]),e._v("实例。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/owners/{ownerId}/pets/{petId}/edit")\nfun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)\n if (result.hasErrors()) {\n return "petForm"\n }\n // ...\n}\n')])])]),r("p",[e._v("请注意,使用"),r("code",[e._v("@ModelAttribute")]),e._v("是可选的(例如,用于设置其属性)。默认情况下,任何不是简单值类型(由"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Beanutils#IsSimpleProperty"),r("OutboundLink")],1),e._v("确定)且未由任何其他参数解析器解析的参数都将被视为已用"),r("code",[e._v("@ModelAttribute")]),e._v("注释。")]),e._v(" "),r("h5",{attrs:{id:"sessionattributes"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#sessionattributes"}},[e._v("#")]),e._v(" "),r("code",[e._v("@SessionAttributes")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-sessionattributes"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@SessionAttributes")]),e._v("用于在请求之间的 HTTP Servlet 会话中存储模型属性。它是一种类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以供后续的访问请求使用。")]),e._v(" "),r("p",[e._v("下面的示例使用"),r("code",[e._v("@SessionAttributes")]),e._v("注释:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@SessionAttributes("pet") (1)\npublic class EditPetForm {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@SessionAttributes")]),e._v("注释。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@SessionAttributes("pet") (1)\nclass EditPetForm {\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@SessionAttributes")]),e._v("注释。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在第一个请求中,当将名称为"),r("code",[e._v("pet")]),e._v("的模型属性添加到模型中时,将自动将其提升到 HTTP Servlet 会话中并将其保存。在另一个控制器方法使用"),r("code",[e._v("SessionStatus")]),e._v("方法参数清除存储之前,它一直保持不变,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@SessionAttributes("pet") (1)\npublic class EditPetForm {\n\n // ...\n\n @PostMapping("/pets/{id}")\n public String handle(Pet pet, BindingResult errors, SessionStatus status) {\n if (errors.hasErrors) {\n // ...\n }\n status.setComplete(); (2)\n // ...\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在 Servlet 会话中存储"),r("code",[e._v("Pet")]),e._v("值。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("清除 Servlet 会话中的"),r("code",[e._v("Pet")]),e._v("值。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@SessionAttributes("pet") (1)\nclass EditPetForm {\n\n // ...\n\n @PostMapping("/pets/{id}")\n fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String {\n if (errors.hasErrors()) {\n // ...\n }\n status.setComplete() (2)\n // ...\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在 Servlet 会话中存储"),r("code",[e._v("Pet")]),e._v("值。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("清除 Servlet 会话中的"),r("code",[e._v("Pet")]),e._v("值。")])])])]),e._v(" "),r("h5",{attrs:{id:"sessionattribute"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#sessionattribute"}},[e._v("#")]),e._v(" "),r("code",[e._v("@SessionAttribute")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-sessionattribute"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("如果你需要访问已存在的会话属性,这些属性是全局管理的(也就是说,在控制器之外——例如,由过滤器管理),并且可能存在,也可能不存在,那么你可以在方法参数上使用"),r("code",[e._v("@SessionAttribute")]),e._v("注释,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping("/")\npublic String handle(@SessionAttribute User user) { (1)\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@SessionAttribute")]),e._v("注释。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping("/")\nfun handle(@SessionAttribute user: User): String { (1)\n // ...\n}\n')])])]),r("p",[e._v("对于需要添加或删除会话属性的用例,可以考虑将"),r("code",[e._v("org.springframework.web.context.request.WebRequest")]),e._v("或"),r("code",[e._v("javax.servlet.http.HttpSession")]),e._v("注入到控制器方法中。")]),e._v(" "),r("p",[e._v("对于将会话中的模型属性临时存储为控制器工作流的一部分,可以考虑使用"),r("code",[e._v("@SessionAttributes")]),e._v(",如["),r("code",[e._v("@SessionAttributes")]),e._v("]中所述。")]),e._v(" "),r("h5",{attrs:{id:"requestattribute"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#requestattribute"}},[e._v("#")]),e._v(" "),r("code",[e._v("@RequestAttribute")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestattrib"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("与"),r("code",[e._v("@SessionAttribute")]),e._v("类似,你可以使用"),r("code",[e._v("@RequestAttribute")]),e._v("注释来访问先前创建的预先存在的请求属性(例如,通过 Servlet "),r("code",[e._v("Filter")]),e._v("或"),r("code",[e._v("HandlerInterceptor")]),e._v("):")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/")\npublic String handle(@RequestAttribute Client client) { (1)\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@RequestAttribute")]),e._v("注释。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/")\nfun handle(@RequestAttribute client: Client): String { (1)\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用"),r("code",[e._v("@RequestAttribute")]),e._v("注释。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"重定向属性"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#重定向属性"}},[e._v("#")]),e._v(" 重定向属性")]),e._v(" "),r("p",[e._v("默认情况下,所有模型属性都被认为是作为重定向 URL 中的 URI 模板变量公开的。在剩余的属性中,那些是基元类型或基元类型的集合或数组的属性将自动附加为查询参数。")]),e._v(" "),r("p",[e._v("如果模型实例是专门为重定向准备的,那么将原始类型属性追加为查询参数可能是期望的结果。然而,在带注释的控制器中,模型可以包含为呈现目的而添加的附加属性(例如,下拉字段值)。为了避免这种属性出现在 URL 中的可能性,"),r("code",[e._v("@RequestMapping")]),e._v("方法可以声明类型"),r("code",[e._v("RedirectAttributes")]),e._v("的参数,并使用它来指定使"),r("code",[e._v("RedirectView")]),e._v("可用的确切属性。如果方法确实重定向,则使用"),r("code",[e._v("RedirectAttributes")]),e._v("的内容。否则,将使用模型的内容。")]),e._v(" "),r("p",[r("code",[e._v("RequestMappingHandlerAdapter")]),e._v("提供了一个名为"),r("code",[e._v("ignoreDefaultModelOnRedirect")]),e._v("的标志,你可以使用它来指示,如果控制器方法重定向,则不应使用默认的"),r("code",[e._v("Model")]),e._v("的内容。相反,Controller 方法应该声明一个类型为"),r("code",[e._v("RedirectAttributes")]),e._v("的属性,或者,如果不这样做,则不应该将任何属性传递到"),r("code",[e._v("RedirectView")]),e._v("。MVC 名称空间和 MVC 爪哇 配置都将此标志设置为"),r("code",[e._v("false")]),e._v(",以保持向后兼容性。但是,对于新的应用程序,我们建议将其设置为"),r("code",[e._v("true")]),e._v("。")]),e._v(" "),r("p",[e._v("请注意,当展开重定向 URL 时,当前请求中的 URI 模板变量将自动可用,并且你不需要通过"),r("code",[e._v("Model")]),e._v("或"),r("code",[e._v("RedirectAttributes")]),e._v("显式地添加它们。下面的示例展示了如何定义重定向:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/files/{path}")\npublic String upload(...) {\n // ...\n return "redirect:files/{path}";\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/files/{path}")\nfun upload(...): String {\n // ...\n return "redirect:files/{path}"\n}\n')])])]),r("p",[e._v("另一种将数据传递给重定向目标的方法是使用 flash 属性。与其他重定向属性不同,flash 属性保存在 HTTP 会话中(因此不会出现在 URL 中)。有关更多信息,请参见"),r("a",{attrs:{href:"#mvc-flash-attributes"}},[e._v("flash 属性")]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"flash-属性"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#flash-属性"}},[e._v("#")]),e._v(" flash 属性")]),e._v(" "),r("p",[e._v("Flash 属性为一个请求提供了一种方式,用于存储打算在另一个请求中使用的属性。这是重定向时最常见的需求——例如,后重定向-get 模式。在重定向之前(通常是在会话中)临时保存 flash 属性,以便在重定向之后使请求可用,并立即删除。")]),e._v(" "),r("p",[e._v("Spring MVC 有两个主要的抽象来支持 Flash 属性。"),r("code",[e._v("FlashMap")]),e._v("用于保存 flash 属性,而"),r("code",[e._v("FlashMapManager")]),e._v("用于存储、检索和管理"),r("code",[e._v("FlashMap")]),e._v("实例。")]),e._v(" "),r("p",[e._v("flash 属性支持始终是“打开”的,并且不需要显式地启用。但是,如果不使用,它永远不会导致 HTTP 会话的创建。在每个请求中,都有一个“input”"),r("code",[e._v("FlashMap")]),e._v(",其中包含从上一个请求(如果有的话)传递的属性,以及一个“output”"),r("code",[e._v("FlashMap")]),e._v(",其中包含用于保存后续请求的属性。这两个"),r("code",[e._v("FlashMap")]),e._v("实例都可以通过"),r("code",[e._v("RequestContextUtils")]),e._v("中的静态方法从 Spring MVC 中的任何地方访问。")]),e._v(" "),r("p",[e._v("带注释的控制器通常不需要直接使用"),r("code",[e._v("FlashMap")]),e._v("。相反,"),r("code",[e._v("@RequestMapping")]),e._v("方法可以接受类型为"),r("code",[e._v("RedirectAttributes")]),e._v("的参数,并使用它为重定向场景添加 flash 属性。通过"),r("code",[e._v("RedirectAttributes")]),e._v("添加的 flash 属性将自动传播到“输出”flashmap。类似地,在重定向之后,来自“input”"),r("code",[e._v("FlashMap")]),e._v("的属性将自动添加到提供目标 URL 的控制器的"),r("code",[e._v("Model")]),e._v("中。")]),e._v(" "),r("p",[e._v("将请求与 flash 属性匹配")]),e._v(" "),r("p",[e._v("flash 属性的概念存在于许多其他 Web 框架中,并已被证明有时会遇到并发问题。这是因为,根据定义,flash 属性将被存储到下一个请求为止。然而,“下一个”请求可能不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,flash 属性被过早地删除。")]),e._v(" "),r("p",[e._v("为了减少此类问题的可能性,"),r("code",[e._v("RedirectView")]),e._v("自动“stamps”"),r("code",[e._v("FlashMap")]),e._v("实例具有重定向 URL 的路径和查询参数的目标。反过来,当它查找“input”"),r("code",[e._v("FlashMap")]),e._v("时,默认的"),r("code",[e._v("FlashMapManager")]),e._v("将该信息与传入请求匹配。")]),e._v(" "),r("p",[e._v("这并不能完全消除并发问题的可能性,但可以通过重定向 URL 中已有的信息大大减少并发问题。因此,我们建议你主要在重定向场景中使用 Flash 属性。")]),e._v(" "),r("h5",{attrs:{id:"多部分"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#多部分"}},[e._v("#")]),e._v(" 多部分")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-multipart-forms"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在"),r("code",[e._v("MultipartResolver")]),e._v("已"),r("a",{attrs:{href:"#mvc-multipart"}},[e._v("enabled")]),e._v("之后,对带有"),r("code",[e._v("multipart/form-data")]),e._v("的 POST 请求的内容进行解析,并将其作为常规的请求参数进行访问。以下示例访问一个常规窗体字段和一个上载文件:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class FileUploadController {\n\n @PostMapping("/form")\n public String handleFormUpload(@RequestParam("name") String name,\n @RequestParam("file") MultipartFile file) {\n\n if (!file.isEmpty()) {\n byte[] bytes = file.getBytes();\n // store the bytes somewhere\n return "redirect:uploadSuccess";\n }\n return "redirect:uploadFailure";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\nclass FileUploadController {\n\n @PostMapping("/form")\n fun handleFormUpload(@RequestParam("name") name: String,\n @RequestParam("file") file: MultipartFile): String {\n\n if (!file.isEmpty) {\n val bytes = file.bytes\n // store the bytes somewhere\n return "redirect:uploadSuccess"\n }\n return "redirect:uploadFailure"\n }\n}\n')])])]),r("p",[e._v("将参数类型声明为"),r("code",[e._v("List")]),e._v("允许为相同的参数名称解析多个文件。")]),e._v(" "),r("p",[e._v("当"),r("code",[e._v("@RequestParam")]),e._v("注释被声明为"),r("code",[e._v("Map")]),e._v("或"),r("code",[e._v("MultiValueMap")]),e._v("时,如果注释中没有指定参数名称,那么映射将被填充每个给定参数名称的多部分文件。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("通过 Servlet 3.0 多部分解析,你还可以声明"),r("code",[e._v("javax.servlet.http.Part")]),e._v("而不是 Spring 的"),r("code",[e._v("MultipartFile")]),e._v(",作为方法参数或集合值类型。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("还可以使用多部分内容作为与"),r("a",{attrs:{href:"#mvc-ann-modelattrib-method-args"}},[e._v("命令对象")]),e._v("的数据绑定的一部分。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyForm {\n\n private String name;\n\n private MultipartFile file;\n\n // ...\n}\n\n@Controller\npublic class FileUploadController {\n\n @PostMapping("/form")\n public String handleFormUpload(MyForm form, BindingResult errors) {\n if (!form.getFile().isEmpty()) {\n byte[] bytes = form.getFile().getBytes();\n // store the bytes somewhere\n return "redirect:uploadSuccess";\n }\n return "redirect:uploadFailure";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class MyForm(val name: String, val file: MultipartFile, ...)\n\n@Controller\nclass FileUploadController {\n\n @PostMapping("/form")\n fun handleFormUpload(form: MyForm, errors: BindingResult): String {\n if (!form.file.isEmpty) {\n val bytes = form.file.bytes\n // store the bytes somewhere\n return "redirect:uploadSuccess"\n }\n return "redirect:uploadFailure"\n }\n}\n')])])]),r("p",[e._v("在 RESTful 服务场景中,还可以从非浏览器客户端提交多部分请求。下面的示例显示了一个使用 JSON 的文件:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('POST /someUrl\nContent-Type: multipart/mixed\n\n--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp\nContent-Disposition: form-data; name="meta-data"\nContent-Type: application/json; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n{\n "name": "value"\n}\n--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp\nContent-Disposition: form-data; name="file-data"; filename="file.properties"\nContent-Type: text/xml\nContent-Transfer-Encoding: 8bit\n... File Data ...\n')])])]),r("p",[e._v("你可以使用"),r("code",[e._v("@RequestParam")]),e._v("作为"),r("code",[e._v("String")]),e._v("访问“元数据”部分,但你可能希望它从 JSON 反序列化(类似于"),r("code",[e._v("@RequestBody")]),e._v(")。在使用"),r("RouterLink",{attrs:{to:"/spring-framework/integration.html#rest-message-conversion"}},[e._v("HtpMessageConverter")]),e._v("转换多个部分后,使用"),r("code",[e._v("@RequestPart")]),e._v("注释访问多个部分:")],1),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/")\npublic String handle(@RequestPart("meta-data") MetaData metadata,\n @RequestPart("file-data") MultipartFile file) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/")\nfun handle(@RequestPart("meta-data") metadata: MetaData,\n @RequestPart("file-data") file: MultipartFile): String {\n // ...\n}\n')])])]),r("p",[e._v("你可以将"),r("code",[e._v("@RequestPart")]),e._v("与"),r("code",[e._v("javax.validation.Valid")]),e._v("组合使用,或者使用 Spring 的"),r("code",[e._v("@Validated")]),e._v("注释,这两种方法都会导致应用标准 Bean 验证。默认情况下,验证错误会导致"),r("code",[e._v("MethodArgumentNotValidException")]),e._v(",并将其转换为 400(bad_request)响应。或者,你可以通过"),r("code",[e._v("Errors")]),e._v("或"),r("code",[e._v("BindingResult")]),e._v("参数在控制器内部本地处理验证错误,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/")\npublic String handle(@Valid @RequestPart("meta-data") MetaData metadata,\n BindingResult result) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/")\nfun handle(@Valid @RequestPart("meta-data") metadata: MetaData,\n result: BindingResult): String {\n // ...\n}\n')])])]),r("h5",{attrs:{id:"requestbody"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#requestbody"}},[e._v("#")]),e._v(" "),r("code",[e._v("@RequestBody")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-requestbody"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("@RequestBody")]),e._v("注释,通过["),r("code",[e._v("HttpMessageConverter")]),e._v("](integration.html#rest-message-conversion)将请求主体读取并反序列化为"),r("code",[e._v("Object")]),e._v("。下面的示例使用了"),r("code",[e._v("@RequestBody")]),e._v("参数:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\npublic void handle(@RequestBody Account account) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\nfun handle(@RequestBody account: Account) {\n // ...\n}\n')])])]),r("p",[e._v("你可以使用"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("的"),r("a",{attrs:{href:"#mvc-config-message-converters"}},[e._v("消息转换器")]),e._v("选项来配置或自定义消息转换。")]),e._v(" "),r("p",[e._v("你可以将"),r("code",[e._v("@RequestBody")]),e._v("与"),r("code",[e._v("javax.validation.Valid")]),e._v("或 Spring 的"),r("code",[e._v("@Validated")]),e._v("注释结合使用,这两种方法都会导致应用标准 Bean 验证。默认情况下,验证错误会导致"),r("code",[e._v("MethodArgumentNotValidException")]),e._v(",并将其转换为 400(bad_request)响应。或者,你可以通过"),r("code",[e._v("Errors")]),e._v("或"),r("code",[e._v("BindingResult")]),e._v("参数在控制器内部本地处理验证错误,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\npublic void handle(@Valid @RequestBody Account account, BindingResult result) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\nfun handle(@Valid @RequestBody account: Account, result: BindingResult) {\n // ...\n}\n')])])]),r("h5",{attrs:{id:"httpentity"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#httpentity"}},[e._v("#")]),e._v(" HttpEntity")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-httpentity"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("HttpEntity")]),e._v("与使用["),r("code",[e._v("@RequestBody")]),e._v("](#mvc-ann-requestBody)或多或少相同,但基于一个容器对象,该对象公开了请求头和主体。下面的清单展示了一个示例:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\npublic void handle(HttpEntity entity) {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping("/accounts")\nfun handle(entity: HttpEntity) {\n // ...\n}\n')])])]),r("h5",{attrs:{id:"responsebody"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#responsebody"}},[e._v("#")]),e._v(" "),r("code",[e._v("@ResponseBody")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-responsebody"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以在方法上使用"),r("code",[e._v("@ResponseBody")]),e._v("注释,通过"),r("RouterLink",{attrs:{to:"/spring-framework/integration.html#rest-message-conversion"}},[e._v("HtpMessageConverter")]),e._v("将返回序列化到响应主体。下面的清单展示了一个示例:")],1),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/accounts/{id}")\n@ResponseBody\npublic Account handle() {\n // ...\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/accounts/{id}")\n@ResponseBody\nfun handle(): Account {\n // ...\n}\n')])])]),r("p",[r("code",[e._v("@ResponseBody")]),e._v("在类级别上也受到支持,在这种情况下,所有控制器方法都会继承它。这是"),r("code",[e._v("@RestController")]),e._v("的效果,它不过是一个标记有"),r("code",[e._v("@Controller")]),e._v("和"),r("code",[e._v("@ResponseBody")]),e._v("的元注释。")]),e._v(" "),r("p",[e._v("你可以将"),r("code",[e._v("@ResponseBody")]),e._v("用于反应类型。有关更多详细信息,请参见"),r("a",{attrs:{href:"#mvc-ann-async"}},[e._v("异步请求")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应类型")]),e._v("。")]),e._v(" "),r("p",[e._v("你可以使用"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("的"),r("a",{attrs:{href:"#mvc-config-message-converters"}},[e._v("消息转换器")]),e._v("选项来配置或自定义消息转换。")]),e._v(" "),r("p",[e._v("你可以将"),r("code",[e._v("@ResponseBody")]),e._v("方法与 JSON 序列化视图合并。详见"),r("a",{attrs:{href:"#mvc-ann-jackson"}},[e._v("JacksonJSON")]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"负责实体"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#负责实体"}},[e._v("#")]),e._v(" 负责实体")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-responseentity"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("ResponseEntity")]),e._v("类似于["),r("code",[e._v("@ResponseBody")]),e._v("](#mvc-ann-responsebody),但具有状态和标题。例如:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/something")\npublic ResponseEntity handle() {\n String body = ... ;\n String etag = ... ;\n return ResponseEntity.ok().eTag(etag).build(body);\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/something")\nfun handle(): ResponseEntity {\n val body = ...\n val etag = ...\n return ResponseEntity.ok().eTag(etag).build(body)\n}\n')])])]),r("p",[e._v("Spring MVC 支持使用单个值"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应型")]),e._v("来异步地产生"),r("code",[e._v("ResponseEntity")]),e._v(",和/或用于主体的单个值和多个值的反应类型。这允许以下类型的异步响应:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("ResponseEntity>")]),e._v("或"),r("code",[e._v("ResponseEntity>")]),e._v("在稍后异步提供主体时,立即使响应状态和头为已知。如果主体由 0.1 个值组成,则使用"),r("code",[e._v("Mono")]),e._v(";如果可以产生多个值,则使用"),r("code",[e._v("Flux")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Mono>")]),e._v("在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。")])])]),e._v(" "),r("h5",{attrs:{id:"jacksonjson"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#jacksonjson"}},[e._v("#")]),e._v(" JacksonJSON")]),e._v(" "),r("p",[e._v("Spring 提供对 JacksonJSON 库的支持。")]),e._v(" "),r("h6",{attrs:{id:"json-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#json-视图"}},[e._v("#")]),e._v(" JSON 视图")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-jsonview"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 提供了对"),r("a",{attrs:{href:"https://www.baeldung.com/jackson-json-view-annotation",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jackson 的序列化视图"),r("OutboundLink")],1),e._v("的内置支持,其仅允许在"),r("code",[e._v("Object")]),e._v("中呈现所有字段的子集。要与"),r("code",[e._v("@ResponseBody")]),e._v("或"),r("code",[e._v("ResponseEntity")]),e._v("控制器方法一起使用它,你可以使用 Jackson 的"),r("code",[e._v("@JsonView")]),e._v("注释来激活序列化视图类,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\npublic class UserController {\n\n @GetMapping("/user")\n @JsonView(User.WithoutPasswordView.class)\n public User getUser() {\n return new User("eric", "7!jd#h23");\n }\n}\n\npublic class User {\n\n public interface WithoutPasswordView {};\n public interface WithPasswordView extends WithoutPasswordView {};\n\n private String username;\n private String password;\n\n public User() {\n }\n\n public User(String username, String password) {\n this.username = username;\n this.password = password;\n }\n\n @JsonView(WithoutPasswordView.class)\n public String getUsername() {\n return this.username;\n }\n\n @JsonView(WithPasswordView.class)\n public String getPassword() {\n return this.password;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\nclass UserController {\n\n @GetMapping("/user")\n @JsonView(User.WithoutPasswordView::class)\n fun getUser() = User("eric", "7!jd#h23")\n}\n\nclass User(\n @JsonView(WithoutPasswordView::class) val username: String,\n @JsonView(WithPasswordView::class) val password: String) {\n\n interface WithoutPasswordView\n interface WithPasswordView : WithoutPasswordView\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("code",[e._v("@JsonView")]),e._v("允许一个视图类的数组,但是每个"),r("br"),e._v("控制器方法只能指定一个。如果需要激活多个视图,可以使用复合接口。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果你希望以编程方式执行上述操作,而不是声明"),r("code",[e._v("@JsonView")]),e._v("注释,请将返回值包装为"),r("code",[e._v("MappingJacksonValue")]),e._v(",并使用它来提供序列化视图:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\npublic class UserController {\n\n @GetMapping("/user")\n public MappingJacksonValue getUser() {\n User user = new User("eric", "7!jd#h23");\n MappingJacksonValue value = new MappingJacksonValue(user);\n value.setSerializationView(User.WithoutPasswordView.class);\n return value;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\nclass UserController {\n\n @GetMapping("/user")\n fun getUser(): MappingJacksonValue {\n val value = MappingJacksonValue(User("eric", "7!jd#h23"))\n value.serializationView = User.WithoutPasswordView::class.java\n return value\n }\n}\n')])])]),r("p",[e._v("对于依赖于视图分辨率的控制器,可以将序列化视图类添加到模型中,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class UserController extends AbstractController {\n\n @GetMapping("/user")\n public String getUser(Model model) {\n model.addAttribute("user", new User("eric", "7!jd#h23"));\n model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class);\n return "userView";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.ui.set\n\n@Controller\nclass UserController : AbstractController() {\n\n @GetMapping("/user")\n fun getUser(model: Model): String {\n model["user"] = User("eric", "7!jd#h23")\n model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java\n return "userView"\n }\n}\n')])])]),r("h4",{attrs:{id:"_1-3-4-模型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-4-模型"}},[e._v("#")]),e._v(" 1.3.4.模型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-modelattrib-methods"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("@ModelAttribute")]),e._v("注释:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("在"),r("a",{attrs:{href:"#mvc-ann-modelattrib-method-args"}},[e._v("方法参数")]),e._v("中的"),r("code",[e._v("@RequestMapping")]),e._v("方法上从模型创建或访问"),r("code",[e._v("Object")]),e._v(",并通过"),r("code",[e._v("WebDataBinder")]),e._v("将其绑定到请求。")])]),e._v(" "),r("li",[r("p",[e._v("作为"),r("code",[e._v("@Controller")]),e._v("或"),r("code",[e._v("@ControllerAdvice")]),e._v("类中的方法级注释,它有助于在任何"),r("code",[e._v("@RequestMapping")]),e._v("方法调用之前初始化模型。")])]),e._v(" "),r("li",[r("p",[e._v("在"),r("code",[e._v("@RequestMapping")]),e._v("方法上标记其返回值是一个模型属性。")])])]),e._v(" "),r("p",[e._v("本节讨论"),r("code",[e._v("@ModelAttribute")]),e._v("方法——前面列表中的第二项。控制器可以有任意数量的"),r("code",[e._v("@ModelAttribute")]),e._v("方法。所有这些方法都是在同一个控制器中的"),r("code",[e._v("@RequestMapping")]),e._v("方法之前调用的。还可以通过"),r("code",[e._v("@ControllerAdvice")]),e._v("在控制器之间共享"),r("code",[e._v("@ModelAttribute")]),e._v("方法。有关更多详细信息,请参见"),r("a",{attrs:{href:"#mvc-ann-controller-advice"}},[e._v("财务总监建议")]),e._v("一节。")]),e._v(" "),r("p",[r("code",[e._v("@ModelAttribute")]),e._v("方法具有灵活的方法签名。它们支持许多与"),r("code",[e._v("@RequestMapping")]),e._v("方法相同的参数,但"),r("code",[e._v("@ModelAttribute")]),e._v("本身或与请求主体相关的任何参数除外。")]),e._v(" "),r("p",[e._v("下面的示例显示了"),r("code",[e._v("@ModelAttribute")]),e._v("方法:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ModelAttribute\npublic void populateModel(@RequestParam String number, Model model) {\n model.addAttribute(accountRepository.findAccount(number));\n // add more ...\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ModelAttribute\nfun populateModel(@RequestParam number: String, model: Model) {\n model.addAttribute(accountRepository.findAccount(number))\n // add more ...\n}\n")])])]),r("p",[e._v("下面的示例只添加了一个属性:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ModelAttribute\npublic Account addAccount(@RequestParam String number) {\n return accountRepository.findAccount(number);\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ModelAttribute\nfun addAccount(@RequestParam number: String): Account {\n return accountRepository.findAccount(number)\n}\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当未显式指定名称时,将根据"),r("code",[e._v("Object")]),e._v("类型选择缺省名称,正如在 爪哇doc 中对["),r("code",[e._v("Conventions")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/conventions.html)所解释的那样。"),r("br"),e._v("通过"),r("code",[e._v("name")]),e._v("上的"),r("code",[e._v("name")]),e._v("属性“/>(用于返回值),始终可以使用重载的"),r("code",[e._v("addAttribute")]),e._v("方法或"),r("br"),e._v("指定一个显式名称。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("你也可以使用"),r("code",[e._v("@ModelAttribute")]),e._v("作为"),r("code",[e._v("@RequestMapping")]),e._v("方法的方法级注释,在这种情况下,"),r("code",[e._v("@RequestMapping")]),e._v("方法的返回值被解释为一个模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是"),r("code",[e._v("String")]),e._v(",否则将被解释为视图名称。"),r("code",[e._v("@ModelAttribute")]),e._v("还可以自定义模型属性名称,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/accounts/{id}")\n@ModelAttribute("myAccount")\npublic Account handle() {\n // ...\n return account;\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/accounts/{id}")\n@ModelAttribute("myAccount")\nfun handle(): Account {\n // ...\n return account\n}\n')])])]),r("h4",{attrs:{id:"_1-3-5-databinder"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-5-databinder"}},[e._v("#")]),e._v(" 1.3.5."),r("code",[e._v("DataBinder")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-initbinder"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@Controller")]),e._v("或"),r("code",[e._v("@ControllerAdvice")]),e._v("类可以具有初始化"),r("code",[e._v("@InitBinder")]),e._v("实例的"),r("code",[e._v("@InitBinder")]),e._v("方法,而这些方法可以:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("将请求参数(即窗体或查询数据)绑定到模型对象。")])]),e._v(" "),r("li",[r("p",[e._v("将基于字符串的请求值(例如请求参数、路径变量、头、cookie 和其他)转换为控制器方法参数的目标类型。")])]),e._v(" "),r("li",[r("p",[e._v("在呈现 HTML 窗体时,将模型对象值格式化为"),r("code",[e._v("String")]),e._v("值。")])])]),e._v(" "),r("p",[r("code",[e._v("@InitBinder")]),e._v("方法可以注册控制器特定的"),r("code",[e._v("java.beans.PropertyEditor")]),e._v("或 Spring "),r("code",[e._v("Converter")]),e._v("和"),r("code",[e._v("Formatter")]),e._v("组件。此外,可以使用"),r("a",{attrs:{href:"#mvc-config-conversion"}},[e._v("MVC config")]),e._v("在全局共享的"),r("code",[e._v("FormattingConversionService")]),e._v("中注册"),r("code",[e._v("Converter")]),e._v("和"),r("code",[e._v("Formatter")]),e._v("类型。")]),e._v(" "),r("p",[r("code",[e._v("@InitBinder")]),e._v("方法支持许多与"),r("code",[e._v("@RequestMapping")]),e._v("方法相同的参数,但"),r("code",[e._v("@ModelAttribute")]),e._v("(命令对象)参数除外。通常,它们是用"),r("code",[e._v("WebDataBinder")]),e._v("参数(用于注册)和"),r("code",[e._v("void")]),e._v("返回值声明的。下面的清单展示了一个示例:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class FormController {\n\n @InitBinder (1)\n public void initBinder(WebDataBinder binder) {\n SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");\n dateFormat.setLenient(false);\n binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));\n }\n\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("定义"),r("code",[e._v("@InitBinder")]),e._v("方法。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\nclass FormController {\n\n @InitBinder (1)\n fun initBinder(binder: WebDataBinder) {\n val dateFormat = SimpleDateFormat("yyyy-MM-dd")\n dateFormat.isLenient = false\n binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))\n }\n\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("定义"),r("code",[e._v("@InitBinder")]),e._v("方法。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("或者,当你通过共享的"),r("code",[e._v("FormattingConversionService")]),e._v("使用基于"),r("code",[e._v("Formatter")]),e._v("的设置时,你可以重新使用相同的方法并注册特定于控制器的"),r("code",[e._v("Formatter")]),e._v("实现,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class FormController {\n\n @InitBinder (1)\n protected void initBinder(WebDataBinder binder) {\n binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));\n }\n\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在自定义格式化程序上定义"),r("code",[e._v("@InitBinder")]),e._v("方法。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\nclass FormController {\n\n @InitBinder (1)\n protected fun initBinder(binder: WebDataBinder) {\n binder.addCustomFormatter(DateFormatter("yyyy-MM-dd"))\n }\n\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("在自定义格式化程序上定义"),r("code",[e._v("@InitBinder")]),e._v("方法。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-3-6-例外"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-6-例外"}},[e._v("#")]),e._v(" 1.3.6.例外")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-controller-exceptions"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@Controller")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-controller-advice"}},[e._v("@controlleradvice")]),e._v("类可以有"),r("code",[e._v("@ExceptionHandler")]),e._v("方法来处理来自控制器方法的异常,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Controller\npublic class SimpleController {\n\n // ...\n\n @ExceptionHandler\n public ResponseEntity handle(IOException ex) {\n // ...\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Controller\nclass SimpleController {\n\n // ...\n\n @ExceptionHandler\n fun handle(ex: IOException): ResponseEntity {\n // ...\n }\n}\n")])])]),r("p",[e._v("异常可能与正在传播的顶级异常(例如,直接的"),r("code",[e._v("IOException")]),e._v("被抛出)或包装异常中的嵌套原因(例如,"),r("code",[e._v("IOException")]),e._v("包装在"),r("code",[e._v("IllegalStateException")]),e._v("中)相匹配。截至 5.3,这可以匹配在任意的原因水平,而以前只考虑一个直接原因。")]),e._v(" "),r("p",[e._v("为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优于原因异常匹配。更具体地说,"),r("code",[e._v("ExceptionDepthComparator")]),e._v("用于根据抛出的异常类型的深度对异常进行排序。")]),e._v(" "),r("p",[e._v("或者,注释声明可以缩小异常类型的范围以进行匹配,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ExceptionHandler({FileSystemException.class, RemoteException.class})\npublic ResponseEntity handle(IOException ex) {\n // ...\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ExceptionHandler(FileSystemException::class, RemoteException::class)\nfun handle(ex: IOException): ResponseEntity {\n // ...\n}\n")])])]),r("p",[e._v("你甚至可以使用带有非常通用的参数签名的特定异常类型列表,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ExceptionHandler({FileSystemException.class, RemoteException.class})\npublic ResponseEntity handle(Exception ex) {\n // ...\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@ExceptionHandler(FileSystemException::class, RemoteException::class)\nfun handle(ex: Exception): ResponseEntity {\n // ...\n}\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("根和原因异常匹配之间的区别可能是令人惊讶的。"),r("br"),r("br"),e._v("在前面显示的"),r("code",[e._v("IOException")]),e._v("变体中,该方法通常以"),r("br"),e._v("实际的"),r("code",[e._v("FileSystemException")]),e._v("或"),r("code",[e._v("RemoteException")]),e._v("实例作为参数,"),r("br"),e._v("因为它们都从"),r("code",[e._v("IOException")]),e._v("扩展。但是,如果任何这样的匹配"),r("br"),e._v("异常是在包装异常中传播的,而包装异常本身是"),r("code",[e._v("IOException")]),e._v(","),r("br"),e._v("传递的异常实例是该包装异常。"),r("br"),r("br"),e._v("在"),r("code",[e._v("handle(Exception)")]),e._v("变体中,行为甚至更简单。在包装场景中,这是"),r("br"),e._v("总是与包装异常一起调用的,在这种情况下,"),r("br"),e._v("实际上匹配通过"),r("code",[e._v("ex.getCause()")]),e._v("发现的异常。"),r("br"),e._v("只有当这些作为顶级异常抛出时,传入的异常才是实际的"),r("code",[e._v("FileSystemException")]),e._v("或"),r("code",[e._v("RemoteException")]),e._v("实例。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("我们通常建议你在参数签名中尽可能地具体,以减少根异常类型和原因异常类型之间不匹配的可能性。考虑将多个匹配方法分解为单独的"),r("code",[e._v("@ExceptionHandler")]),e._v("方法,每个方法通过其签名匹配单个特定的异常类型。")]),e._v(" "),r("p",[e._v("在 multi-"),r("code",[e._v("@ControllerAdvice")]),e._v("安排中,我们建议在"),r("code",[e._v("@ControllerAdvice")]),e._v("上声明主根异常映射,并按相应的顺序进行优先排序。虽然根异常匹配比原因更受欢迎,但这是在给定控制器或"),r("code",[e._v("@ControllerAdvice")]),e._v("类的方法之间定义的。这意味着在较高优先级"),r("code",[e._v("@ControllerAdvice")]),e._v(" Bean 上的原因匹配要优于在较低优先级"),r("code",[e._v("@ControllerAdvice")]),e._v(" Bean 上的任何匹配(例如,根)。")]),e._v(" "),r("p",[e._v("最后但并非最不重要的一点是,"),r("code",[e._v("@ExceptionHandler")]),e._v("方法实现可以通过以其原始形式重抛给定的异常实例来选择退出处理该异常实例。这在只对根级别匹配感兴趣或对无法静态确定的特定上下文中的匹配感兴趣的场景中很有用。一个重新抛出的异常将通过剩余的解析链传播,就好像给定的"),r("code",[e._v("@ExceptionHandler")]),e._v("方法首先不会匹配一样。")]),e._v(" "),r("p",[e._v("Spring MVC 中对"),r("code",[e._v("@ExceptionHandler")]),e._v("方法的支持是建立在"),r("code",[e._v("DispatcherServlet")]),e._v("级别的"),r("a",{attrs:{href:"#mvc-exceptionhandlers"}},[e._v("HandleRexCeptionResolver")]),e._v("机制上的。")]),e._v(" "),r("h5",{attrs:{id:"方法参数-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#方法参数-2"}},[e._v("#")]),e._v(" 方法参数")]),e._v(" "),r("p",[r("code",[e._v("@ExceptionHandler")]),e._v("方法支持以下参数:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Method argument")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[e._v("Exception type")]),e._v(" "),r("td",[e._v("访问提出的异常。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HandlerMethod")])]),e._v(" "),r("td",[e._v("用于访问引发异常的控制器方法。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("WebRequest")]),e._v(", "),r("code",[e._v("NativeWebRequest")])]),e._v(" "),r("td",[e._v("在不直接使用"),r("br"),e._v(" Servlet API 的情况下,对请求参数以及请求和会话属性进行通用访问。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("javax.servlet.ServletRequest")]),e._v(", "),r("code",[e._v("javax.servlet.ServletResponse")])]),e._v(" "),r("td",[e._v("选择任何特定的请求或响应类型(例如,"),r("code",[e._v("ServletRequest")]),e._v("或"),r("code",[e._v("HttpServletRequest")]),e._v("或 Spring 的"),r("code",[e._v("MultipartRequest")]),e._v("或"),r("code",[e._v("MultipartHttpServletRequest")]),e._v(")。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("javax.servlet.http.HttpSession")])]),e._v(" "),r("td",[e._v("强制执行会话的存在。因此,这样的参数永远不是"),r("code",[e._v("null")]),e._v("。"),r("br"),e._v("注意,会话访问不是线程安全的。考虑将"),r("code",[e._v("RequestMappingHandlerAdapter")]),e._v("实例的"),r("code",[e._v("synchronizeOnSession")]),e._v("标志设置为"),r("code",[e._v("true")]),e._v(",如果允许多个"),r("br"),e._v("请求并发访问会话。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.security.Principal")])]),e._v(" "),r("td",[e._v("当前经过身份验证的用户——如果已知的话,可能是特定的"),r("code",[e._v("Principal")]),e._v("实现类。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpMethod")])]),e._v(" "),r("td",[e._v("请求的 HTTP 方法。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Locale")])]),e._v(" "),r("td",[e._v("当前的请求区域设置,由可用的最具体的"),r("code",[e._v("LocaleResolver")]),e._v("确定—在"),r("br"),e._v("效果中,配置的"),r("code",[e._v("LocaleResolver")]),e._v("或"),r("code",[e._v("LocaleContextResolver")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.TimeZone")]),e._v(", "),r("code",[e._v("java.time.ZoneId")])]),e._v(" "),r("td",[e._v("与当前请求相关联的时区,由"),r("code",[e._v("LocaleContextResolver")]),e._v("确定。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.io.OutputStream")]),e._v(", "),r("code",[e._v("java.io.Writer")])]),e._v(" "),r("td",[e._v("用于访问原始响应体,如 Servlet API 所公开的那样。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Map")]),e._v(", "),r("code",[e._v("org.springframework.ui.Model")]),e._v(", "),r("code",[e._v("org.springframework.ui.ModelMap")])]),e._v(" "),r("td",[e._v("用于访问用于错误响应的模型。总是空的。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("RedirectAttributes")])]),e._v(" "),r("td",[e._v("指定在重定向的情况下使用的属性—(即附加到查询"),r("br"),e._v("字符串中)和要临时存储的 flash 属性,直到重定向后的请求为止。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-redirecting-passing-data"}},[e._v("重定向属性")]),e._v("和"),r("a",{attrs:{href:"#mvc-flash-attributes"}},[e._v("flash 属性")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@SessionAttribute")])]),e._v(" "),r("td",[e._v("对于任何会话属性的访问,与存储在"),r("br"),e._v("会话中的模型属性形成对比的是,它是一个类级"),r("code",[e._v("@SessionAttributes")]),e._v("声明的结果。"),r("br"),e._v("有关更多详细信息,请参见["),r("code",[e._v("@SessionAttribute")]),e._v("](#mvc-ann-sessionAttribute)。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@RequestAttribute")])]),e._v(" "),r("td",[e._v("用于访问请求属性。有关更多详细信息,请参见["),r("code",[e._v("@RequestAttribute")]),e._v("]。")])])])]),e._v(" "),r("h5",{attrs:{id:"返回值-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#返回值-2"}},[e._v("#")]),e._v(" 返回值")]),e._v(" "),r("p",[r("code",[e._v("@ExceptionHandler")]),e._v("方法支持以下返回值:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Return value")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("@ResponseBody")])]),e._v(" "),r("td",[e._v("返回值通过"),r("code",[e._v("HttpMessageConverter")]),e._v("实例进行转换,并写入"),r("br"),e._v("响应。参见["),r("code",[e._v("@ResponseBody")]),e._v("]。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("HttpEntity")]),e._v(", "),r("code",[e._v("ResponseEntity")])]),e._v(" "),r("td",[e._v("返回值指定通过"),r("code",[e._v("HttpMessageConverter")]),e._v("实例转换完整的响应(包括 HTTP 头和正文)"),r("br"),e._v("并将其写入响应。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#mvc-ann-responseentity"}},[e._v("负责实体")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("String")])]),e._v(" "),r("td",[e._v("要用"),r("code",[e._v("ViewResolver")]),e._v("实现来解析的视图名称,并与"),r("br"),e._v("隐式模型一起使用——通过命令对象和"),r("code",[e._v("@ModelAttribute")]),e._v("方法确定。"),r("br"),e._v("处理程序方法还可以通过声明"),r("code",[e._v("Model")]),e._v("参数(前面已经描述过)以编程方式丰富模型。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("View")])]),e._v(" "),r("td",[e._v("一个"),r("code",[e._v("View")]),e._v("实例用于与隐式模型一起进行渲染——通过命令对象和"),r("code",[e._v("@ModelAttribute")]),e._v("方法确定"),r("br"),e._v("。处理程序方法还可以通过声明一个"),r("code",[e._v("Model")]),e._v("参数(前面已经描述过)以编程方式丰富模型。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.util.Map")]),e._v(", "),r("code",[e._v("org.springframework.ui.Model")])]),e._v(" "),r("td",[e._v("要添加到隐式模型的属性,其视图名称通过"),r("code",[e._v("RequestToViewNameTranslator")]),e._v("隐式确定"),r("br"),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@ModelAttribute")])]),e._v(" "),r("td",[e._v("通过"),r("br"),e._v("a"),r("code",[e._v("RequestToViewNameTranslator")]),e._v("隐式确定的视图名称要添加到模型中的属性。"),r("br"),r("br"),e._v("注意,"),r("code",[e._v("@ModelAttribute")]),e._v("是可选的。请参阅"),r("br"),e._v("本表末尾的“任何其他返回值”。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("ModelAndView")]),e._v(" object")]),e._v(" "),r("td",[e._v("要使用的视图和模型属性,以及可选的响应状态。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("void")])]),e._v(" "),r("td",[e._v("返回类型(或"),r("code",[e._v("null")]),e._v("返回值)为"),r("code",[e._v("null")]),e._v("的方法被认为具有完全的"),r("br"),e._v("处理响应,如果它还具有"),r("code",[e._v("ServletResponse``OutputStream")]),e._v("参数,或"),r("br"),r("code",[e._v("@ResponseStatus")]),e._v("注释。如果控制器进行了正的"),r("code",[e._v("ETag")]),e._v("或"),r("code",[e._v("lastModified")]),e._v("时间戳检查(详见"),r("a",{attrs:{href:"#mvc-caching-etag-lastmodified"}},[e._v("控制器")]),e._v("),也是如此。"),r("br"),r("br"),e._v("如果上述各项都不是真的,"),r("code",[e._v("void")]),e._v("返回类型还可以表示"),r("br"),e._v("REST 控制器的“无响应体”或 HTML 控制器的默认视图名称选择。")])]),e._v(" "),r("tr",[r("td",[e._v("Any other return value")]),e._v(" "),r("td",[e._v("如果一个返回值与上述任一项不匹配且不是简单类型(由"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-",target:"_blank",rel:"noopener noreferrer"}},[e._v("Beanutils#IsSimpleProperty"),r("OutboundLink")],1),e._v("确定),则默认情况下"),r("br"),e._v("将其视为要添加到模型中的模型属性。如果它是一个简单的类型,"),r("br"),e._v("它仍然是未解决的。")])])])]),e._v(" "),r("h5",{attrs:{id:"rest-api-异常"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#rest-api-异常"}},[e._v("#")]),e._v(" REST API 异常")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-rest-exceptions"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动做到这一点,因为响应主体中的错误详细信息的表示是特定于应用程序的。但是,"),r("code",[e._v("@RestController")]),e._v("可以使用带有"),r("code",[e._v("@ExceptionHandler")]),e._v("返回值的"),r("code",[e._v("ResponseEntity")]),e._v("方法来设置响应的状态和主体。这样的方法也可以在"),r("code",[e._v("@ControllerAdvice")]),e._v("类中声明,以在全局范围内应用它们。")]),e._v(" "),r("p",[e._v("在响应体中实现带有错误详细信息的全局异常处理的应用程序应该考虑扩展["),r("code",[e._v("ResponseEntityExceptionHandler")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/mvc/method/annotation/responseentyExceptionHandler.html),它为 Spring MVC 引发的异常提供处理,并提供钩子来定制响应体。为了利用这一点,创建一个"),r("code",[e._v("ResponseEntityExceptionHandler")]),e._v("的子类,用"),r("code",[e._v("@ControllerAdvice")]),e._v("对其进行注释,覆盖必要的方法,并将其声明为 Spring Bean。")]),e._v(" "),r("h4",{attrs:{id:"_1-3-7-财务总监建议"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-7-财务总监建议"}},[e._v("#")]),e._v(" 1.3.7.财务总监建议")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-ann-controller-advice"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@ExceptionHandler")]),e._v("、"),r("code",[e._v("@InitBinder")]),e._v("和"),r("code",[e._v("@ModelAttribute")]),e._v("方法仅适用于在其中声明它们的"),r("code",[e._v("@Controller")]),e._v("类或类层次结构。如果它们是在"),r("code",[e._v("@ControllerAdvice")]),e._v("或"),r("code",[e._v("@RestControllerAdvice")]),e._v("类中声明的,则它们适用于任何控制器。此外,截至 5.3,"),r("code",[e._v("@ExceptionHandler")]),e._v("中的"),r("code",[e._v("@ControllerAdvice")]),e._v("方法可用于处理来自任何"),r("code",[e._v("@Controller")]),e._v("或任何其他处理程序的异常。")]),e._v(" "),r("p",[r("code",[e._v("@ControllerAdvice")]),e._v("是用"),r("code",[e._v("@Component")]),e._v("进行元注释的,因此可以通过"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#beans-java-instantiating-container-scan"}},[e._v("组件扫描")]),e._v("注册为 Spring Bean。"),r("code",[e._v("@RestControllerAdvice")]),e._v("是用"),r("code",[e._v("@ControllerAdvice")]),e._v("和"),r("code",[e._v("@ResponseBody")]),e._v("进行元注释的,这意味着"),r("code",[e._v("@ExceptionHandler")]),e._v("方法的返回值将通过响应体消息转换呈现,而不是通过 HTML 视图呈现。")],1),e._v(" "),r("p",[e._v("在启动时,"),r("code",[e._v("RequestMappingHandlerMapping")]),e._v("和"),r("code",[e._v("ExceptionHandlerExceptionResolver")]),e._v("检测控制器建议 bean 并在运行时应用它们。全局"),r("code",[e._v("@ExceptionHandler")]),e._v("方法,来自于"),r("code",[e._v("@ControllerAdvice")]),e._v(",应用于"),r("em",[e._v("之后")]),e._v("局部方法,来自于"),r("code",[e._v("@Controller")]),e._v("。相比之下,全局"),r("code",[e._v("@ModelAttribute")]),e._v("和"),r("code",[e._v("@InitBinder")]),e._v("方法则应用于局部方法"),r("em",[e._v("在此之前")]),e._v("。")]),e._v(" "),r("p",[r("code",[e._v("@ControllerAdvice")]),e._v("注释的属性允许你缩小它们所应用的控制器和处理程序的集合。例如:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// Target all Controllers annotated with @RestController\n@ControllerAdvice(annotations = RestController.class)\npublic class ExampleAdvice1 {}\n\n// Target all Controllers within specific packages\n@ControllerAdvice("org.example.controllers")\npublic class ExampleAdvice2 {}\n\n// Target all Controllers assignable to specific classes\n@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})\npublic class ExampleAdvice3 {}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// Target all Controllers annotated with @RestController\n@ControllerAdvice(annotations = [RestController::class])\nclass ExampleAdvice1\n\n// Target all Controllers within specific packages\n@ControllerAdvice("org.example.controllers")\nclass ExampleAdvice2\n\n// Target all Controllers assignable to specific classes\n@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])\nclass ExampleAdvice3\n')])])]),r("p",[e._v("前面示例中的选择器是在运行时进行评估的,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参见["),r("code",[e._v("@ControllerAdvice")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/controlleradvice.html)爪哇doc。")]),e._v(" "),r("h3",{attrs:{id:"_1-4-功能端点"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-功能端点"}},[e._v("#")]),e._v(" 1.4.功能端点")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring Web MVC 包括 WebMVC.FN,这是一种轻量级函数编程模型,其中函数被用来路由和处理请求,而契约被设计为具有不可变性。它是基于注释的编程模型的一种替代方案,但在其他情况下运行在相同的"),r("a",{attrs:{href:"#mvc-servlet"}},[e._v("DispatcherServlet")]),e._v("上。")]),e._v(" "),r("h4",{attrs:{id:"_1-4-1-概述"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-1-概述"}},[e._v("#")]),e._v(" 1.4.1.概述")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn-overview"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在 Webmvc.FN 中,HTTP 请求是用"),r("code",[e._v("HandlerFunction")]),e._v("处理的:一个函数接受"),r("code",[e._v("ServerRequest")]),e._v("并返回"),r("code",[e._v("ServerResponse")]),e._v("。请求和响应对象都具有不可更改的契约,这些契约提供对 HTTP 请求和响应的 JDK8 友好访问。"),r("code",[e._v("HandlerFunction")]),e._v("相当于基于注释的编程模型中的"),r("code",[e._v("@RequestMapping")]),e._v("方法的主体。")]),e._v(" "),r("p",[e._v("传入的请求被路由到带有"),r("code",[e._v("RouterFunction")]),e._v("的处理程序函数:该函数接受"),r("code",[e._v("ServerRequest")]),e._v("并返回可选的"),r("code",[e._v("HandlerFunction")]),e._v("(即"),r("code",[e._v("Optional")]),e._v(")。当路由器函数匹配时,将返回一个处理程序函数;否则将返回一个空的可选函数。"),r("code",[e._v("RouterFunction")]),e._v("相当于"),r("code",[e._v("@RequestMapping")]),e._v("注释,但与此的主要区别是,路由器函数不仅提供数据,还提供行为。")]),e._v(" "),r("p",[r("code",[e._v("RouterFunctions.route()")]),e._v("提供了一个路由器构建器,该构建器有利于路由器的创建,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static org.springframework.web.servlet.function.RequestPredicates.*;\nimport static org.springframework.web.servlet.function.RouterFunctions.route;\n\nPersonRepository repository = ...\nPersonHandler handler = new PersonHandler(repository);\n\nRouterFunction route = route()\n .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)\n .GET("/person", accept(APPLICATION_JSON), handler::listPeople)\n .POST("/person", handler::createPerson)\n .build();\n\npublic class PersonHandler {\n\n // ...\n\n public ServerResponse listPeople(ServerRequest request) {\n // ...\n }\n\n public ServerResponse createPerson(ServerRequest request) {\n // ...\n }\n\n public ServerResponse getPerson(ServerRequest request) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval repository: PersonRepository = ...\nval handler = PersonHandler(repository)\n\nval route = router { (1)\n accept(APPLICATION_JSON).nest {\n GET("/person/{id}", handler::getPerson)\n GET("/person", handler::listPeople)\n }\n POST("/person", handler::createPerson)\n}\n\nclass PersonHandler(private val repository: PersonRepository) {\n\n // ...\n\n fun listPeople(request: ServerRequest): ServerResponse {\n // ...\n }\n\n fun createPerson(request: ServerRequest): ServerResponse {\n // ...\n }\n\n fun getPerson(request: ServerRequest): ServerResponse {\n // ...\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("使用路由器 DSL 创建路由器。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果你将"),r("code",[e._v("RouterFunction")]),e._v("注册为 Bean,例如通过在"),r("code",[e._v("@Configuration")]),e._v("类中将其公开,则 Servlet 将自动检测它,如"),r("a",{attrs:{href:"#webmvc-fn-running"}},[e._v("运行服务器")]),e._v("中所解释的那样。")]),e._v(" "),r("h4",{attrs:{id:"_1-4-2-handlerfunction"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-2-handlerfunction"}},[e._v("#")]),e._v(" 1.4.2.handlerfunction")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn-handler-functions"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("ServerRequest")]),e._v("和"),r("code",[e._v("ServerResponse")]),e._v("是不可变的接口,它们提供对 HTTP 请求和响应的 JDK8 友好访问,包括头、主体、方法和状态代码。")]),e._v(" "),r("h5",{attrs:{id:"serverrequest"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#serverrequest"}},[e._v("#")]),e._v(" ServerRequest")]),e._v(" "),r("p",[r("code",[e._v("ServerRequest")]),e._v("提供对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问是通过"),r("code",[e._v("body")]),e._v("方法提供的。")]),e._v(" "),r("p",[e._v("下面的示例将请求主体提取到"),r("code",[e._v("String")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("String string = request.body(String.class);\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val string = request.body()\n")])])]),r("p",[e._v("下面的示例将主体提取到"),r("code",[e._v("List")]),e._v(",其中"),r("code",[e._v("Person")]),e._v("对象是从序列化形式(例如 JSON 或 XML)中解码的:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("List people = request.body(new ParameterizedTypeReference>() {});\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val people = request.body()\n")])])]),r("p",[e._v("下面的示例展示了如何访问参数:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("MultiValueMap params = request.params();\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val map = request.params()\n")])])]),r("h5",{attrs:{id:"serverresponse"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#serverresponse"}},[e._v("#")]),e._v(" ServerResponse")]),e._v(" "),r("p",[r("code",[e._v("ServerResponse")]),e._v("提供对 HTTP 响应的访问,由于它是不可变的,你可以使用"),r("code",[e._v("build")]),e._v("方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200(OK)响应:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("Person person = ...\nServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val person: Person = ...\nServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)\n")])])]),r("p",[e._v("下面的示例展示了如何使用"),r("code",[e._v("Location")]),e._v("标头构建 201(已创建)响应,而不包含正文:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("URI location = ...\nServerResponse.created(location).build();\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val location: URI = ...\nServerResponse.created(location).build()\n")])])]),r("p",[e._v("你也可以使用异步结果作为主体,其形式为"),r("code",[e._v("CompletableFuture")]),e._v(","),r("code",[e._v("Publisher")]),e._v(",或"),r("code",[e._v("ReactiveAdapterRegistry")]),e._v("所支持的任何其他类型。例如:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("Mono person = webClient.get().retrieve().bodyToMono(Person.class);\nServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person);\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val person = webClient.get().retrieve().awaitBody()\nServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person)\n")])])]),r("p",[e._v("如果不只是主体,而且状态或头也是基于异步类型的,则可以在"),r("code",[e._v("ServerResponse")]),e._v("上使用静态"),r("code",[e._v("async")]),e._v("方法,该方法接受"),r("code",[e._v("CompletableFuture")]),e._v("、"),r("code",[e._v("Publisher")]),e._v("或"),r("code",[e._v("ReactiveAdapterRegistry")]),e._v("支持的任何其他异步类型。例如:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('Mono asyncResponse = webClient.get().retrieve().bodyToMono(Person.class)\n .map(p -> ServerResponse.ok().header("Name", p.name()).body(p));\nServerResponse.async(asyncResponse);\n')])])]),r("p",[r("a",{attrs:{href:"https://www.w3.org/TR/eventsource/",target:"_blank",rel:"noopener noreferrer"}},[e._v("服务器发送的事件"),r("OutboundLink")],1),e._v("可以通过"),r("code",[e._v("sse")]),e._v("上的静态"),r("code",[e._v("sse")]),e._v("方法提供。该方法提供的构建器允许你以 JSON 的形式发送字符串或其他对象。例如:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public RouterFunction sse() {\n return route(GET("/sse"), request -> ServerResponse.sse(sseBuilder -> {\n // Save the sseBuilder object somewhere..\n }));\n}\n\n// In some other thread, sending a String\nsseBuilder.send("Hello world");\n\n// Or an object, which will be transformed into JSON\nPerson person = ...\nsseBuilder.send(person);\n\n// Customize the event by using the other methods\nsseBuilder.id("42")\n .event("sse event")\n .data(person);\n\n// and done at some point\nsseBuilder.complete();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('fun sse(): RouterFunction = router {\n GET("/sse") { request -> ServerResponse.sse { sseBuilder ->\n // Save the sseBuilder object somewhere..\n }\n}\n\n// In some other thread, sending a String\nsseBuilder.send("Hello world")\n\n// Or an object, which will be transformed into JSON\nval person = ...\nsseBuilder.send(person)\n\n// Customize the event by using the other methods\nsseBuilder.id("42")\n .event("sse event")\n .data(person)\n\n// and done at some point\nsseBuilder.complete()\n')])])]),r("h5",{attrs:{id:"处理程序类"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#处理程序类"}},[e._v("#")]),e._v(" 处理程序类")]),e._v(" "),r("p",[e._v("我们可以将处理程序函数编写为 lambda,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('HandlerFunction helloWorld =\n request -> ServerResponse.ok().body("Hello World");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val helloWorld: (ServerRequest) -> ServerResponse =\n { ServerResponse.ok().body("Hello World") }\n')])])]),r("p",[e._v("这很方便,但在一个应用程序中,我们需要多个功能,而多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数组合到一个处理程序类中是有用的,该处理程序类在基于注释的应用程序中具有与"),r("code",[e._v("@Controller")]),e._v("类似的作用。例如,下面的类公开了一个反应性"),r("code",[e._v("Person")]),e._v("存储库:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static org.springframework.web.reactive.function.server.ServerResponse.ok;\n\npublic class PersonHandler {\n\n private final PersonRepository repository;\n\n public PersonHandler(PersonRepository repository) {\n this.repository = repository;\n }\n\n public ServerResponse listPeople(ServerRequest request) { (1)\n List people = repository.allPeople();\n return ok().contentType(APPLICATION_JSON).body(people);\n }\n\n public ServerResponse createPerson(ServerRequest request) throws Exception { (2)\n Person person = request.body(Person.class);\n repository.savePerson(person);\n return ok().build();\n }\n\n public ServerResponse getPerson(ServerRequest request) { (3)\n int personId = Integer.parseInt(request.pathVariable("id"));\n Person person = repository.getPerson(personId);\n if (person != null) {\n return ok().contentType(APPLICATION_JSON).body(person);\n }\n else {\n return ServerResponse.notFound().build();\n }\n }\n\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[r("code",[e._v("listPeople")]),e._v("是一个处理函数,它将存储库中找到的所有"),r("code",[e._v("Person")]),e._v("对象作为"),r("br"),e._v("json 返回。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[r("code",[e._v("createPerson")]),e._v("是一个处理函数,它存储了一个包含在请求主体中的新"),r("code",[e._v("Person")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[r("code",[e._v("getPerson")]),e._v("是一个处理函数,它返回一个人,由"),r("code",[e._v("id")]),e._v("路径"),r("br"),e._v("变量标识。我们从存储库中检索"),r("code",[e._v("Person")]),e._v("并创建一个 JSON 响应,如果找到了"),r("br"),e._v("。如果没有找到它,我们将返回 404Not Found 响应。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class PersonHandler(private val repository: PersonRepository) {\n\n fun listPeople(request: ServerRequest): ServerResponse { (1)\n val people: List = repository.allPeople()\n return ok().contentType(APPLICATION_JSON).body(people);\n }\n\n fun createPerson(request: ServerRequest): ServerResponse { (2)\n val person = request.body()\n repository.savePerson(person)\n return ok().build()\n }\n\n fun getPerson(request: ServerRequest): ServerResponse { (3)\n val personId = request.pathVariable("id").toInt()\n return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).body(it) }\n ?: ServerResponse.notFound().build()\n\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[r("code",[e._v("listPeople")]),e._v("是一个处理函数,它将存储库中找到的所有"),r("code",[e._v("Person")]),e._v("对象作为"),r("br"),e._v("json 返回。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[r("code",[e._v("createPerson")]),e._v("是一个处理函数,它存储了一个包含在请求主体中的新"),r("code",[e._v("Person")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[r("code",[e._v("getPerson")]),e._v("是一个处理函数,它返回一个人,由"),r("code",[e._v("id")]),e._v("路径"),r("br"),e._v("变量标识。我们从存储库中检索"),r("code",[e._v("Person")]),e._v("并创建一个 JSON 响应,如果找到了"),r("br"),e._v("。如果没有找到它,我们将返回 404Not Found 响应。")])])])]),e._v(" "),r("h5",{attrs:{id:"validation"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#validation"}},[e._v("#")]),e._v(" Validation")]),e._v(" "),r("p",[e._v("功能端点可以使用 Spring 的"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation"}},[e._v("验证设施")]),e._v("将验证应用于请求主体。例如,给定一个针对"),r("code",[e._v("Person")]),e._v("的自定义 Spring "),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation"}},[e._v("Validator")]),e._v("实现:")],1),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class PersonHandler {\n\n private final Validator validator = new PersonValidator(); (1)\n\n // ...\n\n public ServerResponse createPerson(ServerRequest request) {\n Person person = request.body(Person.class);\n validate(person); (2)\n repository.savePerson(person);\n return ok().build();\n }\n\n private void validate(Person person) {\n Errors errors = new BeanPropertyBindingResult(person, "person");\n validator.validate(person, errors);\n if (errors.hasErrors()) {\n throw new ServerWebInputException(errors.toString()); (3)\n }\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("创建"),r("code",[e._v("Validator")]),e._v("实例。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("应用验证。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("提出 400 响应的例外情况。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class PersonHandler(private val repository: PersonRepository) {\n\n private val validator = PersonValidator() (1)\n\n // ...\n\n fun createPerson(request: ServerRequest): ServerResponse {\n val person = request.body()\n validate(person) (2)\n repository.savePerson(person)\n return ok().build()\n }\n\n private fun validate(person: Person) {\n val errors: Errors = BeanPropertyBindingResult(person, "person")\n validator.validate(person, errors)\n if (errors.hasErrors()) {\n throw ServerWebInputException(errors.toString()) (3)\n }\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("创建"),r("code",[e._v("Validator")]),e._v("实例。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("应用验证。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("提出 400 响应的例外情况。")])])])]),e._v(" "),r("p",[e._v("处理程序还可以使用标准 Bean 验证 API(JSR-303),方法是基于"),r("code",[e._v("LocalValidatorFactoryBean")]),e._v("创建并注入一个全局"),r("code",[e._v("Validator")]),e._v("实例。见"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation-beanvalidation"}},[e._v("Spring Validation")]),e._v("。")],1),e._v(" "),r("h4",{attrs:{id:"_1-4-3-routerfunction"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-3-routerfunction"}},[e._v("#")]),e._v(" 1.4.3."),r("code",[e._v("RouterFunction")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn-router-functions"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("路由器函数用于将请求路由到相应的"),r("code",[e._v("HandlerFunction")]),e._v("。通常,你不会自己编写路由器函数,而是使用"),r("code",[e._v("RouterFunctions")]),e._v("实用程序类上的一个方法来创建一个。"),r("code",[e._v("RouterFunctions.route()")]),e._v("(无参数)为你提供了用于创建路由器函数的 Fluent 构建器,而"),r("code",[e._v("RouterFunctions.route(RequestPredicate, HandlerFunction)")]),e._v("提供了一种直接创建路由器的方法。")]),e._v(" "),r("p",[e._v("通常,建议使用"),r("code",[e._v("route()")]),e._v("Builder,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现的静态导入。例如,Router Function Builder 提供了方法"),r("code",[e._v("GET(String, HandlerFunction)")]),e._v("来创建 GET 请求的映射;以及"),r("code",[e._v("POST(String, HandlerFunction)")]),e._v("用于 POST。")]),e._v(" "),r("p",[e._v("除了基于 HTTP 方法的映射,Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变量,它将"),r("code",[e._v("RequestPredicate")]),e._v("作为参数,通过该参数可以表示额外的约束。")]),e._v(" "),r("h5",{attrs:{id:"谓词"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#谓词"}},[e._v("#")]),e._v(" 谓词")]),e._v(" "),r("p",[e._v("你可以编写自己的"),r("code",[e._v("RequestPredicate")]),e._v(",但是"),r("code",[e._v("RequestPredicates")]),e._v("实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于"),r("code",[e._v("Accept")]),e._v("头创建一个约束:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('RouterFunction route = RouterFunctions.route()\n .GET("/hello-world", accept(MediaType.TEXT_PLAIN),\n request -> ServerResponse.ok().body("Hello World")).build();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval route = router {\n GET("/hello-world", accept(TEXT_PLAIN)) {\n ServerResponse.ok().body("Hello World")\n }\n}\n')])])]),r("p",[e._v("你可以使用以下方法将多个请求谓词组合在一起:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("RequestPredicate.and(RequestPredicate)")]),e._v("—两者必须匹配。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("RequestPredicate.or(RequestPredicate)")]),e._v("——两者都可以匹配。")])])]),e._v(" "),r("p",[e._v("来自"),r("code",[e._v("RequestPredicates")]),e._v("的许多谓词都是组成的。例如,"),r("code",[e._v("RequestPredicates.GET(String)")]),e._v("是由"),r("code",[e._v("RequestPredicates.method(HttpMethod)")]),e._v("和"),r("code",[e._v("RequestPredicates.path(String)")]),e._v("组成的。上面显示的示例还使用两个请求谓词,因为构建器在内部使用"),r("code",[e._v("RequestPredicates.GET")]),e._v(",并将其与"),r("code",[e._v("accept")]),e._v("谓词组合在一起。")]),e._v(" "),r("h5",{attrs:{id:"路线"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#路线"}},[e._v("#")]),e._v(" 路线")]),e._v(" "),r("p",[e._v("对路由器的功能按顺序进行评估:如果第一条路由不匹配,则对第二条路由进行评估,依此类推。因此,在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时,这一点也很重要,后面将对此进行说明。请注意,这种行为与基于注释的编程模型不同,在该模型中,“最特定的”控制器方法是自动选择的。")]),e._v(" "),r("p",[e._v("当使用 Router 函数 builder 时,所有定义的路由都被组合成一个"),r("code",[e._v("RouterFunction")]),e._v(",从"),r("code",[e._v("build()")]),e._v("返回。还有其他方法可以将多个路由器功能组合在一起:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("add(RouterFunction)")]),e._v("上的"),r("code",[e._v("RouterFunctions.route()")]),e._v("构建器")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("RouterFunction.and(RouterFunction)")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("RouterFunction.andRoute(RequestPredicate, HandlerFunction)")]),e._v("—带有嵌套"),r("code",[e._v("RouterFunctions.route()")]),e._v("的"),r("code",[e._v("RouterFunction.and()")]),e._v("的快捷方式。")])])]),e._v(" "),r("p",[e._v("下面的示例显示了四条路线的组成:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import static org.springframework.http.MediaType.APPLICATION_JSON;\nimport static org.springframework.web.servlet.function.RequestPredicates.*;\n\nPersonRepository repository = ...\nPersonHandler handler = new PersonHandler(repository);\n\nRouterFunction otherRoute = ...\n\nRouterFunction route = route()\n .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)\n .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)\n .POST("/person", handler::createPerson) (3)\n .add(otherRoute) (4)\n .build();\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("带有与 JSON 匹配的"),r("code",[e._v("Accept")]),e._v("标头的"),r("code",[e._v("GET /person/{id}")]),e._v("被路由到"),r("code",[e._v("PersonHandler.getPerson")])])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("带有与 JSON 匹配的"),r("code",[e._v("GET /person")]),e._v("头的"),r("code",[e._v("Accept")]),e._v("被路由到"),r("code",[e._v("PersonHandler.listPeople")])])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("没有附加谓词的"),r("code",[e._v("POST /person")]),e._v("映射到"),r("code",[e._v("PersonHandler.createPerson")]),e._v(",并且")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[r("code",[e._v("otherRoute")]),e._v("是在其他地方创建的路由器功能,并将其添加到所构建的路由中。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.http.MediaType.APPLICATION_JSON\nimport org.springframework.web.servlet.function.router\n\nval repository: PersonRepository = ...\nval handler = PersonHandler(repository);\n\nval otherRoute = router { }\n\nval route = router {\n GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)\n GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)\n POST("/person", handler::createPerson) (3)\n}.and(otherRoute) (4)\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("带有与 JSON 匹配的"),r("code",[e._v("GET /person/{id}")]),e._v("头的"),r("code",[e._v("Accept")]),e._v("被路由到"),r("code",[e._v("PersonHandler.getPerson")])])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("带有与 JSON 匹配的"),r("code",[e._v("Accept")]),e._v("标头的"),r("code",[e._v("GET /person")]),e._v("被路由到"),r("code",[e._v("PersonHandler.listPeople")])])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("没有附加谓词的"),r("code",[e._v("POST /person")]),e._v("映射到"),r("code",[e._v("PersonHandler.createPerson")]),e._v(",并且")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[r("code",[e._v("otherRoute")]),e._v("是在其他地方创建的路由器功能,并将其添加到所构建的路由中。")])])])]),e._v(" "),r("h5",{attrs:{id:"嵌套路线"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#嵌套路线"}},[e._v("#")]),e._v(" 嵌套路线")]),e._v(" "),r("p",[e._v("一组路由器函数通常有一个共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配"),r("code",[e._v("/person")]),e._v("的路径谓词,由三个路由使用。在使用注释时,可以使用映射到"),r("code",[e._v("/person")]),e._v("的类型级别"),r("code",[e._v("@RequestMapping")]),e._v("注释来删除这种重复。在 WebMVC.FN 中,路径谓词可以通过路由器函数生成器上的"),r("code",[e._v("path")]),e._v("方法进行共享。例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('RouterFunction route = route()\n .path("/person", builder -> builder (1)\n .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)\n .GET(accept(APPLICATION_JSON), handler::listPeople)\n .POST("/person", handler::createPerson))\n .build();\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("注意,"),r("code",[e._v("path")]),e._v("的第二个参数是接受路由器生成器的消费者。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval route = router {\n "/person".nest {\n GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)\n GET(accept(APPLICATION_JSON), handler::listPeople)\n POST("/person", handler::createPerson)\n }\n}\n')])])]),r("p",[e._v("尽管基于路径的嵌套是最常见的,但你可以通过在构建器上使用"),r("code",[e._v("nest")]),e._v("方法在任何类型的谓词上进行嵌套。上面仍然包含一些以共享"),r("code",[e._v("Accept")]),e._v("-header 谓词形式出现的重复。我们可以通过使用"),r("code",[e._v("nest")]),e._v("方法和"),r("code",[e._v("accept")]),e._v("方法进一步改进:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('RouterFunction route = route()\n .path("/person", b1 -> b1\n .nest(accept(APPLICATION_JSON), b2 -> b2\n .GET("/{id}", handler::getPerson)\n .GET(handler::listPeople))\n .POST("/person", handler::createPerson))\n .build();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval route = router {\n "/person".nest {\n accept(APPLICATION_JSON).nest {\n GET("/{id}", handler::getPerson)\n GET("", handler::listPeople)\n POST("/person", handler::createPerson)\n }\n }\n}\n')])])]),r("h4",{attrs:{id:"_1-4-4-运行服务器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-4-运行服务器"}},[e._v("#")]),e._v(" 1.4.4.运行服务器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn-running"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你通常通过"),r("a",{attrs:{href:"#mvc-config"}},[e._v("MVC Config")]),e._v("在基于["),r("code",[e._v("DispatcherHandler")]),e._v("](#MVC- Servlet)的设置中运行路由器函数,该设置使用 Spring 配置来声明处理请求所需的组件。MVC 爪哇 配置声明以下基础设施组件以支持功能端点:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("RouterFunctionMapping")]),e._v(":在 Spring 配置中检测一个或多个"),r("code",[e._v("RouterFunction")]),e._v("bean,"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#beans-factory-ordered"}},[e._v("命令他们")]),e._v(",通过"),r("code",[e._v("RouterFunction.andOther")]),e._v("对它们进行组合,并将请求路由到结果组合的"),r("code",[e._v("RouterFunction")]),e._v("。")],1)]),e._v(" "),r("li",[r("p",[r("code",[e._v("HandlerFunctionAdapter")]),e._v(":允许"),r("code",[e._v("DispatcherHandler")]),e._v("调用映射到请求的"),r("code",[e._v("HandlerFunction")]),e._v("的简单适配器。")])])]),e._v(" "),r("p",[e._v("前面的组件让功能端点适合"),r("code",[e._v("DispatcherServlet")]),e._v("请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器的话)并排运行。这也是 Spring 引导 Web 启动器启用功能端点的方式。")]),e._v(" "),r("p",[e._v("下面的示例展示了一个 WebFlux 爪哇 配置:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Bean\n public RouterFunction routerFunctionA() {\n // ...\n }\n\n @Bean\n public RouterFunction routerFunctionB() {\n // ...\n }\n\n // ...\n\n @Override\n public void configureMessageConverters(List> converters) {\n // configure message conversion...\n }\n\n @Override\n public void addCorsMappings(CorsRegistry registry) {\n // configure CORS...\n }\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n // configure view resolution for HTML rendering...\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableMvc\nclass WebConfig : WebMvcConfigurer {\n\n @Bean\n fun routerFunctionA(): RouterFunction<*> {\n // ...\n }\n\n @Bean\n fun routerFunctionB(): RouterFunction<*> {\n // ...\n }\n\n // ...\n\n override fun configureMessageConverters(converters: List>) {\n // configure message conversion...\n }\n\n override fun addCorsMappings(registry: CorsRegistry) {\n // configure CORS...\n }\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n // configure view resolution for HTML rendering...\n }\n}\n")])])]),r("h4",{attrs:{id:"_1-4-5-过滤处理程序函数"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-4-5-过滤处理程序函数"}},[e._v("#")]),e._v(" 1.4.5.过滤处理程序函数")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-fn-handler-filter-function"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以使用路由函数生成器上的"),r("code",[e._v("before")]),e._v("、"),r("code",[e._v("after")]),e._v("或"),r("code",[e._v("filter")]),e._v("方法来过滤处理程序函数。对于注释,你可以通过使用"),r("code",[e._v("@ControllerAdvice")]),e._v("、"),r("code",[e._v("ServletFilter")]),e._v("或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如,考虑以下示例:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('RouterFunction route = route()\n .path("/person", b1 -> b1\n .nest(accept(APPLICATION_JSON), b2 -> b2\n .GET("/{id}", handler::getPerson)\n .GET(handler::listPeople)\n .before(request -> ServerRequest.from(request) (1)\n .header("X-RequestHeader", "Value")\n .build()))\n .POST("/person", handler::createPerson))\n .after((request, response) -> logResponse(response)) (2)\n .build();\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("添加自定义请求头的"),r("code",[e._v("before")]),e._v("过滤器仅应用于两个 GET 路由。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("记录响应的"),r("code",[e._v("after")]),e._v("过滤器应用于所有路由,包括嵌套的路由。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval route = router {\n "/person".nest {\n GET("/{id}", handler::getPerson)\n GET(handler::listPeople)\n before { (1)\n ServerRequest.from(it)\n .header("X-RequestHeader", "Value").build()\n }\n }\n POST("/person", handler::createPerson)\n after { _, response -> (2)\n logResponse(response)\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("添加自定义请求头的"),r("code",[e._v("before")]),e._v("过滤器仅应用于两个 GET 路由。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("记录响应的"),r("code",[e._v("after")]),e._v("过滤器应用于所有路由,包括嵌套的路由。")])])])]),e._v(" "),r("p",[e._v("路由器构建器上的"),r("code",[e._v("filter")]),e._v("方法接受"),r("code",[e._v("HandlerFilterFunction")]),e._v(":一个函数接受"),r("code",[e._v("ServerRequest")]),e._v("和"),r("code",[e._v("HandlerFunction")]),e._v("并返回"),r("code",[e._v("ServerResponse")]),e._v("。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。")]),e._v(" "),r("p",[e._v("现在,我们可以在路由中添加一个简单的安全过滤器,假设我们有一个"),r("code",[e._v("SecurityManager")]),e._v(",它可以确定是否允许特定的路径。下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('SecurityManager securityManager = ...\n\nRouterFunction route = route()\n .path("/person", b1 -> b1\n .nest(accept(APPLICATION_JSON), b2 -> b2\n .GET("/{id}", handler::getPerson)\n .GET(handler::listPeople))\n .POST("/person", handler::createPerson))\n .filter((request, next) -> {\n if (securityManager.allowAccessTo(request.path())) {\n return next.handle(request);\n }\n else {\n return ServerResponse.status(UNAUTHORIZED).build();\n }\n })\n .build();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.servlet.function.router\n\nval securityManager: SecurityManager = ...\n\nval route = router {\n ("/person" and accept(APPLICATION_JSON)).nest {\n GET("/{id}", handler::getPerson)\n GET("", handler::listPeople)\n POST("/person", handler::createPerson)\n filter { request, next ->\n if (securityManager.allowAccessTo(request.path())) {\n next(request)\n }\n else {\n status(UNAUTHORIZED).build();\n }\n }\n }\n}\n')])])]),r("p",[e._v("前面的示例演示了调用"),r("code",[e._v("next.handle(ServerRequest)")]),e._v("是可选的。我们只允许在允许访问的情况下运行处理程序函数。")]),e._v(" "),r("p",[e._v("除了在路由器功能构建器上使用"),r("code",[e._v("filter")]),e._v("方法外,还可以通过"),r("code",[e._v("RouterFunction.filter(HandlerFilterFunction)")]),e._v("对现有的路由器功能应用过滤器。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("CORS 对功能端点的支持是通过专用的["),r("code",[e._v("CorsFilter")]),e._v("]提供的。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h3",{attrs:{id:"_1-5-uri-链接"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-uri-链接"}},[e._v("#")]),e._v(" 1.5.URI 链接")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-uri-building"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("本节描述了 Spring 框架中可用来处理 URI 的各种选项。")]),e._v(" "),r("h4",{attrs:{id:"_1-5-1-尿酸成分"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-1-尿酸成分"}},[e._v("#")]),e._v(" 1.5.1.尿酸成分")]),e._v(" "),r("p",[e._v("Spring MVC 和 Spring WebFlux")]),e._v(" "),r("p",[r("code",[e._v("UriComponentsBuilder")]),e._v("有助于从具有变量的 URI 模板构建 URI,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('UriComponents uriComponents = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}") (1)\n .queryParam("q", "{q}") (2)\n .encode() (3)\n .build(); (4)\n\nURI uri = uriComponents.expand("Westin", "123").toUri(); (5)\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("带有 URI 模板的静态工厂方法。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("添加或替换 URI 组件。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("请求对 URI 模板和 URI 变量进行编码。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[e._v("建立"),r("code",[e._v("UriComponents")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("5")])]),e._v(" "),r("td",[e._v("展开变量并获得"),r("code",[e._v("URI")]),e._v("。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uriComponents = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}") (1)\n .queryParam("q", "{q}") (2)\n .encode() (3)\n .build() (4)\n\nval uri = uriComponents.expand("Westin", "123").toUri() (5)\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("带有 URI 模板的静态工厂方法。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("添加或替换 URI 组件。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("请求对 URI 模板和 URI 变量进行编码。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("4")])]),e._v(" "),r("td",[e._v("建立"),r("code",[e._v("UriComponents")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("5")])]),e._v(" "),r("td",[e._v("展开变量并获得"),r("code",[e._v("URI")]),e._v("。")])])])]),e._v(" "),r("p",[e._v("前面的示例可以合并为一个链,并用"),r("code",[e._v("buildAndExpand")]),e._v("将其缩短,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}")\n .queryParam("q", "{q}")\n .encode()\n .buildAndExpand("Westin", "123")\n .toUri();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}")\n .queryParam("q", "{q}")\n .encode()\n .buildAndExpand("Westin", "123")\n .toUri()\n')])])]),r("p",[e._v("你可以通过直接访问一个 URI(这意味着编码)来进一步缩短它,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}")\n .queryParam("q", "{q}")\n .build("Westin", "123");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}")\n .queryParam("q", "{q}")\n .build("Westin", "123")\n')])])]),r("p",[e._v("可以使用完整的 URI 模板进一步缩短它,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}?q={q}")\n .build("Westin", "123");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder\n .fromUriString("https://example.com/hotels/{hotel}?q={q}")\n .build("Westin", "123")\n')])])]),r("h4",{attrs:{id:"_1-5-2-uribuilder"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-2-uribuilder"}},[e._v("#")]),e._v(" 1.5.2.UriBuilder")]),e._v(" "),r("p",[e._v("Spring MVC 和 Spring WebFlux")]),e._v(" "),r("p",[e._v("["),r("code",[e._v("UriComponentsBuilder")]),e._v("]实现"),r("code",[e._v("UriBuilder")]),e._v("。你可以创建"),r("code",[e._v("UriBuilder")]),e._v(",然后使用"),r("code",[e._v("UriBuilderFactory")]),e._v("。同时,"),r("code",[e._v("UriBuilderFactory")]),e._v("和"),r("code",[e._v("UriBuilder")]),e._v("提供了一种基于共享配置(例如基本 URL、编码首选项和其他细节)的可插入机制,用于从 URI 模板构建 URI。")]),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("UriBuilderFactory")]),e._v("配置"),r("code",[e._v("RestTemplate")]),e._v("和"),r("code",[e._v("WebClient")]),e._v("来定制 URI 的准备。"),r("code",[e._v("DefaultUriBuilderFactory")]),e._v("是"),r("code",[e._v("UriBuilderFactory")]),e._v("的默认实现,它在内部使用"),r("code",[e._v("UriComponentsBuilder")]),e._v("并公开共享配置选项。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何配置"),r("code",[e._v("RestTemplate")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;\n\nString baseUrl = "https://example.org";\nDefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);\nfactory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);\n\nRestTemplate restTemplate = new RestTemplate();\nrestTemplate.setUriTemplateHandler(factory);\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode\n\nval baseUrl = "https://example.org"\nval factory = DefaultUriBuilderFactory(baseUrl)\nfactory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES\n\nval restTemplate = RestTemplate()\nrestTemplate.uriTemplateHandler = factory\n')])])]),r("p",[e._v("下面的示例配置"),r("code",[e._v("WebClient")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;\n\nString baseUrl = "https://example.org";\nDefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);\nfactory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);\n\nWebClient client = WebClient.builder().uriBuilderFactory(factory).build();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode\n\nval baseUrl = "https://example.org"\nval factory = DefaultUriBuilderFactory(baseUrl)\nfactory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES\n\nval client = WebClient.builder().uriBuilderFactory(factory).build()\n')])])]),r("p",[e._v("此外,还可以直接使用"),r("code",[e._v("DefaultUriBuilderFactory")]),e._v("。它类似于使用"),r("code",[e._v("UriComponentsBuilder")]),e._v(",但是,它是一个实际的实例,它保存了配置和首选项,而不是静态工厂方法,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('String baseUrl = "https://example.com";\nDefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);\n\nURI uri = uriBuilderFactory.uriString("/hotels/{hotel}")\n .queryParam("q", "{q}")\n .build("Westin", "123");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val baseUrl = "https://example.com"\nval uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)\n\nval uri = uriBuilderFactory.uriString("/hotels/{hotel}")\n .queryParam("q", "{q}")\n .build("Westin", "123")\n')])])]),r("h4",{attrs:{id:"_1-5-3-uri-编码"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-3-uri-编码"}},[e._v("#")]),e._v(" 1.5.3.URI 编码")]),e._v(" "),r("p",[e._v("Spring MVC 和 Spring WebFlux")]),e._v(" "),r("p",[r("code",[e._v("UriComponentsBuilder")]),e._v("在两个级别上公开编码选项:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html#encode--",target:"_blank",rel:"noopener noreferrer"}},[e._v("uricomponentsbuilder#encode()"),r("OutboundLink")],1),e._v(":先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponents.html#encode--",target:"_blank",rel:"noopener noreferrer"}},[e._v("uricomponents#encode()"),r("OutboundLink")],1),e._v(":编码 URI 组件"),r("em",[e._v("之后")]),e._v("URI 变量被展开。")])])]),e._v(" "),r("p",[e._v("这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而,第一个选项也用 URI 变量中出现的保留意义替换字符。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("考虑一下“;”,它在某种程度上是合法的,但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换"),r("br"),e._v(';";,但不在 URI 模板中。相比之下,第二个选项永远不会'),r("br"),e._v("取代“;”,因为它是路径中的法律字符。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为不透明的数据来进行完全编码,而如果 URI 变量故意包含保留字符,则第二个选项是有用的。当完全不展开 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的内容进行编码。")]),e._v(" "),r("p",[e._v("下面的示例使用了第一个选项:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")\n .queryParam("q", "{q}")\n .encode()\n .buildAndExpand("New York", "foo+bar")\n .toUri();\n\n// Result is "/hotel%20list/New%20York?q=foo%2Bbar"\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")\n .queryParam("q", "{q}")\n .encode()\n .buildAndExpand("New York", "foo+bar")\n .toUri()\n\n// Result is "/hotel%20list/New%20York?q=foo%2Bbar"\n')])])]),r("p",[e._v("可以通过直接访问 URI(这意味着编码)来缩短前面的示例,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")\n .queryParam("q", "{q}")\n .build("New York", "foo+bar");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")\n .queryParam("q", "{q}")\n .build("New York", "foo+bar")\n')])])]),r("p",[e._v("可以使用完整的 URI 模板进一步缩短它,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")\n .build("New York", "foo+bar");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")\n .build("New York", "foo+bar")\n')])])]),r("p",[r("code",[e._v("WebClient")]),e._v("和"),r("code",[e._v("RestTemplate")]),e._v("通过"),r("code",[e._v("UriBuilderFactory")]),e._v("策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('String baseUrl = "https://example.com";\nDefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)\nfactory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);\n\n// Customize the RestTemplate..\nRestTemplate restTemplate = new RestTemplate();\nrestTemplate.setUriTemplateHandler(factory);\n\n// Customize the WebClient..\nWebClient client = WebClient.builder().uriBuilderFactory(factory).build();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val baseUrl = "https://example.com"\nval factory = DefaultUriBuilderFactory(baseUrl).apply {\n encodingMode = EncodingMode.TEMPLATE_AND_VALUES\n}\n\n// Customize the RestTemplate..\nval restTemplate = RestTemplate().apply {\n uriTemplateHandler = factory\n}\n\n// Customize the WebClient..\nval client = WebClient.builder().uriBuilderFactory(factory).build()\n')])])]),r("p",[r("code",[e._v("DefaultUriBuilderFactory")]),e._v("实现在内部使用"),r("code",[e._v("UriComponentsBuilder")]),e._v("来扩展和编码 URI 模板。作为工厂,它提供了一个单独的位置来配置编码方法,该方法基于以下编码模式之一:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("TEMPLATE_AND_VALUES")]),e._v(":使用"),r("code",[e._v("UriComponentsBuilder#encode()")]),e._v("(对应于前面列表中的第一个选项)来预编码 URI 模板,并在展开时严格编码 URI 变量。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("VALUES_ONLY")]),e._v(":不对 URI 模板进行编码,而是通过"),r("code",[e._v("UriUtils#encodeUriVariables")]),e._v("对 URI 变量进行严格编码,然后再将它们扩展到模板中。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("URI_COMPONENT")]),e._v(":使用"),r("code",[e._v("UriComponents#encode()")]),e._v(",对应于前面列表中的第二个选项,来对 URI 组件值的编码"),r("em",[e._v("之后")]),e._v("URI 变量进行展开。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("NONE")]),e._v(":不应用任何编码。")])])]),e._v(" "),r("p",[e._v("由于历史原因和向后兼容,"),r("code",[e._v("RestTemplate")]),e._v("被设置为"),r("code",[e._v("EncodingMode.URI_COMPONENT")]),e._v("。"),r("code",[e._v("WebClient")]),e._v("依赖于"),r("code",[e._v("DefaultUriBuilderFactory")]),e._v("中的默认值,该默认值从 5.0.x 中的"),r("code",[e._v("EncodingMode.URI_COMPONENT")]),e._v("更改为 5.1 中的"),r("code",[e._v("EncodingMode.TEMPLATE_AND_VALUES")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-5-4-相对-servlet-请求"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-4-相对-servlet-请求"}},[e._v("#")]),e._v(" 1.5.4.相对 Servlet 请求")]),e._v(" "),r("p",[e._v("可以使用"),r("code",[e._v("ServletUriComponentsBuilder")]),e._v("创建相对于当前请求的 URI,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('HttpServletRequest request = ...\n\n// Re-uses scheme, host, port, path, and query string...\n\nURI uri = ServletUriComponentsBuilder.fromRequest(request)\n .replaceQueryParam("accountId", "{id}")\n .build("123");\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val request: HttpServletRequest = ...\n\n// Re-uses scheme, host, port, path, and query string...\n\nval uri = ServletUriComponentsBuilder.fromRequest(request)\n .replaceQueryParam("accountId", "{id}")\n .build("123")\n')])])]),r("p",[e._v("你可以创建相对于上下文路径的 URI,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('HttpServletRequest request = ...\n\n// Re-uses scheme, host, port, and context path...\n\nURI uri = ServletUriComponentsBuilder.fromContextPath(request)\n .path("/accounts")\n .build()\n .toUri();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val request: HttpServletRequest = ...\n\n// Re-uses scheme, host, port, and context path...\n\nval uri = ServletUriComponentsBuilder.fromContextPath(request)\n .path("/accounts")\n .build()\n .toUri()\n')])])]),r("p",[e._v("你可以创建相对于 Servlet 的 URI(例如,"),r("code",[e._v("/main/*")]),e._v("),如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('HttpServletRequest request = ...\n\n// Re-uses scheme, host, port, context path, and Servlet mapping prefix...\n\nURI uri = ServletUriComponentsBuilder.fromServletMapping(request)\n .path("/accounts")\n .build()\n .toUri();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val request: HttpServletRequest = ...\n\n// Re-uses scheme, host, port, context path, and Servlet mapping prefix...\n\nval uri = ServletUriComponentsBuilder.fromServletMapping(request)\n .path("/accounts")\n .build()\n .toUri()\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("截至 5.1,"),r("code",[e._v("ServletUriComponentsBuilder")]),e._v("忽略来自"),r("code",[e._v("Forwarded")]),e._v("和"),r("code",[e._v("X-Forwarded-*")]),e._v("头的信息,这两个头指定了客户机发起的地址。考虑使用["),r("code",[e._v("ForwardedHeaderFilter")]),e._v("](#filters-forwarded-header)来提取和使用或丢弃"),r("br"),e._v("这样的 header。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-5-5-与控制器的链接"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-5-与控制器的链接"}},[e._v("#")]),e._v(" 1.5.5.与控制器的链接")]),e._v(" "),r("p",[e._v("Spring MVC 提供了一种用于准备连接到控制器的机制的方法。例如,下面的 MVC 控制器允许创建链接:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@RequestMapping("/hotels/{hotel}")\npublic class BookingController {\n\n @GetMapping("/bookings/{booking}")\n public ModelAndView getBooking(@PathVariable Long booking) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@RequestMapping("/hotels/{hotel}")\nclass BookingController {\n\n @GetMapping("/bookings/{booking}")\n fun getBooking(@PathVariable booking: Long): ModelAndView {\n // ...\n }\n}\n')])])]),r("p",[e._v("你可以通过按名称引用方法来准备链接,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('UriComponents uriComponents = MvcUriComponentsBuilder\n .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42);\n\nURI uri = uriComponents.encode().toUri();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val uriComponents = MvcUriComponentsBuilder\n .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42)\n\nval uri = uriComponents.encode().toUri()\n')])])]),r("p",[e._v("在前面的示例中,我们提供了实际的方法参数值(在本例中,长值:"),r("code",[e._v("21")]),e._v("),将其用作路径变量并插入到 URL 中。此外,我们提供了值"),r("code",[e._v("42")]),e._v("来填充任何剩余的 URI 变量,例如从类型级请求映射继承的"),r("code",[e._v("hotel")]),e._v("变量。如果该方法有更多的参数,我们可以为 URL 不需要的参数提供 NULL。通常,只有"),r("code",[e._v("@PathVariable")]),e._v("和"),r("code",[e._v("@RequestParam")]),e._v("参数与构造 URL 有关。")]),e._v(" "),r("p",[e._v("还有其他使用"),r("code",[e._v("MvcUriComponentsBuilder")]),e._v("的方法。例如,你可以使用一种类似于通过代理模拟测试的技术,以避免按名称引用控制器方法,如下例所示(该示例假定静态导入"),r("code",[e._v("MvcUriComponentsBuilder.on")]),e._v("):")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("UriComponents uriComponents = MvcUriComponentsBuilder\n .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);\n\nURI uri = uriComponents.encode().toUri();\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("val uriComponents = MvcUriComponentsBuilder\n .fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)\n\nval uri = uriComponents.encode().toUri()\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("控制器方法签名在其设计中受到限制,因为它们应该可用于"),r("br"),e._v("与"),r("code",[e._v("fromMethodCall")]),e._v("的链接创建。除了需要适当的参数签名外,"),r("br"),e._v("在返回类型上还有一个技术限制(即,为链接生成器调用生成运行时代理"),r("br"),e._v("),因此返回类型不能是"),r("code",[e._v("final")]),e._v("。特别是,"),r("br"),e._v("视图名称的常见"),r("code",[e._v("String")]),e._v("返回类型在此不工作。你应该使用"),r("code",[e._v("ModelAndView")]),e._v(",甚至使用普通的"),r("code",[e._v("Object")]),e._v("(带有"),r("code",[e._v("String")]),e._v("返回值)。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("较早的示例使用"),r("code",[e._v("MvcUriComponentsBuilder")]),e._v("中的静态方法。在内部,它们依赖"),r("code",[e._v("ServletUriComponentsBuilder")]),e._v("从当前请求的方案、主机、端口、上下文路径和 Servlet 路径准备一个基本 URL。这在大多数情况下都很有效。然而,有时候,这可能是不够的。例如,你可能在请求的上下文之外(例如准备链接的批处理过程),或者你可能需要插入路径前缀(例如从请求路径中删除并需要重新插入到链接中的区域设置前缀)。")]),e._v(" "),r("p",[e._v("对于这种情况,你可以使用静态"),r("code",[e._v("fromXxx")]),e._v("重载方法,该方法接受"),r("code",[e._v("UriComponentsBuilder")]),e._v("来使用基本 URL。或者,你可以使用基本 URL 创建"),r("code",[e._v("MvcUriComponentsBuilder")]),e._v("的实例,然后使用基于实例的"),r("code",[e._v("withXxx")]),e._v("方法。例如,下面的列表使用"),r("code",[e._v("withMethodCall")]),e._v(":")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en");\nMvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base);\nbuilder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42);\n\nURI uri = uriComponents.encode().toUri();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en")\nval builder = MvcUriComponentsBuilder.relativeTo(base)\nbuilder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42)\n\nval uri = uriComponents.encode().toUri()\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("截至 5.1,"),r("code",[e._v("MvcUriComponentsBuilder")]),e._v("忽略来自"),r("code",[e._v("Forwarded")]),e._v("和"),r("code",[e._v("X-Forwarded-*")]),e._v("头的信息,这些头指定了源自客户端的地址。考虑使用"),r("a",{attrs:{href:"#filters-forwarded-headers"}},[e._v("ForwardedHeaderFilter")]),e._v("来提取和使用或丢弃"),r("br"),e._v("这样的标题。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-5-6-视图中的链接"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-5-6-视图中的链接"}},[e._v("#")]),e._v(" 1.5.6.视图中的链接")]),e._v(" "),r("p",[e._v("在 ThymeLeaf、Freemarker 或 JSP 等视图中,你可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注释的控制器的链接。")]),e._v(" "),r("p",[e._v("考虑以下示例:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping("/people/{id}/addresses")\npublic class PersonAddressController {\n\n @RequestMapping("/{country}")\n public HttpEntity getAddress(@PathVariable String country) { ... }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping("/people/{id}/addresses")\nclass PersonAddressController {\n\n @RequestMapping("/{country}")\n fun getAddress(@PathVariable country: String): HttpEntity { ... }\n}\n')])])]),r("p",[e._v("给定上述控制器,你可以从 JSP 准备一个链接,如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("<%@ taglib uri=\"http://www.springframework.org/tags\" prefix=\"s\" %>\n...\nGet Address\n")])])]),r("p",[e._v("前面的示例依赖于 Spring 标记库(即 meta-inf/ Spring.tld)中声明的"),r("code",[e._v("mvcUrl")]),e._v("函数,但是很容易定义自己的函数或为其他模板技术准备类似的函数。")]),e._v(" "),r("p",[e._v("这就是它的工作原理。在启动时,每个"),r("code",[e._v("@RequestMapping")]),e._v("都通过"),r("code",[e._v("HandlerMethodMappingNamingStrategy")]),e._v("分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如,"),r("code",[e._v("getThing")]),e._v("中的"),r("code",[e._v("getThing")]),e._v("方法变成“tc#getthing”)。如果存在名称冲突,可以使用"),r("code",[e._v('@RequestMapping(name="..")')]),e._v("分配显式名称或实现自己的"),r("code",[e._v("HandlerMethodMappingNamingStrategy")]),e._v("。")]),e._v(" "),r("h3",{attrs:{id:"_1-6-异步请求"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-异步请求"}},[e._v("#")]),e._v(" 1.6.异步请求")]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-ann-async-vs-webflux"}},[e._v("与 WebFlux 相比")])]),e._v(" "),r("p",[e._v("Spring MVC 与 Servlet 3.0 异步请求"),r("a",{attrs:{href:"#mvc-ann-async-processing"}},[e._v("processing")]),e._v("具有广泛的集成:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("["),r("code",[e._v("DeferredResult")]),e._v("](#mvc-ann-async-deferredresult)和["),r("code",[e._v("Callable")]),e._v("](#mvc-ann-async-callable)在控制器方法中的返回值为单个异步返回值提供了基本支持。")])]),e._v(" "),r("li",[r("p",[e._v("控制器可以"),r("a",{attrs:{href:"#mvc-ann-async-http-streaming"}},[e._v("stream")]),e._v("多个值,包括"),r("a",{attrs:{href:"#mvc-ann-async-sse"}},[e._v("SSE")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-async-output-stream"}},[e._v("raw data")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("控制器可以使用反应式客户端并返回"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应类型")]),e._v("进行响应处理。")])])]),e._v(" "),r("h4",{attrs:{id:"_1-6-1-deferredresult"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-1-deferredresult"}},[e._v("#")]),e._v(" 1.6.1."),r("code",[e._v("DeferredResult")])]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-ann-async-vs-webflux"}},[e._v("与 WebFlux 相比")])]),e._v(" "),r("p",[e._v("一旦异步请求处理特性在 Servlet 容器中"),r("a",{attrs:{href:"#mvc-ann-async-configuration"}},[e._v("enabled")]),e._v(",控制器方法就可以用"),r("code",[e._v("DeferredResult")]),e._v("包装任何受支持的控制器方法返回值,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/quotes")\n@ResponseBody\npublic DeferredResult quotes() {\n DeferredResult deferredResult = new DeferredResult();\n // Save the deferredResult somewhere..\n return deferredResult;\n}\n\n// From some other thread...\ndeferredResult.setResult(result);\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/quotes")\n@ResponseBody\nfun quotes(): DeferredResult {\n val deferredResult = DeferredResult()\n // Save the deferredResult somewhere..\n return deferredResult\n}\n\n// From some other thread...\ndeferredResult.setResult(result)\n')])])]),r("p",[e._v("控制器可以异步地从不同的线程产生返回值——例如,响应外部事件(JMS 消息)、计划任务或其他事件。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-2-callable"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-2-callable"}},[e._v("#")]),e._v(" 1.6.2."),r("code",[e._v("Callable")])]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-ann-async-vs-webflux"}},[e._v("与 WebFlux 相比")])]),e._v(" "),r("p",[e._v("控制器可以用"),r("code",[e._v("java.util.concurrent.Callable")]),e._v("包装任何受支持的返回值,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping\npublic Callable processUpload(final MultipartFile file) {\n\n return new Callable() {\n public String call() throws Exception {\n // ...\n return "someView";\n }\n };\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@PostMapping\nfun processUpload(file: MultipartFile) = Callable {\n // ...\n "someView"\n}\n')])])]),r("p",[e._v("然后可以通过"),r("a",{attrs:{href:"#mvc-ann-async-configuration-spring-mvc"}},[e._v("configured")]),r("code",[e._v("TaskExecutor")]),e._v("运行给定任务来获得返回值。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-3-处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-3-处理"}},[e._v("#")]),e._v(" 1.6.3.处理")]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-ann-async-vs-webflux"}},[e._v("与 WebFlux 相比")])]),e._v(" "),r("p",[e._v("下面是 Servlet 异步请求处理的一个非常简明的概述:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("可以通过调用"),r("code",[e._v("request.startAsync()")]),e._v("将"),r("code",[e._v("ServletRequest")]),e._v("置于异步模式。这样做的主要效果是, Servlet(以及任何过滤器)都可以退出,但响应仍然是开放的,以便稍后完成处理。")])]),e._v(" "),r("li",[r("p",[e._v("对"),r("code",[e._v("request.startAsync()")]),e._v("的调用返回"),r("code",[e._v("AsyncContext")]),e._v(",你可以使用它来进一步控制异步处理。例如,它提供了"),r("code",[e._v("dispatch")]),e._v("方法,它类似于来自 Servlet API 的转发,只是它允许应用程序在 Servlet 容器线程上恢复请求处理。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("ServletRequest")]),e._v("提供对当前"),r("code",[e._v("DispatcherType")]),e._v("的访问,你可以使用它来区分处理初始请求、异步分派、转发和其他 Dispatcher 类型。")])])]),e._v(" "),r("p",[r("code",[e._v("DeferredResult")]),e._v("处理工作如下:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("控制器返回一个"),r("code",[e._v("DeferredResult")]),e._v(",并将其保存在内存队列或列表中,以便访问它。")])]),e._v(" "),r("li",[r("p",[e._v("Spring MVC 调用"),r("code",[e._v("request.startAsync()")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("同时,"),r("code",[e._v("DispatcherServlet")]),e._v("和所有配置的过滤器退出请求处理线程,但响应仍然是打开的。")])]),e._v(" "),r("li",[r("p",[e._v("应用程序设置来自某个线程的"),r("code",[e._v("DeferredResult")]),e._v(",然后 Spring MVC 将请求分派回 Servlet 容器。")])]),e._v(" "),r("li",[r("p",[e._v("将再次调用"),r("code",[e._v("DispatcherServlet")]),e._v(",并继续使用异步产生的返回值进行处理。")])])]),e._v(" "),r("p",[r("code",[e._v("Callable")]),e._v("处理工作如下:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("控制器返回"),r("code",[e._v("Callable")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("Spring MVC 调用"),r("code",[e._v("request.startAsync()")]),e._v(",并将"),r("code",[e._v("Callable")]),e._v("提交给"),r("code",[e._v("TaskExecutor")]),e._v(",以便在单独的线程中进行处理。")])]),e._v(" "),r("li",[r("p",[e._v("同时,"),r("code",[e._v("DispatcherServlet")]),e._v("和所有过滤器退出 Servlet 容器线程,但响应仍然是打开的。")])]),e._v(" "),r("li",[r("p",[e._v("最终,"),r("code",[e._v("Callable")]),e._v("产生一个结果, Spring MVC 将请求分派回 Servlet 容器以完成处理。")])]),e._v(" "),r("li",[r("p",[e._v("将再次调用"),r("code",[e._v("DispatcherServlet")]),e._v(",并使用来自"),r("code",[e._v("Callable")]),e._v("的异步产生的返回值恢复处理。")])])]),e._v(" "),r("p",[e._v("对于进一步的背景和上下文,你还可以阅读 Spring MVC3.2 中引入了异步请求处理支持的"),r("a",{attrs:{href:"https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support",target:"_blank",rel:"noopener noreferrer"}},[e._v("博客文章"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"异常处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#异常处理"}},[e._v("#")]),e._v(" 异常处理")]),e._v(" "),r("p",[e._v("当使用"),r("code",[e._v("DeferredResult")]),e._v("时,可以选择是否调用"),r("code",[e._v("setResult")]),e._v("或"),r("code",[e._v("setErrorResult")]),e._v(",但有例外。在这两种情况下, Spring MVC 将请求分派回 Servlet 容器以完成处理。然后将其视为控制器方法返回给定的值或产生给定的异常。然后,异常将通过常规的异常处理机制(例如,调用"),r("code",[e._v("@ExceptionHandler")]),e._v("方法)。")]),e._v(" "),r("p",[e._v("当使用"),r("code",[e._v("Callable")]),e._v("时,会出现类似的处理逻辑,主要的区别是从"),r("code",[e._v("Callable")]),e._v("返回结果,或者由它引发异常。")]),e._v(" "),r("h5",{attrs:{id:"拦截"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#拦截"}},[e._v("#")]),e._v(" 拦截")]),e._v(" "),r("p",[r("code",[e._v("HandlerInterceptor")]),e._v("实例可以是类型"),r("code",[e._v("AsyncHandlerInterceptor")]),e._v("的,以便在开始异步处理的初始请求上接收"),r("code",[e._v("afterConcurrentHandlingStarted")]),e._v("回调(而不是"),r("code",[e._v("postHandle")]),e._v("和"),r("code",[e._v("afterCompletion")]),e._v(")。")]),e._v(" "),r("p",[r("code",[e._v("HandlerInterceptor")]),e._v("实现还可以注册"),r("code",[e._v("CallableProcessingInterceptor")]),e._v("或"),r("code",[e._v("DeferredResultProcessingInterceptor")]),e._v(",以更深入地与异步请求的生命周期集成(例如,处理超时事件)。详见["),r("code",[e._v("AsyncHandlerInterceptor")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/asynchandlerinterceptor.html)。")]),e._v(" "),r("p",[r("code",[e._v("DeferredResult")]),e._v("提供"),r("code",[e._v("onTimeout(Runnable)")]),e._v("和"),r("code",[e._v("onCompletion(Runnable)")]),e._v("回调。有关更多详细信息,请参见"),r("code",[e._v("DeferredResult")]),e._v("的[javadoc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/request/async/deferredresult.html)。"),r("code",[e._v("Callable")]),e._v("可以替代"),r("code",[e._v("WebAsyncTask")]),e._v(",后者公开了用于超时和完成回调的其他方法。")]),e._v(" "),r("h5",{attrs:{id:"与-webflux-相比"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#与-webflux-相比"}},[e._v("#")]),e._v(" 与 WebFlux 相比")]),e._v(" "),r("p",[e._v("Servlet API 最初是为通过过滤器- Servlet 链而构建的。 Servlet 3.0 中添加的异步请求处理允许应用程序退出 Filter- Servlet 链,但允许响应开放以进行进一步的处理。 Spring MVC 异步支持是围绕该机制构建的。当控制器返回"),r("code",[e._v("DeferredResult")]),e._v("时,退出 filter- Servlet 链,并释放 Servlet 容器线程。稍后,当设置"),r("code",[e._v("DeferredResult")]),e._v("时,将执行"),r("code",[e._v("ASYNC")]),e._v("调度(发送到相同的 URL),在此期间将再次映射控制器,但是将使用"),r("code",[e._v("DeferredResult")]),e._v("值(就像控制器返回了它一样)来恢复处理。")]),e._v(" "),r("p",[e._v("相比之下, Spring WebFlux 既不是建立在 Servlet API 上的,也不需要这样的异步请求处理特性,因为它在设计上是异步的。异步处理被内置在所有框架契约中,并且在请求处理的所有阶段都得到了本质上的支持。")]),e._v(" "),r("p",[e._v("从编程模型的角度来看, Spring MVC 和 Spring WebFlux 都支持异步和作为控制器方法中的返回值。 Spring MVC 甚至支持流媒体,包括反压力反应。然而,对响应的单独写仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同,后者依赖于非阻塞的 I/O,并且不需要为每个写增加一个线程。")]),e._v(" "),r("p",[e._v("另一个根本的区别是 Spring MVC 不支持控制器方法参数中的异步或反应类型(例如,"),r("code",[e._v("@RequestBody")]),e._v(","),r("code",[e._v("@RequestPart")]),e._v(",以及其他),也不明确支持异步和反应类型作为模型属性。 Spring WebFlux 确实支持这一切。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-4-http-流媒体"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-4-http-流媒体"}},[e._v("#")]),e._v(" 1.6.4.HTTP 流媒体")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-codecs-streaming"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("对于单个异步返回值,可以使用"),r("code",[e._v("DeferredResult")]),e._v("和"),r("code",[e._v("Callable")]),e._v("。如果你想要产生多个异步值并将其写入响应中,该怎么办?这一节描述了如何做到这一点。")]),e._v(" "),r("h5",{attrs:{id:"对象"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#对象"}},[e._v("#")]),e._v(" 对象")]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("ResponseBodyEmitter")]),e._v("返回值来生成一个对象流,其中每个对象都使用["),r("code",[e._v("HttpMessageConverter")]),e._v("](integration.html#rest-message-conversion)序列化并写入响应,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/events")\npublic ResponseBodyEmitter handle() {\n ResponseBodyEmitter emitter = new ResponseBodyEmitter();\n // Save the emitter somewhere..\n return emitter;\n}\n\n// In some other thread\nemitter.send("Hello once");\n\n// and again later on\nemitter.send("Hello again");\n\n// and done at some point\nemitter.complete();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/events")\nfun handle() = ResponseBodyEmitter().apply {\n // Save the emitter somewhere..\n}\n\n// In some other thread\nemitter.send("Hello once")\n\n// and again later on\nemitter.send("Hello again")\n\n// and done at some point\nemitter.complete()\n')])])]),r("p",[e._v("你还可以使用"),r("code",[e._v("ResponseBodyEmitter")]),e._v("作为"),r("code",[e._v("ResponseEntity")]),e._v("中的主体,让你自定义响应的状态和标题。")]),e._v(" "),r("p",[e._v("当"),r("code",[e._v("emitter")]),e._v("抛出"),r("code",[e._v("IOException")]),e._v("时(例如,如果远程客户端消失了),应用程序不负责清理连接,并且不应调用"),r("code",[e._v("emitter.complete")]),e._v("或"),r("code",[e._v("emitter.completeWithError")]),e._v("。相反, Servlet 容器自动发起"),r("code",[e._v("AsyncListener")]),e._v("错误通知,其中 Spring MVC 进行"),r("code",[e._v("completeWithError")]),e._v("调用。然后,这个调用执行对应用程序的最后一次"),r("code",[e._v("ASYNC")]),e._v("分派,在此期间, Spring MVC 调用配置的异常解析器并完成请求。")]),e._v(" "),r("h5",{attrs:{id:"sse"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#sse"}},[e._v("#")]),e._v(" SSE")]),e._v(" "),r("p",[r("code",[e._v("SseEmitter")]),e._v("("),r("code",[e._v("ResponseBodyEmitter")]),e._v("的子类)提供对"),r("a",{attrs:{href:"https://www.w3.org/TR/eventsource/",target:"_blank",rel:"noopener noreferrer"}},[e._v("服务器发送的事件"),r("OutboundLink")],1),e._v("的支持,其中从服务器发送的事件是根据 W3C SSE 规范进行格式化的。要从控制器生成 SSE 流,请返回"),r("code",[e._v("SseEmitter")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE)\npublic SseEmitter handle() {\n SseEmitter emitter = new SseEmitter();\n // Save the emitter somewhere..\n return emitter;\n}\n\n// In some other thread\nemitter.send("Hello once");\n\n// and again later on\nemitter.send("Hello again");\n\n// and done at some point\nemitter.complete();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE])\nfun handle() = SseEmitter().apply {\n // Save the emitter somewhere..\n}\n\n// In some other thread\nemitter.send("Hello once")\n\n// and again later on\nemitter.send("Hello again")\n\n// and done at some point\nemitter.complete()\n')])])]),r("p",[e._v("虽然 SSE 是流媒体进入浏览器的主要选项,但请注意,Internet Explorer 不支持服务器发送的事件。考虑使用 Spring 的"),r("a",{attrs:{href:"#websocket"}},[e._v("WebSocket messaging")]),e._v("和"),r("a",{attrs:{href:"#websocket-fallback"}},[e._v("Sockjs 后援")]),e._v("传输(包括 SSE),这些传输针对广泛的浏览器。")]),e._v(" "),r("p",[e._v("另请参见"),r("a",{attrs:{href:"#mvc-ann-async-objects"}},[e._v("上一节")]),e._v("有关异常处理的说明。")]),e._v(" "),r("h5",{attrs:{id:"原始数据"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#原始数据"}},[e._v("#")]),e._v(" 原始数据")]),e._v(" "),r("p",[e._v("有时,绕过消息转换并直接流到响应"),r("code",[e._v("OutputStream")]),e._v("是有用的(例如,用于文件下载)。你可以使用"),r("code",[e._v("StreamingResponseBody")]),e._v("返回值类型来执行此操作,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/download")\npublic StreamingResponseBody handle() {\n return new StreamingResponseBody() {\n @Override\n public void writeTo(OutputStream outputStream) throws IOException {\n // write...\n }\n };\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/download")\nfun handle() = StreamingResponseBody {\n // write...\n}\n')])])]),r("p",[e._v("你可以使用"),r("code",[e._v("StreamingResponseBody")]),e._v("作为"),r("code",[e._v("ResponseEntity")]),e._v("中的主体来定制响应的状态和标题。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-5-反应类型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-5-反应类型"}},[e._v("#")]),e._v(" 1.6.5.反应类型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-codecs-streaming"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 支持在控制器中使用反应式客户端库(在 WebFlux 部分中也读)。这包括来自"),r("code",[e._v("WebClient")]),e._v("的"),r("code",[e._v("spring-webflux")]),e._v("和其他的,例如 Spring 数据反应性数据存储库。在这样的场景中,能够从控制器方法返回无功类型是很方便的。")]),e._v(" "),r("p",[e._v("反应性返回值的处理如下:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("对单值 promise 进行了调整,类似于使用"),r("code",[e._v("DeferredResult")]),e._v("。例如"),r("code",[e._v("Mono")]),e._v("或"),r("code",[e._v("Single")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("具有流媒体类型的多值流(例如"),r("code",[e._v("application/x-ndjson")]),e._v("或"),r("code",[e._v("text/event-stream")]),e._v(")被适配,类似于使用"),r("code",[e._v("ResponseBodyEmitter")]),e._v("或"),r("code",[e._v("SseEmitter")]),e._v("。例如"),r("code",[e._v("Flux")]),e._v("或"),r("code",[e._v("Observable")]),e._v("。应用程序还可以返回"),r("code",[e._v("Flux")]),e._v("或"),r("code",[e._v("Observable")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("具有任意其他媒体类型(例如"),r("code",[e._v("application/json")]),e._v(")的多值流被适配,类似于使用"),r("code",[e._v("DeferredResult>")]),e._v("。")])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring MVC 通过["),r("code",[e._v("ReactiveAdapterRegistry")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/reactiveadapterregistry.html)从"),r("code",[e._v("spring-core")]),e._v("支持 reactor 和 rxjava,这使得它能够适应多个反应库。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("对于流到响应,支持反应背压,但是写到响应仍然是阻塞的,并且是通过"),r("a",{attrs:{href:"#mvc-ann-async-configuration-spring-mvc"}},[e._v("configured")]),r("code",[e._v("TaskExecutor")]),e._v("在单独的线程上运行的,以避免阻塞上游的源(例如从"),r("code",[e._v("Flux")]),e._v("返回的"),r("code",[e._v("WebClient")]),e._v(")。默认情况下,"),r("code",[e._v("SimpleAsyncTaskExecutor")]),e._v("用于阻塞写操作,但这在加载时不适用。如果计划使用反应式类型的流,则应该使用"),r("a",{attrs:{href:"#mvc-ann-async-configuration-spring-mvc"}},[e._v("MVC 配置")]),e._v("来配置任务执行器。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-6-断开连接"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-6-断开连接"}},[e._v("#")]),e._v(" 1.6.6.断开连接")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-codecs-streaming"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Servlet 当远程客户端消失时,API 不提供任何通知。因此,在流到响应的同时,无论是通过"),r("a",{attrs:{href:"#mvc-ann-async-sse"}},[e._v("SseEmitter")]),e._v("还是"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应类型")]),e._v(",定期发送数据是很重要的,因为如果客户端已断开连接,则写操作就会失败。发送可以采取空(仅限注释)SSE 事件的形式,或者任何其他数据,而另一方必须将其解释为心跳并忽略这些数据。")]),e._v(" "),r("p",[e._v("或者,考虑使用具有内置心跳机制的 Web 消息传递解决方案(例如"),r("a",{attrs:{href:"#websocket-stomp"}},[e._v("STOMP over WebSocket")]),e._v("或 WebSocket with"),r("a",{attrs:{href:"#websocket-fallback"}},[e._v("SockJS")]),e._v(")。")]),e._v(" "),r("h4",{attrs:{id:"_1-6-7-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-6-7-配置"}},[e._v("#")]),e._v(" 1.6.7.配置")]),e._v(" "),r("p",[r("a",{attrs:{href:"#mvc-ann-async-vs-webflux"}},[e._v("与 WebFlux 相比")])]),e._v(" "),r("p",[e._v("异步请求处理特性必须在 Servlet 容器级别上启用。MVC 配置还为异步请求公开了几个选项。")]),e._v(" "),r("h5",{attrs:{id:"servlet-集装箱"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#servlet-集装箱"}},[e._v("#")]),e._v(" Servlet 集装箱")]),e._v(" "),r("p",[e._v("Filter 和 Servlet 声明有一个"),r("code",[e._v("asyncSupported")]),e._v("标志,需要将其设置为"),r("code",[e._v("true")]),e._v(",以启用异步请求处理。此外,应该声明过滤器映射来处理"),r("code",[e._v("ASYNC``javax.servlet.DispatchType")]),e._v("。")]),e._v(" "),r("p",[e._v("在 爪哇 配置中,当你使用"),r("code",[e._v("AbstractAnnotationConfigDispatcherServletInitializer")]),e._v("初始化 Servlet 容器时,这是自动完成的。")]),e._v(" "),r("p",[e._v("在"),r("code",[e._v("web.xml")]),e._v("配置 中,可以将"),r("code",[e._v("true")]),e._v("添加到"),r("code",[e._v("DispatcherServlet")]),e._v("声明和"),r("code",[e._v("Filter")]),e._v("声明中,并添加"),r("code",[e._v("ASYNC")]),e._v("以过滤映射。")]),e._v(" "),r("h5",{attrs:{id:"spring-mvc"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#spring-mvc"}},[e._v("#")]),e._v(" Spring MVC")]),e._v(" "),r("p",[e._v("MVC 配置公开了以下与异步请求处理相关的选项:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("爪哇 配置:在"),r("code",[e._v("WebMvcConfigurer")]),e._v("上使用"),r("code",[e._v("configureAsyncSupport")]),e._v("回调。")])]),e._v(" "),r("li",[r("p",[e._v("XML 命名空间:使用"),r("code",[e._v("")]),e._v("下的"),r("code",[e._v("")]),e._v("元素。")])])]),e._v(" "),r("p",[e._v("你可以配置以下内容:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("异步请求的默认超时值(如果未设置该超时值)取决于底层 Servlet 容器。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("AsyncTaskExecutor")]),e._v("用于在使用"),r("a",{attrs:{href:"#mvc-ann-async-reactive-types"}},[e._v("反应类型")]),e._v("进行流式传输时阻止写操作,以及用于执行从控制器方法返回的"),r("code",[e._v("Callable")]),e._v("实例。我们强烈建议配置此属性,如果你使用无反应类型的流或具有返回"),r("code",[e._v("Callable")]),e._v("的控制器方法,因为默认情况下,它是"),r("code",[e._v("SimpleAsyncTaskExecutor")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("DeferredResultProcessingInterceptor")]),e._v("实现和"),r("code",[e._v("CallableProcessingInterceptor")]),e._v("实现。")])])]),e._v(" "),r("p",[e._v("请注意,你还可以在"),r("code",[e._v("DeferredResult")]),e._v("、"),r("code",[e._v("ResponseBodyEmitter")]),e._v("和"),r("code",[e._v("SseEmitter")]),e._v("上设置默认的超时值。对于"),r("code",[e._v("Callable")]),e._v(",可以使用"),r("code",[e._v("WebAsyncTask")]),e._v("来提供超时值。")]),e._v(" "),r("h3",{attrs:{id:"_1-7-科尔斯"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-科尔斯"}},[e._v("#")]),e._v(" 1.7.科尔斯")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 允许你处理 CORS(跨源资源共享)。这一节描述了如何做到这一点。")]),e._v(" "),r("h4",{attrs:{id:"_1-7-1-导言"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-1-导言"}},[e._v("#")]),e._v(" 1.7.1.导言")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors-intro"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("出于安全原因,浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如,你可以在一个标签中设置你的银行帐户,而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如,从你的帐户中取款!")]),e._v(" "),r("p",[e._v("跨源资源共享是由"),r("a",{attrs:{href:"https://caniuse.com/#feat=cors",target:"_blank",rel:"noopener noreferrer"}},[e._v("大多数浏览器"),r("OutboundLink")],1),e._v("实现的"),r("a",{attrs:{href:"https://www.w3.org/TR/cors/",target:"_blank",rel:"noopener noreferrer"}},[e._v("W3C 规范"),r("OutboundLink")],1),e._v(",它允许你指定授权哪种类型的跨域请求,而不是使用基于 IFRAME 或 JSONP 的安全性较低、功能较弱的解决方案。")]),e._v(" "),r("h4",{attrs:{id:"_1-7-2-处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-2-处理"}},[e._v("#")]),e._v(" 1.7.2.处理")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors-processing"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("CORS 规范区分了飞行前、简单和实际请求。要了解 CORS 的工作原理,你可以阅读"),r("a",{attrs:{href:"https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS",target:"_blank",rel:"noopener noreferrer"}},[e._v("这篇文章"),r("OutboundLink")],1),e._v("等,或者查看规范以获得更多详细信息。")]),e._v(" "),r("p",[e._v("Spring MVC实现方式提供了对 CORS 的内置支持。在成功地将一个请求映射到一个处理程序之后,"),r("code",[e._v("HandlerMapping")]),e._v("实现将检查 CORS 配置中给定的请求和处理程序,并采取进一步的操作。前置请求是直接处理的,而简单和实际的 CORS 请求是截获、验证的,并且设置了所需的 CORS 响应头。")]),e._v(" "),r("p",[e._v("为了启用跨源请求(即存在"),r("code",[e._v("Origin")]),e._v("头并与请求的主机不同),你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。")]),e._v(" "),r("p",[e._v("每个"),r("code",[e._v("HandlerMapping")]),e._v("都可以单独使用基于 URL 模式的"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/handler/AbstractHandlerMapping.html#setCors%E9%85%8D%E7%BD%AEs-java.util.Map-",target:"_blank",rel:"noopener noreferrer"}},[e._v("configured"),r("OutboundLink")],1),e._v("映射"),r("code",[e._v("CorsConfiguration")]),e._v("。在大多数情况下,应用程序使用 MVC 爪哇 配置或 XML 命名空间来声明这样的映射,这会导致将单个全局映射传递给所有"),r("code",[e._v("HandlerMapping")]),e._v("实例。")]),e._v(" "),r("p",[e._v("你可以将"),r("code",[e._v("HandlerMapping")]),e._v("级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如,带注释的控制器可以使用类或方法级别的"),r("code",[e._v("@CrossOrigin")]),e._v("注释(其他处理程序可以实现"),r("code",[e._v("CorsConfigurationSource")]),e._v(")。")]),e._v(" "),r("p",[e._v("结合全局和局部配置的规则通常是累加的——例如,所有全局配置和所有局部配置。对于那些只能接受单个值的属性,例如。"),r("code",[e._v("allowCredentials")]),e._v("和"),r("code",[e._v("maxAge")]),e._v(",局部重写全局值。详见["),r("code",[e._v("CorsConfiguration#combine(CorsConfiguration)")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/corsconfiguration.html#combine-org.springframework.web.cors.corsconfiguration-)。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("要从源代码中了解更多信息或进行高级定制,请检查后面的代码:"),r("br"),r("br"),r("em",[r("code",[e._v("CorsConfiguration")]),r("br"),r("br")]),r("code",[e._v("CorsProcessor")]),e._v(","),r("code",[e._v("DefaultCorsProcessor")]),r("br"),r("code",[e._v("AbstractHandlerMapping")])])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-7-3-crossorigin"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-3-crossorigin"}},[e._v("#")]),e._v(" 1.7.3."),r("code",[e._v("@CrossOrigin")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors-controller"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("["),r("code",[e._v("@CrossOrigin")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/crossorigin.html)注解可以在带注释的控制器方法上实现跨源请求,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\n@RequestMapping("/account")\npublic class AccountController {\n\n @CrossOrigin\n @GetMapping("/{id}")\n public Account retrieve(@PathVariable Long id) {\n // ...\n }\n\n @DeleteMapping("/{id}")\n public void remove(@PathVariable Long id) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RestController\n@RequestMapping("/account")\nclass AccountController {\n\n @CrossOrigin\n @GetMapping("/{id}")\n fun retrieve(@PathVariable id: Long): Account {\n // ...\n }\n\n @DeleteMapping("/{id}")\n fun remove(@PathVariable id: Long) {\n // ...\n }\n}\n')])])]),r("p",[e._v("默认情况下,"),r("code",[e._v("@CrossOrigin")]),e._v("允许:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("所有的起源。")])]),e._v(" "),r("li",[r("p",[e._v("所有标题。")])]),e._v(" "),r("li",[r("p",[e._v("将控制器方法映射到的所有 HTTP 方法。")])])]),e._v(" "),r("p",[r("code",[e._v("allowCredentials")]),e._v("默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用"),r("code",[e._v("allowOrigins")]),e._v("时,要么必须将"),r("code",[e._v("allowOrigins")]),e._v("设置为一个或多个特定的域(但不是特殊值"),r("code",[e._v('"*"')]),e._v("),要么将"),r("code",[e._v("allowOriginPatterns")]),e._v("属性替换为可用于匹配到源集的动态。")]),e._v(" "),r("p",[r("code",[e._v("maxAge")]),e._v("设置为 30 分钟。")]),e._v(" "),r("p",[r("code",[e._v("@CrossOrigin")]),e._v("在类级别上也是支持的,并且所有方法都会继承它,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)\n@RestController\n@RequestMapping("/account")\npublic class AccountController {\n\n @GetMapping("/{id}")\n public Account retrieve(@PathVariable Long id) {\n // ...\n }\n\n @DeleteMapping("/{id}")\n public void remove(@PathVariable Long id) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600)\n@RestController\n@RequestMapping("/account")\nclass AccountController {\n\n @GetMapping("/{id}")\n fun retrieve(@PathVariable id: Long): Account {\n // ...\n }\n\n @DeleteMapping("/{id}")\n fun remove(@PathVariable id: Long) {\n // ...\n }\n')])])]),r("p",[e._v("可以在类级别和方法级别使用"),r("code",[e._v("@CrossOrigin")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@CrossOrigin(maxAge = 3600)\n@RestController\n@RequestMapping("/account")\npublic class AccountController {\n\n @CrossOrigin("https://domain2.com")\n @GetMapping("/{id}")\n public Account retrieve(@PathVariable Long id) {\n // ...\n }\n\n @DeleteMapping("/{id}")\n public void remove(@PathVariable Long id) {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@CrossOrigin(maxAge = 3600)\n@RestController\n@RequestMapping("/account")\nclass AccountController {\n\n @CrossOrigin("https://domain2.com")\n @GetMapping("/{id}")\n fun retrieve(@PathVariable id: Long): Account {\n // ...\n }\n\n @DeleteMapping("/{id}")\n fun remove(@PathVariable id: Long) {\n // ...\n }\n}\n')])])]),r("h4",{attrs:{id:"_1-7-4-全局配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-4-全局配置"}},[e._v("#")]),e._v(" 1.7.4.全局配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors-global"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("除了细粒度的控制器方法级配置外,你可能还需要定义一些全局 CORS 配置。你可以在任何"),r("code",[e._v("HandlerMapping")]),e._v("上单独设置基于 URL 的"),r("code",[e._v("CorsConfiguration")]),e._v("映射。然而,大多数应用程序使用 MVC 爪哇 配置或 MVC XML 命名空间来实现这一点。")]),e._v(" "),r("p",[e._v("默认情况下,全局配置启用以下功能:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("所有的起源。")])]),e._v(" "),r("li",[r("p",[e._v("所有标题。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("GET")]),e._v(","),r("code",[e._v("HEAD")]),e._v(",和"),r("code",[e._v("POST")]),e._v("方法。")])])]),e._v(" "),r("p",[r("code",[e._v("allowCredentials")]),e._v("默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用"),r("code",[e._v("allowOrigins")]),e._v("时,要么必须将"),r("code",[e._v("allowOrigins")]),e._v("设置为一个或多个特定的域(但不是特殊值"),r("code",[e._v('"*"')]),e._v("),要么可选择将"),r("code",[e._v("allowOriginPatterns")]),e._v("属性用于匹配到源集的动态。")]),e._v(" "),r("p",[r("code",[e._v("maxAge")]),e._v("设置为 30 分钟。")]),e._v(" "),r("h5",{attrs:{id:"爪哇-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#爪哇-配置"}},[e._v("#")]),e._v(" 爪哇 配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-cors-global"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("要在 MVC 爪哇 配置中启用 CORS,可以使用"),r("code",[e._v("CorsRegistry")]),e._v("回调,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addCorsMappings(CorsRegistry registry) {\n\n registry.addMapping("/api/**")\n .allowedOrigins("https://domain2.com")\n .allowedMethods("PUT", "DELETE")\n .allowedHeaders("header1", "header2", "header3")\n .exposedHeaders("header1", "header2")\n .allowCredentials(true).maxAge(3600);\n\n // Add more mappings...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addCorsMappings(registry: CorsRegistry) {\n\n registry.addMapping("/api/**")\n .allowedOrigins("https://domain2.com")\n .allowedMethods("PUT", "DELETE")\n .allowedHeaders("header1", "header2", "header3")\n .exposedHeaders("header1", "header2")\n .allowCredentials(true).maxAge(3600)\n\n // Add more mappings...\n }\n}\n')])])]),r("h5",{attrs:{id:"xml-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#xml-配置"}},[e._v("#")]),e._v(" XML 配置")]),e._v(" "),r("p",[e._v("要在 XML 名称空间中启用 COR,可以使用"),r("code",[e._v("")]),e._v("元素,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n\n \n\n\n')])])]),r("h4",{attrs:{id:"_1-7-5-cors-过滤器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-7-5-cors-过滤器"}},[e._v("#")]),e._v(" 1.7.5.CORS 过滤器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/webflux-cors.html#webflux-cors-webfilter"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以通过内置的["),r("code",[e._v("CorsFilter")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/filter/corsfilter.html)应用 CORS 支持。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果尝试使用带有 Spring 安全性的"),r("code",[e._v("CorsFilter")]),e._v(",请记住,对于 CORS,"),r("br"),e._v(" Spring 安全性具有"),r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors",target:"_blank",rel:"noopener noreferrer"}},[e._v("内置支持"),r("OutboundLink")],1),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("要配置过滤器,请将"),r("code",[e._v("CorsConfigurationSource")]),e._v("传递到其构造函数,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('CorsConfiguration config = new CorsConfiguration();\n\n// Possibly...\n// config.applyPermitDefaultValues()\n\nconfig.setAllowCredentials(true);\nconfig.addAllowedOrigin("https://domain1.com");\nconfig.addAllowedHeader("*");\nconfig.addAllowedMethod("*");\n\nUrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();\nsource.registerCorsConfiguration("/**", config);\n\nCorsFilter filter = new CorsFilter(source);\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('val config = CorsConfiguration()\n\n// Possibly...\n// config.applyPermitDefaultValues()\n\nconfig.allowCredentials = true\nconfig.addAllowedOrigin("https://domain1.com")\nconfig.addAllowedHeader("*")\nconfig.addAllowedMethod("*")\n\nval source = UrlBasedCorsConfigurationSource()\nsource.registerCorsConfiguration("/**", config)\n\nval filter = CorsFilter(source)\n')])])]),r("h3",{attrs:{id:"_1-8-网络安全"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-8-网络安全"}},[e._v("#")]),e._v(" 1.8.网络安全")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-web-security"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("a",{attrs:{href:"https://projects.spring.io/spring-security/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security"),r("OutboundLink")],1),e._v("项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档,包括:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/html5/#mvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring MVC Security"),r("OutboundLink")],1)])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-mockmvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring MVC Test Support"),r("OutboundLink")],1)])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf",target:"_blank",rel:"noopener noreferrer"}},[e._v("CSRF 保护"),r("OutboundLink")],1)])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers",target:"_blank",rel:"noopener noreferrer"}},[e._v("安全响应标头"),r("OutboundLink")],1)])])]),e._v(" "),r("p",[r("a",{attrs:{href:"https://hdiv.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("HDIV"),r("OutboundLink")],1),e._v("是与 Spring MVC 集成的另一种 Web 安全框架。")]),e._v(" "),r("h3",{attrs:{id:"_1-9-http-缓存"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-9-http-缓存"}},[e._v("#")]),e._v(" 1.9.HTTP 缓存")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-caching"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕"),r("code",[e._v("Cache-Control")]),e._v("响应头,随后是条件请求头(例如"),r("code",[e._v("Last-Modified")]),e._v("和"),r("code",[e._v("ETag")]),e._v(")。"),r("code",[e._v("Cache-Control")]),e._v("建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。"),r("code",[e._v("ETag")]),e._v("报头用于发出条件请求,如果内容没有更改,则该请求可能在没有正文的情况下导致 304(未 _modified)。"),r("code",[e._v("ETag")]),e._v("可以看作是"),r("code",[e._v("Last-Modified")]),e._v("标头的更复杂的继承。")]),e._v(" "),r("p",[e._v("本节描述了 Spring Web MVC 中可用的与 HTTP 缓存相关的选项。")]),e._v(" "),r("h4",{attrs:{id:"_1-9-1-cachecontrol"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-9-1-cachecontrol"}},[e._v("#")]),e._v(" 1.9.1."),r("code",[e._v("CacheControl")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-caching-cachecontrol"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("["),r("code",[e._v("CacheControl")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/cachecontrol.html)提供了对配置"),r("code",[e._v("Cache-Control")]),e._v("标头相关设置的支持,并在许多地方被接受为参数:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("["),r("code",[e._v("WebContentInterceptor")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/mvc/webcontentinterceptor.html)")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("WebContentGenerator")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/support/webcontentgenerator.html)")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-caching-etag-lastmodified"}},[e._v("控制器")])])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"#mvc-caching-static-resources"}},[e._v("静态资源")])])])]),e._v(" "),r("p",[e._v("虽然"),r("a",{attrs:{href:"https://tools.ietf.org/html/rfc7234#section-5.2.2",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC 7234"),r("OutboundLink")],1),e._v("描述了"),r("code",[e._v("Cache-Control")]),e._v("响应头的所有可能的指令,但"),r("code",[e._v("CacheControl")]),e._v("类型采用了一种面向用例的方法,该方法关注于常见的场景:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// Cache for an hour - "Cache-Control: max-age=3600"\nCacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);\n\n// Prevent caching - "Cache-Control: no-store"\nCacheControl ccNoStore = CacheControl.noStore();\n\n// Cache for ten days in public and private caches,\n// public caches should not transform the response\n// "Cache-Control: max-age=864000, public, no-transform"\nCacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('// Cache for an hour - "Cache-Control: max-age=3600"\nval ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)\n\n// Prevent caching - "Cache-Control: no-store"\nval ccNoStore = CacheControl.noStore()\n\n// Cache for ten days in public and private caches,\n// public caches should not transform the response\n// "Cache-Control: max-age=864000, public, no-transform"\nval ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()\n')])])]),r("p",[r("code",[e._v("WebContentGenerator")]),e._v("还接受一个更简单的"),r("code",[e._v("cachePeriod")]),e._v("属性(以秒为单位定义),其工作方式如下:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("-1")]),e._v("值不会生成"),r("code",[e._v("Cache-Control")]),e._v("响应报头。")])]),e._v(" "),r("li",[r("p",[e._v("通过使用"),r("code",[e._v("'Cache-Control: no-store'")]),e._v("指令,"),r("code",[e._v("0")]),e._v("值可以防止缓存。")])]),e._v(" "),r("li",[r("p",[e._v("通过使用"),r("code",[e._v("'Cache-Control: max-age=n'")]),e._v("指令,"),r("code",[e._v("n > 0")]),e._v("值将给定的响应缓存为"),r("code",[e._v("n")]),e._v("秒。")])])]),e._v(" "),r("h4",{attrs:{id:"_1-9-2-控制器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-9-2-控制器"}},[e._v("#")]),e._v(" 1.9.2.控制器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-caching-etag-lastmodified"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的"),r("code",[e._v("lastModified")]),e._v("或"),r("code",[e._v("ETag")]),e._v("值需要在与条件请求头进行比较之前进行计算。控制器可以将"),r("code",[e._v("ETag")]),e._v("标头和"),r("code",[e._v("Cache-Control")]),e._v("设置添加到"),r("code",[e._v("ResponseEntity")]),e._v("中,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/book/{id}")\npublic ResponseEntity showBook(@PathVariable Long id) {\n\n Book book = findBook(id);\n String version = book.getVersion();\n\n return ResponseEntity\n .ok()\n .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))\n .eTag(version) // lastModified is also available\n .body(book);\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@GetMapping("/book/{id}")\nfun showBook(@PathVariable id: Long): ResponseEntity {\n\n val book = findBook(id);\n val version = book.getVersion()\n\n return ResponseEntity\n .ok()\n .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))\n .eTag(version) // lastModified is also available\n .body(book)\n}\n')])])]),r("p",[e._v("如果与条件请求标题的比较表明内容没有更改,则前面的示例发送带有空主体的 304(not_modified)响应。否则,将"),r("code",[e._v("ETag")]),e._v("和"),r("code",[e._v("Cache-Control")]),e._v("头添加到响应中。")]),e._v(" "),r("p",[e._v("你还可以检查控制器中的条件请求头,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping\npublic String myHandleMethod(WebRequest request, Model model) {\n\n long eTag = ... (1)\n\n if (request.checkNotModified(eTag)) {\n return null; (2)\n }\n\n model.addAttribute(...); (3)\n return "myViewName";\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("应用程序特定的计算。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("响应已设置为 304(未修改)——没有进一步的处理。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("继续处理请求。")])])])]),e._v(" "),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping\nfun myHandleMethod(request: WebRequest, model: Model): String? {\n\n val eTag: Long = ... (1)\n\n if (request.checkNotModified(eTag)) {\n return null (2)\n }\n\n model[...] = ... (3)\n return "myViewName"\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("应用程序特定的计算。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("响应已设置为 304(未修改)——没有进一步的处理。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("继续处理请求。")])])])]),e._v(" "),r("p",[e._v("针对"),r("code",[e._v("eTag")]),e._v("值、"),r("code",[e._v("lastModified")]),e._v("值或两者检查条件请求有三种变体。对于条件"),r("code",[e._v("GET")]),e._v("和"),r("code",[e._v("HEAD")]),e._v("请求,可以将响应设置为 304(不是 _modified)。对于条件"),r("code",[e._v("POST")]),e._v("、"),r("code",[e._v("PUT")]),e._v("和"),r("code",[e._v("DELETE")]),e._v(",你可以将响应设置为 412(前提条件 _ 失败),以防止并发修改。")]),e._v(" "),r("h4",{attrs:{id:"_1-9-3-静态资源"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-9-3-静态资源"}},[e._v("#")]),e._v(" 1.9.3.静态资源")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-caching-static-resources"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("为了获得最佳性能,你应该使用"),r("code",[e._v("Cache-Control")]),e._v("和条件响应头来服务静态资源。请参见配置"),r("a",{attrs:{href:"#mvc-config-static-resources"}},[e._v("静态资源")]),e._v("一节。")]),e._v(" "),r("h4",{attrs:{id:"_1-9-4-etag过滤器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-9-4-etag过滤器"}},[e._v("#")]),e._v(" 1.9.4."),r("code",[e._v("ETag")]),e._v("过滤器")]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("ShallowEtagHeaderFilter")]),e._v("来添加“shallow”"),r("code",[e._v("eTag")]),e._v("值,这些值是从响应内容计算出来的,因此,可以节省带宽,但不会节省 CPU 时间。见"),r("a",{attrs:{href:"#filters-shallow-etag"}},[e._v("浅层 ETAG")]),e._v("。")]),e._v(" "),r("h3",{attrs:{id:"_1-10-查看技术"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-查看技术"}},[e._v("#")]),e._v(" 1.10.查看技术")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring MVC 中视图技术的使用是可插入的。是否决定使用 ThymeLeaf、Groovy 标记模板、JSP 或其他技术主要是配置更改的问题。本章介绍与 Spring MVC 集成的视图技术。我们假设你已经熟悉"),r("a",{attrs:{href:"#mvc-viewresolver"}},[e._v("视图分辨率")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring MVC 应用程序的视图位于该应用程序的内部信任边界"),r("br"),e._v("内。视图可以访问应用程序上下文中的所有 bean。正如"),r("br"),e._v("这样,不建议在应用程序中使用 Spring MVC 的模板支持,因为"),r("br"),e._v("模板可以由外部源进行编辑,因为这可能具有安全含义。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-10-1-百里香叶"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-1-百里香叶"}},[e._v("#")]),e._v(" 1.10.1.百里香叶")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-thymeleaf"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎,强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于在 UI 模板上独立工作(例如,由设计师)非常有帮助,而不需要运行的服务器。如果你想要替换 JSP,ThymeLeaf 提供了最广泛的一组特性,可以使这种转换变得更容易。麝香草叶得到了积极的开发和维护。有关更完整的介绍,请参见"),r("a",{attrs:{href:"https://www.thymeleaf.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Thymeleaf"),r("OutboundLink")],1),e._v("项目主页。")]),e._v(" "),r("p",[e._v("ThymeLeaf 与 Spring MVC 的集成由 ThymeLeaf 项目管理。配置涉及一些 Bean 声明,例如"),r("code",[e._v("ServletContextTemplateResolver")]),e._v("、"),r("code",[e._v("SpringTemplateEngine")]),e._v("和"),r("code",[e._v("ThymeleafViewResolver")]),e._v("。有关更多详细信息,请参见"),r("a",{attrs:{href:"https://www.thymeleaf.org/documentation.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Thymeleaf+Spring"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-2-自由标记"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-2-自由标记"}},[e._v("#")]),e._v(" 1.10.2.自由标记")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-freemarker"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("a",{attrs:{href:"https://freemarker.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Apache Freemarker"),r("OutboundLink")],1),e._v("是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring MVC 和 Freemarker 模板的内置集成。")]),e._v(" "),r("h5",{attrs:{id:"视图配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#视图配置"}},[e._v("#")]),e._v(" 视图配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-freemarker-contextconfig"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("下面的示例展示了如何将 Freemarker 配置为一种视图技术:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.freeMarker();\n }\n\n // Configure FreeMarker...\n\n @Bean\n public FreeMarkerConfigurer freeMarkerConfigurer() {\n FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();\n configurer.setTemplateLoaderPath("/WEB-INF/freemarker");\n return configurer;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.freeMarker()\n }\n\n // Configure FreeMarker...\n\n @Bean\n fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {\n setTemplateLoaderPath("/WEB-INF/freemarker")\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何在 XML 中配置相同的内容:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n\n\x3c!-- Configure FreeMarker... --\x3e\n\n \n\n')])])]),r("p",[e._v("或者,你也可以声明"),r("code",[e._v("FreeMarkerConfigurer")]),e._v(" Bean 以完全控制所有属性,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n')])])]),r("p",[e._v("你的模板需要存储在前面示例中所示的"),r("code",[e._v("FreeMarkerConfigurer")]),e._v("指定的目录中。给定上述配置,如果控制器返回"),r("code",[e._v("welcome")]),e._v("的视图名称,则解析器将查找"),r("code",[e._v("/WEB-INF/freemarker/welcome.ftl")]),e._v("模板。")]),e._v(" "),r("h5",{attrs:{id:"自由标记配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#自由标记配置"}},[e._v("#")]),e._v(" 自由标记配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-views-freemarker"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("通过在"),r("code",[e._v("FreeMarkerConfigurer")]),e._v(" Bean 上设置适当的 Bean 属性,可以将自由标记’settings’和’sharedvariets’直接传递给自由标记"),r("code",[e._v("Configuration")]),e._v("对象(由 Spring 管理)。"),r("code",[e._v("freemarkerSettings")]),e._v("属性需要一个"),r("code",[e._v("java.util.Properties")]),e._v("对象,而"),r("code",[e._v("freemarkerVariables")]),e._v("属性需要一个"),r("code",[e._v("java.util.Map")]),e._v("。下面的示例展示了如何使用"),r("code",[e._v("FreeMarkerConfigurer")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n\n\n\n')])])]),r("p",[e._v("有关应用于"),r("code",[e._v("Configuration")]),e._v("对象的设置和变量的详细信息,请参见 Freemarker 文档。")]),e._v(" "),r("h5",{attrs:{id:"表单处理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#表单处理"}},[e._v("#")]),e._v(" 表单处理")]),e._v(" "),r("p",[e._v("Spring 提供了用于 JSP 的标记库,该标记库包括"),r("code",[e._v("")]),e._v("元素。这个元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中"),r("code",[e._v("Validator")]),e._v("的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持,具有用于生成表单输入元素本身的附加方便宏。")]),e._v(" "),r("h6",{attrs:{id:"bind-宏"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#bind-宏"}},[e._v("#")]),e._v(" BIND 宏")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-bind-macros"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在"),r("code",[e._v("spring-webmvc.jar")]),e._v("freemarker 文件中维护了一组标准的宏,因此对于适当配置的应用程序,它们总是可用的。")]),e._v(" "),r("p",[e._v("Spring 模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围,这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,那么该文件被称为"),r("code",[e._v("spring.ftl")]),e._v(",并且位于"),r("code",[e._v("org.springframework.web.servlet.view.freemarker")]),e._v("包中。")]),e._v(" "),r("h6",{attrs:{id:"简单绑定"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#简单绑定"}},[e._v("#")]),e._v(" 简单绑定")]),e._v(" "),r("p",[e._v("在基于作为 Spring MVC 控制器的窗体视图的自由标记模板的 HTML 表单中,可以使用类似于下一个示例的代码绑定到字段值,并以类似于 JSP 的方式显示每个输入字段的错误消息。下面的示例显示了"),r("code",[e._v("personForm")]),e._v("视图:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\x3c!-- FreeMarker macros have to be imported into a namespace.\n We strongly recommend sticking to \'spring\'. --\x3e\n<#import "/spring.ftl" as spring/>\n\n ...\n
\n Name:\n <@spring.bind "personForm.name"/>\n
\n <#list spring.status.errorMessages as error> ${error}
\n
\n ...\n \n
\n ...\n\n')])])]),r("p",[r("code",[e._v("<@spring.bind>")]),e._v("需要一个’path’参数,该参数包括命令对象的名称(除非在控制器配置中更改了它,否则它是’command’),后面是句号和你希望绑定到的命令对象上的字段的名称。你也可以使用嵌套字段,例如"),r("code",[e._v("command.address.street")]),e._v("。"),r("code",[e._v("bind")]),e._v("宏在"),r("code",[e._v("web.xml")]),e._v("中假定了由"),r("code",[e._v("ServletContext")]),e._v("参数"),r("code",[e._v("defaultHtmlEscape")]),e._v("指定的默认 HTML 转义行为。")]),e._v(" "),r("p",[e._v("宏的另一种形式"),r("code",[e._v("<@spring.bindEscaped>")]),e._v("接受第二个参数,该参数显式地指定在状态错误消息或值中是否应该使用 HTML 转义。你可以根据需要将其设置为"),r("code",[e._v("true")]),e._v("或"),r("code",[e._v("false")]),e._v("。额外的表单处理宏简化了 HTML 转义的使用,你应该尽可能使用这些宏。它们将在下一节中进行解释。")]),e._v(" "),r("h6",{attrs:{id:"输入宏"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#输入宏"}},[e._v("#")]),e._v(" 输入宏")]),e._v(" "),r("p",[e._v("FreeMarker 的附加方便宏简化了绑定和表单生成(包括验证错误显示)。从来没有必要使用这些宏来生成表单输入字段,你可以将它们与简单的 HTML 或直接调用 Spring BIND 宏进行混合和匹配,我们在前面强调了这些宏。")]),e._v(" "),r("p",[e._v("下面的可用宏的表格显示了 Freemarker Template 的定义和每个定义所接受的参数列表:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("宏")]),e._v(" "),r("th",[e._v("FTL definition")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("message")]),e._v("(根据代码参数从资源包输出字符串)")]),e._v(" "),r("td",[e._v("<@spring.message code/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("messageText")]),e._v("(根据代码参数从资源包输出字符串,"),r("br"),e._v("返回到默认参数的值)")]),e._v(" "),r("td",[e._v("<@spring.messageText code, text/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("url")]),e._v("(用应用程序的上下文根作为相对 URL 的前缀)")]),e._v(" "),r("td",[e._v("<@spring.url relativeUrl/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formInput")]),e._v("(收集用户输入的标准输入字段)")]),e._v(" "),r("td",[e._v("<@spring.formInput path, attributes, fieldType/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formHiddenInput")]),e._v("(用于提交非用户输入的隐藏输入字段)")]),e._v(" "),r("td",[e._v("<@spring.formHiddenInput path, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formPasswordInput")]),e._v("(收集密码的标准输入字段。请注意,在这种类型的字段中没有填充"),r("br"),e._v("值。)")]),e._v(" "),r("td",[e._v("<@spring.formPasswordInput path, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formTextarea")]),e._v("(用于收集长的、自由形式的文本输入的大型文本字段)")]),e._v(" "),r("td",[e._v("<@spring.formTextarea path, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formSingleSelect")]),e._v("(下拉框中的选项,可以让单个所需的值被"),r("br"),e._v("选中)")]),e._v(" "),r("td",[e._v("<@spring.formSingleSelect path, options, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formMultiSelect")]),e._v("(允许用户选择 0 个或更多值的选项列表框)")]),e._v(" "),r("td",[e._v("<@spring.formMultiSelect path, options, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formRadioButtons")]),e._v("(一组单选按钮,可以从可用的选项中进行单个选择"),r("br"),e._v(")")]),e._v(" "),r("td",[e._v("<@spring.formRadioButtons path, options separator, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formCheckboxes")]),e._v("(一组复选框,可以选择 0 个或更多的值)")]),e._v(" "),r("td",[e._v("<@spring.formCheckboxes path, options, separator, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("formCheckbox")]),e._v("(一个复选框)")]),e._v(" "),r("td",[e._v("<@spring.formCheckbox path, attributes/>")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("showErrors")]),e._v("(简化绑定字段的验证错误显示)")]),e._v(" "),r("td",[e._v("<@spring.showErrors separator, classOrStyle/>")])])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在自由标记模板中,"),r("code",[e._v("formHiddenInput")]),e._v("和"),r("code",[e._v("formPasswordInput")]),e._v("实际上不是"),r("br"),e._v("所需的,因为你可以使用正常的"),r("code",[e._v("formInput")]),e._v("宏,指定"),r("code",[e._v("hidden")]),e._v("或"),r("code",[e._v("password")]),e._v("作为"),r("code",[e._v("fieldType")]),e._v("参数的值。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("上述任何宏的参数都具有一致的含义:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("path")]),e._v(":要绑定到的字段的名称(即“command.name”)")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("options")]),e._v(":一个"),r("code",[e._v("Map")]),e._v("在输入字段中可以选择的所有可用值。映射的键表示从表单发回并绑定到命令对象的值。相对于键存储的映射对象是在表单上向用户显示的标签,并且可以不同于由表单发回的对应值。通常,这样的映射由控制器提供作为参考数据。你可以使用任何"),r("code",[e._v("Map")]),e._v("实现,这取决于所需的行为。对于严格排序的映射,可以使用带有适当"),r("code",[e._v("Comparator")]),e._v("的"),r("code",[e._v("SortedMap")]),e._v("(例如"),r("code",[e._v("TreeMap")]),e._v("),对于应该以插入顺序返回值的任意映射,可以使用"),r("code",[e._v("LinkedHashMap")]),e._v("或"),r("code",[e._v("LinkedMap")]),e._v("的"),r("code",[e._v("commons-collections")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("separator")]),e._v(":当多个选项作为独立元素(单选按钮或复选框)可用时,用于分隔列表中每个元素的字符序列(例如"),r("code",[e._v("
")]),e._v(")。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("attributes")]),e._v(":在 HTML 标记本身中包含的任意标记或文本的附加字符串。这个字符串在宏的字面上得到了呼应。例如,在"),r("code",[e._v("textarea")]),e._v('字段中,可以提供属性(例如“rows=”5“cols=”60“”),也可以传递样式信息,例如“style=”border:1px solid silver"。')])]),e._v(" "),r("li",[r("p",[r("code",[e._v("classOrStyle")]),e._v(":对于"),r("code",[e._v("showErrors")]),e._v("宏,表示封装每个错误的"),r("code",[e._v("span")]),e._v("元素所使用的 CSS 类的名称。如果没有提供任何信息(或者值为空),则将错误包装在"),r("code",[e._v("")]),e._v("标记中。")])])]),e._v(" "),r("p",[e._v("下面的小节概述了宏的示例。")]),e._v(" "),r("h2",{attrs:{id:"输入字段"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#输入字段"}},[e._v("#")]),e._v(" 输入字段")]),e._v(" "),r("p",[r("code",[e._v("formInput")]),e._v("宏接受"),r("code",[e._v("path")]),e._v("参数("),r("code",[e._v("command.name")]),e._v(")和一个额外的"),r("code",[e._v("attributes")]),e._v("参数(在接下来的示例中为空)。 Spring 宏与所有其他形式的生成宏一起,在路径参数上执行隐式绑定。在发生新的绑定之前,绑定一直有效,因此"),r("code",[e._v("showErrors")]),e._v("宏不需要再次传递路径参数——它在上次创建绑定的字段上进行操作。")]),e._v(" "),r("p",[r("code",[e._v("showErrors")]),e._v("宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数——这次是一个类名或样式属性。请注意,Freemarker 可以为属性参数指定默认值。下面的示例展示了如何使用"),r("code",[e._v("formInput")]),e._v("和"),r("code",[e._v("showErrors")]),e._v("宏:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('<@spring.formInput "command.name"/>\n<@spring.showErrors "
"/>\n')])])]),r("p",[e._v("下一个示例显示了表单片段的输出,生成了 Name 字段,并在提交了该字段中没有值的表单之后显示了一个验证错误。验证通过 Spring 的验证框架进行。")]),e._v(" "),r("p",[e._v("生成的 HTML 类似于以下示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('Name:\n\n
\n required\n
\n
\n')])])]),r("p",[r("code",[e._v("formTextarea")]),e._v("宏的工作方式与"),r("code",[e._v("formInput")]),e._v("宏相同,并接受相同的参数列表。通常,第二个参数("),r("code",[e._v("attributes")]),e._v(")用于为"),r("code",[e._v("textarea")]),e._v("传递样式信息或"),r("code",[e._v("rows")]),e._v("和"),r("code",[e._v("cols")]),e._v("属性。")]),e._v(" "),r("h2",{attrs:{id:"选择字段"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#选择字段"}},[e._v("#")]),e._v(" 选择字段")]),e._v(" "),r("p",[e._v("你可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("formSingleSelect")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("formMultiSelect")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("formRadioButtons")])])]),e._v(" "),r("li",[r("p",[r("code",[e._v("formCheckboxes")])])])]),e._v(" "),r("p",[e._v("这四个宏中的每一个都接受"),r("code",[e._v("Map")]),e._v("选项,这些选项包含窗体字段的值和对应于该值的标签。值和标签可以是相同的。")]),e._v(" "),r("p",[e._v("下一个例子是 FTL 中的单选按钮。表单支持对象为该字段指定了默认值“london”,因此不需要验证。当呈现表单时,可供选择的整个城市列表将作为模型中的参考数据提供,名称为“CityMap”。下面的清单展示了这个示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('...\nTown:\n<@spring.formRadioButtons "command.address.town", cityMap, ""/>

\n')])])]),r("p",[e._v("前面的列表呈现了一行单选按钮,在"),r("code",[e._v("cityMap")]),e._v("中每个值对应一个单选按钮,并使用"),r("code",[e._v('""')]),e._v("的分隔符。不提供其他属性(缺少宏的最后一个参数)。对于映射中的每个键值对,"),r("code",[e._v("cityMap")]),e._v("使用相同的"),r("code",[e._v("String")]),e._v("。映射的键是表单实际提交的"),r("code",[e._v("POST")]),e._v("请求参数。映射值是用户看到的标签。在前面的示例中,给出了三个著名城市的列表和表单支持对象中的默认值,HTML 类似于以下内容:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('Town:\nLondon\nParis\nNew York\n')])])]),r("p",[e._v("如果你的应用程序希望通过内部代码(例如)处理城市,则可以创建带有合适密钥的代码映射,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('protected Map referenceData(HttpServletRequest request) throws Exception {\n Map cityMap = new LinkedHashMap<>();\n cityMap.put("LDN", "London");\n cityMap.put("PRS", "Paris");\n cityMap.put("NYC", "New York");\n\n Map model = new HashMap<>();\n model.put("cityMap", cityMap);\n return model;\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('protected fun referenceData(request: HttpServletRequest): Map {\n val cityMap = linkedMapOf(\n "LDN" to "London",\n "PRS" to "Paris",\n "NYC" to "New York"\n )\n return hashMapOf("cityMap" to cityMap)\n}\n')])])]),r("p",[e._v("该代码现在产生输出,其中无线电值是相关的代码,但用户仍然可以看到更方便用户的城市名称,如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('Town:\nLondon\nParis\nNew York\n')])])]),r("h6",{attrs:{id:"html-转义"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#html-转义"}},[e._v("#")]),e._v(" HTML 转义")]),e._v(" "),r("p",[e._v("前面描述的表格宏的默认使用会导致 HTML 元素与 HTML4.01 兼容,并且使用在"),r("code",[e._v("web.xml")]),e._v("文件中定义的 HTML 转义的默认值,正如 Spring 的 BIND 支持所使用的那样。要使元素与 XHTML 兼容或覆盖默认的 HTML 转义值,你可以在模板中指定两个变量(或者在模型中,它们对模板是可见的)。在模板中指定它们的优点是,它们可以在以后的模板处理中更改为不同的值,从而为表单中的不同字段提供不同的行为。")]),e._v(" "),r("p",[e._v("要为标记切换到 XHTML 遵从性,请为名为"),r("code",[e._v("true")]),e._v("的模型或上下文变量指定一个值"),r("code",[e._v("xhtmlCompliant")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("<#-- for FreeMarker --\x3e\n<#assign xhtmlCompliant = true>\n")])])]),r("p",[e._v("在处理此指令之后,由 Spring 宏生成的任何元素现在都与 XHTML 兼容。")]),e._v(" "),r("p",[e._v("以类似的方式,你可以指定每个字段的 HTML 转义,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('<#-- until this point, default HTML escaping is used --\x3e\n\n<#assign htmlEscape = true>\n<#-- next field will use HTML escaping --\x3e\n<@spring.formInput "command.name"/>\n\n<#assign htmlEscape = false in spring>\n<#-- all future fields will be bound with HTML escaping off --\x3e\n')])])]),r("h4",{attrs:{id:"_1-10-3-groovy-标记"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-3-groovy-标记"}},[e._v("#")]),e._v(" 1.10.3.Groovy 标记")]),e._v(" "),r("p",[r("a",{attrs:{href:"http://groovy-lang.org/templating.html#_the_markuptemplateengine",target:"_blank",rel:"noopener noreferrer"}},[e._v("Groovy 标记模板引擎"),r("OutboundLink")],1),e._v("主要用于生成类似 XML 的标记(XML、XHTML、HTML5 和其他标记),但你可以使用它生成任何基于文本的内容。 Spring 框架具有用于使用带有 Groovy 标记的 Spring MVC 的内置集成。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Groovy 标记模板引擎需要 Groovy2.3.1+。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"configuration"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#configuration"}},[e._v("#")]),e._v(" Configuration")]),e._v(" "),r("p",[e._v("下面的示例展示了如何配置 Groovy 标记模板引擎:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.groovy();\n }\n\n // Configure the Groovy Markup Template Engine...\n\n @Bean\n public GroovyMarkupConfigurer groovyMarkupConfigurer() {\n GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();\n configurer.setResourceLoaderPath("/WEB-INF/");\n return configurer;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.groovy()\n }\n\n // Configure the Groovy Markup Template Engine...\n\n @Bean\n fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {\n resourceLoaderPath = "/WEB-INF/"\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何在 XML 中配置相同的内容:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n\n\x3c!-- Configure the Groovy Markup Template Engine... --\x3e\n\n')])])]),r("h5",{attrs:{id:"例子"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#例子"}},[e._v("#")]),e._v(" 例子")]),e._v(" "),r("p",[e._v("与传统的模板引擎不同,Groovy Markup 依赖于使用 Builder 语法的 DSL。下面的示例展示了 HTML 页面的示例模板:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("yieldUnescaped ''\nhtml(lang:'en') {\n head {\n meta('http-equiv':'\"Content-Type\" content=\"text/html; charset=utf-8\"')\n title('My page')\n }\n body {\n p('This is an example of HTML contents')\n }\n}\n")])])]),r("h4",{attrs:{id:"_1-10-4-脚本视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-4-脚本视图"}},[e._v("#")]),e._v(" 1.10.4.脚本视图")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-script"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring 框架具有用于使用 Spring MVC 和任何模板库的内置集成,这些模板库可以在"),r("a",{attrs:{href:"https://www.jcp.org/en/jsr/detail?id=223",target:"_blank",rel:"noopener noreferrer"}},[e._v("JSR-223"),r("OutboundLink")],1),e._v("Java 脚本引擎之上运行。我们在不同的脚本引擎上测试了以下模板库:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("脚本库")]),e._v(" "),r("th",[e._v("Scripting Engine")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("a",{attrs:{href:"https://handlebarsjs.com/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Handlebars"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://openjdk.java.net/projects/nashorn/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Nashorn"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://mustache.github.io/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Mustache"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://openjdk.java.net/projects/nashorn/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Nashorn"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://facebook.github.io/react/",target:"_blank",rel:"noopener noreferrer"}},[e._v("React"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://openjdk.java.net/projects/nashorn/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Nashorn"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://www.embeddedjs.com/",target:"_blank",rel:"noopener noreferrer"}},[e._v("EJS"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://openjdk.java.net/projects/nashorn/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Nashorn"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://www.stuartellis.name/articles/erb/",target:"_blank",rel:"noopener noreferrer"}},[e._v("ERB"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://www.jruby.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("JRuby"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://docs.python.org/2/library/string.html#template-strings",target:"_blank",rel:"noopener noreferrer"}},[e._v("字符串模板"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://www.jython.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jython"),r("OutboundLink")],1)])]),e._v(" "),r("tr",[r("td",[r("a",{attrs:{href:"https://github.com/sdeleuze/kotlin-script-templating",target:"_blank",rel:"noopener noreferrer"}},[e._v("Kotlin Script templating"),r("OutboundLink")],1)]),e._v(" "),r("td",[r("a",{attrs:{href:"https://kotlinlang.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Kotlin"),r("OutboundLink")],1)])])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("集成任何其他脚本引擎的基本规则是,它必须实现"),r("code",[e._v("ScriptEngine")]),e._v("和"),r("code",[e._v("Invocable")]),e._v("接口。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"所需经费"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#所需经费"}},[e._v("#")]),e._v(" 所需经费")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-script-dependencies"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你需要在 Classpath 上有脚本引擎,其细节因脚本引擎而异:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://openjdk.java.net/projects/nashorn/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Nashorn"),r("OutboundLink")],1),e._v("JavaScript 引擎由 Java8+ 提供。强烈推荐使用最新的可用更新版本。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://www.jruby.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("JRuby"),r("OutboundLink")],1),e._v("应该作为 Ruby 支持的依赖项添加。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://www.jython.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jython"),r("OutboundLink")],1),e._v("应该作为 Python 支持的依赖项添加。")])]),e._v(" "),r("li",[r("p",[e._v("对于 Kotlin 脚本支持,应该添加"),r("code",[e._v("org.jetbrains.kotlin:kotlin-script-util")]),e._v("依赖项和一个"),r("code",[e._v("META-INF/services/javax.script.ScriptEngineFactory")]),e._v("文件,该文件包含"),r("code",[e._v("org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory")]),e._v("行。有关更多详细信息,请参见"),r("a",{attrs:{href:"https://github.com/sdeleuze/kotlin-script-templating",target:"_blank",rel:"noopener noreferrer"}},[e._v("这个例子"),r("OutboundLink")],1),e._v("。")])])]),e._v(" "),r("p",[e._v("你需要有脚本模板库。JavaScript 的一种方法是通过"),r("a",{attrs:{href:"https://www.webjars.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebJars"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"脚本模板"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#脚本模板"}},[e._v("#")]),e._v(" 脚本模板")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-script-integrate"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以声明一个"),r("code",[e._v("ScriptTemplateConfigurer")]),e._v(" Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn JavaScript 引擎:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.scriptTemplate();\n }\n\n @Bean\n public ScriptTemplateConfigurer configurer() {\n ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();\n configurer.setEngineName("nashorn");\n configurer.setScripts("mustache.js");\n configurer.setRenderObject("Mustache");\n configurer.setRenderFunction("render");\n return configurer;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.scriptTemplate()\n }\n\n @Bean\n fun configurer() = ScriptTemplateConfigurer().apply {\n engineName = "nashorn"\n setScripts("mustache.js")\n renderObject = "Mustache"\n renderFunction = "render"\n }\n}\n')])])]),r("p",[e._v("下面的示例用 XML 展示了相同的排列方式:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n\n\n \n\n')])])]),r("p",[e._v("对于 Java 和 XML 配置,控制器看起来没有什么不同,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class SampleController {\n\n @GetMapping("/sample")\n public String test(Model model) {\n model.addAttribute("title", "Sample title");\n model.addAttribute("body", "Sample body");\n return "template";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\nclass SampleController {\n\n @GetMapping("/sample")\n fun test(model: Model): String {\n model["title"] = "Sample title"\n model["body"] = "Sample body"\n return "template"\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了小胡子模板:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n \n {{title}}\n \n \n

{{body}}

\n \n\n")])])]),r("p",[e._v("使用以下参数调用呈现函数:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("String template")]),e._v(":模板内容")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Map model")]),e._v(":视图模型")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("RenderingContext renderingContext")]),e._v(":["),r("code",[e._v("RenderingContext")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/script/renderingcontext.html),它提供了对应用程序上下文、区域设置、模板装入器和 URL(自 5.0 起)的访问权限")])])]),e._v(" "),r("p",[r("code",[e._v("Mustache.render()")]),e._v("与此签名原生兼容,因此你可以直接调用它。")]),e._v(" "),r("p",[e._v("如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,"),r("a",{attrs:{href:"https://handlebarsjs.com",target:"_blank",rel:"noopener noreferrer"}},[e._v("Handlerbars"),r("OutboundLink")],1),e._v("在使用模板之前需要对其进行编译,并且需要"),r("a",{attrs:{href:"https://en.wikipedia.org/wiki/Polyfill",target:"_blank",rel:"noopener noreferrer"}},[e._v("polyfill"),r("OutboundLink")],1),e._v("来模拟服务器端脚本引擎中不可用的一些浏览器功能。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.scriptTemplate();\n }\n\n @Bean\n public ScriptTemplateConfigurer configurer() {\n ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();\n configurer.setEngineName("nashorn");\n configurer.setScripts("polyfill.js", "handlebars.js", "render.js");\n configurer.setRenderFunction("render");\n configurer.setSharedEngine(false);\n return configurer;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.scriptTemplate()\n }\n\n @Bean\n fun configurer() = ScriptTemplateConfigurer().apply {\n engineName = "nashorn"\n setScripts("polyfill.js", "handlebars.js", "render.js")\n renderFunction = "render"\n isSharedEngine = false\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当使用非线程安全的"),r("br"),e._v("脚本引擎时,需要将"),r("code",[e._v("sharedEngine")]),e._v("属性设置为"),r("code",[e._v("false")]),e._v(",该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或"),r("br"),e._v("React。在那种情况下,由于"),r("a",{attrs:{href:"https://bugs.openjdk.java.net/browse/JDK-8076099",target:"_blank",rel:"noopener noreferrer"}},[e._v("this bug"),r("OutboundLink")],1),e._v(",Java SE8Update60 是必需的,但是一般情况下"),r("br"),e._v("推荐在任何情况下使用最近发布的 Java SE 补丁。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[r("code",[e._v("polyfill.js")]),e._v("只定义了处理栏正常运行所需的"),r("code",[e._v("window")]),e._v("对象,如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("var window = {};\n")])])]),r("p",[e._v("这个基本的"),r("code",[e._v("render.js")]),e._v("实现在使用模板之前对其进行编译。生产就绪的实现还应该存储任何重用的缓存模板或预编译模板。你可以在脚本端这样做(并处理你需要的任何定制——例如,管理模板引擎配置)。下面的示例展示了如何做到这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("function render(template, model) {\n var compiledTemplate = Handlebars.compile(template);\n return compiledTemplate(model);\n}\n")])])]),r("p",[e._v("查看 Spring Framework Unit 测试,"),r("a",{attrs:{href:"https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script",target:"_blank",rel:"noopener noreferrer"}},[e._v("Java"),r("OutboundLink")],1),e._v("和"),r("a",{attrs:{href:"https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script",target:"_blank",rel:"noopener noreferrer"}},[e._v("resources"),r("OutboundLink")],1),e._v(",以获得更多配置示例。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-5-jsp-和-jstl"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-5-jsp-和-jstl"}},[e._v("#")]),e._v(" 1.10.5.JSP 和 JSTL")]),e._v(" "),r("p",[e._v("Spring 框架具有用于使用 Spring MVC 与 JSP 和 JSTL 的内置集成。")]),e._v(" "),r("h5",{attrs:{id:"视图解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#视图解析器"}},[e._v("#")]),e._v(" 视图解析器")]),e._v(" "),r("p",[e._v("在使用 JSP 进行开发时,通常声明"),r("code",[e._v("InternalResourceViewResolver")]),e._v(" Bean。")]),e._v(" "),r("p",[r("code",[e._v("InternalResourceViewResolver")]),e._v("可用于向任何 Servlet 资源进行调度,但特别是用于 JSP。作为一种最佳实践,我们强烈建议将你的 JSP 文件放置在"),r("code",[e._v("'WEB-INF'")]),e._v("目录下的目录中,这样客户端就不能直接访问它。")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n\n')])])]),r("h5",{attrs:{id:"jsp-与-jstl"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#jsp-与-jstl"}},[e._v("#")]),e._v(" JSP 与 JSTL")]),e._v(" "),r("p",[e._v("当使用 JSP 标准标记库时,你必须使用一个特殊的视图类"),r("code",[e._v("JstlView")]),e._v(",因为 JSTL 需要在诸如 i18n 特性之类的功能工作之前进行一些准备。")]),e._v(" "),r("h5",{attrs:{id:"spring-的-jsp-标记库"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#spring-的-jsp-标记库"}},[e._v("#")]),e._v(" Spring 的 JSP 标记库")]),e._v(" "),r("p",[e._v("Spring 提供请求参数到命令对象的数据绑定,如前面几章所描述的。 Spring 为了促进结合那些数据绑定特性的 JSP 页面的开发,提供了一些使事情变得更容易的标记。 Spring 所有标记都具有 HTML 转义功能,以启用或禁用字符的转义。")]),e._v(" "),r("p",[r("code",[e._v("spring.tld")]),e._v("标记库描述符包含在"),r("code",[e._v("spring-webmvc.jar")]),e._v("中。有关单个标记的全面引用,请浏览"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/package-summary.html#package.description",target:"_blank",rel:"noopener noreferrer"}},[e._v("API 参考"),r("OutboundLink")],1),e._v("或查看标记库描述。")]),e._v(" "),r("h5",{attrs:{id:"spring-的表单标记库"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#spring-的表单标记库"}},[e._v("#")]),e._v(" Spring 的表单标记库")]),e._v(" "),r("p",[e._v("在版本 2.0 中, Spring 提供了一组全面的数据绑定感知标记,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都提供了对其对应的 HTML 标记对应物的属性集的支持,使标记变得熟悉且使用起来直观。标记生成的 HTML 兼容 HTML4.01/XHTML1.0。")]),e._v(" "),r("p",[e._v("与其他表单/输入标记库不同, Spring 的表单标记库与 Spring Web MVC 集成在一起,使标记能够访问你的控制器处理的命令对象和引用数据。正如我们在下面的示例中所示,表单标记使 JSP 更易于开发、读取和维护。")]),e._v(" "),r("p",[e._v("我们通过表单标记,并查看每个标记如何使用的示例。我们已经包含了生成的 HTML 片段,其中某些标记需要进一步的注释。")]),e._v(" "),r("h6",{attrs:{id:"configuration-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#configuration-2"}},[e._v("#")]),e._v(" Configuration")]),e._v(" "),r("p",[e._v("表单标记库捆绑在"),r("code",[e._v("spring-webmvc.jar")]),e._v("中。库描述符称为"),r("code",[e._v("spring-form.tld")]),e._v("。")]),e._v(" "),r("p",[e._v("要使用这个库中的标记,请在 JSP 页面的顶部添加以下指令:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>\n')])])]),r("p",[e._v("其中"),r("code",[e._v("form")]),e._v("是你要为这个库中的标记使用的标记名称前缀。")]),e._v(" "),r("h6",{attrs:{id:"表单标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#表单标签"}},[e._v("#")]),e._v(" 表单标签")]),e._v(" "),r("p",[e._v("此标记呈现 HTML“Form”元素,并将绑定路径公开给内部标记以进行绑定。它将命令对象放在"),r("code",[e._v("PageContext")]),e._v("中,以便可以通过内部标记访问命令对象。这个库中的所有其他标记都是"),r("code",[e._v("form")]),e._v("标记的嵌套标记。")]),e._v(" "),r("p",[e._v("假设我们有一个名为"),r("code",[e._v("User")]),e._v("的域对象。它是一个 JavaBean,具有"),r("code",[e._v("firstName")]),e._v("和"),r("code",[e._v("lastName")]),e._v("等属性。我们可以使用它作为表单控制器的表单支持对象,它返回"),r("code",[e._v("form.jsp")]),e._v("。下面的示例显示了"),r("code",[e._v("form.jsp")]),e._v("可能是什么样子的:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n
First Name:
Last Name:
\n \n
\n
\n')])])]),r("p",[e._v("由页面控制器从放置在"),r("code",[e._v("PageContext")]),e._v("中的命令对象检索"),r("code",[e._v("firstName")]),e._v("和"),r("code",[e._v("lastName")]),e._v("值。继续阅读,以查看更多关于内部标记如何与"),r("code",[e._v("form")]),e._v("标记一起使用的复杂示例。")]),e._v(" "),r("p",[e._v("下面的清单显示了生成的 HTML,它看起来像一个标准表单:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('
\n \n \n \n \n \n \n \n \n \n \n \n \n
First Name:
Last Name:
\n \n
\n
\n')])])]),r("p",[e._v("前面的 JSP 假设表单支持对象的变量名为"),r("code",[e._v("command")]),e._v("。如果你已将表单备份对象以另一个名称(肯定是最佳实践)放入模型中,则可以将表单绑定到已命名的变量,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n
First Name:
Last Name:
\n \n
\n
\n')])])]),r("h6",{attrs:{id:"input标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#input标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("input")]),e._v("标签")]),e._v(" "),r("p",[e._v("默认情况下,此标记呈现带有绑定值和"),r("code",[e._v("type='text'")]),e._v("元素的 HTML"),r("code",[e._v("input")]),e._v("。有关此标记的示例,请参见"),r("a",{attrs:{href:"#mvc-view-jsp-formtaglib-formtag"}},[e._v("表单标签")]),e._v("。你也可以使用 HTML5 特定的类型,例如"),r("code",[e._v("email")]),e._v("、"),r("code",[e._v("tel")]),e._v("、"),r("code",[e._v("date")]),e._v("和其他类型。")]),e._v(" "),r("h6",{attrs:{id:"checkbox标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#checkbox标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("checkbox")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记呈现 HTML"),r("code",[e._v("input")]),e._v("标记,其"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("checkbox")]),e._v("。")]),e._v(" "),r("p",[e._v("假设我们的"),r("code",[e._v("User")]),e._v("具有首选项,例如订阅时事通讯和列出兴趣爱好。下面的示例显示了"),r("code",[e._v("Preferences")]),e._v("类:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class Preferences {\n\n private boolean receiveNewsletter;\n private String[] interests;\n private String favouriteWord;\n\n public boolean isReceiveNewsletter() {\n return receiveNewsletter;\n }\n\n public void setReceiveNewsletter(boolean receiveNewsletter) {\n this.receiveNewsletter = receiveNewsletter;\n }\n\n public String[] getInterests() {\n return interests;\n }\n\n public void setInterests(String[] interests) {\n this.interests = interests;\n }\n\n public String getFavouriteWord() {\n return favouriteWord;\n }\n\n public void setFavouriteWord(String favouriteWord) {\n this.favouriteWord = favouriteWord;\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("class Preferences(\n var receiveNewsletter: Boolean,\n var interests: StringArray,\n var favouriteWord: String\n)\n")])])]),r("p",[e._v("相应的"),r("code",[e._v("form.jsp")]),e._v("可能如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n <%-- Approach 1: Property is of type java.lang.Boolean --%>\n \n \n\n \n \n <%-- Approach 2: Property is of an array or of type java.util.Collection --%>\n \n \n\n \n \n <%-- Approach 3: Property is of type java.lang.Object --%>\n \n \n
Subscribe to newsletter?:
Interests:\n Quidditch: \n Herbology: \n Defence Against the Dark Arts: \n
Favourite Word:\n Magic: \n
\n
\n')])])]),r("p",[e._v("对于"),r("code",[e._v("checkbox")]),e._v("标记,有三种方法可以满足你的所有复选框需求。")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("方法一:当绑定值类型为"),r("code",[e._v("java.lang.Boolean")]),e._v("时,如果绑定值为"),r("code",[e._v("checked")]),e._v(",则将"),r("code",[e._v("input(checkbox)")]),e._v("标记为"),r("code",[e._v("checked")]),e._v("。"),r("code",[e._v("value")]),e._v("属性对应于"),r("code",[e._v("setValue(Object)")]),e._v("值属性的解析值。")])]),e._v(" "),r("li",[r("p",[e._v("方法二:当绑定值类型为"),r("code",[e._v("array")]),e._v("或"),r("code",[e._v("java.util.Collection")]),e._v("时,如果配置的"),r("code",[e._v("checked")]),e._v("值存在于绑定值"),r("code",[e._v("Collection")]),e._v("中,则将"),r("code",[e._v("input(checkbox)")]),e._v("标记为"),r("code",[e._v("checked")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("方法三:对于任何其他绑定值类型,如果配置的"),r("code",[e._v("setValue(Object)")]),e._v("等于绑定值,则将"),r("code",[e._v("checked")]),e._v("标记为"),r("code",[e._v("checked")]),e._v("。")])])]),e._v(" "),r("p",[e._v("请注意,无论采用哪种方法,都会生成相同的 HTML 结构。以下 HTML 片段定义了一些复选框:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Interests:\n \n Quidditch: \n \n Herbology: \n \n Defence Against the Dark Arts: \n \n \n\n')])])]),r("p",[e._v("你可能不希望在每个复选框之后看到额外的隐藏字段。当 HTML 页面中的复选框未被选中时,其值不会在表单提交后作为 HTTP 请求参数的一部分发送到服务器,因此我们需要在 HTML 中解决此问题,以使 Spring 表单数据绑定工作。"),r("code",[e._v("checkbox")]),e._v("标记遵循现有的 Spring 约定,即为每个复选框包含一个以下划线("),r("code",[e._v("_")]),e._v(")为前缀的隐藏参数。通过这样做,你可以有效地告诉 Spring“复选框在表单中是可见的,并且我希望表单数据绑定到的对象能够反映复选框的状态,无论发生什么情况。”")]),e._v(" "),r("h6",{attrs:{id:"checkboxes标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#checkboxes标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("checkboxes")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记将呈现多个 HTML"),r("code",[e._v("input")]),e._v("标记,并将"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("checkbox")]),e._v("。")]),e._v(" "),r("p",[e._v("本节以前面"),r("code",[e._v("checkbox")]),e._v("标记部分的示例为基础。有时,你不希望在 JSP 页面中列出所有可能的爱好。你更愿意在运行时提供一个可用选项的列表,并将其传递给标记。这就是"),r("code",[e._v("checkboxes")]),e._v("标记的目的。可以传入"),r("code",[e._v("Array")]),e._v("、"),r("code",[e._v("List")]),e._v("或"),r("code",[e._v("Map")]),e._v("属性中包含可用选项的"),r("code",[e._v("items")]),e._v("。通常,绑定属性是一个集合,以便它可以保存用户选择的多个值。下面的示例展示了使用此标记的 JSP:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n
Interests:\n <%-- Property is of an array or of type java.util.Collection --%>\n \n
\n
\n')])])]),r("p",[e._v("这个示例假设"),r("code",[e._v("interestList")]),e._v("是一个"),r("code",[e._v("List")]),e._v(",作为包含要从中选择的值的字符串的模型属性可用。如果使用"),r("code",[e._v("Map")]),e._v(",则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用"),r("code",[e._v("itemValue")]),e._v("提供该值的属性名称,并通过使用"),r("code",[e._v("itemLabel")]),e._v("提供标签。")]),e._v(" "),r("h6",{attrs:{id:"radiobutton标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#radiobutton标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("radiobutton")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记呈现 HTML"),r("code",[e._v("input")]),e._v("元素,其"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("radio")]),e._v("。")]),e._v(" "),r("p",[e._v("典型的使用模式涉及绑定到相同属性但具有不同值的多个标记实例,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Sex:\n \n Male:
\n Female: \n \n\n')])])]),r("h6",{attrs:{id:"radiobuttons标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#radiobuttons标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("radiobuttons")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记呈现多个 HTML"),r("code",[e._v("input")]),e._v("元素,并将"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("radio")]),e._v("。")]),e._v(" "),r("p",[e._v("与["),r("code",[e._v("checkboxes")]),e._v("标记](#mvc-view-jsp-formtaglib-checkboxestag)一样,你可能希望将可用选项作为运行时变量传递。对于这种用法,你可以使用"),r("code",[e._v("radiobuttons")]),e._v("标记。你传入一个"),r("code",[e._v("Array")]),e._v("、一个"),r("code",[e._v("List")]),e._v("或一个"),r("code",[e._v("Map")]),e._v(",它包含"),r("code",[e._v("items")]),e._v("属性中的可用选项。如果使用"),r("code",[e._v("Map")]),e._v(",则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用"),r("code",[e._v("itemValue")]),e._v("提供该值的属性名称,并通过使用"),r("code",[e._v("itemLabel")]),e._v("提供标签,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Sex:\n \n\n')])])]),r("h6",{attrs:{id:"password标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#password标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("password")]),e._v("标签")]),e._v(" "),r("p",[e._v("这个标记呈现一个 HTML"),r("code",[e._v("input")]),e._v("标记,其类型设置为"),r("code",[e._v("password")]),e._v(",并具有绑定值。")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Password:\n \n \n \n\n')])])]),r("p",[e._v("请注意,默认情况下,不会显示密码值。如果确实希望显示密码值,可以将"),r("code",[e._v("showPassword")]),e._v("属性的值设置为"),r("code",[e._v("true")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Password:\n \n \n \n\n')])])]),r("h6",{attrs:{id:"select标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#select标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("select")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记呈现 HTML“select”元素。它支持与所选选项的数据绑定,以及使用嵌套的"),r("code",[e._v("option")]),e._v("和"),r("code",[e._v("options")]),e._v("标记。")]),e._v(" "),r("p",[e._v("假设"),r("code",[e._v("User")]),e._v("有一个技能列表。相应的 HTML 可以如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Skills:\n \n\n')])])]),r("p",[e._v("如果"),r("code",[e._v("User’s")]),e._v("技能是草药学的,那么“技能”行的 HTML 源可以如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Skills:\n \n \n \n\n')])])]),r("h6",{attrs:{id:"option标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#option标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("option")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记呈现 HTML"),r("code",[e._v("option")]),e._v("元素。它基于绑定值设置"),r("code",[e._v("selected")]),e._v("。下面的 HTML 显示了它的典型输出:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n House:\n \n \n \n \n \n \n \n \n\n')])])]),r("p",[e._v("如果"),r("code",[e._v("User’s")]),e._v("house 位于 Gryffindor,则“house”行的 HTML 源代码如下:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n House:\n \n \n \n\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("注意添加了一个"),r("code",[e._v("selected")]),e._v("属性。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h6",{attrs:{id:"tagoptions"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#tagoptions"}},[e._v("#")]),e._v(" tag"),r("code",[e._v("options")])]),e._v(" "),r("p",[e._v("此标记呈现 HTML"),r("code",[e._v("option")]),e._v("元素的列表。它基于绑定值设置"),r("code",[e._v("selected")]),e._v("属性。下面的 HTML 显示了它的典型输出:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Country:\n \n \n \n \n \n \n\n')])])]),r("p",[e._v("如果"),r("code",[e._v("User")]),e._v("存在于 UK 中,则“country”行的 HTML 源代码如下:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Country:\n \n \n \n\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("注意添加了一个"),r("code",[e._v("selected")]),e._v("属性。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("正如前面的示例所示,将"),r("code",[e._v("option")]),e._v("标记与"),r("code",[e._v("options")]),e._v("标记合并使用,会生成相同的标准 HTML,但允许你在 JSP 中显式地指定一个值,该值仅用于显示(在它所属的位置),例如示例中的默认字符串:“--请选择”。")]),e._v(" "),r("p",[r("code",[e._v("items")]),e._v("属性通常填充有条目对象的集合或数组。"),r("code",[e._v("itemValue")]),e._v("和"),r("code",[e._v("itemLabel")]),e._v("引用那些条目对象的 Bean 属性(如果指定的话)。否则,项目对象本身就会变成字符串。或者,你可以指定项目的"),r("code",[e._v("Map")]),e._v(",在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果"),r("code",[e._v("itemValue")]),e._v("或"),r("code",[e._v("itemLabel")]),e._v("(或两者兼而有之)恰好也被指定,则 Item Value 属性将应用于 map 键,而 Item Label 属性将应用于 map 值。")]),e._v(" "),r("h6",{attrs:{id:"tagtextarea"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#tagtextarea"}},[e._v("#")]),e._v(" tag"),r("code",[e._v("textarea")])]),e._v(" "),r("p",[e._v("此标记呈现 HTML"),r("code",[e._v("textarea")]),e._v("元素。下面的 HTML 显示了它的典型输出:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n Notes:\n \n \n\n')])])]),r("h6",{attrs:{id:"hidden标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#hidden标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("hidden")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记将呈现一个 HTML"),r("code",[e._v("input")]),e._v("标记,该标记的"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("hidden")]),e._v(",并具有绑定值。要提交未绑定的隐藏值,请使用 HTML"),r("code",[e._v("input")]),e._v("标记,并将"),r("code",[e._v("type")]),e._v("设置为"),r("code",[e._v("hidden")]),e._v("。下面的 HTML 显示了它的典型输出:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("如果我们选择将"),r("code",[e._v("house")]),e._v("值作为隐藏的值提交,则 HTML 将如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("h6",{attrs:{id:"errors标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#errors标签"}},[e._v("#")]),e._v(" "),r("code",[e._v("errors")]),e._v("标签")]),e._v(" "),r("p",[e._v("此标记在 HTML"),r("code",[e._v("span")]),e._v("元素中呈现字段错误。它提供对在控制器中创建的错误或由与控制器关联的任何验证器创建的错误的访问。")]),e._v(" "),r("p",[e._v("假设我们希望在提交表单后显示"),r("code",[e._v("firstName")]),e._v("和"),r("code",[e._v("lastName")]),e._v("字段的所有错误消息。对于"),r("code",[e._v("User")]),e._v("类的实例,我们有一个名为"),r("code",[e._v("UserValidator")]),e._v("的验证器,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class UserValidator implements Validator {\n\n public boolean supports(Class candidate) {\n return User.class.isAssignableFrom(candidate);\n }\n\n public void validate(Object obj, Errors errors) {\n ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.");\n ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class UserValidator : Validator {\n\n override fun supports(candidate: Class<*>): Boolean {\n return User::class.java.isAssignableFrom(candidate)\n }\n\n override fun validate(obj: Any, errors: Errors) {\n ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.")\n ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.")\n }\n}\n')])])]),r("p",[r("code",[e._v("form.jsp")]),e._v("可以如下:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n <%-- Show errors for firstName field --%>\n \n \n\n \n \n \n <%-- Show errors for lastName field --%>\n \n \n \n \n \n
First Name:
Last Name:
\n \n
\n
\n')])])]),r("p",[e._v("如果我们提交一个在"),r("code",[e._v("firstName")]),e._v("和"),r("code",[e._v("lastName")]),e._v("字段中具有空值的表单,则 HTML 将如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('
\n \n \n \n \n <%-- Associated errors to firstName field displayed --%>\n \n \n\n \n \n \n <%-- Associated errors to lastName field displayed --%>\n \n \n \n \n \n
First Name:Field is required.
Last Name:Field is required.
\n \n
\n
\n')])])]),r("p",[e._v("如果我们想要显示给定页面的整个错误列表,该怎么办?下一个示例显示"),r("code",[e._v("errors")]),e._v("标记还支持一些基本的通配符功能。")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v('path="*"')]),e._v(":显示所有错误。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v('path="lastName"')]),e._v(":显示与"),r("code",[e._v("lastName")]),e._v("字段关联的所有错误。")])]),e._v(" "),r("li",[r("p",[e._v("如果省略"),r("code",[e._v("path")]),e._v(",则只显示对象错误。")])])]),e._v(" "),r("p",[e._v("下面的示例在页面顶部显示错误列表,然后在字段旁边显示特定于字段的错误:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
First Name:
Last Name:
\n \n
\n
\n')])])]),r("p",[e._v("HTML 将如下所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('
\n Field is required.
Field is required.
\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n
First Name:Field is required.
Last Name:Field is required.
\n \n
\n
\n')])])]),r("p",[r("code",[e._v("spring-form.tld")]),e._v("标记库描述符包含在"),r("code",[e._v("spring-webmvc.jar")]),e._v("中。有关单个标记的全面参考,请浏览"),r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/form/package-summary.html#package.description",target:"_blank",rel:"noopener noreferrer"}},[e._v("API 参考"),r("OutboundLink")],1),e._v("或查看标记库描述。")]),e._v(" "),r("h6",{attrs:{id:"http-方法转换"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#http-方法转换"}},[e._v("#")]),e._v(" HTTP 方法转换")]),e._v(" "),r("p",[e._v("REST 的一个关键原则是使用“统一接口”。这意味着所有资源(URL)都可以通过使用相同的四种 HTTP 方法进行操作:GET、PUT、POST 和 DELETE。对于每种方法,HTTP 规范都定义了确切的语义。例如,get 应该始终是一个安全的操作,这意味着它没有副作用,而 put 或 delete 应该是幂等的,这意味着你可以一遍又一遍地重复这些操作,但最终结果应该是相同的。虽然 HTTP 定义了这四种方法,但 HTML 只支持两种:GET 和 POST。幸运的是,有两种可能的解决方法:你可以使用 JavaScript 来执行 PUT 或 DELETE,或者你可以使用“Real”方法作为附加参数(以 HTML 形式的隐藏输入字段建模)来执行 POST。 Spring 的"),r("code",[e._v("HiddenHttpMethodFilter")]),e._v("使用了后一种技巧。 Servlet 该过滤器是一个普通的过滤器,因此,它可以与任何 Web 框架(而不仅仅是 Spring MVC)组合使用。将此筛选器添加到你的 web.xml 中,带有隐藏"),r("code",[e._v("method")]),e._v("参数的 POST 将被转换为相应的 HTTP 方法请求。")]),e._v(" "),r("p",[e._v("为了支持 HTTP 方法转换,更新了 Spring MVC 表单标记以支持设置 HTTP 方法。例如,以下片段来自宠物诊所样本:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n

\n
\n')])])]),r("p",[e._v("前面的示例执行 HTTP POST,在请求参数后面隐藏“real”delete 方法。下面的示例显示了在 web.xml 中定义的"),r("code",[e._v("HiddenHttpMethodFilter")]),e._v("来选择它:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n httpMethodFilter\n org.springframework.web.filter.HiddenHttpMethodFilter\n\n\n\n httpMethodFilter\n petclinic\n\n")])])]),r("p",[e._v("下面的示例显示了相应的"),r("code",[e._v("@Controller")]),e._v("方法:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping(method = RequestMethod.DELETE)\npublic String deletePet(@PathVariable int ownerId, @PathVariable int petId) {\n this.clinic.deletePet(petId);\n return "redirect:/owners/" + ownerId;\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@RequestMapping(method = [RequestMethod.DELETE])\nfun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String {\n clinic.deletePet(petId)\n return "redirect:/owners/$ownerId"\n}\n')])])]),r("h6",{attrs:{id:"html5-标签"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#html5-标签"}},[e._v("#")]),e._v(" HTML5 标签")]),e._v(" "),r("p",[e._v("Spring 表单标记库允许输入动态属性,这意味着可以输入任何 HTML5 特定的属性。")]),e._v(" "),r("p",[e._v("表单"),r("code",[e._v("input")]),e._v("标记支持输入"),r("code",[e._v("text")]),e._v("以外的类型属性。这旨在允许呈现新的 HTML5 特定的输入类型,例如"),r("code",[e._v("email")]),e._v(","),r("code",[e._v("date")]),e._v(","),r("code",[e._v("range")]),e._v(",以及其他类型。请注意,不需要输入"),r("code",[e._v("type='text'")]),e._v(",因为"),r("code",[e._v("text")]),e._v("是默认类型。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-6-瓦片"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-6-瓦片"}},[e._v("#")]),e._v(" 1.10.6.瓦片")]),e._v(" "),r("p",[e._v("你可以在使用 Spring 的 Web 应用程序中集成图块——就像任何其他视图技术一样。这一节以一种宽泛的方式描述了如何做到这一点。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("本节重点介绍 Spring 对"),r("code",[e._v("org.springframework.web.servlet.view.tiles3")]),e._v("包中的磁贴版本 3 的支持。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"依赖关系"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#依赖关系"}},[e._v("#")]),e._v(" 依赖关系")]),e._v(" "),r("p",[e._v("为了能够使用磁贴,你必须在项目中添加对磁贴版本 3.0.1 或更高版本和"),r("a",{attrs:{href:"https://tiles.apache.org/framework/dependency-management.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("它的传递依赖关系"),r("OutboundLink")],1),e._v("的依赖关系。")]),e._v(" "),r("h5",{attrs:{id:"配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#配置"}},[e._v("#")]),e._v(" 配置")]),e._v(" "),r("p",[e._v("为了能够使用磁贴,你必须使用包含定义的文件来配置它(有关定义和其他磁贴概念的基本信息,请参见"),r("a",{attrs:{href:"https://tiles.apache.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("https://tiles.apache.org"),r("OutboundLink")],1),e._v(")。在 Spring 中,这是通过使用"),r("code",[e._v("TilesConfigurer")]),e._v("来完成的。下面的"),r("code",[e._v("ApplicationContext")]),e._v("配置示例展示了如何这样做:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n /WEB-INF/defs/general.xml\n /WEB-INF/defs/widgets.xml\n /WEB-INF/defs/administrator.xml\n /WEB-INF/defs/customer.xml\n /WEB-INF/defs/templates.xml\n \n \n\n')])])]),r("p",[e._v("前面的示例定义了五个包含定义的文件。这些文件都位于"),r("code",[e._v("WEB-INF/defs")]),e._v("目录中。在初始化"),r("code",[e._v("WebApplicationContext")]),e._v("时,文件被加载,定义工厂被初始化。完成此操作后,定义文件中包含的磁贴可以用作 Spring Web 应用程序中的视图。为了能够使用这些视图,与 Spring 中的任何其他视图技术一样,你必须有一个"),r("code",[e._v("ViewResolver")]),e._v(":通常是一个方便的"),r("code",[e._v("TilesViewResolver")]),e._v("。")]),e._v(" "),r("p",[e._v("你可以通过添加下划线和区域设置来指定特定于区域设置的磁贴定义,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n /WEB-INF/defs/tiles.xml\n /WEB-INF/defs/tiles_fr_FR.xml\n \n \n\n')])])]),r("p",[e._v("在前面的配置中,"),r("code",[e._v("tiles_fr_FR.xml")]),e._v("用于使用"),r("code",[e._v("fr_FR")]),e._v("语言环境的请求,默认情况下使用"),r("code",[e._v("tiles.xml")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("由于下划线是用来指示区域设置的,因此我们建议不要在文件名中使用"),r("br"),e._v(",否则将其用于瓷砖定义。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h6",{attrs:{id:"urlbasedviewresolver"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#urlbasedviewresolver"}},[e._v("#")]),e._v(" "),r("code",[e._v("UrlBasedViewResolver")])]),e._v(" "),r("p",[r("code",[e._v("UrlBasedViewResolver")]),e._v("为它必须解析的每个视图实例化给定的"),r("code",[e._v("viewClass")]),e._v("。下面的 Bean 定义了"),r("code",[e._v("UrlBasedViewResolver")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n')])])]),r("h6",{attrs:{id:"simplespringpreparerfactory和springbeanpreparerfactory"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#simplespringpreparerfactory和springbeanpreparerfactory"}},[e._v("#")]),e._v(" "),r("code",[e._v("SimpleSpringPreparerFactory")]),e._v("和"),r("code",[e._v("SpringBeanPreparerFactory")])]),e._v(" "),r("p",[e._v("作为一种高级特性, Spring 还支持两种特殊的贴片"),r("code",[e._v("PreparerFactory")]),e._v("实现方式。有关如何在磁贴定义文件中使用"),r("code",[e._v("ViewPreparer")]),e._v("引用的详细信息,请参见磁贴文档。")]),e._v(" "),r("p",[e._v("你可以根据指定的准备程序类,将"),r("code",[e._v("SimpleSpringPreparerFactory")]),e._v("指定为 AutoWire"),r("code",[e._v("ViewPreparer")]),e._v("实例,应用 Spring 的容器回调以及应用已配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围注释配置已被激活,"),r("code",[e._v("ViewPreparer")]),e._v("类中的注释将被自动检测并应用。请注意,这需要在磁贴定义文件中的准备程序类,就像默认的"),r("code",[e._v("PreparerFactory")]),e._v("所做的那样。")]),e._v(" "),r("p",[e._v("你可以指定"),r("code",[e._v("SpringBeanPreparerFactory")]),e._v("来对指定的编制者名称(而不是类)进行操作,从而从 DispatcherServlet 的应用程序上下文中获得相应的 Spring Bean。在这种情况下,完整的 Bean 创建过程处于 Spring 应用程序上下文的控制中,允许使用显式依赖注入配置、作用域 bean 等。请注意,你需要为每个准备者名称定义一个 Spring Bean 定义(如你的磁贴定义中所使用的)。下面的示例展示了如何在"),r("code",[e._v("TilesConfigurer")]),e._v(" Bean 上定义"),r("code",[e._v("SpringBeanPreparerFactory")]),e._v("属性:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n /WEB-INF/defs/general.xml\n /WEB-INF/defs/widgets.xml\n /WEB-INF/defs/administrator.xml\n /WEB-INF/defs/customer.xml\n /WEB-INF/defs/templates.xml\n \n \n\n \x3c!-- resolving preparer names as Spring bean definition names --\x3e\n \n\n\n')])])]),r("h4",{attrs:{id:"_1-10-7-rss-和-atom"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-7-rss-和-atom"}},[e._v("#")]),e._v(" 1.10.7.RSS 和 Atom")]),e._v(" "),r("p",[r("code",[e._v("AbstractAtomFeedView")]),e._v("和"),r("code",[e._v("AbstractRssFeedView")]),e._v("都继承自"),r("code",[e._v("AbstractFeedView")]),e._v("基类,并分别用于提供 Atom 和 RSS 提要视图。它们基于"),r("a",{attrs:{href:"https://rometools.github.io/rome/",target:"_blank",rel:"noopener noreferrer"}},[e._v("ROME"),r("OutboundLink")],1),e._v("项目,位于包"),r("code",[e._v("org.springframework.web.servlet.view.feed")]),e._v("中。")]),e._v(" "),r("p",[r("code",[e._v("AbstractAtomFeedView")]),e._v("要求你实现"),r("code",[e._v("buildFeedEntries()")]),e._v("方法,并可选地覆盖"),r("code",[e._v("buildFeedMetadata()")]),e._v("方法(默认实现为空)。下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class SampleContentAtomView extends AbstractAtomFeedView {\n\n @Override\n protected void buildFeedMetadata(Map model,\n Feed feed, HttpServletRequest request) {\n // implementation omitted\n }\n\n @Override\n protected List buildFeedEntries(Map model,\n HttpServletRequest request, HttpServletResponse response) throws Exception {\n // implementation omitted\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("class SampleContentAtomView : AbstractAtomFeedView() {\n\n override fun buildFeedMetadata(model: Map,\n feed: Feed, request: HttpServletRequest) {\n // implementation omitted\n }\n\n override fun buildFeedEntries(model: Map,\n request: HttpServletRequest, response: HttpServletResponse): List {\n // implementation omitted\n }\n}\n")])])]),r("p",[e._v("类似的要求也适用于实现"),r("code",[e._v("AbstractRssFeedView")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class SampleContentRssView extends AbstractRssFeedView {\n\n @Override\n protected void buildFeedMetadata(Map model,\n Channel feed, HttpServletRequest request) {\n // implementation omitted\n }\n\n @Override\n protected List buildFeedItems(Map model,\n HttpServletRequest request, HttpServletResponse response) throws Exception {\n // implementation omitted\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("class SampleContentRssView : AbstractRssFeedView() {\n\n override fun buildFeedMetadata(model: Map,\n feed: Channel, request: HttpServletRequest) {\n // implementation omitted\n }\n\n override fun buildFeedItems(model: Map,\n request: HttpServletRequest, response: HttpServletResponse): List {\n // implementation omitted\n }\n}\n")])])]),r("p",[e._v("如果你需要访问区域设置,"),r("code",[e._v("buildFeedItems()")]),e._v("和"),r("code",[e._v("buildFeedEntries()")]),e._v("方法将传入 HTTP 请求。HTTP 响应仅在设置 Cookie 或其他 HTTP 头时传入。方法返回后,提要将自动写入响应对象。")]),e._v(" "),r("p",[e._v("有关创建 Atom 视图的示例,请参见 ALEFArendsen Spring 团队博客"),r("a",{attrs:{href:"https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support",target:"_blank",rel:"noopener noreferrer"}},[e._v("entry"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-8-pdf-和-excel"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-8-pdf-和-excel"}},[e._v("#")]),e._v(" 1.10.8.PDF 和 Excel")]),e._v(" "),r("p",[e._v("Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。本节描述如何使用这些特性。")]),e._v(" "),r("h5",{attrs:{id:"文档视图介绍"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#文档视图介绍"}},[e._v("#")]),e._v(" 文档视图介绍")]),e._v(" "),r("p",[e._v("HTML 页面并不总是用户查看模型输出的最佳方式, Spring 使得从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是视图,并从服务器以正确的内容类型进行流媒体传输,以(希望)使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。")]),e._v(" "),r("p",[e._v("为了使用 Excel 视图,你需要将 Apache POI 库添加到 Classpath 中。为了生成 PDF,你需要添加(最好是)OpenPDF 库。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果可能的话,你应该使用底层文档生成库的最新版本"),r("br"),e._v("。特别是,我们强烈推荐 OpenPDF(例如,OpenPDF1.2.12)"),r("br"),e._v("而不是过时的原始 iText2.1.7,因为 OpenPDF 是积极维护的,"),r("br"),e._v("修复了不可信 PDF 内容的一个重要漏洞。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h5",{attrs:{id:"pdf-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#pdf-视图"}},[e._v("#")]),e._v(" PDF 视图")]),e._v(" "),r("p",[e._v("一个用于单词列表的简单 PDF 视图可以扩展"),r("code",[e._v("org.springframework.web.servlet.view.document.AbstractPdfView")]),e._v("并实现"),r("code",[e._v("buildPdfDocument()")]),e._v("方法,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('public class PdfWordList extends AbstractPdfView {\n\n protected void buildPdfDocument(Map model, Document doc, PdfWriter writer,\n HttpServletRequest request, HttpServletResponse response) throws Exception {\n\n List words = (List) model.get("wordList");\n for (String word : words) {\n doc.add(new Paragraph(word));\n }\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('class PdfWordList : AbstractPdfView() {\n\n override fun buildPdfDocument(model: Map, doc: Document, writer: PdfWriter,\n request: HttpServletRequest, response: HttpServletResponse) {\n\n val words = model["wordList"] as List\n for (word in words) {\n doc.add(Paragraph(word))\n }\n }\n}\n')])])]),r("p",[e._v("控制器可以从外部视图定义(通过名称引用它)返回这样的视图,也可以从处理程序方法返回"),r("code",[e._v("View")]),e._v("实例。")]),e._v(" "),r("h5",{attrs:{id:"excel-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#excel-视图"}},[e._v("#")]),e._v(" Excel 视图")]),e._v(" "),r("p",[e._v("Spring Framework4.2 以来,"),r("code",[e._v("org.springframework.web.servlet.view.document.AbstractXlsView")]),e._v("被提供为 Excel 视图的基类。它是基于 Apache POI 的,具有专门的子类("),r("code",[e._v("AbstractXlsxView")]),e._v("和"),r("code",[e._v("AbstractXlsxStreamingView")]),e._v(")来取代过时的"),r("code",[e._v("AbstractExcelView")]),e._v("类。")]),e._v(" "),r("p",[e._v("编程模型类似于"),r("code",[e._v("AbstractPdfView")]),e._v(",以"),r("code",[e._v("buildExcelDocument()")]),e._v("作为中心模板方法,控制器能够从外部定义(通过名称)返回这样的视图,或者作为处理程序方法的"),r("code",[e._v("View")]),e._v("实例。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-9-jackson"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-9-jackson"}},[e._v("#")]),e._v(" 1.10.9.Jackson")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-httpmessagewriter"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring 提供对 JacksonJSON 库的支持。")]),e._v(" "),r("h5",{attrs:{id:"基于-jackson-的-json-mvc-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#基于-jackson-的-json-mvc-视图"}},[e._v("#")]),e._v(" 基于 Jackson 的 JSON MVC 视图")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-httpmessagewriter"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("MappingJackson2JsonView")]),e._v("使用 Jackson 库的"),r("code",[e._v("ObjectMapper")]),e._v("将响应内容呈现为 JSON。默认情况下,模型映射的全部内容(除了特定于框架的类)都被编码为 JSON。对于需要对映射的内容进行筛选的情况,可以指定一组特定的模型属性来使用"),r("code",[e._v("modelKeys")]),e._v("属性进行编码。你还可以使用"),r("code",[e._v("extractValueFromSingleKeyModel")]),e._v("属性,将单键模型中的值直接提取和序列化,而不是作为模型属性的映射。")]),e._v(" "),r("p",[e._v("你可以根据需要使用 Jackson 提供的注释来定制 JSON 映射。当你需要进一步的控制时,你可以通过"),r("code",[e._v("ObjectMapper")]),e._v("属性注入一个自定义的"),r("code",[e._v("ObjectMapper")]),e._v(",用于需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况。")]),e._v(" "),r("h5",{attrs:{id:"基于-jackson-的-xml-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#基于-jackson-的-xml-视图"}},[e._v("#")]),e._v(" 基于 Jackson 的 XML 视图")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-view-httpmessagewriter"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("MappingJackson2XmlView")]),e._v("使用"),r("a",{attrs:{href:"https://github.com/FasterXML/jackson-dataformat-xml",target:"_blank",rel:"noopener noreferrer"}},[e._v("JacksonXML 扩展的"),r("OutboundLink")],1),r("code",[e._v("XmlMapper")]),e._v("将响应内容呈现为 XML。如果模型包含多个条目,你应该使用"),r("code",[e._v("modelKey")]),e._v(" Bean 属性显式地设置要序列化的对象。如果模型包含单个条目,则自动对其进行序列化。")]),e._v(" "),r("p",[e._v("你可以根据需要使用 JAXB 或 Jackson 提供的注释来定制 XML 映射。当需要进一步的控制时,可以通过"),r("code",[e._v("ObjectMapper")]),e._v("属性注入自定义"),r("code",[e._v("XmlMapper")]),e._v(",用于需要为特定类型提供序列化器和反序列化器的自定义 XML。")]),e._v(" "),r("h4",{attrs:{id:"_1-10-10-xml-编组"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-10-xml-编组"}},[e._v("#")]),e._v(" 1.10.10.XML 编组")]),e._v(" "),r("p",[r("code",[e._v("MarshallingView")]),e._v("使用 XML"),r("code",[e._v("Marshaller")]),e._v("(在"),r("code",[e._v("org.springframework.oxm")]),e._v("包中定义)将响应内容呈现为 XML。可以使用"),r("code",[e._v("MarshallingView")]),e._v("实例的"),r("code",[e._v("modelKey")]),e._v(" Bean 属性显式地设置要编组的对象。或者,该视图对所有模型属性进行迭代,并封送"),r("code",[e._v("Marshaller")]),e._v("支持的第一个类型。有关"),r("code",[e._v("org.springframework.oxm")]),e._v("包中的功能的更多信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/data-access.html#oxm"}},[e._v("使用 O/X 映射器编组 XML")]),e._v("。")],1),e._v(" "),r("h4",{attrs:{id:"_1-10-11-xslt-视图"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-10-11-xslt-视图"}},[e._v("#")]),e._v(" 1.10.11.XSLT 视图")]),e._v(" "),r("p",[e._v("XSLT 是一种 XML 转换语言,在 Web 应用程序中作为一种视图技术很受欢迎。如果你的应用程序自然地处理 XML,或者你的模型可以很容易地转换为 XML,那么 XSLT 作为一种视图技术是一个很好的选择。下面的部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。")]),e._v(" "),r("p",[e._v("Spring 这个示例是一个简单的应用程序,它在"),r("code",[e._v("控制器")]),e._v("中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及 XSLT 视图的视图名称。有关 Spring Web MVC 的"),r("code",[e._v("Controller")]),e._v("接口的详细信息,请参见"),r("a",{attrs:{href:"#mvc-controller"}},[e._v("带注释的控制器")]),e._v("。XSLT 控制器将单词列表转换为一个简单的 XML 文档,以便进行转换。")]),e._v(" "),r("h5",{attrs:{id:"豆子"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#豆子"}},[e._v("#")]),e._v(" 豆子")]),e._v(" "),r("p",[e._v("对于简单的 Spring Web 应用程序,配置是标准的:MVC 配置必须定义"),r("code",[e._v("XsltViewResolver")]),e._v(" Bean 和常规的 MVC 注释配置。下面的示例展示了如何做到这一点:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@EnableWebMvc\n@ComponentScan\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n\n @Bean\n public XsltViewResolver xsltViewResolver() {\n XsltViewResolver viewResolver = new XsltViewResolver();\n viewResolver.setPrefix("/WEB-INF/xsl/");\n viewResolver.setSuffix(".xslt");\n return viewResolver;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@EnableWebMvc\n@ComponentScan\n@Configuration\nclass WebConfig : WebMvcConfigurer {\n\n @Bean\n fun xsltViewResolver() = XsltViewResolver().apply {\n setPrefix("/WEB-INF/xsl/")\n setSuffix(".xslt")\n }\n}\n')])])]),r("h5",{attrs:{id:"controller"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#controller"}},[e._v("#")]),e._v(" Controller")]),e._v(" "),r("p",[e._v("我们还需要一个封装我们的字生成逻辑的控制器。")]),e._v(" "),r("p",[e._v("控制器逻辑封装在"),r("code",[e._v("@Controller")]),e._v("类中,处理程序方法定义如下:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class XsltController {\n\n @RequestMapping("/")\n public String home(Model model) throws Exception {\n Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();\n Element root = document.createElement("wordList");\n\n List words = Arrays.asList("Hello", "Spring", "Framework");\n for (String word : words) {\n Element wordNode = document.createElement("word");\n Text textNode = document.createTextNode(word);\n wordNode.appendChild(textNode);\n root.appendChild(wordNode);\n }\n\n model.addAttribute("wordList", root);\n return "home";\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.ui.set\n\n@Controller\nclass XsltController {\n\n @RequestMapping("/")\n fun home(model: Model): String {\n val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()\n val root = document.createElement("wordList")\n\n val words = listOf("Hello", "Spring", "Framework")\n for (word in words) {\n val wordNode = document.createElement("word")\n val textNode = document.createTextNode(word)\n wordNode.appendChild(textNode)\n root.appendChild(wordNode)\n }\n\n model["wordList"] = root\n return "home"\n }\n}\n')])])]),r("p",[e._v("到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,你也可以将 XML 文件加载为"),r("code",[e._v("Resource")]),e._v(",并使用它来代替自定义 DOM 文档。")]),e._v(" "),r("p",[e._v("有一些软件包可以自动对对象图进行“domify”,但是,在 Spring 之内,你可以完全灵活地以你选择的任何方式从你的模型中创建 DOM。这可以防止 XML 转换在模型数据的结构中起到太大的作用,这在使用工具管理 Domification 过程时是一种危险。")]),e._v(" "),r("h5",{attrs:{id:"转换"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#转换"}},[e._v("#")]),e._v(" 转换")]),e._v(" "),r("p",[e._v("最后,"),r("code",[e._v("XsltViewResolver")]),e._v("解析“home”XSLT 模板文件,并将 DOM 文档合并到其中以生成视图。如"),r("code",[e._v("XsltViewResolver")]),e._v("配置中所示,XSLT 模板位于"),r("code",[e._v("war")]),e._v("目录中的"),r("code",[e._v("war")]),e._v("文件中,并以"),r("code",[e._v("WEB-INF/xsl")]),e._v("文件扩展名结束。")]),e._v(" "),r("p",[e._v("下面的示例展示了一个 XSLT 转换:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n \n \n Hello!\n \n

My First Words

\n
    \n \n
\n \n \n
\n\n \n
  • \n
    \n\n
    \n')])])]),r("p",[e._v("前面的转换呈现为以下 HTML:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n Hello!\n \n \n

    My First Words

    \n
      \n
    • Hello
    • \n
    • Spring
    • \n
    • Framework
    • \n
    \n \n\n')])])]),r("h3",{attrs:{id:"_1-11-mvc-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-mvc-配置"}},[e._v("#")]),e._v(" 1.11.MVC 配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("MVC 爪哇 配置和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,并提供了一个配置 API 来对其进行定制。")]),e._v(" "),r("p",[e._v("有关配置 API 中没有的更高级的定制,请参见"),r("a",{attrs:{href:"#mvc-config-advanced-java"}},[e._v("高级 爪哇 配置")]),e._v("和"),r("a",{attrs:{href:"#mvc-config-advanced-xml"}},[e._v("高级 XML 配置")]),e._v("。")]),e._v(" "),r("p",[e._v("你不需要理解由 MVC 爪哇 配置和 MVC 名称空间创建的底层 bean。如果你想了解更多信息,请参见"),r("a",{attrs:{href:"#mvc-servlet-special-bean-types"}},[e._v("Special Bean Types")]),e._v("和"),r("a",{attrs:{href:"#mvc-servlet-config"}},[e._v("Web MVC 配置")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_1-11-1-启用-mvc-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-1-启用-mvc-配置"}},[e._v("#")]),e._v(" 1.11.1.启用 MVC 配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-enable"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在 爪哇 配置中,可以使用"),r("code",[e._v("@EnableWebMvc")]),e._v("注释来启用 MVC 配置,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig {\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig\n")])])]),r("p",[e._v("在 XML 配置中,可以使用"),r("code",[e._v("")]),e._v("元素来启用 MVC 配置,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n\n')])])]),r("p",[e._v("前面的示例注册了 Spring MVC的数量,并适应于 Classpath 上可用的依赖关系(例如,用于 JSON、XML 和其他的有效负载转换器)。")]),e._v(" "),r("h4",{attrs:{id:"_1-11-2-mvc-配置-api"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-2-mvc-配置-api"}},[e._v("#")]),e._v(" 1.11.2.MVC 配置 API")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-customize"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在 爪哇 配置中,你可以实现"),r("code",[e._v("WebMvcConfigurer")]),e._v("接口,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n // Implement configuration methods...\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n // Implement configuration methods...\n}\n")])])]),r("p",[e._v("在 XML 中,你可以检查"),r("code",[e._v("")]),e._v("的属性和子元素。你可以查看"),r("a",{attrs:{href:"https://schema.spring.io/mvc/spring-mvc.xsd",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring MVC XML schema"),r("OutboundLink")],1),e._v(",或者使用 IDE 的代码完成功能来发现哪些属性和子元素是可用的。")]),e._v(" "),r("h4",{attrs:{id:"_1-11-3-类型转换"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-3-类型转换"}},[e._v("#")]),e._v(" 1.11.3.类型转换")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-conversion"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("默认情况下,安装了各种数字和日期类型的格式化程序,并支持在字段上通过"),r("code",[e._v("@NumberFormat")]),e._v("和"),r("code",[e._v("@DateTimeFormat")]),e._v("进行定制。")]),e._v(" "),r("p",[e._v("要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addFormatters(FormatterRegistry registry) {\n // ...\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addFormatters(registry: FormatterRegistry) {\n // ...\n }\n}\n")])])]),r("p",[e._v("要在 XML Config 中执行相同的操作,请使用以下方法:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n')])])]),r("p",[e._v("Spring 默认情况下,MVC 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addFormatters(FormatterRegistry registry) {\n DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();\n registrar.setUseIsoFormat(true);\n registrar.registerFormatters(registry);\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addFormatters(registry: FormatterRegistry) {\n val registrar = DateTimeFormatterRegistrar()\n registrar.setUseIsoFormat(true)\n registrar.registerFormatters(registry)\n }\n}\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("参见[the"),r("code",[e._v("FormatterRegistrar")]),e._v("SPI]和"),r("code",[e._v("FormattingConversionServiceFactoryBean")]),e._v("有关何时使用"),r("br"),e._v("FormatterRegistrar 实现的更多信息。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-11-4-验证"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-4-验证"}},[e._v("#")]),e._v(" 1.11.4.验证")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-validation"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("默认情况下,如果"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validation-beanvalidation-overview"}},[e._v("Bean Validation")]),e._v("存在于 Classpath(例如, Hibernate 验证器)上,则"),r("code",[e._v("LocalValidatorFactoryBean")]),e._v("注册为全局"),r("RouterLink",{attrs:{to:"/spring-framework/core.html#validator"}},[e._v("Validator")]),e._v(",用于控制器方法参数上的"),r("code",[e._v("@Valid")]),e._v("和"),r("code",[e._v("Validated")]),e._v("。")],1),e._v(" "),r("p",[e._v("在 爪哇 配置中,你可以自定义全局"),r("code",[e._v("Validator")]),e._v("实例,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public Validator getValidator() {\n // ...\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun getValidator(): Validator {\n // ...\n }\n}\n")])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n\n\n')])])]),r("p",[e._v("请注意,你也可以在本地注册"),r("code",[e._v("Validator")]),e._v("实现,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Controller\npublic class MyController {\n\n @InitBinder\n protected void initBinder(WebDataBinder binder) {\n binder.addValidators(new FooValidator());\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Controller\nclass MyController {\n\n @InitBinder\n protected fun initBinder(binder: WebDataBinder) {\n binder.addValidators(FooValidator())\n }\n}\n")])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("如果需要将"),r("code",[e._v("LocalValidatorFactoryBean")]),e._v("注入到某个地方,请创建一个 Bean 并将"),r("br"),e._v("标记为"),r("code",[e._v("@Primary")]),e._v(",以避免与 MVC 配置中声明的值发生冲突。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-11-5-拦截器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-5-拦截器"}},[e._v("#")]),e._v(" 1.11.5.拦截器")]),e._v(" "),r("p",[e._v("在 爪哇 配置中,你可以注册拦截器以应用于传入的请求,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addInterceptors(InterceptorRegistry registry) {\n registry.addInterceptor(new LocaleChangeInterceptor());\n registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**");\n registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addInterceptors(registry: InterceptorRegistry) {\n registry.addInterceptor(LocaleChangeInterceptor())\n registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")\n registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*")\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n \n \n \n\n')])])]),r("h4",{attrs:{id:"_1-11-6-内容类型"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-6-内容类型"}},[e._v("#")]),e._v(" 1.11.6.内容类型")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-content-negotiation"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以配置 Spring MVC 如何从请求中确定所请求的媒体类型(例如,"),r("code",[e._v("Accept")]),e._v("报头、URL 路径扩展、查询参数和其他)。")]),e._v(" "),r("p",[e._v("默认情况下,只检查"),r("code",[e._v("Accept")]),e._v("标头。")]),e._v(" "),r("p",[e._v("如果必须使用基于 URL 的内容类型解析,请考虑在路径扩展上使用查询参数策略。有关更多详细信息,请参见"),r("a",{attrs:{href:"#mvc-ann-requestmapping-suffix-pattern-match"}},[e._v("后缀匹配")]),e._v("和"),r("a",{attrs:{href:"#mvc-ann-requestmapping-rfd"}},[e._v("后缀匹配和 RFD")]),e._v("。")]),e._v(" "),r("p",[e._v("在 爪哇 配置中,你可以自定义所请求的内容类型分辨率,如下例所示:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {\n configurer.mediaType("json", MediaType.APPLICATION_JSON);\n configurer.mediaType("xml", MediaType.APPLICATION_XML);\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {\n configurer.mediaType("json", MediaType.APPLICATION_JSON)\n configurer.mediaType("xml", MediaType.APPLICATION_XML)\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n\n \n \n json=application/json\n xml=application/xml\n \n \n\n')])])]),r("h4",{attrs:{id:"_1-11-7-消息转换器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-7-消息转换器"}},[e._v("#")]),e._v(" 1.11.7.消息转换器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-message-codecs"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以通过覆盖["),r("code",[e._v("configureMessageConverters()")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet//config/annotation/webmvccconfigurer.html#configureMessageConverters-java.util.util.list-)(以取代由 Spring MVC 创建的默认转换器),或者通过覆盖[https:///moverwomfrandframework=“2677”/DOCS:/[56.mframework:/jumframework:/")]),e._v(" "),r("p",[e._v("下面的示例使用定制的"),r("code",[e._v("ObjectMapper")]),e._v("来添加 XML 和 JacksonJSON 转换器,而不是默认的转换器:")]),e._v(" "),r("p",[e._v("爪哇")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfiguration implements WebMvcConfigurer {\n\n @Override\n public void configureMessageConverters(List> converters) {\n Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()\n .indentOutput(true)\n .dateFormat(new SimpleDateFormat("yyyy-MM-dd"))\n .modulesToInstall(new ParameterNamesModule());\n converters.add(new MappingJackson2HttpMessageConverter(builder.build()));\n converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfiguration : WebMvcConfigurer {\n\n override fun configureMessageConverters(converters: MutableList>) {\n val builder = Jackson2ObjectMapperBuilder()\n .indentOutput(true)\n .dateFormat(SimpleDateFormat("yyyy-MM-dd"))\n .modulesToInstall(ParameterNamesModule())\n converters.add(MappingJackson2HttpMessageConverter(builder.build()))\n converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()))\n')])])]),r("p",[e._v("在前面的示例中,["),r("code",[e._v("Jackson2ObjectMapperBuilder")]),e._v("](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2objectmapperbuilder.html)用于为"),r("code",[e._v("MappingJackson2HttpMessageConverter")]),e._v("和"),r("code",[e._v("MappingJackson2XmlHttpMessageConverter")]),e._v("创建一个通用配置,并启用了缩进、自定义的日期格式,以及注册[<"),r("code",[e._v("jackson-module-parameter-names")]),e._v("](https://gitHub.com/gitHub-module-module-names-names),这为访问参数添加了参数")]),e._v(" "),r("p",[e._v("这个构建器自定义 Jackson 的默认属性如下:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("["),r("code",[e._v("DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES")]),e._v("](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/deserializationfeature.html#fail_on_unknown_properties)被禁用。")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("MapperFeature.DEFAULT_VIEW_INCLUSION")]),e._v("](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/mapperfeature.html#default_view_inclusion)被禁用。")])])]),e._v(" "),r("p",[e._v("如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://github.com/FasterXML/jackson-datatype-joda",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jackson-数据类型-Joda"),r("OutboundLink")],1),e._v(":支持 Joda-time 类型。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://github.com/FasterXML/jackson-datatype-jsr310",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jackson-数据类型-JSR310"),r("OutboundLink")],1),e._v(":支持 爪哇8 日期和时间 API 类型。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://github.com/FasterXML/jackson-datatype-jdk8",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jackson-数据类型-JDK8"),r("OutboundLink")],1),e._v(":支持其他 Java8 类型,例如"),r("code",[e._v("Optional")]),e._v("。")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("jackson-module-kotlin")]),e._v("](https://github.com/fasterxml/Jackson-module- Kotlin):支持 Kotlin 类和数据类。")])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在 JacksonXML 支持下启用缩进需要["),r("code",[e._v("woodstox-core-asl")]),e._v("](https://search. Maven.org/#search%7CGAv%7c1%7cg%3a%22org.codehaus.woodstox%22%20and%20a%3a%22woodstox-core-asl%22)的依赖性,此外还需要["),r("code",[e._v("jackson-dataformat-xml")]),e._v("](https://search. Maven.xml%7c1%7caga%7ca%3a%22%xml-dataformat-22%22%22)的依赖性。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("还有其他有趣的 Jackson 模块可供选择:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://github.com/zalando/jackson-datatype-money",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jackson-数据类型-货币"),r("OutboundLink")],1),e._v(":支持"),r("code",[e._v("javax.money")]),e._v("类型(非官方模块)。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://github.com/FasterXML/jackson-datatype-hibernate",target:"_blank",rel:"noopener noreferrer"}},[e._v("jackson-datatype-hibernate"),r("OutboundLink")],1),e._v(":支持 Hibernate 特定的类型和属性(包括惰性加载方面)。")])])]),e._v(" "),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n \n\n\n\n\n\n')])])]),r("h4",{attrs:{id:"_1-11-8-视图控制器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-8-视图控制器"}},[e._v("#")]),e._v(" 1.11.8.视图控制器")]),e._v(" "),r("p",[e._v("这是一个用于定义"),r("code",[e._v("ParameterizableViewController")]),e._v("的快捷方式,该快捷方式在调用时立即转发到视图。如果在视图生成响应之前没有要运行的 Java 控制器逻辑,则可以在静态情况下使用它。")]),e._v(" "),r("p",[e._v("下面的 Java 配置示例将对"),r("code",[e._v("/")]),e._v("的请求转发到一个名为"),r("code",[e._v("home")]),e._v("的视图:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addViewControllers(ViewControllerRegistry registry) {\n registry.addViewController("/").setViewName("home");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addViewControllers(registry: ViewControllerRegistry) {\n registry.addViewController("/").setViewName("home")\n }\n}\n')])])]),r("p",[e._v("下面的示例通过使用"),r("code",[e._v("")]),e._v("元素,实现了与前面示例相同的功能,但是使用了 XML:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("如果"),r("code",[e._v("@RequestMapping")]),e._v("方法被映射到任何 HTTP 方法的 URL,则不能使用视图控制器来处理相同的 URL。这是因为 URL 与带注释的控制器的匹配被认为是对端点所有权的足够强的指示,因此可以将 405(方法 _ 不允许)、415(不支持 _media_type)或类似的响应发送到客户机,以帮助进行调试。出于这个原因,建议避免在带注释的控制器和视图控制器之间分割 URL 处理。")]),e._v(" "),r("h4",{attrs:{id:"_1-11-9-视图解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-9-视图解析器"}},[e._v("#")]),e._v(" 1.11.9.视图解析器")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-view-resolvers"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("MVC 配置简化了视图解析程序的注册。")]),e._v(" "),r("p",[e._v("下面的 Java 配置示例通过使用 JSP 和 Jackson 作为 JSON 呈现的默认"),r("code",[e._v("View")]),e._v("配置内容协商视图解析:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.enableContentNegotiation(new MappingJackson2JsonView());\n registry.jsp();\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.enableContentNegotiation(MappingJackson2JsonView())\n registry.jsp()\n }\n}\n")])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n\n')])])]),r("p",[e._v("但是,请注意,自由标记、磁贴、Groovy 标记和脚本模板也需要配置底层视图技术。")]),e._v(" "),r("p",[e._v("MVC 命名空间提供了专用的元素。下面的示例与 Freemarker 一起工作:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n\n\n\n \n\n')])])]),r("p",[e._v("在 Java 配置中,你可以添加相应的"),r("code",[e._v("Configurer")]),e._v(" Bean,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureViewResolvers(ViewResolverRegistry registry) {\n registry.enableContentNegotiation(new MappingJackson2JsonView());\n registry.freeMarker().cache(false);\n }\n\n @Bean\n public FreeMarkerConfigurer freeMarkerConfigurer() {\n FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();\n configurer.setTemplateLoaderPath("/freemarker");\n return configurer;\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureViewResolvers(registry: ViewResolverRegistry) {\n registry.enableContentNegotiation(MappingJackson2JsonView())\n registry.freeMarker().cache(false)\n }\n\n @Bean\n fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {\n setTemplateLoaderPath("/freemarker")\n }\n}\n')])])]),r("h4",{attrs:{id:"_1-11-10-静态资源"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-10-静态资源"}},[e._v("#")]),e._v(" 1.11.10.静态资源")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-static-resources"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("此选项提供了一种方便的方式来从["),r("code",[e._v("Resource")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/resource.html)-based 位置的列表中提供静态资源。")]),e._v(" "),r("p",[e._v("在下一个示例中,给定一个以"),r("code",[e._v("/resources")]),e._v("开头的请求,相对路径用于在 Web 应用程序根目录下或在"),r("code",[e._v("/static")]),e._v("下的 Classpath 上查找和服务相对于"),r("code",[e._v("/public")]),e._v("的静态资源。这些资源将在一年后到期,以确保最大程度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求。"),r("code",[e._v("Last-Modified")]),e._v("信息是从"),r("code",[e._v("Resource#lastModified")]),e._v("推导出来的,因此"),r("code",[e._v('"Last-Modified"')]),e._v("头支持 HTTP 条件请求。")]),e._v(" "),r("p",[e._v("下面的清单展示了如何使用 Java 配置来实现这一点:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addResourceHandlers(ResourceHandlerRegistry registry) {\n registry.addResourceHandler("/resources/**")\n .addResourceLocations("/public", "classpath:/static/")\n .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addResourceHandlers(registry: ResourceHandlerRegistry) {\n registry.addResourceHandler("/resources/**")\n .addResourceLocations("/public", "classpath:/static/")\n .setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("p",[e._v("另见"),r("a",{attrs:{href:"#mvc-caching-static-resources"}},[e._v("对静态资源的 HTTP 缓存支持")]),e._v("。")]),e._v(" "),r("p",[e._v("资源处理程序还支持["),r("code",[e._v("ResourceResolver")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/resource/resourceresolver.html)实现和["),r("code",[e._v("ResourceTransformer")]),e._v("](https://DOCS. Spring.io/ Spring.io/ Spring-framework/DOCS/5.3.16/javoc-api/org/org/web/ Servlet/resource/resourcefork/resourcer.html)实现,你可以使用这些实现来创建一个优化的工具链,以便使用")]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("VersionResourceResolver")]),e._v("来实现基于从内容、固定应用程序版本或其他版本计算的 MD5 散列的版本管理的资源 URL。"),r("code",[e._v("ContentVersionStrategy")]),e._v("(md5hash)是一个很好的选择——除了一些明显的例外,例如与模块加载程序一起使用的 JavaScript 资源。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何在 Java 配置中使用"),r("code",[e._v("VersionResourceResolver")]),e._v(":")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void addResourceHandlers(ResourceHandlerRegistry registry) {\n registry.addResourceHandler("/resources/**")\n .addResourceLocations("/public/")\n .resourceChain(true)\n .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun addResourceHandlers(registry: ResourceHandlerRegistry) {\n registry.addResourceHandler("/resources/**")\n .addResourceLocations("/public/")\n .resourceChain(true)\n .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n \n \n \n \n \n \n\n')])])]),r("p",[e._v("然后,你可以使用"),r("code",[e._v("ResourceUrlProvider")]),e._v("重写 URL,并应用整个解析器和变压器链——例如,用于插入版本。MVC 配置提供了"),r("code",[e._v("ResourceUrlProvider")]),e._v(" Bean,这样就可以将其注入到其他配置中。你还可以使用"),r("code",[e._v("ResourceUrlEncodingFilter")]),e._v("对 ThymeLeaf、JSP、Freemarker 和其他具有依赖于"),r("code",[e._v("HttpServletResponse#encodeURL")]),e._v("的 URL 标记的文件进行透明的重写。")]),e._v(" "),r("p",[e._v("请注意,当同时使用"),r("code",[e._v("EncodedResourceResolver")]),e._v("(例如,用于服务 gzipped 或 brotli 编码的资源)和"),r("code",[e._v("VersionResourceResolver")]),e._v("时,必须按此顺序注册它们。这确保了基于内容的版本总是基于未编码的文件进行可靠的计算。")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://www.webjars.org/documentation",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebJars"),r("OutboundLink")],1),e._v("也通过"),r("code",[e._v("WebJarsResourceResolver")]),e._v("支持,这是在 Classpath 上存在"),r("code",[e._v("org.webjars:webjars-locator-core")]),e._v("库时自动注册的。解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从"),r("code",[e._v("/jquery/jquery.min.js")]),e._v("到"),r("code",[e._v("/jquery/1.2.0/jquery.min.js")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("基于"),r("code",[e._v("ResourceHandlerRegistry")]),e._v("的 Java 配置为细粒度控制提供了进一步的选项"),r("br"),e._v(",例如,上次修改行为和优化的资源解析。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_1-11-11-默认值-servlet"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-11-默认值-servlet"}},[e._v("#")]),e._v(" 1.11.11.默认值 Servlet")]),e._v(" "),r("p",[e._v("Spring MVC 允许将"),r("code",[e._v("DispatcherServlet")]),e._v("映射到"),r("code",[e._v("/")]),e._v("(从而覆盖容器的默认值 Servlet 的映射),同时仍然允许由容器的默认值处理静态资源请求 Servlet。它使用"),r("code",[e._v("DefaultServletHttpRequestHandler")]),e._v("的 URL 映射配置"),r("code",[e._v("/**")]),e._v(",并且相对于其他 URL 映射的优先级最低。")]),e._v(" "),r("p",[e._v("此处理程序将所有请求转发到缺省 Servlet。因此,它必须以所有其他 URL"),r("code",[e._v("HandlerMappings")]),e._v("的顺序保持在最后。如果使用"),r("code",[e._v("")]),e._v(",就是这种情况。或者,如果你设置了自己定制的"),r("code",[e._v("HandlerMapping")]),e._v("实例,请确保将其"),r("code",[e._v("order")]),e._v("属性设置为一个低于"),r("code",[e._v("DefaultServletHttpRequestHandler")]),e._v("的值,即"),r("code",[e._v("Integer.MAX_VALUE")]),e._v("。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何通过使用默认设置来启用该功能:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {\n configurer.enable();\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {\n configurer.enable()\n }\n}\n")])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n")])])]),r("p",[e._v("要重写"),r("code",[e._v("/")]),e._v(" Servlet 映射的注意事项是,默认 Servlet 的"),r("code",[e._v("RequestDispatcher")]),e._v("必须按名称而不是按路径检索。"),r("code",[e._v("DefaultServletHttpRequestHandler")]),e._v("尝试在启动时自动检测容器的默认 Servlet,使用大多数主要 Servlet 容器(包括 Tomcat、 Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)的已知名称列表。如果默认值 Servlet 已被定制配置为不同的名称,或者在默认值 Servlet 名称未知的情况下正在使用不同的 Servlet 容器,那么你必须显式地提供默认值 Servlet 的名称,如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {\n configurer.enable("myCustomDefaultServlet");\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) {\n configurer.enable("myCustomDefaultServlet")\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n')])])]),r("h4",{attrs:{id:"_1-11-12-路径匹配"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-12-路径匹配"}},[e._v("#")]),e._v(" 1.11.12.路径匹配")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-path-matching"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("你可以自定义与路径匹配和 URL 处理相关的选项。有关单个选项的详细信息,请参见["),r("code",[e._v("PathMatchConfigurer")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/config/annotation/pathmatchconfigrer.html)Javadoc。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何在 Java 配置中定制路径匹配:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\npublic class WebConfig implements WebMvcConfigurer {\n\n @Override\n public void configurePathMatch(PathMatchConfigurer configurer) {\n configurer\n .setPatternParser(new PathPatternParser())\n .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));\n }\n\n private PathPatternParser patternParser() {\n // ...\n }\n}\n')])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebMvc\nclass WebConfig : WebMvcConfigurer {\n\n override fun configurePathMatch(configurer: PathMatchConfigurer) {\n configurer\n .setPatternParser(patternParser)\n .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))\n }\n\n fun patternParser(): PathPatternParser {\n //...\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了如何用 XML 实现相同的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n \n\n\n\n\n')])])]),r("h4",{attrs:{id:"_1-11-13-高级-java-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-13-高级-java-配置"}},[e._v("#")]),e._v(" 1.11.13.高级 Java 配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-config-advanced-java"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[r("code",[e._v("@EnableWebMvc")]),e._v("imports"),r("code",[e._v("DelegatingWebMvcConfiguration")]),e._v(",其中:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("为 Spring MVC 应用程序提供默认的 Spring 配置")])]),e._v(" "),r("li",[r("p",[e._v("检测并委托"),r("code",[e._v("WebMvcConfigurer")]),e._v("实现来定制该配置。")])])]),e._v(" "),r("p",[e._v("对于高级模式,可以删除"),r("code",[e._v("@EnableWebMvc")]),e._v("并直接从"),r("code",[e._v("DelegatingWebMvcConfiguration")]),e._v("进行扩展,而不是实现"),r("code",[e._v("WebMvcConfigurer")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\npublic class WebConfig extends DelegatingWebMvcConfiguration {\n\n // ...\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\nclass WebConfig : DelegatingWebMvcConfiguration() {\n\n // ...\n}\n")])])]),r("p",[e._v("你可以将现有的方法保留在"),r("code",[e._v("WebConfig")]),e._v("中,但是你现在也可以重写 Bean 来自基类的声明,并且你仍然可以在 Classpath 上拥有任何数量的其他"),r("code",[e._v("WebMvcConfigurer")]),e._v("实现。")]),e._v(" "),r("h4",{attrs:{id:"_1-11-14-高级-xml-配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-11-14-高级-xml-配置"}},[e._v("#")]),e._v(" 1.11.14.高级 XML 配置")]),e._v(" "),r("p",[e._v("MVC 命名空间没有高级模式。如果你需要在 Bean 上自定义一个你无法以其他方式更改的属性,那么你可以使用 Spring "),r("code",[e._v("BeanPostProcessor")]),e._v("生命周期钩子"),r("code",[e._v("ApplicationContext")]),e._v(",如下例所示:")]),e._v(" "),r("p",[e._v("Java")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Component\npublic class MyPostProcessor implements BeanPostProcessor {\n\n public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException {\n // ...\n }\n}\n")])])]),r("p",[e._v("Kotlin")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Component\nclass MyPostProcessor : BeanPostProcessor {\n\n override fun postProcessBeforeInitialization(bean: Any, name: String): Any {\n // ...\n }\n}\n")])])]),r("p",[e._v("请注意,你需要将"),r("code",[e._v("MyPostProcessor")]),e._v("声明为 Bean,可以在 XML 中显式地声明,也可以通过"),r("code",[e._v("")]),e._v("声明来检测它。")]),e._v(" "),r("h3",{attrs:{id:"_1-12-http-2"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_1-12-http-2"}},[e._v("#")]),e._v(" 1.12.http/2")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-http2"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Servlet 需要 4 个容器来支持 HTTP/2,并且 Spring Framework5 与 Servlet API4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见"),r("a",{attrs:{href:"https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support",target:"_blank",rel:"noopener noreferrer"}},[e._v("HTTP/2Wiki 页面"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("p",[e._v("Servlet API 确实公开了一个与 HTTP/2 相关的构造。你可以使用"),r("code",[e._v("javax.servlet.http.PushBuilder")]),e._v("来主动地将资源推送到客户端,并且它被支持为"),r("a",{attrs:{href:"#mvc-ann-arguments"}},[e._v("方法参数")]),e._v("到"),r("code",[e._v("@RequestMapping")]),e._v("的方法。")]),e._v(" "),r("h2",{attrs:{id:"_2-rest-客户"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_2-rest-客户"}},[e._v("#")]),e._v(" 2. REST 客户")]),e._v(" "),r("p",[e._v("本节描述客户端访问 REST 端点的选项。")]),e._v(" "),r("h3",{attrs:{id:"_2-1-resttemplate"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-resttemplate"}},[e._v("#")]),e._v(" 2.1."),r("code",[e._v("RestTemplate")])]),e._v(" "),r("p",[r("code",[e._v("RestTemplate")]),e._v("是执行 HTTP 请求的同步客户端。 Spring 它是最初的 REST 客户机,在底层 HTTP 客户库上公开了一个简单的模板方法 API。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("截至 5.0,"),r("code",[e._v("RestTemplate")]),e._v("处于维护模式,只有少量的"),r("br"),e._v("更改请求和 bug 被接受。请考虑使用"),r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-client"}},[e._v("WebClient")]),e._v(",它提供了一个更现代的 API,并且"),r("br"),e._v("支持同步、异步和流场景。")],1)])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("详见"),r("RouterLink",{attrs:{to:"/spring-framework/integration.html#rest-client-access"}},[e._v("REST 端点")]),e._v("。")],1),e._v(" "),r("h3",{attrs:{id:"_2-2-webclient"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-webclient"}},[e._v("#")]),e._v(" 2.2."),r("code",[e._v("WebClient")])]),e._v(" "),r("p",[r("code",[e._v("WebClient")]),e._v("是执行 HTTP 请求的非阻塞、反应式客户端。它是在 5.0 中引入的,并提供了"),r("code",[e._v("RestTemplate")]),e._v("的现代替代方案,有效地支持同步和异步以及流场景。")]),e._v(" "),r("p",[e._v("与"),r("code",[e._v("RestTemplate")]),e._v("相反,"),r("code",[e._v("WebClient")]),e._v("支持以下内容:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("非阻塞 I/O。")])]),e._v(" "),r("li",[r("p",[e._v("反应性气流反压。")])]),e._v(" "),r("li",[r("p",[e._v("高并发性与较少的硬件资源.")])]),e._v(" "),r("li",[r("p",[e._v("功能风格的、流畅的 API,充分利用了 Java8Lambdas。")])]),e._v(" "),r("li",[r("p",[e._v("同步和异步交互。")])]),e._v(" "),r("li",[r("p",[e._v("从服务器往上流或往下流。")])])]),e._v(" "),r("p",[e._v("有关更多详细信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-client"}},[e._v("WebClient")]),e._v("。")],1),e._v(" "),r("h2",{attrs:{id:"_3-测试"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_3-测试"}},[e._v("#")]),e._v(" 3. 测试")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-test"}},[e._v("Same in Spring WebFlux")])],1),e._v(" "),r("p",[e._v("本节总结了用于 Spring MVC 应用程序的"),r("code",[e._v("spring-test")]),e._v("中可用的选项。")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("Servlet API 模拟:用于单元测试控制器、过滤器和其他 Web 组件的 Servlet API 合同的模拟实现。有关更多详细信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/testing.html#mock-objects-servlet"}},[e._v("Servlet API")]),e._v("模拟对象。")],1)]),e._v(" "),r("li",[r("p",[e._v("TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法对加载的配置进行有效缓存,以及支持用"),r("code",[e._v("MockServletContext")]),e._v("加载"),r("code",[e._v("WebApplicationContext")]),e._v("。有关更多详细信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/testing.html#testcontext-framework"}},[e._v("TestContext 框架")]),e._v("。")],1)]),e._v(" "),r("li",[r("p",[e._v("Spring MVC 测试:一种框架,也称为"),r("code",[e._v("MockMvc")]),e._v(",用于通过"),r("code",[e._v("DispatcherServlet")]),e._v("测试带注释的控制器(即,支持注释),用 Spring MVC 基础设施完成,但没有 HTTP 服务器。有关更多详细信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/testing.html#spring-mvc-test-framework"}},[e._v("Spring MVC Test")]),e._v("。")],1)]),e._v(" "),r("li",[r("p",[e._v("客户端 REST:"),r("code",[e._v("spring-test")]),e._v("提供了一个"),r("code",[e._v("MockRestServiceServer")]),e._v(",你可以将其用作模拟服务器,用于测试内部使用"),r("code",[e._v("RestTemplate")]),e._v("的客户端代码。有关更多详细信息,请参见"),r("RouterLink",{attrs:{to:"/spring-framework/testing.html#spring-mvc-test-client"}},[e._v("客户机 REST 测试")]),e._v("。")],1)]),e._v(" "),r("li",[r("p",[r("code",[e._v("WebTestClient")]),e._v(":用于测试 WebFlux 应用程序,但也可以用于通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个非阻塞的、反应性的客户机,非常适合于测试异步和流媒体场景。")])])]),e._v(" "),r("h2",{attrs:{id:"_4-websockets"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-websockets"}},[e._v("#")]),e._v(" 4. WebSockets")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("参考文档的这一部分涵盖了对 Servlet 堆栈、 WebSocket 消息传递的支持,这些消息传递包括原始 WebSocket 交互、 WebSocket 通过 Sockjs 的模拟,以及通过 STOMP 作为 WebSocket 上的子协议的发布-订阅消息传递。")]),e._v(" "),r("h3",{attrs:{id:"_4-1-websocket-介绍"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-websocket-介绍"}},[e._v("#")]),e._v(" 4.1. WebSocket 介绍")]),e._v(" "),r("p",[e._v("WebSocket 协议"),r("a",{attrs:{href:"https://tools.ietf.org/html/rfc6455",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC 6455"),r("OutboundLink")],1),e._v("提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。")]),e._v(" "),r("p",[e._v("WebSocket 交互以一个 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("GET /spring-websocket-portfolio/portfolio HTTP/1.1\nHost: localhost:8080\nUpgrade: websocket (1)\nConnection: Upgrade (2)\nSec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==\nSec-WebSocket-Protocol: v10.stomp, v11.stomp\nSec-WebSocket-Version: 13\nOrigin: http://localhost:8080\n")])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[r("code",[e._v("Upgrade")]),e._v("标头。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("使用"),r("code",[e._v("Upgrade")]),e._v("连接。")])])])]),e._v(" "),r("p",[e._v("具有 WebSocket 支持的服务器将返回类似于以下内容的输出,而不是通常的 200 状态代码:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("HTTP/1.1 101 Switching Protocols (1)\nUpgrade: websocket\nConnection: Upgrade\nSec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=\nSec-WebSocket-Protocol: v10.stomp\n")])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("协议转换")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在成功握手之后,HTTP 升级请求中的 TCP 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。")]),e._v(" "),r("p",[e._v("关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,HTML5 的 WebSocket 章,或 Web 上的许多介绍和教程中的任何一个。")]),e._v(" "),r("p",[e._v("注意,如果 WebSocket 服务器运行在 Web 服务器(例如 Nginx)的后面,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,则检查与 WebSocket 支持相关的云提供商的指令。")]),e._v(" "),r("h4",{attrs:{id:"_4-1-1-http-与-websocket"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-1-http-与-websocket"}},[e._v("#")]),e._v(" 4.1.1.HTTP 与 WebSocket")]),e._v(" "),r("p",[e._v("WebSocket 即使被设计为与 HTTP 兼容并且以 HTTP 请求开始,重要的是要理解这两个协议导致非常不同的体系结构和应用程序编程模型。")]),e._v(" "),r("p",[e._v("在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。")]),e._v(" "),r("p",[e._v("相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息都在相同的 TCP 连接上流动。这指向了一种完全不同的异步、事件驱动的消息传递体系结构。")]),e._v(" "),r("p",[e._v("WebSocket 也是一种低级传输协议,其与 HTTP 不同,不对消息的内容规定任何语义。这意味着,除非客户机和服务器在消息语义上达成一致,否则就没有路由或处理消息的方法。")]),e._v(" "),r("p",[e._v("WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议(例如,STOMP),通过头上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。")]),e._v(" "),r("h4",{attrs:{id:"_4-1-2-何时使用-websockets"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-2-何时使用-websockets"}},[e._v("#")]),e._v(" 4.1.2.何时使用 WebSockets")]),e._v(" "),r("p",[e._v("WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。")]),e._v(" "),r("p",[e._v("例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。")]),e._v(" "),r("p",[e._v("延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。 WebSocket 是低延迟、高频率和大容量的组合,这是使用 WebSocket 的最佳情况。")]),e._v(" "),r("p",[e._v("还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递"),r("code",[e._v("Upgrade")]),e._v("头,或者是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公共的应用程序使用 WebSocket 是一个更直接的决定。")]),e._v(" "),r("h3",{attrs:{id:"_4-2-websocket-api"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-websocket-api"}},[e._v("#")]),e._v(" 4.2. WebSocket API")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket-server"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("Spring 框架提供了一个 WebSocket API,你可以使用该 API 来编写处理 WebSocket 消息的客户端和服务器端应用程序。")]),e._v(" "),r("h4",{attrs:{id:"_4-2-1-websockethandler"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-1-websockethandler"}},[e._v("#")]),e._v(" 4.2.1."),r("code",[e._v("WebSocketHandler")])]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket-server-handler"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("创建 WebSocket 服务器就像实现"),r("code",[e._v("WebSocketHandler")]),e._v("一样简单,或者更有可能的是,扩展"),r("code",[e._v("TextWebSocketHandler")]),e._v("或"),r("code",[e._v("BinaryWebSocketHandler")]),e._v("。下面的示例使用"),r("code",[e._v("TextWebSocketHandler")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("import org.springframework.web.socket.WebSocketHandler;\nimport org.springframework.web.socket.WebSocketSession;\nimport org.springframework.web.socket.TextMessage;\n\npublic class MyHandler extends TextWebSocketHandler {\n\n @Override\n public void handleTextMessage(WebSocketSession session, TextMessage message) {\n // ...\n }\n\n}\n")])])]),r("p",[e._v("有专门的 WebSocket Java 配置和 XML 命名空间支持,用于将前面的 WebSocket 处理程序映射到特定的 URL,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.socket.config.annotation.EnableWebSocket;\nimport org.springframework.web.socket.config.annotation.WebSocketConfigurer;\nimport org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;\n\n@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Override\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n registry.addHandler(myHandler(), "/myHandler");\n }\n\n @Bean\n public WebSocketHandler myHandler() {\n return new MyHandler();\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n\n \n\n\n')])])]),r("p",[e._v("前面的示例是用于 Spring MVC 应用程序中的,并且应该包括在[](#MVC- Servlet)的配置中。然而, Spring 的 WebSocket 支持并不依赖于 Spring MVC。在["),r("code",[e._v("WebSocketHttpRequestHandler")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/server/support/websockettprequesthandler.html)的帮助下,将"),r("code",[e._v("WebSocketHandler")]),e._v("集成到其他 HTTP 服务环境中是相对简单的。")]),e._v(" "),r("p",[e._v("当直接 VS 间接地使用"),r("code",[e._v("WebSocketHandler")]),e._v("API 时,例如通过"),r("a",{attrs:{href:"#websocket-stomp"}},[e._v("STOMP")]),e._v("消息传递时,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话(JSR-356)不允许并发。一个选项是将"),r("code",[e._v("WebSocketSession")]),e._v("与["),r("code",[e._v("ConcurrentWebSocketSessionDecorator")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/handler/concurrentwebsocketsessiondecorator.html)包装在一起。")]),e._v(" "),r("h4",{attrs:{id:"_4-2-2-websocket-握手"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-2-websocket-握手"}},[e._v("#")]),e._v(" 4.2.2. WebSocket 握手")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket-server-handshake"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("定制初始 HTTP WebSocket 握手请求的最简单方法是通过"),r("code",[e._v("HandshakeInterceptor")]),e._v(",该方法公开了握手之前和之后的方法。你可以使用这样的拦截器来阻止握手或使"),r("code",[e._v("WebSocketSession")]),e._v("的任何属性可用。下面的示例使用内置的拦截器将 HTTP 会话属性传递给 WebSocket 会话:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Override\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n registry.addHandler(new MyHandler(), "/myHandler")\n .addInterceptors(new HttpSessionHandshakeInterceptor());\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n \n \n\n \n\n\n')])])]),r("p",[e._v("一个更高级的选项是扩展执行 WebSocket 握手步骤的"),r("code",[e._v("DefaultHandshakeHandler")]),e._v(",包括验证客户机原点、协商子协议和其他细节。如果应用程序需要配置自定义"),r("code",[e._v("RequestUpgradeStrategy")]),e._v("以适应 WebSocket 服务器引擎和尚未支持的版本,则可能还需要使用此选项(有关此主题的更多信息,请参见"),r("a",{attrs:{href:"#websocket-server-deployment"}},[e._v("Deployment")]),e._v(")。Java 配置和 XML 命名空间都使配置自定义"),r("code",[e._v("HandshakeHandler")]),e._v("成为可能。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring 提供了一个"),r("code",[e._v("WebSocketHandlerDecorator")]),e._v("基类,你可以使用它来使用附加行为来装饰"),r("br"),e._v("a"),r("code",[e._v("WebSocketHandler")]),e._v("。当使用 WebSocket Java 配置"),r("br"),e._v("或 XML 命名空间时,默认提供并添加日志记录和异常处理"),r("br"),e._v("实现。"),r("code",[e._v("ExceptionWebSocketHandlerDecorator")]),e._v("捕获由任何"),r("code",[e._v("WebSocketHandler")]),e._v("方法产生的所有未捕获的"),r("br"),e._v("异常,并关闭 WebSocket "),r("br"),e._v("具有状态"),r("code",[e._v("1011")]),e._v("的会话,这表示服务器错误。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_4-2-3-部署"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-3-部署"}},[e._v("#")]),e._v(" 4.2.3.部署")]),e._v(" "),r("p",[e._v("Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中"),r("code",[e._v("DispatcherServlet")]),e._v("同时服务于 HTTP WebSocket 握手和其他 HTTP 请求。通过调用"),r("code",[e._v("WebSocketHttpRequestHandler")]),e._v(",也很容易集成到其他 HTTP 处理场景中。这很方便,也很容易理解。但是,对于 JSR-356 运行时,需要进行特殊的考虑。")]),e._v(" "),r("p",[e._v("Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的 Servlet 容器 Classpath 扫描( Servlet 3 特征)。另一种是在 Servlet 容器初始化时使用的注册 API。这两种机制都不可能对所有 HTTP 处理(包括 WebSocket 握手和所有其他 HTTP 请求)使用单一的“前置控制器”,例如 Spring MVC 的。")]),e._v(" "),r("p",[e._v("这是 JSR-356 的一个重大限制,即 Spring 的 WebSocket 支持具有特定于服务器的"),r("code",[e._v("RequestUpgradeStrategy")]),e._v("实现的地址,即使在 JSR-356 运行时也是如此。此类策略目前存在于 Tomcat、 Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及 Wildfly)。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在 Java WebSocket API 中克服前面的限制的请求已被"),r("br"),e._v("创建,并且可以在"),r("a",{attrs:{href:"https://github.com/eclipse-ee4j/websocket-api/issues/211",target:"_blank",rel:"noopener noreferrer"}},[e._v("eclipse-ee4j/websocket-api#211"),r("OutboundLink")],1),e._v("处被遵循。"),r("br"),e._v(" Tomcat, Undertow,和 WebSphere 提供了它们自己的 API 替代方案,使其能够做到这一点,并且与 Jetty 一起也是可能的。我们希望"),r("br"),e._v("更多的服务器也能做到这一点。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("第二个考虑因素是,支持 JSR-356 的 Servlet 容器预计将执行"),r("code",[e._v("ServletContainerInitializer")]),e._v("扫描,这可能会大大减慢应用程序的启动速度——在某些情况下。如果在升级到具有 JSR-356 支持的 Servlet 容器版本后观察到重大影响,则应该可以通过使用"),r("code",[e._v("")]),e._v("中的"),r("code",[e._v("")]),e._v("元素选择性地启用或禁用 Web 片段(和 SCI 扫描),如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n\n\n')])])]),r("p",[e._v("然后,你可以根据名称选择性地启用 Web 片段,例如 Spring 自己的"),r("code",[e._v("SpringServletContainerInitializer")]),e._v(",它为 Servlet 3Java 初始化 API 提供了支持。下面的示例展示了如何做到这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n spring_web\n \n\n\n')])])]),r("h4",{attrs:{id:"_4-2-4-服务器配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-4-服务器配置"}},[e._v("#")]),e._v(" 4.2.4.服务器配置")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket-server-config"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("WebSocket 每个底层引擎都公开了控制运行时特性的配置属性,例如消息缓冲区大小、空闲超时等。")]),e._v(" "),r("p",[e._v("对于 Tomcat、Wildfly 和 GlassFish,可以在 WebSocket Java 配置中添加"),r("code",[e._v("ServletServerContainerFactoryBean")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Bean\n public ServletServerContainerFactoryBean createWebSocketContainer() {\n ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();\n container.setMaxTextMessageBufferSize(8192);\n container.setMaxBinaryMessageBufferSize(8192);\n return container;\n }\n\n}\n")])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n\n\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("对于客户端 WebSocket 配置,你应该使用"),r("code",[e._v("WebSocketContainerFactoryBean")]),e._v("或"),r("code",[e._v("ContainerProvider.getWebSocketContainer()")]),e._v("(Java 配置)。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("对于 Jetty,你需要提供一个预先配置的 Jetty "),r("code",[e._v("WebSocketServerFactory")]),e._v(",并通过 WebSocket Java 配置将其插入 Spring 的"),r("code",[e._v("DefaultHandshakeHandler")]),e._v("。下面的示例展示了如何做到这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Override\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n registry.addHandler(echoWebSocketHandler(),\n "/echo").setHandshakeHandler(handshakeHandler());\n }\n\n @Bean\n public DefaultHandshakeHandler handshakeHandler() {\n\n WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);\n policy.setInputBufferSize(8192);\n policy.setIdleTimeout(600000);\n\n return new DefaultHandshakeHandler(\n new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n \n \n \n \n\n\n')])])]),r("h4",{attrs:{id:"_4-2-5-允许的来源"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-5-允许的来源"}},[e._v("#")]),e._v(" 4.2.5.允许的来源")]),e._v(" "),r("p",[r("RouterLink",{attrs:{to:"/spring-framework/web-reactive.html#webflux-websocket-server-cors"}},[e._v("WebFlux")])],1),e._v(" "),r("p",[e._v("在 Spring Framework4.1.5 中, WebSocket 和 Sockjs 的默认行为是仅接受同源请求。也可以允许所有或指定的源列表。这种检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户机修改"),r("code",[e._v("Origin")]),e._v("标头值(有关更多详细信息,请参见"),r("a",{attrs:{href:"https://tools.ietf.org/html/rfc6454",target:"_blank",rel:"noopener noreferrer"}},[e._v("RFC6454:Web Origin 概念"),r("OutboundLink")],1),e._v(")。")]),e._v(" "),r("p",[e._v("这三种可能的行为是:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("只允许同源请求(默认):在这种模式下,当启用 Sockjs 时,IFRAME HTTP 响应头设置为,并且禁用 JSONP 传输,因为它不允许检查请求的源。因此,当启用此模式时,IE6 和 IE7 将不受支持。")])]),e._v(" "),r("li",[r("p",[e._v("允许指定的源列表:每个允许的源列表必须以"),r("code",[e._v("http://")]),e._v("或"),r("code",[e._v("https://")]),e._v("开头。在这种模式下,当启用 Sockjs 时,iframe 传输将被禁用。因此,当启用此模式时,IE6 到 IE9 将不受支持。")])]),e._v(" "),r("li",[r("p",[e._v("允许所有原点:要启用此模式,你应该提供"),r("code",[e._v("*")]),e._v("作为允许的原点值。在这种模式下,所有的传输都是可用的。")])])]),e._v(" "),r("p",[e._v("你可以配置 WebSocket 和 Sockjs 允许的起源,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.socket.config.annotation.EnableWebSocket;\nimport org.springframework.web.socket.config.annotation.WebSocketConfigurer;\nimport org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;\n\n@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Override\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");\n }\n\n @Bean\n public WebSocketHandler myHandler() {\n return new MyHandler();\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n\n \n\n\n')])])]),r("h3",{attrs:{id:"_4-3-sockjs-后援"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-sockjs-后援"}},[e._v("#")]),e._v(" 4.3.Sockjs 后援")]),e._v(" "),r("p",[e._v("在公共互联网上,不受你控制的限制性代理可能会阻止 WebSocket 交互,要么是因为它们未被配置为传递"),r("code",[e._v("Upgrade")]),e._v("头,要么是因为它们关闭了似乎处于空闲状态的长期连接。")]),e._v(" "),r("p",[e._v("这个问题的解决方案是 WebSocket 仿真——即,尝试先使用 WebSocket,然后再使用基于 HTTP 的技术,该技术模拟 WebSocket 交互并公开相同的应用程序级 API。")]),e._v(" "),r("p",[e._v("在 Servlet 栈上, Spring 框架为 Sockjs 协议提供了服务器(以及客户端)支持。")]),e._v(" "),r("h4",{attrs:{id:"_4-3-1-概述"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-1-概述"}},[e._v("#")]),e._v(" 4.3.1.概述")]),e._v(" "),r("p",[e._v("SockJS 的目标是让应用程序使用 WebSocket API,但在运行时在必要时退回到非 WebSocket 替代方案,而不需要更改应用程序代码。")]),e._v(" "),r("p",[e._v("Sockjs 包括:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("将"),r("a",{attrs:{href:"https://github.com/sockjs/sockjs-protocol",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs 协议"),r("OutboundLink")],1),e._v("定义为可执行文件"),r("a",{attrs:{href:"https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("叙述测试"),r("OutboundLink")],1),e._v("的形式。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://github.com/sockjs/sockjs-client/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs JavaScript 客户端"),r("OutboundLink")],1),e._v("——用于浏览器的客户库。")])]),e._v(" "),r("li",[r("p",[e._v("Sockjs 服务器实现,包括在 Spring 框架"),r("code",[e._v("spring-websocket")]),e._v("模块中的一个。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("spring-websocket")]),e._v("模块中的 Sockjs Java 客户端(自版本 4.1 起)。")])])]),e._v(" "),r("p",[e._v("Sockjs 是为在浏览器中使用而设计的。它使用各种技术来支持各种浏览器版本。有关 Sockjs 传输类型和浏览器的完整列表,请参见"),r("a",{attrs:{href:"https://github.com/sockjs/sockjs-client/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs 客户端"),r("OutboundLink")],1),e._v("页面。传输可分为三大类: WebSocket、HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参见"),r("a",{attrs:{href:"https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/",target:"_blank",rel:"noopener noreferrer"}},[e._v("这篇博文"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("p",[e._v("Sockjs 客户机通过发送"),r("code",[e._v("GET /info")]),e._v("开始从服务器获取基本信息。在那之后,它必须决定使用什么交通工具。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流媒体选项。如果不是,则使用 HTTP(长)轮询。")]),e._v(" "),r("p",[e._v("所有传输请求都具有以下 URL 结构:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}\n")])])]),r("p",[e._v("地点:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("{server-id}")]),e._v("对于群集中的路由请求很有用,但不用于其他方式。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("{session-id}")]),e._v("关联属于 Sockjs 会话的 HTTP 请求。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("{transport}")]),e._v("表示传输类型(例如,"),r("code",[e._v("websocket")]),e._v(","),r("code",[e._v("xhr-streaming")]),e._v("等)。")])])]),e._v(" "),r("p",[e._v("WebSocket 传输只需要一个 HTTP 请求就可以完成 WebSocket 握手。此后的所有消息都在该套接字上交换。")]),e._v(" "),r("p",[e._v("HTTP 传输需要更多的请求。例如,Ajax/XHR 流依赖于对服务器到客户端消息的一个长时间运行的请求,以及对客户端到服务器消息的额外 HTTP POST 请求。长轮询是类似的,只是它在每个服务器到客户端发送后结束当前请求。")]),e._v(" "),r("p",[e._v("Sockjs 添加了最小的消息框架。例如,服务器最初发送字母"),r("code",[e._v("o")]),e._v("(“打开”框架),消息被发送为"),r("code",[e._v('a["message1","message2"]')]),e._v("(JSON 编码的数组),如果 25 秒内没有消息流(默认情况下),则发送字母"),r("code",[e._v("h")]),e._v("(“心跳”框架),并将字母"),r("code",[e._v("c")]),e._v("(“关闭”框架)关闭会话。")]),e._v(" "),r("p",[e._v("要了解更多信息,请在浏览器中运行一个示例,并观察 HTTP 请求。Sockjs 客户机允许固定传输列表,因此可以一次查看每个传输。Sockjs 客户机还提供了一个调试标志,可以在浏览器控制台中启用有用的消息。在服务器端,你可以启用"),r("code",[e._v("TRACE")]),e._v("的"),r("code",[e._v("org.springframework.web.socket")]),e._v("日志记录。有关更多详细信息,请参见 Sockjs 协议"),r("a",{attrs:{href:"https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("旁白测试"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_4-3-2-启用-sockjs"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-2-启用-sockjs"}},[e._v("#")]),e._v(" 4.3.2.启用 Sockjs")]),e._v(" "),r("p",[e._v("你可以通过 Java 配置启用 Sockjs,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocket\npublic class WebSocketConfig implements WebSocketConfigurer {\n\n @Override\n public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {\n registry.addHandler(myHandler(), "/myHandler").withSockJS();\n }\n\n @Bean\n public WebSocketHandler myHandler() {\n return new MyHandler();\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n\n \n\n\n')])])]),r("p",[e._v("前面的示例是用于 Spring MVC 应用程序中的,并且应该包括在[](#MVC- Servlet)的配置中。然而, Spring 的 WebSocket 和 Sockjs 支持并不依赖于 Spring MVC。在["),r("code",[e._v("SockJsHttpRequestHandler")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/socket/sockkjs/support/sockjshtprequesthandler.html)的帮助下,集成到其他 HTTP 服务环境是相对简单的。")]),e._v(" "),r("p",[e._v("在浏览器方面,应用程序可以使用["),r("code",[e._v("sockjs-client")]),e._v("](https://github.com/sockjs/sockjs-client/)(版本 1.0.x)。它模拟 W3C WebSocket API,并与服务器通信以根据其运行的浏览器选择最佳传输选项。请参阅"),r("a",{attrs:{href:"https://github.com/sockjs/sockjs-client/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs-客户端"),r("OutboundLink")],1),e._v("页面和浏览器支持的传输类型列表。客户机还提供了几个配置选项——例如,指定要包含哪些传输。")]),e._v(" "),r("h4",{attrs:{id:"_4-3-3-ie8-和-ie9"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-3-ie8-和-ie9"}},[e._v("#")]),e._v(" 4.3.3.IE8 和 IE9")]),e._v(" "),r("p",[e._v("Internet Explorer8 和 9 仍在使用中。它们是拥有袜子的一个关键原因。本节介绍了在这些浏览器中运行的重要注意事项。")]),e._v(" "),r("p",[e._v("Sockjs 客户机通过使用微软的["),r("code",[e._v("XDomainRequest")]),e._v("](https://blogs.msdn.com/b/ieinnals/archive/2010/05/13/xdomainRequest-Restrictions-Limitions-and-Workarounds.ASPX),在 IE8 和 9 中支持 Ajax/XHR 流媒体。它可以跨域工作,但不支持发送 cookie。对于 Java 应用程序来说,Cookie 通常是必不可少的。然而,由于 Sockjs 客户机可以用于许多服务器类型(而不仅仅是 Java 类型),因此它需要知道 Cookie 是否重要。如果是这样的话,Sockjs 客户机更喜欢 Ajax/XHR。否则,它依赖于一种基于 iframe 的技术。")]),e._v(" "),r("p",[e._v("来自 Sockjs 客户机的第一个"),r("code",[e._v("/info")]),e._v("请求是对可能影响客户机选择传输方式的信息的请求。这些细节之一是服务器应用程序是否依赖 Cookie(例如,出于身份验证目的或使用粘性会话进行集群)。 Spring 的 Sockjs 支持包括一个名为"),r("code",[e._v("sessionCookieNeeded")]),e._v("的属性。默认情况下,它是启用的,因为大多数 Java 应用程序依赖于"),r("code",[e._v("JSESSIONID")]),e._v("cookie。如果你的应用程序不需要它,你可以关闭此选项,然后 Sockjs 客户端应该在 IE8 和 IE9 中选择"),r("code",[e._v("xdr-streaming")]),e._v("。")]),e._v(" "),r("p",[e._v("如果确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应头"),r("code",[e._v("X-Frame-Options")]),e._v("设置为"),r("code",[e._v("DENY")]),e._v("、"),r("code",[e._v("SAMEORIGIN")]),e._v("或"),r("code",[e._v("ALLOW-FROM ")]),e._v(",指示浏览器阻止在给定页面上使用 iframes。这是用来防止"),r("a",{attrs:{href:"https://www.owasp.org/index.php/Clickjacking",target:"_blank",rel:"noopener noreferrer"}},[e._v("点击劫持"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Spring Security3.2+ 为在每个"),r("br"),e._v("响应上设置"),r("code",[e._v("X-Frame-Options")]),e._v("提供了支持。默认情况下, Spring Security Java 配置将其设置为"),r("code",[e._v("DENY")]),e._v("。"),r("br"),e._v("在 3.2 中, Spring Security XML 命名空间默认情况下不设置该标头"),r("br"),e._v(",但可以配置为这样做。在将来,它可能会默认设置它。"),r("br"),r("br"),e._v("有关如何配置"),r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers",target:"_blank",rel:"noopener noreferrer"}},[e._v("默认安全标头"),r("OutboundLink")],1),e._v("标题"),r("code",[e._v("X-Frame-Options")]),e._v("的设置的详细信息,请参见 Spring 安全文档的"),r("a",{attrs:{href:"https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers",target:"_blank",rel:"noopener noreferrer"}},[e._v("默认安全标头"),r("OutboundLink")],1),e._v("。你还可以查看"),r("a",{attrs:{href:"https://jira.spring.io/browse/SEC-2501",target:"_blank",rel:"noopener noreferrer"}},[e._v("SEC-2501"),r("OutboundLink")],1),e._v("以获取更多背景信息。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果你的应用程序添加了"),r("code",[e._v("X-Frame-Options")]),e._v("响应报头(应该如此!)并依赖于基于 iframe 的传输,则需要将报头值设置为"),r("code",[e._v("SAMEORIGIN")]),e._v("或"),r("code",[e._v("ALLOW-FROM ")]),e._v("。 Spring Sockjs 支持还需要知道 Sockjs 客户机的位置,因为它是从 iframe 加载的。默认情况下,iframe 被设置为从 CDN 位置下载 Sockjs 客户端。将此选项配置为使用来自与应用程序相同来源的 URL 是一个好主意。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何在 Java 配置中实现这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/portfolio").withSockJS()\n .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");\n }\n\n // ...\n\n}\n')])])]),r("p",[e._v("XML 名称空间通过"),r("code",[e._v("")]),e._v("元素提供了类似的选项。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在最初的开发过程中,启用 Sockjs 客户机"),r("code",[e._v("devel")]),e._v("模式,该模式可以防止"),r("br"),e._v("浏览器缓存原本会缓存"),r("br"),e._v("的 Sockjs 请求(如 iframe)。有关如何启用它的详细信息,请参见"),r("a",{attrs:{href:"https://github.com/sockjs/sockjs-client/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs 客户端"),r("OutboundLink")],1),e._v("页面。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_4-3-4-心跳"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-4-心跳"}},[e._v("#")]),e._v(" 4.3.4.心跳")]),e._v(" "),r("p",[e._v("Sockjs 协议要求服务器发送心跳消息,以防止代理得出连接已挂起的结论。 Spring Sockjs 配置具有一个名为"),r("code",[e._v("heartbeatTime")]),e._v("的属性,你可以使用该属性来定制频率。默认情况下,假设在该连接上没有发送其他消息,则会在 25 秒后发送心跳。对于公共互联网应用程序,这个 25 秒的值与下面的"),r("a",{attrs:{href:"https://tools.ietf.org/html/rfc6202",target:"_blank",rel:"noopener noreferrer"}},[e._v("IETF 推荐"),r("OutboundLink")],1),e._v("一致。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在使用 STOMP over WebSocket 和 Sockjs 时,如果 STOMP 客户机和服务器协商"),r("br"),e._v("要交换的心跳,则禁用 Sockjs 心跳。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Spring Sockjs 支持还允许你配置"),r("code",[e._v("TaskScheduler")]),e._v("来调度心跳任务。任务计划程序由线程池支持,并根据可用处理器的数量进行默认设置。你应该考虑根据你的特定需求自定义设置。")]),e._v(" "),r("h4",{attrs:{id:"_4-3-5-客户端断开连接"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-5-客户端断开连接"}},[e._v("#")]),e._v(" 4.3.5.客户端断开连接")]),e._v(" "),r("p",[e._v("HTTP 流和 HTTP 长轮询 Sockjs 传输要求连接的打开时间比通常更长。有关这些技术的概述,请参见"),r("a",{attrs:{href:"https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/",target:"_blank",rel:"noopener noreferrer"}},[e._v("这篇博文"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("p",[e._v("在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该异步支持允许退出 Servlet 容器线程,处理请求,并继续写入来自另一个线程的响应。")]),e._v(" "),r("p",[e._v("一个具体的问题是, Servlet API 不为已经消失的客户机提供通知。见"),r("a",{attrs:{href:"https://github.com/eclipse-ee4j/servlet-api/issues/44",target:"_blank",rel:"noopener noreferrer"}},[e._v("eclipse-ee4j/servlet-api#44"),r("OutboundLink")],1),e._v("。然而, Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下为每 25 秒),这意味着通常会在该时间段内(如果发送消息的频率更高,则会更早)检测到客户端断开连接。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("结果,由于客户端已断开连接,可能会发生网络 I/O 故障,而"),r("br"),e._v("会用不必要的堆栈跟踪来填充日志。 Spring 尽最大努力通过使用专用日志类别来标识"),r("br"),e._v("表示客户端断开连接(特定于每个服务器)和日志"),r("br"),e._v("的这样的网络故障,"),r("code",[e._v("DISCONNECTED_CLIENT_LOG_CATEGORY")]),e._v("(在"),r("code",[e._v("AbstractSockJsSession")]),e._v("中定义)。如果需要查看堆栈跟踪,可以将"),r("br"),e._v("日志类别设置为跟踪。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_4-3-6-sockjs-和-cors"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-6-sockjs-和-cors"}},[e._v("#")]),e._v(" 4.3.6.Sockjs 和 CORS")]),e._v(" "),r("p",[e._v("如果允许跨源请求(参见"),r("a",{attrs:{href:"#websocket-server-allowed-origins"}},[e._v("允许的来源")]),e._v("),那么 Sockjs 协议在 XHR 流和轮询传输中使用 CORS 提供跨域支持。因此,CORS 头是自动添加的,除非检测到响应中存在 CORS 头。因此,如果应用程序已经被配置为提供 CORS 支持(例如,通过 Servlet 过滤器),则 Spring 的"),r("code",[e._v("SockJsService")]),e._v("跳过了这一部分。")]),e._v(" "),r("p",[e._v("还可以通过在 Spring 的 SockjsService 中设置"),r("code",[e._v("suppressCors")]),e._v("属性来禁用这些 CORS 头的添加。")]),e._v(" "),r("p",[e._v("Sockjs 期望以下标题和值:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("Access-Control-Allow-Origin")]),e._v(":从"),r("code",[e._v("Origin")]),e._v("请求头的值初始化。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Access-Control-Allow-Credentials")]),e._v(":总是设置为"),r("code",[e._v("true")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Access-Control-Request-Headers")]),e._v(":从等效请求头的值初始化。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Access-Control-Allow-Methods")]),e._v(":传输支持的 HTTP 方法(参见"),r("code",[e._v("TransportType")]),e._v("枚举)。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("Access-Control-Max-Age")]),e._v(":设置为 31536000(1 年)。")])])]),e._v(" "),r("p",[e._v("有关确切的实现,请参见"),r("code",[e._v("addCorsHeaders")]),e._v("中的"),r("code",[e._v("AbstractSockJsService")]),e._v("和源代码中的"),r("code",[e._v("TransportType")]),e._v("枚举。")]),e._v(" "),r("p",[e._v("或者,如果 CORS 配置允许,可以考虑排除具有 Sockjs 端点前缀的 URL,从而让 Spring 的"),r("code",[e._v("SockJsService")]),e._v("处理它。")]),e._v(" "),r("h4",{attrs:{id:"_4-3-7-sockjsclient"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-7-sockjsclient"}},[e._v("#")]),e._v(" 4.3.7."),r("code",[e._v("SockJsClient")])]),e._v(" "),r("p",[e._v("Spring 提供了一种 Sockjs Java 客户端,以在不使用浏览器的情况下连接到远程 Sockjs 端点。当需要在公共网络上的两个服务器之间进行双向通信时(即,在网络代理可以排除使用 WebSocket 协议的情况下),这可能是特别有用的。对于测试目的(例如,模拟大量并发用户),Sockjs Java 客户机也非常有用。")]),e._v(" "),r("p",[e._v("Sockjs Java 客户端支持"),r("code",[e._v("websocket")]),e._v("、"),r("code",[e._v("xhr-streaming")]),e._v("和"),r("code",[e._v("xhr-polling")]),e._v("传输。剩下的那些只有在浏览器中使用才有意义。")]),e._v(" "),r("p",[e._v("你可以将"),r("code",[e._v("WebSocketTransport")]),e._v("配置为:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("StandardWebSocketClient")]),e._v("在 JSR-356 运行时中。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("JettyWebSocketClient")]),e._v("通过使用 Jetty 9+ 本机 WebSocket API。")])]),e._v(" "),r("li",[r("p",[e._v("Spring 的"),r("code",[e._v("WebSocketClient")]),e._v("的任意实现。")])])]),e._v(" "),r("p",[e._v("根据定义,"),r("code",[e._v("XhrTransport")]),e._v("同时支持"),r("code",[e._v("xhr-streaming")]),e._v("和"),r("code",[e._v("xhr-polling")]),e._v(",因为从客户机的角度来看,除了用于连接到服务器的 URL 之外,没有其他区别。目前有两种实现方式:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("RestTemplateXhrTransport")]),e._v("将 Spring 的"),r("code",[e._v("RestTemplate")]),e._v("用于 HTTP 请求。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("JettyXhrTransport")]),e._v("将 Jetty 的"),r("code",[e._v("HttpClient")]),e._v("用于 HTTP 请求。")])])]),e._v(" "),r("p",[e._v("下面的示例展示了如何创建 Sockjs 客户机并连接到 Sockjs 端点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('List transports = new ArrayList<>(2);\ntransports.add(new WebSocketTransport(new StandardWebSocketClient()));\ntransports.add(new RestTemplateXhrTransport());\n\nSockJsClient sockJsClient = new SockJsClient(transports);\nsockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Sockjs 使用 JSON 格式化的数组来处理消息。默认情况下,使用 Jackson2 并且需要"),r("br"),e._v("才能在 Classpath 上。或者,你可以配置"),r("code",[e._v("SockJsMessageCodec")]),e._v("的自定义实现,并在"),r("code",[e._v("SockJsClient")]),e._v("上配置它。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("要使用"),r("code",[e._v("SockJsClient")]),e._v("来模拟大量并发用户,你需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。下面的示例展示了如何使用 Jetty 来实现这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("HttpClient jettyHttpClient = new HttpClient();\njettyHttpClient.setMaxConnectionsPerDestination(1000);\njettyHttpClient.setExecutor(new QueuedThreadPool(1000));\n")])])]),r("p",[e._v("下面的示例显示了你还应该考虑定制的服务器端 Sockjs 相关属性(详细信息请参见 Javadoc):")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\npublic class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/sockjs").withSockJS()\n .setStreamBytesLimit(512 * 1024) (1)\n .setHttpMessageCacheSize(1000) (2)\n .setDisconnectDelay(30 * 1000); (3)\n }\n\n // ...\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[e._v("将"),r("code",[e._v("streamBytesLimit")]),e._v("属性设置为 512KB(默认值为 128KB—"),r("code",[e._v("128 * 1024")]),e._v(")。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("将"),r("code",[e._v("httpMessageCacheSize")]),e._v("属性设置为 1,000(默认值为"),r("code",[e._v("100")]),e._v(")。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("将"),r("code",[e._v("disconnectDelay")]),e._v("属性设置为 30 个属性秒(默认值为 5 秒—"),r("code",[e._v("5 * 1000")]),e._v(")。")])])])]),e._v(" "),r("h3",{attrs:{id:"_4-4-跺脚"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-跺脚"}},[e._v("#")]),e._v(" 4.4.跺脚")]),e._v(" "),r("p",[e._v("WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,用于客户端和服务器协商在 WebSocket 之上使用的子协议(即更高级别的消息传递协议)来定义各自可以发送什么样的消息、格式是什么、每个消息的内容等等。子协议的使用是可选的,但无论哪种方式,客户机和服务器都需要在定义消息内容的某些协议上达成一致。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-1-概述"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-1-概述"}},[e._v("#")]),e._v(" 4.4.1.概述")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://stomp.github.io/stomp-specification-1.2.html#Abstract",target:"_blank",rel:"noopener noreferrer"}},[e._v("STOMP"),r("OutboundLink")],1),e._v("(简单的文本消息传递协议)最初是为脚本语言(例如 Ruby、Python 和 Perl)创建的,用于连接到 Enterprise 消息代理。它旨在解决常用消息传递模式的最小子集。Stomp 可以在任何可靠的双向流网络协议上使用,例如 TCP 和 WebSocket。尽管 STOMP 是一种面向文本的协议,但消息负载可以是文本的,也可以是二进制的。")]),e._v(" "),r("p",[e._v("STOMP 是一种基于帧的协议,其帧是以 HTTP 为模型的。下面的清单显示了 Stomp 框架的结构:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("COMMAND\nheader1:value1\nheader2:value2\n\nBody^@\n")])])]),r("p",[e._v("客户端可以使用"),r("code",[e._v("SEND")]),e._v("或"),r("code",[e._v("SUBSCRIBE")]),e._v("命令发送或订阅消息,以及一个"),r("code",[e._v("destination")]),e._v("头,该头描述消息的内容以及应该由谁接收。这启用了一个简单的发布-订阅机制,你可以使用该机制通过代理向其他连接的客户机发送消息,或者向服务器发送消息,以请求执行某些工作。")]),e._v(" "),r("p",[e._v("当你使用 Spring 的 STOMP 支持时, Spring WebSocket 应用程序充当客户的 STOMP 代理。消息被路由到"),r("code",[e._v("@Controller")]),e._v("消息处理方法或简单的内存代理,该代理跟踪订阅并向订阅的用户广播消息。还可以配置 Spring 来使用专用的 Stomp 代理(例如 RabbitMQ、ActiveMQ 和其他代理)来实际广播消息。在这种情况下, Spring 维护到代理的 TCP 连接,将消息中继到代理,并将消息从代理向下传递到已连接的 WebSocket 客户端。因此, Spring Web 应用程序可以依赖统一的基于 HTTP 的安全性、公共验证和熟悉的编程模型来进行消息处理。")]),e._v(" "),r("p",[e._v("下面的示例显示了订阅接收股票报价的客户机,服务器可能会定期发送该报价(例如,通过调度任务通过"),r("code",[e._v("SimpMessagingTemplate")]),e._v("向经纪人发送消息):")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("SUBSCRIBE\nid:sub-1\ndestination:/topic/price.stock.*\n\n^@\n")])])]),r("p",[e._v("下面的示例显示了一个发送交易请求的客户机,服务器可以通过"),r("code",[e._v("@MessageMapping")]),e._v("方法处理该请求:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('SEND\ndestination:/queue/trade\ncontent-type:application/json\ncontent-length:44\n\n{"action":"BUY","ticker":"MMM","shares",44}^@\n')])])]),r("p",[e._v("执行后,服务器可以向客户端广播交易确认消息和详细信息。")]),e._v(" "),r("p",[e._v("在 Stomp 规范中,目的地的含义是故意不透明的。它可以是任何字符串,完全由 Stomp 服务器来定义它们所支持的目标的语义和语法。然而,很常见的情况是,目标是类似路径的字符串,其中"),r("code",[e._v("/topic/..")]),e._v("表示发布-订阅(一对多),而"),r("code",[e._v("/queue/")]),e._v("表示点对点(一对一)消息交换。")]),e._v(" "),r("p",[e._v("Stomp 服务器可以使用"),r("code",[e._v("MESSAGE")]),e._v("命令向所有订阅者广播消息。下面的示例显示了一个服务器,该服务器将股票报价发送到一个已订阅的客户端:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('MESSAGE\nmessage-id:nxahklf6-1\nsubscription:sub-1\ndestination:/topic/price.stock.MMM\n\n{"ticker":"MMM","price":129.45}^@\n')])])]),r("p",[e._v("服务器不能发送未经请求的消息。来自服务器的所有消息必须响应特定的客户端订阅,并且服务器消息的"),r("code",[e._v("subscription-id")]),e._v("头必须与客户端订阅的"),r("code",[e._v("id")]),e._v("头匹配。")]),e._v(" "),r("p",[e._v("前面的概述旨在提供对 STOMP 协议的最基本的理解。我们建议对"),r("a",{attrs:{href:"https://stomp.github.io/stomp-specification-1.2.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("规格"),r("OutboundLink")],1),e._v("协议进行全面审查。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-2-福利"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-2-福利"}},[e._v("#")]),e._v(" 4.4.2.福利")]),e._v(" "),r("p",[e._v("与使用原始 WebSockets 相比,使用 STOMP 作为子协议使得 Spring 框架和 Spring 安全性提供了更丰富的编程模型。关于 HTTP 相对于原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,也可以提出同样的观点。以下是一系列好处:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("无需发明定制的消息传递协议和消息格式。")])]),e._v(" "),r("li",[r("p",[e._v("在 Spring 框架中,包括"),r("a",{attrs:{href:"#websocket-stomp-client"}},[e._v("Java 客户端")]),e._v("在内的 STOMP 客户机是可用的。")])]),e._v(" "),r("li",[r("p",[e._v("你可以(可选地)使用消息代理(例如 RabbitMQ、ActiveMQ 和其他代理)来管理订阅和广播消息。")])]),e._v(" "),r("li",[r("p",[e._v("应用程序逻辑可以在任意数量的"),r("code",[e._v("@Controller")]),e._v("实例中进行组织,并且可以基于 stomp 目标头将消息路由到它们,而不是针对给定连接使用单个"),r("code",[e._v("WebSocketHandler")]),e._v("处理原始消息。")])]),e._v(" "),r("li",[r("p",[e._v("Spring 可以使用安全性来保护基于 STOMP 目的地和消息类型的消息。")])])]),e._v(" "),r("h4",{attrs:{id:"_4-4-3-启用-stomp"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-3-启用-stomp"}},[e._v("#")]),e._v(" 4.4.3.启用 Stomp")]),e._v(" "),r("p",[e._v("WebSocket 支持在"),r("code",[e._v("spring-messaging")]),e._v("和"),r("code",[e._v("spring-websocket")]),e._v("模块中可用。一旦有了这些依赖关系,就可以使用"),r("a",{attrs:{href:"#websocket-fallback"}},[e._v("Sockjs 后援")]),e._v("在 WebSocket 上公开 Stomp 端点,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;\nimport org.springframework.web.socket.config.annotation.StompEndpointRegistry;\n\n@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/portfolio").withSockJS(); (1)\n }\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry config) {\n config.setApplicationDestinationPrefixes("/app"); (2)\n config.enableSimpleBroker("/topic", "/queue"); (3)\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th",[r("strong",[e._v("1")])]),e._v(" "),r("th",[r("code",[e._v("/portfolio")]),e._v("是 WebSocket(或 Sockjs)"),r("br"),e._v("客户端为 WebSocket 握手需要连接到的端点的 HTTP URL。")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("strong",[e._v("2")])]),e._v(" "),r("td",[e._v("目标头以"),r("code",[e._v("/app")]),e._v("开头的 stomp 消息被路由到"),r("code",[e._v("@MessageMapping")]),e._v("类中的"),r("code",[e._v("@Controller")]),e._v("方法。")])]),e._v(" "),r("tr",[r("td",[r("strong",[e._v("3")])]),e._v(" "),r("td",[e._v("使用内置的消息代理进行订阅和广播,并将目标头以"),r("code",[e._v("/topic")]),e._v("或"),r("code",[e._v("/queue")]),e._v("开头的消息路由到代理。")])])])]),e._v(" "),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n \n \n\n\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("对于内置的简单代理,"),r("code",[e._v("/topic")]),e._v("和"),r("code",[e._v("/queue")]),e._v("前缀没有任何特殊的"),r("br"),e._v("含义。它们仅仅是区分发布订阅和点对点"),r("br"),e._v("消息传递(即多个订阅者和一个消费者)的一种约定。当你使用外部代理时,"),r("br"),e._v("检查代理的 stomp 页面,以了解它支持什么样的 stomp 目的地和"),r("br"),e._v("前缀。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("要从浏览器连接,对于 Sockjs,你可以使用["),r("code",[e._v("sockjs-client")]),e._v("](https://github.com/sockjs/sockjs-client)。对于 Stomp,许多应用程序使用"),r("a",{attrs:{href:"https://github.com/jmesnil/stomp-websocket",target:"_blank",rel:"noopener noreferrer"}},[e._v("jmesnil/stomp-websocket"),r("OutboundLink")],1),e._v("库(也称为 stomp.js),它是功能完备的,已经在生产中使用了多年,但不再维护。目前,"),r("a",{attrs:{href:"https://github.com/JSteunou/webstomp-client",target:"_blank",rel:"noopener noreferrer"}},[e._v("JSteunou/WebStomp-客户端"),r("OutboundLink")],1),e._v("是该库最活跃的维护和不断发展的后继库。下面的示例代码是基于它的:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('var socket = new SockJS("/spring-websocket-portfolio/portfolio");\nvar stompClient = webstomp.over(socket);\n\nstompClient.connect({}, function(frame) {\n}\n')])])]),r("p",[e._v("或者,如果你通过 WebSocket(不使用 Sockjs)进行连接,则可以使用以下代码:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('var socket = new WebSocket("/spring-websocket-portfolio/portfolio");\nvar stompClient = Stomp.over(socket);\n\nstompClient.connect({}, function(frame) {\n}\n')])])]),r("p",[e._v("注意,在前面的示例中"),r("code",[e._v("stompClient")]),e._v("不需要指定"),r("code",[e._v("login")]),e._v("和"),r("code",[e._v("passcode")]),e._v("头。即使这样做了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参见"),r("a",{attrs:{href:"#websocket-stomp-handle-broker-relay-configure"}},[e._v("连接到代理")]),e._v("和"),r("a",{attrs:{href:"#websocket-stomp-authentication"}},[e._v("认证")]),e._v("。")]),e._v(" "),r("p",[e._v("有关更多示例代码,请参见:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://spring.io/guides/gs/messaging-stomp-websocket/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Using WebSocket to build an interactive web application"),r("OutboundLink")],1),e._v("——入门指南。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://github.com/rstoyanchev/spring-websocket-portfolio",target:"_blank",rel:"noopener noreferrer"}},[e._v("股票投资组合"),r("OutboundLink")],1),e._v("—一个示例应用程序。")])])]),e._v(" "),r("h4",{attrs:{id:"_4-4-4-websocket-服务器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-4-websocket-服务器"}},[e._v("#")]),e._v(" 4.4.4. WebSocket 服务器")]),e._v(" "),r("p",[e._v("要配置底层 WebSocket 服务器,应用"),r("a",{attrs:{href:"#websocket-server-runtime-configuration"}},[e._v("服务器配置")]),e._v("中的信息。然而,对于 Jetty,你需要通过"),r("code",[e._v("StompEndpointRegistry")]),e._v("设置"),r("code",[e._v("HandshakeHandler")]),e._v("和"),r("code",[e._v("WebSocketPolicy")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());\n }\n\n @Bean\n public DefaultHandshakeHandler handshakeHandler() {\n\n WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);\n policy.setInputBufferSize(8192);\n policy.setIdleTimeout(600000);\n\n return new DefaultHandshakeHandler(\n new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));\n }\n}\n')])])]),r("h4",{attrs:{id:"_4-4-5-消息流"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-5-消息流"}},[e._v("#")]),e._v(" 4.4.5.消息流")]),e._v(" "),r("p",[e._v("一旦公开了一个 STOMP 端点, Spring 应用程序就成为连接客户端的 STOMP 代理。本节描述服务器端的消息流。")]),e._v(" "),r("p",[r("code",[e._v("spring-messaging")]),e._v("模块包含对起源于"),r("a",{attrs:{href:"https://spring.io/spring-integration",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Integration"),r("OutboundLink")],1),e._v("的消息传递应用程序的基本支持,该支持后来被提取并合并到 Spring 框架中,以便在许多"),r("a",{attrs:{href:"https://spring.io/projects",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring projects"),r("OutboundLink")],1),e._v("和应用程序场景中更广泛地使用。下面的列表简要描述了一些可用的消息传递抽象:")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/Message.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Message"),r("OutboundLink")],1),e._v(":消息的简单表示,包括消息头和有效负载。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageHandler.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("MessageHandler"),r("OutboundLink")],1),e._v(":用于处理消息的契约。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageChannel.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("MessageChannel"),r("OutboundLink")],1),e._v(":用于发送消息的契约,该消息允许在生产者和消费者之间进行松散耦合。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/SubscribableChannel.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("下标 bablechannel"),r("OutboundLink")],1),e._v(":"),r("code",[e._v("MessageChannel")]),e._v("与"),r("code",[e._v("MessageHandler")]),e._v("订阅者。")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/support/ExecutorSubscribableChannel.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("执行者下标 bablechannel"),r("OutboundLink")],1),e._v(":"),r("code",[e._v("SubscribableChannel")]),e._v("使用"),r("code",[e._v("Executor")]),e._v("传递消息。")])])]),e._v(" "),r("p",[e._v("Java 配置(即"),r("code",[e._v("@EnableWebSocketMessageBroker")]),e._v(")和 XML 名称空间配置(即"),r("code",[e._v("")]),e._v(")都使用前面的组件来组装消息工作流。下图显示了启用简单的内置消息代理时使用的组件:")]),e._v(" "),r("p",[r("img",{attrs:{src:"images/message-flow-simple-broker.png",alt:"消息流简单代理"}})]),e._v(" "),r("p",[e._v("前面的图表显示了三个消息通道:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("clientInboundChannel")]),e._v(":用于传递从 WebSocket 客户端接收的消息。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("clientOutboundChannel")]),e._v(":用于向 WebSocket 客户端发送服务器消息。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("brokerChannel")]),e._v(":用于从服务器端应用程序代码中向 Message Broker 发送消息。")])])]),e._v(" "),r("p",[e._v("下一个关系图显示了当外部代理(例如 RabbitMQ)被配置为管理订阅和广播消息时所使用的组件:")]),e._v(" "),r("p",[r("img",{attrs:{src:"images/message-flow-broker-relay.png",alt:"消息流代理中继"}})]),e._v(" "),r("p",[e._v("前面两个图之间的主要区别是使用“代理中继”通过 TCP 将消息传递到外部的 Stomp 代理,并将消息从代理传递到订阅的客户机。")]),e._v(" "),r("p",[e._v("当接收到来自 WebSocket 连接的消息时,将它们解码为 Stomp 帧,转换为 Spring "),r("code",[e._v("Message")]),e._v("表示,并将其发送到"),r("code",[e._v("clientInboundChannel")]),e._v("以进行进一步的处理。例如,目标标头以"),r("code",[e._v("/app")]),e._v("开头的 stomp 消息可以路由到带注释的控制器中的"),r("code",[e._v("@MessageMapping")]),e._v("方法,而"),r("code",[e._v("/topic")]),e._v("和"),r("code",[e._v("/queue")]),e._v("消息可以直接路由到消息代理。")]),e._v(" "),r("p",[e._v("处理来自客户端的 stomp 消息的带注释的"),r("code",[e._v("@Controller")]),e._v("可以通过"),r("code",[e._v("brokerChannel")]),e._v("向消息代理发送消息,并且代理通过"),r("code",[e._v("clientOutboundChannel")]),e._v("将消息广播给匹配的订阅者。相同的控制器也可以对 HTTP 请求做出相同的响应,因此客户端可以执行 HTTP POST,然后使用"),r("code",[e._v("@PostMapping")]),e._v("方法向消息代理发送消息,以将消息广播到订阅的客户端。")]),e._v(" "),r("p",[e._v("我们可以通过一个简单的例子来追踪这个流程。考虑以下设置服务器的示例:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/portfolio");\n }\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry registry) {\n registry.setApplicationDestinationPrefixes("/app");\n registry.enableSimpleBroker("/topic");\n }\n}\n\n@Controller\npublic class GreetingController {\n\n @MessageMapping("/greeting")\n public String handle(String greeting) {\n return "[" + getTimestamp() + ": " + greeting;\n }\n}\n')])])]),r("p",[e._v("前面的示例支持以下流程:")]),e._v(" "),r("ol",[r("li",[r("p",[e._v("客户机连接到"),r("code",[e._v("[http://localhost:8080/portfolio](http://localhost:8080/portfolio)")]),e._v(",并且,一旦建立了 WebSocket 连接,Stomp 帧就开始在其上流动。")])]),e._v(" "),r("li",[r("p",[e._v("客户机发送一个订阅帧,其目的标头为"),r("code",[e._v("/topic/greeting")]),e._v("。一旦接收并解码,消息将被发送到"),r("code",[e._v("clientInboundChannel")]),e._v(",然后路由到消息代理,该代理存储客户端订阅。")])]),e._v(" "),r("li",[r("p",[e._v("客户端将发送帧发送到"),r("code",[e._v("/app/greeting")]),e._v("。"),r("code",[e._v("/app")]),e._v("前缀有助于将其路由到带注释的控制器。在去掉"),r("code",[e._v("/app")]),e._v("前缀之后,剩余的"),r("code",[e._v("/greeting")]),e._v("部分目标被映射到"),r("code",[e._v("@MessageMapping")]),e._v("中的"),r("code",[e._v("@MessageMapping")]),e._v("方法。")])]),e._v(" "),r("li",[r("p",[e._v("从"),r("code",[e._v("GreetingController")]),e._v("返回的值被转换为 Spring "),r("code",[e._v("Message")]),e._v(",其有效负载基于返回值和默认的目的标头"),r("code",[e._v("/topic/greeting")]),e._v("(派生自输入目的标头,由"),r("code",[e._v("/app")]),e._v("替换为"),r("code",[e._v("/topic")]),e._v(")。生成的消息被发送到"),r("code",[e._v("brokerChannel")]),e._v(",并由消息代理处理。")])]),e._v(" "),r("li",[r("p",[e._v("消息代理找到所有匹配的订阅者,并通过"),r("code",[e._v("clientOutboundChannel")]),e._v("向每个订阅者发送消息帧,从这里消息被编码为 Stomp 帧并在 WebSocket 连接上发送。")])])]),e._v(" "),r("p",[e._v("下一节将提供更多有关带注释方法的详细信息,包括所支持的参数和返回值的类型。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-6-带注释的控制器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-6-带注释的控制器"}},[e._v("#")]),e._v(" 4.4.6.带注释的控制器")]),e._v(" "),r("p",[e._v("应用程序可以使用带注释的"),r("code",[e._v("@Controller")]),e._v("类来处理来自客户端的消息。这样的类可以声明"),r("code",[e._v("@MessageMapping")]),e._v("、"),r("code",[e._v("@SubscribeMapping")]),e._v("和"),r("code",[e._v("@ExceptionHandler")]),e._v("方法,如以下主题中所述:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("["),r("code",[e._v("@MessageMapping")]),e._v("](# WebSocket-stomp-message-mapping)")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("@SubscribeMapping")]),e._v("](# WebSocket-stomp-subscribe-mapping)")])]),e._v(" "),r("li",[r("p",[e._v("["),r("code",[e._v("@MessageExceptionHandler")]),e._v("](# WebSocket-stomp-exception-handler)")])])]),e._v(" "),r("h5",{attrs:{id:"messagemapping"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#messagemapping"}},[e._v("#")]),e._v(" "),r("code",[e._v("@MessageMapping")])]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("@MessageMapping")]),e._v("对基于目的地路由消息的方法进行注释。它在方法级和类型级都受到支持。在类型级别,"),r("code",[e._v("@MessageMapping")]),e._v("用于表示控制器中所有方法之间的共享映射。")]),e._v(" "),r("p",[e._v("默认情况下,映射值是 Ant 样式的路径模式(例如"),r("code",[e._v("/thing*")]),e._v(","),r("code",[e._v("/thing/**")]),e._v("),包括对模板变量的支持(例如,"),r("code",[e._v("/thing/{id}")]),e._v(")。这些值可以通过"),r("code",[e._v("@DestinationVariable")]),e._v("方法参数进行引用。应用程序还可以切换到用于映射的以点分隔的目标约定,如"),r("a",{attrs:{href:"#websocket-stomp-destination-separator"}},[e._v("作为分隔器的点")]),e._v("中所解释的那样。")]),e._v(" "),r("h6",{attrs:{id:"支持的方法参数"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#支持的方法参数"}},[e._v("#")]),e._v(" 支持的方法参数")]),e._v(" "),r("p",[e._v("下表描述了方法参数:")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th",[e._v("Method argument")]),e._v(" "),r("th",[e._v("说明")])])]),e._v(" "),r("tbody",[r("tr",[r("td",[r("code",[e._v("Message")])]),e._v(" "),r("td",[e._v("以获取完整的消息。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("MessageHeaders")])]),e._v(" "),r("td",[e._v("用于访问"),r("code",[e._v("Message")]),e._v("中的标题。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("MessageHeaderAccessor")]),e._v(", "),r("code",[e._v("SimpMessageHeaderAccessor")]),e._v(", and "),r("code",[e._v("StompHeaderAccessor")])]),e._v(" "),r("td",[e._v("用于通过类型化访问器方法访问标头。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@Payload")])]),e._v(" "),r("td",[e._v("为了访问消息的有效负载,通过配置的"),r("code",[e._v("MessageConverter")]),e._v("进行转换(例如,从 JSON),"),r("br"),r("br"),e._v("不需要存在此注释,因为默认情况下是这样,假设没有"),r("br"),e._v("其他参数匹配。"),r("br"),r("br"),e._v("你可以用"),r("code",[e._v("@javax.validation.Valid")]),e._v("或 Spring 的"),r("code",[e._v("@Validated")]),e._v("、"),r("br"),e._v("对有效负载参数进行注释,以使有效负载参数被自动验证。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@Header")])]),e._v(" "),r("td",[e._v("用于访问特定的标头值——如果有必要,还可以使用"),r("code",[e._v("org.springframework.core.convert.converter.Converter")]),e._v("进行类型转换。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@Headers")])]),e._v(" "),r("td",[e._v("用于访问消息中的所有头。此参数必须可分配给"),r("code",[e._v("java.util.Map")]),e._v("。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("@DestinationVariable")])]),e._v(" "),r("td",[e._v("用于访问从消息目标提取的模板变量。"),r("br"),e._v("值根据需要转换为声明的方法参数类型。")])]),e._v(" "),r("tr",[r("td",[r("code",[e._v("java.security.Principal")])]),e._v(" "),r("td",[e._v("反映在 WebSocket HTTP 握手时登录的用户。")])])])]),e._v(" "),r("h6",{attrs:{id:"返回值-3"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#返回值-3"}},[e._v("#")]),e._v(" 返回值")]),e._v(" "),r("p",[e._v("默认情况下,来自"),r("code",[e._v("@MessageMapping")]),e._v("方法的返回值通过匹配的"),r("code",[e._v("MessageConverter")]),e._v("序列化到有效负载,并作为"),r("code",[e._v("Message")]),e._v("发送到"),r("code",[e._v("brokerChannel")]),e._v(",从那里向订阅者广播。出站消息的目的地与入站消息的目的地相同,但前缀为"),r("code",[e._v("/topic")]),e._v("。")]),e._v(" "),r("p",[e._v("你可以使用"),r("code",[e._v("@SendTo")]),e._v("和"),r("code",[e._v("@SendToUser")]),e._v("注释来定制输出消息的目标。"),r("code",[e._v("@SendTo")]),e._v("用于自定义目标目的地或指定多个目的地。"),r("code",[e._v("@SendToUser")]),e._v("用于将输出消息引导到仅与输入消息相关联的用户。见"),r("a",{attrs:{href:"#websocket-stomp-user-destination"}},[e._v("用户目的地")]),e._v("。")]),e._v(" "),r("p",[e._v("你可以在同一个方法上同时使用"),r("code",[e._v("@SendTo")]),e._v("和"),r("code",[e._v("@SendToUser")]),e._v(",并且这两种方法在类级别上都是受支持的,在这种情况下,它们充当类中方法的默认值。但是,请记住,任何方法级别的"),r("code",[e._v("@SendTo")]),e._v("或"),r("code",[e._v("@SendToUser")]),e._v("注释都会覆盖类级别的任何此类注释。")]),e._v(" "),r("p",[e._v("消息可以异步处理,并且"),r("code",[e._v("@MessageMapping")]),e._v("方法可以返回"),r("code",[e._v("ListenableFuture")]),e._v("、"),r("code",[e._v("CompletableFuture")]),e._v("或"),r("code",[e._v("CompletionStage")]),e._v("。")]),e._v(" "),r("p",[e._v("请注意,"),r("code",[e._v("@SendTo")]),e._v("和"),r("code",[e._v("@SendToUser")]),e._v("仅仅是一种方便,相当于使用"),r("code",[e._v("SimpMessagingTemplate")]),e._v("来发送消息。如果有必要,对于更高级的场景,"),r("code",[e._v("@MessageMapping")]),e._v("方法可以直接使用"),r("code",[e._v("SimpMessagingTemplate")]),e._v("。可以这样做,而不是返回一个值,或者可能是另外返回一个值。见"),r("a",{attrs:{href:"#websocket-stomp-handle-send"}},[e._v("发送消息")]),e._v("。")]),e._v(" "),r("h5",{attrs:{id:"subscribemapping"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#subscribemapping"}},[e._v("#")]),e._v(" "),r("code",[e._v("@SubscribeMapping")])]),e._v(" "),r("p",[r("code",[e._v("@SubscribeMapping")]),e._v("类似于"),r("code",[e._v("@MessageMapping")]),e._v(",但仅将映射范围缩小到订阅消息。它支持与"),r("a",{attrs:{href:"#websocket-stomp-message-mapping"}},[e._v("方法参数")]),e._v("相同的"),r("code",[e._v("@MessageMapping")]),e._v("。但是对于返回值,默认情况下,消息是直接发送到客户机的(通过"),r("code",[e._v("clientOutboundChannel")]),e._v(",响应订阅),而不是发送到代理的(通过"),r("code",[e._v("brokerChannel")]),e._v(",作为对匹配订阅的广播)。添加"),r("code",[e._v("@SendTo")]),e._v("或"),r("code",[e._v("@SendToUser")]),e._v("将重写此行为并将其发送给代理。")]),e._v(" "),r("p",[e._v("这个什么时候有用?假设代理被映射到"),r("code",[e._v("/topic")]),e._v("和"),r("code",[e._v("/queue")]),e._v(",而应用程序控制器被映射到"),r("code",[e._v("/app")]),e._v("。在此设置中,代理存储所有用于重复广播的"),r("code",[e._v("/topic")]),e._v("和"),r("code",[e._v("/queue")]),e._v("的订阅,并且不需要应用程序参与其中。客户机还可以订阅某些"),r("code",[e._v("/app")]),e._v("目标,并且控制器可以响应该订阅返回一个值,而不涉及代理,而无需存储或再次使用订阅(实际上是一次性的请求-回复交换)。这样做的一个用例是在启动时用初始数据填充 UI。")]),e._v(" "),r("p",[e._v("这什么时候没用?不要尝试将代理和控制器映射到相同的目标前缀,除非出于某种原因希望两者独立处理消息(包括订阅)。入站消息是并行处理的。不能保证代理或控制器是否首先处理给定的消息。如果目标是在订阅被存储并准备好广播时得到通知,那么如果服务器支持该订阅,客户端应该要求提供收据(Simple Broker 不支持)。例如,使用 Java"),r("a",{attrs:{href:"#websocket-stomp-client"}},[e._v("STOMP 客户端")]),e._v(",你可以执行以下操作来添加收据:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Autowired\nprivate TaskScheduler messageBrokerTaskScheduler;\n\n// During initialization..\nstompClient.setTaskScheduler(this.messageBrokerTaskScheduler);\n\n// When subscribing..\nStompHeaders headers = new StompHeaders();\nheaders.setDestination("/topic/...");\nheaders.setReceipt("r1");\nFrameHandler handler = ...;\nstompSession.subscribe(headers, handler).addReceiptTask(() -> {\n // Subscription ready...\n});\n')])])]),r("p",[e._v("在"),r("code",[e._v("brokerChannel")]),e._v("上的服务器端选项是"),r("a",{attrs:{href:"#websocket-stomp-interceptors"}},[e._v("要注册")]),e._v("和"),r("code",[e._v("ExecutorChannelInterceptor")]),e._v(",并实现"),r("code",[e._v("afterMessageHandled")]),e._v("方法,该方法在处理完消息(包括订阅)后调用。")]),e._v(" "),r("h5",{attrs:{id:"messageexceptionhandler"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#messageexceptionhandler"}},[e._v("#")]),e._v(" "),r("code",[e._v("@MessageExceptionHandler")])]),e._v(" "),r("p",[e._v("应用程序可以使用"),r("code",[e._v("@MessageExceptionHandler")]),e._v("方法来处理来自"),r("code",[e._v("@MessageMapping")]),e._v("方法的异常。如果希望访问异常实例,可以在注释本身中声明异常,也可以通过方法参数声明异常。下面的示例通过方法参数声明异常:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Controller\npublic class MyController {\n\n // ...\n\n @MessageExceptionHandler\n public ApplicationError handleException(MyException exception) {\n // ...\n return appError;\n }\n}\n")])])]),r("p",[r("code",[e._v("@MessageExceptionHandler")]),e._v("方法支持灵活的方法签名,并支持与["),r("code",[e._v("@MessageMapping")]),e._v("](# WebSocket-stomp-message-mapping)方法相同的方法参数类型和返回值。")]),e._v(" "),r("p",[e._v("通常,"),r("code",[e._v("@MessageExceptionHandler")]),e._v("方法应用于声明它们的"),r("code",[e._v("@Controller")]),e._v("类(或类层次结构)中。如果你希望这样的方法更多地全局应用(跨控制器),那么可以在标记为"),r("code",[e._v("@ControllerAdvice")]),e._v("的类中声明它们。这类似于 Spring MVC 中可用的"),r("a",{attrs:{href:"#mvc-ann-controller-advice"}},[e._v("类似的支持")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-7-发送消息"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-7-发送消息"}},[e._v("#")]),e._v(" 4.4.7.发送消息")]),e._v(" "),r("p",[e._v("如果你想要从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向"),r("code",[e._v("brokerChannel")]),e._v("发送消息。这样做的最简单的方法是注入"),r("code",[e._v("SimpMessagingTemplate")]),e._v("并使用它发送消息。通常,你将按类型注入它,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class GreetingController {\n\n private SimpMessagingTemplate template;\n\n @Autowired\n public GreetingController(SimpMessagingTemplate template) {\n this.template = template;\n }\n\n @RequestMapping(path="/greetings", method=POST)\n public void greet(String greeting) {\n String text = "[" + getTimestamp() + "]:" + greeting;\n this.template.convertAndSend("/topic/greetings", text);\n }\n\n}\n')])])]),r("p",[e._v("但是,如果存在另一个相同类型的 Bean,你也可以通过它的名称("),r("code",[e._v("brokerMessagingTemplate")]),e._v(")来限定它。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-8-简单经纪人"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-8-简单经纪人"}},[e._v("#")]),e._v(" 4.4.8.简单经纪人")]),e._v(" "),r("p",[e._v("内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。代理支持类似路径的目标,包括对 Ant 风格的目标模式的订阅。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("应用程序也可以使用点分隔(而不是斜杠分隔)的目的地。"),r("br"),e._v("参见"),r("a",{attrs:{href:"#websocket-stomp-destination-separator"}},[e._v("作为分隔器的点")]),e._v("。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("如果配置了任务调度程序,那么简单代理支持"),r("a",{attrs:{href:"https://stomp.github.io/stomp-specification-1.2.html#Heart-beating",target:"_blank",rel:"noopener noreferrer"}},[e._v("跺脚心跳"),r("OutboundLink")],1),e._v("。要配置计划程序,你可以声明自己的"),r("code",[e._v("TaskScheduler")]),e._v(" Bean,并通过"),r("code",[e._v("MessageBrokerRegistry")]),e._v("对其进行设置。或者,你可以使用在内置 WebSocket 配置中自动声明的配置,但是,你需要"),r("code",[e._v("@Lazy")]),e._v("来避免在内置 WebSocket 配置和"),r("code",[e._v("WebSocketMessageBrokerConfigurer")]),e._v("之间的循环。例如:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n private TaskScheduler messageBrokerTaskScheduler;\n\n @Autowired\n public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) {\n this.messageBrokerTaskScheduler = taskScheduler;\n }\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry registry) {\n registry.enableSimpleBroker("/queue/", "/topic/")\n .setHeartbeatValue(new long[] {10000, 20000})\n .setTaskScheduler(this.messageBrokerTaskScheduler);\n\n // ...\n }\n}\n')])])]),r("h4",{attrs:{id:"_4-4-9-外部经纪人"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-9-外部经纪人"}},[e._v("#")]),e._v(" 4.4.9.外部经纪人")]),e._v(" "),r("p",[e._v("Simple Broker 非常适合入门,但只支持一组 STOMP 命令(它不支持 ACK、Receipts 和其他一些特性),依赖于一个简单的消息发送循环,并且不适合集群。作为替代方案,你可以升级应用程序以使用功能齐全的消息代理。")]),e._v(" "),r("p",[e._v("查看选择的消息代理的 STOMP 文档(例如"),r("a",{attrs:{href:"https://www.rabbitmq.com/stomp.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("RabbitMQ"),r("OutboundLink")],1),e._v(","),r("a",{attrs:{href:"https://activemq.apache.org/stomp.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("ActiveMQ"),r("OutboundLink")],1),e._v("等),安装代理,并在启用了 STOMP 支持的情况下运行它。然后,你可以在 Spring 配置中启用 Stomp 代理中继(而不是简单的代理)。")]),e._v(" "),r("p",[e._v("下面的示例配置启用了功能齐全的代理:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void registerStompEndpoints(StompEndpointRegistry registry) {\n registry.addEndpoint("/portfolio").withSockJS();\n }\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry registry) {\n registry.enableStompBrokerRelay("/topic", "/queue");\n registry.setApplicationDestinationPrefixes("/app");\n }\n\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n \n \n\n\n')])])]),r("p",[e._v("在前面的配置中,Stomp 代理中继是一个 Spring["),r("code",[e._v("MessageHandler")]),e._v("](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/springframework/messing/messagehandler.html),它通过将消息转发到外部消息代理来处理消息。为此,它建立到代理的 TCP 连接,将所有消息转发给它,然后将从代理收到的所有消息通过其 WebSocket 会话转发给客户机。从本质上讲,它充当了双向转发消息的“中继”。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("为 TCP 连接管理将"),r("code",[e._v("io.projectreactor.netty:reactor-netty")]),e._v("和"),r("code",[e._v("io.netty:netty-all")]),e._v("依赖项添加到项目中。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("此外,应用程序组件(例如 HTTP 请求处理方法、业务服务和其他)也可以向代理中继发送消息,如"),r("a",{attrs:{href:"#websocket-stomp-handle-send"}},[e._v("发送消息")]),e._v("中所述,以将消息广播到已订阅的客户端 WebSocket。")]),e._v(" "),r("p",[e._v("实际上,代理中继支持健壮和可伸缩的消息广播。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-10-连接到代理"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-10-连接到代理"}},[e._v("#")]),e._v(" 4.4.10.连接到代理")]),e._v(" "),r("p",[e._v("Stomp 代理中继维护与代理的单个“系统”TCP 连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。可以为此连接配置 STOMP 凭据(即 STOMP 框架"),r("code",[e._v("login")]),e._v("和"),r("code",[e._v("passcode")]),e._v("标头)。这在 XML 名称空间和 Java 配置中都公开为"),r("code",[e._v("systemLogin")]),e._v("和"),r("code",[e._v("systemPasscode")]),e._v("属性,其默认值为"),r("code",[e._v("guest")]),e._v("和"),r("code",[e._v("guest")]),e._v("。")]),e._v(" "),r("p",[e._v("Stomp 代理中继还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接。你可以配置用于代表客户机创建的所有 TCP 连接的 STOMP 凭据。这在 XML 名称空间和 Java 配置中都公开为"),r("code",[e._v("clientLogin")]),e._v("和"),r("code",[e._v("clientPasscode")]),e._v("属性,其默认值为"),r("code",[e._v("guest")]),e._v("和"),r("code",[e._v("guest")]),e._v("。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("Stomp 代理中继总是在它代表客户转发给代理的每个"),r("code",[e._v("CONNECT")]),e._v("框架上设置"),r("code",[e._v("login")]),e._v("和"),r("code",[e._v("passcode")]),e._v("头。因此, WebSocket 客户机"),r("br"),e._v("不需要设置那些头。他们被忽视了。正如"),r("a",{attrs:{href:"#websocket-stomp-authentication"}},[e._v("认证")]),e._v("部分所解释的那样, WebSocket 客户端应该依赖 HTTP 身份验证来保护"),r("br"),e._v(" WebSocket 端点并建立客户端标识。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("Stomp 代理中继还通过“System”TCP 连接向消息代理发送和接收来自消息代理的心跳。你可以配置发送和接收心跳的间隔(默认情况下各为 10 秒)。如果失去了与代理的连接,代理中继将继续尝试每 5 秒重新连接一次,直到成功。")]),e._v(" "),r("p",[e._v("任何 Spring Bean 都可以实现"),r("code",[e._v("ApplicationListener")]),e._v(",以在与代理的“系统”连接丢失并重新建立时接收通知。例如,当没有活动的“系统”连接时,广播股票报价的股票报价服务可以停止尝试发送消息。")]),e._v(" "),r("p",[e._v("默认情况下,STOMP 代理中继总是连接到相同的主机和端口,如果连接丢失,则根据需要重新连接。如果你希望提供多个地址,那么在每次尝试连接时,你可以配置一个地址供应商,而不是一个固定的主机和端口。下面的示例展示了如何做到这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {\n\n // ...\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry registry) {\n registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());\n registry.setApplicationDestinationPrefixes("/app");\n }\n\n private ReactorNettyTcpClient createTcpClient() {\n return new ReactorNettyTcpClient<>(\n client -> client.addressSupplier(() -> ... ),\n new StompReactorNettyCodec());\n }\n}\n')])])]),r("p",[e._v("你还可以使用"),r("code",[e._v("virtualHost")]),e._v("属性配置 Stomp 代理中继。此属性的值被设置为每个"),r("code",[e._v("CONNECT")]),e._v("帧的"),r("code",[e._v("host")]),e._v("头,并且可以是有用的(例如,在云环境中,其中建立 TCP 连接的实际主机与提供基于云的 Stomp 服务的主机不同)。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-11-作为分隔器的点"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-11-作为分隔器的点"}},[e._v("#")]),e._v(" 4.4.11.作为分隔器的点")]),e._v(" "),r("p",[e._v("当消息路由到"),r("code",[e._v("@MessageMapping")]),e._v("方法时,它们将与"),r("code",[e._v("AntPathMatcher")]),e._v("进行匹配。默认情况下,模式应该使用斜杠("),r("code",[e._v("/")]),e._v(")作为分隔符。这是 Web 应用程序中的一种很好的约定,类似于 HTTP URL。但是,如果你更习惯于消息传递约定,则可以切换到使用 dot("),r("code",[e._v(".")]),e._v(")作为分隔符。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何在 Java 配置中实现这一点:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n // ...\n\n @Override\n public void configureMessageBroker(MessageBrokerRegistry registry) {\n registry.setPathMatcher(new AntPathMatcher("."));\n registry.enableStompBrokerRelay("/queue", "/topic");\n registry.setApplicationDestinationPrefixes("/app");\n }\n}\n')])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \n \n\n \n \n \n\n\n')])])]),r("p",[e._v("在此之后,控制器可以在"),r("code",[e._v("@MessageMapping")]),e._v("方法中使用一个点("),r("code",[e._v(".")]),e._v(")作为分隔符,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\n@MessageMapping("red")\npublic class RedController {\n\n @MessageMapping("blue.{green}")\n public void handleGreen(@DestinationVariable String green) {\n // ...\n }\n}\n')])])]),r("p",[e._v("客户机现在可以向"),r("code",[e._v("/app/red.blue.green123")]),e._v("发送消息。")]),e._v(" "),r("p",[e._v("在前面的示例中,我们没有更改“代理中继”的前缀,因为这些前缀完全依赖于外部消息代理。请参阅你使用的代理的 STOMP 文档页,以了解它对目标头支持哪些约定。")]),e._v(" "),r("p",[e._v("另一方面,“简单代理”确实依赖于配置的"),r("code",[e._v("PathMatcher")]),e._v(",因此,如果你切换分隔符,该更改也适用于代理以及代理从消息到订阅模式的目标匹配方式。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-12-认证"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-12-认证"}},[e._v("#")]),e._v(" 4.4.12.认证")]),e._v(" "),r("p",[e._v("在 WebSocket 消息传递会话中的每一次重击都是从一个 HTTP 请求开始的。这可以是一个升级到 WebSockets 的请求(即 WebSocket 次握手),或者在 Sockjs 回退的情况下,是一系列 Sockjs HTTP 传输请求。")]),e._v(" "),r("p",[e._v("许多 Web 应用程序已经具有适当的身份验证和授权,以保护 HTTP 请求。通常,通过使用诸如登录页面、HTTP 基本身份验证或另一种方式的某种机制,通过 Spring 安全性对用户进行身份验证。经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一基于 Cookie 的会话中的后续请求相关联。")]),e._v(" "),r("p",[e._v("因此,对于 WebSocket 握手或对于 Sockjs HTTP 传输请求,通常已经存在通过"),r("code",[e._v("HttpServletRequest#getUserPrincipal()")]),e._v("可访问的经过身份验证的用户。 Spring 自动地将该用户与为他们创建的 WebSocket 或 Sockjs 会话相关联,随后,与通过该会话通过用户头传输的所有 Stomp 消息相关联。")]),e._v(" "),r("p",[e._v("简而言之,一个典型的 Web 应用程序只需要做它在安全性方面已经做过的事情。通过基于 Cookie 的 HTTP 会话(该会话随后与为该用户创建的 WebSocket 或 Sockjs 会话相关联)维护安全上下文,在 HTTP 请求级别上对用户进行身份验证,并在流经该应用程序的每个"),r("code",[e._v("Message")]),e._v("上标记一个用户标头。")]),e._v(" "),r("p",[e._v("在"),r("code",[e._v("CONNECT")]),e._v("框架上,Stomp 协议确实有"),r("code",[e._v("login")]),e._v("和"),r("code",[e._v("passcode")]),e._v("头。它们最初是为 TCP 上的 Stomp 而设计的,现在也需要这样做。然而,对于 STOMP over WebSocket,默认情况下, Spring 忽略了 STOMP 协议级别上的身份验证头,并假定用户已经在 HTTP 传输级别上进行了身份验证。期望 WebSocket 或 Sockjs 会话包含经过身份验证的用户。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-13-令牌认证"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-13-令牌认证"}},[e._v("#")]),e._v(" 4.4.13.令牌认证")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://github.com/spring-projects/spring-security-oauth",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security OAuth"),r("OutboundLink")],1),e._v("提供了对基于令牌的安全性的支持,包括 JSON Web 令牌。你可以将其用作 Web 应用程序中的身份验证机制,包括对 WebSocket 交互的 stomp,如上一节所述(即,通过基于 cookie 的会话来维护身份)。")]),e._v(" "),r("p",[e._v("同时,基于 Cookie 的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中,或者在通常使用头进行身份验证的移动应用程序中)。")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://tools.ietf.org/html/rfc6455#section-10.5",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebSocket protocol, RFC 6455"),r("OutboundLink")],1),e._v("“并没有规定服务器可以在 WebSocket 握手过程中对客户端进行身份验证的任何特定方式。”然而,在实践中,浏览器客户机只能使用标准的身份验证头(即基本的 HTTP 身份验证)或 Cookie,并且不能(例如)提供自定义的头。同样,Sockjs JavaScript 客户机也不提供一种发送带有 Sockjs 传输请求的 HTTP 头的方法。见"),r("a",{attrs:{href:"https://github.com/sockjs/sockjs-client/issues/196",target:"_blank",rel:"noopener noreferrer"}},[e._v("Sockjs-客户端第 196 期"),r("OutboundLink")],1),e._v("。相反,它确实允许发送查询参数,你可以使用这些参数来发送令牌,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("上述限制适用于基于浏览器的客户机,并且不适用于"),r("br"),e._v(" Spring 基于 Java 的 Stomp 客户机,该客户机确实支持发送带有"),r("br"),e._v(" WebSocket 和 Sockjs 请求的头。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("因此,希望避免使用 Cookie 的应用程序可能没有任何好的 HTTP 协议级别的身份验证替代方案。与其使用 Cookie,他们可能更喜欢在 Stomp 消息传递协议级别使用头进行身份验证。这样做需要两个简单的步骤:")]),e._v(" "),r("ol",[r("li",[r("p",[e._v("使用 STOMP 客户机在连接时传递身份验证头。")])]),e._v(" "),r("li",[r("p",[e._v("用"),r("code",[e._v("ChannelInterceptor")]),e._v("处理身份验证头。")])])]),e._v(" "),r("p",[e._v("下一个示例使用服务器端配置来注册自定义身份验证拦截器。请注意,拦截器只需要验证和设置 Connect"),r("code",[e._v("Message")]),e._v("上的用户头。 Spring 记录并保存经过身份验证的用户,并将其与相同会话上的后续 Stomp 消息关联。下面的示例展示了如何注册自定义身份验证拦截器:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocketMessageBroker\npublic class MyConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void configureClientInboundChannel(ChannelRegistration registration) {\n registration.interceptors(new ChannelInterceptor() {\n @Override\n public Message preSend(Message message, MessageChannel channel) {\n StompHeaderAccessor accessor =\n MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);\n if (StompCommand.CONNECT.equals(accessor.getCommand())) {\n Authentication user = ... ; // access authentication header(s)\n accessor.setUser(user);\n }\n return message;\n }\n });\n }\n}\n")])])]),r("p",[e._v("另外,请注意,当你对消息使用 Spring Security 的授权时,目前,你需要确保身份验证"),r("code",[e._v("ChannelInterceptor")]),e._v("配置是在 Spring Security 的授权之前进行的。最好的方法是在其自己的"),r("code",[e._v("WebSocketMessageBrokerConfigurer")]),e._v("实现中声明自定义拦截器,该实现被标记为"),r("code",[e._v("@Order(Ordered.HIGHEST_PRECEDENCE + 99)")]),e._v("。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-14-授权"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-14-授权"}},[e._v("#")]),e._v(" 4.4.14.授权")]),e._v(" "),r("p",[e._v("Spring 安全性提供了"),r("a",{attrs:{href:"https://docs.spring.io/spring-security/reference/servlet/integrations/websocket.html#websocket-authorization",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebSocket sub-protocol authorization"),r("OutboundLink")],1),e._v(",其使用"),r("code",[e._v("ChannelInterceptor")]),e._v("基于其中的用户头来授权消息。另外, Spring 会话提供了"),r("a",{attrs:{href:"https://docs.spring.io/spring-session/reference/web-socket.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebSocket integration"),r("OutboundLink")],1),e._v(",以确保在 WebSocket 会话仍然处于活动状态时用户的 HTTP 会话不会过期。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-15-用户目的地"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-15-用户目的地"}},[e._v("#")]),e._v(" 4.4.15.用户目的地")]),e._v(" "),r("p",[e._v("应用程序可以发送针对特定用户的消息, Spring 的 STOMP 支持为此目的识别带有"),r("code",[e._v("/user/")]),e._v("前缀的目的地。例如,客户端可能订阅"),r("code",[e._v("/user/queue/position-updates")]),e._v("目的地。"),r("code",[e._v("UserDestinationMessageHandler")]),e._v("处理此目的地并将其转换为用户会话所独有的目的地(例如"),r("code",[e._v("/queue/position-updates-user123")]),e._v(")。这提供了订阅一个通用命名的目的地的便利,同时,确保不与订阅相同目的地的其他用户发生冲突,以便每个用户都可以接收唯一的股票位置更新。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("在使用用户目标时,配置代理和"),r("br"),e._v("应用程序目标前缀是很重要的,如"),r("a",{attrs:{href:"#websocket-stomp-enable"}},[e._v("启用 Stomp")]),e._v("中所示,否则"),r("br"),e._v("代理将处理“/user”前缀消息,这些消息只应由"),r("code",[e._v("UserDestinationMessageHandler")]),e._v("处理。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在发送端,消息可以被发送到诸如"),r("code",[e._v("/user/{username}/queue/position-updates")]),e._v("的目的地,这反过来又通过"),r("code",[e._v("UserDestinationMessageHandler")]),e._v("被翻译成一个或多个目的地,一个针对每个会话与用户相关联。这使得应用程序中的任何组件都可以发送针对特定用户的消息,而不必知道他们的名字和通用目的地以外的任何信息。这也通过注释和消息传递模板得到了支持。")]),e._v(" "),r("p",[e._v("一种消息处理方法可以通过"),r("code",[e._v("@SendToUser")]),e._v("注释(也支持在类级上共享一个公共目的地)向与正在处理的消息相关联的用户发送消息,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class PortfolioController {\n\n @MessageMapping("/trade")\n @SendToUser("/queue/position-updates")\n public TradeResult executeTrade(Trade trade, Principal principal) {\n // ...\n return tradeResult;\n }\n}\n')])])]),r("p",[e._v("如果用户有一个以上的会话,默认情况下,目标用户是订阅给定目标的所有会话。然而,有时可能需要只针对发送要处理的消息的会话。可以通过将"),r("code",[e._v("broadcast")]),e._v("属性设置为 false 来实现此目的,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class MyController {\n\n @MessageMapping("/action")\n public void handleAction() throws Exception{\n // raise MyBusinessException here\n }\n\n @MessageExceptionHandler\n @SendToUser(destinations="/queue/errors", broadcast=false)\n public ApplicationError handleException(MyBusinessException exception) {\n // ...\n return appError;\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("虽然用户目的地通常意味着经过身份验证的用户,但并不是严格要求的。"),r("br"),e._v("与经过身份验证的用户不关联的 WebSocket 会话"),r("br"),e._v("可以订阅用户目的地。在这种情况下,"),r("code",[e._v("@SendToUser")]),e._v("注释"),r("br"),e._v("的行为与"),r("code",[e._v("broadcast=false")]),e._v("完全相同(即仅针对发送正在处理的消息的"),r("br"),e._v("会话)。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("你可以通过注入由 Java 配置或 XML 命名空间创建的"),r("code",[e._v("SimpMessagingTemplate")]),e._v(",从任何应用程序组件向用户目的地发送消息。(如果使用"),r("code",[e._v("@Qualifier")]),e._v("进行限定,则 Bean 名称为"),r("code",[e._v("brokerMessagingTemplate")]),e._v("。)下面的示例展示了如何这样做:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Service\npublic class TradeServiceImpl implements TradeService {\n\n private final SimpMessagingTemplate messagingTemplate;\n\n @Autowired\n public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {\n this.messagingTemplate = messagingTemplate;\n }\n\n // ...\n\n public void afterTradeExecuted(Trade trade) {\n this.messagingTemplate.convertAndSendToUser(\n trade.getUserName(), "/queue/position-updates", trade.getResult());\n }\n}\n')])])]),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当你使用带有外部消息代理的用户目的地时,你应该检查代理"),r("br"),e._v("关于如何管理非活动队列的文档,这样,当用户会话"),r("br"),e._v("结束时,所有唯一的用户队列都将被删除。例如,当你使用诸如"),r("code",[e._v("/exchange/amq.direct/position-updates")]),e._v("之类的目标时,RabbitMQ 会创建自动删除"),r("br"),e._v("队列。"),r("br"),e._v("因此,在这种情况下,客户端可以订阅"),r("code",[e._v("/user/exchange/amq.direct/position-updates")]),e._v("。"),r("br"),e._v("类似地,ActiveMQ 也有"),r("a",{attrs:{href:"https://activemq.apache.org/delete-inactive-destinations.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("配置选项"),r("OutboundLink")],1),e._v("用于清除不活动的目标。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在多应用服务器场景中,由于用户连接到不同的服务器,用户目的地可能仍未解决。在这种情况下,你可以配置一个目标来广播未解决的消息,以便其他服务器有机会尝试。这可以通过 Java 配置中"),r("code",[e._v("MessageBrokerRegistry")]),e._v("的"),r("code",[e._v("userDestinationBroadcast")]),e._v("属性和 XML 中"),r("code",[e._v("message-broker")]),e._v("元素的"),r("code",[e._v("user-destination-broadcast")]),e._v("属性来完成。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-16-消息顺序"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-16-消息顺序"}},[e._v("#")]),e._v(" 4.4.16.消息顺序")]),e._v(" "),r("p",[e._v("来自代理的消息被发布到"),r("code",[e._v("clientOutboundChannel")]),e._v(",从那里它们被写到 WebSocket 会话。由于通道由"),r("code",[e._v("ThreadPoolExecutor")]),e._v("支持,消息在不同的线程中进行处理,客户端接收的结果序列可能与发布的确切顺序不匹配。")]),e._v(" "),r("p",[e._v("如果这是一个问题,请启用"),r("code",[e._v("setPreservePublishOrder")]),e._v("标志,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocketMessageBroker\npublic class MyConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n protected void configureMessageBroker(MessageBrokerRegistry registry) {\n // ...\n registry.setPreservePublishOrder(true);\n }\n\n}\n")])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \x3c!-- ... --\x3e\n \n\n\n')])])]),r("p",[e._v("设置该标志后,同一客户机会话中的消息将一次发布到"),r("code",[e._v("clientOutboundChannel")]),e._v(",以保证发布的顺序。请注意,这会带来很小的性能开销,因此你应该仅在需要时才启用它。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-17-事件"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-17-事件"}},[e._v("#")]),e._v(" 4.4.17.事件")]),e._v(" "),r("p",[e._v("发布了几个"),r("code",[e._v("ApplicationContext")]),e._v("事件,并且可以通过实现 Spring 的"),r("code",[e._v("ApplicationListener")]),e._v("接口来接收这些事件:")]),e._v(" "),r("ul",[r("li",[r("p",[r("code",[e._v("BrokerAvailabilityEvent")]),e._v(":表示代理何时变得可用或不可用。虽然“简单”代理在启动时立即可用,并且在应用程序运行时仍然可用,但 STOMP“代理中继”可能会失去与功能齐全的代理的连接(例如,如果代理被重新启动)。代理中继具有重新连接逻辑,并在代理恢复时重新建立与代理的“系统”连接。因此,每当状态从连接变为断开时,此事件就会发布,反之亦然。使用"),r("code",[e._v("SimpMessagingTemplate")]),e._v("的组件应该订阅此事件,并避免在代理不可用时发送消息。在任何情况下,他们都应该准备好在发送消息时处理"),r("code",[e._v("MessageDeliveryException")]),e._v("。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("SessionConnectEvent")]),e._v(":当接收到新的 Stomp 连接时发布,以表示新客户端会话的开始。该事件包含表示连接的消息,包括会话 ID、用户信息(如果有的话)以及客户端发送的任何自定义标头。这对于跟踪客户端会话非常有用。订阅此事件的组件可以用"),r("code",[e._v("SimpMessageHeaderAccessor")]),e._v("或"),r("code",[e._v("StompMessageHeaderAccessor")]),e._v("包装所包含的消息。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("SessionConnectedEvent")]),e._v(":在"),r("code",[e._v("SessionConnectEvent")]),e._v("之后不久发布,此时代理已经发送了一个 Stomp Connected 帧来响应该连接。在这一点上,Stomp 会话可以被认为是完全成立的。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("SessionSubscribeEvent")]),e._v(":在接收到新的 Stomp 订阅时发布。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("SessionUnsubscribeEvent")]),e._v(":当收到新的 stomp 退订时发布。")])]),e._v(" "),r("li",[r("p",[r("code",[e._v("SessionDisconnectEvent")]),e._v(":在 stomp 会话结束时发布。断开连接可以是已经从客户端发送的,也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,此事件在每个会话中发布不止一次。对于多个断开事件,组件应该是幂等的。")])])]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当你使用功能齐全的代理时,如果代理暂时不可用,则 STOMP“代理中继”会自动重新连接"),r("br"),e._v("“系统”连接。但是,"),r("br"),e._v("客户端连接不会自动重新连接。假设启用了心跳,客户机"),r("br"),e._v("通常会注意到代理在 10 秒内没有响应。客户端需要"),r("br"),e._v("实现自己的重新连接逻辑。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("h4",{attrs:{id:"_4-4-18-拦截"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-18-拦截"}},[e._v("#")]),e._v(" 4.4.18.拦截")]),e._v(" "),r("p",[r("a",{attrs:{href:"#websocket-stomp-appplication-context-events"}},[e._v("Events")]),e._v("为 stomp 连接的生命周期提供通知,但不是为每个客户机消息提供通知。应用程序还可以注册一个"),r("code",[e._v("ChannelInterceptor")]),e._v("来拦截任何消息和处理链的任何部分。下面的示例展示了如何截获来自客户端的入站消息:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void configureClientInboundChannel(ChannelRegistration registration) {\n registration.interceptors(new MyChannelInterceptor());\n }\n}\n")])])]),r("p",[e._v("自定义"),r("code",[e._v("ChannelInterceptor")]),e._v("可以使用"),r("code",[e._v("StompHeaderAccessor")]),e._v("或"),r("code",[e._v("SimpMessageHeaderAccessor")]),e._v("来访问有关消息的信息,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class MyChannelInterceptor implements ChannelInterceptor {\n\n @Override\n public Message preSend(Message message, MessageChannel channel) {\n StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);\n StompCommand command = accessor.getStompCommand();\n // ...\n return message;\n }\n}\n")])])]),r("p",[e._v("应用程序还可以实现"),r("code",[e._v("ExecutorChannelInterceptor")]),e._v(",这是"),r("code",[e._v("ChannelInterceptor")]),e._v("的子接口,在处理消息的线程中具有回调。对于发送到通道的每条消息,都会调用一次"),r("code",[e._v("ChannelInterceptor")]),e._v(",而"),r("code",[e._v("ExecutorChannelInterceptor")]),e._v("在每个订阅了来自通道的消息的"),r("code",[e._v("MessageHandler")]),e._v("的线程中提供钩子。")]),e._v(" "),r("p",[e._v("注意,与前面描述的"),r("code",[e._v("SessionDisconnectEvent")]),e._v("一样,断开连接消息可以是来自客户端的,或者也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开事件,组件应该是幂等的。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-19-stomp-客户端"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-19-stomp-客户端"}},[e._v("#")]),e._v(" 4.4.19.STOMP 客户端")]),e._v(" "),r("p",[e._v("Spring 提供了在 WebSocket 客户端上的 stomp 和在 TCP 客户端上的 stomp。")]),e._v(" "),r("p",[e._v("首先,你可以创建和配置"),r("code",[e._v("WebSocketStompClient")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("WebSocketClient webSocketClient = new StandardWebSocketClient();\nWebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);\nstompClient.setMessageConverter(new StringMessageConverter());\nstompClient.setTaskScheduler(taskScheduler); // for heartbeats\n")])])]),r("p",[e._v("在前面的示例中,你可以将"),r("code",[e._v("StandardWebSocketClient")]),e._v("替换为"),r("code",[e._v("SockJsClient")]),e._v(",因为这也是"),r("code",[e._v("WebSocketClient")]),e._v("的实现。"),r("code",[e._v("SockJsClient")]),e._v("可以使用 WebSocket 或基于 HTTP 的传输作为后备。有关更多详细信息,请参见["),r("code",[e._v("SockJsClient")]),e._v("](# WebSocket-fallback-sockjs-client)。")]),e._v(" "),r("p",[e._v("接下来,你可以建立一个连接,并为 STOMP 会话提供一个处理程序,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('String url = "ws://127.0.0.1:8080/endpoint";\nStompSessionHandler sessionHandler = new MyStompSessionHandler();\nstompClient.connect(url, sessionHandler);\n')])])]),r("p",[e._v("当会话准备好使用时,将通知处理程序,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("public class MyStompSessionHandler extends StompSessionHandlerAdapter {\n\n @Override\n public void afterConnected(StompSession session, StompHeaders connectedHeaders) {\n // ...\n }\n}\n")])])]),r("p",[e._v("一旦建立了会话,就可以发送任何有效负载,并使用配置的"),r("code",[e._v("MessageConverter")]),e._v("进行序列化,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('session.send("/topic/something", "payload");\n')])])]),r("p",[e._v("你也可以订阅目的地。"),r("code",[e._v("subscribe")]),e._v("方法需要一个订阅消息的处理程序,并返回一个"),r("code",[e._v("Subscription")]),e._v("句柄,你可以使用它来取消订阅。对于每个接收到的消息,处理程序可以指定目标"),r("code",[e._v("Object")]),e._v("类型,有效负载应该反序列化到该类型,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('session.subscribe("/topic/something", new StompFrameHandler() {\n\n @Override\n public Type getPayloadType(StompHeaders headers) {\n return String.class;\n }\n\n @Override\n public void handleFrame(StompHeaders headers, Object payload) {\n // ...\n }\n\n});\n')])])]),r("p",[e._v("要启用 Stomp heartbeat,可以使用"),r("code",[e._v("WebSocketStompClient")]),e._v("配置"),r("code",[e._v("TaskScheduler")]),e._v("并可选地自定义心跳间隔(10 秒用于写不活动,导致发送心跳;10 秒用于读不活动,关闭连接)。")]),e._v(" "),r("p",[r("code",[e._v("WebSocketStompClient")]),e._v("仅在不活动的情况下发送心跳,即没有发送其他消息时。当使用外部代理时,这可能会带来挑战,因为具有非代理目的地的消息表示活动,但 AREN 并未实际转发给代理。在这种情况下,你可以在初始化"),r("code",[e._v("TaskScheduler")]),e._v("时配置"),r("code",[e._v("TaskScheduler")]),e._v(",从而确保仅在发送具有非代理目的地的消息时也将心跳转发到代理。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[e._v("当你使用"),r("code",[e._v("WebSocketStompClient")]),e._v("进行性能测试以模拟来自同一台机器的数千个"),r("br"),e._v("客户端时,请考虑关闭心跳,因为每个"),r("br"),e._v("连接都调度自己的心跳任务,而这并未针对"),r("br"),e._v("在同一台机器上运行的大量客户端进行优化。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("STOMP 协议还支持 Receipts,其中客户端必须添加"),r("code",[e._v("receipt")]),e._v("头,服务器在处理发送或订阅后用接收帧对其进行响应。为了支持这一点,"),r("code",[e._v("StompSession")]),e._v("提供了"),r("code",[e._v("setAutoReceipt(boolean)")]),e._v(",这会导致在随后的每个发送或订阅事件上添加"),r("code",[e._v("receipt")]),e._v("头。或者,你也可以手动将收据标题添加到"),r("code",[e._v("StompHeaders")]),e._v("中。send 和 subscribe 都返回"),r("code",[e._v("Receiptable")]),e._v("的实例,你可以使用该实例来注册接收成功和失败的回调。对于此功能,你必须为客户机配置"),r("code",[e._v("TaskScheduler")]),e._v("和收据过期前的时间(默认情况下为 15 秒)。")]),e._v(" "),r("p",[e._v("请注意,"),r("code",[e._v("StompSessionHandler")]),e._v("本身是"),r("code",[e._v("StompFrameHandler")]),e._v(",这使得它除了用于处理消息异常的"),r("code",[e._v("handleException")]),e._v("回调和用于处理包括"),r("code",[e._v("ConnectionLostException")]),e._v("在内的传输级别错误的"),r("code",[e._v("handleTransportError")]),e._v("回调外,还可以处理错误帧。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-20-websocket-范围"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-20-websocket-范围"}},[e._v("#")]),e._v(" 4.4.20. WebSocket 范围")]),e._v(" "),r("p",[e._v("WebSocket 每个会话都有一个属性映射。映射作为头附加到入站客户端消息,并且可以从控制器方法访问,如以下示例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Controller\npublic class MyController {\n\n @MessageMapping("/action")\n public void handle(SimpMessageHeaderAccessor headerAccessor) {\n Map attrs = headerAccessor.getSessionAttributes();\n // ...\n }\n}\n')])])]),r("p",[e._v("你可以在"),r("code",[e._v("websocket")]),e._v("范围中声明一个 Spring 管理的 Bean。你可以将 WebSocket 范围的 bean 注入控制器和在"),r("code",[e._v("clientInboundChannel")]),e._v("上注册的任何通道拦截器。这些通常是单例,并且比任何单独的会话活得更长 WebSocket。因此,你需要对 WebSocket 范围的 bean 使用范围代理模式,如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('@Component\n@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)\npublic class MyBean {\n\n @PostConstruct\n public void init() {\n // Invoked after dependencies injected\n }\n\n // ...\n\n @PreDestroy\n public void destroy() {\n // Invoked when the WebSocket session ends\n }\n}\n\n@Controller\npublic class MyController {\n\n private final MyBean myBean;\n\n @Autowired\n public MyController(MyBean myBean) {\n this.myBean = myBean;\n }\n\n @MessageMapping("/action")\n public void handle() {\n // this.myBean from the current WebSocket session\n }\n}\n')])])]),r("p",[e._v("与任何自定义作用域一样, Spring 在第一次从控制器访问新的"),r("code",[e._v("MyBean")]),e._v("实例时初始化该实例,并将该实例存储在 WebSocket 会话属性中。随后将返回相同的实例,直到会话结束。 WebSocket-作用域 bean 具有调用的所有 Spring 生命周期方法,如前面的示例中所示。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-21-表现"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-21-表现"}},[e._v("#")]),e._v(" 4.4.21.表现")]),e._v(" "),r("p",[e._v("谈到业绩,没有灵丹妙药。许多因素都会影响它,包括消息的大小和数量,应用程序方法是否执行需要阻塞的工作,以及外部因素(例如网络速度和其他问题)。本节的目标是提供可用配置选项的概述,以及关于如何推理缩放的一些想法。")]),e._v(" "),r("p",[e._v("在消息传递应用程序中,消息通过通道传递,以进行由线程池支持的异步执行。配置这样的应用程序需要对通道和消息流有很好的了解。因此,建议复习"),r("a",{attrs:{href:"#websocket-stomp-message-flow"}},[e._v("消息流")]),e._v("。")]),e._v(" "),r("p",[e._v("显而易见的开始是配置线程池,这些线程池支持"),r("code",[e._v("clientInboundChannel")]),e._v("和"),r("code",[e._v("clientOutboundChannel")]),e._v("。默认情况下,这两个处理器的配置都是可用处理器数量的两倍。")]),e._v(" "),r("p",[e._v("如果在带注释的方法中处理消息主要是 CPU 绑定的,那么"),r("code",[e._v("clientInboundChannel")]),e._v("的线程数量应该与处理器数量保持接近。如果他们所做的工作更受 IO 约束,并且需要阻塞或等待数据库或其他外部系统,则线程池的大小可能需要增加。")]),e._v(" "),r("table",[r("thead",[r("tr",[r("th"),e._v(" "),r("th",[r("code",[e._v("ThreadPoolExecutor")]),e._v("有三个重要的属性:核心线程池大小,"),r("br"),e._v("最大线程池大小,以及队列存储"),r("br"),e._v("没有可用线程的任务的能力。"),r("br"),e._v("一个常见的混淆之处是,配置核心池大小(例如,10)"),r("br"),e._v("和最大线程池大小(例如,20)会导致线程池中包含 10 到 20 个线程,实际上,如果将容量保持在其默认值 integer.max_value,"),r("br"),e._v(",则线程池永远不会超过核心池大小而增加,由于"),r("br"),e._v("所有额外的任务都是排队的。"),r("br"),r("br"),e._v("参见"),r("code",[e._v("ThreadPoolExecutor")]),e._v("的 Javadoc 来了解这些属性是如何工作的,以及"),r("br"),e._v("了解各种排队策略。")])])]),e._v(" "),r("tbody")]),e._v(" "),r("p",[e._v("在"),r("code",[e._v("clientOutboundChannel")]),e._v("方面,这完全是关于向 WebSocket 客户端发送消息。如果客户机在快速网络上,线程的数量应该与可用处理器的数量保持接近。如果它们速度较慢或带宽较低,则会花费更长的时间来消耗消息,并给线程池带来负担。因此,增加线程池的大小是必要的。")]),e._v(" "),r("p",[e._v("虽然"),r("code",[e._v("clientInboundChannel")]),e._v("的工作负载是可以预测的——毕竟,它是基于应用程序所做的工作——但如何配置“ClientoutboundChannel”比较困难,因为它基于应用程序无法控制的因素。因此,还有两个属性与消息的发送有关:"),r("code",[e._v("sendTimeLimit")]),e._v("和"),r("code",[e._v("sendBufferSizeLimit")]),e._v("。你可以使用这些方法来配置允许发送多长时间,以及在向客户机发送消息时可以缓冲多少数据。")]),e._v(" "),r("p",[e._v("一般的想法是,在任何给定的时间,只能使用单个线程发送到客户端。同时,所有附加的消息都会得到缓冲,你可以使用这些属性来决定允许发送消息需要多长时间,以及在此期间可以缓冲多少数据。有关重要的附加详细信息,请参见 XMLSchema 的 Javadoc 和文档。")]),e._v(" "),r("p",[e._v("下面的示例展示了一种可能的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void configureWebSocketTransport(WebSocketTransportRegistration registration) {\n registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);\n }\n\n // ...\n\n}\n")])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \x3c!-- ... --\x3e\n \n\n\n')])])]),r("p",[e._v("还可以使用前面显示的 WebSocket 传输配置来配置传入的 STOMP 消息的最大允许大小。从理论上讲, WebSocket 条消息的大小几乎可以是无限的。在实践中, WebSocket 服务器施加了限制——例如,在 Tomcat 上施加 8K,在 Jetty 上施加 64K。出于这个原因,STOMP 客户机(例如 JavaScript"),r("a",{attrs:{href:"https://github.com/JSteunou/webstomp-client",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebStomp-客户端"),r("OutboundLink")],1),e._v("和其他)在 16K 边界分割较大的 STOMP 消息,并将它们作为多个消息发送 WebSocket,这需要服务器进行缓冲和重新组装。")]),e._v(" "),r("p",[e._v("Spring 的 Stomp-over- WebSocket 支持做到了这一点,因此应用程序可以为 Stomp 消息配置最大大小,而与 WebSocket 服务器特定的消息大小无关。请记住, WebSocket 消息大小是自动调整的,如果需要的话,以确保它们能够至少携带 16k WebSocket 消息。")]),e._v(" "),r("p",[e._v("下面的示例展示了一种可能的配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("@Configuration\n@EnableWebSocketMessageBroker\npublic class WebSocketConfig implements WebSocketMessageBrokerConfigurer {\n\n @Override\n public void configureWebSocketTransport(WebSocketTransportRegistration registration) {\n registration.setMessageSizeLimit(128 * 1024);\n }\n\n // ...\n\n}\n")])])]),r("p",[e._v("下面的示例展示了与前面示例类似的 XML 配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v('\n\n \n \n \x3c!-- ... --\x3e\n \n\n\n')])])]),r("p",[e._v("关于扩展的一个要点是使用多个应用程序实例。目前,你无法使用简单的代理来实现这一点。然而,当使用全功能代理(例如 RabbitMQ)时,每个应用程序实例都连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的 WebSocket 客户端。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-22-监测"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-22-监测"}},[e._v("#")]),e._v(" 4.4.22.监测")]),e._v(" "),r("p",[e._v("当使用"),r("code",[e._v("@EnableWebSocketMessageBroker")]),e._v("或"),r("code",[e._v("")]),e._v("时,关键基础设施组件会自动收集统计信息和计数器,它们为应用程序的内部状态提供了重要的见解。该配置还声明了类型"),r("code",[e._v("WebSocketMessageBrokerStats")]),e._v("的 Bean,该类型在一个位置收集所有可用的信息,并且默认情况下每 30 分钟将其记录在"),r("code",[e._v("INFO")]),e._v("级别。这个 Bean 可以通过 Spring 的"),r("code",[e._v("MBeanExporter")]),e._v("导出到 JMX,以便在运行时查看(例如,通过 JDK 的"),r("code",[e._v("jconsole")]),e._v(")。以下清单概述了可用的信息:")]),e._v(" "),r("p",[e._v("客户端 WebSocket 会话")]),e._v(" "),r("p",[e._v("当前")]),e._v(" "),r("p",[e._v("指示当前有多少个客户端会话,计数进一步细分为 WebSocket 与 HTTP 流和 Polling Sockjs 会话的比较。")]),e._v(" "),r("p",[e._v("合计")]),e._v(" "),r("p",[e._v("指示总共建立了多少个会话。")]),e._v(" "),r("p",[e._v("异常关闭")]),e._v(" "),r("p",[e._v("连接失败")]),e._v(" "),r("p",[e._v("建立了会话,但在 60 秒内没有收到任何消息后关闭。这通常表示代理或网络问题。")]),e._v(" "),r("p",[e._v("超过发送限制")]),e._v(" "),r("p",[e._v("会话在超过配置的发送超时或发送缓冲区限制后关闭,这可能发生在客户端速度较慢的情况下(请参见上一节)。")]),e._v(" "),r("p",[e._v("传输错误")]),e._v(" "),r("p",[e._v("会话在传输错误之后关闭,例如未能读或写到 WebSocket 连接或 HTTP 请求或响应。")]),e._v(" "),r("p",[e._v("Stomp 框架")]),e._v(" "),r("p",[e._v("处理的连接帧、连接帧和断开帧的总数,表示在 STOMP 级别上连接了多少个客户端。请注意,当会话异常关闭或当客户端关闭而不发送断开连接帧时,断开连接计数可能会更低。")]),e._v(" "),r("p",[e._v("Stomp 经纪商接力")]),e._v(" "),r("p",[e._v("TCP 连接")]),e._v(" "),r("p",[e._v("指示代表客户端 WebSocket 会话向代理建立了多少 TCP 连接。这应该等于客户端 WebSocket 会话的数量 + 用于从应用程序内发送消息的 1 个额外的共享“系统”连接。")]),e._v(" "),r("p",[e._v("Stomp 框架")]),e._v(" "),r("p",[e._v("代表客户端转发给代理或从代理接收的连接、已连接和断开连接帧的总数。请注意,无论客户端 WebSocket 会话是如何关闭的,断开连接帧都会被发送到代理。因此,较低的断开帧计数表示代理正在主动关闭连接(可能是由于心跳没有及时到达,输入帧无效或其他问题)。")]),e._v(" "),r("p",[e._v("客户端入站通道")]),e._v(" "),r("p",[e._v("来自线程池的统计数据支持"),r("code",[e._v("clientInboundChannel")]),e._v(",这些统计数据提供了对传入消息处理的健康状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果存在 I/O 绑定任务(例如,缓慢的数据库查询、对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。")]),e._v(" "),r("p",[e._v("客户端出站通道")]),e._v(" "),r("p",[e._v("来自线程池的统计数据支持"),r("code",[e._v("clientOutboundChannel")]),e._v(",该线程池提供了对向客户广播消息的健康状况的深入了解。在这里排队等待的任务表明客户端太慢,无法使用消息。解决这个问题的一种方法是增加线程池大小,以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(请参见上一节)。")]),e._v(" "),r("p",[e._v("Sockjs 任务调度程序")]),e._v(" "),r("p",[e._v("来自用于发送心跳的 SockJS 任务计划程序的线程池的统计信息。请注意,当心跳在 Stomp 级别协商时,Sockjs 心跳将被禁用。")]),e._v(" "),r("h4",{attrs:{id:"_4-4-23-测试"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-23-测试"}},[e._v("#")]),e._v(" 4.4.23.测试")]),e._v(" "),r("p",[e._v("在使用 Spring 的 Stomp-over- WebSocket 支持时,有两种主要的方法来测试应用程序。第一种方法是编写服务器端测试,以验证控制器的功能及其带注释的消息处理方法。第二种方法是编写完整的端到端测试,其中涉及运行客户机和服务器。")]),e._v(" "),r("p",[e._v("这两种方法并不相互排斥。相反,它们在总体测试策略中都有一席之地。服务器端测试更加集中,并且更容易编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。")]),e._v(" "),r("p",[e._v("服务器端测试的最简单形式是编写控制器单元测试。然而,这是不够有用的,因为控制器所做的很大程度上取决于它的注释。纯粹的单元测试根本无法测试这一点。")]),e._v(" "),r("p",[e._v("理想情况下,被测控制器应该像在运行时那样被调用,这很像通过使用 Spring MVC 测试框架来测试处理 HTTP 请求的控制器的方法——也就是说,不运行 Servlet 容器,而是依赖 Spring 框架来调用带注释的控制器。与 Spring MVC 测试一样,这里有两种可能的选择,要么使用“基于上下文”的设置,要么使用“独立”的设置:")]),e._v(" "),r("ul",[r("li",[r("p",[e._v("借助 Spring TestContext 框架加载实际的 Spring 配置,注入"),r("code",[e._v("clientInboundChannel")]),e._v("作为测试字段,并使用它发送要由控制器方法处理的消息。")])]),e._v(" "),r("li",[r("p",[e._v("手动设置调用控制器(即"),r("code",[e._v("SimpAnnotationMethodMessageHandler")]),e._v(")所需的最低 Spring 框架基础设施,并将控制器的消息直接传递给它。")])])]),e._v(" "),r("p",[e._v("这两种设置场景都在"),r("a",{attrs:{href:"https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web",target:"_blank",rel:"noopener noreferrer"}},[e._v("股票投资组合的测试"),r("OutboundLink")],1),e._v("示例应用程序中进行了演示。")]),e._v(" "),r("p",[e._v("第二种方法是创建端到端集成测试。为此,你需要以嵌入式模式运行一个 WebSocket 服务器,并将其作为一个 WebSocket 客户端连接到它,该客户端发送 WebSocket 包含 Stomp 帧的消息。"),r("a",{attrs:{href:"https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web",target:"_blank",rel:"noopener noreferrer"}},[e._v("股票投资组合的测试"),r("OutboundLink")],1),e._v("示例应用程序还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单的 Stomp 客户端来演示这种方法。")]),e._v(" "),r("h2",{attrs:{id:"_5-其他-web-框架"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-其他-web-框架"}},[e._v("#")]),e._v(" 5. 其他 Web 框架")]),e._v(" "),r("p",[e._v("本章详细介绍了 Spring 与第三方 Web 框架的集成。")]),e._v(" "),r("p",[e._v("Spring 框架的核心价值主张之一是使 * 选择 * 成为可能。在一般意义上, Spring 并不强迫你使用或购买任何特定的架构、技术或方法(尽管它肯定会推荐一些而不是其他)。这种选择与开发人员及其开发团队最相关的架构、技术或方法的自由,可以说在 Web 领域最为明显, Spring 在该领域提供了自己的 Web 框架("),r("a",{attrs:{href:"#mvc"}},[e._v("Spring MVC")]),e._v("和"),r("RouterLink",{attrs:{to:"/spring-framework/webflux.html#webflux"}},[e._v("Spring WebFlux")]),e._v("),同时,支持与许多流行的第三方 Web 框架的集成。")],1),e._v(" "),r("h3",{attrs:{id:"_5-1-公共配置"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-1-公共配置"}},[e._v("#")]),e._v(" 5.1.公共配置")]),e._v(" "),r("p",[e._v("在深入了解每个受支持的 Web 框架的集成细节之前,让我们先来看看不特定于任何一个 Web 框架的常见配置 Spring。(本节同样适用于 Spring 自己的 Web 框架变体。)")]),e._v(" "),r("p",[e._v("Spring 的轻量级应用程序模型支持的概念之一(因为没有更好的词)是分层架构。请记住,在一个“经典”的分层架构中,Web 层只是许多层中的一个。它充当服务器端应用程序的入口点之一,并将其委托给在服务层中定义的服务对象(Facades),以满足特定于业务(和表示技术无关)的用例。在 Spring 中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象存在于不同的“业务上下文”中,其中不包含 Web 或表示层对象(表示对象,例如 Spring MVC 控制器,通常配置在不同的“表示上下文”中)。本节详细介绍了如何配置包含应用程序中所有“business bean”的 Spring 容器(a"),r("code",[e._v("WebApplicationContext")]),e._v(")。")]),e._v(" "),r("p",[e._v("接下来讨论细节,你所需要做的就是在标准的 Java EE Servlet "),r("code",[e._v("web.xml")]),e._v("文件中声明一个["),r("code",[e._v("ContextLoaderListener")]),e._v("(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/contextloaderlistener.html),并添加一个"),r("code",[e._v("contextConfigLocation")]),e._v("节(在同一文件中),该节定义要加载的哪组 XML 配置文件 Spring。")]),e._v(" "),r("p",[e._v("考虑以下"),r("code",[e._v("")]),e._v("配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n org.springframework.web.context.ContextLoaderListener\n\n")])])]),r("p",[e._v("进一步考虑以下"),r("code",[e._v("")]),e._v("配置:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n contextConfigLocation\n /WEB-INF/applicationContext*.xml\n\n")])])]),r("p",[e._v("如果没有指定"),r("code",[e._v("contextConfigLocation")]),e._v("上下文参数,则"),r("code",[e._v("ContextLoaderListener")]),e._v("查找要加载的名为"),r("code",[e._v("/WEB-INF/applicationContext.xml")]),e._v("的文件。一旦加载了上下文文件, Spring 将基于 Bean 定义创建一个["),r("code",[e._v("WebApplicationContext")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/webapplicationcontext.html)对象,并将其存储在 Web 应用程序的"),r("code",[e._v("ServletContext")]),e._v("中。")]),e._v(" "),r("p",[e._v("所有 Java Web 框架都建立在 Servlet API 之上,因此你可以使用以下代码片段来访问由"),r("code",[e._v("ApplicationContext")]),e._v("创建的“业务上下文”"),r("code",[e._v("ContextLoaderListener")]),e._v("。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何获得"),r("code",[e._v("WebApplicationContext")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);\n")])])]),r("p",[e._v("["),r("code",[e._v("WebApplicationContextUtils")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/context/support/webapplicationcontextutils.html)类是为了方便,所以你不需要记住"),r("code",[e._v("ServletContext")]),e._v("属性的名称。如果在"),r("code",[e._v("WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE")]),e._v("键下不存在对象,则其"),r("code",[e._v("getWebApplicationContext()")]),e._v("方法返回"),r("code",[e._v("null")]),e._v("。与其冒险在应用程序中获得"),r("code",[e._v("NullPointerExceptions")]),e._v(",不如使用"),r("code",[e._v("getRequiredWebApplicationContext()")]),e._v("方法。当"),r("code",[e._v("ApplicationContext")]),e._v("丢失时,此方法抛出一个异常。")]),e._v(" "),r("p",[e._v("一旦有了对"),r("code",[e._v("WebApplicationContext")]),e._v("的引用,就可以根据 bean 的名称或类型检索 bean。大多数开发人员按名称检索 bean,然后将它们强制转换到其实现的接口之一。")]),e._v(" "),r("p",[e._v("幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法。它们不仅使从 Spring 容器获得 bean 变得容易,而且还允许你在它们的控制器上使用依赖注入。每个 Web Framework 部分都有关于其特定集成策略的更多详细信息。")]),e._v(" "),r("h3",{attrs:{id:"_5-2-jsf"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-jsf"}},[e._v("#")]),e._v(" 5.2.JSF")]),e._v(" "),r("p",[e._v("JavaServer Faces 是 JCP 的标准的基于组件的、事件驱动的 Web 用户界面框架。它是 Java EE 保护伞的正式部分,但也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。")]),e._v(" "),r("p",[e._v("请注意,最近的 JSF 版本与应用程序服务器中的 CDI 基础架构紧密相关,一些新的 JSF 功能仅在这样的环境中工作。 Spring 的 JSF 支持不再是积极发展的,主要是为了在更新基于 JSF 的旧应用程序时的迁移目的而存在。")]),e._v(" "),r("p",[e._v("Spring 的 JSF 集成中的关键元素是 JSF"),r("code",[e._v("ELResolver")]),e._v("机制。")]),e._v(" "),r("h4",{attrs:{id:"_5-2-1-spring-bean-解析器"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-1-spring-bean-解析器"}},[e._v("#")]),e._v(" 5.2.1. Spring Bean 解析器")]),e._v(" "),r("p",[r("code",[e._v("SpringBeanFacesELResolver")]),e._v("是一个兼容 JSF 的"),r("code",[e._v("ELResolver")]),e._v("实现,与 JSF 和 JSP 使用的标准统一 EL 集成。它首先委托给 Spring 的“业务上下文”"),r("code",[e._v("WebApplicationContext")]),e._v(",然后委托给底层 JSF 实现的默认解析器。")]),e._v(" "),r("p",[e._v("在配置方面,你可以在 JSF"),r("code",[e._v("faces-context.xml")]),e._v("文件中定义"),r("code",[e._v("SpringBeanFacesELResolver")]),e._v(",如下例所示:")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("\n \n org.springframework.web.jsf.el.SpringBeanFacesELResolver\n ...\n \n\n")])])]),r("h4",{attrs:{id:"_5-2-2-使用facescontextutils"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-2-2-使用facescontextutils"}},[e._v("#")]),e._v(" 5.2.2.使用"),r("code",[e._v("FacesContextUtils")])]),e._v(" "),r("p",[e._v("当将属性映射到"),r("code",[e._v("faces-config.xml")]),e._v("中的 bean 时,自定义"),r("code",[e._v("ELResolver")]),e._v("很好地工作,但是,有时你可能需要显式地获取 Bean。["),r("code",[e._v("FacesContextUtils")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/jsf/facescontextutils.html)类使这一点变得很简单。它类似于"),r("code",[e._v("WebApplicationContextUtils")]),e._v(",只是它需要一个"),r("code",[e._v("FacesContext")]),e._v("参数,而不是"),r("code",[e._v("ServletContext")]),e._v("参数。")]),e._v(" "),r("p",[e._v("下面的示例展示了如何使用"),r("code",[e._v("FacesContextUtils")]),e._v(":")]),e._v(" "),r("div",{staticClass:"language- extra-class"},[r("pre",{pre:!0,attrs:{class:"language-text"}},[r("code",[e._v("ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());\n")])])]),r("h3",{attrs:{id:"_5-3-apache-struts2-x"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-3-apache-struts2-x"}},[e._v("#")]),e._v(" 5.3.Apache Struts2.x")]),e._v(" "),r("p",[e._v("由 Craig McClanahan 发明的"),r("a",{attrs:{href:"https://struts.apache.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("Struts"),r("OutboundLink")],1),e._v("是一个由 Apache 软件基金会主持的开源项目。当时,它极大地简化了 JSP/ Servlet 编程范式,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此像 Beer 一样是免费的),并且它拥有一个庞大的社区,这让该项目得以发展并在 Java Web 开发人员中流行起来。")]),e._v(" "),r("p",[e._v("作为原始 Struts1.x 的后续版本,请查看 Struts2.x 和 Struts-提供的"),r("a",{attrs:{href:"https://struts.apache.org/release/2.3.x/docs/spring-plugin.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Plugin"),r("OutboundLink")],1),e._v("用于内置 Spring 集成。")]),e._v(" "),r("h3",{attrs:{id:"_5-4-apache-tapestry5-x"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-4-apache-tapestry5-x"}},[e._v("#")]),e._v(" 5.4.Apache Tapestry5.x")]),e._v(" "),r("p",[r("a",{attrs:{href:"https://tapestry.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Tapestry"),r("OutboundLink")],1),e._v("是一个“面向组件的框架,用于在 Java 中创建动态的、健壮的、高度可扩展的 Web 应用程序。”")]),e._v(" "),r("p",[e._v("Spring 虽然具有自己的"),r("a",{attrs:{href:"#mvc"}},[e._v("强大的 Web 层")]),e._v(",但是通过使用用于 Web 用户界面的 Tapestry 和用于较低层的 Spring 容器的组合来构建 Enterprise 的 Java 应用程序有许多独特的优点。")]),e._v(" "),r("p",[e._v("有关更多信息,请参见 Tapestry 的专用"),r("a",{attrs:{href:"https://tapestry.apache.org/integrating-with-spring-framework.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("integration module for Spring"),r("OutboundLink")],1),e._v("。")]),e._v(" "),r("h3",{attrs:{id:"_5-5-更多资源"}},[r("a",{staticClass:"header-anchor",attrs:{href:"#_5-5-更多资源"}},[e._v("#")]),e._v(" 5.5.更多资源")]),e._v(" "),r("p",[e._v("下面的链接指向关于本章中描述的各种 Web 框架的更多参考资料。")]),e._v(" "),r("ul",[r("li",[r("p",[r("a",{attrs:{href:"https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("JSF"),r("OutboundLink")],1),e._v("主页")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://struts.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Struts"),r("OutboundLink")],1),e._v("主页")])]),e._v(" "),r("li",[r("p",[r("a",{attrs:{href:"https://tapestry.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Tapestry"),r("OutboundLink")],1),e._v("主页")])])])])}),[],!1,null,null,null);t.default=n.exports}}]);