Adam Bien 在企业级 Java 圈里是个异类——他主张零依赖,坚持只用标准,并且把这种做法带来的系统称为"boring"(无聊的)。但这里的"无聊"不是贬义,而是指系统稳定、可预测、不需要天天救火。更关键的是,他靠这套策略让二十年前写的代码一路跑进了云时代,甚至天然适配今天的 AI-Native 场景。
这听起来像是在鼓吹保守主义,但背后有非常具体的工程逻辑。
零依赖不是口号,是风险控制
企业级项目里最常见的依赖膨胀路径是这样的:一开始只需要一个 HTTP 客户端,引入了某个库;这个库又拉进来三个传递依赖;半年后你发现自己在为某个日志框架的版本冲突写排除规则。Adam Bien 的做法是——从一开始就不引入非标准依赖。
所谓"标准",在 Java 世界里指的是 Jakarta EE(原 Java EE)规范:JAX-RS 处理 REST、CDI 处理依赖注入、JPA 处理持久化。这些规范由 JCP 审定,有多家厂商实现,演进路径公开可查。你写的代码面向的是接口(规范),而不是某个具体框架的 API。
好处在哪?当实现从 GlassFish 换到 WildFly,再换到 Quarkus,你的业务代码几乎不用改。因为规范没变,只是底层实现换了。这就是 future-proof 的核心——不是预测未来用什么框架,而是让你的代码不依赖任何特定框架。
一段 JAX-RS 代码的二十年旅程
为了说明这一点,看一个最简单的 REST 端点:
// 2009 年写在 Java EE 6 上,跑在 GlassFish 里
// 2024 年同一个文件跑在 Quarkus 里,零改动
@Path("/hello")
public class HelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, World";
}
}
这段代码在 2009 年是 JAX-RS 1.1 标准,今天在 Quarkus 里是 Jakarta REST 标准。类没变,注解没变,方法签名没变。唯一的变化是运行时从重量式容器变成了 Quarkus 的 SubstrateVM 或 JVM 模式。这不是巧合——Quarkus 本身就是对 Jakarta EE 标准的重新实现,只是用了编译期增强和 GraalVM 的方式来达到云原生所需的启动速度和内存占用。
如果你当年用的是某个非标准的 REST 框架私有 API,迁移到 Quarkus 就意味着重写。而面向标准写的代码,迁移只是换一个依赖坐标。
Quarkus 里的"无聊"实践
下面是一个更贴近实战的例子——一个用纯 Jakarta EE 标准构建的 Quarkus 微服务,包含 REST 端点、依赖注入和定时任务,不引入任何第三方工具库:
// pom.xml 中只需 Quarkus 的标准扩展,不引入任何额外依赖
// 关键依赖坐标:
// io.quarkus:quarkus-rest(Jakarta REST 实现)
// io.quarkus:quarkus-arc(CDI 实现)
// io.quarkus:quarkus-scheduler(定时任务)
@Path("/tasks")
@ApplicationScoped
public class TaskResource {
@Inject
TaskService taskService;
@GET
@Produces(MediaType.APPLICATION_JSON)
public List<Task> list() {
return taskService.pendingTasks();
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(Task task) {
taskService.enqueue(task);
return Response.status(Response.Status.CREATED).build();
}
}
@ApplicationScoped
public class TaskService {
private final List<Task> tasks = new CopyOnWriteArrayList<>();
// 每 30 秒自动清理已完成任务——纯标准注解
@Scheduled(every = "30s")
void cleanup() {
tasks.removeIf(Task::isDone);
}
public void enqueue(Task task) {
tasks.add(task);
}
public List<Task> pendingTasks() {
return tasks.stream()
.filter(t -> !t.isDone())
.toList();
}
}
public record Task(String id, String description, boolean done) {
public Task markDone() {
return new Task(id, description, true);
}
}
对应的 Maven 依赖部分保持极简:
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-scheduler</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
启动和测试命令:
# 开发模式启动,支持热重载
mvn quarkus:dev
# 打包为原生二进制(需要 GraalVM 环境)
mvn package -Pnative
# 打包为传统 JVM jar
mvn package
# 测试端点
curl http://localhost:8080/tasks
curl -X POST http://localhost:8080/tasks \
-H "Content-Type: application/json" \
-d '{"id":"t1","description":"process data","done":false}'
注意这里没有 Lombok、没有 Guava、没有任何"方便"的工具库。record 是 Java 语言标准特性,CopyOnWriteArrayList 是 JDK 标准并发容器,@Scheduled 是 Quarkus 对标准定时任务的实现。代码看起来"无聊",但它的依赖图是扁平的,升级路径是清晰的。
为什么这套做法能接住 AI-Native 时代
Adam Bien 在播客中提到,标准驱动的系统天然为 AI 时代做好了准备。逻辑链条是这样的:
-
LLM 集成本质上是 HTTP 调用。调用 OpenAI 或其他模型服务,就是发一个 JSON POST 请求、拿一个 JSON 响应。JAX-RS 的
ClientBuilder是标准 API,不需要引入任何专门的 AI SDK。 -
AI Agent 的编排是业务逻辑。编排多个模型调用、处理上下文、决定下一步动作——这些是纯 Java 代码,用 CDI 管理服务间依赖,用标准并发模型处理异步。不需要什么 Agent 框架。
-
标准接口让替换实现变得 trivial。今天你调用 OpenAI,明天换本地模型,后天换某个国产大模型——只要接口契约不变(都是 HTTP + JSON),你的代码零改动。这和当年从 GlassFish 换到 Quarkus 是同一个模式。
一个用标准 HTTP Client 调用 LLM 的极简示例:
@ApplicationScoped
public class LlmService {
private final Client client = ClientBuilder.newClient();
@ConfigProperty(name = "llm.endpoint")
String llmEndpoint;
@ConfigProperty(name = "llm.model")
String model;
public String complete(String prompt) {
WebTarget target = client.target(llmEndpoint);
Map<String, Object> request = Map.of(
"model", model,
"prompt", prompt,
"max_tokens", 512
);
Response response = target
.request(MediaType.APPLICATION_JSON)
.post(Entity.json(request));
Map<String, Object> body = response.readEntity(Map.class);
response.close();
// 根据不同模型服务的响应格式提取结果
// 这里假设响应中有 "completion" 字段
return (String) body.getOrDefault("completion", "");
}
}
这段代码用的是 Jakarta REST 的标准 ClientBuilder,没有引入任何 AI 专用库。换模型服务只需要改 llm.endpoint 和 llm.model 两个配置项。如果某个模型服务的响应格式不同,写一个适配方法即可——这比引入一个"统一 AI SDK"然后跟着它的版本升级节奏走,风险小得多。
采用建议与边界
这套"无聊"策略不是万能药,有几个明确的适用边界:
适合的场景: - 长期维护的企业系统,生命周期 5 年以上 - 需要跨运行环境迁移的服务(从传统服务器到容器到 Serverless) - 团队对框架选型有分歧时,标准是最低风险的共识
不适合的场景: - 需要快速原型验证的短期项目——这时候引入一个全功能框架可能更快 - 标准规范尚未覆盖的领域——比如某些实时流处理场景,标准还在制定中 - 团队完全没有 Jakarta EE 经验——学习曲线是真实成本
一个实用的检查清单:
- ✅ 每引入一个新依赖,问自己:这个功能有没有标准 API 可以替代?
- ✅ 依赖图里是否存在只有一处使用的传递依赖?如果有,考虑用标准方式重写那一处。
- ✅ 业务代码中是否直接引用了某个具体实现的类名(如
org.glassfish.*)?如果有,改为引用标准接口。 - ✅ 新功能是否可以用 JDK 标准库完成?
java.util.concurrent、java.net.http、java.time覆盖了大量常见需求。 - ✅ AI 集成是否被包装成了"只需要换配置就能换模型"的形式?如果不是,重新设计接口层。
Adam Bien 的核心论点不是"标准永远最好",而是"标准是最可预测的演进路径"。在技术浪潮一波接一波的环境里,可预测性本身就是最大的竞争优势。你不需要预测下一个浪潮是什么——你只需要确保你的代码不绑死在任何一艘特定的船上。