コミッタによるGuiceプレゼン動画・中級編
GuiceコミッタのKevin Bourrillion氏による第2回。今回は中級編という事で、より実践的な内容になっています。
具体的にはInjectorやScopeの使い方や微妙な仕様(バグ)に関する突っ込んだ話、テスト方法、今後のバージョンアップについてなどです。
動画ページへ
↓貼り付けてみました。
http://video.google.com/videoplay?docid=121403207358911729
資料はまだのようで、フォントが小さいコード例などは動画ではかなり厳しいものがありますが、実案件で使ってるので一刻も早く色々知りたいといった方は是非。
Twitter的マルチインタフェースサービスと、実装基盤としてのESB
追記:長くなったので一言で言うと、以下は「Twitterみたいなの作るならESB見とくと良いかも」という話です。
マルチインタフェースサービス
Twitterが本格的に流行るかどうかは分かりませんが、Twitter的なサービス――Webサイトだけではなく携帯電話やインスタントメッセンジャー、API等様々なインタフェースを通して利用できるサービスは、これから増えていくと思います。
Web2.0の説明として頻繁に参照されるティム・オライリー氏の例の文書でも、「単一デバイスの枠を超えたソフトウェア」が要素の1つとして挙げられています。しかし1つのPCプラットフォームでも様々な種類を持つ事を勘案すると、デバイスよりはインタフェースの方が適切でしょう。という事でこのエントリではそういったサービスを、仮にマルチインタフェースサービス(以下MIS)と呼んでみます*1。
MISは、やや一般化すると次のようなサービスを指しています。
MISのアーキテクチャの特徴
MISにはユーザ環境への適応性(ユビキタス性?)、ビジネスモデル上の特徴など幾つもの側面があると思いますが、このエントリで考えてみたいのはアーキテクチャです。
一般的なWebサイトのアーキテクチャは、大雑把に言って
UIの処理 ⇔ ロジックやデータの処理 ⇔ ストレージ(主にDB)
という感じです。さて、ここに他のインタフェースを追加する場合、どうなるでしょうか?
もしこれが掲示板サービスで、携帯電話からも閲覧や投稿が出来るようにしたいという話であれば、そう難しくはなさそうです*2。UIの処理をURLや境界部分で分岐させて、携帯用のセッション管理や画面生成等を行えば大体対応できるでしょう。しかし追加したいのがインスタントメッセンジャーで、そこから何らかの操作やデータの送受信を行うという話だったら?
まずクライアントとやり取りするメッセージングサーバが必要です。ではそのサーバとWebサイトを処理するサーバはどうやって連携するか。DB連携?リアルタイム性が失われそう。サーバ間で何かのプロトコルを使う事にしたとして、おそらくデータ永続化部分は元々のWebサイト用サーバにあるでしょうから、結局サーバ間は双方向のコネクションが必要になりそうです*3。
やれやれ対応できたかなというところで、じゃあ今度はメールからの投稿を受け付けつつ、特定のデータは指定されたメールにも通知する機能を追加しましょうという話に。メールサーバからのデータプッシュを処理し、結果をメッセンジャーに投げてみる。しかしやってみて分かった事として、メールは結構巨大なデータを送付されて処理に時間がかかる場合があるので、どうもキューに入れて非同期にしたくなってきた。じゃあそこだけ非同期に、ってそれメッセージの順序は大丈夫かな?んーではまず前処理したものを永続化してから短いサイクルのバッチにしようか…と、だんだん難しくなってきました。
そして追い討ちをかけるように、メッセージングサーバは独自のタイミングで機能を閉塞できる必要が出てくるかも知れません。って事はその部分だけ独自の管理機能を追加して、うぅ…これは後でメンテできるんだろうか?
とまあ、適当にハマりシナリオを考えてみましたが、あり得ないとも言い切れない展開です。
ではそうならないよう、複数のインタフェースの追加に柔軟に対応できるアーキテクチャを考えてフレームワークっぽく作りましょう――といきなり行く前に、実はちょっとだけ横を見てみると、そんな用途にピッタリのアーキテクチャがあります。それがESB(エンタープライズ・サービス・バス)です。
MISとESBの類似性
上の例では、敢えて大変そうな感じで複数のインタフェースの追加を考えました。しかしエンタープライズ分野において、あるシステムが他のシステムやネットワークとデータをやり取りするというのは非常にポピュラーなケースです。そこで昔から色々な連携アーキテクチャや製品が提案・使用されてきており、ESBはその分野でここ数年流行っている(?)アーキテクチャの1つです。ESBの厳密な定義はたぶん存在しませんが、敢えて超ざっくり言えば「あっちこっちから入ってくるデータを変換したり処理したりあっちこっちに送ったりする為のアーキテクチャ」です(これだけだと色んなものが当てはまりそうな気もしますが、具体的なイメージは後述する図などを見て下さい)。
つまりMISは、つながる対象(インタフェース)こそだいぶ違いますが、やりたい事や論理的なアーキテクチャのレベルでは、かなりESBと類似していると言えるのです。
ESBの実装
じゃあESBの具体的な実装って何よ、という話。商用の製品は色々ありますが、(広義の)Webサービスを作るのには幾つかの点で不向きです。そこでオープンソースで見てみると、ここからは完全にJava限定の話になってしまいますが、ServiceMix、OpenESB、Muleなどが有名どころとして存在します。これらはいずれもESBのJava版仕様とも言うべきJBI(Java Business Integration)を実装したものです*4。例えばMuleのイメージとかServiceMixのサンプルを見て頂くと分かる通り、色々なサービスやプロトコルがプラグのようにバスに接続されて、そこをメッセージが飛び交うアーキテクチャになっています。まさにTwitter的なMISのアーキテクチャに使えそうに見えてきませんか?
実装例:ServiceMix
ここでもうちょっと具体的に「どのくらい使えそうか?」を探るべく、ServiceMixを見てみたいと思います。これを選んだ理由は、昔JavaOneで発表した時にServiceMix担当だったから、というだけです(w。
まずどんなインタフェースがサポートされてるのかコンポーネントのリストを見てみると、HTTP, FTP, Jabber, SOAP, JMSなどが使えますし(HTTPはJettyのcontinuationsを使えるらしいので、cometな実装もサポートできるかもしれませんね)、RSSの取得や生成、メールの送信などもサポートされています。
更に入力されたデータ(メッセージ)をどこに飛ばすかを決めるルーティングは色々な手段が選べますし、外部からそれらのコンポーネントを監視したりライフサイクルを管理、操作する手段も最初から用意されています。
こうしてみると、先のハマりシナリオで出てくるような事はほぼ考慮、サポートされています。まあそれを知ってて作ったシナリオなので我田引水っぽいですが、それでもそれなりに有用そうだと感じられるのではないでしょうか。パフォーマンスに関して言うと、もちろん直接コンポーネント同士がやり取りする場合と比べるとコンテナ処理のオーバヘッドが多少ある訳ですが、JBIは現実を踏まえた仕様としてメッセージの標準形式変換などの処理を行わないので、問題になるほどではないと思います(とはいえベンチマークがあると嬉しいですが)。また、スケーラビリティに関してはクラスタリングもサポートされています。
終わりに
先述のJavaOneの時には、エンタープライズな技術とWeb2.0的なサービスをどう合わせて見せるかでarclamp鈴木さん、ATL Systemsの北条さんと結構悩んだのですが、こうして実在のサービスをイメージしながらESBについて考えられる時代がついに来たようです(大げさ)。最初にも書いた通り、TwitterがどうなるかはともかくMIS(マルチインタフェースサービス)は増えていくと思うので、そういったサービスを作ろうという方は、言語やイメージに関わらず一度見てみると使わなくても参考になると思います。TwitterはRubyですし(メッセージサーバはErlangだそうですが)、上記の例でももっと軽い実装があり得ると思いますが、既存のよく考えられたアーキテクチャを知ればより適切な設計や実装に近づけることでしょう。
もうちょっと言うと、特別にJavaやESBを推したいとかではなく、サービスを実現する為のアーキテクチャを想像・模索する中で少々縁が薄そうな分野がつながってくるのは楽しいですし、ちょっと範囲を広げて探すと意外と面白いものもあるかも知れません、という話なのでした。一文が長くてすみません。
Spring JavaConfigのM2が出ました
「4月5月に作業するよ」って言って、本当に5月上旬に出てくるのは偉いですね。
リリースのアナウンス
ダウンロード
追加されたアノテーション
- @ExternalBean
- 親のコンテナ設定(Application Contextの設定)を引き継ぐ
- @ScopedProxy
- scoped proxyの指定。これ今まで使った事無くて知らなかったのですが、例えばシングルトンBeanがセッションスコープBeanの参照を持っている時に、直接参照を持つと1つしかinjectされない(だってシングルトンだから)という問題をProxyで解消するもののようですね。proxy scopeが指定されると、プロキシの参照を持つようになって、呼び出し時にスコープに応じて適切に参照が取り出される(セッションならセッションから取得)と。なるほどー。
という感じでした。
ちなみに今後についてはドキュメントの最後に一言、"Future development will be focused on automatic configuration discovery and simplifications"とあります。
*1:ScopedProxyの話じゃないかと思うのですが、何を指しているのかはっきり分からず
SpringFrameworkで命名規約ベースの設定を実現するArid POJOs
記事のタイトルみたいですが、1行で言えばそういう事です。今まで(Springに)無かったのが不思議なくらいですね。
作者はPOJOs IN ACTIONの著者Chris Richardson氏。
では、早速使ってみます。
基本サンプル
まず下記サイトからダウンロード。
Arid POJOsのWebサイト
Guiceに続きGoogle Codeです。ちなみにオープンソースなホストではCodehausが一番尖ったイメージでかっこ良いと思います。私の中で。どうでも良いですね。
- 今回試すサンプルのパッケージ構成
- sample.arid
- SimpleBootStrap : 起動クラス
- SimpleClient : サービス呼び出し元
- sample.arid.service
- SimpleService : サービスインタフェース
- SimpleServiceImple : サービス実装
- sample.arid
実装の中身は例によってどうでも良いので割愛します(1点だけ、SimpleClientはSetterInjectionです)。
さてXML定義ですが、Arid POJOsの定義はSpring2で導入されたXML Schemaの名前空間拡張を用いています。XML拡張と言えばXBeanはどうしてるだろうと思って1年半ぶりくらいに見たら、なんかよく分からなくなってます…。XBeanは、James Strachan氏がSpringチームに提案したら(熟考の末)拒否られて、「Springチームには失望した!」とか怒ってたんですよね。結局2.0で同じような仕様になったんですが。
話が逸れましたが、Aridの定義。名前空間の宣言と、定義本体。
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:arid="http://chrisrichardson.net/schema/arid" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://chrisrichardson.net/schema/arid http://chrisrichardson.net/schema/arid.xsd"> <arid:define-beans package="sample.arid" autowire="byType"/> </beans>
これだけです。パッケージとautowireのタイプを指定しただけ。では実行。
Re: Hello, Arid POJOs!
成功!*1
と、これだけだと何のこっちゃなのですが、上記のXML定義は以下のように書いたのと同じです。
<bean id="simpleClient" class="sample.arid.SimpleClient" autowire="byType" /> <bean id="simpleService" class="sample.arid.service.SimpleServiceImpl" autowire="byType"/>
一般化すると、以下のように動作します。
- package属性で指定したパッケージ以下(サブパッケージを含む)の全具象クラスを対象に、
- クラス名の頭を小文字にし(更に"Impl"で終わる場合は"Impl"を削って)idにし、
- autowire属性で指定したautowireを指定して
を定義したものとする。
ちなみにautowireのデフォルト値はconstractorなので、コンストラクタインジェクションの場合にはautowireも省略可能です。
その他の設定
上記の基本設定に加えて、Aridでは以下のような設定が可能です。
指定したパターンに合致するクラスのみを対象とする
パッケージを指定しただけだと、サブパッケージを含む全具象クラスが対象となってしまいます。なんせAridのソースは String classPattern = "classpath*:/" + packagePart + "/**/*.class"; とかなってる。
そこで、pattern属性をつけて、Spring2.0らしいAspectJ文法で更にクラスを絞り込めます*2。
<arid:define-beans package="sample.arid" pattern="@org.springframework.transaction.annotation.Transactional sample..Simple*" autowire="byType"/>
この例だと、@Transactionalが付いていて、sampleパッケージ以下で、クラス名がSimpleで始まるクラスを対象としています。
クラス名⇒bean名の変換ルールを変える
デフォルトだと頭を小文字化&末尾にImplがあったら除去、というルールですが、自前で変換ルールを実装して使用する事ができます。
実装例は省略しますが、AridBeanNameGeneratorを実装して、XMLで以下のようにname-generator属性を指定するだけです。
<arid:define-beans package="sample.arid" name-generator="sample.arid.MyBeanNameGenerator" />
一部のクラスを例外的な定義にする
以下のように記述する事で、自動的に生成される定義をオーバーライドする事が可能です。
<arid:define-beans package="sample.arid" autowire="byType"> <arid:extras> <bean id="simpleService" class="sample.arid.service.SimpleServiceImpl"> <property name="key" value="1" /> </bean> </arid:extras> </arid:define-beans>
この例では、"simpleService"というidの定義をオーバーライドしていますので、実行時は自動的に作られる定義ではなくこちらの定義が採用されます。
結論
タイトルにも書いた通り、このArid POJOsを使えばSpring Frameworkで所謂CoC(Convention Over Configuration)を部分的に実現できますので、XMLが肥大化する場合のソリューションとして検討する価値があると思います。もちろん省略する事によるデメリットもありますが、マクロ展開に近いようなシンプルな仕組みなので部分的導入も可能ですし、なにより「省略する」という選択肢ができた事がとても喜ばしいですね。
*1:ソース見せずに「成功!」もないですが
*2:AspectJ weaverが必要なので、Springのdependencyに入ってるやつを使うなどして下さい。mavenを使うと勝手に入ると思います。
Guiceコミッタによるプレゼン動画と資料
GuiceコミッタであるKevin Bourrillion氏とBob Lee氏によるプレゼン動画とその資料が公開されました。
Java on Guice: Dependency Injection Java Way
プレゼン資料(PDF)
↓動画を貼り付けてみました。
http://video.google.com/videoplay?docid=6068447410873108038
今回は基本編ですが、今月"Volume II"があるそうなので楽しみに待ちたいと思います。
ところで動画、冒頭のタイトルでいきなりKevinの苗字間違ってますね。まあ本人もb9nって略すくらいだからよくある事なのかも知れませんが…。
Pythonのスペル修正プログラムをJavaに移植してみました
オレンジニュースで紹介されていた、GoogleのPeter Norvig氏による"スペル修正プログラムはどう書くか"(原文)を読んで、ちょっと試してみたかったので深く考えずにJavaに移植しました。Pythonの文法を全く知らなかったのですが、元々とても短い上にコードだけでなく説明もあったので何とか最後まで到達。
という事で、メインのコードを貼り付けます*1。テストコードを含んだ完全版(?)はこちら。一応名前等はできるだけ合わせました。
import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * originated from : http://norvig.com/spell.py */ public class SpellCorrect { static final class CountHolder { int count = 2; } private static Matcher words(String text) { Pattern re = Pattern.compile("[a-z]+"); return re.matcher(text.toLowerCase()); } private static Map<String, CountHolder> train(Matcher matcher) { Map<String, CountHolder> model = new HashMap<String, CountHolder>(); while(matcher.find()) { CountHolder newCount = new CountHolder(); CountHolder exists = model.put(matcher.group(), newCount); if(exists != null) { newCount.count = exists.count + 1; } } return model; } private static final Map<String, CountHolder> NWORDS = train(words(readFile("e:/dev/python/big.txt"))); private static final char[] alphabet = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static Set<String> edits1(String word) { int n = word.length(); Set<String> set = new HashSet<String>(); for(int i = 0; i < n; i++) { set.add(word.substring(0, i) + word.substring(i + 1)); } for(int i = 0; i < n-1; i++) { set.add(word.substring(0, i) + word.charAt(i + 1) + word.charAt(i) + word.substring(i + 2)); } for(int i = 0; i < n; i++) { for(char c : alphabet) { set.add(word.substring(0, i) + c + word.substring(i + 1)); } } for(int i = 0; i < n+1; i++) { for(char c : alphabet) { set.add(word.substring(0, i) + c + word.substring(i)); } } return set; } private static Set<String> known_edits2(String word) { Set<String> set = new HashSet<String>(); for(String e1 : edits1(word)) { for(String e2 : edits1(e1)) { if(NWORDS.containsKey(e2)) { set.add(e2); } } } return set; } private static Set<String> known(Set<String> words) { Set<String> knownWords = new HashSet<String>(); for(String w : words) { if(NWORDS.containsKey(w)) { knownWords.add(w); } } return knownWords; } public static String correct(String word) { Set<String> group; Set<String> input = new HashSet<String>(Arrays.asList(new String[] { word })); group = known(input); if(group.isEmpty()) { group = known(edits1(word)); if(group.isEmpty()) { group = known_edits2(word); if(group.isEmpty()) { group = input; } } } return Collections.max(group, new Comparator<String>() { public int compare(String w1, String w2) { return NWORDS.get(w1).count - NWORDS.get(w2).count; } }); } private static String readFile(String fileName) { BufferedInputStream in = null; try { in = new BufferedInputStream(new FileInputStream(fileName)); byte[] buf = new byte[in.available()]; in.read(buf); return new String(buf, "ISO-8859-1"); } catch (IOException e) { throw new IllegalStateException(e); } finally { if(in != null) { try { in.close(); } catch(Exception ex) {} } } } }
ちょっと調べた感じ、巷にJavaのスペル修正のライブラリ等もあるようですが、ざっくりアルゴリズムを学ぶには良いと思います。間違いなど見つけられた方はコメントでご指摘頂けると幸いです。
ちなみに初めてPythonのコードを読んでみた感想としては、分かり易いのか分かり難いのか微妙なところでした。Javaと比べて直感的(Javaで書いてて「こう書けたらなー」と思うような事が実現できている)な部分もありつつ、「これパッと読むと間違うのでは?」的なところもありますので。何れにせよ、Rubyに入門中の身としてはなかなか面白い体験でした。
想像していたよりコーパスの処理も軽いようなので、monstar.fmでもアーティストや楽曲の検索で使いたいと思ってます。
追記
最初Rubyで書いてみようと思ったのですが、LL系は誰かすぐにやるだろうと思っていたら、やはりid:k12uさんがPerlで書かれていました。
それPerlで書けるよ(当たり前だ)
小飼さんはCPANのライブラリを使って更にAPI化されてます。
JavaでやるならDWRかな?2.0も出て、Reverse AjaxやJavaScript Proxy API、Guice連携などかなり面白く楽に使えそうな機能が目白押しなので、それでなくとも試してみたいところ。
*1:追記:テストコードを一部含んだままだったので削除しました
Spring Java Configuration(2)
1回目に続き、Spring Java Configurationを試してみたいと思います。今回はAOP。ちなみに付属のドキュメントにはAOPの説明が無かったので(さすがM1リリース)、テストケースを参考に試してみます。
Spring Java ConfigurationでのAOP定義
injectするServiceのインタフェースと実装クラス、そしてinjectされるClient実装クラスは、前回と同じものを使用します。AOPは既存の実装を変更せずに透過的に処理を追加(変更)できるのが売りですから、当然と言えば当然です。
で、前回のコンフィギュレーションクラスにAOPの定義を追加して、インターセプタも書いちゃいます。
@Configuration public class SpringConfigurationWithAOP { @Bean public Service service() { return new ServiceImpl(); } @Bean(scope = Scope.PROTOTYPE) public SpringClient client() { return new SpringClient(service()); } // 今回の追加部分。@SpringAdviceによってweavingする場所(Pointcut)を指定 @Bean @SpringAdvice("execution(* sample.guice.service.*.*(..))") public LoggingInterceptor interceptor() { return new LoggingInterceptor(); } // weavingするインターセプタ static class LoggingInterceptor implements MethodInterceptor { public Object invoke(MethodInvocation methodInvocation) throws Throwable { String methodName = methodInvocation.getMethod().getName(); System.out.println("before: " + methodName); Object obj = methodInvocation.proceed(); // 本来の処理を実行 System.out.println("after: " + methodName); return obj; } } }
なおPointcutの指定は、Spring2.0のドキュメントを参考にしました。
では実行してみましょう。
Exception in thread "main" org.springframework.beans.factory.BeanDefinitionStoreException: sample.guice.springjavaconf.SpringConfigurationWithAOP$LoggingInterceptor contains no Bean creation methods or Aspect methods
at org.springframework.beans.factory.java.ConfigurationProcessor.process(ConfigurationProcessor.java:179)
(以下略)
ぎゃふん。インターセプタにBeanの定義もAspectの定義も無いと。そりゃ確かに無いけど、だから@Configurationつけてないんですが…。もしかしてアノテーションが内部クラスに引き継がる仕様なんでしょうか?よく分かりませんが、M1であまり深追いしても仕方ないのでやめときます。
とりあえずLoggingInterceptorを独立したクラスとして外に出して、再度実行。
before: getResponse
after: getResponse
Re: Hello
client == anotherClient : false
client.getService() == anotherClient.getService() : true
できた! beforeとafterがちゃんと出力されているので、正しく実行されたようです。
アノテーションに対してAOPをかけてみる
Guiceのトランザクションの回で、@Transactionalアノテーションをつけたメソッドに対して下記のようにインターセプタを指定しました。
bindInterceptor(any(), annotatedWith(Transactional.class), transactionInterceptor);
Spring Java Configでもできるはずという事で、アノテーションをつけたメソッドをインターセプトする指定を試してみます。
まずServiceImplにアノテーションをつけます。
public class ServiceImpl implements Service { @Transactional public String getResponse(String msg) { return "Re: " + msg; } }
今回は単にログを出すだけなので@Transactionalは不適切かもしれませんが、分かり易いのでドンマイ。
そしてAOPの指定ですが、さっきのここを見るとPointcutの例として
@annotation(org.springframework.transaction.annotation.Transactional)
ってのがあるので、これをそのまま@SpringAdviceに書いてみます。
@Bean @SpringAdvice("@annotation(org.springframework.transaction.annotation.Transactional)") public LoggingInterceptor interceptor() { return new LoggingInterceptor(); }
そして実行
before: getResponse
after: getResponse
Re: Hello
client == anotherClient : false
client.getService() == anotherClient.getService() : true
おぉっ、一発で上手くいきました。
考察
えらいあっさりしてますが、Spring Java ConfigurationではAOPの定義も簡単にできる事が分かりました。AspectJなPointcut文法がタイプセーフではない(SpringIDE使うと解決したりするのかも?たしか昔AspectJ用のEclipseプラグインで、Joinpointをマーキングしたりできたし)のはディスアドバンテージですが、その分指定方法は多彩で強力ですね。
という事で、2回に渡りSpring Java Configurationを簡単に見てみました。設定を全て外部化するという意味ではGuiceよりもクリーンなソリューションだと思います。一方でSpring in Actionの人のブログ(このエントリは私のよりずっとちゃんと考察しているので、GuiceとSpring Java Configurationの比較に興味がある方はコメント欄含めて必見です)では、コメント欄でGuiceのBob Lee氏が
In the Real World, you use dependencies much more often (M) than you define them (N). If Spring were an O(N*M) solution, Guice would be an O(N) solution.
なんて反論を寄せたりもしています。これは普通injectするServiceよりもinjectされるクラス(StrutsであればActionなど)の方が多いけど、Spring Java Configurationの場合こういうケースだと@BeanのメソッドをAction数分書かなきゃいけないんじゃないの?Guiceなら@Injectって書くだけだよ?って話です。確かにinject箇所の明示が必須条件だとすると、そういう事になりそうです。その辺が今後のリリースで解決されるのかされないのかも引き続き注目していきたいところです。
Spring Java Configurationの今後
このプロジェクトのリーダはCostin Leau氏です。氏はプロジェクトの発端となったRod Johnson氏のブログのコメントで
Spring JavaConfig is anything but dead. I plan to spend more time on it in April and have a release out ASAP - probably end of April/May.
と発言しているので、あと1ヵ月くらい待つと進捗するかも知れません。楽しみです。