java—将方法标记为返回现有closeable,以避免在使用站点发出虚假警告

dfuffjeb  于 2021-06-29  发布在  Java
关注(0)|答案(1)|浏览(262)

给一个班 MyClass 使用内部可关闭对象, myCloseable 并提供了一种方法 getCloseable() 它返回它;如果为这种可关闭资源警告配置eclipse,那么每次有人调用它时,它都会系统地发出警告 getCloseable() ,表示可关闭的“在此位置可能无法关闭”。
有这样的关于应该关闭的资源的警告是很好的,以防忘记关闭它们,所以我喜欢保持这个检查处于启用状态。但在刚才描述的场景中,这是相当烦人的。原则上,似乎可以标记 getCloseable() 方法,比如, @existingCloseable ,告诉编译器“没关系,我只是返回一个已经存在的资源,而不是创建一个新的资源,因此调用者不应该关闭它”。
eclipse或其他ide是否考虑采用这种注解?我找不到关于它的讨论,所以我怀疑它没有。我想知道为什么:在我的例子中描述的模式对我来说似乎非常常见和自然。我考虑的带注解的解决方案是不是行不通,或者有我没有想到的缺点?

示例

下面是一个(愚蠢的)例子,其中closeable是一个 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();
    }
}

讨论

我已经编辑了我的问题,它最初问我的代码是否可以修改以避免警告:我意识到这不是我真正感兴趣的问题,我对我考虑的基于注解的解决方案相当感兴趣-对此感到抱歉。
我意识到这个例子很愚蠢:没有进一步的上下文,而且正如几个正确的答案所指出的那样,看起来我应该 Package 流的所有方法,并且永远不要将流返回到外部世界。不幸的是,很难使这个例子变得现实,同时也保持它的小。
然而,有一个例子是众所周知的,所以我不需要在这里给出详细的形式:在servlet中,可以调用 getOutputStream() ,这是一个典型的例子来说明我的观点:方法 getOutputStream() 返回一个调用方不需要关闭的流,但是每次调用它时(即在每个servlet中),eclipse都会警告潜在的资源泄漏,这是一个难题。同样清楚的是,为什么这个方法的概念没有简单地 Package 所有的东西,而不是返回流本身:获得一个实现这个众所周知的接口的对象是很有用的,因为这样就可以将它与其他希望与流交互的库和方法一起使用。
作为我观点的另一个例证,想象一下 getCloseable() 方法仅在内部使用,因此该方法是包可见的,而不是公共的。实施 getCloseable() 可能很复杂,例如继承扮演了一个角色,因此不一定能够像我的小示例中那样用对底层字段的简单访问来替换这个调用。在这种情况下,实现一个完整的 Package 器而不是返回一个已经存在的接口,这感觉就像一点都不亲,只是为了让编译器高兴。

ffscu2ro

ffscu2ro1#

潜在的资源泄漏问题

这里是一个潜在的资源泄漏警告,默认情况下是禁用的,而不是一个常规的资源泄漏警告,默认情况下是启用的。与资源泄漏警告不同,如果不创建 AutoCloseable 你自己,但从某处获取它,不要关闭它,也不要通过调用 close() 也不隐式地使用try with resource。
在您的案例中,资源是否从某个地方关闭,可能会针对简单案例进行计算,但不是针对所有案例。这和停顿问题是同一个问题。例如,创建 AutoCloseable 或者它的结束可能被委托到某个地方,在那里它被再次委托,以此类推,如果有 if ,一个 switch ,或循环,则必须遵循所有分支才能确定。
即使在你简单的例子里,也不是那么简单。通过将只有一个构造函数设为私有,可以防止类被扩展,否则可以重写 close() 不打电话 super.close() 造成 target.close() 从来没人打过电话。但是自从 private OutputStream target; 不是 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");
        // …
    }
}

因此,如果包含内部 AutoCloseable 或者 final 或者 privat 和有效的final(仅在初始化时设置)和 AutoCloseable 将在 close() 外部的 AutoCloseable .
总而言之,如果您不是自己生成资源而是从某处获取资源,则无法保证编译器能够在有限的时间内计算它是否将被关闭(请参见停止问题)。对于这些情况,除了资源泄漏警告之外,还存在潜在的资源泄漏警告,该警告可以单独停用,并且默认情况下处于停用状态。

基于注解的解决方案?

这个 @SuppressWarnings 注解用于抑制注解元素中的命名编译器警告。但在本例中,需要一个注解来告诉编译器 AutoCloseable 不需要关闭,类似于 @SafeVarargs 注解或空注解。据我所知,这样的注解还不存在,至少在系统库中不存在。为了使用这样的注解,首先必须将包含此注解的依赖项添加到项目中(与空注解的情况完全相同,遗憾的是空注解仍然不是系统库的一部分,可能是因为 javac 与eclipse编译器和其他IDE和工具相比,当前不支持基于注解的空值分析。
对于以下情况,还需要一个解决方案,其中由输入参数确定是否需要关闭返回值(可能通过另一个注解):

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

因此,首先,这样的注解必须存在(最好不是特定于eclipse的,并且作为系统库的一部分),以便eclipse能够支持它们。

给定示例的解决方案

作为解决方案,不要暴露 target 例如,将其 Package 并从 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);
        }
    }

}

相关问题