1 背景
1 年前的文章,源于 Sonar 静态代码扫描中, 项目历史代码里有两个规则的 ISSUE 是异常相关的。
对于如何使用异常和设计异常, 借助于业界的经验, 抛砖引玉给大家分享下。尽量引用 Java API 和 Spring 的例子来说明。
2 为什么要有异常处理
正常运行情况下, 程序顺利运行下来不存在异常情况。但是往往程序正确运行依赖各种条件, 既有代码编写逻辑正确, 也有外部软件、硬件运行正常。其中一项无法正常工作, 程序就会发生异常。
因此, 在程序语言层面, 自然就会有异常处理。或捕获错误故障, 记录并处理; 或抛出错误故障 , 让上一层调用方捕获知道, 并做下一步处理。
而 Java 语言程序语言层面, 内置支持异常处理。
3 异常处理作用
调试程序, 定位缺陷。
异常是一种调试手段: 什么出错(异常类型), 在哪出错(异常堆栈), 为什么错(异常信息)
向前恢复, 继续服务。
对异常的处理, 是系统容错、可靠性的一环。
4 如何使用异常
4.1 对公共接口的参数进行检验
通过当参数约定不符时, 抛出 unchecked 异常,如 ArrayList.get
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
/**
* Checks if the given index is in range. If not, throws an appropriate
* runtime exception. This method does *not* check if the index is
* negative: It is always used immediately prior to an array access,
* which throws an ArrayIndexOutOfBoundsException if index is negative.
*/
private void rangeCheck(int index) {
if (index >= size)
// 抛出 unchecked 异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
异常的信息必须清楚, 包括但不限于引起异常的参数值。
/**
* Constructs an IndexOutOfBoundsException detail message.
* Of the many possible refactorings of the error handling code,
* this “outlining” performs best with both server and client VMs.
*/
private String outOfBoundsMsg(int index) {
return “Index: “+index+”, Size: “+size;
}
4.2 不要尝试处理代码错误
对代码错误, 最好的策略是马上失败不要 catch unchecked 异常。如 NullPointerExceptionIndexOutOfBoundsException,留下问题的审计日志, 便于追踪问题。
4.3 不要捕获或抛出通用基础异常
// Noncompliant
public void foo(String bar) throws Throwable {
throw new RuntimeException(“My Message”);
}
// Compliant
public void foo(String bar) {
throw new MyOwnRuntimeException(“My Message”);
}
4.4 当转换异常时使用异常链
try {
/* … */
} catch (Exception e) {
// Noncompliant – exception is lost
throw new RuntimeException(“context”);
}
上面的代码就会把原始异常丢失, 正确应该保留原始异常。
try {
/* … */
} catch (Exception e) {
throw new RuntimeException(e);
}
4.5 记录日志或抛出异常, 但不要同时都做
对同一个代码问题, 多种的日志信息反而会让开发人员难以简单清晰定位问题。
try {
/* … */
} catch (Exception e) {
LOGGER.error(“ 系统执行出错 ”,e);
throw new RuntimeException(e);
}
虽然成熟的日志系统有调用链 ID, 方便我们把请求下面的日志全部拖出来。但上面的例子, 最终系统日志会出现多个异常日志记录, 反而不及最终一个异常 (带异常链) 唯一记录到日志来得清晰。
4.6 不要在 finally 里抛出异常
try {
// 执行时抛出异常 e1
doSomethingThrowExceptionFirst();
} finally {
// 同时抛出异常 e2
doFinallyThrowExceptionSecond();
}
try{}异常 e1,finally{}异常 e2, 当同时出现异常时, 如果 e2 抛出 e1 丢失。正常做法,doFinallyThrowExceptionSecond 内处理异常或者记录异常到日志。
4.7 重用标准 java 异常
Java 内置异常在能明确表达当前代码异常情况下可以拿来重用。
4.8 异常提供上下文
异常在 java 中是对象, 保持和提供足够信息 引起异常的参数值、错误细节描述、错误文本、关于改正的信息(当前重试的次数)。如 org.springframework.core.convert.ConversionFailedException
/**
* Create a new conversion exception.
* @param sourceType the value’s original type
* @param targetType the value’s target type
* @param value the value we tried to convert
* @param cause the cause of the conversion failure
*/
public ConversionFailedException(TypeDescriptor sourceType, TypeDescriptor targetType, Object value, Throwable cause) {
super(“Failed to convert from type ” + sourceType + ” to type ” + targetType + ” for value ‘” + ObjectUtils.nullSafeToString(value) + “‘”cause);
this.sourceType = sourceType;
this.targetType = targetType;
this.value = value;
}
4.9 尽可能地在接近问题产生处处理异常
往往对异常能做出正确决定的是直接调用者。异常离源问题代码越远, 越难跟踪到源问题, 也更难做有用的处理。有时最好的异常处理是显示被设计来控制流程的对象。
4.10 有效的记录异常
对同一个异常只记录一次,注意日志级别。
// 日志级别反例
String productsRequest = prepareProductsRequest(productId);
// 生产上日志级别一般为 INFO, 此处不会打印到日志文件
logger.debug (productsRequest);
try {
String response = retrieveProducts(productsRequest);
logger.debug (response);
} catch (NoSuchProductException e) {
// 当发生异常时, 只记录了异常, 没有 productsRequest 这个值
logger.error(e);
}
// 日志级别正例
String productsRequest = prepareProductsRequest(productId);
try {
String response = retrieveProducts(productsRequest);
} catch (NoSuchProductException e) {
// 当发生异常时, 把 productsRequest 也打印出来
logger.error(“request:” + productsRequest, e);
}
5 如何设计异常
5.1 异常命名明确
名字体现异常是什么。例如 Java API 中的 FileNotFoundException,EOFException。抛出需要具体子异常, 捕获同时也需要具体子异常, 不同子异常处理会不同。
5.2 异常定义归类、有层次
按逻辑子模块定义一个异常或者相关的一系列异常。如 org.springframework.core.NestedRuntimeException
有基础基类异常, 调用者可以选择使用这个基类 catch 该类下面的所有子异常使用统一处理, 也可以单独对子异常处理。
// 选择使用这个基类 catch 该类下面的所有子异常使用统一处理
private Object getPropertyValue(Object obj) {
try {
this.beanWrapper.setWrappedInstance(obj);
return this.beanWrapper.getPropertyValue(this.sortDefinition.getProperty());
}catch (BeansException ex) {
// 调用者可以选择使用这个基类 catch 该类下面的所有子异常使用统一处理
logger.info(“PropertyComparator could not access property – treating as null for sorting”, ex);
return null;
}
}
// 也可以单独对子异常处理
} catch (IllegalStateException ex) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex; }
} catch (BeanCreationException ex) {
// 也可以单独对子异常处理
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
5.3 异常的抽象层次和接口的抽象层次一致
5.3.1 为什么?
异常抛出声明是接口定义的一部分 高级别的处理代码 catch 低级别的异常, 上下文缺少, 不能很好的做出处理 如果不进行抽象, 违反封装原则 (对外信息隐藏) 减少系统可重用和清晰性。
// 异常抽象层次和接口抽象层次一致
@Override
public Object getPropertyValue(String propertyName) throws BeansException {
Field field = this.fieldMap.get(propertyName);
if (field == null) {
//NotReadablePropertyException 是 InvalidPropertyException 的子类
throw new NotReadablePropertyException(this.target.getClass(), propertyName, “Field ‘” + propertyName + “‘ does not exist”);
}
try {
ReflectionUtils.makeAccessible(field);
return field.get(this.target);
}
catch (IllegalAccessException ex) {
// 封装 IllegalAccessException 异常抛出和接口同一抽象层次的异常,InvalidPropertyException 是 BeansException 的子类
throw new InvalidPropertyException(this.target.getClass(), propertyName, “Field is not accessible”, ex);
}
}
5.3.2 在 facade 下的模块化设计下, 严格完整的定义异常。
大前提: 是指如果一个子模块以 package 形式组织, 对外提供少量 public 的 facade 方法, 内部的异常设计。
异常严格完整设计(对于其他情况下的代码也是需要的,5.2 异常定义归类、有层次)
完整列出有可能异常、checked/unchecked 异常;
如果异常能被组织成继承结构, 不仅是父类异常, 所有异常都要定义。
让异常可表达、保护封装(前面已经说明)
使用 checked 异常(注意前提说明,Spring 里 BeanException 是 unchecked 异常, 跟本身 Bean 在 Spring 中设计定位有关)
5.4 什么时候使用检测性异常和非检测性异常(运行时异常)
5.4.1 当异常是可处理的, 使用检测性异常。
有可补偿条件
调用方有协定可以处理
5.4.2 当异常是可处理的, 使用运行时异常(非检测性异常)
程序代码错误
* 接口定义被破坏, 如参数是非法的, 抛出 IllegalArgumentException
5.5 错误类型编码表述转为异常表述
错误类型编码能对错误进行分类定义, 一般出现没有内置异常处理的编程语言, 但有时第三方或者网络协议都会有使用错误类型编码这种方式。Spring RestClientException 的例子,HttpStatusCodeException 下定义两个子异常, 分别是
HttpClientErrorException 代表收到 4xx
HttpServe rErrorException 代表收到 5xx
6 处理异常情况的策略
判断请求不能正确执行时, 不执行
请失败时清理环境返回来异常, 让请求能根据异常选择备选方案
守护挂起, 直接条件允许正确执行, 尝试完成请求
暂时的完成, 请求完成, 但不提交它直到可以完成。
恢复, 通过可接受的备选方案完成。备份资源须简单便于使用。
上升到更高的处理。如请求人员来对系统进行操作处理
回滚, 失败时不产生影响
重试, 通过重试在系统正常时完成请求。
参考资料
Exception Handling 知乎:如何优雅的处理异常(java)?ylxfc 等同学的回复