@nebula-skills/nebula-code-standards 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/bin/cli.cjs +50 -0
- package/package.json +45 -0
- package/scripts/postinstall.cjs +18 -0
- package/skill/SKILL.md +133 -0
- package/skill/backend-standards.md +611 -0
- package/skill/boot-components-catalog.md +546 -0
- package/skill/db-standards.md +232 -0
- package/skill/framework-standards.md +610 -0
- package/skill/frontend-standards.md +310 -0
- package/skill/full-crud-example.md +608 -0
- package/skill/init-new-project.md +842 -0
- package/skill/microapp-guide.md +510 -0
- package/skill/pitfalls-checklist.md +188 -0
- package/skill/rpc-api-reference.md +313 -0
- package/skill/upgrade-decision.md +151 -0
- package/skill/workspace-overview.md +194 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# 框架工具使用规范
|
|
2
|
+
|
|
3
|
+
> 本文档来源于真实开发过程中发现的问题,归纳为规范沉淀。适用于所有基于 Nebula 框架的业务开发与框架扩展工作。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 一、框架工具类优先使用原则
|
|
8
|
+
|
|
9
|
+
**核心原则:Nebula 已封装的工具,禁止绕过直接使用底层开源 API。**
|
|
10
|
+
|
|
11
|
+
Nebula 对常用基础能力做了统一封装,封装目的是:
|
|
12
|
+
1. 保证配置一致性(如 Jackson 的日期格式、时区等)
|
|
13
|
+
2. 统一错误处理和日志输出
|
|
14
|
+
3. 屏蔽第三方 API 变更影响
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 二、JSON 工具:必须使用 `JsonUtils`
|
|
19
|
+
|
|
20
|
+
**模块:** `nebula-framework-core`
|
|
21
|
+
**类名:** `com.huida.nebula.core.json.JsonUtils`
|
|
22
|
+
|
|
23
|
+
### ✅ 正确用法
|
|
24
|
+
|
|
25
|
+
```java
|
|
26
|
+
// 对象 → JSON 字符串
|
|
27
|
+
String json = JsonUtils.toJsonString(obj);
|
|
28
|
+
|
|
29
|
+
// JSON 字符串 → 对象
|
|
30
|
+
MyParam param = JsonUtils.parseObject(json, MyParam.class);
|
|
31
|
+
|
|
32
|
+
// JSON 字符串 → List
|
|
33
|
+
List<MyParam> list = JsonUtils.parseArray(json, MyParam.class);
|
|
34
|
+
|
|
35
|
+
// 需要个性化配置时,获取全局单例
|
|
36
|
+
ObjectMapper mapper = JsonUtils.getObjectMapper();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ❌ 禁止用法
|
|
40
|
+
|
|
41
|
+
```java
|
|
42
|
+
// 禁止:每次 new 一个 ObjectMapper,配置不一致(日期格式、模块等会缺失)
|
|
43
|
+
new ObjectMapper().readValue(json, MyClass.class);
|
|
44
|
+
new ObjectMapper().writeValueAsString(obj);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 适用场景
|
|
48
|
+
|
|
49
|
+
| 场景 | 用法 |
|
|
50
|
+
|------|------|
|
|
51
|
+
| 任务参数 JSON 反序列化 | `JsonUtils.parseObject(ctx.getRawParams(), XxxParam.class)` |
|
|
52
|
+
| 外部 API 响应解析 | `JsonUtils.parseObject(responseBody, ResultDTO.class)` |
|
|
53
|
+
| 日志打印对象 | `JsonUtils.toJsonStringOrEmpty(obj)`(静默模式,不抛异常) |
|
|
54
|
+
| Spring 注入 `ObjectMapper` | 可通过 Bean 注入,但不允许 `new ObjectMapper()` |
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 三、分布式锁:优先使用 `NebulaLockHelper` 或 `RedissonClient`
|
|
59
|
+
|
|
60
|
+
**模块:** `nebula-boot-starter-redis`
|
|
61
|
+
**类名:** `com.huida.nebula.boot.autoconfigure.redis.NebulaLockHelper`
|
|
62
|
+
|
|
63
|
+
### ✅ 正确用法
|
|
64
|
+
|
|
65
|
+
```java
|
|
66
|
+
// 方式一:tryLock + Runnable(推荐,自动释放,抢不到直接返回 false)
|
|
67
|
+
boolean success = lockHelper.tryLock("order:create:" + orderId, () -> {
|
|
68
|
+
orderService.create(dto);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 方式二:tryLock + Supplier(有返回值)
|
|
72
|
+
String result = lockHelper.tryLock("pay:" + payId, () -> payService.execute(payId));
|
|
73
|
+
|
|
74
|
+
// 方式三:框架层(无法注入 Starter 层时)直接使用 RedissonClient
|
|
75
|
+
RLock lock = redissonClient.getLock("nebula:schedule:lock:" + taskCode);
|
|
76
|
+
boolean acquired = lock.tryLock(0, lockTimeoutSeconds, TimeUnit.SECONDS);
|
|
77
|
+
try {
|
|
78
|
+
if (acquired) { /* 执行任务 */ }
|
|
79
|
+
} finally {
|
|
80
|
+
if (acquired && lock.isHeldByCurrentThread()) lock.unlock();
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### ❌ 禁止用法
|
|
85
|
+
|
|
86
|
+
```java
|
|
87
|
+
// 禁止:StringRedisTemplate.setIfAbsent 实现分布式锁
|
|
88
|
+
// 问题:释放时需手动 delete,有误删他人锁的风险;非原子性操作
|
|
89
|
+
Boolean locked = redisTemplate.opsForValue()
|
|
90
|
+
.setIfAbsent(lockKey, "1", Duration.ofSeconds(timeout));
|
|
91
|
+
// ...
|
|
92
|
+
redisTemplate.delete(lockKey); // 可能误删其他节点的锁!
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 选型说明
|
|
96
|
+
|
|
97
|
+
| 场景 | 推荐方案 |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| 业务层(Service)防重入 | `NebulaLockHelper.tryLock()` |
|
|
100
|
+
| 定时任务多节点互斥 | `RedissonClient.getLock().tryLock(waitTime=0, leaseTime=N)` |
|
|
101
|
+
| 声明式锁(方法级) | `@DistributedLock`(AOP 切面,由 `nebula-boot-starter-redis` 提供) |
|
|
102
|
+
|
|
103
|
+
> **架构约束:** `NebulaLockHelper` 在 Starter 层,Framework 层模块(如 `nebula-framework-task`)不可反向依赖 Starter 层。Framework 层需要分布式锁时,直接注入 `RedissonClient`(以 `compileOnly` 形式声明依赖)。
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 三-B、Redis 缓存工具:`NebulaCacheHelper`
|
|
108
|
+
|
|
109
|
+
**模块:** `nebula-boot-starter-redis`
|
|
110
|
+
**类名:** `com.huida.nebula.boot.autoconfigure.redis.NebulaCacheHelper`
|
|
111
|
+
|
|
112
|
+
### ✅ 正确用法
|
|
113
|
+
|
|
114
|
+
```java
|
|
115
|
+
// 存对象(自动 JSON 序列化 + Key 前缀)
|
|
116
|
+
cacheHelper.set("file:meta:" + fileId, metaDO, Duration.ofHours(24));
|
|
117
|
+
|
|
118
|
+
// 取对象
|
|
119
|
+
FileMetaDO meta = cacheHelper.get("file:meta:" + fileId, FileMetaDO.class);
|
|
120
|
+
|
|
121
|
+
// 取泛型集合
|
|
122
|
+
List<FileMetaDO> list = cacheHelper.get("file:list", new TypeReference<List<FileMetaDO>>(){});
|
|
123
|
+
|
|
124
|
+
// Hash 字段操作
|
|
125
|
+
cacheHelper.hashSet("mpart:" + uploadId + ":etags", partNumber, etag);
|
|
126
|
+
String etag = cacheHelper.hashGet("mpart:" + uploadId + ":etags", "1");
|
|
127
|
+
|
|
128
|
+
// Set 操作
|
|
129
|
+
cacheHelper.setAdd("mpart:" + uploadId + ":parts", String.valueOf(partNumber));
|
|
130
|
+
Set<String> parts = cacheHelper.setMembers("mpart:" + uploadId + ":parts");
|
|
131
|
+
|
|
132
|
+
// 过期时间
|
|
133
|
+
cacheHelper.expire("mpart:" + uploadId + ":parts", Duration.ofHours(24));
|
|
134
|
+
|
|
135
|
+
// 删除多个 Key
|
|
136
|
+
cacheHelper.deleteAll(List.of("key1", "key2", "key3"));
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### ⚠️ `NebulaCacheHelper` 没有 `hashGetAll` 方法
|
|
140
|
+
|
|
141
|
+
当需要读取整个 Hash(所有字段)时,**不要**切换回 `StringRedisTemplate`,而是改用 JSON 字符串存储:
|
|
142
|
+
|
|
143
|
+
```java
|
|
144
|
+
// ❌ 错误:为了 hashGetAll 降级回 StringRedisTemplate
|
|
145
|
+
Map<String, String> all = stringRedisTemplate.opsForHash().entries(key); // 违反工具封装规范
|
|
146
|
+
|
|
147
|
+
// ✅ 正确:整体读写的对象改用 JSON 字符串存储
|
|
148
|
+
// 写:
|
|
149
|
+
cacheHelper.set("mpart:" + uploadId + ":meta", metaMap, Duration.ofHours(24));
|
|
150
|
+
// 读:
|
|
151
|
+
Map<String, String> meta = cacheHelper.get("mpart:" + uploadId + ":meta",
|
|
152
|
+
new TypeReference<Map<String, String>>(){});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### ❌ 禁止直接使用 `StringRedisTemplate`
|
|
156
|
+
|
|
157
|
+
```java
|
|
158
|
+
// 禁止:绕过 NebulaCacheHelper 直接用 StringRedisTemplate
|
|
159
|
+
stringRedisTemplate.opsForHash().putAll(key, map);
|
|
160
|
+
stringRedisTemplate.opsForSet().add(key, values);
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 三-C、跨服务 RPC 接口开发规范(三件套)
|
|
166
|
+
|
|
167
|
+
微服务之间通过 Feign 调用时,必须按以下「三件套」模式实现,**参考 `nebula-system` 的 `IdGenRpcService` + `SystemConstants`**:
|
|
168
|
+
|
|
169
|
+
### 三件套组成
|
|
170
|
+
|
|
171
|
+
| 组件 | 位置 | 作用 |
|
|
172
|
+
|------|------|------|
|
|
173
|
+
| `{域}Constants` | `{业务模块}-api` 的 `constant/` | 统一维护服务名、RPC URI 前缀、业务常量 |
|
|
174
|
+
| `I{业务名}RpcService`(Feign Client) | `{业务模块}-api` 的 `provider/` | 调用方只依赖此接口,`primary = false` 避免 LocalStub 冲突 |
|
|
175
|
+
| `{业务名}RpcServiceLocalStub` | `{业务模块}-core` 的 `feign/local/` | 聚合部署时 `@Primary` 短路,直接调用本地 Service |
|
|
176
|
+
|
|
177
|
+
### `{域}Constants` 规范
|
|
178
|
+
|
|
179
|
+
```java
|
|
180
|
+
// 位置:{业务模块}-api/.../api/constant/{域名}Constants.java
|
|
181
|
+
public final class FsmConstants {
|
|
182
|
+
private FsmConstants() {}
|
|
183
|
+
|
|
184
|
+
/** 服务名(Feign Client name + Nacos 注册名) */
|
|
185
|
+
public static final String SERVICE_NAME = "nebula-fsm";
|
|
186
|
+
|
|
187
|
+
/** RPC 路径前缀:CommonConstants.RPC_URI_PREFIX + "/{业务域路径}" */
|
|
188
|
+
public static final String URI_PREFIX = CommonConstants.RPC_URI_PREFIX + "/nebula/fsm";
|
|
189
|
+
|
|
190
|
+
// 业务枚举值(调用方也可能需要判断,所以放在 api 模块)
|
|
191
|
+
public static final String ACCESS_POLICY_PUBLIC = "PUBLIC";
|
|
192
|
+
public static final String ACCESS_POLICY_PRIVATE = "PRIVATE";
|
|
193
|
+
public static final String UPLOAD_STATUS_COMPLETE = "COMPLETE";
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Feign Client 接口规范
|
|
198
|
+
|
|
199
|
+
```java
|
|
200
|
+
// 位置:{业务模块}-api/.../api/provider/I{业务名}RpcService.java
|
|
201
|
+
@FeignClient(
|
|
202
|
+
name = FsmConstants.SERVICE_NAME,
|
|
203
|
+
path = FsmConstants.URI_PREFIX + "/file",
|
|
204
|
+
primary = false // 必须,避免 LocalStub(@Primary) 与 Feign 代理冲突
|
|
205
|
+
)
|
|
206
|
+
public interface IFileRpcService {
|
|
207
|
+
@GetMapping("/{id}")
|
|
208
|
+
R<FileMetaDTO> getFileMeta(@PathVariable("id") Long id);
|
|
209
|
+
|
|
210
|
+
@PostMapping("/batch")
|
|
211
|
+
R<List<FileMetaDTO>> batchGetFileMeta(@RequestBody List<Long> ids);
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### RPC Controller 规范
|
|
216
|
+
|
|
217
|
+
```java
|
|
218
|
+
// 位置:{业务模块}-core/.../controller/{业务名}RpcController.java
|
|
219
|
+
@Hidden // 隐藏在 Swagger 文档中,仅供内部调用
|
|
220
|
+
@RestController
|
|
221
|
+
@RequestMapping(FsmConstants.URI_PREFIX + "/file") // 与 Feign Client path 一致
|
|
222
|
+
@RequiredArgsConstructor
|
|
223
|
+
public class FileRpcController implements IFileRpcService {
|
|
224
|
+
private final ISysFileMetaService fileMetaService;
|
|
225
|
+
// 实现接口方法...
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### LocalStub(聚合部署短路)规范
|
|
230
|
+
|
|
231
|
+
```java
|
|
232
|
+
// 位置:{业务模块}-core/.../feign/local/{业务名}RpcServiceLocalStub.java
|
|
233
|
+
@Slf4j
|
|
234
|
+
@Primary // 聚合部署时优先于 Feign HTTP 代理
|
|
235
|
+
@Component
|
|
236
|
+
@RequiredArgsConstructor
|
|
237
|
+
public class FileRpcServiceLocalStub implements IFileRpcService {
|
|
238
|
+
private final ISysFileMetaService fileMetaService; // 直接调用本地 Service
|
|
239
|
+
|
|
240
|
+
@Override
|
|
241
|
+
public R<FileMetaDTO> getFileMeta(Long id) {
|
|
242
|
+
log.debug("[LocalStub] getFileMeta id={}", id);
|
|
243
|
+
return R.ok(SysFileMetaConvert.INSTANCE.doToDTO(fileMetaService.getByIdOrThrow(id)));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 部署模式说明
|
|
249
|
+
|
|
250
|
+
| 部署模式 | 生效 Bean | 调用链路 |
|
|
251
|
+
|---------|---------|---------|
|
|
252
|
+
| 聚合部署(同 JVM) | `LocalStub`(`@Primary`) | 业务调用 → LocalStub → 本地 Service |
|
|
253
|
+
| 独立部署(不同 JVM) | Feign HTTP 代理 | 业务调用 → Feign Client → HTTP → RPC Controller → Service |
|
|
254
|
+
|
|
255
|
+
> 调用方只需要依赖 `{业务模块}-api`(含 Feign 接口和 LocalStub),无需关心部署模式。
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 三-D、多表联动 Service 协作规范
|
|
260
|
+
|
|
261
|
+
当一次业务操作需要同时写入多张表(如文件物理层 `sys_file_content` + 业务引用层 `sys_file_meta`),必须遵循以下规范:
|
|
262
|
+
|
|
263
|
+
### 通过 Service 接口协作,禁止跨模块注入 Mapper
|
|
264
|
+
|
|
265
|
+
```java
|
|
266
|
+
// ✅ 正确:通过对方 Service 接口完成跨表写入
|
|
267
|
+
@RequiredArgsConstructor
|
|
268
|
+
public class ExcelFileAccessorImpl implements ExcelFileAccessor {
|
|
269
|
+
private final ISysFileMetaService fileMetaService; // 注入 Service 接口
|
|
270
|
+
private final ISysFileContentService fileContentService; // 注入 Service 接口
|
|
271
|
+
|
|
272
|
+
public Long upload(File file, String originalName) {
|
|
273
|
+
// ...
|
|
274
|
+
fileContentService.save(contentDO); // 通过 Service
|
|
275
|
+
fileMetaService.save(metaDO); // 通过 Service
|
|
276
|
+
return metaDO.getId();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ❌ 错误:绕过 Service 直接注入另一业务域的 Mapper
|
|
281
|
+
@RequiredArgsConstructor
|
|
282
|
+
public class ExcelFileAccessorImpl implements ExcelFileAccessor {
|
|
283
|
+
private final SysFileMetaMapper fileMetaMapper; // 直接注入 Mapper ❌
|
|
284
|
+
private final SysFileContentMapper fileContentMapper; // 直接注入 Mapper ❌
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 跨表写操作的事务边界
|
|
289
|
+
|
|
290
|
+
多表写操作必须放在同一个 `@Transactional` 方法内,且事务方法归属于**调用链最顶端的 Service**,不在 Impl 之间重复嵌套事务:
|
|
291
|
+
|
|
292
|
+
```java
|
|
293
|
+
// ✅ 正确:事务边界在调用者的 Service 方法上
|
|
294
|
+
@Transactional(rollbackFor = Exception.class)
|
|
295
|
+
public FileMetaRespVO upload(MultipartFile file, ...) {
|
|
296
|
+
// 1. 写物理文件记录
|
|
297
|
+
fileContentService.save(contentDO); // 不需要事务注解(参与外层事务)
|
|
298
|
+
// 2. 写业务引用记录
|
|
299
|
+
save(metaDO); // 同上
|
|
300
|
+
// 3. 原子递增引用计数
|
|
301
|
+
fileContentService.incrementRefCount(contentId);
|
|
302
|
+
return vo;
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 引用计数 + 延迟删除模式(防误删共享资源)
|
|
307
|
+
|
|
308
|
+
当多条业务记录可能指向同一物理资源时,使用引用计数 + 延迟删除模式,保证任何一方删除自身记录时不影响其他引用方:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
删除业务记录时:
|
|
312
|
+
1. 软删除业务引用记录(delete_flag = 1)
|
|
313
|
+
2. 物理资源 ref_count - 1(原子 SQL:ref_count = GREATEST(ref_count - 1, 0))
|
|
314
|
+
3. ref_count 降为 0 时:同步标记 storage_status = PENDING_DELETE
|
|
315
|
+
4. 后台清理任务定期扫描 storage_status = PENDING_DELETE 的记录,执行真正的物理删除
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
> **核心原则**:物理资源的真实删除必须异步延迟执行,不在同步删除链路中完成,避免事务过长和误删。
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## 三-E、日志与链路追踪规范
|
|
323
|
+
|
|
324
|
+
### 1. 日志框架:Log4j2(不是 Logback)
|
|
325
|
+
|
|
326
|
+
Nebula 服务端使用 **Log4j2**,配置文件通过 `logging.config` 指定:
|
|
327
|
+
|
|
328
|
+
```yaml
|
|
329
|
+
# application-dev.yaml
|
|
330
|
+
logging:
|
|
331
|
+
config: classpath:log4j/log4j2-dev.xml
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Log4j2 与 Logback 的关键语法差异(混淆会导致字段无值):**
|
|
335
|
+
|
|
336
|
+
| 功能 | ❌ Logback 写法 | ✅ Log4j2 写法 |
|
|
337
|
+
|------|---------------|--------------|
|
|
338
|
+
| MDC 取值(无默认值) | `%X{traceId:-}` | `%X{traceId}` |
|
|
339
|
+
| MDC 取值(有默认值) | `%X{traceId:-unknown}` | `%X{traceId}{unknown}` |
|
|
340
|
+
| 进程 ID | `${PID}` | `%processId` |
|
|
341
|
+
|
|
342
|
+
> **典型错误**:`%X{traceId:-}` 在 Log4j2 中,`traceId:-` 整体被当作 Key 名查找,永远为空。正确写法是 `%X{traceId}`。
|
|
343
|
+
|
|
344
|
+
> **`${PID}` 无值原因**:`${PID}` 是 Spring Boot 的占位符,只有 Spring 处理日志配置时才生效。独立加载的 Log4j2 XML 无法解析,必须改用 Log4j2 原生的 `%processId`。
|
|
345
|
+
|
|
346
|
+
### 2. 多环境日志配置规范
|
|
347
|
+
|
|
348
|
+
每个服务维护三份 Log4j2 配置,放在 `src/main/resources/log4j/` 目录:
|
|
349
|
+
|
|
350
|
+
| 文件 | 环境 | 特点 |
|
|
351
|
+
|------|------|------|
|
|
352
|
+
| `log4j2-dev.xml` | 本地开发 | 仅控制台,带 ANSI 颜色,业务包 DEBUG,响应体截断 2000 字符 |
|
|
353
|
+
| `log4j2-uat.xml` | UAT | 控制台 + 文本文件 + ELK JSON 文件,异步写入 |
|
|
354
|
+
| `log4j2-prod.xml` | 生产 | 控制台仅 WARN+,文本文件 + ELK JSON 文件 + 独立 ERROR 文件,关闭 SQL 日志 |
|
|
355
|
+
|
|
356
|
+
各环境 profile YAML 中分别指定对应配置(`application-dev.yaml`、`application-uat.yaml`、`application-prod.yaml`)。
|
|
357
|
+
|
|
358
|
+
### 3. 标准日志格式
|
|
359
|
+
|
|
360
|
+
控制台/文本文件(Log4j2 语法):
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%5p] %processId --- [%15.15t] %-50.50c{1.} : %m%n%xEx
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
关键占位符说明:
|
|
367
|
+
|
|
368
|
+
- `%X{traceId}` —— MDC 中的链路追踪 ID(由 TraceFilter/ScheduledTraceAspect/TraceConsumerContextSetup 写入)
|
|
369
|
+
- `%processId` —— 进程 ID(Log4j2 原生,不依赖 Spring)
|
|
370
|
+
- `%-50.50c{1.}` —— Logger 名缩写(包路径首字母,类名/方法名保留全名),固定宽度 50 字符
|
|
371
|
+
|
|
372
|
+
ELK JSON 格式遵循 ECS(Elastic Common Schema),字段命名:`@timestamp`、`log.level`、`trace.id`、`tenant.code`、`service.name`、`message`、`error.stack`。
|
|
373
|
+
|
|
374
|
+
### 4. traceId 链路追踪覆盖规范
|
|
375
|
+
|
|
376
|
+
Nebula 框架已自动处理三种场景的 traceId 注入,**业务代码无需手动操作**,前提是引入 `nebula-boot-starter-trace`:
|
|
377
|
+
|
|
378
|
+
| 执行场景 | 处理机制 | traceId 来源 |
|
|
379
|
+
|---------|---------|------------|
|
|
380
|
+
| HTTP 请求 | `TraceFilter`(自动注册) | 上游 Header 透传,或本服务新生成(链路起点) |
|
|
381
|
+
| `@Scheduled` 定时任务 | `ScheduledTraceAspect`(自动注册,需 AOP) | 每次触发生成全新 traceId(独立链路起点) |
|
|
382
|
+
| RocketMQ 消费 | `TraceConsumerContextSetup`(自动注册) | 消息 Header(Producer 透传),无 Header 时自动生成 |
|
|
383
|
+
| 异步线程池 | `TraceTaskDecorator`(注入线程池) | 从父线程 MDC 快照传播 |
|
|
384
|
+
|
|
385
|
+
**引入步骤(一次性配置):**
|
|
386
|
+
|
|
387
|
+
```groovy
|
|
388
|
+
// build.gradle({服务}-starter 模块)
|
|
389
|
+
implementation "com.huida.nebula.boot:nebula-boot-starter-trace:${nebulaBootVersion}"
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**不需要做:**
|
|
393
|
+
|
|
394
|
+
- ❌ 不需要在 Controller、Service、MQ Listener 中手动 `MDC.put("traceId", ...)`
|
|
395
|
+
- ❌ 不需要在 `@Scheduled` 方法内手动生成 traceId
|
|
396
|
+
- ❌ 不需要在线程池的 `Runnable` 中手动传递 MDC
|
|
397
|
+
|
|
398
|
+
### 5. `R<T>` 响应体自动携带 traceId
|
|
399
|
+
|
|
400
|
+
`R.ok()` / `R.fail()` 的构造器内部通过 `MDC.get("traceId")` 自动注入,**调用方无需任何额外操作**:
|
|
401
|
+
|
|
402
|
+
```json
|
|
403
|
+
// 每个接口响应自动包含 traceId
|
|
404
|
+
{
|
|
405
|
+
"code": 0,
|
|
406
|
+
"message": "操作成功",
|
|
407
|
+
"traceId": "2036344300517855232",
|
|
408
|
+
"data": { ... }
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**实现原理**:Controller 方法调用 `R.ok(data)` 时,`TraceFilter` 已将 traceId 写入 MDC(同一线程),构造器读取时必然有值。
|
|
413
|
+
|
|
414
|
+
**为什么用 `MDC.get()` 而非 `TraceContextHolder.getTraceId()`**:
|
|
415
|
+
|
|
416
|
+
`R` 类位于 `nebula-framework-core`(纯 Java 工具层,不引入 Spring),`TraceContextHolder` 位于 `nebula-framework-trace`,引入会产生循环依赖风险。`slf4j-api` 已是 `nebula-framework-core` 现有依赖,通过 `MDC.get()` 读取效果完全等价。
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 四、模块分层依赖规则
|
|
421
|
+
|
|
422
|
+
Nebula 框架的模块分为 4 层,**禁止逆向依赖**:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
nebula-boot-project/
|
|
426
|
+
├── nebula-framework-* # 框架层:核心能力抽象,不依赖 Starter 层
|
|
427
|
+
│ ├── nebula-framework-core
|
|
428
|
+
│ ├── nebula-framework-task
|
|
429
|
+
│ ├── nebula-framework-excel
|
|
430
|
+
│ └── nebula-framework-file
|
|
431
|
+
└── nebula-starters/
|
|
432
|
+
├── nebula-boot-starter-* # Starter 层:自动配置,可依赖 Framework 层
|
|
433
|
+
│ ├── nebula-boot-starter-redis (含 NebulaLockHelper、NebulaCacheHelper)
|
|
434
|
+
│ ├── nebula-boot-starter-task
|
|
435
|
+
│ └── nebula-boot-starter-excel
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| 依赖方向 | 是否允许 |
|
|
439
|
+
|---------|---------|
|
|
440
|
+
| Framework 层 → Framework 层 | ✅ |
|
|
441
|
+
| Starter 层 → Framework 层 | ✅ |
|
|
442
|
+
| Framework 层 → Starter 层 | ❌ 禁止(循环依赖风险) |
|
|
443
|
+
| 业务模块 → Starter 层 | ✅(通过 `api` 传递依赖) |
|
|
444
|
+
|
|
445
|
+
**Framework 层依赖可选中间件的正确方式:**
|
|
446
|
+
|
|
447
|
+
```groovy
|
|
448
|
+
// build.gradle:使用 compileOnly,运行时由 Starter 层/业务方提供
|
|
449
|
+
compileOnly 'org.redisson:redisson-spring-boot-starter'
|
|
450
|
+
compileOnly 'com.lmax:disruptor'
|
|
451
|
+
compileOnly 'org.apache.rocketmq:rocketmq-spring-boot-starter'
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## 五、第三方依赖版本管理规范
|
|
457
|
+
|
|
458
|
+
**所有第三方依赖的版本号必须在 `nebula-support-dependencies` 的 BOM 中统一管理,业务模块和框架模块不写死版本号。**
|
|
459
|
+
|
|
460
|
+
### 版本声明位置
|
|
461
|
+
|
|
462
|
+
| 文件 | 作用 |
|
|
463
|
+
|------|------|
|
|
464
|
+
| `nebula-support/gradle.properties` | 定义版本变量(如 `easyexcelVersion=4.0.3`) |
|
|
465
|
+
| `nebula-support/nebula-support-dependencies/build.gradle` | 在 `constraints` 块中声明 `api "groupId:artifactId:${version}"` |
|
|
466
|
+
|
|
467
|
+
### ✅ 正确用法
|
|
468
|
+
|
|
469
|
+
```groovy
|
|
470
|
+
// nebula-support/gradle.properties
|
|
471
|
+
easyexcelVersion=4.0.3
|
|
472
|
+
minioVersion=8.5.17
|
|
473
|
+
disruptorVersion=4.0.0
|
|
474
|
+
|
|
475
|
+
// nebula-support-dependencies/build.gradle(constraints 块内)
|
|
476
|
+
api "com.alibaba:easyexcel:${easyexcelVersion}"
|
|
477
|
+
api "io.minio:minio:${minioVersion}"
|
|
478
|
+
api "com.lmax:disruptor:${disruptorVersion}"
|
|
479
|
+
|
|
480
|
+
// 业务模块/框架模块 build.gradle(不写版本号,从 BOM 继承)
|
|
481
|
+
api 'com.alibaba:easyexcel'
|
|
482
|
+
compileOnly 'io.minio:minio'
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### ❌ 禁止用法
|
|
486
|
+
|
|
487
|
+
```groovy
|
|
488
|
+
// 禁止:在模块 build.gradle 中硬编码版本号
|
|
489
|
+
implementation 'com.alibaba:easyexcel:3.3.2'
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 当前已管理的新增依赖
|
|
493
|
+
|
|
494
|
+
| 分类 | 依赖 | 版本变量 |
|
|
495
|
+
|------|------|---------|
|
|
496
|
+
| Excel | `com.alibaba:easyexcel` | `easyexcelVersion` |
|
|
497
|
+
| 文件存储 | `io.minio:minio` | `minioVersion` |
|
|
498
|
+
| 文件存储 | `com.aliyun.oss:aliyun-sdk-oss` | `aliyunOssVersion` |
|
|
499
|
+
| 文件存储 | `com.qcloud:cos_api` | `tencentCosVersion` |
|
|
500
|
+
| 文件存储 | `com.huaweicloud:esdk-obs-java-bundle` | `huaweiObsVersion` |
|
|
501
|
+
| 文件存储 | `com.qiniu:qiniu-java-sdk` | `qiniuSdkVersion` |
|
|
502
|
+
| 文件存储 | `software.amazon.awssdk:s3` | `awsS3Version` |
|
|
503
|
+
| 并发 | `com.lmax:disruptor` | `disruptorVersion` |
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
## 六、包结构与文件放置规范
|
|
508
|
+
|
|
509
|
+
### Service 实现类必须在 `service/impl/` 目录
|
|
510
|
+
|
|
511
|
+
| 类型 | 正确位置 | 错误位置 |
|
|
512
|
+
|------|---------|---------|
|
|
513
|
+
| Service 接口 | `service/I{业务名}Service.java` | — |
|
|
514
|
+
| Service 实现 | `service/impl/{业务名}ServiceImpl.java` | `service/{业务名}ServiceImpl.java` ❌ |
|
|
515
|
+
| SPI 实现类 | `service/impl/{SPI名}Impl.java` | `service/{SPI名}Impl.java` ❌ |
|
|
516
|
+
|
|
517
|
+
> **典型错误:** `ExcelTemplateRegistryImpl` 实现了框架 SPI `ExcelTemplateRegistry`,应放在 `service/impl/` 目录,而不是与接口平级的 `service/` 目录。
|
|
518
|
+
|
|
519
|
+
### 泛型参数数量必须匹配接口定义
|
|
520
|
+
|
|
521
|
+
```java
|
|
522
|
+
// TaskHandler<P> 只有 1 个泛型参数
|
|
523
|
+
// ✅ 正确
|
|
524
|
+
Map<String, TaskHandler<?>> registry;
|
|
525
|
+
List<TaskHandler<?>> handlers;
|
|
526
|
+
public <P> TaskHandler<P> get(String taskCode) { ... }
|
|
527
|
+
|
|
528
|
+
// ❌ 错误:写成了 2 个泛型参数
|
|
529
|
+
Map<String, TaskHandler<?, ?>> registry;
|
|
530
|
+
public <P, R> TaskHandler<P, R> get(String taskCode) { ... }
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## 七、框架工具速查表
|
|
536
|
+
|
|
537
|
+
| 需求 | 禁止用法 | Nebula 推荐工具 | 模块 |
|
|
538
|
+
|------|---------|---------------|------|
|
|
539
|
+
| JSON 序列化/反序列化 | `new ObjectMapper()` | `JsonUtils` | `nebula-framework-core` |
|
|
540
|
+
| 分布式锁(业务层) | `StringRedisTemplate.setIfAbsent` | `NebulaLockHelper` | `nebula-boot-starter-redis` |
|
|
541
|
+
| 分布式锁(框架层) | `StringRedisTemplate.setIfAbsent` | `RedissonClient.getLock()` | `redisson`(compileOnly) |
|
|
542
|
+
| 声明式分布式锁 | 手动加锁逻辑 | `@DistributedLock` | `nebula-boot-starter-redis` |
|
|
543
|
+
| Redis 读写(业务层) | `StringRedisTemplate.opsForXxx()` | `NebulaCacheHelper` | `nebula-boot-starter-redis` |
|
|
544
|
+
| 多租户动态条件查询 | `@Select` 注解(绕过拦截器) | `MapperExt` + `LambdaQueryWrapper` | `nebula-framework-mybatis` |
|
|
545
|
+
| 文件流复制 | 手动 `byte[]` buffer 循环 | `StreamUtils.copy(in, out)` | `spring-core` |
|
|
546
|
+
| 系统级异常(IO/外部调用) | `throw new RuntimeException` | `SystemException(GlobalErrorCode.INTERNAL_SERVER_ERROR, ...)` | `nebula-framework-core` |
|
|
547
|
+
| 业务校验异常 | `throw new RuntimeException` | `BusinessException(GlobalErrorCode...)` | `nebula-framework-core` |
|
|
548
|
+
| 查询不存在时抛异常 | 手动 `if(obj == null) throw...` | `getByIdOrThrow(id)` | `BaseServiceImpl` |
|
|
549
|
+
| HTTP 响应包装 | 直接返回裸对象 | `R.ok(data)` / `R.ok()` | `nebula-framework-core` |
|
|
550
|
+
| 日志打印 | `System.out.println` | `@Slf4j` + `log.info/warn/error` | Lombok |
|
|
551
|
+
| 跨服务 RPC | 直接 Feign 无 LocalStub | Constants + `@FeignClient(primary=false)` + LocalStub | `{业务模块}-api/core` |
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## 八、代码审查检查清单(扩展)
|
|
556
|
+
|
|
557
|
+
> 是对「十三、新业务模块开发检查清单」的补充,专注于框架合规性。
|
|
558
|
+
|
|
559
|
+
### 依赖与工具使用
|
|
560
|
+
|
|
561
|
+
- [ ] JSON 操作是否全部使用 `JsonUtils`(不存在 `new ObjectMapper()`)
|
|
562
|
+
- [ ] 分布式锁是否使用 `NebulaLockHelper` 或 `RedissonClient`(不使用 `StringRedisTemplate.setIfAbsent`)
|
|
563
|
+
- [ ] Redis 操作是否使用 `NebulaCacheHelper`(不直接使用 `StringRedisTemplate`)
|
|
564
|
+
- [ ] 需要读取整个 Hash 时,是否改为 JSON 字符串存储(避开 `hashGetAll` 缺口)
|
|
565
|
+
- [ ] 新增三方依赖是否已在 `nebula-support-dependencies` 中声明版本约束
|
|
566
|
+
- [ ] 模块 `build.gradle` 中的依赖是否**不写版本号**(从 BOM 继承)
|
|
567
|
+
- [ ] Framework 层模块是否未依赖 Starter 层(用 `compileOnly` 声明可选依赖)
|
|
568
|
+
|
|
569
|
+
### 目录与命名
|
|
570
|
+
|
|
571
|
+
- [ ] Service 实现类(含 SPI 实现)是否在 `service/impl/` 目录
|
|
572
|
+
- [ ] 泛型参数数量是否与接口定义一致,没有多写或少写 `?`
|
|
573
|
+
- [ ] `MapperExt` 类是否在 `mapper/` 目录,`Convert` 类是否在 `convert/` 目录
|
|
574
|
+
- [ ] 常量类(状态值、枚举码)是否放在 API 模块的 `constant/` 目录
|
|
575
|
+
|
|
576
|
+
### 多租户安全
|
|
577
|
+
|
|
578
|
+
- [ ] 需要多租户隔离的查询是否使用 `LambdaQueryWrapper`(不使用 `@Select` 注解)
|
|
579
|
+
- [ ] `@Select` 注解 SQL 是否手动补充了 `AND delete_flag = 0 AND tenant_id = #{tenantId}`
|
|
580
|
+
|
|
581
|
+
### 代码质量
|
|
582
|
+
|
|
583
|
+
- [ ] Service 层是否**未出现** `LambdaQueryWrapper`(动态查询移到 `MapperExt`)
|
|
584
|
+
- [ ] Controller 是否**未出现**业务 `if` 判断,以及文件流/响应头处理等业务逻辑
|
|
585
|
+
- [ ] IO 异常是否用 `SystemException` 包装(不用 `RuntimeException`)
|
|
586
|
+
- [ ] 文件流复制是否用 `StreamUtils.copy`(不用手动 `byte[]` 循环)
|
|
587
|
+
- [ ] 写操作(增删改)是否加 `@Transactional(rollbackFor = Exception.class)`
|
|
588
|
+
- [ ] 是否没有 raw type 警告(如 `TaskHandler` 应写 `TaskHandler<?>` 而非 `TaskHandler`)
|
|
589
|
+
- [ ] 跨模块操作是否通过 Service 接口(不直接注入其他业务域的 Mapper)
|
|
590
|
+
- [ ] 对外 VO / RPC DTO 是否不含存储层内部字段(storagePath、bucketName、storageType 等)
|
|
591
|
+
- [ ] DDL 是否完整包含 BaseEntity 11 个公共字段(`remark` 和 `custom_fields` 高频遗漏)
|
|
592
|
+
|
|
593
|
+
### RPC 接口
|
|
594
|
+
|
|
595
|
+
- [ ] 跨服务 Feign 接口是否按「三件套」实现(Constants + Feign Client + LocalStub)
|
|
596
|
+
- [ ] Feign Client 是否声明了 `primary = false`(防止与 LocalStub 产生 Bean 冲突)
|
|
597
|
+
- [ ] RPC Controller 是否加了 `@Hidden` 注解(隐藏于 Swagger 文档)
|
|
598
|
+
- [ ] 服务名(SERVICE_NAME)是否与 `application.yaml` 中的 `spring.application.name` 一致
|
|
599
|
+
|
|
600
|
+
### 日志与链路追踪
|
|
601
|
+
|
|
602
|
+
- [ ] 服务是否已引入 `nebula-boot-starter-trace` 依赖(缺失则 HTTP/定时任务/MQ 均无 traceId)
|
|
603
|
+
- [ ] Log4j2 日志 pattern 中 MDC 取值是否用 `%X{traceId}`(不是 Logback 写法 `%X{traceId:-}`)
|
|
604
|
+
- [ ] Log4j2 日志 pattern 中进程 ID 是否用 `%processId`(不是 `${PID}`,后者在独立加载的 Log4j2 XML 中无值)
|
|
605
|
+
- [ ] 各 profile(dev/uat/prod)的 `logging.config` 是否指向正确的环境配置文件
|
|
606
|
+
- [ ] UAT/PROD 环境是否配置了 ELK JSON 日志文件(供 Filebeat 采集)
|
|
607
|
+
- [ ] 生产环境(prod)是否关闭了 SQL 日志(`jdbc.*` 全部 `OFF`)
|
|
608
|
+
- [ ] `@Scheduled` 定时任务不需要手动注入 traceId,框架 `ScheduledTraceAspect` 自动处理(确认 `spring-boot-starter-aop` 已引入)
|
|
609
|
+
- [ ] MQ 消费者不需要手动设置 traceId,`TraceConsumerContextSetup` 自动从消息 Header 恢复或生成
|
|
610
|
+
- [ ] 异步线程池任务是否通过 `TraceTaskDecorator` 传播 traceId(避免子线程日志无 traceId)
|