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。
以下代码展示了一个典型的 @RestController,它用于提供 JSON 数据:
- 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 框架的核心部分,详细信息可以在 参考文档 中找到。此外,spring.io/guides 上也有几篇关于 Spring MVC 的指南。
你可以定义任意数量的 RouterFunction bean 来模块化路由的定义。如果需要应用优先级,可以对 bean 进行排序。
Spring MVC 自动配置
Spring Boot 为 Spring MVC 提供了自动配置,适用于大多数应用程序。它取代了 @EnableWebMvc 的需求,并且两者不能同时使用。除了 Spring MVC 的默认设置外,自动配置还提供了以下功能:
-
包含 ContentNegotiatingViewResolver 和 BeanNameViewResolver bean。
-
支持提供静态资源,包括对 WebJars 的支持(在本文档后面会介绍)。
-
自动注册 Converter、GenericConverter 和 Formatter bean。
-
支持 HttpMessageConverters(在本文档后面会介绍)。
-
自动注册 MessageCodesResolver(在本文档后面会介绍)。
-
支持静态
index.html
文件。 -
自动使用 ConfigurableWebBindingInitializer bean(在本文档后面会介绍)。
如果你想保留这些 Spring Boot MVC 的定制化设置,并进一步进行 MVC 定制化(例如拦截器、格式化器、视图控制器和其他功能),你可以添加一个你自己的 @Configuration
类,并实现 WebMvcConfigurer 接口,但不要使用 @EnableWebMvc
。
如果你想提供 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 或 ExceptionHandlerExceptionResolver 的自定义实例,并且仍然保留 Spring Boot MVC 的自定义配置,你可以声明一个类型为 WebMvcRegistrations 的 Bean,并使用它来提供这些组件的自定义实例。这些自定义实例将受到 Spring MVC 的进一步初始化和配置。为了参与并(如有需要)覆盖后续的处理,应该使用 WebMvcConfigurer。
如果你不想使用自动配置,而是想完全控制 Spring MVC,可以添加你自己的 @Configuration 并用 @EnableWebMvc 注解。或者,按照 @EnableWebMvc API 文档中的描述,添加你自己的 @Configuration 注解的 DelegatingWebMvcConfiguration。
Spring MVC 转换服务
Spring MVC 使用了一个不同于用于从 application.properties
或 application.yaml
文件中转换值的 ConversionService。这意味着 Period、Duration 和 DataSize 转换器不可用,并且 @DurationUnit 和 @DataSizeUnit 注解将被忽略。
如果你想自定义 Spring MVC 使用的 ConversionService,你可以提供一个实现了 WebMvcConfigurer 接口的 bean,并实现其中的 addFormatters
方法。在这个方法中,你可以注册任何你想要的转换器,或者你可以委托给 ApplicationConversionService 中提供的静态方法。
转换也可以通过 spring.mvc.format.*
配置属性进行自定义。如果未进行配置,则使用以下默认值:
属性 | DateTimeFormatter | 格式化对象 |
---|---|---|
spring.mvc.format.date | ofLocalizedDate(FormatStyle.SHORT) | java.util.Date 和 LocalDate |
spring.mvc.format.time | ofLocalizedTime(FormatStyle.SHORT) | java.time 的 LocalTime 和 OffsetTime |
spring.mvc.format.date-time | ofLocalizedDateTime(FormatStyle.SHORT) | java.time 的 LocalDateTime, OffsetDateTime, 和 ZonedDateTime |
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 默认配置的某些转换器时,这可能会非常有用。
消息代码解析器
Spring MVC 提供了一种策略,用于生成错误代码以渲染来自绑定错误的错误消息:MessageCodesResolver。如果你将 spring.mvc.message-codes-resolver-format
属性设置为 PREFIX_ERROR_CODE
或 POSTFIX_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/**
的资源如果是以 Webjars 格式打包的,将从 jar 文件中提供。该路径可以通过 spring.mvc.webjars-path-pattern
属性进行自定义。
如果你的应用程序被打包为 jar 文件,请勿使用 src/main/webapp
目录。尽管该目录是一个常见标准,但它 仅 适用于 war 打包方式。如果你生成的是 jar 文件,大多数构建工具会静默忽略该目录。
Spring Boot 同样支持 Spring MVC 提供的高级资源处理功能,允许实现诸如缓存清除静态资源或对 Webjars 使用版本无关的 URL 等用例。
要使用与版本无关的 URL 来引用 Webjars,请添加 org.webjars:webjars-locator-lite
依赖项。然后声明你的 Webjar。以 jQuery 为例,添加 "/webjars/jquery/jquery.min.js"
会生成 "/webjars/jquery/x.y.z/jquery.min.js"
,其中 x.y.z
是 Webjar 的版本号。
要使用缓存清除(cache busting),可以通过以下配置为所有静态资源配置一个缓存清除的解决方案,从而有效地在 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 Boot 支持静态和模板化的欢迎页面。它首先会在配置的静态内容位置中查找 index.html
文件。如果未找到,则会查找 index
模板。如果其中任何一个被找到,它将自动被用作应用程序的欢迎页面。
这仅作为应用程序定义的实际索引路由的备用方案。顺序由 HandlerMapping Bean 的顺序定义,默认顺序如下:
RouterFunctionMapping | 使用 RouterFunction bean 声明的端点 |
RequestMappingHandlerMapping | 在 @Controller bean 中声明的端点 |
WelcomePageHandlerMapping | 欢迎页面支持 |
自定义网站图标
与其他静态资源一样,Spring Boot 会在配置的静态内容位置中检查是否存在 favicon.ico
文件。如果存在这样的文件,它会自动被用作应用程序的 favicon。
路径匹配与内容协商
Spring MVC 可以通过查看请求路径并将其与应用程序中定义的映射进行匹配,从而将传入的 HTTP 请求映射到处理程序(例如,Controller 方法上的 @GetMapping 注解)。
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")
:
- 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 限制了某些路径模式变体的使用。它还不兼容通过路径前缀(spring.mvc.servlet.path
)配置 DispatcherServlet 的情况。
可以通过 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
以完全禁用静态内容的服务。
可配置的 Web 绑定初始化器
Spring MVC 使用 WebBindingInitializer 来为特定请求初始化一个 WebDataBinder。如果你创建了自己的 ConfigurableWebBindingInitializer @Bean,Spring Boot 会自动配置 Spring MVC 来使用它。
模板引擎
除了 REST 网络服务,你还可以使用 Spring MVC 来提供动态 HTML 内容。Spring MVC 支持多种模板技术,包括 Thymeleaf、FreeMarker 和 JSP。此外,许多其他模板引擎也提供了它们自己的 Spring MVC 集成。
Spring Boot 包含对以下模板引擎的自动配置支持:
如果可能的话,应尽量避免使用 JSP。在嵌入式 servlet 容器中使用 JSP 时存在一些已知的限制。
当你使用这些模板引擎并采用默认配置时,你的模板会自动从 src/main/resources/templates
目录中获取。
根据你运行应用程序的方式,你的 IDE 可能会以不同的顺序排列类路径。在 IDE 中通过主方法运行应用程序时,类路径的顺序与使用 Maven 或 Gradle 或从其打包的 jar 文件运行应用程序时的顺序不同。这可能导致 Spring Boot 无法找到预期的模板。如果你遇到这个问题,你可以在 IDE 中重新排列类路径,将模块的类和资源放在首位。
错误处理
默认情况下,Spring Boot 提供了一个 /error
映射,以合理的方式处理所有错误,并将其注册为 Servlet 容器中的“全局”错误页面。对于机器客户端,它会生成一个包含错误详情、HTTP 状态和异常消息的 JSON 响应。对于浏览器客户端,则会有一个“白标”错误视图,以 HTML 格式呈现相同的数据(要自定义它,可以添加一个解析为 error
的 View)。
如果您想自定义默认的错误处理行为,可以设置一些 server.error
属性。请参阅附录中的服务器属性部分。
要完全替换默认行为,你可以实现 ErrorController 并注册该类型的 bean 定义,或者添加一个类型为 ErrorAttributes 的 bean,以使用现有机制但替换其内容。
BasicErrorController 可以作为自定义 ErrorController 的基类。如果你想要为新的内容类型添加处理程序(默认情况下是专门处理 text/html
并为其他所有内容提供回退),这将特别有用。为此,可以继承 BasicErrorController,添加一个带有 @RequestMapping 注解的公共方法,并设置 produces
属性,然后创建你新类型的 bean。
自 Spring Framework 6.0 起,支持 RFC 9457 问题详情。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](https://docs.spring.io/spring-framework/docs/6.2.x/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html)
注解的类,以自定义为特定控制器和/或异常类型返回的 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 观察或指标基础设施记录。应用程序可以通过在观察上下文上设置已处理的异常来确保此类异常被观察记录。
自定义错误页面
如果你想为特定的状态码显示一个自定义的 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
- 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 方法 和 @ControllerAdvice。ErrorController 随后会捕获任何未处理的异常。
在 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"))
}
}
- 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
调度器类型。
WAR 部署中的错误处理
当部署到 Servlet 容器时,Spring Boot 使用其错误页面过滤器将带有错误状态的请求转发到适当的错误页面。这是必要的,因为 Servlet 规范没有提供注册错误页面的 API。根据你部署 war 文件的目标容器以及应用程序使用的技术,可能需要一些额外的配置。
错误页面过滤器只有在响应尚未提交时才能将请求转发到正确的错误页面。默认情况下,WebSphere Application Server 8.0 及更高版本在 servlet 的 service
方法成功完成后会提交响应。您应通过将 com.ibm.ws.webcontainer.invokeFlushAfterService
设置为 false
来禁用此行为。
CORS 支持
从 4.2 版本开始,Spring MVC 支持 CORS。在你的 Spring Boot 应用程序中使用 控制器方法 CORS 配置 和 @CrossOrigin 注解不需要任何特定的配置。全局 CORS 配置 可以通过注册一个 WebMvcConfigurer bean 并自定义 addCorsMappings(CorsRegistry)
方法来定义,如下例所示:
- 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。Jersey 和 Apache CXF 开箱即用,表现良好。CXF 需要你在应用上下文中将其 Servlet 或 Filter 注册为 @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。
所有注册的端点都应该是一个 @Component
,并带有 HTTP 资源注解(如 @GET
等),如下例所示:
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.springframework.stereotype.Component;
@Component
@Path("/hello")
public class MyEndpoint {
@GET
public String message() {
return "Hello";
}
}
由于 @Endpoint 是 Spring 的 @Component,其生命周期由 Spring 管理,你可以使用 @Autowired 注解来注入依赖,并使用 @Value 注解来注入外部配置。默认情况下,Jersey servlet 被注册并映射到 /*
。你可以通过在 ResourceConfig 上添加 @ApplicationPath 来更改映射。
默认情况下,Jersey 被设置为一个类型为 ServletRegistrationBean 的 @Bean 中的 Servlet,名称为 jerseyServletRegistration
。默认情况下,Servlet 是延迟初始化的,但你可以通过设置 spring.jersey.servlet.load-on-startup
来定制该行为。你可以通过创建一个同名 Bean 来禁用或覆盖该 Bean。你也可以通过设置 spring.jersey.type=filter
来使用过滤器而不是 Servlet(在这种情况下,要替换或覆盖的 @Bean 是 jerseyFilterRegistration
)。过滤器有一个 @Order,你可以通过 spring.jersey.filter.order
来设置。当使用 Jersey 作为过滤器时,必须存在一个 Servlet 来处理任何未被 Jersey 拦截的请求。如果你的应用程序不包含这样的 Servlet,你可能需要通过将 server.servlet.register-default-servlet
设置为 true
来启用默认的 Servlet。Servlet 和过滤器的注册都可以通过使用 spring.jersey.init.*
来指定一组属性作为初始化参数。
嵌入式 Servlet 容器支持
Servlets、Filters 和 Listeners
在使用嵌入式 Servlet 容器时,你可以注册 Servlet、过滤器以及所有来自 Servlet 规范的监听器(例如 HttpSessionListener),这可以通过使用 Spring Bean 或扫描 Servlet 组件来实现。
注册 Servlet、Filter 和 Listener 为 Spring Bean
默认情况下,如果上下文中只包含一个 Servlet,它将被映射到 /
。如果有多个 Servlet bean,则使用 bean 名称作为路径前缀。过滤器则映射到 /*
。
如果基于约定的映射不够灵活,你可以使用 ServletRegistrationBean、FilterRegistrationBean 和 ServletListenerRegistrationBean 类来进行完全控制。
通常来说,过滤器 Bean 的顺序不需要特别指定。如果需要特定的顺序,你应该使用 @Order 注解来标注 Filter,或者让它实现 Ordered 接口。你不能通过在 Bean 方法上使用 @Order 注解来配置 Filter 的顺序。如果你无法修改 Filter 类来添加 @Order 注解或实现 Ordered 接口,你必须为该 Filter 定义一个 FilterRegistrationBean,并使用 setOrder(int)
方法来设置注册 Bean 的顺序。避免将读取请求体的过滤器配置为 Ordered.HIGHEST_PRECEDENCE
,因为这可能会与应用程序的字符编码配置冲突。如果 Servlet 过滤器包装了请求,它应该配置为小于或等于 OrderedFilter.REQUEST_WRAPPER_FILTER_MAX_ORDER
的顺序。
在注册 Filter bean 时要格外小心,因为它们在应用程序生命周期的早期就会被初始化。如果需要注册一个与其他 bean 交互的 Filter,请考虑使用 DelegatingFilterProxyRegistrationBean 来代替。
Servlet 上下文初始化
嵌入式 Servlet 容器不会直接执行 ServletContainerInitializer 接口或 Spring 的 WebApplicationInitializer 接口。这是一个有意为之的设计决策,旨在减少那些设计为在 war 包内运行的第三方库可能破坏 Spring Boot 应用程序的风险。
如果你需要在 Spring Boot 应用程序中执行 Servlet 上下文初始化,你应该注册一个实现了 ServletContextInitializer 接口的 Bean。该接口中的唯一方法 onStartup
提供了对 ServletContext 的访问,并且在必要时可以轻松地用作现有 WebApplicationInitializer 的适配器。
扫描 Servlet、Filter 和监听器
在使用嵌入式容器时,可以通过使用 @ServletComponentScan 来启用自动注册带有 @WebServlet、@WebFilter 和 @WebListener 注解的类。
@ServletComponentScan 在独立容器中无效,因为这类容器使用其内置的发现机制。
ServletWebServerApplicationContext
在底层,Spring Boot 使用了一种不同类型的 ApplicationContext 来支持嵌入式 Servlet 容器。ServletWebServerApplicationContext 是一种特殊的 WebApplicationContext,它通过寻找一个单一的 ServletWebServerFactory Bean 来自行启动。通常,TomcatServletWebServerFactory、JettyServletWebServerFactory 或 UndertowServletWebServerFactory 会被自动配置。
通常你不需要了解这些实现类。大多数应用程序都是自动配置的,适当的 ApplicationContext 和 ServletWebServerFactory 会为你自动创建。
在嵌入式容器设置中,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 容器
常见的 Servlet 容器设置可以通过使用 Spring Environment 属性进行配置。通常,你会在 application.properties
或 application.yaml
文件中定义这些属性。
常见的服务器设置包括:
Spring Boot 尽可能地暴露了通用设置,但这并非总是可行的。对于这些情况,专用的命名空间提供了服务器特定的自定义选项(参见 server.tomcat
和 server.undertow
)。例如,访问日志 可以通过嵌入式 servlet 容器的特定功能进行配置。
有关完整列表,请参阅 ServerProperties 类。
SameSite Cookie
SameSite
cookie 属性可以被网络浏览器用来控制跨站请求中是否以及如何提交 cookie。该属性对于现代网络浏览器尤为重要,因为这些浏览器已经开始更改在缺少该属性时使用的默认值。
如果你想更改会话 cookie 的 SameSite
属性,可以使用 server.servlet.session.cookie.same-site
属性。该属性由自动配置的 Tomcat、Jetty 和 Undertow 服务器支持。它还可用于配置基于 Spring Session servlet 的 SessionRepository bean。
例如,如果你希望你的会话 cookie 具有 SameSite
属性为 None
,你可以在 application.properties
或 application.yaml
文件中添加以下内容:
- Properties
- YAML
server.servlet.session.cookie.same-site=none
server:
servlet:
session:
cookie:
same-site: "none"
如果你想更改添加到 HttpServletResponse 的其他 cookie 的 SameSite
属性,你可以使用 CookieSameSiteSupplier。CookieSameSiteSupplier 被传递一个 Cookie,并可以返回一个 SameSite
值,或者返回 null
。
有许多便捷的工厂方法和过滤方法,你可以使用它们来快速匹配特定的 cookie。例如,添加以下 bean 将自动为所有名称与正则表达式 myapp.*
匹配的 cookie 应用 SameSite
为 Lax
的设置。
- 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)
}
}
TomcatServletWebServerFactory、JettyServletWebServerFactory 和 UndertowServletWebServerFactory 是 ConfigurableServletWebServerFactory 的专用变体,它们分别提供了针对 Tomcat、Jetty 和 Undertow 的额外自定义设置方法。以下示例展示了如何自定义 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。
为许多配置选项提供了设置器。如果您需要执行更复杂的操作,还提供了几个受保护的方法“钩子”。详情请参阅 ConfigurableServletWebServerFactory API 文档。
自动配置的自定义器仍然会应用到你的自定义工厂上,因此请谨慎使用该选项。
JSP 的限制
在运行使用嵌入式 Servlet 容器的 Spring Boot 应用程序(并打包为可执行存档)时,JSP 支持存在一些限制。