转载自
https://www.jianshu.com/p/accec85b4039
https://www.cnblogs.com/xuwujing/p/10933082.html
https://www.jianshu.com/p/d458694e739f
前言
在日常web开发中发生了异常,往往是需要通过一个统一的异常处理来保证客户端能够收到友好的提示。
1. Spring Boot默认的异常处理机制
默认情况下,Spring Boot为两种情况提供了不同的响应方式。
一种是浏览器客户端请求一个不存在的页面或服务端处理发生异常时,一般情况下浏览器默认发送的请求头中Accept: text/html,所以Spring Boot默认会响应一个html文档内容,称作“Whitelabel Error Page”。
另一种是使用Postman等调试工具发送请求一个不存在的url或服务端处理发生异常时,Spring Boot会返回类似如下的Json格式字符串信息
1 | { |
原理也很简单,Spring Boot 默认提供了程序出错的结果映射路径/error。这个/error请求会在BasicErrorController中处理,其内部是通过判断请求头中的Accept的内容是否为text/html来区分请求是来自客户端浏览器(浏览器通常默认自动发送请求头内容Accept:text/html)还是客户端接口的调用,以此来决定返回页面视图还是 JSON 消息内容。
相关BasicErrorController中代码如下:
2. 如何自定义错误页面
好了,了解完Spring Boot默认的错误机制后,我们来点有意思的,浏览器端访问的话,任何错误Spring Boot返回的都是一个Whitelabel Error Page
的错误页面,这个很不友好,所以我们可以自定义下错误页面。
1、先从最简单的开始,直接在/resources/templates
下面创建error.html就可以覆盖默认的Whitelabel Error Page
的错误页面,我项目用的是thymeleaf模板,对应的error.html代码如下:
1 | <!DOCTYPE html> |
这样运行的时候,请求一个不存在的页面或服务端处理发生异常时,展示的自定义错误界面如下:
2、此外,如果你想更精细一点,根据不同的状态码返回不同的视图页面,也就是对应的404,500等页面,这里分两种,错误页面可以是静态HTML(即,添加到任何静态资源文件夹下),也可以使用模板构建,文件的名称应该是确切的状态码。
- 如果只是静态HTML页面,不带错误信息的,在resources/public/下面创建error目录,在error目录下面创建对应的状态码html即可 ,例如,要将404映射到静态HTML文件,您的文件夹结构如下所示:
静态404.html简单页面如下:
1 | <!DOCTYPE html> |
这样访问一个错误路径的时候,就会显示静态404错误页面
错误页面
注:这时候如果存在上面第一种介绍的error.html页面,则状态码错误页面将覆盖error.html,具体状态码错误页面优先级比较高.
- 如果是动态模板页面,可以带上错误信息,在
resources/templates/
下面创建error目录,在error目录下面命名即可:
这里我们模拟下500错误,控制层代码,模拟一个除0的错误:
1 | @Controller |
500.html代码:
1 | <!DOCTYPE html> |
注:如果同时存在静态页面500.html和动态模板的500.html,则后者覆盖前者。即
templates/error/
这个的优先级比resources/public/error
高。
整体概括上面几种情况,如下:
- error.html会覆盖默认的 whitelabel Error Page 错误提示
- 静态错误页面优先级别比error.html高
- 动态模板错误页面优先级比静态错误页面高
3、上面介绍的只是最简单的覆盖错误页面的方式来自定义,如果对于某些错误你可能想特殊对待,则可以这样
1 | @Configuration |
上面这段代码中HttpStatus.INTERNAL_SERVER_ERROR
就是对应500错误码,也就是说程序如果发生500错误,就会将请求转发到/error/500
这个映射来,那我们只要实现一个方法是对应这个/error/500
映射即可捕获这个异常做出处理.
1 | @RequestMapping("/error/500") |
这里我们就只对500做了特殊处理,并且返还的是字符串,如果想要返回视图,去掉 @ResponseBody注解,并返回对应的视图页面。如果想要对其他状态码自定义映射,在customize方法中添加即可。
上面这种方法虽然我们重写了/500映射,但是有一个问题就是无法获取错误信息,想获取错误信息的话,我们可以继承BasicErrorController或者干脆自己实现ErrorController接口,除了用来响应/error这个错误页面请求,可以提供更多类型的错误格式等(BasicErrorController在上面介绍SpringBoot默认异常机制的时候有提到)
这里博主选择直接继承BasicErrorController,然后把上面 /error/500
映射方法添加进来即可.
1 | @Controller |
代码也很简单,只是实现了自定义的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>
,如果输出结果有这个信息,则表示我们配置正确了。
3. 通过@ControllerAdvice注解来处理异常
Spring Boot提供的ErrorController是一种全局性的容错机制。此外,你还可以用@ControllerAdvice注解和@ExceptionHandler注解实现对指定异常的特殊处理。
这里介绍两种情况:
- 局部异常处理 @Controller + @ExceptionHandler
- 全局异常处理 @ControllerAdvice + @ExceptionHandler
1. 局部异常处理 @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;
以上罗列的不完全,更加详细的信息可参考:Spring ExceptionHandler.
举个简单例子,这里我们对除0异常用@ExceptionHandler来捕捉。
1 | @Controller |
2. 全局异常处理 @ControllerAdvice + @ExceptionHandler
在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中。
简单的说,进入Controller层的错误才会由@ControllerAdvice处理,拦截器抛出的错误以及访问错误地址的情况@ControllerAdvice处理不了,由SpringBoot默认的异常处理机制处理。
我们实际开发中,如果是要实现RESTful API,那么默认的JSON错误信息就不是我们想要的,这时候就需要统一一下JSON格式,所以需要封装一下。
1 | /** |
上面这个AjaxObject就是我平时用的,如果是正确情况返回的就是:
1 | { |
正确默认code返回0,data里面可以是集合,也可以是对象,如果是异常情况,返回的json则是:
1 | { |
然后创建一个自定义的异常类:
1 | public class BusinessException extends RuntimeException implements Serializable { |
spring 对于 RuntimeException 异常才会进行事务回滚
Controler中添加一个json映射,用来处理这个异常
1 | @Controller |
最后创建这个全局异常处理类:
1 | /** |
@ExceptionHandler 拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型,上面我配置了拦截Exception,
再根据不同异常类型返回不同的相应,最后添加判断,如果是Ajax请求,则返回json,如果是非ajax则返回view,这里是返回到error.html页面。
为了展示错误的时候更友好,我封装了下error.html,不仅展示了错误,还添加了跳转百度谷歌以及StackOverFlow的按钮,如下:
1 | <!DOCTYPE HTML> |
如果是ajax请求,返回的就是错误:
1 | { "msg":"未知异常,请联系管理员", "code":500 } |
这里我给带@ModelAttribute注解的方法通过Model设置了author值,在json映射方法中通过 ModelMwap 获取到改值。
认真的你可能发现,全局异常类我用的是@RestControllerAdvice,而不是@ControllerAdvice,因为这里返回的主要是json格式,这样可以少写一个@ResponseBody。
3. 小结
本文中处理异常的顺序会是这样,当发送一个请求:
拦截器那边先判断是否登录,没有则返回登录页。
在进入Controller之前,譬如请求一个不存在的地址,返回404错误界面。
在执行@RequestMapping时,发现的各种错误(譬如数据库报错、请求参数格式错误/缺失/值非法等)统一由@ControllerAdvice处理,根据是否Ajax返回json或者view。
4. 实战一
1. 依赖
1 | spring-boot-starter-parent |
配置文件这块基本不需要更改,全局异常的处理只需在代码中实现即可。
2. 代码编写
SpringBoot的项目已经对有一定的异常处理了,但是对于我们开发者而言可能就不太合适了,因此我们需要对这些异常进行统一的捕获并处理。SpringBoot中有一个ControllerAdvice
的注解,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用ExceptionHandler
注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。
1 | @ControllerAdvice |
对捕获的异常进行简单的二次处理,返回异常的信息,虽然这种能够让我们知道异常的原因,但是在很多的情况下来说,可能还是不够人性化,不符合我们的要求。那么我们这里可以通过自定义的异常类以及枚举类来实现我们想要的效果。
3. 自定义基础接口类
首先定义一个基础的接口类,自定义的错误描述枚举类需实现该接口。
1 | public interface BaseErrorInfoInterface{ |
4. 自定义枚举类
然后我们这里在自定义一个枚举类,并实现该接口。
1 | public enum CommonEnum implements BaseErrorInfoInterface{ |
5. 自定义异常类
然后我们在来自定义一个异常类,用于处理我们发生的业务异常。
1 | public class BizException extends RuntimeException{ |
6. 自定义数据格式
顺便这里我们定义一下数据的传输格式。
1 | public class ResultBody { |
7. 自定义全局异常处理类
最后我们在来编写一个自定义全局异常处理的类。
1 | @ControllerAdvice |
因为这里我们只是用于做全局异常处理的功能实现以及测试,所以这里我们只需在添加一个实体类和一个控制层类即可。
8. 实体类
万能的用户表
1 | @Data |
9. Controller控制层
控制层这边也比较简单,使用Restful风格实现的CRUD功能,不同的是这里我故意弄出了一些异常,好让这些异常被捕获到然后处理。这些异常中,有自定义的异常抛出,也有空指针的异常抛出,当然也有不可预知的异常抛出(这里我用类型转换异常代替),那么我们在完成代码编写之后,看看这些异常是否能够被捕获处理成功吧!
1 | @RestController |
10. App入口
1 | @SpringBootApplication |
11. 功能测试
我们成功启动该程序之后,使用Postman工具来进行接口测试。
首先进行查询,查看程序正常运行是否ok,使用GET 方式进行请求。
1 | GET http://localhost:8181/api/user |
可以看到程序正常返回,并没有因自定义的全局异常而影响。
然后我们再来测试下自定义的异常是否能够被正确的捕获并处理。
使用POST方式进行请求
1 | POST http://localhost:8181/api/user |
可以看出将我们抛出的异常进行数据封装,然后将异常返回出来。
然后我们再来测试下空指针异常是否能够被正确的捕获并处理。在自定义全局异常中,我们除了定义空指针的异常处理,也定义最高级别之一的Exception异常,那么这里发生了空指针异常之后,它是回优先使用哪一个呢?这里我们来测试下。
使用PUT方式进行请求。
1 | PUT http://localhost:8181/api/user |
我们可以看到这里的的确是返回空指针的异常护理,可以得出全局异常处理优先处理子类的异常。
那么我们在来试试未指定其异常的处理,看该异常是否能够被捕获。
使用DELETE方式进行请求。
1 | DELETE http://localhost:8181/api/user |
5 实战二
1. 全局异常
将返回值统一封装时我们没有考虑当接口抛出异常的情况。当接口抛出异常时让用户直接看到服务端的异常肯定是不够友好的,而我们也不可能每一个接口都去try/catch进行处理,此时只需要使用@ExceptionHandler
注解即可无感知的全局统一处理异常。
1 | import com.chehejia.framework.beans.exception.BizException; |
2. 全局响应
在前后端分离大行其道的今天,有一个统一的返回值格式不仅能使我们的接口看起来更漂亮,而且还可以使前端可以统一处理很多东西,避免很多问题的产生。下面通过实现SpringBoot中为我们提供好的解决方法,只需要在项目中加上以下代码,即可实现相关功能。主要是实现接口ResponseBodyAdvice
1 | import com.chehejia.framework.beans.model.Response; |
3. 统一返回对象
1 | import com.chehejia.framework.beans.exception.BizException; |