装配Bean

我们前面讨论了为什么要使用Spring的IoC容器,因为让容器来为我们创建并装配Bean能获得很大的好处,那么到底如何使用IoC容器?装配好的Bean又如何使用?

我们来看一个具体的用户注册登录的例子。首先,我们用Maven创建工程并引入spring-context依赖:

  1. <project xmlns="http://maven.apache.org/POM/4.0.0"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>com.itranswarp.learnjava</groupId>
  6. <artifactId>spring-ioc-appcontext</artifactId>
  7. <version>1.0-SNAPSHOT</version>
  8. <packaging>jar</packaging>
  9. <properties>
  10. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  11. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  12. <maven.compiler.source>11</maven.compiler.source>
  13. <maven.compiler.target>11</maven.compiler.target>
  14. <java.version>11</java.version>
  15. <spring.version>5.2.3.RELEASE</spring.version>
  16. </properties>
  17. <dependencies>
  18. <dependency>
  19. <groupId>org.springframework</groupId>
  20. <artifactId>spring-context</artifactId>
  21. <version>${spring.version}</version>
  22. </dependency>
  23. </dependencies>
  24. </project>

我们先编写一个MailService,用于在用户登录和注册成功后发送邮件通知:

  1. public class MailService {
  2. private ZoneId zoneId = ZoneId.systemDefault();
  3. public void setZoneId(ZoneId zoneId) {
  4. this.zoneId = zoneId;
  5. }
  6. public String getTime() {
  7. return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
  8. }
  9. public void sendLoginMail(User user) {
  10. System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));
  11. }
  12. public void sendRegistrationMail(User user) {
  13. System.err.println(String.format("Welcome, %s!", user.getName()));
  14. }
  15. }

再编写一个UserService,实现用户注册和登录:

  1. public class UserService {
  2. private MailService mailService;
  3. public void setMailService(MailService mailService) {
  4. this.mailService = mailService;
  5. }
  6. private List<User> users = new ArrayList<>(List.of( // users:
  7. new User(1, "bob@example.com", "password", "Bob"), // bob
  8. new User(2, "alice@example.com", "password", "Alice"), // alice
  9. new User(3, "tom@example.com", "password", "Tom"))); // tom
  10. public User login(String email, String password) {
  11. for (User user : users) {
  12. if (user.getEmail().equalsIgnoreCase(email) && user.getPassword().equals(password)) {
  13. mailService.sendLoginMail(user);
  14. return user;
  15. }
  16. }
  17. throw new RuntimeException("login failed.");
  18. }
  19. public User getUser(long id) {
  20. return this.users.stream().filter(user -> user.getId() == id).findFirst().orElseThrow();
  21. }
  22. public User register(String email, String password, String name) {
  23. users.forEach((user) -> {
  24. if (user.getEmail().equalsIgnoreCase(email)) {
  25. throw new RuntimeException("email exist.");
  26. }
  27. });
  28. User user = new User(users.stream().mapToLong(u -> u.getId()).max().getAsLong(), email, password, name);
  29. users.add(user);
  30. mailService.sendRegistrationMail(user);
  31. return user;
  32. }
  33. }

注意到UserService通过setMailService()注入了一个MailService

然后,我们需要编写一个特定的application.xml配置文件,告诉Spring的IoC容器应该如何创建并组装Bean:

  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. https://www.springframework.org/schema/beans/spring-beans.xsd">
  6. <bean id="userService" class="com.itranswarp.learnjava.service.UserService">
  7. <property name="mailService" ref="mailService" />
  8. </bean>
  9. <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
  10. </beans>

注意观察上述配置文件,其中与XML Schema相关的部分格式是固定的,我们只关注两个<bean …>的配置:

  • 每个<bean …>都有一个id标识,相当于Bean的唯一ID;
  • userServiceBean中,通过<property name="…" ref="…" />注入了另一个Bean;
  • Bean的顺序不重要,Spring根据依赖关系会自动正确初始化。

把上述XML配置文件用Java代码写出来,就像这样:

  1. UserService userService = new UserService();
  2. MailService mailService = new MailService();
  3. userService.setMailService(mailService);

只不过Spring容器是通过读取XML文件后使用反射完成的。

如果注入的不是Bean,而是booleanintString这样的数据类型,则通过value注入,例如,创建一个HikariDataSource

  1. <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
  2. <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
  3. <property name="username" value="root" />
  4. <property name="password" value="password" />
  5. <property name="maximumPoolSize" value="10" />
  6. <property name="autoCommit" value="true" />
  7. </bean>

最后一步,我们需要创建一个Spring的IoC容器实例,然后加载配置文件,让Spring容器为我们创建并装配好配置文件中指定的所有Bean,这只需要一行代码:

  1. ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

接下来,我们就可以从Spring容器中“取出”装配好的Bean然后使用它:

  1. // 获取Bean:
  2. UserService userService = context.getBean(UserService.class);
  3. // 正常调用:
  4. User user = userService.login("bob@example.com", "password");

完整的main()方法如下:

  1. public class Main {
  2. public static void main(String[] args) {
  3. ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");
  4. UserService userService = context.getBean(UserService.class);
  5. User user = userService.login("bob@example.com", "password");
  6. System.out.println(user.getName());
  7. }
  8. }

整个工程的结构如下:

  1. spring-ioc-appcontext
  2. ├── pom.xml
  3. └── src
  4. └── main
  5. ├── java
  6. └── com
  7. └── itranswarp
  8. └── learnjava
  9. ├── Main.java
  10. └── service
  11. ├── MailService.java
  12. ├── User.java
  13. └── UserService.java
  14. └── resources
  15. └── application.xml

ApplicationContext

我们从创建Spring容器的代码:

  1. ApplicationContext context = new ClassPathXmlApplicationContext("application.xml");

可以看到,Spring容器就是ApplicationContext,它是一个接口,有很多实现类,这里我们选择ClassPathXmlApplicationContext,表示它会自动从classpath中查找指定的XML配置文件。

获得了ApplicationContext的实例,就获得了IoC容器的引用。从ApplicationContext中我们可以根据Bean的ID获取Bean,但更多的时候我们根据Bean的类型获取Bean的引用:

  1. UserService userService = context.getBean(UserService.class);

Spring还提供另一种IoC容器叫BeanFactory,使用方式和ApplicationContext类似:

  1. BeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));
  2. MailService mailService = factory.getBean(MailService.class);

BeanFactoryApplicationContext的区别在于,BeanFactory的实现是按需创建,即第一次获取Bean时才创建这个Bean,而ApplicationContext会一次性创建所有的Bean。实际上,ApplicationContext接口是从BeanFactory接口继承而来的,并且,ApplicationContext提供了一些额外的功能,包括国际化支持、事件和通知机制等。通常情况下,我们总是使用ApplicationContext,很少会考虑使用BeanFactory

练习

在上述示例的基础上,继续给UserService注入DataSource,并把注册和登录功能通过数据库实现。

装配Bean - 图1下载练习:使用ApplicationContext (推荐使用IDE练习插件快速下载)

小结

Spring的IoC容器接口是ApplicationContext,并提供了多种实现类;

通过XML配置文件创建IoC容器时,使用ClassPathXmlApplicationContext

持有IoC容器后,通过getBean()方法获取Bean的引用。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

装配Bean - 图2 装配Bean - 图3