概要:
本文主要对基于java的小型数据库hsqldb进行功能扩展,具体是添加一个新的查询函数,涉及到对已有的数据库代码的阅读,以及代码移植。我们的目标是增强hsqldb数据库,使他本地支持一个新的SQL日期/时间函数。我们选择添加的函数式PHASEOFMOON,返回一个0~100的数字,表示给定日期的月相,0表示新月,100表示满月。
学习目标描述:
1. 体会代码阅读过程中,采用机会主义以及目标导向的态度,利用各种试探性(启发性)的捷径来定位需要注意的代码。许多情况下,遇到极难理解的代码,进入死胡同,体会采用广度优先的查找策略,从多方来克服代码阅读问题,直到找到克服它们的方法的过程;
2. Linux下面的基本查找工具find,grep的应用,文本编辑工具 vim的应用;
3. 正则表达式的学习和应用;
需要的平台和资源:
1. java jdk 1.2;http://www.oracle.com/technetwork/java/javasebusiness/downloads/java-archive-downloads-javase12-419414.html
2. hsqldb_v.1.61.zip;
http://zh.sourceforge.jp/projects/sfnet_hsqldb/downloads/hsqldb/hsqldb_1_6_1/hsqldb_v.1.61.zip/
3. 下载《代码阅读方法与实践》对应的光盘数据,里面是很多开源应用的源代码,我们对hsqldb添加的代码主要就移植自其中的某个应用程序的源代码;
本文为了方便起见,java和hsqldb等的安装运行等在windows平台下进行,而代码的阅读,查找则在linux平台下进行;读者如果都想在windows平台下进行,可以按照cywin等软件,就可以在windows平台中使用linux平台的常见工具;
操作流程:
1. 资源和平台准备:
a. 下载和安装java jkd 1.2(oracle官网可以下到旧版本的jdk,安装教程网上有很多,主要就是设置PATH,JAVA_HOME,CLASSPATH等环境变量);
b. 下载hsqldb_v.1.61.zip源代码;
c. Linux平台和window平台;
d. 解压缩源代码后,可以看到如下的代码目录:
bin: 运行数据库的特定组件;
src: 源代码,编译生成过程也很简单,只要运行build.bat文件即可;
demo: 示范脚本,需要运行哪个组件就双击哪个,比如要运行Manager,那么只需要运行”runManager.bat”脚本; 观察demo中的几个组件运行脚本,发现共同的模式:
@java -classpath%CLASSPATH%;../lib/hsqldb.jar org.hsqldb.Server
@java -classpath%classpath%;..\lib\hsqldb.jar org.hsqldb.util.DatabaseManager
@java -classpath%CLASSPATH%;../lib/hsqldb.jar org.hsqldb.WebServer
前面的文字都一样,只是最有一个名字有不同,即代表要运行的不同组件;
我们的目的是新增查询函数,而查询函数的使用可以在DatabaseManager组件中使用,双击运行包含下面内容的run.bat脚本:
@java -classpath%classpath%;..\lib\hsqldb.jar org.hsqldb.util.DatabaseManager
就会出现下面的databasemanager窗口:
可以在命令行端口输入下面的查询语句:
create table test (d date);
insertinto test values (‘2002-09-22’)
selectyear (d) from test;
上面的语句主要的功能是创建一个名字为test的数据表,并且插入一个”2002-09-22”的数据表项,并且查询test数据表中的所有的year项信息,所显示的返回的信息为”2002”,说明上面数据库工作正常;
读者有兴趣还可以多插入几个数据项,比如”2014-04-23”,采用上面的查询语句(第三句),可以得到如下的信息:
“2002,
2014.“
我们最终完成新的查询函数phaseofmoon的添加后,就可以运行下面的查询函数:
selectphaseofmoon (d) from test;
获得关于某天的月相信息,返回的值在0-100之间的,如果是满月就返回100。
e. 便于使用linux下面的grep, vim等工具,我们将hsqldb的源代码以及参考书籍所对应的源代码都放到ubuntu系统下(如果读者采用其他的linux系统也可);
1. 代码阅读与修改:
修改代码中很重要的一个步骤就是参考已经存在的代码,如果有属性,类型以及操作类似的函数就最好不过,可以照葫芦画瓢地添加新的代码。这里我们选择一个可能数据库中已经有的函数(dayofweek),然后再在源代码的根目录,查看是否有这样的函数定义,我们用grep命令:
grep–i dayofweek *.java
-i:表示忽略大小写;
*.java:表示查找的文件类型为java源文件;
运行后得到如下的查询结果:
Library.java:[…], “DAYOFWEEK”;
Library.java:“org.hsq.ldb.Library.dayofweek”, “DAYOFYEAR”,
Library.java:public static int dayofweek(java.sql.Date d){
采用vim打开Library.java文件,查找dayofweek字符串,第一个实例出现在下面的上下文中:
finalstatic String sTimeDate[] = { "CURDATE","org.hsqldb.Library.curdate", "CURTIME", "org.hsqldb.Library.curtime","DAYNAME", "org.hsqldb.Library.dayname", "DAYOFMONTH","org.hsqldb.Library.dayofmonth", "DAYOFWEEK", "org.hsqldb.Library.dayofweek","DAYOFYEAR",
从中很容易看出,数组sTimeData将SQL函数映射到对应的Java实现。继续查找,可以看到如下的dayofweek的定义:
public static int dayofweek(java.sql.Date d) { return getDateTimePart(d,Calendar.DAY_OF_WEEK); }
这段代码里面并没有涉及具体的实现,因此在深入看看getDateTimePart()的定义,采用grep查找,可以看到如下的代码:
private static int getDateTimePart(java.util.Date d, int part) { Calendar c = new GregorianCalendar(); c.setTime(d); return c.get(part); }
这里的类calendar 和GregorainCalendar值得看看,希望里面已经有关于moonphase的相关内容;经过查找JDK文档,遗憾的是并没有相关的内容,所以,我们需要自己编写代码来实现。
二、代码重用:
根据基本原则:凡是我们遇到的问题,前人早就应该遇到。我们不打算自己去编写moonphase的算法,转而去寻找是否有现有的算法可以使用。先不打算去网上查找,我们先对本地的已经收集到的源代码进行查找,通过组合find和grep命令:
find. \(-name “*.h” –o –name “*.c”\) | xargs grep –i “moon”
我们很幸运,根据输出的内容,在./netbsdsrc/games/pom/pom.c: 包含moonphase的相关内容,进一步查询代码的注释:
Thepom utility displays the current phase of the moon. Useful for selecting softwarecompletion target dates and predicting managerial behavior. “Basedon routines from ‘Practical Astronomy with Your Calculator, by Duffett-Smith.Comments give the section from the book.”
当然,我们这里可以去参考所提到的书中的算法,再用java来进行实现,但是这里我们打算直接从代码入手,pom.c源文件主要包含下面的三个子函数以及一个main函数;
void adj360(double *deg);
double dtor(doubledeg);
double potm(doubledays);
代码量不是很多,给了我们将其移植到java中的信心。其中pom函数的注释为:
/*
*potm --
* return phase of the moon
*/
意味着,phaseofmoon主要是通过potm计算得到的,但是代码中并没有注释加以说明,不知道它在做什么。因此我们转向函数的调用处:
today = potm(days)+ .5
我们再逆向阅读,查看days变量的来源:
structtimeval tp; structtimezone tzp; structtm *GMT; time_ttmpt; doubledays, today, tomorrow; intcnt; if(gettimeofday(&tp,&tzp)) err(1,"gettimeofday"); tmpt= tp.tv_sec; GMT= gmtime(&tmpt); days= (GMT->tm_yday + 1) + ((GMT->tm_hour + (GMT->tm_min / 60.0) + (GMT->tm_sec /3600.0)) / 24.0); for(cnt = EPOCH; cnt < GMT->tm_year; ++cnt) days+= isleap(cnt) ? 366 : 365;
上面包含的除以24操作以及常数365操作,这些事实强烈地表明days可能代表从EPOCH开始的天数(包括小数)。其中isleap()应该就是处理闰年问题的。因为上述代码中使用很多标准的ANSIC-C和POSIX函数,不能照搬到Java中来,我们要使用java中的方式来实现,稍后我们再处理该问题。我们再逆向查找的话,发现其他函数依赖于下面的这些参数,因此我们需要将下面的参数移植到java中去:
#define PI 3.141592654 #define EPOCH 85 #define EPSILONg 279.611371 /*solar ecliptic long at EPOCH */ #define RHOg 282.680403 /* solar ecliptic longof perigee at EPOCH */ #define ECCEN 0.01671542 /* solar orbiteccentricity */ #define lzero 18.251907 /* lunar meanlong at EPOCH */ #define Pzero 192.917585 /* lunar mean long ofperigee at EPOCH */ #define Nzero 55.204723 /* lunar meanlong of node at EPOCH */
只是将上面的每句转写为java中的格式,比如对其中的第一句转化为:
private static final double PI = 3.141592654
我们是不是对剩余的参数,都采用上述手动的方式来转化呢?如果参数成百上千呢?此处,我们可以采用vim中基于正则表达式的匹配和替换操作:
vim中的替换操作格式为:
:%s/A/B
其中”%S”命令表示替换命令,而A是原来的要被替换的内容,而B则是替换内容;
此处,用到的命令如下:
:%s/#define\s\([a-zA-Z]\+\)\s*\t*\([0-9]\+.*.*\)/privatestatic final double \1 = \2
下面逐一说明:
%s:表示命令替换符;
\s:表示匹配空格,因为这里的每个’#define’后面都有一个空格;
[a-zA-Z]:表示a-z以及A-Z中的任意一个,也即任意一个英文大小写字符;
\+:表示匹配多个前面的字符,”[a-zA-Z]\+”两者组合在一起,就表示匹配一串英文字符;
“*”:表示匹配0个或者任意个, “\s*”组合在一起,表示匹配0个或者多个空格;
“\t”:表示制表符,与”*”搭配;
“[0-9]\+”:这个组合表示匹配一串数字;
读者可能还注意到了”\(“和”\)”两者的使用,两者配对使用,主要是用于选中一对象,并且作后续使用;比如后面的”\1”就表示前面采用”\(“和”\)”选中的第一个对象:
\([a-zA-Z]\+\)
表示选中的英文字符串;对应到我们的例子中就是”PI”,”EPOCH”等字符串。
如此上面的这条语句就很好理解了。读者再不清楚,可以去查找vim的正则表达式应用。如果还不清楚,可以参考《正则表达式:必知必会》,该书可以认为是正则表达式的通用概述,有助于理解正则表达式在vim中的应用。
对于下面的三个函数,只要相应的修改成为java格式即可:
/* dtor --convert degrees to radians*/ double dtor(double deg) { return(deg* PI / 180); } /* adj360 adjust value so 0 <= deg <=360 */ void adj360(double *deg) { for(;;) if(*deg < 0) *deg+= 360; elseif (*deg > 360) *deg-= 360; else break; } double potm(double days) { doubleN, Msol, Ec, LambdaSol, l, Mm, Ev, Ac, A3, Mmprime; doubleA4, lprime, V, ldprime, D, Nm; N= 360 * days / 365.2422; /*sec 42 #3 */ adj360(&N); Msol= N + EPSILONg - RHOg; /*sec 42 #4 */ adj360(&Msol); Ec= 360 / PI * ECCEN * sin(dtor(Msol)); /*sec 42 #5 */ LambdaSol= N + Ec + EPSILONg; /* sec 42 #6 */ adj360(&LambdaSol); l= 13.1763966 * days + lzero; /*sec 61 #4 */ adj360(&l); Mm= l - (0.1114041 * days) - Pzero; /*sec 61 #5 */ adj360(&Mm); Nm= Nzero - (0.0529539 * days); /*sec 61 #6 */ adj360(&Nm); Ev= 1.2739 * sin(dtor(2*(l - LambdaSol) - Mm)); /*sec 61 #7 */ Ac= 0.1858 * sin(dtor(Msol)); /*sec 61 #8 */ A3= 0.37 * sin(dtor(Msol)); Mmprime= Mm + Ev - Ac - A3; /*sec 61 #9 */ Ec= 6.2886 * sin(dtor(Mmprime)); /*sec 61 #10 */ A4= 0.214 * sin(dtor(2 * Mmprime)); /*sec 61 #11 */ lprime= l + Ev + Ec - Ac + A4; /*sec 61 #12 */ V= 0.6583 * sin(dtor(2 * (lprime - LambdaSol))); /*sec 61 #13 */ ldprime= lprime + V; /*sec 61 #14 */ D= ldprime - LambdaSol; /*sec 63 #2 */ return(50* (1 - cos(dtor(D)))); /*sec 63 #3 */ }
下面是对应的Java格式:
private static double dtor(double deg){ return(deg * Math.PI / 180); } private static double adj360(double deg) { for(;;) if(deg< 0) deg+= 360; elseif (deg > 360 ) deg-= 360; else break; return(deg); } private static double potm(double days){ Ec= 360 / Math.PI * ECCEN*Math.sin(dtor(Msol)); //主要就是对sin函数采用java Math库 //作替换原来的sin或者还cos等; … //后面的与原来的C语言一致; }
剩下的,主要还是要回到之前的days的问题,由于C语言中的版本对于采用ANSIC和POSIX库,所以到java平台下时,我们采用重写的方式,具体由之前的分析,主要是获得天数的信息,那么我们写出大概的代码:
GregorianCalendare = new GregorianCalendar(EPOCH, Calendar.JANUARY, 1);
return potm( (d.getTime() – e.getTime())/1000.0/60.0/60.0/24.0;
注意上面的用词“大概”,因为上面的代码中许多问题没有搞清楚,比如不知道代码如何处理时区问题,以及如何在C和java方法之间一直地处理闰年问题。我们采用突进的方式,先写出个大致的版本,然后再与C版本产生的结果进行对比,看看能有什么启发,摒弃直接深入理解代码再来书写的方式(费时间和精力)。
1) C代码从/netbsdsrc/games/pom/pom.c中抽取出来的pom.c,在ubuntu中,采用g++进行编译;注意需要将头文件”tzfile.h”随着pom.c拷贝到同一个目录,并且将include<tzfile.h>修改为include “tzfile.h”;另外,还需要在增加头文件:include<time.h>;
命令:g++ pom.c
运行编译后的可执行文件a.out;
2) 参考pom.c源文件,将其转化为对应的java版本的moonphase.java;
采用命令:javac moonphase.java;
运行命令:java moonphase
为了对比两者的输出结果,我们采用C代码中的日期作为测试的日期,而pom.c中的日期就是当前的日期:2014-4-28.另外,增加语句输出potm(days)的值;
然后将2014-4-28作为java语言的测试日期,同时也增加语句输出对应的potm(days)信息,如果两者的信息是相同或者接近的,那么我们可以认为代码移植成功,两者兼容。
如果差别比较大的话,那我们再看看其中的days的值,两者有什么样的差距。实际上,我们看到两者的potm(days)的输出值的差的比较大:
C:10709.474725;
Java:有7万多;
因此,我们需要去找找C中为什么这么小的原因,阅读下面的代码:
for(cnt= EPOCH; cnt < G<T->tm_year; cnt++) days+= isleap(cnt)?366:365;
从中我们可以大致推断,tm_year大致和EPOCH相同的数量级,我们进一步查找ctime函数的手册,得出下面的tm_year注释:
inttm_year; //year – 1990;
由此,我们需要将java中的EPOCH修改为:1985(加上1900);
通过这样的修改后,我们再对比两者的输出,发现days的数值在整数方面相同,但是小数不同,这主要在于C版本中还有小时部分,而java中我们不取小时的数据;
最后还可以发现由于小时部分的差别,两者的potm(days)输出存在差别:
C: 3.828;
Java: 6.155;
我们认为这是由于小时部分带来的差别,认为是正常的误差,至此,认为从C语言到java语言的移植正常;接下来主要的问题在于:如何将书写的moonphase.java代码添加到hsqldb的代码树中去,这里我们回到参考dayofweek。
这里我们将moonPhase.java文件放到目录:G:\hsqldb\src\org\hsqldb中,并且参考dayofweek在src/org/hsqldb/Library.java中,并且参考dayofweek对原Library.java的代码也进行相应的修改,主要是在数据结构:
final static String sTimeDate[] ={
增加下面的语句:
“PHASEOFMOON”, “org.hsqldb.moonPhase.moonphase”;
然后,我们到G:\hsqldb\src\中,运行build.bat进行重新编译;
完成编译后,我们再运行databasemanager;
我们输入一些满月的测试数据,然后再用添加的查询语句进行查询,结果基本上都接近100;
如下图所示:
至此,我们的代码添加和修改工作基本上完成。最后,还要完成的工作是对文档进行更新。
这里我们还是参照dayofweek的工作,采用下面的命令:
find. –type f –print | xargs grep –li “dayofweek”
罗列出的文件有如下:
./doc/internet/hsqsyntax.html
./doc/src/org/hsqldb.Library.html
…
大概要修改的也就主要上述./doc/目录下的两个文件,具体修改参照dayofweek即可,这里从略。
注:
如果在实际操作过程中遇到问题,可以随时email交流。
参考文献:
《代码阅读方法与实践》