今回、久しぶりにプログラムに関する覚書きの記事です。実は自分は少し大げさな言い方をすると、JSONという形式を覚えてから、とてもプログラムの幅が広がりました。JSONは、あの「13日の金曜日」に出てくるジェイソン(Jason Voorhees)と同じ発音で、JavaScript Object Notationを略したものだと言われています。簡単にいうと、JavaやC#それに名前の元にもなっているJavaScriptなどのオブジェクトを文字列に変換したもので、C#のオブジェクトをJSON文字列にしてそれをJavaScriptのオブジェクトに戻す、なんてことができる便利な形式なのです。以前はその役割はXML(eXtensible Markup Language)が担っていたのですが、最近ではWebサービスなどでやり取りされるデータも、ほとんどがJSONフォーマットになっています。
さて、JavaのオブジェクトとJSON文字列を相互に変換するには、いくつかのライブラリーがありますが、自分は
Jacksonというライブラリーを使っています(他にもJSONICなんていうライブラリーも有名です)。このJacksonのライブラリーのjarファイルを暮らすパスの通っているところにおけば、すぐにプログラムの中から使うことができますし、簡単に相互変換できるのです。
自分がまず最初にやったのは、Webサーバーのセッション上で保持する情報をJSON形式で保持することでした。というのも、複数のWebサーバーでクラスターを組んで、Webサーバー1でセッション上に保存した情報をWebサーバー2でも取り出して使いたいという場合、保持する情報はSeriarizableというインタフェースを実装していなければなりません。Seriarizableということは、オブジェクトをバイナリー形式に変換してWebサーバー間で同期されているということですが、サードパーティ製のオブジェクトを保持したいケースがどうしても出てきたので、文字列ならば必ずSeriarizableなのだからという理由で、JSON文字列に変換してから保持すればよかろうというのがそもそものアイデアです。
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpSession;
import java.io.IOException;
public class SessionManager {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static void setSessionAttribute(HttpSession session, String key, Object value) throws JsonProcessingException{
session.setAttribute(key, objectMapper.writeValueAsString(value));
}
public static Object getAttribute(HttpSession session, String key, Class<?> type) throws IOException {
Object object = session.getAttribute(key);
if (object == null) return null;
return objectMapper.readValue(object.toString(), type);
}
}
とても簡単ですね。JSON文字列に変換する場合はobjectMapper.writeValueAsString(value) というようにどんなクラスのオブジェクトでも渡せますし、逆に文字列から戻す場合はobjectMapper.readValue(object.toString(), type)です。object.toString()はHttpSession.getAttribute()が格納したJSON文字列をオブジェクト型を返すので、文字列に戻しているだけです。
例えば、次のようなクラスを考えると、上記のsetSessionAttributeメソッドでセッション上には {name: "John Smith", age: 33} のようなJSON文字列で保存されます。その保存したJSON文字列をgetAttributeで戻すと、下記のクラスのインスタンスが得られるというわけです。
public class Person {
private String name;
private int age;
public getName(){ return name; }
public void setName(String name) { this.name=name; }
public int getAge() { return age; }
public void setAge(int age){ this.age=age; }
}
ところがです。意外にも困ったことが起きたのです。Personのインスタンスをセッションに格納する場合はこれで良かったのですが、List<Person>のインスタンスを格納しようとした時に、変なことが起きたのです。上記と同じように、格納時は
List<Person> persons = new ArrayList<Person>();
Person person = new Person();
person.setName("John Smith");
person.setAge(33);
persons.add(person);
SessionManager.setAttribute(session, "persons", persons);
としました。これはいいんです。ところが、JSON文字列からJavaのオブジェクトに戻そうとした次のコードが動かないのです。
List<Person> persons = (List<Person>)SessionManager.getAttribute(session, "persons", List.class);
なんと、Personの時はキャストできたのにList<Person>になった途端にキャストできないのです。しかもデバッガでSessionManager.getAttributeの戻り値がどんな型になっているのかを確認すると、なんとList<LinkedHashMap<String, Object>>という、どこからそんな型が出てきたのかという型になっていたのです。これは、もしかしてJSONはリストを扱えないのか?と思いましたが、分かってみればなんてことのない、よく考えてみれば当たり前のことだったんです。
答えは、SessionManager.getAttribute()に渡した第3引数にありました。Javaの場合、ジェネリックの型変数まで指定してList<Person>.classとはできません。どうしたって、List.classでなければビルドが通らないのです。つまり、Jackson側はリストに含まれる1つ1つの要素は型が分からないんです(この場合はList.classのどの情報を見ても要素がPersonだという情報が含まれていません)。そして、分からないなら分からないなりに、名前と値の組み合わせであるLinkedHashMap<String, Object>になっていて、結果的にList<LinkedHashMap<String, Object>>型なので、List<Person>へのキャストで例外になったのです。これが、トップレベルがリストなのではなく
public class School {
private List<Person> students;
..........(セッター・ゲッターなど省略)..........
}
のような場合で、SessionManager.getAttribute(session, "school", School.class) のようになっていれば、School.classの中にList<Person>型のstudentsというフィールドがあるという情報があるので、JacksonがPerson型を認識してくれるのです。
したがって、解決法その1としてはリストをやめて配列にするという手があります。
Person[] persons = (Person[])SessionManager.getAttribute(session, "persons", Person[].class);
これなら、Person[].classという型情報にPersonという要素の型が分かるようになっているので、Jacksonが思い通りの配列に戻してくれます。
解決法その2は、トップレベルではなくそのリストを囲うクラス(上記のSchool)を使ってSchoolのインスタンスとして扱うことです。ただ、いちいちリストを囲うクラスを作らなければならないのは、ちょっと不便ですね。
他にも解決法その3として、Jackson側でこういうケースを想定して準備してくれているTypeReferenceというクラスを使って、objectMapper.readValue()のところを
List<Person> persons = objectMapper.readValue(object.toString(), new TypeReference<List<Person>>(){});
のようにすれば、リストのまま取り扱うこともできます。
自分は、SessionManagerクラスをそのままにするために、解決法その1を取りましたが、SessionManagerのようなユーティリティクラスを使わない場合は、その3の方法がエレガントかもしれません。今回の問題は、ライブラリーを作る側の気持ちになって考えれば、要素の型情報(Person)がわからないのに戻しようがないことはわかりますが、最初はアレっと思いました。
人気ブログランキングに参加しています。もしよろしければポチッとご協力をお願いします。
↓↓
人気ブログランキング
ブロトピ:ブログ更新しました。
ブロトピ:ブロトピ投稿でアクセスアップ‼
ブロトピ:こんな記事作りました
ブロトピ:アクセスランキング応援サークル