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 层,从而转化成用户友好的错误信息。

目录
相关文章
|
3月前
|
Java 测试技术 API
现代化 java 分层开发实施策略与最佳实践指南
现代化Java分层开发采用清晰的多层架构,包括Controller、Service、Repository和DTO等核心层次。文章详细介绍了标准Maven/Gradle项目结构,各层职责与实现规范:实体层使用JPA注解,DTO层隔离数据传输,Repository继承JpaRepository,Service层处理业务逻辑,Controller层处理HTTP请求。推荐使用Spring Boot、Lombok、MapStruct等技术栈,并强调了单元测试和集成测试的重要性。这种分层设计提高了代码的可维护性、可测试
124 0
|
3月前
|
Java 程序员 数据库连接
我们详细地讲解一下 Java 异常及要如何处理
我是小假 期待与你的下一次相遇 ~
|
3月前
|
存储 监控 Java
Java内存管理集合框架篇最佳实践技巧
本文深入探讨Java 17+时代集合框架的内存管理最佳实践,涵盖不可变集合、Stream API结合、并行处理等现代特性。通过实战案例展示大数据集优化效果,如分批处理与内存映射文件的应用。同时介绍VisualVM、jcmd等内存分析工具的使用方法,总结六大集合内存优化原则,助你打造高性能Java应用。附代码资源链接供参考。
117 3
|
6月前
|
SQL Java 中间件
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
在BeetISQL 2.13.8版本中,客户使用batch insert向yashandb表插入数据并尝试获取自动生成的sequence id时,出现类型转换异常。原因是beetlsql在prepareStatement时未指定返回列,导致yashan JDBC驱动返回rowid(字符串),与Java Bean中的数字类型tid不匹配。此问题影响业务流程,使无法正确获取sequence id。解决方法包括:1) 在batchInsert时不返回自动生成的sequence id;2) 升级至BeetISQL 3,其已修正该问题。
【YashanDB知识库】yasdb jdbc驱动集成BeetISQL中间件,业务(java)报autoAssignKey failure异常
|
6月前
|
存储 设计模式 Java
重学Java基础篇—ThreadLocal深度解析与最佳实践
ThreadLocal 是一种实现线程隔离的机制,为每个线程创建独立变量副本,适用于数据库连接管理、用户会话信息存储等场景。
218 5
|
6月前
|
SQL druid Oracle
【YashanDB知识库】yasdb jdbc驱动集成druid连接池,业务(java)日志中有token IDENTIFIER start异常
客户Java日志中出现异常,影响Druid的merge SQL功能(将SQL字面量替换为绑定变量以统计性能),但不影响正常业务流程。原因是Druid在merge SQL时传入null作为dbType,导致无法解析递归查询中的`start`关键字。
|
6月前
|
缓存 运维 Java
Java静态代码块深度剖析:机制、特性与最佳实践
在Java中,静态代码块(或称静态初始化块)是指类中定义的一个或多个`static { ... }`结构。其主要功能在于初始化类级别的数据,例如静态变量的初始化或执行仅需运行一次的初始化逻辑。
230 4
|
7月前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
470 14
|
7月前
|
Java
Java中执行命令并使用指定配置文件的最佳实践
通过本文的介绍,您可以了解如何在Java中使用 `ProcessBuilder`执行系统命令,并通过指定配置文件、设置环境变量和重定向输入输出流来控制命令的行为。通过这些最佳实践,可以确保您的Java应用程序在执行系统命令时更加健壮和灵活。
153 7
|
7月前
|
缓存 Java 应用服务中间件
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
1290 5