@umacloud/knowledge 1.0.46 → 1.0.48

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.
@@ -0,0 +1,412 @@
1
+ ---
2
+ id: stack-java-spring-engineering-standards
3
+ title: Java + Spring Boot 工程规范(商业级·分层·反屎山)
4
+ domain: development
5
+ category: 01-standards
6
+ difficulty: advanced
7
+ tags: [Java, Spring, SpringBoot, 分层, 分包, 命名规范, DDD, MyBatis, JPA, 反屎山, backend]
8
+ quality_score: 92
9
+ last_updated: 2026-07-12
10
+ ---
11
+
12
+ # Java + Spring Boot 工程规范(商业级·分层·反屎山)
13
+
14
+ > 面向 Spring Boot / jeecg-boot 技术栈的硬性结构标准。商业级后端不是"Controller 里写完 SQL 能跑就行",而是**严格分层、职责单向依赖、业务逻辑只在 Service/Domain、数据库对象绝不裸奔到前端、单类单方法有体积上限**。写第一个 `@RestController` 前先定好包骨架,再填实现。这是 STACK 层的具体落地;语言无关的方法论在同目录另一份文件里,此处只讲 Java/Spring 的可执行细节。
15
+
16
+ ## 0. 一句话原则
17
+
18
+ **请求自外向内穿过 Controller → Service → Mapper/Repository,依赖永远单向;表结构(Entity)不出 Service 层,出口是 DTO/VO;每一层只做自己那一件事。**
19
+
20
+ ## 1. 分层模型与依赖方向
21
+
22
+ ```
23
+ HTTP 请求
24
+ └▶ Controller 参数接收 + @Valid 校验 + 编排调用,不写业务
25
+ └▶ Service(接口) 业务规则、事务边界、领域编排的唯一归属
26
+ └▶ ServiceImpl 实现,@Transactional 挂这里
27
+ └▶ Mapper/Repository 纯数据访问,不写业务判断
28
+ └▶ DB
29
+ Entity ──(仅在 Service 内部流转)── Convert ──▶ VO/DTO ──▶ 返回前端
30
+ ```
31
+
32
+ - **单向依赖**:Controller 依赖 Service 接口,Service 依赖 Mapper;反向禁止(Mapper 不得回调 Service,Service 不得 import Controller)。
33
+ - **面向接口**:Service 拆 `XxxService`(接口)+ `XxxServiceImpl`(实现),Controller 只注入接口,便于替换与测试。Mapper/Repository 是接口本身,无需再包一层。
34
+ - **依赖注入用构造器**,配合 `final` 字段,禁止 `@Autowired` 字段注入(不可测、隐藏依赖、易出空指针)。
35
+
36
+ 正例(构造器注入 + 面向接口):
37
+
38
+ ```java
39
+ @RestController
40
+ @RequestMapping("/api/orders")
41
+ public class OrderController {
42
+ private final OrderService orderService; // 依赖接口,final,构造器注入
43
+
44
+ public OrderController(OrderService orderService) {
45
+ this.orderService = orderService;
46
+ }
47
+ }
48
+ ```
49
+
50
+ 反例(字段注入 + 依赖实现类):
51
+
52
+ ```java
53
+ @RestController
54
+ public class OrderController {
55
+ @Autowired
56
+ private OrderServiceImpl orderService; // 依赖具体实现、字段注入,不可测、耦合死
57
+ }
58
+ ```
59
+
60
+ ## 2. 包结构(feature-based,按业务域分包)
61
+
62
+ **默认按业务域(feature)分包,不按技术类型把全项目 Controller/Service 堆成三大筐。** 改一个"订单"需求应该只动 `order/` 一个目录,而不是翻遍 `controller/`、`service/`、`mapper/` 三处。
63
+
64
+ 单模块工程标准骨架:
65
+
66
+ ```
67
+ com.example.app
68
+ ├─ order/ # 业务域:订单
69
+ │ ├─ controller/ OrderController.java
70
+ │ ├─ service/ OrderService.java # 接口
71
+ │ │ └─ impl/ OrderServiceImpl.java # 实现
72
+ │ ├─ mapper/ OrderMapper.java # MyBatis Mapper / 或 repository/ 放 JPA Repository
73
+ │ ├─ entity/ OrderEntity.java # 数据库映射对象(表结构)
74
+ │ ├─ dto/ OrderCreateDTO.java # 入参:接收前端/上游
75
+ │ ├─ vo/ OrderDetailVO.java # 出参:返回前端的视图对象
76
+ │ ├─ convert/ OrderConvert.java # Entity <-> DTO/VO 转换(MapStruct)
77
+ │ ├─ enums/ OrderStatusEnum.java
78
+ │ └─ config/ OrderProperties.java # 该域的配置绑定
79
+ ├─ user/ # 业务域:用户(同构)
80
+ ├─ common/ # 跨域基础:Result、异常、BaseEntity、通用工具
81
+ │ ├─ exception/ BizException.java, GlobalExceptionHandler.java
82
+ │ ├─ response/ Result.java, ResultCode.java
83
+ │ └─ base/ BaseEntity.java, PageQuery.java
84
+ └─ config/ # 全局装配:MyBatisPlusConfig, WebMvcConfig, 安全配置
85
+ ```
86
+
87
+ 多模块(Maven `<modules>`)在规模变大时再拆,典型分:`app-api`(对外接口/DTO/VO)、`app-service`(业务实现)、`app-dao`(Entity/Mapper)、`app-common`(基础设施)。依赖方向 `api → service → dao → common`,禁止反向与环依赖。**中小项目先单模块按域分包,不要过早上多模块。**
88
+
89
+ - 跨域调用只通过对方 Service 接口,禁止直接 import 对方的 Mapper/Entity。
90
+ - `common` 只放真正跨域复用的基础件;不要变成第二个垃圾场。
91
+
92
+ ## 3. 命名规范
93
+
94
+ | 元素 | 约定 | 示例 |
95
+ |---|---|---|
96
+ | 包名 | 全小写,业务域单数名词 | `com.example.app.order` |
97
+ | 类名 | 大驼峰,名词 | `OrderService` |
98
+ | 接口实现 | 接口 `XxxService`,实现 `XxxServiceImpl` | `OrderServiceImpl` |
99
+ | 方法名 | 小驼峰动词开头 | `createOrder`、`listByUserId` |
100
+ | 常量 | 全大写下划线 | `MAX_RETRY_COUNT` |
101
+ | 后缀约定 | `Controller`/`Service`/`ServiceImpl`/`Mapper`/`Repository`/`Entity`/`DTO`/`VO`/`Convert`/`Enum` | 见名知层 |
102
+
103
+ 方法命名建议动词统一:查单条 `getXxx`/`findById`,查列表 `listXxx`,分页 `pageXxx`,存在性 `existsXxx`,计数 `countXxx`,新增 `createXxx`/`saveXxx`,改 `updateXxx`,删 `removeXxx`/`deleteXxx`。
104
+
105
+ 字段类型的硬规则:
106
+
107
+ ```java
108
+ // 布尔字段:不要用 isXxx 命名成员变量——Lombok/Jackson 生成的 getter 会踩坑
109
+ private Boolean enabled; // 正:getEnabled(),序列化字段名稳定
110
+ // private boolean isEnabled; // 反:基本类型 + is 前缀,getter 变 isEnabled(),
111
+ // Jackson 可能序列化成 "enabled" 导致前后端字段名不一致
112
+
113
+ // 时间字段:统一 LocalDateTime,字段名以 Time 结尾,不要用 java.util.Date
114
+ private LocalDateTime createTime; // 正
115
+ private LocalDateTime payTime; // 正
116
+ // private Date create_time; // 反:Date 已过时、蛇形命名、无时区语义
117
+
118
+ // 金额:一律 BigDecimal,禁止 double/float(浮点丢精度)
119
+ private BigDecimal amount; // 正
120
+ // private double amount; // 反:0.1+0.2 != 0.3,财务数据必错
121
+ // 运算与比较:
122
+ BigDecimal total = price.multiply(new BigDecimal(qty))
123
+ .setScale(2, RoundingMode.HALF_UP); // 显式精度与舍入
124
+ if (total.compareTo(BigDecimal.ZERO) > 0) { } // 用 compareTo,禁止 equals 比较金额
125
+ ```
126
+
127
+ 枚举承载状态,禁止魔法数字散落:
128
+
129
+ ```java
130
+ // 正:状态用枚举,带 code 和描述
131
+ public enum OrderStatusEnum {
132
+ CREATED(0, "已创建"), PAID(1, "已支付"), CANCELLED(2, "已取消");
133
+ private final int code;
134
+ private final String desc;
135
+ OrderStatusEnum(int code, String desc) { this.code = code; this.desc = desc; }
136
+ public int getCode() { return code; }
137
+ public String getDesc() { return desc; }
138
+ }
139
+ // 反:if (status == 1) { ... } // 魔法数字,读者无法知道 1 是什么
140
+ ```
141
+
142
+ ## 4. DTO / VO / Entity 严格分离
143
+
144
+ 三者职责不同,绝不混用:
145
+
146
+ - **Entity**:数据库表映射,字段=列。带 `@TableName`/`@Entity`,可能含 `deleted`、`version`、`createBy` 等基础设施字段。
147
+ - **DTO**:接收入参(Data Transfer Object),带校验注解,只含前端应当提交的字段。
148
+ - **VO**:返回出参(View Object),只含前端需要展示的字段,可含拼装/脱敏后的派生字段。
149
+
150
+ **为什么 Entity 绝不能直接返给前端**:泄露表结构与内部字段(密码散列、逻辑删除位、内部备注);字段随表结构改动而破坏 API 契约;懒加载关联在序列化时触发 N+1 或 `LazyInitializationException`;无法按场景裁剪/脱敏。
151
+
152
+ 正例(MapStruct 转换,Entity 不出边界):
153
+
154
+ ```java
155
+ @Mapper(componentModel = "spring")
156
+ public interface OrderConvert {
157
+ OrderEntity toEntity(OrderCreateDTO dto);
158
+ OrderDetailVO toVO(OrderEntity entity);
159
+ List<OrderDetailVO> toVOList(List<OrderEntity> list);
160
+ }
161
+
162
+ @RestController
163
+ @RequestMapping("/api/orders")
164
+ public class OrderController {
165
+ private final OrderService orderService;
166
+ private final OrderConvert orderConvert;
167
+ // ...构造器省略
168
+
169
+ @GetMapping("/{id}")
170
+ public Result<OrderDetailVO> detail(@PathVariable Long id) {
171
+ OrderEntity entity = orderService.getById(id);
172
+ return Result.ok(orderConvert.toVO(entity)); // 出口是 VO
173
+ }
174
+ }
175
+ ```
176
+
177
+ 反例(直接把 Entity 甩给前端):
178
+
179
+ ```java
180
+ @GetMapping("/{id}")
181
+ public OrderEntity detail(@PathVariable Long id) {
182
+ return orderService.getById(id); // 反:表结构外泄、字段耦合、脱敏无从谈起
183
+ }
184
+ ```
185
+
186
+ ## 5. Service 层纪律与事务边界
187
+
188
+ - **业务规则只允许存在于 Service/Domain**。Controller 只做:接收参数、`@Valid` 校验、调用 Service、包装返回。校验、状态流转、金额计算、库存扣减等一律下沉。
189
+ - **`@Transactional` 挂在 Service 实现方法上,不挂 Controller**。事务要包住一组必须原子的写操作,边界清晰。
190
+ - **自调用事务失效陷阱**:同类内 A 方法直接调用本类带 `@Transactional` 的 B 方法,代理不生效、事务不开启。拆到另一个 Bean,或注入自身代理调用。
191
+
192
+ 反例(业务逻辑塞进 Controller + 事务放错层):
193
+
194
+ ```java
195
+ @PostMapping("/pay")
196
+ @Transactional // 反:事务挂 Controller,代理层级不对,且 Controller 不该管事务
197
+ public Result<Void> pay(@RequestBody PayDTO dto) {
198
+ OrderEntity order = orderMapper.selectById(dto.getOrderId()); // 反:Controller 直连 Mapper
199
+ if (order.getStatus() != 1) { // 反:业务判断在 Controller
200
+ return Result.fail("状态不对");
201
+ }
202
+ order.setStatus(2);
203
+ order.setPayTime(LocalDateTime.now());
204
+ orderMapper.updateById(order);
205
+ accountMapper.deduct(dto.getUserId(), order.getAmount()); // 反:多写无原子保证
206
+ return Result.ok();
207
+ }
208
+ ```
209
+
210
+ 重构正例(编排在 Controller,业务与事务在 Service):
211
+
212
+ ```java
213
+ // Controller:只做校验 + 编排
214
+ @PostMapping("/pay")
215
+ public Result<Void> pay(@Valid @RequestBody PayDTO dto) {
216
+ orderService.pay(dto);
217
+ return Result.ok();
218
+ }
219
+
220
+ // ServiceImpl:业务规则 + 事务边界
221
+ @Service
222
+ public class OrderServiceImpl implements OrderService {
223
+ private final OrderMapper orderMapper;
224
+ private final AccountService accountService;
225
+ // ...构造器省略
226
+
227
+ @Override
228
+ @Transactional(rollbackFor = Exception.class) // 正:原子写在 Service,任何异常回滚
229
+ public void pay(PayDTO dto) {
230
+ OrderEntity order = orderMapper.selectById(dto.getOrderId());
231
+ if (order == null) {
232
+ throw new BizException(ResultCode.ORDER_NOT_FOUND);
233
+ }
234
+ if (order.getStatus() != OrderStatusEnum.CREATED.getCode()) {
235
+ throw new BizException(ResultCode.ORDER_STATUS_ILLEGAL); // 业务规则在此
236
+ }
237
+ order.setStatus(OrderStatusEnum.PAID.getCode());
238
+ order.setPayTime(LocalDateTime.now());
239
+ orderMapper.updateById(order);
240
+ accountService.deduct(dto.getUserId(), order.getAmount()); // 同事务内
241
+ }
242
+ }
243
+ ```
244
+
245
+ ## 6. 反屎山硬规则(超标即打回)
246
+
247
+ - **单类 ≤ 400–500 行**:超了说明职责过多,按内聚拆分(如 `OrderQueryService` / `OrderCommandService`)。
248
+ - **单方法 ≤ 50–80 行**:超了抽私有方法,一个方法只讲一件事。
249
+ - **圈复杂度 ≤ 10–15**:分支/循环过密就拆,或用策略/状态映射替代长 `if-else`/`switch`。
250
+ - **方法参数 ≤ 4**:超了用参数对象(DTO/Query)或 Builder 聚合。
251
+ - **嵌套 ≤ 3 层**:用卫语句(guard clause)早返回,把异常/边界前置,主逻辑保持平铺。
252
+ - **禁 God Service**:一个 `XxxService` 塞几十个不相关方法、上千行——按用例拆。
253
+ - **禁 `Utils`/`CommonUtil` 黑洞**:不要建一个什么都往里塞的静态工具类。工具按主题归类(`MoneyUtils`、`DateUtils`),与业务相关的 helper 放回对应业务域,不进通用工具。
254
+ - **禁在 Entity 里堆业务方法**:Entity 是数据载体(充血领域模型是另一套刻意设计,非默认);默认贫血 + Service 承载业务。
255
+ - **DAO/Mapper 不写业务判断**:Mapper 只做取数/存数,`if 状态==x 则...` 属于 Service。
256
+
257
+ 参数对象正例:
258
+
259
+ ```java
260
+ // 反:一堆平铺参数,调用点全是位置含义不明的实参
261
+ public Page<OrderVO> query(String keyword, Integer status, LocalDateTime start,
262
+ LocalDateTime end, Long userId, int pageNo, int pageSize) { }
263
+
264
+ // 正:聚合成查询对象
265
+ public Page<OrderVO> query(OrderPageQuery query) { }
266
+
267
+ @Data
268
+ public class OrderPageQuery extends PageQuery { // PageQuery 提供 pageNo/pageSize
269
+ private String keyword;
270
+ private Integer status;
271
+ private LocalDateTime startTime;
272
+ private LocalDateTime endTime;
273
+ private Long userId;
274
+ }
275
+ ```
276
+
277
+ 卫语句降嵌套正例:
278
+
279
+ ```java
280
+ // 反:金字塔嵌套
281
+ public void handle(Order o) {
282
+ if (o != null) {
283
+ if (o.getStatus() == 1) {
284
+ if (o.getAmount() != null) {
285
+ // 真正逻辑埋在三层里
286
+ }
287
+ }
288
+ }
289
+ }
290
+ // 正:早返回,主逻辑平铺
291
+ public void handle(Order o) {
292
+ if (o == null) return;
293
+ if (o.getStatus() != 1) return;
294
+ if (o.getAmount() == null) return;
295
+ // 真正逻辑在顶层
296
+ }
297
+ ```
298
+
299
+ ## 7. 异常处理与统一返回
300
+
301
+ - **统一返回体 `Result<T>`**:所有接口返回 `Result<T>`,含 `code` / `message` / `data`。不返回裸 `Map`、裸实体、裸字符串。
302
+ - **统一异常处理 `@RestControllerAdvice`**:业务异常抛 `BizException`,全局处理器兜底转成标准 `Result`;Controller 里不写满屏 try-catch。
303
+ - **错误码分级**:成功 `0`/`200`;业务错误用带域前缀的错误码枚举(如 `ORDER_NOT_FOUND`);系统错误统一 `500` 并记录日志、不把堆栈泄给前端。
304
+ - **不吞异常**:禁止 `catch (Exception e) {}` 空吞或只 `e.printStackTrace()`。要么处理、要么带上下文 `log.error` 后重新抛出。
305
+ - **参数校验用 `@Valid` + 分组**:新增/更新用不同校验组(`Create.class` / `Update.class`),避免"更新时 id 必填、新增时 id 必空"互相打架。
306
+
307
+ 正例:
308
+
309
+ ```java
310
+ @Data
311
+ public class OrderCreateDTO {
312
+ @NotNull(message = "商品ID不能为空")
313
+ private Long productId;
314
+
315
+ @NotNull @Min(value = 1, message = "数量至少为1")
316
+ private Integer quantity;
317
+ }
318
+
319
+ @RestControllerAdvice
320
+ public class GlobalExceptionHandler {
321
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
322
+
323
+ @ExceptionHandler(BizException.class)
324
+ public Result<Void> handleBiz(BizException e) {
325
+ return Result.fail(e.getCode(), e.getMessage()); // 业务错误,不记 error 级
326
+ }
327
+
328
+ @ExceptionHandler(MethodArgumentNotValidException.class)
329
+ public Result<Void> handleValid(MethodArgumentNotValidException e) {
330
+ String msg = e.getBindingResult().getFieldError().getDefaultMessage();
331
+ return Result.fail(ResultCode.PARAM_INVALID.getCode(), msg);
332
+ }
333
+
334
+ @ExceptionHandler(Exception.class)
335
+ public Result<Void> handleSystem(Exception e) {
336
+ log.error("系统异常", e); // 记全堆栈到日志
337
+ return Result.fail(ResultCode.SYSTEM_ERROR); // 只给前端脱敏提示
338
+ }
339
+ }
340
+ ```
341
+
342
+ 反例:
343
+
344
+ ```java
345
+ @GetMapping("/{id}")
346
+ public Map<String, Object> detail(@PathVariable Long id) { // 反:裸 Map,无契约
347
+ Map<String, Object> map = new HashMap<>();
348
+ try {
349
+ map.put("data", orderService.getById(id));
350
+ } catch (Exception e) {
351
+ // 反:空吞异常,前端只拿到空 data,问题被掩盖
352
+ }
353
+ return map;
354
+ }
355
+ ```
356
+
357
+ ## 8. 数据访问规范(MyBatis / JPA)
358
+
359
+ - **禁 `SELECT *`**:显式列出字段,避免多传数据、避免表加列后行为漂移;只需部分字段就用 DTO 投影。
360
+ - **必须分页**:列表查询强制分页(MyBatis-Plus `Page` / JPA `Pageable`),禁止无 `LIMIT` 全表捞。
361
+ - **消灭 N+1**:需要关联数据时用 `JOIN` 一次查全,或 JPA `@EntityGraph` / `join fetch`;禁止先查列表再循环逐条查详情。
362
+ - **禁在循环里查库**:`for` 循环内 `selectById` 是 N+1 的典型;改为 `selectBatchIds(ids)` / `IN` 批量查一次再内存组装。
363
+ - **索引意识**:`WHERE`/`ORDER BY` 命中的列要有索引;不在索引列上套函数(`WHERE DATE(create_time)=...` 使索引失效),改用范围查询。
364
+ - **DTO 投影**:只取需要的列直接映射到 VO/DTO,减少 IO 与序列化开销。
365
+
366
+ N+1 反例与批量正例:
367
+
368
+ ```java
369
+ // 反:循环内逐条查库,100 个订单打 101 次 SQL
370
+ List<OrderEntity> orders = orderMapper.selectList(null);
371
+ for (OrderEntity o : orders) {
372
+ UserEntity u = userMapper.selectById(o.getUserId()); // N+1
373
+ o.setUserName(u.getName());
374
+ }
375
+
376
+ // 正:批量取 ID 一次查回,内存组装
377
+ List<OrderEntity> orders = orderMapper.selectPage(page, wrapper).getRecords();
378
+ Set<Long> userIds = orders.stream().map(OrderEntity::getUserId).collect(toSet());
379
+ Map<Long, UserEntity> userMap = userMapper.selectBatchIds(userIds).stream()
380
+ .collect(toMap(UserEntity::getId, u -> u));
381
+ orders.forEach(o -> o.setUserName(userMap.get(o.getUserId()).getName()));
382
+ ```
383
+
384
+ `SELECT *` 与投影:
385
+
386
+ ```xml
387
+ <!-- 反:SELECT *,多取列、表结构变化即受影响 -->
388
+ <select id="list" resultType="OrderEntity">SELECT * FROM t_order</select>
389
+
390
+ <!-- 正:显式列 + 投影到 VO,只取需要的字段 -->
391
+ <select id="listVO" resultType="com.example.app.order.vo.OrderListVO">
392
+ SELECT id, order_no, amount, status, create_time
393
+ FROM t_order
394
+ WHERE deleted = 0 AND user_id = #{userId}
395
+ ORDER BY create_time DESC
396
+ </select>
397
+ ```
398
+
399
+ ## 9. 评审清单(写完后逐条勾选)
400
+
401
+ - [ ] 分层单向:Controller 不含业务、不直连 Mapper;Service 承载业务与事务;Mapper 只取存数据。
402
+ - [ ] 按业务域分包,一个需求集中在一个域目录;跨域只经 Service 接口,不深层 import 对方 Entity/Mapper。
403
+ - [ ] Service 面向接口 + 构造器注入 `final` 依赖;无字段 `@Autowired`、无依赖具体实现类。
404
+ - [ ] DTO(入参 + 校验)/ VO(出参 + 脱敏)/ Entity(表映射)三者分离,Entity 绝不返给前端,用 Convert/MapStruct 转换。
405
+ - [ ] 命名合规:后缀约定到位;布尔用包装 `Boolean`、时间用 `LocalDateTime` 且以 Time 结尾、金额用 `BigDecimal` 且显式精度、状态用枚举无魔法数字。
406
+ - [ ] `@Transactional` 在 Service 实现且 `rollbackFor = Exception.class`;无自调用导致的事务失效。
407
+ - [ ] 反屎山达标:类 ≤ 500 行、方法 ≤ 80 行、参数 ≤ 4、嵌套 ≤ 3(卫语句早返回);无 God Service、无 Utils 黑洞、无实体堆业务。
408
+ - [ ] 统一 `Result<T>` 返回 + `@RestControllerAdvice` 兜底 + 分级错误码;不吞异常、不返裸 Map;`@Valid` 分组校验。
409
+ - [ ] 数据访问:无 `SELECT *`、列表必分页、无循环查库/无 N+1、索引列不套函数、需要时用 DTO 投影。
410
+
411
+ ---
412
+ **定位**:本文件是 Java/Spring Boot 的 STACK 具体落地;结构分层、依赖方向、反屎山阈值的通用推理见同目录语言无关方法论文件,二者配合注入。