最近在研究springboot的异常原理和处理机制,搜集了很多资料,分析,汇总如下:
一、springboot默认的异常处理机制。
springboot默认的异常产生后,会通过“/error”这个映射去找寻控制器处理,默认情况是通过BasicErrorController中处理。
此时会根据Content-Type是json或者html,
(application/json;charset=UTF-8和 text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8)
有两种默认的处理机制,一种是返回“Whitelabel Error Page“,还一种是返回:
{
"timestamp": "2018-07-19T12:18:41.810+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/hello"
}。(第二种情况如果配置了视图解析器,那么返回的视图,如果没配置返回json数据)
二、自定义异常页面
这个分静态的自定义页面和动态的自定义页面。
1、先从最简单的开始,直接在/resources/templates
下面创建error.html就可以覆盖默认的Whitelabel Error Page
的错误页面,我项目用的是thymeleaf模板,对应的error.html代码如下:
Title
动态error错误页面
2.2、此外,如果你想更精细一点,根据不同的状态码返回不同的视图页面,也就是对应的404,500等页面,这里分两种,错误页面可以是静态HTML(即,添加到任何静态资源文件夹下),也可以使用模板构建,文件的名称应该是确切的状态码。
如果只是静态HTML页面,不带错误信息的,在resources/public/下面创建error目录,在error目录下面创建对应的状态码html即可 ,
静态404.html简单页面如下:
Title
静态404错误页面
这样访问一个错误路径的时候,就会显示静态404错误页面
错误页面
注:这时候如果存在上面第一种介绍的error.html页面,则状态码错误页面将覆盖error.html,具体状态码错误页面优先级比较高。
如果是动态模板页面,可以带上错误信息,在resources/templates/
下面创建error目录,在error目录下面命名即可
这里我们模拟下500错误,控制层代码,模拟一个除0的错误:
@Controller
public class BaseErrorController extends AbstractController{
private Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value="/ex")
@ResponseBody
public String error(){
int i=5/0;
return "ex";
}
}
500.html代码:
Title
动态500错误页面
这时访问 http://localhost:8080/ex 即可看到如下错误,说明确实映射到了500.html
注:如果同时存在静态页面500.html和动态模板的500.html,则后者覆盖前者。即templates/error/
这个的优先级比resources/public/error
高。
整体概括上面几种情况,如下:
error.html会覆盖默认的 whitelabel Error Page 错误提示
静态错误状态吗页面优先级别比error.html高,如果在public/和templates下都配置的静态状态码页面,,如果自定义了容器,默认去找public下的页面。如果是采用自定义controller去转发error/404的话,则会去templates下找页面。
动态模板错误页面优先级比静态错误页面高
3、上面是最简单的覆盖,下面是自定义覆盖:
@Configuration
public class ContainerConfig {
@Bean
public EmbeddedServletContainerCustomizer containerCustomizer(){
return new EmbeddedServletContainerCustomizer(){
@Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500"));
}
};
}
}
上面这段代码中HttpStatus.INTERNAL_SERVER_ERROR
就是对应500错误码,也就是说程序如果发生500错误,就会将请求转发到/error/500
这个映射来,那我们只要实现一个方法是对应这个/error/500
映射即可捕获这个异常做出处理(此处就是自定义了一个内嵌的servlet容器,产生错误后包装一个500的错误,转发到'/error/500”的映射上,而我们只需要写一个方法处理这个映射即可,未定义的错误,则会走默认的异常处理机制)此处如果自己未去手动处理这个映射,会自动去找静态资源。
注意:这里自定义只适用springboot 1.5.2的版本。
@RequestMapping("/error/500")
@ResponseBody
public String showServerError() {
return "server error";
}
这样,我们再请求前面提到的异常请求 http://localhost:8080/spring/ex 的时候,就会被我们这个方法捕获了。
这里我们就只对500做了特殊处理,并且返还的是字符串,如果想要返回视图,去掉 @ResponseBody注解,并返回对应的视图页面。如果想要对其他状态码自定义映射,在customize方法中添加即可。
上面这种方法虽然我们重写了/500映射,但是有一个问题就是无法获取错误信息,想获取错误信息的话,我们可以继承BasicErrorController或者干脆自己实现ErrorController接口,除了用来响应/error这个错误页面请求,可以提供更多类型的错误格式等(BasicErrorController在上面介绍SpringBoot默认异常机制的时候有提到)
这里博主选择直接继承BasicErrorController,然后把上面 /error/500
映射方法添加进来即可
@Controller
public class MyBasicErrorController extends BasicErrorController {
public MyBasicErrorController() {
super(new DefaultErrorAttributes(), new ErrorProperties());
}
/**
* 定义500的ModelAndView
* @param request
* @param response
* @return
*/
@RequestMapping(produces = "text/html",value = "/500")
public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {
response.setStatus(getStatus(request).value());
Map model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
model.put("msg","自定义错误信息");
return new ModelAndView("error/500", model);
}
/**
* 定义500的错误JSON信息
* @param request
* @return
*/
@RequestMapping(value = "/500")
@ResponseBody
public ResponseEntity
代码也很简单,只是实现了自定义的500错误的映射解析,分别对浏览器请求以及json请求做了回应。
BasicErrorController默认对应的@RequestMapping是/error
,固我们方法里面对应的@RequestMapping(produces = "text/html",value = "/500")
实际上完整的映射请求是/error/500
,这就跟上面 customize 方法自定义的映射路径对上了。
errorHtml500 方法中,我返回的是模板页面,对应/templates/error/500.html,这里顺便自定义了一个msg信息,在500.html也输出这个信息,如果输出结果有这个信息,则表示我们配置正确了。
再次访问请求http://localhost:8080/ex ,则会返回
Internal Server Error
/ by zero
500
自定义错误信息
注意JSON 返回结果如下:
{
"timestamp": 1532091929412,
"status": 500,
"error": "Internal Server Error",
"exception": "java.lang.ArithmeticException",
"message": "/ by zero",
"path": "/ex"
}
注意:上述基于继承BasicErrorController的实现,必须在进行内在EmbeddedServletContainerCustomizer自定义的情况下 才起作用。因为这种方式其实只是将/erro/500的映射到具体的视图。故只在s'pringboot1.5.2版本适用。 上述基于ErrorController的实现都是全局异常处理。并且处理的是进入Controller的报错处理。
之前有说全局异常 分几种情况:
1、进入Controller前,就是404的错误,这种情况,采用springboot的默认机制去处理。也可以自定义:
@RestController
public class FinalExceptionHandler implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}
@RequestMapping(value = "/error")
public Object error(HttpServletResponse resp, HttpServletRequest req) {
// 错误处理逻辑
return "其他异常";
}
}
注意着这个会上面的BasicErrorController会冲突,所以跳过。
2、进入Controller之后,在进入方法之后,进入具体业务逻辑前发生的错误。譬如传的参数类型错误,可以通过局部异常去处理。
https://blog.csdn.net/beauxie/article/details/78989730
这篇文章是讲自定义注解实现非空参数校验。
https://blog.csdn.net/tianyaleixiaowu/article/details/70145251
这篇也讲到通过@ControllerAdvice和ResponseEntityExceptionHandler来实现。本人未研究,后期有时间可以再看下。
3、以上都正常时,在controller里执行逻辑代码时出的异常。譬如NullPointerException。
Spring Boot提供的ErrorController是一种全局性的容错机制。此外,你还可以用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。
这里介绍两种情况:
局部异常处理 @Controller + @ExceptionHandler
全局异常处理 @ControllerAdvice + @ExceptionHandler
局部异常主要用到的是@ExceptionHandler注解,此注解注解到类的方法上,当此注解里定义的异常抛出时,此方法会被执行。如果@ExceptionHandler所在的类是@Controller,则此方法只作用在此类。如果@ExceptionHandler所在的类带有@ControllerAdvice注解,则此方法会作用在全局。
该注解用于标注处理方法处理那些特定的异常。被该注解标注的方法可以有以下任意顺序的参数类型:
Throwable、Exception 等异常对象;
ServletRequest、HttpServletRequest、ServletResponse、HttpServletResponse;
HttpSession 等会话对象;
org.springframework.web.context.request.WebRequest;
java.util.Locale;
java.io.InputStream、java.io.Reader;
java.io.OutputStream、java.io.Writer;
org.springframework.ui.Model;
并且被该注解标注的方法可以有以下的返回值类型可选:
ModelAndView;
org.springframework.ui.Model;
java.util.Map;
org.springframework.web.servlet.View;
@ResponseBody 注解标注的任意对象;
HttpEntity or ResponseEntity;
void;
以上罗列的不完全,更加详细的信息可参考:[Spring ExceptionHandler](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html)。
举个简单例子,这里我们对除0异常用@ExceptionHandler来捕捉。
@Controller
public class BaseErrorController extends AbstractController{
private Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value="/ex")
@ResponseBody
public String error(){
int i=5/0;
return "ex";
}
//局部异常处理
@ExceptionHandler(Exception.class)
@ResponseBody
public String exHandler(Exception e){
// 判断发生异常的类型是除0异常则做出响应
if(e instanceof ArithmeticException){
return "发生了除0异常";
}
// 未知的异常做出响应
return "发生了未知异常";
}
}
image.png
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。
简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。
我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。
/**
* 返回数据
*/
public class AjaxObject extends HashMap {
private static final long serialVersionUID = 1L;
public AjaxObject() {
put("code", 0);
}
public static AjaxObject error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static AjaxObject error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static AjaxObject error(int code, String msg) {
AjaxObject r = new AjaxObject();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static AjaxObject ok(String msg) {
AjaxObject r = new AjaxObject();
r.put("msg", msg);
return r;
}
public static AjaxObject ok(Map map) {
AjaxObject r = new AjaxObject();
r.putAll(map);
return r;
}
public static AjaxObject ok() {
return new AjaxObject();
}
public AjaxObject put(String key, Object value) {
super.put(key, value);
return this;
}
public AjaxObject data(Object value) {
super.put("data", value);
return this;
}
public static AjaxObject apiError(String msg) {
return error(1, msg);
}
}
上面这个AjaxObject就是我平时用的,如果是正确情况返回的就是:
{
code:0,
msg:“获取列表成功”,
data:{
queryList :[]
}
}
正确默认code返回0,data里面可以是集合,也可以是对象,如果是异常情况,返回的json则是:
{
code:500,
msg:“未知异常,请联系管理员”
}
然后创建一个自定义的异常类:
public class BusinessException extends RuntimeException implements Serializable {
private static final long serialVersionUID = 1L;
private String msg;
private int code = 500;
public BusinessException(String msg) {
super(msg);
this.msg = msg;
}
public BusinessException(String msg, Throwable e) {
super(msg, e);
this.msg = msg;
}
public BusinessException(int code,String msg) {
super(msg);
this.msg = msg;
this.code = code;
}
public BusinessException(String msg, int code, Throwable e) {
super(msg, e);
this.msg = msg;
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
注:spring 对于 RuntimeException 异常才会进行事务回滚
Controler中添加一个json映射,用来处理这个异常
@Controller
public class BaseErrorController{
@RequestMapping("/json")
public void json(ModelMap modelMap) {
System.out.println(modelMap.get("author"));
int i=5/0;
}
}
最后创建这个全局异常处理类:
/**
* 异常处理器
*/
@RestControllerAdvice
public class BusinessExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 应用到所有@RequestMapping注解方法,在其执行之前初始化数据绑定器
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
System.out.println("请求有参数才进来");
}
/**
* 把值绑定到Model中,使全局@RequestMapping可以获取到该值
* @param model
*/
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("author", "嘟嘟MD");
}
@ExceptionHandler(Exception.class)
public Object handleException(Exception e,HttpServletRequest req){
AjaxObject r = new AjaxObject();
//业务异常
if(e instanceof BusinessException){
r.put("code", ((BusinessException) e).getCode());
r.put("msg", ((BusinessException) e).getMsg());
}else{//系统异常
r.put("code","500");
r.put("msg","未知异常,请联系管理员");
}
//使用HttpServletRequest中的header检测请求是否为ajax, 如果是ajax则返回json, 如果为非ajax则返回view(即ModelAndView)
String contentTypeHeader = req.getHeader("Content-Type");
String acceptHeader = req.getHeader("Accept");
String xRequestedWith = req.getHeader("X-Requested-With");
if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
|| (acceptHeader != null && acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return r;
} else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("msg", e.getMessage());
modelAndView.addObject("url", req.getRequestURL());
modelAndView.addObject("stackTrace", e.getStackTrace());
modelAndView.setViewName("error");
return modelAndView;
}
}
}
@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型,上面我配置了拦截Exception,
再根据不同异常类型返回不同的相应,最后添加判断,如果是Ajax请求,则返回json,如果是非ajax则返回view,这里是返回到error.html页面。
为了展示错误的时候更友好,我封装了下error.html,不仅展示了错误,还添加了跳转百度谷歌以及StackOverFlow的按钮,如下:
Spring Boot管理后台
访问http://localhost:8080/json的时候,因为是浏览器发起的,返回的是error界面:
image.png
如果是ajax请求,返回的就是错误:
{ "msg":"未知异常,请联系管理员", "code":500 }
这里我给带@ModelAttribute注解的方法通过Model设置了author值,在json映射方法中通过 ModelMwap 获取到改值。
认真的你可能发现,全局异常类我用的是@RestControllerAdvice,而不是@ControllerAdvice,因为这里返回的主要是json格式,这样可以少写一个@ResponseBody。
到此,SpringBoot中对异常的使用也差不多全了,本项目中处理异常的顺序会是这样,发送一个请求
拦截器那边先判断是否登录,没有则返回登录页。
在进入Controller之前,譬如请求一个不存在的地址,返回404错误界面。
在执行@RequestMapping时,发现的各种错误(譬如数据库报错、请求参数格式错误/缺失/值非法等)统一由@ControllerAdvice处理,根据是否Ajax返回json或者view。