Spring提供的容器又称为IoC容器,什么是IoC?
1. IoC的定义
IoC(Inversion of Control)即控制反转。简单来说,IoC指的是将对象创建与对象间的依赖关系的控制权从程序本身转交给容器管理。为了理解IoC的概念,我们首先来看一个常见的Java应用组件是如何协作的。
2. 传统的组件协作方式
我们假设有一个在线书店应用,BookService 需要访问数据库来获取书籍信息。为了实现这一点,BookService 类会直接创建数据库连接的实例:
java
public class BookService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public Book getBook(long bookId) {
try (Connection conn = dataSource.getConnection()) {
...
return book;
}
}
}
UserService 类也是如此,它也需要连接数据库:
java
public class UserService {
private HikariConfig config = new HikariConfig();
private DataSource dataSource = new HikariDataSource(config);
public User getUser(long userId) {
try (Connection conn = dataSource.getConnection()) {
...
return user;
}
}
}
接下来,在 CartServlet 和 HistoryServlet 中,我们还需要实例化 BookService 和 UserService:
java
public class CartServlet extends HttpServlet {
private BookService bookService = new BookService();
private UserService userService = new UserService();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
long currentUserId = getFromCookie(req);
User currentUser = userService.getUser(currentUserId);
Book book = bookService.getBook(req.getParameter("bookId"));
cartService.addToCart(currentUser, book);
...
}
}
上述代码的问题是,所有的组件都通过 new
操作符在内部创建,导致以下几个问题:
-
高耦合:每个组件都需要了解如何实例化其他组件(例如,
BookService
和UserService
都需要实例化DataSource
)。 -
难以共享组件:如果多个组件需要使用同一个
DataSource
,那么它们必须相互协作来管理它的生命周期,这增加了复杂度。 -
资源管理困难:当有多个组件共享一个资源时(如
DataSource
),如何确保它们都能正确地释放资源? -
测试困难:测试某个组件时,必须依赖于真实的外部环境(如数据库),这使得测试变得复杂。
3. IoC的核心问题:谁来负责创建和管理组件?
IoC的主要目的是解决这些问题:谁负责创建组件,谁负责管理组件之间的依赖关系,以及如何销毁它们。
4. 传统的方式 vs IoC方式
在传统的应用程序中,控制权在应用程序本身。比如,在 CartServlet
中,它负责创建 BookService
和 UserService
,而这些组件又依赖于 DataSource
。这种方式的缺点是,每个组件都需要自己知道如何创建其他依赖的组件。
而在IoC模式下,控制权发生了反转——从应用程序本身转移到容器中。容器负责创建和管理所有组件,并且将依赖关系注入到这些组件中。
5. 依赖注入:IoC的核心思想
在IoC模式中,组件的依赖不再由组件自己创建,而是由外部容器通过“注入”的方式提供。具体来说,**依赖注入(DI,Dependency Injection)**就是指容器负责将一个组件所依赖的其他组件注入到该组件中,而不是由组件自己去创建这些依赖。
6. 示例:BookService中的依赖注入
为了演示这一点,假设我们将 BookService
中的 DataSource
的创建过程从 BookService
中移除,而是通过 setDataSource()
方法来注入 DataSource
:
java
public class BookService {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}
通过这种方式,BookService
不再关心如何创建 DataSource
,它只需要使用容器已经创建好的 DataSource
实例。
这种改变带来了如下好处:
-
降低了组件的耦合度:
BookService
不需要了解DataSource
的创建过程。 -
便于共享组件:多个组件可以通过依赖注入共享同一个
DataSource
实例。 -
便于测试:可以通过注入虚拟的
DataSource
(如内存数据库)来进行测试,而不必依赖真实的数据库。
7. IoC容器的配置
为了让容器知道如何创建和配置这些组件,我们需要提供一种方式告诉容器哪些组件需要实例化以及它们之间的依赖关系。Spring的IoC容器通过XML配置文件来实现这一点。例如,以下是一个XML配置示例:
xml
<beans>
<bean id="dataSource" class="HikariDataSource" />
<bean id="bookService" class="BookService">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userService" class="UserService">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
在这个XML文件中,我们配置了三个组件:DataSource
、BookService
和 UserService
,并将 dataSource
注入到 BookService
和 UserService
中。
8. 依赖注入的方式
Spring的IoC容器支持两种主要的依赖注入方式:
-
属性注入(Setter Injection)
使用set
方法注入依赖,如上例所示,setDataSource()
方法将DataSource
注入到BookService
中。 -
构造方法注入(Constructor Injection)
依赖通过构造方法传递。以下是通过构造方法注入的示例:
java
public class BookService {
private DataSource dataSource;
public BookService(DataSource dataSource) {
this.dataSource = dataSource;
}
}
Spring同时支持属性注入和构造方法注入,并允许两者混合使用。
9. 无侵入的设计
Spring IoC容器是一个无侵入容器,这意味着应用程序的组件不需要实现Spring的特定接口,或者组件本身不知道它们是如何在Spring容器中运行的。无侵入设计带来了以下好处:
-
灵活性:组件可以既在Spring IoC容器中运行,也可以在不使用容器的情况下运行。
-
测试的便捷性:因为组件不依赖于Spring容器,所以它们可以单独进行测试,避免了对Spring容器的强依赖。
10. 小结
Spring的IoC容器通过依赖注入解决了传统Java程序中组件创建和依赖管理的痛点。它将对象创建与对象使用解耦,让开发者可以更加专注于业务逻辑的实现。同时,Spring的无侵入设计提高了系统的灵活性,使得应用程序能够在不依赖于Spring容器的情况下正常运行,极大地提升了开发效率和可维护性。
在下一章节,我们将深入探讨Spring容器如何管理对象生命周期和作用域,以及如何配置和使用Spring的不同类型容器。