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

时间:2016-11-10 12:23:41

标签: java exception exception-handling static-analysis software-quality

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

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

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

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

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

  1. 方法的签名表示它会抛出未经检查的异常,并且调用方无法捕获它
  2. 一个方法的主体明确地抛出未经检查的异常,并且调用者没有抓住它们

6 个答案:

答案 0 :(得分:5)

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

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

答案 1 :(得分:4)

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

以下是分析的重要部分。

  1. 选中或取消选中所有例外情况必须延长Throwable。听起来你只对#34;未经检查"感兴趣throwables所以你应该考虑直接扩展的类或者是扩展ErrorRuntimeException的类的子类。
  2. Throwable Hierarchy

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

    3. 使用我之前分享过的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);
              }
          }
      
      }
      

      Analysis Result Screenshot

      重要提示:您必须考虑是否在此处执行整个​​程序分析。如果您只分析代码而不是完整的JDK(几百万行代码),那么您将只检测源自应用程序内部的未捕获的运行时异常。例如,你不会捕获&#34; test&#34; .substring(0,10),这会在JDK的String类中声明的substring方法中抛出一个out of bounds异常。虽然Atlas支持使用Java源代码或字节码进行部分或整个程序分析,并且可以扩展到完整的JDK,但如果您计划包含完整的JDK,则需要分配大约一小时的预处理时间和20 GB的内存。 / p>

答案 2 :(得分:2)

  

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

AFAIK,没有。

  

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

AFAIK,没有。

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

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

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

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

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

  • 代码审核。

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

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

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

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

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

答案 3 :(得分:0)

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

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

如果你真的很偏执,你可以抓住RuntimeException,这应该得到你想要处理的所有未经检查的例外 - 这不是推荐的策略,但可以防止你的程序因某些未知/看不见而失败未来的错误。

答案 4 :(得分:0)

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

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

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

答案 5 :(得分:0)

图像像

这样的简单方法
Serialization of 'Closure' is not allowed

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

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

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

它变得更糟,如果

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

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