繁体   English   中英

如何在Java中找到潜在的未经检查的异常?

[英]How to find potential unchecked exceptions in Java?

根据Java规范,Java编译器根据“throw”语句和方法签名自动验证是否捕获了所有已检查的异常,并忽略未经检查的异常。

但是,有时开发人员可以找出可以抛出哪些未经检查的异常,例如某些第三方代码可能会在开发人员倾向于期望检查异常(如Long.parseLong)的情况下抛出未经检查的异常。 或者开发人员可能会将未经检查的异常作为未来检查异常的占位符而忘记替换它。

在这些示例中,理论上可以找到这些未被捕获的未经检查的异常。 在第一种情况下,Long.parseLong的签名表示它抛出NumberFormatException,在第二种情况下,源代码可用,因此编译器知道抛出了哪些未经检查的异常。

我的问题是:是否有可以报告这些案例的工具? 或者也许让Java编译器处理暂时未经检查的异常的方法是检查异常? 这对于手动验证并修复可能导致整个线程或应用程序在运行时崩溃的潜在错误非常有用。

编辑 :在得到一些答案之后,我必须强调我的目标不是找到系统中可能存在的未经检查的异常的详尽列表,而是由于未经检查的异常导致的潜在错误。 我认为归结为两种情况:

  1. 方法的签名表示它抛出一个未经检查的异常,并且调用者没有捕获它
  2. 方法的主体显式抛出未经检查的异常,并且调用者不会捕获它们

有一个Intellij插件可以帮助您发现未经检查的异常。 您可以自定义搜索过程以在搜索时包含/排除库。

https://plugins.jetbrains.com/plugin/8157?pr=

是的,您可以编写静态分析来执行此操作。 我自己做了类似的事情,并在一个名为Atlas的程序分析工具中写道。 这里:https://github.com/EnSoftCorp/java-toolbox-commons/.../ThrowableAnalysis.java是一些可能对你需要的东西有帮助的代码,它静态地计算投掷站点和潜在捕获站点的匹配一块软件(保守地说它不考虑路径可行性)。 对于您的情况,您感兴趣的是没有相应catch块的throw站点。

以下是分析的重要部分。

  1. 检查或取消选中所有异常必须扩展Throwable 听起来你只对“unchecked”throwable感兴趣,所以你应该考虑直接扩展的类,或者是扩展ErrorRuntimeException的类的子类。

可抛出的层次结构

在Atlas Shell中,您可以编写以下查询以查找所有未经检查的throwable。

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))
  1. 可以在运行时捕获的任何异常(已选中或未选中)必须具有相应的“throw”站点。 虽然必须在方法签名中声明抛出的已检查异常,但不需要为抛出的未经检查的异常声明它。 然而,这并不是那么重要,因为我们可以通过查看步骤1中讨论的类型层次结构来检测所有抛出的未经检查的异常。

  2. 为了将throw站点与相应的catch块匹配,我们必须记住抛出的异常会传回备份调用栈,直到它被捕获(或者当它没有被主方法或线程入口点捕获时崩溃程序)。 要进行此分析,您需要一个调用图(调用图越准确,您的分析就越准确)。 对于未经检查的异常类型的每次抛出,沿着调用图向后退步到可能抛出未经检查的异常的方法的调用点。 检查调用点是否包含在try块中(或者如果要分析字节码,则具有陷阱区域)。 如果是,则必须检查catch块/陷阱区域的兼容性,并确定是否将捕获异常。 如果未捕获异常,则重复该过程沿着调用图向后步进到每个调用点,直到捕获到异常或没有可能的catch块。

使用我之前分享的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的String类中声明的substring方法中抛出一个out of bounds异常。 虽然Atlas支持使用Java源代码或字节码进行部分或整个程序分析,并且可以扩展到完整的JDK,但如果计划包含完整的JDK,则需要分配大约一小时的预处理时间和20 GB的内存。

我的问题是:是否有可以报告这些案例的工具?

AFAIK,没有。

或者也许让Java编译器处理暂时未经检查的异常的方法是检查异常?

AFAIK,没有。

虽然这样的工具在理论上是可行的(有一些注意事项1 ),但在实践中它们将毫无用处。 如果您完全依赖于本地方法分析,那么大多数普通Java都会被标记为可能会抛出大量未经检查的异常。 其中一些可以通过一些简单的非局部分析排除,但这远远不足以避免过多的“误报”。


IMO,没有消除所有运行时异常的实用方法。

您应该做的是结合以下实践,以减少使其成为生产代码的错误数(包括意外的运行时异常)

  • 彻底和有条理的测试; 例如,通过开发自动化单元和系统测试套件,并使用覆盖工具来帮助您识别尚未测试的代码路径。

  • 使用可以检测某些类问题的静态分析工具,如PMD和FindBugs。 您可以使用@NotNull注释来帮助这些和类似的工具。

  • 代码评论。

  • 遵循良好的编码实践,特别是在开发多线程代码时。

但请注意,这些做法很昂贵,并没有消除所有错误。


其他一些答案似乎暗示您应该捕获所有异常(例如ExceptionRuntimeException甚至Throwable )作为避免崩溃的方法。

这是错误的。 是的,您可以“全部捕获它们”(这称为Pokemon异常处理!)但您无法安全地从任意意外异常中恢复。 一般来说,当你遇到意想不到的异常时唯一完全安全的事情就是拯救。


1 - 这些警告是:1)分析器需要知道可以隐式抛出未经检查的异常的Java语言结构; 例如,实例方法调用可以抛出一个NPE,一个new可以抛出一个OOOME等.2)你需要分析你的代码使用的所有库方法,包括第三方库,3)Java异常可能会从本机代码抛出,并且4)需要考虑涉及静态和“字节码工程”的事情。

无法获取可能未经检查的异常列表。 由于它们没有在任何地方声明(它们是动态创建的),如果没有一个非常具体的代码分析工具,它就不可能 - 即使这样它也可能无法从编译的库类中捕获一些。

对于要预测的棘手事情的例子,考虑分配内存的任何东西都会引发内存不足异常,你甚至可以反射性地实例化一个异常,这几乎不可能用任何静态分析工具找到。

如果你真的很偏执,你可以捕获RuntimeException ,这应该得到你想要处理的所有未经检查的异常 - 这不是一个推荐的策略,但可以防止你的程序从一些未知/看不见的未来bug中失败。

在许多情况下,无法找到未经检查的异常,或者最终可能会出现很长的可能发生的异常列表。

什么时候无法找到未经检查的例外? 假设您要调用接口的方法。 一个实现可能会抛出某些未经检查的异常,而其他实现则不会。 编译器无法知道,因为它只在运行时才知道。

为什么你最终会得到一长串可能的例外? 好吧,几乎每个方法都可能抛出NullPointerException ,以防你提供未明确检查的null参数。 如果选中它们,则可能存在IllegalArgumentException 此外,此方法调用的每个方法也可能会抛出不同的未经检查的异常,这些异常必须添加到列表中。 您可能随时遇到ClassNotFoundErrorOutOfMemoryError ,这也必须添加到该列表中......

想像一个简单的方法

public Foo bar(String input) {
  return new Foo(input.charAt(0));
}

单独该方法可能至少抛出一个NullPointerException或一些OutOfBounds的东西。

问题是:为了查看“所有”潜在的异常,您的工具必​​须检查进入您的应用程序的每个代码行(编译或源代码)。

我不知道这怎么可能是“可管理的”。

它变得更糟,如果

public Foo(char whatever) {
  String klazz = ... god knows 
  throw (RuntimeException) Class.forName(klazz).newInstance();

当然,对于反射代码具有的已检查异常,需要一些try / catch; 但重点是:了解整个宇宙的潜在异常可能最终会出现在为您绘制的一些“非常巨大的地图”中。 有这么多的路径/分支,你永远不会找到0.001%的有趣路径。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM