如何使用Java读取一个正在被写入的文件?

109

我有一个应用程序,它会将信息写入文件。这些信息在执行后用于确定应用程序的通过/失败/正确性。我希望能够在写入文件时读取文件,以便我可以实时进行这些通过/失败/正确性检查。

我认为这是可能的,但使用Java时会有什么问题?如果读取赶上了写入,它会一直等待更多的写入直到文件关闭,还是在此时读取将抛出异常?如果是后者,那我该怎么办?

我的直觉目前正在推动我使用BufferedStreams。这是正确的方法吗?


1
嗨,由于我面临类似的情况,想问一下你是否找到了比已接受的解决方案更好的解决方法? - Asaf David
我知道这是一个老问题,但为了未来的读者,请问您能否更详细地阐述您的用例?如果没有更多信息,人们会想知道您是否正在解决错误的问题。 - user359996
请考虑使用Apache Commons IO中的Tailer。它可以处理大多数边缘情况。 - Joshua
3
使用数据库。这些“在文件被写入时读取文件”的情况最终都会以泪水收场。 - user207421
@EJP - 你推荐哪个数据库?我猜MySQL是一个不错的开始? - Caffeinated
9个回答

46

使用FileChannel.read(ByteBuffer)时无法使示例正常工作,因为它不是阻塞读取。但是使用下面的代码可以使其工作:

boolean running = true;
BufferedInputStream reader = new BufferedInputStream(new FileInputStream( "out.txt" ) );

public void run() {
    while( running ) {
        if( reader.available() > 0 ) {
            System.out.print( (char)reader.read() );
        }
        else {
            try {
                sleep( 500 );
            }
            catch( InterruptedException ex ) {
                running = false;
            }
        }
    }
}

当然,相同的方法也可以作为一个计时器而不是一个线程来使用,但我将这留给程序员决定。我仍在寻找更好的方法,但现在这对我来说已经足够了。

哦,还要说明一下:我正在使用1.4.2。是的,我知道我还停留在古老的年代。


1
感谢您添加这个……这是我一直没有时间做的事情。我认为Blade的锁定文件的答案也很好。但是,它需要Java 6(我想)。 - Anthony Cramp
@JosephGordon - 你总有一天要进入无人机时代的;-) - TungstenX

15

如果您想在文件被写入时读取它并只读取新内容,则可以使用以下方法实现。

要运行此程序,您需要从命令提示符/终端窗口启动它,并传递要读取的文件名。它会一直读取文件,除非您杀死该程序。

java FileReader c:\myfile.txt

当您从记事本中保存一行文本时,您将看到文本在控制台中打印出来。

public class FileReader {

    public static void main(String args[]) throws Exception {
        if(args.length>0){
            File file = new File(args[0]);
            System.out.println(file.getAbsolutePath());
            if(file.exists() && file.canRead()){
                long fileLength = file.length();
                readFile(file,0L);
                while(true){

                    if(fileLength<file.length()){
                        readFile(file,fileLength);
                        fileLength=file.length();
                    }
                }
            }
        }else{
            System.out.println("no file to read");
        }
    }

    public static void readFile(File file,Long fileLength) throws IOException {
        String line = null;

        BufferedReader in = new BufferedReader(new java.io.FileReader(file));
        in.skip(fileLength);
        while((line = in.readLine()) != null)
        {
            System.out.println(line);
        }
        in.close();
    }
}

3
在读取文件和获取文件长度之间,是否有可能存在外部进程向文件中添加更多数据的情况?如果是,那么读取过程将会错过文件中已写入的数据。 - JohnC
2
这段代码消耗大量的CPU资源,因为循环中没有包含thread.sleep调用。如果不加入一小段延迟,这段代码会让CPU保持繁忙状态。 - ChaitanyaBhatt
1
以上两条评论都是正确的,此外,在每个循环调用中创建BufferedReader是如此无意义。如果它只被创建一次,则不需要跳过,因为缓冲读取器会在新行到达时读取它们。 - Piotr

8

7
我完全同意Joshua的回答Tailer在这种情况下非常适合。以下是一个示例:

它每150毫秒在文件中写入一行,同时每2500毫秒读取这个相同的文件。

public class TailerTest
{
    public static void main(String[] args)
    {
        File f = new File("/tmp/test.txt");
        MyListener listener = new MyListener();
        Tailer.create(f, listener, 2500);

        try
        {
            FileOutputStream fos = new FileOutputStream(f);
            int i = 0;
            while (i < 200)
            {
                fos.write(("test" + ++i + "\n").getBytes());
                Thread.sleep(150);
            }
            fos.close();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    private static class MyListener extends TailerListenerAdapter
    {
        @Override
        public void handle(String line)
        {
            System.out.println(line);
        }
    }
}

你的Tailer链接已经失效。 - Stealth Rabbi
3
那么最好的做法是搜索正确的链接,然后编辑答案并加入链接。 - igracia

3
答案似乎是“不确定”……又似乎是“确定”的。似乎没有真正的方法知道文件是否被其他应用程序打开进行写入。因此,从这样的文件中读取内容只会一直进行,直到内容枯竭为止。我采纳了Mike的建议,并编写了一些测试代码:
Writer.java将字符串写入文件,然后等待用户按下回车键才写入另一行到文件。想法是可以启动它,然后启动读取器以查看它如何处理“部分”文件。我编写的读取器在Reader.java中。 Writer.java
public class Writer extends Object
{
    Writer () {

    }

    public static String[] strings = 
        {
            "Hello World", 
            "Goodbye World"
        };

    public static void main(String[] args) 
        throws java.io.IOException {

        java.io.PrintWriter pw =
            new java.io.PrintWriter(new java.io.FileOutputStream("out.txt"), true);

        for(String s : strings) {
            pw.println(s);
            System.in.read();
        }

        pw.close();
    }
}

Reader.java

public class Reader extends Object
{
    Reader () {

    }

    public static void main(String[] args) 
        throws Exception {

        java.io.FileInputStream in = new java.io.FileInputStream("out.txt");

        java.nio.channels.FileChannel fc = in.getChannel();
        java.nio.ByteBuffer bb = java.nio.ByteBuffer.allocate(10);

        while(fc.read(bb) >= 0) {
            bb.flip();
            while(bb.hasRemaining()) {
                System.out.println((char)bb.get());
            }
            bb.clear();
        }

        System.exit(0);
    }
}

不能保证这段代码是最佳实践。

还有一种方法是像Mike建议的那样定期检查文件中是否有新数据需要读取。这就需要用户在确定读取完成后关闭文件读取器。或者,读取器需要知道文件的内容,并能够确定写入结束条件。如果内容是XML,则可以使用文档结尾来发出信号。


2

如果使用FileInputStream、FileReader或RandomAccessFile打开的文件已被其他进程占用,则无法读取该文件。

但是,直接使用FileChannel可以正常工作:

private static byte[] readSharedFile(File file) throws IOException {
    byte buffer[] = new byte[(int) file.length()];
    final FileChannel fc = FileChannel.open(file.toPath(), EnumSet.of(StandardOpenOption.READ));
    final ByteBuffer dst = ByteBuffer.wrap(buffer);
    fc.read(dst);
    fc.close();
    return buffer;
}

2
有一个开源的Java图形界面工具可以完成这个操作。 https://dev59.com/O3RB5IYBdhLWcg3wpoxm#559146
public void run() {
    try {
        while (_running) {
            Thread.sleep(_updateInterval);
            long len = _file.length();
            if (len < _filePointer) {
                // Log must have been jibbled or deleted.
                this.appendMessage("Log file was reset. Restarting logging from start of file.");
                _filePointer = len;
            }
            else if (len > _filePointer) {
                // File must have had something added to it!
                RandomAccessFile raf = new RandomAccessFile(_file, "r");
                raf.seek(_filePointer);
                String line = null;
                while ((line = raf.readLine()) != null) {
                    this.appendLine(line);
                }
                _filePointer = raf.getFilePointer();
                raf.close();
            }
        }
    }
    catch (Exception e) {
        this.appendMessage("Fatal error reading log file, log tailing has stopped.");
    }
    // dispose();
}

1

虽然不是Java本身的问题,但您可能会遇到这样的情况:您已经将某些内容写入文件,但实际上它还没有被写入 - 它可能在某个缓存中,从同一文件读取可能并不能给您新的信息。

简短地说 - 使用flush()或其他相关系统调用来确保您的数据实际上已写入文件。

请注意,我不是在谈论操作系统级别的磁盘缓存 - 如果您的数据进入此处,则应在此点之后出现在read()中。 可能是语言本身缓存写入,等待缓冲区填满或文件刷新/关闭。


0

我从未尝试过,但你应该编写一个测试用例来查看在已经到达文件结尾后从流中读取是否可行,不管文件中是否有更多的数据。

你为什么不能使用管道输入/输出流?如果数据是从同一应用程序写入和读取的(如果是这样,你已经有了数据,为什么需要从文件中读取?)

否则,可能要一直读到文件结束,然后监视更改并定位到离开的位置继续... 但要注意竞争条件。


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