SpringBoot代码风格推荐
应用SpringBoot框架的项目按功能职责划分目录,包括控制层、服务层、持久层等。实体对象严格区分持久化对象、视图对象、数据传输对象和业务对象,避免耦合。工具类分为静态工具类、容器化工具类和业务专用辅助类。各层职责明确,提升可维护性。
整体包结构
com.example.project
│
├── bean # 实体类统一目录
│ ├── entity # 数据库实体
│ ├── vo # 接口返回对象
│ ├── dto # 接口接收参数
│ ├── bo # 业务层聚合对象
│ ├── doc # MongoDB 文档实体
│ ├── cache # 缓存实体对象
│ ├── index # ES 索引实体
│ ├── message # MQ 消息体
│ └── event # Spring 事件对象
├── controller # Web 接口层
├── service # 业务逻辑接口
│ └── impl # 业务逻辑实现
├── mapper # 数据访问层
├── biz # 业务聚合处理
├── client # Feign 客户端
├── convert # 对象转换器
├── aspect # AOP 切面
├── config # 配置类
├── constant # 常量类
├── enums # 枚举类
├── exception # 自定义异常
├── util # 工具类目录(含 Util 和 Kit)
└── job # 定时任务
├── XxxJob.ja va
└── XxxHelper.ja va # 辅助类,放在对应功能包内
先说说整体包结构这件事。很多团队刚开始做项目时,包结构往往随手一搭,后续随着业务膨胀就变得难以维护。这里给出一个比较成熟的实践方案:按功能职责分层,而不是按技术框架分层。这样做的最大好处是,新成员拿到项目就能快速定位——看到“controller”知道是接口层,“service”是业务逻辑层,一目了然。
值得注意的是,这个结构里特别区分了“bean”目录。很多新手会把所有实体类都塞在一个“model”或“pojo”包里,但实际项目中,数据库实体、接口返回对象、接口接收参数这些对象的变化频率和维度完全不同,混在一起只会让依赖关系变得混乱。分开管理是从源头上控制耦合。
分层命名规范
| 层级职责 | 类名后缀 | 包名 | 说明 |
|---|---|---|---|
| Web 接口层 | xxxController | controller | 接收请求、参数校验、返回 Vo |
| Service 接口 | xxxService | service | 业务逻辑接口定义 |
| Service 实现 | xxxServiceImpl | service.impl | 业务逻辑实现,加事务注解 |
| 数据访问层 | xxxMapper | mapper | MyBatis/MyBatis-Plus 映射接口 |
| 业务聚合处理 | xxxBiz | biz | 跨 Service 或外部调用的复杂业务编排 |
| 远程服务客户端 | xxxClient | client | Feign 声明式 HTTP 客户端 |
| 对象转换器 | xxxConvert | convert | 统一处理对象转换 |
| AOP 切面 | xxxAspect | aspect | 横切关注点(日志、权限、监控等) |
| 常量类 | xxxConst | constant | 常量按业务拆分,不堆在一个类中 |
| 枚举类 | xxxEnum | enums | 统一管理枚举定义 |
| 自定义异常 | xxxException | exception | 业务异常或系统异常 |
| 配置类 | xxxConfig | config | 如 RedisConfig、SwaggerConfig |
| 定时任务 | xxxJob | job | 定时调度任务类 |
| 辅助类(业务/功能) | xxxHelper | 对应功能包内 | 如 job.JobHelper、mail.MailHelper |
分层命名的核心逻辑其实很清晰:每个类名后缀都代表了它的职责和生命周期。比如 Controller 只做请求转发和参数校验,不写业务逻辑;ServiceImpl 负责具体实现,但不直接操作数据库——通过 Mapper 接口做数据访问。这样拆开之后,每层的修改范围都被限定了,改动一个层不会波及到其他层。
需要特别说明的是“Biz”层。很多时候业务逻辑并不是简单的 CRUD堆砌,而是要协调多个 Service 调用外部接口,这时候就应该用 Biz把这些编排逻辑收拢起来。很多团队把这个和 ServiceImpl混淆,结果ServiceImpl越来越臃肿,变成了大泥球。
实体类命名规范
业务实体
| 对象类型 | 类名后缀 | 包路径 | 说明 |
|---|---|---|---|
| 数据库实体 | xxx / XxxPo | bean.entity / bean.po | 与数据库表结构一一对应 |
| 接口返回对象 | xxxVo | bean.vo | 面向前端展示 |
| 接口接收参数 | xxxDto | bean.dto | 接收请求参数,可含校验注解 |
| 业务层聚合对象 | xxxBo | bean.bo | 业务逻辑层内部使用 |
| MongoDB 文档实体 | xxxDoc | bean.doc | MongoDB 实体定义 |
| 缓存实体对象 | xxxCache | bean.cache | Redis/Caffeine 缓存结构 |
| ES 索引实体 | xxxIndex | bean.index | Elasticsearch 文档映射 |
| MQ 消息体 | xxxMessage | bean.message | 队列/主题传输的消息体 |
| Spring 事件对象 | xxxEvent | bean.event | 领域事件或应用事件对象 |
使用示例
// 保存用户:接收 Dto,返回 Vo
@PostMapping("/sa ve")
public SysUserVo sa veUser(@RequestBody @Valid SysUserSa veDto dto) {
SysUserPo po = userConvert.toPo(dto);
userMapper.insert(po);
return userConvert.toVo(po);
}
❌ 不要将 Entity / Po 直接返回给前端
✅ 各层对象严格隔离,降低变动影响面
实体类这一块其实很容易踩坑。很多人嫌麻烦,直接把数据库实体(Po)返回给前端,省去了转换的步骤。这种做法在简单项目里看似高效,但随着需求迭代,你有天突然发现需要给某字段重命名,或者要隐藏一些敏感字段,改动就成了噩梦——因为前端、数据库、内部逻辑全绑在一个类上了。
严格区分 Vo、Dto、Po、Bo 这几个对象类型,本质上是在不同边界处设置“防火墙”。即使每个实体多写几个转换方法,但长期来看收益远远大于成本。特别是用了 MapStruct 等转换工具后,代码量其实并没有增加多少。
业务之外的实体
对于独立于业务之外的实体,如工具类、框架配置等需要的对象,有两种常见处理方式:
| 方式 | 适用场景 | 示例 |
|---|---|---|
| 使用内部类 | 仅被当前类使用、逻辑简单、内聚性强 | Controller 内部的请求/响应类、Service 内部的中间对象 |
| 放在业务类一起 | 与某个业务紧密相关,但不属于标准实体类型 | 业务特有的参数封装、中间计算结果对象 |
示例
方式一:使用内部类(适用于仅当前类使用)
@RestController
public class UserController {
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
// ...
}
// 内部类,仅用于当前 Controller
static class LoginRequest {
private String username;
private String password;
}
}
方式二:放在业务类一起(适用于业务特有对象)
// 放在使用它的 Service 同包下,但不属于标准 bean 类型
// service/OrderStatContext.ja va
public class OrderStatContext {
private Long userId;
private LocalDateTime startTime;
private LocalDateTime endTime;
// 订单统计的中间计算对象
}
选择建议
- 仅当前类使用 → 内部类
- 业务特有但不属于标准 Entity/Vo/Dto/Bo → 放在对应业务包下
这一部分可能容易产生分歧。对于非标准实体对象,有些人倾向于全部放在一个公共包里,但实际经验表明,紧耦合的业务对象放在业务包下更合理:当业务模块被重构或拆分时,这些对象可以跟着迁移,不会产生大的牵扯。
工具类命名规范
| 类型 | 命名后缀 | 包位置 | 是否静态 | 是否依赖容器 | 说明 |
|---|---|---|---|---|---|
| 静态工具类 | xxxUtil | util | ✅ 是 | ❌ 否 | 纯静态方法,无状态,不依赖 Spring |
| 容器化工具类 | xxxKit | util | ❌ 否 | ✅ 是 | 需要注入 Bean,加 @Component |
| 辅助类(业务/功能) | xxxHelper | 对应功能包内 | ❌ 否 | ✅ 是 | 业务或功能专用,如 job.JobHelper |
示例代码
Util(静态调用)
public class DateUtil {
public static String format(LocalDateTime date, String pattern) {
// 静态方法,不依赖 Spring
}
}
// 使用
String dateStr = DateUtil.format(now, "yyyy-MM-dd");
Kit(注入使用,放在 util 包)
@Component
public class RedisKit {
@Autowired
private RedisTemplate redisTemplate;
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
}
// 使用
@Service
public class UserService {
@Autowired
private RedisKit redisKit;
}
Helper(功能专用,放在对应功能包内)
// job/JobHelper.ja va
@Component
public class JobHelper {
@Autowired
private MailService mailService;
public void sendAlert(String jobName, Exception e) {
mailService.send("Job Alert", jobName + " 执行失败:" + e.getMessage());
}
}
工具类的命名看起来是小细节,但实际影响代码的可读性和可维护性。很多项目里的工具类命名很随意,有的叫Utils,有的叫Tools,还有的干脆叫XxxHelper。这里给出明确区分逻辑:纯静态、不依赖Spring的用Util,需要注入容器的用Kit,功能专用的用Helper。区分清楚后,使用者看到类名就能判断它的使用方式,不需要阅读源码或文档。
特别提醒一点:Kit 和 Helper 虽然都是注入使用,但 Kit 是通用的、跨业务的工具性组件(比如封装了对Redis的操作),而 Helper 是业务专用的辅助类(比如定时任务失败发送邮件的逻辑)。这个区别不只是概念上的,更体现在包位置和职责上——Kit放在全局的 util 包,Helper放在具体业务包内,避免通用层与业务层的依赖关系错乱。
Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。
极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

















