请不要介意我上一个系列还没写完就开了个新的,会写的都会写的(●ˇ∀ˇ●)
我们刚开始学习Java Web或自己写一些小项目时,几乎不会意识到"对象管理"本身是一个问题
需要用对象,就new一个;用完了,就让它等着GC回收
这种方式简单、直接,同时也的确能解决问题
但是当项目规模逐渐变大、类越来越多、依赖关系越来越复杂时,你会慢慢发现:真正让代码变得难以维护的,往往不是业务逻辑,而是对象之间的关系
Spring的出现,正是为了解决这个问题。它的核心思想就浓缩在两个词中:IoC 和DI
一、Old school的创建对象方法
来看一个最常见、最自然的写法
java
public class UserService{
private UserDao userDao = new UserDao();
}
这段代码本身没有任何问题,但是它隐含了几个默认前提:UserService必须知道UserDao的具体实现;UserDao的创建时机由UserService决定;UserDao时单例还是多例,由使用者决定
当系统只有一两个类时,这些都不是问题
但是一旦出现以下这些需求,问题就出现了
- 想把UserDao换成另一个类实现
- 想在测试时使用Mock对象
- 想统一管理对象的声明周期
- 想在不改代码的情况下完成扩展
此时你会发现,对象的创建和使用紧紧耦合在了一起
问题的本质在于:对象的控制权始终掌握在"使用者"手里
二、嘛叫IoC?
Spring提出的IoC,全名Inversion of Control,翻译过来就是控制反转,并不是某种神奇技术,而是一种设计思想的转变
有了它,对象不再由使用者创建,而是统一交给容器进行管理
人话就是,Spring把原本分散在各个类中的对象控制权,集中到了一个地方同一管理,这个地方就是Spring IoC容器
在Spring中,对象什么时候创建,由容器决定;对象依赖谁,由容器注入;对象什么时候销毁,由容器管理
而开发者需要做的,只剩下两件事:
- 告诉Spring哪些类需要被管理
- 告诉Spring这些类之间有什么依赖关系
三、Bean(豆?
当一个普通的Java类被Spring容器接管之后,Spring会给它一个统一的称呼:Bean
也就是说,Bean本质上还是Java对象,只是它的生命周期不再有程序员控制
一个对象一旦成为Bean,就意味着它不再通过new创建,可以被注入到其他Bean中,它的创建、使用、销毁都有Spring参与
四、怎么把对象托付给Spring
既然Spring要帮我们管理对象,那我们就需要一种方式,把类注册到Spring容器中
Spring提供了两大类方式实现这种注册
4.1 使用类注解注册Bean
这是最常用的一种方式,直接在想要被管理的类上添加注解(五个)
Spring提供了五个注解来实现这一功能,这些注解功能上都是等价的,更多是在职责上的划分
| 注解 | 含义 |
|---|---|
@Controller |
控制层 |
@Service |
业务逻辑层 |
@Repository |
数据访问层 |
@Component |
通用组件 |
@Configuration |
配置类 |
这些注解都是告诉Spring,这个类需要被扫描,并注册为一个Bean
4.2 使用@Bean方式注册Bean
另一种方式,是通过方法返回对象的形式,将对象交给Spring管理
java
@Configuration
public class AppConfig {
@Bean
public User user() {
return new User();
}
}
乍一看这种写法好像有点反直觉,明明都已经手动new对象了,为什么还要交给Spring?
关键点就在于,@Bean管的不是"new"这一步,而是new之后的这个对象
只要方法返回的是对象,Spring就会把它注册到IoC容器中,这个对象从此就拥有了一个正式身份------Bean
那么什么时候会有这种需求呢?
- 第三方类:类在jar包里,源码改不了
- 创建过程比较复杂:构造参数很多,需要先做一堆初始化操作
- 不适合加@Component的对象:比如纯配置性质的对象,或者你不希望它被"自动扫描"
举个例子:
java
@Bean
public DataSource dataSource() {
DataSource ds = new DataSource();
ds.setUrl(...);
ds.setUsername(...);
ds.setPassword(...);
return ds;
}
这种对象如果你强行让它自己带注解,反而会让类职责变得不清晰
4.2.1 @Bean和@Component区别
看到这里大家一定会有一个疑问(就是会有的),既然两种方式都能注册Bean,那我到底用哪一种?
其实这里它们解决的是两个不同层面的问题
@Component:这个类本身就是一个组件
@Bean:我手里有一个对象,希望你Spring能帮我管
换句话说,@Component更偏向"声明",而@Bean更偏向配置。前者强调这个类的身份,后者强调这个对象的产生过程
4.2.2 @Configuration不是装饰品
你可能注意到,@Bean通常都会写在@Configuration类中
这不是约定俗成,而是必须这么做
它会告诉Spring,这是一个配置类,里面的方法需要被容器管理
最关键的一点是:@Configuration会保证@Bean方法返回的是同一个对象实例
也就是说
java
@Bean
public User user() {
return new User();
}
这段代码中,无论你在容器中获取多少次User,最终拿到的,都是同一个对象。如果去掉@Configuration,这个保障就不存在了
4.2.3 @Bean也可以注入依赖
@Bean方法不是孤立存在的,它同样支持依赖注入(这里提前剧透一下DI,嘿嘿)
java
@Bean
public UserService userService(UserDao userDao) {
return new UserService(userDao);
}
你会发现,这里的方法参数不需要手动传,Spring会自动从容器中查找匹配的Bean
这说明,@Bean方法,本身也参与整个IoC和DI体系
它不是"特殊通道",而是Spring管理对象的一等公民
4.3 BeanName的默认规则
Spring在注册时,将会为每个Bean分配一个名字
- 类注解方式
- 默认是类名首字母小写
- 如果类名前两个字母都是大写,则保持原名
- @Bean方式
- 默认是方法名
理解BeanName,有利于后续理解依赖注入冲突问题
五、ApplicationContext是什么?
Spring启动时,会创建一个ApplicationContext对象
这个对象可以理解为:整个Spring应用的运行环境+ IoC 容器
它负责创建Bean、保存Bean、提供获取Bean的能力
只要一个对象没有进入ApplicationContext,就不可能被Spring管理
5.1 从容器中获取Bean
Spring提供了多中获取方式
按类型获取
java
UserService userService = context.getBean(UserService.class);
这种方式简洁,但是有一个前提:容器中该类型的Bean只能有一个
按名称获取
java
Object bean = context.getBean("userService");
这种方式灵活,但返回的是Object,通常需要强转
按名称 + 类型
java
UserService userService = context.getBean("userService", UserService.class);
这是最严谨、最安全的一种方式
5.2 默认的单例特性
在不做特殊配置的情况下,同一个Bean,多次从容器中获取,得到的是同一个对象实例
这也是为什么我们通常不会担心Bean被频繁创建的问题
六、Spring怎么发现这些类的呢?
Spring并不会"自动知道"项目里有哪些类需要管理,它依赖的是组件扫描机制
6.1 默认扫描规则
在Spring Boot项目中,默认从启动类所在包开始扫描;向下扫描所有子包
这也是为什么通常建议将启动类放在项目的最外层包下,类似这样

6.2 自定义扫描路径
java
@ComponentScan({
"com.example.service",
"com.example.dao"
})
通过这种方式,可以显示指定需要扫描的包路径
七、DI登场!
对象被Spring管理之后,真正的价值在于:它可以被注入到其他Bean中使用,这一步就是依赖注入(DI)
依赖注入:Spring在创建Bean的过程中,将其所依赖的其他Bean自动注入进去
整个过程发生在运行时,由Spring完成
7.1 三种常见的注入方式
(1)属性注入
java
@Autowired
private UserDao userDao;
这种方式最直观,也最常见
优点是代码简洁;缺点是依赖不够显式,且不支持final
(2)构造方法注入
java
@Service
public class UserService {
private final UserDao userDao;
public UserService(UserDao userDao) {
this.userDao = userDao;
}
}
构造方法注入的优势在于:
- 依赖关系清晰
- 对象创建后依赖不可变
- 更利于测试
- 与Spring框架解耦
因此在现代Spring项目中,这是最推荐的方式
(3)Setter注入
java
@Autowired
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
Setter注入提供了更高的灵活性,但也意味着依赖可能被修改
八、@Autowired与@Resource
在前面我们已经看到,无论是属性注入、构造方法注入还是Setter注入,本质上都是在做同一件事:从Spring容器中,找到一个合适的Bean,注入到当前对象中
而@Autowired和@Resource正是完成这件事最常用的两种方式
他们看起来非常相似,但背后的注入规则和设计理念并不一致
8.1 @Autowired:按类型注入
@Autowired是Spring提供的注解,它的核心规则可以总结为一句话:优先按照类型从IoC容器中查找并注入Bean
最常见的写法是直接标在属性上
java
@Service
public class UserService {
@Autowired
private UserDao userDao;
}
Spring在创建UserService这个Bean中,会做这样一件事:
- 发现字段上有@Autowired
- 读取字段类型:UserDao
- 在IoC容器中查找类型为UserDao的Bean
- 找到后,通过反射完成赋值
8.1.1 当容器中只有一个同类型Bean
这是最理想、也是最常见的情况
java
@Repository
public class UserDaoImpl implements UserDao {
}
此时,@Autowired几乎不会出问题
8.1.2 容器中有多个同类型Bean
纷争开始了问题出现了
java
@Repository
public class UserDaoImpl1 implements UserDao {
}
@Repository
public class UserDaoImpl2 implements UserDao {
}
此时再写
java
@Autowired
private UserDao userDao;
Spring会直接抛出异常,NoUniqueBeanDefinftionException
原因也很直观,按照类型查找,能匹配到多个Bean,Spring不知道该选谁
8.1.3 使用@Primary指定默认实现
如果你希望在没有明确指定的情况下,默认使用某一个实现类,可以使用@Primary
java
@Primary
@Repository
public class UserDaoImpl1 implements UserDao {
}
这样一来,容器中仍然存在多个UserDao,但Spring会优先选择被@Primary标记的那个
8.1.4使用@Qualifier精确指定Bean
如果你希望明确指定要注入哪个Bean,可以使用@Qualifier
java
@Autowired
@Qualifier("userDaoImpl2")
private UserDao userDao;
此时Spring的决策顺序会变成:先看@Qualifier指定的名称,再验证类型是否匹配
只要名称能唯一定位到Bean,就不会再出现歧义问题
@Qualifier 不仅可以加在属性上,也可以加在构造方法参数上
java
public UserService(@Qualifier("userDaoImpl2") UserDao userDao) {
this.userDao = userDao;
}
8.2@Resource:先按名称,再按类型
和@Autowired不同,@Resource不是Spring提供的注解,而是JDK的标准注解
它的注入规则刚好和@Autowired相反:先按名称匹配,找不到再按类型匹配
java
@Resource
private UserDao userDao;
Spring在处理这段代码时,实际会按下面的顺序尝试:
- 查找名字叫userDao的Bean
- 如果没有找到,再按类型UserDao查找
- 如果仍然不唯一,才会报错
8.2.1 显示指定名称
此时注入的就是userDaoImpl2这个名字对应的Bean
java
@Resource(name = "@Resource(name = "userDaoImpl2")
private UserDao userDao;
只要名字能对上,即使容器中有多个同类型Bean,也不会产生冲突
8.3 两者的核心差异
@Autowired类型优先,@Resource名称优先
并不是哪一个更高级,而是它们各自更适合不同的语义场景
好啦,本篇就到这里,下一篇我们来介绍一下Bean的生命周期与初始化~