首页 > 编程笔记

Spring Ioc(控制反转)和DI(依赖注入)

在 Spring 框架中,Bean 的实例化和组装都是由 IoC 容器通过配置元数据完成的。本文主要介绍 Spring IoC 容器的理念,以及 spring-beans 模块和 spring-context 模块中的几个关键接口类。

IoC 和 DI 简介

IoC(控制反转)

IoC(Inversion of Control)是“控制反转”的意思。如何理解“控制反转”这个词呢?首先我们需要知道反转的是什么,是由谁来控制。

在 Spring 框架没有出现之前,在 Java 面向对象的开发中,开发者通过 new 关键字完成对 Object 的创建。Spring 框架诞生后,是通过 Spring 容器来管理对象的,因此 Object 的创建是通过 Spring 来完成的。

最终得出结论:控制反转指的是由开发者来控制创建对象变成了由 Spring 容器来控制创建对象,创建对象和销毁对象的过程都由 Spring 来控制。

以 Spring 框架为开发基础的应用尽量不要自己创建对象,应全部交由 Spring 容器管理。

DI(依赖注入)

DI(Dependency Injection)称为依赖注入。在 Java 程序中,类与类之间的耦合非常频繁,如 Class A 需要依赖 Class B 的对象 b。而基于 Spring 框架的开发,在 Class A 中不需要显式地使用 new 关键字新建一个对象 b,只需在对象 b 的声明之上加一行注解 @Autowired,这样在 Class A 用到 b 时,Spring 容器会主动完成对象 b 的创建和注入。

这就是 Class A 依赖 Spring 容器的注入。

总结/对比

通过上面的解释,我们可以发现 IoC 和 DI 其实是同一概念从不同角度的解释。

在 Spring 框架中,org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,它负责实例化、配置和组装 Beans。

容器通过读取元数据的配置来获取对象的实例化,以及配置和组装的描述信息。元数据可以用 XML、Java 注解或 Java 配置代码表示应用的对象及这些对象之间的内部依赖关系。

Spring 框架提供了几个开箱即用的 ApplicationContext 接口的实现类,主要包括:
在独立应用程序中,通常创建一个 ClassPathXmlApplication-Context 或 FileSystemXmlApplicationContext 实例对象来获取 XML 的配置信息。

开发者也可以指示容器使用 Java 注解或 Java 配置作为元数据格式,通过 Annotation-ConfigApplicationContext 来获取 Java 配置的 Bean。

元数据配置

1) 基于 XML 的配置

Spring 框架最早是通过 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--定义UserService类 -->
    <bean id="userService" class="com.spring.boot.UserService">
        <!--id属性 -->
        <property name="id" value="1"/>
        <!--name属性 -->
        <property name="name" value="www.weixueyuan.net"/>
    </bean>
</beans>

在 src/main/resources 目录下新建 spring.xml 文件,内容如上面的代码所示,<bean> 和 </bean>标签用来描述 Bean 的元数据信息。在上面的代码中声明了一个 UserService 类,该类有两个属性,即 id 和 name,通过 <property> 和 </property> 标签直接进行赋值。

UserService实体类的声明代码如下:
//声明 UserService 类
public class UserService {
        private Integer id;//用户ID
        private String name; //用户名称
    //getter和setter方法
        public Integer getId() {
                return id;
        }
        public void setId(Integer id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
    //打印属性值
        public void getUser() {
                System.out.println("id:"+this.id);
                System.out.println("name:"+this.name);
        }
}
以上代码声明了一个 UserService 类,并实现了属性 id 和属性 name 的 setter 和 getter 方法,通过 getUser() 方法打印属性值。

编写测试代码,展示通过 Spring 上下文获取 UserService 对象,具体代码如下:
//测试类
public class SpringXmlTest {
        public static void main(String[] args) {
                //通过spring.xml获取Spring应用上下文
                ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
                UserService userService = context.getBean("userService",UserService.class);
                userService.getUser(); //打印结果
        }
}
输出结果:

id:1
name:www.weixueyuan.net

在上面的示例代码中,ClassPathXmlApplicationContext 可以通过 spring.xml 文件获取 UserService 类的配置元数据,通过 Spring 容器的组装和实例化 UserService 类,最终正确调用 getUser() 方法打印出定义的属性值。

2) 基于 Java 注解的配置

从 Spring 2.5 开始,支持以 Java 注解的方式来配置 Bean,如 @Scope、@Service、@Component、@Controller、@Repository、@Autowired 和 @Resource 等注解。

@Scope 注解可以设置 Bean 的作用域。Spring 容器实例化的对象默认是单例的,如果想要修改作用域,可以通过 @Scope 注解进行修改。表 1 中列出了 @Scope 注解使用的一些作用域。

表1 @Scope注释的作用域
作用域 说  明
singleton Spring IoC容器只有一个单实例
prototype 每次获取一个新实例
request 每一次有HTTP请求时都会产生一个新的实例,同时该实例仅在当前HTTP request 内有效
session 每一次有HTTP请求时都会产生一个新的实例,同时该实例仅在当前HTTP session 内有效
application 全局Web应用级别的作用域,也是在Web环境中使用的,一个Web应用程序对 应一个Bean实例
websocket WebSocket的单实例作用域

request、session、application 和 websocket 作用域只在 Web 应用环境中使用。在普通的 Spring IoC 容器里只有 singleton 和 prototype 两种作用域,其他的设置会抛出异常。

下面改造基于 XML 配置元数据的例子,将其改成基于 Java 注解的方式来注入 Bean,具体代码如下:
//注解的方式声明UserService
@Service
public class UserService {
        private Integer id;//用户ID
        private String name;//用户名称
    //getter和setter方法
        public Integer getId() {
                return id;
        }
        public void setId(Integer id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
    //属性值打印
        public void getUser() {
                System.out.println("id:"+this.id);
                System.out.println("name:"+this.name);
        }
}
上面的代码在 UserService 类中加了一个 @Service 注解,spring.xml 配置文件不再使用。

下面增加一个注解类,添加 @ComponentScan 注解,代码如下:
//@ComponentScan 注解用来扫描 UserService 类
@ComponentScan("com.spring.boot")
public class SpringAnnotationTest {
}
@ComponentScan 注解的值是 com.spring.boot,说明 Spring 容器可以自动扫描这个包路径下可管理的类,并对该类进行实例化。添加测试类代码如下:
@ComponentScan("com.spring.boot")
public class SpringAnnotationTest {
        public static void main(String[] args) {
                //通过注解类获取应用上下文
                ApplicationContext context = new AnnotationConfigApplication
Context(SpringAnnotationTest.class);
                //获取UserService对象
                UserService userService = context.getBean(UserService.class);
                userService.setId(1);
                userService.setName("www.weixueyuan.net");
                userService.getUser(); //调用方法,打印属性值
        }
}
输出结果:

id:1
name:www.weixueyuan.net

通过 AnnotationConfigApplicationContext 类可以获取被 @Service 注解的 User-Service 实例化对象,并正确打印属性值。通过 Java 注解的方式同样完成了实例的初始化,说明 XML 配置方式可以完全被替换。

3) 基于 Java 配置的示例

从 Spring 3.0 开始,Spring 框架开始支持基于 Java 的方式来配置元数据,如 @Configuration、@Bean、@Import和@Profile 等注解。

@Configuration 注解一般用来配置类,配置类中可以使用 @Bean 注解来声明某个类的初始化操作;@Import 注解可以导入由 @Configuration 注解的配置类;@Profile 注解可以根据不同环境生成不同的实例。

下面改造基于 Java 注解的案例,给出一个基于 Java 配置的示例。UserService 类去掉 @Service 注解后,将变成普通的 Bean。UserService 类的声明代码如下:
//声明 UserService 类
public class UserService {
        private Integer id; //用户ID
        private String name; //用户名称
        public Integer getId() {
                return id;
        }
        public void setId(Integer id) {
                this.id = id;
        }
        public String getName() {
                return name;
        }
        public void setName(String name) {
                this.name = name;
        }
        //属性值打印
        public void getUser() {
                System.out.println("id:"+this.id);
                System.out.println("name:"+this.name);
        }
}
新增配置类,代码如下:
//基于 @Configuration 注解生成 UserService 对象
@Configuration
public class SpringConfigTest {
        @Bean
        public UserService userService() {
                return new UserService();
        }
}
SpringConfigTest 类由 @Configuration 注解,表明这个类是个配置类。由 @Bean 注解的 userService() 方法返回了 UserService 类的实例。

添加测试类代码如下:
@Configuration
public class SpringConfigTest {
    @Bean
        public UserService userService() {
                return new UserService();
        }
        public static void main(String[] args) {
                //通过配置类获取Spring应用上下文
                ApplicationContext context = new AnnotationConfigApplication
Context(SpringConfigTest.class);
                UserService userService = context.getBean(UserService.class);
                userService.setId(1);
                userService.setName("www.weixueyuan.net");
                userService.getUser(); //打印属性值
        }
}
输出结果:

id:1
name:www.weixueyuan.net

从上面的例子看,基于 Java 配置实例化对象的方式不再需要对 spring.xml 的依赖。基于 Java 注解或 Java 配置来管理 Bean 的方式已经是当今编程的流行方式。

Bean 管理

如图 2 所示为 Bean 被 Spring 容器组装的简单过程。首先通过 XML 配置、注解配置或 Java 配置等 3 种方式配置元数据,然后装配 BeanDefinition 属性,如果有增强设置,如实现了 BeanFactoryPostProcessor 或 BeanPostProcessor 接口,则进行拦截增强处理,最后通过配置的初始化方法完成 Bean 的实例化。

图2 Bean的组装过程
图2 Bean的组装过程

spring-beans 模块是 Spring 容器组装 Bean 的核心模块,它提供了组装 Bean 的几个关键接口,如图 2 中的 BeanDefinition、BeanFactoryPostProcessor、BeanPost-Processor 和 BeanFactory 等。

本文将通过两个简单的例子,展现 BeanFactoryPostProcessor 和 BeanPostProcessor 接口的扩展能力。

首先来看一个 BeanFactoryPostProcessor 接口扩展的例子。BeanFactory-PostProcessor 接口方法的输入参数是 ConfigurableListableBeanFactory,使用该参数可以获取相关 Bean 的定义信息。示例代码如下:
@Component
public class BeanFactoryPostProcessorImpl implements BeanFactory
PostProcessor {
        @Override
        public void postProcessBeanFactory(ConfigurableListableBean
Factory beanFactory) throws BeansException {
         //获取UserService的BeanDefinition
                BeanDefinition beanDefinition = beanFactory.getBeanDefinition
("userService");
                //修改Scope属性
beanDefinition.setScope("prototype");
                System.out.println(beanDefinition. getScope());
        }
}
输出结果:

prototype

通过输出的结果可以看到,在 UserService 实例化之前修改了该类的作用域,将其从 singleton 改为了 prototype。

对于 BeanPostProcessor 接口的扩展,可以在 Spring 容器实例化 Bean 之后或者执行 Bean 的初始化方法之前添加一些自己的处理逻辑。示例代码如下:
@Component
public class BeanPostProcessorImpl implements BeanPostProcessor {
        //在实例化之前操作
        @Override
        public Object postProcessBeforeInitialization(Object bean, String
beanName) throws BeansException {
        //判断Bean的类型
        if(bean instanceof UserService){
            System.out.println("postProcessBeforeInitialization bean :
" + beanName);
        }
        return bean;
    }
    //在实例化之后操作
        @Override
        public Object postProcessAfterInitialization(Object bean, String
beanName) throws BeansException {
        //判断Bean的类型
        if(bean instanceof UserService){
            System.out.println("postProcessAfterInitialization bean : "
+ beanName);
        }
        return bean;
    }
}
输出结果:

postProcessBeforeInitialization bean : userService
postProcessAfterInitialization bean : userService

从输出结果中可以看到,在 UserService 实例化之前和之后都打印了日志,因此通过 BeanPostProcessor 可以做一些增强逻辑。

优秀文章