ClojureでDropwizardを使う

ClojureでDropwizardを使う
========================

Motivation
----------
- Clojureでも始めたい
- それなら使い慣れたDropwizardでも使ってAPIでも作ってみよう


結論
----
- `io.dropwizard.Application`を継承する部分はJavaで書く
- それ以外はClojureで書いても大丈夫


構成
----

leiningenを使っています。
JavaとClojureのソースを分けるため、src下でさらにディレクトリを区切っています。

```
  ./
    resources/
      test.yml
    src/
      clojure/
        test2/
          Test2Configuration.clj
          Test2Resource.clj
      java/
        test2/
          Test2Service.java
    project.clj
```

---

`Test2Service.java`の中でClojureで作成したモジュールを参照しているため、先にClojureのモジュールをコンパイルするようにしています。


```clojure:project.clj
(defproject test "0.1.0-SNAPSHOT"
  :description "Dropwizard Test Project"
  :url "http://example.com/FIXME"
  :license {:name "MIT License"
            :url "http://opensource.org/licenses/MIT"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [io.dropwizard/dropwizard-core "0.7.1"]]
  :source-paths ["src/clojure"]
  :java-source-paths ["src/java"]
  :prep-tasks [["compile" "test2.Test2Configuration"]
               ["compile" "test2.Test2Resource"]
               "javac"]
  :main test2.Test2Service)
```

---

普通のJavaBeanっぽくアクセスできるようにしています。（Test2Configurationに値が設定されるのは起動時のみです）

```clojure:Test2Configuration.clj
(ns test2.Test2Configuration
  (:import [com.fasterxml.jackson.annotation JsonProperty])
  (:gen-class :name test2.Test2Configuration
              :extends io.dropwizard.Configuration
              :init init
              :state template
              :methods [[getTemplate [] String]
                        [^{JsonProperty {}} setTemplate [String] void]]))

(defn -init [& args]
  [nil (ref "")])

(defn -getTemplate [this]
  @(.template this))

(defn -setTemplate [this template]
  (dosync
    (ref-set (.template this) template)))
```

---

アクセス部分です。`gen-class`部分にアノテーションが集中します。

```clojure:Test2Resource.clj
(ns test2.Test2Resource
  (:gen-class :name ^{javax.ws.rs.Path "/test"
                      javax.ws.rs.Produces ["text/plain"] #_javax.ws.rs.core.MediaType/TEXT_PLAIN}
               test2.Test2Resource
              :init init
              :constructors {[test2.Test2Configuration] []}
              :state template
              :methods [[^{javax.ws.rs.GET {}} hello [] String]]))

(defn -init [template]
  [nil template])

(defn -hello [this]
  (.getTemplate (.template this)))
```

---

Javaのソースです。

```java:TestService.java
package test2;

import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;

public class Test2Service extends Application<Test2Configuration> {

    public static void main(String...args) throws Exception {
        new Test2Service().run(args);
    }

    @Override
    public void run(Test2Configuration configuration, Environment environment) throws Exception {
        System.out.println("run");

        environment.jersey().setUrlPattern("/api/*");

        environment.jersey().register(new Test2Resource(configuration));
    }

    @Override
    public void initialize(Bootstrap<Test2Configuration> bootstrap) {
        System.out.println("initialize");
    }
}
```

---

プロパティファイルです。

```:test.yml
template: hello world!
```

### 実行

```
lein run server resource\test.yml
```

とかやればサーバが動きます。
http://localhost:8080/api/test にアクセスすると`test.yml`で定義した文字列が表示されます。


## なぜJavaを使ったか

`Test2Service.java`のClojure版が`TestService.clj`です。

```clojure:TestService.cjj
(ns test.TestService
  (:import [io.dropwizard.setup Bootstrap]
           [io.dropwizard.setup Environment])
  (:gen-class :extends ^{:parameters [test.TestConfiguration]} io.dropwizard.Application
              :init init
              :constructors {[] []}
              :main true))

(defn -init [& args]
  [[] []])

(defn -main [& args]
  (.run (test.TestService.) (into-array args)))

(defn -run [this configuration environment]
  (println "run")
  (.setUrlPattern (.jersey environment) "/api/*")
  (.register (.jersey environment) (test.TestResource. configuration)))
```


これを実行すると起動直後に`java.lang.IllegalStateException`で異常終了します。

```:StackTrace
Exception in thread "main" java.lang.IllegalStateException: Cannot figure out type parameterization for test.TestService, ...
        at clojure.lang.Compiler.load(Compiler.java:7142)
        ...(省略)...
        at clojure.main.main(main.java:37)
Caused by: java.lang.IllegalStateException: Cannot figure out type parameterization for test.TestService
        at io.dropwizard.util.Generics.getTypeParameter(Generics.java:62)
        at io.dropwizard.Application.getConfigurationClass(Application.java:30)
        at io.dropwizard.cli.ServerCommand.<init>(ServerCommand.java:25)
        at io.dropwizard.Application.run(Application.java:68)
        at test.TestService$_main.doInvoke(TestService.clj:13)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at clojure.lang.Var.invoke(Var.java:383)
        at user$eval5.invoke(form-init1306515106957402759.clj:1)
        at clojure.lang.Compiler.eval(Compiler.java:6703)
        at clojure.lang.Compiler.eval(Compiler.java:6693)
        at clojure.lang.Compiler.load(Compiler.java:7130)
        ... 11 more
```

Javaで作ったクラスとClojureで作ったクラスをjavapで逆アセンブルして比較してみます。

```java:TestServiceをjavap
public class test.TestService extends io.dropwizard.Application {
  public static {};
  public test.TestService();
  public java.lang.Object clone();
  public int hashCode();
  public java.lang.String toString();
  public boolean equals(java.lang.Object);
  public void initialize(io.dropwizard.setup.Bootstrap);
  public java.lang.String getName();
  public void run(io.dropwizard.Configuration, io.dropwizard.setup.Environment);
  public static void main(java.lang.String[]);
}
```

```java:Test2Serviceをjavap
public class test2.Test2Service extends io.dropwizard.Application<test2.Test2Configuration> {
  public test2.Test2Service();
  public static void main(java.lang.String...) throws java.lang.Exception;
  public void run(test2.Test2Configuration, io.dropwizard.setup.Environment) throws java.lang.Exception;
  public void initialize(io.dropwizard.setup.Bootstrap<test2.Test2Configuration>);
  public void run(io.dropwizard.Configuration, io.dropwizard.setup.Environment) throws java.lang.Exception;
}
```

比較すると`io.dropwizard.Application`と`io.dropwizard.Appilcation<test2.Test2Configuration>`が違います。
もとの`io.dropwizard.Application`は`<T>`で型パラメータを受け取るように設計されていて、その型パラメータをあちこち持って回るつくりになっています。

最終的にはここで例外を吐いています。
https://github.com/dropwizard/dropwizard/blob/master/dropwizard-util/src/main/java/io/dropwizard/util/Generics.java#L35


Clojureでコンパイルした方は型パラメータの情報が消えてしまっているので、`io.dropwizard.util.Generics.getTypeParameter`で実行時にエラーとなっています。


## 対策

先にも書いたのですが、`io.dropwizard.Application`を継承する部分だけJavaで書いて、残りはClojureで書きます。
JavaからClojureを使うので、先にClojureのソースをコンパイルするようにしています。

Javaで使っていたからDropwizardを使ってみましたが、[Liberator](http://clojure-liberator.github.io/liberator/)を使うのが良いのかもしれません。


## 参考

extend/implement parameterized types (generics)
http://dev.clojure.org/jira/browse/CLJ-970

Is it possible to extend a parameterized type using gen-class?
  https://groups.google.com/forum/#!topic/clojure/Xv1pKATfP0c

Liberator
  http://clojure-liberator.github.io/liberator/

heion (RESTful Clojure with Dropwizard)
  https://github.com/jblomo/heion
今のバージョンだとこの方法で動かない・・・・