首页 > 编程语言 >【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(简化异常和错误的传播与检查)

【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(简化异常和错误的传播与检查)

时间:2023-06-11 15:01:00浏览次数:30  
标签:指南 Java 抛出 propagate Throwables Throwable Guava 异常 throw

【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(简化异常和错误的传播与检查)_抛出异常

异常传播

有时候,您可能需要重新抛出捕获到的异常。这种情况通常发生在捕获到 Error 或 RuntimeException 时,因为您可能没有预料到这些异常,但在声明捕获 Throwable 和 Exception 时,它们也被包含在内了。为了解决这个问题,Guava 提供了多种方法来判断异常类型并重新抛出异常。例如:

try {
	someMethodThatCouldThrowAnything();
} catch (IKnowWhatToDoWithThisException e) {
	handle(e);
} catch (Throwable t) {
	Throwables.propagateIfInstanceOf(t, IOException.class);
	Throwables.propagateIfInstanceOf(t, SQLException.class);
	throw Throwables.propagate(t);
}

所有这些方法都会自动决定是否需要抛出异常,但也可以直接抛出方法返回的结果。例如,使用 throw Throwables.propagate(t) 可以向编译器声明这里一定会抛出异常。

异常传播类型

Guava 中的异常传播方法简要列举如下:

RuntimeException propagate(Throwable)
  • 如果Throwable是Error 或 RuntimeException,直接抛出;
  • 否则把 Throwable 包装成 RuntimeException 抛出,返回类型是 RuntimeException;

所以你可以像上面说的那样写成throw Throwables.propagate(t),Java 编译器会意识到这行代码保证抛出异常。

void propagateIfInstanceOf(Throwable, Class<X extends Exception>) throws X

方法为 void propagateIfInstanceOf(Throwable, Class),它的作用是只有当 Throwable 的类型为 X 时才抛出 X 异常。

void propagateIfPossible(Throwable)

只有当 Throwable 类型为 Error 或 RuntimeException 时才会抛出异常。

void propagateIfPossible( Throwable, Class<X extends Throwable>) throws X

只有当Throwable 类型为 X, Error 或 RuntimeException 才抛出

Throwables.propagate的用法

模仿 Java7 的多重异常捕获和再抛出

通常来说,如果调用者想让异常传播到栈顶,他不需要写任何 catch 代码块。因为他不打算从异常中恢复,他可 能就不应该记录异常,或者有其他的动作。他可能是想做一些清理工作,但通常来说,无论操作是否成功,清理 工作都要进行,所以清理工作可能会放在 finallly 代码块中。但有时候,捕获异常然后再抛出也是有用的:也许调 用者想要在异常传播之前统计失败的次数,或者有条件地传播异常。

当只对一种异常进行捕获和再抛出时,代码可能还是简单明了的。但当多种异常需要处理时,却可能变得一团 糟:

@Override 
public void run() {
	try {
		delegate.run();
	} catch (RuntimeException e) {
		failures.increment();
		throw e;
	}catch (Error e) {
		failures.increment();
		throw e;
	}
}

Java7 用多重捕获解决了这个问题:

} catch (RuntimeException | Error e) {
	failures.increment();
	throw e;
}

非Java7用户却受困于这个问题。他们想要写如下代码来统计所有异常,但是编译器不允许他们抛出 Throwabl e。

} catch (Throwable t) {
	failures.increment();
	throw t;
}

解决办法是用 throw Throwables.propagate(t)替换 throw t。在限定情况下(捕获 Error 和 RuntimeExcepti on),Throwables.propagate 和原始代码有相同行为。然而,用 Throwables.propagate 也很容易写出有其 他隐藏行为的代码。尤其要注意的是,这个方案只适用于处理 RuntimeException 或 Error。如果 catch 块捕获 了受检异常,你需要调用 propagateIfInstanceOf 来保留原始代码的行为,因为 Throwables.propagate 不能 直接传播受检异常。

总之,Throwables.propagate 的这种用法也就马马虎虎,在 Java7 中就没必要这样做了。在其他 Java 版本 中,它可以减少少量的代码重复,但简单地提取方法进行重构也能做到这一点。此外,使用 propagate 会意外地 包装受检异常。

非必要用法:把抛出的 Throwable 转为 Exception

有少数 API,尤其是 Java 反射 API 和(以此为基础的)JUnit,把方法声明成抛出 Throwable。和这样的 API 交互太痛苦了,因为即使是最通用的 API 通常也只是声明抛出 Exception。当确定代码会抛出 Throwable,而 不是 Exception 或 Error 时,调用者可能会用 Throwables.propagate 转化 Throwable。这里有个用 Callabl e 执行 JUnit 测试的范例:

public Void call() throws Exception {
	try {
		FooTest.super.runTest();
	} catch (Throwable t) {
		Throwables.propagateIfPossible(t, Exception.class);
		Throwables.propagate(t);
	}
	return null;
}

在这儿没必要调用 propagate()方法,因为 propagateIfPossible 传播了 Throwable 之外的所有异常类型,第 二行的 propagate 就变得完全等价于 throw new RuntimeException(t)。(这个例子也提醒我们,pr opagateIfPossible可能也会引起混乱,因为它不但会传播参数中给定的异常类型,还抛出 Error 和 RuntimeE xception)

这种模式(或类似于 throw new RuntimeException(t)的模式)在 Google 代码库中出现了超过 30 次。(搜索’propagateIfPossible[^;]* Exception.class[)];’)绝大多数情况下都明确用了”throw new RuntimeException(t)”。我们也曾想过有个”throwWrappingWeirdThrowable”方法处理 Throwable 到 Exception 的转化。但考虑到我们用两行代码实现了这个模式,除非我们也丢弃 propagateIfPossible 方法,不然定义这个 throwWrappingWeirdThrowable 方法也并没有太大必要。

Throwables.propagate 的有争议用法

在 Java 中,异常分为受检查异常和非受检查异常。受检查异常是指在方法声明中必须声明抛出的异常,而非受检查异常则是指在方法声明中不需要声明抛出的异常。受检查异常必须在方法内部进行处理,否则编译时会报错,而非受检查异常则可以不进行处理,但是如果不进行处理,程序会抛出运行时异常并终止执行。受检查异常一般是由 I/O 操作、网络操作等可能会出现异常的操作所引发的,而非受检查异常则是由程序错误或者逻辑错误所引发的。

争议一:把受检异常转化为非受检异常

原则上,非受检异常代表 bug,而受检异常表示不可控的问题。但在实际运用中,即使 JDK 也有所误用——如 Object.clone()、Integer. parseInt(String)、URI(String)——或者至少对某些方法来说,没有让每个人都信服 的答案,如 URI.create(String)的异常声明。

因此,调用者有时不得不把受检异常和非受检异常做相互转化:

try {
return Integer.parseInt(userInput);
} catch (NumberFormatException e) {
	throw new InvalidInputException(e);
}
try {
return publicInterfaceMethod.invoke();
} catch (IllegalAccessException e) {
	throw new AssertionError(e);
}

有时候,调用者会使用 Throwables.propagate 转化异常。这样做有没有什么缺点?最主要的恐怕是代码的含义 不太明显。

throw Throwables.propagate(ioException)做了什么?throw new RuntimeException(ioException)做了什么?这两者做了同样的事情,但后者的意思更简单直接。前者却引起了疑问:”它做了什么?它并不只是把异常包装进RuntimeException 吧?如果它真的只做了包装,为什么还非得要写个方法?”。应该承认,这些问题部分是因为”propagate”的语义太模糊了(用来抛出未声明的异常吗?)。

也许”wrapIfChecked”更能清楚地表达含义。但即使方法叫做”wrapIfChecked”,用它来包装一个已知类型的受检异常也没什么优点。甚至会有其他缺点:也许比起 RuntimeException,还有更合适的类型——如 IllegalArgumentException。 我们有时也会看到 propagate 被用于传播可能为受检的异常,结果是代码相比以前会稍微简短点,但也稍微有点不清晰:

} catch (RuntimeException e) {
	throw e;
}catch (Exception e) {
	throw new RuntimeException(e);
}

然而,我们似乎故意忽略了把检查型异常转化为非检查型异常的合理性。在某些场景中,这无疑是正确的做 法,但更多时候它被用于避免处理受检异常。这让我们的话题变成了争论受检异常是不是坏主意了,我不想对此 多做叙述。但可以这样说,Throwables.propagate 不是为了鼓励开发者忽略 IOException 这样的异常。

} catch (Exception e) {
	throw Throwables.propagate(e);
}
争议二:异常穿隧

如果你要实现不允许抛出异常的方法呢?有时候你需要把异常包装在非受检异常内。这种做法挺好,但我们再次强调,没必要用 propagate 方法做这种简单的包装。

实际上,手动包装可能更好:如果你手动包装了所有异常(而不仅仅是受检异常),那你就可以在另一端解包所有异常,并处理极少数特殊场景。此外,你可能还想把异常包装成特定的类型,而不是像 propagate 这样统一包装成 RuntimeException。

争议三:重新抛出其他线程产生的异常
try {
return future.get();
} catch (ExecutionException e) {
throw Throwables.propagate(e.getCause());
}

对这样的代码要考虑很多方面:

  • ExecutionException 的 cause 可能是受检异常,见上文”争议一:把检查型异常转化为非检查型异 常”。但如果我们确定 future 对应的任务不会抛出受检异常呢?(可能 future 表示 runnable 任务的结 果)。
  • 如 ExecutorService 中的 submit(Runnable task, T result)方法)如上所述,你可以捕获 异常并抛出 AssertionError 。尤其对于 Future,请考虑 Futures.get 方法。(TODO:对 future.get()抛 出的另一个异常 InterruptedException 作一些说明)
  • ExecutionException 的 cause 可能直接是 Throwable 类型,而不是 Exception 或 Error。(实际上这不 大可能,但你想直接重新抛出 cause 的话,编译器会强迫你考虑这种可能性)见上文”用法二:把抛出 Thr owable 改为抛出 Exception”。
  • ExecutionException 的 cause 可能是非受检异常。如果是这样的话,cause 会直接被 Throwables.prop agate 抛出。不幸的是,cause 的堆栈信息反映的是异常最初产生的线程,而不是传播异常的线程。通常来 说,最好在异常链中同时包含这两个线程的堆栈信息,就像 ExecutionException 所做的那样。(这个问题 并不单单和 propagate 方法相关;所有在其他线程中重新抛出异常的代码都需要考虑这点)。

异常原因链

Guava 提供了如下三个有用的方法,让研究异常的原因链变得稍微简便了,这三个方法的签名是不言自明的:

获取最顶层的异常信息原因

Throwable getRootCause(Throwable)

获取整体的异常链信息原因

List<Throwable> getCausalChain(Throwable)

获取堆栈的内部的异常原因转换为String

String getStackTraceAsString(Throwable)

下节预告

【Java技术专题】「Guava开发指南」手把手教你如何进行使用Guava工具箱进行开发系统实战指南(集合开发实战篇)

标签:指南,Java,抛出,propagate,Throwables,Throwable,Guava,异常,throw
From: https://blog.51cto.com/alex4dream/6457861

相关文章

  • java 聚合项目--pom.xml配置文件
    java聚合项目1.parent--pom.xml配置文件4.0.0<groupId>org.test.java.vip.train</groupId><artifactId>test-train-parent</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><modules>&l......
  • Java开发——Maven
    MVC模式MVC模式是软件工程中常见的一种软件架构模式,该模式把项目分为三个基本部分:模型Model、视图View和控制器Controller。视图:界面的显示,以及与用户的交互功能控制器:用来决定对于视图发来的请求需要那一个模型来处理,以及处理后需要跳回到哪一个视图,用来连接视图和模型模型:持......
  • java——微服务——spring cloud——Nacos——Nacos环境隔离——namespace
              ===========================================================================================              ......
  • java 创建聚合项目的2种方式及 部署Nacos
    创建聚合项目的2种方式byIDEA20231.创建module,erp-parent2.创建子module,设置每个module的pom.xml属性pom及父模块;3.配置erp-parent模块里的pom.xml1.创建module,erp-parent2.创建子module--mavenarchet部署nacosype(archetype:quickstart(java工程),webapp(web工程),设置......
  • java——微服务——spring cloud——Nacos——NacosRule负载均衡
    首先设置下orderservice的集群所属:  设置负载均衡规则:  ......
  • java——微服务——spring cloud——Nacos——Nacos服务实例的权重设置
          ......
  • STL实践指南
    这是一篇指导您如何在Microsoft Visual Studio下学习STL并进行实践的文章。这篇文章从STL的基础知识讲起,循序渐进,逐步深入,涉及到了STL编写代码的方法、STL代码的编译和调试、命名空间(namespace)、STL中的ANSI / ISO字符串、各种不同类型的容器(container)、......
  • java——微服务——spring cloud——Nacos——Nacos服务分级存储模型——集群配置
                  ......
  • 在 Windows 上运行 Podman: 操作指南
    在Windows上运行Podman:操作指南 在Windows上运行Podman:操作指南https://www.redhat.com/sysadmin/run-podman-windows2021年9月的时候,我写过一篇关于如何在Windows上运行Podman的文章。在那篇文章中,我讨论了为什么你必须设置WSL(WindowsSubsystemfor......
  • JavaScript模块化实现方式详解
    前言JavaScript是一门非常灵活的编程语言,但是在开发大型应用时,代码的组织和管理变得非常重要。为了解决这个问题,JavaScript社区提出了模块化的概念。模块化可以将代码分割成小的、独立的模块,每个模块只关注自己的功能,这样可以提高代码的可维护性和可重用性。在JavaScript中,有多种......