[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:

  1. creates server by executing create-server function.
  2. makes Servlet using proxy-handler function.
  3. sets handler into the server that was just created using setHandler method.
  4. 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 mutate HttpServletResponse 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.

Date: 2022-01-22 Sat 00:00

Author: Younghwan Nam

Created: 2024-09-25 Wed 07:42

Emacs 27.2 (Org mode 9.4.4)

Validate