美文网首页
clojure web(luminus)接口开发

clojure web(luminus)接口开发

作者: 小马将过河 | 来源:发表于2019-07-31 19:30 被阅读0次

    项目框架

    本项目使用luminus做模板,参考luminus-template,执行下面的命令init工程:

    lein new luminus alk-wxapi +mysql +service
    

    相关文档

    项目运行

    在命令行工具中启动用lein启动一个repl,lein没有安装的需要自行百度。

    ➜  ~ cd git/redcreation/alk-wxapi
    ➜  alk-wxapi git:(master) lein repl
    nREPL server started on port 50529 on host 127.0.0.1 - nrepl://127.0.0.1:50529
    REPL-y 0.4.3, nREPL 0.6.0
    Clojure 1.10.0
    Java HotSpot(TM) 64-Bit Server VM 1.8.0_192-b12
        Docs: (doc function-name-here)
              (find-doc "part-of-name-here")
      Source: (source function-name-here)
     Javadoc: (javadoc java-object-or-class-here)
        Exit: Control+D or (exit) or (quit)
     Results: Stored in vars *1, *2, *3, an exception in *e
    
    user=>
    

    然后在Intellij Idea中远程连接

    ideaconfig

    run这个配置,然后在下面的repl环境中执行(start)即启动server。

    常见问题及解决方案

    1、处理request

    实际项目开发中经常需要打印request内容,这部分在springMVC中一般用aop来解决。
    clojure中没有对象,更别提aop了,但是没有框架的束缚,处理起request和response反而更加灵活,是用clojure的middleware
    处理的,比如一个打印出入参的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))))
    

    将此swap配置在全局路由中即可,一般是有个统一配置format的middleware的,放在一起即可。

    2、在handler中使用request里自定义的对象

    有了上面说的middleware能处理request,那么往request里放个对象,自然不在话下,比如讲header里的token转换成user对象置于request中,在后面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))))))
    

    3、hendler获取body,path,query的参数

    在handle前后,可以用(keys request)查看request里自己传入的参数,那么在handler里怎么获取这些参数呢,在Luminus中定义了三种与springMVC类似的参数关键词,对应关系如下:

    mvc request luminus 含义
    @RequestParam query-params parameters -> query query参数,URL后面问号的参数,或form的参数
    @PathVariable path-params parameters -> path path参数,URL中/的参数
    @RequestBody body-params parameters ->body post/put方法里的body参数

    这三个keyword是ring自身的处理,是原始request里的参数,但是query-params参数被处理成map的key不是keywords,是普通的string,得用(query-params "id")这样来取值。因此推荐如下示例使用:
    推荐从request的parameters中获取,关键字分别是query,path, body。
    获取的例子:

      ;;非推荐方式
      ;;api返回结果: {"data": "path params: {:id \"1\"}\n query params: {\"name\" \"2\"}\n body params: {:message \"22\"}"}
      ["/path/bad/:id"
       {:post {:summary    "路径上传参--不推荐此方法获取--query参数key变成了str"
               :parameters {:path  {:id int?}
                            :query {:name string?}
                            :body  {:message string?}}
               :handler    (fn [{:keys [path-params query-params body-params]}]
                            {:status 200
                             :body   {:data (str "path params: " path-params
                                                 "\n query params: " query-params
                                                 "\n body params: " body-params)}})}}]
    
      ;;good handler api返回结果:
      ;{
      ;  "code": 1,
      ;  "message": "操作成功",
      ;  "data": "path params: {:id 1},  query params: {:name \"2\"},  body params: {:message \"22\"} "
      ;}
      ["/path/good/:id"
       {:post {:summary    "路径上传参--GOOD--获取到3种map"
               :parameters {:path  {:id int?}
                            :query {:name string?}
                            :body  {:message string?}}
               :handler    (fn [{{:keys [body query path]} :parameters}]
                            (ok (format "path params: %s,  query params: %s,  body params: %s " path query body)))}}]
    
    
      ;;good handler, 接口里三种参数都有,并且想直接获取map中key的vals
      ;; api返回结果:
      ;{
      ;"code": 1,
      ;"message": "操作成功",
      ;"data": "path params 'id': 1, query params 'name': 2 , body params: {:message \"22\"} "
      ;}
      ["/path/good-all-params/:id"
       {:post {:summary    "路径上传参--GOOD--直接得到key的val"
               :parameters {:path  {:id int?}
                            :query {:name string?}
                            :body  {:message string?}}
               :handler    (fn [{{:keys [body]}          :parameters
                                 {{:keys [id]} :path}    :parameters
                                 {{:keys [name]} :query} :parameters}]
                            (ok (format "path params 'id': %s, query params 'name': %s , body params: %s " id name body)))}}]
    

    原因分析:我们在handler.clj的ring/router后面使用[reitit.ring.middleware.dev :as dev]{:reitit.middleware/transform dev/print-request-diffs}方法打印出中间件的处理逻辑,

    handler

    结果如下:

    --- Middleware ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1"}
    
    --- Middleware :reitit.ring.middleware.parameters/parameters ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1"}
    
    --- Middleware :reitit.ring.middleware.muuntaja/format-negotiate ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       +:muuntaja/request #muuntaja.core.FormatAndCharset
       {:charset "utf-8",
        :format "application/json",
        :raw-format "application/json"},
       +:muuntaja/response #muuntaja.core.FormatAndCharset
       {:charset "utf-8",
        :format "application/json",
        :raw-format "application/json"}}
    
    --- Middleware :reitit.ring.middleware.muuntaja/format-response ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       :muuntaja/request {:charset "utf-8",
                          :format "application/json",
                          :raw-format "application/json"},
       :muuntaja/response {:charset "utf-8",
                           :format "application/json",
                           :raw-format "application/json"}}
    
    --- Middleware :reitit.ring.middleware.exception/exception ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       :muuntaja/request {:charset "utf-8",
                          :format "application/json",
                          :raw-format "application/json"},
       :muuntaja/response {:charset "utf-8",
                           :format "application/json",
                           :raw-format "application/json"}}
    
    --- Middleware :reitit.ring.middleware.muuntaja/format-request ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       :muuntaja/request {:charset "utf-8",
                          :format "application/json",
                          :raw-format "application/json"},
       :muuntaja/response {:charset "utf-8",
                           :format "application/json",
                           :raw-format "application/json"},
       +:body-params {:message "22"}}
    
    --- Middleware :reitit.ring.coercion/coerce-request ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :body-params {:message "22"},
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       :muuntaja/request {:charset "utf-8",
                          :format "application/json",
                          :raw-format "application/json"},
       :muuntaja/response {:charset "utf-8",
                           :format "application/json",
                           :raw-format "application/json"},
       +:parameters {:body {:message "22"},
                     :path {:id 1},
                     :query {:name "2"}}}
    
    2019-06-22 11:09:16,537 [XNIO-2 task-2] INFO  alk-wxapi.middleware.log-interceptor - 
    ================================ REQUEST START ================================
     request-id:8ddb3169-e72f-4b90-8811-d500c50d3057
     request-uri: /api/guestbooks/path/good-all-params/1
     request-method: :post
     request-query: {:name "2"}
     request-body: {:message "22"} 
    --- Middleware ---
    
      {:body #<io.undertow.io.UndertowInputStream@39931c66>,
       :body-params {:message "22"},
       :character-encoding "ISO-8859-1",
       :content-length 21,
       :content-type "application/json",
       :context "",
       :cookies {"JSESSIONID" {:value "GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU"},
                 "_ga" {:value "GA1.1.521496834.1555489511"},
                 "_gid" {:value "GA1.1.947080805.1561170619"}},
       :flash nil,
       :form-params {},
       :handler-type :undertow,
       :headers {"accept" "application/json",
                 "accept-encoding" "gzip, deflate, br",
                 "accept-language" "zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7",
                 "connection" "keep-alive",
                 "content-length" "21",
                 "content-type" "application/json",
                 "cookie" "_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619",
                 "host" "localhost:3000",
                 "origin" "http://localhost:3000",
                 "referer" "http://localhost:3000/api/api-docs/index.html",
                 "user-agent" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36"},
       :multipart-params {},
       :parameters {:body {:message "22"},
                    :path {:id 1},
                    :query {:name "2"}},
       :params {:name "2"},
       :path-info "/api/guestbooks/path/good-all-params/1",
       :path-params {:id "1"},
       :query-params {"name" "2"},
       :query-string "name=2",
       :remote-addr "0:0:0:0:0:0:0:1",
       :request-method :post,
       :scheme :http,
       :server-exchange #<io.undertow.server.HttpServerExchange@78f2e776 HttpServerExchange{ POST /api/guestbooks/path/good-all-params/1 request {Accept=[application/json], Accept-Language=[zh-CN,zh;q=0.9,zh-TW;q=0.8,en;q=0.7], Accept-Encoding=[gzip, deflate, br], Origin=[http://localhost:3000], User-Agent=[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36], Connection=[keep-alive], Content-Length=[21], Content-Type=[application/json], Cookie=[_ga=GA1.1.521496834.1555489511; JSESSIONID=GpSiQENkmzM7qQwqWdYxjehYKvNKoEGMG6MIwqwU; _gid=GA1.1.947080805.1561170619], Referer=[http://localhost:3000/api/api-docs/index.html], Host=[localhost:3000]} response {Server=[undertow]}}>,
       :server-name "localhost",
       :server-port 3000,
       :session nil,
       :ssl-client-cert nil,
       :uri "/api/guestbooks/path/good-all-params/1",
       :muuntaja/request {:charset "utf-8",
                          :format "application/json",
                          :raw-format "application/json"},
       :muuntaja/response {:charset "utf-8",
                           :format "application/json",
                           :raw-format "application/json"}}
    

    可以看到在reitit.ring.coercion/coerce-request中间件处理后request里增加了
    :parameters { :body {:message "22"}, :path {:id 1}, :query {:name "2"}}
    3种类型一致的map,这就是我们为什么推荐使用的原因。

    handler里获取request自定义的对象:

    那么,在上一步handle中加入到request中了一个current-user怎么获取和使用呢?其实,body-params,query-params这些也只是从request中获取到的而已,既然能从request中获取这些,那么request里的其他所有自然也能在handler中获取,看下面的例子:

    ["/reset/pwd"
        {:post {:summary    "修改密码"
                :parameters {:body (s/keys :req-un [::old-pwd ::new-pwd])}
                :handler    (fn [{{{:keys [old-pwd new-pwd]} :body} :parameters :as request}]
                              (let [current-id (-> request :current-user :user-id)
                                    db-user (db/get-user-id
                                             {:user-id current-id})]
                                (if (check-old-pwd old-pwd (:password db-user))
                                  (do (conman.core/with-transaction
                                        [*db*]
                                        (db/update-pwd! {:password  (d/sha-256 new-pwd)
                                                             :user-id current-id}))
                                      {:status 200
                                       :body   {:code    1
                                                :message "修改成功,请用新密码登录"}})
                                  {:status 400
                                   :body   {:code    0
                                            :message "密码错误,请输入正确的密码!"}})))}}]
    

    :as request的意思是包含前面指定获取的参数的所有。

    4、分页,动态hugsql

    在springboot里习惯使用spring data jpa,分页使用Pageable、PageRequest,还能携带Sort,放回结果自动分页,确实方便。在luminusweb里没有看到分页的说明,于是在底层的HugSQL里找到的方案,举个动态sql,并且使用like模糊查询的例子:

    -- :name get-patient-like :? :*
    -- :doc 模糊查询患者列表
    SELECT
    /*~ (if (:count params) */
      count(*) AS 'total-elements'
    /*~*/
        p.`patient_id`,
        p.`name`,
        p.`headimgurl`,
        p.`patient_no`
    /*~ ) ~*/
    FROM
        `t_patient` p
    WHERE
        p.deleted = FALSE
        AND p.`hospital_id` = :hospital-id
    /*~ (if (= nil (:keywords params)) */
      AND 1=1
    /*~*/
      AND (
            p.`name` LIKE :keywords
            OR p.mobile LIKE :keywords
          OR p.patient_no LIKE :keywords
        )
    /*~ ) ~*/
    ORDER BY p.`create_time` DESC
    --~ (if (:count params) ";" "LIMIT :page, :size ;")
    
    

    调用:

    ["/patient/search"
        {:get {:summary    "医生模糊检索患者列表"
               :parameters {:query (s/keys :req-un [:base/page :base/size]
                                           :opt-un [::keywords])}
               :handler    (fn [{{{:keys [page size keywords]} :query} :parameters :as request}]
                             (let [hospital-id (-> request :doctor :hospital-id)]
                               {:status 
                                200
                                :body   
                                {:code 1
                                 :data {:total-elements 
                                        (->> (db-pat/get-patient-like
                                               {:count       true
                                                :keywords    (str "%" keywords "%")
                                                :hospital-id hospital-id})
                                             (map :total-elements)
                                             (first))
                                        :content
                                        (db-pat/get-patient-like
                                          {:page        (* page size)
                                           :size        size
                                           :hospital-id hospital-id
                                           :keywords    (str "%" keywords "%")})}}}))}}]
    

    说明:接口的page,size为必须参数,keywords是非必须参数,sql中根据count的boolean值判断是不是求count,根据keywords是否有值判断是否加模糊查询条件,实现动态sql调用。
    更多hugSQL的高阶使用,使用时参考官网
    边用边学吧。

    • 一个in查询的例子,下例中的type用逗号隔开传入:
    :get    {:summary    "分页获取患者检查报告列表"
                  :parameters {:query (s/keys :req-un [:base/patient-id ::type])}
                  :handler    (fn [{{{:keys [type, patient-id]} :query} :parameters}]
                                {:status 200
                                 :body   {:code 1
                                          :data (db/get-examine-reports
                                                  {:patient-id patient-id
                                                   :types      (clojure.string/split type #",")})}})}
    

    sql:

    -- :name get-reports :? :*
    -- :doc 查询列表
    SELECT
    *
    FROM `t_report`
    WHERE `deleted` = FALSE AND `id` =:id AND `type` in (:v*:types)
    

    调用处保证types是个array就行:

    :get    {:summary    "获取报告列表"
                  :parameters {:query (s/keys :req-un [:base/id ::type])}
                  :handler    (fn [{{{:keys [type, id]} :query} :parameters}]
                                {:status 200
                                 :body   {:code 1
                                          :data (db/get-reports
                                                  {:id id
                                                   :types      (str/split type #",")})}})}
    
    • 批量操作,hugsql支持批量操作,语法是:t*,看看sql
    -- :name batch-create-cure-reaction-detail! :! :n
    -- :doc: 新建
    INSERT INTO `t_cure_reaction_detail` (`main_id`, `type`, `dict_key_id`, `dict_key_name`, `dict_value_id`, `dict_value_name`) VALUES
        :t*:reaction-detail
    

    一个UT:

    (ns alk-wxapi.routes.service.cure-reaction-service-test
      (:require [clojure.test :as t]
                [alk-wxapi.routes.service.cure-reaction-service :as sut]
                [alk-wxapi.db.db-patient-cure :as db]
                [alk-wxapi.db.core :refer [*db*]]
                [luminus-migrations.core :as migrations]
                [clojure.java.jdbc :as jdbc]
                [alk-wxapi.config :refer [env]]
                [mount.core :as mount]))
    
    (t/use-fixtures
      :once
      (fn [f]
        (mount/start
          #'alk-wxapi.config/env
          #'alk-wxapi.db.core/*db*)
        (migrations/migrate ["migrate"] (select-keys env [:database-url]))
        (f)))
    
    (def test-batch-create-cure-reaction-detail-data
      '[[1
         "REACTION"
         62
         "哮喘症状"
         68
         "气闭"]
    
        [1
         "REACTION"
         58
         "全身非特异性反应"
         59
         "发热"]
    
        [1
         "DISPOSE"
         86
         "处理方式"
         89
         "局部处理(冰敷)"]])
    
    (t/deftest test-batch-create-cure-reaction-detail
      (jdbc/with-db-transaction
        [t-conn *db*]
        (jdbc/db-set-rollback-only! t-conn)
    
        (t/is (= 2 (db/batch-create-cure-reaction-detail-data!
                     {:reaction-detail test-batch-create-cure-reaction-detail-data})))))
    

    执行结果:

    (alk-wxapi.db.db-patient-cure/batch-create-cure-reaction-detail!
      {:reaction-detail [[1
                          "REACTION"
                          62
                          "哮喘症状"
                          68
                          "气闭"]
    
                         [1
                          "REACTION"
                          58
                          "全身非特异性反应"
                          59
                          "发热"]
    
                         [1
                          "DISPOSE"
                          86
                          "处理方式"
                          89
                          "局部处理(冰敷)"]]})
    => 3
    2019-06-15 15:16:06,929 [nRepl-session-353b6f60-9fd8-415c-9baa-19f7eb4a97f9] INFO  jdbc.sqlonly - batching 1 statements: 1: INSERT INTO `t_cure_reaction_detail` (`main_id`, `type`, `dict_key_id`, 
    `dict_key_name`, `dict_value_id`, `dict_value_name`) VALUES (1,'REACTION',62,'哮喘症状',68,'气闭'),(1,'REACTION',58,'全身非特异性反应',59,'发热'),(1,'DISPOSE',86,'处理方式',89,'局部处理(冰敷)') 
     
    

    需要注意的是传入的vector,里面也是vector,按照sql中的顺序,不是map结构。下面是一个构造的例子

    (conman.core/with-transaction
     [*db*]
     (let [tmz-id (utils/generate-db-id)]
       (db/create-tmz! (assoc body
                              :id tmz-id))
       (when (pos? (count (:detail body)))
         (let [funcs [(constantly tmz-id)
                      :drug-id
                      :injection-num
                      :classify
                      :attribute]
               records (map (apply juxt funcs) (:detail body))]
           (if (pos? (count records))
             (db/batch-create-tmz-detail! {:records records}))))))
    

    对应的传参方式:

    {
      "date": "2019-08-09",
      "patient-id": "222",
      "detail": [
        {
          "drug-id": 1,
          "classify": 167,
          "attribute": "常规法",
          "injection-num": 3
        },
     {
          "drug-id": 2,
          "classify": 168,
          "attribute": "改良法",
          "injection-num": 1
        }
    
      ]
    }
    
    • 动态sql之=和like查询
    --~ (if (:office-id params) "AND p.office_id = :office-id " "AND 1=1 ")
    
    --~ (if (and (:name params) (not= (:name params) "")) (str "AND p.name  LIKE " "'%" (:name params) "%'") "AND 1=1 ")
    /*~
    (let [status (:status params)]
         (cond
            (= status "10") "AND c.date is null AND DATE_FORMAT(c.predict_next_date,'%Y-%m-%d') <= :current-date"
            (= status "20") "AND DATE_FORMAT(c.date,'%Y-%m-%d') = :current-date"
            (= status "40") "AND c.date IS NOT NULL AND DATE_FORMAT(c.date,'%Y-%m-%d') < :current-date"
            (= status "30") "AND c.date IS NULL AND DATE_FORMAT(c.predict_next_date,'%Y-%m-%d') > :current-date"
            :else ""))
    ~*/
    

    HugSql其实支持动态sql的,动态表名也可以,更多使用用到的时候查阅官网

    5、mysql中的字段表名和字段下划线在clojure中用中线连接的统一适配

    druids提供了几个adapter,用来处理转换关系,比如有驼峰,中线等,我们使用连接符转换,即创建connection时加入kebab-case:

    (defstate ^:dynamic *db*
              :start (do (Class/forName "net.sf.log4jdbc.DriverSpy")
                         (if-let [jdbc-url (env :database-url)]
                           (conman/connect! {:jdbc-url jdbc-url})
                           (do
                             (log/warn "database connection URL was not found, please set :database-url in your config, e.g: dev-config.edn")
                             *db*)))
              :stop (conman/disconnect! *db*))
    (conman/bind-connection *db* {:adapter (kebab-adapter)} "sql/queries.sql")
    

    这个adapter在init项目时已经引入了,就看使用不使用。

    6、获取环境变量的值

    环境变量比较好获取,比如微信的配置和获取

    {:weixin           {:app-id "wx9258d165932dad73"
                        :secret "my-secret"}
    

    在dev/test/prod中配置结构相同,

    (require '[alk-wxapi.config :refer [env]])
    (defn get-weixin-access-token [code]
      (let [url (format "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
                        (-> env
                            :weixin
                            :app-id)
                        (-> env
                            :weixin
                            :secret)
                        code)]
        (log/info "请求微信access-token, url: %s" url) url))
    

    如果配置是一层,使用也只需写一层key。
    特别说明
    在将redis的connetion从clj修改成从环境变量中获取时,也是一样的配置和获取,但是碰到了问题,在request里查看env中的redis的各项都有值,但是调用redis的地方却提示无法创建connection,

    (ns alk-wxapi.db.redis
      (:require [taoensso.carmine :as car :refer (wcar)]
                [alk-wxapi.config :refer [env]]
                [mount.core :refer [defstate]]))
    
    (def server1-conn
              :start
              {:pool {}
               :spec {:host       (-> env :redis-host)
                      :port       (-> env :redis-port)
                      :password   (-> env :redis-password)
                      :timeout-ms (-> env :redis-timeout-ms)
                      :db         (-> env :redis-db)}})
    
    (defmacro wcar* [& body] `(car/wcar server1-conn ~@body))
    

    最后得知是因为env被定义了个state,

    (ns alk-wxapi.config
      (:require [cprop.core :refer [load-config]]
                [cprop.source :as source]
                [mount.core :refer [args defstate]]))
    
    (defstate env
      :start
      (load-config
        :merge
        [(args)
         (source/from-system-props)
         (source/from-env)]))
    

    但是按照说明文档redis的conn是个常规的def定义的函数,但是它下面的使用是个宏defmacro,宏是在编译的执行的,因此在初始化时evn环没有ready,所以无法创建出connection。需要将server1-conn改成一个state,state有依赖状态,会等到env完成后才产生。

    (defstate server1-conn 
     ...
    )
    

    7、jar引入及依赖冲突解决:

    • lein deps :tree 查看包依赖。
    • 引入新的jar时在project.clj:dependencies按说明引入,跟maven一样,分groupId、artifactId、version。
    • 排除某sdk里的某些冲突包
    [com.baidu.aip/java-sdk "4.11.0"
     :exclusions [org.slf4j/slf4j-simple]]
    

    8、spec使用

    spec的使用需要引入[clojure.spec.alpha :as s][spec-tools.core :as st],看个spec的定义:

    (s/def ::page
      (st/spec
        {:spec            int?
         :description     "页码,从0开始"
         :swagger/default "0"
         :reason          "页码参数不能为空"}))
    
    (s/def ::size
      (st/spec
        {:spec            int?
         :description     "每页条数"
         :swagger/default 10
         :reason          "条数参数不能为空"}))
    

    使用:

    ["/page"
        {:get {:summary    "分页获取字典数据"
               :parameters {:query (s/keys :req-un [::page ::size])
                            :handler (s/keys :req-un [page]
                                             :opt-un [size])}
               :handler    (fn [{{{:keys [page, size]} :query} :parameters :as request}]
                             {:status 200
                              :body   {:code 10
                                       :data {:total-elements (->> (db/get-dicts-page {:count true})
                                                                   (map :total-elements)
                                                                   (first))
                                              :content        (db/get-dicts-page {:page page
                                                                                  :size size})}}})}}]
    
    • spec的参数也可以定义在其他namespace里,使用时加上namespace的名字即可,比如一个叫base的namespace里定义参数如下:
    (s/def :base/role
      (st/spec
        {:spec        #{"PATIENT", "DOCTOR"}
         :description "角色"
         :reason      "角色不能为空"}))
    

    这个枚举类型的spec在另一个namespace里使用时不需要在require里引入这个base,而直接在spec里加namespace的名字,是这样的:

    :parameters {:header (s/keys :req-un [:base/role])}
    
    • 用coll-of定义出一个list
    (s/def ::head-id id?)
    (s/def ::url string?)
    (s/def ::unmatched-head
      (s/keys :req [::head-id ::url]))
    
    (s/def ::unmatched-head-result
      (st/spec
       {:spec (s/coll-of ::unmatched-head)}))
    

    再比定义一个下面的post的body体:

    {
      "patient-id": "string",
      "patient-ext-list": [
        {
          "dict-id": 0,
          "dict-type": "string",
          "dict-value": "string",
          "other-value": "string"
        }
      ]
    }
    

    spec定义

    (s/def ::dict-id int?)
    (s/def ::dict-value string?)
    (s/def ::dict-type string?)
    (s/def ::other-value string?)
    (s/def ::patient-ext-list (s/coll-of (s/keys :req-un [::dict-id ::dict-type ::dict-value ::other-value])))
    (s/def ::ext-body (s/keys :req-un [:base/patient-id ::patient-ext-list]))
    

    coll-of函数还接收可选的参数,用来对数组中的元素进行限制,可选参数有如下:

    (1):kind- - - -可以指定数组的类型,vector,set,list等;

    (2):count- - - -可以限定数组中元素的个数;

    (3):min-count- - - -限定数组中元素个数的最小值

    (4):max-count- - - -限定数组中元素个数的最大值

    (5):distinct- - - -数组没有重复的元素

    (6):into- - - -可以将数组的元素插入到[],(),{},#{}这些其中之一,主要是为了改变conform函数的返回结果

    • 定义一个指定长度的
    (s/def ::id
      (st/spec
       {:spec (s/and string? #(= (count %) 6))
        :description "一个长度为6字符串"
        :swagger/default "666666"
        :reason "必须是长度为6的字符串"}))
    
    • 使用函数验证参数合法性
    (s/def ::head-body-id
      (st/spec
       {:spec (s/and string? (fn [s]
                               (let [[head-id body-id] (clojure.string/split s #"-")]
                                 (and (s/valid? ::head-id head-id)
                                      (s/valid? ::body-id body-id)))))
        :description "一个长度为13字符串, head-id 和 body-id 用‘-’ 连起来"
        :swagger/default "666666-999999"
        :reason "必须是长度为13的字符串,用-把body-id和head-id连起来"}))
    
    • 定义数组
    (s/def ::dict-id [string?])    ;Good
    (s/def ::dict-id vector?)      ;Bad
    

    更多使用参考:
    clojure.spec库入门学习

    但是我们使用Luminus
    模板默认的参数校验库并不是spec,而是Struct,使用的时候通常引入struct.core就可以,先看个示例的定义:

    (ns myapp.home
      (:require
        ...
        [struct.core :as st]))
    
    (def album-schema
      [[:band st/required st/string]
       [:album st/required st/string]
       [:year st/required st/number]])
    

    也可以定义更加复杂的属性

    (def integer
      {:message "must be a integer"
       :optional true
       :validate integer?}))
    

    至于我们为什么推荐用spec,我觉得只是个约定而已。

    9、新增接口加入route

    创建一个新的namespace,参考官网说明定义出一个routes函数,然后将其加入到handle.clj中即可,像下面这样一直conj即可:

    添加route

    10、文件上传接口

    接口定义

    (defn format-date-time [timestamp]
      (-> "yyyyMMddHHmmss"
          (java.text.SimpleDateFormat.)
          (.format timestamp)))
    
    ;;上传到本地
    (defn upload-file-local [type file]
      (let [file-path (str (-> env :file-path) type
                           "/" (format-date-time (java.util.Date.))
                           "/" (:filename file))]
        (io/make-parents file-path)
        (with-open [writer (io/output-stream file-path)]
          (io/copy (:tempfile file) writer))
        (get-image-data file-path)
        file-path))
    
    (defn common-routes []
      ["/common"
       {:swagger    {:tags ["文件接口"]}
        :parameters {:header (s/keys :req-un [::token ::role])}
        :middleware [token-wrap]}
    
       ["/files"
        {:post {:summary    "附件上传接口"
                :parameters {:multipart {:file multipart/temp-file-part
                                         :type (st/spec
                                                 {:spec        string?
                                                  :description "类型"
                                                  :reason      "类型必填"})}}
    
                :responses  {200 {:body {:code int?, :data {:file-url string?}}}}
                :handler    (fn [{{{:keys [type file]} :multipart} :parameters}]
                              {:status 200
                               :body   {:code    1
                                        :message "上传成功"
                                        :data    {:file-url (:url (upload-file-local type file))}}})}}]])
    
    

    如果要将图片上传至七牛等有CDN能力的云存储空间,可以使用别人的轮子,或者自己需要造轮子,我这里使用了一个别人造的上传七牛的轮子,先在:dependencies里加入依赖

    [clj.qiniu "0.2.1"]
    

    调用api

    (require '[clj.qiniu :as qiniu])
    ;;上传到七牛配置
    (defn set-qiniu-config []
      (qiniu/set-config! :access-key "my-key"
                         :secret-key "my-secret"))
    
    (def qiniu-config
      {:bucket "medical"
       :domain "http://prfmkg8tt.bkt.clouddn.com/"
       :prefix "alk/weixin/"})
    
    (defn qiniu-upload-path [type filename]
      (str (-> qiniu-config :prefix)
           type "/"
           (utils/format-date-time (java.util.Date.))
           "/"
           filename))
    
    ;;七牛云上传,返回上传后地址
    (defn upload-file-qiniu [type file]
      (set-qiniu-config)
      (let [filename (:filename file)
            bucket (-> qiniu-config :bucket)
            key (qiniu-upload-path type filename)
            res (qiniu/upload-bucket bucket
                                     key
                                     (:tempfile file))]
        (log/info "上传七牛云结果:" res)
        (if-not (= 200 (-> res :status))
          (throw (Exception. " 附件上传失败 ")))
        (str (-> qiniu-config :domain) key)))
    
    

    使用的时候将上传local改成upload-file-qiniu即可。

    11、全局跨域配置

    在middleware的wrap-base中加入跨域信息,先配置个*的

    (ns alk-wxapi.middleware
      (:require
       [alk-wxapi.env :refer [defaults]]
       [alk-wxapi.config :refer [env]]
       [ring.middleware.flash :refer [wrap-flash]]
       [immutant.web.middleware :refer [wrap-session]]
       [ring.middleware.cors :refer [wrap-cors]]
       [ring.middleware.defaults :refer [site-defaults wrap-defaults]]))
    
    (defn wrap-base [handler]
      (-> ((:middleware defaults) handler)
          wrap-flash
          (wrap-session {:cookie-attrs {:http-only true}})
          (wrap-cors :access-control-allow-origin [#".*"]
                     :access-control-allow-methods [:get :put :post :delete])
          (wrap-defaults
           (-> site-defaults
               (assoc-in [:security :anti-forgery] false)
               (dissoc :session)))))
    

    12、增加打包环境

    比如增加pre环境,在project.clj中配置uberjar即可,在:profiles里增加,可以参考test环境,比如增加的uberjar-test环境:

       :uberjar-test  {:omit-source    true
                       :aot            :all
                       :uberjar-name   "alk-wxapi-test.jar"
                       :source-paths   ["env/test/clj"]
                       :resource-paths ["env/test/resources"]
                       :jvm-opts       ["-Dconf=test-config.edn"]}
    

    打包:

    ➜  alk-wxapi git:(master) ✗ lein with-profiles uberjar-test uberjar
    Compiling alk-wxapi.common.utils
    Compiling alk-wxapi.config
    Compiling alk-wxapi.core
    Compiling alk-wxapi.db.core
    Compiling alk-wxapi.db.db-dicts
    Compiling alk-wxapi.db.db-doctor
    Compiling alk-wxapi.db.db-guestbook
    Compiling alk-wxapi.db.db-hospital
    Compiling alk-wxapi.db.db-patient
    Compiling alk-wxapi.db.redis
    Compiling alk-wxapi.env
    Compiling alk-wxapi.handler
    Compiling alk-wxapi.middleware
    Compiling alk-wxapi.middleware.exception
    Compiling alk-wxapi.middleware.formats
    Compiling alk-wxapi.middleware.interceptor
    Compiling alk-wxapi.middleware.log-interceptor
    Compiling alk-wxapi.middleware.token-interceptor
    Compiling alk-wxapi.nrepl
    Compiling alk-wxapi.routes.base
    Compiling alk-wxapi.routes.dicts
    Compiling alk-wxapi.routes.doctor
    Compiling alk-wxapi.routes.file
    Compiling alk-wxapi.routes.guestbook
    Compiling alk-wxapi.routes.hospital
    Compiling alk-wxapi.routes.patient
    Compiling alk-wxapi.routes.patient-cost
    Compiling alk-wxapi.routes.patient-examine
    Compiling alk-wxapi.routes.public
    Compiling alk-wxapi.routes.user
    Compiling alk-wxapi.routes.weixin
    Compiling alk-wxapi.validation
    Warning: skipped duplicate file: config.edn
    Warning: skipped duplicate file: logback.xml
    Created /Users/mahaiqiang/git/redcreation/alk-wxapi/target/uberjar+uberjar-test/alk-wxapi-0.1.0-SNAPSHOT.jar
    Created /Users/mahaiqiang/git/redcreation/alk-wxapi/target/uberjar/alk-wxapi-test.jar
    ➜  alk-wxapi git:(master) ✗
    

    13、事务

    发起事务使用conman.core/with-transaction,一个例子:

    (let [timestamp (java.util.Date.)
          id (utils/generate-db-id)]
      (conman.core/with-transaction 
        [*db*]
        (db/create-guestbook! (assoc body-params
                                :timestamp timestamp
                                :id id))
        (db/get-guestbook {:id id})
        (throw (ex-info (str "异常,事务回滚,列表中查看该id的数据是否存在,id:" id) {}))))
    

    注意:只有在transaction中的exception发生,事务的机制才会生效,我测试时就正好稀里糊涂把throw放到了with-transaction里面,导致总是不会回滚。

    14、工具类

    工具类Utils单独一个namespace,目前收纳

    • 获取uuid
    (defn generate-db-id []
      (clojure.string/replace (str (java.util.UUID/randomUUID)) "-" ""))
    
    • 日期时间格式化
    (defn format-time [timestamp]
      (-> "yyyy-MM-dd HH:mm:ss"
          (java.text.SimpleDateFormat.)
          (.format timestamp)))
    
    (defn format-date-time [timestamp]
      (-> "yyyyMMddHHmmss"
          (java.text.SimpleDateFormat.)
          (.format timestamp)))
    

    15、定时任务

    有个比较重量级的http://clojurequartz.info/articles/guides.html库,quartz与在java里的一样,只不过是clojure的实现。
    我们项目里没有很复杂的需要动态修改的定时任务,因此选择了一个轻量级的库:chime,api参考github。下面是项目中的一个demo

    (ns alk-wxapi.common.scheduler
      (:require [chime :refer [chime-ch]]
                [clj-time.core :as t]
                [clj-time.periodic :refer [periodic-seq]]
                [clojure.core.async :as a :refer [<! go-loop]]
                [clojure.tools.logging :as log])
      (:import org.joda.time.DateTimeZone))
    
    ;; FIXME 定时功能应该还没有做    (^_^)
    
    (defn times []
      (rest (periodic-seq (.. (t/now)
                              (withZone (DateTimeZone/getDefault))
                              #_(withTime 0 0 0 0))
                          (t/minutes 10))))
    (defn channel []
      (a/chan))
    
    (defn chime []
      (chime-ch (times) {:ch (channel)}))
    
    (defn start-scheduler []
      (let [chime-channle (chime)]
        (go-loop []
          (when-let [msg (<! chime-channle)]
            (log/error (format "亲爱的 %s, Clojure repl搞一个小时了,休息一下?"
                               (System/getProperty "user.name")))
            (recur)))
        chime-channle))
    

    该定时任务项目启动后一个小时执行一次,执行只是简单打个log,效果如下:


    定时任务

    16、优雅地打印jdbc的执行sql

    项目中默认的jdbc驱动是mysql自身的启动,所以默认的databaseurl也许是这样的

    :database-url "mysql://localhost:3306/demo?user=root&password=password
    

    然而,这样的配置是不会打印出jdbc执行的真正sql的,而我们有时候很需要这些sql,因为他们代表着逻辑,有时候debug也会需要。
    那么怎么配置才能达到目的呢?
    我们使用的是log4jdbc,因此需要在project.clj中引入该库,

    [com.googlecode.log4jdbc/log4jdbc "1.2"]
    

    引入以后修改需要查看sql的profile里的edn配置文件,比如本地dev-config.edn

    :database-url "jdbc:log4jdbc:mysql://localhost:3306/demo?user=root&password=password
    

    然后jdbc连接处自然也得变,routes/db/core.clj

    (defstate ^:dynamic *db*
              :start (do (Class/forName "net.sf.log4jdbc.DriverSpy")
                         (if-let [jdbc-url (env :database-url)]
                           (conman/connect! {:jdbc-url jdbc-url})
                           (do
                             (log/warn "database connection URL was not found, please set :database-url in your config, e.g: dev-config.edn")
                             *db*)))
              :stop (conman/disconnect! *db*))
    

    默认的log配置,使用logback是配置的方式。
    这样会在log控制台看到很多jdbc的log,因为默认这些日志都是info的,需要调整logback里日志级别。
    为了分开打印log、error、sql的log,附上我本地的logback配置文件

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration scan="true" scanPeriod="10 seconds">
        <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 如果只是想要Info级别的日志,只是过滤info还是会输出Error日志,因为Error的级别高,使用filter,可以避免输出Error日志 -->
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <!--过滤 Error-->
                <level>ERROR</level>
                <!--匹配到就禁止-->
                <onMatch>DENY</onMatch>
                <!--没有匹配到就允许-->
                <onMismatch>ACCEPT</onMismatch>
            </filter>
            <file>log/info-wxapi.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>log/info-wxapi.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- keep 30 days of history -->
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
            </encoder>
        </appender>
    
        <appender name="ERRORFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!--如果只是想要 Error 级别的日志,那么需要过滤一下,默认是 info 级别的,ThresholdFilter-->
            <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
                <level>Error</level>
            </filter>
            <file>log/error-wxapi.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>log/error-wxapi.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- keep 30 days of history -->
                <maxHistory>10</maxHistory>
            </rollingPolicy>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
            </encoder>
        </appender>
    
        <appender name="SQLFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>log/sql-wxapi.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>log/sql-wxapi.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- keep 30 days of history -->
                <maxHistory>10</maxHistory>
            </rollingPolicy>
            <encoder>
                <charset>UTF-8</charset>
                <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern>
            </encoder>
        </appender>
    
        <logger name="org.apache.http" level="warn"/>
        <logger name="org.xnio.nio" level="warn"/>
        <logger name="com.zaxxer.hikari" level="warn"/>
        <logger name="io.undertow.session" level="warn"/>
        <logger name="io.undertow.request" level="warn"/>
        <logger name="jdbc.audit" level="warn"/>
        <logger name="jdbc.sqltiming" level="warn"/>
        <logger name="jdbc.connection" level="warn"/>
        <logger name="jdbc.resultset" level="warn"/>
    
        <logger name="wxapi" level="INFO" additivity="false">
            <appender-ref ref="FILE"/>
            <appender-ref ref="ERRORFILE"/>
        </logger>
    
        <logger name="jdbc.sqlonly" level="INFO" additivity="false">
            <appender-ref ref="SQLFILE"/>
        </logger>
    
        <root level="ERROR">
            <appender-ref ref="ERRORFILE"/>
            <appender-ref ref="FILE"/>
        </root>
    </configuration>
    
    

    :as request的意思是包含前面指定获取的参数的所有。
    当然,如你所知,clojure确实足够灵活,取参方式也还有方式,比如

    ["/path/good-all-params/:id"
       {:post {:summary    "更多方式"
               :parameters {:path  {:id int?}
                            :query {:name string?}
                            :body  {:message string?}}
               :handler    (fn [{{data :body} :parameters
                                 {{:keys [id]} :path}    :parameters]
                            (ok (format " body params: %s " data)))}}]
    
    
    

    这里参数名称data可以定义成任何你想叫的名字。

    相关文章

      网友评论

          本文标题:clojure web(luminus)接口开发

          本文链接:https://www.haomeiwen.com/subject/xhbfxctx.html