1. 为什么需要IOC

IOC是面向对象编程中的一种设计原则,它的核心思想是将对象的创建和依赖关系的管理从应用程序代码中转移到外部容器。只需在代码中声明需要的对象或实例,而不需要自己创建它们。IOC容器会根据这些声明自动注入所需的实例。
使用IOC 有以下好处

1.1 解耦与简化代码

在传统的编程方式中,类与类之间的依赖关系往往是通过直接实例化来实现的。例如:

1
2
3
4
5
6
7
8
9
10
11
public class ServiceA {
private ServiceB serviceB;

public ServiceA() {
this.serviceB = new ServiceB();
}

public void performAction() {
serviceB.doSomething();
}
}

在这种方式中,ServiceA直接创建了ServiceB的实例,这导致了两者之间的强耦合。如果我们需要更换ServiceB的实现或修改其创建逻辑,就需要修改ServiceA的代码。这不仅增加了维护难度,也违反了开闭原则(Open/Closed Principle)。

通过IOC,我们可以将依赖关系的管理交给容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class ServiceA {
private final ServiceB serviceB;

@Autowired
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}

public void performAction() {
serviceB.doSomething();
}
}

在这种方式中,ServiceA不再负责创建ServiceB的实例,而是声明它需要一个ServiceB的实例。IOC容器会自动注入ServiceB的实例,从而实现了解耦。

1.2 提高代码的可测试性

传统方式中的强耦合使得单元测试变得困难,因为测试ServiceA时需要实际的ServiceB实例。而通过IOC,依赖关系由容器管理,使得我们可以轻松地替换依赖对象为模拟对象(Mock Object),从而简化了测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class ServiceATest {

@Mock
private ServiceB serviceB;

@InjectMocks
private ServiceA serviceA;

@Test
public void testPerformAction() {
// 配置mock行为
when(serviceB.doSomething()).thenReturn("Mock Response");

// 调用方法并验证
serviceA.performAction();
verify(serviceB).doSomething();
}
}

通过IOC容器管理依赖关系,我们能够更容易地替换实际依赖对象,从而提高了测试的便捷性和覆盖率。

1.3 配置与管理的灵活性

IOC容器通常提供灵活的配置方式,可以通过注解或配置文件来管理依赖关系。例如,在Spring框架中,可以使用Java配置类或XML配置文件来定义依赖关系:

1.4 支持高级功能

除了依赖注入,现代IOC容器通常还提供诸如AOP(Aspect-Oriented Programming,面向切面编程)、声明式事务等高级功能。这些功能进一步提高了代码的灵活性和可维护性。例如,AOP可以用于实现日志记录、事务管理等横切关注点,而无需在业务代码中添加相关逻辑。

2. Spring IOC 容器分类

Spring IOC容器的主要实现有两种:BeanFactoryApplicationContext,它们之间有一定的层次关系和联系。以下是对这些容器及其联系的详细说明:

2.1 Spring IOC 容器的主要实现

1.1 BeanFactory

BeanFactory是Spring框架中最基础的IOC容器。它提供了基本的依赖注入机制,负责管理Bean的实例化、配置和生命周期。

1.2 ApplicationContext

ApplicationContext是在BeanFactory基础上扩展的高级容器,提供了更多面向应用的功能,它还提供了国际化支持和框架事件体系,更易于创建实际应用。

一般称BeanFactory为IoC容器,而称ApplicationContext为应用上下文。但有时为了行文方便,也将ApplicationContext称为Spring容器。

2.2 ApplicationContext的具体实现及其功能

  • ClassPathXmlApplicationContext:从类路径下的XML配置文件加载上下文。
  • FileSystemXmlApplicationContext:从文件系统中的XML配置文件加载上下文。
  • AnnotationConfigApplicationContext:从Java配置类加载上下文。

2.2.1 ClassPathXmlApplicationContext

从类路径下的XML配置文件中加载Spring应用上下文。

1
2
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyBean myBean = context.getBean(MyBean.class);

2.2.2 FileSystemXmlApplicationContext

从文件系统中的XML配置文件中加载Spring应用上下文。

1
2
ApplicationContext context = new FileSystemXmlApplicationContext("C:/config/applicationContext.xml");
MyBean myBean = context.getBean(MyBean.class);

2.2.3 WebApplicationContext

WebApplicationContext 是 Spring 框架中专为 Web 应用设计的应用上下文,它扩展了 ApplicationContext,提供了与 Web 环境相关的特性和功能。在 Spring MVC 中,WebApplicationContext 扮演着关键角色,通过配置控制器、视图解析器等组件,管理 Web 请求的处理流程,访问 ServletContext、获取 Web 应用参数等

  • 与 Servlet 关联WebApplicationContext 可以访问 ServletContext,从而能够获取 Web 应用的环境参数。
  • 支持国际化:通过 Web 环境的 MessageSource,支持国际化消息资源的获取。
  • 与 Spring MVC 集成WebApplicationContext 是 Spring MVC 的核心组件,用于配置和管理控制器、视图解析器等 MVC 组件。

在传统Spring应用中,WebApplicationContext经常与XML配置一起使用,通过ContextLoaderListenerContextLoaderServlet来初始化和管理

2.2.4 AnnotationConfigApplicationContext

从Java配置类中加载Spring应用上下文,支持基于注解的配置。

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
@Bean
public MyBean myBean() {
return new MyBean();
}
}

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyBean myBean = context.getBean(MyBean.class);

2.3 BeanFactory与ApplicationContext的联系

对于两者的用途,可以进行简单划分:BeanFactory是Spring框架的基础设施,面向Spring本身;ApplicationContext面向使用Spring框架的开发者,几乎所有的应用场合都直接使用ApplicationContext而非底层的BeanFactory。

  1. 层次关系
    • ApplicationContext继承了BeanFactory的所有功能,同时扩展了许多高级功能。
    • BeanFactory提供了基础的IOC容器功能,而ApplicationContext在其基础上提供了更丰富的企业级功能。
  2. 使用方式
    • 在实际开发中,通常直接使用ApplicationContext,因为它提供了更强大的功能和更友好的开发体验。
    • BeanFactory更多地用于底层框架的实现和一些特殊的资源受限场景。
  3. 初始化方式
    • BeanFactory的bean是懒加载的,只有在第一次访问时才会被初始化。
    • ApplicationContext的bean默认在启动时就会被全部初始化。

3. Spring IOC 容器的启动过程

3.1 XML 配置下 Spring 容器的启动过程

在Spring Web应用中,Spring IOC容器(WebApplicationContext)的启动和初始化是由Web容器(如Tomcat、Jetty等)驱动的。

3.1.1 ContextLoaderListener

ContextLoaderListener是Spring框架中的一个监听器,用于在Web容器启动时创建并启动Spring的应用上下文WebApplicationContext。它主要负责:

  • 创建并启动Spring的应用上下文WebApplicationContext
  • 管理Spring的根上下文(Root ApplicationContext),使其在整个Web应用中可用。
  • 在应用关闭时(contextDestroyed方法)销毁上下文,正确关闭Spring上下文,释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
<web-app>
<!-- 配置 Spring 的 ContextLoaderListener -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 配置 Spring 的应用上下文文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
</web-app>

当Web容器(如Tomcat、Jetty等)启动时,ContextLoaderListener会监听到容器的启动事件,随后执行以下步骤:

  1. 读取contextConfigLocation参数ContextLoaderListener读取web.xml中的contextConfigLocation参数,获取Spring配置文件的位置。
  2. 加载Spring配置文件:根据contextConfigLocation参数指定的位置,加载Spring的配置文件。通常,这些文件是XML格式的配置文件,但也可以是其他类型的配置文件,如Java配置类。
  3. 初始化WebApplicationContext:创建并初始化WebApplicationContext,加载配置文件中的所有bean定义,并进行依赖注入和初始化。
  4. 注册上下文:将创建的WebApplicationContext注册到ServletContext中,使其在整个Web应用中可用。属性名为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE

    3.1.2 ContextLoaderServlet

ContextLoaderServlet是Spring框架中的一个Servlet,用于在Servlet初始化时启动Spring的WebApplicationContext。与ContextLoaderListener类似,它负责:

  • 加载Spring的应用上下文。
  • 管理Spring的根上下文(Root ApplicationContext),使其在整个Web应用中可用。
  • 在Servlet销毁时,正确关闭Spring上下文,释放资源。

以下是在web.xml中配置ContextLoaderServlet的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<web-app>
<!-- 配置 Spring 的 ContextLoaderServlet -->
<servlet>
<servlet-name>contextLoader</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<!-- 配置 Spring 的应用上下文文件位置 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
</web-app>

当Web容器(如Tomcat、Jetty等)启动时,ContextLoaderServlet会在Servlet初始化阶段执行以下步骤:

  1. 读取contextConfigLocation参数ContextLoaderServlet读取web.xml中的contextConfigLocation参数,获取Spring配置文件的位置。
  2. 加载Spring配置文件:根据contextConfigLocation参数指定的位置,加载Spring的配置文件。通常,这些文件是XML格式的配置文件,但也可以是其他类型的配置文件,如Java配置类。
  3. 初始化WebApplicationContext:创建并初始化WebApplicationContext,加载配置文件中的所有bean定义,并进行依赖注入和初始化。
  4. 注册上下文:将创建的WebApplicationContext注册到ServletContext中,使其在整个Web应用中可用。
  5. Servlet生命周期管理ContextLoaderServletinit方法中启动Spring上下文,在destroy方法中关闭上下文。

    3.1.3 ContextLoaderListener vs ContextLoaderServlet

ContextLoaderListener是启动Spring WebApplicationContext的最常用方式,适用于大多数Web应用。ContextLoaderServlet较少使用,通常用于需要精细控制上下文启动顺序的特殊场景。

3.2 Springboot项目 IOC 容器的启动过程

对于Web应用,Spring Boot使用AnnotationConfigServletWebServerApplicationContext作为ApplicationContext。这是AnnotationConfigApplicationContext的一个派生类,专门用于Web应用程序。

  1. 创建SpringApplication实例
  2. 根据合适的应用类型确定ApplicationContext
  3. 加载配置和初始化
  4. 启动嵌入式Web服务器(如果是Web应用)

Spring Boot 应用的启动过程通常从一个包含 main 方法的类开始,该类使用了 @SpringBootApplication 注解:

1
2
3
4
5
6
@SpringBootApplication
public class MySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringBootApplication.class, args);
}
}

@SpringBootApplication 是一个组合注解,包含了以下三个注解:

  • @SpringBootConfiguration:标识这是一个 Spring Boot 配置类。
  • @EnableAutoConfiguration:启用 Spring Boot 的自动配置机制。
  • @ComponentScan:启用组件扫描,自动发现并注册被 @Component@Service@Repository@Controller 等注解标识的 Spring Bean。

SpringApplication.run 方法执行以下步骤:

  1. 准备环境:创建并配置 Environment,加载应用配置文件(如 application.properties)。
  2. 创建应用上下文:创建合适的 ApplicationContext 实例(如 AnnotationConfigServletWebServerApplicationContext 用于 Web 应用)。
  3. 自动配置:根据类路径中的依赖和应用配置,自动配置 Spring 应用组件。
  4. 刷新上下文:初始化 Spring 应用上下文,扫描和注册所有 Spring Bean。
  5. 启动嵌入式服务器:如果是 Web 应用,启动嵌入式 Web 服务器(如 Tomcat)。
  6. 运行应用:启动应用并处理所有定义的 CommandLineRunnerApplicationRunner 实现。

3.3 Spring Boot vs 传统 Spring 启动方式

3.3.1 配置简化

  • 传统 Spring:通常需要大量的 XML 配置文件或 Java 配置类来定义 Bean 和依赖关系。
  • Spring Boot:通过自动配置和约定优于配置,极大地简化了配置,只需少量配置文件或注解。

    3.3.2 嵌入式服务器

  • 传统 Spring:通常需要在外部 Web 服务器(如 Tomcat)中部署应用。
  • Spring Boot:支持嵌入式服务器,应用可以打包为一个可执行的 JAR 文件,直接运行。

3.3.3 启动速度

  • 传统 Spring:启动过程较为繁琐,需要手动配置和管理许多组件。
  • Spring Boot:启动过程高度自动化,启动速度更快,开发体验更好。

Spring IOC 容器管理着 Bean 的生命周期,控制着 Bean 的依赖注入。 上面介绍完了Spring IOC 容器, 下面就来介绍一下它管理的 bean 一些相关基础知识。

4. Bean 定义方式

不管是XML还是注解,它们都是表达Bean定义的载体,其实质都是为Spring IOC容器提供Bean定义的信息

4.1 XML 配置

1
2
3
4
5
6
7
8
9
10
11
<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="myBean" class="com.example.MyBean">
<property name="property" value="value"/>
</bean>

</beans>

4.2 基于注解的配置

Spring从2.0开始引入基于注解的配置方式,在3.1时得到了进一步的完善。
常见的注解包括 @Component@Service@Repository@Controller,这些注解将类声明为 Spring 管理的 Bean。

1
2
3
4
@Component
public class MyBean {
// class definition
}

  • @Component:通用注解,适用于所有层次的类,用于将类标识为 Spring 容器管理的Bean。在实际开发中也可以选择更具体的注解来标识不同层次的组件
  • @Repository:数据访问层,用于对DAO实现类进行标注。
  • @Service:数据访问层,用于对Service实现类进行标注。
  • @Controller:控制器层,用于对Controller实现类进行标注。

4.3 基于 Java 类的配置 Java Config

Java Config 是通过使用 @Configuration 注解的类来定义 Spring 容器的配置。这样的类可以包含一个或多个 @Bean 注解的方法,这些方法的返回值会被 Spring 容器注册为 Bean。

@Configuration 类本质上是一个特殊的 @Component,其主要目的是为了定义 Bean。

所以任何标注了@Configuration的类,本身也相当于标注了@Component,即它们可以像普通的Bean一样被注入其他Bean中。

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

使用示例: 配置数据源和 JPA

对于数据访问层,可以通过 Java Config 来配置数据源(DataSource)、实体管理器工厂(EntityManagerFactory)和事务管理器(TransactionManager)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

@Configuration
public class DataConfig {

@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}

@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dataSource);
em.setPackagesToScan("com.example.myapp.model");
// Other JPA properties can be set here
return em;
}
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(emf);
return transactionManager;
}
}

5.Bean依赖关系声明方式

Bean 之间的依赖关系可以通过多种方式进行声明和管理,以实现依赖注入(Dependency Injection, DI)

无论是 XML 配置还是基于注解的配置,都可以归类为以下三种主要的依赖注入方式:基于构造函数、基于 Setter 方法和基于字段

5.1 基于构造函数的依赖注入

构造函数注入是通过在构造函数中声明依赖来进行注入的,这种方式有助于确保依赖关系在对象创建时就已经完全满足。

  • XML 配置:通过 <constructor-arg> 元素。
  • 注解配置:通过在构造函数上使用 @Autowired 注解。

5.1.1 XML 配置

在 XML 配置中,通过 <constructor-arg> 元素来指定构造函数参数的依赖注入

1
2
3
4
<bean id="myRepository" class="com.example.MyRepositoryImpl" />
<bean id="myService" class="com.example.MyService">
<constructor-arg ref="myRepository" />
</bean>

5.1.2 基于注解配置

通过 @Autowired 注解在构造函数上标注。

1
2
3
4
5
6
7
8
9
10
@Component
public class MyService {
private MyRepository myRepository;

@Autowired
public MyService(MyRepository myRepository) {
this.myRepository = myRepository;
}
// Business methods
}

5.2 基于 Setter 方法的DI

  • XML 配置:通过 <property> 元素。
  • 注解配置:通过在 Setter 方法上使用 @Autowired 注解。

    5.2.1 XML 配置

    通过 <property> 元素来配置 Setter 方法注入。
    1
    2
    3
    4
    5
    <bean id="myRepository" class="com.example.MyRepositoryImpl" />
    <bean id="myService" class="com.example.MyService">
    <property name="myRepository" ref="myRepository" />
    </bean>

5.2.2 基于注解配置

1
2
3
4
5
6
7
8
9
@Component
public class MyService {
private MyRepository myRepository;

@Autowired
public void setMyRepository(MyRepository myRepository) {
this.myRepository = myRepository;
}
}

5.3 基于字段的依赖注入

字段注入是直接在类的字段上使用 @Autowired 注解来标识依赖。这种方式最为简洁,但XML 配置通常不推荐这种方式

5.3.1 基于注解配置

直接在字段上使用 @Autowired 注解

1
2
3
4
5
6
7
@Component
public class MyService {
@Autowired
private MyRepository myRepository;

// Business methods
}

6. Bean 扫描

无论是使用 XML 配置还是基于注解的方式,定义好的 Bean 和Bean 之间的依赖关系都需要通过 Spring 的扫描机制才能被 Spring 容器识别并管理,从而起作用。
Bean 扫描主要通过两种方式实现:基于 XML 配置和基于注解的方式。

6.1 基于 XML 的 Bean 扫描

在 Spring 早期版本中,XML 配置是一种常见的配置方式,通过 XML 文件指定 Bean 扫描路径来实现自动发现和注册 Bean。

6.1.1 使用 <context:component-scan> 元素

XML 配置文件中的 <context:component-scan> 元素用于指定包路径,Spring 容器会扫描这些包路径下的类,并根据注解自动注册为 Bean。

1
2
3
4
5
6
7
8
9
10
11
12
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 扫描指定包路径下的所有组件类 -->
<context:component-scan base-package="com.example" />

</beans>

在这个示例中,<context:component-scan> 元素会扫描 com.example 包及其子包中的所有类,并自动注册带有 @Component@Service@Repository@Controller 等注解的类为 Spring Bean。

6.1.2 指定多个包路径

可以通过逗号分隔的方式指定多个包路径。

1
<context:component-scan base-package="com.example, com.anotherexample" />

6.2 基于注解的 Bean 扫描(Spring Boot)

Spring Boot 通过自动配置和注解的方式,简化了 Bean 扫描的配置过程。通常情况下,只需在主启动类上添加一些注解,即可完成 Bean 扫描的配置。

自定义扫描的包路径,并可以显式使用 @ComponentScan 注解。

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.example")
public class MyApplication {

public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

如果不显示使用@ComponentScan ,其实@SpringBootApplication注解 是一个组合注解,包含了 @ComponentScan,但是智慧默认会扫描主启动类所在包及其子包中的所有组件类。

同样可以指定多个包路径。

1
@ComponentScan(basePackages = {"com.example", "com.anotherexample"})