JAVA 异常处理的认知学习过程
没有异常处理
学生时代,我编写的java代码中,很少会有try catch.最主要的原因如下:
- 应用的规模很小
- 没有不确定因素
- 代码可控性高
如果规模小,往往就没有复杂的逻辑链路,整个软件的分层也很浅.很多地方的问题都是"编码"的问题.其次,学生时代的作品中,往往没有复杂的组件:数据库连接本地,没有rpc调用,没有微服务化的需求,而这些,往往容易带来网络、服务端的不确定因素.最后,整个工程往往由一个人编写,所以对哪些整个处理流程的全链路都可以掌控到,明白调用的模块到底有没有抛出异常.
很容易理解,这些特性决定了,即使完全不使用java的异常机制,整个应用也能跑起来.
这就是一个很简单的无异常处理的示例代码:
public boolean saveStudentInfo(File file) {
// 读取本地一个csv文件
List<StudentInfo> infoList = readFile(file);
// 通过网络发送到某个服务器
sendSocket(infoList);
}
全量try catch
到了工作的时候,第一堂课就是一定要捕获异常.如果spring MVC的controller不捕获异常,tomcat就会直接把抛出来的异常打印到页面上.对于一些面向用户的系统,会造成很大的影响.所以那个时候,从dao层,service层,到controller层,恨不得整个调用链路上的所有方法都通过捕获
Exception
来捕获一切异常,代码写成了这样:public boolean saveStudentInfo(File file) {
try {
List<StudentInfo> infoList = readFile(file);
sendInfo(infoList);
return true;
} catch (Exception e) {
logger.error("保存学生信息服务调用失败", e);
return false;
}
}
如果这么做的话,有以下几点问题:
- 无法针对不同异常做不同的处理
- 吞并了异常信息
- 无效化某些异常应该的走中断流程
整块代码块分别try catch
为了解决上述的第一个问题,我们会在代码块内部根据不同的异常情况,抛出不同的异常.然后在catch代码块中分别处理.
public boolean saveStudentInfo(File file) {
try {
List<StudentInfo> infoList = readFile(file);
sendInfo(infoList);
return true;
} catch (FileNotFoundException e1) {
logger.error("保存学生信息失败:文件没有找到", e1);
} catch (SocketTimeoutException e2) {
logger.error("保存学生信息失败:网络超时", e2);
} catch (Exception e3) {
logger.error("保存学生信息失败:未知异常", e3);
}
return false;
}
通过上述的做法,我们就可以区分不同的异常了.定位问题也能更准确.
分块 try catch
但上述方案依然有个明显的弊端:如果代码块中有多个地方可能抛出同一种异常,在catch到异常后也无法真正做到区分.为了解决这个问题,我们不仅要从catch的角度进行拆分,还要从try的角度进行拆分.
public boolean saveStudentInfo(File file) {
try {
List<StudentInfo> infoList = readFile(file);
} catch (FileNotFoundException e) {
logger.error("没有找到文件:{}", file);
return false;
} try {
sendInfo(infoList);
} catch (SocketTimeoutException e) {
logger.error("服务器连接超时");
return false;
}
}
非常有意思的一点是,为什么在两次调用中分别只catch了
FileNotFoundException
和SocketTimeoutException
?为什么不在最后catch住Exception
?其实是我想说明这样一个问题:异常catch精细化.所谓精细化,其实是对代码质量进行控制的一个结果.换句话说:为了提高代码质量,你应该尽可能弄清楚每块代码可能抛出什么异常,并进行针对的处理.上面这个例子中.如果你很清楚
List<StudentInfo> infoList = readFile(file);
这句调用只可能产生FileNotFoundException
这个异常.那么你就该精细到这个异常本身.专门去catch这个异常.并作出对应的处理.当然如果这里有第二种异常可能被抛出,你也应当专门去catch.当然这里也有个逻辑分层的概念,一般的业务代码遵循上述原则.但在调用链路上的某个重要环节,比如Controller,是有可能需要捕获全量异常的.
不catch异常
到现在,我们依然面临这些问题:
- 无法真正做到catch全部异常
- catch异常导致信息被吞没
- 中断流程无效化
第一点,如果调用别人的代码,就不知道运行时到底会抛出什么异常,或者RPC调用,可能混入中间件本身的异常.等等这些情况导致无法真正做到把所有异常都分类进行catch.所以,除非catch住
Exception
这个异常,否则必然会有一些漏网之鱼没有被catch到.第二点,有的异常比如:InterruptedException
,如果你catch住了,但是什么也不做(打印日志在某些情况下也约等于什么也不做),是有一定问题的.第三点,有的异常被设计出来就是要中断当前业务逻辑的.如果你catch住了,但是没有正确中断当前流程,会导致更严重的问题.要真正解决上述的问题,需要明白:异常处理本质上想达到的目的不是消除所有的异常本身,而是有效地向上传递错误信息和正确地中断当前处理流程.在前面讲的通过catch
Exception
来处理异常,实际上就是犯了想要消除异常本身的错误.也就是说,考量一个异常到底有没有被正确处理,指导思想是:
- 关于异常的信息有没有被正确传递
- 当前的处理流程有没有被正确中断
到这里,不catch异常,反倒成了某些情形下处理异常的最佳解决方案.
到底catch不catch?
那么,究竟是什么因素决定了是应该catch住异常,还是继续抛出异常呢?如果用最简单的语言描述,答案就是:这个异常当前能不能处理,如果不能,就继续往上抛.于是问题就变成了对处理的定义.一个经常出现的疑惑是:catch一个异常打印一行日志,到底算不算处理?
这个问题要从我们上文中的指导思想中找答案.首先看第二个维度,如果需要中断流程,而只是打印了日志,很明显就不是正确的处理.这个相对比较容易理解.接着再看第一个维度.关于这个信息.可能有人会认为:日志已经打印出去了,按理说信息已经成功传递了啊?
从宏观上,把错误日志打印出去≠100%正确传递信息.比如说某个很严重的问题发生了,虽然及时打印了日志,但是没有及时通知到人身上,而是导致服务挂掉,用户不能访问应用,反馈过来,才发现了日志文件中大量的异常.这就是:虽然信息传递出去了,但是却没有正确传递.所谓正确传递,应该是按照不同重要性,以不同方式,正确及时地通知到正确的处理方.比如用户输入个人信息,名字中带了特殊符号,前端直接提醒用户.这里的重要性就是低,方式就是前端反馈,处理方是用户.
这里看上去和java的try catch无关.但是实质上,却有相通之处.
对应到我们之前说的打印一行日志是否是正确传递信息的问题.如果你确定,应用对于这个异常的处理,只需要打印一样日志就ok了.既没有逻辑回滚,保证操作原子性的需要,也没有必须马上通知处理方的需要,就可以认为这里的处理是合适的.
结束了吗?
最后最有意思的一点来了.上述的东西都不复杂,为什么在实际应用中却难以真正做好呢?
我总结了一个有意思的原因:即使不通过异常,java代码也可以实现异常信息的传递和流程的中断,正是因为有两种方式并行存在,反倒让人疑惑到底应该使用哪一种.
A:
if (condition == -1) {
throw new Exception("异常信息");
} B:
try {
// do something
} catch (Exception e) {
logger.error("异常信息");
return false;
} // do something
上面的代码,分别就是这两种方式.都可以做到中断流程和信息传递.由于第二种的存在,很多时候反倒让人不愿意使用第一种,它看上去更复杂,更危险.而其实在正确考量之后,应该放心大胆得抛出异常.
JAVA 异常处理的认知学习过程的更多相关文章
- 札记:Java异常处理
异常概述 程序在运行中总会面临一些"意外"情况,良好的代码需要对它们进行预防和处理.大致来说,这些意外情况分三类: 交互输入 用户以非预期的方式使用程序,比如非法输入,不正当的操作 ...
- java异常处理(父子异常的处理)
我当初学java异常处理的时候,对于父子异常的处理,我记得几句话“子类方法只能抛出父类方法所抛出的异常或者是其子异常,子类构造器必须要抛出父类构造器的异常或者其父异常”.那个时候还不知道子类方法为什么 ...
- Java 异常处理
异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的. 比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error:如果你用System.out ...
- 《转载》Java异常处理的10个最佳实践
本文转载自 ImportNew - 挖坑的张师傅 异常处理在编写健壮的 Java 应用中扮演着非常重要的角色.异常处理并不是功能性需求,它需要优雅地处理任何错误情况,比如资源不可用.非法的输入.nul ...
- JAVA 异常处理机制
主要讲述几点: 一.异常的简介 二.异常处理流程 三.运行时异常和非运行时异常 四.throws和throw关键字 一.异常简介 异常处理是在程序运行之中出现的情况,例如除数为零.异常类(Except ...
- Java异常处理和设计
在程序设计中,进行异常处理是非常关键和重要的一部分.一个程序的异常处理框架的好坏直接影响到整个项目的代码质量以及后期维护成本和难度.试想一下,如果一个项目从头到尾没有考虑过异常处理,当程序出错从哪里寻 ...
- 深入理解java异常处理机制
异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwable类的众多子类描述各种不同的 ...
- Java提高篇——Java 异常处理
异常的概念 异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的. 比如说,你的代码少了一个分号,那么运行出来结果是提示是错误java.lang.Error:如果你用Syst ...
- java异常处理的设计
有一句这样话:一个衡量Java设计师水平和开发团队纪律性的好方法就是读读他们应用程序里的异常处理代码. 本文主要讨论开发Java程序时,如何设计异常处理的代码,如何时抛异常,捕获到了怎么处理,而不是讲 ...
随机推荐
- git中如何忽略文件上传?
使用原因:至于我们为什么要使用git忽略文件,原因很多.就比如我自己的情况吧!自己一个人多地方开发,为了代码同步,这样很方便.但是有个问题就是,我创建 的是开源项目,上面有一些服务器上面的配置信息,这 ...
- ELK 安装部署实战 (最新6.4.0版本)
一.实战背景 根据公司平台的发展速度,对于ELK日志分析日益迫切.主要的需求有: 1.用户行为分析 2.运营活动点击率分析 作为上述2点需求,安装最新版本6.4.0是非常有必要的,大家可根据本人之前博 ...
- ruby URI类
一. URI require 'uri' uri = URI("http://foo.com/posts?id=30&limit=5#time=1305298413") # ...
- Typora -- 书写即美学
#Typora -- 书写即美学 ##基本快捷键--需要在所见即所想界面进行输入 加粗 Ctrl + B 加粗 斜体 Ctrl + I 斜体 下划线 Ctrl + U 下划线 删除线 Ctrl + S ...
- LeetCode 二叉树的层次遍历 C++
给定一个二叉树,返回其按层次遍历的节点值. (即逐层地,从左到右访问所有节点). 例如:给定二叉树: [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7 返回其层 ...
- malloc分配失败的两个现象
在实际代码中,malloc的反复分配释放,可能会导致某一次malloc分配失败,虽然上一次调用malloc分配成功(然后释放),下一次在相同地方调用malloc分配可能会失败,疑问在于,既然上一次分配 ...
- CSS流布局权威指南
http://www.cnblogs.com/qieguo/p/5421252.html
- jmeter插件下载
https://jmeter-plugins.org/wiki/Start/ 插件下载好后,将插件lib目录下的jar包放在jmeter安装目录下的lib里,插件ext目录下的jar包放在jmeter ...
- Eclipse中JS文件红叉处理
使用新版本的Eclipse 或者 MyEclipse,项目中的 JS文件出现红叉,让人觉得项目中存在错误代码,给人的感觉很不爽. 记录一下去掉红叉的方法: 第1步: 打开工作空间中的项目找到项目的 . ...
- json 处理日期格式
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss",timezone="GMT+8") private Date createT ...