本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Boot非常适用于web应用程序开发。 你可以通过使用嵌入式Tomcat、Jetty、Undertow或Netty创建一个独立的HTTP服务器。 大多数Web应用使用 spring-boot-starter-web 模块来快速启动和运行。 你也可以选择通过使用 spring-boot-starter-webflux 模块来构建响应式Web应用。

如果你还没有开发Spring Boot Web应用,你可以按照 入门 章节中的 "Hello World!"例子来做。

1. Servlet Web Applications

如果你想构建基于Servlet的Web应用,你可以利用Spring Boot对Spring MVC或Jersey的自动配置。

1.1. Spring Web MVC 框架

Spring Web MVC框架(通常被称为 "Spring MVC")是一个丰富的 “model view controller” (MVC)Web框架。 Spring MVC让你创建特殊的 @Controller@RestController Bean来处理进入的HTTP请求。 控制器中的方法通过使用 @RequestMapping 注解被映射到HTTP。

下面的代码显示了一个典型的 @RestController,提供JSON数据服务。

Java
import java.util.List;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public User getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId).get();
    }

    @GetMapping("/{userId}/customers")
    public List<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).map(this.customerRepository::findByUser).get();
    }

    @DeleteMapping("/{userId}")
    public void deleteUser(@PathVariable Long userId) {
        this.userRepository.deleteById(userId);
    }

}
Kotlin
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController


@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {

    @GetMapping("/{userId}")
    fun getUser(@PathVariable userId: Long): User {
        return userRepository.findById(userId).get()
    }

    @GetMapping("/{userId}/customers")
    fun getUserCustomers(@PathVariable userId: Long): List<Customer> {
        return userRepository.findById(userId).map(customerRepository::findByUser).get()
    }

    @DeleteMapping("/{userId}")
    fun deleteUser(@PathVariable userId: Long) {
        userRepository.deleteById(userId)
    }

}

“WebMvc.fn”,功能变体,将路由配置与请求的实际处理分开,如下例所示。

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.servlet.function.RequestPredicates.accept
import org.springframework.web.servlet.function.RouterFunction
import org.springframework.web.servlet.function.RouterFunctions
import org.springframework.web.servlet.function.ServerResponse

@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {

    @Bean
    fun routerFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
        return RouterFunctions.route()
            .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
            .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
            .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
            .build()
    }

    companion object {
        private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
    }

}
Java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {

    public ServerResponse getUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse getUserCustomers(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

    public ServerResponse deleteUser(ServerRequest request) {
        ...
        return ServerResponse.ok().build();
    }

}
Kotlin
import org.springframework.stereotype.Component
import org.springframework.web.servlet.function.ServerRequest
import org.springframework.web.servlet.function.ServerResponse

@Component
class MyUserHandler {

    fun getUser(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

    fun getUserCustomers(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

    fun deleteUser(request: ServerRequest?): ServerResponse {
        return ServerResponse.ok().build()
    }

}

Spring MVC是Spring框架核心的一部分,详细的信息可以在 参考文档 中找到。 还有一些涵盖Spring MVC的指南可参阅 spring.io/guides

你可以定义任意多的 RouterFunction Bean,以使router的定义模块化。 如果你需要应用一个优先级,Bean可以被排序。

1.1.1. Spring MVC 自动配置

Spring Boot为Spring MVC提供了自动配置功能,对大多数应用程序都很适用。

自动配置在Spring的默认值基础上增加了以下功能。

  • 包含了 ContentNegotiatingViewResolverBeanNameViewResolver Bean。

  • 支持为静态资源提供服务,包括对WebJars的支持(本文稍后将介绍)。

  • 自动注册 ConverterGenericConverterFormatter Bean。

  • 支持 HttpMessageConverters(在本文档的后面会提到)。

  • 自动注册 MessageCodesResolver本文稍后将介绍)。

  • 支持静态的 index.html

  • 自动使用 ConfigurableWebBindingInitializer bean(在本文档后面有介绍)。

如果你想保留那些Spring Boot MVC定制,并进行更多的 MVC定制(Interceptor、Formatter、视图控制器和其他功能),你可以添加你自己的 @Configuration 类,类型为 WebMvcConfigurer ,但 @EnableWebMvc

如果你想提供 RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver 的自定义实例,并仍然保持Spring Boot MVC的自定义,你可以声明一个 WebMvcRegistrations 类型的bean,用它来提供这些组件的自定义实例。

如果你想完全控制Spring MVC,你可以添加你自己的 @Configuration 并使用 @EnableWebMvc 注解 ,或者添加你自己的 @Configuration 并使用 DelegatingWebMvcConfiguration 注解 ,如 @EnableWebMvc 的Javadoc中所述。

Spring MVC使用不同的 ConversionService 来转换 application.propertiesapplication.yaml 文件中的值。 这意味着 PeriodDurationDataSize 转换器不可用,@DurationUnit@DataSizeUnit 注释将被忽略。

如果你想定制Spring MVC使用的 ConversionService,你可以提供一个 WebMvcConfigurer Bean,它有一个 addFormatters 方法。 从这个方法中,你可以注册任何你喜欢的转换器,或者你可以委托给 ApplicationConversionService 上的静态方法。

1.1.2. HTTP消息转换器(HttpMessageConverter)

Spring MVC使用 HttpMessageConverter 接口来转换HTTP请求和响应。 合理的默认值是开箱即有的。 例如,对象可以自动转换为JSON(通过使用Jackson库)或XML(通过使用Jackson XML扩展,如果可用的话,或通过使用JAXB,如果Jackson XML扩展不可用)。 默认情况下,字符串是以 UTF-8 编码的。

如果你需要添加或定制转换器,你可以使用Spring Boot的 HttpMessageConverters 类,如下表所示。

Java
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = new AdditionalHttpMessageConverter();
        HttpMessageConverter<?> another = new AnotherHttpMessageConverter();
        return new HttpMessageConverters(additional, another);
    }

}
Kotlin
import org.springframework.boot.autoconfigure.http.HttpMessageConverters
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.converter.HttpMessageConverter

@Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration {

    @Bean
    fun customConverters(): HttpMessageConverters {
        val additional: HttpMessageConverter<*> = AdditionalHttpMessageConverter()
        val another: HttpMessageConverter<*> = AnotherHttpMessageConverter()
        return HttpMessageConverters(additional, another)
    }

}

任何存在于context的 HttpMessageConverter bean都会被添加到转换器的列表中。 你也可以用同样的方式覆盖默认的转换器。

1.1.3. MessageCodesResolver

Spring MVC有一个生成错误代码的策略,用于从绑定错误中渲染错误信息:MessageCodesResolver。 如果你设置 spring.mvc.message-codes-resolver-format 属性 PREFIX_ERROR_CODEPOSTFIX_ERROR_CODE,Spring Boot就会为你创建一个(见 DefaultMessageCodesResolver.Format 的枚举)。

1.1.4. 静态内容

默认情况下,Spring Boot从classpath中的 /static(或 /public/resources/META-INF/resources)目录或 ServletContext 的root中提供静态内容。 它使用了Spring MVC中的 ResourceHttpRequestHandler,因此你可以通过添加你自己的 WebMvcConfigurer 和覆盖 addResourceHandlers 方法来修改该行为。

在一个独立的Web应用程序中,来自容器的默认Servlet没有被启用。 它可以使用 server.servlet.register-default-servlet 属性来启用。

default servlet充当后备,如果Spring决定不处理,就从 ServletContext 的root提供内容。 大多数时候,这种情况不会发生(除非你修改了默认的MVC配置),因为Spring总是可以通过 DispatcherServlet 来处理请求。

默认情况下,资源被映射到 /**,但你可以通过 spring.mvc.static-path-pattern 属性进行调整。 例如,将所有资源重新定位到 /resources/** 可以通过以下方式实现。

Properties
spring.mvc.static-path-pattern=/resources/**
Yaml
spring:
  mvc:
    static-path-pattern: "/resources/**"

你也可以通过使用 spring.web.resources.static-locations 属性来定制静态资源的位置(用目录位置的列表代替默认值)。 根Servlet上下文路径, "/",也被自动添加为一个位置。

除了前面提到的 “标准” 静态资源位置外,还为 Webjars内容 提供了一个特殊情况。 默认情况下,任何路径为 /webjars/** 的资源,如果是以Webjars格式打包的,将从jar文件中提供。 路径可以通过 spring.mvc.webjars-path-pattern 属性来定制。

如果你的应用程序被打包成jar,请不要使用 src/main/webapp 目录。 尽管这个目录是一个通用的标准,但它只适用于war打包,如果你生成一个jar,它就会被大多数构建工具默默地忽略掉。

Spring Boot还支持Spring MVC提供的高级资源处理功能,允许使用的情况有:破坏缓存的静态资源或为Webjars使用版本无关的URL。

要对Webjars使用版本无关的URL,请添加 webjars-locator-core 依赖。 然后声明你的Webjar。 以jQuery为例,添加 "/webjars/jquery/jquery.min.js" 的结果是 "/webjars/jquery/x.y.z/jquery.min.js" ,其中 x.y.z 是Webjar的版本。

如果你使用JBoss,你需要声明 webjars-locator-jboss-vfs 依赖关系而不是 webjars-locator-core。 否则,所有的Webjars都会解析为 404

要使用缓存破坏,以下配置为所有静态资源配置了一个缓存破坏解决方案,有效地在URL中添加了一个内容哈希,如 <link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

Properties
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
Yaml
spring:
  web:
    resources:
      chain:
        strategy:
          content:
            enabled: true
            paths: "/**"
由于Thymeleaf和FreeMarker自动配置了 ResourceUrlEncodingFilter,资源的链接在运行时被重写在模板中。 在使用JSP的时候,你应该手动声明这个过滤器。 其他模板引擎目前没有自动支持,但可以通过自定义模板宏/helper和使用 ResourceUrlProvider 来支持。

当用例如JavaScript模块加载器动态加载资源时,重命名文件不是一个选项。 这就是为什么也支持其他策略,并且可以组合使用。 一个 "固定" 策略在URL中添加一个静态的版本字符串,而不改变文件名,如下面的例子所示。

Properties
spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring.web.resources.chain.strategy.fixed.enabled=true
spring.web.resources.chain.strategy.fixed.paths=/js/lib/
spring.web.resources.chain.strategy.fixed.version=v12
Yaml
spring:
  web:
    resources:
      chain:
        strategy:
          content:
            enabled: true
            paths: "/**"
          fixed:
            enabled: true
            paths: "/js/lib/"
            version: "v12"

通过这种配置,位于 "/js/lib/" 下的JavaScript模块使用固定的版本策略("/v12/js/lib/mymodule.js"),而其他资源仍然使用内容策略(<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>)。

参见 WebProperties.Resources 以了解更多支持的选项。

在一篇专门的 博客 和Spring Framework的 参考文档 中已经对这一特性进行了详尽的描述。

1.1.5. 欢迎页面

Spring Boot同时支持静态和模板化的欢迎页面。 它首先在配置的静态内容位置寻找一个 index.html 文件。 如果没有找到,它就会寻找 index 模板。 如果找到了其中之一,它就会自动作为应用程序的欢迎页面使用。

1.1.6. 自定义 Favicon

与其他静态资源一样,Spring Boot检查配置的静态内容位置中是否有 favicon.ico。 如果存在这样的文件,它就会自动作为应用程序的favicon。

1.1.7. 路径匹配和内容协商

Spring MVC可以通过查看请求路径并将其与你的应用程序中定义的映射(例如,controller方法上的 @GetMapping 注解)相匹配,将传入的HTTP请求映射到处理程序。

Spring Boot默认选择禁用后缀模式匹配,这意味着像 "GET /projects/spring-boot.json" 这样的请求将不会被匹配到 @GetMapping("/projects/spring-boot") 映射。 这被认为是 Spring MVC应用的最佳实践。 这个功能在过去主要是针对HTTP客户端没有发送正确的 "Accept" 请求头的情况;我们需要确保向客户端发送正确的内容类型。 现在,内容协商(Content Negotiation)要可靠得多。

还有其他方法来处理那些没有持续发送正确 "Accept" 请求头的HTTP客户端。 我们可以不使用后缀匹配,而是使用一个查询参数来确保像 "GET /projects/spring-boot?format=json" 这样的请求将被映射到 @GetMapping("/projects/spring-boot")

Properties
spring.mvc.contentnegotiation.favor-parameter=true
Yaml
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

或者如果你喜欢使用一个不同的参数名称。

Properties
spring.mvc.contentnegotiation.favor-parameter=true
spring.mvc.contentnegotiation.parameter-name=myparam
Yaml
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true
      parameter-name: "myparam"

大多数标准 media type 都是开箱即用的,但你也可以定义新的 media type。

Properties
spring.mvc.contentnegotiation.media-types.markdown=text/markdown
Yaml
spring:
  mvc:
    contentnegotiation:
      media-types:
        markdown: "text/markdown"

从Spring Framework 5.3开始,Spring MVC支持几种实现策略,用于将请求路径匹配到控制器处理程序。 它以前只支持 AntPathMatcher 策略,但现在也提供 PathPatternParser。 Spring Boot现在提供了一个配置属性来选择和启用新策略。

Properties
spring.mvc.pathmatch.matching-strategy=path-pattern-parser
Yaml
spring:
  mvc:
    pathmatch:
      matching-strategy: "path-pattern-parser"

关于你为什么要考虑这个新实现的更多细节,请参阅 专门的博客

PathPatternParser 是一个优化的实现,但限制了 一些路径模式变体 的使用。 它与后缀模式匹配或用servlet前缀(spring.mvc.servlet.path)映射 DispatcherServlet 不兼容。

默认情况下,如果一个请求没有找到 handler,Spring MVC将发送一个404 Not Found错误响应。要想抛出 NoHandlerFoundException,请将 configprop:spring.mvc.throw-exception-if-no-handler-found 设为 true。注意,默认情况下,静态内容的服务 被映射到 /**,因此,将为所有请求提供一个 handler。如果要抛出 NoHandlerFoundException,你必须将 spring.mvc.static-path-pattern 设置为一个更具体的值,如 /resources/**,或者将 spring.web.resources.add-mappings 设置为false,以完全禁用静态内容的服务。

1.1.8. ConfigurableWebBindingInitializer

Spring MVC使用 WebBindingInitializer 来为特定请求初始化 WebDataBinder。 如果你创建了你自己的 ConfigurableWebBindingInitializer @Bean,Spring Boot会自动配置Spring MVC来使用它。

1.1.9. 模板引擎

除了REST web服务,你还可以使用Spring MVC来提供动态HTML内容。 Spring MVC支持各种模板技术,包括Thymeleaf、FreeMarker和JSP。 此外,许多其他模板引擎也包括它们自己的Spring MVC集成。

Spring Boot包括对以下模板引擎的自动配置支持。

如果可能的话,应该避免使用JSP。 当把它们与嵌入式Servlet容器一起使用时,有几个已知的限制

当你使用这些模板引擎的默认配置时,你的模板会自动从 src/main/resources/templates 中获取。

根据你运行应用程序的方式,你的IDE可能会对classpath进行不同的排序。 在IDE中从其main方法中运行你的应用程序,与你通过使用Maven或Gradle或从其打包的jar中运行你的应用程序时的排序不同。 这可能导致Spring Boot无法找到预期的模板。 如果你有这个问题,你可以在IDE中重新排序classpath,把模块的类和资源放在前面。

1.1.10. Error 处理

默认情况下,Spring Boot提供了一个 /error 映射,以合理的方式处理所有错误,它被注册为servlet容器中的 “global” 错误页面。 对于机器客户端,它产生一个JSON响应,包含错误的细节、HTTP状态和异常消息。 对于浏览器客户端,有一个 “whitelabel” error view,以HTML格式显示相同的数据(要自定义它,添加一个 View,解析为 error)。

如果你想定制默认的错误处理行为,有一些 server.error 属性可以被设置。 参见附录中的“Server Properties”部分。

要完全替换默认行为,你可以实现 ErrorController 并注册该类型的Bean定义,或者添加 ErrorAttributes 类型的Bean来使用现有机制但替换内容。

BasicErrorController 可以作为自定义 ErrorController 的基类。 如果你想为一个新的内容类型添加一个处理程序,这是特别有用的(默认是专门处理 text/html,并为其他一切提供一个fallback)。 要做到这一点,请继承 BasicErrorController,添加一个带有 @RequestMapping 的public方法,该方法有一个 produces 属性,并创建一个新类型的Bean。

从Spring Framework 6.0开始, RFC 7807 Problem Details 被支持。Spring MVC可以用 application/problem+json 媒体类型(media type)产生自定义错误信息,比如。

{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

可以通过设置 spring.mvc.problemdetails.enabledtrue 来启用该支持。

你也可以定义一个带有 @ControllerAdvice 注释的类,为特定的controller和/或异常类型定制返回的JSON文档,如以下例子所示。

Java
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {

    @ResponseBody
    @ExceptionHandler(MyException.class)
    public ResponseEntity<?> handleControllerException(HttpServletRequest request, Throwable ex) {
        HttpStatus status = getStatus(request);
        return new ResponseEntity<>(new MyErrorBody(status.value(), ex.getMessage()), status);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer code = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
        HttpStatus status = HttpStatus.resolve(code);
        return (status != null) ? status : HttpStatus.INTERNAL_SERVER_ERROR;
    }

}
Kotlin
import jakarta.servlet.RequestDispatcher
import jakarta.servlet.http.HttpServletRequest
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler

@ControllerAdvice(basePackageClasses = [SomeController::class])
class MyControllerAdvice : ResponseEntityExceptionHandler() {

    @ResponseBody
    @ExceptionHandler(MyException::class)
    fun handleControllerException(request: HttpServletRequest, ex: Throwable): ResponseEntity<*> {
        val status = getStatus(request)
        return ResponseEntity(MyErrorBody(status.value(), ex.message), status)
    }

    private fun getStatus(request: HttpServletRequest): HttpStatus {
        val code = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE) as Int
        val status = HttpStatus.resolve(code)
        return status ?: HttpStatus.INTERNAL_SERVER_ERROR
    }

}

在前面的例子中,如果 MyException 被定义在与 SomeController 相同的包中的控制器抛出,那么就会使用 MyErrorBody POJO的JSON表示,而不是 ErrorAttributes 表示。

在某些情况下,在控制器层面处理的错误并没有被度量基础设施所记录。 应用程序可以通过将处理过的异常设置为request attribute来确保此类异常被记录在请求度量中。

Java
import jakarta.servlet.http.HttpServletRequest;

import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;

@Controller
public class MyController {

    @ExceptionHandler(CustomException.class)
    String handleCustomException(HttpServletRequest request, CustomException ex) {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex);
        return "errorView";
    }

}
Kotlin
import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.web.servlet.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler

@Controller
class MyController {

    @ExceptionHandler(CustomException::class)
    fun handleCustomException(request: HttpServletRequest, ex: CustomException?): String {
        request.setAttribute(ErrorAttributes.ERROR_ATTRIBUTE, ex)
        return "errorView"
    }

}
自定义Error页面

如果你想为一个给定的状态代码显示一个自定义的HTML错误页面,你可以在 /error 目录下添加一个文件。 错误页面可以是静态HTML(即添加在任何一个静态资源目录下),也可以通过使用模板建立。 文件的名称应该是准确的状态代码或一系列的掩码。

例如,要把 404 映射到一个静态HTML文件,你的目录结构如下。

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

通过使用FreeMarker模板来映射所有 5xx 错误,你的目录结构如下。

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.ftlh
             +- <other templates>

对于更复杂的映射,你也可以添加实现 ErrorViewResolver 接口的Bean,如下例所示。

Java
import java.util.Map;

import jakarta.servlet.http.HttpServletRequest;

import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;

public class MyErrorViewResolver implements ErrorViewResolver {

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            new ModelAndView("myview");
        }
        return null;
    }

}
Kotlin
import jakarta.servlet.http.HttpServletRequest
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver
import org.springframework.http.HttpStatus
import org.springframework.web.servlet.ModelAndView

class MyErrorViewResolver : ErrorViewResolver {

    override fun resolveErrorView(request: HttpServletRequest, status: HttpStatus,
            model: Map<String, Any>): ModelAndView? {
        // Use the request or status to optionally return a ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // We could add custom model values here
            return ModelAndView("myview")
        }
        return null
    }

}

你也可以使用常规的Spring MVC功能,如 @ExceptionHandler 方法@ControllerAdvice。 然后,ErrorController 来处理任何未处理的异常。

在Spring MVC之外映射错误页面

对于不使用Spring MVC的应用程序,你可以使用 ErrorPageRegistrar 接口来直接注册 ErrorPages。 这种抽象直接与底层的嵌入式servlet容器一起工作,即使你没有Spring MVC的 DispatcherServlet 也能工作。

Java
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;

@Configuration(proxyBeanMethods = false)
public class MyErrorPagesConfiguration {

    @Bean
    public ErrorPageRegistrar errorPageRegistrar() {
        return this::registerErrorPages;
    }

    private void registerErrorPages(ErrorPageRegistry registry) {
        registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
    }

}
Kotlin
import org.springframework.boot.web.server.ErrorPage
import org.springframework.boot.web.server.ErrorPageRegistrar
import org.springframework.boot.web.server.ErrorPageRegistry
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus

@Configuration(proxyBeanMethods = false)
class MyErrorPagesConfiguration {

    @Bean
    fun errorPageRegistrar(): ErrorPageRegistrar {
        return ErrorPageRegistrar { registry: ErrorPageRegistry -> registerErrorPages(registry) }
    }

    private fun registerErrorPages(registry: ErrorPageRegistry) {
        registry.addErrorPages(ErrorPage(HttpStatus.BAD_REQUEST, "/400"))
    }

}
如果你用一个最终由 Filter 处理的路径注册 ErrorPage(这在一些非Spring的Web框架中很常见,比如Jersey和Wicket),那么 Filter 必须明确注册为 ERROR dispatcher,如下例所示。
Java
import java.util.EnumSet;

import jakarta.servlet.DispatcherType;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyFilterConfiguration {

    @Bean
    public FilterRegistrationBean<MyFilter> myFilter() {
        FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(new MyFilter());
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType.class));
        return registration;
    }

}
Kotlin
import jakarta.servlet.DispatcherType
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.util.EnumSet

@Configuration(proxyBeanMethods = false)
class MyFilterConfiguration {

    @Bean
    fun myFilter(): FilterRegistrationBean<MyFilter> {
        val registration = FilterRegistrationBean(MyFilter())
        // ...
        registration.setDispatcherTypes(EnumSet.allOf(DispatcherType::class.java))
        return registration
    }

}

注意,默认的 FilterRegistrationBean 不包括 ERROR dispatcher类型。

War部署中的Error处理

当部署到servlet容器时,Spring Boot使用其error page filter,将具有错误状态的请求转发到适当的错误页面。 这是必要的,因为servlet规范并没有提供注册错误页面的API。 根据你部署war文件的容器和你的应用程序使用的技术,可能需要一些额外的配置。

只有在响应尚未提交的情况下,错误页面过滤器才能将请求转发到正确的错误页面。 默认情况下,WebSphere Application Server 8.0 及更高版本会在成功完成 servlet 的service方法后提交响应。 你应该通过将 com.ibm.ws.webcontainer.invokeFlushAfterService 设置为 false 来禁用这种行为。

1.1.11. CORS 跨域的支持

跨源资源共享(CORS)是由 大多数浏览器实现的 W3C规范,它可以让你以灵活的方式指定哪种跨域请求被授权,而不是使用一些不太安全和不太强大的方法,如IFRAME或JSONP。

从4.2版本开始,Spring MVC 支持CORS。在Spring Boot应用程序中使用带有 @CrossOrigin 注解的 controller方法CORS配置 不需要任何特定配置。 全局CORS配置 可以通过注册一个带有自定义 addCorsMappings(CorsRegistry) 方法的 WebMvcConfigurer bean来定义,如下例所示。

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration(proxyBeanMethods = false)
public class MyCorsConfiguration {

    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {

            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/api/**");
            }

        };
    }

}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.CorsRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration(proxyBeanMethods = false)
class MyCorsConfiguration {

    @Bean
    fun corsConfigurer(): WebMvcConfigurer {
        return object : WebMvcConfigurer {
            override fun addCorsMappings(registry: CorsRegistry) {
                registry.addMapping("/api/**")
            }
        }
    }

}

1.2. JAX-RS 和 Jersey

如果你喜欢REST端点的JAX-RS编程模型,你可以使用其中一个可用的实现,而不是Spring MVC。 JerseyApache CXF 开箱即用,效果相当好。CXF要求你将其 ServletFilter 作为 @Bean 注册在你的application context中。Jersey有一些原生的Spring支持,所以我们也在Spring Boot中为它提供了自动配置支持,同时还有一个starter。

To get started with Jersey, include the spring-boot-starter-jersey as a dependency and then you need one @Bean of type ResourceConfig in which you register all the endpoints, as shown in the following example:

import org.glassfish.jersey.server.ResourceConfig;

import org.springframework.stereotype.Component;

@Component
public class MyJerseyConfig extends ResourceConfig {

    public MyJerseyConfig() {
        register(MyEndpoint.class);
    }

}
Jersey对扫描可执行档案(归档文件)的支持是相当有限的。例如,它不能扫描在完全可执行的jar文件中发现的包中的端点,也不能在运行可执行的war文件时扫描 WEB-INF/classes 中的端点。为了避免这种限制,不应该使用 packages 方法,而应该通过使用 register 方法单独注册端点,如前面的例子所示。

对于更高级的定制,你也可以注册任意数量的实现 ResourceConfigCustomizer 的Bean。

所有注册的端点都应该是带有HTTP资源注解的 @Components (`@GET' 和其他),如下面的例子所示。

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.springframework.stereotype.Component;

@Component
@Path("/hello")
public class MyEndpoint {

    @GET
    public String message() {
        return "Hello";
    }

}

由于 Endpoint 是Spring的 @Component ,它的生命周期由Spring管理,你可以使用 @Autowired 注解来注入依赖关系,使用 @Value 注解来注入外部配置。 默认情况下,Jersey servlet被注册并映射到 /*。 你可以通过添加 @ApplicationPath 到你的 ResourceConfig 来改变映射。

默认情况下,Jersey被设置为Servlet的 @Bean 类型的 ServletRegistrationBean,名为 jerseyServletRegistration。默认情况下,servlet会被懒惰地初始化,但你可以通过设置 spring.jersey.servlet.load-on-startup 来定制这种行为。你可以通过创建一个你自己的同名Bean来禁用或覆盖该Bean。你也可以通过设置 spring.jersey.type=filter 来使用过滤器而不是servlet(在这种情况下,要替换或覆盖的 @BeanjerseyFilterRegistration)。过滤器有一个 @Order,你可以用 spring.jersey.filter.order 来设置它。当使用Jersey作为过滤器时,必须有一个处理任何未被Jersey拦截的请求的Servlet。如果你的应用程序不包含这样的servlet,你可能想通过设置 server.servlet.register-default-servlettrue 来启用默认servlet。通过使用 spring.jersey.init.* 来指定属性映射,可以给servlet和过滤器注册提供初始参数。

1.3. 嵌入式Servlet容器支持

对于Servlet应用,Spring Boot包括对嵌入式 TomcatJettyUndertow 服务器的支持。大多数开发者使用适当的 “Starter” 来获得一个完全配置的实例。默认情况下,嵌入式服务器监听 8080 端口的HTTP请求。

1.3.1. Servlet, Filter 和 Listener

当使用嵌入式servlet容器时,你可以从servlet规范中注册servlet、过滤器和所有监听器(如 HttpSessionListener),可以使用Spring beans或通过扫描servlet组件。

将Servlet、Filter和Listener注册为Spring Bean

任何作为Spring Bean的 ServletFilter 或Servlet *Listener 实例都会在嵌入式容器中注册。 如果你想在配置过程中引用 application.properties 中的一个值,这就特别方便。

默认情况下,如果上下文只包含一个Servlet,它会被映射到 /。 在有多个Servlet Bean的情况下,bean名称被用作路径前缀。 过滤器映射到 /*

如果基于惯例的映射不够灵活,你可以使用 ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean 类来完全控制。

通常,让过filter bean不排序是安全的。 如果需要一个特定的顺序,你应该用 @Order 来注解 Filter 或者让它实现 Ordered。 你不能通过用 @Order 注解其bean方法来配置 Filter 的顺序。 如果你不能改变 Filter 类来添加 @Order 或实现 Ordered,你必须为 Filter 定义一个 FilterRegistrationBean 并使用 setOrder(int) 方法设置注册Bean的顺序。 避免配置一个以 Ordered.HIGHEST_PRECEDENCE 读取请求体的过滤器,因为它可能违背你的应用程序的字符编码配置。 如果一个servlet过滤器包装了请求,它应该被配置成小于或等于 OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER 的顺序。

要查看应用程序中每个 Filter 的顺序,请为 web logging grouplogging.level.web=debug)启用调试级别的日志记录。 注册的过滤器的详细信息,包括它们的顺序和URL模式,将在启动时被记录下来。
在注册 Filter Bean时要小心,因为它们在应用程序生命周期的早期就被初始化了。 如果你需要注册一个与其他Bean交互的 Filter,请考虑使用 DelegatingFilterProxyRegistrationBean

1.3.2. Servlet Context 初始化

嵌入式Servlet容器不直接执行 jakarta.servlet.ServletContainerInitializer 接口或Spring的 org.springframework.web.WebApplicationInitializer 接口。 这是一个有意的设计决定,旨在减少设计在war中运行的第三方库可能破坏Spring Boot应用程序的风险。

如果你需要在Spring Boot应用程序中执行Servlet上下文初始化,你应该注册一个实现 org.springframework.boot.web.servlet.ServletContextInitializer 接口的bean。 单一的 onStartup 方法提供了对 ServletContext 的访问,如果有必要,可以很容易地作为现有 WebApplicationInitializer 的适配器。

对Servlet、Filter和Listener进行扫描

当使用嵌入式容器时,可以通过使用 @ServletComponentScan 来启用对 @WebServlet@WebFilter@WebListener 注解的类的自动注册。

@ServletComponentScan 在独立的容器中没有作用,而是使用容器的内置发现机制。

1.3.3. ServletWebServerApplicationContext

在底层,Spring Boot使用不同类型的 ApplicationContext 来支持嵌入式Servlet容器。 ServletWebServerApplicationContext 是一种特殊类型的 WebApplicationContext ,它通过搜索单个 ServletWebServerFactory bean来引导自己。 通常一个 TomcatServletWebServerFactoryJettyServletWebServerFactoryUndertowServletWebServerFactory 已经被自动配置。

你通常不需要知道这些实现类。 大多数应用程序都是自动配置的,适当的 ApplicationContextServletWebServerFactory 是代表你创建的。

在嵌入式容器设置中,ServletContext 被设置为服务器启动的一部分,它发生在application context初始化期间。 正因为如此,ApplicationContext 中的bean不能可靠地用 ServletContext 初始化。 解决这个问题的方法之一是将 ApplicationContext 作为Bean的依赖关系注入,只有在需要时才访问 ServletContext。 另一种方法是在服务器启动后使用一个回调。 这可以通过一个 ApplicationListener 来实现,它监听 ApplicationStartedEvent,如下所示。

import jakarta.servlet.ServletContext;

import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.web.context.WebApplicationContext;

public class MyDemoBean implements ApplicationListener<ApplicationStartedEvent> {

    private ServletContext servletContext;

    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        ApplicationContext applicationContext = event.getApplicationContext();
        this.servletContext = ((WebApplicationContext) applicationContext).getServletContext();
    }

}

1.3.4. 定制嵌入式Servlet容器

普通的servlet容器设置可以通过使用Spring的 Environment properties来进行配置。 通常情况下,你会在 application.propertiesapplication.yaml 文件中定义这些属性。

常见的Server设置包括。

  • 网络设置。传入HTTP请求的监听端口(server.port),与 server.address 绑定的接口地址,等等。

  • session设置。session是否持久(server.servlet.session.persistent),session超时(server.servlet.session.timeout),session数据的位置(server.servlet.session.store-dir),以及session cookie的配置(server.servlet.session.cookie.*)。

  • 错误管理。错误页面的位置(server.error.path),等等。

  • SSL

  • HTTP 压缩

Spring Boot尽可能地公开通用设置,但这并不总是可能的。 对于这些情况,专门的命名空间提供了针对服务器的自定义设置(见 server.tomcatserver.undertow)。 例如,access logs可以配置嵌入式Servlet容器的特定功能。

完整的列表请参见 ServerProperties 类。
Cookie SameSite

网络浏览器可以使用cookie的 SameSite 属性来控制是否以及如何在跨站请求中提交cookie。 该属性与现代网络浏览器特别相关,它们已经开始改变该属性缺失时使用的默认值。

如果你想改变会话cookie的 SameSite 属性,你可以使用 server.servlet.session.cookie.same-site 属性。 这个属性被自动配置的Tomcat、Jetty和Undertow服务器所支持。 它也被用来配置基于Spring Session servlet的 SessionRepository Bean。

例如,如果你希望你的会话cookie有一个 SameSite 属性为 None,你可以在你的 application.propertiesapplication.yaml 文件中添加以下内容。

Properties
server.servlet.session.cookie.same-site=none
Yaml
server:
  servlet:
    session:
      cookie:
        same-site: "none"

如果你想改变添加到你的 HttpServletResponse 中的其他cookie的 SameSite 属性,你可以使用一个 CookieSameSiteSupplierCookieSameSiteSupplier 被传递给一个 Cookie,并可能返回一个 SameSite 值,或 null

有一些方便的工厂和过滤器方法,你可以用来快速匹配特定的cookies。 例如,添加以下Bean将自动为所有名称与正则表达式 myapp.* 相匹配的cookies应用 LaxSameSite

Java
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {

    @Bean
    public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*");
    }

}
Kotlin
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class MySameSiteConfiguration {

    @Bean
    fun applicationCookieSameSiteSupplier(): CookieSameSiteSupplier {
        return CookieSameSiteSupplier.ofLax().whenHasNameMatching("myapp.*")
    }

}
程序化定制

如果你需要以编程方式配置你的嵌入式Servlet容器,你可以注册一个实现 WebServerFactoryCustomizer 接口的Spring Bean。 WebServerFactoryCustomizer 提供了对 ConfigurableServletWebServerFactory 的访问,其中包括许多定制设置方法。 下面的例子显示了以编程方式设置端口。

Java
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}
Kotlin
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory
import org.springframework.stereotype.Component

@Component
class MyWebServerFactoryCustomizer : WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    override fun customize(server: ConfigurableServletWebServerFactory) {
        server.setPort(9000)
    }

}

TomcatServletWebServerFactoryJettyServletWebServerFactoryUndertowServletWebServerFactoryConfigurableServletWebServerFactory 的专用变体,分别为Tomcat、Jetty和Undertow提供额外的定制设置方法。 下面的例子展示了如何定制 TomcatServletWebServerFactory ,它提供对Tomcat特定配置选项的访问。

Java
import java.time.Duration;

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class MyTomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory server) {
        server.addConnectorCustomizers((connector) -> connector.setAsyncTimeout(Duration.ofSeconds(20).toMillis()));
    }

}
Kotlin
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component
import java.time.Duration

@Component
class MyTomcatWebServerFactoryCustomizer : WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    override fun customize(server: TomcatServletWebServerFactory) {
        server.addConnectorCustomizers({ connector -> connector.asyncTimeout = Duration.ofSeconds(20).toMillis() })
    }

}
直接定制 ConfigurableServletWebServerFactory

对于需要你从 ServletWebServerFactory 中扩展出来的更高级的用例,你可以自己公开这种类型的bean。

为许多配置选项提供了Setter。 如果你需要做一些比较特殊的事情,也提供了几个protected的 “hooks” 方法 。 详情请见 源代码文档

自动配置的定制器仍然应用在你的 custom factory上,所以要小心使用这个选项。

1.3.5. JSP的局限性

当运行使用嵌入式servlet容器的Spring Boot应用程序时(并被打包成可执行档案),对JSP的支持存在一些限制。

  • 对于Jetty和Tomcat,如果你使用war打包,它应该可以工作。 一个可执行的war在用 java -jar 启动时可以工作,也可以部署到任何标准容器中。 在使用可执行jar时,不支持JSP。

  • Undertow不支持JSP。

  • 创建一个自定义 error.jsp 页面并不能覆盖Error 处理的默认视图。应该使用自定义Error页面来代替。

2. 响应式(Reactive)web应用

Spring Boot通过为Spring Webflux提供自动配置,简化了响应式Web应用的开发。

2.1. “Spring WebFlux” 框架

Spring WebFlux是Spring Framework 5.0中引入的新的响应式Web框架。与Spring MVC不同,它不需要servlet API,是完全异步和非阻塞的,并通过 Reactor 项目实现了 Reactive Streams 规范。

Spring WebFlux有两种风格:“函数(编程式)”和“注解”。 基于注解的模式与Spring MVC模式相当接近,如下面的例子所示。

Java
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class MyRestController {

    private final UserRepository userRepository;

    private final CustomerRepository customerRepository;

    public MyRestController(UserRepository userRepository, CustomerRepository customerRepository) {
        this.userRepository = userRepository;
        this.customerRepository = customerRepository;
    }

    @GetMapping("/{userId}")
    public Mono<User> getUser(@PathVariable Long userId) {
        return this.userRepository.findById(userId);
    }

    @GetMapping("/{userId}/customers")
    public Flux<Customer> getUserCustomers(@PathVariable Long userId) {
        return this.userRepository.findById(userId).flatMapMany(this.customerRepository::findByUser);
    }

    @DeleteMapping("/{userId}")
    public Mono<Void> deleteUser(@PathVariable Long userId) {
        return this.userRepository.deleteById(userId);
    }

}
Kotlin
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono

@RestController
@RequestMapping("/users")
class MyRestController(private val userRepository: UserRepository, private val customerRepository: CustomerRepository) {

    @GetMapping("/{userId}")
    fun getUser(@PathVariable userId: Long): Mono<User?> {
        return userRepository.findById(userId)
    }

    @GetMapping("/{userId}/customers")
    fun getUserCustomers(@PathVariable userId: Long): Flux<Customer> {
        return userRepository.findById(userId).flatMapMany { user: User? ->
            customerRepository.findByUser(user)
        }
    }

    @DeleteMapping("/{userId}")
    fun deleteUser(@PathVariable userId: Long): Mono<Void> {
        return userRepository.deleteById(userId)
    }

}

“WebFlux.fn”,功能变体,将路由配置与请求的实际处理分开,如下例所示。

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

    private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

    @Bean
    public RouterFunction<ServerResponse> monoRouterFunction(MyUserHandler userHandler) {
        return route()
                .GET("/{user}", ACCEPT_JSON, userHandler::getUser)
                .GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
                .DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
                .build();
    }

}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RequestPredicates.DELETE
import org.springframework.web.reactive.function.server.RequestPredicates.GET
import org.springframework.web.reactive.function.server.RequestPredicates.accept
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerResponse

@Configuration(proxyBeanMethods = false)
class MyRoutingConfiguration {

    @Bean
    fun monoRouterFunction(userHandler: MyUserHandler): RouterFunction<ServerResponse> {
        return RouterFunctions.route(
            GET("/{user}").and(ACCEPT_JSON), userHandler::getUser).andRoute(
            GET("/{user}/customers").and(ACCEPT_JSON), userHandler::getUserCustomers).andRoute(
            DELETE("/{user}").and(ACCEPT_JSON), userHandler::deleteUser)
    }

    companion object {
        private val ACCEPT_JSON = accept(MediaType.APPLICATION_JSON)
    }

}
Java
import reactor.core.publisher.Mono;

import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

@Component
public class MyUserHandler {

    public Mono<ServerResponse> getUser(ServerRequest request) {
        ...
    }

    public Mono<ServerResponse> getUserCustomers(ServerRequest request) {
        ...
    }

    public Mono<ServerResponse> deleteUser(ServerRequest request) {
        ...
    }

}
Kotlin
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class MyUserHandler {

    fun getUser(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

    fun getUserCustomers(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

    fun deleteUser(request: ServerRequest?): Mono<ServerResponse> {
        return ServerResponse.ok().build()
    }

}

WebFlux是Spring框架的一部分,详细的信息可以在其 参考文档 中找到。

你可以定义任意多的 RouterFunction bean,以使router的定义模块化。如果你需要应用一个优先级,Bean可以被排序。

要开始使用,请将 spring-boot-starter-webflux 模块添加到你的应用程序中。

在你的应用程序中同时添加 spring-boot-starter-webspring-boot-starter-webflux 模块会导致Spring Boot自动配置Spring MVC,而不是WebFlux。之所以选择这种行为,是因为许多Spring开发者将 spring-boot-starter-webflux 添加到他们的Spring MVC应用中,只是为了使用响应式 WebClient。你仍然可以通过将选择的应用程序类型设置为 SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE) 来执行你的选择。

2.1.1. Spring WebFlux 自动配置

Spring Boot为Spring WebFlux提供了自动配置,对大多数应用程序都很适用。

自动配置在Spring的默认值基础上增加了以下功能。

如果你想保留Spring Boot的WebFlux功能,并想添加额外的WebFlux配置,你可以添加自己的 @Configuration 类,类型为 WebFlux configuration,但不包括 @EnableWebFlux

如果你想完全控制Spring WebFlux,你可以添加你自己的 @Configuration ,用 @EnableWebFlux 注解。

2.1.2. HttpMessageReader 和 HttpMessageWriter (HTT消息编解码器)

Spring WebFlux使用 HttpMessageReaderHttpMessageWriter 接口来转换HTTP请求和响应。通过查看classpath中可用的库,用 CodecConfigurer 对它们进行配置,以获得合理的默认值。

Spring Boot为编解码器提供了专门的配置属性,即 spring.codec.*。它还通过使用 CodecCustomizer 实例应用进一步的定制。例如,spring.jackson.* 配置key被应用于Jackson编解码器。

如果你需要添加或定制编解码器,你可以创建一个自定义的 CodecCustomizer 组件,如下例所示。

Java
import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.ServerSentEventHttpMessageReader;

@Configuration(proxyBeanMethods = false)
public class MyCodecsConfiguration {

    @Bean
    public CodecCustomizer myCodecCustomizer() {
        return (configurer) -> {
            configurer.registerDefaults(false);
            configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
            // ...
        };
    }

}
Kotlin
import org.springframework.boot.web.codec.CodecCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.http.codec.CodecConfigurer
import org.springframework.http.codec.ServerSentEventHttpMessageReader

class MyCodecsConfiguration {

    @Bean
    fun myCodecCustomizer(): CodecCustomizer {
        return CodecCustomizer { configurer: CodecConfigurer ->
            configurer.registerDefaults(false)
            configurer.customCodecs().register(ServerSentEventHttpMessageReader())
        }
    }

}

2.1.3. 静态内容

默认情况下,Spring Boot从classpath中名为 /static(或 /public/resources/META-INF/resources)的目录提供静态内容。它使用Spring WebFlux的 ResourceWebHandler,因此你可以通过添加你自己的 WebFluxConfigurer 并重写 addResourceHandlers 方法来修改该行为。

默认情况下,资源被映射到 /**,但你可以通过设置 spring.webflux.static-path-pattern 属性进行调整。例如,将所有资源重新定位到 /resources/**,可以通过以下方式实现。

Properties
spring.webflux.static-path-pattern=/resources/**
Yaml
spring:
  webflux:
    static-path-pattern: "/resources/**"

你也可以通过使用 spring.web.resources.static-locations 自定义静态资源的位置。这样做将默认值替换为目录位置的列表。如果你这样做,默认的欢迎页面检测会切换到你的自定义位置。因此,如果在启动时,在你的任何位置有一个 index.html,它就是应用程序的主页。

除了前面列出的 "标准" 静态资源位置外,还为 Webjars内容 提供了一种特殊情况。默认情况下,任何路径为 /webjars/** 的资源,如果是以Webjars格式打包的,则从jar文件中提供。路径可以通过 spring.webflux.webjars-path-pattern 属性来定制。

Spring WebFlux应用程序并不严格依赖servlet API,因此它们不能作为war文件部署,也不使用 src/main/webapp 目录。

2.1.4. 欢迎页面

Spring Boot同时支持静态和模板化的欢迎页面。它首先在配置的静态内容位置寻找 index.html 文件。如果没有找到,它就会寻找一个 index 模板。如果找到了其中之一,它就会自动作为应用程序的欢迎页面。

2.1.5. 模板引擎

除了REST Web服务,你还可以使用Spring WebFlux来提供动态HTML内容。Spring WebFlux支持各种模板技术,包括Thymeleaf、FreeMarker和Mustache。

Spring Boot包括对以下模板引擎的自动配置支持。

当你使用这些模板引擎的默认配置时,你的模板会自动从 src/main/resources/templates 中获取。

2.1.6. Error 处理

Spring Boot提供了一个 WebExceptionHandler,以合理的方式处理所有错误。它在处理顺序中的位置紧挨着WebFlux提供的处理程序,后者被认为是最后一个。对于机器客户端,它产生一个JSON响应,包含错误的细节、HTTP状态和异常消息。对于浏览器客户端,有一个 “whitelabel” 错误处理程序,以HTML格式呈现相同的数据。你也可以提供你自己的HTML模板来显示错误(见下一节)。

在直接定制 Spring Boot 中的error处理之前,你可以利用Spring WebFlux中的 RFC 7807 Problem Details 支持。 Spring WebFlux可以用 application/problem+json 媒体类型(media type)产生自定义错误信息,比如。

{
  "type": "https://example.org/problems/unknown-project",
  "title": "Unknown project",
  "status": 404,
  "detail": "No project found for id 'spring-unknown'",
  "instance": "/projects/spring-unknown"
}

可以通过设置 spring.webflux.problemdetails.enabledtrue 来启用该支持。

定制这一功能的第一步通常是使用现有的机制,但要替换或增强错误内容。为此,你可以添加一个 ErrorAttributes 类型的bean。

要改变Error处理行为,你可以实现 ErrorWebExceptionHandler 并注册该类型的Bean定义。由于 ErrorWebExceptionHandler 是相当低级的,Spring Boot还提供了一个方便的 AbstractErrorWebExceptionHandler,让你以WebFlux的功能方式来处理错误,如下例所示。

Java
import reactor.core.publisher.Mono;

import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.ServerResponse.BodyBuilder;

@Component
public class MyErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    public MyErrorWebExceptionHandler(ErrorAttributes errorAttributes, Resources resources,
            ApplicationContext applicationContext) {
        super(errorAttributes, resources, applicationContext);
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml);
    }

    private boolean acceptsXml(ServerRequest request) {
        return request.headers().accept().contains(MediaType.APPLICATION_XML);
    }

    public Mono<ServerResponse> handleErrorAsXml(ServerRequest request) {
        BodyBuilder builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR);
        // ... additional builder calls
        return builder.build();
    }

}
Kotlin
import org.springframework.boot.autoconfigure.web.WebProperties
import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

@Component
class MyErrorWebExceptionHandler(errorAttributes: ErrorAttributes?, resources: WebProperties.Resources?,
    applicationContext: ApplicationContext?) : AbstractErrorWebExceptionHandler(errorAttributes, resources, applicationContext) {

    override fun getRoutingFunction(errorAttributes: ErrorAttributes): RouterFunction<ServerResponse> {
        return RouterFunctions.route(this::acceptsXml, this::handleErrorAsXml)
    }

    private fun acceptsXml(request: ServerRequest): Boolean {
        return request.headers().accept().contains(MediaType.APPLICATION_XML)
    }

    fun handleErrorAsXml(request: ServerRequest?): Mono<ServerResponse> {
        val builder = ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
        // ... additional builder calls
        return builder.build()
    }

}

为了更全面地了解情况,你也可以直接继承 DefaultErrorWebExceptionHandler,并重写特定的方法。

在某些情况下,在控制器或处理函数层面上处理的错误并没有被度量基础设施所记录。应用程序可以通过将处理过的异常设置为request attribute来确保这些异常被记录在请求度量中。

Java
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;

@Controller
public class MyExceptionHandlingController {

    @GetMapping("/profile")
    public Rendering userProfile() {
        // ...
        throw new IllegalStateException();
    }

    @ExceptionHandler(IllegalStateException.class)
    public Rendering handleIllegalState(ServerWebExchange exchange, IllegalStateException exc) {
        exchange.getAttributes().putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc);
        return Rendering.view("errorView").modelAttribute("message", exc.getMessage()).build();
    }

}
Kotlin
import org.springframework.boot.web.reactive.error.ErrorAttributes
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.reactive.result.view.Rendering
import org.springframework.web.server.ServerWebExchange

@Controller
class MyExceptionHandlingController {

    @GetMapping("/profile")
    fun userProfile(): Rendering {
        // ...
        throw IllegalStateException()
    }

    @ExceptionHandler(IllegalStateException::class)
    fun handleIllegalState(exchange: ServerWebExchange, exc: IllegalStateException): Rendering {
        exchange.attributes.putIfAbsent(ErrorAttributes.ERROR_ATTRIBUTE, exc)
        return Rendering.view("errorView").modelAttribute("message", exc.message ?: "").build()
    }

}
自定义Error页面

如果你想为一个给定的状态代码显示一个自定义的HTML错误页面,你可以添加从 error/* 解析的视图,例如通过添加文件到 /error 目录。错误页面可以是静态HTML(即添加在任何一个静态资源目录下),也可以用模板构建。文件的名称应该是准确的状态代码,一个状态代码系列的掩码,或者如果没有其他匹配的,就用 error 作为默认。注意,默认的错误视图的路径是 error/error,而对于Spring MVC,默认的错误视图是 error

例如,要把 404 映射到一个静态HTML文件,你的目录结构如下。

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
             +- <other public assets>

通过使用Mustache模板来映射所有 5xx 错误,你的目录结构如下。

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.mustache
             +- <other templates>

2.1.7. Web Filter

Spring WebFlux提供了一个 WebFilter 接口,可以实现它来过滤HTTP request/response exchange。 在应用程序上下文中发现的 WebFilter Bean 将被自动用于过滤每个exchange。

如果过滤器的顺序很重要,它们可以实现 Ordered 或者用 @Order 来注解。Spring Boot的自动配置可以为你配置web过滤器。当它这样做时,将使用下表所示的顺序。

Web Filter 顺序

ServerHttpObservationFilter (Micrometer Observability)

Ordered.HIGHEST_PRECEDENCE + 1

WebFilterChainProxy (Spring Security)

-100

HttpExchangesWebFilter

Ordered.LOWEST_PRECEDENCE - 10

2.2. 嵌入式响应式服务器支持

Spring Boot包括对以下嵌入式反应式Web服务器的支持。Reactor Netty、Tomcat、Jetty和Undertow。 大多数开发者使用适当的 "Starter" 来获得一个完全配置的实例。 默认情况下,嵌入式服务器监听8080端口的HTTP请求。

2.3. 响应式服务器资源配置

在自动配置Reactor Netty或Jetty服务器时,Spring Boot将创建特定的Bean,为服务器实例提供HTTP资源: ReactorResourceFactoryJettyResourceFactory

默认情况下,这些资源也将与Reactor Netty和Jetty客户端共享,以获得最佳性能,因为。

  • 服务器和客户端使用相同的技术

  • 客户端实例是使用Spring Boot自动配置的 WebClient.Builder Bean建立的。

开发者可以通过提供自定义的 ReactorResourceFactoryJettyResourceFactory bean来覆盖Jetty和Reactor Netty的资源配置 - 这将同时应用于客户端和服务器。

你可以在WebClient Runtime部分了解更多关于客户端的资源配置。

3. 优雅停机

所有四个嵌入式Web服务器(Jetty、Reactor Netty、Tomcat和Undertow)以及基于响应式和Servlet的Web应用都支持优雅关闭。 它作为关闭应用程序上下文的一部分发生,并在停止 SmartLifecycle bean的最早阶段执行。 这种停止处理使用一个超时,提供一个宽限期,在此期间,现有的请求将被允许完成,但不允许有新的请求。 不允许新请求的确切方式取决于正在使用的网络服务器。 Jetty、Reactor Netty和Tomcat将在网络层停止接受请求。 Undertow将接受请求,但立即响应服务不可用(503)的回应。

使用Tomcat的优雅关机需要Tomcat 9.0.33或更高版本。

要启用优雅关机,配置 server.shutdown 属性,如下例所示。

Properties
server.shutdown=graceful
Yaml
server:
  shutdown: "graceful"

要配置超时时间,请配置 spring.lifecycle.timeout-per-shutdown-phase 属性,如以下例子所示。

Properties
spring.lifecycle.timeout-per-shutdown-phase=20s
Yaml
spring:
  lifecycle:
    timeout-per-shutdown-phase: "20s"
如果你的IDE没有发送正确的 SIGTERM 信号,使用优雅关机可能无法正常工作。请参阅你的IDE的文档以了解更多细节。

4. Spring Security

如果 Spring Security 在classpath上,那么Web应用程序默认是安全的。Spring Boot依靠Spring Security的内容协商策略来决定是使用 httpBasic 还是 formLogin。要给Web应用添加方法级安全,你也可以添加 @EnableGlobalMethodSecurity ,并加上你想要的设置。其他信息可以在 《Spring安全参考指南》中找到。

默认的 UserDetailsService 有一个用户。用户名是 user,密码是随机的,在应用程序启动时打印在 WARN 级别,如下面的例子所示。

Using generated security password: 78fa095d-3f4c-48b1-ad50-e24c31d5cf35

This generated password is for development use only. Your security configuration must be updated before running your application in production.
如果你微调了你的日志配置,确保 org.springframework.boot.autoconfigure.security 类被设置为log WARN 级别的消息。否则,就不会打印默认密码。

你可以通过提供 spring.security.user.namespring.security.user.password 来改变用户名和密码。

在web应用中默认提供了如下特性。

  • 一个 UserDetailsService (如果是WebFlux应用程序,则为 ReactiveUserDetailsService)bean,具有内存存储和一个具有生成密码的单一用户(用户的属性见 SecurityProperties.User)。

  • 整个应用程序(包括actuator端点,如果actuator在classpath上)的基于表单的登录或HTTP Basic 安全(取决于请求中的 Accept 头)。

  • 一个 DefaultAuthenticationEventPublisher 用于发布认证事件。

你可以通过为它添加一个bean来提供不同的 AuthenticationEventPublisher

4.1. MVC Security

默认的security配置是由 SecurityAutoConfigurationUserDetailsServiceAutoConfiguration 实现的。SecurityAutoConfiguration 导入 SpringBootWebSecurityConfiguration 用于web security, UserDetailsServiceAutoConfiguration 配置认证,这在非Web应用中也是相关的。要完全关闭默认的Web应用安全配置,或结合多个Spring安全组件,如OAuth2客户端和资源服务器,请添加一个 SecurityFilterChain 类型的bean(这样做不会禁用 UserDetailsService 配置或 Actuator 的安全性)。

要关闭 UserDetailsService 配置,你可以添加一个 UserDetailsServiceAuthenticationProviderAuthenticationManager 类型的bean。

访问规则可以通过添加自定义的 SecurityFilterChain bean 来重写。 Spring Boot提供了方便的方法,可以用来覆盖执行器端点和静态资源的访问规则。 EndpointRequest 可以用来创建一个 RequestMatcher,它基于 management.endpoints.web.base-path 属性。 PathRequest 可以用来创建一个 RequestMatcher,用于常用位置的资源。

4.2. WebFlux Security

与Spring MVC应用程序类似,你可以通过添加 spring-boot-starter-security 依赖项来保护你的WebFlux应用程序。 默认的安全配置在 ReactiveSecurityAutoConfigurationUserDetailsServiceAutoConfiguration 中实现。 ReactiveSecurityAutoConfiguration 导入 WebFluxSecurityConfiguration 用于web security,UserDetailsServiceAutoConfiguration 配置认证,这在非web应用中也很重要。 要完全关闭默认的Web应用安全配置,你可以添加一个 WebFilterChainProxy 类型的bean(这样做不会禁用 UserDetailsService 配置或Actuator的安全)。

为了关闭 UserDetailsService 配置,你可以添加一个 ReactiveUserDetailsServiceReactiveAuthenticationManager 类型的bean。

访问规则和多个Spring安全组件的使用,如OAuth 2客户端和资源服务器,可以通过添加自定义的 SecurityWebFilterChain bean进行配置。 Spring Boot提供了方便的方法,可用于覆actuator端点和静态资源的访问规则。 EndpointRequest 可以用来创建一个 ServerWebExchangeMatcher,它是基于 management.endpoints.web.base-path 属性。

PathRequest 可以用来为常用位置的资源创建一个 ServerWebExchangeMatcher

例如,你可以通过添加以下内容来定制你的安全配置。

Java
import org.springframework.boot.autoconfigure.security.reactive.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration(proxyBeanMethods = false)
public class MyWebFluxSecurityConfiguration {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.authorizeExchange((exchange) -> {
            exchange.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll();
            exchange.pathMatchers("/foo", "/bar").authenticated();
        });
        http.formLogin(withDefaults());
        return http.build();
    }

}
Kotlin
import org.springframework.boot.autoconfigure.security.reactive.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.Customizer
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.web.server.SecurityWebFilterChain

@Configuration(proxyBeanMethods = false)
class MyWebFluxSecurityConfiguration {

    @Bean
    fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
        http.authorizeExchange { spec ->
            spec.matchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
            spec.pathMatchers("/foo", "/bar").authenticated()
        }
        http.formLogin(withDefaults())
        return http.build()
    }

}

4.3. OAuth2

OAuth2 is a widely used authorization framework that is supported by Spring.

4.3.1. 客户端(Client)

如果你的classpath上有 spring-security-oauth2-client,你可以利用一些自动配置来设置 OAuth2/Open ID Connect 客户端。 这种配置利用了 OAuth2ClientProperties 下的属性。 同样的属性也适用于servlet和响应式应用程序。

你可以在 spring.security.oauth2.client 前缀下注册多个OAuth2客户端和提供者(provider),如以下例子所示。

Properties
spring.security.oauth2.client.registration.my-login-client.client-id=abcd
spring.security.oauth2.client.registration.my-login-client.client-secret=password
spring.security.oauth2.client.registration.my-login-client.client-name=Client for OpenID Connect
spring.security.oauth2.client.registration.my-login-client.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-login-client.scope=openid,profile,email,phone,address
spring.security.oauth2.client.registration.my-login-client.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.my-login-client.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-login-client.authorization-grant-type=authorization_code

spring.security.oauth2.client.registration.my-client-1.client-id=abcd
spring.security.oauth2.client.registration.my-client-1.client-secret=password
spring.security.oauth2.client.registration.my-client-1.client-name=Client for user scope
spring.security.oauth2.client.registration.my-client-1.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-1.scope=user
spring.security.oauth2.client.registration.my-client-1.redirect-uri={baseUrl}/authorized/user
spring.security.oauth2.client.registration.my-client-1.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-1.authorization-grant-type=authorization_code

spring.security.oauth2.client.registration.my-client-2.client-id=abcd
spring.security.oauth2.client.registration.my-client-2.client-secret=password
spring.security.oauth2.client.registration.my-client-2.client-name=Client for email scope
spring.security.oauth2.client.registration.my-client-2.provider=my-oauth-provider
spring.security.oauth2.client.registration.my-client-2.scope=email
spring.security.oauth2.client.registration.my-client-2.redirect-uri={baseUrl}/authorized/email
spring.security.oauth2.client.registration.my-client-2.client-authentication-method=client_secret_basic
spring.security.oauth2.client.registration.my-client-2.authorization-grant-type=authorization_code

spring.security.oauth2.client.provider.my-oauth-provider.authorization-uri=https://my-auth-server.com/oauth2/authorize
spring.security.oauth2.client.provider.my-oauth-provider.token-uri=https://my-auth-server.com/oauth2/token
spring.security.oauth2.client.provider.my-oauth-provider.user-info-uri=https://my-auth-server.com/userinfo
spring.security.oauth2.client.provider.my-oauth-provider.user-info-authentication-method=header
spring.security.oauth2.client.provider.my-oauth-provider.jwk-set-uri=https://my-auth-server.com/oauth2/jwks
spring.security.oauth2.client.provider.my-oauth-provider.user-name-attribute=name
Yaml
spring:
  security:
    oauth2:
      client:
        registration:
          my-login-client:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for OpenID Connect"
            provider: "my-oauth-provider"
            scope: "openid,profile,email,phone,address"
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

          my-client-1:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for user scope"
            provider: "my-oauth-provider"
            scope: "user"
            redirect-uri: "{baseUrl}/authorized/user"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

          my-client-2:
            client-id: "abcd"
            client-secret: "password"
            client-name: "Client for email scope"
            provider: "my-oauth-provider"
            scope: "email"
            redirect-uri: "{baseUrl}/authorized/email"
            client-authentication-method: "client_secret_basic"
            authorization-grant-type: "authorization_code"

        provider:
          my-oauth-provider:
            authorization-uri: "https://my-auth-server.com/oauth2/authorize"
            token-uri: "https://my-auth-server.com/oauth2/token"
            user-info-uri: "https://my-auth-server.com/userinfo"
            user-info-authentication-method: "header"
            jwk-set-uri: "https://my-auth-server.com/oauth2/jwks"
            user-name-attribute: "name"

对于支持 OpenID Connect discovery 的OpenID Connect provider,配置可以进一步简化。提供者需要配置一个 issuer-uri,这是它作为其 Issuer Identifier 所主张的 URI。例如,如果提供的发行者标识符是 "https://example.com",那么"OpenID Provider Configuration Request" 将被发送到 "https://example.com/.well-known/openid-configuration"。结果将是一个 "OpenID Provider Configuration Response"。下面的例子显示了如何用发行者标识符来配置OpenID Connect provider。

Properties
spring.security.oauth2.client.provider.oidc-provider.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
Yaml
spring:
  security:
    oauth2:
      client:
        provider:
          oidc-provider:
            issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"

默认情况下,Spring Security的 OAuth2LoginAuthenticationFilter 只处理匹配 /login/oauth2/code/* 的URL。 如果你想自定义 redirect-uri 以使用不同的模式,你需要提供configuration来处理该自定义模式。 例如,对于servlet应用程序,你可以添加你自己的 SecurityFilterChain,类似于以下内容。

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
public class MyOAuthClientConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((requests) -> requests
                .anyRequest().authenticated()
            )
            .oauth2Login((login) -> login
                .redirectionEndpoint((endpoint) -> endpoint
                    .baseUri("/login/oauth2/callback/*")
                )
            );
        return http.build();
    }

}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.invoke
import org.springframework.security.web.SecurityFilterChain

@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
open class MyOAuthClientConfiguration {

    @Bean
    open fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeHttpRequests {
                authorize(anyRequest, authenticated)
            }
            oauth2Login {
                redirectionEndpoint {
                    baseUri = "/login/oauth2/callback/*"
                }
            }
        }
        return http.build()
    }

}
Spring Boot自动配置了一个 InMemoryOAuth2AuthorizedClientService,它被Spring Security用来管理客户端注册。 InMemoryOAuth2AuthorizedClientService 的功能有限,我们建议只在开发环境中使用它。 对于生产环境,请考虑使用 JdbcOAuth2AuthorizedClientService 或创建你自己的 OAuth2AuthorizedClientService 实现。
常见provider的OAuth2客户注册

对于常见的OAuth2和OpenID provider,包括Google、Github、Facebook和Okta,我们提供了一组provider的默认值(分别为 googlegithubfacebookokta)。

如果你不需要定制这些provider,你可以将 provider 属性设置为你需要推断默认值的那个。 另外,如果客户端注册的密钥与默认支持的provider相匹配,Spring Boot也会推断出这一点。

换句话说,下面例子中的两个配置使用的是Google的提供者。

Properties
spring.security.oauth2.client.registration.my-client.client-id=abcd
spring.security.oauth2.client.registration.my-client.client-secret=password
spring.security.oauth2.client.registration.my-client.provider=google
spring.security.oauth2.client.registration.google.client-id=abcd
spring.security.oauth2.client.registration.google.client-secret=password
Yaml
spring:
  security:
    oauth2:
      client:
        registration:
          my-client:
            client-id: "abcd"
            client-secret: "password"
            provider: "google"
          google:
            client-id: "abcd"
            client-secret: "password"

4.3.2. 资源服务器(Resource Server)

如果你的classpath上有 spring-security-oauth2-resource-server,Spring Boot可以设置一个OAuth2资源服务器。 对于JWT配置,需要指定JWK Set URI或OIDC Issuer URI,如以下例子所示。

Properties
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://example.com/oauth2/default/v1/keys
Yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: "https://example.com/oauth2/default/v1/keys"
Properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=https://dev-123456.oktapreview.com/oauth2/default/
Yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: "https://dev-123456.oktapreview.com/oauth2/default/"
如果授权服务器不支持JWK Set URI,你可以用用于验证JWT签名的公钥来配置资源服务器。 这可以使用 spring.security.oauth2.resourceserver.jwt.public-key-location 属性来完成,其中的值需要指向一个包含 PEM 编码 x509 格式的公钥的文件。

spring.security.oauth2.resourceserver.jwt.audiences 属性可用于指定 JWT 中 aud claim 的预期值。例如,要求 JWT 包含一个值为 my-audience 的 aud claim:

Properties
spring.security.oauth2.resourceserver.jwt.audiences[0]=my-audience
Yaml
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          audiences:
            - "my-audience"

同样的属性适用于Servlet和响应式应用程序。

另外,你可以为Servlet应用程序定义你自己的 JwtDecoder bean,或者为响应式应用程序定义 ReactiveJwtDecoder。 在使用 opaque token 而不是JWT的情况下,你可以配置以下属性,通过自内省(introspection)来验证令牌。

Properties
spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=https://example.com/check-token
spring.security.oauth2.resourceserver.opaquetoken.client-id=my-client-id
spring.security.oauth2.resourceserver.opaquetoken.client-secret=my-client-secret
Yaml
spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: "https://example.com/check-token"
          client-id: "my-client-id"
          client-secret: "my-client-secret"

同样,同样的属性也适用于servlet和reactive应用程序。

另外,你可以为Servlet应用程序定义你自己的 OpaqueTokenIntrospector bean,或者为响应式应用程序定义 ReactiveOpaqueTokenIntrospector

4.3.3. 授权服务器(Authorization Server)

如果你的classpath上有 spring-security-oauth2-authorization-server,你可以利用一些自动配置来设置一个基于Servlet的OAuth2授权服务器。

你可以在 spring.security.oauth2.authorizationserver.client 前缀下注册多个 OAuth2 客户端,如以下例子所示:

Properties
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-id=abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-secret={noop}secret1
spring.security.oauth2.authorizationserver.client.my-client-1.registration.client-authentication-methods[0]=client_secret_basic
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[0]=authorization_code
spring.security.oauth2.authorizationserver.client.my-client-1.registration.authorization-grant-types[1]=refresh_token
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[0]=https://my-client-1.com/login/oauth2/code/abcd
spring.security.oauth2.authorizationserver.client.my-client-1.registration.redirect-uris[1]=https://my-client-1.com/authorized
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[0]=openid
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[1]=profile
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[2]=email
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[3]=phone
spring.security.oauth2.authorizationserver.client.my-client-1.registration.scopes[4]=address
spring.security.oauth2.authorizationserver.client.my-client-1.require-authorization-consent=true
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-id=efgh
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-secret={noop}secret2
spring.security.oauth2.authorizationserver.client.my-client-2.registration.client-authentication-methods[0]=client_secret_jwt
spring.security.oauth2.authorizationserver.client.my-client-2.registration.authorization-grant-types[0]=client_credentials
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[0]=user.read
spring.security.oauth2.authorizationserver.client.my-client-2.registration.scopes[1]=user.write
spring.security.oauth2.authorizationserver.client.my-client-2.jwk-set-uri=https://my-client-2.com/jwks
spring.security.oauth2.authorizationserver.client.my-client-2.token-endpoint-authentication-signing-algorithm=RS256
Yaml
spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client-1:
            registration:
              client-id: "abcd"
              client-secret: "{noop}secret1"
              client-authentication-methods:
                - "client_secret_basic"
              authorization-grant-types:
                - "authorization_code"
                - "refresh_token"
              redirect-uris:
                - "https://my-client-1.com/login/oauth2/code/abcd"
                - "https://my-client-1.com/authorized"
              scopes:
                - "openid"
                - "profile"
                - "email"
                - "phone"
                - "address"
            require-authorization-consent: true
          my-client-2:
            registration:
              client-id: "efgh"
              client-secret: "{noop}secret2"
              client-authentication-methods:
                - "client_secret_jwt"
              authorization-grant-types:
                - "client_credentials"
              scopes:
                - "user.read"
                - "user.write"
            jwk-set-uri: "https://my-client-2.com/jwks"
            token-endpoint-authentication-signing-algorithm: "RS256"
client-secret 属性必须是可以被配置的 PasswordEncoder 匹配的格式。 PasswordEncoder 的默认实例是通过 PasswordEncoderFactories.createDelegatingPasswordEncoder() 创建。

Spring Boot为Spring Authorization Server提供的自动配置是为快速入门而设计的。大多数应用程序都需要进行定制,并希望定义几个Bean来覆盖自动配置。

以下组件可以被定义为 Bean,以覆盖 Spring 授权服务器特有的自动配置:

  • RegisteredClientRepository

  • AuthorizationServerSettings

  • SecurityFilterChain

  • com.nimbusds.jose.jwk.source.JWKSource<com.nimbusds.jose.proc.SecurityContext>

  • JwtDecoder

Spring Boot 自动配置了一个 InMemoryRegisteredClientRepository,它被Spring 授权服务器用来管理注册客户。InMemoryRegisteredClientRepository 的功能有限,我们建议只在开发环境中使用它。对于生产环境,请考虑使用 JdbcRegisteredClientRepository 或创建你自己的 RegisteredClientRepository 的实现。

其他信息可以在 Spring Authorization Server 参考文档入门 章节找到。

4.4. SAML 2.0

4.4.1. 信赖方(Relying Party)

如果你的classpath上有 spring-security-saml2-service-provider,你可以利用一些自动配置来设置一个SAML 2.0的信赖方。这个配置利用了 Saml2RelyingPartyProperties 下面的属性。

依赖方注册代表了身份提供者(IDP)和服务提供商(SP)之间的配对配置。你可以在 spring.security.saml2.relyingparty 前缀下注册多个依赖方,如以下例子所示。

Properties
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.response-url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party1.singlelogout.binding=POST
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.verification.credentials[0].certificate-location=path-to-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.entity-id=remote-idp-entity-id1
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.sso-url=https://remoteidp1.sso.url

spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=path-to-private-key
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=path-to-certificate
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.verification.credentials[0].certificate-location=path-to-other-verification-cert
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.entity-id=remote-idp-entity-id2
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.sso-url=https://remoteidp2.sso.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.url=https://remoteidp2.slo.url
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.response-url=https://myapp/logout/saml2/slo
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.singlelogout.binding=POST
Yaml
spring:
  security:
    saml2:
      relyingparty:
        registration:
          my-relying-party1:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            singlelogout:
               url: "https://myapp/logout/saml2/slo"
               response-url: "https://remoteidp2.slo.url"
               binding: "POST"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "path-to-verification-cert"
              entity-id: "remote-idp-entity-id1"
              sso-url: "https://remoteidp1.sso.url"

          my-relying-party2:
            signing:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            decryption:
              credentials:
              - private-key-location: "path-to-private-key"
                certificate-location: "path-to-certificate"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "path-to-other-verification-cert"
              entity-id: "remote-idp-entity-id2"
              sso-url: "https://remoteidp2.sso.url"
              singlelogout:
                url: "https://remoteidp2.slo.url"
                response-url: "https://myapp/logout/saml2/slo"
                binding: "POST"

对于SAML2注销,默认情况下,Spring Security的 Saml2LogoutRequestFilterSaml2LogoutResponseFilter 只处理匹配 /logout/saml2/slo 的URL。 如果你想自定义AP发起的注销请求的 url,或AP发送注销响应的 response-url,使用不同的模式,你需要提供configuration来处理该自定义模式。 例如,对于servlet应用程序,你可以添加你自己的 SecurityFilterChain,类似于以下内容。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration(proxyBeanMethods = false)
public class MySamlRelyingPartyConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((requests) -> requests.anyRequest().authenticated());
        http.saml2Login(withDefaults());
        http.saml2Logout((saml2) -> saml2.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
            .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2")));
        return http.build();
    }

}

5. Spring Session

Spring Boot为各种数据存储提供 Spring Session 自动配置。 在构建一个servlet Web应用程序时,可以自动配置以下存储。

  • Redis

  • JDBC

  • Hazelcast

  • MongoDB

此外, Spring Boot for Apache Geode 提供了自动配置,以 使用Apache Geode作为会话存储

该Servlet自动配置取代了使用 @Enable*HttpSession 的需要。

如果classpath上有一个Spring Session模块,Spring Boot会自动使用该存储实现。 如果你有一个以上的实现,Spring Boot使用以下顺序来选择特定的实现。

  1. Redis

  2. JDBC

  3. Hazelcast

  4. MongoDB

  5. 如果Redis、JDBC、Hazelcast和MongoDB都不可用,我们就不配置 SessionRepository

当建立一个reactive(响应式)web应用程序时,可以自动配置以下store。

  • Redis

  • MongoDB

响应式自动配置取代了使用 @Enable*WebSession 的需要。

与servlet配置类似,如果你有一个以上的实现,Spring Boot使用以下顺序来选择特定的实现。

  1. Redis

  2. MongoDB

  3. 如果Redis和MongoDB都不可用,我们就不配置 ReactiveSessionRepository

每个store都有特定的附加设置。 例如,可以定制JDBC存储的表的名称,如下例所示。

Properties
spring.session.jdbc.table-name=SESSIONS
Yaml
spring:
  session:
    jdbc:
      table-name: "SESSIONS"

为了设置session的超时,你可以使用 spring.session.timeout 属性。 如果该属性在servlet web应用程序中没有设置,自动配置会退回到 server.servlet.session.timeout 的值。

你可以使用 @Enable*HttpSession(servlet)或 @Enable*WebSession(reactive)来控制Spring Session的配置。 这将导致自动配置的后退。 然后,Spring Session可以使用注解的属性进行配置,而不是之前描述的配置属性。

6. Spring for GraphQL

如果你想构建GraphQL应用程序,你可以利用Spring Boot对 Spring for GraphQL 的自动配置。Spring for GraphQL项目是基于 GraphQL Java 的。你至少需要 spring-boot-starter-graphql starter。因为GraphQL与传输无关,你还需要在你的应用程序中拥有一个或多个额外的 starter,以便在网络上公开你的GraphQL API。

Starter 传输 实现

spring-boot-starter-web

HTTP

Spring MVC

spring-boot-starter-websocket

WebSocket

用于Servlet应用程序的WebSocket

spring-boot-starter-webflux

HTTP, WebSocket

Spring WebFlux

spring-boot-starter-rsocket

TCP, WebSocket

在Reactor Netty上运行的Spring WebFlux

6.1. GraphQL Schema

一个Spring GraphQL应用程序在启动时需要一个定义好的schema。 默认情况下,你可以在 src/main/resources/graphql/** 下编写 ".graphqls" 或 ".gqls" schema文件,Spring Boot会自动接收它们。

你可以用 spring.graphql.schema.locations 来定制文件的位置,用 spring.graphql.schema.file-extensions 来定制文件的扩展名称。

如果你想让Spring Boot检测你所有应用模块中的schema文件,以及该位置的依赖。 你可以将 spring.graphql.schema.locations 设置为 "classpath*:graphql/**/" (注意 classpath*: 前缀)。

在下面的章节中,我们将考虑这个GraphQL模式样本,定义两种类型和两个查询。

type Query {
    greeting(name: String! = "Spring"): String!
    project(slug: ID!): Project
}

""" A Project in the Spring portfolio """
type Project {
    """ Unique string id used in URLs """
    slug: ID!
    """ Project name """
    name: String!
    """ URL of the git repository """
    repositoryUrl: String!
    """ Current support status """
    status: ProjectStatus!
}

enum ProjectStatus {
    """ Actively supported by the Spring team """
    ACTIVE
    """ Supported by the community """
    COMMUNITY
    """ Prototype, not officially supported yet  """
    INCUBATING
    """ Project being retired, in maintenance mode """
    ATTIC
    """ End-Of-Lifed """
    EOL
}
默认情况下,schema上将允许 field introspection,因为GraphiQL等工具需要它。如果你希望不暴露schema的信息,你可以通过设置 spring.graphql.schema.introspection.enabledfalse 来禁用introspection。

6.2. GraphQL RuntimeWiring

GraphQL Java RuntimeWiring.Builder 可用于注册自定义标量(scalar)类型、指令、类型解析器、DataFetcher 等。 你可以在Spring配置中声明 RuntimeWiringConfigurer Bean,以获得对 RuntimeWiring.Builder 的访问。 Spring Boot会检测到此类Bean,并将其添加到 GraphQlSource builder 中。

然而,通常情况下,应用程序不会直接实现 DataFetcher,而是创建 注解控制器。 Spring Boot会自动检测带有注解处理方法的 @Controller 类,并将其注册为 DataFetcher。 下面是一个用 @Controller 类实现我们的问候查询的例子。

Java
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

@Controller
public class GreetingController {

    @QueryMapping
    public String greeting(@Argument String name) {
        return "Hello, " + name + "!";
    }

}
Kotlin
;

import org.springframework.graphql.data.method.annotation.Argument
import org.springframework.graphql.data.method.annotation.QueryMapping
import org.springframework.stereotype.Controller

@Controller
class GreetingController {

    @QueryMapping
    fun greeting(@Argument name: String): String {
        return "Hello, $name!"
    }

}

6.3. QueryDsl 和 QueryByExample Repository 的支持

Spring Data提供对 Querydsl 和 QueryByExample Repository 的支持。 Spring GraphQL可以将 Querydsl和QueryByExample资源库配置为 DataFetcher

@GraphQlRepository 注解的 Spring Data Repository,并扩展其中一个。

  • QuerydslPredicateExecutor

  • ReactiveQuerydslPredicateExecutor

  • QueryByExampleExecutor

  • ReactiveQueryByExampleExecutor

被Spring Boot检测到,并被认为是 DataFetcher 的候选者,用于匹配顶层查询。

6.4. Transports

6.4.1. HTTP 和 WebSocket

GraphQL HTTP端点默认为 HTTP POST /graphql。 路径可以用 spring.graphql.path 来定制。

Spring MVC和Spring WebFlux的HTTP端点是由一个 @Order0`RouterFunction bean提供的。 如果你定义了你自己的 RouterFunction bean,你可能想添加适当的 @Order 注解,以确保它们被正确排序。

默认情况下,GraphQL WebSocket端点是关闭的。要启用它:

  • 对于Servlet应用程序,添加WebSocket starter spring-boot-starter-websocket

  • 对于WebFlux应用程序,不需要额外的依赖

  • 对于这两者,spring.graphql.websocket.path 应用程序属性必须被设置

Spring GraphQL 提供了一个 Web Interception 模型。 这对于从HTTP请求头中获取信息并将其设置在GraphQL上下文中,或者从同一上下文中获取信息并将其写入响应头中,都是非常有用的。 通过Spring Boot,你可以声明一个 WebInterceptor bean,让它与Web transport注册。

Spring MVCSpring WebFlux 支持CORS(Cross-Origin Resource Sharing)请求。 对于从使用不同域的浏览器访问的GraphQL应用程序来说,CORS是Web配置的一个关键部分。

Spring Boot支持 spring.graphql.cors.* 命名空间下的许多配置属性;这里有一个简短的配置样本。

Properties
spring.graphql.cors.allowed-origins=https://example.org
spring.graphql.cors.allowed-methods=GET,POST
spring.graphql.cors.max-age=1800s
Yaml
spring:
  graphql:
    cors:
      allowed-origins: "https://example.org"
      allowed-methods: GET,POST
      max-age: 1800s

6.4.2. RSocket

在WebSocket或TCP的基础上,RSocket也被支持作为一种transport。 一旦配置了RSocket服务器,我们就可以使用 spring.graphql.rsocket.mapping 在特定路由上配置我们的GraphQL处理器。 例如,将该映射配置为 "graphql" 意味着我们在使用 RSocketGraphQlClient 发送请求时可以将其作为路由。

Spring Boot自动配置了一个 RSocketGraphQlClient.Builder<?> Bean,你可以在你的组件中注入。

Java
@Component
public class RSocketGraphQlClientExample {

    private final RSocketGraphQlClient graphQlClient;

    public RSocketGraphQlClientExample(RSocketGraphQlClient.Builder<?> builder) {
        this.graphQlClient = builder.tcp("example.spring.io", 8181).route("graphql").build();
    }
Kotlin
@Component
class RSocketGraphQlClientExample(private val builder: RSocketGraphQlClient.Builder<*>) {

And then send a request:

Java
Mono<Book> book = this.graphQlClient.document("{ bookById(id: \"book-1\"){ id name pageCount author } }")
    .retrieve("bookById")
    .toEntity(Book.class);
Kotlin
val book = graphQlClient.document(
    """
    {
        bookById(id: "book-1"){
            id
            name
            pageCount
            author
        }
    }               
    """
)
    .retrieve("bookById").toEntity(Book::class.java)

6.5. 异常处理(Exceptions Handling)

Spring GraphQL使应用程序能够注册一个或多个Spring DataFetcherExceptionResolver 组件,这些组件被顺序调用。 异常必须被解析为 graphql.GraphQLError 对象的list,见 Spring GraphQL异常处理文档。 Spring Boot将自动检测 DataFetcherExceptionResolver Bean,并将其注册到 GraphQlSource.Builder 中。

6.6. GraphiQL 和 Schema printer

在消费或开发GraphQL API时,Spring GraphQL提供了帮助开发者的基础设施。

Spring GraphQL有一个默认的 GraphiQL 页面,默认在 "/graphiql" 中显示。这个页面默认是禁用的,可以通过 spring.graphql.graphiql.enabled 属性来打开。许多暴露于这种页面的应用程序会倾向于使用自定义构建。默认实现在开发过程中非常有用,这就是为什么它在开发过程中被spring-boot-devtools自动暴露。

spring.graphql.schema.printer.enabled 属性被启用时,你也可以选择在 /graphql/schema 中以文本格式公开GraphQL schema。

7. Spring HATEOAS

如果你开发的RESTful API使用了超媒体,Spring Boot为Spring HATEOAS提供了自动配置,对大多数应用程序都很适用。 自动配置取代了使用 @EnableHypermediaSupport 的需要,并注册了一些Bean,以方便构建基于超媒体的应用程序,包括 LinkDiscoverers(用于客户端支持)和 ObjectMapper 配置为将响应正确整合为所需的表示方式。 ObjectMapper 是通过设置各种 spring.jackson.* 属性来定制的,也可以可以通过自定义 Jackson2ObjectMapperBuilder bean来定制。

你可以通过使用 @EnableHypermediaSupport 来控制Spring HATEOAS的configuration。 请注意,这样做会使前面描述的 ObjectMapper 定制功能失效。

spring-boot-starter-hateoas 是针对Spring MVC的,不应该与Spring WebFlux结合。为了在Spring WebFlux中使用Spring HATEOAS,你可以在 spring-boot-starter-webflux 中加入对 org.springframework.hateoas:spring-hateoas 的直接依赖。

8. 接下来读什么

现在你应该对如何用Spring Boot开发Web应用有了充分的了解。 接下来的几节将描述Spring Boot如何与各种“data”技术消息系统和其他IO功能集成。 你可以根据你的应用程序的需求来挑选其中的任何一个。