- 如果要统一处理项目的异常,就需要分析产生异常的类别,针对相同逻辑的异常进一步处理,项目的异常可以大概分为以下类别:
类别 |
异常类 |
说明 |
处理方案 |
业务异常 |
BusinessException |
规范的用户行为产生的异常 不规范的用户行为操作产生的异常 |
发送对应消息传递给用户,提醒规范操作 |
系统异常 |
SystemException |
项目运行过程中可预计且无法避免的异常 |
记录日志 发送固定消息传递给用户,安抚用户 发送特定消息给运维人员,提醒维护 |
其他异常 |
Exception |
编程人员未预期到的异常 |
记录日志 发送固定消息传递给用户,安抚用户 发送特定消息给运维人员,提醒维护 (纳入预期范围内) |
- 为了清晰的定义异常,首先需要自定义相关的异常类,自定义异常类建议继承 RuntimeException。因为 RuntimeException 是一种非检查型异常,所以在程序中抛出这种异常时不需要显式地声明或捕获它们。
- 这意味着,如果自定义的异常类继承了 RuntimeException,那么在程序中抛出该异常时就不需要使用 try-catch 块来捕获它,而可以直接将其抛出进行异常处理,这样可以使代码更加简洁、易读。
示例
- SystemException.java
public class SystemException extends RuntimeException{ private ResultCode resultCode;
public SystemException(ResultCode resultCode) { super(resultCode.getMsg()); this.resultCode = resultCode; }
public SystemException(String message, ResultCode resultCode) { super(message); this.resultCode = resultCode; }
public SystemException(String message, Throwable cause, ResultCode resultCode) { super(message, cause); this.resultCode = resultCode; }
public ResultCode getResultCode() { return resultCode; }
public void setResultCode(ResultCode resultCode) { this.resultCode = resultCode; } } |
- BusinessException.java
public class BusinessException extends RuntimeException { private ResultCode resultCode;
public BusinessException(ResultCode resultCode) { super(resultCode.getMsg()); this.resultCode = resultCode; }
public BusinessException(String message, ResultCode resultCode) { super(message); this.resultCode = resultCode; }
public BusinessException(String message, Throwable cause, ResultCode resultCode) { super(message, cause); this.resultCode = resultCode; }
public ResultCode getResultCode() { return resultCode; }
public void setResultCode(ResultCode resultCode) { this.resultCode = resultCode; } } |
- 同时需要将具体异常对应的错误码添加到枚举类,具体业务的异常可以自定义,以下业务异常和系统异常的错误码示例
示例
public enum Code {
//其他错误码 … /** 业务错误 */ BUSINESS_ERROR(40001,"业务错误"),
/** 业务超时错误 */ BUSINESS_TIMEOUT_ERROR(40002,"业务超时错误"),
/** 业务未知错误 */ BUSINESS_UNKNOW_ERROR(40003,"业务未知错误"),
/** 系统错误 */ SYSTEM_ERROR(50001,"系统错误"),
/** 系统超时错误 */ SYSTEM_TIMEOUT_ERROR(50002,"系统超时错误"),
/** 系统未知错误 */ SYSTEM_UNKNOW_ERROR(50004,"系统未知错误");
//get方法和构造方法 } |
- 在 Spring 中,统一处理异常有 2 种方式,使用 @ExceptionHandler、@ControllerAdvice(@RestControlleradvice) 注解或自定义实现 HandlerExceptionResolver 接口
1. 注解方式
@ExceptionHandler + @ControllerAdvice(@RestControllerAdvice)
2. 接口方式
实现 HandlerExceptionResolver 接口
注解方式
- 定义好特定的错误处理类后,只需要在 @RestControllerAdvice 注解的类下集中处理错误,这时我们可以根据错误添加一些错误处理方案,并对错误内容进行数据的封装,把错误返回给前端
示例
@RestControllerAdvice public class GlobalExceptionAdvice {
@ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Result<String> doBusinessException(BusinessException ex) { return Result.fail(ex.getResultCode(), ex.getMessage(), null); }
@ExceptionHandler(SystemException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<String> doSystemException(SystemException ex) { //记录日志 //发送特定消息给运维人员,提醒维护 return Result.fail(ex.getResultCode(), ex.getMessage(), null); }
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result<String> doOtherException(Exception ex) { //记录日志 //发送特定消息给运维人员,提醒维护 return Result.fail(ResultCode.SYSTEM_UNKNOW_ERROR, ex.getMessage(), null); } } |
- 当发生错误时,只需要在 Controller 层中抛出异常到 GlobalExceptionAdvice 就完成了异常处理,以下以模拟实际错误为例
示例
@RestController @RequestMapping("/books") public class BookController { @Resource private BookService bookService;
@PostMapping public Result<> save(@RequestBody Book book) { //模拟业务异常,包装成自定义异常 if(book.name == "Spring实战 第5版"){ throw new BusinessException("数据已存在,请勿重复提交", ResultCode.BUSINESS_ERROR); } //模拟系统异常,将可能出现的异常进行包装,转换成自定义异常 try{ int i = 1/0; }catch (Exception e){ throw new SystemException("服务器访问超时,请重试", ResultCodeCode.SYSTEM_TIMEOUT_ERROR); } boolean flag = bookService.save(book); if (!flag){ return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, ResultCode.INTERNAL_SERVER_ERROR.getMsg(), flag); } return Result.success(flag); } } |
接口数据返回如下:
接口方式
- 在 SpringMVC 中提供的 HandlerExceptionResolver 接口可以实现全局异常处理器,通过实现此接口可以对异常进行处理
示例
@Component public class GlobalExceptionHandler implements HandlerExceptionResolver {
@Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setCharacterEncoding("UTF-8"); response.setHeader("Cache-Control", "no-cache, must-revalidate");
Result<String> result;
if (ex instanceof BusinessException) { response.setStatus(HttpStatus.BAD_REQUEST.value()); result = new Result<>(ResultCode.BUSINESS_ERROR,ex.getMessage(),null); } else if (ex instanceof SystemException) { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); result = new Result<>(ResultCode.SYSTEM_ERROR,ex.getMessage(),null); } else { response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); result = new Result<>(ResultCode.INTERNAL_SERVER_ERROR,"未知错误",null); }
try { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.writeValue(response.getWriter(), result);
return new ModelAndView(); } catch (IOException e) { e.printStackTrace(); } return null; } } |
- 当发生错误时,只需要在 Controller 层中抛出异常就完成了异常处理,以下以模拟实际错误为例
示例
@RestController @RequestMapping("/books") public class BookController { @Resource private BookService bookService;
@PostMapping public Result<> save(@RequestBody Book book) { //模拟业务异常,包装成自定义异常 if(book.name == "Spring实战 第5版"){ throw new BusinessException("数据已存在,请勿重复提交", ResultCode.BUSINESS_ERROR); } //模拟系统异常,将可能出现的异常进行包装,转换成自定义异常 try{ int i = 1/0; }catch (Exception e){ throw new SystemException("服务器访问超时,请重试", ResultCodeCode.SYSTEM_TIMEOUT_ERROR); } boolean flag = bookService.save(book); if (!flag){ return Result.fail(ResultCode.INTERNAL_SERVER_ERROR, ResultCode.INTERNAL_SERVER_ERROR.getMsg(), flag); } return Result.success(flag); } } |
接口数据返回如下:
注
实现 HandlerExceptionResolver 接口处理全局异常的方式并不被推荐,很明显这种处理方式不够灵活,异常全部写在一个方法中,同时需要对所有异常进行判断,难以对代码进行维护和扩展,代码耦合度高