如何让Java的文件加载支持有BOM的文件?

2 分钟阅读

Java的FileInputStream提供最基本的文件加载功能。不支持BOM头,直接使用会错误的将无意义的BOM信息当做正文内容加载进来。

本文旨在找到通用的方式处理BOM头,并以UTF系列编码为例。需要扩展支持GB 18030等其他字符编码规范定义的特殊BOM,可以自行扩展。

有很多开源库和工具支持UTF系列编码。例如:Apache Commons-io。但是不支持GB 18030或者其他编码的BOM,这也是为什么写本文重要原因之一。

1
2
3
4
5
6
7
8
try (FileInputStream fis = new FileInputStream(file);
        BomInputStream bis = new BomInputStream(fis);
        BufferedReader reader = new BufferedReader(new InputStreamReader(bis, bis.getBomCharset()))) {
    reader.lines()
            .forEach(line -> /*...*/);
} catch (Exception e) {
    //    handle exception.
}

在BOM_PREFIX_ARRAY中定义你所需的BOM头格式。

详细的BomInputStream实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public final class BomInputStream extends PushbackInputStream {

    private static final int MAX_BOM_SIZE = 4;

    private static final ByteOrderMark[] BOM_PREFIX_ARRAY = new ByteOrderMark[]{
            new ByteOrderMark(Charset.forName("UTF-32BE"), (byte) 0x00, (byte) 0x00, (byte) 0xFE, (byte) 0xFF),
            new ByteOrderMark(Charset.forName("UTF-32LE"), (byte) 0xFF, (byte) 0xFE, (byte) 0x00, (byte) 0x00),
            new ByteOrderMark(StandardCharsets.UTF_8, (byte) 0xEF, (byte) 0xBB, (byte) 0xBF),
            new ByteOrderMark(StandardCharsets.UTF_16BE, (byte) 0xFE, (byte) 0xFF),
            new ByteOrderMark(StandardCharsets.UTF_16LE, (byte) 0xFF, (byte) 0xFE)
    };

    private final Charset charset;

    public BomInputStream(InputStream in) throws IOException {
        super(in, MAX_BOM_SIZE);
        this.charset = this.checkAndSkipBom(in);
    }

    Charset checkAndSkipBom(InputStream in) throws IOException {
        // if file without BOM mark, unread all bytes
        Charset encoding = StandardCharsets.UTF_8;
        int n, unread;
        byte[] bom = new byte[MAX_BOM_SIZE];
        unread = n = in.read(bom, 0, bom.length);
        //
        for (ByteOrderMark mark : BOM_PREFIX_ARRAY) {
            boolean match = true;
            for (int i = 0; i < mark.bytes.length; i++) {
                if (mark.bytes[i] != bom[i]) {
                    match = false;
                    break;
                }
            }
            if (match) {
                encoding = mark.charset;
                unread = n - mark.bytes.length;
                break;
            }
        }
        if (unread > 0)
            this.unread(bom, (n - unread), unread);
        return encoding;
    }

    public Charset getBomCharset() {
        return this.charset;
    }

    static class ByteOrderMark {

        private final Charset charset;
        private final byte[] bytes;

        public ByteOrderMark(Charset charset, byte... bytes) {
            this.charset = charset;
            this.bytes = bytes;
        }
    }
}

snakeyaml

假如你的项目中使用Spring Framework,那么使用yaml内置的UnicodeReader是一个不错的选择。支持UTF-16和UTF-8,不支持UTF-32的BOM头。

很多类库会提供UnicodeReader,

UnicodeReader核心方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
protected void init() throws IOException {
    if (this.internalIn2 == null) {
        byte[] bom = new byte[3];
        int n = this.internalIn.read(bom, 0, bom.length);
        Charset encoding;
        int unread;
        if (bom[0] == -17 && bom[1] == -69 && bom[2] == -65) {
            encoding = UTF8;
            unread = n - 3;
        } else if (bom[0] == -2 && bom[1] == -1) {
            encoding = UTF16BE;
            unread = n - 2;
        } else if (bom[0] == -1 && bom[1] == -2) {
            encoding = UTF16LE;
            unread = n - 2;
        } else {
            encoding = UTF8;
            unread = n;
        }

        if (unread > 0) {
            this.internalIn.unread(bom, n - unread, unread);
        }

        CharsetDecoder decoder = encoding.newDecoder().onUnmappableCharacter(CodingErrorAction.REPORT);
        this.internalIn2 = new InputStreamReader(this.internalIn, decoder);
    }
}

Apache Commons-io

使用BOMInputStream替代。核心加载方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int read(byte[] buf, int off, int len) throws IOException {
    int firstCount = 0;
    int b = 0;

    while(len > 0 && b >= 0) {
        b = this.readFirstBytes();
        if (b >= 0) {
            buf[off++] = (byte)(b & 255);
            --len;
            ++firstCount;
        }
    }

    int secondCount = this.in.read(buf, off, len);
    return secondCount < 0 ? (firstCount > 0 ? firstCount : -1) : firstCount + secondCount;
}

知识共享许可协议

本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 TinyZ Zzh (包含链接: https://tinyzzh.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请 与我联系 (tinyzzh815@gmail.com)

TinyZ Zzh

TinyZ Zzh

专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。

评论

  点击开始评论...