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里,即实现目的。