理解Spring中依赖注入(DI)与控制反转(IoC)
相关概念
依赖注入(Dependency Injection,简称DI)与控制反转(IoC)的含义相同,只不过这两个称呼是从两个角度描述的同一个概念。对于一个Spring初学者来说,这两种称呼很难理解,下面我们将通过简单的语言来描述这两个概念。
当某个Java对象(调用者)需要调用另一Java对象(被调用者,即被依赖对象)时,在传统模式下,调用者通常会采用“new被调用者”的代码方式来创建对象。这种方式会导致调用者与被调用者之间的耦合性增加,不利于后期项目的升级和维护。
在使用Spring框架之后,对象的实例不再由调用者来创建,而是由Spring容器来创建,Spring容器会负责控制程序之间的关系,而不是由调用者的程序代码直接控制。这样,控制权由应用代码转移到了Spring容器,控制权发生了反转,这就是Spring的控制反转。
从Spring容器的角度来看,Spring容器负责将被依赖对象赋值给调用者的成员变量,这相当于为调用者注入了它依赖的实例,这就是Spring的依赖注入。
相对于“控制反转”,“依赖注入”的说法也许更容易理解一些,即由容器(如Spring)负责把组件所“依赖”的具体对象“注入”(赋值)给组件,从而避免组件之间以硬编码的方式结合在一起。
依赖注入的实现方式
依赖注入的作用就是在使用Spring框架创建对象时,动态地将其所依赖的对象注入Bean组件中,其实现方式通常有两种,一种是属性setter方法注入,另一种是构造方法注入,具体介绍如下。
属性setter方法注入 指IoC容器使用setter方法注入被依赖的实例。通过调用无参构造器或无参静态工厂方法实例化Bean后,调用该Bean的setter方法,即可实现基于setter方法的依赖注入。
构造方法注入 指IoC容器使用构造方法注入被依赖的实例。基于构造方法的依赖注入通过调用带参数的构造方法来实现,每个参数代表着一个依赖。
了解了两种注入方式后,上一示例就是以属性setter方法注入的方式为例,下面修改上述案例,使用构造方法在Spring容器在应用中是如何实现依赖注入的。
(1)在MyEclipse中,创建一个Java项目,将Spring的4个基础包以及commons-logging的JAR包复制到lib目录中,并发布到类路径下,与上一项目基础配置相同。
(2)在src目录下,创建一个cn.springdemo包,并在包中创建HelloSpring.java,为期添加无参构造方法、有参构造方法,然后在类中定义一个print()方法,示例代码如下:
【示例】 HelloSpring.java
1 public class HelloSpring {
2 // 定义who属性,该属性的值将通过Spring框架进行设置
3 private String who = null;
4
5 /**
6 * 定义打印方法,输出一句完整的问候。
7 */
8 public void print() {
9 System.out.println("Hello," + who + "!");
10 }
11
12 public HelloSpring() {
13 super();
14 }
15
16 public HelloSpring(String who) {
17 super();
18 this.who = who;
19 }
20 }
(3)在resources目录下,编写Spring配置文件,在Spring配置文件中修改id为helloSpring的Bean为HelloSpring类的实例,并通过构造方法为who属性注入属性值。Spring配置文件内容示例代码如下:
【示例】 applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://www.springframework.org/schema/beans
5 http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
6 <bean id="helloSpring" class="cn.springdemo.HelloSpring">
7 <!-- 通过定义的单参构造为helloSpring的who属性赋值 -->
8 <constructor-arg index="0" value="Spring" />
9 </bean>
10 </beans>
(4)在cn.test包下,创建测试类HelloTest,并在类中编写test()方法,示例代码如下:
【示例】 HelloTest.java
1 @Test
2 public void test() {
3 // 通过ClassPathXmlApplicationContext实例化Spring的上下文
4 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
5 // 通过ApplicationContext的getBean()方法,根据id来获取bean的实例
6 HelloSpring helloSpring = (HelloSpring) context.getBean("helloSpring");
7 // 执行print()方法
8 helloSpring.print();
9 }
执行程序后,控制台的输出结果与之前属性setter方法注入效果一致,注意两个HelloSpring.java的差异,使用属性setter方法注入必须要为属性提供getter方法实现属性值得注入,使用构造方法注入必须要为类提供对应参数属性值的构造实现才能注入值。
理解“控制反转”
控制反转(Inversion of Control,IoC),也称为依赖注入(Dependency Injection,DI),是面向对象编程中的一种设计理念,用来降低程序代码之间的耦合度,在MVC的设计模式中经常使用。首先考虑什么是依赖。依赖,在代码中一般指通过局部变量、方法参数、返回值等建立的对于其他对象的调用关系。例如,在A类的方法中,实例化了B类的对象并调用其方法以完成特定的功能,我们就说A类依赖于B类。
几乎所有的应用都是由两个或更多的类通过彼此合作来实现完整的功能。类与类之间的依赖关系增加了程序开发的复杂程度,我们在开发一个类的时候,还要考虑对正在使用该类的其他类的影响。
例如,常见的业务层调用数据访问层实现持久化操作,解决问题的步骤如下:
(1)获取Spring开发包并为工程添加Spring支持。
(2)为业务层和数据访问层设计接口,声明所需方法。
(3)编写数据访问层接口UserDao的实现类,完成具体的持久化操作。
(4)在业务实现类中声明UserDao接口类型的属性,并添加适当的构造方法为属性赋值。
(5)在Spring的配置文件中将DAO对象以构造注入的方式赋值给业务实例中的UserDao类型的属性。
(6)在代码中获取Spring配置文件中装配好的业务类对象,实现程序功能。
实现步骤如下:
(1)在MyEclipse中,创建一个Java项目,在该项目的lib目录中加入Spring支持和依赖的JAR包。
(2)为业务层调用数据访问层实现持久化操作,如示例所示。
【示例】 UserDao.java
1 /**
2 * 增加DAO接口,定义了所需的持久化方法
3 */
4 public interface UserDao {
5 public void save(User user);
6 }
【示例】 UserDaoImpl.java
1 /**
2 * 用户DAO类,实现IDao接口,负责User类的持久化操作
3 */
4 public class UserDaoImpl implements UserDao {
5 public void save(User user) {
6 // 这里并未实现完整的数据库操作,仅为说明问题
7 System.out.println("保存用户信息到数据库");
8 }
9 }
【示例】 UserService.java
1 /**
2 * 用户业务接口,定义了所需的业务方法
3 */
4 public interface UserService {
5 public void addNewUser(User user);
6 }
【示例】 UserServiceImpl.java
1 /**
2 * 用户业务类,实现对User功能的业务管理
3 */
4 public class UserServiceImpl implements UserService {
5 // 声明接口类型的引用,和具体实现类解耦合
6 private UserDao userDao;
7
8 // userDao 属性的setter访问器,会被Spring调用,实现设值注入
9 public UserDao getUserDao() {
10 return userDao;
11 }
12 public void setUserDao(UserDao userDao) {
13 this.userDao = userDao;
14 }
15 public void addNewUser(User user) {
16 // 调用用户DAO的方法保存用户信息
17 userDao.save(user);
18 }
19 }
如以上代码所示,UserServiceImpl对UserDaoImpl存在依赖关系。这样的代码很常见,但是存在一个严重的问题,即UserServiceImpl和UserDaoImpl高度耦合,如果因为需求变化需要替换UserDao的实现类,将导致UserServiceImpl中的代码随之发生修改。如此,程序将不具备优良的可扩展性和可维护性,甚至在开发中难以测试。
(3)这里我们改为使用Spring的IoC的方式实现,在配置文件applicationContext.xml 中,创建一个id为UserService的Bean,该Bean用于实例化UserServiceImpl类的信息,并将userDao的实例注入到UserService中,其代码如下所示。
【示例】 applicationContext.xml
1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://www.springframework.org/schema/beans
5 http://www.springframework.org/schema/beans/spring-beans-3.2.xsd">
6 <!--添加一个id为userService的实例 -->
7 <bean id="userDao" class="cn.dsscm.dao.UserDaoImpl" />
8 <!--添加一个id为userService的实例 -->
9 <bean id="userService" class="cn.dsscm.service.UserServiceImpl">
10 <!-- 将id为userDao的Bean实例注入到userService实例中 -->
11 <property name="userDao" ref="userDao" />
12 </bean>
13 </beans>
在上述代码中,<property>是<bean>元素的子元素,它用于调用Bean实例中的setUserDao()方法完成属性赋值,从而实现依赖注入。其name属性表示Bean实例中的相应属性名,ref属性用于指定其属性值。
(4)在cn.dsscm.test包中,创建测试类IoCTest,来对程序进行测试,编辑后其代码如下所示。
【示例】 IoCTest.java
1 import org.junit.Test;
2 import org.springframework.context.ApplicationContext;
3 import org.springframework.context.support.ClassPathXmlApplicationContext;
4
5 import cn.dsscm.pojo.User;
6 import cn.dsscm.service.UserService;
7
8 public class IoCTest {
9 @Test
10 public void test() {
11 // 通过ClassPathXmlApplicationContext实例化Spring的上下文
12 ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
13 // 通过ApplicationContext的getBean()方法,根据id来获取bean的实例
14 UserService userService = (UserService) context.getBean("userService");
15 // 执行print()方法
16 userService.addNewUser(new User());
17 }
18 }
执行程序后,控制台的输出结果.
可以看出,使用Spring容器通过UserService实现类中的addNewUser()方法,调用了UserDao实现类中的addNewUser()方法,并输出了结果。这就是Spring容器属性setter注入的方式,也是实际开发中最为常用的一种方式。
分析其使用“控制反转”方式,其利用简单工厂和工厂方法模式的思路分析此类问题,其代码如下所示。
【示例】 简单工厂和工厂方法模式
1 /**
2 *增加用户DAO工厂,负责用户DAO实例的创建工作
3 */
4 public class UserDaoFactory {
5 //负责创建用户DAO实例的方法
6 public static UserDao getInstance() {
7 //具体实现过程略
8 }
9 }
10
11 /**
12 * 用户业务类,实现对User功能的业务管理
13 */
14 public class UserServiceImpl implements UserService {
15 private UserDao dao = UserDaoFactory.getInstance();
16 public void addNewUser(User user) {
17 // 调用用户DAO的方法保存用户信息
18 dao.save(user);
19 }
20 }
这里的用户DAO工厂类UserDaoFactory体现了"控制反转"的思想:UserServiceImpl不再依靠自身的代码去获得所依赖的具体DAO对象,而是把这一工作转交给了"第三方"——UserDaoFactory,从而避免了和具体UserDao实现类之间的耦合。由此可见,在如何获取所依赖的对象这件事上,“控制权"发生了"反转”——从UserServiceImpl转移到了UserDaoFactory,这就是所谓的"控制反转"。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了