JeecgBoot 项目理解与使用心得(内容增强版)
给第一次上手的同学一条清晰路线,也给已经在用的团队一套"更稳、更快、更可维护"的实践清单。本文聚焦 项目定位、模块地图、上手路线、生成器最佳实践、工程化与安全基线、性能优化、前端协作、DevOps 与可观测性、测试策略、常见坑排雷,并附带可直接落地的代码片段与检查清单。
1. 我对 JeecgBoot 的定位
一句话:JeecgBoot = 低代码平台(在线建模) + 代码生成器(前后端同生) + 企业级脚手架(Spring Boot + MyBatis-Plus + Ant Design Vue)。
-
优势
- 交付效率:模型 → 生成 → 小改小调,即可交付 CRUD 模块。
- 上手成本:分层清晰、生态主流、社区活跃。
- 可维护:生成的是真实工程代码,适合二次开发与长期运维。
-
边界
- 模板即规范:模板与公共库的质量,决定批量复制的质量。
- 复杂业务仍需良好领域建模和工程治理,低代码不是银弹。
2. 模块地图与协作关系(后端/前端)
-
后端
jeecg-boot-module-system
:系统基础能力(用户、角色、字典、文件、日志等)jeecg-boot-base-core
:公共库(工具、异常、统一返回、查询生成器、拦截器)- 技术栈:Spring Boot + MyBatis-Plus(Wrapper/分页/乐观锁/代码生成器)
-
前端
- Ant Design Vue + 路由/权限指令 + 代码生成页面(列表/表单/导入导出)
- 表单、表格、字典组件完备,适合中后台场景
协作:模型与字段 → 生成前后端骨架 → 后端补业务逻辑/查询 → 前端补交互/校验/样式。
3. 快速上手路线图(从"能跑"到"跑得稳")
-
准备与启动
- 初始化数据库(官方脚本),配置数据源与文件存储;
- 启动后端、前端(或使用打包版);
- 登录系统,熟悉系统模块(用户/角色/字典)。
-
模型→生成
- 使用在线建模/代码生成器,生成单表/主从表页面;
- 生成后在 IDE 中补业务逻辑(Service/Mapper);
- 在前端完善校验与交互(必要时自定义组件)。
-
工程化加固(务必做)
- 把 安全基线/性能限幅/统一异常/日志脱敏 固化到公共库与模板;
- 接入 CI / 质量门禁 / 可观测性;
- 规范 DTO/VO 分离、包结构、单元/集成测试。
4. 代码生成器最佳实践(决定"复制出来"的质量)
4.1 数据建模与命名
- 字段统一命名风格(
snake_case
或camelCase
),尽量一致; - 通用字段提前规划:
id
、tenant_id
、org_id
、create_by
、create_time
、update_by
、update_time
、del_flag
; - 选择合适主键策略(雪花或数据库自增),大表建议雪花,避免热点自增锁。
4.2 主从表与关联
- 生成主子表结构时,外键字段与索引要就位;
- 读多写少的从表可考虑懒加载;写频繁则在 Service 里控制事务边界。
4.3 校验与字典
- 后端 DTO 加 JSR-380 校验注解(
@NotBlank @Size @Pattern ...
),Controller 上加@Validated
; - 字典/枚举建议双轨:数据库字典 + 枚举常量(关键枚举落到代码可读)。
4.4 模板"开箱即工程化"(强烈建议)
- Controller 模板固化:分页限幅、排序白名单、LIKE 转义、统一异常与状态码;
- 导出模板固化:Excel 公式注入防护;
- 上传模板固化:魔数/大小/后缀校验;
- 前端模板固化:表单校验 与后端
@Validated
对齐;新 API (如v-model:open
)。
5. 工程化与安全基线(可直接复制的片段)
5.1 LIKE 转义(防止"全表扫描"与越权匹配)
java
public final class SqlLike {
private SqlLike(){}
public static String esc(String s){
if (s==null) return null;
return s.replace("\\","\\\\").replace("%","\\%").replace("_","\\_");
}
public static void like(com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<?> w,
String col, String kw){
String v = "%" + esc(kw) + "%";
w.apply("`"+col+"` LIKE {0} ESCAPE '\\\\'", v);
}
}
5.2 排序白名单(禁止任意列/片段传入)
java
public final class SortGuard {
private SortGuard(){}
public static void assertSortable(String col, java.util.Set<String> whitelist){
if (col==null || !whitelist.contains(col))
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.BAD_REQUEST, "非法排序字段");
}
public static boolean isAsc(String dir){ return !"desc".equalsIgnoreCase(dir); }
}
5.3 下载/预览的路径规范化与软链接防护(Path Traversal)
java
public final class PathSafe {
private PathSafe(){}
public static java.nio.file.Path safeResolve(java.nio.file.Path base, String userInput) throws IOException {
String decoded = java.net.URLDecoder.decode(userInput, java.nio.charset.StandardCharsets.UTF_8);
decoded = decoded.replace('\\','/'); // 统一分隔符
java.nio.file.Path baseReal = base.toRealPath(java.nio.file.LinkOption.NOFOLLOW_LINKS);
java.nio.file.Path normalized = baseReal.resolve(decoded).normalize();
if (!normalized.startsWith(baseReal)) throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.FORBIDDEN,"非法路径");
java.nio.file.Path real = normalized.toRealPath(); // 解析软链接
if (!real.startsWith(baseReal) || !java.nio.file.Files.isRegularFile(real))
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.NOT_FOUND,"目标不存在或非法");
return real;
}
}
5.4 SSRF 防护(URL 转发/外链中转)
java
public final class UrlSafe {
private UrlSafe(){}
public static void assertSafe(java.net.URI u){
String s = u.getScheme();
if(!"http".equalsIgnoreCase(s) && !"https".equalsIgnoreCase(s))
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.BAD_REQUEST,"非法协议");
int p = u.getPort();
if(p!=-1 && p!=80 && p!=443)
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.BAD_REQUEST,"非法端口");
try {
for (java.net.InetAddress a : java.net.InetAddress.getAllByName(u.getHost())) {
if (a.isAnyLocalAddress() || a.isLoopbackAddress()
|| a.isLinkLocalAddress() || a.isSiteLocalAddress())
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.FORBIDDEN,"禁止访问内网地址");
}
} catch (java.net.UnknownHostException e) {
throw new org.springframework.web.server.ResponseStatusException(
org.springframework.http.HttpStatus.BAD_REQUEST,"域名解析失败");
}
}
}
5.5 Excel 公式注入(CSV/Excel Injection)
java
public static String sanitizeExcel(String v){
return v!=null && v.matches("^[=+\\-@].*") ? "'"+v : v;
}
// 写单元格前:
cell.setCellValue(sanitizeExcel(value));
5.6 文件上传:统一入口校验 + 流式写入(避免 OOM)
java
// Controller 入口(无论 local/oss/minio,一律先校验)
if (!(request instanceof org.springframework.web.multipart.MultipartHttpServletRequest)) {
return Result.error("需要 multipart/form-data");
}
MultipartFile file = ((org.springframework.web.multipart.MultipartHttpServletRequest)request).getFile("file");
if (file==null || file.isEmpty()) return Result.error("文件为空");
// 你们已有 SsrfFileTypeFilter,务必统一调用
org.jeecg.common.util.filter.SsrfFileTypeFilter.checkUploadFileType(file);
// 流式落盘(或 file.transferTo(target))
try (java.io.InputStream in = file.getInputStream();
java.io.OutputStream out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(target))) {
byte[] buf = new byte[8192];
for (int n; (n=in.read(buf))!=-1; ) out.write(buf,0,n);
}
5.7 统一异常与状态码(简版)
java
@RestControllerAdvice
public class GlobalEx {
@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<?> badReq(org.springframework.web.bind.MethodArgumentNotValidException e){
java.util.List<String> msgs = new java.util.ArrayList<>();
e.getBindingResult().getFieldErrors().forEach(er -> msgs.add(er.getField()+": "+er.getDefaultMessage()));
return Result.error("参数校验失败: " + String.join("; ", msgs));
}
@ExceptionHandler(org.springframework.web.multipart.MaxUploadSizeExceededException.class)
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
public Result<?> tooLarge(){ return Result.error("上传文件过大"); }
}
6. 性能与可维护性(从"单点优化"到"系统性治理")
- 分页限幅 :后端统一限制
pageSize<=1000
,异常值回落默认。 - 索引与 SQL:每个模糊查询列评估索引;多条件组合下关注执行计划(MySQL 8 慢 SQL 日志务必开启)。
- 缓存策略:字典/常量/配置放 Redis,有效期与更新策略明确;热点 key 做本地二级缓存。
- 异步化:大导出、消息通知、耗时计算放入队列;导出按分页流式写入,避免卡接口。
- 连接池与线程池:压测后调优;给线程池"命名 + 指标暴露"。
- 读写分离/分库分表 :增长到一定量级后再考虑;先做好 观察 → 评估 → 演进。
7. 前端协作心得(Ant Design Vue)
- 表单校验 :与后端
@Validated
一致;对手机号、邮箱、金额等用统一校验器。 - 表格:大列表开启虚拟滚动/列宽控制;分页与后端同步。
- 字典组件:约定 value/label/disabled 字段格式;前后端一个口径。
- 弹窗组件 :注意新 API(如
v-model:open
);生成器模板与组件版本保持同步。 - 跨端:如需 H5/小程序,尽早抽离"服务接口层"与"UI 层",减少耦合。
8. DevOps 与可观测性
- CI/质量门禁:SonarQube(Bug/Vuln/Code Smell 阈值)、SpotBugs、Checkstyle/Spotless;
- 依赖安全:OWASP Dependency-Check/Snyk;
- 数据库迁移:Flyway/Liquibase 管理脚本版本;
- 日志:Logback JSON + TraceId/SpanId;
- 指标:Micrometer + Prometheus + Grafana(QPS、P95、错误率、线程池、JVM、连接池);
- 链路:OpenTelemetry 分析慢链路与异常。
9. 测试策略(最低配也要有)
- 单元测试:Service/Utils;
- 集成测试:Testcontainers 起 MySQL/Redis,覆盖核心查询/事务;
- E2E:关键生成页面用 Playwright/Cypress 跑"查询/新增/编辑/导出"主链路;
- 回归用例:LIKE 转义、排序白名单、导出公式、路径/SSRF 等安全基线用例常驻。
10. 常见坑排雷
- Long 精度丢失(前端):ID 用字符串传输;前端 json 解析用 bigInt 方案或统一转 string。
- 时间与时区 :后端存 UTC,前端按时区展示;统一
yyyy-MM-dd HH:mm:ss
。 - 逻辑删除 :MyBatis-Plus
@TableLogic
与数据库唯一键配合(避免"软删后唯一冲突")。 - 事务边界:主从表写入建议一事务;跨服务用消息保证最终一致性。
- 乐观锁 :大并发更新场景使用
@Version
,冲突重试或提示。 - 导出大文件:务必异步化 + 流式;避免一次性读入内存。
11. 生产上线检查清单
- 代码生成模板已固化:分页限幅 / 排序白名单 / LIKE 转义 / 导出防注入 / 上传魔数
- 转发/下载口:SSRF 防护 / 路径规范化 + 软链接防护 / Content-Disposition 安全编码
- CAS/外部调用:系统信任库 + 主机名校验 + 连接/读取超时
- 日志脱敏:password/token/Authorization/手机号 等统一过滤
- 大导出:异步任务 + 分页流式;前端可查看任务状态
- 质量门禁:Sonar/依赖安全扫描通过;Checkstyle/Spotless 已启用
- 测试:关键路径单测/集成/E2E 覆盖
- 可观测性:QPS、P95、错误率、线程池、JVM、连接池指标可见;Trace 打通
- 数据权限/多租户:拦截器注入范围过滤,导出/下载同样受控
12. 总结
JeecgBoot 的价值在于:把低代码效率与工程化可维护结合起来 。想要"跑得稳、跑得久",关键是把 安全、性能、规范、可观测性 这四条"地基",沉到模板与公共库里,让每个新模块 开箱即工程化 。 按照本文的清单逐条落地,你的 JeecgBoot 项目会更好地满足企业级场景对 稳定性、安全性与迭代效率 的双重要求。