Skip to content
 

Apache ThriftとSpringを組み合わせて例外のマッピングを実現する

最近Apache Thriftの調査をしているのですが、全体像がようやく見えてきました。ただ、実際にプロダクトに組み込もうとすると、「コード生成」というものをどのように扱うべきかちょっと悩んだりします。

プラットフォームを跨いでRPCを行なう場合、各プラットフォーム上で値クラスや例外を定義するのが煩雑になります。Apache Thriftの良いところはこれらのコードまでふくめて、各プラットフォームのRPC層のソースコードを自動生成してくれるところにあります。

ただ、Thriftが生成したクラスをビジネスロジック層で使ってしまうのはいささか抵抗があります

Thriftが生成したクラスはどの範囲で使うべきか

過去の調査で「CardService」というRPCを題材として用いてThriftを動かしてきました。CardServiceの例では、Thriftで  Cardクラスという値クラスを定義し、そのオブジェクトをクライアントとサーバの間でやりとりしています。また、サーバ上で発生した例外をクライアントにも伝わるように、TIllegalArgumentExceptionという例外を定義し、それを throwしてみるテストを行ないました。

しかし、このCardクラスと TIllegalArgumentException例外は、どのレイヤまで利用して良いのでしょうか?

  • CardをDBに永続化する場合、永続化層に渡す引数として、Thriftが生成したCardクラスを渡しても良いものか?
  • ThriftによるRPC以外に、JAX-RSなどでRESTfulインターフェースも平行して提供したい場合、JAX-RS層でもThriftが生成したCardクラスを使っても良いか?
  • ビジネスロジック層で引数のエラーが発生した時に、TIllegalArgumentExceptionを throwしてもよいか?つまり、ビジネスロジック層のメソッドに throws TIllegalArgumentExceptionが現れても良いか?

「レイヤ間の依存性を極力下げる」という考えからすれば、これらは全てNGです。つまり、Thriftが自動生成してくれたクラスや例外は、あくまでRPC層のみで用い、他のレイヤに渡す時は別のオブジェクトに変換してやる必要がありそうです。

これは嬉しいような嬉しく無いような、、。

それでもThriftはメリットがある

「自動生成されたコードが限定的にしか使えないなんて、それってお自動生成した意味あるの?」

という気持ちになってきますが、僕は「意味はある」と思っています。理由は2つあります。

まず一つ目の理由。例えば、ちょっとしたメンテナンスツールを作りたい場合などに、使い慣れた環境から簡単にRPCを呼び出せると嬉しい場合があります。このクライアントコードは半ば使い捨てのようなものなので、レイヤ間の依存性などをいちいち気にする必要がない場合が多いでしょう。このようなツールを作る場合に、値クラスや例外が自動生成されるのはとても嬉しいです。

もう一つの理由は、「XMLやJSONをパースするよりは簡単にコードが書ける」というものです。自動生成された値クラスが無い場合はどうしていたかというと、サーバからXMLが帰ってきて、それをパースしてビジネスロジック層のオブジェクトに詰め込んでいたのです。つまり、Thriftが生成したCardクラスをTCard、ビジネスロジック層のCardクラスをBCardとすると、

今まで:

XML ---パース---> BCard

Thrift:

TCard ---コピー---> BCard

というように対比できます。前者のコードより、後者のコードの方が圧倒的に簡単です。うまくプロパティ名などを合わせれば、全自動でコピーを行なう事もできます。

実際、僕はReflectionを使って TCardから BCardへプロパティをコピーするトランスレータを自作しましたが、とてもうまく動いています。

例外はどうする?

ThriftはRPCなので、例外は一方通行、つまりサーバ側で発生した例外がクライアントに渡る方向しかありません。メソッドの引数に問題があった時にArgumentExceptionを送出する、というシチュエーションを例にして図示してみます。クライアントにビジネスロジックがある場合も考えると、例外は次のように変換されるべきです。

クライアントビジネスロジック層            クライアントRPC層                 サーバRPC層                  サーバビジネスロジック層
      BCArgumentException  <=変換= TArgumentException <==Thrift/HTTP== TArgumentException <=変換=  BSArgumentException

これをベタにやろうとすると、例えばサーバRPC層とサーバビジネスロジック層との間で次のようなコードを書く事になります。

try {
    bService.someMethod(arg1, arg2);
} catch( BSArgumentException e) {
    throw new TArgumentException(e.getMessage);
}

あー、、、。こういうコードを全てのメソッドの呼び出しに書くのは骨が折れます。

実は、こういった「全てのメソッドの呼び出しに◯◯する」というのはアスペクト指向プログラミング(AOP)という考え方を使うとうまく解決できます。Javaは言語仕様としてはメソッドの呼び出しをフックする仕組みを持っていませんが、Proxyクラスを使うと実現できます。また、Javaassistやcglibというツールを使う方法もあります。

今回僕はサーバサイドにSpringフレームワークを使っていたので、SpringのAOP機能を使って例外を変換する方法を紹介します。

TServletの使い方を少し変えて、Springを利用する

Thriftに付いてくる TServletというクラスを用いると、ThriftのRPCを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());
    }
}

このコードでは、CardServiceの実装クラスであるCardServiceHandlerを直接 new していますが、Springから取得するように変更してみます。

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;

    private CardService.Iface getHandler() {
        return (CardService.Iface) applicationContext.getBean("cardServiceHandler");
    }

    public CardServiceServlet() {
        // Binary Protocolを扱うサーブレットとして初期化する
        super(new CardService.Processor(getHandler()), new TBinaryProtocol.Factory());
    }
}

applicationContextは SpringのApplicationContextのインスタンスです。このオブジェクトはSpringの初期化時に作ったものですが、環境によって取り方はいろいろあります。ひとまずどこかに保存しておいた者を取ってきたものと思ってください。

このようにする事で、new CardServiceHandler() となっていた部分が無くなり、Springによってインスタンスを作るようになります。

Springの設定をする

springの設定ファイルには次のような記述を書きます。

 <!-- CardServiceのハンドラ。例外変換の為に Porxyを使って ThriftExceptionMapperAdviceをinjectしている -->
 <bean id="cardServiceHandler" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="proxyInterfaces" value="jp.ohnaka.thrift.CardService$Iface"/>
  <property name="interceptorNames">
   <list>
    <value>thriftExceptionMapperAdvice</value>
   </list>
  </property>
  <property name="target">
   <bean class="jp.ohnaka.thrift.CardServiceHandler"/>
  </property>
 </bean>

 <bean id="thriftExceptionMapperAdvice" class="jp.ohnaka.thrift.ThriftExceptionMapperAdvice"/>

通常のBeanであれば、

<bean id="cardServiceHandler" class="jp.ohnaka.thrift.CardServiceHandler"/>

と書くだけなのですが、なんだか長ったらしくなっています。実は、Springの ProxyFactoryBeanというものを使って、CardServiceHandlerにちょっとした仕掛けを仕込めるようにしています。これが「アスペクト指向プログラミング」の考え方です。
では、プロパティの意味を順に説明します。

proxyInterfaces

このBeanの持っているインターフェースを列挙します。今回のCardServiceHandlerは CardServiceクラスの内部インターフェース であるIfaceインターフェースを実装していますので、”jp.ohnaka.thrift.CardService$Iface” としています。なお、複数列挙する時は<list>を使えばいいみたいです。

interceptorNames

proxyInterfacesで定義したインターフェースの各メソッドを呼び出す時にどういう「仕掛け(Proxy)」を仕込むのかを定義しています。ここでは thriftExceptionMapperAdviceというものを指定します。これは後ろで定義しているBeanの idと対応しています。

target

この「仕掛け」をかます対象のBeanです。refで外部の Beanを参照しても良いですが、ここではその場で定義しています。定義している Beanは今までソースコード内で newしていた jp.ohnaka.thrift.CardServiceHandlerです。

これで何が起こるの?

このようにして作られた cardServiceHandlerというIDの Beanは、CardService$Ifaceの各メソッドを呼び出す時に、thriftExceptionMapperAdviceを経由して呼び出されるようになります。この ThriftExceptionMapperAdviceは、次のようなクラスです。

package jp.ohnaka.thrift;

import org.springframework.aop.ThrowsAdvice;

/**
 * サービス層で発生したIllegalArgumentExceptionを Thrift層の TIllegalArgumentExceptionに変換する。
 * SpringのAOPを使って、CardServiceHandlerの各メソッドにこの Adviceを injectしている。
 * @author ohnaka
 */
public class ThriftExceptionMapperAdvice implements ThrowsAdvice {
    public void afterThrowing(IllegalArgumentException e) throws Throwable {
        throw new TIllegalArgumentException(e.getMessage());
    }
}

ThriftExceptionMapperAdviceは Springの ThrowsAdviceというインターフェースを実装したクラスです。ThrowsAdviceは「メソッドの呼び出しで例外が発生したときに処理を仕込みたい」という事をSpringに教える為のマーカインターフェースです。

ThrowsAdviceをマーカインターフェースをして実装した上で、afterThrowing()というメソッドを定義します。この例のように afterThrowing(IllegalArgumentException e) と定義すると、CardServiceHandlerで IllegalArgumentExceptionが発生した時に、呼び出し元に例外が渡る前にこのメソッドを呼び出してくれるようになります。

つまり、CardServiceHandlerの全てのメソッドの呼び出しに

try {
} catch(IllegalArgumentException e) {
   throw new TIllegalArgumentException(e.getMessage());
}

というコードを付け足したのと同じ効果が得られるわけです。afterThrowing(HogeHogeException e) というのを定義すれば、別の例外 HogeHogeExceptionに対してフックを仕掛けることもできます。

ちなみに、afterThrowing()メソッドの中で、「どのメソッドが呼び出されたのか」を知りたい場合は次のような書き方もできます。

public void afterThrowing(Method m, Object[] args, Object target, IllegalArgumentException ex)

これでThriftのRPC層とビジネスロジック層で例外を変換できるようになりました。めでたしめでたし。

補足

本文中では適当に飛ばしてしまった Springの ApplicationContextの取り方ですが、僕はこんな風にしています。

まず、web.xmlで Springの SpringContextLoaderListenerというリスナを定義します。そのリスナの後に、自作のContextListenerを定義します。

web.xml

 <listener>
  <listener-class>org.jboss.resteasy.plugins.spring.SpringContextLoaderListener</listener-class>
 </listener>

 <listener>
  <listener-class>jp.ohnaka.web.MyContextListener</listener-class>
 </listener>

順番は必ずSpringContextLoaderListenerが先にくるようにしてください。

MyContextListenerは次のようにします。

public class MyContextListener implements ServletContextListener {

    public void contextInitialized(ServletContextEvent arg0) {
        WebApplicationContext wac = WebApplicationContextUtils.getRequiredWebApplicationContext(arg0.getServletContext());
        MyApplicationContext.getInstance().setApplicationContext(wac);
    }
}

このようにすると、SpringContextLoaderListenerが先に起動するので、MyContextListenerの contextInitializedが呼ばれた時には、Springの初期化が終わっており、Servlet Contextに ApplicationContextがセットされています。そのApplicationContextは WebApplicationContextUtilsを使って取得できます。

WebApplicationContextUtilsのgetRequiredWebApplicationContextメソッドは、staticメソッドなのでどこからでも呼べるのですが、引数に ServletContextが必要なので、先ほどの CardServiceHandlerなどからは呼び出せません。そこで MyContextListenerの contextInitializedメソッドの中で呼び出しておき、自作のシングルトンクラス(MyAppliationContext)にセットしておいて、どこからでも取れるようにしています。

こうすると、CardServiceHandlerでは、

MyApplicationContext.getInstance().getApplicationContext();

というような感じで、AppliationContextを取得できるようになります。

コメントをどうぞ