惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Check Point Blog
U
Unit 42
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Martin Fowler
Martin Fowler
L
LangChain Blog
博客园_首页
博客园 - 【当耐特】
Vercel News
Vercel News
I
InfoQ
GbyAI
GbyAI
爱范儿
爱范儿
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
B
Blog RSS Feed
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
G
Google Developers Blog
大猫的无限游戏
大猫的无限游戏
Apple Machine Learning Research
Apple Machine Learning Research
F
Fortinet All Blogs
N
Netflix TechBlog - Medium
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Proofpoint News Feed
美团技术团队
V
V2EX
Stack Overflow Blog
Stack Overflow Blog
有赞技术团队
有赞技术团队
Y
Y Combinator Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
H
Help Net Security
Recent Announcements
Recent Announcements
Microsoft Azure Blog
Microsoft Azure Blog
D
Docker
宝玉的分享
宝玉的分享
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
量子位
小众软件
小众软件
J
Java Code Geeks
S
SegmentFault 最新的问题
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
MongoDB | Blog
MongoDB | Blog
The Cloudflare Blog
Recorded Future
Recorded Future
阮一峰的网络日志
阮一峰的网络日志
T
The Blog of Author Tim Ferriss
MyScale Blog
MyScale Blog
Microsoft Security Blog
Microsoft Security Blog

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 读:Immutability 不是万能药,它是一种权衡 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日
2026-04-14 · via 暗无天日

本文是学习 Leiningen — Complete Tutorial & Best Practices 过程中的笔记整理。全文以一个 greeting-api 项目为统一示例,从零开始演示 Leiningen 的完整工作流。

Leiningen 是什么

Leiningen(简称 lein)是 Clojure 的构建工具和项目管理器。如果你用过其他语言,可以这样类比:

  • Node.js → npm
  • Java → Maven
  • Python → pip
  • Clojure → Leiningen

它围绕 Clojure 的哲学设计,主要功能包括:

  1. 创建和脚手架项目
  2. 管理依赖(从 Clojars 和 Maven Central)
  3. 运行 REPL、测试和构建
  4. 编译打包应用(JAR/uberjar)
  5. 通过插件运行自定义任务
  6. 管理不同环境的配置(dev/test/prod)

安装

Leiningen 需要 JDK 8 或更高版本。安装步骤如下:

curl -L -o ~/bin/lein https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein

chmod +x ~/bin/lein

lein version

我的环境输出如下:

Leiningen 2.10.0 on Java 21.0.10 OpenJDK 64-Bit Server VM

[注] 简单来说,Leiningen 就是 Clojure 世界里的"瑞士军刀",项目从创建到部署几乎都离不开它。

创建项目

使用 lein new app 命令创建一个应用项目:

lein new app greeting-api

实际输出:

Generating a project called greeting-api based on the 'app' template.

生成的目录结构

Leiningen 实际生成了如下文件:

greeting-api/
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc/
│   └── intro.md
├── project.clj              ← 核心:依赖、配置、构建定义
├── resources/               ← 静态文件、配置、资源
├── src/
│   └── greeting_api/
│       └── core.clj         ← 主命名空间
└── test/
    └── greeting_api/
        └── core_test.clj    ← 测试文件

[注] 这里有个容易踩坑的地方:Clojure 命名空间用 - (连字符),但对应的文件夹和文件名用 _ (下划线)。比如命名空间 greeting-api.core 对应的文件路径是 src/greeting_api/core.clj 。这个转换规则是 Clojure 的惯例,Leiningen 会自动处理,但如果手动创建文件时搞混了就会找不到命名空间。

Leiningen 自动生成的初始文件

生成后 project.clj 的内容如下:

(defproject greeting-api "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.11.1"]]
  :main ^:skip-aot greeting-api.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all
                       :jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})

生成后 src/greeting_api/core.clj 的内容如下:

(ns greeting-api.core
  (:gen-class))

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (println "Hello, World!"))

[注] 注意 (:gen-class) 声明——它告诉编译器为这个命名空间生成 Java 类。这是后面打包 uberjar 时能找到入口点的关键,如果忘了加,即使配置了 :aot ,JVM 也找不到入口。另外 :main ^:skip-aot 中的 ^:skip-aot 表示日常开发时不做 AOT 编译,只在打包 uberjar 时才编译。

第一次运行

在项目根目录下执行:

cd greeting-api
lein run

实际输出(首次运行会下载依赖,后续运行不再显示下载信息):

Retrieving nrepl/nrepl/1.0.0/nrepl-1.0.0.pom from clojars
Retrieving nrepl/nrepl/1.0.0/nrepl-1.0.0.jar from clojars
...
Hello, World!

project.clj 详解

project.clj 是整个项目的核心配置文件。现在我们在初始版本的基础上逐步添加功能,将其改造为一个完整的配置。

下面是 greeting-api 项目最终的 project.clj ,每个部分会在后续章节中详细演示:

(defproject greeting-api "0.1.0-SNAPSHOT"
  :description "一个简单的问候 API 服务"
  :url "https://github.com/you/greeting-api"
  :license {:name "MIT"}

  ;; ===== 依赖 =====
  ;; 格式:[group/artifact "version"]
  ;; 实际上就是 Maven 坐标的简写形式
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [ring/ring-core "1.13.0"]      ; HTTP 抽象层
                 [hiccup "1.0.5"]]               ; HTML 生成

  ;; 入口命名空间(lein run 时调用其中的 -main 函数)
  :main greeting-api.core

  ;; 源码路径(这些是默认值,通常不需要显式配置)
  :source-paths ["src"]
  :test-paths ["test"]
  :resource-paths ["resources"]

  ;; ===== 插件(详见后文 Plugins 章节)=====
  :plugins [[lein-ring "0.12.6"]]

  ;; Ring 插件配置
  :ring {:handler greeting-api.core/app
         :port 3000}

  ;; ===== Aliases(详见后文 Aliases 章节)=====
  :aliases {"fmt"   ["cljfmt" "fix"]
            "lint"  ["eastwood"]
            "build" ["with-profile" "uberjar" "uberjar"]
            "ci"    ["do" "clean," "cljfmt" "check," "eastwood," "test," "uberjar"]}

  ;; ===== Profiles(详见后文 Profiles 章节)=====
  :profiles {:dev     {:dependencies [[ring/ring-mock "0.4.0"]]
                       :plugins [[lein-cljfmt "0.9.2"]
                                 [jonase/eastwood "1.4.3"]]
                       :source-paths ["dev"]}
             :uberjar {:aot :all
                       :omit-source true}})

[注] defproject 宏的第一个参数是项目名,第二个是版本号(遵循 语义化版本 规范)。=0.1.0-SNAPSHOT= 中的 SNAPSHOT 表示开发版本,这在 Java/Clojure 生态中是惯例。

编写核心代码

现在我们来编写 greeting-api 的核心代码,后续所有配置演示都围绕这个项目。

修改 src/greeting_api/core.clj

(ns greeting-api.core
  (:require [ring.util.response :as resp]
            [ring.middleware.params :refer [wrap-params]])
  (:gen-class))

(defn greet
  "返回问候字符串"
  [name]
  (str "Hello, " name "!"))

;; Ring handler:处理 HTTP 请求
(defn handler [request]
  (let [name (get-in request [:query-params "name"] "World")]
    (resp/response (greet name))))

;; 用 wrap-params 中间件自动解析查询参数,然后供 lein-ring 使用
(def app (wrap-params handler))

(defn -main [& args]
  (println (greet "World")))

[注] 这里用到了 wrap-params 中间件,它的作用是将 URL 中的查询参数(如 ?name=Alice )解析到 :query-params 中。Ring 默认不解析查询参数,需要手动添加这个中间件。

编写测试 test/greeting_api/core_test.clj

(ns greeting-api.core-test
  (:require [clojure.test :refer :all]
            [greeting-api.core :refer :all]))

(deftest test-greet
  (testing "按名字问候"
    (is (= "Hello, Alice!" (greet "Alice"))))
  (testing "默认问候"
    (is (= "Hello, World!" (greet "World")))))

运行测试

lein test

实际输出:

lein test greeting-api.core-test

Ran 1 tests containing 2 assertions.
0 failures, 0 errors.

[注] lein test 会自动运行 test/ 目录下所有命名空间中以 deftest 定义的测试。

运行项目

确保 project.clj 中配置了 :main greeting-api.core 后:

lein run

实际输出:

Hello, World!

也可以通过 -m 参数直接指定要运行的命名空间和函数:

lein run -m greeting-api.core/-main

Profiles(环境配置)

Profile 是 Leiningen 中非常实用的功能,它允许你为不同环境(开发、测试、生产)定义不同的配置,而这些配置会合并覆盖默认配置。

合并规则与默认激活

Profile 的合并规则是"后面的覆盖前面的"。Leiningen 有几个默认激活的 profile:

  1. :user (来自 ~/.lein/profiles.clj )——总是激活
  2. :dev ——在大多数命令中自动激活
  3. :test ——在运行 lein test 时自动激活

这意味着你在 :dev 里配置的依赖和插件不需要手动指定就会生效。

在 greeting-api 中使用 Profiles

我们在 greeting-apiproject.clj 中定义以下 profiles:

:profiles {:dev     {:dependencies [[ring/ring-mock "0.4.0"]]
                       :plugins [[lein-cljfmt "0.9.2"]
                                 [jonase/eastwood "1.4.3"]]
                       :source-paths ["dev"]}
             :uberjar {:aot :all
                       :omit-source true}})

逐项说明:

  • :dev profile(开发时自动激活):
    • 添加 ring-mock 依赖用于模拟 HTTP 请求
    • 添加 lein-cljfmt 插件用于代码格式化
    • 添加 dev/ 作为额外源码目录,可以在 dev/user.clj 中放 REPL 辅助函数
  • :uberjar profile(打包时使用):
    • :aot :all 启用 AOT 编译所有命名空间
    • :omit-source true 不把源码打入 JAR

手动激活 Profile

lein with-profile uberjar uberjar    lein with-profile dev,test test      

#'leiningen.core.project/project Warning: The Main-Class specified does not exist within the jar. It may not be executable as expected. A gen-class directive may be missing in the namespace which contains the main method, or the namespace has not been AOT-compiled. Created /tmp/greeting-api/target/greeting-api-0.1.0-SNAPSHOT.jar Created /tmp/greeting-api/target/greeting-api-0.1.0-SNAPSHOT-standalone.jar #'leiningen.core.project/project

lein test user

Ran 0 tests containing 0 assertions. 0 failures, 0 errors.

常见使用模式

开发工具不打入生产包

把开发工具放在 :dev profile 里是个好习惯。如上面的配置所示, ring-mocklein-cljfmt 都只在开发时生效,打包 uberjar 时这些依赖不会被包含进去,保持生产包干净。

额外的源码目录

:dev profile 中配置 :source-paths ["dev"] 后,Leiningen 会把项目根目录(即 project.clj 所在目录)下的 dev/ 也作为源码目录。这样就可以在 dev/user.clj 中放 REPL 辅助函数和启动代码,方便开发但不影响生产构建。

例如创建 dev/user.clj

(ns user
  (:require [greeting-api.core :as core]))

(defn start-dev []
  (println "Dev mode started!")
  (core/greet "Developer"))

在 REPL 中,由于默认进入的是 :main 指定的命名空间(这里是 greeting-api.core ),且 :source-paths 只是让目录在 classpath 上可被找到,并不会自动加载其中的代码,所以需要先 require 加载 user 命名空间:

greeting-api.core=> (require 'user)
nil
greeting-api.core=> (user/start-dev)
Dev mode started!
"Hello, Developer!"

也可以切换到 user 命名空间后直接调用:

greeting-api.core=> (in-ns 'user)
nil
user=> (start-dev)
Dev mode started!
"Hello, Developer!"

Plugins(插件体系)

插件用来扩展 Leiningen 的功能。根据使用场景,插件有三种放置位置。

这里我们以 greeting-api 项目为例,演示如何安装和使用插件。

项目级插件(团队共享)

放在 project.clj 的顶层 :plugins 中,所有人 clone 后都能使用。

greeting-apiproject.clj 中添加:

:plugins [[lein-ring "0.12.6"]]

启动 Ring 服务器:

lein ring server-headless

服务器启动后,用 curl 测试:

curl http://localhost:3000

实际输出:

Hello, World!

带参数访问:

curl "http://localhost:3000?name=Alice"

实际输出:

Hello, Alice!

Ctrl+C 停止服务器。

[注] lein-ring:handler 配置指向 core.clj 中的 app 变量。注意我们的 app 使用了 wrap-params 中间件来解析查询参数。

Profile 内插件(按需激活)

放在特定 profile 中,只在激活该 profile 时生效。

greeting-apiproject.clj:dev profile 中添加:

:profiles {:dev {:plugins [[lein-cljfmt "0.9.2"]
                           [jonase/eastwood "1.4.3"]]}}

使用 lein-cljfmt 检查代码格式:

lein cljfmt check

实际输出(代码格式正确时):

All source files formatted correctly

如果有格式问题,用以下命令自动修复:

lein cljfmt fix

全局插件(个人工具)

放在 ~/.lein/profiles.clj 中,不进入项目仓库,适合个人常用工具:

{:user {:plugins [[lein-ancient "0.7.0"]
                  [lein-pprint "1.3.2"]]}}

使用 lein-ancient 检查依赖是否过时:

lein ancient :all
Retrieving lein-ancient/lein-ancient/0.7.0/lein-ancient-0.7.0.jar from clojars
[org.clojure/clojure "1.12.4"] is available but we use "1.11.1" (use :check-clojure to upgrade)
[ring/ring-core "1.15.4"] is available but we use "1.13.0"
[hiccup "2.0.0"] is available but we use "1.0.5"
(warn)  [central] - Δ 13667ms - failure when checking ring/ring-mock: java.net.SocketTimeoutException: Read timed out
[ring/ring-mock "0.6.2"] is available but we use "0.4.0"

lein-ring 配置

在上面的配置中,我们为 greeting-api 配置了 lein-ring 插件:

:ring {:handler greeting-api.core/app    ; 指向 core.clj 中的 app 变量
       :port 3000}                        ; 端口号

lein-ring 还支持更多选项:

:ring {:handler greeting-api.core/app
       :port 3000
       :auto-reload? true           ; 自动重载(默认 true)
       :reload-paths ["src"]        ; 监听的目录
       :open-browser? false}        ; 是否自动打开浏览器

常用插件一览

类别 插件 用途 常用命令
Web 开发 lein-ring 运行 Ring 应用,自动重载 lein ring server
代码格式 lein-cljfmt 格式化代码(类似 Prettier) lein cljfmt fix
静态分析 jonase/eastwood 发现常见 bug lein eastwood
惯用法 lein-kibit 建议更地道的写法 lein kibit
依赖管理 lein-ancient 检查过时的依赖 lein ancient :all

查找更多插件可以访问 Clojars(搜索 lein-* 前缀)或 Leiningen 官方插件列表

Aliases(自定义快捷命令)

Aliases 允许你在 project.clj 中定义快捷命令,避免每次输入一长串参数。

在 greeting-api 中配置 Aliases

:aliases {"fmt"   ["cljfmt" "fix"]
          "lint"  ["eastwood"]
          "build" ["with-profile" "uberjar" "uberjar"]}

使用简单别名

lein fmt    lein lint   

链式任务(do)

do 将多个任务串联执行。注意逗号是 Leiningen do 任务的语法要求——它分隔 do 中的不同任务表达式,每个任务(除最后一个)后需要逗号。

例如配置一个 CI 流水线:

:aliases {"ci" ["do" "clean," "cljfmt" "check," "test," "uberjar"]}

运行:

lein ci

Profile + 任务组合

"build" 别名演示了如何组合 profile 和任务:

"build" ["with-profile" "uberjar" "uberjar"]

等价于:

lein with-profile uberjar uberjar

即用 :uberjar profile 来执行 uberjar 任务(触发 AOT 编译)。

AOT 编译与打包

AOT 即 Ahead-Of-Time(提前编译)。正常情况下,Clojure 代码是在 JVM 启动时即时编译的。而 AOT 则是在构建阶段就把 Clojure 代码编译成 .class 文件。

为什么需要 AOT?

当你用 lein run 运行项目时,Leiningen 会启动 JVM、加载 Clojure 运行时、即时编译你的代码,然后调用 -main 函数——这个过程依赖 Leiningen 的运行时支持。

但当你想用 java -jar greeting-api.jar 直接运行一个 JAR 包时,JVM 需要一个标准的 Java 入口方法:

public static void main(String[] args)

Clojure 的 -main 函数本身不是这样的入口。AOT 编译会将 :aot 配置中指定的所有命名空间编译成 .class 文件,其中包含符合 Java 入口点规范的 main 方法,这样 JVM 就能直接运行了。

为什么只在 uberjar profile 中使用?

AOT 有几个缺点,不适合在开发时使用:

  • 编译变慢:每次都要编译所有命名空间
  • 过期的 .class 文件:修改代码后如果旧的 .class 文件没清理干净,可能引发难以排查的 bug
  • 产物更大: target/ 目录里多出一堆文件

因此标准做法是把 AOT 放在 :uberjar profile 中(这也是 lein new app 生成的默认配置):

:profiles {:uberjar {:aot :all
                     :omit-source true}}

打包 greeting-api

确保 src/greeting_api/core.clj 中有 (:gen-class) 声明(前面已经添加),然后执行:

lein build

实际输出:

Compiling greeting-api.core
Created /tmp/greeting-api/target/greeting-api-0.1.0-SNAPSHOT.jar
Created /tmp/greeting-api/target/greeting-api-0.1.0-SNAPSHOT-standalone.jar

验证打包结果:

java -jar target/greeting-api-0.1.0-SNAPSHOT-standalone.jar

实际输出:

Hello, World!

[注] standalone.jar 就是 uberjar——它把 Clojure 运行时和所有依赖都打进了同一个 JAR 文件,可以直接用 java -jar 运行,无需安装 Leiningen 或 Clojure。

总结

通过这篇笔记,我们用一个 greeting-api 项目演示了 Leiningen 的完整工作流:

  1. **安装与创建**:用 lein new app 创建项目,注意命名空间 - 与文件路径 _ 的转换
  2. **依赖与运行**:在 project.clj 中声明依赖,用 lein runlein test 运行和测试
  3. **Profiles**:用 :dev 放开发工具,用 :uberjar 做 AOT 编译,保持生产包干净
  4. **Plugins**:通过插件扩展功能(如 lein-ring 运行 Web 服务)
  5. **Aliases**:用 :aliases 定义快捷命令,统一团队工作流
  6. **打包**:用 lein uberjar 生成可独立运行的 JAR 文件