Spring Boot异常处理
一.默认映射
我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射:/error
,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容。
选择一个之前实现过的Web应用为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如:
1 @RequestMapping("/hello") 2 public String hello() throws Exception { 3 throw new Exception("发生错误"); 4 }
此时,可以看到类似下面的报错页面,该页面就是Spring Boot提供的默认error映射页面。
alt=默认的错误页面
二.映射到页面
虽然,Spring Boot中实现了默认的error映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示。
下面我们以之前的Web应用例子为基础,进行统一异常处理的改造。
- 创建全局异常处理类:通过使用
@ControllerAdvice
定义统一的异常处理类,而不是在每个Controller中逐个定义。@ExceptionHandler
用来定义函数针对的异常类型,最后将Exception对象和请求URL映射到error.html
中
1 @ControllerAdvice 2 class GlobalExceptionHandler { 3 4 public static final String DEFAULT_ERROR_VIEW = "error"; 5 6 @ExceptionHandler(value = Exception.class) 7 public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception { 8 ModelAndView mav = new ModelAndView(); 9 mav.addObject("exception", e); 10 mav.addObject("url", req.getRequestURL()); 11 mav.setViewName(DEFAULT_ERROR_VIEW); 12 return mav; 13 } 14 15 }
- 实现
error.html
页面展示:在templates
目录下创建error.html
,将请求的URL和Exception对象的message输出。
1 <!DOCTYPE html> 2 <html> 3 <head lang="en"> 4 <meta charset="UTF-8" /> 5 <title>统一异常处理</title> 6 </head> 7 <body> 8 <h1>Error Handler</h1> 9 <div th:text="${url}"></div> 10 <div th:text="${exception.message}"></div> 11 </body> 12 </html>
启动该应用,访问:http://localhost:8080/hello
,可以看到如下错误提示页面。
alt=自定义的错误页面
通过实现上述内容之后,我们只需要在Controller
中抛出Exception
,当然我们可能会有多种不同的Exception
。然后在@ControllerAdvice
类中,根据抛出的具体Exception
类型匹配@ExceptionHandler
中配置的异常类型来匹配错误映射和处理。
三.返回JSON格式
在上述例子中,通过@ControllerAdvice
统一定义不同Exception映射到不同错误处理页面。而当我们要实现RESTful API时,返回的错误是JSON格式的数据,而不是HTML页面,这时候我们也能轻松支持。
在上述例子中,通过@ControllerAdvice
统一定义不同Exception映射到不同错误处理页面。而当我们要实现RESTful API时,返回的错误是JSON格式的数据,而不是HTML页面,这时候我们也能轻松支持。
本质上,只需在@ExceptionHandler
之后加入@ResponseBody
,就能让处理函数return的内容转换为JSON格式。
下面以一个具体示例来实现返回JSON格式的异常处理。
- 创建统一的JSON返回对象,code:消息类型,message:消息内容,url:请求的url,data:请求返回的数据
1 public class ErrorInfo<T> { 2 3 public static final Integer OK = 0; 4 public static final Integer ERROR = 100; 5 6 private Integer code; 7 private String message; 8 private String url; 9 private T data; 10 11 // 省略getter和setter 12 13 }
- 创建一个自定义异常,用来实验捕获该异常,并返回json
1 public class MyException extends Exception { 2 3 public MyException(String message) { 4 super(message); 5 } 6 7 }
Controller
中增加json映射,抛出MyException
异常
1 @Controller 2 public class HelloController { 3 4 @RequestMapping("/json") 5 public String json() throws MyException { 6 throw new MyException("发生错误2"); 7 } 8 9 }
- 为
MyException
异常创建对应的处理
1 @ControllerAdvice 2 public class GlobalExceptionHandler { 3 4 @ExceptionHandler(value = MyException.class) 5 @ResponseBody 6 public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception { 7 ErrorInfo<String> r = new ErrorInfo<>(); 8 r.setMessage(e.getMessage()); 9 r.setCode(ErrorInfo.ERROR); 10 r.setData("Some Data"); 11 r.setUrl(req.getRequestURL().toString()); 12 return r; 13 } 14 15 }
- 启动应用,访问:http://localhost:8080/json,可以得到如下返回内容:
{ code: 100, data: "Some Data", message: "发生错误2", url: "http://localhost:8080/json" }
代码示例至此,已完成在Spring Boot中创建统一的异常处理,实际实现还是依靠Spring MVC的注解,更多更深入的使用可参考Spring MVC的文档。
本文的相关例子可以查看下面仓库中的chapter3-1-6
目录:
- Github:https://github.com/dyc87112/SpringBoot-Learning
- Gitee:https://gitee.com/didispace/SpringBoot-Learning
附:
当一个Controller中有方法加了@ExceptionHandler之后,这个Controller其他方法中没有捕获的异常就会以参数的形式传入加了@ExceptionHandler注解的那个方法中。
首先需要为自己的系统设计一个自定义的异常类,通过它来传递状态码。
1 /** * Created by liuruijie. 2 * 自定义异常 3 */ 4 public class SystemException extends RuntimeException{ 5 private String code;//状态码 6 public SystemException(String message, String code) { 7 super(message); 8 this.code = code; 9 } 10 public String getCode() { 11 return code; 12 } 13 }
第一种思路,设计一个基类。
1 /** 2 * Created by liuruijie. 3 * 处理异常的类,需要处理异常的Controller直接继承这个类 4 */ 5 public class BaseController { 6 /** 7 * 处理Controller抛出的异常 8 * @param e 异常实例 9 * @return Controller层的返回值 10 */ 11 @ExceptionHandler 12 @ResponseBody 13 public Object expHandler(Exception e){ 14 if(e instanceof SystemException){ 15 SystemException ex= (SystemException) e; 16 return WebResult.buildResult().status(ex.getCode()) 17 .msg(ex.getMessage()); 18 }else{ 19 e.printStackTrace(); 20 return WebResult.buildResult().status(Config.FAIL) 21 .msg("系统错误"); 22 } 23 } 24 }
之后所有需要异常处理的Controller都继承这个类,从而获取到异常处理的方法。
虽然这种方式可以解决问题,但是极其不灵活,因为动用了继承机制就只为获取一个默认的方法,这显然是不好的。
第二种思路,将这个基类变为接口,提供此方法的默认实现(也就是接口中的default方法,java8开始支持接口方法的默认实现)
1 /** 2 * Created by liuruijie. 3 * 接口形式的异常处理 4 */ 5 public interface DataExceptionSolver { 6 @ExceptionHandler 7 @ResponseBody 8 default Object exceptionHandler(Exception e){ 9 try { 10 throw e; 11 } catch (SystemException systemException) { 12 systemException.printStackTrace(); 13 return WebResult.buildResult().status(systemException.getCode()) 14 .msg(systemException.getMessage()); 15 } catch (Exception e1){ 16 e1.printStackTrace(); 17 return WebResult.buildResult().status(Config.FAIL) 18 .msg("系统错误"); 19 } 20 } 21 }
于是可以写一个全局的异常处理类:
1 /** 2 * Created by liuruijie on 2016/12/28. 3 * 全局异常处理,捕获所有Controller中抛出的异常。 4 */ 5 @ControllerAdvice 6 public class GlobalExceptionHandler { 7 //处理自定义的异常 8 @ExceptionHandler(SystemException.class) 9 @ResponseBody 10 public Object customHandler(SystemException e){ 11 e.printStackTrace(); 12 return WebResult.buildResult().status(e.getCode()).msg(e.getMessage()); 13 } 14 //其他未处理的异常 15 @ExceptionHandler(Exception.class) 16 @ResponseBody 17 public Object exceptionHandler(Exception e){ 18 e.printStackTrace(); 19 return WebResult.buildResult().status(Config.FAIL).msg("系统错误"); 20 } 21 }
这个类中只处理了两个异常,但是已经满足了大部分需要,如果还有需要特殊处理的地方,可以再加上处理的方法就行了。第三种实现方式是目前我知道的最优雅的方式了。
如此,我们现在的Controller中的方法就可以很简洁了,比如处理登陆的逻辑就可以这样简单的写:
1 /** 2 * Created by liuruijie on 2016/12/28. 3 * 账号 4 */ 5 @RestController 6 @RequestMapping("passport") 7 public class PassportController { 8 PassportService passportService; 9 @RequestMapping("login") 10 public Object doLogin(HttpSession session, String username, String password){ 11 User user = passportService.doLogin(username, password); 12 session.setAttribute("user", user); 13 return WebResult.buildResult().redirectUrl("/student/index"); 14 } 15 }
而在passprotService的doLogin方法中,可能会抛出用户名或密码错误等异常,然后就会交由GlobalExceptionHandler去处理,直接返回异常信息给前端,然后前端也不需要关心是否返回了异常,因为这些都已经定义好了。
前端js代码只需要这样写:
1 //登陆 2 AJAX.POST("/passport/login", { 3 username:name, 4 password:psw 5 })
一个异常在其中流转的过程为:
比如doLogin方法抛出了自定义异常,其code为:FAIL,message为:用户名或密码错误,由于在controller的方法中没有捕获这个异常,所以会将异常抛给GlobalExceptionHandler,然后GlobalExceptionHandler通过WebResult将状态码和提示信息返回给前端,前端通过默认的处理函数,弹框提示用户“用户名或密码错误”。而对于这样的一次交互,我们根本不用编写异常处理部分的逻辑。
到这里,代码已经简洁了很多,而且重用性大大提高。
HTTP状态码
100 到199 的状态码代表信息,描述对于请求的处理。
200 到 299 的状态码表示客户端发来的请求已经被接收并正确处理。
300 到 399 的状态码表示客户端需要进一步的处理才能完成请求,比如重定向到另一个地址。
400 到 499 的状态码表示客户端的请求有错误,需要修正。404就是这种情况。
500 到 599 的状态码表示服务器在处理客户端请求时发生了内部错误。