項目框架
本項目使用luminus做模板,參考luminus-template,執行下面的命令init工程:
lein new luminus alk-wxapi +mysql +service
相關文檔
- 后臺基礎框架luminus
- 后臺sql支持HugSQL
- 后臺web框架
- 前后端路由框架
- 前端狀態管理框架
- 前端PC版UI框架-antizer
- clojure 函數定義及demo查詢
- clojure編程風格指南
- spec guide
- 單元測試
項目運行
在命令行工具中啟動用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中遠程連接
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}
方法打印出中間件的處理邏輯,
結果如下:
--- 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即可:
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
可以定義成任何你想叫的名字。