首页 > 解决方案 > 将方法标记为返回现有的可关闭对象,以避免在使用站点出现虚假警告

问题描述

给定一个MyClass使用内部可关闭对象的类,myCloseable并提供一个getCloseable()返回它的方法;Eclipse,如果配置了这种可关闭的资源警告,每次有人调用时都会系统地发出警告getCloseable(),说可关闭的“可能不会在这个位置关闭”。

很高兴有这样的关于应该关闭的资源的警告,以防忘记关闭它们,所以我喜欢启用此检查。但是在刚才描述的场景中,这很烦人。原则上,似乎可以getCloseable()用注释标记方法,例如@existingCloseable,告诉编译器“没关系,我只是返回一个已经存在的资源,而不是创建一个新资源,因此调用者不应该关闭它”。

Eclipse 或其他 IDE 是否考虑采用此类注释?我找不到关于它的讨论,所以我怀疑它没有。我想知道为什么:我的示例中描述的模式对我来说似乎很常见和自然。我考虑的带有注释的解决方案会不起作用,还是有我没有想到的缺点?

例子

这是一个(愚蠢的)示例,其中可关闭对象是OutputStream. 该方法fromPath会产生一个关于未关闭的警告(如果没有抑制)s,我介意这一点:这个警告似乎足够了,只需要抑制一次。烦人的部分,也是我的问题的对象,是方法中出现的警告main:“潜在的资源泄漏:'目标'可能未关闭”。每次我班任何用户使用getTarget. 我想通过注释方法一劳永逸地禁用它getTarget,以便让编译器知道此方法返回调用者不是的资源应该关闭。据我所知,Eclipse 目前不支持此功能,我想知道为什么。

public class MyWriter implements AutoCloseable {
    public static void main(String[] args) throws Exception {
        try (MyWriter w = MyWriter.fromPath(Path.of("out.txt"))) {
            // …
            OutputStream target = w.getTarget();
            target.flush();
            // …
        }

    }

    @SuppressWarnings("resource")
    public static MyWriter fromPath(Path target) throws IOException {
        OutputStream s = Files.newOutputStream(target);
        return new MyWriter(s);
    }

    private OutputStream target;

    public OutputStream getTarget() {
        return target;
    }

    private MyWriter(OutputStream target) {
        this.target = target;
    }

    @Override
    public void close() throws Exception {
        target.close();
    }
}

讨论

我已经编辑了我的问题,该问题最初询问是否可以修改我的代码以避免警告:我意识到这不是我真正感兴趣的问题,我对我想到的基于注释的解决方案相当感兴趣——对此感到抱歉。

我意识到这个例子很愚蠢:没有进一步的上下文,正如几个答案正确指出的那样,看起来我宁愿包装流的所有方法,并且永远不要将流返回到外部世界。不幸的是,很难使这个例子既现实又保持小

然而,一个例子是众所周知的,所以我不需要在这里给出详细的形式:在一个 servlet 中,可以调用getOutputStream(),这是说明我的观点的一个典型案例:该方法getOutputStream()返回一个调用者执行的流不需要关闭,但 Eclipse 会在每次调用(即在每个 servlet 中)时警告潜在的资源泄漏,这很痛苦。也很清楚为什么这个方法的概念不是简单地包装所有东西而不是返回流本身:获取一个实现这个众所周知的接口的对象很有用,因为这样就可以将它与其他库和方法一起使用期望与流交互。

作为我观点的另一个说明,假设该getCloseable()方法仅在内部使用,因此该方法是包可见的,而不是公共的。的实现getCloseable()可能很复杂,例如,继承发挥了作用,因此不一定可以像我的小示例中那样用对基础字段的简单访问来替换此调用。在这种情况下,实现一个完整的包装器而不是返回一个已经存在的接口,这感觉根本不像 KISS,只是为了让编译器满意。

标签: javaeclipsesuppress-warningsautocloseable

解决方案


潜在的资源泄漏问题

你会在这里看到一个潜在的资源泄漏警告,默认情况下是禁用的,而不是默认启用的常规资源泄漏警告。与资源泄漏警告相反,如果您自己创建一个资源泄漏警告,而是从某个地方获取它并且不关闭它,则将显示潜在资源泄漏警告,既不是通过调用显式调用,也不是通过使用 try-with-resource 隐式生成。AutoCloseableclose()

资源是否在您从中获得它的地方关闭,如您的情况,可能会针对简单情况计算,但并非针对所有情况。这与停机问题相同。例如,创建AutoCloseable或关闭它可能会被委托到再次被委托的地方,依此类推,如果有一个if、一个switch或一个循环,则必须遵循所有分支才能确定。

即使在您看起来很简单的示例中,也不是那么简单。通过将唯一的一个构造函数设为私有,可以防止类可以扩展,否则可能会在close()不调用的情况下进行覆盖,super.close()从而导致target.close()永远不会被调用。但由于private OutputStream target;is not final,仍然可能存在真正的资源泄漏,如下所示:

public static void main(String[] args) throws Exception {
    try (MyWriter w = MyWriter.toPath(Path.of("out.txt"))) {
        // …
        OutputStream target = w.getTarget();
        w.target = new FileOutputStream("out2.txt");
        // …
    }
}

所以编译器必须检查除了类不能被覆盖之外,如果保存内部的字段AutoCloseablefinal或者privat有效地最终的(仅在初始化时设置)并且内部AutoCloseable将在close()外部关闭AutoCloseable

总而言之,如果您不自己生成资源而是从某个地方获取资源,则无法保证编译器可以在有限的时间内计算出它是否会被关闭(参见暂停问题)。对于这些情况,除了资源泄漏警告之外,还有潜在资源泄漏警告,可以单独停用并默认停用。

基于注释的解决方案?

@SuppressWarnings注释用于抑制注释元素中的命名编译器警告。但是在这种情况下,这里需要一个注解来告诉编译器返回的AutoCloseable不需要关闭,类似于@SafeVarargs注解或空注解。据我所知,这样的注释还不存在,至少在系统库中不存在。为了使用这样的注解,首先必须向项目添加一个包含此注解的依赖项(与空注解的情况完全相同,不幸的是它仍然不是系统库的一部分,可能是因为javac目前没有与 Eclipse 编译器和其他 IDE 和工具相比,支持基于注释的 null 分析)。

对于以下情况,还需要一个解决方案,其中由输入参数确定返回值是否需要关闭(可能通过另一个注释):

public static BufferedOutputStream toBufferedOutputStream(OutputStream outputStream) {
    return new BufferedOutputStream(outputStream);
}

所以首先,这样的注释必须存在(最好不是特定于 Eclipse 并且作为系统库的一部分),以便 Eclipse 可以支持它们。

给定示例的解决方案

作为解决方案,不要target通过例如包装和扩展来暴露OutputStream

public class MyWriter extends OutputStream {
    public static void main(String[] args) throws Exception {
        try (MyWriter w = MyWriter.of(Path.of("out.txt"))) {
            // …
            w.flush();
            // …
        }
    }

    @SuppressWarnings("resource")
    public static MyWriter of(Path target) throws IOException {
        OutputStream s = Files.newOutputStream(target);
        return new MyWriter(s);
    }

    private final OutputStream target;

    private MyWriter(OutputStream target) {
        this.target = target;
    }

    @Override
    public void close() throws IOException {
        target.close();
    }

    @Override
    public void write(int b) throws IOException {
        target.write(b);
    }
    
    @Override
    public void flush() throws IOException {
        target.flush();
    }
}

甚至更好:

public class MyWriter {
    
    public static void main(String[] args) throws Exception {
        MyWriter.writeToFile(Path.of("out.txt"), w -> {
            try {
                // …
                w.flush();
                // …
            } catch (IOException e) {
                // error handling
            }
        });
    }

    public static void writeToFile(Path target, Consumer<OutputStream> writer) throws IOException {
        try (OutputStream out = Files.newOutputStream(target);) {
           writer.accept(out);
        }
    }

}

推荐阅读