Clojure 现代 Web 全栈开发:Ring、reitit 与数据库集成

实战 Clojure Web 开发全技术栈:Ring 请求/响应处理与中间件链、reitit 高性能路由与数据驱动 API 设计、muuntaja 内容协商、next.jdbc 数据库访问与连接池管理、Pedestal 拦截器模式对比及完整的 RESTful CRUD 服务可运行代码。

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

特性Compojurereitit
路由语法宏定义(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:+UseG1GCG1 垃圾回收器(Java 9+ 默认)

8. 总结

Clojure Web 开发栈以 Ring 为核心抽象,reitit 提供数据驱动的高性能路由,next.jdbc 实现现代数据库访问。整个栈的设计遵循函数式原则:数据即配置、handler 即纯函数、middleware 即高阶函数组合。

组件作用替代方案
RingHTTP 请求/响应抽象
reitit路由与 coercionCompojure, Pedestal
muuntaja内容协商ring-middleware-format
next.jdbc数据库访问clojure.java.jdbc
HikariCP连接池c3p0, DBCP
JettyHTTP 服务器Netty, http-kit

框架选型建议:若追求极致性能和异步处理,选择 Pedestal + 拦截器架构;若偏好简洁的数据驱动路由和 Clojure 生态一致性,选择 reitit + Ring。对于大多数 CRUD API 场景,reitit 是性价比最高的选择。

延伸阅读可参考 Clojure 调用 Java 深度实践 了解底层 Jetty/HikariCP 的 JVM 集成细节,以及 Clojure spec 与测试 中为 Web API 添加数据验证层的方案。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「clojure」更多文章

  1. Clojure 并发设计模式:STM、core.async 与 Agent 实战
  2. Clojure spec 与测试:数据验证、生成测试与属性驱动
  3. Clojure 工具链演进:Leiningen、Clojure CLI 与 deps.edn