This article focuses on some misunderstandings in the selection and use of Java exceptions. I hope that readers can master some points and principles of exception handling and pay attention to summary and induction. Only by handling exceptions can we improve the basic literacy of developers, improve the robustness of the system, improve the user experience, and increase the value of the product.
Misunderstanding 1: Selection of exceptions
Figure 1. Exception classification
Figure 1 describes the structure of exceptions. In fact, we all know Anomalies are divided into detected anomalies and undetected anomalies, but in practice the applications of these two anomalies are confused. Because unchecked exceptions are so easy to use, many developers think that checked exceptions are of little use. In fact, abnormal application scenarios can be summarized as follows:
The calling code cannot continue to execute and needs to be terminated immediately. There are many possibilities for this situation, such as the server cannot be connected, the parameters are incorrect, etc. Unchecked exceptions are applicable in these situations, as they do not require explicit catching and processing of the calling code, and the code is concise and clear.
The calling code requires further processing and recovery. If SQLException is defined as an undetected exception, developers will take it for granted that SQLException does not need to be explicitly captured and processed by the calling code when operating data, which will lead to serious Connection not closing, Transaction not rolling back, dirty data appearing in the DB, etc. Because SQLException is defined as a detected exception, developers are driven to explicitly catch it and clean up resources after the code generates an exception. Of course, after cleaning up the resources, you can continue to throw undetected exceptions to prevent the execution of the program. Based on observation and understanding, detecting anomalies can mostly be applied to tool classes.
Misunderstanding 2: Display exceptions directly on the page or client
It is common to print exceptions directly on the client. Taking JSP as an example, once an exception occurs when the code is running, the container will by default Exception stack information is printed directly on the page. In fact, from the customer's perspective, any exception has no practical significance. The vast majority of customers cannot understand the exception information at all. Software development should also try to avoid presenting exceptions directly to users.
Listing 1
package com.ibm.dw.sample.exception; /** * 自定义 RuntimeException * 添加错误代码属性 */ public class RuntimeException extends java.lang.RuntimeException { //默认错误代码 public static final Integer GENERIC = 1000000; //错误代码 private Integer errorCode; public RuntimeException(Integer errorCode, Throwable cause) { this(errorCode, null, cause); } public RuntimeException(String message, Throwable cause) { //利用通用错误代码 this(GENERIC, message, cause); } public RuntimeException(Integer errorCode, String message, Throwable cause) { super(message, cause); this.errorCode = errorCode; } public Integer getErrorCode() { return errorCode; } }
As shown in the sample code, the error code is introduced in the exception. Once an exception occurs, we only need to present the error code of the exception to user, or convert the error code into a more understandable prompt. In fact, the error code here also contains another function. Developers can also know exactly what type of exception occurred based on the error code.
Misunderstanding 3: Pollution of the code hierarchy
We often divide the code into different hierarchies such as Service, Business Logic, DAO, etc. The DAO layer will contain methods that throw exceptions, such as Listing 2 is shown:
Listing 2
public Customer retrieveCustomerById(Long id) throw SQLException { //根据 ID 查询数据库 }
The above code seems fine at first glance, but if you think about it carefully from the perspective of design coupling, the SQLException here has polluted the upper calling code and the calling layer You need to explicitly use try-catch to catch it, or throw it to a higher level. According to the principle of design isolation, we can appropriately modify it to:
清单 3
public Customer retrieveCustomerById(Long id) { try{ //根据 ID 查询数据库 }catch(SQLException e){ //利用非检测异常封装检测异常,降低层次耦合 throw new RuntimeException(SQLErrorCode, e); }finally{ //关闭连接,清理资源 } }
误区四:忽略异常
如下异常处理只是将异常输出到控制台,没有任何意义。而且这里出现了异常并没有中断程序,进而调用代码继续执行,导致更多的异常。
清单 4
public void retrieveObjectById(Long id){ try{ //..some code that throws SQLException }catch(SQLException ex){ /** *了解的人都知道,这里的异常打印毫无意义,仅仅是将错误堆栈输出到控制台。 * 而在 Production 环境中,需要将错误堆栈输出到日志。 * 而且这里 catch 处理之后程序继续执行,会导致进一步的问题*/ ex.printStacktrace(); } }
可以重构成:
清单 5
public void retrieveObjectById(Long id){ try{ //..some code that throws SQLException } catch(SQLException ex){ throw new RuntimeException(“Exception in retieveObjectById”, ex); } finally{ //clean up resultset, statement, connection etc } }
这个误区比较基本,一般情况下都不会犯此低级错误。
误区五:将异常包含在循环语句块中
如下代码所示,异常包含在 for 循环语句块中。
清单 6
for(int i=0; i<100; i++){ try{ }catch(XXXException e){ //…. } }
我们都知道异常处理占用系统资源。一看,大家都认为不会犯这样的错误。换个角度,类 A 中执行了一段循环,循环中调用了 B 类的方法,B 类中被调用的方法却又包含 try-catch 这样的语句块。褪去类的层次结构,代码和上面如出一辙。
误区六:利用 Exception 捕捉所有潜在的异常
一段方法执行过程中抛出了几个不同类型的异常,为了代码简洁,利用基类 Exception 捕捉所有潜在的异常,如下例所示:
清单 7
public void retrieveObjectById(Long id){ try{ //…抛出 IOException 的代码调用 //…抛出 SQLException 的代码调用 }catch(Exception e){ //这里利用基类 Exception 捕捉的所有潜在的异常,如果多个层次这样捕捉,会丢失原始异常的有效信息 throw new RuntimeException(“Exception in retieveObjectById”, e); } }
可以重构成
清单 8
public void retrieveObjectById(Long id){ try{ //..some code that throws RuntimeException, IOException, SQLException }catch(IOException e){ //仅仅捕捉 IOException throw new RuntimeException(/*指定这里 IOException 对应的错误代码*/code,“Exception in retieveObjectById”, e); }catch(SQLException e){ //仅仅捕捉 SQLException throw new RuntimeException(/*指定这里 SQLException 对应的错误代码*/code,“Exception in retieveObjectById”, e); } }
误区七:多层次封装抛出非检测异常
如果我们一直坚持不同类型的异常一定用不同的捕捉语句,那大部分例子可以绕过这一节了。但是如果仅仅一段代码调用会抛出一种以上的异常时,很多时候没有必要每个不同类型的 Exception 写一段 catch 语句,对于开发来说,任何一种异常都足够说明了程序的具体问题。
清单 9
try{ //可能抛出 RuntimeException、IOExeption 或者其它; //注意这里和误区六的区别,这里是一段代码抛出多种异常。以上是多段代码,各自抛出不同的异常 }catch(Exception e){ //一如既往的将 Exception 转换成 RuntimeException,但是这里的 e 其实是 RuntimeException 的实例,已经在前段代码中封装过 throw new RuntimeException(/**/code, /**/, e); }
如果我们如上例所示,将所有的 Exception 再转换成 RuntimeException,那么当 Exception 的类型已经是 RuntimeException 时,我们又做了一次封装。将 RuntimeException 又重新封装了一次,进而丢失了原有的 RuntimeException 携带的有效信息。
解决办法是我们可以在 RuntimeException 类中添加相关的检查,确认参数 Throwable 不是 RuntimeException 的实例。如果是,将拷贝相应的属性到新建的实例上。或者用不同的 catch 语句块捕捉 RuntimeException 和其它的 Exception。个人偏好方式一,好处不言而喻。
误区八:多层次打印异常
我们先看一下下面的例子,定义了 2 个类 A 和 B。其中 A 类中调用了 B 类的代码,并且 A 类和 B 类中都捕捉打印了异常。
清单 10
public class A { private static Logger logger = LoggerFactory.getLogger(A.class); public void process(){ try{ //实例化 B 类,可以换成其它注入等方式 B b = new B(); b.process(); //other code might cause exception } catch(XXXException e){ //如果 B 类 process 方法抛出异常,异常会在 B 类中被打印,在这里也会被打印,从而会打印 2 次 logger.error(e); throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e); } } } public class B{ private static Logger logger = LoggerFactory.getLogger(B.class); public void process(){ try{ //可能抛出异常的代码 } catch(XXXException e){ logger.error(e); throw new RuntimeException(/* 错误代码 */ errorCode, /*异常信息*/msg, e); } } }
同一段异常会被打印 2 次。如果层次再复杂一点,不去考虑打印日志消耗的系统性能,仅仅在异常日志中去定位异常具体的问题已经够头疼的了。
其实打印日志只需要在代码的最外层捕捉打印就可以了,异常打印也可以写成 AOP,织入到框架的最外层。
误区九:异常包含的信息不能充分定位问题
异常不仅要能够让开发人员知道哪里出了问题,更多时候开发人员还需要知道是什么原因导致的问题,我们知道 java .lang.Exception 有字符串类型参数的构造方法,这个字符串可以自定义成通俗易懂的提示信息。
简单的自定义信息开发人员只能知道哪里出现了异常,但是很多的情况下,开发人员更需要知道是什么参数导致了这样的异常。这个时候我们就需要将方法调用的参数信息追加到自定义信息中。下例只列举了一个参数的情况,多个参数的情况下,可以单独写一个工具类组织这样的字符串。
清单 11
public void retieveObjectById(Long id){ try{ //..some code that throws SQLException }catch(SQLException ex){ //将参数信息添加到异常信息中 throw new RuntimeException(“Exception in retieveObjectById with Object Id :”+ id, ex); } }
误区十:不能预知潜在的异常
在写代码的过程中,由于对调用代码缺乏深层次的了解,不能准确判断是否调用的代码会产生异常,因而忽略处理。在产生了 Production Bug 之后才想起来应该在某段代码处添加异常补捉,甚至不能准确指出出现异常的原因。这就需要开发人员不仅知道自己在做什么,而且要去尽可能的知道别人做了什么,可能会导致什么结果,从全局去考虑整个应用程序的处理过程。这些思想会影响我们对代码的编写和处理。
误区十一:混用多种第三方日志库
现如今 Java 第三方日志库的种类越来越多,一个大项目中会引入各种各样的框架,而这些框架又会依赖不同的日志库的实现。最麻烦的问题倒不是引入所有需要的这些日志库,问题在于引入的这些日志库之间本身不兼容。如果在项目初期可能还好解决,可以把所有代码中的日志库根据需要重新引入一遍,或者换一套框架。但这样的成本不是每个项目都承受的起的,而且越是随着项目的进行,这种风险就越大。
How can we effectively avoid similar problems? Most current frameworks have already considered similar problems. You can configure Properties or xml files, parameters, or scan the log implementation class in the Lib library at runtime. Really, Which specific log library is used is determined only when the application is running.
In fact, based on the principle that there is no need to print logs at multiple levels, we can simplify many classes that originally call the log printing code. In many cases, we can use interceptors or filters to print logs and reduce the cost of code maintenance and migration.
Conclusion
The above is purely personal experience and summary. Things are dialectical. There are no absolute principles. What suits you is the most effective. in principle. I hope the above explanation and analysis can be helpful to you.
The above is the misunderstanding and experience summary of Java exception handling. For more related content, please pay attention to the PHP Chinese website (m.sbmmt.com)!