IoC是什么?为什么要用?

在学习java后端的过程,spring框架已经是一个绕不开的话题,其中的新技术我们虽然会用却很少有人会思考问什么要用这个技术,IoC就是如此,因此在讨论IoC是什么之前,我想先讨论为什么Spring要引入这项技术。

为什么要用IoC?

假设我们现在没有Spring框架可用,也没有什么所谓的Ioc,仅仅用传统的Servlet开发项目

传统开发模式

三层架构是经典的开发模式,我们一般将视图控制、业务逻辑和数据库操作分别抽离出来单独形成一个类,这样各个职责就非常清晰且易于复用和维护。大致代码如下:

视图控制层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet("/user")
public class UserServlet extends HttpServlet {
// 用于执行业务逻辑的对象
private UserService userService = new UserServiceImpl();

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 其他代码

// 执行业务逻辑
userService.doService();

// ...返回页面视图
}
}

业务逻辑层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserServiceImpl implements UserService{
// 用于操作数据库的对象
private UserDao userDao = new UserDaoImpl();

@Override
public void doService() {
// 业务逻辑代码

// 执行数据库操作
userDao.doUpdate();

// 业务逻辑代码
}
}

数据库操作层

1
2
3
4
5
6
7
8
9
10
public class UserDaoImpl implements UserDao{
@Override
public void doUpdate() {
// JDBC代码
}
@Override
public void doQuery(){
//JDBC代码
}
}

上层依赖下层,抽象之后如下图:

业界普遍按这种分层方式组织代码,其核心思想是职责分离。层次越低复用程度越高,比如一个 DAO 对象往往会被多个 Service 对象使用,一个 Service 对象往往也会被多个 Controller 对象使用:

条理分明,井然有序。这些被复用的对象就像一个个的组件,供多方使用。

虽然这个倒三角看上去非常漂亮,然而我们目前的代码有一个比较大的问题,那就是我们只做到了逻辑复用,并没有做到资源复用

上层调用下一层时,必然会持有下一层的对象引用,即成员变量。目前我们每一个成员变量都会实例化一个对象,如下图所示:

每一个链路都创建了同样的对象,造成了极大的资源浪费。许多组件只需要实例化一个对象就够了,创建多个没有任何意义。针对对象重复创建的问题,我们自然而然想到了单例模式。只要编写类时都将其写为单例,这样就避免了资源浪费。但是,引入设计模式必然会带来复杂性,况且还是每一个类都为单例,每一个类都会有相似的代码,其弊端不言自明。

仅仅是资源的浪费倒还好,因为可以堆硬件,然而三层架构还有另一个一个致命缺陷,那就是变化的代价太大

假设有 10 个 Controller 依赖了 UserService,最开始实例化的是 UserServiceImpl,后面需要换一个实现类 OtherUserServiceImpl,我就得逐个修改那 10 个 Controller,非常麻烦。更换实现类的需求可能不会太多,没多大说服力。那咱们看另一个情况。

传统创建组件的过程非常简单,new一下就行了,可很多时候创建一个组件没那么容易。比如 DAO 对象要依赖一个这样的数据源组件:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserDaoImpl implements UserDao{
private MyDataSource dataSource;

public UserDaoImpl() {
// 构造数据源
dataSource = new MyDataSource("jdbc:mysql://localhost:3306/mybatis", "root", "123456");
// 进行一些其他配置
dataSource.setInitiaSize(10);
dataSource.setMaxActive(100);
// ...省略更多配置项
}
}

该数据源组件要想真正生效需要对其进行许多配置,这个创建和配置过程是非常麻烦的。而且配置可能会随着业务需求的变化经常更改,这时候你就需要修改每一个依赖该组件的地方,牵一发而动全身。这还只是演示了一个数据源的创建配置过程,真实开发中可有太多组件和太多配置需要编码了,其麻烦程度堪称恐怖。

当然,这些问题都可以引入设计模式来解决,不过这样一来又绕回去了:设计模式本身也会带来复杂性。这就像一种死循环:传统开发模式编码复杂,要想解决这种复杂却得陷入另一种复杂。

难道没有办法解决了吗?当然不是的,在引出解决方案前,我们先来梳理一下目前出现的问题:

  • 创建了许多重复对象,造成大量资源浪费
  • 更换实现类需要改动多个地方
  • 创建和配置组件工作繁杂,给组件调用方带来极大不便

透过现象看本质,这些问题的出现都是同一个原因:组件的调用方参与了组件的创建和配置工作

就相当于你去下馆子,想要吃饭还要自己去后厨做饭一样,显然这不符合常理

其实调用方只需关注组件如何调用,至于这个组件如何创建和配置又与调用方有什么关系呢?我去餐馆吃饭应该只需点菜,饭菜并不需要我亲自去做,餐馆自然会做好给我送过来。如果我们编码时,有一个「东西」能帮助我们创建和配置好那些组件,我们只负责调用该多好。这个「东西」就是容器。

容器这一概念我们已接触过,Tomcat 就是 Servlet 的容器,它帮我们创建并配置好 Servlet,我们只需编写业务逻辑即可。试想一下,如果 Servlet 要我们自己创建,HttpRequest、HttpResponse 对象也需要我们自己配置,那代码量得有多恐怖。

Tomcat 是 Servlet 容器,只负责管理 Servlet。我们平常使用的组件则需要另一种容器来管理,这种容器我们称之为 IoC 容器

IoC是什么?

通过上面的讲解,相比你已经对IoC的诞生背景有了一定的了解,接下来我们就来介绍IoC究竟是什么。

控制反转和依赖注入

IoC(Inversion of Control),又名控制反转,是指对象的创建和配置的控制权从调用方转移给容器,而不必再通过传统方式new出来,IoC已经帮我们做好了这一切。

有了 IoC 容器,我们可以将对象交由容器管理,交由容器管理后的对象称之为 Bean。调用方不再负责组件的创建,要使用组件时直接通过注解获取 Bean 即可:

1
2
3
4
5
@Component
public class UserServiceImpl implements UserService{
@Autowired // 获取 Bean
private UserDao userDao;
}

我只想说注解永远的神

调用方只需按照约定声明依赖项,所需要的 Bean 就自动配置完毕了,就好像在调用方外部注入了一个依赖项给其使用,所以这种方式称之为 依赖注入(Dependency Injection,缩写为 DI)。

控制反转和依赖注入是一体两面,都是同一种开发模式的表现形式

IoC 轻而易举地解决了我们刚刚总结的问题:

  • 对象交由容器管理后,默认是单例的,这就解决了资源浪费问题。

  • 若要更换实现类,只需更改 Bean 的声明配置,即可达到无感知更换:

1
2
3
4
5
6
7
8
9
10
@Component
public class UserServiceImpl implements UserService{
...
}

// 将该实现类声明为 Bean
@Component
public class OtherUserServiceImpl implements UserService{
...
}

现在组件的使用和组件的创建与配置完全分离开来。调用方只需调用组件而无需关心其他乱七八糟的东西,这极大提高了我们的开发效率,也让整个应用充满了灵活性、扩展性。

这样看来,我们如此中意 IoC 不是没有道理的。