前回までの記事で、Apache Thriftを使ったRPC呼び出しを試してきました。
ただ、前回のものだとXML-RPCなどの他の言語非依存のRPC技術と大差のない単なるメソッド呼び出しになってしまっていました。
Apache Thriftの真骨頂は、intや stringといったプリミティブ型だけでなく、構造体(クラス)や例外までもがプラットフォームを超えて共用できるというところにあります。
今回は構造体や例外も含めて、もう少し実際のサービスに近いレベルでテストをしてみたいと思います。
Cardの管理を行なうサービスを定義する
JAX-RSでRESTfulサービスを定義する記事と同じく、名刺(Card)を追加したり一覧を取ったりする事のできるサービスを考えてみます。
メソッドとしては次の3つを考えました。
CardService.thrift
service CardService
{
void addCard(1: Card card);
list<Card> getCards();
Card getCardById(1: string id);
}
addCard()はCardを追加するメソッド、getCards()は全てのCardを取得するメソッド、getCardByIdはIDを指定してCardを取得するメソッドです。ここで、前回までの記事では出てこなかった要素が2つあります。
1つ目は、list型です。Thriftでは複数の要素を扱う入れ物(Container)として、list, set, mapがデフォルトで用意されています。これらはJavaのGenerics、C++のTemplateと同じような表記で、型の指定が可能です。
詳細を公式サイトから引用します。
- list<type>
- 順序の決まった要素のリストです。C++ではSTLのvectorクラス、JavaではArrayList、その他のスクリプト言語では標準の配列にマッピングされます。
- set<type>
- 順序の決まっていない、ユニークな要素の集合です。C++ではSTLのsetクラス、JavaではHashSet、Pythonではsetクラスにマッピングされます。PHPはsetをサポートしないため、Listのように扱われます。
- map<type1,type2>
- 厳密にユニークなキーとそれに対応する値のマップです。C++ではSTLのmap、JavaではHashMap,PHPでは連想配列、PythonやRubyでは dictionaryにマッピングされます。
2つ目は引数や返り値の型として使っているCardというクラスです。これはThriftがデフォルトで持っている者ではありません。自分で定義する必要があります。CardService.thriftに以下の定義を追加しましょう。
CardService.thrift
struct Card {
1: string id,
2: string name
}
まさにC言語の構造体です。ですが、フィールドの先頭に1: や 2: という数字が付いています。これはField IDというもので、フィールドを一意に指定するIDとして利用されます。IDLの定義としてはオプションなので、無くてもコンパイルはできるのですが、「下位互換性を保つ為には定義しておいた方が無難だよ」というwarningが出ます。
たとえば、リリース後に nameというフィールドを firstname に変更したい場合があったとします。そのような場合でもフィールドIDを同じにしておけば古いクライアントともきちんと通信できるようになるのだろうとおもいます(試してません)。
外部に公開するAPIを考えると下位互換性の問題は避けて通れません。注意して設計するようにしましょう。
ちなみに、Cの構造体やJavaのクラスのように、フィールドの区切りをコンマ(,)の代わりにセミコロン(;)にする事もできます。IDL上はどちらでも良いようです。
CardService.thrift (セミコロンバージョン)
struct Card {
1: string id;
2: string name;
}
パッケージの宣言を追加する
このままThriftでコード生成をしても良いのですが、Javaのクラスがデフォルトパッケージの(package宣言が無い)クラスとして生成されてしまうのはちょっとかっこわるい感じがありました。
実は thriftファイルにはパッケージ名を指定する方法がちゃんとあります。この宣言を頭に足しておいてください。
CardService.thrift
namespace java jp.ohnaka.thrift
この宣言は、「Javaのコードを生成する時は jp.ohnaka.thriftパッケージにしてね」という意味になります。もちろんC++や他の言語でも個別に指定できます。
ただし、一つのthriftファイル内では namespace宣言は1つしかする事はできません。すなわちCardクラスとCardServiceクラスは必ず同じパッケージになってしまうという事です。まあ仕方ないんでしょうかね。
CardServiceのサーバサイド実装
それでは実際にCardServiceクラスを実装してみます。
詳細は以前説明したもの同じなので省略しますが、ビジネスロジックを実装するCardServiceHandlerと、Servlet化するためのCardServiceServletを実装すればOKです。
CardServiceHandler.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jp.ohnaka.thrift.Card;
import jp.ohnaka.thrift.CardService;
import org.apache.thrift.TException;
/**
* CardServiceのサーバサイド実装
*/
public class CardServiceHandler implements CardService.Iface {
static private Map<String, Card> cardDb_ = new HashMap<String, Card>();
@Override
public void addCard(Card card) {
cardDb_.put(card.getId(), card);
}
@Override
public List<Card> getCards() {
return new ArrayList<Card>(cardDb_.values());
}
@Override
public Card getCardById(String id) {
return cardDb_.get(id);
}
}
DBの代わりにstaticな Map<String,Card>を用意して、そこにストアするようにしてあります。本当のサービスであればMyBatisなどを使ってDBにストアするようなイメージです。
CardServiceServlet.java
import jp.ohnaka.thrift.CardService;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.server.TServlet;
/**
* CardServiceサービスへの呼び出しを受け付けるサーブレット
*/
public class CardServiceServlet extends TServlet {
private static final long serialVersionUID = 1L;
public CardServiceServlet() {
// Binary Protocolを扱うサーブレットとして初期化する
super(new CardService.Processor(new CardServiceHandler()), new TBinaryProtocol.Factory());
}
}
前回の記事と同じで、TServletを継承して使用するプロトコルを明示するだけです。
web.xml
サーブレットを /card に配置するために、web.xmlは以下のようにしました。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>thrift-test-server</display-name> <servlet> <servlet-name>CardServiceServlet</servlet-name> <servlet-class>CardServiceServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CardServiceServlet</servlet-name> <url-pattern>/card</url-pattern> </servlet-mapping> </web-app>
これで完成です。
明示的に例外を投げるようにする
CardServiceHandlerの実装では引数のチェックなどを一切していません。addCard()に nullが渡されたり、Cardオブジェクトのidがnullだったりした場合に、内部でRuntimeExceptionが発生してしまいます。NullPointerExceptionなどのRuntimeExceptionが発生すると、HTTPの500 Internal Server Errorとなってクライアント側に問題が発生した事が伝えられます。
そういう仕様でもいいのですが、クライアントからするといったいなぜ500 Internal Server Errorになったのか原因を特定する事ができず困ってしまいます。
そこで、TIllegalArgumentExceptionという例外を定義して、明示的に例外を投げるようにしてみます。Javaであればjava.lang.IllegalArgumentExceptionを投げるのが一般的ですが、他の言語で理解できない例外なので、例外もきちんと Thrift上で定義してあげる必要があります。
ちょっと細切れになってきたので、CardService.thriftの全体を示します。
CardService.thrift
#!/usr/bin/thrift
namespace java jp.ohnaka.thrift
struct Card {
1: string id;
2: string name;
}
exception TIllegalArgumentException
{
1: string message
}
service CardService
{
void addCard(1: Card card) throws (1:TIllegalArgumentException e);
list<Card> getCards();
Card getCardById(1: string id) throws (1:TIllegalArgumentException e);
}
exception TIllegalArgumentExceptionというところが例外を定義しているところです。文法は構造体の定義と同じで、structの部分を exceptionに変えただけです。
このように例外を定義すると、サービスのメソッドにthrows節を書けるようになります。throwsの意味はJavaと同じですが、1: というフィールドIDと例外変数の名前として”e”という名前を明示する必要があるところがちょっと変わっています。詳細は未調査ですが、これも後方互換性のためでしょうか。
例外を追加したので、CardServiceHandlerも以下のように修正します。
CardServiceHandler.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jp.ohnaka.thrift.Card;
import jp.ohnaka.thrift.CardService;
import jp.ohnaka.thrift.TIllegalArgumentException;
/**
* CardServiceのサーバサイド実装
*/
public class CardServiceHandler implements CardService.Iface {
static private Map<String, Card> cardDb_ = new HashMap<String, Card>();
@Override
public void addCard(Card card) throws TIllegalArgumentException {
if (card == null) {
// クライアントサイドに飛ばす例外にはjava.lang.IllegalArgumentExceptionは使えない
// ので、独自に定義した例外を利用する
throw new TIllegalArgumentException("Argumnent shouldn't be null");
}
if (card.getId() == null) {
// クライアントサイドに飛ばす例外にはjava.lang.IllegalArgumentExceptionは使えない
// ので、独自に定義した例外を利用する
throw new TIllegalArgumentException("Card ID shouldn't be null");
}
cardDb_.put(card.getId(), card);
}
@Override
public List<Card> getCards() {
return new ArrayList<Card>(cardDb_.values());
}
@Override
public Card getCardById(String id) throws TIllegalArgumentException {
if (id == null) {
throw new TIllegalArgumentException("Argumnent shouldn't be null");
}
return cardDb_.get(id);
}
}
クライアントサイドを実装する
サーバサイドの準備ができたので、これを呼び出すクライアントサイドを実装してみます。
Client.java
import java.util.List;
import jp.ohnaka.thrift.Card;
import jp.ohnaka.thrift.CardService;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.THttpClient;
public class Client {
public static void main(String[] args) throws Exception {
// 接続先のURLを指定
THttpClient transport = new THttpClient("http://localhost:8080/thrift-test-server/card");
// バイナリプロトコルを使用(サーバ側と合わせる)
TProtocol protocol = new TBinaryProtocol(transport);
// クライアントスタブを作成
CardService.Client client = new CardService.Client(protocol);
try {
transport.open();
// サーバ側のaddCardメソッドを呼び出す
Card card1 = new Card();
card1.setId("1000");
card1.setName("ohnaka");
client.addCard(card1);
// もう一個カードを作ってみる
Card card2 = new Card();
card2.setId("1001");
card2.setName("kuni");
client.addCard(card2);
// サーバ側のgetCardsメソッドを呼び出す
List<Card> cards = client.getCards();
System.out.println("Num of cards: " + cards.size());
// サーバ側のgetCardメソッドを呼び出す
Card retrievedCard = client.getCardById("1000");
System.out.println("Retrieved card id = " + retrievedCard.getId());
System.out.println("Retrieved card name = " + retrievedCard.getName());
// 不正な引数を渡してみる
try {
client.addCard(null);
} catch (Throwable t) {
// どんな例外が飛ぶか調べる
t.printStackTrace();
}
// IDが null なCardを渡してみる
try {
client.addCard(new Card());
} catch (Throwable t) {
// どんな例外が飛ぶか調べる
t.printStackTrace();
}
} catch (TException te) {
te.printStackTrace();
}
}
}
実行結果
クライアントコードを実行した結果です。
Num of cards: 2 Retrieved card id = 1000 Retrieved card name = ohnaka TIllegalArgumentException(message:Argumnent shouldn't be null) at jp.ohnaka.thrift.CardService$addCard_result.read(CardService.java:972) at jp.ohnaka.thrift.CardService$Client.recv_addCard(CardService.java:110) at jp.ohnaka.thrift.CardService$Client.addCard(CardService.java:85) at Client.main(Client.java:47) TIllegalArgumentException(message:Card ID shouldn't be null) at jp.ohnaka.thrift.CardService$addCard_result.read(CardService.java:972) at jp.ohnaka.thrift.CardService$Client.recv_addCard(CardService.java:110) at jp.ohnaka.thrift.CardService$Client.addCard(CardService.java:85) at Client.main(Client.java:54)
カードを2つ作ったので、Num of cardが 2と帰ってきています。また、IDを指定して正しくCardオブジェクトを取得する事ができています。
addCard()メソッドに不正な引数をあたえた場合も、TILlegalArgumentExceptionがスローされている事がわかります。エラーになった理由がメッセージに含まれていて理由もよくわかりますね。
気になるポイント
なかなか強力ですね。いままでRPC上に構造体を流すのに苦労していたのが嘘のようです。Apache Thriftの強力なコード生成能力は構造体や例外を定義してこそと言えるかもしれません。
ただ、ちょっと気になる記述を公式サイト(http://wiki.apache.org/thrift/ThriftTypes)で見つけてしまいました。
Structs
Thrift structs define a common object — they are essentially equivalent to classes in OOP languages, but without inheritance. A struct has a set of strongly typed fields, each with a unique name identifier. Fields may have various annotations (numeric field IDs, optional default values, etc.) that are described in the ThriftIDL.
” but without inheritance”の部分です。継承が利用できないという事のようですが、これが本当だとすると、Cardクラスを継承したJapaneseCard, EnglishCardなどを作って addCard()メソッドで統一的追加処理を扱う事ができません。
もう少し調査が必要なようです。

Twitter