在Java中禁用System.out的访问

5
我维护一个应用程序,它充当多个单独程序的容器。这些程序有自己专用的日志记录设施,即它们记录的所有内容都会写入特殊的日志文件。
尽管如此,应用程序开发人员似乎喜欢在各个地方使用 System.out.println 和 e.printStackTrace 调用,使得在运行容器时无法保持干净的控制台。
我该如何防止这些应用程序污染 System.out 和 System.err?
实现注意事项:
- 应用程序使用 Log4j 进行日志记录; - 容器也使用控制台进行日志记录,但严格保留用于生命周期事件和问题,因此我仍然需要控制台; - 应用程序使用自定义类加载器加载,但不应用任何安全检查。
更新:
简单地重定向 System.out 并不能起到作用,因为它会重定向所有输出,所以像这样的方式会失败:
    System.setOut(new PrintStream(new OutputStream() {

        @Override
        public void write(int b) {

            throw new Error("Not on my watch you don't");

        }
    }));

    Logger logger = Logger.getLogger(Runner.class);
    logger.info("My log message");

这应该会成功。

更新2:

类似以下代码的代码加载和配置应用程序:

App app = new UrlClassLoader(...).loadClass(className)).newInstance();
app.setLogger(loggerForClass(app));

Log4j是从系统类加载器中加载的。


这并不算是一个答案,但是作为一名开发人员,我也倾向于这样做,因为检查控制台比查找日志文件更容易。所以当我提交代码时,我首先进行查找和替换:搜索 System.out (替换为 log.debug()),搜索 System.err (替换为 log.info()),搜索 printStackTrace (删除)。我甚至认为这可能已经自动化了,例如 svn(不确定如何实现,但我认为可以)。 - laura
嗯,在系统语言中,惯用语是使用 fork(),重定向子进程的标准输出和标准错误,然后再使用 exec(),但我不确定在 Java 类加载器中的等效方法是什么。你能详细说明一下你是如何加载应用程序的吗? - Ted Kaplan
容器是使用log4j还是直接打印到控制台? - Fedearne
该容器正在使用 log4j - Robert Munteanu
@thirtyseven:请查看问题描述中的更新2。 - Robert Munteanu
12个回答

12

假设你可以控制你的容器输出,你可以按照以下步骤进行操作:

import java.io.*;
public class SysOut {
    public static void main(String[] args) throws Exception {
            PrintStream pw = new PrintStream(new FileOutputStream("a.txt"));
            PrintStream realout = System.out;
            System.setOut(pw);
            System.out.println("junk");
            realout.print("useful");
}
}

$ java SysOut 
useful
$ cat a.txt 
junk

10
你可以使用System.setOut()System.setErr()stdoutstderr重定向到PrintStream实例。

6
虽然Java定义了标准的System.out和System.err,但这些可以被覆盖为您自己的流。请参见http://www.devx.com/tips/Tip/5616。基本上,您可以设置新的流,将其连接到日志记录,或者只是让数据消失。我的偏好是后者,因为它会立即使开发人员不再依赖于System.out和err,因为他们在那里写的任何内容都会消失。

**更新: 我刚刚重新阅读了您在问题中的规定,并且看到您仍然需要容器应用程序的控制台。如果您编写一个包装器来包装标准流,以便可以检查每个调用并查看它是否来自父应用程序(并传递它)或子应用程序(并阻止它),则此方法仍可能奏效。


5

使用厌恶疗法。每当提交包含不愉快结构的代码时,都会安排“检查员”进行访问。

Nice cubicle you got ere, be shame if anyfing appened to it.

哇,这个建议解决了我的完全无关的问题...谢谢!;-) - Andrzej Doyle
我还有另一个有用的模式:“奖励”,它涉及红酒、派对帽子和“合唱团”的到访。 “哈利路亚!” - djna

3

System.setOut将重定向所有输出,但您提供的PrintStream可以决定如何处理输出。因此,我相信您可以提供这样一个流,仅实际打印来自应用程序的语句。

唯一棘手的部分实际上是能够检测什么是有效调用而什么不是。一种可行但可能非常慢的方法是调用Thread.currentThread().getStackTrace()并查看谁(至少是哪个包)在调用您(如果不是有效的,则简单地返回)。我不建议这样做,因为性能损失会很大,尤其是在每次读取字节时进行此操作。

更好的想法可能是在所有有效的容器线程中设置ThreadLocal标志。然后,您可以像以下示例一样实现PrintStream:

public class ThreadValidity extends ThreadLocal<Boolean>
{
    private static final INSTANCE = new ThreadValidity();

    @Override Boolean initialValue() { return false; }
    public static ThreadValidity getInstance() { return INSTANCE; }
}

class VerifyingPrintStream extends PrintStream
{
    private boolean isValidThread()
    {
        return ThreadValidity.instance().get();
    }

    public void println(String s)
    {
        if (!isValidThread()) return;
        super.println(s);
    }

    public void println(Object o)
    {
        if (!isValidThread()) return;
        super.println(o);
    }

    // etc
}

或者,如果您能够更改容器代码中的println,则事情会变得更加容易。 您可以将所有控制台写入交给特定的工作程序; 并且让这个工作程序“窃取”System.out(将其存储在自己的字段中并直接使用它来编写输出),同时将实际的System.out设置为无操作编写器。


3
如果您有一个无界面的构建机制,比如ant等,那么您可以将CheckStyle添加到构建中,并配置checkstyle以在代码中发现System.out.println或e.printStackTrace时失败构建。
如果您没有无界面构建,我建议您建立一个,因为这意味着您可以拥有可重复、可预测的构建。

2
关键在于在重定向输出流之前配置log4j,例如:

BasicConfigurator.configure();
System.setOut(...);
System.setErr(...);

System.out.println("I fail");
Logger.getLogger(...).info("I work");

1

在替换System.out/err之前,您实际上可以获取并存储它们。

OutputStream out=System.getOut();  // I think the names are right
System.setOut(some predefined output stream, null won't work);
out.println("Hey, this still goes to the output");
System.out.println("Oh noes, this does not");

我使用了这个工具来拦截代码库中所有的System.out.println,并在每行输出前缀中加入它所属的方法名/行号。

1

将System.out和System.err流转换为特殊实现,每次写入字符时都会抛出RuntimeException("使用日志记录而不是System.out")。

如果您的容器很重要,他们很快就会明白这个想法:)

(对于额外的奖励,可以抛出OutOfMemoryException ;-))


在这种情况下,请举一个例子来区分您喜欢的输出和您不喜欢的输出。有些类允许这样做,而其他类则不允许吗? - Thorbjørn Ravn Andersen
如果您有容器的源代码,为什么不重构它以保存和使用原始流,然后再替换System.out和System.err呢? - Thorbjørn Ravn Andersen

1
我所做的是将System.out和System.err的PrintStream重定向到commons-logging作为INFO和ERROR级别的日志记录。
如果您希望某些线程能够写入控制台或者希望日志也能输出到控制台,那么这会变得更加棘手,但是这是可以实现的。

你可以使代码可重入。例如,在调用OutputStream.write时设置一个标志。如果标志未设置,则打印到日志记录器。如果已设置,则正常打印,因为它必须来自记录器。 - Peter Lawrey
注意:你必须在 finally 块中取消设置标志。;) - Peter Lawrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接