java 异常设计_java异常的设计和使用

59eecb97bff85d795e1b920e5f68860b.png

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 处理异常情况的策略

判断请求不能正确执行时,不执行

请失败时清理环境返回来异常,让请求能根据异常选择备选方案

守护挂起,直接条件允许正确执行,尝试完成请求

暂时的完成,请求完成,但不提交它直到可以完成。

恢复,通过可接受的备选方案完成。备份资源须简单便于使用。

上升到更高的处理。如请求人员来对系统进行操作处理

回滚,失败时不产生影响

重试,通过重试在系统正常时完成请求。

参考资料

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值