一、先说核心结论
Controller 可以不用接口,Service 规范上推荐「接口 + 实现类」分层,不是语法强制,是业务分层、解耦、AOP、多实现场景的工程规范差异,两者定位完全不同:
- Controller:接收前端请求,对外唯一入口,几乎不存在多种实现,没必要抽象接口;
- Service:承载业务逻辑,存在多实现、替换业务逻辑、面向抽象编程的需求,所以行业统一规范用接口+Impl。
1. 为什么 Controller 能直接写类,不用接口
Controller 的职责:接收HTTP请求、参数校验、调用Service、封装返回结果。
特点
- 对外唯一入口,几乎不会多套实现
一个接口路径/dept/list只会对应一套控制器逻辑,不会出现「切换不同Controller实现」的场景,没有多实现需求; - SpringMVC 注解驱动,耦合天然可控
@RestController直接标记为处理器,依赖注入时直接注入本类即可,不存在替换实现的场景; - 没有复杂业务替换需求
控制器只是转发层,业务全下沉到Service,控制器本身逻辑很薄,不需要抽象隔离。
简单说:Controller 是对外门面,单一实现,没必要抽象接口。
2. 为什么 Service 规范要求「接口 + Impl 实现类」
① 面向接口编程,解耦业务(最核心)
业务层逻辑经常会有多种实现场景:
举例子:部门统计功能
- 版本1:从数据库查部门数据
- 版本2:对接第三方OA系统拉取部门数据
如果 Controller 直接依赖DeptServiceImpl(具体类),切换业务时所有注入代码全要改;
如果 Controller 依赖DeptService接口,只需要换@Service标注的实现类,上层代码一行不动。
java
// 推荐:依赖抽象,不依赖具体实现
@RestController
public class DeptController {
// 注入接口,随便换实现类,控制器不用改
@Autowired
private DeptService deptService;
}
② Spring AOP / 动态代理必须依赖接口(JDK动态代理限制)
Spring 默认有两种代理方式:
- JDK动态代理:只支持代理「实现了接口的类」,基于接口生成代理对象;
- CGLIB代理:直接生成子类代理普通类。
如果你的Service没有接口,Spring只能强制启用CGLIB;
如果有接口,优先用更轻量、性能更好的JDK代理。
像事务 @Transactional、日志切面、权限切面全靠AOP实现,接口模式兼容性更好,避免CGLIB带来的各种坑(构造函数注入、final类无法代理等)。
③ 分层职责清晰,代码可读性强
DeptService:定义业务能力契约(这个模块能做哪些操作:增删改查部门);DeptServiceImpl:业务逻辑具体实现 (数据库操作、计算、组装数据);
看代码时,先看接口就能知道该模块提供哪些业务,不用看实现细节,大型项目协作优势极大。
④ 单元测试友好,方便Mock
单元测试Controller时,只需要Mock DeptService 接口,不用关心底层实现;
如果直接依赖实现类,Mock、隔离数据库会非常麻烦。
⑤ 分层架构统一规范(企业开发约定)
标准三层架构规范:
- Controller:控制层(接收请求,无接口)
- Service:业务层(接口+Impl实现)
- Mapper/DAO:数据层(Mybatis Mapper本身就是接口)
整套架构统一「业务/数据层面向接口」,团队代码风格统一,降低维护成本。
3. 补充:Service 不写接口行不行?
语法上完全允许,直接写 @Service 普通类也能注入运行,但企业项目不推荐,会丢失上面说的解耦、代理、多实现、规范等优势,只有极小工具类Service才会省略接口。
4. 一句话区分两者设计初衷
- Controller:请求入口层,单一固定实现,无需抽象;
- Service:可变业务层,存在替换、扩展、切面增强需求,必须用接口抽象隔离实现。