在启动应用时会发现在控制台打印的日志中出现了两个路径为 {[/error]} 的访问地址,当系统中发送异常错误时,Spring Boot 会根据请求方式分别跳转到以 JSON 格式或以界面显示的 /error 地址中显示错误信息。
2018-12-18 09:36:24.627 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" ...2018-12-18 09:36:24.632 INFO 19040 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" ...
默认异常处理
使用 AJAX 方式请求时返回的 JSON 格式错误信息。
{ "timestamp": "2018-12-18T01:50:51.196+0000", "status": 404, "error": "Not Found", "message": "No handler found for GET /err404", "path": "/err404"}
使用浏览器请求时返回的错误信息界面。
自定义异常处理
引入依赖
com.alibaba fastjson 1.2.54 org.springframework.boot spring-boot-starter-freemarker
fastjson 是 JSON 序列化依赖, spring-boot-starter-freemarker 是一个模板引擎,用于我们设置错误输出模板。
增加配置
properties
# 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404)spring.mvc.throw-exception-if-no-handler-found=true# 不要为工程中的资源文件建立映射spring.resources.add-mappings=false
yml
spring: # 出现错误时, 直接抛出异常(便于异常统一处理,否则捕获不到404) mvc: throw-exception-if-no-handler-found: true # 不要为工程中的资源文件建立映射 resources: add-mappings: false
新建错误信息实体
/** * 信息实体 */public class ExceptionEntity implements Serializable { private static final long serialVersionUID = 1L; private String message; private int code; private String error; private String path; @JSONField(format = "yyyy-MM-dd hh:mm:ss") private Date timestamp = new Date(); public static long getSerialVersionUID() { return serialVersionUID; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getError() { return error; } public void setError(String error) { this.error = error; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public Date getTimestamp() { return timestamp; } public void setTimestamp(Date timestamp) { this.timestamp = timestamp; }}
新建自定义异常
/** * 自定义异常 */public class BasicException extends RuntimeException { private static final long serialVersionUID = 1L; private int code = 0; public BasicException(int code, String message) { super(message); this.code = code; } public int getCode() { return this.code; }}
/** * 业务异常 */public class BusinessException extends BasicException { private static final long serialVersionUID = 1L; public BusinessException(int code, String message) { super(code, message); }}
BasicException 继承了 RuntimeException ,并在原有的 Message 基础上增加了错误码 code 的内容。而 BusinessException 则是在业务中具体使用的自定义异常类,起到了对不同的异常信息进行分类的作用。
新建 error.ftl 模板文件
位置:/src/main/resources/templates/ 用于显示错误信息
Exception Datas
Code ${(exception.code)!} Time ${(exception.timestamp?datetime)!} Path ${(exception.path)!} Exception ${(exception.error)!} Message ${(exception.message)!}
编写全局异常控制类
/** * 全局异常控制类 */@ControllerAdvicepublic class GlobalExceptionHandler { /** * 404异常处理 */ @ExceptionHandler(value = NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ModelAndView errorHandler(HttpServletRequest request, NoHandlerFoundException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.NOT_FOUND.value(), exception.getMessage()); } /** * 405异常处理 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ModelAndView errorHandler(HttpServletRequest request, HttpRequestMethodNotSupportedException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.METHOD_NOT_ALLOWED.value(), exception.getMessage()); } /** * 415异常处理 */ @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ModelAndView errorHandler(HttpServletRequest request, HttpMediaTypeNotSupportedException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), exception.getMessage()); } /** * 500异常处理 */ @ExceptionHandler(value = Exception.class) public ModelAndView errorHandler (HttpServletRequest request, Exception exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), HttpStatus.INTERNAL_SERVER_ERROR.value(), exception.getMessage()); } /** * 业务异常处理 */ @ExceptionHandler(value = BasicException.class) private ModelAndView errorHandler (HttpServletRequest request, BasicException exception, HttpServletResponse response) { return commonHandler(request, response, exception.getClass().getSimpleName(), exception.getCode(), exception.getMessage()); } /** * 表单验证异常处理 */ @ExceptionHandler(value = BindException.class) @ResponseBody public ExceptionEntity validExceptionHandler(BindException exception, HttpServletRequest request, HttpServletResponse response) { ListfieldErrors = exception.getBindingResult().getFieldErrors(); Map errors = new HashMap<>(); for (FieldError error:fieldErrors) { errors.put(error.getField(), error.getDefaultMessage()); } ExceptionEntity entity = new ExceptionEntity(); entity.setMessage(JSON.toJSONString(errors)); entity.setPath(request.getRequestURI()); entity.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); entity.setError(exception.getClass().getSimpleName()); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); return entity; } /** * 异常处理数据处理 */ private ModelAndView commonHandler (HttpServletRequest request, HttpServletResponse response, String error, int httpCode, String message) { ExceptionEntity entity = new ExceptionEntity(); entity.setPath(request.getRequestURI()); entity.setError(error); entity.setCode(httpCode); entity.setMessage(message); return determineOutput(request, response, entity); } /** * 异常输出处理 */ private ModelAndView determineOutput(HttpServletRequest request, HttpServletResponse response, ExceptionEntity entity) { if (!( request.getHeader("accept").contains("application/json") || (request.getHeader("X-Requested-With") != null && request.getHeader("X-Requested-With").contains("XMLHttpRequest")) )) { ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("exception", entity); return modelAndView; } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); response.setCharacterEncoding("UTF8"); response.setHeader("Content-Type", "application/json"); try { response.getWriter().write(ResultJsonTools.build( ResponseCodeConstant.SYSTEM_ERROR, ResponseMessageConstant.APP_EXCEPTION, JSONObject.parseObject(JSON.toJSONString(entity)) )); } catch (IOException e) { e.printStackTrace(); } return null; } }}
@ControllerAdvice
作用于类上,用于标识该类用于处理全局异常。
@ExceptionHandler
作用于方法上,用于对拦截的异常类型进行处理。value 属性用于指定具体的拦截异常类型,如果有多个 ExceptionHandler 存在,则需要指定不同的 value 类型,由于异常类拥有继承关系,所以 ExceptionHandler 会首先执行在继承树中靠前的异常类型。
BindException
该异常来自于表单验证框架 Hibernate calidation,当字段验证未通过时会抛出此异常。
编写测试 Controller
@RestControllerpublic class TestController { @RequestMapping(value = "err") public void error(){ throw new BusinessException(400, "业务异常错误信息"); } @RequestMapping(value = "err2") public void error2(){ throw new NullPointerException("手动抛出异常信息"); } @RequestMapping(value = "err3") public int error3(){ int a = 10 / 0; return a; }}
使用 AJAX 方式请求时返回的 JSON 格式错误信息。
# /err{ "msg": "应用程序异常", "code": -1, "status_code": 0, "data": { "path": "/err", "code": 400, "error": "BusinessException", "message": "业务异常错误信息", "timestamp": "2018-12-18 11:09:00" }}# /err2{ "msg": "应用程序异常", "code": -1, "status_code": 0, "data": { "path": "/err2", "code": 500, "error": "NullPointerException", "message": "手动抛出异常信息", "timestamp": "2018-12-18 11:15:15" }}# /err3{ "msg": "应用程序异常", "code": -1, "status_code": 0, "data": { "path": "/err3", "code": 500, "error": "ArithmeticException", "message": "/ by zero", "timestamp": "2018-12-18 11:15:46" }}# /err404{ "msg": "应用程序异常", "code": -1, "status_code": 0, "data": { "path": "/err404", "code": 404, "error": "NoHandlerFoundException", "message": "No handler found for GET /err404", "timestamp": "2018-12-18 11:16:11" }}
使用浏览器请求时返回的错误信息界面。
示例代码:
参考资料
《微服务 分布式架构开发实战》 龚鹏 著
https://www.jianshu.com/p/1a49fa436623