1、根據SpringBoot的慣例或者說方法論,我們研究錯誤處理的話應該先找到錯誤處理的自動配置,趕巧,SpringBoot中確實有一個叫ErrorMvcAutoConfiguration的自動配置類,我們能看到它向Spring容器中注入了一系列的對象,包括DefaultErrorAttributes、BasicErrorController、ErrorPageCustomizer以及通過內部類DefaultErrorViewResolverConfiguration注入的DefaultErrorViewResolver
部分源碼如下:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes(this.serverProperties.getError().isIncludeException());
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
@Bean
public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
return new PreserveErrorControllerTargetClassPostProcessor();
}
@Configuration(proxyBeanMethods = false)
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
ResourceProperties resourceProperties) {
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resourceProperties);
}
}
2、ErrorPageCustomizer,錯誤頁面定製器
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
在源碼中有一個方法registerErrorPages()註冊錯誤頁面,傳入了一個ErrorPageRegistry對象,通過獲取ErrorProperties的path屬性的值(可通過server.error.path進行配置,默認是/error)實例化了一個ErrorPage對象,然後將一個ErrorPage添加到errorPageRegistry中;ErrorPageRegistry接口的實現類很多,SpringBoot默認是嵌入式Tomcat容器,因此此處是TomcatServletWebServerFactory,所以這裏將ErrorPage添加到了TomcatServletWebServerFactory中,也就是說SpringBoot默認出現錯誤時重定向到/error請求。
3、BasicErrorController,基礎錯誤處理器,SpringBoot幫我們定義了一個常見的Controller來處理錯誤
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
* @param errorViewResolvers error view resolvers
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorViewResolvers);
Assert.notNull(errorProperties, "ErrorProperties must not be null");
this.errorProperties = errorProperties;
}
@Override
public String getErrorPath() {
return this.errorProperties.getPath();
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
IncludeStacktrace include = getErrorProperties().getIncludeStacktrace();
if (include == IncludeStacktrace.ALWAYS) {
return true;
}
if (include == IncludeStacktrace.ON_TRACE_PARAM) {
return getTraceParameter(request);
}
return false;
}
/**
* Provide access to the error properties.
* @return the error properties
*/
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}
從源碼不難看出該Controller處理的就是上面提到的/error請求或者server.error.path配置的路徑,也就是說當請求出現錯誤時,便通過該Controller的相應方法進行處理,這樣的話事情就好辦了,我們把目光聚焦到該Controller的下面兩個方法上
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
第一個方法errorHtml上標註了@RequestMapping(produces = MediaType.TEXT_HTML_VALUE),因此當請求類型是text/html,也即是通過瀏覽器訪問時進入該方法,其他客戶端訪問時當然就進入第二個方法了,所以大家會發現在瀏覽器和Postman訪問同一個錯誤路徑時返回的數據是不同了,瀏覽器返回html,而postman返回json數據。
那麼這兩個方法的數據從哪裏來的呢,我們看到他們都調用瞭如下方法
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
該方法調用了當前對象中的errorAttributes屬性,這不就是最開頭提到的自動配置類中實例化的幾個類中的一個嗎?
4、DefaultErrorAttributes(ErrorAttributes接口的默認實現類)
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
上面的方法就是在BasicErrorController中的兩個處理方法中調用的方法,錯誤頁面和json數據就從此而來。
5、ErrorViewResolver,錯誤頁面解決器
我們再次回到BasicErrorController的errorHtml方法中,其中有如下這行代碼
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
接着是下面這個方法
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
我們看到該方法中出現了全局屬性errorViewResolvers,這裏從List遍歷出ErrorViewResolver來創建ModelAndView,只要不爲空便返回,由此可見只要我們向Spring容器中注入一個或多個ErrorViewResolver便可自定義錯誤頁面,並優先取出List中的第一個。
進入BasicErrorController父類AbstractErrorController中可看到如下代碼
public AbstractErrorController(ErrorAttributes errorAttributes, List<ErrorViewResolver> errorViewResolvers) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
}
private List<ErrorViewResolver> sortErrorViewResolvers(List<ErrorViewResolver> resolvers) {
List<ErrorViewResolver> sorted = new ArrayList<>();
if (resolvers != null) {
sorted.addAll(resolvers);
AnnotationAwareOrderComparator.sortIfNecessary(sorted);
}
return sorted;
}
從源碼中可以看到對errorViewResolvers這個List進行了排序,用的是AnnotationAwareOrderComparator比較器,深入該比較器不難看到,是通過@Order註解從小到大排序的,也即是Order標註的數字越小優先級越高。
好,到我們真正進入ErrorViewResolver的時候了,源碼如下,部分省略:
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resourceProperties resource properties
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//用錯誤代碼作爲ModelAndView的name,錯誤頁面的名稱
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
//如果頁面不存在,則判斷是否存在4xx,5xx命名的頁面
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
private ModelAndView resolve(String viewName, Map<String, Object> model) {
//錯誤頁面需要放到error目錄中
String errorViewName = "error/" + viewName;
//判斷當前項目是否使用了模板引擎
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
//如果當前項目沒有使用模板引擎,則調用該方法
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resourceProperties.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
//錯誤頁面需.html結尾
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
}
}
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
}
從源碼的resolveErrorView方法可看出,默認是通過錯誤代碼(404,500等)作爲錯誤頁面的名稱的,如果找不到錯誤代碼命名的頁面,則查找4xx,5xx命名的頁面。
這裏還需要注意resolve方法,在該方法中先進行判斷當前項目是否使用了模板引擎,如果使用了則從templates目錄下的error目錄中查找對應的錯誤頁面;如果未使用,從resolveResource方法中可以看到需要從ResourceProperties的staticLocations屬性保存的路徑下的error目錄中去查找,他們默認是["classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"],如下源碼
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
如果以上都找不到則使用SpringBoot提供的默認的StaticView,該類定義在ErrorMvcAutoConfiguration自動配置類中。
6、如何定製錯誤處理頁面
通過上面的分析我們已完全掌握了SpringBoot的錯誤處理流程,因此如何定製肯定不是難事了。
我們再次回到ErrorMvcAutoConfiguration,會發現上面幾個類的實例化方法除了basicErrorController()都有@ConditionalOnMissingBean(value = XXXX.class, search = SearchStrategy.CURRENT)註解,意思是當Spring容器中不存在上述這些類的對象時才通過該方法注入對象,因此咱們可以定義自己的類去繼承他們,並在配置類中實例化注入到Spring容器中即可能使用。
1)、如果項目使用了模板引擎,那麼只需要在templates目錄下新建error目錄,在其中建我們的錯誤頁面即可;如果未使用則一般在static目錄下新建error目錄保存錯誤頁面。
2)、重寫ErrorViewResolvers並注入到Spring容器中,改寫創建ModelAndView的方法從而達到目的
3)、重寫DefaultErrorAttributes來改寫創建Model數據的方法,這種辦法可以改寫json返回的數據
4)、最後當然是使用SpringBoot提供的@ControllerAdvice和@ExceptionHandler(XXXException.class)兩個註解來根據不同的異常定製錯誤處理方法。