在 Clojure 众多的 Web 框架中,Ring 以其简单统一的 HTTP 抽象模型脱颖而出。Ring 充分体现了函数式编程的思想——经过一系列函数的组合造成了一个易于理解、扩展的 HTTP 处理链。javascript
本篇文章首先介绍 Ring 核心概念及其实现原理,而后介绍如何基于 Ring + Compojure 实现一 RESTful 服务。html
Ring 规范里面有以下5个核心概念:java
这5个组件的关系可用下图表示(By Ring 做者):git
+---------------+
| Middleware |
| +---------+ | +---------+ +--------+
| | |<-- request ----| | | | | | Handler | | | Adapter |<---->| Client | | | |--- response -->| | | | | +---------+ | +---------+ +--------+ +---------------+复制代码
(ns learn-ring.core
(:require [ring.adapter.jetty :refer [run-jetty]]))
(defn handler [req]
{:headers {}
:status 200
:body "Hello World"})
(defn middleware [handler]
"Audit a log per request"
(fn [req]
(println (:uri req))
(handler req)))
(def app
(-> handler
middleware))
(defn -main [& _]
(run-jetty app {:port 3000}))复制代码
运行上面的程序,就能够启动一 Web 应用,而后在浏览器访问就能够返回Hello World
,同时在控制台里面会打印出请求的 uri。github
run-jetty
是 Ring 提供的基于 jetty 的 adapter,方便开发测试。其主要功能是两个转换:web
HttpServletRequest
---> request map
response map
---> HttpServletResponse
;; ring.adapter.jetty
(defn- ^AbstractHandler proxy-handler [handler]
(proxy [AbstractHandler] []
(handle [_ ^Request base-request request response]
(let [request-map (servlet/build-request-map request)
response-map (handler request-map)]
(servlet/update-servlet-response response response-map)
(.setHandled base-request true)))))
;; ring.util.servlet
;; HttpServletRequest --> request map
(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)})
;; response map --> HttpServletResponse
(defn update-servlet-response
"Update the HttpServletResponse using a response map. Takes an optional
AsyncContext."
([response response-map]
(update-servlet-response response nil response-map))
([^HttpServletResponse response context response-map]
(let [{:keys [status headers body]} response-map]
(when (nil? response)
(throw (NullPointerException. "HttpServletResponse is nil")))
(when (nil? response-map)
(throw (NullPointerException. "Response map is nil")))
(when status
(.setStatus response status))
(set-headers response headers)
(let [output-stream (make-output-stream response context)]
(protocols/write-body-to-stream body response-map output-stream)))))复制代码
Ring 里面采用 Middleware 模式去扩展 handler 的功能,这实际上是函数式编程中经常使用的技巧,用高阶函数去组合函数,实现更复杂的功能。在 Clojure 里面,函数组合更常见的是用 comp
,好比编程
((comp #(* % 2) inc) 1)
;; 4复制代码
这对一些简单的函数很是合适,可是若是逻辑比较复杂,Middleware 模式就比较合适了。例如能够进行一些逻辑判断决定是否须要调用某函数:json
(defn middleware-comp [handler]
(fn [x]
(if (zero? 0)
(handler (inc x))
(handler x))))
((-> #(* 2 %)
middleware-comp) 1)
;; 4
((-> #(* 2 %)
middleware-comp) 0)
;; 2复制代码
虽然 Middleware 使用很是方便,可是有一点须要注意:多个 middleware 组合的顺序。后面在讲解 RESTful 示例时会演示不一样顺序的 middleware 对请求的影响。flask
Middleware 这一模式在函数式编程中很是常见,Clojure 生态里面新的构建工具 boot-clj 里面的 task 也是经过这种模式组合的。api
$ cat build.boot
(deftask inc-if-zero-else-dec
[n number NUM int "number to test"]
(fn [handler]
(fn [fileset]
(if (zero? number)
(handler (merge fileset {:number (inc number)}))
(handler (merge fileset {:number (dec number)}))))))
(deftask printer
[]
(fn [handler]
(fn [fileset]
(println (str "number is " (:number fileset)))
fileset)))
$ boot inc-if-zero-else-dec -n 0 printer
number is 1
$ boot inc-if-zero-else-dec -n 1 printer
number is 0复制代码
因为 Ring 只是提供了一个 Web 服务最基本的抽象功能,不少其余功能,像 url 路由规则,参数解析等均需经过其余模块实现。Compojure 是 Ring 生态里面默认的路由器,一样短小精悍,功能强大。基本用法以下:
(def handlers
(routes
(GET "/" [] "Hello World")
(GET "/about" [] "about page")
(route/not-found "Page not found!")))复制代码
使用这里的 handlers 代替上面 Hello World 的示例中的 handler 便可获得一个具备2条路由规则的 Web 应用,同时针对其余路由返回 Page not found!
。
Compojure 里面使用了大量宏来简化路由的定义,像上面例子中的GET
、not-found
等。Compojure 底层使用 clout 这个库实现,而 clout 自己是基于一个 parser generator(instaparse) 定义的“路由”领域特定语言。核心规则以下:
(def ^:private route-parser
(insta/parser
"route = (scheme / part) part*
scheme = #'(https?:)?//'
<part> = literal | escaped | wildcard | param
literal = #'(:[^\\p{L}_*{}\\\\]|[^:*{}\\\\])+'
escaped = #'\\\\.'
wildcard = '*'
param = key pattern?
key = <':'> #'([\\p{L}_][\\p{L}_0-9-]*)'
pattern = '{' (#'(?:[^{}\\\\]|\\\\.)+' | pattern)* '}'"
:no-slurp true))复制代码
Compojure 中路由匹配的方式也很是巧妙,这里详细介绍一下。
Compojure 经过 routes 把一系列 handler 封装起来,其内部调用 routing 方法找到正确的 handler。这两个方法代码很是简洁:
(defn routing
"Apply a list of routes to a Ring request map."
[request & handlers]
(some #(% request) handlers))
(defn routes
"Create a Ring handler by combining several handlers into one."
[& handlers]
#(apply routing % handlers))复制代码
routing 里面经过调用 some
函数返回第一个非 nil 调用,这样就解决了路由匹配的问题。由这个例子能够看出 Clojure 语言的表达力。
在使用 GET
等这类宏定义 handler 时,会调用wrap-route-matches
来包装真正的处理逻辑,逻辑以下:
(defn- wrap-route-matches [handler method path]
(fn [request]
(if (method-matches? request method)
(if-let [request (route-request request path)]
(-> (handler request)
(head-response request method))))))复制代码
这里看到只有在 url 与 http method 均匹配时,才会去调用 handler 处理 http 请求,其余状况直接返回 nil,这与前面讲的 some 联合起来就造成了完整的路由功能。
因为 routes
的返回值与 handler 同样,是一个接受 request map 返回 response map 的函数,因此能够像堆积木同样进行任意组合,实现相似于 Flask 中 blueprints 的模块化功能。例如:
;; cat student.clj
(ns demo.student
(:require [compojure.core :refer [GET POST defroutes context]])
(defroutes handlers
(context "/student" []
(GET "/" [] "student index")))
;;cat demo.teacher
(ns demo.teacher
(:require [compojure.core :refer [GET POST defroutes context]])
(defroutes handlers
(context "/teacher" []
(GET "/" [] "teacher index")))
;; cat demo.core.clj
(ns demo.core
(:require [demo.student :as stu]
[demo.teacher :as tea])
;; core 里面进行 handler 的组合
(defroutes handlers
(GET "/" [] "index")
(stu/handlers)
(tea/handlers))复制代码
Compojure 解决了路由问题,参数获取是经过定制不能的 middleware 实现的,compojure.handler
命名空间提供了经常使用的 middleware 的组合,针对 RESTful 可使用 api 这个组合函数,它会把 QueryString 中的参数解析到 request map 中的:query-params
key 中,表单中的参数解析到 request map 中的 :form-params
。
(def app
(-> handlers
handler/api))复制代码
因为 RESTful 服务中,请求的数据与返回的数据一般都是 JSON 格式,因此须要增长两个额外的功能来实现 JSON 的序列化。
;; 首先引用 ring.middleware.json
(def app
(-> handlers
wrap-json-response
wrap-json-body
handler/api))复制代码
一般,咱们须要纪录每一个请求的处理时间,这很简单,实现个 record-response-time
便可:
(defn record-response-time [handler]
(fn [req]
(let [start-date (System/currentTimeMillis)]
(handler req)
(let [res-time (- (System/currentTimeMillis) start-date)]
(println (format "%s took %d ms" (:uri req) res-time))))))
(def app
(-> handlers
wrap-json-response
wrap-json-body
handler/api
record-response-time))复制代码
须要注意的是 record-response-time
须要放在 middleware 最外层,这样它才能纪录一个请求通过全部 middleware + handler 处理的时间。
其次,另外一个很常见的需求就是封装异常,当服务端出现错误时返回给客户端友好的错误信息,而不是服务端的错误堆栈。
(defn wrap-exception
[handler]
(fn [request]
(try
(handler request)
(catch Throwable e
(response {:code 20001
:msg "inner error})))))
(def app
(-> handlers
wrap-json-response
wrap-json-body
handler/api
wrap-exception
record-response-time))复制代码
一个 App 中的 middleware 调用顺序很是重要,由于不一样的 middleware 之间 request map 与 response map 是相互依赖的,因此在定义 middleware 时必定要注意顺序。一图胜千言:
在 Java EE 中,编写 Web 项目一般是配置各类 XML 文件,代码还没开始写就配置了一大堆jar包依赖,这些 jar 包颇有可能会冲突,而后须要花大量时间处理这些依赖冲突,真心麻烦。
Ring 与其说是一个框架,不如说是由各个短小精悍的函数组成的 lib,充分展现了 Clojure 语言的威力,经过函数的组合定义出一套完整的 HTTP 抽象机制,经过宏来实现“路由”特定领域语言,极大简化了路由的定义,方便了模块的分解。
除了上面的介绍,Ring 生态里面还有 lein-ring ,它能够在不重启服务的状况下从新加载有修改的命名空间(以及其影响的),开发从未如何顺畅。
Ring + Compojure + lein-ring 你值得拥有。