外观
框架工具使用规范
2026-04-01
本文档来源于真实开发过程中发现的问题,归纳为规范沉淀。
核心原则:Nebula 已封装的工具,禁止绕过直接使用底层开源 API。
目录
- 一、JSON 工具:必须使用 JsonUtils
- 二、分布式锁:NebulaLockHelper / RedissonClient
- 三、Redis 缓存:NebulaCacheHelper
- 四、跨服务 RPC 接口:三件套规范
- 五、多表联动 Service 协作规范
- 六、日志与链路追踪规范
- 七、模块分层依赖规则
- 八、第三方依赖版本管理
- 九、框架工具速查表
- 十、代码审查检查清单
一、JSON 工具:必须使用 JsonUtils
模块: nebula-framework-core
类名: com.huida.nebula.core.json.JsonUtils
正确用法
// 对象 → JSON 字符串
String json = JsonUtils.toJsonString(obj);
// JSON 字符串 → 对象
MyParam param = JsonUtils.parseObject(json, MyParam.class);
// JSON 字符串 → List
List<MyParam> list = JsonUtils.parseArray(json, MyParam.class);
// 需要个性化配置时,获取全局单例
ObjectMapper mapper = JsonUtils.getObjectMapper();禁止用法
// 禁止:每次 new 一个 ObjectMapper,配置不一致(日期格式、模块等会缺失)
new ObjectMapper().readValue(json, MyClass.class);
new ObjectMapper().writeValueAsString(obj);适用场景
| 场景 | 用法 |
|---|---|
| 任务参数 JSON 反序列化 | JsonUtils.parseObject(ctx.getRawParams(), XxxParam.class) |
| 外部 API 响应解析 | JsonUtils.parseObject(responseBody, ResultDTO.class) |
| 日志打印对象 | JsonUtils.toJsonStringOrEmpty(obj)(静默模式,不抛异常) |
二、分布式锁:NebulaLockHelper / RedissonClient
模块: nebula-boot-starter-redis
正确用法
// 方式一:tryLock + Runnable(推荐,自动释放,抢不到直接返回 false)
boolean success = lockHelper.tryLock("order:create:" + orderId, () -> {
orderService.create(dto);
});
// 方式二:tryLock + Supplier(有返回值)
String result = lockHelper.tryLock("pay:" + payId, () -> payService.execute(payId));
// 方式三:框架层(无法注入 Starter 层时)直接使用 RedissonClient
RLock lock = redissonClient.getLock("nebula:schedule:lock:" + taskCode);
boolean acquired = lock.tryLock(0, lockTimeoutSeconds, TimeUnit.SECONDS);
try {
if (acquired) { /* 执行任务 */ }
} finally {
if (acquired && lock.isHeldByCurrentThread()) lock.unlock();
}禁止用法
// 禁止:StringRedisTemplate.setIfAbsent 实现分布式锁
// 问题:释放时需手动 delete,有误删他人锁的风险;非原子性操作
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(timeout));
// ...
redisTemplate.delete(lockKey); // 可能误删其他节点的锁!选型说明
| 场景 | 推荐方案 |
|---|---|
| 业务层(Service)防重入 | NebulaLockHelper.tryLock() |
| 定时任务多节点互斥 | RedissonClient.getLock().tryLock(waitTime=0, leaseTime=N) |
| 声明式锁(方法级) | @DistributedLock(AOP 切面,由 nebula-boot-starter-redis 提供) |
架构约束:
NebulaLockHelper在 Starter 层,Framework 层模块(如nebula-framework-task)不可反向依赖 Starter 层。Framework 层需要分布式锁时,直接注入RedissonClient(以compileOnly形式声明依赖)。
三、Redis 缓存:NebulaCacheHelper
模块: nebula-boot-starter-redis
类名: com.huida.nebula.boot.autoconfigure.redis.NebulaCacheHelper
正确用法
// 存对象(自动 JSON 序列化 + Key 前缀)
cacheHelper.set("file:meta:" + fileId, metaDO, Duration.ofHours(24));
// 取对象
FileMetaDO meta = cacheHelper.get("file:meta:" + fileId, FileMetaDO.class);
// 取泛型集合
List<FileMetaDO> list = cacheHelper.get("file:list", new TypeReference<List<FileMetaDO>>(){});
// Hash 字段操作
cacheHelper.hashSet("mpart:" + uploadId + ":etags", partNumber, etag);
String etag = cacheHelper.hashGet("mpart:" + uploadId + ":etags", "1");
// Set 操作
cacheHelper.setAdd("mpart:" + uploadId + ":parts", String.valueOf(partNumber));
Set<String> parts = cacheHelper.setMembers("mpart:" + uploadId + ":parts");
// 过期时间
cacheHelper.expire("mpart:" + uploadId + ":parts", Duration.ofHours(24));
// 删除多个 Key
cacheHelper.deleteAll(List.of("key1", "key2", "key3"));注意:NebulaCacheHelper 没有 hashGetAll 方法
当需要读取整个 Hash(所有字段)时,不要切换回 StringRedisTemplate,而是改用 JSON 字符串存储:
// ❌ 错误:为了 hashGetAll 降级回 StringRedisTemplate
Map<String, String> all = stringRedisTemplate.opsForHash().entries(key);
// ✅ 正确:整体读写的对象改用 JSON 字符串存储
// 写:
cacheHelper.set("mpart:" + uploadId + ":meta", metaMap, Duration.ofHours(24));
// 读:
Map<String, String> meta = cacheHelper.get("mpart:" + uploadId + ":meta",
new TypeReference<Map<String, String>>(){});禁止直接使用 StringRedisTemplate
// 禁止:绕过 NebulaCacheHelper 直接用 StringRedisTemplate
stringRedisTemplate.opsForHash().putAll(key, map);
stringRedisTemplate.opsForSet().add(key, values);四、跨服务 RPC 接口:三件套规范
微服务之间通过 Feign 调用时,必须按「三件套」模式实现。
三件套组成
| 组件 | 位置 | 作用 |
|---|---|---|
{域}Constants | {业务模块}-api 的 constant/ | 统一维护服务名、RPC URI 前缀、业务常量 |
I{业务名}RpcService(Feign Client) | {业务模块}-api 的 provider/ | 调用方只依赖此接口,primary = false 避免 LocalStub 冲突 |
{业务名}RpcServiceLocalStub | {业务模块}-core 的 feign/local/ | 聚合部署时 @Primary 短路,直接调用本地 Service |
Constants 规范
// 位置:{业务模块}-api/.../api/constant/{域名}Constants.java
public final class LtcConstants {
private LtcConstants() {}
public static final String SERVICE_NAME = "nebula-ltc";
public static final String URI_PREFIX = CommonConstants.RPC_URI_PREFIX + "/nebula/ltc";
// 业务枚举值(调用方也可能需要判断,放在 api 模块)
public static final String ACCESS_POLICY_PUBLIC = "PUBLIC";
public static final String ACCESS_POLICY_PRIVATE = "PRIVATE";
}Feign Client 接口规范
// 位置:{业务模块}-api/.../api/provider/I{业务名}RpcService.java
@FeignClient(
name = LtcConstants.SERVICE_NAME,
path = LtcConstants.URI_PREFIX + "/file",
primary = false // 必须,避免 LocalStub(@Primary) 与 Feign 代理冲突
)
public interface IFileRpcService {
@GetMapping("/{id}")
R<FileMetaDTO> getFileMeta(@PathVariable("id") Long id);
@PostMapping("/batch")
R<List<FileMetaDTO>> batchGetFileMeta(@RequestBody List<Long> ids);
}RPC Controller 规范
// 位置:{业务模块}-core/.../controller/{业务名}RpcController.java
@Hidden // 隐藏在 Swagger 文档中,仅供内部调用
@RestController
@RequestMapping(LtcConstants.URI_PREFIX + "/file")
@RequiredArgsConstructor
public class FileRpcController implements IFileRpcService {
private final IFileMetaService fileMetaService;
// 实现接口方法...
}LocalStub(聚合部署短路)规范
// 位置:{业务模块}-core/.../feign/local/{业务名}RpcServiceLocalStub.java
@Slf4j
@Primary // 聚合部署时优先于 Feign HTTP 代理
@Component
@RequiredArgsConstructor
public class FileRpcServiceLocalStub implements IFileRpcService {
private final IFileMetaService fileMetaService; // 直接调用本地 Service
@Override
public R<FileMetaDTO> getFileMeta(Long id) {
log.debug("[LocalStub] getFileMeta id={}", id);
return R.ok(FileMetaConvert.INSTANCE.doToDTO(fileMetaService.getByIdOrThrow(id)));
}
}部署模式说明
| 部署模式 | 生效 Bean | 调用链路 |
|---|---|---|
| 聚合部署(同 JVM) | LocalStub(@Primary) | 业务调用 → LocalStub → 本地 Service |
| 独立部署(不同 JVM) | Feign HTTP 代理 | 业务调用 → Feign Client → HTTP → RPC Controller → Service |
五、多表联动 Service 协作规范
通过 Service 接口协作,禁止跨模块注入 Mapper
// ✅ 正确:通过对方 Service 接口完成跨表写入
@RequiredArgsConstructor
public class ExcelFileAccessorImpl implements ExcelFileAccessor {
private final IFileMetaService fileMetaService;
private final IFileContentService fileContentService;
public Long upload(File file, String originalName) {
fileContentService.save(contentDO); // 通过 Service
fileMetaService.save(metaDO); // 通过 Service
return metaDO.getId();
}
}
// ❌ 错误:绕过 Service 直接注入另一业务域的 Mapper
@RequiredArgsConstructor
public class ExcelFileAccessorImpl implements ExcelFileAccessor {
private final FileMetaMapper fileMetaMapper; // 直接注入 Mapper ❌
private final FileContentMapper fileContentMapper; // 直接注入 Mapper ❌
}跨表写操作的事务边界
多表写操作必须放在同一个 @Transactional 方法内,且事务方法归属于调用链最顶端的 Service:
// ✅ 正确:事务边界在调用者的 Service 方法上
@Transactional(rollbackFor = Exception.class)
public FileMetaRespVO upload(MultipartFile file) {
fileContentService.save(contentDO); // 参与外层事务
save(metaDO);
fileContentService.incrementRefCount(contentId);
return vo;
}引用计数 + 延迟删除模式
当多条业务记录可能指向同一物理资源时,使用引用计数 + 延迟删除模式:
删除业务记录时:
1. 软删除业务引用记录(delete_flag = 1)
2. 物理资源 ref_count - 1(原子 SQL:ref_count = GREATEST(ref_count - 1, 0))
3. ref_count 降为 0 时:标记 storage_status = PENDING_DELETE
4. 后台清理任务定期扫描并执行真正的物理删除核心原则:物理资源的真实删除必须异步延迟执行,不在同步删除链路中完成,避免事务过长和误删。
六、日志与链路追踪规范
6.1 日志框架:Log4j2(不是 Logback)
Nebula 服务端使用 Log4j2,配置文件通过 logging.config 指定:
# application-dev.yaml
logging:
config: classpath:log4j/log4j2-dev.xmlLog4j2 与 Logback 的关键语法差异(混淆会导致字段无值):
| 功能 | ❌ Logback 写法 | ✅ Log4j2 写法 |
|---|---|---|
| MDC 取值(无默认值) | %X{traceId:-} | %X{traceId} |
| MDC 取值(有默认值) | %X{traceId:-unknown} | %X{traceId}{unknown} |
| 进程 ID | ${PID} | %processId |
典型错误:
%X{traceId:-}在 Log4j2 中,traceId:-整体被当作 Key 名查找,永远为空。
6.2 多环境日志配置规范
每个服务维护三份 Log4j2 配置,放在 src/main/resources/log4j/ 目录:
| 文件 | 环境 | 特点 |
|---|---|---|
log4j2-dev.xml | 本地开发 | 仅控制台,带 ANSI 颜色,业务包 DEBUG,响应体截断 2000 字符 |
log4j2-uat.xml | UAT | 控制台 + 文本文件 + ELK JSON 文件,异步写入 |
log4j2-prod.xml | 生产 | 控制台仅 WARN+,文本文件 + ELK JSON 文件 + 独立 ERROR 文件,关闭 SQL 日志 |
6.3 标准日志格式
%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%5p] %processId --- [%15.15t] %-50.50c{1.} : %m%n%xEx关键占位符说明:
%X{traceId}—— MDC 中的链路追踪 ID(由框架自动写入)%processId—— 进程 ID(Log4j2 原生,不依赖 Spring)
6.4 traceId 链路追踪覆盖规范
引入 nebula-boot-starter-trace 后,业务代码无需手动操作 traceId,框架已自动处理三种场景:
| 执行场景 | 处理机制 | traceId 来源 |
|---|---|---|
| HTTP 请求 | TraceFilter(自动注册) | 上游 Header 透传,或本服务新生成 |
@Scheduled 定时任务 | ScheduledTraceAspect(自动注册,需 AOP) | 每次触发生成全新 traceId |
| RocketMQ 消费 | TraceConsumerContextSetup(自动注册) | 消息 Header 透传,无则自动生成 |
| 异步线程池 | TraceTaskDecorator(注入线程池) | 从父线程 MDC 快照传播 |
不需要做:
- 不需要在 Controller、Service、MQ Listener 中手动
MDC.put("traceId", ...) - 不需要在
@Scheduled方法内手动生成 traceId - 不需要在线程池的
Runnable中手动传递 MDC
引入步骤:
// build.gradle({服务}-starter 模块)
implementation "com.huida.nebula.boot:nebula-boot-starter-trace:${nebulaBootVersion}"6.5 R<T> 响应体自动携带 traceId
R.ok() / R.fail() 内部通过 MDC.get("traceId") 自动注入,每个接口响应均包含:
{
"code": 0,
"message": "操作成功",
"traceId": "2036344300517855232",
"data": { }
}七、模块分层依赖规则
nebula-boot-project/
├── nebula-framework-* # 框架层:核心能力抽象,不依赖 Starter 层
│ ├── nebula-framework-core
│ ├── nebula-framework-task
│ ├── nebula-framework-excel
│ └── nebula-framework-file
└── nebula-starters/
└── nebula-boot-starter-* # Starter 层:自动配置,可依赖 Framework 层
├── nebula-boot-starter-redis
├── nebula-boot-starter-task
└── nebula-boot-starter-excel| 依赖方向 | 是否允许 |
|---|---|
| Framework 层 → Framework 层 | ✅ |
| Starter 层 → Framework 层 | ✅ |
| Framework 层 → Starter 层 | ❌ 禁止(循环依赖风险) |
| 业务模块 → Starter 层 | ✅ |
Framework 层依赖可选中间件的正确方式:
// build.gradle:使用 compileOnly,运行时由 Starter 层/业务方提供
compileOnly 'org.redisson:redisson-spring-boot-starter'
compileOnly 'com.lmax:disruptor'八、第三方依赖版本管理
所有第三方依赖的版本号必须在 nebula-support-dependencies 的 BOM 中统一管理,业务模块和框架模块不写死版本号。
// ✅ 正确:业务模块 build.gradle 不写版本号,从 BOM 继承
api 'com.alibaba:easyexcel'
compileOnly 'io.minio:minio'
// ❌ 禁止:在模块 build.gradle 中硬编码版本号
implementation 'com.alibaba:easyexcel:3.3.2'版本声明位置:
| 文件 | 作用 |
|---|---|
nebula-support/gradle.properties | 定义版本变量(如 easyexcelVersion=4.0.3) |
nebula-support/nebula-support-dependencies/build.gradle | 在 constraints 块中声明 api "groupId:artifactId:${version}" |
九、框架工具速查表
| 需求 | 禁止用法 | Nebula 推荐工具 | 模块 |
|---|---|---|---|
| JSON 序列化/反序列化 | new ObjectMapper() | JsonUtils | nebula-framework-core |
| 分布式锁(业务层) | StringRedisTemplate.setIfAbsent | NebulaLockHelper | nebula-boot-starter-redis |
| 分布式锁(框架层) | StringRedisTemplate.setIfAbsent | RedissonClient.getLock() | redisson(compileOnly) |
| 声明式分布式锁 | 手动加锁逻辑 | @DistributedLock | nebula-boot-starter-redis |
| Redis 读写(业务层) | StringRedisTemplate.opsForXxx() | NebulaCacheHelper | nebula-boot-starter-redis |
| 多租户动态条件查询 | @Select 注解(绕过拦截器) | MapperExt + LambdaQueryWrapper | nebula-framework-mybatis |
| 文件流复制 | 手动 byte[] buffer 循环 | StreamUtils.copy(in, out) | spring-core |
| 系统级异常(IO/外部调用) | throw new RuntimeException | SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, ...) | nebula-framework-core |
| 业务校验异常 | throw new RuntimeException | BusinessException(GlobalErrorCode...) | nebula-framework-core |
| 查询不存在时抛异常 | 手动 if(obj == null) throw... | getByIdOrThrow(id) | BaseServiceImpl |
| HTTP 响应包装 | 直接返回裸对象 | R.ok(data) / R.ok() | nebula-framework-core |
| 日志打印 | System.out.println | @Slf4j + log.info/warn/error | Lombok |
| 跨服务 RPC | 直接 Feign 无 LocalStub | Constants + @FeignClient(primary=false) + LocalStub | {业务模块}-api/core |
