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了。