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 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 使用的 ConversionService 與用於轉換 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 都會新增到轉換器列表中。您也可以以相同的方式覆蓋預設轉換器。

如果需要新增或自定義轉換器,您可以宣告一個或多個 ClientHttpMessageConvertersCustomizerServerHttpMessageConvertersCustomizer 作為 Bean,如以下列表所示

  • Java

  • Kotlin

import org.springframework.boot.http.converter.autoconfigure.ClientHttpMessageConvertersCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class MyHttpMessageConvertersConfiguration {

	@Bean
	public ClientHttpMessageConvertersCustomizer myClientConvertersCustomizer() {
		return (clientBuilder) -> clientBuilder.addCustomConverter(new AdditionalHttpMessageConverter())
			.addCustomConverter(new AnotherHttpMessageConverter());
	}

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

@Configuration(proxyBeanMethods = false)
class MyHttpMessageConvertersConfiguration {

	@Bean
	fun myClientConvertersCustomizer(): ClientHttpMessageConvertersCustomizer {
		return ClientHttpMessageConvertersCustomizer { clientBuilder: HttpMessageConverters.ClientBuilder ->
			clientBuilder
				.addCustomConverter(AdditionalHttpMessageConverter())
				.addCustomConverter(AnotherHttpMessageConverter())
		}
	}

}

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/** 可以透過以下方式實現

  • 屬性

  • 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 等用例。

要為 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"/>

  • 屬性

  • 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 中新增一個靜態版本字串而不更改檔名,如以下示例所示

  • 屬性

  • 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

歡迎頁面支援

自定義網站圖示

與其他靜態資源一樣,Spring Boot 會在配置的靜態內容位置中查詢 favicon.ico。如果存在這樣的檔案,它將自動用作應用程式的網站圖示。

路徑匹配和內容協商

Spring MVC 可以透過檢視請求路徑並將其與應用程式中定義的對映(例如,控制器方法上的 @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")

  • 屬性

  • YAML

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

或者如果您更喜歡使用不同的引數名稱

  • 屬性

  • YAML

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

大多數標準媒體型別都開箱即用,但您也可以定義新的媒體型別

  • 屬性

  • 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 配置屬性來配置策略,如以下示例所示

  • 屬性

  • 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 響應。對於瀏覽器客戶端,有一個“白標籤”錯誤檢視,以 HTML 格式呈現相同的資料(要自定義它,請新增一個解析為 errorView)。

如果您想自定義預設錯誤處理行為,可以設定一些 server.error 屬性。請參閱附錄的伺服器屬性部分。

要完全替換預設行為,您可以實現 ErrorController 並註冊該型別的 bean 定義,或者新增一個型別為 ErrorAttributes 的 bean 來使用現有機制但替換內容。

BasicErrorController 可以用作自定義 ErrorController 的基類。這在您想要為新的內容型別新增處理程式時特別有用(預設是專門處理 text/html 併為所有其他內容提供回退)。為此,請擴充套件 BasicErrorController,新增一個帶有 produces 屬性的 @RequestMapping 公共方法,並建立您新型別的 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 註解的類,以自定義要為特定控制器和/或異常型別返回的 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.webmvc.autoconfigure.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.webmvc.autoconfigure.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.error.ErrorPage;
import org.springframework.boot.web.error.ErrorPageRegistrar;
import org.springframework.boot.web.error.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.error.ErrorPage
import org.springframework.boot.web.error.ErrorPageRegistrar
import org.springframework.boot.web.error.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

  • 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 的服務方法成功完成後提交響應。您應該透過將 com.ibm.ws.webcontainer.invokeFlushAfterService 設定為 false 來停用此行為。

CORS 支援

跨域資源共享 (CORS) 是一個由W3C 規範實現的功能,被大多數瀏覽器支援。它允許您靈活地指定哪種跨域請求是授權的,而不是使用一些不太安全且功能較弱的方法,如 IFRAME 或 JSONP。

從 4.2 版本開始,Spring MVC 支援 CORS。在 Spring Boot 應用程式中使用帶有 @CrossOrigin 註解的控制器方法 CORS 配置不需要任何特定配置。全域性 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/**")
			}
		}
	}

}

API 版本控制

Spring MVC 支援 API 版本控制,可用於隨著時間的推移演進 HTTP API。同一個 @Controller 路徑可以多次對映以支援不同版本的 API。

有關更多詳細資訊,請參閱 Spring Framework 的參考文件

新增對映後,您還需要配置 Spring MVC,使其能夠使用請求中傳送的任何版本資訊。通常,版本以 HTTP 標頭、查詢引數或路徑的一部分發送。

要配置 Spring MVC,您可以選擇使用 WebMvcConfigurer Bean 並覆蓋 configureApiVersioning(…​) 方法,或者您可以使用屬性。

例如,以下將使用 X-Version HTTP 標頭獲取版本資訊,並且在未傳送標頭時預設為 1.0.0

  • 屬性

  • YAML

spring.mvc.apiversion.default=1.0.0
spring.mvc.apiversion.use.header=X-Version
spring:
  mvc:
    apiversion:
      default: 1.0.0
      use:
        header: X-Version

為了更全面的控制,您還可以定義 ApiVersionResolverApiVersionParserApiVersionDeprecationHandler Bean,它們將注入到自動配置的 Spring MVC 配置中。

API 版本控制也支援 WebClientRestClient。有關詳細資訊,請參閱API 版本控制

JAX-RS 和 Jersey

如果您更喜歡 REST 端點的 JAX-RS 程式設計模型,可以使用其中一種可用的實現而不是 Spring MVC。JerseyApache CXF 開箱即用。CXF 要求您在應用程式上下文中將其 ServletFilter 註冊為 @Bean。Jersey 對 Spring 有一些原生支援,因此我們還在 Spring Boot 中為它提供了自動配置支援,以及一個啟動器。

要開始使用 Jersey,請將 spring-boot-starter-jersey 作為依賴項包含進來,然後您需要一個型別為 ResourceConfig@Bean,在該 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 已註冊並對映到 /*。您可以透過將 @ApplicationPath 新增到您的 ResourceConfig 來更改對映。

預設情況下,Jersey 被設定為 @Bean 型別 ServletRegistrationBean,名為 jerseyServletRegistration 中的 servlet。預設情況下,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。servlet 和過濾器註冊都可以透過使用 spring.jersey.init.* 來指定屬性對映來提供初始化引數。

嵌入式 Servlet 容器支援

對於 servlet 應用程式,Spring Boot 支援嵌入式 TomcatJetty 伺服器。大多數開發人員使用適當的啟動器來獲取完全配置的例項。預設情況下,嵌入式伺服器在埠 8080 監聽 HTTP 請求。

Servlets、過濾器和監聽器

當使用嵌入式 servlet 容器時,您可以透過使用 Spring Bean 或掃描 servlet 元件來註冊 servlet、過濾器和所有監聽器(例如 HttpSessionListener)。

將 Servlet、過濾器和監聽器註冊為 Spring Bean

任何作為 Spring Bean 的 ServletFilter 或 servlet *Listener 例項都會註冊到嵌入式容器中。如果您想在配置期間引用 application.properties 中的值,這會特別方便。

預設情況下,如果上下文僅包含一個 Servlet,它將對映到 /。如果有多個 servlet bean,則 bean 名稱用作路徑字首。過濾器對映到 /*

如果基於約定的對映不夠靈活,您可以使用 ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean 類來完全控制。如果您更喜歡註解而不是 ServletRegistrationBeanFilterRegistrationBean,您也可以使用 @ServletRegistration@FilterRegistration 作為替代。

通常可以安全地不對過濾器 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 上下文初始化

嵌入式 servlet 容器不直接執行 ServletContainerInitializer 介面或 Spring 的 WebApplicationInitializer 介面。這是一個有意為之的設計決策,旨在降低為在 war 中執行而設計的第三方庫可能破壞 Spring Boot 應用程式的風險。

如果您需要在 Spring Boot 應用程式中執行 servlet 上下文初始化,則應註冊一個實現 ServletContextInitializer 介面的 bean。單個 onStartup 方法提供對 ServletContext 的訪問,如果需要,可以輕鬆地用作現有 WebApplicationInitializer 的介面卡。

初始化引數

可以使用 server.servlet.context-parameters.* 屬性在 ServletContext 上配置初始化引數。例如,屬性 server.servlet.context-parameters.com.example.parameter=example 將配置一個名為 com.example.parameterServletContext 初始化引數,其值為 example

掃描 Servlets、過濾器和監聽器

使用嵌入式容器時,可以透過使用 @ServletComponentScan 啟用自動註冊用 @WebServlet@WebFilter@WebListener 註解的類。

@ServletComponentScan 在獨立容器中沒有效果,因為在這種情況下會使用容器內建的發現機制。

ServletWebServerApplicationContext

在底層,Spring Boot 為嵌入式 servlet 容器支援使用不同型別的 ApplicationContextServletWebServerApplicationContext 是一種特殊型別的 WebApplicationContext,它透過查詢單個 ServletWebServerFactory Bean 來引導自身。通常,TomcatServletWebServerFactoryJettyServletWebServerFactory 已自動配置。

您通常不需要了解這些實現類。大多數應用程式都是自動配置的,並且會為您建立適當的 ApplicationContextServletWebServerFactory

在嵌入式容器設定中,ServletContext 在伺服器啟動期間(應用程式上下文初始化期間)設定。因此,ApplicationContext 中的 Bean 無法可靠地使用 ServletContext 進行初始化。解決此問題的一種方法是將 ApplicationContext 注入為 Bean 的依賴項,並且僅在需要時才訪問 ServletContext。另一種方法是在伺服器啟動後使用回撥。這可以透過使用監聽 ApplicationStartedEventApplicationListener 來實現,如下所示

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.*)。

  • 錯誤管理:錯誤頁面的位置 (spring.web.error.path) 等。

  • SSL

  • HTTP 壓縮

Spring Boot 儘可能地公開通用設定,但這並非總是可行。對於這些情況,專用名稱空間提供了伺服器特定的自定義(參見 server.tomcat)。例如,訪問日誌可以透過嵌入式 servlet 容器的特定功能進行配置。

有關完整列表,請參閱 ServerProperties 類。

SameSite Cookie

SameSite cookie 屬性可由 Web 瀏覽器用於控制在跨站點請求中是否以及如何提交 cookie。此屬性對於現代 Web 瀏覽器尤為重要,它們已開始更改當屬性缺失時使用的預設值。

如果您想更改會話 cookie 的 SameSite 屬性,可以使用 server.servlet.session.cookie.same-site 屬性。此屬性受自動配置的 Tomcat 和 Jetty 伺服器支援。它也用於配置基於 Spring Session servlet 的 SessionRepository Bean。

例如,如果您希望會話 cookie 的 SameSite 屬性為 None,您可以將以下內容新增到您的 application.propertiesapplication.yaml 檔案中

  • 屬性

  • 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.server.servlet.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.server.servlet.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 配置屬性來自定義對映,如以下示例所示

  • 屬性

  • 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 的訪問,後者包含許多自定義設定器方法。以下示例展示了以程式設計方式設定埠

  • Java

  • Kotlin

import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.servlet.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.servlet.ConfigurableServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.stereotype.Component

@Component
class MyWebServerFactoryCustomizer : WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

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

}

TomcatServletWebServerFactoryJettyServletWebServerFactoryConfigurableServletWebServerFactory 的專用變體,分別具有用於 Tomcat 和 Jetty 的附加自定義設定器方法。以下示例展示瞭如何自定義 TomcatServletWebServerFactory,它提供了對特定於 Tomcat 的配置選項的訪問

  • Java

  • Kotlin

import java.time.Duration;

import org.springframework.boot.tomcat.servlet.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.server.WebServerFactoryCustomizer
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory
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 支援存在一些限制。

  • 使用 Jetty 和 Tomcat,如果您使用 war 包,它應該可以工作。可執行 war 在使用 java -jar 啟動時將正常工作,並且也可以部署到任何標準容器。使用可執行 jar 時不支援 JSP。

  • 建立自定義 error.jsp 頁面不會覆蓋 錯誤處理 的預設檢視。應改用 自定義錯誤頁面

© . This site is unofficial and not affiliated with VMware.