Memcache的使用和协议分析详解(php)

Memcache的使用和协议分析详解


作者:heiyeluren
博客:
http://blog.csdn.net/heiyeshuwu
时间:2006-11-12
关键字:PHP Memcache Linux 缓存



Memcache是danga.com的一个项目,最早是为 LiveJournal 服务的,目前全世界不少人使用这个缓存项目来构建自己大负载的网站,来分担数据库的压力。(关于Memcache的更多信息请Google)
Memcache官方网站:http://www.danga.com/memcached



【安装Memcache服务器端】


我目前的平台,服务器是Fedora Core 1(内核:2.4.22),客户端是Windows XP SP2,需要安装的就是服务器的Memcached的守护进程和客户端的PHP扩展php_memcache两个东西。现在我分别来讲。


服务器端主要是安装memcache服务器端,目前的最新版本是 memcached-1.2.0 。
下载:http://www.danga.com/memcached/dist/memcached-1.2.0.tar.gz
另外,Memcache用到了libevent这个库用于Socket的处理,所以还需要安装libevent,libevent的最新版本是libevent-1.2。(如果你的系统已经安装了libevent,可以不用安装)
官网:http://www.monkey.org/~provos/libevent/
下载:http://www.monkey.org/~provos/libevent-1.2.tar.gz


我分别把两个东东下载回来,放到 /tmp 目录下:
# cd /tmp
# wget
http://www.danga.com/memcached/dist/memcached-1.2.0.tar.gz
# wget http://www.monkey.org/~provos/libevent-1.2.tar.gz


先安装libevent:
# tar zxvf libevent-1.2.tar.gz
# cd libevent-1.2
# ./configure –prefix=/usr
# make
# make install


然后看看我们的libevent是否安装成功:
# ls -al /usr/lib | grep libevent
lrwxrwxrwx    1 root     root          21 11?? 12 17:38 libevent-1.2.so.1 -> libevent-1.2.so.1.0.3
-rwxr-xr-x       1 root     root          263546 11?? 12 17:38 libevent-1.2.so.1.0.3
-rw-r–r–        1 root     root          454156 11?? 12 17:38 libevent.a
-rwxr-xr-x       1 root     root          811 11?? 12 17:38 libevent.la
lrwxrwxrwx    1 root     root          21 11?? 12 17:38 libevent.so -> libevent-1.2.so.1.0.3


还不错,都安装上了,再来安装memcache,同时需要安装中指定libevent的安装位置:


# cd /tmp
# tar zxvf memcached-1.2.0.tar.gz
# cd memcached-1.2.0
# ./configure –with-libevent=/usr
# make
# make install


如果中间出现报错,请仔细检查错误信息,按照错误信息来配置或者增加相应的库或者路径。
安装完成后会把memcached放到 /usr/local/bin/memcached ,我们看以下是否安装了:


# ls -al /usr/local/bin/mem*
-rwxr-xr-x    1 root     root       137986 11?? 12 17:39 /usr/local/bin/memcached
-rwxr-xr-x    1 root     root       140179 11?? 12 17:39 /usr/local/bin/memcached-debug


恩,安装完成了,现在我们看以下memcache的帮助:


# /usr/local/bin/memecached -h
memcached 1.2.0
-p <num>            port number to listen on
-s <file>               unix socket path to listen on (disables network support)
-l <ip_addr>        interface to listen on, default is INDRR_ANY
-d                          run as a daemon
-r                           maximize core file limit
-u <username> assume identity of <username> (only when run as root)
-m <num>          max memory to use for items in megabytes, default is 64 MB
-M                         return error on memory exhausted (rather than removing items)
-c <num>            max simultaneous connections, default is 1024
-k                          lock down all paged memory
-v                          verbose (print errors/warnings while in event loop)
-vv                        very verbose (also print client commands/reponses)
-h                         print this help and exit
-i                          print memcached and libevent license
-b                         run a managed instanced (mnemonic: buckets)
-P <file>             save PID in <file>, only used with -d option
-f <factor>          chunk size growth factor, default 1.25
-n <bytes>         minimum space allocated for key+value+flags, default 48


参数不算多,我们来启动一个Memcache的服务器端:
# /usr/local/bin/memcached -d -m 10  -u root -l 192.168.0.200 -p 12000 -c 256 -P /tmp/memcached.pid


-d选项是启动一个守护进程,-m是分配给Memcache使用的内存数量,单位是MB,我这里是10MB,-u是运行Memcache的用户,我这里是root,-l是监听的服务器IP地址,如果有多个地址的话,我这里指定了服务器的IP地址192.168.0.200,-p是设置Memcache监听的端口,我这里设置了12000,最好是1024以上的端口,-c选项是最大运行的并发连接数,默认是1024,我这里设置了256,按照你服务器的负载量来设定,-P是设置保存Memcache的pid文件,我这里是保存在 /tmp/memcached.pid,如果要结束Memcache进程,执行:
# kill `cat /tmp/memcached.pid`

也可以启动多个守护进程,不过端口不能重复。


【安装Memcache的PHP扩展】


Memcache就是在服务器监听端口,通过一定的协议交互来写入数据到服务器内存中,或者获取一些值。如果你了解Memcache的交互协议,完全可以自己构建Memcache的客户端,目前网上也有很多构建好的Memcache客户端的PHP Class,可以直接用,不过我这里为了效率,还是决定使用PECL中Memcache的专用扩展,因为毕竟是用C写的,效率比较高,而且安装部署比较方便。


下载PECL中的Memcache,因为我的客户端是Windows XP,所以需要下载dll版,我的PHP版本是PHP 5.1.4,必须下载PHP 5.1专用的扩展。
PECL官网:http://pecl.php.net (For Linux)
                       http://pecl4win.php.net(For Windows)
扩展下载: http://pecl4win.php.net/download.php/ext/5_1/5.1.2/php_memcache.dll


如果你的PHP是其他版本,请到 http://pecl4win.php.net/ext.php/php_memcache.dll 选择你相应的版本,如果是Linux下的PHP,请到 http://pecl.php.net/package/memcache 选择相应想要下载的版本。


下载完了以后,我把php_memcache.dll 拷贝到 c:\php5\ext 目录下,如果你的扩展目录是在是缺省路径,(就是没有修改过php.ini中的扩展路径) 请拷贝到 c:\windows\ 目录下,如果是Linux平台,请自己编译安装,可以在程序中使用dl()函数加载,或者在编译php的时候加载进去。最后重启Web服务器,IIS/Apache。


我的网站目录是在:d:\mysite 目录下,现在建立一个 phpinfo.php 文件在网站根目录下,代码是:
<?phpinfo()?>
看有没有成功加载 php_memcache.dll 扩展。如果显示了 Memcache 选项和相应的版本信息,则说明成功加载了,否则请仔细检查上面的步骤。


如果一切正确无误,那么说明安装成功。


 


【Memcache初试】


[ 接口介绍 ]
服务器端和客户端都安装配置好了,现在我们就来测试以下我们的成果。Memcache客户端包含两组接口,一组是面向过程的接口,一组是面向对象的接口,具体可以参考PHP手册 “LXXV. Memcache Functions” 这章。我们为了简单方便,就使用面向对象的方式,也便于维护和编写代码。Memcache面向对象的常用接口包括:


Memcache::connect — 打开一个到Memcache的连接
Memcache::pconnect — 打开一个到Memcache的长连接
Memcache::close — 关闭一个Memcache的连接
Memcache::set — 保存数据到Memcache服务器上
Memcache::get — 提取一个保存在Memcache服务器上的数据
Memcache::replace — 替换一个已经存在Memcache服务器上的项目(功能类似Memcache::set)
Memcache::delete — 从Memcache服务器上删除一个保存的项目
Memcache::flush — 刷新所有Memcache服务器上保存的项目(类似于删除所有的保存的项目)
Memcache::getStats — 获取当前Memcache服务器运行的状态


[ 测试代码 ]
现在我们开始一段测试代码:



<?php
//连接
$mem = new Memcache;
$mem->connect(192.168.0.200, 12000);

//保存数据
$mem->set(key1, This is first value, 0, 60);
$val = $mem->get(key1);
echo Get key1 value:  . $val .<br>;

//替换数据
$mem->replace(key1, This is replace value, 0, 60);
$val = $mem->get(key1);
echo Get key1 value:  . $val . <br>;

//保存数组
$arr = array(aaa, bbb, ccc, ddd);
$mem->set(key2, $arr, 0, 60);
$val2 = $mem->get(key2);
echo Get key2 value: ;
print_r($val2);
echo <br>;

//删除数据
$mem->delete(key1);
$val = $mem->get(key1);
echo Get key1 value:  . $val . <br>;

//清除所有数据
$mem->flush();
$val2 = $mem->get(key2);
echo Get key2 value: ;
print_r($val2);
echo <br>;

//关闭连接
$mem->close();
?>



如果正常的话,浏览器将输出:
Get key1 value: This is first value
Get key1 value: This is replace value
Get key2 value: Array ( [0] => aaa [1] => bbb [2] => ccc [3] => ddd )
Get key1 value:
Get key2 value:



基本说明我们的Memcache安装成功,我们再来分析以下上面的这段程序。



[ 程序分析 ]


初始化一个Memcache的对象:
$mem = new Memcache;


连接到我们的Memcache服务器端,第一个参数是服务器的IP地址,也可以是主机名,第二个参数是Memcache的开放的端口:
$mem->connect(“192.168.0.200”, 12000);


保存一个数据到Memcache服务器上,第一个参数是数据的key,用来定位一个数据,第二个参数是需要保存的数据内容,这里是一个字符串,第三个参数是一个标记,一般设置为0或者MEMCACHE_COMPRESSED就行了,第四个参数是数据的有效期,就是说数据在这个时间内是有效的,如果过去这个时间,那么会被Memcache服务器端清除掉这个数据,单位是秒,如果设置为0,则是永远有效,我们这里设置了60,就是一分钟有效时间:
$mem->set(‘key1‘, ‘This is first value’, 0, 60);


从Memcache服务器端获取一条数据,它只有一个参数,就是需要获取数据的key,我们这里是上一步设置的key1,现在获取这个数据后输出输出:
$val = $mem->get(‘key1′);
echo “Get key1 value: “ .
$val;


现在是使用replace方法来替换掉上面key1的值,replace方法的参数跟set是一样的,不过第一个参数key1是必须是要替换数据内容的key,最后输出了:
$mem->replace(‘key1’, ‘This is replace value’, 0, 60);
$val = $mem->get(‘key1’);
echo “Get key1 value: ” . $val;


同样的,Memcache也是可以保存数组的,下面是在Memcache上面保存了一个数组,然后获取回来并输出
$arr = array(‘aaa’, ‘bbb’, ‘ccc’, ‘ddd’);
$mem->set(‘key2’, $arr, 0, 60);
$val2 = $mem->get(‘key2’);
print_r($val2);


现在删除一个数据,使用delte接口,参数就是一个key,然后就能够把Memcache服务器这个key的数据删除,最后输出的时候没有结果
$mem->delete(‘key1’);
$val = $mem->get(‘key1’);
echo “Get key1 value: ” . $val .
“<br>”;


最后我们把所有的保存在Memcache服务器上的数据都清除,会发现数据都没有了,最后输出key2的数据为空,最后关闭连接
$mem->flush();
$val2 = $mem->get(‘key2’);
echo “Get key2 value: “;
print_r($val2);
echo
“<br>”;


 



【Memcache协议分析】


如果你不喜欢 php_memcache.dll 扩展或者服务器器目前不支持这个扩展,那么就可以考虑自己构建,需要构建Memcahe的客户端,要先了解Memcache协议的交互,这样才能开发自己的客户端,我这里就简单的分析以下Memcache的协议。
(更详细的协议内容请在Memcache服务器端的源码的 doc/protocol.txt 文件中,本文基本来源于此)


Memcache既支持TCP协议,也支持UDP协议,不过我们这里是以TCP协议的协议作为主要考虑对象,想了解UDP协议的过程,请参考 doc/protocol.txt 文件。


[ 错误指令]
Memcache的协议的错误部分主要是三个错误提示之提示指令:
普通错误信息,比如指令错误之类的
ERROR\r\n


客户端错误
CLIENT_ERROR <错误信息>\r\n


服务器端错误
SERVER_ERROR <错误信息>\r\n


[ 数据保存指令]
数据保存是基本的功能,就是客户端通过命令把数据返回过来,服务器端接收后进行处理。
指令格式:
<命令> <键> <标记> <有效期> <数据长度>\r\n


<命令>command name
主要是三个储存数据的三个命令, set, add, replace
set 命令是保存一个叫做key的数据到服务器上
add 命令是添加一个数据到服务器,但是服务器必须这个key是不存在的,能够保证数据不会被覆盖
replace 命令是替换一个已经存在的数据,如果数据不存在,就是类似set功能


<键>key
就是保存在服务器上唯一的一个表示符,必须是跟其他的key不冲突,否则会覆盖掉原来的数据,这个key是为了能够准确的存取一个数据项目


<标记>flag
标记是一个16位的无符号整形数据,用来设置服务器端跟客户端一些交互的操作


<有效期>expiration time
是数据在服务器上的有效期限,如果是0,则数据永远有效,单位是秒,Memcache服务器端会把一个数据的有效期设置为当前Unix时间+设置的有效时间


<数据长度>bytes
数据的长度,block data 块数据的长度,一般在这个个长度结束以后下一行跟着block data数据内容,发送完数据以后,客户端一般等待服务器端的返回,服务器端的返回:


数据保存成功
STORED\r\n


数据保存失败,一般是因为服务器端这个数据key已经存在了
NOT_STORED\r\n



[ 数据提取命令]
从服务器端提取数据主要是使用get指令,格式是:
get <键>*\r\n


<键>*key
key是是一个不为空的字符串组合,发送这个指令以后,等待服务器的返回。如果服务器端没有任何数据,则是返回:
END\r\n


证明没有不存在这个key,没有任何数据,如果存在数据,则返回指定格式:
VALUE <> <标记> <数据长度>\r\n
<数据块>\r\n


返回的数据是以VALUE开始的,后面跟着key和flags,以及数据长度,第二行跟着数据块。


<键> -key
是发送过来指令的key内容


<标记> – flags
是调用set指令保存数据时候的flags标记


<数据长度> – bytes
是保存数据时候定位的长度


<数据块> – data block
数据长度下一行就是提取的数据块内容


 


[ 数据删除指令]
数据删除指令也是比较简单的,使用get指令,格式是:
delete <键> <超时时间>\r\n


<键> – key
key是你希望在服务器上删除数据的key键


<超时时间> – timeout
按照秒为单位,这个是个可选项,如果你没有指定这个值,那么服务器上key数据将马上被删除,如果设置了这个值,那么数据将在超时时间后把数据清除,该项缺省值是0,就是马上被删除


删除数据后,服务器端会返回:
DELETED\r\n
删除数据成功
NOT_FOUND\r\n
这个key没有在服务器上找到


如果要删除所有服务器上的数据,可以使用flash_all指令,格式:
flush_all\r\n

这个指令执行后,服务器上所有缓存的数据都被删除,并且返回:
OK\r\n


这个指令一般不要轻易使,除非你却是想把所有数据都干掉,删除完以后可以无法恢复的。



[其他指令]
如果想了解当前Memcache服务器的状态和版本等信息,可以使用状态查询指令和版本查询指令。


如果想了解当前所有Memcache服务器运行的状态信息,可以使用stats指令,格式
stats\r\n
服务器将返回每行按照 STAT 开始的状态信息,包括20行,20项左右的信息,包括守护进程的pid、版本、保存的项目数量、内存占用、最大内存限制等等信息。

如果只是想获取部分项目的信息,可以指定参数,格式:
stats <参数>\r\n
这个指令将只返回指定参数的项目状态信息。


如果只是想单独了解当前版本信息,可以使用version指令,格式:
version\r\n
将返回以 VERSION 开头的版本信息


如果想结束当前连接,使用quit指令,格式:
quit\r\n


将断开当前连接


另外还有其他指令,包括incr, decr 等,我也不太了解作用,就不做介绍了,如果感兴趣,可以自己去研究。


 


【Memcache在中型网站的使用】


使用Memcache的网站一般流量都是比较大的,为了缓解数据库的压力,让Memcache作为一个缓存区域,把部分信息保存在内存中,在前端能够迅速的进行存取。那么一般的焦点就是集中在如何分担数据库压力和进行分布式,毕竟单台Memcache的内存容量的有限的。我这里简单提出我的个人看法,未经实践,权当参考。


[ 分布式应用]
Memcache本来支持分布式,我们客户端稍加改造,更好的支持。我们的key可以适当进行有规律的封装,比如以user为主的网站来说,每个用户都有User ID,那么可以按照固定的ID来进行提取和存取,比如1开头的用户保存在第一台Memcache服务器上,以2开头的用户的数据保存在第二胎Mecache服务器上,存取数据都先按照User ID来进行相应的转换和存取。


但是这个有缺点,就是需要对User ID进行判断,如果业务不一致,或者其他类型的应用,可能不是那么合适,那么可以根据自己的实际业务来进行考虑,或者去想更合适的方法。


[ 减少数据库压力]
这个算是比较重要的,所有的数据基本上都是保存在数据库当中的,每次频繁的存取数据库,导致数据库性能极具下降,无法同时服务更多的用户,比如MySQL,特别频繁的锁表,那么让Memcache来分担数据库的压力吧。我们需要一种改动比较小,并且能够不会大规模改变前端的方式来进行改变目前的架构。


我考虑的一种简单方法:
后端的数据库操作模块,把所有的Select操作提取出来(update/delete/insert不管),然后把对应的SQL进行相应的hash算法计算得出一个hash数据key(比如MD5或者SHA),然后把这个key去Memcache中查找数据,如果这个数据不存在,说明还没写入到缓存中,那么从数据库把数据提取出来,一个是数组类格式,然后把数据在set到Memcache中,key就是这个SQL的hash值,然后相应的设置一个失效时间,比如一个小时,那么一个小时中的数据都是从缓存中提取的,有效减少数据库的压力。


缺点是数据不实时,当数据做了修改以后,无法实时到前端显示,并且还有可能对内存占用比较大,毕竟每次select出来的数据数量可能比较巨大,这个是需要考虑的因素。


上面只是我两点没有经过深思熟虑的简单想法,也许有用,那就最好了。


 


【Memcache的安全】


我们上面的Memcache服务器端都是直接通过客户端连接后直接操作,没有任何的验证过程,这样如果服务器是直接暴露在互联网上的话是比较危险,轻则数据泄露被其他无关人员查看,重则服务器被入侵,因为Mecache是以root权限运行的,况且里面可能存在一些我们未知的bug或者是缓冲区溢出的情况,这些都是我们未知的,所以危险性是可以预见的。


为了安全起见,我做两点建议,能够稍微的防止黑客的入侵或者数据的泄露。


[ 内网访问]
最好把两台服务器之间的访问是内网形态的,一般是Web服务器跟Memcache服务器之间。普遍的服务器都是有两块网卡,一块指向互联网,一块指向内网,那么就让Web服务器通过内网的网卡来访问Memcache服务器,我们Memcache的服务器上启动的时候就监听内网的IP地址和端口,内网间的访问能够有效阻止其他非法的访问。


# memcached -d -m 1024  -u root -l 192.168.0.200 -p 11211 -c 1024 -P /tmp/memcached.pid


Memcache服务器端设置监听通过内网的192.168.0.200的ip的11211端口,占用1024MB内存,并且允许最大1024个并发连接



[ 设置防火墙]
防火墙是简单有效的方式,如果却是两台服务器都是挂在网的,并且需要通过外网IP来访问Memcache的话,那么可以考虑使用防火墙或者代理程序来过滤非法访问。
一般我们在Linux下可以使用iptables或者FreeBSD下的ipfw来指定一些规则防止一些非法的访问,比如我们可以设置只允许我们的Web服务器来访问我们Memcache服务器,同时阻止其他的访问。


# iptables -F
# iptables -P INPUT DROP
# iptables -A INPUT -p tcp -s 192.168.0.2 –dport 11211 -j ACCEPT
# iptables -A INPUT -p udp -s 192.168.0.2 –dport 11211 -j ACCEPT


上面的iptables规则就是只允许192.168.0.2这台Web服务器对Memcache服务器的访问,能够有效的阻止一些非法访问,相应的也可以增加一些其他的规则来加强安全性,这个可以根据自己的需要来做。


 


【Memcache的扩展性】


Memcache算是比较简洁高效的程序,Memcache 1.2.0 的源代码大小才139K,在Windows平台上是不可想象的,但是在开源世界来说,这是比较正常合理的。
Memcache目前都只是比较简单的功能,简单的数据存取功能,我个人希望如果有识之士,能够在下面两方面进行扩展。


1. 日志功能
目前Memcache没有日志功能,只有一些命令在服务器端进行回显,这样是很不利于对一个服务器的稳定性和负载等等进行监控的,最好能够相应的加上日志的等功能,便于监控。


2. 存储结构
目前的数据形式就是: key => data 的形式,特别单一,只能够存储单一的一维数据,如果能够扩展的话,变成类似数据库的格式,能够存储二维数据,那样会让可以用性更强,使用面更广,当然相应的可能代码效率和存取效率更差一些。


3. 同步功能
数据同步是个比较重要的技术,因为谁都不能保证一台服务器是持久正常的运行的,如果能够具有类似MySQL的 Master/Slave 的功能,那么将使得Memcache的数据更加稳定,那么相应的就可以考虑存储持久一点的数据,并且不用害怕Memcache的down掉,因为有同步的备份服务器,这个问题就不是问题了。


以上三点只是个人拙见,有识之士和技术高手可以考虑。


 


【结束语】


我上面的内容都只是自己安装和使用的一些想法,不能保证绝对正确,只是给需要的人一个参考,一个推广Memcache的文章,希望更多的人能够认识和了解这个技术,并且为自己所用。


我花费了整整一个晚上的时间洋洋洒洒的写了这么长,无非是对于这项开源技术的热爱,我想开源世界能够繁荣起来,就是源于大家的热爱并且愿意做出贡献,开源世界才这么精彩。


希望本文能够给需要的人一些帮助,希望不会误导他们,呵呵。

Maven入门–概念与实例


Maven入门–概念与实例
    最近由于工作原因在研究、应用Maven,有了一些体会就写成了此文。本文虽然是Maven2的入门文章,但并不涉及Maven的历史、下载与安装,这些内容可以到Maven的官方网站上了解。本文主要是关注Maven中的重要概念,并以一个实例来阐述使用Maven的基本方法。文末有例子代码下载的链接。(2007.01.02最后更新)
1 关键名词
    Project:任何您想build的事物,Maven都可以认为它们是工程。这些工程被定义为工程对象模型(POM,Poject Object Model)。一个工程可以依赖其它的工程;一个工程也可以由多个子工程构成。
    POM:POM(pom.xml)是Maven的核心文件,它是指示Maven如何工作的元数据文件,类似于Ant中的build.xml文件。POM文件位于每个工程的根目录中。
    GroupId:groupId是一个工程的在全局中唯一的标识符,一般地,它就是工程名。groupId有利于使用一个完全的包名,将一个工程从其它有类似名称的工程里区别出来。
    Artifact:artifact是工程将要产生或需要使用的文件,它可以是jar文件,源文件,二进制文件,war文件,甚至是pom文件。每个artifact都由groupId和artifactId组合的标识符唯一识别。需要被使用(依赖)的artifact都要放在仓库(见Repository)中,否则Maven无法找到(识别)它们。
    Dependency:为了能够build或运行,一个典型的Java工程会依赖其它的包。在Maven中,这些被依赖的包就被称为dependency。dependency一般是其它工程的artifact。
    Plug-in:Maven是由插件组织的,它的每一个功能都是由插件提供的。插件提供goal(类似于Ant中的target),并根据在POM中找到的元数据去完成工作。主要的Maven插件要是由Java写成的,但它也支持用Beanshell或Ant脚本写成的插件。
    Repository:仓库用于存放artifact,它可以是本地仓库,也可以是远程仓库。Maven有一个默认的远程仓库–central,可以从http://www.ibiblio.org/maven2/下载其中的artifact。在Windows平台上,本地仓库的默认地址是User_Home\.m2\repository
    Snapshot:工程中可以(也应该)有一个特殊版本,它的版本号包括SNAPSHOT字样。该版本可以告诉Maven,该工程正处于开发阶段,会经常更新(但还未发布)。当其它工程使用此类型版本的artifact时,Maven会在仓库中寻找该artifact的最新版本,并自动下载、使用该最新版。
2 Maven Build Life Cycle
    软件项目一般都有相似的开发过程:准备,编译,测试,打包和部署,Maven将上述过程称为Build Life Cycle。在Maven中,这些生命周期由一系列的短语组成,每个短语对应着一个(或多个)操作;或对应着一个(或多个)goal(类似于Ant中的target)。
    如编译源文件的命令mvn compile中的compile是一个生命周期短语。同时该命令也可以等价于mvn compiler:compile,其中的compiler是一个插件,它提供了compile(此compile与mvn compile中的compile意义不同)goal;compiler还可提供另一个goal–testCompile,该goal用于编译junit测试类。
    在执行某一个生命周期时,Maven会首先执行该生命周期之前的其它周期。如要执行compile,那么将首先执行validate,generate-source,process-source和generate-resources,最后再执行compile本身。关于Maven中默认的生命周期短语,请见参考资源[6]中的附录B.3
3 标准目录布局
    Maven为工程中的源文件,资源文件,配置文件,生成的输出和文档都制定了一个标准的目录结构。Maven鼓励使用标准目录布局,这样就不需要进行额外的配置,而且有助于各个不同工程之间的联接。当然,Maven也允许定制个性的目录布局,这就需要进行更多的配置。关于Maven的标准目录布局,请见参考资源[6]中的附录B.1
4 Maven的优点
    [1]build逻辑可以被重用。在Ant中可能需要多次重复地写相同的语句,但由于POM的继承性,可以复用其它的POM文件中的语句。这样既可以写出清晰的build语句,又可以构造出层次关系良好的build工程。
    [2]不必关注build工作的实现细节。我们只需要使用一些build生命周期短语就可以达到我们的目标,而不必管Maven是如何做到这些的。如,只需要告诉Maven要安装(install),那么它自然就会验证,编译,打包,及安装。
    [3]Maven会自动加载工程依赖的artifact所依赖的其它artifact(Transitive Dependency),而不用显示的将这些artifact全部写到dependency中。
    [4]如果完全使用Maven的标准目录布局,那么可以极大地减少配置细节。
5 实例
5.1 构想
    由于只是阐述Maven的基本使用方法,所以本文将要设计的实例,只是一个简单的Maven demo。该实例包含两个工程:普通应用程序工程(app)和Web应用工程(webapp)。app工程提供一个简单的Java类;webapp工程只包含一个Servlet,并将使用app中的Java类。
    该Demo的目标是能够正确地将webapp制成war包,以供部署时使用。要能够正确制作war,自然首先就必须要能够正确的编译源代码,且要将App模块制成jar包。本文创建的工程所在的目录是D:\maven\demo
5.2 App工程
    可以使用Maven的archetype插件来创建新工程,命令如下:
    D:\maven\demo>mvn archetype:create -DgroupId=ce.demo.mvn -DartifactId=app
该工程的groupId是ce.demo.mvn,那么该工程的源文件将放在Java包ce.demo.mvn中。artifactId是app,那么该工程根目录的名称将为app。
    当第一次执行该命令时,Maven会从central仓库中下载一些文件。这些文件包含插件archetype,以及它所依赖的其它包。该命令执行完毕后,在目录D:\maven\demo下会出现如下目录布局:

app
|– pom.xml
`– src
    |– main
    |   `– java
    |       `– ce
    |           `– demo
    |               `– mvn
    |                   `– App.java
    `– test
        `– java
            `– ce
                `– demo
                    `– mvn
                        `– AppTest.java
因本文暂时不涉及JUnit测试,故请将目录app\src\test目录删除(不删除也没关系 ^_^)。然后再修改App.java文件,其完全内容如下:

package ce.demo.mvn;
public class App 
{
    
public String getStr(String str) 
{
        
return
 str;
    }

}
其实,如果我们能够清楚地知道Maven的标准目录布局,就可以不使用archetype插件来创建工程原型;如果我们要定制个性的目录布局,那么就更没有必要使用archetype插件了。
5.3 WebApp工程
    我们仍然如创建app工程一样使用archetype插件来创建webapp工程,命令如下:
    D:\maven\demo>mvn archetype:create -DgroupId=ce.demo.mvn -DartifactId=webapp -DarchetypeArtifactId=maven-archetype-webapp
    第一次运行此命令时,也会从central仓库中下载一些与Web应用相关的artifact(如javax.servlet)。此命令与创建app的命令的不同之处是,多设置了一个属性archetypeArtifacttId,该属性的值为maven-archetype-webapp。即告诉Maven,将要创建的工程是一个Web应用工程。创建app工程时没有使用该属性值,是由于archetype默认创建的是应用程序工程。同样的,执行完该命令之后,会出现如下标准目录布局:

webapp
|– pom.xml
`– src
    `– main
        `– webapp
           
|
— index.jsp
            |– WEB-INF
                `– web.xml
    根据5.1节的构想,webapp工程将只包含一个Servlet,所以我们不需要index.jsp文件,请将其删除。此时大家可以发现,目前的目录布局中并没有放Servlet,即Java源文件的地方。根据参考资源[6]中的附录B.1,以及app工程中Java源文件的布局,可以知道Servlet(它仍然是一个Java类文件)仍然是放在webapp\src\main\java目录中,请新建该目录。此处的Servlet是一个简单HelloServlet,其完整代码如下:

package hello;

import
 java.io.IOException;
import
 java.io.PrintWriter;
import
 javax.servlet.ServletException;
import
 javax.servlet.http.HttpServlet;
import
 javax.servlet.http.HttpServletRequest;
import
 javax.servlet.http.HttpServletResponse;

import ce.demo.mvn.App;  // 引用app工程中的App类


public class HelloServlet extends HttpServlet {
    
private static final long serialVersionUID = 3696470690560528247L
;
    
public void
 doGet(HttpServletRequest request, HttpServletResponse response)
            
throws ServletException, IOException 
{
        App app 
= new
 App();
        String str 
= app.getStr(CE Maven Demo
);
        PrintWriter out 
=
 response.getWriter();
        out.print(
<html><body>
);
        out.print(
<h1> +
 str);
        out.print(
</body></html>
);
    }

}
5.4 POM文件
    大家可以发现,在前面新建工程时,我们并没有提到各个工程中的pom.xml文件。现在将要讨论这个问题。我们先看看app工程中的POM文件,其完整内容如下:

<project>
  
<modelVersion>4.0.0</modelVersion>
  
<groupId>ce.demo.mvn</groupId>
  
<artifactId>app</artifactId>
  
<packaging>jar</packaging>
  
<version>1.0</version>
  
<name>CE Maven Demo — App</name>
</project>
    大家可以发现此我帖出来的内容与实际由archetype插件生成的POM文件的内容有些不同,但基本上是一致的。只是为了使文件中的语句更清晰,此处删除了一些冗余的内容,并修改了该工程的version和name的值,以与此例子的背景来符合。在目前情况下modelVersion值将被固定为4.0.0,这也是Maven2唯一能够识别的model版本。groupId,artifactId的值与创建工程时使用的命令中的相关属性值是一致的。packaging的值由工程的类型决定,如应用程序工程的packaging值为jar,Web应用工程的packaging值为war。上述情况也可以从webapp的POM文件中看出,下面将看看这个pom的完整内容。

<project>
  
<modelVersion>4.0.0</modelVersion>
  
<groupId>ce.demo.mvn</groupId>
  
<artifactId>webapp</artifactId>
  
<packaging>war</packaging>
  
<version>1.0</version>
  
<name>CE Maven Demo — WebApp</name>
  
  
<dependencies>
      
<dependency>
          
<groupId>ce.demo.mvn</groupId>
          
<artifactId>app</artifactId>
          
<version>1.0</version>
      
</dependency>
    
<dependency>
        
<groupId>javax.servlet</groupId>
        
<artifactId>servlet-api</artifactId>
        
<version>2.4</version>
        
<scope>provided</scope>
    
</dependency> 
  
</dependencies>

</project>
    比较app与webapp中的POM,除前面已经提过的packaging的差别外,我们还可以发现webapp中的POM多了dependencies项。由于webapp需要用到app工程中的类(见HelloServlet源代码),它还需要javax.servlet包(因为该包并不默认存在于jsdk中)。故,我们必须要将它们声明到依赖关系中。
5.5 执行
    上述两个工程创建完毕后,就需要执行一些命令来看看会有什么结果出现。我们首先进入app目录,并执行命令mvn compile,然后会在该目录下发现新生成的目录target\classes,即编译后的class文件(包括它的包目录)就放在了这里。再执行命令mvn package,在目录target中就会生成app-1.0.jar文件。该文件的全名由如下形式确定:artifactId-version.packaging。根据第2章的叙述可以知道,执行命令mvn package时,将首先将产生执行命令mvn compile之后的结果,故如果要打包,那么只需要执行mvn package即可。
    在app工程中执行完之后,就需要进入webapp工程了。进入webapp目录,此次将只执行mvn package命令(隐示地执行了compile过程)。此次命令的执行并不成功,会出现如下问题:

D:\maven\demo\webapp>mvn package
……
Downloading: http://repo1.maven.org/maven2/ce/demo/mvn/app/
1.0/app-1.0
.pom
[INFO]
 ————————————————————————
[ERROR]
 BUILD ERROR
[INFO]
 ————————————————————————
[INFO]
 Error building POM (may not be this project’s POM).
Project ID: ce.demo.mvn:app
Reason: Error getting POM for ‘ce.demo.mvn:app’ from the repository: Error transferring file
  ce.demo.mvn:app:pom:
1.0

from the specified remote repositories:
  central (http://repo1.maven.org/maven2)
……
    由粗体内容可知,Maven正试图从central仓库下载app工程的artifact,但central仓库肯定不会有这个artifact,其结果只能是执行失败!由第1章artifact名词的解释可知,被依赖的artifact必须存在于仓库(远程或本地)中,但目前webapp所依赖的app必不存在于仓库中,所以执行只能失败。
    解决这个问题有两种方法:[1]将app-1.0.jar安装到仓库中,使它成为一个artifact;[2]构建一个更高层次的工程,使app和webapp成为这个工程的子工程,然后从这个更高层次工程中执行命令。
    第一种方法比较简单(http://www.blogjava.net/jiangshachina/admin/EditPosts.aspx中的第一个主题),此处将详细讨论第2种方法(见5.6节)。
5.6 更高层次工程
    我们可以将app和webapp的上一级目录demo作为这两个工程的 一个 更高层次工程,即使用app和webapp成为这个工程的子工程。为了使demo目录成为一个demo工程,只需要在这个目录下添加一个pom.xml文件,该文件内容如下:

<project>
    
<modelVersion>4.0.0</modelVersion>
    
<groupId>ce.demo</groupId>
    
<artifactId>mvn-demo</artifactId>
    
<packaging>pom</packaging>
    
<version>1.0</version>
    
<name>CE Maven Demo</name>
    
    
<modules>
        
<module>app</module>
        
<module>webapp</module>
    
</modules>
</project>
    与app和webapp中的POM相比,demo的POM使用了modules项,modules用于声明本工程的子工程,module中的值对应于子工程的artifact名。而且该POM的packaging类型必须为pom。
    有了demo工程后,我们只需要在demo目录下执行相关命令就可以了。通过如下命令即可验证:
    [1]mvn clean – 消除工程(包括所有子工程)中产生的所有输出。这本文的实例中,实际上是删除target目录。由于之前的操作只有app工程产生了target目录,而webapp并没有,所以将只会删除app工程中的target目录。
    [2]mvn package – 将工程制作成相应的包,app工程是作成jar包(app-1.0.jar),webapp工程是作成war包(webapp-1.0.war)。打开webapp-1.0.war包,可以发现app-1.0.jar被放到了WEB-INF的lib目录中。
6 小结
    通过以上的叙述与实例,应该可以对Maven有一个粗略的认识了。使用Maven关键是要弄清楚如何写pom.xml文件,就如同使用Ant要会写build.xml文件一样。在POM中可以直接写入Ant的task脚本,也可以调用Ant的build.xml文件(推荐),所以Maven也可以完成Ant的绝大多数工作(但不必安装Ant)。注意:使用Maven就不要再过多的使用Ant脚本
    利用好Maven的继承特性及子工程的关系,可以很好地简化POM文件,并能够构建层次结构良好的工程,有利于工程的维护。
7 参考资源
[1]Maven官方网站. http://maven.apache.org
[2]Maven POM文件参考结构. http://maven.apache.org/ref/current/maven-model/maven.html
[3]Super POM. http://maven.apache.org/guides/introduction/introduction-to-the-pom.html
[4]Maven主要插件的列表. http://maven.apache.org/plugins
[5]Maven基本使用指南. http://maven.apache.org/guides/index.html
[6]Better Build with Maven. http://www.mergere.com/m2book_download.jsp — 强烈推荐
[7]介绍Maven2. http://www.javaworld.com/javaworld/jw-12-2005 /jw-1205-maven_p.html
[8]揭秘Maven2 POM. http://www.javaworld.com/javaworld/jw-05-2006/jw-0529-maven.html
[9]Maven让事情变得简单. http://www-128.ibm.com/developerworks/cn/java/j-maven
[10]Maven文档集. http://docs.codehaus.org/display/MAVENUSER/Home
[11]有效利用Maven2的站点生成功能. http://www.matrix.org.cn/resource/article/44/44491_Maven2.html
文中例子程序下载:http://www.blogjava.net/files/jiangshachina/maven.rar

五款常用mysql slow log分析工具的比较

mysql slow log 是用来记录执行时间较长(超过long_query_time秒)的sql的一种日志工具.

启用 slow log


有两种启用方式:
1, 在my.cnf 里 通过 log-slow-queries[=file_name]

2, 在mysqld进程启动时,指定–log-slow-queries[=file_name]选项

比较的五款常用工具

mysqldumpslow, mysqlsla, myprofi, mysql-explain-slow-log, mysqllogfilter


mysqldumpslow, mysql官方提供的慢查询日志分析工具. 输出图表如下:



主要功能是, 统计不同慢sql的

出现次数(Count), 

执行最长时间(Time), 

累计总耗费时间(Time), 

等待锁的时间(Lock), 

发送给客户端的行总数(Rows), 

扫描的行总数(Rows), 

用户以及sql语句本身(抽象了一下格式, 比如 limit 1, 20 用 limit N,N 表示).



mysqlsla, hackmysql.com推出的一款日志分析工具(该网站还维护了 mysqlreport, mysqlidxchk 等比较实用的mysql工具)



整体来说, 功能非常强大. 数据报表,非常有利于分析慢查询的原因, 包括执行频率, 数据量, 查询消耗等.



格式说明如下:

总查询次数 (queries total), 去重后的sql数量 (unique)

输出报表的内容排序(sorted by)

最重大的慢sql统计信息, 包括 平均执行时间, 等待锁时间, 结果行的总数, 扫描的行总数.



Count, sql的执行次数及占总的slow log数量的百分比.

Time, 执行时间, 包括总时间, 平均时间, 最小, 最大时间, 时间占到总慢sql时间的百分比.

95% of Time, 去除最快和最慢的sql, 覆盖率占95%的sql的执行时间.

Lock Time, 等待锁的时间.

95% of Lock , 95%的慢sql等待锁时间.

Rows sent, 结果行统计数量, 包括平均, 最小, 最大数量.
Rows examined, 扫描的行数量.

Database, 属于哪个数据库

Users, 哪个用户,IP, 占到所有用户执行的sql百分比



Query abstract, 抽象后的sql语句

Query sample, sql语句



除了以上的输出, 官方还提供了很多定制化参数, 是一款不可多得的好工具.



mysql-explain-slow-log, 德国人写的一个perl脚本.
http://www.willamowius.de/mysql-tools.html







功能上有点瑕疵, 不仅把所有的 slow log 打印到屏幕上, 而且统计也只有数量而已. 不推荐使用.

mysql-log-filter, google code上找到的一个分析工具.提供了 python 和 php 两种可执行的脚本.
http://code.google.com/p/mysql-log-filter/


功能上比官方的mysqldumpslow, 多了查询时间的统计信息(平均,最大, 累计), 其他功能都与 mysqldumpslow类似.
特色功能除了统计信息外, 还针对输出内容做了排版和格式化, 保证整体输出的简洁. 喜欢简洁报表的朋友, 推荐使用一下.

myprofi, 纯php写的一个开源分析工具.项目在 sourceforge 上.
http://myprofi.sourceforge.net/


功能上, 列出了总的慢查询次数和类型, 去重后的sql语句, 执行次数及其占总的slow log数量的百分比.
从整体输出样式来看, 比mysql-log-filter还要简洁. 省去了很多不必要的内容. 对于只想看sql语句及执行次数的用户来说, 比较推荐.


总结








































工具/功能 一般统计信息 高级统计信息 脚本 优势
mysqldumpslow 支持 不支持 perl mysql官方自带
mysqlsla 支持 支持 perl 功能强大,数据报表齐全,定制化能力强.
mysql-explain-slow-log 支持 不支持 perl
mysql-log-filter 支持 部分支持 python or php 不失功能的前提下,保持输出简洁
myprofi 支持 不支持 php 非常精简

Spring中基于aop命名空间的AOP


本文地址:http://www.blogjava.net/cmzy/archive/2008/08/23/223870.html
下篇地址:Spring中基于aop命名空间的AOP 二(声明一个切面、切入点和通知)


    在某些时候,我们工程中使用的JDK 不一定就是1.5 以上,也就是说可能不支持Annotation 注解,这时自然也就不能使用@AspectJ 注解驱动的AOP 了,那么如果我们仍然想使用AspectJ 灵活的切入点表达式,那么该如何呢?Spring 为我们提供了基于xml schematic 的aop 命名空间,它的使用方式和@AspectJ 注解类似,不同的是配置信息从注解中转移到了Spring 配置文件中。在这里,我们将详细介绍如何使用Spring 提供的<aop:config/> 标签来配置Spring AOP 。



1 、一点准备工作和一个例子


    使用<aop:config/> 标签,需要给Spring 配置文件中引入基于xml schema 的Spring AOP 命名空间。完成后的Spring 配置文件如下(在该节,所有例程的配置文件中添加了Spring AOP 命名空间,除非特殊情况外,为了节约空间,这部分将在给出的代码中省略),粗体内容即为我们需要添加的内容:






  1. <?xml version=“1.0” encoding=“UTF-8”?>  

  2. <beans xmlns=“http://www.springframework.org/schema/beans”  

  3.         xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”  

  4.         xmlns:aop=“http://www.springframework.org/schema/aop”  

  5.         xsi:schemaLocation=”http://www.springframework.org/schema/beans   

  6.               http://www.springframework.org/schema/beans/spring-beans-2.5.xsd    

  7.               http://www.springframework.org/schema/aop    

  8.               http://www.springframework.org/schema/aop/spring-aop-2.5.xsd >  

  9. ………… Spring配置信息   

  10. </beans>  


    关于aop命名空间的标签,我们前面使用过的有<aop:aspectj-autoproxy/>,在这一节,我们将以<aop:config/>标签作为重点。事实上,我们在这一节介绍的所有标签都是该标签的子标签。



   下面有一个例程来直观的展示如何使用<aop:config/>标签来配置Spring AOP(完整代码见例程4.15)。在例子中,我们使用<aop:config/>配置一个切面并拦截目标对象Peoples的SayHello()方法,在它执行前输出提示信息。
首先创建工程AOP_Test4.15,添加Spring IoC和Spring AOP库后,创建aop.test包,新建目标类People,代码如下:






  1. package aop.test;   

  2.   

  3. /**  

  4.  * 该类将作为目标对象对应的类。  

  5.  * @author zhangyong  

  6.  * */  

  7. public class People{   

  8.   

  9.         public String SayHello(String str){   

  10.                 System.out.println(this.getClass().getName()+ “说:”+str);   

  11.                 return str;   

  12.         }   

  13. }   


    修改Spring xml配置文件,将该类注册为一个受管Bean:






  1. <bean id=“TestBean” class=“aop.test.People” />  


    创建含有main()方法的测试类TestMain,从Spring IoC容器中获取Peoples对象,并调用其SayHello()方法,代码如下:






  1. package aop.test;   

  2.   

  3. // import省略   

  4. public class TestMain {   

  5.         public static void main(String[] args) {   

  6.                 // 实例化Spring IoC容器   

  7.                 ApplicationContext ac = new ClassPathXmlApplicationContext(   

  8.                                 “applicationContext.xml”);   

  9.                 // 获取受管Bean的实例   

  10.                 People p = (People) ac.getBean(“TestBean”);   

  11.                 p.SayHello(“传入的参数值”);   

  12.         }   

  13. }   


   创建MyAspect类,添加一个beforeAdvice()方法作为前置通知方法,代码如下:






  1. package aop.test;   

  2.   

  3. import org.aspectj.lang.JoinPoint;   

  4.   

  5. public class MyAspect {   

  6.            

  7.         public void beforeAdvice(JoinPoint point) {   

  8.             System.out.println(“前置通知被触发:” +    

  9.                                 point.getTarget().getClass().getName()+    

  10.                                 “将要” + point.getSignature().getName());   

  11.         }   

  12. }   


    修改xml配置文件,为其添加aop命名空间,并把MyAspect注册为一个受管Bean,作为我们下面定义切面的backing bean。代码如下:






  1. <?xml version=“1.0” encoding=“UTF-8”?>  

  2. <beans xmlns=“http://www.springframework.org/schema/beans”  

  3.         xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”  

  4.         xmlns:aop=“http://www.springframework.org/schema/aop”  

  5.         xsi:schemaLocation=”http://www.springframework.org/schema/beans   

  6. http://www.springframework.org/schema/beans/spring-beans-2.5.xsd    

  7.                http://www.springframework.org/schema/aop    

  8. http://www.springframework.org/schema/aop/spring-aop-2.5.xsd”>  

  9.   

  10.         <bean id=“MyAspect” class=“aop.test.MyAspect” />  

  11.         <bean id=“TestBean” class=“aop.test.People” />  

  12.            

  13.         <aop:config proxy-target-class=“true”>  

  14.                 <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  15.                         <aop:pointcut id=“testPointcut”  

  16.                                 expression=“execution(* aop..*(..))” />  

  17.                         <aop:before pointcut-ref=“testPointcut”  

  18.                                 method=“beforeAdvice” />  

  19.                 </aop:aspect>  

  20.         </aop:config>  

  21. </beans>  


    运行主类,输出如下:


例程4.15输出结果


例程4.15输出结果


 


2、声明一个切面
      在基于AOP命名空间的Spring AOP中,要声明一个切面,需要使用<aop:config/>的子标签<aop:aspect>。<aop:aspect>标签有一个ref属性必须被赋值,它用于指定和该切面关联的受管Bean(backing bean,以后我们都将使用Backing Bean来称呼这样的Bean)。正如下例所示,该Bean对应的java类是一个普通的java类,在该类中定义了切面的通知方法。此外,<aop:aspect>标签还有两个可选的order属性和id属性,order属性用于指定该切面的加载顺序,id属性用于标识该切面。范例如下:





  1. <?xml version=“1.0” encoding=“UTF-8”?>  

  2. <beans ……>  

  3.         <bean id=“MyAspect” class=“aop.test.MyAspect” />  

  4.         <aop:config proxy-target-class=“true”>  

  5.                 <aop:aspect ref=“MyAspect” order=“1” id=“TestAspectName”>  

  6.                         ……切面其他配置   

  7.                 </aop:aspect>  

  8.         </aop:config>  

  9. ……其他配置   

  10. </beans>    

 



3、声明一个切入点
      要声明一个切入点,可以使用<aop:aspect>的子标签<aop:pointcut>,在Spring2.5中它有两个属性id和expression,分别用于标示该切入点和设定该切入点表达式。例如:






  1. <?xml version=“1.0” encoding=“UTF-8”?>  

  2. <beans ……>  

  3.     <bean id=“MyAspect” class=“aop.test.MyAspect”/>  

  4.     <aop:config proxy-target-class=“true”>  

  5.         <aop:aspect ref=“MyAspect” order=“1” id=”TestAspectName”>  

  6.               <aop:pointcut id=“test”  

  7.                 expression=“execution(* aop.test.TestBean.*(..))”/>  

  8.               <aop:before pointcut=“aop.test.MyAspect.Pointcut1()”    

  9.                                method=“beforeAdvice” />  

  10.         </aop:aspect>  

  11.     </aop:config>  

  12. ……其他配置   

  13. </beans>  

 




<aop:pointcut>标签的expression属性使用前面介绍的切入点表达式语言,也就是说支持AspectJ切入点表达式。但是由于xml对”&&”、”||”、”!”等逻辑运算符不友好,@AspectJ切入点表达式语言中使用的这些逻辑运算符在xml配置中需要分别用”and”、”or”和”not”来代替。
有时候,我们也需要在xml中使用@Pointcut注解声明的切入点,那么该如何呢?大家可能记得,我们可以在切入点表达式中可以引用另一个切入点。对了,就在这里,我们使用该特性可以完成这个任务,如下:






  1. <aop:pointcut id=“test”   expression=“aop.test.MyAspect.Pointcut1()” />  

 



注意:这里我们必须使用全路径来标示引用的切入点。


4、 声明一个通知
      和@AspectJ一样,基于AOP命名空间的配置也可以定义五种通知类型,并且使用方式和特性类似。与@AspectJ不同的是,配置信息从Annotation中转移到了xml配置文件。
    1)、前置通知
    声明一个前置通知可以使用<aop:aspect>的子标签<aop:before/>。该标签的属性说明如下表:

<aop:before/>标签属性说明





















属性


说明


pointcut


指定该通知的内置切入点


pointcut-ref


通过 id 引用已定义的切入点


method


指定通知对应的方法,该方法必须已在切面的 backing bean 中被声明


arg-names


通过方法的参数名字来匹配切入点参数


      对于一个通知来说,切入点和对应的通知方法是必须的。也就是说,在这些属性中,method属性是必须的,我们必须要给通知指定一个对应的方法;pointcut属性和pointcut-ref必须有一个被指定,以此确定该通知的切入点。范例如下:




  1. <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  2.     <aop:pointcut id=“testPointcut”  

  3.         expression=“execution(* aop.test.TestBean.*(..))”/>  

  4.     <aop:before pointcut-ref=“testPointcut” method=“beforeAdvice”/>  

  5. </aop:aspect>  

 



     2)、 后置通知
     声明一个后置通知使用<aop:after/>标签,它的属性等和<aop:before/>标签类似,下面是范例:




  1. <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  2.     <aop:pointcut id=“testPointcut”  

  3.         expression=“execution(* aop.test.TestBean.*(..))” />  

  4.     <aop:after  pointcut-ref=“testPointcut” method=“AfterAdvice”/>  

  5. </aop:aspect>  

 



     3)、 返回后通知
      <aop:after-returning/>标签可以声明一个返回后通知,该标签的属性和<aop:before/>相比它多了一个returning属性。该属性的意义类似于@AfterReturning注解的returning属性,用于将链接点的返回值传给通知方法。用法如下:




  1. <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  2.     <aop:pointcut id=“testPointcut”  

  3.         expression=“execution(* aop.test.TestBean.*(..))” />  

  4.     <aop:after-returning pointcut-ref=“testPointcut”  

  5.         method=“AfterReturnAdvice” returning=“reVlue” />  

  6. </aop:aspect>  

 



       4)、 异常通知
        声明一个异常通知使用<aop:after-throwing />标签,它有一个类似于throwing属性又来指定该通知匹配的异常类型。用法如下:




  1. <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  2.      <aop:pointcut id=“testPointcut”  

  3.         expression=“execution(* aop.test.TestBean.*(..))” />  

  4.      <aop:after-throwing pointcut-ref=“testPointcut”  

  5.         method=“afterThrowingAdvice” throwing=“throwable” />  

  6. </aop:aspect>  

 



      5)、 环绕通知
      环绕通知是所有通知中功能最强大的通知,用<aop:around/>标签来声明。用法如下:




  1. <aop:aspect ref=“MyAspect” order=“0” id=“Test”>  

  2.     <aop:pointcut id=“testPointcut”  

  3.         expression=“execution(* aop.test.TestBean.*(..))” />  

  4.         <aop:around pointcut-ref=“testPointcut” method=“aroundAdvice”/>  

  5. </aop:aspect>  




(完)


Eclipse热键大全~~转滴!

个人常用快捷键

【Content Assist 】:默认为【ctrl+space】与其他快捷建冲突失效,修改为【Alt+/】(原来【Alt+/】为【word completion】的默认快捷键,有了【Content Assist 】这个可以不用了。)

(1)Ctrl+M切换窗口的大小
(2)Ctrl+Q跳到最后一次的编辑处
(3)F2当鼠标放在一个标记处出现Tooltip时候按F2则把鼠标移开时Tooltip还会显示即Show Tooltip Description。
            F3跳到声明或定义的地方。
            F5单步调试进入函数内部。
            F6单步调试不进入函数内部,如果装了金山词霸2006则要把“取词开关”的快捷键改成其他的。
            F7由函数内部返回到调用处。
            F8一直执行到下一个断点。
(4)Ctrl+Pg~对于XML文件是切换代码和图示窗口
(5)Ctrl+Alt+I看Java文件中变量的相关信息
(6)Ctrl+PgUp对于代码窗口是打开“Show List”下拉框,在此下拉框里显示有最近曾打开的文件
(7)Ctrl+/ 在代码窗口中是这种//~注释。
            Ctrl+Shift+/ 在代码窗口中是这种/*~*/注释,在JSP文件窗口中是<!–~–>。
(8)Alt+Shift+O(或点击工具栏中的Toggle Mark Occurrences按钮) 当点击某个标记时可使本页面中其他地方的此标记黄色凸显,并且窗口的右边框会出现白色的方块,点击此方块会跳到此标记处。
(9)右击窗口的左边框即加断点的地方选Show Line Numbers可以加行号。
(10)Ctrl+I格式化激活的元素Format Active Elements。
              Ctrl+Shift+F格式化文件Format Document。
(11)Ctrl+S保存当前文件。
              Ctrl+Shift+S保存所有未保存的文件。
(12)Ctrl+Shift+M(先把光标放在需导入包的类名上) 作用是加Import语句。
              Ctrl+Shift+O作用是缺少的Import语句被加入,多余的Import语句被删除。
(13)Ctrl+Space提示键入内容即Content Assist,此时要将输入法中Chinese(Simplified)IME-Ime/Nonlme Toggle的快捷键(用于切换英文和其他文字)改成其他的。
              Ctrl+Shift+Space提示信息即Context Information。
(14)双击窗口的左边框可以加断点。
(15)Ctrl+D删除当前行。

Alt+↓ 当前行和下面一行交互位置(特别实用,可以省去先剪切,再粘贴了)
Alt+↑ 当前行和上面一行交互位置(同上)
Alt+← 前一个编辑的页面
Alt+→ 下一个编辑的页面(当然是针对上面那条来说了)

Alt+Enter 显示当前选择资源(工程,or 文件 or文件)的属性

Shift+Enter 在当前行的下一行插入空行(这时鼠标可以在当前行的任一位置,不一定是最后)
Shift+Ctrl+Enter 在当前行插入空行(原理同上条)

Ctrl+Q 定位到最后编辑的地方
Ctrl+L 定位在某行 (对于程序超过100的人就有福音了)
Ctrl+M 最大化当前的Edit或View (再按则反之)
Ctrl+/ 注释当前行,再按则取消注释
Ctrl+O 快速显示 OutLine
Ctrl+T 快速显示当前类的继承结构
Ctrl+W 关闭当前Editer
Ctrl+K 参照选中的Word快速定位到下一个
Ctrl+E 快速显示当前Editer的下拉列表(如果当前页面没有显示的用黑体表示)

Ctrl+/(小键盘) 折叠当前类中的所有代码

Ctrl+×(小键盘) 展开当前类中的所有代码

Ctrl+Space 代码助手完成一些代码的插入(但一般和输入法有冲突,可以修改输入法的热键,也可以暂用Alt+/来代替)

Ctrl+Shift+E 显示管理当前打开的所有的View的管理器(可以选择关闭,激活等操作)

Ctrl+J 正向增量查找(按下Ctrl+J后,你所输入的每个字母编辑器都提供快速匹配定位到某个单词,如果没有,则在stutes line中显示没有找到了,查一个单词时,特别实用,这个功能Idea两年前就有了)

Ctrl+Shift+J 反向增量查找(和上条相同,只不过是从后往前查)

Ctrl+Shift+F4 关闭所有打开的Editer

Ctrl+Shift+X 把当前选中的文本全部变味小写

Ctrl+Shift+Y 把当前选中的文本全部变为小写

Ctrl+Shift+F 格式化当前代码

Ctrl+Shift+P 定位到对于的匹配符(譬如{}) (从前面定位后面时,光标要在匹配符里面,后面到前面,则反之)

下面的快捷键是重构里面常用的,本人就自己喜欢且常用的整理一下(注:一般重构的快捷键都是Alt+Shift开头的了)

Alt+Shift+R 重命名 (是我自己最爱用的一个了,尤其是变量和类的Rename,比手工方法能节省很多劳动力)

Alt+Shift+M 抽取方法 (这是重构里面最常用的方法之一了,尤其是对一大堆泥团代码有用)

Alt+Shift+C 修改函数结构(比较实用,有N个函数调用了这个方法,修改一次搞定)

Alt+Shift+L 抽取本地变量( 可以直接把一些魔法数字和字符串抽取成一个变量,尤其是多处调用的时候)

Alt+Shift+F 把Class中的local变量变为field变量 (比较实用的功能)

Alt+Shift+I 合并变量(可能这样说有点不妥Inline)
Alt+Shift+V 移动函数和变量(不怎么常用)
Alt+Shift+Z 重构的后悔药(Undo)


Eclipse快捷键大全
Ctrl+1 快速修复(最经典的快捷键,就不用多说了)
Ctrl+D: 删除当前行
Ctrl+Alt+↓ 复制当前行到下一行(复制增加)
Ctrl+Alt+↑ 复制当前行到上一行(复制增加)

基于OO的可移动DIV窗口。

  项目需要一个可移动的DIV窗口,要求调用简单,可以重用。本人马上就考虑到使用OO了。代码如下:

 

 

<!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.0 Transitional//EN”>
<HTML>
 <HEAD>
  <TITLE> New Document </TITLE>
  <META NAME=”Generator” CONTENT=”EditPlus”>
  <META NAME=”Author” CONTENT=””>
  <META NAME=”Keywords” CONTENT=””>
  <META NAME=”Description” CONTENT=””>
 </HEAD>

 <BODY>
<SCRIPT LANGUAGE=”JavaScript”>
  <!–
var ie=document.all;
var nn6=document.getElementById&&!document.all;
var isdrag=false;
var y,x;
var oDragObj;
var qtD = document;
function $(id){
 return getObj(id);
}
function getObj(id){
 return qtD.getElementById(id);
}
function cleanDiv(obj){
(typeof(obj)==”string”?$(obj):obj).innerHTML=”;
}

function msgWindow(msgContext){

 this.width=400;
 this.height=300;
 this.title = ”;
 this.content = ”;
 this.context = (typeof(msgContext)==”string”?$(msgContext):msgContext);

 this.popup = function(){
  var left = (screen.width – this.width) / 2;
  var top = (screen.height – this.height) / 2 -20;
  this.context.innerHTML ='<div id=”msgwin” style=”position:absolute;left:’+left+’px;top:’+top+’px;width:’+this.width+’px; height:’+this.height+’px; border:#999999 1pt solid; background-color:#F7F7F7″>’+
      ‘<div id=”title_0″ style=”width:100%; height:30px; background-image:url(http://www.1363.cn/images/btn_close.gif” onclick=”javascript:cleanDiv(\’context\’)” style=”cursor:hand; margin-top:6px;”/></div></div>’+
      ‘<div id=”txt”  style=”width:95%; height:90%; text-align:left;overflow: hidden; padding-left:10px;line-height:24px; color:#333333; font-size:12px; “>’+this.content+'</div></div>’;
  document.onmousedown=initDrag;
  document.onmouseup=new Function(“isdrag=false”);
 }
}

function moveMouse(e) {
 if (isdrag) {
  oDragObj.style.top  =  (nn6 ? nTY + e.clientY – y : nTY + event.clientY – y)+”px”;
  oDragObj.style.left  =  (nn6 ? nTX + e.clientX – x : nTX + event.clientX – x)+”px”;
  return false;
 }
}

function initDrag(e) {
 var oDragHandle = nn6 ? e.target : event.srcElement;
 if(oDragHandle.tagName!=’DIV’ || oDragHandle.id==’txt’)return;
 var topElement = “HTML”;
 while (oDragHandle.tagName != topElement && oDragHandle.id != “msgwin”) {
  oDragHandle = nn6 ? oDragHandle.parentNode : oDragHandle.parentElement;
 }
 if (oDragHandle.id==”msgwin”) {
  isdrag = true;
  oDragObj = oDragHandle;
  nTY = parseInt(oDragObj.style.top+0);
  y = nn6 ? e.clientY : event.clientY;
  nTX = parseInt(oDragObj.style.left+0);
  x = nn6 ? e.clientX : event.clientX;
  document.onmousemove=moveMouse;
  return false;
 }
}


function testNotes(member_id,truename){
 msg = new msgWindow(‘context’);
 msg.title=”提示窗口”;
 msg.content =’  欢迎光临本站。’;
 msg.popup();
}
  //–>
  </SCRIPT> 
  <div id=’context’></div>

  <SCRIPT LANGUAGE=”JavaScript”>
  <!–
 
testNotes(1,’aa’);
  //–>
  </SCRIPT>
 </BODY>
</HTML>

另类加水印——根据明暗度分别加不同的水印


  1. package image;   

  2.   

  3. import java.awt.AlphaComposite;   

  4. import java.awt.Color;   

  5. import java.awt.Font;   

  6. import java.awt.Graphics;   

  7. import java.awt.Graphics2D;   

  8. import java.awt.Image;   

  9. import java.awt.image.BufferedImage;   

  10. import java.awt.image.ColorModel;   

  11. import java.awt.image.PixelGrabber;   

  12. import java.io.File;   

  13. import java.io.FileOutputStream;   

  14.   

  15. import javax.imageio.ImageIO;   

  16.   

  17. import com.sun.image.codec.jpeg.JPEGCodec;   

  18. import com.sun.image.codec.jpeg.JPEGImageEncoder;   

  19.   

  20. public class WaterMark {   

  21.   

  22.     /**  

  23.      * 获取指定矩形中的像素的矩阵  

  24.      *   

  25.      * @param imageSrc  

  26.      * @param startX  

  27.      * @param startY  

  28.      * @param w  

  29.      * @param h  

  30.      * @return  

  31.      */  

  32.     private int[] getPixArray(Image imageSrc, int startX, int startY,   

  33.             int w, int h) {   

  34.         int[] pix = new int[(w – startX) * (h – startY)];   

  35.            

  36.         /*下面是别人程序中的一段,我实在不明白为何要加这一段,因为我去掉也没有问题,加上还会报错*/  

  37.         PixelGrabber pg = null;   

  38.         try {   

  39.             pg = new PixelGrabber(imageSrc, startX, startY, w-startX, h-startY, pix, 0, w);   

  40.             if (pg.grabPixels() != true) {   

  41.                 try {   

  42.                     throw new java.awt.AWTException(“pg error” + pg.status());   

  43.                 } catch (Exception eq) {   

  44.                     eq.printStackTrace();   

  45.                 }   

  46.             }   

  47.         } catch (Exception ex) {   

  48.             ex.printStackTrace();   

  49.         }   

  50.         return pix;   

  51.     }   

  52.   

  53.     /**  

  54.      * 将1张图片和另1张图片的指定区域重合。可用于制作水印。图片的左上角坐标为0,0  

  55.      *   

  56.      * @param lightnessWaterImg  

  57.      *            颜色比较亮的水印图片,适合底色比较暗的情况  

  58.      * @param darknessWaterImg  

  59.      *            颜色比较暗的水印图片,适合底色比较亮的情况,如果不想区分,则输入null,平均灰度边界同时失效。  

  60.      * @param targetImg  

  61.      *            源图片  

  62.      * @param startX  

  63.      * @param startY  

  64.      * @param x  

  65.      * @param y  

  66.      * @param alpha  

  67.      *            透明度,0f为全透明,1f为完全不透明,0.5f为半透明  

  68.      * @param averageGray  

  69.      *            平均灰度边界(0-255),大于此值,则打暗的水印图片,小于此值则打亮的水印图片。  

  70.      *            默认值128。超过范围,按默认值进行。  

  71.      */  

  72.     private final void pressImage(String lightnessWaterImg,   

  73.             String darknessWaterImg, String targetImg, int startX, int startY,   

  74.             int x, int y, float alpha, float averageGray) {   

  75.         try {   

  76.             // 先判断亮水印和源文件的值是否为null,否则抛出异常   

  77.             if (lightnessWaterImg == null || lightnessWaterImg == “”  

  78.                     || targetImg == null || targetImg == “”) {   

  79.                 throw new Exception(“亮水印或者源图片的地址不能为空”);   

  80.             }    

  81.             // 再判断平均灰度边界是否越界   

  82.             if (averageGray>255||averageGray<0) {   

  83.                 averageGray = 128;   

  84.             }   

  85.                

  86.   

  87.             // 装载源图片   

  88.             File _file = new File(targetImg);   

  89.             // 图片装入内存   

  90.             BufferedImage src = ImageIO.read(_file);   

  91.             // 获取图片的尺寸   

  92.             int width = src.getWidth(null);   

  93.             int height = src.getHeight(null);   

  94.             // 根据源图片尺寸,设置预装载的一个图片,默认是RGB格式的   

  95.             BufferedImage image = new BufferedImage(width, height,   

  96.                     BufferedImage.TYPE_INT_RGB);   

  97.             Graphics2D graphics = image.createGraphics();   

  98.             // 绘制内存中的源图片至指定的矩形内   

  99.             graphics.drawImage(src, 00, width, height, null);   

  100.             // 在已经绘制的图片中加入透明度通道   

  101.             graphics.setComposite(AlphaComposite.getInstance(   

  102.                     AlphaComposite.SRC_ATOP, alpha));   

  103.                

  104.   

  105.             // 获取源图片中和设定的同样大小的区域内的像素集合   

  106.             int[] pixels = getPixArray(src, startX, startY, x, y);   

  107.   

  108.             //查询此集合的平均灰度   

  109.             float average = getAverageGrap(x-startX,y-startY,pixels);   

  110.   

  111.             // 如果平均灰度大于130,则说明此区域比较亮,否则则比较暗   

  112.             System.out.println(average);   

  113.   

  114.                

  115.             //装载水印图片所需参数   

  116.             File water;   

  117.             BufferedImage bufferwater;   

  118.                

  119.             // 根据设定的平均灰度边界来装载不同的水印   

  120.             if (darknessWaterImg == null||average>=averageGray) {   

  121.                 // 装载亮水印文件   

  122.                 water = new File(darknessWaterImg);   

  123.             }else{   

  124.                 // 装载暗水印文件   

  125.                 water = new File(lightnessWaterImg);   

  126.             }   

  127.             // 装入内存   

  128.             bufferwater = ImageIO.read(water);   

  129.                            

  130.             graphics.drawImage(bufferwater, startX, startY, x, y,   

  131.                     null);   

  132.             // 水印文件结束   

  133.             graphics.dispose();   

  134.             FileOutputStream out = new FileOutputStream(targetImg);   

  135.             JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);   

  136.             // 绘制新的文件   

  137.             encoder.encode(image);   

  138.             out.close();   

  139.                

  140.         } catch (Exception e) {   

  141.             e.printStackTrace();   

  142.         }   

  143.     }   

  144.            

  145.     /**  

  146.      * 查询某个区域的平均灰度  

  147.      * @param width  

  148.      * @param height  

  149.      * @param pixels  

  150.      * @return  

  151.      */  

  152.     private float getAverageGrap(int width,int height,int[] pixels){   

  153.         /* 下面是开始算这个区域的亮度了,灰度等同于亮度 */  

  154.         ColorModel colorModel = ColorModel.getRGBdefault();   

  155.         int i = 0;   

  156.         int j = 0;   

  157.         int k = 0;   

  158.         int r = 0;   

  159.         int g = 0;   

  160.         int b = 0;   

  161.         int gray = 0;   

  162.         float average = 0;// 平均灰度   

  163.         for (i = 0; i < height; i++) {   

  164.             for (j = 0; j < width; j++) {   

  165.                 // 定位像素点   

  166.                 k = i * width + j;   

  167.                 r = colorModel.getRed(pixels[k]);   

  168.                 g = colorModel.getGreen(pixels[k]);   

  169.                 b = colorModel.getBlue(pixels[k]);   

  170.   

  171.                 // 计算灰度值   

  172.                 gray = (r * 38 + g * 75 + b * 15) >> 7;   

  173.   

  174.                 average = average + gray;   

  175.             }   

  176.         }   

  177.         // 计算平均灰度   

  178.         average = average / ((i – 1) * (j – 1));   

  179.         return average;   

  180.     }   

  181.     public static void main(String[] args) {   

  182.         WaterMark waterMark = new WaterMark();   

  183.   

  184.         waterMark.pressImage(“F:\\Mine\\My Pictures\\素材\\w2.png”“F:\\Mine\\My Pictures\\素材\\w1.png”,   

  185.                 “F:\\Mine\\My Pictures\\素材\\2.jpg”5205009008000.5f, 50);   

  186.         System.out.print(“添加成功”);   

  187.     }   

  188.   

  189. }  

JavaRebel 1.2发布,充分支持Spring

JavaRebel是一个JVM插件(-javaagent),能够即时重载java class更改,因此不需要重新部署一个应用或者重启容器,节约开发者时间。它是一个Java EE和Java 独立应用的常用解决方案。最新的稳定版本JavaRebel代码加载包含一个Spring的插件,能重新加载Spring配置快速的促进生产力的发展,甚至更多。查看Screencast ,下载JavaRebel插件

这个稳定的JavaRebel主要致力于两个主题:稳定和可扩展性。下面是它与1.1版本的区别:

core:增强了core的JavaRebel现在可以处理任何复杂的代码或容器。
SDK:您可以对应用程序和库的任何部分进行增值,无论您身在何处或怎么设置。SDk 为开放源代码。
JavaRebel插件:现在支持自定义的classloaders更容易,容器和框架只需登记一个小插件即可。
Eauinox OSGi容器,现在支持IBM WebSphere 和 Atlassian 混合插件。

查看完整的更新列表 或现在就开始下载JavaRebel

我们已经准备了一个Screencast来说明Spring插件的功能。查看该插件的安装手册 ,了解更多的详情或现在就下载

一个体现Java接口及工厂模式优点的例子

     随着模式概念的普及,了解模式和使用模式的程序员越来越多,很多人在学习模式的时候,都会有这样一种疑惑:“有必要搞得这么复杂吗?”。的确,因为教程的例子过于简单化(这样方便读者学习),或者是作者选例子的时候并没有很好体现所讲模式的优点,很多情况下如果仅就其例子的问题来说,用模式是太复杂了。因此才导致这样的误解:“模式就是把简单的问题复杂化吗?”。当然不是,随着你开发实践的不断丰富,你终会发现模式强大威力,而且模式也并非贵族化的编程方式,它就是一些经过提炼了的解决问题的方法技巧。

 

      通过学习模式,程序员开始告别过去准直线式的代码方式,模式开扩了我们的视野,强化了我们面向对象编程的思维方式。然而现在又出现了另一个普遍的问题,盲目应用模式。模式是问题的解决方案,先有问题才有模式,模式是依附于所要解决的问题的而生的。必须了解模式在很多情况下是以提高代码的复杂度为代价来增强灵活性、可复用性。如果在自已的代码中使用某一模式仅只提高了代码的复杂度,而其它方面收效甚微,或者某部份代码根本就不存在灵活性及高复用性的需求,那么我们就没有必要为使用模式而放弃更直观简单的代码写法。

 

      一流的高手90%精力关注问题的解决方案,因为找到了好的解决方案,再写起代码会很轻松代码也简洁流畅,看这样的代码是一种享受和提高;二流的熟手90%精力关注代码实现,因为问题的解决方案并非最佳,实现的代码也会比较复杂;三流菜鸟记流水帐,90%精力在敲键盘,常常做了大半才发现行不通,回过头来再用90%的时间敲键盘,根本不会用到任何模式,写出来的代码的只有他自已才能看懂。做出来的软件也是支离破碎,做一丁点改动都要大费周折,而且你还不知道改动后会产生什么问题,大有住危房里的感觉。

 

      在这里还是举一个滥用模式的例子吧。我曾参与过一个大集团公司OA系统的第二期开发,开发沿用原有代码架构并增加新的功能模块。文档很少我读原代码时就被它程序里的代码转来转去搞得头大如斗,最后读懂了:原代码架构总体采用工厂模式,而且是最复杂的抽象工厂模式。它把所有模块类都通过工厂生成还工厂套工厂,并且每一个模块类都有一个接口,每个接口也只有一个模块现实类,因为涉及权限控制还用了代理(proxy)模式。 读懂代码后我开始嵌入代码,发现每新增一个类,都要到六个Java文件中去增加相应代码,而在类中每增加一个方法,也要到它的接口等四个Java文件中去增加相应代码。天呀!!!记得当时我的小姆指常会不听使唤,就是因为频繁的使用Ctrl+C 、Ctrl+V,小姆指按着Ctrl键给累的。整个项目组苦不堪言,真烦透了。项目结束后我回顾发现:代理模式用得还对(现在针对权限这类横向控制有AOP编程这种新的解决办法了)但工厂模式在这里根本就是画蛇添足,不仅没有解决什么问题,反而增加代码复杂度和耦合性,降低了开发效率连维护难度都提高了。而且那种每个类简单的加一个接口的方式,更是没有道理,这让我很想说周星驰说过的一句话:“球~~~不是这么踢~~~~的,接口~~~不是这么用~~~的”。言归正传,我们先来看这样一个常见问题:某系统需要支持多种类型的数据库。用过Oracle、MSSQL等数据库的人都知道,它们的SQL编写方式都各有些不同。比如说Oracle的唯一标识自动+1字段用的是序列,MSSQL改一下字段属性就成了,还有各种各自特有的SQL用法。为了支持多数据库,难道我们要开发多套系统?当然NO。请看下面的解决方案。

 

      即然数据库存在多种,我们可以将系统中所有对数据库的操作抽象出来,写成一个个方法组合到一个类中,有几种数据库我们就写几个这样的类。具体设计类图如下:

20060910.gif

 

 

简要说明:


  • OracleDataOperate、SqlserverDataOperate、MysqlDataOperate,分别代表Oracle、Sqlserver、Mysql这三种数据库的操作类。继承自AbstractDataOperate
  • AbstractDataOperate是一个抽象类,包含了那些不同种类数据库都是一样代码的操作方法。继承自DataOperate
  • DataOperate是上面说的数据操作类的统一接口,只有两个方法:取得一条记录、插入一条记录。
  • DataOperateFactory是一个工厂方法,统一用它的方法来得到数据库操作类的实例。
  • SampleClass是我们系统的某个功能模块的类。
  • People是一个实体类,代表一条记录。三个字段 oid唯一标识符、name姓名、date生日。

 

详细说明:

1、所有系统功能模块类只认DataOperat这个接口还不必管具体的实现类是OracleDataOperate还SqlserverDataOperate。DataOperate源代码如下:

publicinterface DataOperate {

    //根据记录的唯一标识取出一条记录

    People getPeople(String oid);

    //插入一条记录

    boolean insertPeople(People people);

}

 

2、AbstractDataOperate、OracleDataOperate、SqlserverDataOperate、MysqlDataOperate都是继承DataOperate接口的,没什么好说的,省略。

 

3、DataOperateFactory。我们看看工厂方法怎么写的。

publicclass DataOperateFactory {

    publicstaticfinalint ORACLE = 0; //定义三个表示数据库类型的常量

    publicstaticfinalint MYSQL = 1;

    publicstaticfinalint SQLSERVER = 2;

 

    privatestatic DataOperate db;

    privatestaticint dataType = MYSQL;

    /**

     * 根据数据库类型(dataType)取得一个数据库操作类的实例,

     * 这里对DataOperate使用了单例模式,因为OracelDataOperate等都是无状态的工具类,

     * 所以整个系统只保留一个实例就行了。

     *

     * @return 返回的是接口,客户端不必关心具体是用那个实现类

     */

    publicstatic DataOperate getInstance() {

        if (db == null) {

            if (dataType == ORACLE) //根据dateType返回相应的实现类

                returnnew OracelDataOperate();

            if (dataType == MYSQL)

                returnnew MysqlDataOperate();

            if (dataType == SQLSERVER)

                returnnew SqlserverDataOperate();

        }

        return db;

    }

}

 

4、接下来就看看使用端是如何调用工厂方法和使用数据操作类的。

/**

 * 系统某个功能类

 */

publicclass SampleClass {

private DataOperate db; //声明一个数据库操作类,注意这里用的是接口噢

    /**某方法*/

    publicvoid sampleMethod() {

        db = DataOperateFactory.getInstance();//得到单一实例

        People p = db.getPeople(“123”); //取得一条记录

        db.insertPeople(p);//再插回去

    }

}

 

  我们发现SampleClass中根本没有出现OracelDataOperate、MysqlDataOperate等的影子,这就是接口的威力。客户端不必针对OracelDataOperate等写不同的代码,它只关心DataOperate即可,具体要取那个类的逻辑就由DataOperateFactory负责了。

 

总结:



  • 从例子中我们可以看到什么是面向接口的编程方式。SampleClass使用数据操作类可以不必关心具体是那个类,只要是符合接口的都行


  • 要实例?只须调用DataOperateFactory.getInstance()即可,其它的交于DataOperateFactory这个工厂来做吧,使用端什么都不用关心。


  • 我们要支持新的数据库类型,只须要象OracelDataOperate那样,再写一个继承AbstractDataOperate的类即可,比如SysbaseDataOperate。然后到DataOperateFactory中加入相应代码即可。


  • 如果我们想要可配置性更高,可以用privatestaticint dataType = MYSQL;中的值设置到一个文本文件中。

  对于开发支持多种数据库的系统,强烈建议使用hibernate,我现在做的系统就是用hibernate的,开发时用Mysql,到要给客户时将数据库换了DB2,程序不用做任何改动,真正的无逢移植。不过这样,本文所提到的方法就没什么用了.