August 5, 2019
By: 马海强

clojure luminus开发之非常重要的handler和middleware

两个应用场景

  • 在生产中,经常需要通过log来复现bug和查看出问题的地方,那么在clojure里需要像在spring里使用AOP一样,来打印request和response
  • 用过spring方法参数解析器的同志都知道,通过token把当前用户信息封装起来,后面controller里使用是个不错的选择,尤其jwt的token,省去了查db。

整个web其实是由下面几个组件构成的

  • handler
  • request
  • response
  • middleware

要解决上述两个问题,自然是要搞清除上面这几个概念。详细解释请阅读wiki里的概念,不再搬运。

handler

我的理解,handler是真正处理request和返回response的,比如:

  ["/json"
   {:post {:summary    "JSON换map"
           :parameters {:body {:id int?
                               :name string?
                               :age int?}}
           :handler    (fn [{{:keys [body]} :parameters}]
                        (log/info body)
                        (ok body))}}]

定义的api、接受的参数、返回结果,这就是一个完整的接口了。

在luminus模板里handler就是以request作为入参,以response的map作为出参的一个匿名函数。

至于真正的业务逻辑,可以直接在匿名函数里实现,也可以在其他任何namespace里搞,这都非常自由, 全靠团队自定义规范和个人编码习惯,比如Dirk受java毒害较深,就非喜欢叫“service”的东西来处理实际业务,完全ok。

request

request再熟悉不过了,它包含着我们常用和不常用的很多信息, 其结构仍然是个map,包含的标准的key如下:

  • :server-port The port on which the request is being handled.

  • :server-name The resolved server name, or the server IP address.

  • :remote-addr The IP address of the client or the last proxy that sent the request.

  • :uri The request URI (the full path after the domain name).

  • :query-string The query string, if present.

  • :scheme The transport protocol, either :http or :https.

  • :request-method The HTTP request method, which is one of :get, :head, :options, :put, :post, or :delete.

  • :headers A Clojure map of lowercase header name strings to corresponding header value strings.

  • :body An InputStream for the request body, if present.

  • :content-type The MIME type of the request body, if known.

  • :content-length The number of bytes in the request body, if known.

  • :character-encoding The name of the character encoding used in the request body, if known.

第一篇web接口里的参数便是从request里获取的。

其他的content-length之类的比较少用。

response

response更简单,信息都存储在status、body、header三个关键词里,像cookie、stream这类也是response。

其中status便是常见的网络协议状态码,像常见的

 404 > 地址不存在
 400 > 参数类型不匹配
 401/403 > 权限问题
 500 > 服务器异常
 502 > 网络异常

等等。

在luminus模板里,因为是rest接口,所以response的body被中间件统一处理成json了。

middleware

自如其名,中间件往往是一个高阶函数,是专门处理handler的,它以handler为入参,处理后的返回仍然是个新的handler, 正如参数解析的log一样,一个项目中,一般来说也会有不止一个的middleware来实现不同的目的。

比如这个:

(defn content-type-response [response content-type]
  (assoc-in response [:headers "Content-Type"] content-type))

(defn wrap-content-type [handler content-type]
  (fn
    ([request]
      (-> (handler request) (content-type-response content-type)))
    ([request respond raise]
      (handler request #(respond (content-type-response % content-type)) raise))))

这个middleware就是给response的header里统添加了一个参数"Content-Type",其值便是调用该函数传进来的。

很明显,middleware更多的是在统一处理request和response的一些资源, 多个middleware可以用->这个语法糖按顺序依次作用在一个handler上。

有了以上知识。

解决第一个问题,写一个打印log的middleware

(require '[clojure.tools.logging :as log])

(defn log-wrap [handler]
  (fn [request]
    (if-not (:dev env)
      (let [request-id (java.util.UUID/randomUUID)]
        (log/info (str "\n================================ REQUEST START ================================"
                       "\n request-id:" request-id
                       "\n request-uri: " (:uri request)
                       "\n request-method: " (:request-method request)
                       "\n request-query: " (:query (:parameters request))
                       "\n request-body: " (:body (:parameters request))))
        (let [res (handler request)]
          (log/info (str "response: " (:body res)
                         "\n request-id:" request-id))
          (log/info (str "\n================================ response END ================================"))
          res))
      (handler request))))

解决第二个问题,将当前登录用户放在reqeust里并在handler里拿出来

(defn token-wrap [handler]
  (fn [request]
    (let [token (get-in request [:headers "token"])
          user (-> token
                   str->jwt
                   :claims)]
      (log/info (str "解析后的user:" (-> token
                                      str->jwt
                                      :claims)))
      (log/info (str "******* the current user is " (:iss user)))
      (handler (assoc request :current-user (:iss user))))))

将这两个middleware照葫芦画瓢的加到routes里,即实现目的。

Tags: clojure web