jeecg-boot-base-core 02 day

以 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 实现

java 复制代码
public 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. 要么前端再调一次接口 查字典 / 查部门表,把 1 翻译成「男」 ------ 额外一次 HTTP 请求;

  2. 要么前端在表格里写 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等实现

业务场景举例

  1. 支付场景

问题

用户连续点击:

支付

支付

支付

结果:

扣款100

扣款100

扣款100

解决方案

方案 实现

Redis SETNX 订单号作为Key

数据库唯一索引 payment_no唯一

支付状态校验 已支付不可再次支付

  1. 下单场景

问题

网络卡顿:

提交订单

刷新

再次提交

产生:

订单A

订单B

解决方案

方案 实现

Token防重 一次性Token

Redis幂等 userId+商品Id

唯一订单号 数据库约束

  1. 用户注册

问题

两个请求同时注册:

手机号:

138xxxx

生成两个账号。

解决方案

数据库唯一索引:

ALTER TABLE user

ADD UNIQUE(phone);

  1. 秒杀场景

问题

100库存:

1000人同时抢

出现:

超卖

解决方案

方案 实现

Redis预扣库存 原子递减

分布式锁 锁商品

乐观锁 version控制

  1. 积分兑换

问题

连续点击:

兑换

兑换

兑换

结果:

扣300积分

解决方案

用户ID

商品ID

生成唯一Key:

exchange:1001:2001

Redis控制。

  1. 审批流程

问题

审批人连续点击:

同意

同意

同意

结果:

重复审批

解决方案

状态机:

待审批

已通过

再次提交:

已通过

直接拒绝。

业务 推荐指数
登录 ★★★★★
提交表单 ★★★★★
新增数据 ★★★★★
保存草稿 ★★★★★
发起审批 ★★★★★
支付 ★★★★
秒杀 ★★★
库存扣减 ★★
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 已存在,则失败。

相关推荐
yaoxin5211231 小时前
434. Java 日期时间 API - Period 基于日期的时间段
java·开发语言·python
岁月宁静2 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
JaydenAI3 小时前
[对比学习LangChain和MAF-07]如何引入人机交互的审批流程
python·ai·langchain·c#·agent·hitl·maf
神奇元创3 小时前
商用级光路加速卡:大模型推理的极速落地方案
python·神经网络·fpga开发·dsp开发
运筹vivo@4 小时前
Python ContextVar 底层机制与内存模型拆解
前端·数据库·python
大白菜和MySQL4 小时前
java应用排查高线程
java·python
嵌入式协会20240724 小时前
(已解决)MinIO python 获取预签名出现forbidden、errornetwork等错误
java·开发语言·python
宸丶一4 小时前
Day 14:任务追踪 - 让 Agent 拥有项目管理能力
开发语言·python