如何在 Java 中找到潜在的未经检查的异常?
How to find potential unchecked exceptions in Java?
根据 Java 规范,Java 编译器会根据 "throw" 语句和方法签名自动验证是否捕获了所有已检查的异常,并忽略未检查的异常。
但是,有时开发人员找出可以抛出哪些未经检查的异常会很有用,例如,某些第 3 方代码可能会在开发人员倾向于期望经过检查的异常的情况下抛出未经检查的异常(例如 Long.parseLong).或者开发人员可能会抛出一个未经检查的异常作为未来检查异常的占位符而忘记替换它。
在这些例子中,理论上可以找到这些未捕获的未检查异常。第一种情况Long.parseLong的签名表明它抛出NumberFormatException,而第二种情况,源代码是可用的,所以编译器知道抛出什么未经检查的异常。
我的问题是:有没有可以报告这些案例的工具?或者也许是一种让 Java 编译器暂时将未检查异常视为已检查异常的方法?这对于手动验证和修复可能导致整个线程或应用程序在运行时崩溃的潜在错误非常有用。
编辑:经过一些回答,我必须强调我的目标不是找到系统中可能出现的未检查异常的详尽列表,而是由于未检查异常导致的潜在错误.我认为归结为两种情况:
- 一个方法的签名表明它抛出一个未经检查的异常,而调用者没有捕获它
- 一个方法的主体抛出显式未经检查的异常,而调用者没有捕获它们
无法获取可能的未检查异常列表。由于它们没有在任何地方声明(它们是动态创建的),所以如果没有非常具体的代码分析器工具是不可能的——即使那样它也可能无法从编译库中捕获一些 类.
对于预测棘手事情的例子,考虑任何分配内存的东西都可能抛出内存不足异常,你甚至可以反射性地实例化一个异常,这几乎不可能用任何静态分析工具找到。
如果你真的很偏执,你可以捕获 RuntimeException
,那应该会得到你想要处理的所有未经检查的异常——这不是推荐的策略,但可以防止你的程序因某些原因而失败 unknown/unseen 未来的错误。
在许多情况下无法找到未经检查的异常,或者您可能会得到一长串可能发生的异常。
什么时候无法找到未经检查的异常?假设您要调用接口的方法。一种实现可能会抛出某些未经检查的异常,而其他实现则不会。编译器无法知道,因为它只在 运行 时间知道。
为什么您最终会列出一长串可能的例外情况?好吧,如果您提供未明确检查的 null
参数,几乎每个方法都可能抛出 NullPointerException
。如果选中它们,则可能是 IllegalArgumentException
。此外,此方法调用的每个方法也可能抛出不同的未经检查的异常,这些异常必须添加到列表中。您可以随时 运行 到 ClassNotFoundError
或 OutOfMemoryError
,这也必须添加到该列表中...
想像一个简单的方法
public Foo bar(String input) {
return new Foo(input.charAt(0));
}
单独使用该方法至少可以抛出 NullPointerException 或一些 OutOfBounds 异常。
要注意的是:为了查看 "all" 潜在的异常,您的工具必须检查 每个和任何 代码行(编译的或源代码)进入您的应用程序。
我不明白这怎么可能 "manageable"。
而且情况变得更糟,如果
public Foo(char whatever) {
String klazz = ... god knows
throw (RuntimeException) Class.forName(klazz).newInstance();
当然,这将需要一些 try/catch 用于反射代码具有的已检查异常;但要点是:了解潜在异常的整个宇宙可能最终会出现在为您绘制的 "pretty huge map" 中。里面有这么多 paths/branches,你永远找不到 0.001% 的有趣路径。
My question is: is there a tool that can report these cases?
据我所知,没有。
Or maybe a way to let the Java compiler treat temporarily unchecked exceptions are checked exceptions?
据我所知,没有。
虽然像这样的工具在理论上是可行的(有一些注意事项1),但它们在实践中几乎毫无用处。如果您仅依赖方法的本地分析,大多数普通的 Java 将被标记为可能抛出范围广泛的未经检查的异常。其中一些可以通过一些简单的非本地分析排除,但这还不足以避免过度 "false positives".
IMO,没有消除所有运行时异常的实用方法。
您应该做的是结合以下实践,以减少进入生产代码的错误数量(包括意外的运行时异常)
全面而有条理的测试;例如通过开发自动化单元和系统测试套件,并使用覆盖工具来帮助您识别尚未测试的代码路径。
使用 PMD 和 FindBugs 等静态分析工具可以检测某些 类 问题。您可以使用 @NotNull
.
等注释来帮助这些工具和类似工具
代码审查。
遵循良好的编码习惯,尤其是在开发多线程代码时。
但请注意,这些做法代价高昂,而且它们并不能消除所有错误。
其他一些答案似乎建议您捕获所有异常(例如 Exception
、RuntimeException
甚至 Throwable
)作为避免崩溃的一种方式。
这是误导。是的,您可以 "catch them all"(这称为 Pokemon 异常处理!)但是您无法从任意意外异常中安全地恢复。通常,当您遇到意外异常时,唯一完全安全的做法就是退出。
1 - 这些注意事项是:1) 分析器需要知道 Java 语言结构可能会隐式抛出未经检查的异常;例如实例方法调用可以抛出 NPE,new
可以抛出 OOOME,等等。2) 您需要分析代码使用的所有库方法,包括第 3 方库,3) Java 异常可能从本机代码中抛出,并且 4) 需要考虑涉及静态和 "bytecode engineering" 的事情。
是的,您可以编写静态分析来执行此操作。我自己也做过类似的事情,并在名为 Atlas. Here: https://github.com/EnSoftCorp/java-toolbox-commons/.../ThrowableAnalysis.java 的程序分析工具中编写了我的代码,这些代码可能对您的需要有帮助,它静态计算了一个软件中投掷站点和潜在捕获站点的匹配项(保守,因为它不考虑路径可行性)。对于您的情况,您对没有相应 catch 块的 throw 站点感兴趣。
以下是分析的重要部分。
- 所有检查或未检查的异常必须扩展 Throwable. It sounds like you are only interested in "unchecked" throwables so you should consider classes that directly extend or are children of a class that extends Error or RuntimeException。
在 Atlas Shell 中,您可以编写以下查询来查找所有未检查的 throwables。
var supertypeEdges = Common.universe().edgesTaggedWithAny(XCSG.Supertype)
var errors = supertypeEdges.reverse(Common.typeSelect("java.lang", "Error"))
var uncheckedExceptions = supertypeEdges.reverse(Common.typeSelect("java.lang", "RuntimeException"))
show(errors.union(uncheckedExceptions))
任何可以在运行时捕获的异常(选中或未选中)都必须有相应的 "throw" 站点。虽然抛出的检查异常必须在方法签名中声明,但不需要为抛出的未经检查的异常声明。然而,这并不是那么重要,因为我们可以简单地通过查看我们在步骤 1 中讨论的类型层次结构来检测所有抛出的未经检查的异常。
为了将抛出站点与相应的 catch 块相匹配,我们必须记住,抛出的异常会传播回调用堆栈,直到它被捕获(或者在未被 main 捕获时使程序崩溃)方法或线程入口点)。要进行此分析,您需要一个调用图(调用图越精确,您在这里的分析就越准确)。对于每个未经检查的异常类型的抛出,沿着调用图向后退回到可能抛出未经检查的异常的方法的调用点。检查调用点是否包含在 try 块中(或者如果您正在分析字节码,则有陷阱区域)。如果是,您必须检查 catch blocks/trap 区域的兼容性并确定是否会捕获异常。如果未捕获到异常,则重复该过程,沿着调用图向后步进到每个调用点,直到捕获到异常或没有可能的捕获块。
使用我之前分享的 ThrowableAnalysis 代码,您可以将它们放在一起以找到每个未捕获的抛出的未检查的可抛出类型。
public class Analysis {
// execute show(Analysis.run()) on the Atlas shell
public static Q run(){
Q supertypeEdges = Common.universe().edgesTaggedWithAny(XCSG.Supertype);
Q errors = supertypeEdges.reverse(Common.typeSelect("java.lang", "Error"));
Q uncheckedExceptions = supertypeEdges.reverse(Common.typeSelect("java.lang", "RuntimeException"));
Q typeOfEdges = Common.universe().edgesTaggedWithAny(XCSG.TypeOf);
Q thrownUncheckedThrowables = typeOfEdges.predecessors(errors.union(uncheckedExceptions)).nodesTaggedWithAny(XCSG.ThrownValue);
AtlasSet<Node> uncaughtThrownUncheckedThrowables = new AtlasHashSet<Node>();
for(Node thrownUncheckedThrowable : thrownUncheckedThrowables.eval().nodes()){
if(ThrowableAnalysis.findCatchForThrows(Common.toQ(thrownUncheckedThrowable)).eval().nodes().isEmpty()){
uncaughtThrownUncheckedThrowables.add(thrownUncheckedThrowable);
}
}
Q uncaughtThrownUncheckedThrowableMethods = Common.toQ(uncaughtThrownUncheckedThrowables).containers().nodesTaggedWithAny(XCSG.Method);
Q callEdges = Common.universe().edgesTaggedWithAny(XCSG.Call);
Q rootMethods = callEdges.reverse(uncaughtThrownUncheckedThrowableMethods).roots();
Q callChainToUncaughtThrowables = callEdges.between(rootMethods, uncaughtThrownUncheckedThrowableMethods);
return callChainToUncaughtThrowables.union(Common.toQ(uncaughtThrownUncheckedThrowables));
}
}
这是运行此代码在以下测试用例中的结果截图。
public class Test {
public static void main(String[] args) {
foo();
}
private static void foo(){
throw new Pig("Pigs can fly!");
}
public static class Pig extends RuntimeException {
public Pig(String message){
super(message);
}
}
}
重要提示:您必须考虑是否在这里进行整个程序分析。如果您只分析您的代码而不是完整的 JDK(几百万行代码),那么您将只能检测到源自您的应用程序内部的未捕获的运行时异常。例如,您不会捕获 "test".substring(0,10),它会在 JDK 中的字符串 class 中声明的子字符串方法中抛出越界异常。虽然 Atlas 支持使用 Java 源代码或字节码进行部分或整个程序分析,并且可以扩展到完整的 JDK,但如果您需要分配大约一个小时的预处理时间和 20 GB 的内存计划包括完整的 JDK.
有一个 Intellij 插件可以帮助您发现未经检查的异常。
您可以在搜索 include/exclude 个库时自定义搜索过程。
根据 Java 规范,Java 编译器会根据 "throw" 语句和方法签名自动验证是否捕获了所有已检查的异常,并忽略未检查的异常。
但是,有时开发人员找出可以抛出哪些未经检查的异常会很有用,例如,某些第 3 方代码可能会在开发人员倾向于期望经过检查的异常的情况下抛出未经检查的异常(例如 Long.parseLong).或者开发人员可能会抛出一个未经检查的异常作为未来检查异常的占位符而忘记替换它。
在这些例子中,理论上可以找到这些未捕获的未检查异常。第一种情况Long.parseLong的签名表明它抛出NumberFormatException,而第二种情况,源代码是可用的,所以编译器知道抛出什么未经检查的异常。
我的问题是:有没有可以报告这些案例的工具?或者也许是一种让 Java 编译器暂时将未检查异常视为已检查异常的方法?这对于手动验证和修复可能导致整个线程或应用程序在运行时崩溃的潜在错误非常有用。
编辑:经过一些回答,我必须强调我的目标不是找到系统中可能出现的未检查异常的详尽列表,而是由于未检查异常导致的潜在错误.我认为归结为两种情况:
- 一个方法的签名表明它抛出一个未经检查的异常,而调用者没有捕获它
- 一个方法的主体抛出显式未经检查的异常,而调用者没有捕获它们
无法获取可能的未检查异常列表。由于它们没有在任何地方声明(它们是动态创建的),所以如果没有非常具体的代码分析器工具是不可能的——即使那样它也可能无法从编译库中捕获一些 类.
对于预测棘手事情的例子,考虑任何分配内存的东西都可能抛出内存不足异常,你甚至可以反射性地实例化一个异常,这几乎不可能用任何静态分析工具找到。
如果你真的很偏执,你可以捕获 RuntimeException
,那应该会得到你想要处理的所有未经检查的异常——这不是推荐的策略,但可以防止你的程序因某些原因而失败 unknown/unseen 未来的错误。
在许多情况下无法找到未经检查的异常,或者您可能会得到一长串可能发生的异常。
什么时候无法找到未经检查的异常?假设您要调用接口的方法。一种实现可能会抛出某些未经检查的异常,而其他实现则不会。编译器无法知道,因为它只在 运行 时间知道。
为什么您最终会列出一长串可能的例外情况?好吧,如果您提供未明确检查的 null
参数,几乎每个方法都可能抛出 NullPointerException
。如果选中它们,则可能是 IllegalArgumentException
。此外,此方法调用的每个方法也可能抛出不同的未经检查的异常,这些异常必须添加到列表中。您可以随时 运行 到 ClassNotFoundError
或 OutOfMemoryError
,这也必须添加到该列表中...
想像一个简单的方法
public Foo bar(String input) {
return new Foo(input.charAt(0));
}
单独使用该方法至少可以抛出 NullPointerException 或一些 OutOfBounds 异常。
要注意的是:为了查看 "all" 潜在的异常,您的工具必须检查 每个和任何 代码行(编译的或源代码)进入您的应用程序。
我不明白这怎么可能 "manageable"。
而且情况变得更糟,如果
public Foo(char whatever) {
String klazz = ... god knows
throw (RuntimeException) Class.forName(klazz).newInstance();
当然,这将需要一些 try/catch 用于反射代码具有的已检查异常;但要点是:了解潜在异常的整个宇宙可能最终会出现在为您绘制的 "pretty huge map" 中。里面有这么多 paths/branches,你永远找不到 0.001% 的有趣路径。
My question is: is there a tool that can report these cases?
据我所知,没有。
Or maybe a way to let the Java compiler treat temporarily unchecked exceptions are checked exceptions?
据我所知,没有。
虽然像这样的工具在理论上是可行的(有一些注意事项1),但它们在实践中几乎毫无用处。如果您仅依赖方法的本地分析,大多数普通的 Java 将被标记为可能抛出范围广泛的未经检查的异常。其中一些可以通过一些简单的非本地分析排除,但这还不足以避免过度 "false positives".
IMO,没有消除所有运行时异常的实用方法。
您应该做的是结合以下实践,以减少进入生产代码的错误数量(包括意外的运行时异常)
全面而有条理的测试;例如通过开发自动化单元和系统测试套件,并使用覆盖工具来帮助您识别尚未测试的代码路径。
使用 PMD 和 FindBugs 等静态分析工具可以检测某些 类 问题。您可以使用
@NotNull
. 等注释来帮助这些工具和类似工具
代码审查。
遵循良好的编码习惯,尤其是在开发多线程代码时。
但请注意,这些做法代价高昂,而且它们并不能消除所有错误。
其他一些答案似乎建议您捕获所有异常(例如 Exception
、RuntimeException
甚至 Throwable
)作为避免崩溃的一种方式。
这是误导。是的,您可以 "catch them all"(这称为 Pokemon 异常处理!)但是您无法从任意意外异常中安全地恢复。通常,当您遇到意外异常时,唯一完全安全的做法就是退出。
1 - 这些注意事项是:1) 分析器需要知道 Java 语言结构可能会隐式抛出未经检查的异常;例如实例方法调用可以抛出 NPE,new
可以抛出 OOOME,等等。2) 您需要分析代码使用的所有库方法,包括第 3 方库,3) Java 异常可能从本机代码中抛出,并且 4) 需要考虑涉及静态和 "bytecode engineering" 的事情。
是的,您可以编写静态分析来执行此操作。我自己也做过类似的事情,并在名为 Atlas. Here: https://github.com/EnSoftCorp/java-toolbox-commons/.../ThrowableAnalysis.java 的程序分析工具中编写了我的代码,这些代码可能对您的需要有帮助,它静态计算了一个软件中投掷站点和潜在捕获站点的匹配项(保守,因为它不考虑路径可行性)。对于您的情况,您对没有相应 catch 块的 throw 站点感兴趣。
以下是分析的重要部分。
- 所有检查或未检查的异常必须扩展 Throwable. It sounds like you are only interested in "unchecked" throwables so you should consider classes that directly extend or are children of a class that extends Error or RuntimeException。
在 Atlas Shell 中,您可以编写以下查询来查找所有未检查的 throwables。
var supertypeEdges = Common.universe().edgesTaggedWithAny(XCSG.Supertype)
var errors = supertypeEdges.reverse(Common.typeSelect("java.lang", "Error"))
var uncheckedExceptions = supertypeEdges.reverse(Common.typeSelect("java.lang", "RuntimeException"))
show(errors.union(uncheckedExceptions))
任何可以在运行时捕获的异常(选中或未选中)都必须有相应的 "throw" 站点。虽然抛出的检查异常必须在方法签名中声明,但不需要为抛出的未经检查的异常声明。然而,这并不是那么重要,因为我们可以简单地通过查看我们在步骤 1 中讨论的类型层次结构来检测所有抛出的未经检查的异常。
为了将抛出站点与相应的 catch 块相匹配,我们必须记住,抛出的异常会传播回调用堆栈,直到它被捕获(或者在未被 main 捕获时使程序崩溃)方法或线程入口点)。要进行此分析,您需要一个调用图(调用图越精确,您在这里的分析就越准确)。对于每个未经检查的异常类型的抛出,沿着调用图向后退回到可能抛出未经检查的异常的方法的调用点。检查调用点是否包含在 try 块中(或者如果您正在分析字节码,则有陷阱区域)。如果是,您必须检查 catch blocks/trap 区域的兼容性并确定是否会捕获异常。如果未捕获到异常,则重复该过程,沿着调用图向后步进到每个调用点,直到捕获到异常或没有可能的捕获块。
使用我之前分享的 ThrowableAnalysis 代码,您可以将它们放在一起以找到每个未捕获的抛出的未检查的可抛出类型。
public class Analysis {
// execute show(Analysis.run()) on the Atlas shell
public static Q run(){
Q supertypeEdges = Common.universe().edgesTaggedWithAny(XCSG.Supertype);
Q errors = supertypeEdges.reverse(Common.typeSelect("java.lang", "Error"));
Q uncheckedExceptions = supertypeEdges.reverse(Common.typeSelect("java.lang", "RuntimeException"));
Q typeOfEdges = Common.universe().edgesTaggedWithAny(XCSG.TypeOf);
Q thrownUncheckedThrowables = typeOfEdges.predecessors(errors.union(uncheckedExceptions)).nodesTaggedWithAny(XCSG.ThrownValue);
AtlasSet<Node> uncaughtThrownUncheckedThrowables = new AtlasHashSet<Node>();
for(Node thrownUncheckedThrowable : thrownUncheckedThrowables.eval().nodes()){
if(ThrowableAnalysis.findCatchForThrows(Common.toQ(thrownUncheckedThrowable)).eval().nodes().isEmpty()){
uncaughtThrownUncheckedThrowables.add(thrownUncheckedThrowable);
}
}
Q uncaughtThrownUncheckedThrowableMethods = Common.toQ(uncaughtThrownUncheckedThrowables).containers().nodesTaggedWithAny(XCSG.Method);
Q callEdges = Common.universe().edgesTaggedWithAny(XCSG.Call);
Q rootMethods = callEdges.reverse(uncaughtThrownUncheckedThrowableMethods).roots();
Q callChainToUncaughtThrowables = callEdges.between(rootMethods, uncaughtThrownUncheckedThrowableMethods);
return callChainToUncaughtThrowables.union(Common.toQ(uncaughtThrownUncheckedThrowables));
}
}
这是运行此代码在以下测试用例中的结果截图。
public class Test {
public static void main(String[] args) {
foo();
}
private static void foo(){
throw new Pig("Pigs can fly!");
}
public static class Pig extends RuntimeException {
public Pig(String message){
super(message);
}
}
}
重要提示:您必须考虑是否在这里进行整个程序分析。如果您只分析您的代码而不是完整的 JDK(几百万行代码),那么您将只能检测到源自您的应用程序内部的未捕获的运行时异常。例如,您不会捕获 "test".substring(0,10),它会在 JDK 中的字符串 class 中声明的子字符串方法中抛出越界异常。虽然 Atlas 支持使用 Java 源代码或字节码进行部分或整个程序分析,并且可以扩展到完整的 JDK,但如果您需要分配大约一个小时的预处理时间和 20 GB 的内存计划包括完整的 JDK.
有一个 Intellij 插件可以帮助您发现未经检查的异常。 您可以在搜索 include/exclude 个库时自定义搜索过程。