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。