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のモジュールをコンパイルするようにしています。
(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に値が設定されるのは起動時のみです)
(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
部分にアノテーションが集中します。
(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のソースです。
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");
}
}
プロパティファイルです。
template: hello world!
実行
lein run server resource\test.yml
とかやればサーバが動きます。
http://localhost:8080/api/test にアクセスするとtest.yml
で定義した文字列が表示されます。
なぜJavaを使ったか
Test2Service.java
のClojure版がTestService.clj
です。
(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
で異常終了します。
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で逆アセンブルして比較してみます。
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[]);
}
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を使うのが良いのかもしれません。
参考
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
今のバージョンだとこの方法で動かない・・・・