Java 异常设计最佳实践

简介: 关于异常在讲Java异常实践之前,先理解一下什么是异常。到底什么才算是异常呢?其实异常可以看做在我们编程过程中遇到的一些意外情况,当出现这些意外情况时我们无法继续进程正常的逻辑处理,此时我们就可以抛出一个异常。广义的讲,抛出异常分三种不同的情况:编程错误导致的异常 :在这个类别里,异常的出现是由于代码的错误(譬如NullPointerException、Ille

关于异常

在讲Java异常实践之前,先理解一下什么是异常。到底什么才算是异常呢?其实异常可以看做在我们编程过程中遇到的一些意外情况,当出现这些意外情况时我们无法继续进程正常的逻辑处理,此时我们就可以抛出一个异常。

广义的讲,抛出异常分三种不同的情况:

  • 编程错误导致的异常 :在这个类别里,异常的出现是由于代码的错误(譬如NullPointerException、IllegalArgumentException、IndexOutOfBoundsException )。代码通常对编程错误没有什么对策,所以它一般是非检查异常。

  • 客户端的错误导致的异常 :客户端代码试图违背制定的规则,调用API不支持的资源。如果在异常中显示有效信息的话,客户端可以采取其他的补救方法。例如:解析一个格式不正确的XML文档时会抛出异常,异常中含有有效的信息。客户端可以利用这个有效信息来采取恢复的步骤。

  • 资源错误导致的异常 :当获取资源错误时引发的异常。例如,系统内存不足,或者网络连接失败。客户端对于资源错误的反应是视情况而定的。客户端可能一段时间之后重试或者仅仅记录失败然后将程序挂起。

Java 异常基本概念

Java为异常设计了一套异常处理机制,当程序运行过程中发生一些异常情况时,程序不会返回任何值,而是抛出封装了错误信息的异常对象,Java 语言提供了专门的异常处理机制去处理这些异常。

那么为什么编程语言要设计异常呢?首先,引入异常之后,我们就可以把错误代码从正常代码中分离出来进行单独处理,这样使代码变得更加整洁;其次,当出现一些特殊情况时,我们还可以抛出一个检查异常,告知调用者让其处理。

Java 异常体系结构

这里写图片描述
从上面异常继承树可以看出,所以异常都继承自Throwable,这也意味着所有异常都是可以抛出的。

具体来说,广义的异常可以分为ErrorException两大类。

Error表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,Java虚拟机运行错误Virtual MachineError,当 JVM 不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError,虚拟机错误还有StackOverflowError 、InternalError、 UnknownError等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。经常见到的Error还有LinkageError(结合错误),具体有 NoSuchMethodError 、IllegalAccessError 、NoClassDefFoundError

所以,对于Error我们编程中基本是用不到的,也就是说我们在编程中可以忽略Error错误。所以我们通常所说的异常只的是Exception,而Exception可分为检查异常和非检查异常。

检查异常与非检查异常

通常我们所说的异常指的都是Exception的子类,它们具体可以分为两大类在Java,Exception的子类和RuntimeException的子类,它们分别对应着检查异常和非检查异常。

Checked exception

检查异常,继承自Exception类。对于检查异常,Java强制我们必须进行处理。对于抛出检查异常的API我们有两种处理方式:

  • 对抛出检查异常的API进程try catch
  • 继续把检查异常往上抛

常见的检查异常有:

 SQLException
 IOException
 InterruptedException

ps:其实Error也是一个检查型的。

Unchecked exception

非检查异常,也称运行时异常RuntimeException ,继承自RuntimeException,所有非检查都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作unchecked exception。RuntimeException 本身也是继承自Exception

常见的非检查异常有:

NullPointerException
IllegalArgumentException
NumberFormatException
IndexOutOfBoundsException
IllegalStateException

检查异常 or 非检查异常?

其实对于检查异常存在的必要性一直都很有争议,Java是第一个使用检查异常的主流面向对象语言,而C++和C#都是没有检查异常的。所以我们在编程中全部使用非检查异常(RuntimeException的子类)也是可以的。

但是Java为什么又设计了检查异常呢,其实个人觉得检查异常的存在还是有必要的。检查异常可以使API的调用者明确知道API可能抛出的异常信息并让他们能够对这种异常情况做处理。或者是说,检查异常允许调用者从异常中恢复,而非检查异常一般是编程错误,调用者无法对其进行处理。 但是使用检查异常需要谨慎。因为检查异常会强制调用者对其进行try catch或者往上层抛,这样就给调用者造成了不必要的负担。

那么到底何时适合使用检查异常呢?有一个简单的原则是:

如果你希望调用者有意识地采取措施,那么抛出检查型异常;如果你自己也搞不清楚到底该不该让调用者采取措施,那么久干脆抛出非检查异常。

尽量少使用检查异常

上面提到,对于检查异常,强制要求开发者必须进行处理,也就是开发者要么对其进行try catch,要么往上层抛。如果检查异常很多,就意味着程序中需要添加很多的异常处理代码,导致晦涩的异常处理,并且检查异常容易破坏接口方法。为了解决检查异常带来的缺陷,我们可以利用异常转译的方法,将检查异常转化为非检查异常。

异常转译

异常转译就是将一种异常转换为另一种异常。异常转译针对所有继承 Throwable 超类的异常类而言的。对于我们开发者来说,如果遇到检查异常,而我们又不知道该对其做出如何处理,那么我们完全可以在catch块里将其封装成一个非检查异常然后抛出。例如下面这个例子:

public List getAllAccounts() throws SQLException{
  ...
}

getAllAccounts()抛出了两个checked exception。这个方法的调用者就必须处理这两个异常,尽管它也不知道在getAllAccounts()中什么文件找不到以及什么数据库语句失败,也不知道该提供什么文件系统或者数据库的事务层逻辑。这样,异常处理就在方法调用者和方法之间形成了一个不恰当的紧耦合。但是如果我们真正遇到这种情况,我们完全可以这么做:

try{
    getAllAccounts(SQLException  sqle)
}catch( ){
  throw new RuntimeException("获取失败!",sqle);//正确的做法
  throw new RuntimeException("获取失败!");//错误的做法
}

需要注意的是,我们在对异常进行转译的时候一定要在构造方法中传入原异常的throwable对象,这样可以保留原异常栈信息,而不是把原异常用另一个异常完全替换掉。

当然,我们也可以根据实际情况将一个非检查异常包装成一个检查异常。

异常的架构设计

从系统不同角度看异常

从系统最终用户的角度来看,系统对于用户来说就是一个黑盒,用户并不知道系统如何实现及运行,对用户而言,系统所出现的任何异常或错误,都属于系统运行时异常。所以在设计面向最终用户服务的API时,应该捕获API所有可能出现的异常,并把异常情况封装成与用户业务相近的提示信息,用户可以根据这些提示信息作出一些处理。

而对于系统开发者而言,更多的是从系统内部逻辑来看异常。有一部分异常需要内部截获处理即try catch,而另外一部分异常对于异常产生源而言无法进行有效处理,从而需要向外抛出异常以待合适的调用者进行处理。对于开发者而言,需要预见异常,并且需要考虑何时处理异常,何时抛出异常,必要时以某种方式记录或通知异常。总而言之,开发者需要通过对系统运行时可能出现的异常尽可能地处理以保证系统的正常运行,并对于无法处理的异常以一种合适的方式记录、通知、呈现以便找到发生异常的原因,从而解决或避免异常。

设计一个统一的异常处理类

一般当程序发生异常时,通常异常处理可能需要做一些通用处理,如异常日志记录、异常通知,重定向到一个统一的错误页面(如 Web 应用)等。如果这些通用异常处理放置于 catch 块中,将导致大量的重复代码,从而可能引起日志冗余、同一异常的实现多样化等问题。另外,大量异常处理程序放置于 catch 块中造成程序的高耦合性。为了解决此类问题,有必要分离出异常处理程序、统一异常处理风格、降低耦合性、增强异常处理模块的复用程度。通常的异常处理模式包括业务委托模式(Business Delegate)、前端控制器模式(Front Controller)、拦截过滤器模式(Intercepting Filter)、AOP 模式、模板方法模式等。

在web编程中,一般对控制层的异常都应该做统一处理,因为控制层向上面对用户,所以我们要在控制层捕获service所有可能出现的异常。上面也提到我们在控制层对每个service调用进行try catch显然会很繁琐而且也会导致大量重复代码,所以在遇到这种情况时我们一定要考虑引入统一异常处理机制,而很多框架也提供了这样的处理机制,如Spring的AOP,SpringMVC的 ExceptionHandler、RESTEasy的ExceptionMapper。

对于Spring MVC框架统一异常处理机制请参考:Spring MVC 中的异常处理 (handling exceptions)
对于Restful框架的统一异常处理机制请参考: RESTEasy中的通用异常处理ExceptionMapper

异常层次定义

异常层次结构应该以一种普遍通用的原则定义。为此,我们可以利用面向对象语言具备多态的性质,隐藏异常的实际实现。对于异常 service 而言,只需要捕获最基本的应用程序异常 AppException,异常处理过滤器会自动过滤实际异常类型并找到相应的异常处理器。另外,在方法的 throws 语句中勿需放入大量的检查异常;对方法调用者也不会出现混乱的 catch 块,最多可能只存在一个用于处理基本应用程序异常 AppException(委托给异常 service 处理)。

前面的章节过,应用系统异常可以从用户和开发者两个视角去考虑。因此,我们可以把异常划分为业务操作异常和系统内部运行时异常两种类型。抛出业务级异常或系统运行时异常的决策,需要与应用系统本身的架构层次相结合,考虑所要处理异常的层次。如图所示为一个典型的异常层次结构:
这里写图片描述
其中,BussinessException 属于基本业务操作异常,所有业务操作异常都继承于该类。例如,通常 UI 层或 Web 层是由系统最终用户执行业务操作驱动,因此最好抛出业务类异常。ServiceException 一般属于中间服务层异常,该层操作引起的异常一般包装成基本 ServiceException。DAOException 属于数据访问相关的基本异常。

对于多层系统,每一层都有该层的基本异常。在层与层之间的信息传递与方法调用时候,一旦在某层发生异常,传递到上一层的时候,一般包装成该层异常,直至与用户最接近的 UI 层,从而转化成用户友好的错误信息。

目录
相关文章
|
6天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
6天前
|
Java
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
|
20天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
39 5
|
6天前
|
IDE 前端开发 Java
怎样避免 Java 中的 NoSuchFieldError 异常
在Java中避免NoSuchFieldError异常的关键在于确保类路径下没有不同版本的类文件冲突,避免反射时使用不存在的字段,以及确保所有依赖库版本兼容。编译和运行时使用的类版本应保持一致。
|
8天前
|
Java 编译器
如何避免在 Java 中出现 NoSuchElementException 异常
在Java中,`NoSuchElementException`通常发生在使用迭代器、枚举或流等遍历集合时,尝试访问不存在的元素。为了避免该异常,可以在访问前检查是否有下一个元素(如使用`hasNext()`方法),或者使用`Optional`类处理可能为空的情况。正确管理集合边界和条件判断是关键。
|
8天前
|
Java 数据库连接 开发者
Java中的异常处理机制及其最佳实践####
在本文中,我们将探讨Java编程语言中的异常处理机制。通过深入分析try-catch语句、throws关键字以及自定义异常的创建与使用,我们旨在揭示如何有效地管理和响应程序运行中的错误和异常情况。此外,本文还将讨论一些最佳实践,以帮助开发者编写更加健壮和易于维护的代码。 ####
|
12天前
|
Java
Java 异常处理下篇:11 个异常处理最佳实践
本文深入探讨了 Java 异常处理的最佳实践,包括早抛出晚捕获、只捕获可处理的异常、不要忽略捕获的异常、抛出具体检查性异常、正确包装自定义异常、记录或抛出异常但不同时执行、避免在 `finally` 块中抛出异常、避免使用异常进行流程控制、使用模板方法处理重复的 `try-catch`、尽量只抛出与方法相关的异常以及异常处理后清理资源。通过遵循这些实践,可以提高代码的健壮性和可维护性。
|
9天前
|
安全 Java 编译器
Java多线程编程的陷阱与最佳实践####
【10月更文挑战第29天】 本文深入探讨了Java多线程编程中的常见陷阱,如竞态条件、死锁、内存一致性错误等,并通过实例分析揭示了这些陷阱的成因。同时,文章也分享了一系列最佳实践,包括使用volatile关键字、原子类、线程安全集合以及并发框架(如java.util.concurrent包下的工具类),帮助开发者有效避免多线程编程中的问题,提升应用的稳定性和性能。 ####
37 1
|
11天前
|
Java
Java异常捕捉处理和错误处理
Java异常捕捉处理和错误处理
12 1
|
13天前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
29 2