Java系のミドルウェアとかで遊ぶ時に、ちょっとずつClojureに移っていこうと思っているのですが、フレームワークとかを使う場合につまづきそうな気がしたのがアノテーション。
これ、ClojureでJavaのクラスを作る時に付与できるの?ってことで、試してみました。Leiningenのaotを使ったりすることになりましたが、一応できました。
お題は、InfinispanのListenerにしました。…パッと手頃なものが思い浮かばなかったんですよ。
project.clj
(defproject annotated "0.1.0-SNAPSHOT" :description "FIXME: write description" :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.5.1"] [org.infinispan/infinispan-core "5.3.0.Final"]] :main annotated.core :aot [annotated.listener annotated.core] :repositories {"JBoss Public Maven Repository Group" "http://repository.jboss.org/nexus/content/groups/public-jboss"})
メインのソース。
src/annotated/core.clj
(ns annotated.core (:gen-class) (:import (annotated.listener CacheListener) (org.infinispan.manager DefaultCacheManager))) (defn -main [& args] (let [manager (DefaultCacheManager.) cache (.getCache manager)] (try (.addListener cache (CacheListener.)) (doto cache (.put "key1" "value1") (.get "key1")) (finally (.stop manager)))))
importしているのは、InfinispanのDefaultCacheManagerと、この後に出てくるClojureで書いたJavaのクラスです。
先のproject.cljで、core.cljからlistener.cljに対して依存関係があるため、aotの順番は
:aot [annotated.listener annotated.core]
としています。これが最初わからずに、めちゃくちゃハマりました。何回やってもClassNotFound、みたいな。
で、InfinispanのListener系のアノテーションを付与したClojureコード。
src/annotated/listener.clj
(ns annotated.listener (:import (org.infinispan.notifications Listener) (org.infinispan.notifications.cachelistener.annotation CacheEntryCreated CacheEntryVisited))) (gen-class :name ^{Listener {:sync false}} annotated.listener.CacheListener :methods [[^{CacheEntryCreated []} cacheEntryCreated [org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent] void] [^{CacheEntryVisited []} cacheEntryVisited [org.infinispan.notifications.cachelistener.event.CacheEntryVisitedEvent] void]]) (defn- current-thread-name [] (.getName (Thread/currentThread))) (defn -cacheEntryCreated [this event] (println (format "作成イベント: key[%s], value[%s], thread[%s]" (.getKey event) (.getValue event) (current-thread-name)))) (defn -cacheEntryVisited [this event] (println (format "参照イベント: key[%s], value[%s], thread[%s]" (.getKey event) (.getValue event) (current-thread-name))))
通常、ns関数に:gen-classキーワードを付与してJavaのクラスを作ると思いますが、そうはせずにgen-class関数でクラスを宣言します。アノテーションは、メタデータとして記述するみたいです。
(gen-class :name ^{Listener {:sync false}} annotated.listener.CacheListener :methods [[^{CacheEntryCreated []} cacheEntryCreated [org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent] void] [^{CacheEntryVisited []} cacheEntryVisited [org.infinispan.notifications.cachelistener.event.CacheEntryVisitedEvent] void]])
この時、クラスに付与しているアノテーションはListenerで、syncパラメータを設定しています。マップの形で設定してあげればいいみたいです。
^{Listener {:sync false}}
これで、
@Listener(sync = false)
と同じみたいです。
アノテーションに引数がない場合はからのベクタでOKみたいです。
^{CacheEntryCreated []}
今回は扱っていませんが、アノテーションのパラメータの名前がvalueの場合はJavaでもパラメータ名を省略できますが、その場合は
SuppressWarnings ["Warning1"]
みたいに書けばいいみたいです。もしくは
java.lang.annotation.Retention java.lang.annotation.RetentionPolicy/SOURCE
引数がひとつの場合は、ベクタとしなくてもよい?
2つ以上アノテーションを付ける場合は、
^{Deprecated [] SuppressWarnings ["Warning1"] java.lang.annotation.Target []
という感じで。
ところで、アノテーションとは関係ないのですが、:methodsキーワードで宣言するメソッドのパラメータのクラス名、FQCNじゃないといけないんでしょうか?
:methods [[^{CacheEntryCreated []} cacheEntryCreated [org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent] void] [^{CacheEntryVisited []} cacheEntryVisited [org.infinispan.notifications.cachelistener.event.CacheEntryVisitedEvent] void]]
:importを書いていても、ここをクラス名だけにするとjava.langから探しに行っちゃってClassNotFoundになりまして…。
では、先ほどのコードを動かしてみます。
$ lein run Compiling annotated.listener Compiling annotated.core 8 30, 2013 12:19:34 午前 org.infinispan.factories.GlobalComponentRegistry start INFO: ISPN000128: Infinispan version: Infinispan 'Tactical Nuclear Penguin' 5.3.0.Final 8 30, 2013 12:19:35 午前 org.infinispan.jmx.CacheJmxRegistration start INFO: ISPN000031: MBeans were successfully registered to the platform MBean server. 作成イベント: key[key1], value[null], thread[notification-thread-0] 作成イベント: key[key1], value[value1], thread[notification-thread-0] 参照イベント: key[key1], value[value1], thread[notification-thread-0] 参照イベント: key[key1], value[value1], thread[notification-thread-0]
動きましたね。Listenerも非同期になっています。
今回は、gen-classでアノテーションを付与したクラスを作成しましたが、deftypeとかでもできるみたいです。
この辺りが例としてすごく参考になりました。
https://github.com/clojure/clojure/blob/master/test/clojure/test_clojure/genclass/examples.clj
https://github.com/clojure/clojure/blob/master/test/clojure/test_clojure/annotations/java_6.clj
http://stackoverflow.com/questions/7703467/attaching-metadata-to-a-clojure-gen-class
http://stackoverflow.com/questions/7622036/using-clojure-with-an-annotation-based-rest-server
これで、きっとアノテーションを要求するJavaのコードを求められてもある程度戦える…はず。ただ、フィールドにアノテーションを求められた場合って、できるんでしたっけ…?
:stateだと、finalでObjectなフィールドができるので、微妙じゃないかなと。