在搭建完项目骨架之后,代码如何组织便成为第一个架构决策。Spring Boot 本身并没有强制分层规范,但行业实践中形成了一套清晰的分层模型,既能满足单体应用的需求,也便于未来向微服务演进。遵循这套约定,可以让团队成员快速理解代码归属,降低沟通成本。
2.3.1 通用分层架构
最经典的分层方式是将应用划分为 表示层、业务层、数据访问层,并辅以通用工具层和领域模型层。在 Spring Boot 项目中,通常对应以下包结构:
com.example.demo
├── controller # 表示层 - REST API
├── service # 业务层 - 接口定义
│ └── impl # 业务层 - 实现类
├── repository # 数据访问层 - Repository 接口
│ └── entity # 持久化对象(PO)
├── model # 通用模型(DTO、VO、请求/响应对象)
│ ├── dto
│ └── vo
├── config # 配置类
├── common # 通用工具、异常、常量
│ ├── exception
│ ├── utils
│ └── constant
├── security # 安全相关(可选)
└── DemoApplication.java
各层的职责与边界如下:
1. controller 层(表示层)
负责接收 HTTP 请求,进行参数校验,调用业务层,并封装响应结果。Controller 应该尽可能“薄”,不包含业务逻辑,只做流程编排和数据转换。
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<UserVO> create(@Valid @RequestBody UserCreateRequest request) {
UserVO user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
}
关键原则:
- 使用
@Valid或@Validated触发 Bean Validation,在进入业务层之前拦截非法输入。 - 参数与返回类型应使用专门的前端交互对象(如
XxxRequest、XxxResponse、XxxVO),绝不直接暴露持久化实体。
2. service 层(业务层)
封装核心业务逻辑,是应用中最重要、代码量最大的层。通常分为接口和实现两个包,接口定义契约,实现类承载具体逻辑。
// 接口
public interface UserService {
UserVO createUser(UserCreateRequest request);
UserVO getUserById(Long id);
}
// 实现
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
@Transactional
public UserVO createUser(UserCreateRequest request) {
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException("用户名已存在");
}
UserEntity entity = new UserEntity();
entity.setUsername(request.getUsername());
entity.setPassword(passwordEncoder.encode(request.getPassword()));
userRepository.save(entity);
return UserConverter.toVO(entity);
}
}
原则:
- 事务边界定义在 service 层,使用
@Transactional通常标注在实现类的公共方法上。 - 业务校验与规则集中在此层,不要泄漏到 controller 或 repository 中。
- 调用外部服务、消息发送等也应在 service 层完成,保持 controller 的单一职责。
3. repository 层(数据访问层)
负责与数据库交互,Spring Data JPA 下通常为接口,无需实现类,框架自动代理。
public interface UserRepository extends JpaRepository<UserEntity, Long> {
boolean existsByUsername(String username);
Optional<UserEntity> findByUsername(String username);
}
- 方法命名遵循 Spring Data 的关键字规则(
findBy...,existsBy...),复杂查询可使用@Query。 - 只承担 CRUD 操作,不包含业务逻辑(例如密码加密不应在此层)。
- 若使用 MyBatis,则对应
mapper包,Mapper 接口功能类似,但需搭配 XML 或注解定义 SQL。
4. model 层与数据对象划分
真实项目中,内部使用的数据结构非常多样,应在命名上明确区分:
- Entity / PO(Persistent Object):与数据库表映射的持久化对象,放在
repository.entity或entity包。 - DTO(Data Transfer Object):服务间或模块间传输数据的对象,放在
model.dto。 - VO(View Object):返回给前端的视图对象,放在
model.vo,可根据前端需求裁剪字段,不应直接返回 Entity。 - Request / Response:用于接收请求参数和封装响应体,通常放在
model.request和model.response,或者直接放在 controller 包内的request/response子包。
这种划分起初会带来转换工作,但有效隔离了前端展示、接口协议和数据库设计的变动,长期收益显著。
5. config 层
集中存放 @Configuration 类,用于自定义 Bean、拦截器注册、过滤器配置、跨域设置、安全策略等。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**").allowedOrigins("*");
}
}
- 将配置分散到多个按功能命名的配置类(
WebConfig、SecurityConfig、RedisConfig),不要让一个类承载所有配置。
6. common 包
存放应用级工具类、自定义异常、常量定义、统一响应体等横切基础设施。
// 统一响应体
public class ApiResponse<T> {
private int code;
private String message;
private T data;
// 静态工厂方法 success(), error()...
}
// 自定义业务异常
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
}
- 全局异常处理器(
@ControllerAdvice)也可放在common.exception中。
2.3.2 命名规范与约定
分层结构只是骨架,一致性命名才能让项目“开口说话”。下表给出最常用的约定:
| 元素 | 规范 | 示例 |
|------|------|------|
| 包名 | 全小写,点分隔,用单数形式 | com.example.demo.controller |
| Controller 类 | 名词 + Controller | UserController、OrderController |
| Service 接口 | 名词 + Service | UserService |
| Service 实现 | 名词 + ServiceImpl | UserServiceImpl |
| Repository 接口 | 名词 + Repository | UserRepository |
| 实体类 | 名词,可加 Entity 后缀 | User 或 UserEntity |
| 请求对象 | 功能 + Request | UserCreateRequest、OrderQueryRequest |
| 响应对象 | 功能 + Response/VO | UserResponse、UserVO |
| 工具类 | 名词 + Utils/Helper | DateUtils、JwtHelper |
| 配置类 | 模块名 + Config | WebConfig、SecurityConfig |
| 常量类 | 领域 + Constants | RedisConstants、ApiConstants |
| 方法名(Service) | 动词 + 名词,体现业务操作 | createUser()、cancelOrder() |
| 方法名(Repository) | findBy/query/list/count + 字段 | findByUsername()、listByStatus() |
需要注意的是,这些并非硬性教条,团队可以有自己的变体,但必须保证项目内统一。例如有人喜欢将 DTO 直接放在 service 层下,只要能自圆其说且全员遵守,就没有问题。
2.3.3 一个真实的项目包结构实例
以下是一个简化版电商应用 shop 的完整包树,供参考:
com.example.shop
├── ShopApplication.java
├── controller
│ ├── ProductController.java
│ ├── OrderController.java
│ └── request
│ ├── OrderCreateRequest.java
│ └── ProductSearchRequest.java
├── service
│ ├── ProductService.java
│ ├── OrderService.java
│ └── impl
│ ├── ProductServiceImpl.java
│ └── OrderServiceImpl.java
├── repository
│ ├── UserRepository.java
│ ├── ProductRepository.java
│ ├── OrderRepository.java
│ └── entity
│ ├── UserEntity.java
│ ├── ProductEntity.java
│ └── OrderEntity.java
├── model
│ ├── vo
│ │ ├── UserVO.java
│ │ └── ProductVO.java
│ └── dto
│ ├── OrderDTO.java
│ └── StockDTO.java
├── config
│ ├── WebConfig.java
│ ├── RedisConfig.java
│ └── SecurityConfig.java
├── common
│ ├── ApiResponse.java
│ ├── BusinessException.java
│ ├── GlobalExceptionHandler.java
│ └── utils
│ └── JwtUtils.java
└── security
├── JwtTokenProvider.java
└── UserDetailsServiceImpl.java
2.3.4 分层架构的演进考量
当业务复杂度增长,单体分层结构可能逐步膨胀。但不必过早进行物理拆分,先在模块级别进行纵向分割更为务实:
- 若订单模块与商品模块相对独立,可以在
com.example.shop下创建order和product两个顶层包,各自内部再沿用 controller/service/repository 子结构。 - 当模块间需要明确边界时,可借助 Spring Modulith 或直接使用 Gradle/Maven 多模块项目,将 order 和 product 拆分为独立模块,但保持共享同一个代码库。
分层与命名的最终目的是让代码在“看到”的那一刻就传递出准确的信息。一个刚接手维护的开发者,打开 OrderServiceImpl 时能立刻感知到它属于业务层、承载订单相关的核心逻辑,而不会迷失在一堆没有规则的类名中。正是这些细小的约束,奠定了工程化开发的基础。
在下一节中,我们将基于这套分层结构,深入 Spring Boot 的自动配置原理,理解为什么在添加了某些 starter 后,无需编写任何配置就能直接运行。