Arantium Maestum

プログラミング、囲碁、読書の話題

Clojure Web Development勉強 - Ring (その2) コード詳解

とりあえず前回張ったコードの説明をしておく。

gist.github.com

「詳解も何もそのまんまだろうが(怒」と言われたら「その通りですハハーッ(平伏」と言うしかないのだが、まあ自分のアウトプット&備忘録を兼ねて。

まずproject.clj。超簡単。というかlein newでできたものを削りまくった後で、ちょこっと1行足して作っている。

(defproject ring-min-log "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [ring "1.5.0"]])

足したのは[ring "1.5.0"]の部分だけ。

ringは4つのライブラリに分割されていて、必要なものだけdependenciesに記述する方式でもいける。というか多分そちらの方が推奨かと思う(特に本番に向けた開発では)。今回はできるだけ単純なままに留めたい、ということでring全てを一括でdependencyとして導入している。

ちなみに分割されたライブラリは以下のとおり:

  • ring-core - essential functions for handling parameters, cookies and more
  • ring-devel - functions for developing and debugging Ring applications
  • ring-servlet - construct Java servlets from Ring handlers
  • ring-jetty-adapter - a Ring adapter that uses the Jetty webserver

ソース

今回はring-coreとring-jetty-adapterがあれば充分。(他の二つはイマイチ何ができるのかわかっていない)

次はcore.clj。これはsrc/ring_min_log/に入っている前提。当然ながら冒頭はnamespaceの定義とnamespace内で使うライブラリの宣言。

(ns ring-min-log.core
  (require
    [ring.util.response :as res]
    [ring.adapter.jetty :refer [run-jetty]]
    ))

これでring-coreからring.util.responseを、ring-jetty-adapterからring.adapter.jettyをとってきている。これらの説明は、実際に使っているコードのところで行う。

ということでこれ:

(defn handler [req]
  (-> (res/response "Hello Goodbye")
      (res/content-type "text/plain")))

ハンドラとはリクエストマップを受け取りレスポンスマップを返す関数であり、ringでwebアプリを作る場合、このハンドラがほとんどの動的ページ作成ロジックを担う。

上記のコードはなんらかのリクエストマップに対して、"Hello Goodbye"という文字列を表示させるレスポンスマップを返すだけのハンドラを作っている。

前述の通り、ringの最大の責務はHTTP requestをclojureのimmutable mapに変換してくれ、そしてまた開発者が書いたコードの最終的な戻り値であるimmutable mapをHTTP responseに変換して出力してくれることだ。

レスポンスに変換されるimmutable mapは、もちろん自分で(必要なフィールドを全て指定して)書くこともできるのだが、デフォルト値などもちゃんと設定してくれてレスポンスマップ作成を相当簡易にしてくれる関数がいろいろとring.util.responseに入っている。

(res/response "Hello Goodbye"){:status 200 :header {} :body "Hello Goodbye"}というマップを作成してくれ、そのマップを(res/content-type % "text/plain")に入れることでそこに:header {"Content-Type" "text/plain"}が加わる。

直に書くなら

(defn handler [req]
  {:status 200
   :header {"Content-Type" "text/plain"}
   :body "Hello Goodbye"}

になる。

次に、リクエストマップとレスポンスマップを全部保存してreplから確認できるようにしたい!ということで、その機能をmiddlewareとして実装してみる。

middlewareとは、ハンドラ関数を引数に取り、なんらかの機能を付け加えた新しいハンドラ関数を返す高階関数である。

(defonce log-atom (atom []))
(defn logger [handler]
  (fn [req]
    (let [res (handler req)]
      (swap! log-atom #(conj % {:request req :response res}))
      res)))

loggerが返す新しいハンドラは、

  1. まずリクエストマップを受け取り
  2. loggerの引数となった元のハンドラに渡してレスポンスマップを作成
  3. 副作用としてlog-atomにそのリクエストマップとレスポンスマップを追加
  4. 最後にレスポンスマップを返す

という関数になっている。これでreplからlog-atomの中身を調べて、どういうリクエストに対してどういうレスポンスが返されているのか、非常に簡単に参照することができる。バリバリ副作用なのが悲しいところだが・・・

次はhandlerとloggerの組み合わせ:

(def my-app
  (-> handler
      logger))

ここはhandlerをloggerに渡して新しいmy-appハンドラを作成しているだけ。middleware一つだけなのでthreading macroのありがたみが薄いが、これでいろんなmiddlewareを重ねることで様々な機能を追加していくことが簡単になる。

最後にサーバの立ち上げとハンドラの登録:

(defonce server (run-jetty #'my-app {:port 8080 :join? false}))

Javaで書かれたJettyというHTTPサーバ兼Java Servletコンテナを使い、replが使えるようにnon-blockingな設定でポート8080からのリクエストをマップに変換したのちにmy-appに投げ、my-appからの戻り値をHTTPレスポンスに変換したのちにクライアントに返すサーバを立ち上げている。

defonce#'my-appとしているのはやはりrepl開発のため。

defonceのおかげでコードを全部再評価させてもこの部分は読み込まれない(サーバを新しく立ち上げない)。これがないと、とりあえず再読み込み時にサーバを閉じるロジックが必要になる。

しかし、素直に(defonce server (run-jetty my-app {:port 8080 :join? false}))と書くとmy-appの定義がdefonceされた時点でのものに固定されてしまい、コードに対する変更が全く反映されなくなってしまう。それを避けるためにclojureのvar-quote dispatcherマクロ#'を使い、run-jettyにmy-appそのものではなくmy-appへの参照を渡すことで、my-appが更新されたらserverも最新の定義を使うようにしている。

あとはこのcore.cljをREPLで評価し、ブラウザでlocalhost:8080に繋げればオッケー。例えばREPLから

user=> (ns ring-min-log.core)
ring-min-log.core=> @log-atom

でリクエストマップとレスポンスマップが見れたりする。