
当所有低代码都在卷画布时,我们押注了源代码本身
钉钉宜搭、腾讯微搭、简道云、JeecgBoot……
过去两年,国内每一家低代码厂商的发布会,PPT 几乎是同一张:一个浏览器画布、一个 AI 副驾驶、一句"业务人员也能搭"。
只有 Erupt 在做一件反方向的事——
一行 @Erupt 注解,一个 DataProxy<T> 扩展点,一组 Handler 接口。
没有画布。
没有"AI 生成代码"。
注解,就是 UI Schema 本身。
这一期,把我们为什么没去做拖拽画布的工程理由讲透。
01|国内"低代码"三个字底下,藏着三种完全不同的东西
先做一个不太友好的拆解。
市面上喊自己"低代码"的产品,可以分成三派——
第一派:画布即应用
代表:钉钉宜搭、腾讯微搭、简道云、明道云。
特点:浏览器里画一个表单,发布就能用。配置存在 SaaS 内部数据库里,导出仅作为备份。
适合:业务部门、运营、轻 IT。
第二派:画布生成代码
代表:JeecgBoot、JNPF。
特点:在线设计器画好表单,导出一份 Java + Vue 代码给你拿回去改。
适合:想快速跑通 PoC 的 Java 小团队。
第三派:代码即配置
代表:Erupt。
特点:没有画布,没有"生成",注解就是配置。
适合:有工程团队、领域模型稳定、合规审计是硬约束的项目。

02|画布派最大的隐藏成本,不是"会用",而是"长大之后"
听起来很反直觉,但仔细想想——
画布派的产品前两个月真的爽:业务自己拖几下,admin 后台就跑起来了。
可是当系统活到第二年,下面这些问题会一起冒出来:
- 业务逻辑稍微复杂一点,画布属性面板里写的那段 JS 就开始"看不懂、不敢删、不能测"。
- 想 code review?只能截图对比。
- 想跨环境部署?只能"导出应用包"。
- 想跑单测?没有这个概念。
- 想接外部 HTTP / 消息队列 / Redis?要么去找平台扩展,要么不行。
第二派"画布生成代码"听起来折中,但它有一个致命的双向同步问题——
一旦你回到 IDE 改了 service 层,画布就不再可信。下次想用画布再加个字段,要么承担合并冲突,要么放弃画布。
最后大家都在做同一件事:用画布生成完之后,永远不再回画布。
那它和"脚手架"的区别,到底在哪里?
03|Erupt 的反向押注:领域模型 + 注解 = UI
直接看代码——
@Erupt(name = "客户",power = @Power(importable = true, export = true)
)
@Table(name = "t_customer")
@Entity
public class Customer extends BaseModel {@EruptField(views = @View(title = "客户名"),edit = @Edit(title = "客户名", notNull = true, search = @Search))private String name;@EruptField(views = @View(title = "等级"),edit = @Edit(title = "等级",type = EditType.CHOICE,choiceType = @ChoiceType(vl = {@VL(value = "A", label = "A 类"),@VL(value = "B", label = "B 类")})))private String level;
}

3 个注解 + 2 个字段,Erupt 自动给你生成——
✅ 列表页(搜索框、Excel 导入导出、按等级过滤)
✅ 新建 / 编辑表单(必填校验、下拉选项)
✅ 权限 / 菜单 / API endpoint
✅ 前端表格行为(排序、列宽、操作列)
为什么这么短的注解能撑起整个 UI?
打开 erupt-core/proxy/AnnotationProxyPool.java 你会看到:Erupt 启动时把 @Erupt、@EruptField、@View、@Edit 这些注解通过 AnnotationProxy<T,R> 序列化成 JSON,前端 Angular 拿到 JSON 就动态渲染界面。
也就是说——
注解不是装饰品,注解就是 UI Schema 的物化形态。
只不过这份 schema 写在源码里、被 IDE 自动补全、被 Git 版本管理。
04|业务逻辑写在哪?写在 Spring Bean 里
画布派被问到的第一个问题永远是:
那如果业务逻辑很复杂怎么办?
宜搭、微搭的答案是"事件 + JS 片段"——你在画布属性面板里写 function onSubmit(formData) { ... },没有 IDE、没有 Spring 容器、没有事务。
JeecgBoot 的答案是"回 IDE 改 service"——然后画布就废了。
Erupt 的答案在 erupt-annotation 里有一个接口:
public interface DataProxy<MODEL> extends MetaProxy<MODEL> {default void validate(MODEL model) throws EruptException {}default void beforeAdd(MODEL model) {}default void afterAdd(MODEL model) {}default void beforeUpdate(MODEL model) {}default void afterUpdate(MODEL model) {}default void beforeDelete(MODEL model) {}default void afterDelete(MODEL model) {}// ...
}
任意实现这个接口的 Spring Bean,被 @Erupt(dataProxy = ...) 引用之后,就在 admin 的增删改查生命周期里跑。
它是普通的 @Service,所以——
🔸 能 @Autowired 任何东西
🔸 能打 @Transactional、@PreAuthorize
🔸 能调 JPA、Redis、MQ、HTTP
🔸 能 code review、写单测、IDE 重构
@Service
public class CustomerProxy implements DataProxy<Customer> {@Resource private CustomerService service;@Resource private AuditLogger auditLogger;@Override@Transactionalpublic void beforeAdd(Customer model) {service.assertNameUnique(model.getName());model.setOwnerId(MetaContext.getUser().getId());}@Overridepublic void afterUpdate(Customer model) {auditLogger.log("customer.update", model.getId());}
}
和画布派"在属性面板里写 JS"的本质区别是——
这段代码,跟你 service 层共享同一个事务管理器、同一个安全上下文、同一份单元测试。
05|所有"动态行为",都收敛到 Java 接口
画布派第二个常吹的点是"无代码动态行为"——
某个下拉框的选项动态拉取、某行只对管理员可见、某个按钮触发自定义流程……
画布派的方案是"事件配置面板 + 自创 DSL"。
Erupt 的方案是 Handler 接口:
| 想做的事 | 在注解里写 | 实现这个接口 |
|---|---|---|
| 数据过滤(行级权限) | @Filter(conditionHandler=…) |
FilterHandler |
| 自定义操作按钮 | @RowOperation(operationHandler=…) |
OperationHandler |
| 动态权限控制 | @Erupt(powerHandler=…) |
PowerHandler |
| 下拉选项动态拉取 | @ChoiceType(fetchHandler=…) |
ChoiceFetchHandler |
| 输入框自动补全 | @InputType(autoCompleteHandler=…) |
AutoCompleteHandler |
源码位置:erupt-annotation/src/main/java/xyz/erupt/annotation/fun/
举个最常见的——只让用户看到自己创建的订单:
@Service
public class MyOrderFilter implements FilterHandler {@Overridepublic String filter(String condition, String[] params) {Long uid = MetaContext.getUser().getId();return "ownerId = " + uid;}
}@Erupt(name = "订单",filter = @Filter(conditionHandler = MyOrderFilter.class)
)
@Entity
public class Order extends BaseModel { /* ... */ }
注意这里的设计选择——
Handler 不是字符串表达式,不是平台 DSL,是一个真的 Java 接口。
这意味着——
✅ 能 @Autowired 注入 RoleService、TenantContext
✅ 能写单元测试
✅ 能 IDE 跳转、追引用、重构
✅ 复杂逻辑(多租户、季度切片、A/B 实验)就是普通 Java,不需要学任何"低代码 DSL"
我们的赌注是——
宁可让你写 6 行 Java,也不要让你学一套只能用一次的 DSL。
06|跟国内主流低代码到底怎么比?
放一张干货对比,截图收藏用——

真理来源
宜搭:SaaS 画布的内部 DB
JeecgBoot:一次性生成的 .java / .vue
Erupt:Git 里的 .java 源文件
加一个字段
宜搭:进画布配 → 同步表结构
JeecgBoot:改实体 + 改 Vue + 改 mapper
Erupt:@EruptField 加一行
Diff / Review
宜搭:截图对比
JeecgBoot:git diff(但 diff 的是自动生成的代码)
Erupt:git diff,且只 diff 注解
业务逻辑写在哪
宜搭:表单后端事件 + JS 片段
JeecgBoot:改 service / controller
Erupt:Spring Bean + DataProxy<T>
跨环境部署
宜搭:应用包导入导出
JeecgBoot:jar + 同步 SQL
Erupt:mvn package 一个 jar
我们不是在说画布派一定错——
对没有工程团队的业务部门来说,"在浏览器里搭出一个查库 App"就是真实生产力。
JeecgBoot 这类生成派也有它的位置——
只要你只关心"快速跑起来",且永远不会回到画布,它是合理的脚手架。
但当系统已经长大、领域模型稳定、合规和审计是硬约束时——
画布即应用会撞上「逻辑表达力上限」。
画布生成代码会撞上「双向同步漂移」。
这两个问题在第一性原理上都不可解——因为它们都在尝试"既要画布的轻,又要代码的强"。
Erupt 的反向押注是:
承认代码本身就是低代码的下限。
注解已经是配置面,再压缩就不诚实了。
剩下的事,交给 IDE、Git、Spring——这套已经被验证了 20 年的工具链。
07|5 分钟跑通:从空项目到 admin 页面
如果你看完想立刻动手,下面这条路径已经踩平了——
Step 1:建一个空 Spring Boot 工程,引入 erupt-jpa 依赖
Step 2:在 application.yml 配数据库
Step 3:写第一个带 @Erupt 的实体
Step 4:启动,访问 /,默认账号 erupt / erupt
整个过程不超过 5 分钟。
跑通之后,把本文 §04(DataProxy)和 §05(Handler)翻回来对照——
加业务逻辑写 DataProxy<T>。
加动态过滤写 FilterHandler。
加自定义按钮写 OperationHandler。
每一步都是写 Java,每一步都被 IDE、被 Git、被 CI 接管,没有任何一步回到画布。

写在最后
低代码的尽头,可能不是 AI 自动拖拽。
而是——
让代码本身,成为最小的配置面。
如果你认同这条路,欢迎做三件事——
⭐ GitHub 给 Erupt 点个 Star:github.com/erupts/erupt
📌 关注公众号:每周三一篇深度技术文
💬 加入 Erupt 用户群:后台回复"加群"
本文由mdnice多平台发布
