文档章节

Spring Boot全局异常处理

狼王黄师傅
 狼王黄师傅
发布于 10/16 12:26
字数 3180
阅读 66
收藏 0

Spring Boot默认的异常处理机制

默认情况下,Spring Boot为两种情况提供了不同的响应方式。

    一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。

    另一种是使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的Json格式字符串信息

{
    "timestamp": "2018-05-12T06:11:45.209+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/index.html"
} 

    Spring Boot 默认提供了程序出错的结果映射路径/error。这个/error请求会在BasicErrorController中处理,其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。
    相关BasicErrorController中代码如下:

一、覆盖默认的错误页面

1、直接在/resources/templates下面创建error.html就可以覆盖默认的Whitelabel Error Page的错误页面,我项目用的是thymeleaf模板,对应的error.html代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
动态error错误页面
<p th:text="${error}"></p>
<p th:text="${status}"></p>
<p th:text="${message}"></p>
</body>
</html>

    这样运行的时候,请求一个不存在的页面或服务端处理发生异常时,展示的自定义错误界面如下:

2、如果根据不同的状态码返回不同的视图页面,也就是对应的404,500等页面,这里分两种,错误页面可以是静态HTML(即,添加到任何静态资源文件夹下),也可以使用模板构建,文件的名称应该是确切的状态码。

    静态页面:    

        如果只是静态HTML页面,不带错误信息的,在resources/public/下面创建error目录,在error目录下面创建对应的状态码html即可 ,例如,要将404映射到静态HTML文件,您的文件夹结构如下所示:

    静态404.html简单页面如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    静态404错误页面
</body>
</html>

    这样访问一个错误路径的时候,就会显示静态404错误页面错误页面

    如果/resources/templates存在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代码:

<!DOCTYPE html> 
<html xmlns:th="http://www.thymeleaf.org"> 
<head> 
<meta charset="UTF-8"> 
<title>Title</title> 
</head> 
<body> 
    动态500错误页面 
    <p th:text="${error}"></p> 
    <p th:text="${status}"></p> 
    <p th:text="${message}"></p> 
</body> 
</html> 

    这时访问 http://localhost:8080/spring/ex 即可看到如下错误,说明确实映射到了500.html

    如果同时存在静态页面500.html和动态模板的500.html,则后者覆盖前者。即templates/error/这个的优先级比resources/public/error高。

总结:

  • error.html会覆盖默认的 whitelabel Error Page 错误提示
  • 静态错误页面优先级别比error.html高
  • 动态模板错误页面优先级比静态错误页面高

 

二、自定义异常页面的路径

@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映射即可捕获这个异常做出处理

@ResponseBody
@RequestMapping("/error/500") 
public String showServerError() { 
    return "server error"; 
} 

    这样,我们再请求前面提到的异常请求 http://localhost:8080/spring/ex 的时候,就会被我们这个方法捕获了。

    这里只对500做了特殊处理,并且返还的是字符串,如果想要返回视图,去掉@ResponseBody注解,并返回对应的视图页面。如果想要对其他状态码自定义映射,在customize方法中添加即可。

    上面这种方法虽然我们重写了/500映射,但是有一个问题就是无法获取错误信息,想获取错误信息的话,我们可以继承BasicErrorController,除了用来响应/error这个错误页面请求,可以提供更多类型的错误格式等

@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<String, Object> 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<Map<String, Object>> error500(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));
        HttpStatus status = getStatus(request);
        return new ResponseEntity<Map<String, Object>>(body, status);
    }
}

    实现了自定义的500错误的映射解析,分别对浏览器请求以及json请求做了回应。

    BasicErrorController默认对应的@RequestMapping是/error,所有我们方法里面对应的@RequestMapping(produces = "text/html",value = "/500")实际上完整的映射请求是/error/500,这就跟上面 customize 方法自定义的映射路径对上了。

    errorHtml500 方法中,我返回的是模板页面,对应/templates/error/500.html,这里顺便自定义了一个msg信息,在500.html也输出这个信息<p th:text="${msg}"></p>,如果输出结果有这个信息,则表示我们配置正确了。

    再次访问请求http://localhost:8080/spring/ex ,结果如下:

 

三、局部异常处理 @Controller + @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;

    举个简单例子,这里我们对除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 "发生了未知异常"; 
	}
} 

 

四、全局异常处理 @ControllerAdvice + @ExceptionHandler

    在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。

    简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。

    我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。

/**
* 返回数据
*/
public class AjaxObject extends HashMap<String, Object> {
	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<String, Object> 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;
    }
}

    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的按钮,如下:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" layout:decorator="layout">
<head>
    <title>Spring Boot管理后台</title>
    <script type="text/javascript">
    </script>
</head>
<body>
<div layout:fragment="content" th:remove="tag">
    <div  id="navbar">
        <h1>系统异常统一处理</h1>
        <h3 th:text="'错误信息:'+${msg}"></h3>
        <h3 th:text="'请求地址:'+${url}"></h3>

        <h2>Debug</h2>
        <a th:href="@{'https://www.google.com/webhp?hl=zh-CN#safe=strict&hl=zh-CN&q='+${msg}}"
           class="btn btn-primary btn-lg" target="_blank" id="Google">Google</a>
        <a th:href="@{'https://www.baidu.com/s?wd='+${msg}}" class="btn btn-info btn-lg"  target="_blank" id="Baidu">Baidu</a>
        <a th:href="@{'http://stackoverflow.com/search?q='+${msg}}"
           class="btn btn-default btn-lg"  target="_blank" id="StackOverFlow">StackOverFlow</a>
        <h2>异常堆栈跟踪日志StackTrace</h2>
        <div th:each="line:${stackTrace}">
            <div th:text="${line}"></div>
        </div>
    </div>
</div>
<div layout:fragment="js" th:remove="tag">
</div>
</body>
</html>

    访问http://localhost:8080/json的时候,因为是浏览器发起的,返回的是error界面:

    如果是ajax请求,返回的就是错误:

{ "msg":"未知异常,请联系管理员", "code":500 }

    这里我给带@ModelAttribute注解的方法通过Model设置了author值,在json映射方法中通过 ModelMwap 获取到改值。

    全局异常类我用的是@RestControllerAdvice,而不是@ControllerAdvice,因为这里返回的主要是json格式,这样可以少写一个@ResponseBody。

总结

本项目中处理异常的顺序:当发送一个请求,

  • 拦截器那边先判断是否登录,没有则返回登录页。
  • 在进入Controller之前,譬如请求一个不存在的地址,返回404错误界面。
  • 在执行@RequestMapping时,发现的各种错误(譬如数据库报错、请求参数格式错误/缺失/值非法等)统一由@ControllerAdvice处理,根据是否Ajax返回json或者view。

© 著作权归作者所有

共有 人打赏支持
狼王黄师傅
粉丝 6
博文 205
码字总数 420619
作品 0
成都
程序员
私信 提问
baomidou/kaptcha-spring-boot-starter

kaptcha-spring-boot-starter 简介 kaptcha-spring-boot-starter 基于 springBoot2.0 和 Google Kaptcha 验证码组件,kaptcha-spring-boot-starter可以很方便的集成验证码到你的系统中。 如何...

baomidou
05/05
0
0
Spring boot 前后台分离项目 怎么处理spring security 抛出的异常

最近在开发一个项目 前后台分离的 使用 spring boot + spring security + jwt 实现用户登录权限控制等操作。但是 在用户登录的时候,怎么处理spring security 抛出的异常呢?使用了@RestCont...

道可到非常道
05/07
0
0
苞米豆 — 谷歌验证码快速启动器 1.1.0 发布

https://gitee.com/baomidou/kaptcha-spring-boot-starter 修复了一个重复校验引起的Np bug 支持了jdk 1.7 简介 kaptcha-spring-boot-starter 基于 springBoot 和 Google Kaptcha 验证码组件......

小锅盖
07/20
0
0
Spring Boot整合MyBatis学习总结

公司的很多项目都陆陆续续引入了Spring Boot,通过对Spring Boot的接触了解发现其真的是大大地简化了开发、简化了依赖配置,很多功能注解一下就可以实现,真的是太方便了。下面记录了一个Spr...

zhuwensheng
06/29
0
0
SpringCloud SpringBoot mybatis分布式Web应用的统一异常处理

我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射:/error,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用...

itcloud
08/17
0
0

没有更多内容

加载失败,请刷新页面

加载更多

docker部署springboot项目

安装docker 菜鸟教程 springboot项目 maven依赖 <?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001......

yimingkeji
今天
10
0
ios多个target

1.建立3个target,分别为heroone,heroone test,heroone dev;分别为正式环境,test环境,dev环境 2.注意取消掉autocreate以防止名字不对,分别以Duplicate的方式建立另外两个scheme 3.创建...

HeroHY
今天
5
0
php获取客户端IP

php获取客户端IP 首先先阅读关于IP真实性安全的文章:如何正確的取得使用者 IP? 「任何從客戶端取得的資料都是不可信任的!」 HTTP_CLIENT_IP头是有的,但未成标准,不一定服务器都实现。 ...

DrChenXX
昨天
0
0
. The valid characters are defined in RFC 7230 and RFC 问题

通过这里的回答,我们可以知道: Tomcat在 7.0.73, 8.0.39, 8.5.7 版本后,添加了对于http头的验证。 具体来说,就是添加了些规则去限制HTTP头的规范性 参考这里 具体来说: org.apache.tom...

west_coast
昨天
1
0
刷leetcode第704题-二分查找

今天双十一买的算法书到货了,路上刷到有人说的这个题,借(chao)鉴(xi)一下别人的思路,这个是C++标准库里面的经典方法,思路精巧,优雅好品味 int search(int* nums, int numsSize, in...

锟斤拷烫烫烫
昨天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部