ClojureでGoogle App Engine Webアプリケーションの作成
Google App EngineのWebアプリをClojureで簡単に作成できるか試してみた。
appengine-magic
appengine-magicを使うと簡単そうだな。
https://github.com/gcv/appengine-magic/tree/
Getting Startedに従って、作ってみよう。
Google AppEngine SDKのインストール
Python版とJava版があるが、Java版をダウンロードする。
~/optにappengne-java-sdk-1.4.0を解凍する。
~/opt/appengine-java-sdk-1.4.0/binにPATHを通す。
Hello,world!を作る
まずはHello,world!を作ってみるよ。
不要ファイルの削除
src/hello/core.cljは後でappengine-magicで生成するので、削除する。
$ rm src/hello/core.clj
依存ライブラリの取得
project.cljを編集し、:dev-dependenciesに次のライブラリを追加する。
:dev-dependencies [ [appengine-magic "0.3.2"] ]
依存ライブラリを取得し、appengine-magicプラグインを有効にする。
$ lein deps
デフォルトファイルの作成
appengine-newタスクを実行し、次のデフォルトファイルを作成する。
$ lein appengine-new
動作確認
REPLを起動する。
user=> (use 'hello.core) nil user=> (require '[appengine-magic.core :as ae]) nil user=> (ae/start hello-app) 2011-01-15 10:11:10.314:INFO::Logging to STDERR via org.mortbay.log.StdErrLog 2011-01-15 10:11:10.371:INFO::jetty-6.1.x 2011-01-15 10:11:14.795:INFO::Started SocketConnector@0.0.0.0:8080 #<Server Server@62fb35> user=>
ブラウザで http://localhost:8080/ を表示する。
「Hello, world!」が表示されればOK。
サーバ停止は
user=> (ae/stop)
ポート番号を変えるには、
user=> (ae/start foo-app :port 8095)
開発用サーバでのテスト
エントリポイントのサーブレットをコンパイルし、アプリケーションのjarファイルを作成する。
このjarファイルと依存ライブラリをresorces/WEB-INF/libへコピーする。
$ lein appengine-prepare
開発用サーバを起動する。
$ dev_appserver.sh resources/ ..snip.. 情報: Started SelectChannelConnector@127.0.0.1:8080 2011/01/15 1:35:05 com.google.appengine.tools.development.DevAppServerImpl start 情報: The server is running at http://localhost:8080/
ブラウザで http://localhost:8080/ を表示する。
「Hello, world!」が表示されればOK。
Google App Engineへのデプロイ
resouces/WEB-INF/appengine-web.xmlのapplicationに公開するアプリケーションIDを設定する。
hogehoge-appを取得していれば、
<application>hogehoge-app</application>
を設定する。
アプリケーションをデプロイする。
$ appcfg.sh update resources/
..snip..
Email: メールアドレスを入力
Password for 入力したメールアドレス: パスワードを入力
..snip..
Update completed successfully.
Success.
Cleaning up temporary files...
http://アプリケーションID.appspot.com/ にアクセスしてみる。
「Hello, world!」が表示された。
動いた。\(^▽^)/
マルチスレッドでWebページを取得する
HttpClientを使って、マルチスレッドでWebページを取得するサンプル。
leiningenを使う場合は、project.cljの:dependenciesに[commons-httpclient"3.1"]を記述し、lein depsを実行すればライブラリを取得してくれる。
(ns web-get (:import [org.apache.commons.httpclient HttpClient MultiThreadedHttpConnectionManager]) (:import [org.apache.commons.httpclient.methods GetMethod])) (defn fetch [client url] (let [get (GetMethod. url)] (println (str "Fetching: " url)) (try (let [status (.executeMethod client get)] (println (str "Got " url ": " status))) (finally (.releaseConnection get))))) (def client (HttpClient. (MultiThreadedHttpConnectionManager.))) (def pages ["http://www.yahoo.co.jp/" "http://www.google.com/" "http://localhost/"]) (defn do-it [] (doseq [url pages] (.start (Thread. (fn [] (fetch client url))))))
実行結果
user> (in-ns 'web-get) #<Namespace web-get> web-get> (do-it) nil Fetching: http://www.yahoo.co.jp/ Fetching: http://www.google.com/ Fetching: http://localhost/ Got http://localhost/: 200 Got http://www.yahoo.co.jp/: 200 Got http://www.google.com/: 200
動いた。\(^▽^)/
参考URL
Echo server
Programming Clojureを読んだ直後にも書いたが、またEcho serverを書いてみた。以前に作成したのは冗長だった。
clojure.contrib.server-socketとclojure.contrib.duck-streams/copyを使うと簡単だ。
(ns socket-test (:use [clojure.contrib.server-socket :only (create-server close-server)] [clojure.contrib.duck-streams :only (copy)])) (def *server* (atom nil)) (def *port* 10000) (defn echo-fn [in out] (println "echo start") (copy in out) (println "echo end")) (defn start-server [] (reset! *server* (create-server *port* echo-fn))) (defn stop-server [] (when-not (nil? @*server*) (close-server @*server*) (reset! *server* nil)))
Echo serverを実行する。
user> (in-ns 'socket-test) #<Namespace socket-test> socket-test> (start-server) {:server-socket #<ServerSocket ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=10000]>, :connections #<Ref@1c1eceb: #{}>}
telnetしてみる。
satoshi@tpx61 /cygdrive/c/home $ telnet localhost 10000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. hoge ←◆入力 hoge ←◆返ってきた fuga ←◆入力 fuga ←◆返ってきた telnet> Connection closed. satoshi@tpx61 /cygdrive/c/home $
動いた。\(^▽^)/
Echo serverを終了する。
socket-test> (stop-server) nil
printlnが不要なら、start-server関数のcreate-server部分は
(create-server *port* copy)
だけで良さそうだ。
trace - 便利なdotraceマクロ
ここしばらくHaskellやAndroidプログラミングに興味を引かれ、Clojureからは遠ざかっていた。blogの更新は5ヶ月ぶりか。月日が経つのは早いなぁ。
print関数を使ってデバッグしていたが、面倒くさくなってきたので、調べてみたら、ClojureでもCommon Lispみたいなtraceマクロがあった。初めから調べておけば良かったよ。
トレース出力を試すためのサンプルコード。特に意味は無いコード。
(defn mul [a b] (* a b)) (defn mul-n [n] (loop [n n sum 0] (if (<= n 0) sum (recur (dec n) (+ sum (mul n n))))))
user> (mul-n 3) 14
mul関数をトレースしてみる。
トレースするにはdotraceマクロを使用する。dotraceの第一引数にはトレースしたい関数名をリストで渡す(複数指定できる)。第二引数以降は評価したい式を渡す。
user> (use 'clojure.contrib.trace) ←◆useして nil user> (dotrace [mul] (mul-n 3)) ←◆トレースする TRACE t2746: (mul 3 3) TRACE t2746: => 9 TRACE t2747: (mul 2 2) TRACE t2747: => 4 TRACE t2748: (mul 1 1) TRACE t2748: => 1 14
これは便利。\(^▽^)/
ここ http://groups.google.com/group/clojure/browse_thread/thread/5553d6ab004ad8a0/3cfe2a8709b43a99 を見ると、他のtraceライブラリの情報あるが、一年以上前の投稿なので、今はもっと良いものがあるかもしれない。
フィールドとメソッド探しにちょっと便利な関数を作ってみた
SLIMEで開発している際に、Javaのクラスが持つフィールドやメソッド名を調べたい事がある。
例えば、文字列を大文字に変換するメソッドを忘れてしまって、uppercaseだったかな?toUpperだったかな?なんて時に、ちょっと便利な関数。
使い方
(inspect 調べたいオブジェクトまたはクラス マッチ文字列)
すべてのフィールド・メソッドを表示したい場合は
(inspect 調べたいオブジェクトまたはクラス)
とする。
使用例
user> (use 'tools) ; tools名前空間をuseする nil user> (inspect String "upper") ; Stringクラスをupperで調べる String toUpperCase(Locale) String toUpperCase() nil user> (inspect Integer) ; Integerクラスの全てのフィールド・メソッドを調べる Integer MIN_VALUE Integer MAX_VALUE Integer TYPE Integer SIZE int hashCode() int reverseBytes(int) boolean equals(Object) ...略... user> (inspect 1.0 "infi") ; オブジェクトを指定しても良い Double NEGATIVE_INFINITY Double POSITIVE_INFINITY boolean isInfinite(double) boolean isInfinite() nil user>
コード
(ns tools (:use [clojure.contrib.str-utils :only (str-join)])) (defn to-class [obj] (if (class? obj) obj (class obj))) (defn- filter-name [coll s] (filter #(not= (.. % getName toUpperCase (indexOf s)) -1) (sort-by #(. % getName) coll))) (defn- match-field [cls s] (let [fields (. cls getFields)] (if s (filter-name fields s) fields))) (defn- match-method [cls s] (let [methods (. cls getMethods)] (if s (filter-name methods s) methods))) (defn inspect ([obj] (inspect obj nil)) ([obj s] (let [cls (to-class obj) mstr (if s (. s toUpperCase))] (doseq [field (match-field cls mstr)] (let [field-name (. field getName) class-name (.. field getDeclaringClass getSimpleName)] (println (str class-name " " field-name)))) (doseq [method (match-method cls mstr)] (let [method-name (. method getName) return-class-name (.. method getReturnType getSimpleName) param-class-names (for [param-class (. method getParameterTypes)] (. param-class getSimpleName))] (println (str return-class-name " " method-name "(" (str-join ", " param-class-names) ")")))))))
clojure,clojure-contribのバージョンアップ(1.1.0のビルド方法)
今更なのですが、http://d.hatena.ne.jp/e-o-n/20091024 に書いた手順でバージョン1.0をビルド後に、バージョン1.1.0のjarを作成する手順のメモ。
~/opt以下にclojureとclojure-contribをgit cloneしている。
まずはclojureから。
~% cd opt/clojure ~/opt/clojure% git branch * (no branch) master ~/opt/clojure% git checkout master Previous HEAD position was f85444e... tag for 1.0 Switched to branch "master" ~/opt/clojure% git pull Already up-to-date. ※いつの間にかgit pullしていた... ~/opt/clojure% git tag 1.0 1.1.0 ~/opt/clojure% git checkout 1.1.0 Note: moving to "1.1.0" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new_branch_name> HEAD is now at 5293929... remove qualifier for 1.1.0 release ~/opt/clojure% ant Buildfile: build.xml ...略... clojure: [jar] Building jar: ~/opt/clojure/clojure-1.1.0.jar [copy] Copying 1 file to ~/opt/clojure ...略... BUILD SUCCESSFUL Total time: 21 seconds
次はclojure-contrib。
~/opt/clojure% cd ../clojure-contrib/ ~/opt/clojure-contrib% git branch * (no branch) master ~/opt/clojure-contrib% git checkout master Previous HEAD position was b985ecf... Set version to 1.0.0 in Ant build and POM Switched to branch "master" ~/opt/clojure-contrib% git pull remote: Counting objects: 106, done. remote: Compressing objects: 100% (70/70), done. ...略... ~/opt/clojure-contrib% git tag 1.0.0 1.0.0-RC1 1.0.0-RC2 1.1.0 1.1.0-RC1 1.1.0-RC2 1.1.0-RC3 ~/opt/clojure-contrib% git checkout 1.1.0 Note: moving to "1.1.0" which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new_branch_name> HEAD is now at d132c5f... Set version to 1.1.0 in Ant build and POM ~/opt/clojure-contrib% ant Buildfile: build.xml ...略... jar: [jar] Building jar: ~/opt/clojure-contrib/clojure-contrib.jar [jar] Building jar: ~/opt/clojure-contrib/clojure-contrib-slim.jar BUILD SUCCESSFUL Total time: 1 second
次の場所にclojureとclojure-contribのjarファイルが作成される。
~/opt/clojure/clojure-1.1.0.jar
~/opt/clojure-contrib/clojure-contrib.jar
Twitter4jでつぶやく
ClojureでTwitterのbotを作ろうと思い、TWitterライブラリを探してみたら、Twitter4Jは扱いが簡単そうだったので試してみた。
Twitter4Jは最新の安定バージョンである2.1.0を使用した。
まずは、Twitter4Jのサンプルコードを参考に、つぶやくコードを書いてみた。
(ns twitter.hellobot (:import (twitter4j TwitterFactory Twitter))) (def user-id "******") (def password "******") (def twitter (. (TwitterFactory.) getInstance user-id password)) (. twitter updateStatus "Hello, world!")
実行すると、twitterインスタンスの取得でエラーとなる。
(def twitter (. (TwitterFactory.) getInstance user-id password)) ←◆ここでエラー
getInstanceメソッドが見えないようだ。
No matching method found: getInstance for class twitter4j.TwitterFactory [Thrown class java.lang.IllegalArgumentException] Restarts: 0: [ABORT] Return to SLIME's top level. Backtrace: 0: clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:85) 1: clojure.lang.Reflector.invokeInstanceMethod(Reflector.java:28) 2: clojure.lang.Compiler$InstanceMethodExpr.eval(Compiler.java:1150) 3: clojure.lang.Compiler$DefExpr.eval(Compiler.java:298) 4: clojure.lang.Compiler.eval(Compiler.java:4537)
何が原因が良く分からないので、とりあえずclojureのバージョンを1.1.0に上げて試してみたら、以下のエラーとなった。
Can't call public method of non-public class: public java.lang.Object twitter4j.TwitterFactoryBase.getInstance(java.lang.String,java.lang.String) [Thrown class java.lang.IllegalArgumentException] Restarts: 0: [ABORT] Return to SLIME's top level. Backtrace: 0: clojure.lang.Reflector.invokeMatchingMethod(Reflector.java:85) 1: clojure.lang.Reflector.invokeInstanceMethod(Reflector.java:28) 2: clojure.lang.Compiler$InstanceMethodExpr.eval(Compiler.java:1206) 3: clojure.lang.Compiler$DefExpr.eval(Compiler.java:302) 4: clojure.lang.Compiler.eval(Compiler.java:4647) 5: clojure.core$eval__5236.invoke(core.clj:2017)
バージョン1.1.0ではエラーメッセージが親切になっている。
TwitterFactoryBaseクラスはTwitterFactoryクラスの親クラスであり、そのクラスがgetInstanceメソッドを実装している。エラーを見ると親クラスTwitterFactoryBaseも可視属性がpublicでないと駄目のようだ。
リフレクションの仕様?時間ができたら調べてみよう。
面倒になってきたので、twitter4jのソースを修正し、TwitterFactoryBaseクラスをpublicにしてみる。
twitter.hellobot> (def twitter (. (TwitterFactory.) getInstance user-id password)) #'twitter.hellobot/twitter twitter.hellobot>
正常にインスタンスを取得できたようだ。
一歩前進だが、いきあたりばったり方式でちょっと鬱。
twitterインスタンスが取得できたので、つぶやいてみる。
twitter.hellobot> (. twitter updateStatus "hello, world!") #<StatusJSONImpl StatusJSONImpl{createdAt=Sat Jan 30 21:32:43 JST 2010, id=8409854604, text='hello, world!', ...以下略... twitter.hellobot>
http://twitter.com/ で確認してみる。
書き込めた。\(^▽^)/
Twitter4Jは依存ライブラリが無いので、jarファイル一つで済むので、なかなか良いです。
オープンソースで公開して頂ける作者に感謝です。