这篇博客适合已经会写 Java、会写接口和实现类,但还不太理解 Spring 到底解决了什么问题的人。
不会一上来背概念,而是从我们写代码时真实会遇到的问题开始:明明已经用了接口和实现类,为什么还要 Spring?为什么不能自己写一个工厂、配置文件、反射来管理对象?
1. 没有 Spring 时,代码会怎么写?
假设我们有一个支付功能。
先定义一个支付接口:
public interface PaymentService {
void pay(double amount);
}
然后写一个支付宝实现:
public class AlipayPayment implements PaymentService {
public void pay(double amount) {
System.out.println("支付宝支付了:" + amount + "元");
}
}
再写一个订单服务,它需要调用支付服务:
public class OrderService {
private PaymentService paymentService = new AlipayPayment();
public void createOrder(double amount) {
System.out.println("创建订单...");
paymentService.pay(amount);
}
}
这段代码看起来没有问题。
接口也有了,实现类也有了,OrderService 依赖的是 PaymentService 接口,而不是直接依赖 AlipayPayment 的具体逻辑。
但问题在于这一行:
private PaymentService paymentService = new AlipayPayment();
虽然变量类型是接口,但真正创建对象的地方,仍然写死了 new AlipayPayment()。
也就是说,OrderService 虽然面向接口编程,但它仍然知道:"我现在用的是支付宝。"
2. 这里真正的问题不是"不能用接口"
很多人会说:
我不用 Spring,也可以写接口和实现类啊。
这句话是对的。
接口和实现类是 Java 多态的能力,不是 Spring 发明的。
Spring 解决的不是"能不能写接口"的问题,而是:
当项目里有很多类都依赖同一个接口时,谁来负责决定"到底 new 哪个实现类"?
如果项目很小,只有几个类,手动 new 其实没什么问题。
但如果项目变大,比如 OrderService、ProductService、RefundService、PayService、CheckoutService 等 30 个类都依赖 PaymentService,每个类里都写了:
private PaymentService paymentService = new AlipayPayment();
那么当需求变化时,麻烦就来了。
3. 需求一变,手动 new 的缺点就暴露了
假设现在业务要求:
支付服务从支付宝切换到微信支付。
如果所有地方都是手动 new,你可能需要这样改:
// OrderService.java
private PaymentService paymentService = new WechatPayment(); // 改!
// ProductService.java
private PaymentService paymentService = new WechatPayment(); // 改!
// RefundService.java
private PaymentService paymentService = new WechatPayment(); // 改!
如果项目里有 30 个地方,就要改 30 个地方。
这会带来几个问题:
| 问题 | 说明 |
|---|---|
| 修改范围大 | 需要找到所有依赖这个实现的地方 |
| 容易漏改 | 漏掉一个地方,线上就可能出现 bug |
| 耦合度高 | 业务类里仍然知道具体实现是谁 |
| 测试困难 | 想换成 Mock 支付时,不方便替换 |
| 环境切换麻烦 | 开发环境、测试环境、生产环境想用不同实现时,不好处理 |
所以,真正的问题不是"能不能用接口",而是:
当依赖很多、实现经常变化、环境经常切换时,手动管理对象会变得很麻烦。
4. 一个现实比喻:餐厅里的厨师和经理
可以把项目里的各个 Service 想象成餐厅里的厨师。
厨师只需要负责炒菜,也就是业务逻辑。
但如果没有 Spring,厨师不仅要炒菜,还要自己决定:
- 今天用哪个供应商的菜
- 哪个洗碗工来洗碗
- 哪个收银员来收银
- 哪个保安来检查权限
- 哪个保洁来打扫卫生
这显然不合理。
Spring 就像餐厅里的经理。
它负责:
- 招聘员工
- 分配岗位
- 管理员工之间的关系
- 统一处理公共事务,比如日志、事务、权限校验
厨师只需要说:
我需要一个支付服务。
至于具体给支付宝还是微信支付,由经理决定。
这就是 Spring 想做的事情:把对象的创建和管理从业务代码里抽出来。
5. 用 Spring 之后,代码会变成什么样?
使用 Spring 后,支付接口不变:
public interface PaymentService {
void pay(double amount);
}
支付宝实现加上注解:
@Service("alipay")
public class AlipayPayment implements PaymentService {
public void pay(double amount) {
System.out.println("支付宝支付了:" + amount + "元");
}
}
微信支付也加上注解:
@Service("wechat")
public class WechatPayment implements PaymentService {
public void pay(double amount) {
System.out.println("微信支付了:" + amount + "元");
}
}
订单服务不再手动 new:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public void createOrder(double amount) {
System.out.println("创建订单...");
paymentService.pay(amount);
}
}
这里最关键的变化是:
@Autowired
private PaymentService paymentService;
OrderService 不再说:
我要 new 一个支付宝。
而是说:
我需要一个
PaymentService,具体给谁,由 Spring 容器决定。
6. 切换实现时,业务代码可以不动
如果现在要切换到微信支付,只需要让 Spring 知道默认使用微信支付。
比如给微信支付加上 @Primary:
@Service("alipay")
public class AlipayPayment implements PaymentService {
public void pay(double amount) {
System.out.println("支付宝支付了:" + amount + "元");
}
}
@Service("wechat")
@Primary
public class WechatPayment implements PaymentService {
public void pay(double amount) {
System.out.println("微信支付了:" + amount + "元");
}
}
此时,所有没有特别指定的地方:
@Autowired
private PaymentService paymentService;
都会默认注入 WechatPayment。
也就是说,原来 30 个 Service 里的业务代码一行都不用改。
这就是 Spring IoC 的价值之一:
把"用哪个实现"的决定权,从业务代码里拿出来,交给容器统一管理。
7. 为什么需要 @Primary?
假设容器里有两个 PaymentService 实现:
@Service("alipay")
public class AlipayPayment implements PaymentService { ... }
@Service("wechat")
public class WechatPayment implements PaymentService { ... }
当某个类这样注入时
@Autowired
private PaymentService paymentService;
Spring 会按类型去找 PaymentService。
但此时它找到了两个:
alipaywechat
Spring 不知道你到底要哪一个,就会报错。
@Primary 的作用就是告诉 Spring:
当有多个同类型 Bean 时,如果没有特别说明,默认选我。
例如:
@Service("alipay")
@Primary
public class AlipayPayment implements PaymentService { ... }
这样,默认会注入支付宝实现。
但如果某个地方明确想要微信支付,可以用 @Qualifier 指定:
@Autowired
@Qualifier("wechat")
private PaymentService paymentService;
@Qualifier 的优先级高于 @Primary。
所以可以这样理解:
| 注解 | 作用 |
|---|---|
@Primary |
默认首选 |
@Qualifier |
明确指定某一个 |
8. 那能不能不用 Spring,自己写一个"自动找实现类"的机制?
可以。
而且这个想法并不是错的。
比如我们可以这样设计:
public class OrderService {
private PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
这里 OrderService 不再自己 new,而是通过构造方法接收一个 PaymentService。
然后再写一个配置文件:
properties
paymentService.impl=com.example.AlipayPayment
再写一个工厂
public class ServiceFactory {
public static PaymentService getPaymentService() throws Exception {
Properties props = loadProperties("app.properties");
String className = props.getProperty("paymentService.impl");
Class<?> clazz = Class.forName(className);
return (PaymentService) clazz.getDeclaredConstructor().newInstance();
}
}
使用时:
PaymentService paymentService = ServiceFactory.getPaymentService();
OrderService orderService = new OrderService(paymentService);
orderService.createOrder(100.0);
这样确实可以做到:
- 业务代码不直接
new实现类 - 切换实现时只改配置文件
- 多个类共用同一个工厂获取依赖
这个方案已经具备了 IoC 的雏形。
所以,"接口声明 + 配置文件 + 反射创建对象"这个思路本身并不依赖 Spring。
9. 既然自己也能做,为什么还要 Spring?
因为当项目变大以后,自己写工厂会变得越来越复杂。
你很快会遇到这些问题:
| 问题 | 说明 |
|---|---|
| 单例管理 | 同一个 Bean 是否应该只创建一次? |
| 依赖注入 | A 依赖 B,B 依赖 C,怎么自动组装? |
| 初始化顺序 | A 依赖 B,但 B 还没创建怎么办? |
| 循环依赖 | A 依赖 B,B 又依赖 A,怎么处理? |
| 生命周期 | Bean 创建后要做初始化,销毁前要释放资源,怎么管理? |
| AOP 能力 | 日志、事务、权限校验怎么统一处理? |
| 多环境配置 | 开发、测试、生产环境如何自动切换? |
如果只有一两个类,自己写工厂没问题。
但如果有几百个 Bean,每个 Bean 又有多个依赖,还涉及事务、代理、生命周期管理,手写工厂就会变成另一个复杂系统。
Spring 的价值就在这里:
它不是发明了"面向接口编程",也不是发明了"反射",而是把对象创建、依赖管理、生命周期、AOP、Web 开发、事务管理这些能力整合成了一个成熟框架。
也就是说,Spring 不是让你"不能自己实现",而是让你"不用每次都自己实现"。
10. Spring 的演进过程
Spring 的发展可以简单理解成一条演进路线:
手动 new
↓
XML 配置
↓
注解配置
↓
Spring Boot 自动配置
每一步都在解决上一步的问题。
10.1 手动 new
优点:
- 简单直接
- 不需要框架
缺点:
- 实现类和业务代码耦合
- 切换实现要改代码
- 依赖多时维护困难
10.2 XML 配置
业务代码可以保持干净:
public class OrderService {
private PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
然后在 XML 中配置:
xml
<bean id="alipay" class="com.example.AlipayPayment" />
<bean id="orderService" class="com.example.OrderService">
<constructor-arg ref="alipay" />
</bean>
优点:
- 业务代码和配置分离
- 切换实现时改配置即可
缺点:
- XML 文件多了以后很难维护
- 配置分散,不够直观
10.3 注解配置
现在更常见的是注解方式:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
}
优点:
- 配置和代码在一起,直观
- 开发效率高
缺点:
- 业务代码中会混入框架注解
10.4 Spring Boot 自动配置
Spring Boot 进一步简化了配置。
很多场景下,你只需要加一个注解:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后就可以开始写业务代码。
Spring Boot 并不是取代 Spring,而是让 Spring 更容易使用。
11. Spring 在项目里到底扮演什么角色?
在一个 Web 后端项目里,Spring 通常像一个隐形的大管家。
你写代码时不一定能明显感觉到它,但它一直在背后工作。
比如你写一个 Service:
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Transactional
public void createOrder(double amount) {
System.out.println("创建订单...");
paymentService.pay(amount);
}
}
Spring 会帮你做这些事情:
- 创建
OrderService对象 - 创建
PaymentService实现对象 - 把
PaymentService注入到OrderService中 - 管理这些对象的生命周期
- 如果加了
@Transactional,还会处理事务
你不需要手动写:
OrderService orderService = new OrderService(new AlipayPayment());
而是直接从 Spring 容器中获取已经组装好的对象。
12. 一次请求的数据流
假设用户点击"保存订单",数据在 Spring 项目里大致会这样流转:
用户点击保存订单
↓
请求到达 Web 容器,比如 Tomcat
↓
Spring MVC 的 DispatcherServlet 接收请求
↓
根据请求路径找到对应的 Controller
↓
Controller 接收参数,调用 Service
↓
Service 执行业务逻辑
↓
Service 调用 Mapper 或 DAO 操作数据库
↓
数据库返回结果
↓
Service 返回结果给 Controller
↓
Controller 把结果封装成 JSON 返回给前端
在这个过程中,Spring 可能还会自动处理:
- 参数校验
- 日志记录
- 权限检查
- 事务开启和提交
- 异常处理
- 对象注入
如果业务执行过程中发生异常,并且方法上有事务控制,Spring 会帮助回滚事务。
如果请求还没处理完服务就宕机了,那已经提交的数据不会丢失,但没有提交的事务也不会生效。
Spring 能管理事务,但不能凭空恢复网络请求或客户端状态。
13. 一个完整的对比示例
13.1 不用 Spring
public class OrderService {
private PaymentService paymentService = new AlipayPayment();
public void createOrder(double amount) {
paymentService.pay(amount);
}
}
public class ProductService {
private PaymentService paymentService = new AlipayPayment();
public void buyProduct(double amount) {
paymentService.pay(amount);
}
}
public class RefundService {
private PaymentService paymentService = new AlipayPayment();
public void refund(double amount) {
paymentService.pay(amount);
}
}
切换实现时,需要修改每一个类中的 new AlipayPayment()。
13.2 使用 Spring
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
public void createOrder(double amount) {
paymentService.pay(amount);
}
}
@Service
public class ProductService {
@Autowired
private PaymentService paymentService;
public void buyProduct(double amount) {
paymentService.pay(amount);
}
}
@Service
public class RefundService {
@Autowired
private PaymentService paymentService;
public void refund(double amount) {
paymentService.pay(amount);
}
}
切换实现时,只需要改变 Spring 容器中默认使用的 PaymentService 实现。
业务类本身不需要修改。
14. 面试时可以怎么说?
如果面试时被问到"什么是 Spring",可以这样回答:
Spring 是一个轻量级的 Java 开发框架,主要用来简化企业级应用开发。它的核心价值不是让代码行数变少,而是降低类与类之间的耦合度。
在没有 Spring 的时候,如果多个 Service 都依赖同一个接口,我们通常会在代码里手动 new 实现类。这样一旦实现类发生变化,比如支付服务从支付宝切换到微信支付,就需要修改很多地方的代码。
Spring 通过 IoC 把对象的创建和依赖管理交给容器。业务代码只需要声明自己需要什么依赖,比如使用
@Autowired注入一个接口,而具体使用哪个实现类,由 Spring 容器来决定。这就好比餐厅里的厨师不需要自己去找食材供应商,餐厅经理会统一安排好,厨师只需要专心炒菜。除了 IoC,Spring 还有 AOP,可以把日志、事务、权限校验这些公共逻辑从业务代码中抽出来统一处理。Spring Boot 又进一步简化了配置,让项目可以更快启动和开发。
这段回答不需要一字不差地背,重点是表达清楚三个层次:
- Spring 解决什么问题
- IoC 和 AOP 分别做什么
- Spring Boot 的作用是什么
15. 面试追问:Spring IoC 和手动 new 的本质区别是什么?
这个问题很适合作为追问。
可以这样回答:
手动 new 时,对象之间的依赖关系是写死在代码里的。比如
OrderService里直接new AlipayPayment(),那么OrderService就和AlipayPayment产生了直接依赖。使用 Spring IoC 后,对象之间的依赖关系由容器管理。
OrderService只需要声明自己需要一个PaymentService,不需要关心具体实现是谁。Spring 容器启动时会根据配置、注解或类型匹配,把合适的实现注入进去。所以本质区别是:手动 new 是业务代码自己决定依赖对象;Spring IoC 是把依赖对象的创建和管理交给外部容器,从而实现解耦。
16. 最后可以记住的一句话
Spring 不是让"接口和实现类"变得可能,因为 Java 本身就可以做到。
Spring 真正做的是:
把"用哪个实现类"这件事,从业务代码里拿出来,交给容器统一管理。
所以理解 Spring 时,不要只记:
Spring 是一个框架。
而要理解它解决的真实问题:
当项目变大、依赖变多、实现经常变化时,手动管理对象会让代码越来越难维护。Spring 通过 IoC 容器统一管理对象创建和依赖关系,让业务代码只关注业务逻辑,不关注对象是怎么来的。