Appearance
第78课:企业模式
🎯 学习目标
- 理解企业模式关注的是分层、边界、数据流和职责划分。
- 掌握 Controller、Service、Repository、DTO、DAO、VO、Assembler 等常见角色。
- 能设计清晰的请求模型、响应模型、领域对象和持久化对象。
- 能识别贫血模型、上帝 Service、Entity 泄露、事务边界混乱等问题。
- 能把前面学过的 Spring、Validation、JSON、Swagger、设计模式串成项目结构。
📖 一、企业项目为什么要分层
小程序可以把所有逻辑写在一个类里:
java
@RestController
public class UserController {
@PostMapping("/users")
public User create(@RequestBody User user) {
// 校验参数
// 查数据库
// 加密密码
// 保存用户
// 返回结果
}
}项目变大后会出现:
text
Controller 过胖
数据库结构暴露给前端
业务逻辑难测试
事务边界混乱
重复转换代码到处都是
接口字段一改牵动数据库分层的目标是让每一层只关心自己的职责。
📖 二、典型分层结构
text
interfaces/controller 接口入口:HTTP 参数、响应、状态码
application/service 应用服务:用例编排、事务边界
domain 领域模型:核心业务规则
infrastructure 基础设施:数据库、Redis、消息、外部接口传统 Spring Boot 项目也常见:
text
controller
service
repository/mapper
entity
dto
config
exception两种不是互斥的。关键是职责清楚,而不是包名高级。
📖 三、Controller 模式
Controller 负责 HTTP 层:
text
接收参数
触发 Validation
调用应用服务
返回响应
不写复杂业务逻辑示例:
java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserApplicationService userApplicationService;
@PostMapping
public UserResponse create(@Valid @RequestBody UserCreateRequest request) {
return userApplicationService.create(request);
}
}Controller 不应该:
text
直接写 SQL
直接操作 Redis
编排复杂业务流程
处理大量 if-else 业务规则
返回数据库 Entity📖 四、DTO / Request / Response
DTO 是跨层或跨进程传输数据的对象。
建议区分:
text
UserCreateRequest:创建用户请求
UserUpdateRequest:更新用户请求
UserQueryRequest :查询用户请求
UserResponse :返回给前端的用户数据示例:
java
public class UserCreateRequest {
@NotBlank
private String username;
@NotBlank
private String password;
@Email
private String email;
}响应:
java
public class UserResponse {
private Long id;
private String username;
private String email;
private LocalDateTime createdAt;
}不要用一个 UserDTO 承担所有场景。创建、更新、查询、响应的字段和校验规则通常不同。
📖 五、Entity 与领域对象
Entity 通常对应数据库表:
java
@Entity
@Table(name = "users")
public class UserEntity {
@Id
private Long id;
private String username;
private String passwordHash;
private String email;
}它不应该直接暴露给前端:
java
@GetMapping("/{id}")
public UserEntity getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}问题:
text
密码哈希可能泄露
数据库字段变化影响接口
JPA 懒加载可能序列化失败
双向关联可能递归
接口文档混乱推荐转换为 Response:
java
public UserResponse toResponse(UserEntity entity) {
UserResponse response = new UserResponse();
response.setId(entity.getId());
response.setUsername(entity.getUsername());
response.setEmail(entity.getEmail());
response.setCreatedAt(entity.getCreatedAt());
return response;
}📖 六、Service 模式
Service 负责业务用例和事务边界。
java
@Service
@RequiredArgsConstructor
public class UserApplicationService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserAssembler userAssembler;
@Transactional
public UserResponse create(UserCreateRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("用户名已存在");
}
UserEntity entity = new UserEntity();
entity.setUsername(request.getUsername());
entity.setPasswordHash(passwordEncoder.encode(request.getPassword()));
entity.setEmail(request.getEmail());
UserEntity saved = userRepository.save(entity);
return userAssembler.toResponse(saved);
}
}Service 应该:
text
表达一个用例
控制事务
编排领域对象和基础设施
处理业务异常Service 不应该:
text
包含大量格式转换细节
直接处理 HTTP 请求响应对象
无限膨胀成几千行上帝类📖 七、Repository / DAO 模式
Repository 或 DAO 负责数据访问。
MyBatis 示例:
java
@Mapper
public interface UserMapper {
UserEntity selectById(Long id);
int insert(UserEntity user);
boolean existsByUsername(String username);
}Spring Data JPA 示例:
java
public interface UserRepository extends JpaRepository<UserEntity, Long> {
boolean existsByUsername(String username);
}Repository 不应该包含复杂业务流程。它的职责是查询和持久化。
📖 八、Assembler / Converter
对象转换代码如果散落在 Controller 和 Service,会很快失控。
java
@Component
public class UserAssembler {
public UserResponse toResponse(UserEntity entity) {
UserResponse response = new UserResponse();
response.setId(entity.getId());
response.setUsername(entity.getUsername());
response.setEmail(entity.getEmail());
response.setCreatedAt(entity.getCreatedAt());
return response;
}
}复杂项目可以使用 MapStruct,但简单项目手写转换更直观。
📖 九、统一响应和异常
统一响应:
java
public class ApiResponse<T> {
private String code;
private String message;
private T data;
}统一异常:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException e) {
return ResponseEntity.badRequest()
.body(ApiResponse.fail(e.getCode(), e.getMessage()));
}
}这样接口层能保持一致,Swagger 文档也更清晰。
📖 十、事务边界
事务通常放在应用服务层:
java
@Transactional
public OrderResponse createOrder(OrderCreateRequest request) {
// 校验
// 扣库存
// 创建订单
// 写订单明细
}避免:
text
Controller 上开事务
Repository 上开大事务
事务内做远程 HTTP 调用
事务方法内部自调用导致注解失效事务是企业模式里最容易被忽略的边界问题。
⚠️ 十一、常见陷阱
1. Controller 过胖
Controller 如果超过几百行,通常说明业务逻辑没有下沉。
2. Entity 泄露
数据库结构和 API 契约耦合,会让系统越来越难改。
3. 上帝 Service
一个 OrderService 什么都做,最终会变成无法测试和维护的大类。
4. DTO 复用过度
创建、更新、查询、响应强行共用一个 DTO,会导致字段含义混乱和校验规则冲突。
5. 包名高级但职责混乱
DDD 包名不能自动带来好设计。职责边界比术语更重要。
🆚 十二、Java vs C 对比
| 维度 | C 项目 | Java 企业项目 |
|---|---|---|
| 分层 | 函数模块、头文件、源文件 | Controller/Service/Repository/DTO |
| 数据结构 | struct 常跨层传递 | DTO/Entity/Domain 分离 |
| 事务 | 手动控制连接和提交 | @Transactional 代理管理 |
| 对象转换 | 手动字段赋值 | Assembler/MapStruct |
| Web 边界 | 依赖具体框架 | Spring MVC 标准化 |
Java 企业模式的核心价值是把 Web、业务、持久化和基础设施分开,让复杂系统能长期演进。
💡 十三、最佳实践
- Controller 保持薄,只处理 HTTP 边界。
- Service 表达业务用例,控制事务边界。
- Repository/DAO 只负责数据访问。
- Request 和 Response DTO 分开建模。
- Entity 不直接返回给前端。
- 复杂转换集中到 Assembler/Converter。
- 全局异常和统一响应要尽早建立。
- 事务内不要做远程调用,必要时用事件或消息拆分。
- 先保证职责清晰,再考虑是否引入 DDD 复杂结构。
🎓 小结
企业模式不是某个单独的设计模式,而是一组工程约定:分层、DTO 隔离、事务边界、统一异常、对象转换、接口契约和数据访问边界。
掌握这些模式后,你写出的 Spring Boot 项目才不会只是“能跑”,而是能维护、能测试、能扩展、能交给团队协作。