リングでClojure Webappsを書く

  • link:/category/programming/ [プログラミング]

1. 前書き

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

    このチュートリアルでは、Ringを紹介し、Ringで達成できることのいくつかを示します。
    Ringは、非常に多くの最新のツールキットのように、REST APIを作成するために設計されたフレームワークではありません。 *これは一般的なHTTPリクエストを処理するための下位レベルのフレームワーク*であり、従来のWeb開発に焦点を当てています。 ただし、一部のライブラリは、その上に構築され、他の多くの望ましいアプリケーション構造をサポートします。

2. 依存関係

Ringでの作業を開始する前に、プロジェクトに追加する必要があります。 *必要な最小依存関係は次のとおりです*:
  • ring/ring-core

  • _https://mvnrepository.com/artifact/ring/ring-jetty-adapter [ring / ring-jetty-adapter] _

    これらを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}))
ここでは、すぐに説明するハンドラー関数を定義しました。これは、常に文字列「Hello World」を返します。 また、このハンドラーを使用するメイン関数を追加しました。ポート3000で要求をリッスンします。

3. コアコンセプト

ライニンゲンには、すべてを構築するための中心的な概念がいくつかあります:要求、応答、ハンドラー、ミドルウェア。

* 3.1。 リクエスト*

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

  • _:query-string _–完全なクエリ文字列。

  • :request-method – method_:get、:head、:post、
    :put, :delete_ or :options.

  • :headers –リクエストに提供されるすべてのHTTPヘッダーのマップ。

  • :body –リクエスト本文を表す_InputStream_(存在する場合)。

    *ミドルウェアは、必要に応じてこのマップにさらにキーを追加することができます*。

* 3.2。 反応*

同様に、応答は発信HTTP応答の表現です。 *リングは、これらを3つの標準キーを持つマップとしても表します*:
  • :status –返信するステータスコード

  • headers –返信するすべてのHTTPヘッダーのマップ

  • body –返信するオプションの本文

    前と同じように、*ミドルウェアは、ハンドラーを生成するハンドラーとクライアントに送信される最終結果との間でこれを変更する可能性があります*。
    *リングには、応答を簡単に作成できるようにするヘルパーも用意されています*。
    これらの最も基本的なものは_ring.util.response / response_関数であり、_200 OK_のステータスコードで簡単な応答を作成します。
ring.core=> (ring.util.response/response "Hello")
{:status 200, :headers {}, :body "Hello"}
*一般的なステータスコード*には、これに沿った他のいくつかのメソッドがあります。たとえば、_bad-request _、_ not-found _、_ redirect_などです。
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 __methodメソッドもあります。
ring.core=> (ring.util.response/status (ring.util.response/response "Hello") 409)
{:status 409, :headers {}, :body "Hello"}
*その後、同様に応答の他の機能を調整するためのいくつかのメソッドがあります*-例えば、__ content-type、header __or _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は、https://clojure.org/guides/threading_macros [Threading Macros] *を使用することにより、より自然な方法で多くを連結する方法も提供します。 これらは、呼び出す関数のリストを提供する方法であり、各関数には前の関数の出力があります。

    *特に、Thread Firstマクロが必要です。* _ *-> * ._これにより、指定された値を最初のパラメーターとして各ミドルウェアを呼び出すことができます。
(def app-handler
  (-> handler
      (wrap-content-type "text/html")
      wrap-keyword-params
      wrap-params))
これにより、3つの異なるミドルウェア関数にラップされた元のハンドラーであるハンドラーが作成されました。

4. ハンドラーの作成

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は、これらのクリーナーをHTTP API経由で使用するための追加のミドルウェアも提供します。
(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。 要求パラメーターへのアクセス*

要求を処理するとき、クライアントがサーバーに情報を提供できる重要な方法がいくつかあります。 これらには、URLおよびフォームパラメーターに含まれるクエリ文字列パラメーターが含まれます。これらは、POSTおよびPUT要求の要求ペイロードとして送信されます。
*パラメーターを使用する前に、_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プロトコルでは、これは通常、マルチパート要求を使用して処理されます。 これらにより、1つのリクエストにフォームパラメータと一連のファイルの両方を含めることができます。
*リングには、この種の要求を処理する_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 __middlewareを使用するのと同じです。 このエントリは、使用するストアに応じて、ファイルを操作するために必要な詳細を含むマップです。
たとえば、デフォルトの一時ファイルストアは値を返します。
  {"file" {:filename     "words.txt"
           :content-type "text/plain"
           :tempfile     #object[java.io.File ...]
           :size         51}}
__:tempfile __entryは、ファイルシステム上のファイルを直接表す_java.io.File_オブジェクトです。

* 4.4。 Cookieの使用*

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

    *リングにはミドルウェアがあり、クッキーを簡単に操作できます*。 .
    このミドルウェアの構成は、以前と同じパターンに従います。
(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を制限するパス

  • :securetrue HTTPS接続でのみCookieを送信する

  • :http-only – _true_は、CookieにJavaScriptがアクセスできないようにします

  • :max-age –ブラウザーが削除するまでの秒数
    クッキー

  • :expires –ブラウザが削除する特定のタイムスタンプ
    クッキー

  • :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。 セッション*

Cookieを使用すると、クライアントがリクエストごとにサーバーに送り返す情報のビットを保存できます。 これを実現するより強力な方法は、セッションを使用することです。 これらは完全にサーバーに格納されますが、クライアントは使用するセッションを決定する識別子を保持します。
ここの他のすべてと同様に、*セッションはミドルウェア機能を使用して実装されます*:
(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. ライニンゲンプラグイン

  • Ringは、開発と生産の両方を支援するLeiningen build toolのプラグインを提供します。*

    _project.clj_ファイルに正しいプラグインの詳細を追加して、プラグインをセットアップします。
  :plugins [[lein-ring "0.12.5"]]
  :ring {:handler ring.core/handler}
  • lein-ringのバージョンがRing のバージョンに合っていることが重要です。 ここでは、リング1.7.1を使用しています。つまり、lein-ring 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 [email protected]{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。 開発モードで実行中*

開発目的のために、*手動でビルドして実行する必要なしに、Leingingenから直接ハンドラーを実行できます*。 これにより、実際のブラウザでアプリケーションをテストしやすくなります。
$ 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 [email protected]{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ライブラリーについて簡単に紹介しました。 次のプロジェクトで試してみませんか?
ここで取り上げたいくつかの概念の例は、https://github.com/eugenp/tutorials/tree/master/clojure/ring [GitHub]で見ることができます。