Spring并不是某一种单一技术,而是一个分层的Java平台。它的核心是容器和Bean,最常被提到的功能是控制反转和依赖注入。第一次学Spring时,不要把注意力全部放在注解名称上,先要理解一件事:Spring负责创建对象、管理对象之间的关系,并在合适的时机把它们交给调用方。这个定位理解了,后面的配置、AOP、事务才会顺理成章。

Spring到底解决了什么问题:从手动new到容器托管
在传统的Java项目里,对象之间的依赖关系由开发者自己维护。比如OrderService需要调用OrderRepository,就要写OrderRepository repository = new OrderRepository();,如果OrderRepository又依赖DataSource,还要继续手动创建。项目规模变大后,这种方式的缺点很明显:创建逻辑散落各处,替换实现要改大量代码,单元测试也不容易做。
Spring的切入点正是对象创建和依赖管理。它的核心是一个Bean容器,程序启动时,容器根据配置信息创建对象,并完成依赖关系的组装。开发者只需要声明需要哪些Bean、它们之间如何协作,剩下的事情交给Spring。这个机制通常称为控制反转(IoC),也就是说,对象创建的控制权从业务代码转移到了框架容器。很多人把IoC和依赖注入(DI)混为一谈,实际上DI是IoC的一种实现方式,Spring通过构造器、Setter或字段把依赖注入到目标对象中。
// 传统方式:自己创建依赖
public class OrderService {
private OrderRepository repository = new OrderRepository();
}
// Spring托管方式:依赖由容器注入
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
上面的对比可以直观看出变化:OrderService不再负责创建OrderRepository,只声明自己需要一个实现。这个实现由容器创建并传递进来。带来的好处是,当你想把OrderRepository替换成Mock实现做测试时,传统方式要修改源码,而Spring托管方式只需要在配置里更换Bean即可。理解了这一点,后续学习Bean配置和自动装配时就不会只停留在记忆注解的层面。
Bean装配与配置方式:XML、注解和Java配置怎么选
Spring支持多种方式告诉容器如何创建Bean。早期项目主要用XML配置文件,后来注解配置成为主流,Spring Boot中又大量使用Java配置类。理解这三种方式的关系,对阅读旧项目和新项目都有帮助。以订单服务为例,XML配置通常这样写:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="orderRepository" class="com.example.OrderRepository" />
<bean id="orderService" class="com.example.OrderService">
<constructor-arg ref="orderRepository" />
</bean>
</beans>
XML的优点是配置集中,Bean之间的依赖关系一目了然,不侵入业务代码。缺点是随着Bean数量增加,文件会变得很庞大,而且字符串形式的类名和id在编译期不容易发现错误。后来出现了基于注解的配置,例如用@Component标记类,用@Autowired注入依赖,再通过组件扫描让容器自动发现这些Bean。下面是一个简单示例:
@Component
public class OrderRepository {
// 仓储实现
}
@Service
public class OrderService {
private final OrderRepository repository;
@Autowired
public OrderService(OrderRepository repository) {
this.repository = repository;
}
}
注解方式减少了配置文件,开发效率更高,但Bean的定义比较分散,排查问题时要借助IDE的Bean图或Spring Actuator。Java配置类则介于两者之间,用@Configuration和@Bean显式声明Bean,集中度比注解高,比XML更直观。初学者不必过分纠结哪一种最好,实际项目中可以先掌握注解方式,再看懂XML和Java配置即可。
关于装配方式的几点操作要点:第一,如果同一个接口有多个实现,仅靠@Autowired按类型注入会报错,需要配合@Qualifier或者使用@Primary指定首选Bean。第二,构造器注入比字段注入更利于测试和不可变性,官方也推荐使用构造器注入。第三,配置文件中的Bean id应当遵循小写驼峰或短横线命名,避免和类名冲突。
常见疑问与避坑指南:自动装配失败、循环依赖和单例线程安全
入门Spring时,报错最多的一类问题是自动装配失败。典型提示包括No qualifying bean、expected single matching bean but found 2等。遇到No qualifying bean,要检查目标类是否被容器托管,比如有没有加@Component、是否在组件扫描的包路径内,或者XML里是否漏写<bean>。如果扫描路径配置错误,Spring根本不会创建该Bean,注入时自然找不到。expected single matching bean but found 2则是同一个接口存在多个实现,必须用@Qualifier或@Primary消除歧义。
循环依赖也是一个绕不开的问题。当A依赖B,B又依赖A时,如果两方都使用构造器注入,Spring启动会直接抛BeanCurrentlyInCreationException。原因是构造器注入要求先创建依赖对象,而A和B互相等待,无法完成初始化。解决思路不是盲目开启循环依赖,而是重新设计依赖关系,或者至少将其中一方改为Setter或字段注入。Spring对单例Bean的Setter循环依赖有一定支持,但构造器循环依赖无法自动解决。
另一个高频疑问是单例Bean是否线程安全。Spring容器默认创建的Bean作用域是singleton,也就是说整个应用共享同一个实例。如果这个Bean内部持有可变状态,比如一个private int count;,在多线程下同时修改就会产生数据竞争。因此单例Bean里尽量不要定义可变的成员变量,必要的状态应该放在方法局部变量或使用线程安全的数据结构。对需要每次请求独立实例的场景,可以使用@Scope("prototype"),但要注意prototype Bean不会在容器关闭时自动销毁,需要自行管理生命周期。
Bean的生命周期也值得提前了解:容器启动后,先实例化Bean,注入依赖,然后执行初始化回调,比如@PostConstruct方法,最后Bean进入可服务状态。关闭容器时,如果Bean实现了DisposableBean或者标注了@PreDestroy,就会执行销毁回调。调试启动错误时,可以打开日志观察Bean创建顺序,但更建议从设计上避免复杂依赖。
读完这些内容,最有效的练习是在本地搭一个最小工程,分别用XML、注解和Java配置创建同一种Bean,再故意制造自动装配冲突和循环依赖,观察报错信息。只有亲自动手触发问题,才能真正理解Spring容器的工作方式,少走弯路。