«1. Введение

Ring — это библиотека для написания веб-приложений на языке Clojure. Он поддерживает все необходимое для написания полнофункциональных веб-приложений и имеет процветающую экосистему, которая делает его еще более мощным.

В этом уроке мы познакомимся с Ring и покажем некоторые вещи, которых мы можем достичь с его помощью.

Ring — это не фреймворк, предназначенный для создания REST API, как многие современные наборы инструментов. Это низкоуровневая структура для обработки HTTP-запросов в целом с упором на традиционную веб-разработку. Однако некоторые библиотеки строятся на его основе для поддержки многих других желаемых структур приложений.

2. Зависимости

Прежде чем мы сможем начать работать с Ring, нам нужно добавить его в наш проект. Минимальные зависимости, которые нам нужны:

    ring/ring-core 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-запросов. Ring представляет запрос в виде карты, что позволяет нашему приложению Clojure легко взаимодействовать с отдельными полями. На этой карте есть стандартный набор ключей, включая, помимо прочего:

    :uri — полный путь URI. :query-string — полная строка запроса. :request-method — метод запроса, один из :get, :head, :post, :put, :delete или :options. :headers — карта всех заголовков HTTP, предоставленных запросу. :body — InputStream, представляющий тело запроса, если оно присутствует.

Промежуточное ПО может добавлять дополнительные ключи к этой карте по мере необходимости.

3.2. Ответы

Точно так же ответы представляют собой исходящие ответы HTTP. Ring также представляет их в виде карт с тремя стандартными ключами:

    :status — код состояния для отправки обратно :headers — карта всех заголовков HTTP для отправки обратно :body — необязательный текст для отправки назад

Как и прежде, ПО промежуточного слоя может изменить это между нашим обработчиком, производящим его, и конечным результатом, отправляемым клиенту.

Ring также предоставляет несколько помощников, облегчающих построение ответов.

Самой простой из них является функция 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 ""}

У нас также есть метод состояния, который преобразует существующий ответ в любой произвольный код состояния:

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

Мы затем есть несколько методов для аналогичной настройки других функций ответа — например, тип содержимого, заголовок или 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. Концептуально они похожи на фильтры сервлетов и перехватчики Spring.

«В 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 также предлагает более естественный способ объединения многих обработчиков — с помощью макросов многопоточности. Это способ предоставить список функций для вызова, каждая из которых имеет вывод предыдущей.

В частности, нам нужен макрос Thread First -\u003e. Это позволит нам вызывать каждое ПО промежуточного слоя с заданным значением в качестве первого параметра:

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

Затем создается обработчик, являющийся исходным обработчиком, заключенным в три разные функции ПО промежуточного слоя.

4. Написание обработчиков

Теперь, когда мы понимаем компоненты, из которых состоит приложение Ring, нам нужно знать, что мы можем делать с реальными обработчиками. Это сердце всего приложения, и именно здесь будет выполняться большая часть бизнес-логики.

Мы можем поместить в эти обработчики любой код, который пожелаем, включая доступ к базе данных или вызов других служб. Ring дает нам некоторые дополнительные возможности для работы напрямую с входящими запросами или исходящими ответами, которые также очень полезны.

4.1. Обслуживание статических ресурсов

Одной из простейших функций, которые может выполнять любое веб-приложение, является обслуживание статических ресурсов. Для упрощения этой задачи Ring предоставляет две промежуточные функции — wrap-file и wrap-resource.

Промежуточное программное обеспечение Wrap-file использует каталог в файловой системе. Если входящий запрос соответствует файлу в этом каталоге, то вместо вызова функции обработчика возвращается этот файл: для файлов:

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

(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"))

«

«Этот обработчик вернет ответ, содержащий значение входного параметра.

Параметры сопоставляются с одной строкой, если присутствует только одно значение, или со списком, если присутствует несколько значений.

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

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

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

Например, мы получаем следующие карты параметров:

4.3. Получение загружаемых файлов

Часто мы хотим иметь возможность писать веб-приложения, в которые пользователи могут загружать файлы. В протоколе HTTP это обычно обрабатывается с помощью составных запросов. Это позволяет одному запросу содержать как параметры формы, так и набор файлов.

Ring поставляется с промежуточным программным обеспечением, называемым wrap-multipart-params, для обработки такого рода запросов. Это похоже на то, как wrap-params анализирует простые запросы.

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

wrap-multipart-params автоматически декодирует и сохраняет любые загруженные файлы в файловую систему и сообщает обработчику, где они находятся для работы с ними:

По умолчанию загруженные файлы сохраняются во временном системный каталог и автоматически удаляется через час. Обратите внимание, что это требует, чтобы JVM все еще работала в течение следующего часа для выполнения очистки.

При желании, есть также хранилище в памяти, хотя, очевидно, это может привести к нехватке памяти, если будут загружены большие файлы.

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

Мы также можем написать свои механизмы хранения, если это необходимо, если они соответствуют требованиям API.

После настройки этого промежуточного программного обеспечения загруженные файлы доступны в объекте входящего запроса по ключу 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

Файлы cookie — это механизм, в котором сервер может предоставить небольшой объем данных, которые клиент будет продолжать отправлять обратно при последующих запросах. Обычно это используется для идентификаторов сеансов, токенов доступа или постоянных пользовательских данных, таких как настроенные параметры локализации.

Ring имеет промежуточное программное обеспечение, которое позволит нам легко работать с файлами cookie. Это позволит автоматически анализировать файлы cookie для входящих запросов, а также позволит нам создавать новые файлы cookie для исходящих ответов.

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

Настройка этого промежуточного программного обеспечения выполняется по тем же шаблонам, что и раньше:

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

На этом этапе все входящие запросы будут анализироваться и помещаться в ключ :cookies в запросе. Он будет содержать карту имени и значения файла cookie:

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

Затем мы можем добавить файлы cookie в исходящие ответы, добавив ключ :cookies в исходящий ответ. Мы можем сделать это, создав ответ напрямую:

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

Также есть вспомогательная функция, которую мы можем использовать для добавления файлов cookie к ответам, аналогично тому, как раньше мы могли устанавливать коды состояния или заголовки:

    ~ ~~ Файлы cookie также могут иметь дополнительные параметры, необходимые для спецификации HTTP. Если мы используем set-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})

:domain — домен, на который следует ограничить cookie; -only — значение true, чтобы сделать файл cookie недоступным для JavaScript. :max-age — количество секунд, по истечении которого браузер удаляет файл cookie. :expires — конкретная метка времени, после которой браузер удаляет файл cookie. :same-site – Если установлено значение :strict, браузер не будет отправлять этот файл cookie обратно с межсайтовыми запросами.

4.5. Сеансы

Файлы cookie дают нам возможность хранить биты информации, которую клиент отправляет обратно на сервер при каждом запросе. Более мощный способ добиться этого — использовать сеансы. Они полностью хранятся на сервере, но клиент поддерживает идентификатор, который определяет, какой сеанс использовать.

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

Как и все остальное здесь, сеансы реализуются с помощью промежуточной функции:

«

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

«По умолчанию это сохраняет данные сеанса в памяти. Мы можем изменить это, если необходимо, и Ring поставляется с альтернативным хранилищем, которое использует файлы cookie для хранения всех данных сеанса.

Как и в случае с загрузкой файлов, мы можем предоставить функцию хранения, если это необходимо.

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

Мы также можем настроить детали файла cookie, используемого для хранения сеансового ключа.

Например, чтобы сделать так, чтобы файл cookie сеанса сохранялся в течение одного часа, мы могли бы сделать:

Атрибуты файла 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))))

Вместо этого мы получаем доступ к данным сеанса из запроса и возвращаем карту данных для сохранения в ней как часть ответа. Это все состояние сеанса для сохранения, а не только измененные значения.

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

Например, в следующем примере ведется подсчет того, сколько раз был запрошен обработчик:

Работая таким образом, мы можем удалить данные из сеанса, просто не включая ключ. Мы также можем удалить весь сеанс, вернув nil для новой карты.

5. Плагин Leiningen

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

Ring предоставляет плагин для инструмента сборки Leiningen, который помогает как в разработке, так и в производстве.

$ 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

Мы настраиваем плагин, добавляя правильные данные плагина в файл project.clj:

Важно, чтобы версия lein-ring соответствовала версии Ring. Здесь мы использовали Ring 1.7.1, а это значит, что нам нужен lein-ring 0.12.5. В общем, безопаснее просто использовать последнюю версию обоих, как видно из Maven Central или с помощью команды lein search:

Параметр :handler для вызова :ring — это полное имя обработчика. которые мы хотим использовать. Это может включать любое промежуточное ПО, которое мы определили.

Использование этого плагина означает, что нам больше не нужна основная функция. Мы можем использовать Leiningen для запуска в режиме разработки, или же мы можем создать производственный артефакт для целей развертывания. Теперь наш код сводится именно к нашей логике и не более того.

$ 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

5.1. Создание производственного артефакта

$ 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

После того, как это настроено, теперь мы можем создать файл WAR, который мы можем развернуть в любом стандартном контейнере сервлетов:

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

Мы также можем создать отдельный файл JAR, который точно запустит наш обработчик. как и ожидалось:

Этот файл JAR будет включать основной класс, который запустит обработчик во встроенном контейнере, который мы включили. Это также будет учитывать переменную среды PORT, позволяющую нам легко запускать ее в производственной среде:

$ 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 Serv[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

5.2. Запуск в режиме разработки

В целях разработки мы можем запустить обработчик непосредственно из Leiningen без необходимости сборки и запуска вручную. Это упрощает тестирование нашего приложения в реальном браузере:

[ring/ring-devel "1.7.1"]

Это также учитывает переменную среды PORT, если мы ее установили.

Кроме того, есть библиотека Ring Development, которую мы можем добавить в наш проект. Если это доступно, сервер разработки попытается автоматически перезагрузить любые обнаруженные изменения исходного кода. Это может дать нам эффективный рабочий процесс изменения кода и просмотра его в реальном времени в нашем браузере. Для этого необходимо добавить зависимость ring-devel:

6. Заключение