CLOVER🍀

That was when it all began.

Clojureで、アノテーションを付与したJavaのクラスを作成する

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なフィールドができるので、微妙じゃないかなと。