SpringBoot錯誤信息處理機制
在一個web項目中,總需要對一些錯誤進行界面或者json數據返回,已實現更好的用戶體驗,SpringBoot中提供了對於錯誤處理的自動配置
ErrorMvcAutoConfiguration
這個類存放了所有關於錯誤信息的自動配置。
1. SpringBoot處理錯誤請求的流程
訪問步驟:
- 首先客戶端訪問了錯誤界面。例:404或者500
SpringBoot
注冊錯誤請求/error
。通過ErrorPageCustomizer
組件實現- 通過
BasicErrorController
處理/error
,對錯誤信息進行了自適應處理,瀏覽器會響應一個界面,其他端會響應一個json
數據 - 如果響應一個界面,通過
DefaultErrorViewResolver
類來進行具體的解析。可以通過模板引擎解析也可以解析靜態資源文件,如果兩者都不存在則直接返回默認的錯誤JSON
或者錯誤View
- 通過
DefaultErrorAttributes
來添加具體的錯誤信息
源代碼
//錯誤信息的自動配置
public class ErrorMvcAutoConfiguration {
//響應具體的錯誤信息
@Bean
public DefaultErrorAttributes errorAttributes() {
return
}
//處理錯誤請求
@Bean
public BasicErrorController basicErrorController() {
return
}
//注冊錯誤界面
@Bean
public ErrorMvcAutoConfiguration.ErrorPageCustomizer errorPageCustomizer() {
return
}
//注冊錯誤界面,錯誤界面的路徑為/error
private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
//服務器基本配置
private final ServerProperties properties;
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
//獲取服務器配置中的錯誤路徑/error
ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
//注冊錯誤界面
errorPageRegistry.addErrorPages(new ErrorPage[]{errorPage});
}
//this.properties.getError()
public class ServerProperties{
//錯誤信息的配置文件
private final ErrorProperties error = new ErrorProperties();
}
//getPath
public class ErrorProperties {
@Value("${error.path:/error}")
private String path = "/error";
//處理/error請求,從配置文件中取出請求的路徑
@RequestMapping({"${server.error.path:${error.path:/error}}"})
public class BasicErrorController extends AbstractErrorController {
//瀏覽器行為,通過請求頭來判斷,瀏覽器返回一個視圖
@RequestMapping(
produces = {"text/html"}
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
//其他客戶端行為處理,返回一個JSON數據
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
//添加錯誤信息
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
2. 響應一個視圖
步驟:
- 客戶端出現錯誤
SpringBoot
創建錯誤請求/error
BasicErrorController
處理請求
@RequestMapping(
produces = {"text/html"}
)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = this.getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
//解析錯誤界面,返回一個ModelAndView,調用父類AbstractErrorController的方法
ModelAndView modelAndView = this.resolveErrorView(request, response, status, model);
return modelAndView != null ? modelAndView : new ModelAndView("error", model);
}
public abstract class AbstractErrorController{
private final List<ErrorViewResolver> errorViewResolvers;
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
Iterator var5 = this.errorViewResolvers.iterator();
//遍歷所有的錯誤視圖解析器
ModelAndView modelAndView;
do {
if (!var5.hasNext()) {
return null;
}
ErrorViewResolver resolver = (ErrorViewResolver)var5.next();
//調用視圖解析器的方法,
modelAndView = resolver.resolveErrorView(request, status, model);
} while(modelAndView == null);
return modelAndView;
}
}
public interface ErrorViewResolver {
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
}
- 處理具體的視圖跳轉
//處理視圖跳轉
public DefaultErrorViewResolver{
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
//將狀態碼作為視圖名稱傳入解析
ModelAndView modelAndView = this.resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = this.resolve((String)SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
//視圖名稱為error文件夾下的400.html等狀態碼文件
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
//是否存在模板引擎進行解析
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext);
//存在則返回解析以后的數據,不存在調用resolveResource方法進行解析
return provider != null ? new ModelAndView(errorViewName, model) : this.resolveResource(errorViewName, model);
}
//如果靜態資源文件中存在,返回靜態文件下的,如果不存在返回SpringBoot默認的
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
String[] var3 = this.resourceProperties.getStaticLocations();
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
String location = var3[var5];
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
}
} catch (Exception var8) {
}
}
return null;
}
應用:
- 在模板引擎文件下創建error文件夾,里面放置各種狀態碼的視圖文件,模板引擎會解析
- 在靜態資源下常見error文件夾,里面放置各種狀態碼的視圖文件,模板引擎不會解析
- 如果沒有狀態碼文件,則返回springBoot默認界面視圖
3.響應一個json數據
在BasicErrorController
處理/error
請求的時候不適用瀏覽器默認請求
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = this.getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity(status);
} else {
//調用父類的方法獲取所有的錯誤屬性
Map<String, Object> body = this.getErrorAttributes(request, this.isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity(body, status);
}
}
父類方法:
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, boolean includeStackTrace) {
WebRequest webRequest = new ServletWebRequest(request);
//調用ErrorAttributes接口的getErrorAttributes方法,
return this.errorAttributes.getErrorAttributes(webRequest, includeStackTrace);
}
添加錯誤信息
public class DefaultErrorAttributes{
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
this.addStatus(errorAttributes, webRequest);
this.addErrorDetails(errorAttributes, webRequest, includeStackTrace);
this.addPath(errorAttributes, webRequest);
return errorAttributes;
}
返回的json數據有:
- status
- error
- exception
- message
- trace
- path
可以通過模板引擎獲取這些值
4.自定義異常返回自定義的異常數據
4.1@ControllerAdvice注解
SpringMVC提供的注解,可以用來定義全局異常,全局數據綁定,全局數據預處理
@ControllerAdivice
定義全局的異常處理
- 通過
@ExceptionHandler(XXXException.class)
執行該方法需要處理什么異常,然后返回什么數據或者視圖
//json數據返回 ,處理自定義用戶不存在異常
@ResponseBody
@ExceptionHandler(UserException.class)
public Map<String,String> userExceptionMethod(UserException us){
Map<String,String> map = new HashMap<>();
map.put("message",us.getMessage());
return map ;
}
@ControllerAdvice
定義全局數據
- 通過
@ModelAttribute(Name="key")
定義全局數據的key - 默認方法的返回值的名稱作為鍵
- 在
Controller
中通過Model
獲取對應的key的值
@ControllerAdvice
public MyConfig{
@ModelAttribute(name = "key")
public Map<String,String> defineAttr(){
Map<String,String> map = new HashMap<>();
map.put("message","幻聽");
map.put("update","許嵩");
return map ;
}
@Controller
public UserController{
@GetMapping("/hello")
public Map<String, Object> hello(Model model){
Map<String, Object> asMap = model.asMap();
System.out.println(asMap);
//{key={message='上山',update='左手一式太極拳'}}
return asMap ;
}
}
@ControllerAdvice
處理預處理數據(當需要添加的實體,屬性名字相同的時候)
- 在
Controller
的參數中添加ModelAttribute
作為屬性賦值的前綴 - 在
ControllerAdvice
修飾的類中,結合InitBinder
來綁定對應的屬性(該屬性為ModelAttribite的value值 - 在
@InitBinder
修飾的方法中通過WebDataBinder
添加默認的前綴
@Getter@Setter
public class Book {
private String name ;
private int age ;
@Getter@Setter
public class Music {
private String name ;
private String author ;
//這種方式的處理,spring無法判斷Name屬性給哪個bean賦值,所以需要通過別名的方式來進行賦值
@PostMapping("book")
public String book(Book book , Music music){
System.out.println(book);
System.out.println(music);
return "404" ;
}
//使用以下的方式
@PostMapping("/book")
public String book(@ModelAttribute("b")Book book , @ModelAttribute("m")Music music){
System.out.println(book);
System.out.println(music);
return "404" ;
}
public MyCOnfiguration{
@InitBinder("b")
public void b(WebDataBinder webDataBinder){
webDataBinder.setFieldDefaultPrefix("b.");
}
@InitBinder("m")
public void m(WebDataBinder webDataBinder){
webDataBinder.setFieldDefaultPrefix("m.");
}
}
4.2自定義異常JSON
瀏覽器和其他客戶端都只能獲取json
數據
@ControllerAdvice
public class MyExceptionHandler {
//處理UserException異常
@ResponseBody
@ExceptionHandler(UserException.class)
public Map<String,String> userExceptionMethod(UserException us){
Map<String,String> map = new HashMap<>();
map.put("message",us.getMessage());
map.put("status","500");
return map ;
}
4.2自定義異常返回一個視圖,擁有自適應效果
@ExceptionHandler(UserException.class)
public String allException(UserException e,HttpServletRequest request){
Map<String,String> map = new HashMap<>();
map.put("message",e.getMessage());
map.put("load","下山");
request.setAttribute("myMessage",map);
//設置狀態碼,SpringBoot通過java.servlet.error.status_code來設置狀態碼
request.setAttribute("javax.servlet.error.status_code",400);
return "forward:/error" ;
}
當拋出UserException
異常的時候,來到這個異常處理器,給這個請求中添加了數據,再轉發到這個error請求中,交給ErrorPageCustomizer
處理,由於設置了請求狀態碼400
則返回的視圖為400或4XX視圖,或者直接返回一個JSON
數據
{
"timestamp": "2020-02-19T04:17:43.394+0000",
"status": 400,
"error": "Bad Request",
"message": "用戶名不存在異常",
"path": "/crud/user/login"
}
- 不足:JSON數據中沒有顯示我們自己定義的錯誤信息
4.3自定義錯誤信息
前面提到SpringBoot對錯誤信息的定義存在於DefaultErrorAttributes類的getErrorAttributes中,我們可以直接繼承這個類,或者實現ErrorAttributes接口,然后將我們自己實現的錯誤處理器添加到容器中即可。
繼承DefaultErrorAttributes
和實現ErrorAttributes
接口的區別是,繼承以后仍然可以使用SpringBoot默認的錯誤信息,我們僅僅對該錯誤信息進行了增強;實現了ErrorAttributes
接口,完全自定義錯誤信息
- 實現
ErrorAttributes
接口
public class MyErrorHandler implements ErrorAttributes {
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("status",500);
errorAttributes.put("message","用戶不存在異常");
return errorAttributes;
}
@Override
public Throwable getError(WebRequest webRequest) {
return null;
}
- 繼承
DefaultErrorAttributes
的方法
public class MyErrorHandler extends DefaultErrorAttributes {
public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
//調用父類方法,直接在默認錯誤信息的基礎上添加
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest,includeStackTrace);
errorAttributes.put("timestamp", new Date());
errorAttributes.put("message","用戶不存在異常");
return errorAttributes;
}
}
- 將定義的錯誤信息器添加到容器中
- 通過
@Component
組件直接將MyErrorHandler
組件添加到容器中 - 通過
@Bean
在配置類中將組件添加到容器中
- 通過
@Bean
public DefaultErrorAttributes getErrorHandler(){
return new MyErrorHandler();
}
- 下面解決上一節中沒有出現我們自定義的異常信息
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest,includeStackTrace);
errorAttributes.put("timestamp", new Date());
errorAttributes.put("message","用戶不存在異常");
//指定從哪個作用域中取值
webRequest.getAttribute("myMessage", RequestAttributes.SCOPE_REQUEST);
return errorAttributes;
將在異常處理器中定義的錯誤信息取出,然后添加到錯誤信息中。