Jackson雕虫技

Jackson雕虫技(一)

Tag: jackson

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://www.blogbus.com/dreamhead-logs/230244031.html

Jackson是一个高性能JSON处理器,堪称目前Java世界处理JSON的首选。在Moco里,JSON文件的处理就是用它来做的。下面就以Moco处理中用到几点技巧介绍一下Jackson。

读取对象

采用Jackson的databind包,可以很容易地将一个JSON流映射为一个Java对象。下面是一个JSON流:

{
  "request": {
    "text": "foo"
  },
  "response": {
    "text": "bar"
  }
}

我们要把它映射为下面的对象:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class SessionSetting {
  private RequestSetting request;
  private ResponseSetting response;
  ...
}

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class RequestSetting {
  private String text;
  ...
}

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class ResponseSetting {
 private String text;
  ...
}

通过下面这段代码就可以完成映射:

  ObjectMapper mapper = new ObjectMapper();
  SessionSetting setting = mapper.readValue(inputStream, SessionSetting.class);

上面代码中的映射几乎一对一的,很好理解,唯一需要解释是每个类上的声明

  @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)

这声明了字段可见性,如果不做任何声明,缺省情况下需要给每个字段增加一个getter,才能完成映射。

读取集合

目前Moco配置文件里实际上是一组Session配置,也就是一个JSON数组对象:

[
{
  "request": {
    "text": "foo"
  },
  "response": {
    "text": "bar"
  }
}
]

众所周知,JVM的范型是假范型,所以,下面的代码不起作用

  mapper.readValue(inputStream, List.class);

当然,Jackson给我们提供了其它的方式读取集合,下面就是:

  TypeFactory factory = TypeFactory.defaultInstance();
  mapper.readValue(is, factory.constructCollectionType(List.class, SessionSetting.class));

忽略字段

在Moco的配置文件里,我们可以用description描述一个配置的作用,其实,它就是一个注释,在实际的代码里没有任何作用。我们当然像下面这么做,把它读到内存里:

public class SessionSetting {
  private String description;
  ...
}

但Jackson给我们提供了更好的做法:

@JsonIgnoreProperties({ "description" })
public class SessionSetting {
  ...
}

这样一来,连运行时的内存占用都省了。

Jackson雕虫技(二)

Tag: jackson

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://www.blogbus.com/dreamhead-logs/230244140.html

定制解析

前面我们看到了,在Moco配置文件里,我们可以支持text匹配,但是,目前Moco还支持下面这种结构:

{
  "request": {
    "text": {
      "match": "foo\\w*bar"
    }
  },
  "response": {
    "text": "match"
  }
}

这里的做法是支持正则表达式的匹配,因为text后面跟的是一个对象,不再是一个字符串,所以,原来声明为String的做法就不起作用了。Moco目前的解决方式是采用了自定义解析的方式,在Jackson里,称之为JsonDeserializer。

首先,我们定义对象,包含对直接字符串和带操作符对象的支持:

public class TextContainer {
    private String text;
    private String operation;
    ...
}

然后,为其定义一个JsonDeserializer,专用于解析TextContainer,代码如下:

public class TextContainerDeserializer extends JsonDeserializer {
    @Override

    public TextContainer deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        JsonToken currentToken = jp.getCurrentToken();
        if (currentToken == JsonToken.VALUE_STRING) {
            return new TextContainer(jp.getText().trim(), null);
        } else if (currentToken == JsonToken.START_OBJECT) {
            JsonToken jsonToken = jp.nextToken();
            if (jsonToken == JsonToken.FIELD_NAME) {
                String operation = jp.getText().trim();
                jp.nextToken();
                String text = jp.getText().trim();
                jp.nextToken();
                return new TextContainer(text, operation);
            }
        }

        throw ctxt.mappingException(TextContainer.class, currentToken);
    }
}

如果你熟悉解析器的一点技巧,便不难理解,我们得到了一个个Token,然后,根据自己需要进行解析。实际上,我们就是参与到解析的过程中去了。从 这段代码,可以看到,如果是一个字符串(JsonToken.VALUE_STRING),则TextContainer里的operation字段为 null,如果是一个对象(JsonToken.START_OBJECT),则要把字段(JsonToken.FIELD_NAME)内文本和操作都解 析出来。这里唯一需要提醒的点是最后一个jp.nextToken(),从这段代码本身它并没有任何意义,实际上,它处理的是 END_OBJECT(“}”)。在Jackson的处理中,每一个JsonDeserializer都要把自己涉及到的Token用光,否则会影响进一 步处理的。

有了JsonDeserializer,接下来就是使用它了。我们可以在字段上使用Annotation进行标记。

public class RequestSetting {
    @JsonDeserialize(using = TextContainerDeserializer.class)
    private TextContainer text;
    ...
}

如果我们只有一个字段用到这个类型,这种做法或许是可以接受的。但如果有大量字段用这个类型,我们就要用一种全局的方式来处理了。在Jackson里,这种方式成为Module:

  Module textContainerModule = new SimpleModule("TextContainerModule",
    new Version(1, 0, 0, null, null, null))
    .addDeserializer(TextContainer.class, new TextContainerDeserializer());
  mapper.registerModule(textContainerModule);

如此一来,只要是TextContainer类型,就会使用我们的TextContainerDeserializer进行处理了,省去了一处一处配置的繁琐。

比较JSON

Moco里有一个操作符,用以比较请求是否是预期的JSON请求。由于空格、Tab、回车换行等字符的存在,两个JSON对象是否相等,不能仅靠字 符串的比较。一种比较方案是,将两个流都读成内存对象进行比较,类似于XML中的DOM。在Jackson里,读出的是一棵树:

  JsonNode requestNode = mapper.readTree(requestStream);
  JsonNode resourceNode = mapper.readTree(targetStream);
  return requestNode.equals(resourceNode);

在这段代码里,只要两个JsonNode相等,它们就是相等的。

Jackson雕虫技(三)

Tag: jackson

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://www.blogbus.com/dreamhead-logs/258185547.html

Jackson雕虫技(一)
Jackson雕虫技(二)

使用Builder模式

在日常开发中,我们希望自己编写的类尽可能不变的,对于参数比较多的类,我们通常采用的方法是Builder模式。但如果我们使用Builder模式构造这样的不变对象,当我们将json反序列化成Java对象该怎么办呢?

Jackson已经为这种做法做好了准备,我们可以告诉它这个类是采用Builder模式构建的:

@JsonDeserialize(builder = DefaultHttpRequest.Builder.class)
public class DefaultHttpRequest implements HttpRequest {
 ...
}

我们使用了一个Annotaiton:@JsonDeserialize,通过builder参数告诉它,用哪个类做Builder。这里用的就是这个类的一个内嵌类:Builder。

public class DefaultHttpRequest implements HttpRequest {
   ...

  public static final class Builder {

    ...

    public Builder withVersion(String version) {
      this.version = version;
      return this;
   }

    ...

    public DefaultHttpRequest build() {
      ...
      return request;
   }
  }
}

其中,以with开头的就是用来传参数的方法,而build方法则用以构建最终的对象,这是一个缺省的约定。我们还可以按照自己的需要进行订制,只不过要给我们的Builder加上另外一个Annotation:@JsonPOJOBuilder。下面是一个例子:

@JsonPOJOBuilder(buildMethodName="create", withPrefix="con")
public static final class Builder {
   ...
}

这样一来,所有传参的方法都是以con开头,而构建对象的方法名则改成了create。

使用对象简化解析

在《Jackson雕虫技(二)》,我们提到了可以用自定义解析的方式解析对象,但一个一个字段解析写起来并不直观。其实,我们还可以借用已有的对象解析机制简化这个过程。下面是MocoProxyContainerDeserializer,它根据当前正在解析的目标进行处理,要么解析成一个URL,要么解析成一个Proxy的配置。

public class ProxyContainerDeserializer extends JsonDeserializer {
   @Override
   public ProxyContainer deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
    JsonToken currentToken = jp.getCurrentToken();
     if (currentToken == JsonToken.VALUE_STRING) {
      return builder().withUrl(jp.getText().trim()).build();
    } else if (currentToken == JsonToken.START_OBJECT) {
      InternalProxyContainer container = get(jp.readValuesAs(InternalProxyContainer.class), 0);
      return container.toProxyContainer();
   }

    throw ctxt.mappingException(TextContainer.class, currentToken);
  }
}

这里的关键是readValuesAs,我们没有直接解析接下来的Token,而是确定解析目标之后,又借用了解析器本身的能力,把它解析成一个对象。至于InternalProxyContainer,它只是一个简单的对象类,用以装载解析的结果。

private static class InternalProxyContainer {
  public String url;
  public String from;
  public String to;
  public String failover;
  public String playback;

  public ProxyContainer toProxyContainer() {
    ...
  }
}

Jackson雕虫技(四)

Tag: jackson

版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://www.blogbus.com/dreamhead-logs/270174350.html

Jackson雕虫技(一)
Jackson雕虫技(二)
Jackson雕虫技(三)

忽略空字段

有时候,我们返回对象的字段可能会有为空的情况。缺省情况下,这些字段会以null的形式呈现出来。但如果我们希望忽略这些字段该如何处理呢?如果是针对某个具体的类,我们可以使用JsonInclude这个annotation。

@JsonInclude(JsonInclude.Include.NON_NULL)
class Entity {
   ...
}

如果所有类都能够遵循这样的规则,那就要在全局配置了,配在Object Mapper上:

  mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

Joda-Time

在《你应该更新的Java知识》里面,我推荐大家在Java 8之前使用Joda Time,而非JDK原生的Date和Calendar。但是,如果我们使用了Joda Time,到了Jackson该怎么办呢?

Java对象和JSON的相互转换,只是一个序列化和反序列化的过程。我们在之前的雕虫技中已经见识过如何针对自己的类自定义序列化和反序列化。所 以,本质上来说,为Joda Time自定义一套,自然也不是什么难事。不过,通用如Joda Time这样的程序库,自然也应该有现成的支持。我们直接拿过来用就好了。

实际上,Jackson已经为Joda Time提供了官方支持。如果你和我一样喜欢Gradle,下面就是依赖的添加方式,其中,jacksonVersion自然是Jackson的版本号。

  "com.fasterxml.jackson.datatype:jackson-datatype-joda:$jacksonVersion"

有了依赖,我们只要在Object Mapper上注册一个模块即可。

  mapper.registerModule(new JodaModule());

现在就可以自己的对象里使用DateTime、LocalDate、LocalTime这样的类型了,有了JodaModule,Jackson就会为我们照顾好这些类型的转换。

我们习惯的日期表示方式一般是年月日时分秒,但Joda Time缺省的做法却是一个数字,一个以毫秒为单位计算出的数字,它叫时间戳。如果你希望让它变成我们喜欢的样子,可以这样做:

  mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

如果你还让自己的日期表现方式中加入时区,那就再加上时区:

  mapper.setTimeZone(TimeZone.getDefault());

Guava

同样在《你应该更新的Java知识》,我还提到一个观点,只要是Java项目就应该使用Guava。我也展示过Guava中一些库的用法。我现在已 经无可救药地爱上了Guava中的不变集合。但同Joda Time一样,Guava不是标准的JDK的一部分,也需要额外的支持。值得高兴的是,这个支持也有现成的,也来自Jackson的官方,依赖如下:

  "com.fasterxml.jackson.datatype:jackson-datatype-guava:$jacksonVersion"

同样,它也要Object Mapper上注册一个模块。

  mapper.registerModule(new GuavaModule());

如此一来,Jackson就可以支持Guava提供的大部分内容,包括Optional和一些不变集合,以及一些新增的集合。

猜你喜欢

转载自zzc1684.iteye.com/blog/2124766