Servlet Web 应用程序

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

“Spring Web MVC 框架”

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

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

  • Java

  • Kotlin

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

}
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

  • Kotlin

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

}
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

  • Kotlin

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

	public ServerResponse getUserCustomers(ServerRequest request) {
		...
	}

	public ServerResponse deleteUser(ServerRequest request) {
		...
	}

}
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 {
		...
	}

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

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

}

Spring MVC 是 Spring Framework 核心的一部分,详细信息可在 参考文档 中获取。 在 spring.io/guides 上也有几个涵盖 Spring MVC 的指南。

提示:你可以定义任意数量的 RouterFunction bean 来模块化路由定义。 如果需要应用优先级,可以对 bean 进行排序。

Spring MVC 自动配置

Spring Boot 为 Spring MVC 提供了适用于大多数应用程序的自动配置。 它替代了对 @EnableWebMvc 的需求,并且两者不能一起使用。 除了 Spring MVC 的默认值外,自动配置还提供以下功能:

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

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

如果你不想使用自动配置并想完全控制 Spring MVC,添加你自己的带有 @EnableWebMvc 注解的 @Configuration。 或者,按照 @EnableWebMvc API 文档中的描述,添加你自己的带有 @Configuration 注解的 DelegatingWebMvcConfiguration

Spring MVC 转换服务

Spring MVC 使用与用于转换 application.propertiesapplication.yaml 文件中的值的 ConversionService 不同的转换服务。 这意味着 PeriodDurationDataSize 转换器不可用,并且 @DurationUnit@DataSizeUnit 注解将被忽略。

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

转换也可以使用 spring.mvc.format.* 配置属性进行自定义。 未配置时,使用以下默认值:

属性 DateTimeFormatter 格式

spring.mvc.format.date

ofLocalizedDate(FormatStyle.SHORT)

java.util.DateLocalDate

spring.mvc.format.time

ofLocalizedTime(FormatStyle.SHORT)

java.time 的 LocalTimeOffsetTime

spring.mvc.format.date-time

ofLocalizedDateTime(FormatStyle.SHORT)

java.time 的 LocalDateTimeOffsetDateTimeZonedDateTime

HttpMessageConverters

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

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

如果需要添加或自定义转换器,可以使用 Spring Boot 的 HttpMessageConverters 类,如下所示:

  • Java

  • 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)
public class MyHttpMessageConvertersConfiguration {

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

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

}

为了进一步控制,你还可以子类化 HttpMessageConverters 并覆盖其 postProcessConverters 和/或 postProcessPartConverters 方法。 当你想重新排序或删除 Spring MVC 默认配置的一些转换器时,这很有用。

MessageCodesResolver

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

静态内容

默认情况下,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/**

  • Properties

  • YAML

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

你还可以使用 spring.web.resources.static-locations 属性自定义静态资源位置(用目录位置列表替换默认值)。 根 servlet 上下文路径 "/" 也会自动添加为位置。

除了前面提到的"`标准`"静态资源位置外,还对 Webjars 内容 进行了特殊处理。 默认情况下,如果资源以 Webjars 格式打包,则从 jar 文件中提供路径为 /webjars/** 的任何资源。 可以使用 spring.mvc.webjars-path-pattern 属性自定义路径。

提示:如果你的应用程序打包为 jar,请不要使用 src/main/webapp 目录。 虽然这个目录是一个常见标准,但它*仅*适用于 war 打包,如果你生成 jar,大多数构建工具会静默忽略它。

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

要为 Webjars 使用版本无关的 URL,添加 org.webjars:webjars-locator-lite 依赖。 然后声明你的 Webjar。 以 jQuery 为例,添加 "/webjars/jquery/jquery.min.js" 会得到 "/webjars/jquery/x.y.z/jquery.min.js",其中 x.y.z 是 Webjar 版本。

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

  • Properties

  • YAML

spring.web.resources.chain.strategy.content.enabled=true
spring.web.resources.chain.strategy.content.paths=/**
spring:
  web:
    resources:
      chain:
        strategy:
          content:
            enabled: true
            paths: "/**"

注意:由于为 Thymeleaf 和 FreeMarker 自动配置了 ResourceUrlEncodingFilter,资源链接在运行时在模板中重写。 使用 JSP 时,你应该手动声明此过滤器。 其他模板引擎目前不自动支持,但可以通过自定义模板宏/助手和使用 ResourceUrlProvider 来支持。

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

  • Properties

  • YAML

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
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 的 参考文档 中有详细描述。

欢迎页面

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

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

RouterFunctionMapping

使用 RouterFunction bean 声明的端点

RequestMappingHandlerMapping

@Controller bean 中声明的端点

WelcomePageHandlerMapping

欢迎页面支持

自定义 Favicon

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

路径匹配和内容协商

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

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

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

  • Properties

  • YAML

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

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

  • Properties

  • YAML

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

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

  • Properties

  • YAML

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

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

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

  • Properties

  • YAML

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

如果找不到请求的处理器,Spring MVC 将抛出 NoHandlerFoundException。 注意,默认情况下,静态内容服务 映射到 /**,因此将为所有请求提供处理器。 如果没有可用的静态内容,ResourceHttpRequestHandler 将抛出 NoResourceFoundException。 要抛出 NoHandlerFoundException,将 spring.mvc.static-path-pattern 设置为更具体的值,如 /resources/**,或将 spring.web.resources.add-mappings 设置为 false 以完全禁用静态内容服务。

ConfigurableWebBindingInitializer

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

模板引擎

除了 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 中重新排序类路径,将模块的类和资源放在首位。

错误处理

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

如果你想自定义默认错误处理行为,可以设置许多 server.error 属性。 请参阅附录的 服务器属性 部分。

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

提示:BasicErrorController 可以用作自定义 ErrorController 的基类。 这在你想为新的内容类型添加处理器时特别有用(默认是专门处理 text/html 并为其他所有内容提供后备)。 为此,扩展 BasicErrorController,添加一个带有 @RequestMapping 的公共方法,该注解具有 produces 属性,并创建你的新类型的 bean。

从 Spring Framework 6.0 开始,支持 RFC 9457 Problem Details。 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

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

}
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 观察或 指标基础设施记录。 应用程序可以通过 在观察上下文上设置已处理的异常来确保记录此类异常。

Custom Error Pages

如果想要为给定的状态码显示自定义的 HTML 错误页面,可以将文件添加到 /error 目录中。 错误页面可以是静态 HTML(即,添加到任何静态资源目录下)或通过使用模板构建。 文件名应为确切的状态码或系列掩码。

例如,要将 404 映射到静态 HTML 文件,你的目录结构应如下所示:

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

要将所有 5xx 错误映射到 FreeMarker 模板,你的目录结构应如下所示:

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

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

  • Java

  • Kotlin

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

}
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 methods@ControllerAdviceErrorController 然后会拾取任何未处理的异常。

Mapping Error Pages Outside of Spring MVC

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

  • Java

  • 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)
public class MyErrorPagesConfiguration {

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

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

}
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 dispatcher,如下例所示:

  • Java

  • Kotlin

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

}
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 类型。

Error Handling in a WAR Deployment

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

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

CORS 支持

跨源资源共享 (CORS) 是一个由 大多数浏览器实现的 W3C 规范,它允许你以灵活的方式指定允许哪些类型的跨域请求,而不是使用一些不太安全和功能较弱的方案,如 IFRAME 或 JSONP。

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

  • Java

  • 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)
public class MyCorsConfiguration {

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

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

		};
	}

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

}

JAX-RS 和 Jersey

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

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

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 资源注解(@GET 等)的 @Component,如下例所示:

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 被设置为类型为 ServletRegistrationBean@Bean 中的 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-servlettrue 来启用默认 servlet。 servlet 和过滤器注册都可以通过使用 spring.jersey.init.* 来指定属性映射来获得初始化参数。

嵌入式 Servlet 容器支持

对于 servlet 应用程序,Spring Boot 包含对嵌入式 TomcatJettyUndertow 服务器的支持。 大多数开发人员使用适当的 starter 来获得完全配置的实例。 By default, the embedded server listens for HTTP requests on port 8080.

Servlets、Filters 和 Listeners

使用嵌入式 servlet 容器时,你可以通过使用 Spring bean 或扫描 servlet 组件来注册 servlets、filters 和所有来自 servlet 规范的 listeners(如 HttpSessionListener)。

将 Servlets、Filters 和 Listeners 注册为 Spring Beans

任何作为 Spring bean 的 ServletFilter 或 servlet *Listener instance 都会注册到嵌入式容器中。 如果你想在配置期间引用 application.properties 中的值,这会特别方便。

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

如果基于约定的映射不够灵活,你可以使用 ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean 类来完全控制。 如果你更喜欢注解而不是 ServletRegistrationBeanFilterRegistrationBean,你也可以使用 @ServletRegistration@FilterRegistration 作为替代方案。

通常可以安全地让 filter bean 保持无序。 如果需要特定顺序,你应该用 @Order 注解 Filter 或让它实现 Ordered。 你不能通过用 @Order 注解其 bean 方法来配置 Filter 的顺序。 如果你无法更改 Filter 类来添加 @Order 或实现 Ordered,你必须为 Filter 定义一个 FilterRegistrationBean,并使用 setOrder(int) 方法设置注册 bean 的顺序。 或者,如果你更喜欢注解,你也可以使用 @FilterRegistration 并设置 order 属性。 避免在 Ordered.HIGHEST_PRECEDENCE 配置读取请求体的过滤器,因为这可能会与应用程序的字符编码配置相冲突。 如果 servlet 过滤器包装了请求,它应该配置一个小于或等于 OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER 的顺序。

提示:要查看应用程序中每个 Filter 的顺序,请为 web 日志组启用调试级别日志记录(logging.level.web=debug)。 然后将在启动时记录已注册过滤器的详细信息,包括它们的顺序和 URL 模式。

警告:注册 Filter bean 时要小心,因为它们在应用程序生命周期的很早阶段就被初始化。 如果你需要注册一个与其他 bean 交互的 Filter,考虑使用 DelegatingFilterProxyRegistrationBean 代替。

Servlet Context 初始化

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

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

扫描 Servlets、Filters 和 listeners

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

提示:@ServletComponentScan 在独立容器中没有效果,因为那里使用容器的内置发现机制。

ServletWebServerApplicationContext

在底层,Spring Boot 使用不同类型的 ApplicationContext 来支持嵌入式 servlet 容器。 ServletWebServerApplicationContext 是一种特殊类型的 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();
	}

}

自定义嵌入式 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 文件中添加以下内容:

  • Properties

  • YAML

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

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

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

  • Java

  • Kotlin

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

}
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 容器用于请求和响应处理的字符编码行为可以使用 server.servlet.encoding.* 配置属性进行配置。

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

  • Properties

  • YAML

server.servlet.encoding.mapping.ko=UTF-8
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

  • Kotlin

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

}
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

  • Kotlin

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

}
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。 如果你需要做一些更特殊的事情,还提供了几个受保护的"`hooks`"方法。 有关详细信息,请参见 ConfigurableServletWebServerFactory API 文档。

注意:自动配置的自定义器仍然应用于你的自定义工厂,所以要谨慎使用该选项。

JSP 限制

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

  • 对于 Jetty 和 Tomcat,如果你使用 war 打包,它应该可以工作。 An executable war will work when launched with java -jar, and will also be deployable to any standard container. JSPs are not supported when using an executable jar.

  • Undertow does not support JSPs.

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