2022-06-06:IoC容器
1、容器
容器是一种为某种特定组件的运行提供必要支持的一个软件环境。
例如,Tomcat就是一个Servlet容器,它可以为Servlet的运行提供运行环境。类似Docker这样的软件也是一个容器,它提供了必要的Linux环境以便运行一个特定的Linux进程。
通常来说,使用容器运行组件,除了提供一个组件运行环境之外,容器还提供了许多底层服务。例如,Servlet容器底层实现了TCP连接、解析HTTP协议等非常复杂的服务,如果没有容器来提供这些服务,我们就无法编写像Servlet这样代码简单,功能强大的组件。
早期的JavaEE服务器提供的EJB容器最重要的功能就是通过声明式事务服务,使得EJB组件的开发人员不必自己编写冗长的事务处理代码,极大地简化了事务处理。
Spring的核心就是提供了一个IoC容器,它可以管理所有轻量级的JavaBean组件,提供的底层服务包括组件的生命周期管理、配置和组装服务、AOP支持,以及建立在AOP基础上的声明式事务服务等。
本文我们讨论的IoC容器,主要介绍Spring容器主要对组件进行生命周期管理和配置组装服务。
2、IoC原理
IoC全称Inversion of Control,即控制反转。
在理解IoC之前,我们先看通常的Java组件如何协作的。
假定一个在线书店,通过BookService获取书籍(像这种以XxxService命名的类都是业务逻辑类):
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; } } }
为了从数据库查阅书籍,BookService持有一个DataSource。为了实例化一个HikariDataSource,又不得不实例化一个HikariConfig。
现在我们继续编写UserService获取用户:
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; } } }
因为UserService也需要访问数据库,因此,我们不得不也实例化一个HikariDataSource。
在处理用户购买的CartServlet,我们需要实例化UserService和BookService:
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); ... } }
类似的,在购买历史HistoryServlet,也需要实例化UserService
和BookService:
public class HistoryServlet extends HttpServlet { private BookService bookService = new BookService(); private UserService userService = new UserService(); }
上述每个组件都采用了一种简单的通过new创建实例并持有的方式。
仔细观察之后会发现有如下缺点:
- 实例化一个组件其实很难:BookService和UserService要创建HikarDataSource,实际上需要读取配置,才能先实例化HikariConfig,再实例化HikariDataSource;
- 没有必要让BookService和UserService分别创建DataSource实例,完全可以共享同一个DataSource。但谁负责创建DataSource,谁负责获取其他组件已经创建的DataSource,不好处理。
- 很多组件需要销毁以便释放资源,例如DataSource,但是如果该组件被多个组件共享,如何确保它的使用方都已经全部被销毁?
- 随着更多组件被引入,需要共享的组件写起来会更困难,这些组件的依赖关系会越来越复杂。
- 测试某个组件,例如BookService,是复杂的,因为必须要在真实的数据库环境下执行。
从该例子可以看出,如果一个系统有大量组件,其生命周期和相互之间的依赖关系由组件自身来维护,不但大大增加了系统复杂度,而且会导致组件之间极为紧密的耦合,继而给测试和维护带来极大困难。
核心问题就是:
-
谁负责创建组件?
-
谁负责根据依赖关系组装组件?
-
销毁时,如何按照依赖顺序正确销毁?
解决该问题的核心方案就是IoC。
传统的应用程序中,控制权在程序本身,程序的控制流程完全由开发者控制。
例如:CartServlet创建了BookService,在创建BookService的过程中,又创建了DataSource组件。该模式的确定的是,一个组件如果要使用另一个组件,必须先知道如何正确地创建它。
在IoC模式下,控制权发生了反转,即从应用程序转移到了IoC容器,所有组件不再由应用程序自己创建和配置,而是由IoC容器负责,这样应用程序只需要直接使用已经创建好且配置好的组件。为了让组件可以在IoC容器中被装配出来,需要某种注入机制。
例如,
BookService自己不会创建DataSource,而是等待外部通过setDataSource()方法来注入一个DataSource:
public class BookService { private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } }
不用直接new一个DataSource,而是注入一个DataSource,这个改动虽然简单,却带来了一系列好处:
- BookService不再关心如何创建DataSource,因此,不必编写读取数据库配置之类的代码;
- DataSource实例被注入到BookService,同时也可以注入到UserService,因此共享一个组件非常简单;
- 测试BookService更容易,因为注入的是DataSource,可以使用内存数据库,而不是真实的MySQL配置。
因此,IoC又称为依赖注入(DI:Dependency Injection),它解决了一个最主要的问题:将组件的创建+配置与组件的使用相分离,并且由IoC容器负责管理组件的生命周期。
因为IoC容器要负责实例化所有的组件,因此,有必要告诉容器如何创建组件,以及各种的依赖关系。最简单的配置是通过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配置文件指示IoC容器创建3个JavaBean组件,并把id为dataSource的组件通过属性dataSource(即调用setDataSource()方法)注入到另外两个组件中。
在Spring的IoC容器中,我们把所有组件统称为JavaBean,即配置一个组件就是配置一个Bean。
2.1、依赖注入方式
从之前的代码可以看到,依赖注入可以通过set()等方法实现。但依赖注入也可以通过构造方法实现。
很多Java类都有带参构造方法,如果我们把BookService改造为通过构造方法注入,那么实现方法如下:
public class BookService { private DataSource dataSource; public BookService(DataSource dataSource) { this.dataSource = dataSource; } }
Spring的IoC容器同时支持属性注入(setXxx)和构造方法注入,并允许混合使用。
2.2、无侵入容器
在设计上,Spring的IoC容器是一个高度可扩展的无侵入容器。何为无侵入呢?这是指应用程序的组件无需实现Spring的特定接口,或者说,组件不知道自己在Spring的容器中运行。这种无侵入的设计有以下好处:
- 应用程序组件既可以在Spring的IoC容器中运行,也可以自己编写代码自行组装配置;
- 测试的时候并不依赖Spring容器,可单独进行测试,大大提高了开发效率。
3、装配Bean
我们之前讨论了为什么要使用Spring的IoC容器,因为让容器为我们创建并装配Bean能获得很大的好处,那么究竟如何使用IoC容器?装配好的Bean又如何使用呢?
我们来看一个具体的用户注册登录的例子。该工程的结构如下:
首先,我们用Maven创建工程,并引入spring-context依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itranswarp.learnjava</groupId> <artifactId>spring-ioc-appcontext</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <java.version>11</java.version> <spring.version>5.2.3.RELEASE</spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> </dependencies> </project>
我们先编写一个MailService,用于在用户登录和注册成功之后发送邮件通知:
public class MailService { private ZoneId zoneId = ZoneId.systemDefault(); public void setZoneId(ZoneId zoneId) { this.zoneId = zoneId; } public String getTime() { return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME); } public void sendLoginMail(User user) { System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime())); } public void sendRegistrationMail(User user) { System.err.println(String.format("Welcome, %s!", user.getName())); } }
再编写一个UserService,实现用户注册和登录:
public class UserService { private MailService mailService; public void setMailService(MailService mailService) { this.mailService = mailService; } private List<User> users = new ArrayList<>(List.of( // users: new User(1, "bob@example.com", "password", "Bob"), // bob new User(2, "alice@example.com", "password", "Alice"), // alice new User(3, "tom@example.com", "password", "Tom"))); // tom public User login(String email, String password) { for (User user : users) { if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) { mailService.sendLoginMail(user); return user; } } throw new RuntimeException("login failed."); } public User getUser(long id) { return this.users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow(); } public User register(String email, String password, String name) { users.forEach((user) -> { if (user.getEmail().equalsIgnoreCase(email)) { throw new RuntimeException("email exist."); } }); User user = new User(users.stream().mapToLong(u -> u.getId()).max().getAsLong() + 1, email, password, name); users.add(user); mailService.sendRegistrationMail(user); return user; } }
注意到UserService通过setMailService()注入了一个MailService。
然后,我们需要编写一个特定的application.xml配置文件,告诉Spring的IoC容器应该如何创建并组装Bean:
<?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"> <bean id="userService" class="com.itranswarp.learnjava.service.UserService"> <property name="mailService" ref="mailService" /> </bean> <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" /> </beans>
观察上述配置文件,其中与XML Schema相关的部分格式是固定的,我们只关注两个<bean ...>的配置:
- 每个<bean ...>都有一个id标识,相当于Bean的唯一ID;
- 在
userService
Bean中,通过<property name="..." ref="..." />注入了另一个Bean; - Bean顺序不重要,Spring根据依赖关系会自动正确初始化。
把上述XML配置文件用Java代码写出来,就像这样:
UserService userService = new UserService();
MailService mailService = new MailService();
userService.setMailService(mailService);
只不过Spring容器是通过读取XML文件后使用反射完成的。
如果注入的不是Bean,而是boolean、int、String这种数据类型,则通过value注入,例如,创建一个HikariDataSource:
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" /> <property name="username" value="root" /> <property name="password" value="password" /> <property name="maximumPoolSize" value="10" /> <property name="autoCommit" value="true" /> </bean>
最后一步,我们需要创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码:
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml")
接下来,我们就能从Spring容器中取出装配好的Bean然后使用它:
// 获取Bean: UserService userService = context.getBean(UserService.class); // 正常调用: User user = userService.login("bob@example.com", "password");
完整的main()方法如下:
public class Main { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("application.xml"); UserService userService = context.getBean(UserService.class); User user = userService.login("bob@example.com", "password"); System.out.println(user.getName()); } }
3.1、ApplicationContext
我们从创建Spring容器的代码:
ApplicationContext context = new ClassPathXmlApplicationContext("application.xml")
可以看出,Spring容器就是ApplicationContext,它是一个接口,有很多实现类,这里我们选择classPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。
获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中我们可以根据Bean的ID获取Bean,但更多的时候我们根据Bean的类型获取Bean的引用:
UserService userService = context.getBean(UserService.class);
Spring还提供了另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似:
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml")); MailService mailService = factory.getBean(MailService.class);
BeanFactory和ApplicationContext的区别在于,BeanFactory是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次成型创建所有Bean。实际上,ApplicationContext接口是从BeanFactory接口继承而来,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。
通常情况下,我们总是使用ApplicationContext,很少考虑使用BeanFactory。
小结
Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;
通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext;
持有IoC容器后,通过getBean()获取Bean的引用。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
2021-06-06 Qt:QDateTime、QDate、QTime与QDateTimeEdit
2021-06-06 Pandas:读取数据库read_sql