Controller无需接口,Service必须分层

一、先说核心结论

Controller 可以不用接口,Service 规范上推荐「接口 + 实现类」分层,不是语法强制,是业务分层、解耦、AOP、多实现场景的工程规范差异,两者定位完全不同:

  1. Controller:接收前端请求,对外唯一入口,几乎不存在多种实现,没必要抽象接口;
  2. Service:承载业务逻辑,存在多实现、替换业务逻辑、面向抽象编程的需求,所以行业统一规范用接口+Impl。

1. 为什么 Controller 能直接写类,不用接口

Controller 的职责:接收HTTP请求、参数校验、调用Service、封装返回结果。

特点

  1. 对外唯一入口,几乎不会多套实现
    一个接口路径 /dept/list 只会对应一套控制器逻辑,不会出现「切换不同Controller实现」的场景,没有多实现需求;
  2. SpringMVC 注解驱动,耦合天然可控
    @RestController 直接标记为处理器,依赖注入时直接注入本类即可,不存在替换实现的场景;
  3. 没有复杂业务替换需求
    控制器只是转发层,业务全下沉到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 默认有两种代理方式:

  1. JDK动态代理:只支持代理「实现了接口的类」,基于接口生成代理对象;
  2. 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:可变业务层,存在替换、扩展、切面增强需求,必须用接口抽象隔离实现。