[代码阅读]数据库源代码阅读练习

概要:

         本文主要对基于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.   解压缩源代码后,可以看到如下的代码目录:

[代码阅读]数据库源代码阅读练习_第1张图片

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窗口:

[代码阅读]数据库源代码阅读练习_第2张图片

可以在命令行端口输入下面的查询语句:

         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交流。

参考文献:

《代码阅读方法与实践》


你可能感兴趣的:(C语言,代码阅读)