Java:同步标准输出和标准错误

12

我有一个奇怪的问题,如果能解决就太好了。出于调试目的(和其他一些原因),我需要将控制台Java应用程序的日志记录在标准输出上。有些内容会被写入标准输出,而一些错误信息则会被打印到标准错误流上。问题在于这两个流并不完全同步,因此打印行的顺序并不总是正确的。我猜测这是因为有许多内容被打印出来,可能导致其中一个输出的缓冲区已满,另一个输出流在刷新之前就已经打印了。

比如,我想要写出这样的东西:

syso: aaa
syso: bbb
syso: ccc
syso: ddd
syso: eee
syserr: ---

有时会打印什么

aaa
bbb
ccc
---
ddd
eee
有时候两行文字之间没有换行,看起来就像这样:
aaa
bbb
ccc---

ddd
eee
每次我在输出上打印东西时,都会使用相同的输出进行清除。
System.out.flush();
或者
System.err.flush();

如何解决这个问题? 顺便说一句,所有内容都是在Eclipse控制台中打印的。


1
你是否考虑过使用专门的日志框架,例如Log4J? - Péter Török
1
@Péter 确实,那是一个选项,但我更喜欢如果有一个简单的解决方案来解决这个问题(至少为了将来参考)。 - Ivan
6个回答

10
问题在于终端仿真器(在您的情况下是Eclipse)负责处理应用程序的标准输出和标准错误。如果不与终端仿真器通信,您永远无法确定outerr是否以正确的顺序显示。因此,我建议将所有内容都打印到err并将其重定向到文件。您仍然可以使用out进行用户交互。
不过,还有一种(非常糟糕,但严格的)解决方案:
System.out.println(...);
System.out.flush();
Thread.sleep(100);

System.err.println(...);
System.err.flush();
Thread.sleep(100);

根据你的配置,你可能需要更改睡眠时间!


为什么这个解决方案非常糟糕? - nikodaemus
3
@skia.heliou 因为延迟是完全任意的,可能会因处理器速度等因素而有所不同。 - invertedPanda

3

我知道这篇文章是古老的,但今天仍然存在问题。因此,这里提供另一个解决方案,可以修复使用 @EserAygün's 的答案所遇到的问题,但不需要您在项目中的每个写入 System.outSystem.err 的地方都进行查找和修改。

创建名为 EclipseTools 的类,并添加以下内容(包括所需的包声明和导入):

public class EclipseTools {

    private static OutputStream lastStream = null;
    private static boolean      isFixed    = false;

    private static class FixedStream extends OutputStream {

        private final OutputStream target;

        public FixedStream(OutputStream originalStream) {
            target = originalStream;
        }

        @Override
        public void write(int b) throws IOException {
            if (lastStream!=this) swap();
            target.write(b);
        }

        @Override
        public void write(byte[] b) throws IOException {
            if (lastStream!=this) swap();
            target.write(b);
        }

        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            if (lastStream!=this) swap();
            target.write(b, off, len);
        }

        private void swap() throws IOException {
            if (lastStream!=null) {
                lastStream.flush();
                try { Thread.sleep(200); } catch (InterruptedException e) {}
            }
            lastStream = this;
        }

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

    /**
     * Inserts a 200ms delay into the System.err or System.out OutputStreams
     * every time the output switches from one to the other. This prevents
     * the Eclipse console from showing the output of the two streams out of
     * order. This function only needs to be called once.
     */
    public static void fixConsole() {
        if (isFixed) return;
        isFixed = true;
        System.setErr(new PrintStream(new FixedStream(System.err)));
        System.setOut(new PrintStream(new FixedStream(System.out)));
    }
}

然后,在代码的开头调用EclipseTools.fixConsole()即可。问题解决了。
基本上,这会用一组自定义流替换两个流System.errSystem.out,这些流只是将它们的数据转发到原始流,但跟踪最后写入的流。如果被写入的流发生更改,例如System.err.something(...)后面是System.out.something(...),它会刷新最后一个流的输出,并等待200毫秒以给Eclipse控制台完成打印。
注意:200毫秒只是一个粗略的初始值。如果此代码减少了您的问题但没有完全解决,请将Thread.sleep中的延迟从200增加到更高的值,直到它起作用。或者,如果此延迟有效但影响代码的性能(如果您经常交替使用流),则可以逐渐减少它,直到开始出现错误。

3

错误已在Eclipse 2019-09中修复。现在您可以选择同步System.out和System.err(默认情况下禁用)。但是,System.err输出将失去其红色,并且也将变为黑色。 - Marcono1234
1
失去红色会使其大部分的实用性消失。 - NateS

2

java.lang.System.setErr(java.lang.System.out);

将标准输出作为错误流使用,使应用程序使用标准输出作为错误流。


2
这样我无法区分标准输出和标准错误流(它们都打印为黑色,而不是像通常那样的黑红色)。 - Ivan

0
问题出在使用Eclipse控制台上。通常情况下,std out会将字节逐个写入控制台,而std err也是如此,但是以红色显示。然而,该方法在返回之前不会等待所有字节都被写入。因此,我建议采用以下方法:
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.function.IntConsumer;

public final class Printer extends PrintStream {
    public static final Printer out = new Printer(
            e -> System.out.print((char) e));
    public static final Printer err = new Printer(
            e -> System.err.print((char) e));
    private final IntConsumer printer;
    private static final Object lock = "";

    private Printer(IntConsumer printer) {
        super(new OutputStream() {
            public void write(int b) {
                printer.accept(b);
            }
        });
        this.printer = printer;
    }

    public void print(int x) {
        synchronized (lock) {
            this.print(Integer.toString(x));
        }
    }

    public void print(boolean x) {
        synchronized (lock) {
            this.print(Boolean.toString(x));
        }
    }

    public void print(double x) {
        synchronized (lock) {
            this.print(Double.toString(x));
        }
    }

    public void print(float x) {
        synchronized (lock) {
            this.print(Float.toString(x));
        }
    }

    public void print(long x) {
        synchronized (lock) {
            this.print(Long.toString(x));
        }
    }

    public void print(char x) {
        synchronized (lock) {
            this.print(Character.toString(x));
        }
    }

    public void print(char[] x) {
        synchronized (lock) {
            StringBuffer str = new StringBuffer(x.length);
            for (char c : x) {
                str.append(c);
            }
            this.print(str);
        }
    }

    public void print(Object x) {
        synchronized (lock) {
            this.print(x.toString());
        }
    }

    public void print(String x) {
        synchronized (lock) {
            x.chars().forEach(printer);
        }
    }

    public void println(int x) {
        synchronized (lock) {
            this.print(Integer.toString(x) + "\n");
        }
    }

    public void println(boolean x) {
        synchronized (lock) {
            this.print(Boolean.toString(x) + "\n");
        }
    }

    public void println(double x) {
        synchronized (lock) {
            this.print(Double.toString(x) + "\n");
        }
    }

    public void println(float x) {
        synchronized (lock) {
            this.print(Float.toString(x) + "\n");
        }
    }

    public void println(long x) {
        this.print(Long.toString(x) + "\n");
    }

    public void println(char x) {
        synchronized (lock) {
            this.print(Character.toString(x) + "\n");
        }
    }

    public void println(char[] x) {
        synchronized (lock) {
            StringBuffer str = new StringBuffer(x.length);
            for (char c : x) {
                str.append(c);
            }
            this.print(str + "\n");
        }
    }

    public void println(Object x) {
        synchronized (lock) {
            this.print(x.toString() + "\n");
        }
    }

    public void println(String x) {
        synchronized (lock) {
            x.chars().forEach(printer);
            printer.accept('\n');
        }
    }
}

使用Printer.outPrinter.err代替System.outSystem.err。它仍然会出现相同的错误,但这种方法效果更好。

-1
public class Util

    synchronized public static void printToOut(...)
        out.print(...)

    synchronized public static void printToErr(...)
        err.print(...)

4
可以实现;即使在JVM中同步这两个流,它们在终端设备上仍然可以异步地输出。 - irreputable

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