java 异常分类和处理机制

    一、背景介绍

       程序在运行过程中发生错误或异常情况是不可避免的,如果每一个运行时错误都由程序员手动控制和处理,其工作量是不可想象的。

      Java语言中的异常处理机制就解决的上述问题,把错误与异常的管理带到了面向对象的世界

 

 

Java语言定义了很多异常类,将运行错误和异常的信息和处理方法封装在了异常类中,帮助程序员检查和控制异常。即Java造好了工具,程序员只要学会使用工具。

 

二、java异常分类

Java语言按照错误严重性,从throwale根类衍生出Error和Exception两大派系

Error(错误)

程序在执行过程中所遇到的硬件或操作系统的错误。错误对程序而言是致命的,将导致程序无法运行。常见的错误有内存溢出,jvm虚拟机自身的非正常运行,calss文件没有主方法。程序本生是不能处理错误的,只能依靠外界干预。Error是系统内部的错误,由jvm抛出,交给系统来处理。

     Exception(异常)

是程序正常运行中,可以预料的意外情况。比如数据库连接中断,空指针,数组下标越界。异常出现可以导致程序非正常终止,也可以预先检测,被捕获处理掉,使程序继续运行。

 

EXCEPTION(异常)按照性质,又分为编译异常(可检测)和运行时异常(不可检测)。

     编译时异常:

又叫可检查异常,通常时由语法错和环境因素(外部资源)造成的异常。比如输入输出异常IOException,数据库操作SQLException。其特点是,Java语言强制要求捕获和处理所有非运行时异常。通过行为规范,强化程序的健壮性和安全性。

     运行时异常:

又叫不检查异常RuntimeException,这些异常一般是由程序逻辑错误引起的,即语义错。比如算术异常,空指针异常NullPointerException,下标越界IndexOutOfBoundsException。运行时异常应该在程序测试期间被暴露出来,由程序员去调试,而避免捕获。

 

所以,java语言处理运行时错误有三种方式,

  1. 一是程序不能处理的错误,
  2. 二是程序应该避免而可以不去捕获的运行时异常,
  3. 三是必须捕获的非运行时异常。

 

三、java异常处理机制

java默认处理机制:

1抛出异常

2终止程序

异常处理程序机制:

1抛出异常

2try-catch-finally 捕获和处理异常

当Java程序运行到某个方法发生异常时,产生一个对应异常类对象,包含异常事件类型,发生异常是应用程序的状态,和调用过程等信息,然后抛出,运行系统开始查找有没又匹配异常处理程序,么有,就中断程序,有就将控制权交个程序处理程序,处理异常。

异常处理是程序开发中必不可少操作之一,但如何正确优雅的对异常进行处理确是一门学问,笔者在此处转载了一篇微博的内容,来谈一谈何对异常进行处理的。

四、如何优雅的设计异常

4.1 如何选择异常

从开发经验来看,如果在一个应用中,需要开发一个方法(如某个功能的service方法),这个方法如果中间可能出现异常,那么你需要考虑这个异常出现之后是否调用者可以处理,并且你是否希望调用者进行处理,如果调用者可以处理,并且你也希望调用者进行处理,那么就要抛出受检异常,提醒调用者在使用你的方法时,考虑到如果抛出异常时如果进行处理,相似的,如果在写某个方法时,你认为这是个偶然异常,理论上说,你觉得运行时可能会碰到什么问题,而这些问题也许不是必然发生的,也不需要调用者显示的通过异常来判断业务流程操作的,那么这时就可以使用一个RuntimeException这样的非受检异常.

4.2 什么时候才需要抛异常

首先我们需要了解一个问题,什么时候才需要抛异常?异常的设计是方便给开发者使用的,但不是乱用的,笔者对于什么时候抛异常这个问题也问了很多朋友,能给出准确答案的确实不多。其实这个问题很简单,如果你觉得某些”问题”解决不了了,那么你就可以抛出异常了。比如,你在写一个service,其中在写到某段代码处,你发现可能会产生问题,那么就请抛出异常吧,相信我,你此时抛出异常将是一个最佳时机。

4.3 应该抛出怎样的异常

了解完了什么时候才需要抛出异常后,我们再思考一个问题,真的当我们抛出异常时,我们应该选用怎样的异常呢?究竟是受检异常还是非受检异常呢(RuntimeException)呢?我来举例说明一下这个问题,先从受检异常说起,比如说有这样一个业务逻辑,需要从某文件中读取某个数据,这个读取操作可能是由于文件被删除等其他问题导致无法获取从而出现读取错误,那么就要从redis或mysql数据库中再去获取此数据,参考如下代码,getKey(Integer)为入口程序.

 

ok,看了以上代码以后,你也许心中有一些想法,原来受检异常可以控制义务逻辑,对,没错,通过受检异常真的可以控制业务逻辑,但是切记不要这样使用,我们应该合理的抛出异常,因为程序本身才是流程,异常的作用仅仅是当你进行不下去的时候找到的一个借口而已,它并不能当成控制程序流程的入口或出口,如果这样使用的话,是在将异常的作用扩大化,这样将会导致代码复杂程度的增加,耦合性会提高,代码可读性降低等问题。那么就一定不要使用这样的异常吗?其实也不是,在真的有这样的需求的时候,我们可以这样使用,只是切记,不要把它真的当成控制流程的工具或手段。那么究竟什么时候才要抛出这样的异常呢?要考虑,如果调用者调用出错后,一定要让调用者对此错误进行处理才可以,满足这样的要求时,我们才会考虑使用受检异常。
接下来,我们来看一下非受检异常呢(RuntimeException),对于RuntimeException这种异常,我们其实很多见,比如java.lang.NullPointerException/java.lang.IllegalArgumentException等,那么这种异常我们时候抛出呢?当我们在写某个方法的时候,可能会偶然遇到某个错误,我们认为这个问题时运行时可能为发生的,并且理论上讲,没有这个问题的话,程序将会正常执行的时候,它不强制要求调用者一定要捕获这个异常,此时抛出RuntimeException异常,举个例子,当传来一个路径的时候,需要返回一个路径对应的File对象:

上述例子表明,如果调用者调用getFiles(String)的时候如果path是空,那么就抛出空指针异常(它是RuntimeException的子类),调用者不用显示的进行try…catch…操作进行强制处理.这就要求调用者在调用这样的方法时先进行验证,避免发生RuntimeException.如下:

4.4 应该选用哪种异常

通过以上的描述和举例,可以总结出一个结论,RuntimeException异常和受检异常之间的区别就是:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检异常,否则就选择非受检异常(RuntimeException)。一般来讲,如果没有特殊的要求,我们建议使用RuntimeException异常。

4.5 何优雅的设计java异常示例

domain介绍

根据项目场景来看,需要两个domain模型,一个是用户实体,一个是地址实体.
Address domain如下:


 User domain如下:      

ok,上边是一个模型关系,用户-收货地址的关系是1-n的关系。上边的@Data是使用了一个叫做lombok的工具,它自动生成了Setter和Getter等方法,用起来非常方便,感兴趣的读者可以自行了解一下。

dao介绍

数据连接层,我们使用了spring-data-jpa这个框架,它要求我们只需要继承框架提供的接口,并且按照约定对方法进行取名,就可以完成我们想要的数据库操作。
用户数据库操作如下:

收货地址操作如下:

正如读者所看到的,我们的DAO只需要继承JpaRepository,它就已经帮我们完成了基本的CURD等操作,如果想了解更多关于spring-data的这个项目,请参考一下spring的官方文档,它比不方案我们对异常的研究。

Service异常设计

ok,终于到了我们的重点了,我们要完成service一些的部分操作:添加收货地址,删除收货地址,获取收货地址列表.
首先看我的service接口定义:

我们来关注一下实现:

添加收货地址

首先再来看一下之前整理的约束条件:

入参:

  1. 用户id

  2. 收货地址实体信息

约束:

  1. 用户id不能为空,且此用户确实是存在的

  2. 收货地址的必要字段不能为空

  3. 如果用户还没有收货地址,当此收货地址创建时设置成默认收货地址

先看以下代码实现:

其中,已经完成了上述所描述的三点约束条件,当三点约束条件都满足时,才可以进行正常的业务逻辑,否则将抛出异常(一般在此处建议抛出运行时异常-RuntimeException)。

介绍以下以上我所用到的技术:

1.Preconfitions.checkNotNull(T t)这个是使用Guava中的com.google.common.base.Preconditions进行判断的,因为service中用到的验证较多,所以建议将Preconfitions改成静态导入的方式:

当然Guava的github中的说明也建议我们这样使用。

2.BeanValidators.validateWithException(validator, address);
这个使用了hibernate实现的jsr 303规范来做的,需要传入一个validator和一个需要验证的实体,那么validator是如何获取的呢,如下:

他将获取一个Validator对象,然后我们在service中进行注入便可以使用了:

那么BeanValidators这个类是如何实现的?其实实现方式很简单,只要去判断jsr 303的标注注解就ok了。
那么jsr 303的注解写在哪里了呢?当然是写在address实体类中了:

写好你需要的约束条件来进行判断,如果合理的话,才可以进行业务操作,从而对数据库进行操作。

这块的验证是必须的,一个最主要的原因是:这样的验证可以避免脏数据的插入。如果读者有正式上线的经验的话,就可以理解这样的一个事情,任何的代码错误都可以容忍和修改,但是如果出现了脏数据问题,那么它有可能是一个毁灭性的灾难。程序的问题可以修改,但是脏数据的出现有可能无法恢复。所以这就是为什么在service中一定要判断好约束条件,再进行业务逻辑操作的原因了。

此处的判断为业务逻辑判断,是从业务角度来进行筛选判断的,除此之外,有可能在很多场景中都会有不同的业务条件约束,只需要按照要求来做就好。

对于约束条件的总结如下:

  1. 基本判断约束(null值等基本判断)

  2. 实体属性约束(满足jsr 303等基础判断)

  3. 业务条件约束(需求提出的不同的业务约束)

当这个三点都满足时,才可以进行下一步操作

ok,基本介绍了如何做一个基础的判断,那么再回到异常的设计问题上,上述代码已经很清楚的描述如何在适当的位置合理的判断一个异常了,那么如何合理的抛出异常呢?

只抛出RuntimeException就算是优雅的抛出异常吗?当然不是,对于service中的抛出异常,笔者认为大致有两种抛出的方法:

  1. 抛出带状态码RumtimeException异常

  2. 抛出指定类型的RuntimeException异常

相对这两种异常的方式进行结束,第一种异常指的是我所有的异常都抛RuntimeException异常,但是需要带一个状态码,调用者可以根据状态码再去查询究竟service抛出了一个什么样的异常。

 

第二种异常是指在service中抛出什么样的异常就自定义一个指定的异常错误,然后在进行抛出异常。

一般来讲,如果系统没有别的特殊需求的时候,在开发设计中,建议使用第二种方式。但是比如说像基础判断的异常,就可以完全使用guava给我们提供的类库进行操作。jsr 303异常也可以使用自己封装好的异常判断类进行操作,因为这两种异常都是属于基础判断,不需要为它们指定特殊的异常。但是对于第三点义务条件约束判断抛出的异常,就需要抛出指定类型的异常了。

定义一个特定的异常类来进行这个义务异常的判断:

然后将此处改为:

ok,通过以上对service层的修改,代码更改如下:

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值