(window.webpackJsonp=window.webpackJsonp||[]).push([[410],{845:function(e,r,t){"use strict";t.r(r);var n=t(56),o=Object(n.a)({},(function(){var e=this,r=e.$createElement,t=e._self._c||r;return t("ContentSlotsDistributor",{attrs:{"slot-key":e.$parent.slotKey}},[t("h1",{attrs:{id:"万维网"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#万维网"}},[e._v("#")]),e._v(" 万维网")]),e._v(" "),t("p",[e._v("Spring 启动非常适合于 Web 应用程序开发。你可以使用嵌入式 Tomcat、 Jetty、 Undertow 或 Netty 创建一个自包含的 HTTP 服务器。大多数 Web 应用程序使用"),t("code",[e._v("spring-boot-starter-web")]),e._v("模块来快速启动和运行。你还可以选择使用"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("模块构建反应性 Web 应用程序。")]),e._v(" "),t("p",[e._v("如果你还没有开发 Spring 启动 Web 应用程序,那么可以按照 "),t("em",[t("RouterLink",{attrs:{to:"/spring-boot/getting-started.html#getting-started.first-application"}},[e._v("开始")])],1),e._v(" 部分中的“Hello World!”示例进行操作。")]),e._v(" "),t("h2",{attrs:{id:"_1-servlet-web-应用程序"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-servlet-web-应用程序"}},[e._v("#")]),e._v(" 1. Servlet Web 应用程序")]),e._v(" "),t("p",[e._v("如果你想构建基于 Servlet 的 Web 应用程序,可以利用 Spring Boot 的自动配置来实现 Spring MVC 或 Jersey。")]),e._v(" "),t("h3",{attrs:{id:"_1-1-spring-web-mvc-框架"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-spring-web-mvc-框架"}},[e._v("#")]),e._v(" 1.1.“ Spring Web MVC 框架”")]),e._v(" "),t("p",[t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Web MVC framework"),t("OutboundLink")],1),e._v("(通常被称为“ Spring MVC”)是一个丰富的“模型视图控制器”Web 框架。 Spring MVC 允许你创建特殊的"),t("code",[e._v("@Controller")]),e._v("或"),t("code",[e._v("@RestController")]),e._v("bean 来处理传入的 HTTP 请求。通过使用"),t("code",[e._v("@RequestMapping")]),e._v("注释,将控制器中的方法映射到 HTTP。")]),e._v(" "),t("p",[e._v("下面的代码显示了提供 JSON 数据的典型"),t("code",[e._v("@RestController")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import java.util.List;\n\nimport org.springframework.web.bind.annotation.DeleteMapping;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping("/users")\npublic class MyRestController {\n\n private final UserRepository userRepository;\n\n private final CustomerRepository customerRepository;\n\n public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {\n this.userRepository = userRepository;\n this.customerRepository = customerRepository;\n }\n\n @GetMapping("/{user}")\n public User getUser(@PathVariable Long userId) {\n return this.userRepository.findById(userId).get();\n }\n\n @GetMapping("/{user}/customers")\n public List getUserCustomers(@PathVariable Long userId) {\n return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();\n }\n\n @DeleteMapping("/{user}")\n public void deleteUser(@PathVariable Long userId) {\n this.userRepository.deleteById(userId);\n }\n\n}\n\n')])])]),t("p",[e._v("“WebMVC.FN”是一种功能变体,它将路由配置与请求的实际处理分离开来,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.servlet.function.RequestPredicate;\nimport org.springframework.web.servlet.function.RouterFunction;\nimport org.springframework.web.servlet.function.ServerResponse;\n\nimport static org.springframework.web.servlet.function.RequestPredicates.accept;\nimport static org.springframework.web.servlet.function.RouterFunctions.route;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyRoutingConfiguration {\n\n private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);\n\n @Bean\n public RouterFunction routerFunction(MyUserHandler userHandler) {\n return route()\n .GET("/{user}", ACCEPT_JSON, userHandler::getUser)\n .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)\n .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)\n .build();\n }\n\n}\n\n')])])]),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.springframework.stereotype.Component;\nimport org.springframework.web.servlet.function.ServerRequest;\nimport org.springframework.web.servlet.function.ServerResponse;\n\n@Component\npublic class MyUserHandler {\n\n public ServerResponse getUser(ServerRequest request) {\n ...\n return ServerResponse.ok().build();\n }\n\n public ServerResponse getUserCustomers(ServerRequest request) {\n ...\n return ServerResponse.ok().build();\n }\n\n public ServerResponse deleteUser(ServerRequest request) {\n ...\n return ServerResponse.ok().build();\n }\n\n}\n\n")])])]),t("p",[e._v("Spring MVC 是核心 Spring 框架的一部分,详细信息可在"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("参考文献"),t("OutboundLink")],1),e._v("中获得。在"),t("a",{attrs:{href:"https://spring.io/guides",target:"_blank",rel:"noopener noreferrer"}},[e._v("spring.io/guides"),t("OutboundLink")],1),e._v("上也有几个涵盖 Spring MVC 的指南。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("你可以定义任意多的"),t("code",[e._v("RouterFunction")]),e._v("bean 来模块化路由器的定义。如果需要应用优先级,可以订购"),t("br"),e._v("bean。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-1-1-spring-mvc-自动配置"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-1-spring-mvc-自动配置"}},[e._v("#")]),e._v(" 1.1.1. Spring MVC 自动配置")]),e._v(" "),t("p",[e._v("Spring Boot 为 Spring MVC 提供了自动配置,该配置在大多数应用程序中都能很好地工作。")]),e._v(" "),t("p",[e._v("自动配置在 Spring 的默认值之上添加了以下功能:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("包含"),t("code",[e._v("ContentNegotiatingViewResolver")]),e._v("和"),t("code",[e._v("BeanNameViewResolver")]),e._v("bean。")])]),e._v(" "),t("li",[t("p",[e._v("对服务静态资源的支持,包括对 WebJAR 的支持(覆盖"),t("RouterLink",{attrs:{to:"/spring-boot/features.html#web.servlet.spring-mvc.static-content"}},[e._v("在本文的后面部分")]),e._v(")。")],1)]),e._v(" "),t("li",[t("p",[e._v("自动注册"),t("code",[e._v("Converter")]),e._v("、"),t("code",[e._v("GenericConverter")]),e._v("和"),t("code",[e._v("Formatter")]),e._v("bean。")])]),e._v(" "),t("li",[t("p",[e._v("支持"),t("code",[e._v("HttpMessageConverters")]),e._v("(覆盖"),t("RouterLink",{attrs:{to:"/spring-boot/features.html#web.servlet.spring-mvc.message-converters"}},[e._v("在本文的后面部分")]),e._v(")。")],1)]),e._v(" "),t("li",[t("p",[e._v("自动注册"),t("code",[e._v("MessageCodesResolver")]),e._v("(覆盖"),t("RouterLink",{attrs:{to:"/spring-boot/features.html#web.servlet.spring-mvc.message-codes"}},[e._v("在本文的后面部分")]),e._v(")。")],1)]),e._v(" "),t("li",[t("p",[e._v("静态"),t("code",[e._v("index.html")]),e._v("支持。")])]),e._v(" "),t("li",[t("p",[e._v("自动使用"),t("code",[e._v("ConfigurableWebBindingInitializer")]),e._v(" Bean(覆盖"),t("RouterLink",{attrs:{to:"/spring-boot/features.html#web.servlet.spring-mvc.binding-initializer"}},[e._v("在本文的后面部分")]),e._v(")。")],1)])]),e._v(" "),t("p",[e._v("如果你希望保留那些 Spring 引导 MVC 自定义并使更多"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("MVC 定制"),t("OutboundLink")],1),e._v("(拦截器、格式化程序、视图控制器和其他功能),则可以添加你自己的"),t("code",[e._v("@Configuration")]),e._v("类型的"),t("code",[e._v("WebMvcConfigurer")]),e._v("但"),t("strong",[e._v("没有")]),t("code",[e._v("@EnableWebMvc")]),e._v("类。")]),e._v(" "),t("p",[e._v("如果希望提供"),t("code",[e._v("RequestMappingHandlerMapping")]),e._v("、"),t("code",[e._v("RequestMappingHandlerAdapter")]),e._v("或"),t("code",[e._v("ExceptionHandlerExceptionResolver")]),e._v("的自定义实例,并且仍然保持 Spring 引导 MVC 自定义,则可以声明类型"),t("code",[e._v("WebMvcRegistrations")]),e._v("的 Bean 并使用它来提供这些组件的自定义实例。")]),e._v(" "),t("p",[e._v("如果你想完全控制 Spring MVC,你可以添加你自己的"),t("code",[e._v("@Configuration")]),e._v(",并使用"),t("code",[e._v("@EnableWebMvc")]),e._v("进行注释,或者也可以添加你自己的"),t("code",[e._v("@Configuration")]),e._v("-注释"),t("code",[e._v("DelegatingWebMvcConfiguration")]),e._v(",如"),t("code",[e._v("@EnableWebMvc")]),e._v("的 Javadoc 中所描述的。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("Spring MVC 使用不同的"),t("code",[e._v("ConversionService")]),e._v("来转换来自"),t("code",[e._v("application.properties")]),e._v("或"),t("code",[e._v("application.yaml")]),e._v("文件的值,"),t("br"),e._v("表示"),t("code",[e._v("Period")]),e._v(","),t("code",[e._v("Duration")]),e._v("和"),t("code",[e._v("DataSize")]),e._v("转换器不可用,并且"),t("code",[e._v("@DurationUnit")]),e._v("和"),t("code",[e._v("@DataSizeUnit")]),e._v("注释将被忽略。"),t("br"),t("br"),e._v("如果你想定制 Spring MVC 使用的"),t("code",[e._v("ConversionService")]),e._v(",你可以提供带有"),t("code",[e._v("WebMvcConfigurer")]),e._v(" Bean 的"),t("code",[e._v("addFormatters")]),e._v("方法。"),t("br"),e._v("从该方法你可以注册任何你喜欢的转换器,或者你可以将其委托给"),t("code",[e._v("ApplicationConversionService")]),e._v("上可用的静态方法。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-1-2-htpmessageconverters"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-2-htpmessageconverters"}},[e._v("#")]),e._v(" 1.1.2.HtpMessageConverters")]),e._v(" "),t("p",[e._v("Spring MVC 使用"),t("code",[e._v("HttpMessageConverter")]),e._v("接口来转换 HTTP 请求和响应。合理的默认值是开箱即用的。例如,对象可以自动转换为 JSON(通过使用 Jackson 库)或 XML(通过使用 JacksonXML 扩展(如果可用的话),或者通过使用 JAXB(如果 JacksonXML 扩展不可用的话)。默认情况下,字符串编码为"),t("code",[e._v("UTF-8")]),e._v("。")]),e._v(" "),t("p",[e._v("如果需要添加或自定义转换器,可以使用 Spring boot 的"),t("code",[e._v("HttpMessageConverters")]),e._v("类,如以下清单所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.springframework.boot.autoconfigure.http.HttpMessageConverters;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.converter.HttpMessageConverter;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyHttpMessageConvertersConfiguration {\n\n @Bean\n public HttpMessageConverters customConverters() {\n HttpMessageConverter additional = new AdditionalHttpMessageConverter();\n HttpMessageConverter another = new AnotherHttpMessageConverter();\n return new HttpMessageConverters(additional, another);\n }\n\n}\n\n")])])]),t("p",[e._v("上下文中存在的任何"),t("code",[e._v("HttpMessageConverter")]),e._v(" Bean 都被添加到转换器列表中。你也可以用同样的方法覆盖默认的转换器。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-3-自定义-json-序列化器和反序列化器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-3-自定义-json-序列化器和反序列化器"}},[e._v("#")]),e._v(" 1.1.3.自定义 JSON 序列化器和反序列化器")]),e._v(" "),t("p",[e._v("如果使用 Jackson 序列化和反序列化 JSON 数据,则可能需要编写自己的"),t("code",[e._v("JsonSerializer")]),e._v("和"),t("code",[e._v("JsonDeserializer")]),e._v("类。自定义序列化器通常是"),t("a",{attrs:{href:"https://github.com/FasterXML/jackson-docs/wiki/JacksonHowToCustomSerializers",target:"_blank",rel:"noopener noreferrer"}},[e._v("通过模块在 Jackson 中注册"),t("OutboundLink")],1),e._v(",但是 Spring Boot 提供了一种替代的"),t("code",[e._v("@JsonComponent")]),e._v("注释,使得直接注册 Spring bean 更加容易。")]),e._v(" "),t("p",[e._v("你可以直接在"),t("code",[e._v("JsonSerializer")]),e._v("、"),t("code",[e._v("JsonDeserializer")]),e._v("或"),t("code",[e._v("KeyDeserializer")]),e._v("实现上使用"),t("code",[e._v("@JsonComponent")]),e._v("注释。你也可以在包含序列化器/反序列化器作为内部类的类上使用它,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import java.io.IOException;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.JsonProcessingException;\nimport com.fasterxml.jackson.core.ObjectCodec;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonDeserializer;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.JsonSerializer;\nimport com.fasterxml.jackson.databind.SerializerProvider;\n\nimport org.springframework.boot.jackson.JsonComponent;\n\n@JsonComponent\npublic class MyJsonComponent {\n\n public static class Serializer extends JsonSerializer {\n\n @Override\n public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {\n jgen.writeStringField("name", value.getName());\n jgen.writeNumberField("age", value.getAge());\n }\n\n }\n\n public static class Deserializer extends JsonDeserializer {\n\n @Override\n public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt)\n throws IOException, JsonProcessingException {\n ObjectCodec codec = jsonParser.getCodec();\n JsonNode tree = codec.readTree(jsonParser);\n String name = tree.get("name").textValue();\n int age = tree.get("age").intValue();\n return new MyObject(name, age);\n }\n\n }\n\n}\n\n')])])]),t("p",[e._v("在"),t("code",[e._v("ApplicationContext")]),e._v("中的所有"),t("code",[e._v("@JsonComponent")]),e._v("bean 都会自动注册到 Jackson 中。由于"),t("code",[e._v("@JsonComponent")]),e._v("是用"),t("code",[e._v("@Component")]),e._v("进行元注释的,因此通常的组件扫描规则适用。")]),e._v(" "),t("p",[e._v("Spring Boot 还提供了["),t("code",[e._v("JsonObjectSerializer")]),e._v('](https://github.com/ Spring-projects/ Spring-boot/tree/v2.6.4/ Spring-boot-project/ Spring-boot/SRC/main/java/org/springframework/Jackson/jsonobjectserializer.java.java)和[](https://github.com/[ Spring-projects/ Spring-boot/tree/v2.6.4/ Spring- Spring-boot-project-project/[参见['),t("code",[e._v("JsonObjectSerializer")]),e._v("](https://DOCS. Spring.io/ Spring-boot/DOCS/2.6.4/api/org/springframework/boot/Jackson/jsonobjectserializer.html)和["),t("code",[e._v("JsonObjectDeserializer")]),e._v("](https://DOCS. Spring.io/ Spring-boot/DOCS/2.6.4/api/org/springframework/boot/Jackson/jsonobjeserializer.html)中的 JavaDoc 以获取详细信息。")]),e._v(" "),t("p",[e._v("上面的示例可以重写为使用"),t("code",[e._v("JsonObjectSerializer")]),e._v("/"),t("code",[e._v("JsonObjectDeserializer")]),e._v("如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import java.io.IOException;\n\nimport com.fasterxml.jackson.core.JsonGenerator;\nimport com.fasterxml.jackson.core.JsonParser;\nimport com.fasterxml.jackson.core.ObjectCodec;\nimport com.fasterxml.jackson.databind.DeserializationContext;\nimport com.fasterxml.jackson.databind.JsonNode;\nimport com.fasterxml.jackson.databind.SerializerProvider;\n\nimport org.springframework.boot.jackson.JsonComponent;\nimport org.springframework.boot.jackson.JsonObjectDeserializer;\nimport org.springframework.boot.jackson.JsonObjectSerializer;\n\n@JsonComponent\npublic class MyJsonComponent {\n\n public static class Serializer extends JsonObjectSerializer {\n\n @Override\n protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)\n throws IOException {\n jgen.writeStringField("name", value.getName());\n jgen.writeNumberField("age", value.getAge());\n }\n\n }\n\n public static class Deserializer extends JsonObjectDeserializer {\n\n @Override\n protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,\n JsonNode tree) throws IOException {\n String name = nullSafeValue(tree.get("name"), String.class);\n int age = nullSafeValue(tree.get("age"), Integer.class);\n return new MyObject(name, age);\n }\n\n }\n\n}\n\n')])])]),t("h4",{attrs:{id:"_1-1-4-messenger-ecodesresolver"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-4-messenger-ecodesresolver"}},[e._v("#")]),e._v(" 1.1.4.Messenger EcodesResolver")]),e._v(" "),t("p",[e._v("Spring MVC 具有用于生成用于从绑定错误呈现错误消息的错误代码的策略:。如果你设置"),t("code",[e._v("spring.mvc.message-codes-resolver-format")]),e._v("属性"),t("code",[e._v("PREFIX_ERROR_CODE")]),e._v("或"),t("code",[e._v("POSTFIX_ERROR_CODE")]),e._v(", Spring boot 将为你创建一个(参见["),t("code",[e._v("DefaultMessageCodesResolver.Format")]),e._v("]中的枚举(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/defalidation/ressMessagecover.format.html))。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-5-静态内容"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-5-静态内容"}},[e._v("#")]),e._v(" 1.1.5.静态内容")]),e._v(" "),t("p",[e._v("默认情况下, Spring 引导从 Classpath 中的一个名为"),t("code",[e._v("/static")]),e._v("(或"),t("code",[e._v("/public")]),e._v("或"),t("code",[e._v("/resources")]),e._v("或"),t("code",[e._v("/META-INF/resources")]),e._v(")的目录或从"),t("code",[e._v("ServletContext")]),e._v("的根目录提供静态内容。它使用 Spring MVC 中的"),t("code",[e._v("ResourceHttpRequestHandler")]),e._v(",这样你就可以通过添加自己的"),t("code",[e._v("WebMvcConfigurer")]),e._v("并重写"),t("code",[e._v("addResourceHandlers")]),e._v("方法来修改该行为。")]),e._v(" "),t("p",[e._v("在独立的 Web 应用程序中,来自容器的默认 Servlet 也被启用,并充当后备,如果 Spring 决定不处理它,则从"),t("code",[e._v("ServletContext")]),e._v("的根目录中提供内容。在大多数情况下,这种情况不会发生(除非你修改了默认的 MVC 配置),因为 Spring 始终可以通过"),t("code",[e._v("DispatcherServlet")]),e._v("处理请求。")]),e._v(" "),t("p",[e._v("默认情况下,资源映射在"),t("code",[e._v("/**")]),e._v("上,但你可以使用"),t("code",[e._v("spring.mvc.static-path-pattern")]),e._v("属性对其进行调优。例如,将所有资源重新定位到"),t("code",[e._v("/resources/**")]),e._v("可以通过以下方式实现:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.static-path-pattern=/resources/**\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n mvc:\n static-path-pattern: "/resources/**"\n')])])]),t("p",[e._v("你还可以通过使用"),t("code",[e._v("spring.web.resources.static-locations")]),e._v("属性(用目录位置列表替换默认值)来定制静态资源位置。根 Servlet 上下文路径"),t("code",[e._v('"/"')]),e._v("也会自动添加为位置。")]),e._v(" "),t("p",[e._v("除了前面提到的“标准”静态资源位置之外,"),t("a",{attrs:{href:"https://www.webjars.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Webjars 内容"),t("OutboundLink")],1),e._v("还有一个特殊情况。路径在"),t("code",[e._v("/webjars/**")]),e._v("中的任何资源,如果以 WebJARS 格式打包,都将从 jar 文件中得到服务。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果你的应用程序被打包为 jar,请不要使用"),t("code",[e._v("src/main/webapp")]),e._v("目录。"),t("br"),e._v("虽然这个目录是一个通用的标准,但是它在 WAR 打包中是"),t("strong",[e._v("只有")]),e._v("的,如果你生成 jar,大多数构建工具都会默默地忽略它。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("Spring 启动还支持由 Spring MVC 提供的高级资源处理功能,允许使用诸如破坏缓存的静态资源或使用用于 WebJAR 的与版本无关的 URL 的情况。")]),e._v(" "),t("p",[e._v("要为 WebJAR 使用与版本无关的 URL,请添加"),t("code",[e._v("webjars-locator-core")]),e._v("依赖项。然后申报你的 Webjar。以 jQuery 为例,添加"),t("code",[e._v('"/webjars/jquery/jquery.min.js"')]),e._v("会导致"),t("code",[e._v('"/webjars/jquery/x.y.z/jquery.min.js"')]),e._v(",其中"),t("code",[e._v("x.y.z")]),e._v("是 WebJAR 版本。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果使用 JBoss,则需要声明"),t("code",[e._v("webjars-locator-jboss-vfs")]),e._v("依赖项,而不是"),t("code",[e._v("webjars-locator-core")]),e._v("。"),t("br"),e._v("否则,所有 WebJAR 解析为"),t("code",[e._v("404")]),e._v("。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("为了使用缓存破坏,以下配置为所有静态资源配置了一个缓存破坏解决方案,有效地在 URL 中添加了内容散列,例如"),t("code",[e._v('')]),e._v(":")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.web.resources.chain.strategy.content.enabled=true\nspring.web.resources.chain.strategy.content.paths=/**\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n web:\n resources:\n chain:\n strategy:\n content:\n enabled: true\n paths: "/**"\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("资源的链接在运行时在模板中被重写,由于"),t("code",[e._v("ResourceUrlEncodingFilter")]),e._v("是为 ThymeLeaf 和 Freemarker 自动配置的。"),t("br"),e._v("在使用 JSP 时,你应该手动声明此过滤器。"),t("br"),e._v("其他模板引擎目前不能自动支持,但可以与自定义模板宏/助手一起使用["),t("code",[e._v("ResourceUrlProvider")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javc-api/org/spramework/spramework/ Servlet/resource/resourceurceurlprovider.html)。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("例如,当使用 JavaScript 模块加载程序动态加载资源时,不能选择重命名文件。这就是为什么其他策略也得到了支持,并且可以结合在一起。“固定”策略在不更改文件名的情况下在 URL 中添加静态版本字符串,如下例所示:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.web.resources.chain.strategy.content.enabled=true\nspring.web.resources.chain.strategy.content.paths=/**\nspring.web.resources.chain.strategy.fixed.enabled=true\nspring.web.resources.chain.strategy.fixed.paths=/js/lib/\nspring.web.resources.chain.strategy.fixed.version=v12\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n web:\n resources:\n chain:\n strategy:\n content:\n enabled: true\n paths: "/**"\n fixed:\n enabled: true\n paths: "/js/lib/"\n version: "v12"\n')])])]),t("p",[e._v("通过这种配置,位于"),t("code",[e._v('"/js/lib/"')]),e._v("下的 JavaScript 模块使用固定的版本控制策略("),t("code",[e._v('"/v12/js/lib/mymodule.js"')]),e._v("),而其他资源仍然使用 Content One("),t("code",[e._v('')]),e._v(")。")]),e._v(" "),t("p",[e._v("参见["),t("code",[e._v("Web属性.Resources")]),e._v("](https://github.com/ Spring-projects/ Spring-boot/tree/v2.6.4/ Spring-boot-project/ Spring-boot-autofigure/SRC/main/java/org/springframework/boot/autofigure/web/webproperties.java)以获得更多支持的选项。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("这个特性已经在一个专用的"),t("a",{attrs:{href:"https://spring.io/blog/2014/07/24/spring-framework-4-1-handling-static-web-resources",target:"_blank",rel:"noopener noreferrer"}},[e._v("blog post"),t("OutboundLink")],1),e._v("和 Spring 框架的"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-config-static-resources",target:"_blank",rel:"noopener noreferrer"}},[e._v("参考文献"),t("OutboundLink")],1),e._v("中进行了详细的描述。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-1-6-欢迎页面"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-6-欢迎页面"}},[e._v("#")]),e._v(" 1.1.6.欢迎页面")]),e._v(" "),t("p",[e._v("Spring 启动支持静态和模板化欢迎页面。它首先在配置的静态内容位置中查找"),t("code",[e._v("index.html")]),e._v("文件。如果没有找到一个,那么它将查找"),t("code",[e._v("index")]),e._v("模板。如果找到其中之一,它将自动用作应用程序的欢迎页面。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-7-路径匹配和内容协商"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-7-路径匹配和内容协商"}},[e._v("#")]),e._v(" 1.1.7.路径匹配和内容协商")]),e._v(" "),t("p",[e._v("Spring MVC 可以通过查看请求路径并将其与你的应用程序中定义的映射(例如,控制器方法上的"),t("code",[e._v("@GetMapping")]),e._v("注释)匹配,将传入的 HTTP 请求映射到处理程序。")]),e._v(" "),t("p",[e._v("Spring 引导默认情况下选择禁用后缀模式匹配,这意味着像"),t("code",[e._v('"GET /projects/spring-boot.json"')]),e._v("这样的请求将不会匹配到"),t("code",[e._v('@GetMapping("/projects/spring-boot")')]),e._v("映射。这被认为是"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-ann-requestmapping-suffix-pattern-match",target:"_blank",rel:"noopener noreferrer"}},[e._v("best practice for Spring MVC applications"),t("OutboundLink")],1),e._v("。这个特性在过去主要用于 HTTP 客户机,因为 HTTP 客户机没有发送正确的“接受”请求头;我们需要确保向客户机发送正确的内容类型。如今,内容协商更可靠了。")]),e._v(" "),t("p",[e._v("还有其他方法来处理不能始终发送正确的“接受”请求头的 HTTP 客户机。与使用后缀匹配不同,我们可以使用一个查询参数来确保像"),t("code",[e._v('"GET /projects/spring-boot?format=json"')]),e._v("这样的请求将被映射到"),t("code",[e._v('@GetMapping("/projects/spring-boot")')]),e._v(":")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.contentnegotiation.favor-parameter=true\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring:\n mvc:\n contentnegotiation:\n favor-parameter: true\n")])])]),t("p",[e._v("或者,如果你更喜欢使用不同的参数名:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.contentnegotiation.favor-parameter=true\nspring.mvc.contentnegotiation.parameter-name=myparam\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n mvc:\n contentnegotiation:\n favor-parameter: true\n parameter-name: "myparam"\n')])])]),t("p",[e._v("大多数标准媒体类型都支持开箱即用,但你也可以定义新的媒体类型:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.contentnegotiation.media-types.markdown=text/markdown\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n mvc:\n contentnegotiation:\n media-types:\n markdown: "text/markdown"\n')])])]),t("p",[e._v("后缀模式匹配不受欢迎,将在未来的版本中删除。如果你理解这些警告,并且仍然希望你的应用程序使用后缀模式匹配,则需要进行以下配置:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.contentnegotiation.favor-path-extension=true\nspring.mvc.pathmatch.use-suffix-pattern=true\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring:\n mvc:\n contentnegotiation:\n favor-path-extension: true\n pathmatch:\n use-suffix-pattern: true\n")])])]),t("p",[e._v("或者,与其打开所有后缀模式,不如只支持已注册的后缀模式更安全:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.contentnegotiation.favor-path-extension=true\nspring.mvc.pathmatch.use-registered-suffix-pattern=true\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring:\n mvc:\n contentnegotiation:\n favor-path-extension: true\n pathmatch:\n use-registered-suffix-pattern: true\n")])])]),t("p",[e._v("在 Spring Framework5.3 中, Spring MVC 支持几种实现策略,用于将请求路径匹配到控制器处理程序。它以前只支持"),t("code",[e._v("AntPathMatcher")]),e._v("策略,但现在也提供"),t("code",[e._v("PathPatternParser")]),e._v("策略。 Spring 现在的启动提供了一种可在新策略中选择和 OPT 的配置属性:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.mvc.pathmatch.matching-strategy=path-pattern-parser\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n mvc:\n pathmatch:\n matching-strategy: "path-pattern-parser"\n')])])]),t("p",[e._v("有关为什么应该考虑这个新实现的更多详细信息,请参见"),t("a",{attrs:{href:"https://spring.io/blog/2020/06/30/url-matching-with-pathpattern-in-spring-mvc",target:"_blank",rel:"noopener noreferrer"}},[e._v("专门的博客文章"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[t("code",[e._v("PathPatternParser")]),e._v("是一个优化的实现,但限制了"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-ann-requestmapping-uri-templates",target:"_blank",rel:"noopener noreferrer"}},[e._v("一些路径模式变体"),t("OutboundLink")],1),e._v("的使用,并且与后缀模式匹配("),t("code",[e._v("spring.mvc.pathmatch.use-suffix-pattern")]),e._v(","),t("code",[e._v("spring.mvc.pathmatch.use-registered-suffix-pattern")]),e._v(")或映射带有 Servlet 前缀("),t("code",[e._v("spring.mvc.servlet.path")]),e._v(")的"),t("code",[e._v("DispatcherServlet")]),e._v("不兼容。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-1-8-配置-webbindinginitializer"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-8-配置-webbindinginitializer"}},[e._v("#")]),e._v(" 1.1.8.配置 WebBindingInitializer")]),e._v(" "),t("p",[e._v("Spring MVC 使用"),t("code",[e._v("WebBindingInitializer")]),e._v("对特定请求初始化"),t("code",[e._v("WebDataBinder")]),e._v("。如果你创建自己的"),t("code",[e._v("ConfigurableWebBindingInitializer``@Bean")]),e._v(", Spring boot 会自动配置 Spring MVC 来使用它。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-9-模板引擎"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-9-模板引擎"}},[e._v("#")]),e._v(" 1.1.9.模板引擎")]),e._v(" "),t("p",[e._v("与 REST Web 服务一样,你也可以使用 Spring MVC 来提供动态 HTML 内容。 Spring MVC 支持各种模板化技术,包括 ThymeLeaf、FreeMarker 和 JSP。此外,许多其他模板引擎还包括它们自己的 Spring MVC 集成。")]),e._v(" "),t("p",[e._v("Spring 启动包括对以下模板引擎的自动配置支持:")]),e._v(" "),t("ul",[t("li",[t("p",[t("a",{attrs:{href:"https://freemarker.apache.org/docs/",target:"_blank",rel:"noopener noreferrer"}},[e._v("FreeMarker"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine",target:"_blank",rel:"noopener noreferrer"}},[e._v("Groovy"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://www.thymeleaf.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("Thymeleaf"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://mustache.github.io/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Mustache"),t("OutboundLink")],1)])])]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果可能的话,应该避免使用 JSP。"),t("br"),e._v("在使用嵌入式 Servlet 容器时,有几个"),t("a",{attrs:{href:"#web.servlet.embedded-container.jsp-limitations"}},[e._v("已知限制")]),e._v("。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("当你使用这些模板引擎中的一个具有默认配置时,你的模板将自动从"),t("code",[e._v("src/main/resources/templates")]),e._v("中提取。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("取决于你如何运行你的应用程序,你的 IDE 可能会以不同的方式对 Classpath 进行排序。"),t("br"),e._v("在 IDE 中从其主方法运行你的应用程序会导致与使用 Maven 或 Gradle 运行你的应用程序时不同的排序。或者从其打包的 jar。"),t("br"),e._v("这可能会导致 Spring 引导无法找到预期的模板。"),t("br"),e._v("如果你有这个问题,你可以重新排序 IDE 中的 Classpath,以便首先放置模块的类和资源。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-1-10-错误处理"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-10-错误处理"}},[e._v("#")]),e._v(" 1.1.10.错误处理")]),e._v(" "),t("p",[e._v("默认情况下, Spring Boot 提供了一个"),t("code",[e._v("/error")]),e._v("映射,该映射以合理的方式处理所有错误,并将其注册为 Servlet 容器中的“全局”错误页。对于机器客户机,它生成一个 JSON 响应,其中包含错误、HTTP 状态和异常消息的详细信息。对于浏览器客户机,有一个“WhiteLabel”错误视图,该视图以 HTML 格式呈现相同的数据(要对其进行自定义,请添加一个"),t("code",[e._v("View")]),e._v(",将其解析为"),t("code",[e._v("error")]),e._v(")。")]),e._v(" "),t("p",[e._v("如果你想定制默认的错误处理行为,那么可以设置许多"),t("code",[e._v("server.error")]),e._v("属性。参见附录中的"),t("RouterLink",{attrs:{to:"/spring-boot/application-properties.html#appendix.application-properties.server"}},[e._v("“服务器属性”")]),e._v("部分。")],1),e._v(" "),t("p",[e._v("要完全替换缺省行为,可以实现"),t("code",[e._v("ErrorController")]),e._v("并注册该类型的 Bean 定义,或者添加"),t("code",[e._v("ErrorAttributes")]),e._v("类型的 Bean 来使用现有机制但替换内容。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[t("code",[e._v("BasicErrorController")]),e._v("可以用作自定义"),t("code",[e._v("ErrorController")]),e._v("的基类,"),t("br"),e._v("如果你想为新的内容类型添加一个处理程序,这一点特别有用(默认情况是专门处理"),t("code",[e._v("text/html")]),e._v(",并为其他所有内容提供一个后备),"),t("br"),e._v("这样做,扩展"),t("code",[e._v("BasicErrorController")]),e._v(",添加一个带有"),t("code",[e._v("@RequestMapping")]),e._v("属性的公共方法,并创建新类型的 Bean。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("还可以定义一个用"),t("code",[e._v("@ControllerAdvice")]),e._v("注释的类,以定制 JSON 文档,从而返回特定的控制器和/或异常类型,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import javax.servlet.RequestDispatcher;\nimport javax.servlet.http.HttpServletRequest;\n\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.ResponseEntity;\nimport org.springframework.web.bind.annotation.ControllerAdvice;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.ResponseBody;\nimport org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;\n\n@ControllerAdvice(basePackageClasses = SomeController.class)\npublic class MyControllerAdvice extends ResponseEntityExceptionHandler {\n\n @ResponseBody\n @ExceptionHandler(MyException.class)\n public ResponseEntity handleControllerException(HttpServletRequest request, Throwable ex) {\n HttpStatus status = getStatus(request);\n return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);\n }\n\n private HttpStatus getStatus(HttpServletRequest request) {\n Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);\n HttpStatus status = HttpStatus.resolve(code);\n return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;\n }\n\n}\n\n")])])]),t("p",[e._v("在前面的示例中,如果"),t("code",[e._v("YourException")]),e._v("由在与"),t("code",[e._v("SomeController")]),e._v("相同的包中定义的控制器抛出,则使用"),t("code",[e._v("CustomErrorType")]),e._v("POJO 的 JSON 表示,而不是"),t("code",[e._v("ErrorAttributes")]),e._v("表示。")]),e._v(" "),t("p",[e._v("在某些情况下,在控制器级别处理的错误不会被"),t("RouterLink",{attrs:{to:"/spring-boot/actuator.html#actuator.metrics.supported.spring-mvc"}},[e._v("度量基础设施")]),e._v("记录。通过将已处理的异常设置为请求属性,应用程序可以确保将此类异常与请求度量一起记录:")],1),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import javax.servlet.http.HttpServletRequest;\n\nimport org.springframework.boot.web.servlet.error.ErrorAttributes;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\n\n@Controller\npublic class MyController {\n\n @ExceptionHandler(CustomException.class)\n String handleCustomException(HttpServletRequest request, CustomException ex) {\n request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);\n return "errorView";\n }\n\n}\n\n')])])]),t("h5",{attrs:{id:"自定义错误页面"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#自定义错误页面"}},[e._v("#")]),e._v(" 自定义错误页面")]),e._v(" "),t("p",[e._v("如果你想为给定的状态代码显示自定义 HTML 错误页,那么可以将文件添加到"),t("code",[e._v("/error")]),e._v("目录。错误页可以是静态 HTML(即添加到任何静态资源目录下),也可以通过使用模板构建。文件的名称应该是确切的状态代码或系列掩码。")]),e._v(" "),t("p",[e._v("例如,要将"),t("code",[e._v("404")]),e._v("映射到静态 HTML 文件,你的目录结构如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("src/\n +- main/\n +- java/\n | + \n +- resources/\n +- public/\n +- error/\n | +- 404.html\n +- \n")])])]),t("p",[e._v("要使用 freemarker 模板映射所有"),t("code",[e._v("5xx")]),e._v("错误,你的目录结构如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("src/\n +- main/\n +- java/\n | + \n +- resources/\n +- templates/\n +- error/\n | +- 5xx.ftlh\n +- \n")])])]),t("p",[e._v("对于更复杂的映射,还可以添加实现"),t("code",[e._v("ErrorViewResolver")]),e._v("接口的 bean,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import java.util.Map;\n\nimport javax.servlet.http.HttpServletRequest;\n\nimport org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.web.servlet.ModelAndView;\n\npublic class MyErrorViewResolver implements ErrorViewResolver {\n\n @Override\n public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map model) {\n // Use the request or status to optionally return a ModelAndView\n if (status == HttpStatus.INSUFFICIENT_STORAGE) {\n // We could add custom model values here\n new ModelAndView("myview");\n }\n return null;\n }\n\n}\n\n')])])]),t("p",[e._v("你也可以使用常规的 Spring MVC 特性,例如["),t("code",[e._v("@ExceptionHandler")]),e._v("方法](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/Reference/html/web.html#MVC-ExceptionHandlers)和["),t("code",[e._v("@ControllerAdvice")]),e._v("](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/reference/html/web.html-controller-mover-advc-advice)。然后,"),t("code",[e._v("ErrorController")]),e._v("将获取任何未处理的异常。")]),e._v(" "),t("h5",{attrs:{id:"映射-spring-mvc-之外的错误页面"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#映射-spring-mvc-之外的错误页面"}},[e._v("#")]),e._v(" 映射 Spring MVC#### 之外的错误页面")]),e._v(" "),t("p",[e._v("对于不使用 Spring MVC 的应用程序,可以使用"),t("code",[e._v("ErrorPageRegistrar")]),e._v("接口直接注册"),t("code",[e._v("ErrorPages")]),e._v("。这个抽象可以直接与底层嵌入的 Servlet 容器一起工作,并且即使你没有 Spring MVC也可以工作。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.boot.web.server.ErrorPage;\nimport org.springframework.boot.web.server.ErrorPageRegistrar;\nimport org.springframework.boot.web.server.ErrorPageRegistry;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.HttpStatus;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyErrorPagesConfiguration {\n\n @Bean\n public ErrorPageRegistrar errorPageRegistrar() {\n return this::registerErrorPages;\n }\n\n private void registerErrorPages(ErrorPageRegistry registry) {\n registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));\n }\n\n}\n\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果你注册了一个"),t("code",[e._v("ErrorPage")]),e._v(",其路径最终由"),t("code",[e._v("Filter")]),e._v("处理(这在一些非 Spring Web 框架中很常见,例如 Jersey 和 Wicket),那么"),t("code",[e._v("Filter")]),e._v("必须显式地注册为"),t("code",[e._v("ERROR")]),e._v("调度器,如以下示例所示:")])])]),e._v(" "),t("tbody")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import java.util.EnumSet;\n\nimport javax.servlet.DispatcherType;\n\nimport org.springframework.boot.web.servlet.FilterRegistrationBean;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyFilterConfiguration {\n\n @Bean\n public FilterRegistrationBean myFilter() {\n FilterRegistrationBean registration = new FilterRegistrationBean<>(new MyFilter());\n // ...\n registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));\n return registration;\n }\n\n}\n\n")])])]),t("p",[e._v("请注意,默认的"),t("code",[e._v("FilterRegistrationBean")]),e._v("不包括"),t("code",[e._v("ERROR")]),e._v("Dispatcher 类型。")]),e._v(" "),t("h5",{attrs:{id:"war-部署中的错误处理"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#war-部署中的错误处理"}},[e._v("#")]),e._v(" WAR 部署中的错误处理")]),e._v(" "),t("p",[e._v("当部署到 Servlet 容器时, Spring 引导使用其错误页过滤器将具有错误状态的请求转发到适当的错误页。这是必要的,因为 Servlet 规范不提供用于注册错误页的 API。根据部署 WAR 文件的容器和应用程序使用的技术,可能需要进行一些额外的配置。")]),e._v(" "),t("p",[e._v("如果响应尚未提交,则错误页筛选器只能将请求转发到正确的错误页。默认情况下,WebSphere Application Server8.0 及以后版本在成功完成 Servlet 的服务方法后提交响应。你应该通过将"),t("code",[e._v("com.ibm.ws.webcontainer.invokeFlushAfterService")]),e._v("设置为"),t("code",[e._v("false")]),e._v("来禁用此行为。")]),e._v(" "),t("p",[e._v("如果你正在使用 Spring 安全,并且希望访问错误页中的主体,则必须配置 Spring Security 的筛选器,以便在错误分派时调用该筛选器。为此,将"),t("code",[e._v("spring.security.filter.dispatcher-types")]),e._v("属性设置为"),t("code",[e._v("async, error, forward, request")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_1-1-11-cors-支持"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-1-11-cors-支持"}},[e._v("#")]),e._v(" 1.1.11.CORS 支持")]),e._v(" "),t("p",[t("a",{attrs:{href:"https://en.wikipedia.org/wiki/Cross-origin_resource_sharing",target:"_blank",rel:"noopener noreferrer"}},[e._v("跨源资源共享"),t("OutboundLink")],1),e._v("是一个"),t("a",{attrs:{href:"https://www.w3.org/TR/cors/",target:"_blank",rel:"noopener noreferrer"}},[e._v("W3C 规范"),t("OutboundLink")],1),e._v("由"),t("a",{attrs:{href:"https://caniuse.com/#feat=cors",target:"_blank",rel:"noopener noreferrer"}},[e._v("大多数浏览器"),t("OutboundLink")],1),e._v("实现的"),t("a",{attrs:{href:"https://www.w3.org/TR/cors/",target:"_blank",rel:"noopener noreferrer"}},[e._v("W3C 规范"),t("OutboundLink")],1),e._v(",它允许你以一种灵活的方式指定授权哪种类型的跨域请求,而不是使用一些安全性较低、功能较弱的方法,例如 IFRAME 或 JSONP。")]),e._v(" "),t("p",[e._v("在版本 4.2 中, Spring MVC"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-cors",target:"_blank",rel:"noopener noreferrer"}},[e._v("支持 CORS"),t("OutboundLink")],1),e._v("。在 Spring 引导应用程序中使用"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-cors-controller",target:"_blank",rel:"noopener noreferrer"}},[e._v("控制器方法 CORS 配置"),t("OutboundLink")],1),e._v("与["),t("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)注释不需要任何特定的配置。"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web.html#mvc-cors-global",target:"_blank",rel:"noopener noreferrer"}},[e._v("全局 CORS 配置"),t("OutboundLink")],1),e._v("可以通过使用定制的"),t("code",[e._v("WebMvcConfigurer")]),e._v("[gt116]注册一个"),t("code",[e._v("addCorsMappings(CorsRegistry)")]),e._v("方法来定义,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.CorsRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyCorsConfiguration {\n\n @Bean\n public WebMvcConfigurer corsConfigurer() {\n return new WebMvcConfigurer() {\n\n @Override\n public void addCorsMappings(CorsRegistry registry) {\n registry.addMapping("/api/**");\n }\n\n };\n }\n\n}\n\n')])])]),t("h3",{attrs:{id:"_1-2-jax-rs-和泽西岛"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-2-jax-rs-和泽西岛"}},[e._v("#")]),e._v(" 1.2.JAX-RS 和泽西岛")]),e._v(" "),t("p",[e._v("如果你更喜欢用于 REST 端点的 JAX-RS 编程模型,那么你可以使用其中一个可用的实现,而不是 Spring MVC。和在开箱即用的情况下工作得很好。CXF 要求你在应用程序上下文中将其"),t("code",[e._v("Servlet")]),e._v("或"),t("code",[e._v("Filter")]),e._v("注册为"),t("code",[e._v("@Bean")]),e._v("。Jersey 有一些本机的 Spring 支持,因此我们还在 Spring 启动中为它提供了自动配置支持,以及一个启动器。")]),e._v(" "),t("p",[e._v("要开始使用 Jersey,将"),t("code",[e._v("spring-boot-starter-jersey")]),e._v("作为依赖项,然后需要一个类型为"),t("code",[e._v("ResourceConfig")]),e._v("的"),t("code",[e._v("@Bean")]),e._v(",在其中注册所有端点,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.glassfish.jersey.server.ResourceConfig;\n\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class MyJerseyConfig extends ResourceConfig {\n\n public MyJerseyConfig() {\n register(MyEndpoint.class);\n }\n\n}\n\n")])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("Jersey 对扫描可执行文档的支持相当有限,例如,当运行可执行 WAR 文件时,它不能扫描在"),t("RouterLink",{attrs:{to:"/spring-boot/deployment.html#deployment.installing"}},[e._v("fully executable jar file")]),e._v("或"),t("code",[e._v("WEB-INF/classes")]),e._v("中找到的包中的端点。"),t("br"),e._v("为了避免这种限制,不应使用"),t("code",[e._v("packages")]),e._v("方法,端点应该使用"),t("code",[e._v("register")]),e._v("方法单独注册,如前面的示例所示。")],1)])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("对于更高级的定制,你还可以注册任意数量的 bean 来实现"),t("code",[e._v("ResourceConfigCustomizer")]),e._v("。")]),e._v(" "),t("p",[e._v("所有注册的端点都应该是"),t("code",[e._v("@Components")]),e._v(",带有 HTTP 资源注释("),t("code",[e._v("@GET")]),e._v("等),如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import javax.ws.rs.GET;\nimport javax.ws.rs.Path;\n\nimport org.springframework.stereotype.Component;\n\n@Component\n@Path("/hello")\npublic class MyEndpoint {\n\n @GET\n public String message() {\n return "Hello";\n }\n\n}\n\n')])])]),t("p",[e._v("由于"),t("code",[e._v("Endpoint")]),e._v("是一个 Spring "),t("code",[e._v("@Component")]),e._v(",其生命周期由 Spring 管理,你可以使用"),t("code",[e._v("@Autowired")]),e._v("注释来注入依赖项,并使用"),t("code",[e._v("@Value")]),e._v("注释来注入外部配置。默认情况下,泽西 Servlet 被注册并映射到"),t("code",[e._v("/*")]),e._v("。可以通过将"),t("code",[e._v("@ApplicationPath")]),e._v("添加到"),t("code",[e._v("ResourceConfig")]),e._v("来更改映射。")]),e._v(" "),t("p",[e._v("默认情况下,在"),t("code",[e._v("ServletRegistrationBean")]),e._v("类型的"),t("code",[e._v("@Bean")]),e._v("中将泽西设置为 Servlet,名称为"),t("code",[e._v("jerseyServletRegistration")]),e._v("。默认情况下, Servlet 是惰性初始化的,但是你可以通过设置"),t("code",[e._v("spring.jersey.servlet.load-on-startup")]),e._v("来定制这种行为。你可以通过创建自己的同名应用程序来禁用或重写该 Bean。还可以通过设置"),t("code",[e._v("spring.jersey.type=filter")]),e._v("(在这种情况下,要替换或覆盖的"),t("code",[e._v("@Bean")]),e._v("是"),t("code",[e._v("jerseyFilterRegistration")]),e._v(")来使用过滤器而不是 Servlet。过滤器有一个"),t("code",[e._v("@Order")]),e._v(",你可以用"),t("code",[e._v("spring.jersey.filter.order")]),e._v("设置它。当使用 Jersey 作为过滤器时,必须有一个 Servlet 来处理未被 Jersey 截获的任何请求。如果你的应用程序不包含这样的 Servlet,那么你可能希望通过将"),t("code",[e._v("server.servlet.register-default-servlet")]),e._v("设置为"),t("code",[e._v("true")]),e._v("来启用缺省 Servlet。 Servlet 和过滤器注册都可以通过使用"),t("code",[e._v("spring.jersey.init.*")]),e._v("来指定属性的映射来给定 init 参数。")]),e._v(" "),t("h3",{attrs:{id:"_1-3-嵌入式-servlet-容器支持"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-嵌入式-servlet-容器支持"}},[e._v("#")]),e._v(" 1.3.嵌入式 Servlet 容器支持")]),e._v(" "),t("p",[e._v("对于 Servlet 应用程序, Spring 引导包括对嵌入式"),t("a",{attrs:{href:"https://tomcat.apache.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Tomcat"),t("OutboundLink")],1),e._v("、"),t("a",{attrs:{href:"https://www.eclipse.org/jetty/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Jetty"),t("OutboundLink")],1),e._v("和"),t("a",{attrs:{href:"https://github.com/undertow-io/undertow",target:"_blank",rel:"noopener noreferrer"}},[e._v("Undertow"),t("OutboundLink")],1),e._v("服务器的支持。大多数开发人员使用适当的“启动器”来获得完全配置的实例。默认情况下,嵌入式服务器侦听端口"),t("code",[e._v("8080")]),e._v("上的 HTTP 请求。")]),e._v(" "),t("h4",{attrs:{id:"_1-3-1-servlet、过滤器和侦听器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-1-servlet、过滤器和侦听器"}},[e._v("#")]),e._v(" 1.3.1.servlet、过滤器和侦听器")]),e._v(" "),t("p",[e._v("当使用嵌入式 Servlet 容器时,可以通过使用 Spring bean 或通过扫描 Servlet 组件来注册来自 Servlet 规范的 servlet、过滤器和所有侦听器(例如"),t("code",[e._v("HttpSessionListener")]),e._v(")。")]),e._v(" "),t("h5",{attrs:{id:"将-servlet、过滤器和侦听器注册为-spring-bean"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#将-servlet、过滤器和侦听器注册为-spring-bean"}},[e._v("#")]),e._v(" 将 servlet、过滤器和侦听器注册为 Spring bean#####")]),e._v(" "),t("p",[e._v("任何"),t("code",[e._v("Servlet")]),e._v(","),t("code",[e._v("Filter")]),e._v(",或 Servlet "),t("code",[e._v("*Listener")]),e._v("实例中的 Spring Bean 是在嵌入式容器中注册的。如果你希望在配置过程中引用来自"),t("code",[e._v("application.properties")]),e._v("的值,那么这将非常方便。")]),e._v(" "),t("p",[e._v("默认情况下,如果上下文只包含一个 Servlet,则将其映射到"),t("code",[e._v("/")]),e._v("。在多个 Servlet bean 的情况下, Bean 名称被用作路径前缀。过滤器映射到"),t("code",[e._v("/*")]),e._v("。")]),e._v(" "),t("p",[e._v("如果基于约定的映射不够灵活,则可以使用"),t("code",[e._v("ServletRegistrationBean")]),e._v("、"),t("code",[e._v("FilterRegistrationBean")]),e._v("和"),t("code",[e._v("ServletListenerRegistrationBean")]),e._v("类来完成控制。")]),e._v(" "),t("p",[e._v("通常情况下,保持过滤豆的无序状态是安全的。如果需要特定的顺序,你应该用"),t("code",[e._v("@Order")]),e._v("注释"),t("code",[e._v("Filter")]),e._v(",或者使其实现"),t("code",[e._v("Ordered")]),e._v("。不能通过用"),t("code",[e._v("@Order")]),e._v("注释其 Bean 方法来配置"),t("code",[e._v("Filter")]),e._v("的顺序。如果不能将"),t("code",[e._v("Filter")]),e._v("类更改为添加"),t("code",[e._v("@Order")]),e._v("或实现"),t("code",[e._v("Ordered")]),e._v(",则必须为"),t("code",[e._v("Filter")]),e._v("定义一个"),t("code",[e._v("FilterRegistrationBean")]),e._v(",并使用"),t("code",[e._v("setOrder(int)")]),e._v("方法设置注册 Bean 的顺序。避免配置读取请求主体"),t("code",[e._v("Ordered.HIGHEST_PRECEDENCE")]),e._v("的筛选器,因为它可能与应用程序的字符编码配置不一致。如果 Servlet 过滤器包装了该请求,则应将其配置为小于或等于"),t("code",[e._v("OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER")]),e._v("的顺序。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("要查看应用程序中每个"),t("code",[e._v("Filter")]),e._v("的顺序,请启用"),t("code",[e._v("web")]),t("RouterLink",{attrs:{to:"/spring-boot/features.html#features.logging.log-groups"}},[e._v("伐木组")]),e._v("("),t("code",[e._v("logging.level.web=debug")]),e._v(")的调试级别日志。"),t("br"),e._v("将在启动时记录已注册过滤器的详细信息,包括它们的顺序和 URL 模式。")],1)])]),e._v(" "),t("tbody")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("注册"),t("code",[e._v("Filter")]),e._v("bean 时要小心,因为它们在应用程序生命周期的很早就被初始化了。"),t("br"),e._v("如果需要注册一个与其他 bean 交互的"),t("code",[e._v("Filter")]),e._v(",请考虑使用一个["),t("code",[e._v("DelegatingFilterProxyRegistrationBean")]),e._v("](https://DOCS. Spring.io/ Spring-boot/DOCS/2.6.4/api/org/springframework/boot/web/ Servlet/delegatingfilterbean.html)来代替。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-3-2-servlet-上下文初始化"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-2-servlet-上下文初始化"}},[e._v("#")]),e._v(" 1.3.2. Servlet 上下文初始化")]),e._v(" "),t("p",[e._v("嵌入式 Servlet 容器不直接执行 Servlet 3.0+"),t("code",[e._v("javax.servlet.ServletContainerInitializer")]),e._v("接口或 Spring 的"),t("code",[e._v("org.springframework.web.WebApplicationInitializer")]),e._v("接口。这是一种有意的设计决策,旨在降低设计用于在 WAR 内部运行的第三方库可能会破坏 Spring 引导应用程序的风险。")]),e._v(" "),t("p",[e._v("如果需要在 Spring 引导应用程序中执行 Servlet 上下文初始化,则应该注册一个实现"),t("code",[e._v("org.springframework.boot.web.servlet.ServletContextInitializer")]),e._v("接口的 Bean。单一的"),t("code",[e._v("onStartup")]),e._v("方法提供了对"),t("code",[e._v("ServletContext")]),e._v("的访问,并且如果需要,可以很容易地用作现有"),t("code",[e._v("WebApplicationInitializer")]),e._v("的适配器。")]),e._v(" "),t("h5",{attrs:{id:"扫描-servlet、过滤器和侦听器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#扫描-servlet、过滤器和侦听器"}},[e._v("#")]),e._v(" 扫描 servlet、过滤器和侦听器")]),e._v(" "),t("p",[e._v("当使用嵌入式容器时,使用"),t("code",[e._v("@WebServlet")]),e._v("、"),t("code",[e._v("@WebFilter")]),e._v("和"),t("code",[e._v("@WebListener")]),e._v("注释的类的自动注册可以通过使用"),t("code",[e._v("@ServletComponentScan")]),e._v("来启用。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[t("code",[e._v("@ServletComponentScan")]),e._v("在独立容器中没有任何作用,相反,在独立容器中使用的是容器的内置发现机制。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-3-3-servletwebserverapplicationcontext"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-3-servletwebserverapplicationcontext"}},[e._v("#")]),e._v(" 1.3.3.ServletWebServerApplicationContext")]),e._v(" "),t("p",[e._v("在引擎盖下, Spring 引导使用不同类型的"),t("code",[e._v("ApplicationContext")]),e._v("用于嵌入式 Servlet 容器支持。"),t("code",[e._v("ServletWebServerApplicationContext")]),e._v("是"),t("code",[e._v("WebApplicationContext")]),e._v("的一种特殊类型,它通过搜索单个"),t("code",[e._v("ServletWebServerFactory")]),e._v(" Bean 来引导自身。通常"),t("code",[e._v("TomcatServletWebServerFactory")]),e._v("、"),t("code",[e._v("JettyServletWebServerFactory")]),e._v("或"),t("code",[e._v("UndertowServletWebServerFactory")]),e._v("已被自动配置。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("你通常不需要了解这些实现类。"),t("br"),e._v("大多数应用程序都是自动配置的,并且适当的"),t("code",[e._v("ApplicationContext")]),e._v("和"),t("code",[e._v("ServletWebServerFactory")]),e._v("是代表你创建的。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-3-4-自定义嵌入式-servlet-容器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-4-自定义嵌入式-servlet-容器"}},[e._v("#")]),e._v(" 1.3.4.自定义嵌入式 Servlet 容器")]),e._v(" "),t("p",[e._v("可通过使用 Spring "),t("code",[e._v("Environment")]),e._v("属性来配置公共 Servlet 容器设置。通常,你会在"),t("code",[e._v("application.properties")]),e._v("或"),t("code",[e._v("application.yaml")]),e._v("文件中定义属性。")]),e._v(" "),t("p",[e._v("常见的服务器设置包括:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("网络设置:用于接收 HTTP 请求("),t("code",[e._v("server.port")]),e._v(")的侦听端口,绑定到"),t("code",[e._v("server.address")]),e._v("的接口地址,以此类推。")])]),e._v(" "),t("li",[t("p",[e._v("会话设置:会话是否持久("),t("code",[e._v("server.servlet.session.persistent")]),e._v(")、会话超时("),t("code",[e._v("server.servlet.session.timeout")]),e._v(")、会话数据的位置("),t("code",[e._v("server.servlet.session.store-dir")]),e._v(")和会话-cookie 配置("),t("code",[e._v("server.servlet.session.cookie.*")]),e._v(")。")])]),e._v(" "),t("li",[t("p",[e._v("错误管理:错误页面的位置("),t("code",[e._v("server.error.path")]),e._v(")等等。")])]),e._v(" "),t("li",[t("p",[t("RouterLink",{attrs:{to:"/spring-boot/howto.html#howto.webserver.configure-ssl"}},[e._v("SSL")])],1)]),e._v(" "),t("li",[t("p",[t("RouterLink",{attrs:{to:"/spring-boot/howto.html#howto.webserver.enable-response-compression"}},[e._v("HTTP 压缩")])],1)])]),e._v(" "),t("p",[e._v("Spring 引导尝试尽可能多地公开公共设置,但这并不总是可能的。对于这些情况,专用的名称空间提供了特定于服务器的定制(参见"),t("code",[e._v("server.tomcat")]),e._v("和"),t("code",[e._v("server.undertow")]),e._v(")。例如,"),t("RouterLink",{attrs:{to:"/spring-boot/howto.html#howto.webserver.configure-access-logs"}},[e._v("访问日志")]),e._v("可以配置具有嵌入式 Servlet 容器的特定特征。")],1),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("参见["),t("code",[e._v("Server属性")]),e._v("](https://github.com/ Spring-projects/ Spring-boot/tree/v2.6.4/ Spring-boot-project/ Spring-boot-autofigure/SRC/main/java/org/springframework/boot/autofigure/web/serverproperties.java)类的完整列表。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h5",{attrs:{id:"samesite-饼干"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#samesite-饼干"}},[e._v("#")]),e._v(" Samesite 饼干")]),e._v(" "),t("p",[e._v("Web 浏览器可以使用"),t("code",[e._v("SameSite")]),e._v("cookie 属性来控制跨站点请求中是否以及如何提交 cookie。该属性对于现代 Web 浏览器特别相关,因为它们已经开始更改缺省属性时使用的默认值。")]),e._v(" "),t("p",[e._v("如果要更改会话 cookie 的"),t("code",[e._v("SameSite")]),e._v("属性,可以使用"),t("code",[e._v("server.servlet.session.cookie.same-site")]),e._v("属性。自动配置的 Tomcat、 Jetty 和 Undertow 服务器支持此属性。它还用于配置基于"),t("code",[e._v("SessionRepository")]),e._v("bean 的 Spring 会话 Servlet。")]),e._v(" "),t("p",[e._v("例如,如果你希望你的会话 cookie 具有"),t("code",[e._v("SameSite")]),e._v("的"),t("code",[e._v("None")]),e._v("属性,则可以将以下内容添加到你的"),t("code",[e._v("application.properties")]),e._v("或"),t("code",[e._v("application.yaml")]),e._v("文件中:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("server.servlet.session.cookie.same-site=none\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('server:\n servlet:\n session:\n cookie:\n same-site: "none"\n')])])]),t("p",[e._v("如果要更改添加到"),t("code",[e._v("HttpServletResponse")]),e._v("中的其他 cookie 上的"),t("code",[e._v("SameSite")]),e._v("属性,可以使用"),t("code",[e._v("CookieSameSiteSupplier")]),e._v("。"),t("code",[e._v("CookieSameSiteSupplier")]),e._v("传递了一个"),t("code",[e._v("Cookie")]),e._v(",并可能返回一个"),t("code",[e._v("SameSite")]),e._v("值,或"),t("code",[e._v("null")]),e._v("。")]),e._v(" "),t("p",[e._v("有许多方便的工厂和过滤方法,你可以使用它们来快速匹配特定的 Cookie。例如,添加以下 Bean 将自动为所有名称与正则表达式"),t("code",[e._v("myapp.*")]),e._v("匹配的 cookie 应用"),t("code",[e._v("SameSite")]),e._v("的"),t("code",[e._v("SameSite")]),e._v("。")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\n\n@Configuration(proxyBeanMethods = false)\npublic class MySameSiteConfiguration {\n\n @Bean\n public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {\n return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*");\n }\n\n}\n\n')])])]),t("h5",{attrs:{id:"程序化定制"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#程序化定制"}},[e._v("#")]),e._v(" 程序化定制")]),e._v(" "),t("p",[e._v("如果需要以编程方式配置你的嵌入式 Servlet 容器,则可以注册一个实现"),t("code",[e._v("WebServerFactoryCustomizer")]),e._v("接口的 Spring Bean。"),t("code",[e._v("WebServerFactoryCustomizer")]),e._v("提供对"),t("code",[e._v("ConfigurableServletWebServerFactory")]),e._v("的访问,其中包括许多自定义 setter 方法。下面的示例以编程方式显示了端口的设置:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.springframework.boot.web.server.WebServerFactoryCustomizer;\nimport org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer {\n\n @Override\n public void customize(ConfigurableServletWebServerFactory server) {\n server.setPort(9000);\n }\n\n}\n\n")])])]),t("p",[t("code",[e._v("TomcatServletWebServerFactory")]),e._v("、"),t("code",[e._v("JettyServletWebServerFactory")]),e._v("和"),t("code",[e._v("UndertowServletWebServerFactory")]),e._v("是"),t("code",[e._v("ConfigurableServletWebServerFactory")]),e._v("的专用变体,它们分别具有用于 Tomcat、 Jetty 和 Undertow 的附加自定义 setter 方法。下面的示例展示了如何定制"),t("code",[e._v("TomcatServletWebServerFactory")]),e._v(",从而提供对 Tomcat 特定配置选项的访问:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import java.time.Duration;\n\nimport org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;\nimport org.springframework.boot.web.server.WebServerFactoryCustomizer;\nimport org.springframework.stereotype.Component;\n\n@Component\npublic class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer {\n\n @Override\n public void customize(TomcatServletWebServerFactory server) {\n server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));\n }\n\n}\n\n")])])]),t("h5",{attrs:{id:"直接自定义配置-webserverfactory"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#直接自定义配置-webserverfactory"}},[e._v("#")]),e._v(" 直接自定义配置 WebServerFactory#####")]),e._v(" "),t("p",[e._v("对于需要从"),t("code",[e._v("ServletWebServerFactory")]),e._v("扩展的更高级的用例,你可以自己公开这种类型的 Bean。")]),e._v(" "),t("p",[e._v("为许多配置选项提供了设置器。如果你需要做一些更奇特的事情,还提供了几种受保护的方法“挂钩”。有关详细信息,请参见"),t("a",{attrs:{href:"https://docs.spring.io/spring-boot/docs/2.6.4/api/org/springframework/boot/web/servlet/server/ConfigurableServletWebServerFactory.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("源代码文档"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("自动配置的 Customizer 仍然应用于你的自定义工厂,因此请小心使用该选项。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_1-3-5-jsp-限制"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_1-3-5-jsp-限制"}},[e._v("#")]),e._v(" 1.3.5.JSP 限制")]),e._v(" "),t("p",[e._v("当运行使用嵌入式 Servlet 容器(并打包为可执行归档文件)的 Spring 引导应用程序时,JSP 支持中存在一些限制。")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("对于 Jetty 和 Tomcat,如果使用战争包装,它应该可以工作。当使用"),t("code",[e._v("java -jar")]),e._v("启动时,可执行 WAR 将工作,并且还可以部署到任何标准容器。在使用可执行文件 jar 时,不支持 JSP。")])]),e._v(" "),t("li",[t("p",[e._v("Undertow 不支持 JSP。")])]),e._v(" "),t("li",[t("p",[e._v("创建自定义"),t("code",[e._v("error.jsp")]),e._v("页面不会覆盖"),t("a",{attrs:{href:"#web.servlet.spring-mvc.error-handling"}},[e._v("错误处理")]),e._v("的默认视图。应该使用"),t("a",{attrs:{href:"#web.servlet.spring-mvc.error-handling.error-pages"}},[e._v("自定义错误页面")]),e._v("。")])])]),e._v(" "),t("h2",{attrs:{id:"_2-反应式-web-应用程序"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-反应式-web-应用程序"}},[e._v("#")]),e._v(" 2. 反应式 Web 应用程序")]),e._v(" "),t("p",[e._v("Spring 启动通过为 Spring WebFlux 提供自动配置简化了反应性 Web 应用程序的开发。")]),e._v(" "),t("h3",{attrs:{id:"_2-1-spring-webflux-框架"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-spring-webflux-框架"}},[e._v("#")]),e._v(" 2.1.“ Spring WebFlux 框架”")]),e._v(" "),t("p",[e._v("Spring WebFlux 是在 Spring Framework5.0 中引入的新的反应性 Web 框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步和非阻塞的,并且通过"),t("a",{attrs:{href:"https://projectreactor.io/",target:"_blank",rel:"noopener noreferrer"}},[e._v("反应堆项目"),t("OutboundLink")],1),e._v("实现了"),t("a",{attrs:{href:"https://www.reactive-streams.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("反应流"),t("OutboundLink")],1),e._v("规范。")]),e._v(" "),t("p",[e._v("Spring WebFlux 有两种风格:基于功能的和基于注释的。基于注释的模型非常接近 Spring MVC 模型,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import reactor.core.publisher.Flux;\nimport reactor.core.publisher.Mono;\n\nimport org.springframework.web.bind.annotation.DeleteMapping;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.PathVariable;\nimport org.springframework.web.bind.annotation.RequestMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\n@RequestMapping("/users")\npublic class MyRestController {\n\n private final UserRepository userRepository;\n\n private final CustomerRepository customerRepository;\n\n public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {\n this.userRepository = userRepository;\n this.customerRepository = customerRepository;\n }\n\n @GetMapping("/{user}")\n public Mono getUser(@PathVariable Long userId) {\n return this.userRepository.findById(userId);\n }\n\n @GetMapping("/{user}/customers")\n public Flux getUserCustomers(@PathVariable Long userId) {\n return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser);\n }\n\n @DeleteMapping("/{user}")\n public void deleteUser(@PathVariable Long userId) {\n this.userRepository.deleteById(userId);\n }\n\n}\n\n')])])]),t("p",[e._v("“WebFlux.FN”是一种功能变体,它将路由配置与请求的实际处理分离开来,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static org.springframework.web.reactive.function.server.RouterFunctions.route;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyRoutingConfiguration {\n\n private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);\n\n @Bean\n public RouterFunction monoRouterFunction(MyUserHandler userHandler) {\n return route()\n .GET("/{user}", ACCEPT_JSON, userHandler::getUser)\n .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)\n .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)\n .build();\n }\n\n}\n\n')])])]),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import reactor.core.publisher.Mono;\n\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@Component\npublic class MyUserHandler {\n\n public Mono getUser(ServerRequest request) {\n ...\n }\n\n public Mono getUserCustomers(ServerRequest request) {\n ...\n }\n\n public Mono deleteUser(ServerRequest request) {\n ...\n }\n\n}\n\n")])])]),t("p",[e._v("WebFlux 是 Spring 框架的一部分,其"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web-reactive.html#webflux-fn",target:"_blank",rel:"noopener noreferrer"}},[e._v("参考文献"),t("OutboundLink")],1),e._v("中提供了详细信息。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("你可以定义任意多的"),t("code",[e._v("RouterFunction")]),e._v("bean 来模块化路由器的定义。如果需要应用优先级,可以订购"),t("br"),e._v("bean。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("要开始,将"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("模块添加到应用程序中。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("在应用程序中同时添加"),t("code",[e._v("spring-boot-starter-web")]),e._v("和"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("模块会导致 Spring 引导自动配置 Spring MVC,不是 WebFlux."),t("br"),e._v("之所以选择这种行为,是因为许多 Spring 开发人员将"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("添加到他们的 Spring MVC 应用程序中,以使用反应式"),t("code",[e._v("WebClient")]),e._v("。"),t("br"),e._v("你仍然可以通过将选择的应用程序类型设置为"),t("code",[e._v("SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)")]),e._v("来强制执行你的选择。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("“WebFlux.FN”是一种功能变体,它将路由配置与请求的实际处理分离开来,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.MediaType;\nimport org.springframework.web.reactive.function.server.RequestPredicate;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\nimport static org.springframework.web.reactive.function.server.RequestPredicates.accept;\nimport static org.springframework.web.reactive.function.server.RouterFunctions.route;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyRoutingConfiguration {\n\n private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);\n\n @Bean\n public RouterFunction monoRouterFunction(MyUserHandler userHandler) {\n return route()\n .GET("/{user}", ACCEPT_JSON, userHandler::getUser)\n .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)\n .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)\n .build();\n }\n\n}\n\n')])])]),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import reactor.core.publisher.Mono;\n\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\n\n@Component\npublic class MyUserHandler {\n\n public Mono getUser(ServerRequest request) {\n ...\n }\n\n public Mono getUserCustomers(ServerRequest request) {\n ...\n }\n\n public Mono deleteUser(ServerRequest request) {\n ...\n }\n\n}\n\n")])])]),t("p",[e._v("WebFlux 是 Spring 框架的一部分,其"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web-reactive.html#webflux-fn",target:"_blank",rel:"noopener noreferrer"}},[e._v("参考文献"),t("OutboundLink")],1),e._v("中提供了详细信息。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("你可以定义任意多的"),t("code",[e._v("RouterFunction")]),e._v("bean 来模块化路由器的定义。如果需要应用优先级,可以订购"),t("br"),e._v("bean。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("要开始,将"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("模块添加到应用程序中。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("在应用程序中同时添加"),t("code",[e._v("spring-boot-starter-web")]),e._v("和"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("模块将导致 Spring 引导自动配置 Spring MVC,不是 WebFlux."),t("br"),e._v("之所以选择这种行为,是因为许多 Spring 开发人员将"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("添加到他们的 Spring MVC 应用程序中,以使用反应式"),t("code",[e._v("WebClient")]),e._v("。"),t("br"),e._v("你仍然可以通过将选择的应用程序类型设置为"),t("code",[e._v("SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)")]),e._v("来强制执行你的选择。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_2-1-1-spring-webflux-自动配置"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-1-spring-webflux-自动配置"}},[e._v("#")]),e._v(" 2.1.1. Spring WebFlux 自动配置")]),e._v(" "),t("p",[e._v("Spring 启动为 Spring WebFlux 提供了自动配置,该配置在大多数应用程序中都能很好地工作。")]),e._v(" "),t("p",[e._v("自动配置在 Spring 的默认值之上添加了以下功能:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("为"),t("code",[e._v("HttpMessageReader")]),e._v("和"),t("code",[e._v("HttpMessageWriter")]),e._v("实例配置编解码器(描述"),t("a",{attrs:{href:"#web.reactive.webflux.httpcodecs"}},[e._v("在本文的后面部分")]),e._v(")。")])]),e._v(" "),t("li",[t("p",[e._v("对服务静态资源的支持,包括对 WebJAR 的支持(描述"),t("a",{attrs:{href:"#web.servlet.spring-mvc.static-content"}},[e._v("在本文的后面部分")]),e._v(")。")])])]),e._v(" "),t("p",[e._v("如果你希望保留 Spring boot WebFlux 特性并且希望添加额外的"),t("a",{attrs:{href:"https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/web-reactive.html#webflux-config",target:"_blank",rel:"noopener noreferrer"}},[e._v("WebFlux 配置"),t("OutboundLink")],1),e._v(",则可以添加你自己的"),t("code",[e._v("@Configuration")]),e._v("类型的类别"),t("code",[e._v("WebFluxConfigurer")]),e._v("但是"),t("strong",[e._v("没有")]),t("code",[e._v("@EnableWebFlux")]),e._v("。")]),e._v(" "),t("p",[e._v("如果你想完全控制 Spring WebFlux,那么可以添加你自己的"),t("code",[e._v("@Configuration")]),e._v(",并使用"),t("code",[e._v("@EnableWebFlux")]),e._v("进行注释。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-2-带有-httpmessagereaders-和-httpmessagewriters-的-http-编解码器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-2-带有-httpmessagereaders-和-httpmessagewriters-的-http-编解码器"}},[e._v("#")]),e._v(" 2.1.2.带有 HttpMessageReaders 和 HttpMessageWriters 的 HTTP 编解码器")]),e._v(" "),t("p",[e._v("Spring WebFlux 使用"),t("code",[e._v("HttpMessageReader")]),e._v("和"),t("code",[e._v("HttpMessageWriter")]),e._v("接口来转换 HTTP 请求和响应。通过查看 Classpath 中可用的库,将"),t("code",[e._v("CodecConfigurer")]),e._v("配置为具有合理的默认值。")]),e._v(" "),t("p",[e._v("Spring Boot 为编解码器提供了专用的配置属性,"),t("code",[e._v("spring.codec.*")]),e._v("。它还通过使用"),t("code",[e._v("CodecCustomizer")]),e._v("实例应用进一步的定制。例如,"),t("code",[e._v("spring.jackson.*")]),e._v("配置键被应用于 Jackson 编解码器。")]),e._v(" "),t("p",[e._v("如果需要添加或自定义编解码器,可以创建自定义"),t("code",[e._v("CodecCustomizer")]),e._v("组件,如下例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import org.springframework.boot.web.codec.CodecCustomizer;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.http.codec.ServerSentEventHttpMessageReader;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyCodecsConfiguration {\n\n @Bean\n public CodecCustomizer myCodecCustomizer() {\n return (configurer) -> {\n configurer.registerDefaults(false);\n configurer.customCodecs().register(new ServerSentEventHttpMessageReader());\n // ...\n };\n }\n\n}\n\n")])])]),t("p",[e._v("你还可以利用"),t("a",{attrs:{href:"#web.servlet.spring-mvc.json"}},[e._v("Boot 的自定义 JSON 序列化器和反序列化器")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-3-静态内容"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-3-静态内容"}},[e._v("#")]),e._v(" 2.1.3.静态内容")]),e._v(" "),t("p",[e._v("默认情况下, Spring 引导从 Classpath 中的一个名为"),t("code",[e._v("/static")]),e._v("(或"),t("code",[e._v("/public")]),e._v("或"),t("code",[e._v("/resources")]),e._v("或"),t("code",[e._v("/META-INF/resources")]),e._v(")的目录中提供静态内容。它使用 Spring WebFlux 中的"),t("code",[e._v("ResourceWebHandler")]),e._v(",以便你可以通过添加自己的"),t("code",[e._v("WebFluxConfigurer")]),e._v("并重写"),t("code",[e._v("addResourceHandlers")]),e._v("方法来修改该行为。")]),e._v(" "),t("p",[e._v("默认情况下,资源映射在"),t("code",[e._v("/**")]),e._v("上,但是你可以通过设置"),t("code",[e._v("spring.webflux.static-path-pattern")]),e._v("属性对其进行调优。例如,将所有资源重新定位到"),t("code",[e._v("/resources/**")]),e._v("可以通过以下方式实现:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.webflux.static-path-pattern=/resources/**\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n webflux:\n static-path-pattern: "/resources/**"\n')])])]),t("p",[e._v("你还可以使用"),t("code",[e._v("spring.web.resources.static-locations")]),e._v("自定义静态资源位置。这样做可以用目录位置列表替换默认值。如果你这样做,默认的欢迎页面检测将切换到你的自定义位置。因此,如果在启动时的任何位置都有"),t("code",[e._v("index.html")]),e._v(",那么它就是应用程序的主页。")]),e._v(" "),t("p",[e._v("除了前面列出的“标准”静态资源位置之外,"),t("a",{attrs:{href:"https://www.webjars.org/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Webjars 内容"),t("OutboundLink")],1),e._v("还有一个特殊情况。路径在"),t("code",[e._v("/webjars/**")]),e._v("中的任何资源,如果以 WebJARS 格式打包,都将从 jar 文件中得到服务。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("Spring WebFlux 应用程序不严格依赖于 Servlet API,因此它们不能作为 WAR 文件部署,也不使用"),t("code",[e._v("src/main/webapp")]),e._v("目录。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h4",{attrs:{id:"_2-1-4-欢迎页面"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-4-欢迎页面"}},[e._v("#")]),e._v(" 2.1.4.欢迎页面")]),e._v(" "),t("p",[e._v("Spring 启动支持静态和模板化欢迎页面。它首先在配置的静态内容位置中查找"),t("code",[e._v("index.html")]),e._v("文件。如果没有找到一个,那么它将查找"),t("code",[e._v("index")]),e._v("模板。如果找到其中之一,它将自动用作应用程序的欢迎页面。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-5-模板引擎"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-5-模板引擎"}},[e._v("#")]),e._v(" 2.1.5.模板引擎")]),e._v(" "),t("p",[e._v("与 REST Web 服务一样,你也可以使用 Spring WebFlux 来服务动态 HTML 内容。 Spring WebFlux 支持各种模板化技术,包括胸腺叶、自由标记和胡子。")]),e._v(" "),t("p",[e._v("Spring 启动包括对以下模板引擎的自动配置支持:")]),e._v(" "),t("ul",[t("li",[t("p",[t("a",{attrs:{href:"https://freemarker.apache.org/docs/",target:"_blank",rel:"noopener noreferrer"}},[e._v("FreeMarker"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://www.thymeleaf.org",target:"_blank",rel:"noopener noreferrer"}},[e._v("Thymeleaf"),t("OutboundLink")],1)])]),e._v(" "),t("li",[t("p",[t("a",{attrs:{href:"https://mustache.github.io/",target:"_blank",rel:"noopener noreferrer"}},[e._v("Mustache"),t("OutboundLink")],1)])])]),e._v(" "),t("p",[e._v("当你使用这些模板引擎中的一个具有默认配置时,你的模板将从"),t("code",[e._v("src/main/resources/templates")]),e._v("中自动拾取。")]),e._v(" "),t("h4",{attrs:{id:"_2-1-6-错误处理"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-6-错误处理"}},[e._v("#")]),e._v(" 2.1.6.错误处理")]),e._v(" "),t("p",[e._v("Spring Boot 提供了一个"),t("code",[e._v("WebExceptionHandler")]),e._v(",它以合理的方式处理所有错误。它在处理顺序中的位置紧随 WebFlux 提供的处理程序之前,后者被认为是最后一个处理程序。对于机器客户机,它生成一个 JSON 响应,其中包含错误、HTTP 状态和异常消息的详细信息。对于浏览器客户端,有一个“WhiteLabel”错误处理程序,它以 HTML 格式呈现相同的数据。你也可以提供自己的 HTML 模板来显示错误(参见"),t("a",{attrs:{href:"#web.reactive.webflux.error-handling.error-pages"}},[e._v("下一节")]),e._v(")。")]),e._v(" "),t("p",[e._v("定制此功能的第一步通常涉及使用现有机制,但要替换或增加错误内容。为此,你可以添加类型"),t("code",[e._v("ErrorAttributes")]),e._v("的 Bean。")]),e._v(" "),t("p",[e._v("要更改错误处理行为,可以实现"),t("code",[e._v("ErrorWebExceptionHandler")]),e._v("并注册该类型的 Bean 定义。因为"),t("code",[e._v("ErrorWebExceptionHandler")]),e._v("是相当低级的, Spring boot 还提供了一个方便的"),t("code",[e._v("AbstractErrorWebExceptionHandler")]),e._v(",让你以 WebFlux 功能方式处理错误,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("import reactor.core.publisher.Mono;\n\nimport org.springframework.boot.autoconfigure.web.WebProperties.Resources;\nimport org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;\nimport org.springframework.boot.web.reactive.error.ErrorAttributes;\nimport org.springframework.context.ApplicationContext;\nimport org.springframework.http.HttpStatus;\nimport org.springframework.http.MediaType;\nimport org.springframework.stereotype.Component;\nimport org.springframework.web.reactive.function.server.RouterFunction;\nimport org.springframework.web.reactive.function.server.RouterFunctions;\nimport org.springframework.web.reactive.function.server.ServerRequest;\nimport org.springframework.web.reactive.function.server.ServerResponse;\nimport org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;\n\n@Component\npublic class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {\n\n public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources,\n ApplicationContext applicationContext) {\n super(errorAttributes, resources, applicationContext);\n }\n\n @Override\n protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) {\n return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml);\n }\n\n private boolean acceptsXml(ServerRequest request) {\n return request.headers().accept().contains(MediaType.APPLICATION_XML);\n }\n\n public Mono handleErrorAsXml(ServerRequest request) {\n BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);\n // ... additional builder calls\n return builder.build();\n }\n\n}\n\n")])])]),t("p",[e._v("为了获得更完整的图片,你还可以直接重写子类"),t("code",[e._v("DefaultErrorWebExceptionHandler")]),e._v("并覆盖特定的方法。")]),e._v(" "),t("p",[e._v("在某些情况下,在控制器或处理程序函数级别处理的错误不会被"),t("RouterLink",{attrs:{to:"/spring-boot/actuator.html#actuator.metrics.supported.spring-webflux"}},[e._v("度量基础设施")]),e._v("记录。通过将已处理的异常设置为请求属性,应用程序可以确保将此类异常与请求度量一起记录:")],1),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.boot.web.reactive.error.ErrorAttributes;\nimport org.springframework.stereotype.Controller;\nimport org.springframework.web.bind.annotation.ExceptionHandler;\nimport org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.reactive.result.view.Rendering;\nimport org.springframework.web.server.ServerWebExchange;\n\n@Controller\npublic class MyExceptionHandlingController {\n\n @GetMapping("/profile")\n public Rendering userProfile() {\n // ...\n throw new IllegalStateException();\n }\n\n @ExceptionHandler(IllegalStateException.class)\n public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) {\n exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);\n return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build();\n }\n\n}\n\n')])])]),t("h5",{attrs:{id:"自定义错误页面-2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#自定义错误页面-2"}},[e._v("#")]),e._v(" 自定义错误页面")]),e._v(" "),t("p",[e._v("如果你想为给定的状态代码显示自定义 HTML 错误页,那么可以将文件添加到"),t("code",[e._v("/error")]),e._v("目录。错误页可以是静态 HTML(即添加到任何静态资源目录下),也可以是用模板构建的。文件的名称应该是确切的状态代码或系列掩码。")]),e._v(" "),t("p",[e._v("例如,要将"),t("code",[e._v("404")]),e._v("映射到静态 HTML 文件,你的目录结构如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("src/\n +- main/\n +- java/\n | + \n +- resources/\n +- public/\n +- error/\n | +- 404.html\n +- \n")])])]),t("p",[e._v("要使用 mustache 模板映射所有"),t("code",[e._v("5xx")]),e._v("错误,你的目录结构如下:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("src/\n +- main/\n +- java/\n | + \n +- resources/\n +- templates/\n +- error/\n | +- 5xx.mustache\n +- \n")])])]),t("h4",{attrs:{id:"_2-1-7-网页过滤器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-1-7-网页过滤器"}},[e._v("#")]),e._v(" 2.1.7.网页过滤器")]),e._v(" "),t("p",[e._v("Spring WebFlux 提供了一个"),t("code",[e._v("WebFilter")]),e._v("接口,其可以实现为过滤 HTTP 请求-响应交换。"),t("code",[e._v("WebFilter")]),e._v("在应用程序上下文中找到的 bean 将自动用于过滤每个交换。")]),e._v(" "),t("p",[e._v("在过滤器的顺序很重要的地方,它们可以实现"),t("code",[e._v("Ordered")]),e._v("或用"),t("code",[e._v("@Order")]),e._v("进行注释。 Spring 引导自动配置可以为你配置 Web 过滤器。当它这样做时,将使用下表所示的订单:")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th",[e._v("网页过滤器")]),e._v(" "),t("th",[e._v("Order")])])]),e._v(" "),t("tbody",[t("tr",[t("td",[t("code",[e._v("MetricsWebFilter")])]),e._v(" "),t("td",[t("code",[e._v("Ordered.HIGHEST_PRECEDENCE + 1")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("WebFilterChainProxy")]),e._v("( Spring 证券)")]),e._v(" "),t("td",[t("code",[e._v("-100")])])]),e._v(" "),t("tr",[t("td",[t("code",[e._v("HttpTraceWebFilter")])]),e._v(" "),t("td",[t("code",[e._v("Ordered.LOWEST_PRECEDENCE - 10")])])])])]),e._v(" "),t("h3",{attrs:{id:"_2-2-嵌入式反应式服务器支持"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-2-嵌入式反应式服务器支持"}},[e._v("#")]),e._v(" 2.2.嵌入式反应式服务器支持")]),e._v(" "),t("p",[e._v("Spring 引导包括对以下嵌入式反应性 Web 服务器的支持:Reactor Netty、 Tomcat、 Jetty 和 Undertow。大多数开发人员使用适当的“启动器”来获得完全配置的实例。默认情况下,嵌入式服务器监听端口 8080 上的 HTTP 请求。")]),e._v(" "),t("h3",{attrs:{id:"_2-3-反应式服务器资源配置"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_2-3-反应式服务器资源配置"}},[e._v("#")]),e._v(" 2.3.反应式服务器资源配置")]),e._v(" "),t("p",[e._v("当自动配置反应堆网络或 Jetty 服务器时, Spring 引导将创建特定的 bean,这些 bean 将向服务器实例提供 HTTP 资源:"),t("code",[e._v("ReactorResourceFactory")]),e._v("或"),t("code",[e._v("JettyResourceFactory")]),e._v("。")]),e._v(" "),t("p",[e._v("默认情况下,这些资源也将与 Reactor Netty 和 Jetty 客户端共享,以获得最佳性能,给定:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("同样的技术也用于服务器和客户端。")])]),e._v(" "),t("li",[t("p",[e._v("客户机实例是使用由 Spring 引导自动配置的"),t("code",[e._v("WebClient.Builder")]),e._v(" Bean 来构建的。")])])]),e._v(" "),t("p",[e._v("开发人员可以通过提供自定义"),t("code",[e._v("ReactorResourceFactory")]),e._v("或"),t("code",[e._v("JettyResourceFactory")]),e._v(" Bean 来覆盖 Jetty 和反应器网络的资源配置-这将同时应用于客户机和服务器。")]),e._v(" "),t("p",[e._v("你可以在"),t("RouterLink",{attrs:{to:"/spring-boot/io.html#io.rest-client.webclient.runtime"}},[e._v("WebClient 运行时部分")]),e._v("中了解有关客户端资源配置的更多信息。")],1),e._v(" "),t("h2",{attrs:{id:"_3-优雅的关机"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_3-优雅的关机"}},[e._v("#")]),e._v(" 3. 优雅的关机")]),e._v(" "),t("p",[e._v("所有四个嵌入式 Web 服务器( Jetty、Reactor Netty、 Tomcat 和 Undertow)以及反应式和基于 Servlet 的 Web 应用程序都支持优雅的关闭。它作为关闭应用程序上下文的一部分出现,并在停止"),t("code",[e._v("SmartLifecycle")]),e._v("bean 的最早阶段执行。此停止处理使用超时,超时提供了一个宽限期,在此期间将允许完成现有的请求,但不允许新的请求。不允许新请求的确切方式取决于所使用的 Web 服务器。 Jetty、反应堆网络和 Tomcat 将在网络层停止接受请求。 Undertow 将接受请求,但立即响应具有服务不可用的(503)响应。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("使用 Tomcat 的优雅关机需要 Tomcat 9.0.33 或更高版本。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("要启用优雅的关机,请配置"),t("code",[e._v("server.shutdown")]),e._v("属性,如以下示例所示:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("server.shutdown=graceful\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('server:\n shutdown: "graceful"\n')])])]),t("p",[e._v("要配置超时周期,请配置"),t("code",[e._v("spring.lifecycle.timeout-per-shutdown-phase")]),e._v("属性,如以下示例所示:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.lifecycle.timeout-per-shutdown-phase=20s\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n lifecycle:\n timeout-per-shutdown-phase: "20s"\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果 IDE 没有发送适当的"),t("code",[e._v("SIGTERM")]),e._v("信号,那么在 IDE 中使用 Graceful Shutdown 可能无法正常工作。"),t("br"),e._v("有关更多详细信息,请参见 IDE 的文档。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h2",{attrs:{id:"_4-spring-security"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-spring-security"}},[e._v("#")]),e._v(" 4. Spring Security")]),e._v(" "),t("p",[e._v("如果"),t("a",{attrs:{href:"https://spring.io/projects/spring-security",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security"),t("OutboundLink")],1),e._v("在 Classpath 上,那么默认情况下 Web 应用程序是安全的。 Spring 启动依赖于 Spring 安全性的内容协商策略来确定是否使用"),t("code",[e._v("httpBasic")]),e._v("或"),t("code",[e._v("formLogin")]),e._v("。要向 Web 应用程序添加方法级安全性,还可以添加带有所需设置的"),t("code",[e._v("@EnableGlobalMethodSecurity")]),e._v("。其他信息可以在"),t("a",{attrs:{href:"https://docs.spring.io/spring-security/reference/5.6.2/servlet/authorization/method-security.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security Reference Guide"),t("OutboundLink")],1),e._v("中找到。")]),e._v(" "),t("p",[e._v("默认的"),t("code",[e._v("UserDetailsService")]),e._v("只有一个用户。用户名是"),t("code",[e._v("user")]),e._v(",密码是随机的,并在应用程序启动时在信息级别打印,如以下示例所示:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35\n")])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果你微调了日志配置,请确保将"),t("code",[e._v("org.springframework.boot.autoconfigure.security")]),e._v("类别设置为 log"),t("code",[e._v("INFO")]),e._v("-level 消息。"),t("br"),e._v("否则,将不打印默认密码。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("你可以通过提供"),t("code",[e._v("spring.security.user.name")]),e._v("和"),t("code",[e._v("spring.security.user.password")]),e._v("来更改用户名和密码。")]),e._v(" "),t("p",[e._v("默认情况下,在 Web 应用程序中获得的基本特性是:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("a"),t("code",[e._v("UserDetailsService")]),e._v("(或"),t("code",[e._v("ReactiveUserDetailsService")]),e._v("在 WebFlux 应用程序的情况下) Bean 具有内存存储和单个用户生成的密码(参见["),t("code",[e._v("Security属性.User")]),e._v("](https://DOCS. Spring.io/ Spring-boot/DOCS/2.6.4/api/org/springframework/boot/autoformit/autofigure/security/securityproperties.user.html)用于用户的属性)。")])]),e._v(" "),t("li",[t("p",[e._v("基于表单的登录或 HTTP 基本安全性(取决于请求中的头)用于整个应用程序(如果致动器在 Classpath 上,则包括致动器端点)。")])]),e._v(" "),t("li",[t("p",[e._v("用于发布身份验证事件的"),t("code",[e._v("DefaultAuthenticationEventPublisher")]),e._v("。")])])]),e._v(" "),t("p",[e._v("你可以通过为它添加 Bean 来提供不同的"),t("code",[e._v("AuthenticationEventPublisher")]),e._v("。")]),e._v(" "),t("h3",{attrs:{id:"_4-1-mvc-安全"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-1-mvc-安全"}},[e._v("#")]),e._v(" 4.1.MVC 安全")]),e._v(" "),t("p",[e._v("默认的安全配置是在"),t("code",[e._v("SecurityAutoConfiguration")]),e._v("和"),t("code",[e._v("UserDetailsServiceAutoConfiguration")]),e._v("中实现的。"),t("code",[e._v("SecurityAutoConfiguration")]),e._v("用于 Web 安全的 imports"),t("code",[e._v("SpringBootWebSecurityConfiguration")]),e._v("和"),t("code",[e._v("UserDetailsServiceAutoConfiguration")]),e._v("配置身份验证,这在非 Web 应用程序中也是相关的。要完全关闭默认的 Web 应用程序安全配置,或者合并多个 Spring 安全组件,例如 OAuth2 客户机和资源服务器,请添加类型"),t("code",[e._v("SecurityFilterChain")]),e._v("的 Bean(这样做不会禁用"),t("code",[e._v("UserDetailsService")]),e._v("配置或执行器的安全性)。")]),e._v(" "),t("p",[e._v("要同时关闭"),t("code",[e._v("UserDetailsService")]),e._v("配置,可以添加类型为"),t("code",[e._v("UserDetailsService")]),e._v("、"),t("code",[e._v("AuthenticationProvider")]),e._v("或"),t("code",[e._v("AuthenticationManager")]),e._v("的 Bean。")]),e._v(" "),t("p",[e._v("可以通过添加自定义"),t("code",[e._v("SecurityFilterChain")]),e._v("或"),t("code",[e._v("WebSecurityConfigurerAdapter")]),e._v("来覆盖访问规则 Bean。 Spring Boot 提供了方便的方法,这些方法可用于重写执行器端点和静态资源的访问规则。"),t("code",[e._v("EndpointRequest")]),e._v("可用于创建"),t("code",[e._v("RequestMatcher")]),e._v("这是基于"),t("code",[e._v("management.endpoints.web.base-path")]),e._v("属性的。"),t("code",[e._v("PathRequest")]),e._v("可用于为常用位置中的资源创建"),t("code",[e._v("RequestMatcher")]),e._v("。")]),e._v(" "),t("h3",{attrs:{id:"_4-2-webflux-安全性"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-2-webflux-安全性"}},[e._v("#")]),e._v(" 4.2.WebFlux 安全性")]),e._v(" "),t("p",[e._v("与 Spring MVC 应用程序类似,你可以通过添加"),t("code",[e._v("spring-boot-starter-security")]),e._v("依赖项来保护你的 WebFlux 应用程序。默认的安全配置是在"),t("code",[e._v("ReactiveSecurityAutoConfiguration")]),e._v("和"),t("code",[e._v("UserDetailsServiceAutoConfiguration")]),e._v("中实现的。"),t("code",[e._v("ReactiveSecurityAutoConfiguration")]),e._v("导入"),t("code",[e._v("WebFluxSecurityConfiguration")]),e._v("用于 Web 安全和"),t("code",[e._v("UserDetailsServiceAutoConfiguration")]),e._v("配置身份验证,这在非 Web 应用程序中也是相关的。要完全关闭默认的 Web 应用程序安全配置,可以添加类型"),t("code",[e._v("WebFilterChainProxy")]),e._v("的 Bean(这样做不会禁用"),t("code",[e._v("UserDetailsService")]),e._v("配置或执行器的安全性)。")]),e._v(" "),t("p",[e._v("要同时关闭"),t("code",[e._v("UserDetailsService")]),e._v("配置,可以添加类型"),t("code",[e._v("ReactiveUserDetailsService")]),e._v("或"),t("code",[e._v("ReactiveAuthenticationManager")]),e._v("的 Bean。")]),e._v(" "),t("p",[e._v("Spring 使用诸如 OAuth2 客户端和资源服务器的多个安全组件的访问规则可以通过添加自定义 Bean 来配置。 Spring Boot 提供了方便的方法,这些方法可以用来覆盖执行器端点和静态资源的访问规则。"),t("code",[e._v("EndpointRequest")]),e._v("可以用来创建一个"),t("code",[e._v("ServerWebExchangeMatcher")]),e._v(",即基于"),t("code",[e._v("management.endpoints.web.base-path")]),e._v("的属性。")]),e._v(" "),t("p",[t("code",[e._v("PathRequest")]),e._v("可用于为常用位置中的资源创建"),t("code",[e._v("ServerWebExchangeMatcher")]),e._v("。")]),e._v(" "),t("p",[e._v("例如,你可以通过添加以下内容来定制你的安全配置:")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.boot.autoconfigure.security.reactive.PathRequest;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.web.server.ServerHttpSecurity;\nimport org.springframework.security.web.server.SecurityWebFilterChain;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyWebFluxSecurityConfiguration {\n\n @Bean\n public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {\n http.authorizeExchange((spec) -> {\n spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();\n spec.pathMatchers("/foo", "/bar").authenticated();\n });\n http.formLogin();\n return http.build();\n }\n\n}\n\n')])])]),t("h3",{attrs:{id:"_4-3-oauth2"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-oauth2"}},[e._v("#")]),e._v(" 4.3.OAuth2")]),e._v(" "),t("p",[t("a",{attrs:{href:"https://oauth.net/2/",target:"_blank",rel:"noopener noreferrer"}},[e._v("OAuth2"),t("OutboundLink")],1),e._v("是 Spring 支持的一个广泛使用的授权框架。")]),e._v(" "),t("h4",{attrs:{id:"_4-3-1-客户"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-1-客户"}},[e._v("#")]),e._v(" 4.3.1.客户")]),e._v(" "),t("p",[e._v("如果你的 Classpath 上有"),t("code",[e._v("spring-security-oauth2-client")]),e._v(",则可以利用一些自动配置来设置 OAuth2/Open ID Connect 客户端。此配置使用"),t("code",[e._v("OAuth2Client属性")]),e._v("下的属性。相同的性质适用于 Servlet 和反应性应用。")]),e._v(" "),t("p",[e._v("你可以在"),t("code",[e._v("spring.security.oauth2.client")]),e._v("前缀下注册多个 OAuth2 客户机和提供者,如以下示例所示:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.client.registration.my-client-1.client-id=abcd\nspring.security.oauth2.client.registration.my-client-1.client-secret=password\nspring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope\nspring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider\nspring.security.oauth2.client.registration.my-client-1.scope=user\nspring.security.oauth2.client.registration.my-client-1.redirect-uri=https://my-redirect-uri.com\nspring.security.oauth2.client.registration.my-client-1.client-authentication-method=basic\nspring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization-code\n\nspring.security.oauth2.client.registration.my-client-2.client-id=abcd\nspring.security.oauth2.client.registration.my-client-2.client-secret=password\nspring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope\nspring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider\nspring.security.oauth2.client.registration.my-client-2.scope=email\nspring.security.oauth2.client.registration.my-client-2.redirect-uri=https://my-redirect-uri.com\nspring.security.oauth2.client.registration.my-client-2.client-authentication-method=basic\nspring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code\n\nspring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server/oauth/authorize\nspring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server/oauth/token\nspring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server/userinfo\nspring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header\nspring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server/token_keys\nspring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n client:\n registration:\n my-client-1:\n client-id: "abcd"\n client-secret: "password"\n client-name: "Client for user scope"\n provider: "my-oauth-provider"\n scope: "user"\n redirect-uri: "https://my-redirect-uri.com"\n client-authentication-method: "basic"\n authorization-grant-type: "authorization-code"\n\n my-client-2:\n client-id: "abcd"\n client-secret: "password"\n client-name: "Client for email scope"\n provider: "my-oauth-provider"\n scope: "email"\n redirect-uri: "https://my-redirect-uri.com"\n client-authentication-method: "basic"\n authorization-grant-type: "authorization_code"\n\n provider:\n my-oauth-provider:\n authorization-uri: "https://my-auth-server/oauth/authorize"\n token-uri: "https://my-auth-server/oauth/token"\n user-info-uri: "https://my-auth-server/userinfo"\n user-info-authentication-method: "header"\n jwk-set-uri: "https://my-auth-server/token_keys"\n user-name-attribute: "name"\n')])])]),t("p",[e._v("对于支持"),t("a",{attrs:{href:"https://openid.net/specs/openid-connect-discovery-1_0.html",target:"_blank",rel:"noopener noreferrer"}},[e._v("OpenID 连接发现"),t("OutboundLink")],1),e._v("的 OpenID Connect 提供者,可以进一步简化配置。提供程序需要配置"),t("code",[e._v("issuer-uri")]),e._v(",这是它所断言的作为其发行者标识符的 URI。例如,如果提供的"),t("code",[e._v("issuer-uri")]),e._v("是“https://example.com”,那么将对“https://example.com/.well-nown/openid-configuration”进行"),t("code",[e._v("OpenID Provider Configuration Request")]),e._v("。预期结果是"),t("code",[e._v("OpenID Provider Configuration Response")]),e._v("。下面的示例展示了如何使用"),t("code",[e._v("issuer-uri")]),e._v("配置 OpenID Connect 提供程序:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n client:\n provider:\n oidc-provider:\n issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"\n')])])]),t("p",[e._v("默认情况下, Spring Security 的"),t("code",[e._v("OAuth2LoginAuthenticationFilter")]),e._v("只处理匹配"),t("code",[e._v("/login/oauth2/code/*")]),e._v("的 URL。如果希望自定义"),t("code",[e._v("redirect-uri")]),e._v("以使用不同的模式,则需要提供处理该自定义模式的配置。例如,对于 Servlet 应用程序,你可以添加你自己的类似于以下内容的"),t("code",[e._v("SecurityFilterChain")]),e._v(":")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('import org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.web.SecurityFilterChain;\n\n@Configuration(proxyBeanMethods = false)\npublic class MyOAuthClientConfiguration {\n\n @Bean\n public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n http.authorizeRequests().anyRequest().authenticated();\n http.oauth2Login().redirectionEndpoint().baseUri("custom-callback");\n return http.build();\n }\n\n}\n\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("Spring 引导自动配置由 Spring Security 用于管理客户端注册的"),t("code",[e._v("InMemoryOAuth2AuthorizedClientService")]),e._v("的"),t("br"),e._v("的"),t("code",[e._v("InMemoryOAuth2AuthorizedClientService")]),e._v("的功能有限,我们建议仅将其用于开发环境。"),t("br"),e._v("对于生产环境,可以考虑使用"),t("code",[e._v("JdbcOAuth2AuthorizedClientService")]),e._v("或创建自己的"),t("code",[e._v("OAuth2AuthorizedClientService")]),e._v("实现。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h5",{attrs:{id:"oauth2-共同提供者的客户端注册"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#oauth2-共同提供者的客户端注册"}},[e._v("#")]),e._v(" OAuth2 共同提供者的客户端注册")]),e._v(" "),t("p",[e._v("对于常见的 OAuth2 和 OpenID 提供者,包括 Google、GitHub、Facebook 和 OKTA,我们提供了一组提供者默认值(分别为"),t("code",[e._v("google")]),e._v("、"),t("code",[e._v("github")]),e._v("、"),t("code",[e._v("facebook")]),e._v("和"),t("code",[e._v("okta")]),e._v(")。")]),e._v(" "),t("p",[e._v("如果不需要自定义这些提供程序,可以将"),t("code",[e._v("provider")]),e._v("属性设置为需要推断默认值的属性。此外,如果客户端注册的键与缺省支持的提供者匹配, Spring 启动也会推断出这一点。")]),e._v(" "),t("p",[e._v("换句话说,以下示例中的两个配置使用了 Google Provider:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.client.registration.my-client.client-id=abcd\nspring.security.oauth2.client.registration.my-client.client-secret=password\nspring.security.oauth2.client.registration.my-client.provider=google\nspring.security.oauth2.client.registration.google.client-id=abcd\nspring.security.oauth2.client.registration.google.client-secret=password\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n client:\n registration:\n my-client:\n client-id: "abcd"\n client-secret: "password"\n provider: "google"\n google:\n client-id: "abcd"\n client-secret: "password"\n')])])]),t("h4",{attrs:{id:"_4-3-2-资源服务器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-2-资源服务器"}},[e._v("#")]),e._v(" 4.3.2.资源服务器")]),e._v(" "),t("p",[e._v("如果你的 Classpath 上有"),t("code",[e._v("spring-security-oauth2-resource-server")]),e._v(",则 Spring 引导可以设置 OAuth2 资源服务器。对于 JWT 配置,需要指定一个 JWK 集 URI 或 OIDC Issuer URI,如以下示例所示:")]),e._v(" "),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n resourceserver:\n jwt:\n jwk-set-uri: "https://example.com/oauth2/default/v1/keys"\n')])])]),t("p",[e._v("属性")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n resourceserver:\n jwt:\n issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("如果授权服务器不支持 JWK 设置的 URI,则可以使用用于验证 JWT 签名的公钥配置资源服务器。"),t("br"),e._v("这可以使用"),t("code",[e._v("spring.security.oauth2.resourceserver.jwt.public-key-location")]),e._v("属性完成,值需要指向包含 PEM 编码的 X509 格式的公钥的文件。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("同样的性质也适用于 Servlet 和活性应用。")]),e._v(" "),t("p",[e._v("或者,你可以为 Servlet 应用程序定义自己的"),t("code",[e._v("JwtDecoder")]),e._v(" Bean,或者为反应性应用程序定义"),t("code",[e._v("ReactiveJwtDecoder")]),e._v("。")]),e._v(" "),t("p",[e._v("在使用不透明令牌而不是 JWTS 的情况下,你可以配置以下属性来通过内省来验证令牌:")]),e._v(" "),t("p",[e._v("Properties")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token\nspring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id\nspring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n oauth2:\n resourceserver:\n opaquetoken:\n introspection-uri: "https://example.com/check-token"\n client-id: "my-client-id"\n client-secret: "my-client-secret"\n')])])]),t("p",[e._v("同样,同样的性质也适用于 Servlet 和无反应的应用。")]),e._v(" "),t("p",[e._v("或者,你可以为 Servlet 应用程序定义自己的"),t("code",[e._v("OpaqueTokenIntrospector")]),e._v(" Bean,或者为反应性应用程序定义"),t("code",[e._v("ReactiveOpaqueTokenIntrospector")]),e._v("。")]),e._v(" "),t("h4",{attrs:{id:"_4-3-3-授权服务器"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-3-3-授权服务器"}},[e._v("#")]),e._v(" 4.3.3.授权服务器")]),e._v(" "),t("p",[e._v("Spring 目前,安全性不提供对实现 OAuth2.0 授权服务器的支持。然而,该功能可从"),t("a",{attrs:{href:"https://spring.io/projects/spring-security-oauth",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Security OAuth"),t("OutboundLink")],1),e._v("项目中获得,该项目最终将完全被 Spring 安全性所取代。在此之前,你可以使用"),t("code",[e._v("spring-security-oauth2-autoconfigure")]),e._v("模块轻松地设置 OAuth2.0 授权服务器;有关说明,请参见其"),t("a",{attrs:{href:"https://docs.spring.io/spring-security-oauth2-boot/",target:"_blank",rel:"noopener noreferrer"}},[e._v("文件"),t("OutboundLink")],1),e._v("。")]),e._v(" "),t("h3",{attrs:{id:"_4-4-saml2-0"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-saml2-0"}},[e._v("#")]),e._v(" 4.4.SAML2.0")]),e._v(" "),t("h4",{attrs:{id:"_4-4-1-依赖方"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_4-4-1-依赖方"}},[e._v("#")]),e._v(" 4.4.1.依赖方")]),e._v(" "),t("p",[e._v("如果你的 Classpath 上有"),t("code",[e._v("spring-security-saml2-service-provider")]),e._v(",则可以利用一些自动配置来设置 SAML2.0 依赖方。此配置使用"),t("code",[e._v("Saml2RelyingPartyProperties")]),e._v("下的属性。")]),e._v(" "),t("p",[e._v("依赖方注册表示身份提供程序 IDP 和服务提供程序 SP 之间的配对配置。你可以在"),t("code",[e._v("spring.security.saml2.relyingparty")]),e._v("前缀下注册多个依赖方,如下例所示:")]),e._v(" "),t("p",[e._v("Properties")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key\nspring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate\nspring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=path-to-private-key\nspring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=path-to-certificate\nspring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.verification.credentials[0].certificate-location=path-to-verification-cert\nspring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.entity-id=remote-idp-entity-id1\nspring.security.saml2.relyingparty.registration.my-relying-party1.identityprovider.sso-url=https://remoteidp1.sso.url\n\nspring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key\nspring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate\nspring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=path-to-private-key\nspring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=path-to-certificate\nspring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.verification.credentials[0].certificate-location=path-to-other-verification-cert\nspring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.entity-id=remote-idp-entity-id2\nspring.security.saml2.relyingparty.registration.my-relying-party2.identityprovider.sso-url=https://remoteidp2.sso.url\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n security:\n saml2:\n relyingparty:\n registration:\n my-relying-party1:\n signing:\n credentials:\n - private-key-location: "path-to-private-key"\n certificate-location: "path-to-certificate"\n decryption:\n credentials:\n - private-key-location: "path-to-private-key"\n certificate-location: "path-to-certificate"\n identityprovider:\n verification:\n credentials:\n - certificate-location: "path-to-verification-cert"\n entity-id: "remote-idp-entity-id1"\n sso-url: "https://remoteidp1.sso.url"\n\n my-relying-party2:\n signing:\n credentials:\n - private-key-location: "path-to-private-key"\n certificate-location: "path-to-certificate"\n decryption:\n credentials:\n - private-key-location: "path-to-private-key"\n certificate-location: "path-to-certificate"\n identityprovider:\n verification:\n credentials:\n - certificate-location: "path-to-other-verification-cert"\n entity-id: "remote-idp-entity-id2"\n sso-url: "https://remoteidp2.sso.url"\n')])])]),t("h2",{attrs:{id:"_5-spring-届会议"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_5-spring-届会议"}},[e._v("#")]),e._v(" 5. Spring 届会议")]),e._v(" "),t("p",[e._v("Spring 引导为广泛的数据存储范围提供"),t("a",{attrs:{href:"https://spring.io/projects/spring-session",target:"_blank",rel:"noopener noreferrer"}},[e._v("Spring Session"),t("OutboundLink")],1),e._v("自动配置。 Servlet 在构建 Web 应用程序时,可以自动配置以下存储:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("JDBC")])]),e._v(" "),t("li",[t("p",[e._v("雷迪斯")])]),e._v(" "),t("li",[t("p",[e._v("黑泽尔卡斯特")])]),e._v(" "),t("li",[t("p",[e._v("MongoDB")])])]),e._v(" "),t("p",[e._v("Servlet 自动配置取代了使用"),t("code",[e._v("@Enable*HttpSession")]),e._v("的需要。")]),e._v(" "),t("p",[e._v("在构建反应式 Web 应用程序时,可以自动配置以下存储:")]),e._v(" "),t("ul",[t("li",[t("p",[e._v("雷迪斯")])]),e._v(" "),t("li",[t("p",[e._v("MongoDB")])])]),e._v(" "),t("p",[e._v("反应式自动配置取代了使用"),t("code",[e._v("@Enable*WebSession")]),e._v("的需要。")]),e._v(" "),t("p",[e._v("如果在 Classpath 上存在单个 Spring 会话模块,则 Spring 引导自动使用该存储实现。如果有多个实现,则必须选择希望用于存储会话的["),t("code",[e._v("StoreType")]),e._v("](https://github.com/ Spring-projects/ Spring-boot/tree/v2.6.4/ Spring-boot-project/ Spring-boot-autofigure/SRC/main/java/org/org/springframework/boot/autofigure/autofigure/session/storetype.java)。例如,要使用 JDBC 作为后端存储,你可以按以下方式配置应用程序:")]),e._v(" "),t("p",[e._v("Properties")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.session.store-type=jdbc\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n session:\n store-type: "jdbc"\n')])])]),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[e._v("通过将"),t("code",[e._v("store-type")]),e._v("设置为"),t("code",[e._v("none")]),e._v(",可以禁用 Spring 会话。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("p",[e._v("每个商店都有特定的附加设置。例如,可以自定义 JDBC 存储表的名称,如下例所示:")]),e._v(" "),t("p",[e._v("Properties")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v("spring.session.jdbc.table-name=SESSIONS\n")])])]),t("p",[e._v("Yaml")]),e._v(" "),t("div",{staticClass:"language- extra-class"},[t("pre",{pre:!0,attrs:{class:"language-text"}},[t("code",[e._v('spring:\n session:\n jdbc:\n table-name: "SESSIONS"\n')])])]),t("p",[e._v("为了设置会话的超时,你可以使用"),t("code",[e._v("spring.session.timeout")]),e._v("属性。如果在 Servlet Web 应用程序中未设置该属性,则自动配置将返回到"),t("code",[e._v("server.servlet.session.timeout")]),e._v("的值。")]),e._v(" "),t("p",[e._v("你可以使用"),t("code",[e._v("@Enable*HttpSession")]),e._v("( Servlet)或"),t("code",[e._v("@Enable*WebSession")]),e._v("(反应式)来控制 Spring 会话的配置。这将导致自动配置后退。 Spring 然后可以使用注释的属性而不是先前描述的配置属性来配置会话。")]),e._v(" "),t("h2",{attrs:{id:"_6-spring-仇恨"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_6-spring-仇恨"}},[e._v("#")]),e._v(" 6. Spring 仇恨")]),e._v(" "),t("p",[e._v("如果你开发了一个使用超媒体的 RESTful API, Spring Boot 为 Spring Hateoas 提供了自动配置,这在大多数应用程序中都能很好地工作。自动配置取代了使用"),t("code",[e._v("@EnableHypermediaSupport")]),e._v("的需要,并注册了许多 bean,以方便构建基于超媒体的应用程序,包括"),t("code",[e._v("LinkDiscoverers")]),e._v("(用于客户端支持)和"),t("code",[e._v("ObjectMapper")]),e._v(",这些配置用于将响应正确地编组到所需的表示中。"),t("code",[e._v("ObjectMapper")]),e._v("是通过设置各种"),t("code",[e._v("spring.jackson.*")]),e._v("属性来定制的,或者,如果存在一个属性,则通过"),t("code",[e._v("Jackson2ObjectMapperBuilder")]),e._v("来定制 Bean。")]),e._v(" "),t("p",[e._v("你可以通过使用"),t("code",[e._v("@EnableHypermediaSupport")]),e._v("来控制 Spring Hateoas 的配置。注意,这样做会禁用前面描述的"),t("code",[e._v("ObjectMapper")]),e._v("定制。")]),e._v(" "),t("table",[t("thead",[t("tr",[t("th"),e._v(" "),t("th",[t("code",[e._v("spring-boot-starter-hateoas")]),e._v("是特定于 Spring MVC 的,不应与 Spring WebFlux 合并。"),t("br"),e._v("为了在 Spring WebFlux 中使用 Spring Hateoas,可以在"),t("code",[e._v("org.springframework.hateoas:spring-hateoas")]),e._v("以及"),t("code",[e._v("spring-boot-starter-webflux")]),e._v("上添加一个直接依赖项。")])])]),e._v(" "),t("tbody")]),e._v(" "),t("h2",{attrs:{id:"_7-接下来要读什么"}},[t("a",{staticClass:"header-anchor",attrs:{href:"#_7-接下来要读什么"}},[e._v("#")]),e._v(" 7. 接下来要读什么?")]),e._v(" "),t("p",[e._v("你现在应该对如何使用 Spring 引导来开发 Web 应用程序有了很好的了解。接下来的几个部分描述了 Spring 引导如何集成各种"),t("RouterLink",{attrs:{to:"/spring-boot/data.html#data"}},[e._v("数据技术")]),e._v("、"),t("RouterLink",{attrs:{to:"/spring-boot/messaging.html#messaging"}},[e._v("消息传递系统")]),e._v("和其他 IO 功能。你可以根据应用程序的需求选择其中的任何一个。")],1)])}),[],!1,null,null,null);r.default=o.exports}}]);