2012年7月26日

一个log4j的配置问题

为了查看Spring中注册RMI的问题,继承了org.springframework.remoting.rmi.RmiRegistryFactoryBean,同时想把这个类的log级别调成DEBUG.

修改后的配置如下

bean的配置:

<bean id="registry" class="xxx.MyRmiRegistryFactoryBean">
    <property name="port" value="12345"/>
</bean>

log4j的配置:

log4j.logger.org.springframework.remoting.rmi.RmiRegistryFactoryBean=debug

应该说没啥特别的,可是没有任何输出.

确定配置的写法没有问题后,只能在common-logging的LogFactory.getLog方法被调用时打印出类名.
结果打印了xxx.MyRmiRegistryFactoryBean.

此时再回过头去看RmiRegistryFactoryBean里的logger定义,坑爹啊,居然是个实例变量:

protected final Log logger = LogFactory.getLog(getClass());

平时定义logger的时候一般都是习惯定义成static的,已经形成了思维定势...之前看了好久RmiRegistryFactoryBean的代码,居然都没发现这里logger是实例变量.

因为现在实例的是MyRmiRegistryFactoryBean,getClass()返回的自然就是xxx.MyRmiRegistryFactoryBean

因此只要修改log4j的配置如下就可以了:

log4j.logger.xxx.MyRmiRegistryFactoryBean=debug

恩,我一直以为是log4j的问题, 调试了半天log4j的代码,要是一开始就调试spring的代码,应该一下子就发现问题了.

2012年6月17日

泛型返回值自动装箱导致javac编译报错的问题

public class A {
    private Map<String, Object> paras = new HashMap<String, Object>();
    public <T> T get(String key) {
        Object obj = paras.get(key);
        if (obj == null) {
            return null;
        }
        return (T) obj;
    }

    public void test() {
        long value1 = get("akey");
        int value2 = get("bkey");
    }
}

上面这个类,在Eclipse中是可以正确编译的,但是使用javac编译就会报以下错误:

A.java:14: 无法确定 T 的类型参数;对于上限为 long,java.lang.Object 的类型变量 T,不存在唯一最大实例 long value1 = get("akey");
A.java:15: 无法确定 T 的类型参数;对于上限为 int,java.lang.Object 的类型变量 T,不存在唯一最大实例 int value2 = get("bkey");

这是因为eclipse和jdk使用的编译器不一样,eclipse使用jdt,jdk使用的是javac。

根据报错信息,get方法并无错误,问题是出在调用的代码上。方法的返回值赋给了原子类型(long,int),这时候会用到自动装箱功能,应该是javac在处理泛型返回值自动装箱的时候遇到了问题。

看来javac在这里的检查比jdt更加严格。

我们只要把调用的代码改成不要让java自动装箱,就可以通过编译了:

Long value1 = get("akey");
Integer value2 = get("bkey");

2012年3月22日

URL中包含"\"导致apache和tomcat不能正确处理的问题


URL中包含"\"会导致apache和tomcat无法正确处理,比如访问/a\b时:
  • apache会返回404
  • tomcat则会返回400
如果希望apache能处理包含"\"的url,则需要做如下修改: 在conf中加上AllowEncodedSlashes On即可,默认为off,参见apache docs.

而tomcat之所以会返回400,是由于org.apache.catalina.connector.CoyoteAdapter中有这么一个配置

protected static final boolean ALLOW_BACKSLASH = Boolean.valueOf(System.getProperty("org.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH", "false")).booleanValue();

默认是不允许"\"的,当/a\b这个url进入tomcat的时候,CoyoteAdapter的normalize方法会返回false。 从而能导致CoyoteAdapter的service方法返回400:

// Normalization
if (!normalize(req.decodedURI())) {
    res.setStatus(400);
    res.setMessage("Invalid URI");
    connector.getService().getContainer().logAccess(request, response, 0, true);
    return false;
}

以下是normalize方法的说明:

Normalize URI. 

This method normalizes "\", "//", "/./" and "/../". This method will return false when trying to go above the root, or if the URI contains a null byte.

这是相关代码,完整实现可自行查看源码:

for (pos = start; pos < end; pos++) {
    if (b[pos] == (byte) '\\') {
        if (ALLOW_BACKSLASH) {
            b[pos] = (byte) '/';
        } else {
            return false;
        }
    }
    if (b[pos] == (byte) 0) {
        return false;
    }
}
因此,只要在tomcat启动参数中加上-Dorg.apache.catalina.connector.CoyoteAdapter.ALLOW_BACKSLASH=true就可以正常访问类似/a\b的url了。

2011年10月26日

rewrite模块和connector的URIEncoding对url解析的影响

如果url中包含非ASCII字符(比如中文),W3C建议对url使用UTF-8编码并把编码后的每个byte用%HH的形式表示:比如"啊&啊""就被编码成"%E5%95%8A%26%E5%95%8A",使用java的话,就是调用URLEncoder.encode("啊&啊", "UTF-8")。

如果url是/?tag=%E5%95%8A%26%E5%95%8A,那么request.getQueryString()返回tag=%E5%95%8A%26%E5%95%8A,request.getParameter("tag")返回啊&å(在iso-8859-1环境下就是显示???&???)

根据api文档,getQueryString返回的是 String containing the query string or null if the URL contains no query string. The value is not decoded by the container.这是对的。

getParameter返回的是乱码,这也是对的,在上一篇日志中写过,"啊&啊"被tomcat用iso-8859-1编码了。

但是当url应用重写规则并给connector配置URIEncoding的时候,又会出现什么情况呢?

重写规则为了能匹配规则,会对url进行decode,connector的URIEncoding也会影响url的解析。

假设有这么个重写规则:RewriteRule ^/tag/(.)$ /tag.do?tag=$1 [PT,L]*
我在win7+apache2.2+tomcat6.0.29下,加上jboss的rewrite模块,针对 /tag/%E5%95%8A%26%E5%95%8A(需要被rewrite)和/?tag=%E5%95%8A%26%E5%95%8A(无需rewrite)

测试了各种条件下getQueryString和getParameter的返回值:

观察以上数据,可以看出:

  1. 在需要rewrite的时候:使用apache和jboss的rewrite模块,没有本质区别,经过rewrite后,getParameter得到的值都是一样的。唯一的不同是ajp的URIEncoding为"UTF-8"时,getQueryString返回的值不同。这是因为当使用jboss的rewrite模块时,"/tag/%E5%95%8A%26%E5%95%8A"被转为"/tag/啊&啊"后再rewrite,getQueryString就返回"tag=啊&啊了"。
  2. 在不需要rewrite的时候:apache通过ajp传给tomcat是"tag=%E5%95%8A%26%E5%95%8A",只要AJP Connector的URIEncoding为"UTF-8",getParameter就可以得到正确的值
  3. 由于url都是apache通过ajp传给tomcat,因此这里http connector的配置不起作用

再来看一下乱码,"啊"变成"???":字节信息还是正确的,通过new String(value.getBytes("ISO-8859-1"), "UTF-8")就能获取原来的值

在实践中可以遵循以下规则:

  1. 如果url由我们控制,就把url调用URLEncoder.encode(url, "UTF-8"),然后把对应的connector的URIEncoding设置成"UTF-8"(apache+tomcat就设置ajp,仅tomcat就设置http)。如果url经过rewrite,需要我们自己处理QueryString,否则直接getParameter即可。
  2. 如果url是根据用户输入产生,浏览器可能对url采用非UTF-8编码,此时最好不要对url做rewrite,并自己处理QueryString。

2011年9月16日

Sublime Text 2的Tortoise插件中文路径问题

今天在使用Sublime Text 2的Tortoise插件的时候,由于svn的路径中有中文,导致Tortoise一直报错:

error: 'ascii' codec can't encode character in position 25-28: ordinal not in range(128)

调试发现是Tortoise.py中如下代码抛出的错误:

proc = subprocess.Popen(self.args, stdin=subprocess.PIPE,stdout=subprocess.PIPE, stderr=subprocess.STDOUT,startupinfo=startupinfo, cwd=self.cwd)

经过一番google,发现这应该是python的bug,具体分析请看subprocess.call fails with unicode strings in command line.
这个bug一直到python2.7都没有fix,而Sublime Text 2用的还是python2.6...

幸好这个页面有人提供了两个解决方案:

  1. 给subprocess.py打patch
  2. 给_subprocess.c打patch

我尝试了第一个patch,原本的修改如下:

--- Lib/subprocess.py.orig    2008-10-01 11:37:34.034500000 -0700  
 +++ Lib/subprocess.py    2008-10-01 12:35:10.018875000 -0700  
 @@ -766,11 +766,15 @@  
               startupinfo, creationflags, shell,  
               p2cread, p2cwrite,  
               c2pread, c2pwrite,  
 -              errread, errwrite):  
 +              errread, errwrite, sys=sys):  
        """Execute program (MS Windows version)"""  
        if not isinstance(args, types.StringTypes):  
          args = list2cmdline(args)  
 +      if isinstance(args, unicode):  
 +        args = args.encode(sys.getfilesystemencoding())  
 +      if isinstance(executable, unicode):  
 +        executable = executable.encode(sys.getfilesystemencoding())  
        # Process startup details  
        if startupinfo is None:  

但是使用这个patch还是报错,这是因为除了args和executable有可能是unicode的,cwd也有可能是unicode。
在这里,Tortoise插件传给subprocess的cwd就是中文路径,所以还要对cwd进行encode,因此还需要加上下面这段:

if isinstance(cwd, unicode):
    cwd = cwd.encode(sys.getfilesystemencoding())

Sublime Text 2是通过python26.zip中的subprocess.pyo来加载subprocess的,把修改过的subprocess.py编译成subprocess.pyo再替换就可以了.
当然直接删掉subprocess.pyo,再添加修改过的subprocess.py也没啥问题.
重启Sublime Text 2就可以顺利使用Tortoise插件了.

2011年7月23日

tumblr自定义风格系统的实现

要实现类似tumblr的自定义风格系统,首先需要一个模板引擎。 该模板引擎需要满足以下需求:

  1. 语法足够简单,最好只支持variable、block元素,不要支持循环。由于要对所有用户开放,支持循环的模板存在安全问题,可能遭到攻击,因为用户可以构造一个死循环的模板。

  2. 数据不通过${对象.属性}的方式来访问,这可能会造成普通用户理解困难,数据是单层的。比如日志标题就是${Title},而不是${post.title}

这样的话,freemarker、velocity虽然功能强大,却不符合上面两点,就不适合在这里使用了。

不过还是有很多轻量级的模板引擎可供选择的,比如MiniTemplator、Mustache,这两个引擎都有多种语言的实现。

其中MiniTemplator的语法更接近tumblr,稍加修改就能改成和tumblr一样的语法,正好符合我们的要求。MiniTemplatorde的解析部分基本可以直接使用,无需做太大修改。

由于tumblr模板的数据是单层的,因此同一个变量在不同的块级元素下可能需要填充不同的数据,比如${Title}在post的block中就是日志的标题,而在顶级的block中就是博客的标题。

MiniTemplator只提供了最基本的数据填充方法,因此需要自己实现一套处理上下文相关的数据填充框架。对这种数据的填充逻辑必然是和具体的数据定义耦合在一起的:

/**  
  * 数据填充接口  
  *  
  */  
 public interface Data {  
     /**  
     * 该block是否需要处理  
     * @param blockName  
     * @return  
     */  
     public boolean needProcessBlock(String blockName);  
     /**  
     * 填充该block数据  
     * @param xpath  
     * @param blockName  
     * @param blockNo  
     * @param miniTemplator  
     */  
     public void process(String xpath, String blockName, int blockNo, MiniTemplator miniTemplator);  
     /**  
     * 获取该变量的值  
     * @param key  
     * @return  
     */  
     public Object getValue(String key);  
 }  

process是个递归的过程,调用process("","",0,templator)就完成对模板的数据填充:

public void process(String xpath, String curBlockName, int curBlockNo, MiniTemplator miniTemplator) {  
     List blockNos = miniTemplator.getSubBlocks(curBlockNo);  
     for (int blockNo : blockNos) {  
         String blockName = miniTemplator.getBlockName(blockNo);  
         if (needProcessBlock(blockName)) {  
             process(xpath, blockName, blockNo, miniTemplator);  
         }  
     }  
     List variables = miniTemplator.getSubVariables(curBlockNo);  
     for (String variable : variables) {  
         miniTemplator.setVariable(variable, String.valueOf(getValue(variable)));  
     }  
     miniTemplator.addBlockByNo(curBlockNo);  
 }  

getValue是个类似回溯的过程,根据数据填充逻辑的复杂度,可以实现多个Data接口,比如GlobalData表示全局数据,PostData表示日志数据,PostData会保存对GlobalData的引用,调用PostData的getValue时,如果postData无法处理该值,就调用GlobalData的getValue。

Appearance Options是tumblr模板的一个特色,可以给模板的使用者提供一些选项,模板根据用户的设置会显示成不同的效果,比如就是一个条件值。

虽然minitemplator本身提供条件判断,但是如果把这些条件值在初始化minitemplator的时候传给TemplateSpecification,就会导致模板的结构和用户的数据交织在一起,这样每个用户都会有一个minitemplator的实例,给缓存模板带来麻烦。

因此还是使用和tumblt一样的语法,把这些参数都作为数据,和templator的解析区分开:
如果if:Show Likes的选项会真,就设置数据IfShowLikes为true,IfNotShowLikes为false,{block:IfShowLikes}{/block:IfShowLikes}中内容会被渲染,{block:IfNotShowLikes}{/block:IfNotShowLikes}中内容不会被渲染。

Appearance Options的定义也单独解析:

(?i)<meta\s+name=\"(color|font|if|image|text|customcss):([^"]+)"\s+content="([^"]*)"

只要再对MiniTemplator做一些扩展(主要是添加一些接口),这样就基本上实现tumblr的自定义风格模板系统,只要针对自己的需求添加一些处理各种块数据的逻辑就可以了。

使用这样的自定义风格系统,所有的数据都是在服务器端填充的,因此数据的加载速度是很重要的。
需要用一些方法来加速页面的加载:

  1. 服务器端对于相互间没有依赖的数据的获取,应该使用并行的方式来查询数据,这样可以成倍的减少加载时间。

  2. 对某些特殊的数据块,服务器不直接填充数据,而是填充js代码,当页面加载完成后,再异步加载这些数据并显示。

  3. 对某些用户数据,可是适当的使用本地缓存。

由于tumblr允许用户上传自己的html模板,因此防范恶意代码也是相当重要的。tumblr的cookie是放在www.tumblr.com下的,因此可以防止个人主页窃取cookie。

但是在个人主页加入xss代码似乎还是比较容易的。安全方面不熟悉,就不多说了。

2011年7月6日

切换无线网络连接脚本

由于公司网络使用DHCP,而自己家里是用静态IP的,每天手动切换很是麻烦,所以写了个批处理来切换网络连接。

公司的无线网络连上后,还要做web认证;家里网络连上后,还要连vpn,脚本顺便把这些都做了,直接开机启动即可。

wlan_config.bat:

@ECHO OFF
 @rem 家庭无线名称
 set home=xxx
 @rem 公司无线名称
 set office=yyy
 echo detecting current wlan config...
 setlocal enabledelayedexpansion
 netsh wlan show networks | findstr /C:"%home%" > nul
 IF !errorlevel! equ 0 (
     ipconfig /all | findstr /C:"192.168.0.100" > nul
     IF !errorlevel! equ 0 (
         echo already static ip, exit...
     ) ELSE (
         echo use static ip...
         call %~dp0home.bat
         echo connect %home%...
         netsh wlan connect %home%
     )
     echo connect vpn...
     call %~dp0vpn.bat
     echo done
 ) ELSE (
     netsh wlan show networks | findstr /C:"%office%" > nul
     IF !errorlevel! equ 0 (
         echo use dhcp...
         call %~dp0dhcp.bat
         echo connect %office%...
         netsh wlan connect %office%
         call %~dp0auth.bat
         echo done
     ) ELSE (
         echo unknow wlan, use dhcp default...
         call %~dp0dhcp.bat
         echo done
         pause
     )
 )  

其中用到了home.bat,dhcp.bat,vpn.bat,auth.bat,如果不需要执行某个bat,去掉调用的那句就可以了。
这些bat都要放在相同目录下。

home.bat(设置静态IP):

netsh interface ip set address "无线网络连接"  static 192.168.0.100 255.255.255.0 192.168.0.1 1
netsh interface ip set dns "无线网络连接"  static 202.101.172.35
netsh interface ip add dns "无线网络连接"  202.101.172.47

dhcp.bat(设置为DHCP):

netsh interface ip set address name="无线网络连接" source=dhcp
netsh interface ip set dns name="无线网络连接" source=dhcp register=PRIMARY

vpn.bat(vpn拨号):

rasdial vpn username pwd

auth.bat(web认证,这个脚本需要cygwin才能运行,因为用了curl):

@echo off
d:
chdir d:\cygwin\bin
curl -d "username=xxx&password=yyy" http://wireless-gateway.netease.com/login.html >nul

最后在启动里创建一个快捷方式指向wlan_config.bat就可以了。