1. 序章

Ringは、ClojureでWebアプリケーションを作成するためのライブラリです。 フル機能のWebアプリを作成するために必要なすべてをサポートし、さらに強力にするためのエコシステムが繁栄しています。

このチュートリアルでは、Ringの概要を説明し、Ringで実現できることのいくつかを示します。

Ringは、多くの最新のツールキットのように、RESTAPIを作成するために設計されたフレームワークではありません。 これは、従来のWeb開発に重点を置いた、一般的なHTTPリクエストを処理するための低レベルのフレームワークです。 ただし、一部のライブラリは、他の多くの必要なアプリケーション構造をサポートするために、その上に構築されています。

2. 依存関係

Ringの使用を開始する前に、Ringをプロジェクトに追加する必要があります。 必要な最小の依存関係はです。

これらをLeiningenプロジェクトに追加できます。

  :dependencies [[org.clojure/clojure "1.10.0"]
                 [ring/ring-core "1.7.1"]
                 [ring/ring-jetty-adapter "1.7.1"]]

次に、これを最小限のプロジェクトに追加できます。

(ns ring.core
  (:use ring.adapter.jetty))

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello World"})

(defn -main
  [& args]
  (run-jetty handler {:port 3000}))

ここでは、ハンドラー関数を定義しました。これについては後で説明しますが、常に文字列「HelloWorld」を返します。 また、このハンドラーを使用するためのメイン関数を追加しました。ポート3000でリクエストをリッスンします。

3. コアコンセプト

Leiningenには、すべてが構築されるいくつかのコアコンセプトがあります。リクエスト、レスポンス、ハンドラー、ミドルウェアです。

3.1. リクエスト

リクエストは、着信HTTPリクエストを表したものです。 Ringはリクエストをマップとして表し、Clojureアプリケーションが個々のフィールドと簡単に対話できるようにします。 このマップには、以下を含むがこれらに限定されない標準のキーセットがあります。

  • :uri –完全なURIパス。
  • :query-string –完全なクエリ文字列。
  • :request-method :get、:head、:post、:put、:delete 、または:optionsのいずれかのリクエストメソッド。
  • :headers –リクエストに提供されたすべてのHTTPヘッダーのマップ。
  • :body –要求本文を表す InputStream (存在する場合)。

ミドルウェアは、必要に応じてこのマップにさらにキーを追加する場合があります

3.2. 反応

同様に、応答は発信HTTP応答の表現です。 Ringは、これらを3つの標準キーを持つマップとしても表します

  • :status –返送するステータスコード
  • headers –返送するすべてのHTTPヘッダーのマップ
  • body –返送するオプションの本文

以前のように、ミドルウェアはそれを生成するハンドラーとクライアントに送信される最終結果の間でこれを変更する可能性があります。

Ringは、応答の作成を容易にするためのヘルパーも提供します

これらの最も基本的なものは、 ring.util.response / response 関数です。この関数は、ステータスコード 200OKで単純な応答を作成します。

ring.core=> (ring.util.response/response "Hello")
{:status 200, :headers {}, :body "Hello"}

一般的なステータスコードに対してこれに対応する方法は他にもいくつかあります。たとえば、 bad-request not-found redirect[ X159X]:

ring.core=> (ring.util.response/bad-request "Hello")
{:status 400, :headers {}, :body "Hello"}
ring.core=> (ring.util.response/created "/post/123")
{:status 201, :headers {"Location" "/post/123"}, :body nil}
ring.core=> (ring.util.response/redirect "https://ring-clojure.github.io/ring/")
{:status 302, :headers {"Location" "https://ring-clojure.github.io/ring/"}, :body ""}

また、既存の応答を任意のステータスコードに変換するstatusメソッドもあります。

ring.core=> (ring.util.response/status (ring.util.response/response "Hello") 409)
{:status 409, :headers {}, :body "Hello"}

次に、応答の他の機能を同様に調整するいくつかのメソッドがあります-たとえば、 content-type、header 、または set-cookie

ring.core=> (ring.util.response/content-type (ring.util.response/response "Hello") "text/plain")
{:status 200, :headers {"Content-Type" "text/plain"}, :body "Hello"}
ring.core=> (ring.util.response/header (ring.util.response/response "Hello") "X-Tutorial-For" "Baeldung")
{:status 200, :headers {"X-Tutorial-For" "Baeldung"}, :body "Hello"}
ring.core=> (ring.util.response/set-cookie (ring.util.response/response "Hello") "User" "123")
{:status 200, :headers {}, :body "Hello", :cookies {"User" {:value "123"}}}

set-cookieメソッドは、応答マップにまったく新しいエントリを追加することに注意してください。 これを正しく処理するには、wrap-cookiesミドルウェアが必要です。

3.3. ハンドラー

リクエストとレスポンスを理解したので、ハンドラー関数を作成して結び付けることができます。

ハンドラーは、着信要求をパラメーターとして受け取り、発信応答を返す単純な関数です。 この関数で行うことは、このコントラクトに適合する限り、完全にアプリケーション次第です。

最も単純な方法では、常に同じ応答を返す関数を作成できます。

(defn handler [request] (ring.util.response/response "Hello"))

必要に応じてリクエストを処理することもできます。

たとえば、着信IPアドレスを返すハンドラーを作成できます。

(defn check-ip-handler [request]
    (ring.util.response/content-type
        (ring.util.response/response (:remote-addr request))
        "text/plain"))

3.4. ミドルウェア

ミドルウェアは一部の言語では一般的な名前ですが、Javaの世界ではあまり一般的ではありません。 概念的には、サーブレットフィルターやスプリングインターセプターに似ています。

Ringでは、ミドルウェアはメインハンドラーをラップし、そのいくつかの側面を何らかの方法で調整する単純な関数を指します。 これは、処理前に着信要求を変更すること、生成後に送信応答を変更すること、または処理にかかった時間をログに記録することだけを意味する可能性があります。

一般に、ミドルウェア関数は、ハンドラーの最初のパラメーターを取得してラップし、新しい機能を備えた新しいハンドラー関数を返します。

ミドルウェアは必要な数の他のパラメーターを使用できます。 たとえば、次を使用して、ラップされたハンドラーからのすべての応答にContent-Typeヘッダーを設定できます。

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

それを読むと、リクエストを受け取る関数が返されることがわかります。これが新しいハンドラーです。 次に、提供されたハンドラーを呼び出して、応答の変更されたバージョンを返します。

これを使用して、それらをチェーンするだけで新しいハンドラーを作成できます。

(def app-handler (wrap-content-type handler "text/html"))

Clojureは、スレッドマクロを使用して、より自然な方法で多くをチェーンする方法も提供します。 これらは、呼び出す関数のリストを提供する方法であり、それぞれに前の関数の出力が含まれています。

特に、スレッドファーストマクロが必要です。 ->。 これにより、最初のパラメーターとして提供された値を使用して各ミドルウェアを呼び出すことができます。

(def app-handler
  (-> handler
      (wrap-content-type "text/html")
      wrap-keyword-params
      wrap-params))

これにより、3つの異なるミドルウェア関数にラップされた元のハンドラーであるハンドラーが生成されました。

4. ハンドラーの作成

Ringアプリケーションを構成するコンポーネントを理解したので、実際のハンドラーで何ができるかを知る必要があります。 これらはアプリケーション全体の中心であり、ビジネスロジックの大部分が行く場所です。

データベースアクセスや他のサービスの呼び出しなど、必要なコードをこれらのハンドラーに入れることができます。 Ringは、着信要求または発信応答を直接操作するためのいくつかの追加機能を提供します。これらも非常に便利です。

4.1. 静的リソースの提供

Webアプリケーションが実行できる最も単純な機能の1つは、静的リソースを提供することです。 Ringは、これを簡単にする2つのミドルウェア関数であるwrap-fileとwrap-resourceを提供します。

wrap-fileミドルウェアはファイルシステム上のディレクトリを取ります。 着信要求がこのディレクトリ内のファイルと一致する場合、ハンドラ関数を呼び出す代わりに、そのファイルが返されます。

(use 'ring.middleware.file)
(def app-handler (wrap-file your-handler "/var/www/public"))

非常によく似た方法で、 wrap-resourceミドルウェアは、ファイルを検索するクラスパスプレフィックスを取得します

(use 'ring.middleware.resource)
(def app-handler (wrap-resource your-handler "public"))

どちらの場合も、ラップされたハンドラー関数は、ファイルがクライアントに戻ることが見つからない場合にのみ呼び出されます。

Ringは、HTTPAPIを介してこれらをよりクリーンに使用できるようにする追加のミドルウェアも提供します。

(use 'ring.middleware.resource
     'ring.middleware.content-type
     'ring.middleware.not-modified)

(def app-handler
  (-> your-handler
      (wrap-resource "public")
      wrap-content-type
      wrap-not-modified)

wrap-content-type ミドルウェアは、要求されたファイル名拡張子に基づいて、設定するContent-Typeヘッダーを自動的に決定します。 wrap-not-modified ミドルウェアは、If-Not-ModifiedヘッダーをLast-Modified値と比較して、HTTPキャッシングをサポートし、ファイルが返される場合にのみファイルを返します。必要です。

4.2. リクエストパラメータへのアクセス

要求を処理するとき、クライアントがサーバーに情報を提供できるいくつかの重要な方法があります。 これらには、POSTおよびPUTリクエストのリクエストペイロードとして送信されるクエリ文字列パラメータ(URLおよびフォームパラメータに含まれる)が含まれます。

パラメーターを使用する前に、wrap-paramsミドルウェアを使用してハンドラーをラップする必要があります。 これにより、パラメータが正しく解析され、URLエンコードがサポートされ、リクエストで利用できるようになります。 これにより、オプションで使用する文字エンコードを指定できます。指定されていない場合は、デフォルトでUTF-8になります。

(def app-handler
  (-> your-handler
      (wrap-params {:encoding "UTF-8"})
  ))

完了すると、リクエストが更新され、パラメータが利用可能になります。 これらは、着信要求の適切なキーに入ります。

  • :query-params –クエリ文字列から解析されたパラメータ
  • :form-params –フォーム本体から解析されたパラメーター
  • :params :query-params :form-paramsの両方の組み合わせ

これをリクエストハンドラで期待どおりに利用できます。

(defn echo-handler [{params :params}]
    (ring.util.response/content-type
        (ring.util.response/response (get params "input"))
        "text/plain"))

このハンドラーは、パラメーターinputからの値を含む応答を返します。

パラメータは、値が1つしかない場合は単一の文字列にマップされ、複数の値が存在する場合はリストにマップされます

たとえば、次のパラメータマップを取得します。

// /echo?input=hello
{"input "hello"}

// /echo?input=hello&name=Fred
{"input "hello" "name" "Fred"}

// /echo?input=hello&input=world
{"input ["hello" "world"]}

4.3. ファイルのアップロードを受信する

多くの場合、ユーザーがファイルをアップロードできるWebアプリケーションを作成できるようにしたいと考えています。 HTTPプロトコルでは、これは通常、マルチパートリクエストを使用して処理されます。 これらにより、単一のリクエストにフォームパラメータとファイルのセットの両方を含めることができます。

Ringには、この種の要求を処理するためのwrap-multipart-paramsと呼ばれるミドルウェアが付属しています。 これは、 wrap-params 単純なリクエストを解析します。

wrap-multipart-params は、アップロードされたファイルを自動的にデコードしてファイルシステムに保存し、ハンドラーにファイルを処理する場所を通知します。

(def app-handler
  (-> your-handler
      wrap-params
      wrap-multipart-params
  ))

デフォルトでは、アップロードされたファイルは一時システムディレクトリに保存され、1時間後に自動的に削除されます。 これには、クリーンアップを実行するために、JVMが次の1時間実行されている必要があることに注意してください。

必要に応じて、メモリ内ストアもありますが、大きなファイルがアップロードされると、メモリが不足するリスクがあります。

API要件を満たしている限り、必要に応じてストレージエンジンを作成することもできます。

(def app-handler
  (-> your-handler
      wrap-params
      (wrap-multipart-params {:store ring.middleware.multipart-params.byte-array/byte-array-store})
  ))

このミドルウェアがセットアップされると、アップロードされたファイルは、paramsキーの下の着信要求オブジェクトで使用可能になります。 これは、wrap-paramsミドルウェアを使用するのと同じです。 このエントリは、使用するストアに応じて、ファイルを操作するために必要な詳細を含むマップです。

たとえば、デフォルトの一時ファイルストアは次の値を返します。

  {"file" {:filename     "words.txt"
           :content-type "text/plain"
           :tempfile     #object[java.io.File ...]
           :size         51}}

ここで、:tempfile エントリは、ファイルシステム上のファイルを直接表すjava.io.Fileオブジェクトです。

4.4. クッキーの操作

Cookieは、サーバーが少量のデータを提供できるメカニズムであり、クライアントは後続のリクエストで引き続きデータを送り返します。 これは通常、セッションID、アクセストークン、または構成されたローカリゼーション設定などの永続的なユーザーデータに使用されます。

Ringには、Cookieを簡単に操作できるミドルウェアがありますこれにより、自動的に着信リクエストのCookieが解析され、発信応答で新しいCookieを作成できるようになります。

このミドルウェアの構成は、以前と同じパターンに従います。

(def app-handler
  (-> your-handler
      wrap-cookies
  ))

この時点で、すべての着信リクエストのCookieが解析され、リクエストの:cookiesキーに配置されます。 これには、Cookieの名前と値のマップが含まれます。

{"session_id" {:value "session-id-hash"}}

送信応答に:cookiesキーを追加することで、送信応答にCookieを追加できます。 これを行うには、応答を直接作成します。

{:status 200
 :headers {}
 :cookies {"session_id" {:value "session-id-hash"}}
 :body "Setting a cookie."}

以前にステータスコードまたはヘッダーを設定したのと同様の方法で、応答にCookieを追加するために使用できるヘルパー関数もあります

(ring.util.response/set-cookie 
    (ring.util.response/response "Setting a cookie.") 
    "session_id" 
    "session-id-hash")

Cookieには、HTTP仕様の必要に応じて、追加のオプションを設定することもできます set-cookie を使用している場合は、キーと値の後にマップパラメーターとしてこれらを提供します。 このマップの鍵は次のとおりです。

  • :domain –Cookieを制限するドメイン
  • :path –Cookieを制限するパス
  • :secure true は、HTTPS接続でのみCookieを送信します
  • :http-only true は、JavaScriptにCookieにアクセスできないようにします
  • :max-age –ブラウザがCookieを削除するまでの秒数
  • :expires –ブラウザがCookieを削除するまでの特定のタイムスタンプ
  • :same-site :strict に設定されている場合、ブラウザーはクロスサイトリクエストでこのCookieを送り返しません。
(ring.util.response/set-cookie
    (ring.util.response/response "Setting a cookie.")
    "session_id"
    "session-id-hash"
    {:secure true :http-only true :max-age 3600})

4.5. セッション

クッキーは、クライアントがリクエストごとにサーバーに送り返す情報のビットを保存する機能を提供します。 これを実現するためのより強力な方法は、セッションを使用することです。 これらは完全にサーバーに保存されますが、クライアントは使用するセッションを決定する識別子を維持します。

ここにある他のすべてと同様に、セッションはミドルウェア関数を使用して実装されます。

(def app-handler
  (-> your-handler
      wrap-session
  ))

デフォルトでは、これはセッションデータをメモリに保存します。 必要に応じてこれを変更できます。Ringには、Cookieを使用してすべてのセッションデータを保存する代替ストアが付属しています。

ファイルのアップロードと同様に、必要に応じてストレージ機能を提供できます

(def app-handler
  (-> your-handler
      wrap-cookies
      (wrap-session {:store (cookie-store {:key "a 16-byte secret"})})
  ))

セッションキーの保存に使用されるCookieの詳細を調整することもできます

たとえば、セッションCookieが1時間持続するようにするには、次のようにします。

(def app-handler
  (-> your-handler
      wrap-cookies
      (wrap-session {:cookie-attrs {:max-age 3600}})
  ))

ここでのCookie属性は、wrap-cookiesミドルウェアでサポートされているものと同じです。

セッションは、多くの場合、操作するデータストアとして機能します。 これは関数型プログラミングモデルでは常にうまく機能するとは限らないため、Ringはそれらをわずかに異なる方法で実装します。

代わりに、リクエストからセッションデータにアクセスし、データのマップを返し、レスポンスの一部としてデータに保存します。 これは、変更された値だけでなく、保存するセッション状態全体です。

たとえば、以下は、ハンドラーが要求された回数の実行カウントを保持します。

(defn handler [{session :session}]
  (let [count   (:count session 0)
        session (assoc session :count (inc count))]
    (-> (response (str "You accessed this page " count " times."))
        (assoc :session session))))

このように機能することで、キーを含めないだけで、セッションからデータを削除できます。 新しいマップに対してnilを返すことにより、セッション全体を削除することもできます。

(defn handler [request]
  (-> (response "Session deleted.")
      (assoc :session nil)))

5. Leiningenプラグイン

Ringは、Leiningenビルドツールのプラグインを提供して開発と本番の両方を支援します。

project.clj ファイルに正しいプラグインの詳細を追加して、プラグインを設定します。

  :plugins [[lein-ring "0.12.5"]]
  :ring {:handler ring.core/handler}

ラインリングのバージョンがリングのバージョンに対して正しいことが重要です。 ここではリング1.7.1を使用しています。つまり、ラインリング0.12.5が必要です。 一般に、Mavenセントラルまたは lein search コマンドで見られるように、両方の最新バージョンを使用するのが最も安全です。

$ lein search ring-core
Searching clojars ...
[ring/ring-core "1.7.1"]
  Ring core libraries.

$ lein search lein-ring
Searching clojars ...
[lein-ring "0.12.5"]
  Leiningen Ring plugin

:ring 呼び出しの:handler パラメーターは、使用するハンドラーの完全修飾名です。 これには、定義したミドルウェアを含めることができます。

このプラグインを使用すると、メイン関数が不要になります。 Leiningenを使用して開発モードで実行することも、展開目的で本番アーティファクトを構築することもできます。コードはロジックに正確に依存し、それ以上のものはありません

5.1. プロダクションアーティファクトの構築

これが設定されると、標準のサーブレットコンテナにデプロイできるWARファイルを作成できるようになります。

$ lein ring uberwar
2019-04-12 07:10:08.033:INFO::main: Logging initialized @1054ms to org.eclipse.jetty.util.log.StdErrLog
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.war

ハンドラーを期待どおりに実行するスタンドアロンJARファイルを作成することもできます

$ lein ring uberjar
Compiling ring.core
2019-04-12 07:11:27.669:INFO::main: Logging initialized @3016ms to org.eclipse.jetty.util.log.StdErrLog
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT.jar
Created ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar

このJARファイルには、インクルードした組み込みコンテナーでハンドラーを開始するメインクラスが含まれます。 これにより、 PORT の環境変数も尊重され、実稼働環境で簡単に実行できるようになります。

PORT=2000 java -jar ./clojure/ring/target/uberjar/ring-0.1.0-SNAPSHOT-standalone.jar
2019-04-12 07:14:08.954:INFO::main: Logging initialized @1009ms to org.eclipse.jetty.util.log.StdErrLog
WARNING: seqable? already refers to: #'clojure.core/seqable? in namespace: clojure.core.incubator, being replaced by: #'clojure.core.incubator/seqable?
2019-04-12 07:14:10.795:INFO:oejs.Server:main: jetty-9.4.z-SNAPSHOT; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_77-b03
2019-04-12 07:14:10.863:INFO:oejs.AbstractConnector:main: Started ServerConnector@44a6a68e{HTTP/1.1,[http/1.1]}{0.0.0.0:2000}
2019-04-12 07:14:10.863:INFO:oejs.Server:main: Started @2918ms
Started server on port 2000

5.2. 開発モードで実行

開発の目的で、ハンドラーを手動でビルドして実行することなく、Leiningenから直接実行できます。 これにより、実際のブラウザでアプリケーションをテストするのが簡単になります。

$ lein ring server
2019-04-12 07:16:28.908:INFO::main: Logging initialized @1403ms to org.eclipse.jetty.util.log.StdErrLog
2019-04-12 07:16:29.026:INFO:oejs.Server:main: jetty-9.4.12.v20180830; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_77-b03
2019-04-12 07:16:29.092:INFO:oejs.AbstractConnector:main: Started ServerConnector@69886d75{HTTP/1.1,[http/1.1]}{0.0.0.0:3000}
2019-04-12 07:16:29.092:INFO:oejs.Server:main: Started @1587ms

これは、 PORT 環境変数を設定している場合は、それも尊重します。

さらに、プロジェクトに追加できるリング開発ライブラリがあります。 これが利用可能な場合、開発サーバーは検出されたソース変更を自動的にリロードしようとします。 これにより、コードを変更してブラウザでライブで確認する効率的なワークフローが得られます。 これには、ring-devel依存関係を追加する必要があります。

[ring/ring-devel "1.7.1"]

6. 結論

この記事では、ClojureでWebアプリケーションを作成する手段としてRingライブラリーについて簡単に紹介しました。 次のプロジェクトで試してみませんか?

ここで取り上げたいくつかの概念の例は、GitHubで見ることができます。