TinyDBF-用200行的DBF解析器来展示良好架构设计

由于工作关系,需要工作当中,需要读取DBF文件,找了一些DBF读取开源软件,要么是太过庞大,动不动就上万行,要么是功能有问题,编码,长度,总之是没有找到一个非常爽的。在万般无奈之下,我老人家怒从心头起,恶向胆边生,决定自己写一下。结果只用了不到300行代码就搞定了,当然搞定不是唯一目标,还要优雅简洁的搞定,亲们跟随我的脚步一起感受一下简洁的设计与实现吧。 
在开始编码之前,先介绍一下DBF,这个DBF可是个老东西,在DOS时代就已经出现,并且风骚了相当一段时间,后来随着大型数据库的应用,它逐步没落,但是由于其简洁易用的特点,还是应用在大量的数据交换当中。但是其发展过程中,也形成了许多种版本,不同版本的结构不一样,也就决定 了其解析程序也是不一样的。 
今天我只实现了Foxbase/DBaseIII的解析,但是也为扩展各种其它版本做好了准备。 
接口设计 

083756_dzpL_1245989.jpg (37.32 KB, 下载次数: 0)

下载附件

6 天前 上传

上面一共就两个类,一个接口,Field和Header就是两个简单的POJO类,分别定义了文件头及字段相关的信息。 
Reader接口是DBF文件读取的接口,主要定义了获取文件类型,编码,字段以及记录移动相关的方法。 
代码实现 首先实现Reader的抽象类

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public abstract class DbfReader implements Reader {
    protected String encode = "GBK";
    private FileChannel fileChannel;
    protected Header header;
    protected List<Field> fields;
    private boolean recordRemoved;
    int position = 0;
    static Map<Integer, Class> readerMap = new HashMap<Integer, Class>();

static {
        addReader(3, FoxproDBase3Reader.class);
    }

public static void addReader(int type, Class clazz) {
        readerMap.put(type, clazz);
    }

public static void addReader(int type, String className) throws ClassNotFoundException {
        readerMap.put(type, Class.forName(className));
    }

public byte getType() {
        return 3;
    }

public String getEncode() {
        return encode;
    }

public Header getHeader() {
        return header;
    }

public List<Field> getFields() {
        return fields;
    }

public boolean isRecordRemoved() {
        return recordRemoved;
    }

public static Reader parse(String dbfFile, String encode) throws IOException, IllegalAccessException, InstantiationException {
        return parse(new File(dbfFile), encode);
    }

public static Reader parse(String dbfFile) throws IOException, IllegalAccessException, InstantiationException {
        return parse(new File(dbfFile), "GBK");
    }

public static Reader parse(File dbfFile) throws IOException, IllegalAccessException, InstantiationException {
        return parse(dbfFile, "GBK");
    }

public static Reader parse(File dbfFile, String encode) throws IOException, IllegalAccessException, InstantiationException {
        RandomAccessFile aFile = new RandomAccessFile(dbfFile, "r");
        FileChannel fileChannel = aFile.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1);
        fileChannel.read(byteBuffer);
        byte type = byteBuffer.array()[0];
        Class<Reader> readerClass = readerMap.get((int) type);
        if (readerClass == null) {
            fileChannel.close();
            throw new IOException("不支持的文件类型[" + type + "]。");
        }
        DbfReader reader = (DbfReader) readerClass.newInstance();
        reader.setFileChannel(fileChannel);
        reader.readHeader();
        reader.readFields();
        return reader;
    }

public void setFileChannel(FileChannel fileChannel) {
        this.fileChannel = fileChannel;
    }

protected abstract void readFields() throws IOException;

public void moveBeforeFirst() throws IOException {
        position = 0;
        fileChannel.position(header.getHeaderLength());
    }

/**
     * @param position 从1开始
     * @throws java.io.IOException
     */
    public void absolute(int position) throws IOException {
        checkPosition(position);
        this.position = position;
        fileChannel.position(header.getHeaderLength() + (position - 1) * header.getRecordLength());
    }

private void checkPosition(int position) throws IOException {
        if (position >= header.getRecordCount()) {
            throw new IOException("期望记录行数为" + (this.position + 1) + ",超过实际记录行数:" + header.getRecordCount() + "。");
        }
    }

protected abstract Field readField() throws IOException;

protected abstract void readHeader() throws IOException;

private void skipHeaderTerminator() throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1);
        readByteBuffer(byteBuffer);
    }

public void close() throws IOException {
        fileChannel.close();
    }

public void next() throws IOException {
        checkPosition(position);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1);
        readByteBuffer(byteBuffer);
        this.recordRemoved = (byteBuffer.array()[0] == ‘*‘);
        for (Field field : fields) {
            read(field);
        }
        position++;
    }

public boolean hasNext() {
        return position < header.getRecordCount();
    }

private void read(Field field) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(field.getLength());
        readByteBuffer(buffer);
        field.setStringValue(new String(buffer.array(), encode).trim());
        field.setBuffer(buffer);
    }

protected void readByteBuffer(ByteBuffer byteBuffer) throws IOException {
        fileChannel.read(byteBuffer);
    }
}

这个类是最大的一个类,值得注意的是几个静态方法:  addReader和parse, addReader用于增加新的类型的Reader,parse用于解析文件。 
parse的执行过程是首先读取第一个字节,判断是否有对应的解析实现类,如果有,就有对应的解析实现类去解析,如果没有,则抛出错误声明不支持。 
下面写实现类就简单了,下面是FoxproDBase3的解析器:

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
public class FoxproDBase3Reader extends DbfReader {
    protected void readFields() throws IOException {
        fields = new ArrayList<Field>();
        for (int i = 0; i < (header.getHeaderLength() - 32 - 1) / 32; i++) {
            fields.add(readField());
        }
    }

public byte getType() {
        return 3;
    }

protected Field readField() throws IOException {
        Field field = new Field();
        ByteBuffer byteBuffer = ByteBuffer.allocate(32);
        readByteBuffer(byteBuffer);
        byte[] bytes = byteBuffer.array();
        field.setName(new String(bytes, 0, 11, encode).trim().split("\0")[0]);
        field.setType((char) bytes[11]);
        field.setDisplacement(Util.getUnsignedInt(bytes, 12, 4));
        field.setLength(Util.getUnsignedInt(bytes, 16, 1));
        field.setDecimal(Util.getUnsignedInt(bytes, 17, 1));
        field.setFlag(bytes[18]);
        return field;
    }

protected void readHeader() throws IOException {
        header = new Header();
        ByteBuffer byteBuffer = ByteBuffer.allocate(31);
        readByteBuffer(byteBuffer);
        byte[] bytes = byteBuffer.array();
        header.setLastUpdate((Util.getUnsignedInt(bytes, 0, 1) + 1900) * 10000 + Util.getUnsignedInt(bytes, 1, 1) * 100 + Util.getUnsignedInt(bytes, 2, 1));
        header.setRecordCount(Util.getUnsignedInt(bytes, 3, 4));
        header.setHeaderLength(Util.getUnsignedInt(bytes, 7, 2));
        header.setRecordLength(Util.getUnsignedInt(bytes, 9, 2));
    }
}

测试用例 

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
public class DbfReaderTest {
    static String[] files = {"BESTIMATE20140401", "BHDQUOTE20140401"};

public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
        for (String file : files) {
            printFile(file);
        }
    }

public static void printFile(String fileName) throws IOException, InstantiationException, IllegalAccessException {
        Reader dbfReader = DbfReader.parse("E:\\20140401\\" + fileName + ".DBF");
        for (Field field : dbfReader.getFields()) {
            System.out.printf("name:%s %s(%d,%d)\n", field.getName(), field.getType(), field.getLength(), field.getDecimal());
        }
        System.out.println();
        for (int i = 0; i < dbfReader.getHeader().getRecordCount(); i++) {
            dbfReader.next();
            for (Field field : dbfReader.getFields()) {
                System.out.printf("%" + field.getLength() + "s", field.getStringValue());
            }
            System.out.println();
        }
        dbfReader.close();

}
}

可以看到最后的使用也是非常简洁的。 
代码统计 

085959_EzQV_1245989.jpg (15.06 KB, 下载次数: 0)

下载附件

6 天前 上传

总共的代码行数是282行,去掉import和接口声明之类的,真正干活的代码大概就200行了: 
总结 上面不仅展示了如何实现DBF文件的解析,同时还展示了如何在现在面临的需求与未来的扩展进行合理均衡的设计方式。 
比如:要实现另外一个标准的DBF文件支持,只要类似上面FoxproDBase3Reader类一样,简单实现之后,再调用DbfParser.addReader(xxxReader); 
好的设计需要即避免过度设计,搞得太复杂,同时也要对未来的变化与扩展做适当考虑,避免新的需求来的时候需要这里动动,那里改改导致结构上的调整与变化,同时要注意遵守DRY原则,可以这样说如果程序中有必要的大量的重复,就说明一定存在结构设计上的问题。 
所有的代码都可以在下面的连接看到: 
https://git.oschina.net/tinyframework/tiny/tree/master/framework/org.tinygroup.dbf/src/main/java/org/tinygroup/dbf

时间: 2024-07-30 01:09:57

TinyDBF-用200行的DBF解析器来展示良好架构设计的相关文章

《自己动手写框架2》:用200行的DBF解析器来展示良好架构设计

由于工作关系,需要工作当中,需要读取DBF文件,找了一些DBF读取开源软件,要么是太过庞大,动不动就上万行,要么是功能有问题,编码,长度,总之是没有找到一个非常爽的.在万般无奈之下,我老人家怒从心头起,恶向胆边生,决定自己写一下.结果只用了不到300行代码就搞定了,当然搞定不是唯一目标,还要优雅简洁的搞定,亲们跟随我的脚步一起感受一下简洁的设计与实现吧. 在开始编码之前,先介绍一下DBF,这个DBF可是个老东西,在DOS时代就已经出现,并且风骚了相当一段时间,后来随着大型数据库的应用,它逐步没落

Atitit。Tree文件解析器的原理流程与设计实现&#160;&#160;java&#160;&#160;c#&#160;php&#160;js

Atitit.Tree文件解析器的原理流程与设计实现  java  c# php js 1. 解析原理与流程1 1.1. 判断目录  ,表示服  dirFlagChar = "└├─";1 1.2. 剑豪制表符出现的位置与文件夹级别对应表1 1.3. 主要判读流程2 2. Tree结果2 3. Code----3 4. 结果5 1. 解析原理与流程 1.1. 判断目录  ,表示服  dirFlagChar = "└├─"; 其中-类似于剑豪的制表符是表示目录的..够

2本Hadoop技术内幕电子书百度网盘下载:深入理解MapReduce架构设计与实现原理、深入解析Hadoop Common和HDFS架构设计与实现原理

这是我收集的两本关于Hadoop的书,高清PDF版,在此和大家分享: 1.<Hadoop技术内幕:深入理解MapReduce架构设计与实现原理>董西成 著  机械工业出版社2013年5月出版 2.<Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理>蔡斌.陈湘萍 著  机械工业出版社2013年4月出版 百度网盘下载地址: http://pan.baidu.com/s/1sjNmkFj

解析大型.NET ERP系统架构设计 Framework+ Application 设计模式

我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应该具备良好的可扩展性和可维护性,系统中的功能紧密关联.除去业务上的复杂性,如何设计这样的一个协作良好的系统,搭建开发人员基础平台,一直是我研究的方向. SouceCounter(版本3.3.91.79)对源代码的统计信息如下: 下面来详细解析一下这个系统的设计架构,纯.NET技术架构方案,C/S WinForms系统. 系统分为Framework和Application两个部分,前者是框架(

Java端ACM输入解析器(高效)

最近注册了一个codeforeces的帐号,想在那上面刷刷题目.但是发现它用的是控制台输入数据.因此为了能够接收到这些数据,就动手写了一个Acm的输入解析器. 先说说这个解析器的缺点: 这个解析器只能用于ascii码的输入,而由于acm一般为了照顾像c和c++这样的语言,都会选择使用ascii码作为输入,因此能完美符合这一限制. 代码比较长,总共有300行代码. 使用需要一定的缓存空间(约256字节) 再说说这个解析器的优点: 速度较Scanner快不少. 使用简单. 功能强大.(对于ACM来说

深入基础(二)练习题,REPL交互解析器

NPM     关于npm命令其实不算很多很多,起码比dos命令少不少呢废话少说npm命令大全and各个命令用处持续更新中..来自园子内另外一位大神~:http://www.cnblogs.com/PeunZhang/p/5553574.html 简单说一下需要用到的命令,npm install <Module name>  添加模块,一看就知道 npm install 模块名称 没难度无压力.还有一点需要注意一下啊,当你install的时候 默认是node安装目录的,也就是你的本地目录loc

parseConf(配置文件解析器)

1 /****************************************************************************** 2 * 3 * parseConf(配置文件解析器) 4 * 5 * 1. 很多时候,我们安装一些软件,都可以通过改一些软件的配置文件来修改程序的 6 * 运行性能,如Tomcat修改端口号,访问数据库时一些固定的参数等等; 7 * 2. 本Demo就是干着这么一件事,从properties.conf文件中取出键值对(keyvalue

Java 最常见 200+ 面试题全解析:面试必备

本文分为十九个模块,分别是: Java 基础.容器.多线程.反射.对象拷贝.Java Web .异常.网络.设计模式.Spring/Spring MVC.Spring Boot/Spring Cloud.Hibernate.MyBatis.RabbitMQ.Kafka.Zookeeper.MySQL.Redis.JVM ,如下图所示: Java 基础 1. JDK 和 JRE 有什么区别? JDK:Java Development Kit 的简称,Java 开发工具包,提供了 Java 的开发环

DjangoRestFramework学习一之restful规范、APIview、解析器组件、Postman等

DjangoRestFramework学习一之restful规范.APIview.解析器组件.Postman等 本节目录 [TOC] 一 预备知识 预备知识:django的CBV和FBV CBV(class based view):多用,简单回顾一下 FBV(function based view): CBV模式的简单操作:来个登陆页面吧 login.html文件内容如下: <!DOCTYPE html> <html lang="en"> <head>