WD
Classnote Docs课程课件
11

Spring IOC - 控制反转与依赖注入

学习目标:

  • 掌握 IOC 和 DI 的概念,并能说清两者之间的关系
  • 熟悉注册组件的几种方式和相关注解
  • 熟悉从容器中取出组件的几种方式和相关 API
  • 理解 Spring 组件生命周期的含义
  • 熟悉 BeanPostProcessor 的执行时机
  • 掌握使用 FactoryBean 向容器中注册组件的方式

本章重点:

  • 从手动创建实例到 IOC 容器统一管理实例的思路演变
  • IOC 控制反转与 DI 依赖注入的概念边界
  • XML、配置类、注解三类组件注册方式
  • 构造器注入、方法注入、成员变量注入的使用方式
  • Bean 实例化、作用域、生命周期与 BeanPostProcessor 扩展点

前置知识准备

  • 注解的相关知识@Target、@Retention、属性
  • @Target → 描述注解可以出现在什么位置
  • @Retention → 注解何时生效
  • 属性,如果有提供默认值可以省略不写,如果没有提供默认值就必须要写;
  • 数组 → 数组中如果只有一个值可以省略掉{}
  • value属性 → 如果只使用了value属性,“value=”可以省略掉
  • 理解容器的思想
01 / Section

1. 介绍Spring

1.1 SpringFramework的起源

Spring Framework通常人们称之为Spring。

Spring是一个开源框架,Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Development and Design中阐述的部分理念和原型衍生而来。

它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 J2EE 应用程序开发提供集成的框架。

Spring是一个分层的Java SE/EE full-stack(一站式) 轻量级开源框架。

1.2 思路演变:从手动创建到容器管理

阶段一:每个入口自己创建实例。 在传统 JavaEE 写法中,UserServletProductServletGoodsServlet 都可能在自己的代码里直接 new 出需要的 Service 对象。这样虽然能完成调用,但实例创建逻辑分散在各个 Servlet 中,同一个 Service 还可能被重复创建,后续替换实现或统一增强都会变得困难。

阶段一:每个入口自己创建实例

阶段二:把创建动作从 Servlet 中拿出去。 Servlet 的核心职责应该是接收请求、调用业务、返回响应,而不是管理业务对象的创建。如果 Service 仍然由 Servlet 直接创建,控制层就会和具体实现类强耦合,因此需要把对象创建权从 Servlet 中剥离出来。

阶段二:把创建动作从 Servlet 中拿出去

阶段三:通过统一对象管理区获取引用。 当对象不再由 Servlet 创建后,可以把程序运行所需的实例集中保存到一个统一位置。Servlet 只负责根据需要获取对象引用,这样创建逻辑被集中管理,调用方也不必关心对象到底是如何产生的。

阶段三:通过统一对象管理区获取引用

阶段四:容器用映射关系维护实例。 这个统一对象管理区可以先简单理解成一个线程安全的 Map,例如 ConcurrentHashMap:key 保存组件名称,value 保存组件实例。Spring IOC 容器就是在这个基础思想上继续扩展,进一步负责实例创建、依赖装配、生命周期管理和扩展点回调。

阶段四:容器用映射关系维护实例

核心点:我们保存的是什么,我们取出的是什么?

保存的是应用程序运行过程中所需要的实例,比如adminService、userService等;取的也是这些实例

1.3 IOC控制反转

Inverse of Control

控制反转这个词要拆开来看

控制:实例的生成权

反转:由应用程序反转给Spring容器

容器:生成并管理实例的抽象空间

获取依赖对象被反转了,它是被动获取;

正转就是自己去new一个对象,自己获取对象

1.4 DI依赖注入

Dependency Injection

Q1谁依赖谁?Q2为什么需要依赖?Q3谁注入谁?Q4注入了什么?

思考上面的问题,一定要在控制反转的基础上去思考。

思考的是应用程序和Spring容器之间的关系 👉 经过了控制反转,Spring容器(IoC容器)掌握了更多的实例,变得更加富有,而应用程序变得“贫穷”

Answer1 应用程序依赖于IoC容器;

Answer2 应用程序需要IoC容器来提供对象需要的外部资源;

Answer3 IoC容器注入应用程序某个对象,应用程序依赖的对象;

Answer4 注入某个对象所需要的外部资源(包括对象、资源、常量数据);

核心点:从容器中获得应用程序所需要的实例,并且给应用程序中的成员变量做赋值

1.5 Spring的优点

  • 方便解耦,简化开发(高内聚低耦合)
  • AOP编程的支持
  • 声明式 事务的支持
  • 方便程序的测试
  • 方便集成各种优秀框架
  • 降低JavaEE API的使用难度

本章小结

Spring 的 IOC 容器负责集中管理实例,应用程序不再到处手动 new 对象,而是从容器中获取或接收容器注入的对象。

02 / Section

2. 入门案例

2.1 思考:如何自己管理实例

如果给你提供一个类的信息,你能否管理对应的实例

比如,提供一个properties配置文件

properties
classlist=com.cskaoyan.demo1.service.UserServiceImpl,com.cskaoyan.demo1.service.GoodServiceImpl,com.cskaoyan.demo1.service.AdminServiceImpl

step1. List\<String> classList;

step2. 遍历 单个值 是不是class的全限定类名 →

java
 Class clazz = Class.forName("com.cskaoyan.demo1.service.UserServiceImpl")

step3. 可以通过反射获得实例

java
Object instance = clazz.newInstance();

step4. 放入到map中

java
// Map map = new ConcurrentHashMap<>();
map.put("userServiceImpl",instance);

结论:Spring框架其实就是通过反射来创建的实例

2.2 入门案例1:从容器中取出组件

引入依赖

引入依赖 beans、context、aop、expression、core、jcl 5+1

xml
<dependencies>
  <!--引入依赖 beans、context、aop、expression、core、jcl、micrometer-observation 5+2-->
  <!--io.micrometer:micrometer-observation:1.14.14内置的可观测性埋点-->
  <!--spring-context-->
  <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.2.15</version>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.10.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

提供接口和实现类

java
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello(String name) {
        System.out.println("hello " + name);
    }
}

原先使用实例的时候,是通过构造方法生成的;后续要变更为来源Spring容器

Spring配置文件(匆匆过客)

配置文件为Xml格式的文件,需要引入对应的

[Schema约束]: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#xsd-schemas-context

xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- bean definitions here -->

</beans>

在配置文件中注册组件

xml
<!--spring配置文件的名称通常叫application(-xxx).xml-->
<!-- bean definitions here -->
<!--控制反转-->
<!--id属性 👉 组件在容器中的唯一标识-->
<!--name属性 👉 名称 👉 通常省略不写-->
<!--class 全类名 👉 实现类的全类名-->
<!--组件 注册 👉 将实例交给spring管理的过程我们称之为注册-->
<bean id="userService" class="com.cskaoyan.demo1.service.UserServiceImpl"/>

从容器中取出组件,执行方法

从容器中取出组件

注意:取出组件的3种方式!

java
/**
     * 向容器中注册组件,从容器中取出组件
     */
@Test
public void testCase1() {
  // Spring容器
  // 初始化容器,并且注册组件
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
  // 从容器中取出组件
  // 按照组件的id(name)取出组件
  UserService userService1 = (UserService) applicationContext.getBean("userService");
  userService1.sayHello("spring");
  // 可以写接口的class,也可以写实现类的class,建议写接口
  // 如果容器中某个类型的组件只有一个,可以按照类型取出
  UserService userService2 = applicationContext.getBean(UserService.class);
  // id + 类型
  UserService userService3 = applicationContext.getBean("userService", UserService.class);
}

后续绝大多数情况是按照类型取出组件,因为绝大多数情况容器中某个类型的组件只有一个

2.3 入门案例2:维护组件之间的依赖关系

维护组件之间的依赖关系,在容器中注册dao层和service层组件,并且service层的组件依赖于dao层组件(谁需要谁就是谁依赖谁)

service类和dao类

通过在service类中增加dao类成员变量维护依赖关系

java
public class UserServiceImpl implements UserService{
    //主动 → 控制
    //UserDao userDao = new UserDaoImpl();
    UserDao userDao;

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public void serviceSayHello() {
        System.out.println("service层的sayHello");
        userDao.sayHello();
    }
}
java
public class UserDaoImpl implements UserDao{
    @Override
    public void sayHello() {
        System.out.println("hello xiaowu");
    }
}

维护组件之间依赖关系

通过property标签的ref属性维护组件之间的依赖关系

xml
<bean id="userService" class="com.cskaoyan.demo1.service.UserServiceImpl">
  <!--注册UserServiceImpl这个类型的组件的时候,实例化的过程会执行set方法(setUserDao)
            这个set方法的形参是UserDao类型的实例,通过id从容器中取出作为形参
            this.userDao = userDao; 给userServiceImpl这个组件的userDao成员变量做了赋值
            → 依赖注入
        -->
  <property name="userDao" ref="userDao"/>
</bean>
<bean id="userDao" class="com.cskaoyan.demo1.dao.UserDaoImpl"/>

单元测试

直接从容器中取出service组件,执行对应的方法

java
@Test
public void testCase2() {
  ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
  UserDao userDao = applicationContext.getBean(UserDao.class);
  UserService userService = applicationContext.getBean(UserService.class);
  // 直接从容器中取出的userDao实例和从容器中取出的userService实例中的userDao成员变量是否是同一个
  /*UserServiceImpl userService = applicationContext.getBean(UserServiceImpl.class);

        userService.setUserDao(userDao);*/
}

注意:一定是从容器中取出对应的组件

思考:右侧应用程序中的userDaoImpl的实例是否是同一个

容器中的实例引用关系

入门案例2实例关系

本章小结

Spring 如何通过配置注册组件,Spring 如何维护组件之间的依赖关系。

掌握

  • 能根据类的全限定名理解 Spring 底层可以通过反射创建实例
  • 能用 XML 注册一个 Bean,并通过 ApplicationContext 取出组件
  • 能通过 property 标签理解最基础的依赖注入过程
03 / Section

3. 注解驱动开发

在使用Spring注解的时候,我们按照功能来进行划分

1、 标记 isAnnotationPresent

2、 提供值 getDeclaredAnnotation(xxx.class).方法

接下来的注解有意义的前提是使用Spring技术 → 要有容器 ApplicationContext

3.1 配置类

配置类,承担做通用配置的功能,同时在配置类中可以组件注册

  • 我们在类定义上增加一些功能性的注解,增加一些通用性的配置
  • 我们在类中的方法里注册组件

比如我们定义一个配置类,需要在类上增加一个@Configuration注解

java
// 这里增加功能性的注解
@Configuration
public class SpringConfiguration {
    // 类中的方法做组件的注册
}

3.2 组件注册功能(IOC)

类直接注册

组件注册功能首先要打开扫描开关

java
// 这里增加功能性的注解
@Configuration
@ComponentScan("com.cskaoyan.demo1")
public class SpringConfiguration {
    // 类中的方法做组件的注册
}

组件注册功能的注解@Component

除了@Component注解,还有什么类似的注解

java
@Target(ElementType.TYPE)// 该注解写在类上
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {

   /**
    * The value may indicate a suggestion for a logical component name,
    * to be turned into a Spring bean in case of an autodetected component.
    * @return the suggested component name, if any (or empty String otherwise)
    */
   String value() default "";

}
  • @Service → 通常是Service层的组件使用的注解,Service层组件也能使用@Component
  • @Repository → 通常是Dao层的组件使用@Repository注解,dao层组件也能使用@Component
  • @Configuration → 配置类组件
  • @Controller(SpringMVC阶段)

@Service、@Repository、@Controller、@Configuration,这些注解的ElementType都是TYPE,也就是这些注解都是要写在类定义上。

组件id默认为类名的首字母小写,另外也可以使用注解的value属性来指定组件id

java
//@Component
//@Repository("userDao")  //组件id为userDao
@Repository               //组件id为userDaoImpl
public class UserDaoImpl implements UserDao{
}
/**
 * 增加其value属性,value属性值就是id
 * 如果没有增加value属性,id默认值是类名的首字母小写
 * @author stone
 * @date 2023/04/11 15:11
 */
//@Component // 组件id的默认值是goodsServiceImpl
@Component("goodsService") // 使用value属性值指定了组件id为goodsService
public class GoodServiceImpl implements GoodsService{
}

思考:为什么我们提供一个扫描包目录(@ComponentScan("包目录")),然后在包目录下的类中使用注解就可以注册组件?

它是按照什么思路来做的?

  1. 提供包目录,能否获得这个包以及这个包的子包下的所有的类的全限定类名
  2. 通过全限定类名,通过反射的方式获得对应的class → Class.forName()
  3. List\<Class> classList = 通过上面的过程获得
  4. 遍历获得其中的单个class呢
  5. class.isAnnotationPresent(注解的class) → 判断这个类上是否有注解
java
@SneakyThrows
@Test
public void testIsAnnotationPresent() {
    Map<String,Object> map = new ConcurrentHashMap<>();
    List<Class<?>> classList = Arrays.asList(UserServiceImpl.class, GoodServiceImpl.class, UserServiceImpl.class);
    for (Class<?> singleClass : classList) {
        if (singleClass.isAnnotationPresent(Component.class)) {
            Object instance = singleClass.newInstance();
            String key = singleClass.getName();
            map.put(key, instance);
        }
    }
    System.out.println(map);
}

配置类注册(JavaConfig)

java
@Configuration
@ComponentScan("com.cskaoyan.demo2")
public class ApplicationConfiguration {

    // 在配置类中注册AdminServiceImpl组件
    // 在配置类中写的是方法 → 提供一个返回值为AdminServiceImpl类型的实例
    // 应用程序启动的时候 → 加载配置类 → 执行配置类中的方法(@Bean) → 方法的返回值注册为容器中的组件
    @Bean("wdAdminService")
    public AdminService adminService() {
        AdminService adminService = new AdminServiceImpl();
        System.out.println(adminService);
        return adminService;
    }

    // 容器中userService类型的组件有几个
    // 组件id默认值是方法名;可以使用@Bean的value属性指定
    // @Bean对应的方法的形参,默认是按照类型从容器中取出组件
    // 如果形参这个类型的组件在容器中不止一个,可以使用@Qualifier指定组件
    @Bean
    public UserService userService(@Qualifier("userDaoImpl") UserDao userDao) {
        UserServiceImpl userService = new UserServiceImpl();
        // 可否在组件注册过程中,从容器中取出UserDao的实例,给这个组件的成员变量赋值呢
        userService.setUserDao(userDao);
        return userService;
    }
}

在配置类中完成对应的组件注册以及相关配置,配置类的核心就是提供对应的信息

JavaConfig的目标是干掉配置文件,JavaConfig也是SpringBoot推荐使用的配置方式,SpringBoot中不再使用Spring的xml配置文件

在配置类中,使用注册功能的注解@Bean 以及方法来完成组件注册

java
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {}

该注解增加在方法上,并且可以和其他注解共存

这个方法的返回值注册为容器中的组件

java
/**
 * 要通过这个方法注册一个DruidDataSource组件
 * @return 应该返回的是一个DruidDataSource的实例,这个返回值会注册为容器中的组件
 * 返回值的定义:可以写实现类,也可以写接口;提供组件的类型信息给到容器中;建议写接口
 * SE的代码风格来提供属性值就可以
 * 通过这种方式注册的组件id:1、默认值是方法名;2、@Bean注解的value属性可以指定组件id
 */
@Bean
public DataSource dataSource(){
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/cskaoyan_db?useUnicode=true&characterEncoding=utf-8");
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    return dataSource;
}

想要通过@Bean注册Component1这个组件

java
public class Component1 {
    
    DataSource dataSource;

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
java
/**
 * Component1这个类中有一个成员变量 叫dataSource需要的是一个DataSource类型的组件
 * 设置的这个DataSource想要从容器中来获得
 * 形参:默认是按照类型从容器中取出组件的  ac.getBean(Class)
 * @return
 */
@Bean
public Component1 component1(DataSource dataSource) {
    Component1 component1 = new Component1();
    component1.setDataSource(dataSource);
    return component1;
}

额外增加一个DataSource组件的话,容器中DataSource类型的组件不止一个,通过形参从容器中取出组件需要指定组件id

java
@Bean
public DataSource dataSource2(){
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName("com.mysql.jdbc.Driver");
    dataSource.setUrl("jdbc:mysql://localhost:3306/cskaoyan_db2?useUnicode=true&characterEncoding=utf-8");
    dataSource.setUsername("root");
    dataSource.setPassword("123456");
    return dataSource;
}

/**
 * Component1这个类中有一个成员变量 叫dataSource需要的是一个DataSource类型的组件
 * 设置的这个DataSource想要从容器中来获得
 * 形参:默认是按照类型从容器中取出组件的  ac.getBean(Class)
 * 如果形参所需的组件在容器中不止一个,需要额外指定组件id的信息 → @Qualifier 的value属性指定组件id
 * 形参的名称也可以作为组件的id,但是我们更建议使用@Qualifier ,更直观一些
 * @return
 */
@Bean
public Component1 component1(@Qualifier("dataSource2") DataSource dataSource) {
    Component1 component1 = new Component1();
    component1.setDataSource(dataSource);
    return component1;
}

@Bean

返回值类型、方法名、形参、返回值、@Bean注解的value属性 各自的含义大家需要关注

3.3 组件注入功能(DI)

注意:要求是容器中的组件,才能够使用注入功能的注解

JavaConfig的@Bean

方法的形参,就是从容器中取出的组件

构造器注入

如果类中没有无参构造方法的话,如果这个类上有组件注册功能的注解,它会使用有参构造方法来完成实例化。

有参构造方法的形参会从容器中取出组件

java
@Component
public class Component2 {
    DataSource dataSource;

    public Component2(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

Component2中没有无参构造方法,就会使用有参构造方法来完成实例化

  • 默认是按照类型从容器中取出组件
  • 如果这个类型的组件在容器中不止一个,会出现 NoUniqueBeanDefinitionException
  • 可以使用 @Qualifier 指定组件 id形参名称也可以但不建议
java
@Component
public class Component2 {
    DataSource dataSource;

    public Component2(@Qualifier("dataSource1") DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
  • 其中的常量值可以通过@Value注解来提供
java
public UserServiceImpl(UserDao userDao, @Value("zhangsan") String name) {
    this.userDao = userDao;
    this.name = name;
}
  • 如果有多个有参构造方法,需要指定构造方法
java
@Service
public class UserServiceImpl implements UserService {
    UserDao userDao;
    String name;

    public UserServiceImpl(UserDao userDao, @Value("zhangsan") String name) {
        this.userDao = userDao;
        this.name = name;
    }

    @Autowired
    public UserServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
}

场景:实际我们开发过程中很少使用有参构造器注入,成员变量变化是比较快,如果要使用有参构造器注入,意味着要经常修改中合格有参构造器,比较繁琐

但是后续SpringBoot中使用的比较多

方法注入

可以是组件中的任意方法,但是通常这样的方法我们用的是set方法在方法上增加@Autowired注解

java
@Component
public class Component3 {
    DataSource dataSource;

    // 默认这个方法并不会自动执行
    // 如果我们在上面增加了@Autowired 注解的话,在生命周期设置属性值的过程中会自动执行
    // 形参默认按照类型从容器中取出;如果要指定组件id,还是@Qualifier
    @Autowired
    public void setDataSource(@Qualifier("dataSource2") DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

(set)方法这种形式是Spring框架建议使用的方式,但其实绝大部分程序员用的都不是这种方式

成员变量注入

注入功能的注解使用这三组:

  1. @Autowired
  2. @Autowired + @Qualifier
  3. @Resource

容器中注册了这些类型的组件,OrderDao类型的组件(orderDaoImpl)、UserDao类型的组件userDaoImpl1和userDaoImpl2

java
@Repository
public class OrderDaoImpl implements OrderDao{
}
@Repository
public class UserDaoImpl1 implements UserDao{
}
@Repository
public class UserDaoImpl2 implements UserDao{
}

从容器中取出的对应的组件,执行注入,要注意,要求是在容器中的组件里注入

java
/**
 * 要使用注入功能注解,一定要保证当前类是容器中的组件
 */
@Service
public class UserServiceImpl implements UserService{
    @Autowired //容器中该类型的组件只有一个
    OrderDao orderDao;
    @Autowired //使用@Qualifier注解指定组件id
    @Qualifier("userDaoImpl1")
    UserDao userDao1;
    @Resource(name = "userDaoImpl2") //默认是按照组件的类型去注入,使用name属性指定组件id
    UserDao userDao2;
}

注意事项

  • 开发业务代码过程中,最常用的方式只使用一个@Autowired :绝大部分组件在容器中这个类型的组件只有一个
  • 要在容器中的组件中使用这些注解,使用注解的话,所处的类上要有组件注册功能的注解,且处于扫描包目录

3.4 Spring的单元测试

目的是把单元测试类当做是容器中的组件,那么就可以直接再单元测试类中直接去做注入(也就是直接使用注解)

引入spring-test依赖(和前面使用的Spring保证是同一个版本)

xml
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>6.2.15</version>
  <scope>test</scope>
</dependency>

使用@Extendwith和@ContextConfiguration注解,在单元测试类中可以使用注入功能的注解

java
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:application.xml")
public class MyTest2 {

    @Autowired
    UserService userService;
    @Autowired
    OrderDao orderDao;
    @Autowired
    @Qualifier("userDaoImpl1")
    UserDao userDao1;
    /**
     * 从容器中取出UserService组件
     * 查看userService组件中的成员变量是否取出容器中的组件
     */
    @Test
    public void mytest1(){

    }
}

本章小结

本章核心概念:注解驱动开发把组件注册、组件扫描、配置类和依赖注入组合起来,是日常 Spring 开发中最常用的写法。

你现在应该掌握

  • 能通过 @Configuration@Bean@ComponentScan 完成组件注册
  • 能区分 JavaConfig 注册和类注解注册的适用场景
  • 能使用构造器注入、方法注入和成员变量注入完成组件装配
  • 能用 Spring 单元测试加载容器并验证组件是否生效
04 / Section

4. Bean的准备

4.1 Bean的实例化

无参构造方法(默认方式)

这也是最常用的一种方式

java
@Service
public class CategoryServiceImpl implements CategoryService{
    @Autowired
    UserDao userDao;

    public CategoryServiceImpl() {
        System.out.println("CategoryServiceImpl的无参构造方法");
    }
}

有参构造方法

java
@Component
public class AdminServiceImpl implements AdminService{
    UserDao userDao;
    String username;
    // 如果你有无参构造方法,默认使用无参构造方法;
    // 如果你没有无参构造方法,实例化时会使用有参构造方法
    // 形参,默认是按照类型从容器中取出组件
    // 如果形参这个类型的组件在容器中不止一个,可以使用@Qualifier指定组件
    // 如果存在多个构造器,可以指定使用你标记构造器
    //@Autowired
    public AdminServiceImpl(UserDao userDao) {
        this.userDao = userDao;
    }
    @Autowired
    public AdminServiceImpl(UserDao userDao,@Value("zhangsan") String username) {
        this.userDao = userDao;
        this.username = username;
    }
}

工厂

工厂提供实例,而实例交给Spring容器来进行管理

工厂(略)

通过配置类中的方法,方法上增加@Bean注解,该方法的返回值注册为容器中的组件

java
@Configuration
@ComponentScan("com.cskaoyan")
public class AppConfiguration {

  //这就是一种工厂
  @Bean
  public UserService serviceProxy(UserService userService) {
    return ProxyUtil.getServiceProxy(userService);
  }
}

FactoryBean

实现FactoryBean接口,组件类型和FactoryBean接口中的getObject方法的类型相同

java
public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

也就是你明面上看起来注册时FactoryBean组件,实际上取出组件的时候取出的是getObject方法返回的实例;

这个其实是Spring为了第三方组件开的一个口子,最开始有些框架设计的时候没有考虑Spring技术,但是后面又想要使用Spring,那么必然要有Spring来管理实例,可以通过提供额外实现的拓展类FactoryBean,完成对应所需要实例的管理。比如MyBatis中的SqlSessionFactory通过一个SqlSessionFactoryBean来完成实例的管理

比如要注册一个User类型的组件,可以通过User对应的FactoryBean来注册组件。

java
/**
 * 直接注册为容器中的组件
 * FactoryBean 👉 XXXFactoryBean
 *      👉 组件类型和FactoryBean接口中的getObject方法相关
 * BeanFactory和factoryBean:
 *      BeanFactory:生产的是容器中的所有的组件
 *      FactoryBean:生产的是特定的组件
 */
@Component
public class UserFactoryBean implements FactoryBean<User> {

    /**
     * 完成组件的实例化
     * @return 组件类型和返回值相关
     * @throws Exception
     */
    @Override
    public User getObject() throws Exception {
        User user = new User();
        return user;
    }

    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

这里在举一个例子,通过FactoryBean的形式向容器中注册一个UserService的代理组件

java
@Component
public class UserServiceFactoryBean implements FactoryBean<UserService> {
  @Autowired
  UserService instance;
  @Override
  public UserService getObject() throws Exception {
    UserService proxy = (UserService) Proxy.newProxyInstance(UserServiceFactoryBean.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), new InvocationHandler() {
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开启事务");
        Object invoke = method.invoke(instance, args);
        System.out.println("提交并关闭事务");
        return invoke;
      }
    });
    return proxy;
  }

  @Override
  public Class<?> getObjectType() {
    return UserService.class;
  }
}

思考1

我们 在什么情况下使用的工厂的方式?

难道我们不是直接用构造方法更方便么,为啥还要使用工厂?

确实,使用构造方法是很方便,但是有些情况用不了构造方法。比如动态代理 ProxyFactoryBean

使用构造器要提供大量的参数,而提供大量的参数过程又比较繁琐,也就是通常在使用一些框架的时候,会给你提供一些对应的工厂

一些框架已经写好了一些代码,要在此基础上增加对Spring框架的支持,要将框架中的一些核心的对象交给Spring容器来管理,通常就会提供一个框架的拓展包,拓展包中提供对应的工厂,使用工厂可以直接将这个框架需要的核心对象注册为容器中的组件 SqlSessionFactoryBean

工厂:主要就是对已有的代码做些拓展

面试题

BeanFactory和FactoryBean之间的联系和区别

联系:通过他两都可以向容器中注册组件

区别:BeanFactory是容器,所有的组件注册都是通过BeanFactory;而FactoryBean注册的特定的单个组件

4.2 作用域 Scope

Singleton:单例,每一次取出组件都是同一个组件

Prototype:原型,每一次取出组件都是全新的组件

默认值是singleton,我们通常省略不写

@Scope 使用value属性指定作用域

java
@Component
@Scope("singleton")
public class SingletonBean {
}
@Component
@Scope("prototype")
public class PrototypeBean {
}
@Component
public class DefaultBean {
}

注册3个组件,分别给到不同的scope,然后从容器中取出组件多次,查看是否是同一个组件(查看内存地址)

java
@Test
public void mytest1(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    SingletonBean bean1 = applicationContext.getBean(SingletonBean.class);
    SingletonBean bean2 = applicationContext.getBean(SingletonBean.class);
    SingletonBean bean3 = applicationContext.getBean(SingletonBean.class);
}
@Test
public void mytest2(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    PrototypeBean bean1 = applicationContext.getBean(PrototypeBean.class);
    PrototypeBean bean2 = applicationContext.getBean(PrototypeBean.class);
    PrototypeBean bean3 = applicationContext.getBean(PrototypeBean.class);
}
@Test
public void mytest3(){
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    DefaultBean bean1 = applicationContext.getBean(DefaultBean.class);
    DefaultBean bean2 = applicationContext.getBean(DefaultBean.class);
    DefaultBean bean3 = applicationContext.getBean(DefaultBean.class);
}
java
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfiguration.class})
public class ScopeTest {
    @Autowired
    SingletonBean singletonBean1;
    @Autowired
    SingletonBean singletonBean2;
    @Autowired
    SingletonBean singletonBean3;

    @Autowired
    PrototypeBean prototypeBean1;
    @Autowired
    PrototypeBean prototypeBean2;
    @Autowired
    PrototypeBean prototypeBean3;

    @Autowired
    DefaultBean defaultBean1;
    @Autowired
    DefaultBean defaultBean2;
    @Autowired
    DefaultBean defaultBean3;
    @Test
    public void testScope() {
        System.out.println(1);
    }
}

4.3 生命周期

概念

生命周期指组件在容器中要完成实例化,组件从实例化开始直至可用状态会执行到哪些过程。

学Servlet的时候学过生命周期,Servlet的生命周期:init、service、destroy

  • 准备阶段
  • 服务阶段
  • 销毁阶段

对于Bean(容器中的组件),在容器中也会经历这样的一些阶段

  • 容器初始化的时候,组件做准备性工作 → 放入到容器中之前,会执行哪一些方法来准备实例
  • 组件可以从容器中取出,提供服务,比如从容器中取出userService实例,调用其sayHello方法
  • 容器关闭,组件做销毁工作

在特定的时间执行一系列的方法

额外引入的依赖

初始化和销毁的方法需要引入依赖(之前版本的jdk不需要引入)

Spring6版本

xml
<dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>3.0.0</version>
</dependency>

初始化阶段的方法

  1. Bean的实例化(有参构造方法、无参构造方法)
  2. 设置参数方法(方法注入、成员变量注入)
  3. BeanNameAware、BeanFactoryAware、ApplicationContextAware
  4. BeanPostProcessor(Bean的后处理器,这个后指的是实例化之后,其实还是在放入到容器中之前)的postProcessBeforeInitialization(后面有初始化方法)
  5. InitializingBean的afterPropertiesSet方法(通常是一些框架提供的类初始化的方式)
  6. 自定义的init方法(我们自己做开发的时候通常使用的方式)
  7. BeanPostProcessor的postProcessAfterInitialization

Bean 生命周期流程

Bean 生命周期执行顺序

BeanPostProcessor和正在执行生命周期的组件并不是同一个,BeanPostProcessor是额外提供的,而额外提供的这个BeanPostProcessor组件 它的作用范围:除了BeanPostProcessor本身,其他的所有组件

组件是什么时候开始执行生命周期的:容器初始化的时候(单例组件)

prototype组件 → 从容器中取出组件的时候执行的生命周期,不取就不执行,取一次执行一次,取两次就执行两次

容器关闭阶段的方法

单例的组件才会执行到对应的方法

DisposableBean的destroy方法(通常是框架提供的类采用这种方式)

自定义的destroy方法(通常是自定义的)

BeanPostProcessor

非常特殊:是针对于所有组件的生命周期的强化方法

如果定义了BeanPostProcessor,容器中全部组件生命周期过程中都会执行

方法

  • postProcessBeforeInitialization:初始化阶段自定义初始化方法之前
  • postProcessAfterInitialization:初始化阶段自定义初始化方法之后

参数

  • 参数1:正在生命周期的组件实例(还没有放入到容器中)
  • 参数2:组件的名称(组件的id)

返回值:实例 交给下一个流程(容器中的组件其实是返回的这个实例) 也就是说容器中管理的实例取决于return返回的值 这两个方法按需提供

代码

示例生命周期的组件

java
@Component
public class LifeCycleBean implements BeanNameAware, BeanFactoryAware, ApplicationContextAware,
        InitializingBean,
        DisposableBean
{

    // 1. 构造器实例化(有参无参都行)
    public LifeCycleBean() {
        System.out.println("1. 构造器实例化(一定是最开始执行的)");
    }

    // 2.设置属性值(主要就是set方法、也可以直接增加在成员变量上)
    private String name;
    private UserDao userDao;
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
        System.out.println("2. 设置属性值(主要就是set方法、也可以直接增加在成员变量上)");
    }
    // 设置属性值如果不为实例,可以使用@Value注解设置
    @Autowired
    public void setName(@Value("张三") String name) {
        this.name = name;
    }


    // 3. Aware接口实现的方法,会在生命周期过程中自动执行
    //    这些方法会有参数,beanName、beanFactory、applicationContext
    //    主要就是给组件中的成员变量做赋值,容器中的这个组件的其他方法就可以使用这些成员变量了
    //    这几个接口我们一般不太用,一般是看源码的时候大家要能看懂
    private String beanName;
    private BeanFactory beanFactory;
    // 自己写的代码,直接取就完了;但是我们会看一些源码,源码里使用的就是实现接口的方法的方式
    //    第三方或一些源码里,并不会在类上直接增加注解(比如@Component),加注解要有扫描包配置
    //@Autowired
    private ApplicationContext applicationContext;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("3. BeanNameAware.setBeanName");
    }
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
        System.out.println("3. BeanFactoryAware.setBeanFactory");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        System.out.println("3. ApplicationContextAware.setApplicationContext");
        //System.out.println(applicationContext);
    }

    // 初始化方法通常只使用其中的一种
    // 5. 自定义的init方法
    @PostConstruct // 构造器之后(但是这是一个初始化方法)
    public void customInitMethod() {
        System.out.println("5. 自定义的init方法(自己开发的类,直接使用即可)");
    }

    // 5. 实现接口的init方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("5. InitializingBean.afterPropertiesSet(一般是第三方的类)");
    }


    // 销毁方法通常只使用其中的一种
    @PreDestroy // 容器销毁之前(但是这是一个销毁方法)
    public void customDestroyMethod() {
        System.out.println("7. 自定义的销毁方法(自己开发的类,直接使用即可)");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("7. DisposableBean.destroy(一般是第三方的类)");
    }
}

BeanPostProcessor

java
/**
 * 它就是提供方法的组件(方法的载体)
 *
 *    这两个方法:容器中的每一个组件 生命周期的初始化阶段都会去执行
 *      参数1:正在生命周期的组件实例(还没有放入到容器中)
 *      参数2:组件的名称(组件的id)
 *    返回值:实例
 *      交给下一个流程(容器中的组件其实是返回的这个实例)
 *      也就是说容器中管理的实例取决于return返回的值
 *   这两个方法按需提供
 *
 *
 *   全部组件初始化的时候的时候会执行
 *
 *   我是否可以针对特定的组件做处理呢???
 *
 */
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

  @Nullable
  @Override
  public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    System.out.println(beanName + " :4. CustomBeanPostProcessor postProcessBeforeInitialization");
    return bean;
  }

  // 两个方法之间:初始化方法(自定义的init方法或实现InitializingBean接口)

  @Nullable
  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    System.out.println(beanName + " :6. CustomBeanPostProcessor postProcessAfterInitialization");
    return bean;
  }
}

组件注册到容器中才会生效

可以针对特定的组件做处理,在其中做判断即可,比如

java
Class clazz = bean.getClass();
if(clazz.isAnnotationPresent(WdFlag.class)){
  System.err.println(beanName + " :hello world");
}

4.4 注意

注意:生命周期的方法,不是都会执行到的,有些执行是需要条件的。另外要注意BeanPostProcessor的作用范围

本章小结

本章核心概念:Bean 进入容器前后会经历实例化、依赖设置、Aware 回调、初始化、后处理器增强和销毁等过程,不同作用域会影响生命周期触发时机。

你现在应该掌握

  • 能说明 Bean 的常见实例化方式,包括构造方法、工厂方法和 FactoryBean
  • 能区分 singleton、prototype的使用边界
  • 能按顺序说出 Bean 初始化阶段的关键回调
  • 能解释 BeanPostProcessor 为什么是 AOP、事务等框架能力的重要扩展点
05 / Section

实战练习

<!-- 实战练习内容已分离到 practices/markdown/11-spring-ioc-practice.md -->

建议先完成本章重点内容复习,再进入配套练习。 练习建议按照:基础 → 进阶 → 综合挑战 的顺序完成。