异常

异常

1. 简介

在Java中,如果某个方法不能以正常的途径完成任务,可以通过另一个路径退出方法。此时,方法并不返回任何值,而是抛出throw一个封装了错误信息的对象。

2. 异常分类

所有异常都继承自Throwable,并分解为Error(错误)和Exception(异常)。

Throwable:

  • Error(错误):Error分支的类结构主要负责描述运行时系统的内部错误和资源耗尽错误。程序不应该抛出此类错误,出现此错误时除了告诉客户外还应该使程序安全的终止。

    • OutOfMemoryError:当 JVM 内存不足时抛出。
    • StackOverflowError:当程序调用堆栈溢出(如无限递归)时抛出。
    • VirtualMachineError:当 JVM 出现严重问题时抛出。
  • Exception(异常):Exception分为两个分支,RuntimeException受检异常

    • RuntimeException(运行时异常):由程序错误导致的异常
      • NullPointerException:当程序尝试访问空对象的属性或方法时抛出。
      • ArrayIndexOutOfBoundsException:当数组索引越界时抛出。
      • ArithmeticException:当数学运算异常(如除以零)时抛出。
      • ClassCastException:当对象强制转换失败时抛出。
      • IllegalArgumentException:当传递给方法的参数不合法时抛出。
      • 等等…
    • CheckedException(受检异常):程序本身没问题但因为IO等错误导致的异常。受检异常是编译时检查的异常,必须在方法中声明 throws 或用 try-catch 块处理,否则编译器会报错。
      • IOException:表示输入输出操作失败时的异常。
      • SQLException:表示数据库操作失败时的异常。
      • FileNotFoundException:表示文件未找到时的异常。
      • ClassNotFoundException:表示找不到类时的异常。
      • 等等…

Java语言规范将派生于Error和RuntimeException的异常称为非受查异常unchecked,其它异常成为受查异常checked。若子类重写了父类的方法,其声明的受查异常不能比父类声明异常更通用。

3. 应用

可以创建自己的异常类,继承Exception或其子类,然后至少要创建默认构造器和包含错误描述信息的构造器。

1
2
3
4
5
6
7
8
9
public class CustomException extends Exception {

public CustomException() {
}

public CustomException(String message) {
super(message);
}
}

实际项目开发会设计一些常用的异常类,如登陆时UserNotFoundException、PwdNotMatchException、下载时FileNotFoundException等。可以根据实际情况设计项目所需要的各类异常。

1
2
3
4
5
6
7
8
public class ResourceNotEnoughException extends RuntimeException {
public ResourceNotEnoughException() {
}

public ResourceNotEnoughException(String message) {
super(message);
}
}

4. 抛出异常

  1. 找到合适的异常类
  2. 创建异常类的对象
  3. 抛出对象
1
2
3
4
5
6
7
8
9
public int countValue(String s) throws NullPointerException{
return s.length();
}

public void method(){
...
throw new RuntimeException();//各类运行时异常
}

什么时候抛出异常?try-catch,try-with-resource有谈到选择抛出还是处理异常需要根据情况判断,具体哪些情况下抛出异常会好一些呢?

  1. 无法处理异常,打印和输出并不能算是异常处理,统一抛出异常。
  2. 异常机制设计了统一异常处理,按照机制的规定情况抛出异常。
  3. 具体情况具体分析,切忌偷懒滥用。

5. 捕获异常

通过try/catch语句块来捕获异常,具体可参考try-catch,try-with-resource

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
} finally {
// 可选,释放资源的代码,不论是否发生异常都会执行
}

6.简单的设计项目内的异常机制

业务流程不能正确执行通常有以下原因:

  1. 输入必填项验证未通过,或数据根本就没有非空验证
  2. 业务状态验证未通过
  3. 权限验证未通过
  4. 调用外部服务接口时,返回数据不符合预期

软件一些常见的异常还有:

  1. 服务调用异常:超时、中断、状态码非200的调用异常

  2. 第三方异常:数据库、缓存、消息队列等

  3. 代码常规异常:JDK定义的那些Exception

  4. 根据以上和实际项目需求,分类设计各种异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ApiException extends RuntimeException {
private Integer code;
private String errMsg;

public ApiException(CodeEnums codeEnums) {
super(codeEnums.getMsg());
this.code = codeEnums.getCode();
this.errMsg = codeEnums.getMsg();
}

public ApiException(int code,String msg) {
super(msg);
this.code = code;
this.errMsg = msg;
}
}

@Getter
public enum CodeEnums {
SUCCESS(0, "操作成功"),
SYSTEM_ERR(1, "系统异常"),
PARA_ERR(2, "[参数异常]:"),
USER_EXIST(10001, "用户已经注册"),
PWD_CHECK_ERR(10002, "重复密码有误"),
USER_NOT_EXIST(10003, "用户不存在"),
USER_IS_LOCK(10004, "用户状态被锁定"),
USER_PWD_FAIL(10005, "登录密码错误"),
USER_TOKEN_FAIL(10006, "鉴权失败,请重新登录"),
USER_TOKEN_EMPTY(10007, "获取鉴权信息失败"),
USER_ROLE_LIMIT(10008, "权限不足");

private Integer code;
private String msg;
CodeEnums(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
  1. 实现全局异常拦截处理,如Spring中可以通过@ControllerAdvice或继承AbstractHandlerExceptionResolver来实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@ControllerAdvice
public class GlobalExceptionHandler {
private static Logger logger = Logger.getLogger(GlobalExceptionHandler.class);
private static final String DEFAULT_ERROR_VIEW = "/error";

@ExceptionHandler(value = RuntimeException.class)
@ResponseBody
public String handleRuntimeException(RuntimeException ex) {//可以返回JSON数据
logger.error("捕获异常-RuntimeException");
logger.error(ex.getMessage());
ex.printStackTrace();//不要隐藏异常栈信息
JSONObject object = new JSONObject();
object.put("msg", ex.getMessage());
object.put("code", 1);
return object.toString();
}

@ExceptionHandler(value = ApiException.class)
@ResponseBody
public String handleRuntimeException(ApiException ex) {
logger.error("捕获异常-ApiException");
logger.error(ex.getMessage());
ex.printStackTrace();
JSONObject object = new JSONObject();
object.put("msg", ex.getMessage());
object.put("code", 1);
return object.toString();
}

@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public ModelAndView handleIllegalParamException(HttpRequestMethodNotSupportedException ex) {//也可以返回某个页面
logger.error("捕获异常-HttpRequestMethodNotSupportedException");
logger.error(ex.getMessage());
ex.printStackTrace();
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", "错误的访问系统服务,服务接口不存在");
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}

@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ModelAndView handleNullPointerException(MissingServletRequestParameterException ex) {
logger.error("捕获异常-MissingServletRequestParameterException");
logger.error(ex.getMessage());
ex.printStackTrace();
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", "缺少服务所需要的参数");
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}

@ExceptionHandler(value = HttpSessionRequiredException.class)
public ModelAndView handleHttpSessionRequiredException(HttpSessionRequiredException ex) {
logger.error("捕获异常-HttpSessionRequiredException");
logger.error(ex.getMessage());
ex.printStackTrace();
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", "登陆已过时,请重新登陆");
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}

@ExceptionHandler(value = JDBCConnectionException.class)
public ModelAndView handleJDBCConnectionException(JDBCConnectionException ex) {
logger.error("捕获异常-JDBCConnectionException");
logger.error(ex.getMessage());
ex.printStackTrace();
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", "网络连接失常,无法连接数据库");
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}

@ExceptionHandler(value = FileNotFoundException.class)
public ModelAndView handleFileNotFoundException(FileNotFoundException ex) {
logger.error("捕获异常-FileNotFoundException");
logger.error(ex.getMessage());
ex.printStackTrace();
ModelAndView mav = new ModelAndView();
mav.addObject("errorMsg", "找不到指定文件,请联系管理员");
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
  1. 在业务中适当位置抛出异常
1
2
3
4
public void method(){
...
throw new ApiException(CodeEnums.EMAIL_USED);
}

7.异常机制的使用

  1. 不要滥用异常机制

  2. 不要过分细化异常

  3. 利用异常层次结构,不要只抛出RuntimeException,应该找到合适的子类或创建自定义异常。

  4. 处理异常时,在出错的地方抛出异常永远比取虚拟值或默认值要可靠

  5. 一些情况下应该多抛出异常,而不是在函数内捕获异常。

个人在实际开发中感觉没有必要去规范化的check Exception,无需频繁使用try/catch,编写工具类或基层代码时可以严谨一些,并严格按照Java异常体系分层设计。而业务部分则可以考虑设计统一的运行时异常来描述业务问题,并配置全局捕获异常并处理,没必要在正常业务流程中很少发生的异常上面花费过多精力和时间。


参考:

🔗《Java核心技术 卷Ⅰ》