分析日期:2026-06-03 框架版本:JeecgBoot 3.9.2 分析范围:JeecgBoot 原框架(排除
jeecg-module-zjrs业务子模块)
jeecg-boot/
├── jeecg-boot-base-core/ # 框架核心基础库
│ └── src/main/java/org/jeecg/
│ ├── common/ # 通用工具类、切面、异常、常量、VO
│ ├── config/ # 全局配置(Shiro/MyBatis/WebMVC/Swagger等)
│ └── modules/base/ # 基础公共服务(日志、消息)
├── jeecg-boot-module/
│ ├── jeecg-module-system/ # 系统管理模块(用户/角色/部门/字典/消息/定时任务等)
│ ├── jeecg-module-demo/ # 示例模块(单表CRUD/主子表/动态数据源/MCP等)
│ └── jeecg-module-zjrs/ # 湛江人社业务模块(本次分析排除)
└── jeecg-server-cloud/ # 微服务Cloud版本相关
| 层级 | 技术 |
|---|---|
| 基础框架 | Spring Boot 2.x + Spring MVC |
| ORM | MyBatis-Plus 3.x |
| 安全认证 | Apache Shiro + JWT |
| 缓存 | Redis(Spring Cache + RedisTemplate) |
| 数据库连接池 | Druid |
| Excel | AutoPoi(基于 Apache POI 封装) |
| 模板引擎 | Freemarker |
| 定时任务 | Quartz |
| API文档 | Swagger3 / OpenAPI 3.0 |
| 对象存储 | MinIO / 阿里云 OSS / 本地存储 |
| 消息推送 | WebSocket + 邮件 + 短信 + 微信 + 钉钉 + 企业微信 |
Controller 层 → 接收请求、参数校验、调用Service、返回Result
│ 继承 JeecgController<T, S>
▼
Service 层 → 业务逻辑、事务管理、调用Mapper
│ 接口 extends IService<T>,实现 extends ServiceImpl<M, T>
▼
Mapper 层 → 数据访问、自定义SQL
│ 继承 BaseMapper<T>,XML 可选
▼
Entity 层 → 数据实体、数据库表映射
继承 JeecgEntity(推荐),加 @TableName、@Excel 等
@Data
@TableName("table_name") // 数据库表名
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true) // 支持链式调用
public class XxxEntity extends JeecgEntity { // 继承基类,自动获得 id/createBy/createTime/updateBy/updateTime
@Excel(name = "字段名", width = 15) // 需要导出/导入的字段加 @Excel
private String fieldName;
@Excel(name = "字典字段", width = 15, dicCode = "dict_code") // 字典翻译
private String dictField;
@Excel(name = "日期字段", width = 20, format = "yyyy-MM-dd HH:mm:ss") // 日期格式
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date dateField;
// 不需要导出/导入的字段不加 @Excel 注解
private String internalField;
}
JeecgEntity 基类提供的能力:
id、createBy、createTime、updateBy、updateTime)MybatisInterceptor 拦截器自动注入 createBy/createTime/sysOrgCode(插入时)和 updateBy/updateTime(更新时)// 标准 Mapper(无自定义 SQL 场景)
public interface XxxMapper extends BaseMapper<XxxEntity> {
// MyBatis-Plus 内置方法即可满足常规 CRUD
}
// 带自定义 SQL 的 Mapper(需要配合 XML)
public interface XxxMapper extends BaseMapper<XxxEntity> {
@Select("SELECT * FROM table WHERE field = #{value}") // 简单 SQL 可用注解
List<XxxEntity> selectByField(String value);
List<XxxEntity> selectByCondition(@Param("param") XxxEntity param); // 复杂 SQL 用 XML
}
// 接口
public interface IXxxService extends IService<XxxEntity> {
// 自定义业务方法
void customBusinessMethod(XxxEntity entity);
}
// 实现
@Service
public class XxxServiceImpl extends ServiceImpl<XxxMapper, XxxEntity>
implements IXxxService {
@Autowired
private XxxMapper xxxMapper;
@Override
@Transactional(rollbackFor = Exception.class) // 涉及多表操作必须加事务
public void customBusinessMethod(XxxEntity entity) {
// 业务逻辑
}
}
@Slf4j
@RestController
@RequestMapping("/模块/实体")
public class XxxController extends JeecgController<XxxEntity, IXxxService> {
// ========== 分页列表查询 ==========
@GetMapping(value = "/list")
@PermissionData(pageComponent = "system/模块/index") // 数据权限
public Result<?> list(XxxEntity entity,
@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<XxxEntity> queryWrapper = QueryGenerator.initQueryWrapper(entity, req.getParameterMap());
Page<XxxEntity> page = new Page<>(pageNo, pageSize);
IPage<XxxEntity> pageList = service.page(page, queryWrapper);
return Result.OK(pageList);
}
// ========== 新增 ==========
@PostMapping(value = "/add")
@AutoLog(value = "新增XXX")
public Result<?> add(@RequestBody XxxEntity entity) {
service.save(entity);
return Result.OK("添加成功!");
}
// ========== 修改 ==========
@PutMapping(value = "/edit")
@AutoLog(value = "编辑XXX", operateType = CommonConstant.OPERATE_TYPE_3)
public Result<?> edit(@RequestBody XxxEntity entity) {
service.updateById(entity);
return Result.OK("更新成功!");
}
// ========== 删除 ==========
@DeleteMapping(value = "/delete")
@AutoLog(value = "删除XXX")
public Result<?> delete(@RequestParam(name="id", required=true) String id) {
service.removeById(id);
return Result.OK("删除成功!");
}
// ========== 批量删除 ==========
@DeleteMapping(value = "/deleteBatch")
public Result<?> deleteBatch(@RequestParam(name="ids", required=true) String ids) {
service.removeByIds(Arrays.asList(ids.split(",")));
return Result.OK("批量删除成功!");
}
// ========== 通过ID查询 ==========
@GetMapping(value = "/queryById")
public Result<?> queryById(@RequestParam(name="id", required=true) String id) {
XxxEntity obj = service.getById(id);
return Result.OK(obj);
}
// ========== Excel导出 ==========
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
return super.exportXls(request, entity, XxxEntity.class, "文件名");
}
// ========== Excel导入 ==========
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, XxxEntity.class);
}
}
QueryWrapper<T> queryWrapper = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());
这是整个 CRUD 体系的核心引擎,实现了前端零配置即可完成复杂查询条件自动构建。
QueryGenerator 通过分析请求参数的值特征,自动推断查询规则:
| 值特征 | 推断规则 | 示例 |
|---|---|---|
>= 值 |
大于等于(GE) | >= 100 |
<= 值 |
小于等于(LE) | <= 100 |
> 值 |
大于(GT) | > 100 |
< 值 |
小于(LT) | < 100 |
*值* |
全模糊 LIKE | *张* → %张% |
*值 |
左模糊 LIKE | *张 → %张 |
值* |
右模糊 LIKE | 张* → 张% |
值1,值2 |
IN 查询 | 1,2,3 → IN (1,2,3) |
!值 |
不等于(NE) | !admin |
| 普通值 | 等于(EQ) | admin |
通过 字段_begin 和 字段_end 参数实现:
?createTime_begin=2026-01-01&createTime_end=2026-12-31
通过 superQueryParams 参数传入 JSON 格式的复杂查询条件,支持 AND/OR 组合。
优先级从高到低:
sortInfoString — 多字段排序(最高优先级)defSortString — 默认排序column + order — 单字段排序QueryGenerator 在构建查询条件时,会自动从 Request 的 Attribute 中读取由 PermissionDataAspect 注入的数据权限规则,拼接到
SQL 条件中,实现行级数据过滤。
| 属性 | 说明 | 示例 |
|---|---|---|
name |
列标题(必填) | @Excel(name = "姓名") |
width |
列宽 | @Excel(width = 15) |
format |
日期格式 | @Excel(format = "yyyy-MM-dd HH:mm:ss") |
dicCode |
字典编码(自动翻译) | @Excel(dicCode = "sex") |
dictTable |
字典表名(表字典) | @Excel(dictTable = "sys_depart") |
dicText |
字典文本字段 | @Excel(dicText = "depart_name") |
dicCode |
字典编码字段 | @Excel(dicCode = "id") |
type |
字段类型 | 1=文本(默认), 2=图片, 4=数值 |
单表导出(直接调用父类方法):
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
return super.exportXls(request, entity, XxxEntity.class, "文件名");
}
父类 JeecgController.exportXls 执行流程:
Step 1: QueryGenerator.initQueryWrapper() 组装查询条件
Step 2: selections 参数过滤选中数据
Step 3: service.list(queryWrapper) 获取导出数据
Step 4: 构建 ModelAndView(JeecgEntityExcelView)
├── FILE_NAME → 文件名
├── CLASS → 实体类(含 @Excel 注解定义列结构)
├── PARAMS → ExportParams(标题、Sheet名、格式)
├── DATA_LIST → 导出数据列表
└── EXPORT_FIELDS → 可选,指定导出列
导出变体:
super.exportXlsSheet(request, entity, Class, "文件名", exportFields, sheetSize)super.exportXlsForBigData(request, entity, Class, "文件名", pageSize)主子表导出(手动实现):
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, XxxEntity entity) {
QueryWrapper<XxxEntity> qw = QueryGenerator.initQueryWrapper(entity, request.getParameterMap());
List<XxxVo> pageList = new ArrayList<>();
List<XxxEntity> mainList = service.list(qw);
for (XxxEntity main : mainList) {
XxxVo vo = new XxxVo();
BeanUtils.copyProperties(main, vo);
vo.setSubList(subService.selectByMainId(main.getId())); // 查询子表
pageList.add(vo);
}
ModelAndView mv = new ModelAndView(new JeecgEntityExcelView());
mv.addObject(NormalExcelConstants.FILE_NAME, "文件名");
mv.addObject(NormalExcelConstants.CLASS, XxxVo.class); // 使用 VO(含 @ExcelCollection)
mv.addObject(NormalExcelConstants.PARAMS, new ExportParams("标题", "导出人:xxx", "Sheet名"));
mv.addObject(NormalExcelConstants.DATA_LIST, pageList);
return mv;
}
单表导入(直接调用父类方法):
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, XxxEntity.class);
}
带校验和错误汇总的导入:
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Map<String, MultipartFile> fileMap = multipartRequest.getFileMap();
for (Map.Entry<String, MultipartFile> entity : fileMap.entrySet()) {
MultipartFile file = entity.getValue();
ImportParams params = new ImportParams();
params.setTitleRows(2);
params.setHeadRows(1);
params.setNeedSave(true);
try {
List<XxxEntity> list = ExcelImportUtil.importExcel(file.getInputStream(), XxxEntity.class, params);
List<String> errorMessage = new ArrayList<>();
int successLines = 0, errorLines = 0;
for (int i = 0; i < list.size(); i++) {
try {
service.save(list.get(i));
successLines++;
} catch (Exception e) {
errorLines++;
errorMessage.add("第 " + (i + 1) + " 行:" + e.getMessage());
}
}
return ImportExcelUtil.imporReturnRes(errorLines, successLines, errorMessage);
} catch (Exception e) {
return Result.error("文件导入失败:" + e.getMessage());
}
}
return Result.error("文件导入失败!");
}
JeecgBoot 没有独立的"下载导入模板"接口。当前端请求 exportXls 接口但不带查询条件时,查询结果为空列表,AutoPoi
仍生成包含标题行和表头行的 Excel 文件——这就是"导入模板"。
┌──────────────────────────────────────────────────┐
│ 第1行: 大标题(ExportParams.title) │ ← titleRows=2
├──────────────────────────────────────────────────┤
│ 第2行: 副标题(ExportParams.secondTitle) │
├──────┬──────┬──────┬──────┬──────────────────────┤
│ 列1 │ 列2 │ 列3 │ 列4 │ ... │ ← headRows=1
├──────┼──────┼──────┼──────┼──────────────────────┤
│ (空) │ (空) │ (空) │ (空) │ ... │ ← 数据行为空
└──────┴──────┴──────┴──────┴──────────────────────┘
@ExcelCollection 注解)@Data
@TableName("sub_table")
public class SubEntity implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private String id;
private String name;
/** 外键 — 关联主表ID */
private String mainId; // 命名规范: {主表实体名小写}Id
}
@Data
public class MainEntityPage {
private String id;
@Excel(name = "主表字段", width = 15)
private String field;
@ExcelCollection(name = "子表") // AutoPoi 注解,Excel 导入导出时识别
private List<SubEntity> subEntityList;
}
public interface SubEntityMapper extends BaseMapper<SubEntity> {
/** 通过主表外键批量删除 */
@Delete("DELETE FROM sub_table WHERE main_id = #{mainId}")
boolean deleteByMainId(String mainId);
/** 通过主表外键查询 */
@Select("SELECT * FROM sub_table WHERE main_id = #{mainId}")
List<SubEntity> selectByMainId(String mainId);
}
// 新增:先插主表,再循环插子表,设置外键
@Override
@Transactional(rollbackFor = Exception.class)
public void saveMain(MainEntity main, List<SubEntity> subList) {
mainMapper.insert(main);
if (subList != null) {
for (SubEntity sub : subList) {
sub.setMainId(main.getId()); // 设置外键
subMapper.insert(sub);
}
}
}
// 修改策略一(先删后插)
@Override
@Transactional(rollbackFor = Exception.class)
public void updateMain(MainEntity main, List<SubEntity> subList) {
mainMapper.updateById(main);
subMapper.deleteByMainId(main.getId()); // 删除旧子表数据
if (subList != null) {
for (SubEntity sub : subList) {
sub.setMainId(main.getId());
subMapper.insert(sub); // 重新插入
}
}
}
// 修改策略二(差异对比——推荐):前端传完整子表列表,后端对比新增/更新/删除
@Override
@Transactional(rollbackFor = Exception.class)
public void updateCopyMain(MainEntity main, List<SubEntity> subList) {
mainMapper.updateById(main);
for (SubEntity sub : subList) {
SubEntity dbItem = subMapper.selectById(sub.getId());
if (dbItem == null) {
sub.setMainId(main.getId());
subMapper.insert(sub); // 数据库不存在 → 新增
} else {
subMapper.updateById(sub); // 数据库存在 → 更新
}
}
// 取差集:数据库有但前端没传的 → 删除
List<SubEntity> dbList = subMapper.selectByMainId(main.getId());
List<String> submittedIds = subList.stream().map(SubEntity::getId).collect(Collectors.toList());
dbList.stream()
.filter(item -> !submittedIds.contains(item.getId()))
.forEach(item -> subMapper.deleteById(item.getId()));
}
// 删除:先删子表,再删主表
@Override
@Transactional(rollbackFor = Exception.class)
public void delMain(String id) {
subMapper.deleteByMainId(id);
mainMapper.deleteById(id);
}
// 新增(接收 VO,拆分为主表实体和子表列表)
@PostMapping(value = "/add")
public Result<?> add(@RequestBody MainEntityPage vo) {
MainEntity main = new MainEntity();
BeanUtils.copyProperties(vo, main);
service.saveMain(main, vo.getSubEntityList());
return Result.ok("添加成功!");
}
// 查询子表(每个子表一个独立接口)
@GetMapping(value = "/querySubListByMainId")
public Result<?> querySubListByMainId(@RequestParam(name="id", required=true) String id) {
List<SubEntity> list = subService.selectByMainId(id);
return Result.ok(list);
}
| 对比维度 | 单表 CRUD | 主子表 CRUD |
|---|---|---|
| Controller入参 | @RequestBody Entity |
@RequestBody EntityPage(VO对象) |
| 新增方法 | service.save(entity) |
service.saveMain(main, subList) |
| 修改方法 | service.updateById(entity) |
service.updateMain() 或 service.updateCopyMain() |
| 删除方法 | service.removeById(id) |
service.delMain(id) |
| 查询子表 | 无 | 每个子表一个独立查询接口 |
| Service事务 | 框架默认 | 显式 @Transactional(rollbackFor = Exception.class) |
| 导入 headRows | 1 | 2(主子表表头占2行) |
JeecgBoot 提供 6 种主子表生成模板风格(位于 jeecg-system-biz/src/main/resources/jeecg/code-template-online/):
| 风格 | 路径 | 说明 |
|---|---|---|
| default | default/onetomany/ |
默认经典风格 |
| tab | tab/onetomany/ |
Tab页签风格 |
| jvxe | jvxe/onetomany/ |
JVxe行编辑风格 |
| inner-table | inner-table/onetomany/ |
内嵌表格风格 |
| erp | erp/onetomany/ |
ERP风格 |
RuntimeException
├── JeecgBootException (核心业务异常, errCode=500)
│ └── JeecgBootAssertException (断言异常, 由 AssertUtils 使用)
├── JeecgBootBizTipException (业务提醒异常, errCode=500, 不记系统日志)
├── JeecgBoot401Exception (认证/鉴权异常)
└── JeecgSqlInjectionException (SQL注入防护异常)
| 维度 | JeecgBootException | JeecgBootBizTipException |
|---|---|---|
| 语义 | 系统级错误、不可恢复的业务错误 | 业务操作提醒、可预期的用户提示 |
| 记录系统日志 | 是(写入 sys_log 表) |
否(仅 log.error 打印) |
| 适用场景 | 数据不存在、管理员保护、验证码失效 | 密码错误、权限不足提醒、操作冲突 |
| 典型示例 | "admin用户,不允许删除!"、"token非法" |
"旧密码输入错误"、"您已是该租户成员" |
方式一:直接抛出 JeecgBootException(最常用,用于系统级错误)
throw new JeecgBootException("操作数据不存在!");
throw new JeecgBootException("admin用户,不允许删除!");
throw new JeecgBootException("短信接口请求太多,请稍后再试!", CommonConstant.PHONE_SMS_FAIL_CODE);
throw new JeecgBootException("获取钉钉用户信息失败", cause);
方式二:抛出 JeecgBootBizTipException(业务提醒,不记系统日志)
throw new JeecgBootBizTipException("旧密码输入错误!");
throw new JeecgBootBizTipException("您已是该租户成员");
方式三:使用 AssertUtils 断言(声明式校验)
AssertUtils.assertNotEmpty("姓名不能为空", name);
AssertUtils.assertNotEmpty("用户ID不能为空", userId);
AssertUtils.assertTrue("操作数据不存在", data != null);
AssertUtils.assertEmpty("该记录已存在", existingRecord);
AssertUtils.assertEquals("验证码不匹配", expectedCode, actualCode);
AssertUtils.assertGt("数量必须大于0", quantity, 0);
AssertUtils.assertIn("非法状态", status, "ACTIVE", "PENDING");
方式四:直接返回 Result.error()(仅 Controller 层使用)
if (obj == null) {
return Result.error("未找到对应数据");
}
使用 @RestControllerAdvice 统一拦截所有 Controller 层异常,转换为 Result<?> 返回:
| 异常类型 | 返回code | 记录系统日志 |
|---|---|---|
JeecgBootException |
自定义errCode(默认500) | 是 |
JeecgBootBizTipException |
自定义errCode(默认500) | 否 |
JeecgBoot401Exception |
401 | 是 |
JeecgSqlInjectionException |
500 | 视情况 |
MethodArgumentNotValidException |
500 | 是 |
DuplicateKeyException |
500 | 是 |
DataIntegrityViolationException |
500 | 是 |
UnauthorizedException |
510 | 否 |
MaxUploadSizeExceededException |
500 | 是 |
Exception(兜底) |
500 | 是 |
JeecgBootExceptionJeecgBootBizTipExceptionAssertUtils 或 JeecgBootBizTipExceptionRuntimeException,必须使用框架异常类最优实践是三种方式混合使用:
@ValidatedAssertUtils 或直接抛异常SqlInjectionUtil + SsrfFileTypeFilter// 实体类
@Data
@TableName("xxx")
public class XxxEntity extends JeecgEntity {
@NotBlank(message = "姓名不能为空")
@Size(max = 50, message = "姓名长度不能超过50")
@Excel(name = "姓名", width = 15)
private String name;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Excel(name = "手机号", width = 15)
private String phone;
}
// Controller
@PostMapping(value = "/add")
public Result<?> add(@Validated @RequestBody XxxEntity entity) {
service.save(entity);
return Result.OK("添加成功!");
}
@Override
public void saveEntity(XxxEntity entity) {
AssertUtils.assertNotEmpty("姓名不能为空", entity.getName());
// 业务规则校验
long count = this.count(new LambdaQueryWrapper<XxxEntity>()
.eq(XxxEntity::getCode, entity.getCode()));
AssertUtils.assertTrue("编码已存在", count == 0);
this.save(entity);
}
框架内置了三层防护:
| 工具类 | 功能 |
|---|---|
SqlInjectionUtil |
SQL关键词/正则检测、表名/字段名/排序字段合法性校验 |
AbstractQueryBlackListHandler |
查询表/字段黑名单(如 sys_user.password 禁止查询) |
SsrfFileTypeFilter |
上传文件类型白名单 + 文件头黑名单检测 |
所有工具类位于 jeecg-boot-base-core/src/main/java/org/jeecg/common/util/。
| 工具类 | 功能 | 常用方法 |
|---|---|---|
oConvertUtils |
通用转换工具 | getString()、getInt()、isEmpty()、randomGen()、copyNonNullFields()、listIsNotEmpty() |
SpringContextUtils |
Spring上下文 | getApplicationContext()、getBean()、getHttpServletRequest() |
TokenUtils |
Token工具 | getTokenByRequest()、verifyToken() |
PasswordUtil |
密码加密 | encrypt(username, password, salt) |
SqlInjectionUtil |
SQL注入防护 | filterContent()、checkSqlInject() |
QueryGenerator |
查询条件生成 | initQueryWrapper(entity, paramMap) |
ImportExcelUtil |
导入结果封装 | imporReturnRes(errorLines, successLines, errorMessages) |
IPUtils |
IP获取 | getIpAddr() |
DateUtils |
日期处理 | parseDate()、formatDate() |
FillRuleUtil |
编码规则生成 | 自动生成订单号、部门编码等 |
YouBianCodeUtil |
流水号生成 | getNextYouBianCode() |
UUIDGenerator |
UUID生成 | generate() |
RestUtil |
HTTP调用 | get()、post()、put()、delete() |
CommonUtils |
文件上传路由 | upload(file, bizPath, uploadType) |
| 工具类 | 位置 | 功能 |
|---|---|---|
AesEncryptUtil |
util/encryption/ |
AES加解密(CBC+PKCS5Padding) |
SecurityTools |
util/security/ |
RSA/AES加解密、签名验证 |
Md5Util |
util/ |
MD5加密 |
SsrfFileTypeFilter |
util/filter/ |
文件上传安全校验 |
| 工具类 | 功能 |
|---|---|
MinioUtil |
MinIO 对象存储(上传/下载/删除/预签名URL) |
OssBootUtil |
阿里云 OSS(上传/下载/删除/预签名URL) |
CommonUtils |
上传路由(根据配置自动选择 local/minio/alioss) |
| 工具类 | 功能 |
|---|---|
DynamicDBUtil |
动态数据源下通过 Spring JDBC Template 直接操作数据库 |
DataSourceCachePool |
动态数据源缓存池 |
FreemarkerParseFactory |
Freemarker模板解析工厂(用于代码生成和SQL模板渲染) |
所有切面位于 jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/。
| 切面类 | 拦截注解 | 功能 |
|---|---|---|
AutoLogAspect |
@AutoLog |
自动记录操作日志到 sys_log 表(含方法名、参数、耗时、IP) |
DictAspect |
@Dict / @AutoDict |
在 Controller 返回前自动将字典code翻译为文本(如 sex: 1 → sex_dictText: "男") |
PermissionDataAspect |
@PermissionData |
查询数据权限规则并注入 Request Attribute,供 MyBatis 拦截器组装 SQL 条件 |
DynamicTableAspect |
@DynamicTable |
运行时动态切换表名 |
最佳实践:在实体类字段上标注 @Dict,Controller 方法上标注 @AutoDict,框架会自动在下发 JSON 时追加 _dictText
后缀的翻译字段。
// 实体类
@Dict(dicCode = "sex")
@Excel(name = "性别", width = 15, dicCode = "sex")
private Integer sex;
@Dict(dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
@Excel(name = "部门", dictTable = "sys_depart", dicText = "depart_name", dicCode = "id")
private String departId;
// Controller
@AutoDict // 方法级注解,启用自动字典翻译
@GetMapping(value = "/list")
public Result<?> list(...) { ... }
返回的 JSON 中会自动追加:
{
"sex": 1,
"sex_dictText": "男",
"departId": "xxx",
"departId_dictText": "技术部"
}
@PermissionData(pageComponent = "system/user/index")
@GetMapping(value = "/list")
public Result<?> list(...) { ... }
执行流程:
1. @PermissionData 注解 → PermissionDataAspect 切面拦截
2. 查询当前用户在此菜单组件的 SysPermissionDataRule
3. 将规则写入 Request Attribute
4. QueryGenerator 从 Request 读取规则 → 拼接到 SQL WHERE 条件
| 组件 | 说明 |
|---|---|
| Redis | 底层缓存存储 |
RedisUtil |
Redis 操作工具类(来自 jeecg-boot-common 依赖) |
Spring Cache(@Cacheable/@CacheEvict) |
声明式缓存 |
CacheConstant |
缓存Key常量(jeecg-boot-common 依赖) |
字典缓存(框架已内置):
// 字典查询自动缓存
@Cacheable(value = CacheConstant.SYS_DICT_TABLE_CACHE)
public List<DictModel> queryDictItemsByCode(String code) { ... }
// 字典变更时清除缓存
@CacheEvict(value = {CacheConstant.SYS_DICT_CACHE, CacheConstant.SYS_ENABLE_DICT_CACHE}, allEntries = true)
public Result<?> delete(@RequestParam String id) { ... }
用户缓存(框架已内置):
// 用户变更时清除用户缓存
@CacheEvict(value = {CacheConstant.SYS_USERS_CACHE}, allEntries = true)
public void updateUser(...) { ... }
自定义业务缓存示例:
@Cacheable(value = "myBusinessCache", key = "#id")
public XxxEntity getFromDb(String id) {
return mapper.selectById(id);
}
@CacheEvict(value = "myBusinessCache", key = "#entity.id")
public void updateEntity(XxxEntity entity) {
mapper.updateById(entity);
}
DictAspect 在字典翻译时直接使用 RedisTemplate:
sys:cache:dict::{dictCode}:{value}sys:cache:dictTable::SimpleKey [{table},{text},{code},{value}]请求到达 → JwtFilter(校验Token)→ ShiroRealm(认证+授权)→ Controller
核心文件清单:
| 文件 | 功能 |
|---|---|
ShiroConfig.java |
Shiro核心配置,注册过滤器链、SecurityManager、Redis缓存 |
ShiroRealm.java |
认证授权Realm(JWT解析、角色/权限查询) |
JwtFilter.java |
JWT过滤器(Token校验、跨域处理、租户上下文注入) |
JwtToken.java |
JWT Token封装 |
@IgnoreAuth |
免认证注解(标注在方法上跳过Token校验) |
// 功能权限
@RequiresPermissions("system:user:add")
@PostMapping("/add")
public Result<?> add(@RequestBody SysUser user) { ... }
// 角色权限
@RequiresRoles("admin")
@GetMapping("/adminOnly")
public Result<?> adminOnly() { ... }
// 数据权限
@PermissionData(pageComponent = "system/user/index")
@GetMapping("/list")
public Result<?> list(...) { ... }
// 方式一:@IgnoreAuth 注解(方法级)
@IgnoreAuth
@GetMapping("/publicApi")
public Result<?> publicApi() { ... }
// 方式二:jeecg.shiro.excludeUrls 配置(路径级)
// 在 application.yml 中配置
jeecg:
shiro:
excludeUrls: /sys/common/static/**, /sys/common/upload, ...
业务代码调用 PushMsgUtil.sendMessage(templateCode, params)
↓
Freemarker 渲染模板内容 → 写入 sys_sms 表
↓
Quartz 定时任务 SendMsgJob 扫描未发送消息
↓
根据消息类型分发到不同实现:
├── EmailSendMsgHandle → 邮件
├── SmsSendMsgHandle → 短信(支持阿里云/腾讯云)
├── WxSendMsgHandle → 微信公众号
├── DdSendMsgHandle → 钉钉
├── QywxSendMsgHandle → 企业微信
└── SystemSendMsgHandle → 站内消息(WebSocket推送)
@Autowired
private PushMsgUtil pushMsgUtil;
// 发送消息(模板编码 + 参数)
pushMsgUtil.sendMessage("C2010006", JSON.toJSONString(params));
通过 SysAnnouncementController 管理通告的发布、撤回,SysAnnouncementSend 跟踪用户阅读状态。
// 1. 创建 Job 类,实现 org.quartz.Job 接口
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) {
// 定时任务逻辑
}
}
// 2. 通过系统管理界面或 API 配置
// 实体: QuartzJob(jobClassName + cronExpression + parameter + status)
// 管理界面: /sys/quartzJob
SampleJob — 无参示例SampleParamJob — 带参示例AsyncJob — 异步执行示例SendMsgJob — 消息发送定时扫描前端 POST /sys/common/upload (file + biz)
↓
CommonController.upload()
↓
SsrfFileTypeFilter.checkUploadFileType() // 安全校验
↓
uploadType == "local" ?
YES → uploadLocal() → /opt/upFiles
NO → CommonUtils.upload() → MinioUtil / OssBootUtil
↓
返回 Result(success=true, message=相对路径)
jeecg:
uploadType: local # local / minio / alioss
path:
upload: /opt/upFiles # 本地上传根目录
webapp: /opt/webapp # webapp路径
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 10MB
| 检测项 | 方式 |
|---|---|
| 文件后缀白名单 | jpg, jpeg, png, gif, bmp, pdf, doc, docx, xls, xlsx, zip, rar 等30+ |
| 文件头黑名单 | jsp, php, class, sql(检测文件魔数) |
| 路径遍历防护 | 过滤 .. 字符、限制深度5层 |
| 下载类型校验 | checkDownloadFileType() |
GET /sys/common/static/{相对路径}anon(匿名访问)模板位于 jeecg-system-biz/src/main/resources/jeecg/code-template-online/:
| 类型 | 风格 | 模板路径 | 支持前端 |
|---|---|---|---|
| 单表 | 经典 | default/one/ |
vue2, vue3, vue3Native, uniapp |
| 一对多 | 经典 | default/onetomany/ |
vue2 |
| 一对多 | JVxe | jvxe/onetomany/ |
vue3, vue3Native |
| 一对多 | ERP | erp/onetomany/ |
vue3, vue3Native |
| 一对多 | 内嵌子表 | inner-table/onetomany/ |
vue3 |
| 一对多 | Tab | tab/onetomany/ |
vue3 |
| 树形 | 默认 | default/tree/ |
vue3, vue3Native |
每个模板生成的文件:
通过 /online/cgform/** 接口提供零代码开发能力,核心路由:
| URL | 功能 |
|---|---|
/online/cgform/api/getData/{code} |
获取表单数据 |
/online/cgform/api/getTreeData/{code} |
获取树形数据 |
/online/cgform/api/addAll |
新增配置 |
/online/cgform/api/editAll |
编辑配置 |
/online/cgform/head/enhanceJs/** |
JS增强 |
/online/cgform/head/enhanceJava/** |
Java增强回调 |
/online/cgreport/** |
Online报表 |
/online/graphreport/** |
Online图表 |
/bigscreen/** |
大屏配置 |
/drag/** |
仪表盘配置 |
基于以上分析,在 JeecgBoot 框架中开发一个带完整 CRUD + 导入导出 的新业务模块,所需步骤:
JeecgEntity,加 @TableName、@Excel、@Dict 注解BaseMapper<T>IService<T>,实现继承 ServiceImpl<Mapper, T>JeecgController<T, IService<T>>,实现6个CRUD方法 + 2个导入导出方法无需额外开发的:
QueryGenerator 已实现MybatisInterceptor 已实现DictAspect + @Dict 已实现AutoLogAspect + @AutoLog 已实现JeecgEntityExcelView 已实现ExcelImportUtil 已实现exportXls 接口JeecgEntity,加 @TableName、@TableId@ExcelCollection 标注的子表列表字段BaseMapper<T>BaseMapper<T>,增加 deleteByMainId + selectByMainIdIService<T> + 实现 ServiceImpl<M, T>saveMain、updateMain、delMain、delBatchMainJeecgController<T, S>,实现CRUD + 子表查询 + 导入导出| 组件 | 文件 |
|---|---|
| Controller基类 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/controller/JeecgController.java |
| Entity基类 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/entity/JeecgEntity.java |
| Service基类接口 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/base/service/JeecgService.java |
| 查询生成器 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/query/QueryGenerator.java |
| 统一返回结果 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/api/vo/Result.java |
| 全局异常处理器 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootExceptionHandler.java |
| 核心业务异常 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootException.java |
| 业务提醒异常 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/exception/JeecgBootBizTipException.java |
| 断言工具类 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/AssertUtils.java |
| 导入工具类 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/ImportExcelUtil.java |
| 通用工具类 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/oConvertUtils.java |
| SQL注入防护 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/SqlInjectionUtil.java |
| 文件安全过滤器 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/filter/SsrfFileTypeFilter.java |
| 全局常量 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java |
| 组件 | 文件 |
|---|---|
| 日志切面 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/AutoLogAspect.java |
| 字典翻译切面 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/DictAspect.java |
| 数据权限切面 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/PermissionDataAspect.java |
| @AutoLog注解 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/AutoLog.java |
| @Dict注解 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/Dict.java |
| @AutoDict注解 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/AutoDict.java |
| @PermissionData注解 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/common/aspect/annotation/PermissionData.java |
| @IgnoreAuth注解 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/IgnoreAuth.java |
| 组件 | 文件 |
|---|---|
| Shiro配置 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java |
| ShiroRealm | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java |
| JWT过滤器 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/filters/JwtFilter.java |
| MVC配置 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebMvcConfiguration.java |
| MyBatis拦截器 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisInterceptor.java |
| 核心配置加载 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/JeecgBaseConfig.java |
| AutoPoi配置 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/AutoPoiConfig.java |
| Swagger3配置 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java |
| 多租户配置 | jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/mybatis/MybatisPlusSaasConfig.java |
| 组件 | 文件 |
|---|---|
| 用户管理Controller | jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java |
| 字典管理Controller | jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java |
| 角色管理Controller | jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysRoleController.java |
| 通用上传Controller | jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/CommonController.java |
| 定时任务Controller | jeecg-boot/jeecg-boot-module/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/quartz/controller/QuartzJobController.java |
| 组件 | 文件 |
|---|---|
| 单表CRUD Controller | jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgDemoController.java |
| 主子表Controller | jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/controller/JeecgOrderMainController.java |
| 主子表Service实现 | jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/service/impl/JeecgOrderMainServiceImpl.java |
| 主子表VO | jeecg-boot/jeecg-boot-module/jeecg-module-demo/src/main/java/org/jeecg/modules/demo/test/vo/JeecgOrderMainPage.java |
| 文件 | 操作 | 原因 |
|---|---|---|
.docs/260603-JeecgBoot后端框架全局开发规范与最佳实践.md |
新增 | 重新分析框架,撰写全面开发规范文档 |