Java 微服务框架为什么越做越重?我重新设计 MetaLite 的三个原则

0 阅读7分钟

Java 微服务框架为什么越做越重?我重新设计 MetaLite 的三个原则

摘要: 从 RuoYi、Yudao 的实际使用感受出发,本文解释我为什么重新设计 MetaLite,以及“业务服务保持简单、技术组件独立复用、可排查性进入架构”三项核心取舍。

第一次接触 RuoYi、Yudao 这类开源项目时,我也曾认为:功能越全,项目启动得越快。

真正把它们用于开发以后,我逐渐遇到了另一个问题:代码和模块很多,自己真正需要的只是其中一部分;一旦出现异常,还要跨越多层封装、多个模块和大量业务代码定位根因。

“开箱即用”解决了第一天的问题,却不一定解决第一年、第三年的问题。

这也是我从零设计 MetaLite 的起点。它不是要再做一套预置业务更丰富的后台,而是尝试回答三个更基础的问题:

  1. 一个企业级 Java 项目真正需要长期复用的是什么?
  2. 框架怎样降低排查问题的成本,而不是只降低创建项目的成本?
  3. 微服务已经按业务拆分以后,单个服务内部还需要多重工程边界吗?

一、问题不一定是功能少,而是边界太多

很多 Java 后台项目会按技术职责拆出多个 Maven 模块:公共模块、系统模块、框架模块、接口模块、代码生成模块、任务模块……

这种结构并非错误。一个模块如果需要独立发布、被多个团队复用,或者拥有不同的生命周期,当然应该拆开。

问题在于,拆分有时会变成一种机械动作。

当一个微服务本身已经对应一个明确业务域,服务内部的 Controller、Service、DAO 又被拆进多个 Maven 模块,一次普通问题排查可能变成下面这样:

请求入口
  → API 模块
  → 公共接口模块
  → Service 模块
  → 数据访问模块
  → 公共工具模块
  → 再回到异常转换模块

代码并没有因为目录更多而自动获得清晰边界,开发者却要额外理解模块依赖、构建顺序和对象来自哪里。

MetaLite 采用了另一种组织方式:

backend-demo/
├── pom.xml
└── src/main/java/com/metalite/demo/
    └── module1/
        ├── entrance/
        │   ├── api/
        │   ├── consumer/
        │   └── job/
        ├── service/
        ├── dao/
        └── domain/

先按业务域聚合,再在业务域内部划分入口、服务、数据访问和领域对象。

这样做的重点不是“少建几个目录”,而是让一次业务变更尽量停留在同一个业务包中。API、消息消费和定时任务只是三种不同入口,它们不应该把同一份业务能力拆散到项目各处。

二、单个服务保持简单,不等于整个框架做成单体

这里很容易产生一个误解:单个业务服务采用一个 Maven 工程,是不是意味着所有能力都堆在一个项目里?

答案恰恰相反。

MetaLite 将具有独立复用价值的技术能力拆成不同工程:

  • backend-bom:统一依赖与插件版本;
  • backend-application:请求协议、切面、RPC、缓存和通用能力;
  • backend-orm:JDBC、Elasticsearch、动态数据源与事务相关能力;
  • backend-mq:Kafka、RocketMQ 接入;
  • backend-gateway:外部协议、安全治理与服务转发;
  • backend-admin:企业后台基础管理服务;
  • web-admin:前端管理后台。

判断是否拆分的依据,不是“这段代码属于 Controller 还是 Service”,而是:

它是否需要独立复用、独立发布和独立演进?

框架组件需要跨服务复用,所以独立维护。一个具体业务服务中的代码共同发布、共同部署,则优先用清晰的包结构管理。

这两种拆分并不冲突,它们解决的是不同层级的问题。

三、技术底座不应该替企业预设业务

通用权限、字典、日志等基础能力可以复用,但企业真正的业务流程很难被一个通用开源项目完整预设。

订单、营销、会员、审批等模块看起来能让项目快速起步,可一旦企业规则不同,就会进入漫长的理解、删除和改造过程。

我更希望 MetaLite 把精力放在业务无关、却会反复出现的工程问题上:

  • 外网请求怎样完成签名、加解密和登录校验;
  • 网关怎样把外部协议转换成干净的内部请求;
  • 多个横切逻辑怎样保证执行顺序并统一异常边界;
  • ORM 怎样减少字符串字段名和重复 CRUD;
  • 动态数据源与事务怎样在第一次 DAO 调用时正确绑定;
  • RPC、缓存、消息和任务怎样拥有一致的日志与上下文。

这些能力不会替企业决定“业务应该怎么做”,但会决定业务代码是否容易维护、故障是否容易定位、系统是否能够继续演进。

四、可排查性必须进入框架设计

很多框架介绍强调功能数量,我更关心一条请求失败以后能不能回答下面的问题:

  • 请求从公网进入后,在哪一步认证、解密和转换?
  • 调用了哪个服务、哪个接口?
  • 进入了哪一次 DAO、Redis 或 MQ 操作?
  • 失败属于业务异常、调用异常还是基础设施异常?
  • 日志能否使用同一个 TraceId 串起来?

因此,MetaLite 没有只做一个统一日志注解,而是把一次调用分成 8 类场景:3 类入口和 5 类内部调用。

入口:API_RECEIVE / JOB_RUN / MQ_CONSUME
调用:DAO_CALL / API_CALL / MQ_PRODUCE / REDIS_CALL / CAFFEINE_CALL

每类场景都有自己的处理器链和统一上下文。入口负责建立调用边界,内部调用沿用同一条上下文继续记录。

可排查性不是上线后再补日志,而应该从请求模型、工程结构和组件边界开始设计。

五、轻量不等于功能简陋

“轻量”经常被误解为代码越少越好。

我理解的轻量,是开发者面对一个问题时,需要同时理解的概念尽可能少:

  • 一个业务服务只有一个主要构建单元;
  • 业务代码按领域聚合;
  • 公网与内网请求类型明确分开;
  • 横切逻辑进入统一生命周期;
  • 技术组件通过稳定入口暴露能力;
  • 没有使用到的业务模块不进入工程。

有些复杂性无法消失,例如分布式事务、缓存一致性和消息可靠性。框架能做的不是用一句“开箱即用”把它们藏起来,而是把边界暴露清楚,让开发者知道系统保证了什么、没有保证什么。

六、选择框架时,建议先问这三个问题

如果正在为企业项目选择技术底座,可以先不看功能清单,而是问:

  1. 删除所有不需要的预置业务以后,还剩下多少真正可复用的工程能力?
  2. 出现一次跨服务故障时,需要跨多少层、多少模块才能找到根因?
  3. 三年后要升级技术组件或拆分业务时,现有边界是在帮助你,还是阻碍你?

MetaLite 仍在持续演进,也不会适合所有团队。但它的出发点非常明确:不替企业预设业务,把复杂度用在请求、安全、数据访问、调用链和工程治理这些真正会长期复用的地方。

后续文章会从源码出发,逐一拆解这些设计,同时明确当前实现的边界和仍需改进之处。


框架简介:元界 MetaLite — 下一代企业级 Java 微服务技术底座

作者简介:基于 Spring 体系 15 年企业级开发经验,专注于通过企业级生产环境落地的工程思维和架构思想打造下一代Java 微服务技术底座

完整文档与源码:Gitee 搜索 MetaLite (gitee.com/MetaLite)