Java精讲:异常处理方法

异常对象均派生于Throwable类,而Throwable又分解为Error和Exception两个分支,其中Error应该在代码层面避免,而不是作异常处理。 设计程序时一般只关注Exception类下的异常,这下面包含两个异常子类,一个是RuntimeException,另一个是IOException。

一般来说,RuntimeException属于编程错误造成的异常,包括数组越界、空指针等等,这类异常与Error类下的异常统称为非检查型异常。

IOException属于非编程错误造成的异常,例如试图超越文件尾部读取数据、打开一个不存在的文件等等。这类异常称为检查型异常,程序员要为检查型异常提供异常处理器。

有时需要抛出不包括在标准异常类中的异常,就需要定义一个派生于Exception(或其子类)的类,用来满足需求。

自定义异常类通常需要有两个构造器,一个无参,一个带详细信息参数。

1)在方法首部声明这个方法可能抛出的异常,用的是throws关键字,这将把异常抛给上一级处理。如果一个方法可能出现的异常不止一种,那么就需要用逗号分隔不同的异常全部列举出来。同时,也不允许声明非检查型的异常,即从Error类或者RuntimeException类继承的异常。

这里需要注意的是,如果超类方法没有声明任何检查型异常,那么子类方法也不能声明。

2)也可以在可能出错的地方主动抛出一个异常对象,用的是throw关键字,如果这里没有对异常进行捕获处理,那么同样需要在方法头部声明该异常。

3)throw throws的区别

throw是抛出一个异常类对象,在代码中去抛出,一般格式为throw new xx()

而throws是抛出异常给上一级去作处理,在方法首部去声明,这里只是抛出异常类名,一般格式为:throws xxx

1)try…catch…finally

把可能出现异常的地方放在try代码块内,在后面接上catch处理对应的异常,一个try可以有多个catch子句(不能存在子类关系)用于捕获不同的异常。如果try中的语句出现异常,那么将跳过剩下的try代码,直接执行catch语句内容,如果带有finally子句,将在catch代码执行完毕后执行,一般用于IO设备的关闭等操作,注意不要把控制流的语句放在里面。

在工具类中一般不适用这种方法,而是将异常throws给调用者去做对应的处理。更加合适的操作是:在catch子句中对异常类型进行转换,将原始异常设置为新异常的原因,然后再throw抛给调用者。

2)try-with-resource

这就出现一个问题,如果在finally语句的关闭操作也出现异常,那还需要进行再一次的异常处理,于是有了try-with-resource这种捕获异常的方法,与python的with xxx as xxx 操作同理。在try代码块运行结束之后,资源会被自动关闭,而对于这种操作也可以有catch和finally子句,这些子句会在资源关闭后执行。

1、断言是jdk1.4后引入的内容,用关键字assert来表示。如果在程序中需要检测一个参数是否合法,一般是使用if语句来操作,但是测试完毕这部分代码依旧会存于程序中,这时候就需要引入assert断言,断言不是程序的一部分,在测试完毕之后移除该代码,程序仍不会收到影响。(注:idea默认断言是关闭的,需要加上 -ea 运行参数启动)

2、断言语法格式为:assert condition : expression(可省略)

如果condition不成立,程序将运行expression然后终止运行并抛出一个AssertionError。如果condition成立,程序正常运行。

3、断言失败是致命的,不可恢复的,不应使用断言与用户进行沟通,应该只用于内部调试测试过程。

1)Log4j 或 Log4j 2 —— Apache的开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件、甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;用户也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,用户能够更加细致地控制日志的生成过程。这些可以通过一个配置文件(XML或Properties文件)来灵活地进行配置,而不需要修改程序代码。Log4j 2则是前任的一个升级,参考了Logback的许多特性;

2)Logback—— Logback是由log4j创始人设计的又一个开源日记组件。logback当前分成三个模块:logback-core,logback- classic和logback-access。logback-core是其它两个模块的基础模块。logback-classic是log4j的一个改良版本。此外logback-classic完整实现SLF4J API使你可以很方便地更换成其它日记系统如log4j或JDK14 Logging;

3)java.util.logging —— JDK内置的日志接口和实现,功能比较简;

4)Slf4j —— SLF4J是为各种Logging API提供一个简单统一的接口),从而使用户能够在部署的时候配置自己希望的Logging API实现;

5)Apache Commons Logging —— Apache Commons Logging (JCL)希望解决的问题和Slf4j类似。

为了避免引入依赖的繁琐性,这里用java内部的日志框架来记录各项操作。

1)基本日志使用:要生成简单的日志可使用全局日志记录器并调用info方法。使用这种方法日志将会直接打印在控制台。

2)高级日志使用:可以自定义日志记录器,而不是全部记录在全局记录器中,而且用静态变量存储日志存记录器的引用可以避免垃圾误回收。

日志级别如下,一般默认情况下只会记录INFO以上的日志,也可以手动调节显示级别。

3)注意点

a、在一个对象中通常只使用一个Logger对象,Logger应该是static final的,只有在少数需要在构造函数中传递logger的情况下才使用private final。

b、输出Exceptions的全部Throwable信息,因为logger.error(msg)和logger.error(msg,e.getMessage())这样的日志输出方法会丢失掉最重要的StackTrace信息。

c、不允许记录日志后又抛出异常,因为这样会多次记录日志,只允许记录一次日志。

d、不允许出现System print(包括System.out.println和System.error.println)语句。

e、不允许出现printStackTrace。

f、日志性能的考虑,如果代码为核心代码,执行频率非常高,则输出日志建议增加判断,尤其是低级别的输出<debug、info、warn>。

作者:玛卡bug卡链接:https://juejin.cn/post/6945775161148719140

三十七、Java异常处理

在Java编程中,异常处理是一项至关重要的技能,让我们能够有效地应对程序运行过程中可能出现的各种错误状况,从而使程序更具健壮性。

Java异常程序运行时出现的问题或错误的表示,代表了程序正常的控制流程被中断的情况。Java将异常分为两大类:checked异常unchecked异常(也称运行时异常)。

Java使用 try catch finally 语句结构来处理异常。

在上述代码中,尝试打开一个文件,如果文件不存在,就会触发 FileNotFoundException ,并在catch块中处理该异常。最后,无论是否发生异常,finally块都会执行,用于关闭文件流以释放系统资源。

通过 throw 关键字,程序员可以主动抛出自定义的异常或系统内置异常。

在方法签名中,使用 throws 关键字声明方法可能会抛出的异常,将异常处理的责任转移给方法的调用者。

try-with-resources 是 Java 7 引入的一个特性,用于自动管理资源,特别是那些实现了 AutoCloseable Closeable 接口的资源,如文件流数据库连接等。使用 try-with-resources 语句可以确保在 try 代码块执行完毕后,资源能够正确、及时地关闭,即使发生异常也是如此。

示例,假设我们有一个实现了 AutoCloseable 接口的资源类:

在上面的例子中,BufferedReaderAutoCloseable 的一个子接口 Closeable 的实现。当 try 代码块执行完毕时,无论是否发生异常,BufferedReader 的 close() 方法都会被自动调用。这样,就无需在 finally 代码块中显式地关闭资源,从而简化了代码,并减少了忘记关闭资源导致资源泄露的风险。

try-with-resources 语句中的资源声明必须是局部变量并且这些资源在 try 代码块执行完毕后必须能够被关闭。如果资源不能被关闭(即 close() 方法抛出异常),那么这个异常会被抑制,并且原始的异常(如果有的话)会被重新抛出。如果需要处理 close() 方法抛出的异常,可以使用额外的 try-catch 块来捕获。

除了Java内置的异常类外,我们还可以创建自定义的异常类。这通常用于表示特定于应用程序的错误条件。要创建自定义异常类,需要继承自Exception类或其子类,并定义构造函数。

例如:

然后,可以在需要的地方抛出这个自定义异常:

异常链是Java异常处理机制中的一个重要特性,允许在抛出新的异常时,将原始异常作为新异常的“原因”传递。这样做有助于保留原始异常的上下文信息,使得在后续处理中能够更准确地了解异常发生的根本原因。

在Java中,可以通过在构造新的异常时,将原始异常作为参数传递给新异常的构造函数,来创建异常链。这样,新异常就会包含原始异常的引用,从而形成一个链式结构。

简单的示例,演示如何使用Java异常链:

在这个示例中,method2抛出了一个IOException。当method1捕获到这个异常时,创建了一个新的MyCustomException,并将原始的IOException作为原因传递给了新异常。这样,当在main方法中捕获到MyCustomException时,我们可以通过调用getCause()方法来获取原始的IOException,从而了解异常发生的根本原因。

异常链的好处在于它保留了原始异常堆栈跟踪信息,使得在调试和排查问题时能够更容易地定位到问题的源头。同时,通过封装原始异常,还可以在自定义异常中添加更多的上下文信息,使得异常信息更加丰富和有用。

在处理异常时,建议总是尽量保留并使用异常链,以便在后续的处理中能够充分利用原始异常的信息。

Java内置了许多异常类,这些类都是Throwable类的直接或间接子类。Throwable类有两个主要的子类:ErrorExceptionError类通常表示严重的问题,这些问题通常是Java虚拟机无法或不应该尝试修复的问题,如OutOfMemoryError或StackOverflowError。而Exception类及其子类则用于表示程序可以处理的异常情况。

一些常见的内置异常类及其描述:

输入输出异常

运行时异常

其他常见异常

Error是程序无法处理的严重错误。例如,OutOfMemoryError 表明虚拟机没有更多的内存空间来分配对象,并且垃圾回收器也无法回收更多的空间。

由于Error类及其子类通常表示无法恢复严重问题,因此当这些错误发生时,应用程序通常无法继续正常运行。在编写Java程序时,虽然不需要显式地处理这些错误,但了解它们以及它们可能的原因仍

终于有人把所有的Java异常处理方法给总结出来了

背景

最近专门负责团队的项目质量。我在治理异常日志过程中,总结了一下Java的异常处理。上面是我整理的最近自己比较常见的异常知识地图。

异常知识地图概述

从异常知识地图最左边的根开始看,地图从左到右的连线连接的类之间有实实在在的父子关系,在java里通过继承来实现(除了非RuntimeException是个虚拟父节点)。

  1. Java所有异常的父类是Throwable,它又分为Error和Exception。
  2. Error是程序判定如果执行了XX逻辑,则应该是至少JVM层面出现了问题。正常情况下不应该发生的。
  3. Exception意思是环境没有什么问题,出现Exception请开发人员自己搞定。
  4. Exception分为RuntimeException运行时异常和非运行时异常。

说到这里,我们从另外一个维度给异常分类。Java异常又分为检查异常和非检查异常。Error和RuntimeException以及RuntimeException的子类是非检查异常。其他是检查异常。这个很好区分。在写Java代码的时候,编译器提示需要try catch或者throws的就是检查异常。其他是非检查异常。后面在具体代码实现里有体现。

  • 异常分为检查异常和非检查异常。

典型异常发生场景

典型异常发生的场景我做了一些demo,上传到了github,地址:https://github.com/xiexiaojing/yuna

为了方面展示使用一个统一的切面来截获异常:

Error

Error及其子类一般不是用来捕获的,而用来抛出的。因为Error的发生意味着环境有问题,该停下来检修了。所以一般的处理是一旦发生Error,会停止JVM。也就是平时看到的程序起不来。如下java.awt.image.Kernel的源码。

Error除了手工抛出,在常用的类库中不用黑科技是不能稳定复现的。所以我测试类是这么写的

直接访问页面的结果

ControllerThrowableAdvice消息:

org.springframework.web.util.NestedServletException:Handler dispatch failed; nested exception is java.lang.Error: 人工抛出一个Error

上面错误消息意思是spring mvc通过其核心逻辑DispatcherServlet没有找到任何一个可以处理这个返回model的,因为直接返回就是一个Error。最后显示的消息通过ControllerThrowableAdvice进行展示。

注意Error是非检查异常,不用显示处理。

NPE

NPE也就是平时说的空指针异常,它非常常见,很多类都没有对null做支持。直到apache提供了common包专门来处理这种情况。防不胜防,时不时项目还是需要为了处理这个异常上线个bugfix。

直接访问页面的结果ControllerThrowableAdvice消息:java.lang.NullPointerException这是因为new HashSet的时候传入null。程序走不到return就抛出异常了。最后显示的消息通过ControllerThrowableAdvice进行展示。

注意NullPointerException是非检查异常,不用显示处理。

算数异常

算数异常非常常见,比如做除0,会抛出异常java.lang.ArithmeticException: / by zero,提示我们该找数学老师帮我们检查作业了。值得注意的是如果使用BigDecimal.divide来做除法,请直接使用divide(BigDecimal divisor, int scale, RoundingMode roundingMode)这个传3个参数的,避免divide(BigDecimal divisor)这个传1个参数的,因为如果传的值除不尽会抛出java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. 带三个参数的方法会在除不尽的时候按照传入的摄入模式和保留小数点后的位数对数据做处理。注意ArithmeticException是非检查异常,不用显示处理。

未声明异常

未声明异常代码量稍大,想知道测试源码的直接去我github里下载。地址:https://github.com/xiexiaojing/yuna

抛出异常的原理是使用动态代理时,如果被代理的类抛出了一个异常。但是却没有throws声明。代理类找不到匹配的异常类型会抛出InvocationTargetException。从知识地图上可以看到它是非检查异常。最后会被UndeclaredThrowableException来处理。这是java动态代理不优雅的处理方式。建议喜欢看源码、模仿源码的朋友对这一点不要借鉴哦。

重点来看一下运行抛出异常的打印堆栈

从打印的堆栈可以看到这个是最终的ArithmeticException抛出时被InvocationTargetException捕获。将原来的参数传给了InvocationTargetException后继续抛出,最后被UndeclaredThrowableException捕获。

注意UndeclaredThrowableException是非检查异常,不用显示处理。

不合法参数异常

不合法参数异常是很多类或者方法自己定义了java基本规则外的一些规则,不满足会抛出的异常。比如java动态代理的源码里就写了被代理的接口不能超过65535个。否则就抛不合法参数异常。

注意IllegalArgumentException是非检查异常,不用显示处理。

以上都说的是非检查异常。下面开始检查异常。由于IO异常很常见好构造,我们直接来看它的子类。

套接字异常

套接字异常在通信编程时非常常见。比如如下代码

启动了一个套接口服务端,马上关闭。关闭后才去调用setReuseAddress。这时候就会抛出java.net.SocketException: Socket is closed。

注意SocketException是检查异常,需要显示处理

绑定异常

套接字异常有一种情况,可以明确的知道是绑定异常,就不用抛出套接字异常这样模糊的异常了。

如上,80端口是http默认端口,不能在自定义通信程序里使用。这时候就会抛出java.net.BindException: Permission denied。

注意BindException是检查异常,需要显示处理。

主机名未知异常

主机名未知异常在比如内网DNS出现问题、或者远程调用时由于机器下线等原因找不到主机时出现。可以人为连接一个未启用的端口来构造。

注意UnknownHostException是检查异常,需要显示处理。

超时异常超时异常因为在分布式系统中涉及程序内部线程间、程序之间的通信多,所以非常常见。具体代码有点长,详见https://github.com/xiexiaojing/yuna

抛出java.util.concurrent.TimeoutException。它是concurrent包里的一个类。注意TimeoutException是检查异常,需要显示处理。

反射操作异常及其子类

反射操作异常一般只在启动时看到,线上程序运行中一般不会发生。因为常见类里它是这么处理的

上面可以看到在java.net.InetAddress的源码里,ReflectiveOperationException的处理是直接抛出Error。在程序启动时,经常会由于maven pom里引入的包冲突、版本不合适、或者是缺少包引起「类找不到异常」。例如下面的测试例子:

由于com.XXX不存在。会直接抛出java.lang.ClassNotFoundException: com.XXX。它是反射操作异常的子类。平时反射操作异常及它的子类异常一旦发生就会抛出Error,JVM停止。如下面的源码:

Spring对于异常的处理

默认异常处理

Spring的MVC在默认情况下对不能处理的异常如404、500会抛出白页。像下面这样:

这是因为Spring MVC的核心处理类DispatcherServlet的doDispatch方法包含代码片段:

它最终处理是返回/error页。也就是白页。不知道大家有没有注意到我前面在介绍Error的时候,定义了error页面的url地址为errorThrowable。

这是因为error是被Spring自身占用了。如果定义为error,我们将看不到预期的结果,而是下面的白页

这里因为/error是默认页面,返回了999的http错误码,意思是请求被拒绝。

自定义异常处理在典型异常的发生场景里一开始就介绍了定义了一个统一错误处理如下:

这是使用了spring aop做了统一拦截。Advice在AOP的概念中翻译成增强。包括Before、After、Around等增强时机。这里类名用到了Advice意思是在controller发生Throwable时做的增强。看到有的项目喜欢用

这个也OK。但是我会假设Everything fails! 程序在发生平时不会遇到的问题时也可控。

总结

本文先围绕着异常知识地图介绍了各种异常及出现场景,最后结合Spring论述了在实际工作中如何统一处理异常。这里推荐一个学习方法:梳理知识地图,给地图框架填充内容,让自己的知识体系化。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。