2014年4月10日

Java2D旋转图片边缘锯齿问题

在使用Java2D旋转图片时,如果旋转的角度不是45度的倍数,图片边缘会产生非常明显的锯齿。

旋转的代码如下:

public static BufferedImage rotate(BufferedImage image, double angle) {
    double sin = Math.abs(Math.sin(angle)), cos = Math.abs(Math.cos(angle));
    int w = image.getWidth(), h = image.getHeight();
    int neww = (int) Math.floor(w * cos + h * sin), newh = (int) Math.floor(h * cos + w * sin);
    BufferedImage result = new BufferedImage(neww, newh, BufferedImage.TYPE_INT_ARGB_PRE); 
    Graphics2D g = result.createGraphics();
    g.addRenderingHints(RENDER_HINT);
    g.translate((neww - w) / 2, (newh - h) / 2);
    g.rotate(angle, w / 2, h / 2);
    g.drawRenderedImage(image, null);
    g.dispose();
    return result;
}

其中已经加上了相应的RenderingHints:

private static HashMap<Object, Object> RENDER_HINT = new HashMap<Object, Object>();
static{
    RENDER_HINT.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    RENDER_HINT.put(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    RENDER_HINT.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    RENDER_HINT.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    RENDER_HINT.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
}

现在对下面这张图片旋转-10度

BufferedImage logo = ImageIO.read(new File("logo.png"));
BufferedImage rotated = rotate(logo, Math.toRadians(-10));

旋转后的图片如下,图片边缘的锯齿惨不忍睹

这里有个取巧的方法可以产生出效果比较好的图片,那就是在旋转前先处理一下图片,给图片加上1-3个像素的透明边框:

public static BufferedImage prerotate(BufferedImage image) {
    BufferedImage temp =
            new BufferedImage(image.getWidth() + 2, image.getHeight() + 2,
                    BufferedImage.TYPE_INT_ARGB_PRE);
    Graphics2D g = temp.createGraphics();
    g.addRenderingHints(RENDER_HINT);
    g.drawImage(image, 1, 1, null);
    g.dispose();
    return temp;
}

先调用一下prerotaterotate,产生的图片效果就好很多了:

2014年2月18日

给cab加数字签名

如果拿到的证书是pfx格式,首先需要生成pvk和spc文件
pfx生成pvk和spc的步骤如下:

  1. openssl pkcs12 -in xxx.pfx -nocerts -nodes -out xxx.key
  2. pvk -in xxx.key -topvk -out xxx.pvk
  3. openssl pkcs12 -in xxx.pfx -nokeys -out xxx.pem
  4. openssl crl2pkcs7 -nocrl -certfile xxx.pem -outform DER -out xxx.spc

然后运行signtool signwizard打开图形界面对cab签名,大致步骤如下:

  1. 选择要签名的cab文件
  2. 选择自定义
  3. 从文件选择 xxx.spc
  4. 磁盘上的私钥文件 xxx.pvk
  5. 选择sha1
  6. 之后一直下一步到完成即可

用到的pvk和signtool:https://dl.dropboxusercontent.com/u/1970520/share/signtool.7z

2013年11月24日

Android性能优化

最近学习了一下Android开发中的性能优化,文中列出的大部分优化技术都来自于对Best Practices for Performance《Android应用性能优化》的学习总结。

大家可能对服务器端的Java的性能优化都有比较丰富的经验了,但是由于移动设备的硬件性能还是远远落后于服务器的,因此很多在开发服务器上运行的应用时不需要考虑的问题到了移动设备上就变成了必须要关注的首要问题。今天主要介绍一下使用Java开发Android应用时所需要关注的一些性能方面的问题。

先来比较一下服务器和移动设备在硬件上的差异:
现在一台普通的服务器配置可能是:16核CPU,2.7G主频,48G内存
而前不久刚刚发布的Nexus 5的配置是:4核CPU,2.2G主频,2G内存,2300mAh电池

Nexus 5是目前相对很高端的Android设备了,但是和服务器的CPU和内存比起来就弱爆了。服务器CPU核心数是Nexus 5的4倍, 内存更是Nexus 5的24倍。因此一些在服务器上可能会碰到的问题比如因为heap太大导致GC过慢,在移动设备上是不太可能发生的。

再来看一下电池,Nexus 5配备了一块2300mAh的电池,可以有17小时的通话时间。尽管移动设备配备的电源容量越来越大,但终归是有限的容量,屏幕显示、网络传输都是耗电大户,如果APP太耗电,用户可能就直接卸载你的APP了。而服务器呢?一般情况下我们都不需要担心服务器的电源问题,有时可能需要考虑服务器断电,但是基本不会去想着如何为机房省电,而这在开发移动应用是必须要考虑的一个因素。

再来看看移动应用的运行环境。 Android设备的操作系统是Linux,因此可以使用各种语言来开发Android应用,今天我们主要关注使用Java开发的APP的运行环境。
对于使用Java开发的APP来说,程序也是运行在jvm中运行的,这个jvm是google为Android平台定制的,叫做dalvik。
当然从Android 4.4开始,引入了ART,在不久的将来预计会替代dalvik。ART会在应用安装时就把字节码编译成机器码,成为一个真正的本地应用,完全不需要虚拟机了。

不过今天还是先来了解一下dalvik,在4.4中,它依旧还是默认的运行环境。它有以下几个主要特点

  • 加载dex文件,java文件会首先编译成class,然后把所有class文件编译成一个dex文件,dex还可以进一步优化成odex文件
  • 由Zygote统一管理,多个dalvik进程可以共享一些framework的代码和资源
  • 支持并发GC(2.3+)
  • 支持JIT(2.2+)

和通常的jvm相比,dalvik指令集是基于寄存器的,而通常的jvm是基于栈的,因此dalvik具有以下优点

  • 编译时提前优化,dex整合了多个class文件中的重复信息,对冗余的部分做全局优化,使得dex体积更小
  • 执行更快

当然jvm也有自己的优点

  • 更好的可移植性,基于寄存器的指令集需要特定的硬件支持
  • 指令集简单

可以来看一下class和dex字节码的比较:

java:
public long add(long a, long b) {
  return a + b;
}

class:
0: lload_1
1: lload_3
2: ladd
3: lretuen

dex:
0df438:9b00 0305  |0000: add-long v0, v3, v5
0df43c:1000       |0002: return-wide v0

在开发应用时,大致可以从代码、内存、电源和布局这四个方面来对应用做相应的优化,代码和内存使用的优化,在服务器端开发时应该已经接触过不少了,而电源和布局优化则是在开发移动应用所特有的。

代码优化

  • 减少不必要的对象创建
    越少的对象创建,越少的GC。
    特别要避免Java的自动装箱,能使用基本类型完成的计算就不要使用对象。
  • 减少虚方法调用, 特别要避免类内部的getter/setter调用
    在类内部直接访问实例变量
    使用ProGuard可以在build时自动inline getter/setter调用
  • 用package访问级别代替private

    public class Foo {
    private class Inner {
        void a() {
            Foo.this.b(Foo.this.v);
        }
    }
    private int v;
    public void run() {
        Inner in = new Inner();
        v = 1;
        in.a();
    }
    private void b(int v) {}
    }

    在上面这个类中,尽管Java的语法允许在Foo的内部类Inner中直接引用Foo.this.bFoo.this.v的,但是在jvm中直接访问private属性或方法是非法的,因此编译生成的字节码会把直接引用替换成方法调用。
    首先Foo.classs会多出两个静态方法用于访问private属性和方法

    static int access$000(com.example.android_demo.Foo);
    Code:
    0:   aload_0
    1:   getfield        #2; //Field v:I
    4:   ireturn
    
    static void access$100(com.example.android_demo.Foo, int);
    Code:
    0:   aload_0
    1:   iload_1
    2:   invokespecial   #1; //Method b:(I)V
    5:   return

    然后在Inner中对private属性的访问和方法调用被替换成对以上两个方法的调用

    void a();
    Code:
    0:   aload_0
    1:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    4:   aload_0
    5:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    8:   invokestatic    #4; //Method com/example/android_demo/Foo.access$000:(Lcom/example/android_demo/Foo;)I
    11:  invokestatic    #5; //Method com/example/android_demo/Foo.access$100:(Lcom/example/android_demo/Foo;I)V
    14:  return

    这种代码即使编译成dex,也不能得到优化,会依然保留这些方法调用,从而降低性能。

    000794:                 |[000794] com.example.android_demo.Foo.Inner.a:()V
    0007a4: 5420 0100       |0000: iget-object v0, v2, Lcom/example/android_demo/Foo$Inner;.this$0:Lcom/example/android_demo/Foo; // field@0001
    0007a8: 5421 0100       |0002: iget-object v1, v2, Lcom/example/android_demo/Foo$Inner;.this$0:Lcom/example/android_demo/Foo; // field@0001
    0007ac: 7110 0b00 0100  |0004: invoke-static {v1}, Lcom/example/android_demo/Foo;.access$000:(Lcom/example/android_demo/Foo;)I // method@000b
    0007b2: 0a01            |0007: move-result v1
    0007b4: 7120 0c00 1000  |0008: invoke-static {v0, v1}, Lcom/example/android_demo/Foo;.access$100:(Lcom/example/android_demo/Foo;I)V // method@000c
    0007ba: 0e00            |000b: return-void

    对于以上这种情况,只要把private访问级别改成package访问就可以减少两次方法调用了。

    void a();
    Code:
    0:   aload_0
    1:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    4:   aload_0
    5:   getfield        #2; //Field this$0:Lcom/example/android_demo/Foo;
    8:   getfield        #4; //Field com/example/android_demo/Foo.v:I
    11:  invokevirtual   #5; //Method com/example/android_demo/Foo.b:(I)V
    14:  return
  • 利用多个核心
    现在很多移动设备的CPU核心数都已经是两个甚至四个了,对于一些计算工作可以让多个核心来完成,使UI线程响应更快。

内存管理

很多Android设备的内存已经是2G了,和个人电脑比起来也差不了多少,但是Android是没有虚拟内存的,这意味着应用更加容易导致内存不足,因此在使用内存时需要更加小心。

  • 监听onTrimMemory事件(API 14+)
    通过监听onTrimMemory或onLowMemory,可以在内存紧张时释放非关键性的资源比如缓存等,降低应用被kill的几率
  • 优化图片加载
    从Android 3.0开始,图片需要的内存是在虚拟机的heap里分配的,从而能够被更好的被垃圾收集器管理,提高了性能。
    但是在显示图片时还是需要对图片做一些处理,比如裁减图片到合适尺寸用于显示,使用LRU缓存最近显示的图片,使用引用计数管理图片的使用情况等。
    具体可以参考Displaying Bitmaps Efficiently了解如何在Android中更好的显示图片。
  • 使用优化过的容器类
    在key是int或long型的时候,使用SparseArray和LongSparseArray替代HashMap,因为HashMap的key必须是对象,Java的自动装箱会产生大量对象。
  • 使用恰当的数据类型
    目前大部分Android设备的CPU都是32位的,使用int产生的指令会比使用short和long的更少。
    short类型的数组排序会比其他类型的快一个数量级,因为使用了计数排序
  • 减少抽象
    抽象可以带来灵活性和可维护性,但是抽象会额外的增加类层次和代码量,程序代码也是要加载到内存中占用内存的。因此代码越精简,占用的内存就越少。
  • 不要使用依赖注入
    依赖注入在web框架中使用的非常广泛,但是使用了依赖注入的框架需要更久的初始化,在初始化时需要扫描更多的类,即使这些类并未被使用,在扫描时也会被载入到内存中,从而导致更多的内存占用。
  • 使用StrictMode(2.3+)在开发时检查内存泄漏,耗时调用等
  • 使用ProGuard在build时清理、混肴代码,减少代码体积

电源管理

  • 获取电源信息
    SDK提供了多种方法用来获取电源信息,比如是否正在充电,充电方式是USB还是AC适配器,以及当前电量等。
    应用可以监听自己感兴趣的事件,比如当连接上外部电源的时候,可以加大刷新数据的频率、加大传输速率等;当电量很少的时候,减少数据更新的频率。
  • 管理广播接收器
    应用不在前台的时候,很多事件都不需要处理,应该及时禁用广播接收器,重新回到前台时再重新打开接收器
  • 检查网络
    不同网络环境的传输速率差别非常大,GPRS可能只有100Kb每秒,而Wi-Fi可以达到几十M每秒。
    同样一个文件,在GPRS下下载会比在Wi-Fi下下载耗费更多的电量,因此在显示图片时,可以根据网络环境的不同下载不同压缩比的图片。
    对于实时的搜索请求,也可以根据网络的不同调整请求的频率。
  • 定位服务,传感器使用
    使用恰当的通知频率,权衡用户体验和省电
  • WakeLock
    播放视频时使用带超时的WakeLock,防止WakeLock没有被关闭

布局优化

  • 减少布局层次
    过深的布局层次会严重影响应用的性能,在低版本的系统上可能就直接崩溃了。可以从几个方面来减少布局层次

    • 嵌套的LinearLayout会深化布局层次,特别是LinearLayout只是用来起定位的话就更加没有意义,应该合理的使用RelativeLayout取代LinearLayout。
    • 使用Merge合并布局,使用merge取代顶层的Layout,可以减少一层布局

    下面来看一个例子,对于如下如下的界面

    先来看一下使用LinearLayout实现的布局

    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    <LinearLayout android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">
            <TextView android:text="title2"
                      android:gravity="center"
                      android:layout_width="match_parent"
                      android:layout_height="wrap_content"></TextView>
            <LinearLayout android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="horizontal">
                <EditText android:id="@+id/edit"
                          android:layout_width="0px"
                          android:layout_height="wrap_content"
                          android:layout_weight="0.82"></EditText>
                <Button android:text="btn"
                        android:layout_width="0px"
                        android:layout_height="wrap_content"
                        android:layout_weight="0.18"></Button>
            </LinearLayout>
    </LinearLayout>
    </RelativeLayout>

    直观的布局层次可以看下图

    现在来看一下使用RelativeLayout和Merge优化后的布局:

    <merge xmlns:android="http://schemas.android.com/apk/res/android">
    <RelativeLayout android:layout_width="match_parent"
                  android:layout_height="wrap_content">
        <TextView android:text="title1"
                  android:gravity="center"
                  android:id="@+id/text"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"></TextView>
        <Button android:id="@+id/btn"
                android:text="btn"
                android:layout_width="60dp"
                android:layout_height="wrap_content"
                android:layout_below="@id/text"
                android:layout_alignParentRight="true"></Button>
        <EditText android:id="@+id/edit"
                  android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_below="@id/text"
                  android:layout_toLeftOf="@id/btn"></EditText>
    </RelativeLayout>
    </merge>

    可以看到布局从原来的四层减少到了两层

    再来比较一下这两个布局的渲染时间

    可以看到Measure和Layout的性能有了显著提升,Measure的时间从原来的6.2ms减少到了0.8ms,可见在使用LinearLayout时,用于Measure的时间是相当可观的,Layout的时间也从0.2ms减少到了0.06ms。

  • 重用布局 和代码一样,对于可以重用的布局,也应该尽量使用include重用这些布局,减少冗余的布局代码

  • ViewStub
    使用ViewStub可以实现view的延迟加载,ViewStub引入的布局既不会显示也不会占用位置,可以在解析layout时节省cpu和内存从而提高性能

  • ViewHolder
    在ListView中,系统会重用每个item的view,在ViewHolder中保持对控件的引用可以避免每次都去查找

以上列出的这些优化技巧,虽然大部分都只能带来一些微小的性能提升,但是如果用在日常的实践中,积少成多,也会对性能有很大的影响。

但是由于业务逻辑的复杂性,应用往往不可避免的还会出现性能瓶颈。此时就需要一些额外的工具来帮助我们分析应用的性能瓶颈, 再来做针对性的优化。
目前Android平台主要提供了如下3个工具用于分析应用的性能瓶颈:
1. Traceview
2. Systrace
3. Oprofile
Android系统性能调优工具介绍这篇文章中比较全面的介绍了这三个工具的基本用法。

以上主要介绍的是一些通用的性能优化点,由于Android的硬件环境非常多样,拿屏幕尺寸来说,现在的各种盒子主要是连接到电视机上的,屏幕可以大到四、五十寸,将来可能非常流行的各种可穿戴设备如手表、眼镜等,屏幕可能不到1英寸,因此针对不同的设备,也有必要需要做一些特殊的优化。比如开发盒子应用时就可以参考Android Developers:针对电视优化布局

2013年5月20日

IntelliJ配置tomcat遇到的一些问题

IntelliJ是个很优秀的IDE,对Clojure的支持相当不错,Theme也很喜欢。

最近想把Java的开发环境也从Eclipse切换到IntelliJ,经过一番尝试,觉得它对tomcat的集成跟Eclipse比起来还是有很大差距的,目前还不适合开发基于Tomcat的Web工程(当然也有可能是我没找到正确的方法)。

先来看一下在Eclipse中配置Tomcat,一般有两种方式:

  1. com.sysdeo.eclipse.tomcat
    这是个插件,需要自己安装,目前版本是3.2.1,感觉用了好多年了,配置简单,非常好用。
    优点是对非静态方法里的代码的修改直接生效,不需要反复重启。
    缺点是不能运行多个tomcat实例(貌似只能指定一个CATALINA_BASE)。
  2. Eclipse自带的Server面板
    相当于为每个Server都配置了单独的CATALINA_BASE,因此可以同时启动多个tomcat实例。但是要额外的执行一些deploy步骤,没有插件方便。
    如果要同时调试多个工程,就只能用这种方式了。

现在来看一下IntelliJ的不足:

  1. 不能配置ajp
    本地的环境都是apache通过ajp连tomcat的,但是IntelliJ的tomcat的配置里居然不可以指定ajp端口,每次启动都是随机端口,这是最不能让人接受的。
    除非在启动参数里指定CATALINA_BASE,才会加载对应的server.xml。以这种方式启动的话是不支持热部署的(貌似可以使用JRebel,不过需要license),并且这样也无法运行多个tomcat实例。

  2. tomcat无法停止
    默认使用cataline.bat的stop来停止tomcat,但对很多工程其实是无法成功的。

目前IntelliJ的版本是12.1,希望以后的版本至少能支持ajp配置。

2013年4月27日

识别字符串编码是GBK或UTF-8

public static String guess(byte[] bytes) {
        try {
            String guess = new String(bytes, "gbk");
            String verify = new String(new String(bytes, "utf-8").getBytes("utf-8"), "gbk");
            if (!verify.equals(guess)) {
                return guess;
            } else {
                return new String(bytes, "utf-8");
            }
        } catch (UnsupportedEncodingException ignore) {}
        return new String(bytes, Charset.forName("utf-8"));
    }

一段猜测字符串的编码的代码,如果不是GBK,就认为是UTF-8。

String str = "参数test";
ByteBuffer gbkbb = Charset.forName("gbk").encode(str);
byte[] gbkbytes = new byte[gbkbb.remaining()];
gbkbb.get(gbkbytes);
ByteBuffer utf8bb = Charset.forName("utf-8").encode(str); byte[] utf8bytes = new byte[utf8bb.remaining()]; utf8bb.get(utf8bytes);
System.out.println(guess(gbkbytes).equals(guess(utf8bytes)));

输出true,测试通过。

不保证100%正确。

2013年3月14日

Google Reader


From your 603 subscriptions, over the last 30 days you read 1,282 items, clicked 3 items, starred 11 items, and emailed 0 items.

Since 17 November 2008 you have read a total of 182,402 items.

Google Reader, 留念一下。

2012年12月7日

Jackson处理json的一些常见使用

Jackson是一个功能强大的Java序列化库。除了支持常用的json,同时还支持Smile,BSON,XML,CSV,YAML。
接下来就介绍一些处理json时常见的使用场景,文中的例子都是在1.9版本下运行的。
Jackson的json库提供了3种API:

  • Streaming API : 性能最好
  • Tree Model : 最灵活
  • Data Binding : 最方便

其中最常用到的就是Data Binding了,基本的用法如下

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(foo);
Foo foo = mapper.readValue(json, Foo.class);

ObjectMapper是线程安全的,应该尽量的重用。
需要注意的是,Jackson是基于JavaBean来序列化属性的,如果属性没有GETTER方法,默认是不会输出该属性的。

但是在序列化的时候,经常会有特殊的需求来对输出的结果进行自定义。
比如不输出某几个属性,或者自定义属性的名字,等等。
Jackson提供了非常多的方法来满足我们的自定义需求。

假设有这么一个对象:

class User {
        private long id;
        private String name;
        private String avator240;
        private String avator160;
        private String address;
        public long getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        public String getAvator240() {
            return avator240;
        }
        public String getAvator160() {
            return avator160;
        }
        public String getAddress() {
            return address;
        }
    }

如果不想输出id,最简单的方法,就是给该属性加上注解JsonIgnore:

@JsonIgnore
private long id;

或者

@JsonIgnore
public long getId() {
    return id;
}

因为JsonIgnore的target可以是CONSTRUCTOR, FIELD, METHOD

如果不想输出多个属性,比如idaddressavator160,除了在每个属性上添加JsonIgnore,也可以直接在类上添加注解JsonIgnoreProperties:

@JsonIgnoreProperties({"id","avator160","address"})
class User {

这里的User类只有5个属性,使用annotation控制忽略哪些属性还是绰绰有余的。
加入有一个类有上百个属性,如果只想输出其中的10来个属性,使用JsonIgnore就显得太繁琐了。
此时就可以使用JSON View或MixIn Annotation了。

先来看一下JSON View,和数据库的view一样,可以为一个对象创建view,输出时只会输出view中定义的那些属性。 特别的,一个对象可以定义任意多个view,同时view也是可以继承的。
先来看看如何使用view来过滤idaddressavator160

public class JsonViewDemo {
    private static class User {
        private long id;
        @JsonView({FilterView.Output.class})
        private String name;
        @JsonView({FilterView.Output.class})
        private String avator240;
        private String avator160;
        private String address;
        public long getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        public String getAvator240() {
            return avator240;
        }
        public String getAvator160() {
            return avator160;
        }
        public String getAddress() {
            return address;
        }
    }

    private static class FilterView {
        static class Output {}
    }

    public static void main(String[] args) throws Exception {
        User user = new User();
        user.id = 1000L;
        user.name = "test name";
        user.avator240 = "240.jpg";
        user.avator160 = "160.jpg";
        user.address = "some address";

        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
        System.out.println(mapper.writerWithView(FilterView.Output.class).writeValueAsString(user));
    }
}

首先需要定义一个需要输出的属性的View:FilterView.Output,然后在需要输出属性上声明该View,之后使用writerWithView(FilterView.Output.class)来序列化就可以了。
需要注意的是,在这里需要把DEFAULT_VIEW_INCLUSION设置为false,因为默认是会输出没有JsonView注解的属性的。

其实View的作用远不止如此,再来看一个更实用的例子: 假设现有个API接口,需要针对不同的客户端(ios,android)输出不同的属性,通过创建多个View就能轻松完成。

public class JsonApiViewDemo {

    private static class User {
        private long id;

        @JsonView({ApiView.Default.class})
        private String name;

        @JsonView({ApiView.Ios.class})
        private String avator240;

        @JsonView({ApiView.Android.class})
        private String avator160;

        private String address;

        public long getId() {
            return id;
        }
        public String getName() {
            return name;
        }
        public String getAddress() {
            return address;
        }
        public String getAvator240() {
            return avator240;
        }
        public String getAvator160() {
            return avator160;
        }

    }

    private static class ApiView {
        static class Default {}
        static class Ios extends Default {}
        static class Android extends Default {}
    }

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);

        User user = new User();
        user.id = 10000L;
        user.name = "test name";
        user.avator240 = "240.jpg";
        user.avator160 = "160.jpg";
        user.address = "some address";

        String apiViewJson = mapper.writerWithView(ApiView.Default.class).writeValueAsString(user);
        String iosViewJson = mapper.writerWithView(ApiView.Ios.class).writeValueAsString(user);
        String androidViewJson = mapper.writerWithView(ApiView.Android.class).writeValueAsString(user);

        System.out.println(apiViewJson);
        System.out.println(iosViewJson);
        System.out.println(androidViewJson);

    }

}

使用ApiView.Ios只会输出nameavator240
使用ApiView.Android只会输出nameavator160

但是,以上的所有方法都有一个缺点,那就是需要修改源代码,它们都需要在要输出的类上加上annotation。
假设没有那些要序列化的类的源代码,甚至那些类都不符合JavaBean规范,又该怎么办呢? 此时就可以使用MixIn Annotation了,其实和View差不多,也相当于是为要序列化的对象定义了一个View。

public class JsonMixInDemo {
    static class User {
        private long id;
        private String name;
        private String avator240;
        private String avator160;
        private String address;

        public long getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public String getAddress() {
            return address;
        }

    }

    abstract class MixIn {
        @JsonIgnore abstract int getAddress();

        @JsonIgnore long id;

        @JsonProperty("custom_name") abstract String getName();

        @JsonProperty("avator") String avator240;
    }


    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User();
        user.id = 1234567L;
        user.name = "test name";
        user.avator240 = "240.jpg";
        user.avator160 = "160.jpg";
        user.address = "some address";

        mapper.getSerializationConfig().addMixInAnnotations(User.class, MixIn.class);
        String json = mapper.writeValueAsString(user);
        System.out.println(json);

    }
}

将输出

{"custom_name":"test name","avator":"240.jpg"}

其中关键在于MixIn这个类,MixIn也可以定义成接口。
在这里,既可以过滤属性/方法,也可以定义哪些属性/方法会被输出,顺便还可以自定义输出的属性名。
在序列化前只要配置一下

addMixInAnnotations(User.class, MixIn.class)

就可以在完全不修改该类的情况下自定义输出了。

MixIn Annotation应该能满足几乎所有需要对属性进行自定义的情况了,但是MixIn Annotation的配置是静态的,不能在运行时修改。
结合JSON Filter和Mixin就可以实现动态的过滤属性了

public class JsonFilterDemo {

    private static class User {
        private long id;
        private String name;
        private String avator240;
        private String avator160;
        private String address;

        public String getName() {
            return name;
        }
        public String getAddress() {
            return address;
        }
        public String getAvator240() {
            return avator240;
        }
        public String getAvator160() {
            return avator160;
        }
        public long getId() {
            return id;
        }
    }

    @JsonFilter("userFilter")
    private static interface UserFilterMixIn
    {

    }

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User();
        user.id = 1000L;
        user.name = "test name";
        user.avator240 = "240.jpg";
        user.avator160 = "160.jpg";
        user.address = "some address";


        FilterProvider idFilterProvider = new SimpleFilterProvider().addFilter("userFilter", SimpleBeanPropertyFilter.filterOutAllExcept(new String[]{"name", "avator240"}));
        mapper.setFilters(idFilterProvider);
        mapper.getSerializationConfig().addMixInAnnotations(User.class, UserFilterMixIn.class);
        String userFilterJson = mapper.writeValueAsString(user);

        System.out.println(userFilterJson);
    }
}

前面介绍了很多自定义输出属性的方法,如果需要在序列化时修改值,要怎么办呢?
只要实现自己的JsonSerializer就可以了,下面这个例子就会输出id的md5值

public class JsonCustomSerializerDemo {
    static class User {
        @JsonSerialize(using = Md5IdSerializer.class)
        private long id;
        private String name;
        private String address;

        public long getId() {
            return id;
        }

        public String getName() {
            return name;
        }

        public String getAddress() {
            return address;
        }

    }

    public static class Md5IdSerializer extends JsonSerializer {

        public void serialize(Long value, JsonGenerator generator, SerializerProvider provider)
                throws IOException, JsonProcessingException {
            generator.writeString(md5(value));
        }

        private String md5(Long value) {
            return value + "-md5-mock";
        }

    }

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();

        User user = new User();
        user.id = 1234567L;
        user.name = "test name";
        user.address = "some address";

        String json = mapper.writeValueAsString(user);
        System.out.println(json);

    }
}

接下来看一下反序列化,现在很多网站都开放了api接口,支持json格式的返回。
比如在调用了某个api后,需要解析返回的json数据获取信息,这种情况下为json创建一个对应的类是很不方便的。
此时使用Tree Model来解析json就比较方便了,下面这段代码就是用来解析人人网的用户信息的

JsonNode root = mapper.readTree(rerenjson);
JsonNode user = root.get("user");
String id = user.get("id").asText();
String name = user.get("name").asText();
JsonNode avators = user.get("avatar");
if (avators.isArray()) {
    for (Iterator it = avators.getElements(); it.hasNext(); ){
        JsonNode avator = it.next();
        if ("tiny".equals(avator.get("type").asText())) {
            String ava = avator.get("url").asText();
            break;
        }
    }
}

最后列一些使用Jackson的最佳实践:

  • 重用重量级对象: ObjectMapper, JsonFactory
  • 序列化性能(从高到低): OutputStream > Writer > writeValueAsString
  • 反序列化性能(从高到低): byte[] > InputStream > Reader
  • 用更轻量ObjectReader/ObjectWriter替代ObjectMapper
  • 及时关闭JsonParser, JsonGenerator

上面这些tips都摘自https://github.com/FasterXML/jackson-docs/wiki/Presentation:-Jackson-Performance,上面还有更多tips。

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就可以了。

2011年6月9日

tumblr和blogger的自定义风格模板

tumblr和blogger都具有高度可定制化的,可扩展的风格模板系统,允许用户制作十分精美的风格模板。

下面就简单介绍一下这两套模板系统。

tumblr的自定义风格模板的语法非常简单,只有两种类型:

  1. Variables是需要被填充的动态数据
  2. Blocks表示块级的结构,在不同的情况下会有不同的语义

    • 大部分时候都是条件判断(隐式或显式的)。
      • {Block:Text},{block:PermalinkPage}就是隐式的判断,{Block:Text}表示如果日志是Text类型的,就渲染{Block:Text}里的内容,{block:PermalinkPage}表示如果当前是单日志页面,就渲染{block:PermalinkPage}里的内容
      • {block:IfFlickrUsername}就是显式的判断,Flickr Username是模板作者自定义的变量,允许使用模板的用户自定义。
    • 对某些特殊的block,起了循环的作用,{block:Posts}就会循环渲染多篇日志

对于下面这个模板片段

{block:Posts}  
     {block:Text}  
         ${Content}  
     {/block:Text}  
     {block:Photo}  
         ${LinkURL}  
     {/block:Photo}  
 {/block:Posts}  

用freemarker来写就是这样:

<#list post as posts>  
     <#if post.type=='text'>  
         ${post.content}  
     <#elseif post.type=='photo'>  
         ${post.linkURL}  
     </#if>  
 </#list> 

对普通用户来说,应该会觉得tumblr的模板更容易上手,因为从模板上看不到条件判断、循环什么的,都被隐含在block中了。

tumblr还允许模板作者提供扩展点,比如上面提到的Flickr Username,只要在head中定义,这个值就会出现在Appeareance菜单中,使用该风格的用户就可以提供必要的值,或者覆盖默认值。 比如定义:

<meta name="image:Background" content="">
<meta name="text:Disqus Shortname" content="" />
<meta name="if:Enable endless scrolling" content="1">
就会在apearance菜单中出现以下选项:

tumblr的模板系统对国际化的支持也相当的好,{lang:Follow me}就可以在使用不同语言的用户博客显示相应的文本。

但是tumblr文档有个缺点,某些部分的描述不清晰:
比如在示例中定义了<meta name="image:Header" content=""/> 使用{block:IfHeaderImage}判断是否有值

定义<meta name="text:Flickr Username" content=""/> 却使用{block:IfFlickrUsername}判断是否有值。

不够统一,也没找到文档在哪里有说明。也有可能是历史原因造成的兼容性问题。

而blogger的风格模板的语法相对比较复杂,样式使用<b:skin>,html结构使用<b:section>,<b:widget>,这里的“b”应该就是block的缩写。

<b:skin>块中可以定义Variable,多个Variable可以属于一个group。这里的Variable就相当于tumblr中在meta中定义的扩展点。

blogger提供了更加高级的所见即所得的模板编辑器,这些定义的Variable就会出现在模板编辑器中给风格的使用者修改。

<Group description="Backgrounds" selector=".body-fauxcolumns-outer">  
    <Variable name="body.background.color" description="Outer Background" type="color" default="#296695" value="#296695"/>  
    <Variable name="header.background.color" description="Header Background" type="color" default="transparent" value="transparent"/>  
    <Variable name="post.background.color" description="Post Background" type="color" default="#ffffff" value="#ffffff"/>  
 </Group>

就会在模板编辑器中出现这些选项:

<b:section>,<b:widget>则用来定义页面元素,widget需要包含在section中,section是widget的容器。

页面语法元素有以下几种:

  • Includes:定义widget里的具体结构
  • Data:需要填充的数据,相当于tumblr中的变量,使用obj.properties的方式来访问,比如<data:photo.url/>
  • Loops:循环
  • If/Else:条件判断

用blogger的模板来写tumblr的这个片段:

{block:Posts}
    ${Content}
    {block:IndexPage}
        {block:NoteCount}${NoteCount}{/block:NoteCount}
    {/block:IndexPage}
{/block:Posts}

就是:

<b:section>  
     <b:widget type='Blog'>  
         <b:loop var='post' values='posts'>  
             <b:includable id='post' var='post'>  
                 <data:post.body/>  
                 <b:if cond='data:blog.pageType == "index"'>  
                     <b:if cond='data:post.numComments >0'><data:post.numComments/></b:if>  
                 </b:if>  
             </b:includable>  
         </b:loop>  
     </b:widget>  
 </b:section>

写法上显然是tumblr的比较简洁。

但是可读性是blogger的较好,如果完全不看各自的文档,还是大概能明白blogger模板的逻辑,但tumblr的模板就很难理解了。

比较一下模板的数量,tumblr到现在差不多有900套风格,数量每天都在增加,而blogger只有区区十几套风格,tumblr的风格系统显然更加活跃,这和他模板语法的简单性,降低了普通用户的学习门槛有很大关系的。打个比方的话,tumblr的模板系统就是jquery,而blogger的模板系统就是YUI。

由于两者都允许用户上传javascript,因此安全性是必须要考虑的。

tumblr的做法是把认证cookie设到www.tumblr.com下,而自定义的风格都是只在个性化域名下才起作用,比如xxx.tumblr.com,从而阻止xss。

在tumblr上传一套原创风格貌似是需要人工审核的,这可以保证public的风格不会存在恶意代码。

另外技术上tumblr也会限制iframe的使用,但是用户还是很容易的通过修改自己主页的代码给自己的主页加上恶意代码。

blooger的安全机制不清楚。

2011年5月13日

SSLHandshakeException问题

这两天发现服务器上使用httpclient访问https的网站时会抛出下面这个异常:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target  
    at com.sun.net.ssl.internal.ssl.Alerts.getSSLException(Alerts.java:174)

查了一下,都说是证书没有导入导致的,可是以前使用是好的,只是最近某一天开始才出现这个问题。

其实这时候就应该想到是证书过期了,但是以前没接触过这类问题,所以还是继续查看日志。发现一批旧的机器才有问题,新机器却是好的。猜测是caserts的问题,在旧机器上指定 -Djavax.net.ssl.trustStore=cacerts-new 果然好了。

解决证书过期,直接的办法就是升级jre,如果条件不允许的话,直接使用最新的cacerts替换旧的就可以了。

cacerts文件在java安装目录的security目录里。 当然如果是证书不存在,就是另外一回事了,需要手工导入。

2011年3月22日

cygwin编译python3

因为目前用Cygwin在线安装的python还是2.6的,想要在Cygwin里使用python3的话,就需要自己编译python3了。

以下就是编译的过程,主要是make会报一个错,解决掉就ok了:

  1. 下载源代码后解压
  2. 运行./configure --enable-shared --with-wide-unicode
  3. 运行make,make会报一个错误退出:

    make: *** No rule to make target `libpython3.2mu.dll.a', needed by `python.exe'. Stop.

    此时make已经在当前目录下生成了一个libpython3.2mu.a,只要复制这个文件并重名为libpython3.2mu.dll.a,然后重新执行make即可。

  4. 运行make install就安装好了

  5. 最后在.bash_profile加上
    alias python3=/usr/local/bin/python3.2mu export python3

打开Cygwin后运行python3就进入交互模式了。

2011年1月31日

xhtmlrenderer死循环问题

今天使用xhtmlrenderer时发现它在css背景图片不存在时存在bug,程序在渲染这种html时会死循环。

调试发现问题出在org.xhtmlrenderer.render.AbstractOutputDevicepaintTiles方法(paintVerticalBand和paintHorizontalBand 也有一样问题):

private void paintTiles(FSImage image, int left, int top, int right, int bottom) {  
     int width = image.getWidth();  
     int height = image.getHeight();  
     for (int x = left; x < right; x+= width) {  
         for (int y = top; y < bottom; y+= height) {  
             drawImage(image, x, y);  
         }  
     }  
 }  

当图片不存在时(比如404),此时会提供一个默认的Image对象org.xhtmlrenderer.swing.AWTFSImage.NULL_FS_IMAGE, 这个对象的width和height都是0.当把它作为参数传给paintTiles时,就死循环了。

找到原因就很好解决了,只要在paint前判断一下,如果是NULL_FS_IMAGE,直接return即可。

core-renderer-repack.jar是我重新打的包。

今天还在The Perils of Image.getScaledInstance()找到一个提高图片压缩质量的方法,用着还不错:

/**  
  * Convenience method that returns a scaled instance of the  
  * provided {@code BufferedImage}.  
  *  
  * @param img the original image to be scaled  
  * @param targetWidth the desired width of the scaled instance,  
  *  in pixels  
  * @param targetHeight the desired height of the scaled instance,  
  *  in pixels  
  * @param hint one of the rendering hints that corresponds to  
  *  {@code RenderingHints.KEY_INTERPOLATION} (e.g.  
  *  {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},  
  *  {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},  
  *  {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})  
  * @param higherQuality if true, this method will use a multi-step  
  *  scaling technique that provides higher quality than the usual  
  *  one-step technique (only useful in downscaling cases, where  
  *  {@code targetWidth} or {@code targetHeight} is  
  *  smaller than the original dimensions, and generally only when  
  *  the {@code BILINEAR} hint is specified)  
  * @return a scaled version of the original {@code BufferedImage}  
  */  
 public BufferedImage getScaledInstance(BufferedImage img,  
                     int targetWidth,  
                     int targetHeight,  
                     Object hint,  
                     boolean higherQuality)  
 {  
   int type = (img.getTransparency() == Transparency.OPAQUE) ?  
     BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;  
   BufferedImage ret = (BufferedImage)img;  
   int w, h;  
   if (higherQuality) {  
     // Use multi-step technique: start with original size, then  
     // scale down in multiple passes with drawImage()  
     // until the target size is reached  
     w = img.getWidth();  
     h = img.getHeight();  
   } else {  
     // Use one-step technique: scale directly from original  
     // size to target size with a single drawImage() call  
     w = targetWidth;  
     h = targetHeight;  
   }  
   do {  
     if (higherQuality && w > targetWidth) {  
       w /= 2;  
       if (w < targetWidth) {  
         w = targetWidth;  
       }  
     }  
     if (higherQuality && h > targetHeight) {  
       h /= 2;  
       if (h < targetHeight) {  
         h = targetHeight;  
       }  
     }  
     BufferedImage tmp = new BufferedImage(w, h, type);  
     Graphics2D g2 = tmp.createGraphics();  
     g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);  
     g2.drawImage(ret, 0, 0, w, h, null);  
     g2.dispose();  
     ret = tmp;  
   } while (w != targetWidth || h != targetHeight);  
   return ret;  
 }  

2010年12月24日

浏览器预览本地图片

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">  
 <html xmlns="http://www.w3.org/1999/xhtml">  
 <head>  
 <script>  
 var __userAgent = window.navigator.userAgent;  
 isIE = /msie\s+(.*?)\;/i.test(__userAgent);   
 isFF = !isIE&&/rv\:(.*?)\)\s+gecko\//i.test(__userAgent);  
 function imageChange() {  
     var file = document.getElementById('__file__');  
     var _fileurl = file.value;  
     if (typeof FileReader != 'undefined' && !!FileReader) {  
     //html5  
       var reader = new FileReader();   
           reader.readAsDataURL(file.files[0]);    
           reader.onload = preview;  
           reader.onerror = function(evt){console.log("load fail:[error="+evt.target.error.code+"]")};  
     } else {  
         if (isIE) {  
             var _version = getInternetExplorerVersion();  
             if (_version >= 7.0) {  
                 file.select();  
                   try{  
                   _fileurl = document.selection.createRange().text;  
                   if (!_fileurl || _fileurl == '') {  
                       _fileurl = file.value;  
                   }  
                   } finally { document.selection.empty(); }  
             }  
         }   
         if (isFF){  
             //不支持html5的低版本ff  
             _fileurl = file.files[0].getAsDataURL();  
         }  
           preview(_fileurl);  
     }  
 }  
 function preview(fileurl){  
     var img = document.getElementById('__image__');  
     if (typeof FileReader != 'undefined' && !!FileReader) {  
         fileurl = fileurl.target.result;  
     }   
     if (!isImage(fileurl)){  
         alert('请选择正确的图片文件!'); return;  
     }  
     img.src = fileurl;  
 }  
 function isImage(url) {  
     return /^data:image|\.(?:jpg|png|jpeg|gif|bmp)\s*$/i.test(url);  
 }  
 function getInternetExplorerVersion() {  
     var rv = -1; // Return value assumes failure.  
     if (navigator.appName == 'Microsoft Internet Explorer') {  
         var ua = navigator.userAgent;  
         var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");  
         if (re.exec(ua) != null)  
              rv = parseFloat( RegExp.$1 );  
     }  
     return rv;  
 };  
 </script>  
 <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />  
 </head>  
 <body>  
 <input type="file" id="__file__" onchange="imageChange()" onpropertychange="imageChange()"/>  
 <div><img id="__image__" src=""></div>  
 </body>  
 </html>

不支持safari和opera。

在chrome中直接打开html文件不能加载图片,会报NOT_READABLE_ERR,必须通过服务器来访问。

2010年12月4日

在sping中获取自身的代理

假设有这么一个类

public class A{
  func1(){}
  func2(){}
}

func1加了事务的aop,并且func2需要通过proxy来调用func1,那func2如何拿到A的proxy呢?

一个办法是实现BeanFactoryAware接口,另一个办法就是这样

A self = this;  
 try {  
     A proxy = (A) AopContext.currentProxy();  
     if (proxy != null) {  
         self = proxy;  
     }  
 } catch (IllegalStateException e) {  
     e.printStackTrace();  
 }  

然后在bean的配置中加上 <property name="exposeProxy" value="true" />

这个方法和第一种办法比起来,和spring的耦合少一点。