1. 项目概述
作为一个在Java后端开发领域摸爬滚打多年的老码农,我见过太多因为项目目录结构混乱而导致的维护噩梦。今天我们就来聊聊REST API项目中最经典的三层架构目录规划,这可不是简单的"分几个包"那么简单,而是关乎整个项目生命周期的工程实践。
三层架构(Controller-Service-DAO)看似基础,但90%的团队都没用对。合理的目录结构应该像城市道路规划一样,让不同职能的代码各归其位,同时为未来的扩展预留空间。我参与过十几个从零到百万级用户量的Java项目,发现前期合理的目录规划能为后期节省至少30%的维护成本。
2. 三层架构核心解析
2.1 传统三层架构的现代演进
经典的Controller-Service-DAO三层在微服务时代已经进化出了更多细节:
code复制src/main/java
├── com.example.project
│ ├── application # 应用层(原Controller)
│ ├── domain # 领域层(原Service)
│ ├── infrastructure # 基础设施层(原DAO)
│ └── shared # 共享组件
这种改进版的DDD风格分层更符合现代Java项目的需求。以用户注册为例:
- application层处理HTTP协议转换
- domain层实现业务规则校验
- infrastructure层完成数据库持久化
2.2 各层职责边界定义
重要原则:上层可以调用下层,下层绝对不允许调用上层
-
Application层:
- 只做参数校验和DTO转换
- 禁止出现业务逻辑
- 典型类命名:XxxController、XxxDTO
-
Domain层:
- 核心业务逻辑所在地
- 应该保持POJO纯净性
- 典型类命名:XxxService、XxxEntity
-
Infrastructure层:
- 数据库访问实现
- 第三方服务集成
- 典型类命名:XxxRepository、XxxMapper
3. 实战目录规划
3.1 基础目录结构示例
这是经过多个生产项目验证的标准结构:
code复制src/main/java
├── com.company.project
│ ├── api # Application层
│ │ ├── v1 # API版本控制
│ │ │ ├── request
│ │ │ ├── response
│ │ │ └── UserController.java
│ │ └── config # Web相关配置
│ ├── domain # Domain层
│ │ ├── model # 领域对象
│ │ ├── service
│ │ └── event # 领域事件
│ ├── infrastructure # Infrastructure层
│ │ ├── dao # 数据访问
│ │ ├── cache # 缓存实现
│ │ └── client # 外部服务调用
│ └── common # 公共组件
│ ├── exception
│ ├── util
│ └── constants
3.2 关键目录详解
api/v1目录:
- 采用API版本化设计,v1表示第一版API
- request/response子目录存放各接口的DTO
- 每个Controller不超过300行代码
domain/model:
- 使用贫血模型还是充血模型需要团队统一
- 推荐结合Lombok减少样板代码
- 示例User实体:
java复制@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String username;
private UserStatus status; // 枚举类型
public boolean isActive() {
return status == UserStatus.ACTIVE;
}
}
4. 进阶架构建议
4.1 模块化拆分策略
当项目规模超过10万行代码时,建议采用模块化:
code复制project/
├── user-module/
│ ├── src/
│ └── pom.xml
├── order-module/
│ ├── src/
│ └── pom.xml
└── pom.xml # 父POM
每个模块内部仍然保持三层结构,通过maven/gradle进行依赖管理。
4.2 测试目录匹配
测试代码应该镜像主代码结构:
code复制src/test/java
└── com.company.project
├── api
├── domain
└── infrastructure
测试命名规范:
- 单元测试:XxxTest
- 集成测试:XxxIT
- 控制器测试:XxxControllerTest
5. 常见陷阱与解决方案
5.1 循环依赖问题
典型症状:ServiceA依赖ServiceB,ServiceB又依赖ServiceA
解决方案:
- 提取公共逻辑到第三个Service
- 使用事件驱动架构
- 应用CQRS模式分离读写
5.2 层间参数传递污染
错误示例:
java复制// Controller直接传递HttpServletRequest到Service
userService.create(request);
正确做法:
java复制// Controller中提取参数
UserCreateCommand command = UserCreateCommand.from(request);
userService.create(command);
5.3 事务管理边界
经验法则:
- 事务应该在Service层开启
- 不要在Controller加@Transactional
- 跨Service调用需要特别注意事务传播
推荐配置:
java复制@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional
public User create(UserCreateCommand command) {
// 业务逻辑
}
}
6. 工具链推荐
6.1 静态分析工具
- ArchUnit:验证架构约束
java复制@ArchTest
static final ArchRule layer_dependencies = layeredArchitecture()
.layer("Controller").definedBy("..api..")
.layer("Service").definedBy("..domain..")
.layer("Repository").definedBy("..infrastructure..")
.whereLayer("Controller").mayNotBeAccessedByAnyLayer()
.whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
.whereLayer("Repository").mayOnlyBeAccessedByLayers("Service");
- Checkstyle:强制代码规范
- SpotBugs:检测潜在BUG
6.2 代码生成工具
- MapStruct:DTO转换
- QueryDSL:类型安全查询
- Spring Data REST:快速生成API
7. 性能优化要点
7.1 N+1查询问题
典型场景:
java复制// Service方法
public List<UserDTO> listUsers() {
List<User> users = userRepository.findAll(); // 第一次查询
return users.stream()
.map(user -> {
Profile profile = profileRepository.findByUserId(user.getId()); // 第N次查询
return UserDTO.from(user, profile);
}).toList();
}
解决方案:
- 使用JOIN FETCH
java复制@Query("SELECT u FROM User u JOIN FETCH u.profile")
List<User> findAllWithProfile();
- 使用@EntityGraph
- 批量查询后内存组装
7.2 缓存策略设计
推荐多级缓存方案:
- 方法级缓存:@Cacheable
- 分布式缓存:Redis
- 本地缓存:Caffeine
缓存注解最佳实践:
java复制@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getById(Long id) { ... }
@CacheEvict(value = "users", key = "#user.id")
public void update(User user) { ... }
}
8. 微服务适配方案
当项目演进为微服务架构时,目录结构需要调整:
code复制user-service/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com.company.user
│ │ │ ├── api
│ │ │ ├── domain
│ │ │ └── infrastructure
│ │ └── resources/
│ │ ├── application.yml
│ │ └── bootstrap.yml
│ └── test/
└── pom.xml
关键变化:
- 增加服务发现配置
- 添加Feign客户端接口
- 引入分布式追踪
- 独立数据库schema
9. 代码评审要点
在评审三层架构代码时,我通常会重点检查:
- 跨层调用:DAO层是否出现业务逻辑
- DTO膨胀:是否有多余的字段转换
- 异常处理:是否合理使用自定义异常
- 测试覆盖:各层是否有对应测试
- 依赖管理:是否有循环依赖
常见坏味道示例:
java复制// 错误:Controller处理业务逻辑
@PostMapping
public ResponseEntity create(@RequestBody User user) {
if (user.getAge() < 18) {
throw new IllegalArgumentException("Age must >= 18");
}
// ...
}
// 正确:业务逻辑应该在Service层
@PostMapping
public ResponseEntity create(@RequestBody UserCreateRequest request) {
User user = userService.create(request.toCommand());
return ResponseEntity.ok(UserResponse.from(user));
}
10. 演进式架构建议
好的目录结构应该能适应业务发展:
-
初期(0-5万行):
- 严格三层架构
- 按功能分包(user/order等)
-
中期(5-20万行):
- 引入模块化
- 分离读写模型
-
后期(20万+行):
- 演进为微服务
- 采用CQRS模式
- 引入事件溯源
记住:目录结构不是一成不变的,应该每半年评估一次是否需要调整。我在实际项目中发现,当出现以下信号时就需要考虑重构目录:
- 单个包超过20个类
- 经常需要跨多个包修改代码
- 新成员需要超过1天才能找到相关代码位置