【JavaEE】Spring IoC(一)

请不要介意我上一个系列还没写完就开了个新的,会写的都会写的(●ˇ∀ˇ●)

我们刚开始学习Java Web或自己写一些小项目时,几乎不会意识到"对象管理"本身是一个问题

需要用对象,就new一个;用完了,就让它等着GC回收

这种方式简单、直接,同时也的确能解决问题

但是当项目规模逐渐变大、类越来越多、依赖关系越来越复杂时,你会慢慢发现:真正让代码变得难以维护的,往往不是业务逻辑,而是对象之间的关系

Spring的出现,正是为了解决这个问题。它的核心思想就浓缩在两个词中:IoCDI

一、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中,会做这样一件事:

  1. 发现字段上有@Autowired
  2. 读取字段类型:UserDao
  3. 在IoC容器中查找类型为UserDao的Bean
  4. 找到后,通过反射完成赋值
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在处理这段代码时,实际会按下面的顺序尝试:

  1. 查找名字叫userDao的Bean
  2. 如果没有找到,再按类型UserDao查找
  3. 如果仍然不唯一,才会报错
8.2.1 显示指定名称

此时注入的就是userDaoImpl2这个名字对应的Bean

java 复制代码
@Resource(name = "@Resource(name = "userDaoImpl2")
private UserDao userDao;

只要名字能对上,即使容器中有多个同类型Bean,也不会产生冲突

8.3 两者的核心差异

@Autowired类型优先,@Resource名称优先

并不是哪一个更高级,而是它们各自更适合不同的语义场景

好啦,本篇就到这里,下一篇我们来介绍一下Bean的生命周期与初始化~

相关推荐
身如柳絮随风扬4 小时前
Java中的CAS机制详解
java·开发语言
风筝在晴天搁浅6 小时前
hot100 78.子集
java·算法
故事和你917 小时前
sdut-Java面向对象-06 继承和多态、抽象类和接口(函数题:10-18题)
java·开发语言·算法·面向对象·基础语法·继承和多态·抽象类和接口
Configure-Handler7 小时前
buildroot System configuration
java·服务器·数据库
:Concerto8 小时前
JavaSE 注解
java·开发语言·sprint
电商API_180079052478 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
一点程序9 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
C雨后彩虹9 小时前
计算疫情扩散时间
java·数据结构·算法·华为·面试
2601_949809599 小时前
flutter_for_openharmony家庭相册app实战+我的Tab实现
java·javascript·flutter
vx_BS813309 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计