[220122] How Clojure Ring works
Table of Contents
1 What is Ring?
Ring is a popular clojure library for a web application. On this page, We will look through essential namespaces in ring and build our own.
2 ring.adapter.jetty
Let's look into a wiki page. [link]
You will see the code below
(defn handler [request] {:status 200 :handlers {"Content-Type" "text/html"} :body "Hello World"}) (use 'ring.adapter.jetty) (run-jetty handler {:port 3000 :join? false})
The code is quite intuitive. you just pass a handler function into run-jetty
. (hashmap is for configuration)
Let's take a look at ring.adapter.jetty
namespace. [link]
I will simplify the run-jetty
function to clarify what it is doing.
(defn ^Server run-jetty [handler options] (let [server (create-server options)] (.setHandler server (proxy-handler handler)) (try (.start server) server (catch Exception ex (.stop server) (throw ex)))))
AS you can see, run-jetty
function:
- creates server by executing
create-server
function. - makes Servlet using
proxy-handler
function. - sets handler into the server that was just created using
setHandler
method. - it runs the server using
start
method and returns it.
2.1 create-server function
create-server
function creates a Server.
It makes a thread pool and adds it to the server we just created.
However, to simplify the source code, I will skip configuring a thead pool part.
(ns yhnam.jetty-wrapper (:import [org.eclipse.jetty.util.thread QueuedThreadPool])) (defn thread-pool [] (QueuedThreadPool.)) (defn http-connector [server options] (let [sc (ServerConnector. server)] (.setPort sc (options :port 3000)) sc)) (defn create-server [opts] (let [server (Server. (thread-pool))] (.addConnector server (http-connector server opts)) server)) (comment (create-server {:port 8080}) ;; )
2.2 proxy-handler
proxy-handler
is one of the most important functions in ring project.
It lets your plain handler function act as a Servlet
.
(ns yhnam.jetty-wrapper (:import [javax.servlet.http HttpServletRequest HttpServletResponse] [org.eclipse.jetty.server.handler AbstractHandler])) (defn ^AbstractHandler proxy-handler [handler] (proxy [AbstractHandler] [] (handle [this ^Request base-request ^HttpServletRequest req ^HttpServletResponse response] (let [req-map (build-request-map req) response-map (handler req-map)] (update-servlet-response response response-map) (.setHandled base-request true)))))
build-request
, response-map
is a killer function.
build-request
: It gets rid of annoying method executions.response-map
: It let your handler function immutable (otherwise you need to mutateHttpServletResponse
object)
Since it is placed in a different namespace. I will introduce it in the next chapter.
3 ring.util.servlet
Now we will delv into ring.util.servlet
namespace. [link]
3.1 build-request-map
As I said before build-request-map
function has done boring and annoying stuff for us.
It is THE legacy of object-oriented programming in java.
(defn build-request-map "Create the request map from the HttpServletRequest object." [^HttpServletRequest request] {:server-port (.getServerPort request) :server-name (.getServerName request) :remote-addr (.getRemoteAddr request) :uri (.getRequestURI request) :query-string (.getQueryString request) :scheme (keyword (.getScheme request)) :request-method (keyword (.toLowerCase (.getMethod request) Locale/ENGLISH)) :protocol (.getProtocol request) :headers (get-headers request) :content-type (.getContentType request) :content-length (get-content-length request) :character-encoding (.getCharacterEncoding request) :ssl-client-cert (get-client-cert request) :body (.getInputStream request)})
Having simple data as a request is much simpler, understandable and I believe that it is an essence of the ring project.
3.2 update-servlet-response
update-servet-response
makes your response data simple. you just return a simple response hashmap and update-servlet-response
will mutate HttpServletResponse
object from it.
(require '[ring.core.protocols :as protocols]) (defn set-headers [^HttpServletResponse response headers] (let [[key val-or-vals] headers] (if (string? val-or-vals) (.setHeader response key val-or-vals) (doseq [val val-or-vals] (.addHeader response key val)))) (when-let [content-type (get headers "Content-Type")] (set-headers response (:headers response-map)) (.setContentType response content-type))) (defn update-servlet-response [response response-map] (let [output-stream (make-output-stream response)] (set-headers response (:headers response-map)) (protocols/write-body-to-stream (:body response-map) response output-stream)))
There are two methods for setting Header (.setHeader
, .addHeader
) that is bad. [set-headers] function deals with it for you.
write-body-to-stream
writes a response body to an output stream. It is defined by defprotocol
that it will treat differently depending on the data structure of the response body.
4 ring.core.protocols
4.1 StreamableResponseBody
In ring handler function, you can return various data structures as a body.[link]
StreamableResponseBody
will take from it and write to output stream and close. Each data structure has its solution to this respectively. Polymorphism solves this problem.
In java, you can't extend class
dynamically.
In Clojure, extend-protocol
extends an existing java type dynamically.
(ns ring.core.protocols "Protocols necessary for Ring." (:import [java.io Writer OutputStream]) (:require [clojure.java.io :as io])) (defprotocol StreamableResponseBody (write-body-to-stream [body response output-stream])) (extend-protocol StreamableResponseBody (Class/forName "[B") (write-body-to-stream [body _ ^OutputStream output-stream] (.write output-stream ^Byte body) (.close output-stream)) String (write-bo-to-stream [body response output-stream] (doto (io/writer output-stream) (.write body) (.close))) ,,, ;; other types described. )
5 Middleware
Let's take a look at run-jetty
again. In ring, you can only pass a single function.
There is no space for middleware.
However, we have a function.
;; simple handler (defn hello-world [req] {:status 200 :headers {"Content-Type" "text/plain"} :body "Hello World"}) ;; middleware (defn log-middleware [handler] (fn [req] (println req) (handler req))) (comment (def app (run-jetty (-> hello-world log-middleware))) (.stop app) ;; )
and you notice that is just a function. there is no annotation(@Controller
, @Component
) nor implements XXXInterceptor
.
It is a beauty of ring, clojure and functional programming.
6 source code
You can easily get a simplified source code. https://github.com/ssisksl77/diy-ring-jetty I wish that you have fun with it and will enjoy clojure programming with ring.