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 应用

如果你想构建基于servlet的web应用程序,你可以利用Spring Boot为Spring MVC或Jersey提供的自动配置。

1.1. “Spring Web MVC Framework”

Spring Web MVC框架(通常称为“Spring MVC”)是一个丰富的“模型视图控制器”Web框架。Spring MVC允许您创建特殊的@Controller@RestController bean来处理传入的HTTP请求。您的控制器中的方法通过使用@RequestMapping注解映射到HTTP。

以下代码展示了一个典型的提供JSON数据的@RestController

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 Framework的一部分,详细信息可在参考文档中找到。还有几个指南涵盖了Spring MVC,可在spring.io/guides上找到。

您可以定义尽可能多的RouterFunction bean,以模块化路由的定义。如果需要应用优先级,可以对bean进行排序。

1.1.1. Spring MVC自动配置

Spring Boot为Spring MVC提供了自动配置,适用于大多数应用程序。它取代了@EnableWebMvc的需要,两者不能同时使用。除了Spring MVC的默认值外,自动配置还提供以下功能:

  • 包含ContentNegotiatingViewResolverBeanNameViewResolver bean。

  • 支持提供静态资源,包括对WebJars的支持(在本文档的后面有介绍)。

  • 自动注册ConverterGenericConverterFormatter bean。

  • 支持HttpMessageConverters(在本文档的后面有介绍)。

  • 自动注册MessageCodesResolver(在本文档的后面有介绍)。

  • 静态index.html支持。

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

如果您想保留这些Spring Boot MVC自定义并进行更多的MVC自定义(拦截器、格式化程序、视图控制器和其他功能),可以添加自己的@Configuration类型的WebMvcConfigurer类,但不要使用@EnableWebMvc

如果您想提供自定义的RequestMappingHandlerMappingRequestMappingHandlerAdapterExceptionHandlerExceptionResolver实例,并仍然保留Spring Boot MVC自定义,可以声明一个类型为WebMvcRegistrations的bean,并使用它提供这些组件的自定义实例。这些自定义实例将进一步由Spring MVC进行初始化和配置。要参与并在需要时覆盖后续处理,应使用WebMvcConfigurer

如果您不想使用自动配置并希望完全控制Spring MVC,可以添加自己的带有@EnableWebMvc注释的@Configuration。或者,根据@EnableWebMvc的Javadoc中描述的方式,添加自己的带有@Configuration注释的DelegatingWebMvcConfiguration

1.1.2. Spring MVC 转换服务

Spring MVC 使用不同的 ConversionService 来转换值,而不是使用在您的 application.propertiesapplication.yaml 文件中使用的那个。这意味着 PeriodDurationDataSize 转换器不可用,并且 @DurationUnit@DataSizeUnit 注解将被忽略。

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

还可以使用 spring.mvc.format.* 配置属性来自定义转换。当未配置时,将使用以下默认值:

属性 DateTimeFormatter

spring.mvc.format.date

ofLocalizedDate(FormatStyle.SHORT)

spring.mvc.format.time

ofLocalizedTime(FormatStyle.SHORT)

spring.mvc.format.date-time

ofLocalizedDateTime(FormatStyle.SHORT)

1.1.3. HttpMessageConverters

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)
    }

}

任何存在于上下文中的 HttpMessageConverter bean 都会被添加到转换器列表中。您也可以以相同的方式覆盖默认转换器。

1.1.4. MessageCodesResolver

Spring MVC 有一个用于生成错误代码以从绑定错误中呈现错误消息的策略: MessageCodesResolver。如果设置了 spring.mvc.message-codes-resolver-format 属性为 PREFIX_ERROR_CODEPOSTFIX_ERROR_CODE,Spring Boot 会为您创建一个(请参阅 DefaultMessageCodesResolver.Format 中的枚举)。

1.1.5. 静态内容

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

在独立的Web应用程序中,默认情况下不启用容器中的默认servlet。可以使用server.servlet.register-default-servlet属性来启用它。

默认servlet充当后备,如果Spring决定不处理内容,则会从ServletContext的根目录提供内容。大多数情况下,这种情况不会发生(除非您修改默认的MVC配置),因为Spring始终可以通过DispatcherServlet处理请求。

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

属性
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
<link href="/css/spring-2a2d595e6ed9a0b24f027f2b63b134d6.css"/>

属性
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时,您应手动声明此过滤器。其他模板引擎目前不受自动支持,但可以通过自定义模板宏/助手和使用ResourceUrlProvider来支持。

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

属性
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.6. 欢迎页面

Spring Boot支持静态和模板化的欢迎页面。它首先在配置的静态内容位置中查找index.html文件。如果找不到,则会查找index模板。如果找到任何一个,它将自动用作应用程序的欢迎页面。

这仅作为应用程序定义的实际索引路由的后备。排序由HandlerMapping bean的顺序定义,默认情况下如下:

RouterFunctionMapping

使用RouterFunction bean声明的端点

RequestMappingHandlerMapping

@Controller bean中声明的端点

WelcomePageHandlerMapping

欢迎页面支持

1.1.7. 自定义Favicon

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

1.1.8. 路径匹配和内容协商

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

Spring Boot选择默认禁用后缀模式匹配,这意味着像"GET /projects/spring-boot.json"这样的请求将不会匹配到@GetMapping("/projects/spring-boot")映射。这被视为Spring MVC应用程序的最佳实践。这个特性在过去主要用于那些没有发送正确"Accept"请求头的HTTP客户端;我们需要确保向客户端发送正确的内容类型。如今,内容协商更加可靠。

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

属性
spring.mvc.contentnegotiation.favor-parameter=true
Yaml
spring:
  mvc:
    contentnegotiation:
      favor-parameter: true

或者,如果您更喜欢使用不同的参数名称:

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

大多数标准媒体类型都受到开箱即用的支持,但您也可以定义新的媒体类型:

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

从Spring Framework 5.3开始,Spring MVC支持两种将请求路径与控制器匹配的策略。默认情况下,Spring Boot使用PathPatternParser策略。PathPatternParser是一个优化实现,但与AntPathMatcher策略相比存在一些限制。PathPatternParser限制了一些路径模式变体的使用。它还与配置DispatcherServlet的路径前缀(spring.mvc.servlet.path)不兼容。

可以使用spring.mvc.pathmatch.matching-strategy配置属性来配置策略,如下例所示:

属性
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
Yaml
spring:
  mvc:
    pathmatch:
      matching-strategy: "ant-path-matcher"

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

1.1.9. ConfigurableWebBindingInitializer

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

1.1.10. 模板引擎

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

Spring Boot包含以下模板引擎的自动配置支持:

如果可能的话,应避免使用JSP。在使用嵌入式servlet容器时,存在一些已知限制。

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

根据您运行应用程序的方式,您的IDE可能会以不同的顺序排列类路径。在IDE中从其主方法运行应用程序会导致与使用Maven或Gradle运行应用程序或从打包的jar运行应用程序时不同的排序。这可能导致Spring Boot无法找到预期的模板。如果遇到此问题,您可以在IDE中重新排列类路径,将模块的类和资源放在第一位。

1.1.11. 错误处理

默认情况下,Spring Boot提供了一个处理所有错误的/error映射,并将其注册为Servlet容器中的“全局”错误页面。对于机器客户端,它会生成一个包含错误详情、HTTP状态和异常消息的JSON响应。对于浏览器客户端,有一个“whitelabel”错误视图以HTML格式呈现相同的数据(要自定义它,请添加一个解析为errorView)。

如果要自定义默认的错误处理行为,可以设置一些server.error属性。请参阅附录中的“服务器属性”部分。

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

可以使用BasicErrorController作为自定义ErrorController的基类。如果要为新的内容类型添加处理程序(默认情况下是专门处理text/html并为其他所有内容提供回退),这将特别有用。要实现这一点,扩展BasicErrorController,添加一个带有produces属性的@RequestMapping的公共方法,并创建一个新类型的bean。

从Spring Framework 6.0开始,支持RFC 7807问题详情。Spring MVC可以生成带有application/problem+json媒体类型的自定义错误消息,例如:

{
  "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.enabled设置为true来启用此支持。

还可以定义一个使用@ControllerAdvice注解的类,以自定义要为特定控制器和/或异常类型返回的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的表示形式。

在某些情况下,控制器级别处理的错误不会被Web观察或度量基础设施记录。应用程序可以通过在观察上下文中设置已处理的异常来确保这些异常被记录。

自定义错误页面

如果要为特定状态代码显示自定义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) {
        // 使用请求或状态可选择返回ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // 在此处可以添加自定义模型值
            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? {
        // 使用请求或状态可选择返回ModelAndView
        if (status == HttpStatus.INSUFFICIENT_STORAGE) {
            // 在此处可以添加自定义模型值
            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"))
    }

}
如果您注册的ErrorPage的路径最终由一个Filter处理(这在一些非Spring Web框架(如Jersey和Wicket)中很常见),那么Filter必须显式注册为ERROR分发器,如下例所示:
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分发器类型。

在WAR部署中的错误处理

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

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

1.1.12. 跨域资源共享支持

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

从4.2版本开始,Spring MVC支持CORS。在您的Spring Boot应用程序中使用控制器方法CORS配置@CrossOrigin注解不需要任何特定配置。全局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

如果您更喜欢使用JAX-RS编程模型来构建REST端点,您可以使用可用的实现之一,而不是Spring MVC。 JerseyApache CXF 都可以直接使用。 CXF要求您在应用程序上下文中将其ServletFilter注册为@Bean。 Jersey具有一些原生的Spring支持,因此我们在Spring Boot中为其提供了自动配置支持,以及一个starter。

要开始使用Jersey,请将spring-boot-starter-jersey包含为依赖项,然后您需要一个@Bean类型为ResourceConfig的对象,在其中注册所有端点,如下例所示:

import org.glassfish.jersey.server.ResourceConfig;

import org.springframework.stereotype.Component;

@Component
public class MyJerseyConfig extends ResourceConfig {

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

}
Jersey对扫描可执行存档的支持相当有限。例如,它无法扫描在fully executable jar file中找到的包中的端点,或者在运行可执行war文件时无法扫描WEB-INF/classes中的端点。为避免此限制,不应使用packages方法,而应通过使用register方法逐个注册端点,如前面的示例所示。

对于更高级的自定义,您还可以注册任意数量实现ResourceConfigCustomizer的bean。

所有注册的端点应为带有HTTP资源注解(@GET等)的@Components,如下例所示:

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被注册并映射到/*。您可以通过向您的ResourceConfig添加@ApplicationPath来更改映射。

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

1.3. 嵌入式Servlet容器支持

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

1.3.1. Servlets、Filters和Listeners

在使用嵌入式servlet容器时,您可以注册servlets、filters和所有监听器(如HttpSessionListener)从servlet规范中,可以通过使用Spring beans或扫描servlet组件来实现。

将Servlets、Filters和Listeners注册为Spring Beans

任何作为Spring bean的ServletFilter或servlet *Listener实例都会被注册到嵌入式容器中。如果您想在配置过程中引用application.properties中的值,这可能特别方便。

默认情况下,如果上下文只包含一个Servlet,则将其映射到/。在存在多个servlet bean的情况下,将使用bean名称作为路径前缀。Filters映射到/*

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

通常情况下,将过滤器bean无序化是安全的。如果需要特定顺序,应该使用@OrderFilter进行注解或使其实现Ordered。您不能通过在其bean方法上注解@Order来配置Filter的顺序。如果无法更改Filter类以添加@Order或实现Ordered,则必须为Filter定义一个FilterRegistrationBean,并使用setOrder(int)方法设置注册bean的顺序。避免在Ordered.HIGHEST_PRECEDENCE配置一个读取请求体的过滤器,因为它可能与应用程序的字符编码配置相冲突。如果servlet过滤器包装请求,则应配置一个小于或等于OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER的顺序。

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

1.3.2. Servlet上下文初始化

嵌入式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的适配器。

扫描Servlets、Filters和Listeners

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

@ServletComponentScan在独立容器中没有效果,独立容器将使用容器内置的发现机制。

1.3.3. ServletWebServerApplicationContext

在幕后,Spring Boot为嵌入式servlet容器支持使用了一种不同类型的ApplicationContextServletWebServerApplicationContext是一种特殊类型的WebApplicationContext,通过搜索单个ServletWebServerFactory bean来引导自身。通常会自动配置一个TomcatServletWebServerFactoryJettyServletWebServerFactoryUndertowServletWebServerFactory

通常情况下,您不需要了解这些实现类。大多数应用程序都是自动配置的,并且适当的ApplicationContextServletWebServerFactory会代表您创建。

在嵌入式容器设置中,ServletContext是在服务器启动过程中设置的,这发生在应用程序上下文初始化期间。因此,在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容器

可以使用Spring Environment属性配置常见的servlet容器设置。通常,您会在application.propertiesapplication.yaml文件中定义这些属性。

常见的服务器设置包括:

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

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

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

  • SSL

  • HTTP压缩

Spring Boot尽可能地暴露常见设置,但这并非总是可能的。对于这些情况,专用的命名空间提供了特定于服务器的自定义(参见server.tomcatserver.undertow)。例如,可以使用特定于嵌入式servlet容器的特定功能来配置访问日志

查看ServerProperties类以获取完整列表。
SameSite Cookies

SameSite cookie属性可被Web浏览器用于控制在跨站点请求中如何提交cookie。该属性对于现代Web浏览器特别重要,这些浏览器已经开始更改当属性缺失时使用的默认值。

如果要更改会话cookie的SameSite属性,可以使用server.servlet.session.cookie.same-site属性。此属性受到自动配置的Tomcat、Jetty和Undertow服务器支持。它还用于配置基于Spring Session servlet的SessionRepository bean。

例如,如果希望会话cookie具有SameSite属性为None,可以将以下内容添加到您的application.propertiesapplication.yaml文件中:

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

如果要更改添加到HttpServletResponse的其他cookie的SameSite属性,可以使用CookieSameSiteSupplierCookieSameSiteSupplier接收一个Cookie,可以返回一个SameSite值或null

有许多便利的工厂和过滤器方法可用于快速匹配特定cookie。例如,添加以下bean将自动为所有名称与正则表达式myapp.*匹配的cookie应用SameSiteLax

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.*")
    }

}
字符编码

可以使用server.servlet.encoding.*配置属性来配置嵌入式servlet容器用于请求和响应处理的字符编码行为。

当请求的Accept-Language头指示请求的区域设置时,servlet容器将自动将其映射到字符集。每个容器提供默认的区域设置到字符集映射,您应验证它们是否符合应用程序的需求。当不符合时,使用server.servlet.encoding.mapping配置属性来自定义映射,如下例所示:

属性
server.servlet.encoding.mapping.ko=UTF-8
Yaml
server:
  servlet:
    encoding:
      mapping:
        ko: "UTF-8"

在上面的示例中,将韩文(ko)区域设置映射到UTF-8。这相当于传统war部署的web.xml文件中的<locale-encoding-mapping-list>条目。

编程定制

如果您需要以编程方式配置嵌入式Servlet容器,可以注册一个实现WebServerFactoryCustomizer接口的Spring bean。WebServerFactoryCustomizer提供对ConfigurableServletWebServerFactory的访问,其中包括许多自定义setter方法。以下示例展示了如何以编程方式设置端口:

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的额外自定义setter方法。以下示例展示了如何自定义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。如果需要执行更多特殊操作,还提供了几个受保护的方法“钩子”。有关详细信息,请参阅源代码文档

自动配置的自定义器仍然会应用于您的自定义工厂,因此请谨慎使用该选项。

1.3.5. JSP 限制

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

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

  • Undertow不支持JSP。

  • 创建自定义error.jsp页面不会覆盖错误处理的默认视图。应改用自定义错误页面

2. 响应式Web应用程序

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

2.1. “Spring WebFlux Framework”

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

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是Spring Framework的一部分,详细信息可在其参考文档中找到。

“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.fn”是Spring Framework的一部分,详细信息可在其参考文档中找到。

您可以定义尽可能多的RouterFunction bean,以模块化路由的定义。如果需要应用优先级,可以对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的默认基础上添加了以下功能:

  • HttpMessageReaderHttpMessageWriter实例配置编解码器(在本文档的后面描述)。

  • 支持提供静态资源,包括对WebJars的支持(在本文档的后面描述)。

如果您想保留Spring Boot WebFlux功能,并且想要添加额外的WebFlux配置,您可以添加自己的@Configuration类,类型为WebFluxConfigurer,但不要使用@EnableWebFlux

如果您想完全控制Spring WebFlux,可以添加自己的@Configuration,并使用@EnableWebFlux进行注释。

2.1.2. Spring WebFlux 转换服务

如果您想自定义Spring WebFlux使用的ConversionService,您可以提供一个带有addFormatters方法的WebFluxConfigurer bean。

还可以使用spring.webflux.format.*配置属性来自定义转换。当未配置时,将使用以下默认值:

属性 DateTimeFormatter

spring.webflux.format.date

ofLocalizedDate(FormatStyle.SHORT)

spring.webflux.format.time

ofLocalizedTime(FormatStyle.SHORT)

spring.webflux.format.date-time

ofLocalizedDateTime(FormatStyle.SHORT)

2.1.3. 使用HttpMessageReaders和HttpMessageWriters的HTTP编解码器

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

Spring Boot为编解码器提供了专用的配置属性,spring.codec.*。它还通过使用CodecCustomizer实例进行进一步的自定义。例如,spring.jackson.*配置键适用于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.4. 静态内容

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

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

属性
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.5. 欢迎页面

Spring Boot支持静态和模板化的欢迎页面。它首先在配置的静态内容位置中查找index.html文件。如果找不到,则查找index模板。如果找到任何一个,它将自动用作应用程序的欢迎页面。

这仅作为应用程序定义的实际索引路由的后备。排序由HandlerMapping bean的顺序定义,默认情况下如下:

RouterFunctionMapping

使用RouterFunction bean声明的端点

RequestMappingHandlerMapping

@Controller bean中声明的端点

RouterFunctionMapping用于欢迎页面

欢迎页面支持

2.1.6. 模板引擎

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

Spring Boot包含以下模板引擎的自动配置支持:

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

2.1.7. 错误处理

Spring Boot提供了一个WebExceptionHandler,以合理的方式处理所有错误。它在处理顺序中的位置位于WebFlux提供的处理程序之前,被认为是最后一个。对于机器客户端,它生成一个带有错误详细信息、HTTP状态和异常消息的JSON响应。对于浏览器客户端,有一个“whitelabel”错误处理程序以HTML格式呈现相同的数据。您还可以提供自己的HTML模板来显示错误(请参阅下一节)。

在直接定制Spring Boot中的错误处理之前,您可以利用Spring WebFlux中的RFC 7807问题详细信息支持。Spring WebFlux可以生成带有application/problem+json媒体类型的自定义错误消息,例如:

{
  "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.enabled设置为true来启用此支持。

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

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

Java
import reactor.core.publisher.Mono;

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.http.codec.ServerCodecConfigurer;
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, WebProperties webProperties,
            ApplicationContext applicationContext, ServerCodecConfigurer serverCodecConfigurer) {
        super(errorAttributes, webProperties.getResources(), applicationContext);
        setMessageReaders(serverCodecConfigurer.getReaders());
        setMessageWriters(serverCodecConfigurer.getWriters());
    }

    @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.http.codec.ServerCodecConfigurer
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, webProperties: WebProperties,
        applicationContext: ApplicationContext, serverCodecConfigurer: ServerCodecConfigurer
) : AbstractErrorWebExceptionHandler(errorAttributes, webProperties.resources, applicationContext) {

    init {
        setMessageReaders(serverCodecConfigurer.readers)
        setMessageWriters(serverCodecConfigurer.writers)
    }

    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并覆盖特定方法。

在某些情况下,在控制器级别处理的错误不会被Web观察或度量基础设施记录。应用程序可以通过在观察上下文中设置已处理的异常来确保这些异常被记录。

自定义错误页面

如果要为给定状态代码显示自定义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.8. Web过滤器

Spring WebFlux提供了一个WebFilter接口,可用于实现过滤HTTP请求-响应交换。在应用程序上下文中找到的WebFilter bean将自动用于过滤每个交换。

在过滤器的顺序很重要的情况下,它们可以实现Ordered或用@Order注解。Spring Boot自动配置可能会为您配置Web过滤器。当这样做时,将使用以下表中显示的顺序:

Web过滤器 顺序

WebFilterChainProxy(Spring Security)

-100

HttpExchangesWebFilter

Ordered.LOWEST_PRECEDENCE - 10

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

Spring Boot包含对以下嵌入式响应式Web服务器的支持:Reactor Netty、Tomcat、Jetty和Undertow。大多数开发人员使用适当的“Starter”来获取一个完全配置的实例。默认情况下,嵌入式服务器在端口8080上监听HTTP请求。

2.2.1. 自定义响应式服务器

可以使用Spring Environment属性配置常见的响应式Web服务器设置。通常,您会在application.propertiesapplication.yaml文件中定义这些属性。

常见的服务器设置包括:

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

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

  • SSL

  • HTTP压缩

Spring Boot尽可能地暴露常见设置,但这并非总是可能的。对于这些情况,专用的命名空间(如server.netty.*)提供了服务器特定的自定义。

查看完整列表,请参阅ServerProperties类。
编程自定义

如果需要以编程方式配置您的响应式Web服务器,可以注册一个实现WebServerFactoryCustomizer接口的Spring bean。WebServerFactoryCustomizer提供对ConfigurableReactiveWebServerFactory的访问,其中包括许多自定义setter方法。以下示例展示了如何以编程方式设置端口:

Java
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> {

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

}
Kotlin
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.boot.web.reactive.server.ConfigurableReactiveWebServerFactory
import org.springframework.stereotype.Component

@Component
class MyWebServerFactoryCustomizer : WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> {

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

}

JettyReactiveWebServerFactoryNettyReactiveWebServerFactoryTomcatReactiveWebServerFactoryUndertowReactiveWebServerFactoryConfigurableReactiveWebServerFactory的专用变体,分别具有用于Jetty、Reactor Netty、Tomcat和Undertow的附加自定义setter方法。以下示例展示了如何自定义NettyReactiveWebServerFactory,该工厂提供了对Reactor Netty特定配置选项的访问:

Java
import java.time.Duration;

import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class MyNettyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {

    @Override
    public void customize(NettyReactiveWebServerFactory factory) {
        factory.addServerCustomizers((server) -> server.idleTimeout(Duration.ofSeconds(20)));
    }

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

@Component
class MyNettyWebServerFactoryCustomizer : WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {

    override fun customize(factory: NettyReactiveWebServerFactory) {
        factory.addServerCustomizers({ server -> server.idleTimeout(Duration.ofSeconds(20)) })
    }

}
直接自定义ConfigurableReactiveWebServerFactory

对于需要从ReactiveWebServerFactory扩展的更高级用例,可以自行公开此类型的bean。

提供了许多配置选项的setter。还提供了一些受保护的方法“钩子”,以便在需要执行更多特殊操作时使用。有关详细信息,请参阅源代码文档

自动配置的自定义器仍然会应用于您的自定义工厂,因此请谨慎使用该选项。

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

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

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

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

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

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

您可以在WebClient运行时部分了解有关客户端端资源配置的更多信息。

3. 优雅关闭

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

使用Tomcat进行优雅关闭需要Tomcat 9.0.33或更高版本。

要启用优雅关闭,请配置server.shutdown属性,如以下示例所示:

属性
server.shutdown=graceful
Yaml
server:
  shutdown: "graceful"

要配置超时期限,请配置spring.lifecycle.timeout-per-shutdown-phase属性,如以下示例所示:

属性
spring.lifecycle.timeout-per-shutdown-phase=20s
Yaml
spring:
  lifecycle:
    timeout-per-shutdown-phase: "20s"
如果IDE未发送正确的SIGTERM信号,则使用IDE进行优雅关闭可能无法正常工作。有关更多详细信息,请参阅您的IDE文档。

4. Spring安全

如果在类路径上存在Spring Security,则Web应用程序默认受到保护。Spring Boot依赖于Spring Security的内容协商策略来确定是否使用httpBasicformLogin。要向Web应用程序添加方法级安全性,还可以添加@EnableGlobalMethodSecurity并使用所需的设置。更多信息可以在Spring Security参考指南中找到。

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

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

此生成的密码仅用于开发目的。在将应用程序部署到生产环境之前,必须更新您的安全配置。
如果调整日志配置,请确保org.springframework.boot.autoconfigure.security类别设置为记录WARN级别的消息。否则,默认密码将不会被打印。

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

Web应用程序默认提供的基本功能包括:

  • 具有内存存储和单个用户的UserDetailsService(或在WebFlux应用程序中为ReactiveUserDetailsService)bean,并具有生成的密码(有关用户属性,请参阅SecurityProperties.User)。

  • 基于表单的登录或HTTP基本安全性(取决于请求中的Accept标头)适用于整个应用程序(包括如果actuator在类路径上则包括actuator端点)。

  • 用于发布身份验证事件的DefaultAuthenticationEventPublisher

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

4.1. MVC安全性

默认的安全配置实现在SecurityAutoConfigurationUserDetailsServiceAutoConfiguration中。 SecurityAutoConfiguration导入SpringBootWebSecurityConfiguration用于Web安全性,UserDetailsServiceAutoConfiguration配置身份验证,这在非Web应用程序中也是相关的。

要完全关闭默认的Web应用程序安全配置,或者要组合多个Spring Security组件(如OAuth2客户端和资源服务器),请添加一个SecurityFilterChain类型的bean(这样做不会禁用UserDetailsService配置或Actuator的安全性)。要同时关闭UserDetailsService配置,可以添加一个UserDetailsServiceAuthenticationProviderAuthenticationManager类型的bean。

UserDetailsService的自动配置还会在类路径上存在以下任何Spring Security模块时进行回退:

  • spring-security-oauth2-client

  • spring-security-oauth2-resource-server

  • spring-security-saml2-service-provider

要在这些依赖项之一及更多依赖项中使用UserDetailsService,请定义自己的InMemoryUserDetailsManager bean。

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

4.2. WebFlux安全性

与Spring MVC应用程序类似,您可以通过添加spring-boot-starter-security依赖项来保护您的WebFlux应用程序。默认的安全配置实现在ReactiveSecurityAutoConfigurationUserDetailsServiceAutoConfiguration中。 ReactiveSecurityAutoConfiguration导入WebFluxSecurityConfiguration用于Web安全性,UserDetailsServiceAutoConfiguration配置身份验证,这在非Web应用程序中也是相关的。

要完全关闭默认的Web应用程序安全配置,可以添加一个WebFilterChainProxy类型的bean(这样做不会禁用UserDetailsService配置或Actuator的安全性)。要同时关闭UserDetailsService配置,可以添加一个ReactiveUserDetailsServiceReactiveAuthenticationManager类型的bean。

当类路径上存在以下任何Spring Security模块时,自动配置也会进行回退:

  • spring-security-oauth2-client

  • spring-security-oauth2-resource-server

要在这些依赖项及更多依赖项中使用ReactiveUserDetailsService,请定义自己的MapReactiveUserDetailsService bean。

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

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.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 是一个广泛使用的授权框架,Spring 支持该框架。

4.3.1. 客户端

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

您可以在spring.security.oauth2.client前缀下注册多个OAuth2客户端和提供者,如下例所示:

属性
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=用于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=用于用户范围的客户端
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=用于电子邮件范围的客户端
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: "用于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: "用于用户范围的客户端"
            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: "用于电子邮件范围的客户端"
            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发现的提供者,配置可以进一步简化。提供者需要配置一个issuer-uri,这是它断言为其发行者标识符的URI。例如,如果提供的issuer-uri是"https://example.com",那么将向"https://example.com/.well-known/openid-configuration"发出"OpenID提供者配置请求"。预期结果是一个"OpenID提供者配置响应"。以下示例显示了如何配置OpenID Connect提供者的issuer-uri

属性
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以使用不同的模式,您需要提供配置来处理该自定义模式。例如,对于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实现。
常见提供商的OAuth2客户端注册

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

如果您不需要自定义这些提供商,可以将provider属性设置为您需要推断默认值的提供商。此外,如果客户端注册的键与默认支持的提供商匹配,Spring Boot也会推断。

换句话说,在以下示例中,两个配置使用了Google提供商:

属性
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. 资源服务器

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

属性
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"
属性
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声明的预期值。例如,要求JWT包含值为my-audience的aud声明:

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

相同的属性适用于Servlet和响应式应用程序。或者,您可以为Servlet应用程序定义自己的JwtDecoder bean,或者为响应式应用程序定义ReactiveJwtDecoder

在使用不是JWT而是不透明令牌的情况下,您可以配置以下属性通过内省来验证令牌:

属性
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和响应式应用程序。或者,您可以为Servlet应用程序定义自己的OpaqueTokenIntrospector bean,或者为响应式应用程序定义ReactiveOpaqueTokenIntrospector

4.3.3. 授权服务器

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

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

属性
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"
必须以可以与配置的PasswordEncoder匹配的格式提供client-secret属性。默认的PasswordEncoder实例是通过PasswordEncoderFactories.createDelegatingPasswordEncoder()创建的。

Spring Boot为Spring授权服务器提供的自动配置旨在快速入门。大多数应用程序将需要自定义,并希望定义多个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授权服务器参考指南中的入门章节。

4.4. SAML 2.0

4.4.1. 依赖方

如果在类路径中有spring-security-saml2-service-provider,您可以利用一些自动配置来设置 SAML 2.0 依赖方。此配置利用了Saml2RelyingPartyProperties下的属性。

依赖方注册表示标识提供方(IDP)和服务提供方(SP)之间配对配置。您可以在spring.security.saml2.relyingparty前缀下注册多个依赖方,如下例所示:

属性
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].private-key-location=私钥路径
spring.security.saml2.relyingparty.registration.my-relying-party1.signing.credentials[0].certificate-location=证书路径
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].private-key-location=私钥路径
spring.security.saml2.relyingparty.registration.my-relying-party1.decryption.credentials[0].certificate-location=证书路径
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=验证证书路径
spring.security.saml2.relyingparty.registration.my-relying-party1.assertingparty.entity-id=远程IDP实体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=私钥路径
spring.security.saml2.relyingparty.registration.my-relying-party2.signing.credentials[0].certificate-location=证书路径
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].private-key-location=私钥路径
spring.security.saml2.relyingparty.registration.my-relying-party2.decryption.credentials[0].certificate-location=证书路径
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.verification.credentials[0].certificate-location=其他验证证书路径
spring.security.saml2.relyingparty.registration.my-relying-party2.assertingparty.entity-id=远程IDP实体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: "私钥路径"
                certificate-location: "证书路径"
            decryption:
              credentials:
              - private-key-location: "私钥路径"
                certificate-location: "证书路径"
            singlelogout:
               url: "https://myapp/logout/saml2/slo"
               response-url: "https://remoteidp2.slo.url"
               binding: "POST"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "验证证书路径"
              entity-id: "远程IDP实体ID1"
              sso-url: "https://remoteidp1.sso.url"

          my-relying-party2:
            signing:
              credentials:
              - private-key-location: "私钥路径"
                certificate-location: "证书路径"
            decryption:
              credentials:
              - private-key-location: "私钥路径"
                certificate-location: "证书路径"
            assertingparty:
              verification:
                credentials:
                - certificate-location: "其他验证证书路径"
              entity-id: "远程IDP实体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,以使用不同的模式,您需要提供配置来处理该自定义模式。例如,对于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

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

如果类路径上存在单个Spring Session模块,则Spring Boot会自动使用该存储实现。如果存在多个实现,Spring Boot会按照以下顺序选择特定的实现:

  1. Redis

  2. JDBC

  3. Hazelcast

  4. MongoDB

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

构建响应式Web应用程序时,可以自动配置以下存储:

  • Redis

  • MongoDB

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

与Servlet配置类似,如果存在多个实现,Spring Boot会按照以下顺序选择特定的实现:

  1. Redis

  2. MongoDB

  3. 如果Redis和MongoDB都不可用,则不配置ReactiveSessionRepository

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

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

要设置会话超时,可以使用spring.session.timeout属性。如果未在Servlet Web应用程序中设置该属性,则自动配置将退回到server.servlet.session.timeout的值。

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

6. Spring用于GraphQL

如果您想构建GraphQL应用程序,可以利用Spring Boot对Spring GraphQL的自动配置。Spring GraphQL项目基于GraphQL Java。您至少需要spring-boot-starter-graphql启动器。由于GraphQL是与传输无关的,您还需要在应用程序中添加一个或多个额外的启动器来通过网络公开您的GraphQL API:

启动器 传输 实现

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模式

Spring GraphQL应用程序在启动时需要一个定义的模式。默认情况下,您可以在src/main/resources/graphql/**下编写“.graphqls”或“.gqls”模式文件,Spring Boot将自动检测到它们。您可以使用spring.graphql.schema.locations自定义位置,并使用spring.graphql.schema.file-extensions自定义文件扩展名。

如果您希望Spring Boot在所有应用程序模块和依赖项中检测该位置的模式文件,可以将spring.graphql.schema.locations设置为"classpath*:graphql/**/"(注意classpath*:前缀)。

在接下来的章节中,我们将考虑这个示例GraphQL模式,定义了两种类型和两个查询:

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

""" Spring产品组合中的一个项目 """
type Project {
    """ 用于URL的唯一字符串ID """
    slug: ID!
    """ 项目名称 """
    name: String!
    """ git存储库的URL """
    repositoryUrl: String!
    """ 当前支持状态 """
    status: ProjectStatus!
}

enum ProjectStatus {
    """ Spring团队积极支持 """
    ACTIVE
    """ 社区支持 """
    COMMUNITY
    """ 原型,尚未正式支持 """
    INCUBATING
    """ 项目正在退役,处于维护模式 """
    ATTIC
    """ 终止生命周期 """
    EOL
}
默认情况下,字段内省将允许在模式上,因为它对于诸如GraphiQL之类的工具是必需的。如果您不希望公开有关模式的信息,可以通过将spring.graphql.schema.introspection.enabled设置为false来禁用内省。

6.2. GraphQL RuntimeWiring

GraphQL Java的RuntimeWiring.Builder可用于注册自定义标量类型、指令、类型解析器、DataFetcher等。您可以在Spring配置中声明RuntimeWiringConfigurer bean,以便访问RuntimeWiring.Builder。Spring Boot会检测到这样的bean,并将它们添加到GraphQlSource构建器中。

通常,应用程序不会直接实现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存储库支持

Spring Data支持Querydsl和QueryByExample存储库。Spring GraphQL可以将Querydsl和QueryByExample存储库配置为DataFetcher

带有@GraphQlRepository注释并扩展以下之一的Spring Data存储库:

  • QuerydslPredicateExecutor

  • ReactiveQuerydslPredicateExecutor

  • QueryByExampleExecutor

  • ReactiveQueryByExampleExecutor

将被Spring Boot检测到,并被视为匹配顶层查询的DataFetcher候选者。

6.4. 传输

6.4.1. HTTP和WebSocket

GraphQL的HTTP端点默认为HTTP POST /graphql。路径可以使用spring.graphql.path进行自定义。

Spring MVC和Spring WebFlux的HTTP端点由具有@Order0RouterFunction bean提供。如果定义自己的RouterFunction bean,可能需要添加适当的@Order注解以确保它们正确排序。

GraphQL的WebSocket端点默认关闭。要启用它:

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

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

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

Spring GraphQL提供了一个Web拦截模型。这对于从HTTP请求头中检索信息并将其设置到GraphQL上下文中,或者从相同上下文中获取信息并将其写入响应头非常有用。使用Spring Boot,您可以声明一个WebInterceptor bean,以便将其注册到Web传输中。

Spring MVCSpring WebFlux支持CORS(跨源资源共享)请求。对于通过不同域从浏览器访问的GraphQL应用程序,CORS是Web配置的关键部分。

Spring Boot支持在spring.graphql.cors.*命名空间下的许多配置属性;以下是一个简短的配置示例:

属性
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

RSocket也支持作为传输,基于WebSocket或TCP。一旦配置了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<*>) {

然后发送请求:

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. 异常处理

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

6.6. GraphiQL和模式打印机

Spring GraphQL提供了基础设施,帮助开发人员在消费或开发GraphQL API时更加便捷。

Spring GraphQL附带一个默认的GraphiQL页面,默认情况下在"/graphiql"处公开。默认情况下此页面处于禁用状态,可以使用spring.graphql.graphiql.enabled属性打开。许多公开此页面的应用程序将更喜欢自定义构建。默认实现在开发过程中非常有用,这就是为什么在开发过程中使用spring-boot-devtools时会自动公开它。

您还可以选择在spring.graphql.schema.printer.enabled属性启用时以文本格式在/graphql/schema处公开GraphQL模式。

7. Spring HATEOAS

如果您正在开发一个使用超媒体的RESTful API,Spring Boot为Spring HATEOAS提供了自动配置,适用于大多数应用程序。自动配置替代了使用@EnableHypermediaSupport的需要,并注册了一些bean,以便轻松构建基于超媒体的应用程序,包括一个LinkDiscoverers(用于客户端支持)和一个配置正确将响应转换为所需表示形式的ObjectMapper。通过设置各种spring.jackson.*属性或(如果存在)通过Jackson2ObjectMapperBuilder bean来自定义ObjectMapper

您可以通过使用@EnableHypermediaSupport来控制Spring HATEOAS的配置。请注意,这样做会禁用前面描述的ObjectMapper自定义。

spring-boot-starter-hateoas专门用于Spring MVC,不应与Spring WebFlux结合使用。为了在Spring WebFlux中使用Spring HATEOAS,您可以直接添加依赖org.springframework.hateoas:spring-hateoas以及spring-boot-starter-webflux

默认情况下,接受application/json的请求将收到一个application/hal+json的响应。要禁用此行为,请将spring.hateoas.use-hal-as-default-json-media-type设置为false,并定义一个HypermediaMappingInformationHalConfiguration来配置Spring HATEOAS以满足您的应用程序及其客户端的需求。

8. 接下来阅读什么

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