封面图片

编程

编程指南:认识Java异常

说到异常(Exception),很多Java开发人员都会或多或少有些困惑。比如运行时异常(RuntimeException)和检查异常(Checked Exception)是什么,他们有什么区别?哪些异常需要我去捕获处理?捕获之后我该如何处理?错误(Error)又是什么? 只有理解了异常,我们在编写代码的过程中才能正确处理它们,让我们的程序更加健壮


什么是异常

先看一张图: Java异常类层次结构图

Java异常类层次结构

异常的分类

从结构上来看,异常是指Exception类及其子类,其中RuntimeException类和其子类就是指运行时异常。没有非运行时异常这个类,像IOException就是直接继承自Exception。 看一下源码:

1public class Exception extends Throwable { 2 static final long serialVersionUID = -3387516993124229948L; 3 ... 4} 5 6public class RuntimeException extends Exception { 7 static final long serialVersionUID = -7034897190745766939L; 8 ... 9} 10 11public class UnsupportedOperationException extends RuntimeException { 12 /** 13 * Constructs an UnsupportedOperationException with no detail message. 14 */ 15 public UnsupportedOperationException() { 16 } 17 ... 18} 19 20public class IllegalArgumentException extends RuntimeException { 21 /** 22 * Constructs an <code>IllegalArgumentException</code> with no 23 * detail message. 24 */ 25 public IllegalArgumentException() { 26 super(); 27 } 28 ... 29} 30 31public class NumberFormatException extends IllegalArgumentException { 32 static final long serialVersionUID = -2848938806368998894L; 33 ... 34} 35 36public class ReflectiveOperationException extends Exception { 37 static final long serialVersionUID = 123456789L; 38 ... 39} 40public class NoSuchMethodException extends ReflectiveOperationException { 41 private static final long serialVersionUID = 5034388446362600923L; 42}

那么,为什么Java的设计者要这么设计异常,为什么不统一继承自Exception,中间还要搞出来一个运行时异常? 异常的设计分为2种情况让开发者去考虑。

第一种情况:错误来自外部

当开发者无法控制输入时,如果出错,该怎么处理?这是无法在编写代码阶段去避免的问题。这种情况下就需要开发者通过捕获异常,然后写异常处理逻辑。这个时候IDE会提示你去处理异常(编写额外的代码去处理异常)。 当程序中出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。 那为什么不是所有异常都设计成需要人工处理呢?

第二种情况:错误来自内部

因为Java语言的设计者认为,有些异常的产生是开发者自身编写代码导致的,需要在开发阶段就避免产生这类错误。这类错误就是RuntimeException设计出来的原因。 设计者通过这种设计告诉我们,当程序运行过程中出现运行时异常时,赶紧去修复代码。所以在你使用了抛出运行时异常的方法时,IDE不会报警让你去捕获并处理异常,因为系统想让这种异常赶紧抛出来给你,让你去修改完善代码(修复原有代码Bug)。 比如我是用来了org.springframework.jdbc.core下的JdbcTemplate的queryForMap方法,IDE没有报红,我也就没有必要去做异常处理。

1jdbcTemplate.queryForMap(sql);

我们点进queryForMap的方法里面去看是这样子的:

1public Map<String, Object> queryForMap(String sql) throws DataAccessException { 2 return (Map)result(this.queryForObject(sql, this.getColumnMapRowMapper())); 3 }

它抛出了DataAccessException。那么这个DataAccessException是什么呢?再看:

1public abstract class DataAccessException extends NestedRuntimeException { 2 public DataAccessException(String msg) { 3 super(msg); 4 } 5 6 public DataAccessException(@Nullable String msg, @Nullable Throwable cause) { 7 super(msg, cause); 8 } 9} 10 11public abstract class NestedRuntimeException extends RuntimeException { 12 private static final long serialVersionUID = 5439915454935047936L; 13 ... 14}

那么这个QueryForMap是抛出了一个运行时异常,而我们在调用这个方法时,没有做异常处理。 这里就是设计者告诉你的,如果这里抛出来异常,赶紧去检查一下是不是你的SQL写错了。 那如果我在使用了继承运行时异常的类的方法中使用try...catch去处理了,那会怎么样? 那就是掩耳盗铃

认识老朋友,常见的运行时异常

这些异常,最好一次都不要见到。 这些异常通过在你刚刚开发完功能进行调试的时候会出现。如果应用部署到线上还出现这些异常,说明代码写的不够好,测试不够全面。 保证部署上线的项目不要出现运行时异常!!! 以下是常见的运行时异常:

  • NullPointerException - 空指针引用异常
  • IllegalArgumentException - 传递非法参数异常
  • IndexOutOfBundsException - 下标越界异常
  • ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
  • NumberFormatException - 数据类型转换异常
  • ClassCastException - 类型强制转换异常
  • ArithmeticException - 算术运算异常
  • SecurityException - 安全异常
  • UnsupportedOperationException - 不支持的操作异常
  • NegativeArraySizeException - 创建一个大小为负数的数组错误异常
  • DataAccessException - 数据访问异常(数据库)继承自NestedRuntimeException(嵌套异常),Spring DAO(Data Access Object)框架提供

其他的不是继承自RuntimeException**的异常就是需要我们编写额外代码去处理的异常了。**比如跟DataAccessException很相似的SQLException,SQLException是一个检查性异常,在使用JDK提供的JDBC API编写代码时,就需要手动去处理很多检查异常。因为对代码的侵入性太大,Spring定义了DataAccessException这个运行时异常,让开发者可以不必去处理持久化异常,同时也屏蔽了底层不同技术抛出的不同异常(HibernateException等) 使用JDK的JDBC API需要手动去处理检查异常。

1@Override 2public Connection getConnection(DataSourceDTO dataSourceDTO) { 3 try { 4 Class.forName("ru.yandex.clickhouse.ClickHouseDriver"); 5 String defaultDataBaseName = "default"; 6 String url = "jdbc:clickhouse://" + dataSourceDTO.getIp() + ":" + dataSourceDTO.getPort() + "/" + defaultDataBaseName; 7 return DriverManager.getConnection(url, dataSourceDTO.getUserName(), dataSourceDTO.getPassword()); 8 } catch (SQLException | ClassNotFoundException e) { 9 throw new CustomException(ResultJson.failure(ResultCode.JDBC_FAIL, e.getMessage())); 10 } 11} 12 13public void close(Connection connection) { 14 try { 15 connection.close(); 16 } catch (SQLException e) { 17 throw new CustomException(ResultJson.failure(ResultCode.JDBC_EXEC_FAIL)); 18 } 19}

对于检查异常,就需要我们编写代码去处理了。通常的做法就是try...catch或者throw。如果不做处理,则代码无法编译通过。

该如何处理异常

运行时异常

在开发阶段、测试阶段尽量暴露运行时异常,然后修复它。线上环境争取不要出现运行时异常。

检查型异常

需要我们额外处理的异常是检查型异常,如果不处理,则编译不会通过。 最具代表的检查性异常是用户错误或问题引起的异常,这是程序员无法预见的。例如要打开一个不存在文件时,一个异常就发生了。 异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

对于异常的处理,主要是try...catch来处理。 通过try包裹会抛出异常的代码块,在catch中处理异常,主要是通过日志记录、打印异常信息或者抛出异常几种方式。

建议

这是些处理异常的建议。

  • 不要忘记finally。有些资源需要通过finally进行关闭,比如打开的文件输入流。
  • 使用更明确的异常。在该抛出NumberFormatException时,用NumberFormatException比使用IllegalArgumentException或者Exception的含义更明确。
  • 方法注释中,对抛出的异常进行说明
1/** 2 * 这个方法内部做了什么什么事情... 3 * 4 * @param input 5 * @throws BusinessException 如果出现xxx情况,则会抛出这个异常 6 */ 7public void doSomething(String input) throws BusinessException { 8 ... 9}
  • 保留足够的异常信息。主要是用来排查问题。
1public static void testLong(){ 2 try { 3 Long abc = new Long("ABC"); 4 } catch (NumberFormatException e) { 5 log.error("格式异常", e); 6 } 7}

简介的说明和异常信息e

  • 不要捕获了一场,然后什么也不做。
1public void doNotIgnoreExceptions(){ 2 try { 3 // 一些业务代码 4 } catch (NumberFormatException e) { 5 // 什么也没做 6 } 7}
  • 不要在不需要捕获异常的地方捕获异常。
  • 打印了异常日志后,不要再将异常抛出。
1public void testCatchEx(){ 2 try { 3 new Long("heihei"); 4 } catch (NumberFormatException e) { 5 log.error("数字格式异常", e); 6 throw e; 7 } 8}

这样做,相当于需要处理2次异常,同时会打印多条相同的错误信息。

  • 在包装异常时,使用原始异常
1public void wrapException(String input) throws BusinessException { 2 try { 3 // do something 4 } catch (NumberFormatException e) { 5 // 将e作为构造参数中的cause 6 throw new MyCustomException("一段对异常的 stack trace描述信息.", e); 7 } 8}

Exception和Throwable

介绍

Exception类是Throwable的子类。Exception中有个构造器,其中一个参数类型就是Throwable,它调用了父类也就是Throwable的构造器。也就是说异常在构造的时候是可以嵌套的。 Spring 在 org.springframework.dao 中提供了一套优雅的 DAO 异常体系, 这些异常都继承自 DataAccessException, DataAccessException 继承自NestedRuntimeException, NestedRuntimeException 异常以嵌套的方式封装了源异常 。 因此,虽然不同的持久化技术的特定异常被转换到 Spring 的 DAO 异常体系中,但我们可以通过 getCause() 方法获取原始异常信息 。

1public class Exception extends Throwable { 2 static final long serialVersionUID = -3387516993124229948L; 3 4 ... 5 public Exception(String message, Throwable cause) { 6 super(message, cause); 7 } 8}
1public class Throwable implements Serializable { 2 private String detailMessage; 3 ... 4 //fillInstackTrace()用来初始化堆栈追踪数据。 5 public Throwable(String message, Throwable cause) { 6 fillInStackTrace(); 7 detailMessage = message; 8 this.cause = cause; 9 } 10 11 public String getMessage() { 12 return detailMessage; 13 } 14 15 // 该方法可以重写,返回自定义信息 16 public String getLocalizedMessage() { 17 return getMessage(); 18 } 19}

堆栈追踪数据格式:

1//该数据格式通过 Throwable.getStackTrace()获得 2public final class StackTraceElement implements java.io.Serializable { 3 // Normally initialized by VM (public constructor added in 1.5) 4 private String declaringClass; 5 private String methodName; 6 private String fileName; 7 private int lineNumber; 8} 9

getMessage()getLocalizeMessage()正常情况下返回的数据是一致的。除非你重写了getLocalizedMessage()。 一个异常栈追踪打印信息:

12023-11-20 14:58:20.509 ERROR [task-1] org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler Line:39 - Unexpected exception occurred invoking async method: public void cn.les.datagovernance.service.impl.BloodAnalyseImpl.analyse(java.lang.String,java.lang.Long) 2java.lang.NullPointerException: null 3 at cn.les.datagovernance.service.impl.BloodAnalyseImpl.analyse(BloodAnalyseImpl.java:34) 4 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 5 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 6 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 7 at java.lang.reflect.Method.invoke(Method.java:498) 8 at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) 9 at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) 10 at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) 11 at org.springframework.aop.interceptor.AsyncExecutionInterceptor.lambda$invoke$0(AsyncExecutionInterceptor.java:115) 12 at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266) 13 at java.util.concurrent.FutureTask.run(FutureTask.java) 14 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) 15 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) 16 at java.lang.Thread.run(Thread.java:750)

以上打印信息来自如下方法:

1public class Throwable implements Serializable { 2 ... 3 public void printStackTrace(PrintStream s) { 4 printStackTrace(new WrappedPrintStream(s)); 5 } 6 7 private void printStackTrace(PrintStreamOrWriter s) { 8 // Guard against malicious overrides of Throwable.equals by 9 // using a Set with identity equality semantics. 10 Set<Throwable> dejaVu = 11 Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>()); 12 dejaVu.add(this); 13 14 synchronized (s.lock()) { 15 // Print our stack trace 16 s.println(this); 17 StackTraceElement[] trace = getOurStackTrace(); 18 for (StackTraceElement traceElement : trace) 19 s.println("\tat " + traceElement); 20 21 // Print suppressed exceptions, if any 22 for (Throwable se : getSuppressed()) 23 se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu); 24 25 // Print cause, if any 26 Throwable ourCause = getCause(); 27 if (ourCause != null) 28 ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu); 29 } 30 } 31}

打印的内容以at开头,具体内容来自StackTraceElement重写的toString()

1public String toString() { 2 return getClassName() + "." + methodName + 3 (isNativeMethod() ? "(Native Method)" : 4 (fileName != null && lineNumber >= 0 ? 5 "(" + fileName + ":" + lineNumber + ")" : 6 (fileName != null ? "("+fileName+")" : "(Unknown Source)"))); 7}

一个问题

为什么打印的元素叫StackTraceElement栈追踪元素? 接触过JVM的都知道,JVM有个Runtime Data Area(运行时数据区),其中有JVM Stacks(Java虚拟机栈)和Native Method Stacks(本地方法栈)。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。 而栈中存放的就是调用的一个个方法信息。所以打印出来的错误信息就是来自栈中的存放的方法数据。 感兴趣的可以去学习一下JVM内存模型。

2023年12月12日
在初学者眼中,世界充满了可能;专家眼中,世界大都已经既定。--铃木俊隆