正常的Web应用开发时,需要考虑到应用运行发生异常时或出现错误时如何来被处理,例如捕获必要的异常信息,记录日志方便日后排错,友好的用户响应输出等等.而应用程序发生错误,有可能是应用自身的问题,也有可能是客户端操作的问题.在我们的项目中全局异常处理非常重要.
大概有三种异常情况:
1️⃣.在进入Controller之前,譬如请求一个不存在的地址404错误;
2️⃣.在执行@RequestMapping时,进入逻辑处理阶段前,譬如传的参数类型错误;
3️⃣.以上都正常时,在controller里执行逻辑代码时出的异常,比如出现NullPointerException.
默认情况下,Spring Boot为两种情况提供了不同的响应方式。
一种是浏览器客户端访问应用发生错误时,一般情况下浏览器默认发送的请求头中Accept: text/html(当然你更改了就另当别论了),所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。
2. 第三方工具请求接口时
另一种是机器客户端访问应用发送错误时,Spring Boot会响应Json格式内容。这种情况更常见于利用第三方的Http工具请求接口时。
如果看过源码,会发现两种方式输出用到的内容项是一样的,只不过第一种方式用html格式显示,第二种方式使用了Json格式。
Spring Boot提供这个错误处理机制靠自动配置的BasicErrorController类。如果好奇Spring Boot是如何实现这种机制的。可以参看下BasicErrorController源码以及SpringMVC请求映射匹配规则。
上述的默认错误处理机制是一种通用的做法,你可能更期望细化一些处理,对于某些错误你可能想特殊对待。
Spring Boot提供了一种方式,笔者认为这种方式是“容器级别”的操作。
如下:
@Configuration
public class ContainerConfig {
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer(){
return new MyCustomizer();
}
private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/500"));
}
}
}
笔者新增了一个配置类,里面定义了一个bean,主要目的在于针对响应码为500的错误,采用笔者自定义的方式处理。如下:
@RestController
public class ExceptionController {
@RequestMapping("/exception")
public void catchException() {
throw new RuntimeException("error occur");
}
@RequestMapping("/500")
public String showServerError() {
return "server error";
}
}
那么浏览器中结果如下:
很明显这种方式依赖于响应状态码进行定制。看到这里你应该会眼熟,还记得web.xml文件里的配置吗?
java.lang.Throwable
/error/500
500
/error/500
401
/error/401
403
/error/403
笔者认为,Spring Boot的默认错误处理机制,包括自定义的错误页面,与原来的web.xml中的error-page配置是相通的,只不过Spring Boot应用默认使用内嵌Servlet容器,web.xml不见了而已。查看源码你会发现,Spring Boot在应用发生错误时会转向"/error"请求,即交由BasicErrorController处理。
默认错误处理机制的响应内容格式不一定是你相中的,理由可能如下:
1️⃣.“Whitelabel Error Page”页面的样式太单调,用户体验不好;
2️⃣.Json格式的结果字符串不统一,与你配合的前端人员更希望统一格式,好做统一的显示处理。
1️⃣.自定义一个bean,实现ErrorController接口,那么默认的错误处理机制将不再生效。
2️⃣.自定义一个bean,继承BasicErrorController类,使用一部分现成的功能,自己也可以添加新的public方法,使用@RequestMapping及其produces属性指定新的地址映射。
3️⃣.自定义一个ErrorAttribute类型的bean,那么还是默认的两种响应方式,只不过改变了内容项而已。
先说下第三种方法,其实查看BasicErrorController源码,响应结果不论是html还是json,内容源都是ErrorAttribute。第三种方法只能改变内容,却改变不了格式,特别是html页面的样式。
其实指南上只是轻描淡写了几种方法,没有很好的示例,不知道是不是指南的作者认为Spring Boot默认的错误处理机制已经很适用了。无论第一种还是第二种办法,都需要你看下BasicErrorController的继承体系及实现,因为BasicErrorController也是实现了ErrorController。
采用第一种方式你可以具有完全的控制权,你可以摒弃默认的“Whitelabel Error Page”,指定自己的视图及视图样式,你可以指定响应的Json格式内容等等,因为BasicErrorController不再起作用,可以参考这里。
由于笔者参照了BasicErrorController的源码,感觉第二种方法可能更简便些,所以实现了第二种方法。第二种方法的思路其实就是你可以通过继承,利用BasicErrorController已有的功能,或者进行扩展。
那么如何覆盖默认的处理行为呢(虽然是自定义bean,但因为是继承,没有覆盖的话还是会采用默认的处理行为)?大致有两种思路。
按照指南上所述,你可以新建public方法,使用@RequestMapping及其produces属性,例如@RequestMapping(produces="application/json"),那么只要请求头中包含“Accept: application/json”,则会映射到你的方法进行处理。看到这儿你可能会疑惑,why?还是建议你先看下BasicErrorController源码。
因为是继承,所以对BasicErrorController默认两种错误处理方式的方法进行override。
笔者希望完全覆盖及可控,所以选择了第二种思路。笔者自定义了MyErrorController,继承于BasicErrorController,注意一定要添加@Controller,不然Spring无法感知自定义的bean,继承于BasicErrorController还是会起作用!
其他
Spring Boot提供的ErrorController是一种全局性的容错机制。你还可以使用SpringMVC提供的@ControllerAdvice。
如字面意思,@ControllerAdvice是切面技术的应用,允许你对Controller中抛出的某个或某些异常进行捕获并响应输出。用法如下:
@ControllerAdvice
public class DefaultExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultExceptionHandler.class); //日志记录器
@ExceptionHandler({MissingServletRequestParameterException.class, TypeMismatchException.class, IllegalArgumentException.class, IllegalStateException.class})
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public JsonResult conversionErrorHandler(Exception ex) {
//记录日志
LOGGER.error("参数异常捕获", ex);
return new JsonResult(false, ErrorCode2Msg.getMessage(10004));
}
}
笔者以往的开发习惯,使用@ControllerAdvice捕获应用级别的异常,使用web.xml中的error-page配置处理容器级别的报错。假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的(假设定义的过滤器抛出的异常,@ControllerAdvice是无法处理的)。
改用Spring Boot后,@ControllerAdvice没有捕获的异常,ErrorController会帮你“捡起来”。
package com.syc.boot.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
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.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* ControllerAdvice注解:控制器增强,
* 用来使@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法应用到
* 所有的 @RequestMapping注解的方法。
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 拦截所有Exception类的异常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Map exceptionHandler(Exception e) {
Map result = new HashMap<>();
result.put("respCode", "1000");
result.put("respMsg", e.getMessage());
return result;
}
/**
* 400 请求参数封装到bean时 类型转换错误
* 400 (Bad Request)
*/
@ExceptionHandler({MethodArgumentTypeMismatchException.class,
HttpMessageNotReadableException.class,
MissingServletRequestParameterException.class})
@ResponseBody
public Map handleRequestParamFormatError(Exception ex) {
log.error("handleRequestParamFormatError() bad request ex:{}", ex.getLocalizedMessage());
Map result = new HashMap<>();
result.put("respCode", "400");
result.put("respMsg", ex.getLocalizedMessage());
return result;
}
/**
* 404 (Not Found)
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseBody
public Map handleNoHandlerFoundException(NoHandlerFoundException ex, WebRequest request) {
log.error("handleNoHandlerFoundException() {}{}{} ", request, " exception message:", ex.getLocalizedMessage());
Map result = new HashMap<>();
result.put("respCode", "404");
result.put("respMsg", ex.getLocalizedMessage());
return result;
}
/**
* 405 (Method Not Allowed)
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
public Map handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
log.error("handleHttpRequestMethodNotSupportedException() request Method Not Allowed(405) exception message:{}", ex.getLocalizedMessage());
Map result = new HashMap<>();
result.put("respCode", "405");
result.put("respMsg", ex.getLocalizedMessage());
return result;
}
//跳转到自定义的错误处理页面
//由于工作中都是才有前后端分离开发模式,所以一般上都没有直接返回资源页的需求了,
// 一般上都是返回固定的响应格式,如respCode、respMsg、data,
// 前端通过判断respCode的值进行业务判断,是弹窗还是跳转页面。
//在校验不通过时,返回的异常信息是不友好的,此时可利用统一异常处理,
// 对校验异常进行特殊处理,特别说明下,对于异常处理类,
// 共有以下几种情况(被@RequestBody和@RequestParam注解的请求实体,校验异常类是不同的)
// @ExceptionHandler(MethodArgumentNotValidException.class)
// public Map handleBindException(MethodArgumentNotValidException ex) {
// FieldError fieldError = ex.getBindingResult().getFieldError();
// log.info("参数校验异常:{}({})", fieldError.getDefaultMessage(), fieldError.getField());
// Map result = new HashMap<>();
// result.put("respCode", "1001");
// result.put("respMsg", fieldError.getDefaultMessage());
// return result;
// }
}