Clojure 在 Web 开发领域有着独特的优势:不可变数据结构天然适合表示 HTTP 请求/响应,纯函数编写 handler 使得逻辑清晰易测,JVM 生态则提供了成熟的数据库连接池和 HTTP 服务器。本文将系统性地构建一个完整的 Clojure Web 应用,从 Ring 基础架构到现代路由库 reitit,再到数据库集成与监控,覆盖现代 RESTful 服务开发的完整链路。
1. Ring:HTTP 抽象层
1.1 核心概念:请求与响应
Ring 将 HTTP 请求抽象为一个不可变的 Clojure map,响应也是一个 map,这种设计的简洁性使其成为 Clojure Web 的事实标准:
;; HTTP 请求表示
{:server-port 8080
:server-name "localhost"
:remote-addr "127.0.0.1"
:uri "/users"
:query-string "page=1"
:scheme :http
:request-method :get
:headers {"content-type" "application/json"
"authorization" "Bearer abc123"}
:body #object[java.io.InputStream]}
;; HTTP 响应表示
{:status 200
:headers {"Content-Type" "application/json"}
:body "{\"name\":\"Alice\"}"}
Ring 的设计之美在于无 AST、无 DSL、无宏——请求就是一个 Clojure map,响应也是一个。这种统一的数据表示使 middleware 可以任意组合,handler 可以任意测试。
1.2 Handler 函数
;; 最简单的 handler
(defn hello-handler [request]
{:status 200
:headers {"Content-Type" "text/plain"}
:body "Hello, Clojure Web!"})
;; 带路由解析的 handler
(defn user-handler [request]
(let [user-id (get-in request [:path-params :id])]
{:status 200
:headers {"Content-Type" "application/json"}
:body (json/generate-string {:id user-id :name "Alice"})}))
1.3 Middleware:横切关注点
Middleware 是高阶函数,接收一个 handler 并返回增强后的 handler,用于处理日志、权限、异常等横切关注点:
;; 请求日志 middleware
(defn wrap-request-log [handler]
(fn [request]
(let [start (System/currentTimeMillis)
response (handler request)
elapsed (- (System/currentTimeMillis) start)]
(println (format "%s %s -> %d (%d ms)"
(name (:request-method request))
(:uri request)
(:status response)
elapsed))
response)))
;; 错误处理 middleware
(defn wrap-error-handler [handler]
(fn [request]
(try
(handler request)
(catch Exception e
{:status 500
:headers {"Content-Type" "application/json"}
:body (json/generate-string {:error (.getMessage e)})}))))
;; 组合 middleware(注意顺序:最后包装的最先执行)
(def app
(-> hello-handler
wrap-request-log
wrap-error-handler))
Middleware 的执行顺序遵循洋葱模型:请求从外到内穿入,响应从内到外穿出。最外层 middleware 最先看到请求、最后看到响应。
1.4 Jetty 服务器启动
(require '[ring.adapter.jetty :refer [run-jetty]])
;; 开发模式(带 var 引用,支持实时重载)
(defonce server (run-jetty #'app
{:port 8080
:join? false}))
;; 生产模式
(run-jetty app {:port 8080
:join? true
:min-threads 10
:max-threads 200})
2. reitit:现代数据驱动路由
2.1 为什么用 reitit 替代 Compojure
| 特性 | Compojure | reitit |
|---|---|---|
| 路由语法 | 宏定义(DSL) | 纯数据(数据驱动) |
| 性能 | 运行时匹配 | 编译期优化 Trie 匹配,性能极优 |
| 类型安全 | 无 | 可选 schema/spec 集成 |
| 中间件 | 手动组合 | 声明式路由级/级联中间件 |
| 文档生成 | 困难 | 路由数据本身即文档 |
reitit 的设计哲学是「路由即数据」,与 Clojure 的函数式和数据优先理念完美契合。
2.2 基础路由定义
(require '[reitit.ring :as ring])
(require '[reitit.coercion.spec])
(def app-routes
["/api"
["/users"
{:get {:handler list-users}
:post {:handler create-user}}]
["/users/:id"
{:get {:handler get-user}
:put {:handler update-user}
:delete {:handler delete-user}}]
["/health"
{:get {:handler (fn [_] {:status 200 :body {:status "ok"}})}}]])
(def app
(ring/ring-handler
(ring/router app-routes)
(ring/create-default-handler)))
2.3 路由中间件声明
(defn create-app []
(ring/ring-handler
(ring/router
app-routes
{:data {:middleware [wrap-request-log
wrap-error-handler
wrap-json-response]}})))
;; 或路由级中间件(细粒度控制)
(def app-routes
["/api"
{:middleware [wrap-auth]}
["/admin"
{:middleware [wrap-admin-only]
:get {:handler admin-dashboard}}]])
2.4 数据强制转换与验证
(require '[reitit.coercion.spec :as spec-coercion])
(require '[clojure.spec.alpha :as s])
(s/def :user/id pos-int?)
(s/def :user/name string?)
(s/def :user/email (s/and string? #(re-matches #".+@.+\..+" %)))
(def app-routes
["/api"
["/users/:id"
{:get {:parameters {:path {:id :user/id}}
:handler get-user}}]
["/users"
{:post {:parameters {:body (s/keys :req [:user/name :user/email])}
:handler create-user}}]])
(def app
(ring/ring-handler
(ring/router
app-routes
{:data {:coercion spec-coercion/coercion
:muuntaja m/instance}})))
2.5 路由级拦截器与 Pedestal 对比
Pedestal 是另一款 Clojure Web 框架,使用**拦截器(Interceptor)**替代 Middleware:
;; Pedestal 拦截器
(def logger-interceptor
{:name :logger
:enter (fn [context]
(println "请求进入:" (get-in context [:request :uri]))
context)
:leave (fn [context]
(println "请求离开:" (get-in context [:response :status]))
context)})
拦截器相比 middleware 的优势:
- 可以在 leave 阶段访问 response(middleware 只能在返回后处理)
- 支持异步处理(基于 core.async)
- 拦截器栈可以动态修改
reitit 同时支持 middleware 和 interceptor 两种模式,提供了灵活的选型空间。
3. muuntaja:内容协商与序列化
3.1 自动内容协商
muuntaja 自动处理请求内容的解码和响应内容的编码:
(require '[muuntaja.core :as m])
(require '[reitit.ring.middleware.muuntaja :as muuntaja])
;; 配置 muuntaja 支持 JSON 和 EDN
(def muuntaja-instance
(m/create
(-> m/default-options
(m/update-in-interceptors
[m/select-interceptor]
#(conj % (m/map->Interceptor
{:name ::edn
:matches #{"application/edn"}
:encode (partial pr-str)
:decode clojure.edn/read-string}))))))
;; 使用:客户端 accept: application/json → 返回 JSON
;; 客户端 accept: application/edn → 返回 EDN
;; POST application/json → 自动解析为 Clojure 数据结构
3.2 路由集成
(def app
(ring/ring-handler
(ring/router
app-routes
{:data {:muuntaja muuntaja-instance
:middleware [muuntaja/format-middleware]}})))
4. next.jdbc:现代数据库访问
4.1 基础查询与执行
(require '[next.jdbc :as jdbc])
;; 数据源配置
(def db {:dbtype "postgresql"
:dbname "myapp"
:host "localhost"
:port 5432
:user "app"
:password "secret"})
;; 执行查询
(jdbc/execute! db ["SELECT * FROM users WHERE id = ?" 42])
;; => [{:users/id 42, :users/name "Alice", :users/email "alice@example.com"}]
;; 执行插入(返回 generated key)
(jdbc/execute-one! db
["INSERT INTO users (name, email) VALUES (?, ?) RETURNING *"
"Bob" "bob@example.com"])
;; 事务处理
(jdbc/with-transaction [tx db]
(jdbc/execute! tx ["UPDATE accounts SET balance = balance - ? WHERE id = ?" 100 1])
(jdbc/execute! tx ["UPDATE accounts SET balance = balance + ? WHERE id = ?" 100 2]))
4.2 连接池:HikariCP
(require '[hikari-cp.core :as hikari])
(def datasource
(hikari/make-datasource
{:jdbc-url "jdbc:postgresql://localhost:5432/myapp"
:username "app"
:password "secret"
:maximum-pool-size 10
:minimum-idle 2
:connection-timeout 30000}))
;; 传入 datasource 而不是 map
(jdbc/execute! datasource ["SELECT 1"])
4.3 结果集构建器
(require '[next.jdbc.result-set :as rs])
;; 自定义结果集处理方式
(jdbc/execute! ds ["SELECT * FROM users"]
{:builder-fn rs/as-unqualified-maps})
;; => [{:id 1, :name "Alice"} ...] ; 不带表前缀
(jdbc/execute! ds ["SELECT * FROM users"]
{:builder-fn rs/as-modified-maps
:column-fn keyword
:label-fn clojure.string/lower-case})
5. 完整实战:RESTful CRUD 服务
(ns myapp.core
(:require [reitit.ring :as ring]
[reitit.coercion.spec :as spec-coercion]
[reitit.ring.middleware.muuntaja :as muuntaja]
[muuntaja.core :as m]
[next.jdbc :as jdbc]
[next.jdbc.sql :as sql]
[hikari-cp.core :as hikari]
[ring.adapter.jetty :refer [run-jetty]]
[clojure.spec.alpha :as s]))
;; --- 配置 ---
(def ds
(hikari/make-datasource
{:jdbc-url "jdbc:postgresql://localhost:5432/myapp"
:username "app"
:password "secret"
:maximum-pool-size 10}))
;; --- Spec ---
(s/def :user/name string?)
(s/def :user/email string?)
(s/def :user/id pos-int?)
;; --- Handlers ---
(defn list-users [_]
{:status 200
:body (sql/query ds ["SELECT * FROM users ORDER BY id"]
{:builder-fn next.jdbc.result-set/as-unqualified-maps})})
(defn get-user [{{:keys [id]} :path-params}]
(if-let [user (sql/get-by-id ds :users (Long/parseLong id))]
{:status 200 :body user}
{:status 404 :body {:error "User not found"}}))
(defn create-user [{{:keys [name email]} :body-params}]
(let [result (sql/insert! ds :users {:name name :email email})]
{:status 201 :body result}))
(defn update-user [{{:keys [id]} :path-params {:keys [name email]} :body-params}]
(sql/update! ds :users {:name name :email email} {:id (Long/parseLong id)})
{:status 200 :body {:message "Updated"}})
(defn delete-user [{{:keys [id]} :path-params}]
(sql/delete! ds :users {:id (Long/parseLong id)})
{:status 204 :body nil})
;; --- Routing ---
(def routes
["/api"
["/users"
{:get {:handler list-users}
:post {:parameters {:body (s/keys :req [:user/name :user/email])}
:handler create-user}}]
["/users/:id"
{:get {:parameters {:path {:id :user/id}}
:handler get-user}
:put {:parameters {:path {:id :user/id}
:body (s/keys :req [:user/name :user/email])}
:handler update-user}
:delete {:parameters {:path {:id :user/id}}
:handler delete-user}}]])
;; --- Middleware ---
(defn wrap-exception [handler]
(fn [request]
(try
(handler request)
(catch Exception e
{:status 500
:body {:error "Internal server error"
:message (.getMessage e)}}))))
;; --- App ---
(def app
(ring/ring-handler
(ring/router routes
{:data {:coercion spec-coercion/coercion
:muuntaja m/instance
:middleware [muuntaja/format-middleware
wrap-exception]}})))
;; --- Main ---
(defn -main [& args]
(run-jetty app {:port 8080 :join? true})
(println "Server started on http://localhost:8080"))
5.1 请求测试
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}'
# 列出用户
curl http://localhost:8080/api/users
# 获取单个用户
curl http://localhost:8080/api/users/1
# 更新用户
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Alice Updated","email":"alice.new@example.com"}'
# 删除用户
curl -X DELETE http://localhost:8080/api/users/1
6. 监控与可观测性
6.1 简单指标收集 Middleware
(defonce ^:private metrics (atom {}))
(defn wrap-metrics [handler]
(fn [request]
(let [start (System/currentTimeMillis)
response (handler request)
elapsed (- (System/currentTimeMillis) start)
uri (:uri request)
status (:status response)]
(swap! metrics update-in [uri status :count] (fnil inc 0))
(swap! metrics update-in [uri status :total-ms] (fnil + 0) elapsed)
response)))
(defn get-metrics []
@metrics)
6.2 健康检查端点
(defn health-check [_]
(try
(jdbc/execute-one! ds ["SELECT 1"])
{:status 200 :body {:status "healthy" :db "connected"}}
(catch Exception e
{:status 503 :body {:status "unhealthy" :db (.getMessage e)}})))
;; 添加路由
(def routes
["/api"
["/health" {:get {:handler health-check}}]
;; ... 其他路由
])
7. 部署与生产优化
7.1 Uberjar 打包
;; deps.edn
:aliases {:uber {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.1.303"}}
:exec-fn hf.depstar/uberjar
:exec-args {:aot true
:main-class myapp.core
:jar "target/myapp.jar"}}}
clojure -T:uber uber
java -Xmx1g -Dclojure.compiler.direct-linking=true -jar target/myapp.jar
7.2 Docker 化
FROM clojure:temurin-17-tools-deps as builder
COPY . /app
WORKDIR /app
RUN clojure -T:uber uber
FROM eclipse-temurin:17-jre-alpine
COPY --from=builder /app/target/myapp.jar /app.jar
EXPOSE 8080
CMD ["java", "-Xmx1g", "-jar", "/app.jar"]
7.3 生产环境 JVM 参数建议
| 参数 | 说明 |
|---|---|
-Xmx1g | 最大堆内存 1GB |
-Xms1g | 初始堆内存 1GB,避免运行时扩容 |
-Dclojure.compiler.direct-linking=true | 直接链接优化(跳过 var 查找) |
-server | 使用 server JVM(默认) |
-XX:+UseG1GC | G1 垃圾回收器(Java 9+ 默认) |
8. 总结
Clojure Web 开发栈以 Ring 为核心抽象,reitit 提供数据驱动的高性能路由,next.jdbc 实现现代数据库访问。整个栈的设计遵循函数式原则:数据即配置、handler 即纯函数、middleware 即高阶函数组合。
| 组件 | 作用 | 替代方案 |
|---|---|---|
| Ring | HTTP 请求/响应抽象 | — |
| reitit | 路由与 coercion | Compojure, Pedestal |
| muuntaja | 内容协商 | ring-middleware-format |
| next.jdbc | 数据库访问 | clojure.java.jdbc |
| HikariCP | 连接池 | c3p0, DBCP |
| Jetty | HTTP 服务器 | Netty, http-kit |
框架选型建议:若追求极致性能和异步处理,选择 Pedestal + 拦截器架构;若偏好简洁的数据驱动路由和 Clojure 生态一致性,选择 reitit + Ring。对于大多数 CRUD API 场景,reitit 是性价比最高的选择。
延伸阅读可参考 Clojure 调用 Java 深度实践 了解底层 Jetty/HikariCP 的 JVM 集成细节,以及 Clojure spec 与测试 中为 Web API 添加数据验证层的方案。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。