项目框架
本项目使用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
可以定义成任何你想叫的名字。