zoukankan      html  css  js  c++  java
  • JAVA 异常处理的认知学习过程

    1. 没有异常处理

      学生时代,我编写的java代码中,很少会有try catch.最主要的原因如下:

      • 应用的规模很小
      • 没有不确定因素
      • 代码可控性高

      如果规模小,往往就没有复杂的逻辑链路,整个软件的分层也很浅.很多地方的问题都是"编码"的问题.其次,学生时代的作品中,往往没有复杂的组件:数据库连接本地,没有rpc调用,没有微服务化的需求,而这些,往往容易带来网络、服务端的不确定因素.最后,整个工程往往由一个人编写,所以对哪些整个处理流程的全链路都可以掌控到,明白调用的模块到底有没有抛出异常.

      很容易理解,这些特性决定了,即使完全不使用java的异常机制,整个应用也能跑起来.

      这就是一个很简单的无异常处理的示例代码:

       public boolean saveStudentInfo(File file) {
           // 读取本地一个csv文件
           List<StudentInfo> infoList = readFile(file);
           // 通过网络发送到某个服务器
           sendSocket(infoList);
       }
      
    2. 全量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;
           }
       }
      

      如果这么做的话,有以下几点问题:

      • 无法针对不同异常做不同的处理
      • 吞并了异常信息
      • 无效化某些异常应该的走中断流程
    3. 整块代码块分别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;
       }
      

      通过上述的做法,我们就可以区分不同的异常了.定位问题也能更准确.

    4. 分块 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,是有可能需要捕获全量异常的.

    5. 不catch异常

      到现在,我们依然面临这些问题:

      • 无法真正做到catch全部异常
      • catch异常导致信息被吞没
      • 中断流程无效化

      第一点,如果调用别人的代码,就不知道运行时到底会抛出什么异常,或者RPC调用,可能混入中间件本身的异常.等等这些情况导致无法真正做到把所有异常都分类进行catch.所以,除非catch住Exception这个异常,否则必然会有一些漏网之鱼没有被catch到.第二点,有的异常比如: InterruptedException,如果你catch住了,但是什么也不做(打印日志在某些情况下也约等于什么也不做),是有一定问题的.第三点,有的异常被设计出来就是要中断当前业务逻辑的.如果你catch住了,但是没有正确中断当前流程,会导致更严重的问题.

      要真正解决上述的问题,需要明白:异常处理本质上想达到的目的不是消除所有的异常本身,而是有效地向上传递错误信息和正确地中断当前处理流程.在前面讲的通过catch Exception来处理异常,实际上就是犯了想要消除异常本身的错误.

      也就是说,考量一个异常到底有没有被正确处理,指导思想是:

      • 关于异常的信息有没有被正确传递
      • 当前的处理流程有没有被正确中断

      到这里,不catch异常,反倒成了某些情形下处理异常的最佳解决方案.

    6. 到底catch不catch?

      那么,究竟是什么因素决定了是应该catch住异常,还是继续抛出异常呢?如果用最简单的语言描述,答案就是:这个异常当前能不能处理,如果不能,就继续往上抛.于是问题就变成了对处理的定义.一个经常出现的疑惑是:catch一个异常打印一行日志,到底算不算处理?

      这个问题要从我们上文中的指导思想中找答案.首先看第二个维度,如果需要中断流程,而只是打印了日志,很明显就不是正确的处理.这个相对比较容易理解.接着再看第一个维度.关于这个信息.可能有人会认为:日志已经打印出去了,按理说信息已经成功传递了啊?

      从宏观上,把错误日志打印出去≠100%正确传递信息.比如说某个很严重的问题发生了,虽然及时打印了日志,但是没有及时通知到人身上,而是导致服务挂掉,用户不能访问应用,反馈过来,才发现了日志文件中大量的异常.这就是:虽然信息传递出去了,但是却没有正确传递.所谓正确传递,应该是按照不同重要性,以不同方式,正确及时地通知到正确的处理方.比如用户输入个人信息,名字中带了特殊符号,前端直接提醒用户.这里的重要性就是低,方式就是前端反馈,处理方是用户.

      这里看上去和java的try catch无关.但是实质上,却有相通之处.

      对应到我们之前说的打印一行日志是否是正确传递信息的问题.如果你确定,应用对于这个异常的处理,只需要打印一样日志就ok了.既没有逻辑回滚,保证操作原子性的需要,也没有必须马上通知处理方的需要,就可以认为这里的处理是合适的.

    7. 结束了吗?

      最后最有意思的一点来了.上述的东西都不复杂,为什么在实际应用中却难以真正做好呢?

      我总结了一个有意思的原因:即使不通过异常,java代码也可以实现异常信息的传递和流程的中断,正是因为有两种方式并行存在,反倒让人疑惑到底应该使用哪一种.

       A:
       if (condition == -1) {
           throw new Exception("异常信息");
       }
       
       B:
       try {
           // do something
       } catch (Exception e) {
           logger.error("异常信息");
           return false;
       }
       
       // do something
      

      上面的代码,分别就是这两种方式.都可以做到中断流程和信息传递.由于第二种的存在,很多时候反倒让人不愿意使用第一种,它看上去更复杂,更危险.而其实在正确考量之后,应该放心大胆得抛出异常.

  • 相关阅读:
    修改RedHat7的root用户密码
    Linux目录,rpm及top,vi命令简记
    Centos7或RedHat7下安装Mysql
    异常、线程
    File类
    JDBC的学习(一)
    MySql多表查询_事务_DCL(资料三)
    MySql约束_设计_备份还原(资料二)
    MySql基础_DDL_DML_DQL(资料一)
    算法小结(一)
  • 原文地址:https://www.cnblogs.com/dsj2016/p/8530972.html
Copyright © 2011-2022 走看看