用标准堆出"无聊"系统:从 Java EE 到 Quarkus 再到 AI 时代的生存策略

2026-05-11 19 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

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 时代做好了准备。逻辑链条是这样的:

  1. LLM 集成本质上是 HTTP 调用。调用 OpenAI 或其他模型服务,就是发一个 JSON POST 请求、拿一个 JSON 响应。JAX-RS 的 ClientBuilder 是标准 API,不需要引入任何专门的 AI SDK。

  2. AI Agent 的编排是业务逻辑。编排多个模型调用、处理上下文、决定下一步动作——这些是纯 Java 代码,用 CDI 管理服务间依赖,用标准并发模型处理异步。不需要什么 Agent 框架。

  3. 标准接口让替换实现变得 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.endpointllm.model 两个配置项。如果某个模型服务的响应格式不同,写一个适配方法即可——这比引入一个"统一 AI SDK"然后跟着它的版本升级节奏走,风险小得多。

采用建议与边界

这套"无聊"策略不是万能药,有几个明确的适用边界:

适合的场景: - 长期维护的企业系统,生命周期 5 年以上 - 需要跨运行环境迁移的服务(从传统服务器到容器到 Serverless) - 团队对框架选型有分歧时,标准是最低风险的共识

不适合的场景: - 需要快速原型验证的短期项目——这时候引入一个全功能框架可能更快 - 标准规范尚未覆盖的领域——比如某些实时流处理场景,标准还在制定中 - 团队完全没有 Jakarta EE 经验——学习曲线是真实成本

一个实用的检查清单:

  • ✅ 每引入一个新依赖,问自己:这个功能有没有标准 API 可以替代?
  • ✅ 依赖图里是否存在只有一处使用的传递依赖?如果有,考虑用标准方式重写那一处。
  • ✅ 业务代码中是否直接引用了某个具体实现的类名(如 org.glassfish.*)?如果有,改为引用标准接口。
  • ✅ 新功能是否可以用 JDK 标准库完成?java.util.concurrentjava.net.httpjava.time 覆盖了大量常见需求。
  • ✅ AI 集成是否被包装成了"只需要换配置就能换模型"的形式?如果不是,重新设计接口层。

Adam Bien 的核心论点不是"标准永远最好",而是"标准是最可预测的演进路径"。在技术浪潮一波接一波的环境里,可预测性本身就是最大的竞争优势。你不需要预测下一个浪潮是什么——你只需要确保你的代码不绑死在任何一艘特定的船上。


相关推荐