如何为大日志文件编写Java文本文件查看器

时间:2010-05-20 12:59:10

标签: java design-patterns scalability

我正在开发一个带有集成日志文件查看器的软件产品。问题是,它对于非常大的文件来说是缓慢且不稳定的,因为它在您查看日志文件时将整个文件读入内存。我想写一个新的日志文件查看器来解决这个问题。

为大型文本文件编写查看器的最佳做法是什么?像notepad ++和VIM这样的编辑器如何实现这一目标?我正在考虑使用缓冲的双向文本流阅读器和Java的TableModel。我是否按照正确的思路思考,是否有适用于Java的流实现?

编辑:是否值得运行一次文件来索引每行文本开头的位置,以便知道在哪里寻找?我可能需要大量的行,所以可能需要至少扫描一次文件?

Edit2:我已将我的实现添加到下面的答案中。请对其进行评论或编辑,以帮助我/我们达到更好的实践实施或以其他方式提供您自己的实施。

3 个答案:

答案 0 :(得分:4)

我不确定NotePad ++实际上是否实现了随机访问,但我认为这是可行的方法,特别是对于日志文件查看器,这意味着它将是只读的。

由于您的日志查看器是只读的,因此您可以使用只读random access内存映射文件“stream”。在Java中,这是FileChannel

然后根据需要在文件中跳转,并在屏幕上呈现数据的滚动窗口。

FileChannel的一个优点是并发线程可以打开文件,并且读取不会影响当前文件指针。因此,如果您在另一个线程中附加到日志文件,它将不会受到影响。

另一个优点是你可以随时调用FileChannel的size方法来获取文件大小。

将内存直接映射到随机访问文件(一些文本编辑器允许的文件(例如HxD和UltraEdit))的问题是任何更改都会直接影响文件。因此,更改是立即的(写入缓存除外),这是用户通常不想要的。相反,用户通常不希望在他们单击“保存”之前进行更改。但是,由于这只是一个观众,因此您没有同样的顾虑。

答案 1 :(得分:2)

一种典型的方法是使用可搜索文件阅读器,通过日志记录一行线偏移索引,然后根据要求在文件的一部分上只显示一个窗口。

这样可以减少快速调用所需的数据,也不会加载其中99%的内容当前不可见的小部件。

答案 2 :(得分:0)

我发布了我的测试实施(遵循Marcus Adams和msw的建议),为了您的方便以及进一步的评论和批评。它很快。

我没有受到Unicode编码安全的困扰。我想这将是我的下一个问题。任何提示非常受欢迎。

class LogFileTableModel implements TableModel {

    private final File f;
    private final int lineCount;
    private final String errMsg;
    private final Long[] index;
    private final ByteBuffer linebuf = ByteBuffer.allocate(1024);
    private FileChannel chan;

    public LogFileTableModel(String filename) {
        f = new File(filename);
        String m;
        int l = 1;
        Long[] idx = new Long[] {};
        try {
            FileInputStream in = new FileInputStream(f);
            chan = in.getChannel();
            m = null;
            idx = buildLineIndex();
            l = idx.length;
        } catch (IOException e) {
            m = e.getMessage();
        }
        errMsg = m;
        lineCount = l;
        index = idx;
    }

    private Long[] buildLineIndex() throws IOException {
        List<Long> idx = new LinkedList<Long>();
        idx.add(0L);

        ByteBuffer buf = ByteBuffer.allocate(8 * 1024);
        long offset = 0;
        while (chan.read(buf) != -1) {
            int len = buf.position();
            buf.rewind();            
            int pos = 0;
            byte[] bufA = buf.array();
            while (pos < len) {
                byte c = bufA[pos++];
                if (c == '\n')
                    idx.add(offset + pos);
            }
            offset = chan.position();
        }
        System.out.println("Done Building index");
        return idx.toArray(new Long[] {});
    }

    @Override
    public int getColumnCount() {
        return 2;
    }

    @Override
    public int getRowCount() {
        return lineCount;
    }

    @Override
    public String getColumnName(int columnIndex) {
        switch (columnIndex) {
        case 0:
            return "#";
        case 1:
            return "Name";
        }
        return "";
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        switch (columnIndex) {
            case 0:                
                return String.format("%3d", rowIndex);
            case 1:
                if (errMsg != null)
                    return errMsg;
                try { 
                    Long pos = index[rowIndex];
                    chan.position(pos);
                    chan.read(linebuf);
                    linebuf.rewind();
                    if (rowIndex == lineCount - 1)
                        return new String(linebuf.array());
                    else    
                        return new String(linebuf.array(), 0, (int)(long)(index[rowIndex+1]-pos));
                } catch (Exception e) {
                    return "Error: "+ e.getMessage();
                }
        }            
        return "a";
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        return String.class;
    }

    // ... other methods to make interface complete


}