说到异常(Exception),很多Java开发人员都会或多或少有些困惑。比如运行时异常(RuntimeException)和检查异常(Checked Exception)是什么,他们有什么区别?哪些异常需要我去捕获处理?捕获之后我该如何处理?错误(Error)又是什么? 只有理解了异常,我们在编写代码的过程中才能正确处理它们,让我们的程序更加健壮。
什么是异常
先看一张图:
异常的分类
从结构上来看,异常是指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内存模型。