月度归档: 2009年2月

在Linux上安装awstats

awstats是一个Apache/Windows IIS的日志分析工具,每次安装的时候都参考车东的《AWStats简介》,这次我把安装步骤记录下来,下次再安装时就比较方便了。

我的Apache安装在/usr/local/apache/,access_log日志文件用cronolog工具按天截断

wget http://nchc.dl.sourceforge.net/sourceforge/awstats/awstats-6.9.tar.gz
tar zxvf awstats-6.9.tar.gz
mv awstats-6.9/wwwroot/cgi-bin /usr/local/apache/cgi-bin/awstats
(wwwroot下还有一些静态文件夹,最后Copy到相应Web目录下)

下面安装GeoIP相关包,GeoIP是一个ip-location的工具,可以根据IP确定位置,包括国家、城市,具体参考其网站 http://www.maxmind.com/app/ip-location

wget http://geolite.maxmind.com/download/geoip/api/c/GeoIP.tar.gz
wget http://geolite.maxmind.com/download/geoip/api/perl/Geo-IP-1.36.tar.gz
wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz

tar zxvf GeoIP.tar.gz
cd GeoIP-1.4.5
./configure
make
make install
cd ..

tar zxvf Geo-IP-1.36.tar.gz
cd Geo-IP-1.36
perl Makefile.PL(注意,在这里可能出错,是因为找不到GeoIP的包,可以用perl Makefile.PL LIBS=’-L/usr/local/lib’ INC=’-I/usr/local/include’)
make
make install
cd ..

gunzip GeoIP.dat.gz
mv GeoIP.dat /usr/local/apache/cgi-bin/awstats/

gunzip GeoLiteCity.dat.gz
mv GeoLiteCity.dat /usr/local/apache/cgi-bin/awstats/

cd /usr/local/apache/cgi-bin/awstats/
mv awstats.model.conf common.conf
修改common.conf文件,加入
LoadPlugin=”decodeutfkeys”
LoadPlugin=”geoip GEOIP_STANDARD /usr/local/apache/cgi-bin/awstats/GeoIP.dat”
LoadPlugin=”geoip_city_maxmind GEOIP_STANDARD /usr/local/apache/cgi-bin/awstats/GeoLiteCity.dat”

保存退出之后,还在/usr/local/apache/cgi-bin/awstats/目录下
mkdir data

vi awstats.pl
修改里面的内容
$LIMITFLUSH =
50000;   # Nb of records in data arrays after how we need to flush data on disk
保存退出

新建你网站的配置文件
vi awstats.domainname.conf
加入内容
Include “common.conf”
LogFile=”/usr/local/apache/logs/domainname-access_log.%YYYY-24%MM-24%DD-24″
SiteDomain=”domainname”
DefaultFile=”index.html”
DirData=”/usr/local/apache/cgi-bin/awstats/data/”
保存退出

执行
./awstats.pl -update -config=domainname -lang=cn
就可以生成domainname的分析报表,通过http://domainname/cgi-bin/awstats/awstats.pl?config=domainname就要可以在web上查看了
需要每日生成的话,写一个crontab的脚本在每天凌晨执行就可以了

3DES加密

DES加密算法是一种标准算法,应该可以通过不同程序语言实现,我在异构系统里,可能需要用不同的语言加密解密,整理不同语言的DES算法还是有必要的,先前我发过一个Java的3DES算法代码,我再发一遍

import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;

public class DES {

public static int _DES = 1;
public static int _DESede = 2;
public static int _Blowfish = 3;

private Cipher p_Cipher;
private SecretKey p_Key;
private String p_Algorithm;

private void selectAlgorithm(int al) {
switch (al) {
default:
case 1:
this.p_Algorithm = “DES”;
break;
case 2:
this.p_Algorithm = “DESede”;
break;
case 3:
this.p_Algorithm = “Blowfish”;
break;
}
}

public DES(int algorithm) throws Exception {
this.selectAlgorithm(algorithm);
Security.addProvider(new com.sun.crypto.provider.SunJCE());
this.p_Cipher = Cipher.getInstance(this.p_Algorithm);
}

public byte[] getKey() {
return this.checkKey().getEncoded();
}

private SecretKey checkKey() {
try {
if (this.p_Key == null) {
KeyGenerator keygen = KeyGenerator.getInstance(this.p_Algorithm);
/*
SecureRandom sr = new SecureRandom(key.getBytes());
keygen.init(168, sr);*/
this.p_Key = keygen.generateKey();
}
}
catch (Exception nsae) {}
return this.p_Key;
}

public void setKey(byte[] enckey) {
this.p_Key = new SecretKeySpec(enckey, this.p_Algorithm);
}

public byte[] encode(byte[] data) throws Exception {
this.p_Cipher.init(Cipher.ENCRYPT_MODE, this.checkKey());
return this.p_Cipher.doFinal(data);
}

public byte[] decode(byte[] encdata, byte[] enckey) throws Exception {
this.setKey(enckey);
this.p_Cipher.init(Cipher.DECRYPT_MODE, this.p_Key);
return this.p_Cipher.doFinal(encdata);
}

public String byte2hex(byte[] b) {
String hs = “”;
String stmp = “”;
for (int i = 0; i < b.length; i++) {
stmp = Integer.toHexString(b[i] & 0xFF);
if (stmp.length() == 1) {
hs += “0” + stmp;
}
else {
hs += stmp;
}
}
return hs.toUpperCase();
}

public byte[] hex2byte(String hex) throws IllegalArgumentException {
if (hex.length() % 2 != 0) {
throw new IllegalArgumentException();
}
char[] arr = hex.toCharArray();
byte[] b = new byte[hex.length() / 2];
for (int i = 0, j = 0, l = hex.length(); i < l; i++, j++) {
String swap = “” + arr[i++] + arr[i];
int byteint = Integer.parseInt(swap, 16) & 0xFF;
b[j] = new Integer(byteint).byteValue();
}
return b;
}

public static void main(String[] args) throws Exception {

String info = “要加密的字串”;
System.out.println(“region string:” + info);
byte[] key; //密钥文件(byte)
DES des = new DES(DES._DESede); // 声明DES
key = des.getKey(); //获取随机生成的密钥
System.out.println(“encrypted key(byte):” + new String(key));
String hexkey = des.byte2hex(key); //生成十六进制密钥
System.out.println(“encrypted key(hex):” + hexkey);
byte[] enc = des.encode(info.getBytes()); //生成加密文件(byte)
System.out.println(“encrypted string(byte):” + new String(enc));

String hexenc = des.byte2hex(enc); //生成十六进制加密文件
System.out.println(“encrypted string(hex):” + hexenc);

byte[] dec = des.decode(enc, des.hex2byte(hexkey)); //解密文件,其中转换十六进制密钥为byte
System.out.println(“decrypted string:” + new String(dec)); //生成解密文件字符串, 与info相同
}

}

最近我在看PHP的时候,研究了一下PHP对于3DES的加密代码

<?php

function fmt3DESEncode($s,$base64key){

$key = base64_decode($base64key);

$td = mcrypt_module_open(MCRYPT_3DES, ”, MCRYPT_MODE_ECB, ”);

$iv_size = mcrypt_enc_get_iv_size($td);

$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);

mcrypt_generic_init($td, $key,$iv);

$encrypted_data = mcrypt_generic($td, $s);

mcrypt_generic_deinit($td);

mcrypt_module_close($td);

return $encrypted_data;

}

function PaddingPKCS7($data) {

$block_size = mcrypt_get_block_size(‘tripledes’, ‘ecb’);

$padding_char = $block_size – (strlen($data) % $block_size);

$data .= str_repeat(chr($padding_char),$padding_char);

return $data;

}

function fmt3DESDecode($s,$base64key) {

$key = base64_decode($base64key);

$td = mcrypt_module_open(MCRYPT_3DES, ”, MCRYPT_MODE_ECB, ”);

$iv_size = mcrypt_enc_get_iv_size($td);

$iv = mcrypt_create_iv($iv_size, MCRYPT_RAND);

mcrypt_generic_init($td, $key,$iv);

$p_t = mdecrypt_generic($td, $s);

mcrypt_generic_deinit($td);

mcrypt_module_close($td);

return $p_t;

}

function unPaddingPKCS7($data) {

$pad = ord($data{strlen($data)-1});

if ($pad > strlen($data)) {

return false;

}

if (strspn($data, chr($pad), strlen($data) – $pad) != $pad) {

return false;

}

$srcdata = substr($data, 0, -1 * $pad);

return $srcdata;

}

$S = “laoer”;

$str = BASE64_Encode(fmt3DESEncode(PaddingPKCS7($S),”XXXXXXXXX-BASE64-密钥”));

echo $str;

echo”<br/>”;

$str = fmt3DESDecode(base64_decode($str), “XXXXXXXXX-BASE64-密钥”);

echo unPaddingPKCS7($str);

?>

有几点注意,在Java里3DES默认ENCRYPT_MODE是ECB,并采用PKCS7补码,关于着两个概念,我不是专业人士,也解释不清楚,需要研究加密解密了,所以在PHP里也进行了相应的处理,还有就是Java代码里密钥和加密结果是用16进制编码了,PHP代码里用了Base64,大家用的时候相应改一下就可以了。

现在可以测试一下,JAVA和PHP的加密解密结果是否一致!

补:.NET下3DES的算法也没有问题,我不做这方面,但以前同事做过,都测试通过。

CVS配置

以Linux为例:

一、CVS服务器的安装

首先确认系统中是否安装CVS服务:
[root@localhost /]# rpm -qa|grep cvs
cvs-1.11.2-cvshome.7x.1
如果命令输出类似于上面的输出则说明系统已经安装有cvs,否则就需要从安装光盘中安装cvs的rpm包,或者到http://www.cvshome.org下载。

1、建立 CVSROOT

目录,因为这里涉及到用户对CVSROOT里的文件读写的权限问题,所以比较简单的方法是建立一个组,然后再建立一个属于该组的帐户,而且以后有读写权限的用户都要属于该组。假设我们建一个组叫cvs,用户名是cvsroot。建组和用户的命令如下

#groupadd cvs
#useradd -g cvs -G cvs -d /home/cvsroot cvsroot

生成的用户宿主目录在/home/cvsroot(根据自己的系统调整)

2、建立CVS仓库,用cvsroot 用户登陆,用下面命令:

$cvs -d /home/cvsroot init

3、仍然是 cvsroot 用户,修改 /home/cvsroot (CVSROOT)的权限,赋与同组人有读写的权限:

$chmod 775 -R /home/cvsroot

4、用root登录,加入cvs服务(我的redhath7.3缺省就有cvs服务,所以不用加)

vi /etc/services
cvspserver 2401/tcp #pserver cvs service
cvspserver 2401/udp #pserver cvs service

cvs服务由inted来唤起,因此需要改动inetd提供的服务,如果你的redhat使用的是inetd 方式则在文件/etc/inetd.conf中加入如下的内容:cvspserver stream tcp nowait root /usr/bin/cvs cvs –allow-root=/home/cvsroot pserver

我的redhat7.3使用的是xinetd方式,所以在xinetd.d目录下添加需要启动的服务:
cd /etc/xinetd.d
vi cvspserver
文件内容:
service cvspserver
{
disable = no
flags = REUSE
socket_type = stream
wait = no
user = cvsroot
server = /usr/bin/cvs
server_args = -f –allow-root=/home/cvsroot pserver
log_on_failure += USERID
}

重新启动inetd或者xinetd:
/etc/rc.d/init.d/xinetd restart

检查cvspserver服务是否已经启动
[root@localhost /]# netstat -lnp|grep 2401
tcp        0      0 0.0.0.0:2401            0.0.0.0:*               LISTEN      1041/xinetd
则说明cvs服务器已经运行。

二、管理CVS服务器

服务器可以用了,现在大家最关心的就是如何管理服务器,比如,我想让一些人有读和/或写 CVS 仓库的权限,但是不想给它系统权限怎么办呢?

不难,在 cvs 管理员用户(在我这里是 cvsroot 用户)的家目录里有一个 CVSROOT 目录,这个目录里有三个配置文件,passwd, readers, writers,我们可以通过设置这三个文件来配置 CVS 服务器,下面分别介绍这几个文件的作用:

passwd:cvs 用户的用户列表文件,它的格式很象 shadow 文件:

{cvs 用户名}:[加密的口令]:[等效系统用户名]

readers:有 cvs 读权限的用户列表文件。就是一个一维列表。在这个文件中的用户对 cvs只有读权限。

writers:有 cvs 写权限的用户的列表文件。和 readers 一样,是一个一维列表。在这个文件中的用户对 cvs 有写权限。

上面三个文件在缺省安装的时候可能都不存在,需要我们自己创建,好吧,现在还是让我们用一个例子来教学吧。假设我们有下面几个用户需要使用 cvs:

laser, gumpwu, henry, betty, anonymous。

其中 laser 和 gumpwu 是系统用户,而 henry, betty, anonymous 我们都不想给系统用户权限,并且 betty 和 anonymous 都是只读用户,而且 anonymous 更是连口令都没有。

然后编辑 cvs 管理员家目录里 CVSROOT/passwd 文件(cvsroot用户),加入下面几行:

laser:$xxefajfka;faffa33:cvsroot
gumpwu:$ajfaal;323r0ofeeanv:cvsroot
henry:$fajkdpaieje:cvsroot
betty:fjkal;ffjieinfn/:cvsroot
anonymous::cvsroot

注意:上面的第二个字段(分隔符为 :)是密文口令。

密码可以有以下方法生成
vi /home/cvsroot/passwdgen.pl
文件内容:
#!/usr/bin/perl
srand (time());
my $randletter = “(int (rand (26)) + (int (rand (1) + .5) % 2 ? 65 : 97))”;
my $salt = sprintf (“%c%c”, eval $randletter, eval $randletter);
my $plaintext = shift;
my $crypttext = crypt ($plaintext, $salt);
print “${crypttext}\n”;

如果需要密码为:some,则敲入:
./passwdgen.pl some
(注意passwdgen.pl要先设为可执行)
回车即可得到加密密码,用其替换passwd文件中的passwd部分就可以了
编辑 readers 文件,加入下面几行:
anonymous
betty

编辑 writers 文件,加入下面几行:

laser
gumpwu
henry

注意:writers中的用户不能在readers中,要不然不能上传更新文件。
对于使用CVS的用户要修改它的环境变量,例如laser用户的环境变量,打开/home/laser(laser的宿主目录)下的.bash_profile文件,加入

CVSROOT=/home/cvsroot
export CVSROOT

用laser登陆就可以建立CVS项目,如果要root使用,可以修改/etc/profile文件。
现在我们各项都设置好了,那么怎么用呢,我在这里写一个最简单的(估计也是最常用的)命令介绍:

首先,建立一个新的CVS项目,一般我们都已经有一些项目文件了,这样我们可以用下面步骤生成一个新的CVS项目:

进入到你的已有项目的目录,比如叫 cvstest:

$cd  cvstest
运行命令:
$cvs import -m “this is a cvstest project” cvstest     v_0_0_1     start

说明:import 是cvs的命令之一,表示向cvs仓库输入项目文件。 -m参数后面的字串是描述文本,随便写些有意义的东西,如果不加 -m 参数,那么cvs会自动运行一个编辑器(一般是vi,但是可以通过修改环境变量EDITOR来改成你喜欢用的编辑器。)让你输入信息,cvstest 是项目名称(实际上是仓库名,在CVS服务器上会存储在以这个名字命名的仓库里。)
v_0_0_1是这个分支的总标记。没啥用(或曰不常用。)
start 是每次 import 标识文件的输入层次的标记,没啥用。
这样我们就建立了一个CVS仓库了。

建立CVS仓库的文件夹应该是“干净”的文件夹,即只包括源码文件和描述的文件加,而不应该包括编译过的文件代码等!

ApplicationContext研究

ApplicationContext是Spring的核心,Context我们通常解释为上下文环境,我想用“容器”来表述它更容易理解一些,ApplicationContext则是“应用的容器”了:P,Spring把Bean放在这个容器中,在需要的时候,用getBean方法取出,虽然我没有看过这一部分的源代码,但我想它应该是一个类似Map的结构。
在Web应用中,我们会用到WebApplicationContext,WebApplicationContext继承自ApplicationContext,先让我们看看在Web应用中,怎么初始化WebApplicationContext,在web.xml中定义:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!– OR USE THE CONTEXTLOADERSERVLET INSTEAD OF THE LISTENER
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
–>

可以看出,有两种方法,一个是用ContextLoaderListener这个Listerner,另一个是ContextLoaderServlet这个Servlet,这两个方法都是在web应用启动的时候来初始化WebApplicationContext,我个人认为Listerner要比Servlet更好一些,因为Listerner监听应用的启动和结束,而Servlet得启动要稍微延迟一些,如果在这时要做一些业务的操作,启动的前后顺序是有影响的。

那么在ContextLoaderListener和ContextLoaderServlet中到底做了什么呢?
以ContextLoaderListener为例,我们可以看到
public void contextInitialized(ServletContextEvent event) {
this.contextLoader = createContextLoader();
this.contextLoader.initWebApplicationContext(event.getServletContext());
}
protected ContextLoader createContextLoader() {
return new ContextLoader();
}
ContextLoader是一个工具类,用来初始化WebApplicationContext,其主要方法就是initWebApplicationContext,我们继续追踪initWebApplicationContext这个方法(具体代码我不贴出,大家可以看Spring中的源码),我们发现,原来ContextLoader是把WebApplicationContext(XmlWebApplicationContext是默认实现类)放在了ServletContext中,ServletContext也是一个“容器”,也是一个类似Map的结构,而WebApplicationContext在ServletContext中的KEY就是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,我们如果要使用WebApplicationContext则需要从ServletContext取出,Spring提供了一个WebApplicationContextUtils类,可以方便的取出WebApplicationContext,只要把ServletContext传入就可以了。

上面我们介绍了WebApplicationContext在Servlet容器中初始化的原理,一般的Web应用就可以轻松的使用了,但是,随着Struts的广泛应用,把Struts和Spring整个起来,是一个需要面对的问题,Spring本身也提供了Struts的相关类,主要使用的有org.springframework.web.struts.ActionSupport,我们只要把自己的Action继承自ActionSupport,就是可以调用ActionSupport中getWebApplicationContext()的方法取出WebApplicationContext,但这样一来在Action中,需要取得业务逻辑的地方都要getBean,看上去不够简洁,所以Spring又提供了另一个方法,用org.springframework.web.struts.ContextLoaderPlugIn,这是一个Struts的Plug,在Struts启动时加载,对于Action,可以像管理Bean一样来管理,在struts-config.xml中Action的配置变成类似下面的样子
<action attribute=”aForm” name=”aForm” path=”/aAction” scope=”request”  type=”org.springframework.web.struts.DelegatingActionProxy”>
<forward name=”forward” path=”forward.jsp” />
</action>
注意type变成了org.springframework.web.struts.DelegatingActionProxy,之后我们需要建立action-servlet.xml这样的文件,action-servlet.xml符合Spring的spring-beans.dtd标准,在里面定义类似下面的
<bean name=”/aAction” class=”com.web.action.Aaction” singleton=”false”>
<property name=”businessService”>
<ref bean=”businessService”/>
</property>
</bean>

com.web.action.Aaction是Action的实现类,businessService是需要的业务逻辑,Spring会把businessService注入到Action中,在Action中只要写businessService的get和set方法就可以了,还有一点,action的bean是singleton=”false”,即每次新建一个实例,这也解决了Struts中Action的线程同步问题,具体过程是当用户做“/aAction”的HTTP请求(当然应该是“/aAction.do”),Struts会找到这个Action的对应类org.springframework.web.struts.DelegatingActionProxy,DelegatingActionProxy是个代理类,它会去找action-servlet.xml文件中“/aAction”对应的真正实现类,然后把它实例化,同时把需要的业务对象注入,然后执行Action的execute方法。

使用了ContextLoaderPlugIn,在struts-config.xml中变成类似这样配置
<plug-in className=”org.springframework.web.struts.ContextLoaderPlugIn”>
<set-property property=”contextConfigLocation” value=”/WEB-INF/applicationContext.xml,/WEB-INF/action-servlet.xml” />
</plug-in>
而在web.xml中不再需要ContextLoaderListener或是ContextLoaderServlet。

说到这里不知道大家会不会有这样的问题,如果使用ContextLoaderPlugIn,如果我们有些程序是脱离Struts的Action环境,我们怎么处理,比如我们要自定义标记库,在标记库中,我们需要调用Spring管理的业务层逻辑对象,这时候我们就很麻烦,因为只有在action中动态注入业务逻辑,其他我们似乎不能取得Spring的WebApplicationContext。

别急,我们还是来看一下ContextLoaderPlugIn的源码(源码不再贴出),我们可以发现,原来ContextLoaderPlugIn仍然是把WebApplicationContext放在ServletContext中,只是这个KEY不太一样了,这个KEY值为ContextLoaderPlugIn.SERVLET_CONTEXT_PREFIX+ModuleConfig.getPrefix()(具体请查看源代码),这下好了,我们知道了WebApplicationContext放在哪里,只要我们在Web应用中能够取到ServletContext也就能取到WebApplicationContext了:)

再说“i”

想了想,我博客的标题就用I@laoer.com吧,为什么用i,这是有历史原因的,呵呵:)

当年我在网易工作,丁磊在网易通行证的代号就是i,他的电子邮件也就是i@163.com,他会经常在网易的各个产品里溜达,如果我们发现了在线用户里有“i”,就知道丁磊在看,这时一定不能出什么纰漏,要不然,哈哈…

如今我也效仿他老人家一下,I@laoer.com

Struts2小结

天乙社区8.0(http://www.laoer.com)已经完全用Struts2实现,在使用Struts的过程中也遇到了一些问题,现在总结一下。

1、在web.xml中EncodingFilter的位置应该在Struts2的FilterDispatcher之前,道理很简单,要先调整字符集,再进入Action。

2、如果使用Urlrewrite,要指定filter-mapping的dispatcher方式,如下
<filter-mapping>
<filter-name>Struts2</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>

3、在做上传文件的时候,要在web.xml中增加ActionContextCleanUp这个filter,如果不增加,会发生第一次上传取不到文件的情况
<filter>
<filter-name>struts-cleanup</filter-name>
<filter-class>org.apache.struts2.dispatcher.ActionContextCleanUp</filter-class>
</filter>
<filter-mapping>
<filter-name>struts-cleanup</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
按照Struts2的API,filter的顺序是
struts-cleanup filter
SiteMesh filter
FilterDispatcher

4、在Apache+Resin的情况下,要在WEB-INF下增加resin-web.xml,该文件只针对Resin有效,作用是指定后缀与 Resin的Servlet引擎匹配,要不然从Apache转发过去的请求到Resin后会出现404的情况,resin-web.xml举例如下:
<web-app xmlns=”http://caucho.com/ns/resin”>
<servlet-mapping url-pattern=’*.bbscs’ servlet-name=’plugin_match’/>
</web-app>

5、在使用<s:url/>标签的时候,会出现将get或post数值带入url参数的情况,如果不需要这些参数,可以在struts.properties文件中设置
struts.url.includeParams=none
或是在<s:url/>标记中将includeParams属性设为none
另外还有两个值
all,是把get和post中的参数加入到url参数中
get,是只把get中的参数加入到url参数中

6、与webwork基本相同,Struts2提供了几种ui.theme,有xhtml、css_xhtml、simple等等,在 struts.properties中可以设置使用何种theme,这一点很关键,不同的theme,struts的tag会生成不同的html代码,而且在某些情况下这些theme不能满足页面要求,则需要自己进行扩展了,这些theme都是由freemarker写的,仿照这写就可以。

7、单个checkbox的标记库好像只能返回boolean的值,如果在数据库中设计为int型,则需要做一些转换,这一点我觉得不如Struts1.x的方便。

8、总体来说Struts2的标记库使用上比Struts1.x的方便,页面整体也比较简洁,Struts2采用stack的方式存取数据,与Struts1相比各有千秋吧。

Struts2主要延续自webwork,以前使用webwork的朋友转过来并不困难,Struts2的几个核心的部分,比如拦截器、Result Configuration、OGNL stack等等还是需要仔细的体会,深入了解,才能做出优秀的系统。

(这篇文章在JavaEye上发表过)

Tomcat AJP Connector参数调整

最近公司的一个面向C端客户的网站应用频繁的Down掉,昨天早上重起Tomcat后,快到下午下班时有不响应了,我们采用的是Apache+Tomcat,Apache和Tomcat之间采用的是AJP,在应用Down机的时候发现直接访问Tomcat的端口没有问题,而Apache的性能应该没有问题,其参数我也调整过,而且我们的访问量并不算很大,服务器的Idel也一直处于空闲状态,由此判断,估计是Apache到Tomcat的连接出了什么问题,晚上回家我查了查Tomcat的文档,找到AJP这部分配置(http://tomcat.apache.org/tomcat-5.5-doc/config/ajp.html),里面Standard Implementation部分有maxThreads、maxSpareThreads、minSpareThreads这几个参数,我想应该是AJP Connector原有的参数设置的太小或是不合理造成的,相应修改这几个参数,重起Tomcat看看情况如何。