詳解SpringBoot錯誤處理
環境:SpringBoot2.7.16
1. 簡介
默認情況下,Spring Boot提供了一個/error映射,以合理的方式處理所有錯誤,并且它在servlet容器中注冊為“全局”錯誤頁面。對于機器客戶端,它會生成一個JSON響應,其中包含錯誤、HTTP狀態和異常消息的詳細信息。對于瀏覽器客戶端,有一個“白標簽”錯誤視圖,它以HTML格式呈現相同的數據(要自定義它,只需要定義一個以error 為beanName的View bean對象)。
如果需要自定義默認的錯誤處理行為,可以通過設置server.error相應屬性。
要完全替換默認行為,可以實現ErrorController并注冊為Bean,或者添加ErrorAttributes類型的bean。
BasicErrorController可以用作自定義ErrorController的基類。如果想為新的內容類型添加處理程序,這一點尤其有用(默認情況是專門處理text/html,并為其他所有內容提供后備)。要做到這一點,請擴展BasicErrorController,添加一個帶有具有products屬性的@RequestMapping的公共方法,并創建一個新類型的bean。
從Spring Framework 6.0開始,支持RFC 7807 Problem Details。Spring MVC可以使用application/pproblem+json媒體類型生成自定義錯誤消息,如:
{
"type": "http://www.pack.com/users/666",
"title": "Unknown project",
"status": 404,
"detail": "xxxxx",
"instance": "/users/666"
}
可以通過將spring.mvc.problemdetails.enabled設置為true來啟用此支持。
還可以定義一個用@ControllerAdvice注釋的類,以自定義JSON格式輸出,如以下示例所示:
@RestControllerAdvice(basePackageClasses = SomeController.class)
public class MyControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.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;
}
}
2. 自定義錯誤頁
如果要顯示給定狀態代碼的自定義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,如以下示例所示:
@Component
public class PackErrorViewResolver implements ErrorViewResolver {
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
if (status == HttpStatus.INTERNAL_SERVER_ERROR) {
return new ModelAndView("error") ;
}
return null ;
}
}
3. 向容器注冊錯誤頁
對于不使用Spring MVC的應用程序,可以使用ErrorPageRegistrar接口直接注冊ErrorPages。這種抽象直接與底層嵌入式Servlet容器一起工作,即使沒有Spring MVC DispatcherServlet也能工作。
@Configuration
public class PackErrorPagesConfiguration {
@Bean
public ErrorPageRegistrar errorPageRegistrar() {
return this::registerErrorPages;
}
private void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
}
}
4. 默認錯誤頁注冊原理
這里以Tomcat為例,SpringBoot內嵌tomcat容器會自動注冊TomcatServletWebServerFactory該類進行Tomcat容器的配置,這其中就包括將錯誤頁注冊到tomcat中。并且該類實現了ErrorPageRegistry接口,該類專門用來注冊錯誤頁。
public class TomcatServletWebServerFactory {
public WebServer getWebServer(...) {
Tomcat tomcat = new Tomcat();
// ...
prepareContext(...);
}
protected void prepareContext(...) {
// ...
configureContext(...)
}
protected void configureContext(...) {
// ...
// 獲取容器中定義的所有ErrorPage錯誤頁
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
}
}
這些ErrorPage通過如下方式被添加到上面的TomcatServletWebServerFactory中
SpringBoot會注冊一個ErrorPageRegistrarBeanPostProcessor處理器
public class ErrorPageRegistrarBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 上面說了TomcatServletWebServerFactory實現了ErrorPageRegistry接口
if (bean instanceof ErrorPageRegistry) {
postProcessBeforeInitialization((ErrorPageRegistry) bean);
}
return bean;
}
private void postProcessBeforeInitialization(ErrorPageRegistry registry) {
for (ErrorPageRegistrar registrar : getRegistrars()) {
registrar.registerErrorPages(registry);
}
}
private Collection<ErrorPageRegistrar> getRegistrars() {
if (this.registrars == null) {
// 獲取容器中的所有ErrorPageRegistrar
this.registrars = new ArrayList<>(
this.beanFactory.getBeansOfType(ErrorPageRegistrar.class, false, false).values());
this.registrars.sort(AnnotationAwareOrderComparator.INSTANCE);
this.registrars = Collections.unmodifiableList(this.registrars);
}
return this.registrars;
}
}
注意:自定義ErrorPageRegistrar時,我們可以通過實現Ordered接口控制優先級
以上是本篇文章的全部內容,希望對你有幫助。