以 HTTP 请求生命周期为主线,从统一契约、安全认证、持久化增强、横切能力到整体架构贯通,逐步掌握 JeecgBoot 公共基础层的设计方法。

在开始读任何源码前,先搞清楚"这项目有多少文件、依赖谁、谁继承谁"。

Common



common -api
"一个接口、两种运行方式" ------ 业务代码永远只写 @Autowired CommonAPI commonApi; commonApi.queryUserRoles(...) ,不需要知道是本地调用还是远程调用。
base-core 模块不依赖任何具体实现 ------ 它只定义契约,实现交给 system-biz。
切换架构(单体 ↔ 微服务)时,业务代码零改动 ------ 由 @ConditionalOnMissingClass 控制 Bean 选择。
这个包里放的是 "任何模块都有可能用到的、最底层的系统能力契约" ------只定义接口和传输对象, 完全不写实现 。真实实现放到 jeecg-system-biz 的 SysBaseApiImpl 里。

/**
* 通用api
* @author: jeecg-boot
*/
public interface CommonAPI {
}
记录API 继承关系
微服务模式
Set<String> queryUserRoles(String username);
\jeecg-module-system\jeecg-system-api
java@Component @FeignClient(contextId = "sysBaseRemoteApi", value = ServiceNameConstants.SERVICE_SYSTEM, fallbackFactory = SysBaseAPIFallbackFactory.class) @ConditionalOnMissingClass("org.jeecg.modules.system.service.impl.SysBaseApiImpl") public interface ISysBaseAPI extends CommonAPI远程调用
\jeecg-boot\jeecg-module-system\jeecg-system-api\jeecg-system-cloud-api\src\main\java\org\jeecg\common\system\api\ISysBaseAPI.java
| 组件 / 参数 | 示例 | 作用 | 核心理解 | 常见场景 |
|---|---|---|---|---|
value / name |
ServiceNameConstants.SERVICE_SYSTEM |
指定远程服务名(服务ID) | 通过服务注册中心找到目标服务 | Nacos / Eureka 服务调用 |
contextId |
"sysBaseRemoteApi" |
Feign Bean 唯一标识 | 防止多个 FeignClient 冲突 | 同一服务多个接口拆分 |
fallback |
SysBaseFallback.class |
降级实现类 | 失败时返回固定逻辑 | 简单兜底返回 |
fallbackFactory |
SysBaseAPIFallbackFactory.class |
降级工厂(可拿异常) | 可以获取失败原因并动态处理 | Sentinel / 精细化降级 |
| 模块 | 说明 | 本质 |
|---|---|---|
| 动态代理 | Feign 接口运行时生成实现类 | JDK Proxy |
| HTTP调用 | 接口方法 → HTTP请求 | REST 调用封装 |
| 序列化 | Java对象 ↔ JSON | Jackson / Gson |
| 负载均衡 | 多实例自动选择 | Spring Cloud LoadBalancer |
| 服务发现 | 根据服务名找地址 | Nacos / EurF |
| 步骤 | 过程 |
|---|---|
| 1 | 调用 Feign 接口方法 |
| 2 | 生成 HTTP 请求 |
| 3 | 通过服务名查找实例 |
| 4 | 负载均衡选择节点 |
| 5 | 发起 HTTP 请求 |
| 6 | 返回 JSON |
| 7 | 反序列化为 Java 对象 |
| 问题 | 结论 | 原因 |
|---|---|---|
| Feign 调用是否需要 Controller? | 必须需要 | Feign 调用的是 HTTP 接口 |
| 可以直接调用 Service 吗? | 不可以 | Feign 不支持 JVM 内方法调用 |
| Controller作用 | 提供 HTTP 入口 | 对外暴露 API |
@FeignClient :Spring Cloud OpenFeign 会在启动时为这个接口生成 动态代理实例
@GetMapping / @PostMapping / @RequestParam / @RequestBody :Feign 扫描这些注解,知道"调哪个 URL、怎么传参"
@ConditionalOnMissingClass :如果本地扫描到了 SysBaseApiImpl (单体模式),就 不要 生成这个 Feign 代理
按照常理应该去controoler找了
java/** * 进入fallback的方法 检查是否token未设置 * @author: jeecg-boot */ @Slf4j public class SysBaseAPIFallback implements ISysBaseAPI { @Override public Set<String> queryUserRoles(String username) { return null; } }
java/** * 服务化 system模块 对外接口请求类 * @author: jeecg-boot */ @Slf4j @RestController @RequestMapping("/sys/api") public class SystemApiController
单体分析
注入为bean 实现
javapublic interface ISysBaseAPI extends CommonAPI再一次封装
关键点 :base-core 只定义 CommonAPI 接口;真正的实现 SysBaseApiImpl 在 jeecg-system-biz 模块里。启动单体工程时,Spring 扫描到 @Service SysBaseApiImpl ,自动把它注入到所有依赖 CommonAPI / ISysBaseAPI 的 Bean 里 ------ 完全在同一个 JVM 内运行,不需要 HTTP。
package org.jeecg.modules.system.service.impl;
java/** * @Description: 底层共通业务API,提供其他独立模块调用 * @Author: scott * @Date:2019-4-20 * @Version:V1.0 */ @Slf4j @Service public class SysBaseApiImpl implements ISysBaseAPI { /** * 查询用户拥有的角色集合 common api 里面的接口实现 * @param username * @return */ @Override public Set<String> queryUserRoles(String username) { return getUserRoleSet(username); } @Override public Set<String> getUserRoleSet(String username) { // 查询用户拥有的角色集合 List<String> roles = sysUserRoleMapper.getRoleByUserName(username); log.debug("-------通过数据库读取用户拥有的角色Rules------username: " + username + ",Roles size: " + (roles == null ? 0 : roles.size())); return new HashSet<>(roles); } }
java/** * @Description: 用户登录鉴权和获取用户授权 * @Author: Scott * @Date: 2019-4-23 8:13 * @Version: 1.1 */ @Component @Slf4j @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class ShiroRealm extends AuthorizingRealm { @Lazy @Resource private CommonAPI commonApi; @Lazy @Resource private RedisUtil redisUtil; }使用
内部包注解注入为bena 直接调用,不走接口,单体服务
总结
| 层级 | 组件 | 所属项目 / 模块 | 文件路径(典型) | 核心职责 | 为什么放在这里(设计原因) |
|---|---|---|---|---|---|
| ① 顶层契约接口 | CommonAPI | jeecg-boot-base-core |
CommonAPI.java |
定义最小公共能力:登录用户、角色、权限、缓存、字典翻译、动态数据源、数据权限、RAG流程等 | base-core 是所有模块的公共依赖。必须"零依赖业务实现",只定义契约,保证任何模块都能直接引用(JVM层可见),避免依赖污染 |
| ② 扩展接口(单体本地版) | ISysBaseAPI(local) | jeecg-module-system / jeecg-system-local-api |
ISysBaseAPI.java |
在 CommonAPI 基础上扩展完整系统能力(约60+方法):用户、部门、角色、权限、岗位、租户、消息、日志等 | 单体架构下 JVM 内调用,不需要 Feign / HTTP。单独拆模块避免 system-biz 被 core 污染,同时提供"本地直调能力" |
| ③ Feign远程接口(微服务版) | ISysBaseAPI(Feign) | jeecg-module-system / jeecg-system-cloud-api |
ISysBaseAPI.java |
在接口上增加 @FeignClient + @GetMapping/@PostMapping,用于远程 HTTP 调用系统服务 |
微服务模式下需要 HTTP 通信,因此单独拆 cloud-api,避免单体误引入 Feign;通过条件装配实现"单体/微服务切换无感" |
| ④ 业务实现层 | SysBaseApiImpl | jeecg-module-system / jeecg-system-biz |
SysBaseApiImpl.java |
真正业务实现:调用 Mapper/Service 查用户、角色、权限、字典等数据库数据 | system-biz 拥有完整业务与持久层依赖(Mapper/Service)。实现必须集中在这里,避免逻辑分散,保证事务与数据一致性 |
| ⑤ HTTP暴露层(Controller) | SystemApiController | jeecg-module-system / jeecg-system-biz |
SystemApiController.java |
将系统能力通过 REST API 暴露(/sys/api/**),供 Feign 或前端调用 |
在微服务模式下作为"服务入口"。在单体模式下也保留,便于接口统一与未来平滑迁移(Feign路径与Controller路径对齐) |
| 设计点 | 说明 |
|---|---|
| 接口分层 | CommonAPI(最小能力)→ ISysBaseAPI(扩展能力) |
| 双模式兼容 | 同一套接口支持 单体(JVM) + 微服务(Feign) |
| 低侵入切换 | 通过条件装配(ConditionalOnMissingClass)实现无缝切换 |
| 依赖下沉 | base-core只定义契约,不依赖实现 |
| 实现集中 | system-biz统一处理数据库逻辑 |
| 路径对齐 | Feign mapping ≈ Controller mapping(便于迁移) |

aspect
认识



Spring AOP 在 Bean 创建阶段判断是否存在切点匹配的方法,只要有一个方法匹配,就会为整个 Bean 创建代理对象,但只有匹配的那些方法才会执行增强逻辑,其它方法仍然直接执行目标方法。
Spring AOP 并不是对 Bean 多次代理,而是在 Bean 创建阶段将原始对象替换为代理对象,多个切面不会生成多个代理类,而是通过一个代理对象内部维护多个拦截器链来实现增强逻辑的顺序执行。
Spring 容器中存放的 Bean,在经过 BeanPostProcessor(如 AOP、事务、权限等)处理后,如果命中增强条件,会被替换成代理对象;否则就是原始对象。
当一个 Bean 被多个切面命中时,Spring 不会创建多个代理对象,而是只创建一个代理对象,并在内部构建"拦截器链(Interceptor Chain)"来统一执行所有切面逻辑。
这个包里是 JeecgBoot 用 Spring AOP(AspectJ 风格) 实现的 3 个全局横切关注点 + 5 个业务注解 + 1 个路由映射枚举 。
它的核心设计思想: 把「字典翻译、数据权限、操作日志」这些"不是业务但处处要用"的横切能力,做成 AOP 切面 + 注解触发,让业务 Controller / Service 代码里完全看不到这些逻辑 。
| 切面类 | 触发方式(切点) | 实际做了什么(执行流程) | 最终呈现效果(对前端/系统) |
|---|---|---|---|
| DictAspect.java(字典切面) | - @RestController / @Controller 方法- 或 @AutoDict 注解- 返回值必须为 Result- @Around 环绕通知 |
1. pjp.proceed() 先执行业务拿结果2. 解析返回 Result / IPage3. 扫描 records 中实体字段的 @Dict 注解4. 调用 CommonAPI.translateDict / translateDictFromTableByKeys 查询字典值5. 动态追加 xxx_dictText 字段6. Redis 缓存字典数据(默认 1 天)7. @JsonFormat 处理日期格式 |
返回 JSON 自动增强:json<br>{ "sex": "1", "sex_dictText": "男" }<br>前端无需再查字典表,可直接渲染中文 |
| PermissionDataAspect.java(数据权限切面) | - 方法上标注 @PermissionData- @Around 拦截 |
1. 获取 request URI + pageComponent2. 解析 JWT 获取 username3. 调用 CommonAPI.queryPermissionDataRule() 查询权限规则4. 从 sys_permission_data_rule 获取数据权限配置5. 写入 JeecgDataAutorUtils.installDataSearchConditon()6. 注入当前用户上下文7. MyBatis 查询时读取规则自动拼接 SQL 条件 |
实现"自动数据权限过滤":- 只能看本部门数据- 只能看本人数据- 按角色动态过滤查询结果自动被 SQL 限制,无需业务层写条件 |
| AutoLogAspect.java(操作日志切面) | - 方法上标注 @AutoLog- @Around 环绕通知 |
1. 记录开始时间2. 获取方法名 / 注解描述 / 参数 / IP / 用户信息3. 执行业务 pjp.proceed()4. 计算耗时5. 封装 LogDTO(模块类型、操作内容等)6. 调用 baseCommonService.addLog() 写入 sys_log7. Online 表单额外拼接业务日志内容 |
后台"系统日志 → 操作日志"可查看:- 谁操作了什么接口- 请求参数是什么- 耗时多少- 是否成功用于审计 / 排错 / 性能分析 |
| 切面 | 解决的问题 | 核心价值 |
|---|---|---|
| DictAspect | 前端字典转换重复请求 | 后端自动"数据翻译增强" |
| PermissionDataAspect | SQL手写数据权限冗余 | 自动"行级权限控制" |
| AutoLogAspect | 操作记录分散、不可追踪 | 全链路"审计日志自动化" |
Jeecg Boot 通过 AOP 切面在 Controller 层统一实现字典翻译、数据权限控制和操作日志记录,在不侵入业务代码的情况下,对返回结果、SQL 查询和操作行为进行统一增强,从而实现"前端免字典查询、后端自动权限过滤、全链路可审计"的企业级能力。
| 类型 | 名称 | 标记位置 | 核心字段 | 解决的问题 |
|---|---|---|---|---|
| 注解 | @Dict |
Entity 字段(ElementType.FIELD) |
dicCode:字典codedicText:显示字段dictTable:表字典表名ds:数据源 |
标记字段为"字典值字段",供 DictAspect 反射翻译并补充 xxx_dictText |
| 注解 | @AutoDict |
Controller 方法(ElementType.METHOD) |
无业务字段 | 显式开启字典翻译切面;用于非标准 Controller 或第三方接口,强制触发 DictAspect |
| 注解 | @PermissionData |
Controller 类/方法 | pageComponent:前端菜单路由路径 |
绑定数据权限规则(菜单维度),让 PermissionDataAspect 注入 SQL 行级过滤条件 |
| 注解 | @AutoLog |
Service / Controller 方法 | value:操作描述logType:0操作/1登录/2任务operateType:CRUD类型module:模块归类 |
控制是否记录操作日志,并定义日志分类与模块归属 |
| 注解 | @DynamicTable |
Service / Mapper 方法 | value:表名占位符(如 act_ru_task_#{tenantId}) |
支持多租户动态表名替换,供 MyBatis-Plus 拦截器解析 |
| 注解 | @OnlineAuth |
Online 低代码 Controller(类/方法) | value:URL前缀(如 /online/cgform) |
Online 低代码权限控制入口,使生成页面也能走统一权限体系 |
| 枚举 | UrlMatchEnum.java |
工具类(非注解) | CGFORM_DATA``CGFORM_EXCEL_DATA``CGFORM_TREE_DATA``CGREPORT_* |
解决 Online 低代码 URL 与菜单路由不一致问题 |
| 分类 | 代表 | 核心作用 |
|---|---|---|
| 字典增强 | @Dict / @AutoDict |
自动把 code → 中文,增强返回 JSON |
| 权限控制 | @PermissionData / UrlMatchEnum |
实现"菜单级 + 行级数据权限" |
| 日志审计 | @AutoLog |
自动记录操作行为 |
| 多租户能力 | @DynamicTable |
动态表名替换 |
| 低代码体系 | @OnlineAuth |
Online 表单权限统一入口 |
AutoLogAspect
切面认识
作用
|---------------|-------------|--------------|
| AutoLogAspect | 操作记录分散、不可追踪 | 全链路"审计日志自动化" |
方法增强 -注解,切面类匹配注解,匹配方法,进行增强
实际代码
java
@Component
@Aspect
public class AutoLogAspect {
@Resource
private BaseCommonService baseCommonService;
//定义切点变量
@Pointcut("@annotation(org.jeecg.common.aspect.annotation.AutoLog)")
public void logPointCut() {
}
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(point, time, result);
return result;
}
}
@Around("@annotation(org.jeecg.common.aspect.annotation.AutoLog)")
public Object around(){}
多了一层,spring过程
@Pointcut("@annotation(AutoLog)")
public void logPointCut(){}
会建立一个切点定义:
logPointCut
→
@annotation(AutoLog)
@Around("logPointCut()")发现:
Around ↓ 引用 logPointCut ↓ 展开 ↓ @annotation(AutoLog)最后实际还是:
@Around("@annotation(AutoLog)")
为什么设计成这样?
因为一个切点可能被多个通知共享。
@Pointcut("@annotation(AutoLog)")
public void logPointCut(){}
前置通知:
@Before("logPointCut()")
后置通知:
@After("logPointCut()")
流程
先定义注解 @AutoLog
定义数据载体 LogDTO ------ 切面要把「操作描述、方法、参数、IP、用户、耗时、时间...」打包传给 service
实现切面类 AutoLogAspec
日志
java
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
saveSysLog(point, time, result);
return result;
}
用法
java
@AutoLog(value = "用户管理-添加")
@PostMapping("/add")
public Result<String> add(@RequestBody SysUser user) {
sysUserService.saveUser(user);
return Result.OK("添加成功!");
}
java
| 数据 | 来源 |
| ---------------- | ----------------------------------------------- |
| 日志标题(logContent) | `@AutoLog.value()` |
| 日志类型(logType) | `@AutoLog.logType()` |
| 操作类型 | `@AutoLog.operateType()` |
| 方法名 | `joinPoint.getTarget()` + `signature.getName()` |
| 请求参数 | `joinPoint.getArgs()` |
| IP | `HttpServletRequest` |
| 当前用户 | Shiro `SecurityUtils` |
| 执行耗时 | Around通知统计 |
| 返回值 | `joinPoint.proceed()` 返回的 `obj` |
| ONLINE模块日志内容 | `obj + 注解内容` 共同生成 |
业务模块也可以注入 BaseCommonService 自己写日志
java
@Service
public class MyCustomService {
@Resource
private BaseCommonService baseCommonService;
public void doSomething() {
baseCommonService.addLog("后台任务: 每月结账", 2, 3); // 2=操作日志, 3=修改
}
}
它们的结构都是 "定义注解 → 定义 AOP 切点 → 用反射拿注解值 + 参数 + IP + 用户 → 做业务 → 放行方法" ,和 AutoLogAspect 一模一样。
DictAspect
前后端接口返回的列表里通常有「码值字段」------比如 sex=1 代表男、 status=0 代表启用、 departId="A0101" 代表某个部门、 categoryId=23 代表某个分类。
前端拿到这些码值后,要做两件额外的事才能展示给用户:
-
要么前端再调一次接口 查字典 / 查部门表,把 1 翻译成「男」 ------ 额外一次 HTTP 请求;
-
要么前端在表格里写 customRender 手工翻译 ------ 每个字典字段都要写一次。
JeecgBoot 的做法是: 在后端切面里自动完成翻译,返回 JSON 里给每个字典字段追加一个 xxx_dictText ------ 前端直接用,不用再做任何字典翻译代码。
java【切面前】接口返回: { "username": "admin", "sex": 1, "status": 0, "departId": "A0101" } 【切面后】接口自动补齐: { "username": "admin", "sex": 1, "sex_dictText": "男", "status": 0, "status_dictText": "正常", "departId": "A0101", "departId_dictText": "总经理办/研发部" }
幂等切面
找到一个不变的唯一ID,进行重复判断
| 方案 | 业务场景 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|---|
| 前端按钮禁用 | 提交表单、登录 | 点击后按钮置灰 | 简单 | 可绕过 |
| Loading遮罩 | 支付、提交订单 | 请求期间禁止再次点击 | 用户体验好 | 只能防正常用户 |
| Token机制 | 表单提交 | 页面生成唯一Token,提交后失效 | 安全性高 | 实现稍复杂 |
| Redis SETNX | 秒杀、支付、下单 | Redis原子操作防重复 | 分布式友好 | 依赖Redis |
| 数据库唯一索引 | 用户注册、订单号 | 唯一约束 | 最可靠 | 会产生异常 |
| 乐观锁Version | 库存扣减 | version字段控制 | 并发性能高 | 适用更新场景 |
| 状态机控制 | 审批流、订单流 | 状态流转校验 | 业务严谨 | 代码复杂 |
| 分布式锁 | 秒杀、抢券 | Redis/Zookeeper锁 | 控制最严格 | 性能较低 |
| 消息队列去重 | MQ消费 | messageId去重 | 防止重复消费 | 实现复杂 |
| AOP幂等切面 | 通用接口 | 注解+AOP统一处理 | 代码零侵入 | 需结合Redis等实现 |
业务场景举例
- 支付场景
问题
用户连续点击:
支付
支付
支付
结果:
扣款100
扣款100
扣款100
解决方案
方案 实现
Redis SETNX 订单号作为Key
数据库唯一索引 payment_no唯一
支付状态校验 已支付不可再次支付
- 下单场景
问题
网络卡顿:
提交订单
刷新
再次提交
产生:
订单A
订单B
解决方案
方案 实现
Token防重 一次性Token
Redis幂等 userId+商品Id
唯一订单号 数据库约束
- 用户注册
问题
两个请求同时注册:
手机号:
138xxxx
生成两个账号。
解决方案
数据库唯一索引:
ALTER TABLE user
ADD UNIQUE(phone);
- 秒杀场景
问题
100库存:
1000人同时抢
出现:
超卖
解决方案
方案 实现
Redis预扣库存 原子递减
分布式锁 锁商品
乐观锁 version控制
- 积分兑换
问题
连续点击:
兑换
兑换
兑换
结果:
扣300积分
解决方案
用户ID
商品ID
生成唯一Key:
exchange:1001:2001
Redis控制。
- 审批流程
问题
审批人连续点击:
同意
同意
同意
结果:
重复审批
解决方案
状态机:
待审批
↓
已通过
再次提交:
已通过
直接拒绝。
| 业务 | 推荐指数 |
|---|---|
| 登录 | ★★★★★ |
| 提交表单 | ★★★★★ |
| 新增数据 | ★★★★★ |
| 保存草稿 | ★★★★★ |
| 发起审批 | ★★★★★ |
| 支付 | ★★★★ |
| 秒杀 | ★★★ |
| 库存扣减 | ★★ |
| MQ消费 | ★ |
| 业务 | 实际方案 |
|---|---|
| 登录 | AOP + Redis |
| 提交表单 | AOP + Redis |
| 用户注册 | 唯一索引 |
| 支付 | Redis + 状态机 |
| 秒杀 | Redis + Lua + MQ |
| 库存扣减 | 乐观锁 |
| 审批流 | 状态机 |
| MQ消费 | messageId去重 |
| 问题 | 回答 |
|---|---|
| 为什么用幂等? | 防止重复请求导致数据异常 |
| 为什么用AOP? | 统一处理,业务代码无侵入 |
| AOP如何实现? | 自定义注解 + Around通知 + Redis SETNX |
| Key怎么设计? | 用户ID + URL + 参数 |
| Redis为什么可以? | SETNX是原子操作 |
| 生产环境只靠AOP够吗? | 不够,通常还要结合数据库唯一索引、状态机等保证最终一致性 |
SETNX(SET if Not eXists)是 Redis 提供的一个原子性命令:
当且仅当 key 不存在时,才设置成功;如果 key 已存在,则失败。