JSON Patch(RFC6902) with Spring MVC

イントロダクション

ウェブアプリケーションは多くのケースで以下のようにサーバと同期します。
  • ボタンクリック時に画面で更新された値をサーバに送り、サーバから必要な情報を取得する
  • スクロール時に表示に必要な情報を取得する
  • ページ遷移時に次のページに必要な情報を取得する
このように特定のイベント実行時だけサーバと同期するとき問題になることがあるのが、データサイズが大きいと通信に時間がかかるということです。
更新されたフィールドの数が多く何層にもネストしているようなModelをやりとりするケースでは1回の通信に対する待ち時間が長いと実用には耐えられません。
特にモバイル端末のように制限された環境ではなんらかの対策は必須です。
これを解決するには、
  • 最小単位のデータを常にサーバに送る
    1項目単位で変更をサーバに送り、ユーザのセッション単位にサーバ側にクライアントと同期されたデータを一時的に保持しておく。そして特定のボタンクリック時にサーバ側にある画面の情報をコミット(永続化, or etc...)する。
  • 前回サーバ通信時との差分だけをサーバに送る
    直近にサーバ通信した際のデータをクライアント側に保持しておき、特定のイベント実行時にその前回のデータと変更後のデータとの差分だけを送信する。
この記事では後者(前回サーバ通信時との差分だけをサーバに送る)に対する1つの回答として JSON Patch を使います。

JSON Patch(RFC6902) とは

JSON形式を使ってPatchを表現した JSON PatchというものがRFC(RFC6902)にあります。
これは、差分適用のオペレーション(addやreplaceなど)と、差分をHTTP PATCHメソッドを使って送信するものです。
クライアントからは、下記のようなJSONを送ります (下記は属性の追加例で、path, valueが追加データ)。
{ "op": "add", "path": "/baz/bat", "value": "qux" }

Javaでは現状だとJersey(JAX-RSのリファレンス実装)には実装されています。
ですが、Javaで大規模なWeb applicationを開発する場合、Spring MVCが使われることが多く、そのSpring MVCでJSONを扱う場合には、Springがデフォルトで提供しているJacksonを使用したJSON Deserializer/Serializerを使うケースがほとんどでしょう。
そこで今回は Spring MVC(+ Jackson) にJSON Patchを組込みたいと思います。

JSON PatchをSpring MVC frameworkで使えるようにする

JSON Patchが扱えるようにSpring MVC + Jacksonの拡張します。

I/Fを決める

まずは使う側がどのように使用(実装)したいかを決めましょう。
SpringではControllerは以下のように定義します
@RequestMapping(value = "/memo/{id}", method = RequestMethod.PATCH, produces = "application/json-patch+json")
@ResponseBody
public ? patchMemo(@PathVariable final long id, @RequestBody ? memo) {
  ...
  return memo;
}
引数と戻り値にはなにを指定するのが良いでしょうか。
まず引数には、
部分的に値を保持しているものが渡ってくるのが良さそうです。
以下の様なI/Fを持つPartialModel型を作りましょう。
PartialModel.java
package diff;

@Partial
public interface PartialModel<T> {

  public T apply(T original) throws IllegalPatchException;
}
Partial.java
(あとで使うJacksonの拡張のためにアノテーションを用意します)
package diff;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Partial {

}
これは以下のような使われ方を想定しています。
public ? patchMemo(@PathVariable final long id, @RequestBody PartialModel<Memo> partialMemo) {

  // 永続化されたメモをどこからか取ってくる
  Memo original = repository.getMemo(id);

  // 引数の部分メモに永続化されていたメモを適用する
  Memo current = partialMemo.apply(original);

  // 更新されたメモを永続化する
  repository.updateMemo(current);

  ....
そして戻り値は、
クライアント側に差分だけを返せるるように更新前と更新後の値を戻します。
以下のように更新前と更新後の値を保持できるDifference型を作ります。
  ...
  return Difference.of(original, current);
}
Difference.java
package diff;

@Subtractable
public class Difference<T> {

  public final T original;
  public final T updated;

  public Difference(T original, T updated) {
    this.original = original;
    this.updated = updated;
  }

  public static <T> Difference<T> of(T original, T updated) {
    return new Difference<T>(original, updated);
  }
}
Subtractable.java
(あとで使うJacksonの拡張のためにアノテーションを用意します)
package diff;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subtractable {

}
最終的にControllerのメソッドはこうなりました。
@RequestMapping(value = "/memo/{id}", method = RequestMethod.PATCH, produces = "application/json-patch+json")
@ResponseBody
public Difference<Memo> patchMemo(@PathVariable final long id, @RequestBody PartialModel<Memo> partialMemo) {

  // 永続化されたメモをどこからか取ってくる
  Memo original = repository.getMemo(id);

  // 引数の部分メモに永続化されていたメモを適用する
  Memo current = partialMemo.apply(original);

  // 最新のMemoに対する処理
  repository.updateMemo(current);
  ...

  // クライアントには差分だけ返す
  return Difference.of(original, current);
}
I/Fが決まったので、
これに合わせてSpring frameworkの拡張をしましょう。

Spring WEB MVC Frameworkを拡張する

まずは、
HttpMessageConverterへJSONをやりとりするためのMessageConverterを設定するために、以下のようにSpringへJSONを扱うための設定を追加します。
@Configuration
public class SampleJsonConfig extends WebMvcConfigurerAdapter {

  @Override
  public List extends HttpMessageConverter> createHttpMessageConverter() {

    // JSON(application/json)用Message converter
    // ('application/json', 'application/*+json'のときにこれが使用される)
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    ...
    return Arrays.asList(converter);
  }
}
これだけだとRFC6902には対応できていないので、MappingJackson2HttpMessageConverterがデフォルトで使用するObjectMapper(*)を拡張します。
(*)Jacksonはcom.fasterxml.jackson.databind.ObjectMapperがJavaのObjectのSerialize/Deserialize処理を提供しています。
以下のようにObjectMapperを継承しJson patch用の処理を追加します。
public class SampleObjectMapper extends ObjectMapper {

  public SampleObjectMapper() {

    // DEFAULT_ANNOTATION_INTROSPECTORはJacksonがデフォルトで提供するannotation処理機能
    AnnotationIntrospector introspector = AnnotationIntrospector.pair(new PartialJsonAnnotationIntrospector(this), DEFAULT_ANNOTATION_INTROSPECTOR);

    setAnnotationIntrospector(introspector);
  }
}
Jacksonのデフォルト機能を簡単に拡張するには、annotation introspectorを追加します。これはannotationベースでダイナミックに処理を追加するための機構です。新しく作る PartialJsonAnnotationIntrospector を上のように追加することで簡単にJacksonを拡張することができます。
次にこのPartialJsonAnnotationIntrospectorの実装をしましょう。
ここには以下のようにDeserializeとSerializeの手段を書きます。
class PartialJsonAnnotationIntrospector extends NopAnnotationIntrospector {

  private final ObjectMapper mapper;

  PartialJsonAnnotationIntrospector(ObjectMapper mapper) {
    this.mapper = mapper;
  }

  @Override
  public boolean isAnnotationBundle(Annotation ann) {
    return false;
  }

  @Override
  public Object findDeserializer(Annotated a) {
    // a = Deserialize対象のClass

    // PartialというannotationがあればJSON patch専用のDeserializerを返す
    Partial ann = a.getAnnotation(Partial.class);
    if (ann != null) {
      return new PartialJsonDeserializer(this.mapper);
    }
    return null;
  }

  @Override
  public Object findSerializer(Annotated a) {
    // a = Serialize対象のClass

    // SubtractableというannotationがあればJSON patch専用のDeserializerを返す
    Subtractable ann = a.getAnnotation(Subtractable.class);
    if (ann != null) {
      return new PartialJsonSerializer(this.mapper);
    }
    return null;
  }
}
あとは、
JSON patch専用のDeserializer, Serializerの実装をしたらおしまいです。
一からJSON patchのDeserializer, Serializerを作るのは本題ではありませんので、簡単のために今回この部分にはありもののライブラリを利用することにします。
以下がDeserializerの実装です。
import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

final class PartialJsonDeserializer extends JsonDeserializer<Object> {

  private final ObjectMapper mapper;

  public PartialJsonDeserializer(ObjectMapper mapper) {
    this.mapper = mapper;
  }

  @Override
  public Object deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    return new PartialJson<Object>(this.mapper, (JsonNode) jp.readValueAsTree());
  }
}
Deserizeメソッドが戻しているPartialJsonクラスは以下のようにします。
import java.io.IOException;

import diff.IllegalPatchException;
import diff.PartialModel;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.fge.jsonpatch.JsonPatch;
import com.github.fge.jsonpatch.JsonPatchException;

public class PartialJson<T> implements PartialModel<T> {
  private final ObjectMapper mapper;
  private final JsonNode node;

  public PartialJson(ObjectMapper mapper, JsonNode node) {
    this.mapper = mapper;
    this.node = node;
  }

  @Override
  @SuppressWarnings("unchecked")
  public T apply(T original) throws IllegalPatchException {
    try {
      JsonPatch patch = JsonPatch.fromJson((JsonNode) this.node);
      JsonNode patched = patch.apply(mapper.valueToTree(original));
      return mapper.treeToValue(patched, (Class<T>)original.getClass());
    } catch (IllegalArgumentException | JsonPatchException | IOException e) {
      throw new IllegalPatchException(e);
    }
  }
}
そして、Serializerです。
import java.io.IOException;

import diff.Difference;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.github.fge.jsonpatch.diff.JsonDiff;

final class PartialJsonSerializer extends JsonSerializer<Difference> {

  private final ObjectMapper mapper;

  protected PartialJsonSerializer(ObjectMapper mapper) {
    this.mapper = mapper;
  }

  @Override
  public void serialize(Difference value, JsonGenerator jgen,
      SerializerProvider provider) throws IOException,
      JsonProcessingException {
    provider.defaultSerializeValue(JsonDiff.asJson(
        mapper.valueToTree(value.original),
        mapper.valueToTree(value.updated)), jgen);
  }
}
これでSpringの拡張は完了です。
使ったライブラリ
Spring 4.0.x
Jackson 2.2.x
json-patch 1.7 https://github.com/fge/json-patch

使ってみる

最後に、上のコードを組み込んでサーバを起動したのち、HTTPでアクセスしてみましょう。
$ curl http://localhost:8080/memo/1 -X PATCH -H "Content-Type: application/json-patch+json" -H "Accept: application/json-patch+json" -d '[ { "op": "replace", "path": "/id", "value": 2 } ]'
サーバに送った差分が適用され、クライアントに適用するための新しい差分が返ってきました。
[{"op":"replace","path":"/id","value":"2"}]

シングルページアプリケーションとDurandal.js

先月終わったばっかりのエンタープライズ向けのプロジェクトで最近話題になっているシングルページアプリケーションを作ったので、感想をシェアしたいと思います。


シングルページアプリケーション(SPA)とは?

シングルページアプリケーション(以下:SPA)という言葉、このページを読んでる皆さんはおそらく聞いたことがあると思いますが、実際にSPAで大規模プロジェクトをこなした経験を踏まえ、いくつかの観点からSPAの特徴をまとめておきます。

SPAは、画面の挙動と画面間の遷移が完全にクライアントサイドのJavaScriptにコントロールされ、サーバーの役割は主にクライアントが求めるデータの提供/処理に限るアプリケーションのことです。


主なメリット

  • リッチなUI

    UIが豊富で複雑なアプリケーションを作るときに、画面の挙動と画面間の遷移をコントロールしやすく、ネイティブアプリケーションのような感覚をユーザにもたらすことができます。

  • 通信量の削減で、反応速度アップ

    画面を構成するほとんどのリソース初期ロードし、あとはデータ通信のみなので、3GやLTEを使うモバイル端末にとってやさしく、画面表示が速いです。例えば、サーバーからのデータを必要としない画面を即時に表示できます。

    一方、画面を表示するのに複数のAPIからのデータが必要になったときに、平行の複数のHTTPリクエストでそのデータが取得できるので、こちらも画面の表示をはやくすることができます。

SPA用のフレームワーク

SPA用のフレームワークは幾つかありますが、僕らが使ったのは、Durandal.js というものです。

Durandal.js を選んだメインの理由は以下の通りです。

  • Durandal.js 内部では、実績の多い Knockout.js を利用している
  • SPAを作るのに必要な機能を完備している
  • Durandal.js による開発は非常に容易で、学習コストが比較的に低い
  • Durandal.js 内部では、 Require.js を利用しているので、モジュラリティが高く、様々なライブラリが簡単に使える

大規模の業務系アプリケーションの作成に Durandal.js を使った経験を顧みると、フレームワークとしての簡単さとアプリケーションの実装における自由度の高さが魅力と感じました。

上記で平行の複数のHTTPリクエストのメリットの話しをしていたので、Durandal.jsでの実際の実装方法を紹介したいと思います。

ちなみに、使ったバージョンは1.2でしたが、既に2013年8月にバージョン2.0がリリース済みで、このブログを執筆時点で、2.1は開発中というステータスです。

DB2用のHibernateカスタムDialect作成

HibernateのカスタムDialect

 DB2用にHibernateのSQLのカスタムDialectを作成したので、作成方法をご紹介します。HibernateなどのORマッパーを使用してDB操作する場合、開発者は接続用のDBに合わせて、使用するSQLのDialectを指定するだけ、というケースがほとんどだと思います。
 ただ今回は、下記の事情からDialectをカスタマイズすることになりました。

事情1.今回は、EntityクラスからDDLを生成していた
事情2.文字列データ型としてDB2のVARGRAPHICを使うことにした
 (HibernateのデフォルトのDialectだとJavaのStringをVARCHARに変換します。)
事情3.開発環境ではH2を使用していたため、Entityクラスのアノテーションで直接データ型指定ができなかった

 事情3について補足すると、文字列データ型のVARGRAPHICを指定する方法としては、Entityクラスに直接指定する方法もあるのですが、そうすると開発環境で使用しているH2にはないデータ型になってしまうので、この方法は使えません。

 HibernateがEntityクラス(Javaのソースコード)からDDLを生成するタイミングで各DB用のDialectが言語の型を、DB専用のデータ型に変換します。ここにカスタムDialectを作成することで、開発環境ではH2を使いつつ、本番環境ではDB2を利用することも可能となります。今回は、DB2Dialectのデフォルトの動作を変更し、JavaのStringをDB2のVARCHARに変換するところを、VARGRAPHICに変換するようにカスタムDialectを作成しました。

Dialectの作成方法/設定方法

・作成するクラス:
 下記の2つのクラスを継承したクラス
 org.hibernate.dialect.DB2Dialect
 org.hibernate.dialect.unique.DB2UniqueDelegate


・設定箇所:
 Dialectを指定している設定ファイルも変更する必要があります。
 hibernate.dialect:<パッケージ名>.CustomDB2Dialect
 (Hibernateのプロパティです。設定ファイルは、様々な種類があるので割愛しています。)

これで、Hibernateがスキーマを自動生成するときにStringに対応するDBのデータ型がVARGRAPHICになります。

所感

 地味な機能ですが、必要なケースは意外とありそうだなというのが作ってみての感想です。
 今回はデータ型のマッピングのみの修正でしたが、Dialectを修正すると実行時の値の変換などもカスタマイズできます。例えば、DB2のデータ型には、真偽値がありません。Javaのbooleanは、DB2のsmallintになり、trueは数値の1、falseは数値の0に変換されます。これも裏でDialectが密かにやっていることです。

Hibernate(4.2)のDialectに関しては、下記に情報がありますので参考にしてください。
http://docs.jboss.org/hibernate/orm/4.2/devguide/en-US/html/ch01.html#configuring-dialects


TypeScriptのデメリット

イントロダクション

まずは、私達がどのようにTypeScriptを使ってきたかを簡単に紹介します。

2012年の終わり頃から最近までの約1年半TypeScriptを使ってあるプロダクトの開発をしてきました。
作っていたものは、SPA(シングルページアプリケーション)で、クライアント側の構成はこんな感じです。

  • TypeScript
  • RequireJS(AMD)
  • Underscore.js(便利)
  • Durandal(SPAフレームワーク)
  • Knockout.js(ModelとViewの双方向バインディング)

AltJSは過去にもいくつか使っていて、
今回はなぜTypeScriptにしたのかというと、一言でいえば「流れがきていた(*1)」ということです。

*1 :
・EcmaScript6の先取りができる
・(頼りなくても)型が欲しかった
・Microsoftがメンテナンスしている

こんな理由で約1年半使ってきて感じたTypeScriptのデメリットを書きます。


TypeScriptのデメリット

  1. コンパイルが遅い
  2. コンパイラのアップデートの追従にコストがかかる

1. TypeScriptはコンパイルが遅い


最終的にこのプロダクトでは、約500個のTypeScriptのファイルが作られ、コンパイルの時間が20秒程度。1年前に買った15インチ Mac book Pro Retina(*2)で、です。

*2 :
  • プロセッサ: 2.7 GHz Intel Core i7
  • メモリ: 16 GB 1600 MHz DDR3
  • ディスク:  SATA 6 Gbit/s SSD
  • TypeScript version 0.9.1.1

星の数ほど「コンパイル遅い」と口にしてきました。
ですが、JavaScriptだけを使った場合の以下の状況よりは格段に良いと言えます。

a. ブラウザで対象画面を開いて、Syntaxエラーを確認するのにかかる時間

 「メソッド名をTypoしている…」


これを確認するのに程度に差はありますが、1分はかかるでしょう。
さらにテストケースを追加するなら、5分は余計にかかるでしょう。

TypeScriptなら、ほぼ全てのSyntaxエラーは静的なチェックにより解消されます。

b. ブラウザで対象画面を開いて、対象のアクションを実施し、scriptエラーを発見するのにかかる時間

「ここにはNumber型が来る想定だったのに...」


これも程度に差はありますが、確認に1分はかかるでしょう。
このような実行時例外は型チェックにより解消可能な場合が多いです。


動作確認時間に、本質的でないテストケースの追加…、
コンパイル時間の20秒がとても短く感じられます。
これは開発者が増えるほどより大きな効果となるでしょう。


2. TypeScriptコンパイラのアップデートの追従にコストがかかる



大きな理由はまだ枯れていないということです。
開発を開始したころ、0.8.1を使っていました。
初めのうちはコードの量が少なく、リリースごとの変更に追従できていましたが、0.9から 0.9.1.1 へのアップデートは時間がかかりました。

  • 対応時間: 4人日
  • 対応期間: 2週間(0.9.1.1のリリース日からだと、1ヶ月と2週間)

0.9.1.1のバグ報告等の情報収集にリリースから1ヶ月程度導入を見送り、各開発者のコードベースが落ち着くのを待ってアップデートを行った為、適用が完了するまでに期間がかかりました。

開発も一段落するころ、バージョン0.9.5がリリースされましたが、互換性の無い変更による影響が大きく、対応にはさらに倍の時間が必要になると考え導入はしていません。



これからも使いたい

開発の終わりに開発者全員にアンケートをとったところ、「これからもTypeScriptを使いたい」が満票を獲得しました。
開発者の皆さんはTypeScriptが好きなようで、次のプロダクトでもAltJSにはTypeScriptを使おうと思っています。

Welcome to TypeScript
http://www.typescriptlang.org

Popular Posts