Spring IoC 容器的几种使用方式 | Java Debug 笔记

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Spring IoC 容器的几种使用方式 | Java Debug 笔记

当我们使用 Spring 的 IoC 容器管理 Bean 的时候,Spring 不能凭空帮我们创建。需要我们提前准备 XML 配置文件或者使用注解提前告知 Spring ,有哪些 Bean 是需要被用到的,以及它们该如何被创建。


XML 配 Bean



通过 XML 实现 Bean 配置,主要工作都在 XML 完成,代码上获取方式始终不变。可以封装一个工具类从 IoC 容器获取也可以直接使用 API 获取:


public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    Account account = applicationContext.getBean("account", Account.class);
}
复制代码


注意:“使用 ApplicationContext 实现类获取 Bean" 和 "使用 BeanFactory 实现类获取 Bean"不一样:


使用 ApplicationContext 实现类获取 Bean:在读取配置文件之后,会根据 Bean 所配置的作用范围(默认为 singleton )决定是马上创建 Bean 加入容器,还是等待 getBean 的调用进行按需加载; 使用 BeanFactory 实现类获取 Bean:在读取配置文件并不会做任何 Bean 的创建操作,只有在真正调用 getBean 的时候才会被创建并加入容器,不受作用范围配置的影响。


由于 ApplicationContext 能够更好的控制 Bean 的创建加载,所以日常中都是使用 ApplicationContext 实现类。以上示例代码使用 ApplicationContext 实现类获取 Bean 。


  • 没有依赖注入的情况


<bean id="accountService" class="com.acoier.service.impl.AccountServiceImpl"></bean>
  <bean id="accountRepository" class="com.acoier.repository.impl.AccountRepositoryImpl"></bean>
  <!-- 
  * 以上是最简单的 bean 配置,它默认是取 class 对应的全类名调用无参构造方法进行实例化。如果没有对应的无参构造方法则报错。
  * 使用静态工厂方法
  <bean id="accountRepository" class="com.acoier.repository.factory.AccountRepositoryFactory" factory-method="accountRepository"></bean>
  * 使用实例工厂方法
  <bean id="accountRepositoryFactory" class="com.acoier.repository.factory.AccountRepositoryFactory"></bean>
  <bean id="accountRepository" factory-bean="accountRepositoryFactory" factory-method="getAccountRepository"></bean>
  * 指定作用范围
  <bean id="accountService" class="com.acoier.service.impl.AccountServiceImpl" scope="singleton"></bean>
  -->
复制代码


  • 有参构造方法参数注入


<bean id="account" class="com.acoier.entity.Account">
      <constructor-arg name="id" value="23"></constructor-arg>
      <constructor-arg name="name" value="LEBRON JAMES"></constructor-arg>
      <constructor-arg name="bestTime" ref="now"></constructor-arg>
  </bean>
  <bean id="now" class="java.util.Date"></bean>
  <!-- 
  * value: 指定基本数据类型或者String类型
  * ref: 指定其他引用类型,必须是在 Ioc 容器中注册过的 Bean 对象
  -->
复制代码


  • set 方法注入


<bean id="account" class="com.acoier.entity.Account">
      <property name="id" value="23"></property>
      <property name="name" value="LEBRON JAMES"></property>
      <property name="bestTime" ref="now"></property>
      <property name="achievement">
          <set>
              <value>MVP</value>
              <value>FMVP</value>
              <value>AMVP</value>
          </set>
      </property>
  </bean>
  <bean id="now" class="java.util.Date"></bean>
  <!-- 
  * set 注入原理:
  * 之所以叫 set 方法注入,而不是叫属性注入或者成员注入。
  * 是因为 Spring 获取的是所有的 set 方法,然后将 set 字眼去掉,并将后面的内容首字母转小写,只有转换成功才能注入。
  * e.g. 
  * 1. 获取到 setBestTime 方法 
  * 2. 截取 set 后半部分,得到 BestTime
  * 3. 首字母是大写,将首字母转换成小写得到 bestTime 
  * 4. 将得到 bestTime 的作为 <property> 的 id
  * 如果将 setBestTime 改为 setBestTime1 则需要用 bestTime1 作为 <property> 的 id
  * 如果将 setBestTime 改为 setbestTime 则因为不符合 set 后半段不是首字母大写而无法被 Spring 识别
  -->
  <!-- 
  * 复杂类型注入:
  * List 类型可互换:<array> <list> <set>
  * Map 类型可互换:<map> <props>
  -->
复制代码


在可能的情况下,尽量使用 set 方法注入依赖,而不是使用构造参数注入依赖。因为使用构造参数依赖容易引发循环依赖问题( Bean A 创建需要使用 Bean B 作为构造参数,而 Bean B 创建需要使用 Bean A 作为构造参数,会引发 BeanCurrentlyInCreationException 异常)。


注解



注解是 Spring 简化配置的手段,所有能够通过 XML 进行配置的选项都能够使用注解替代。


首先修改 XML 配置文件,开启自动扫描(开启注解):


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
  <context:component-scan base-package="com.acoierpeterxx"></context:component-scan>
</beans>
复制代码


注意:也有用于指定扫描的注解,下面会讲到。


  • 将 Bean 注册到 IoC 容器:


@Component :用于代替  的注解,通常添加在 Bean 类上。


// 等同于配置:<bean id="customAccountDTO" class="com.acoier.dto.AccountDTO"></bean>
// @Component("customAccountDTO") 
// 等同于配置:<bean id="accountDTO" class="com.acoier.dto.AccountDTO"></bean>
@Component 
public class AccountDTO {
    // 忽略业务代码...
}
复制代码


当将 @Component 添加到类的时候,效果等同于添加一条  记录,并以该类的全类名作为 class 属性,类名的首字母转小写作为 id 属性。当然 @Component 注解也可以通过设置 value 属性来指定 bean id。


@Component 的另外三个别名注解 @Controller 、@Service 、@Repository ,分别对应了 Java Web 的三层架构。所以通常的用法是三层架构分别采用对应的注解,而三层架构以外的的使用 @Component 注解。


  • 从 IoC 容器获取 Bean 实例:


@Autowired :使用该注解必须满足“容器中只有唯一的能够与之匹配的 Bean”。什么意思呢?用代码来说明:


// AccountRepositoryImpl.java
@Repository
public class AccountRepositoryImpl implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountRepository accountRepository;
    // 忽略业务代码...
}
复制代码


@Autowired 能够成功注入是因为 IoC 容器中只有唯一一个 AccountRepository 实现类 AccountRepository Impl 能够与之匹配。日常开发中很少真的遇到多数据源的情况,所以基本上都是一个 Repository 接口对应唯一一个实现类,所以 @Autowired 就能很好满足使用。如果 IoC 容器中存在多个与之匹配的 Bean ,该如何处理?


假如 AccountRepository 有两个实现类:


// AccountRepositoryImplA.java
@Repository
public class AccountRepositoryImplA implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountRepositoryImplB.java
@Repository
public class AccountRepositoryImplB implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountRepository accountRepository;
    // 忽略业务代码...
}
复制代码


这时候使用 @Autowired 由于无法确认唯一的实现类,所以无法注入。但是 @Autowired 当匹配到有多个合适类型的时候还会去判断是否有 bean id 与待注入对象变量名一致而且类型匹配的 Bean ,如果有也能匹配成功:


// AccountRepositoryImplA.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplA
public class AccountRepositoryImplA implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountRepositoryImplB.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplB
public class AccountRepositoryImplB implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountRepository AccountRepositoryImplA; // 匹配到两个合适类型,取 bean id 与 变量名相同的 AccountRepositoryImplA 进行注入
//    private AccountRepository AccountRepositoryImplB; // 匹配到两个合适类型,取 bean id 与 变量名相同的 AccountRepositoryImplB 进行注入
    // 忽略业务代码...
}
复制代码


但是要我们的待注入对象的变量名和 bean id 始终保持一致显然不合理,所以引入了另外一个能够指定 bean id 的注解 @Qulifier 。


@Qulifier :在写注解的同时指定要注入的 bean id ,不能独立使用,要依赖 @Autowired 使用。


// AccountRepositoryImplA.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplA
public class AccountRepositoryImplA implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountRepositoryImplB.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplB
public class AccountRepositoryImplB implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    @Qulifier("AccountRepositoryImplA")
    private AccountRepository accountRepository; 
    // 忽略业务代码...
}
复制代码


@Autowired 不能满足,而 @Qulifier 不能独立使用,是否有更好的解决方案?答案是 @Resource 。


@Resource :能够独立使用,指定 bean id 实现注入。但是 @Resource 并不是使用默认属性 value 来接收 bean id,所以我们在使用 @Resource 注入的时候需要指定使用 name 属性,而不能忽略。


// AccountRepositoryImplA.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplA
public class AccountRepositoryImplA implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountRepositoryImplB.java
@Repository // 不指定 bean id ,默认为类名首字母转小写,accountRepositoryImplB
public class AccountRepositoryImplB implements AccountRepository {    
    // 忽略业务代码...
}
...
// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Resource(name = "AccountRepositoryImplA")
    private AccountRepository accountRepository; 
    // 忽略业务代码...
}
复制代码


@Autowired 、@Qulifier 和 @Resource 都能够实现注入。但是注入的都是通过 @Component 及其别名注解 ( @Controller 、@Service 和 @Repository ) 注册到 IoC 容器的 Bean 类别。


  • Bean 的作用范围 & 生命周期


关于 Bean 对象的作用范围,使用 @Scope 注解:


@Scope("singleton") : 单例,默认值。若配置该值,Bean 会在配置文件加载完成之后马上将 Bean 对象创建。多次获取是同一对象。


@Scope("prototype") : 非单例。若配置该值,只有在获取 Bean 对象的时候才会创建, 并且每次获取的时候创建新值返回。


@Scope("request") : 作用范围在单次请求内,也就是针对每一次 HTTP 清楚创建一次 Bean 对象。


@Scope("session") : 作用范围在当前 HTTP session 内,当 session 失效,Bean 对象会被回收。


@Scope("global session") : 限定使用范围是全局 session ,一般指分布式环境中的 session 。


关于 Bean 的生命周期,可使用 @PostConstruct 和 @PreDestroy 注解:


@PostConstruct :作用于方法上,指定该 Bean 对象创建时执行的方法


@PreDestroy :作用于方法上,指定该 Bean 对象销毁时执行的方法


  • 注入其他值


如果需要注入基本类型或者 String 类型的数据话,需要使用 @Value 。


@Value :可以注入基本类型或者 String 类型数据,但更多用来注入配置文件中的属性。也可以使用 SpEL 。


主要有两种用法:


@Value("${property:default_value}") :直接传入已加载的配置文件中的 key


@Value("#{obj.property?:default_value}") :传入 SpEL 表达式


// AccountServiceImpl.java
@Service
public class AccountServiceImpl implements AccountService {
    @Resource(name = "AccountRepositoryImplA")
    private AccountRepository accountRepository; 
    @Value("${server.port}")
    private String port;
    @Value("#{otherBean.url}")
    private String url;
    // 忽略业务代码...
}
复制代码


  • 配置类


有时候我们还会在 XML 中声明一些来自于第三方的 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 http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="accountRepository" class="com.acoier.repository.impl.AccountRepositoryImpl">
        <property name="sqlTemplate" ref="sqlTemplate"></property>
    </bean>
    <bean id="sqlTemplate" class="the.third.party.package">
       <property name="url" value="jdbc:mysql://localhost:3306/db_name"></property>
       <property name="user" value="root"></property>
       <property name="password" value="root"></property>
    </bean>
</beans>
复制代码


这时候使用注解如何解决呢?我们自然可以在用到 sqlTemplate 的地方写上 @Autowired 。但是如何向 IoC 容器中添加呢?


// AccountRepositoryImpl.java
@Repository 
public class AccountRepositoryImpl implements AccountRepository {
    // 写上了 @Autowired 但是当前 IoC 容器中没有与 SqlTemplate 匹配的类 ...
    @Autowired
    private SqlTemplate sqlTemplate; 
    // 忽略业务代码...
}
...
复制代码


所以问题变为需要想办法将 SqlTemplate Bean 放进 IoC 容器,但是我们不太可能往别人源码中插入 @Component 注解。


也就是说我们除了使用注解往 IoC 容器中添加 Bean 以外,还需要一个能够在 Bean 源码文件以外的地方将该 Bean 往 IoC 容器插入的功能。


@Configuraion + @Bean 可以很好解决这个问题:


@Configuration
public class Config {
//    @Bean(name = "customSqlTemplate")
    @Bean
    @Scope("prototype")
    public SqlTemplate sqlTemplate() {
        SqlTemplate sqlTemplate = new SqlTemplate();
        sqlTemplate.setUrl("jdbc:mysql://localhost:3306/db_name");
        sqlTemplate.setUser("root");
        sqlTemplate.setPassword("root");
        return sqlTemplate;
    } 
}
复制代码


@Configuration 声明了该类是一个配置类,@Configuration 的作用等同于  标签。


@Bean 声明了一个 Bean 对象,等于同于  标签。在不指定 name 属性时,默认使用方法名作为 Bean id ,方法的返回值作为 Bean 实例。


这样就解决了我们在非 Bean 源码文件位置将 Bean 加入 IoC 容器的问题。


基本上使用 Spring 注解遇到需要将第三方 Bean 加入 IoC 时,都可以通过配置类解决。


如果是多个第三方 Bean 相互依赖呢?这时候 Config 类里面的注册 Bean 的方法就会涉及参数,谁负责给传参赋值呢?


别担心,之前没有参数的时候,是 Spring 负责调用这些方法将返回值加入容器。所以同样的,Spring 会负责从 IoC 容器中查找对应的 Bean 来为这些参数赋值,查找的机制就是之前说的 @Autowired 查找机制:


@Configuration
public class Config {
    @Bean
    @Scope("prototype")
    // Spring 会自动从 IoC 容器中查找匹配 DataSource 的 Bean 作为参数
    public SqlTemplate sqlTemplate(DataSource dataSource) { 
        SqlTemplate sqlTemplate = new SqlTemplate(dataSource);
        return sqlTemplate;
    } 
    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new DataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db_name");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        return dataSource;
    } 
}
复制代码


如果配置多了,东西都写在一个配置类里也不合适,最终配置类文件可能会变为以前臃肿的 XML 文件。如果只是使用 @Configuration 标记一个配置类的话,我们大可以将配置分类建立多个配置类文件,然后使用一个总配置类来组织其他配置文件。这时候可以使用 @Import 进行组织:


@Configuration
@Import(DBConfig.class)
public class Config {
    // 忽略业务代码...
}
...
@Configuration
public class DBConfig {
    @Bean
    @Scope("prototype")
    // Spring 会自动从 IoC 容器中查找匹配 DataSource 的 Bean 作为参数
    public SqlTemplate sqlTemplate(DataSource dataSource) { 
        SqlTemplate sqlTemplate = new SqlTemplate(dataSource);
        return sqlTemplate;
    } 
    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new DataSource();
        dataSource.setUrl("jdbc:mysql://localhost:3306/db_name");
        dataSource.setUser("root");
        dataSource.setPassword("root");
        return dataSource;
    } 
} 
复制代码


其实只要我们在小配置类上加上 @Configuration ,并且保证小配置类也在扫描包范围内就可以保证小配置类里的配置生效,那么为什么还要在主配置类内使用 @Import 来组织呢?


这个主要是为了易读性和可维护性,当我们遵循在主配置类里统一书写 @Import 来进行组织的话,那么我们只需要记住主配置类入口,至于主配置类背后有多少其他配置可以通过 @Import 得知,这对于我们定位某些配置十分有帮助。


最后还有个小问题,就是我一直严格遵循的一个规范“绝不在除了常量文件或者配置文件以外的地方使用字符串硬编码”。所以这里的与数据库链接相关的信息,我更希望是在某一个配置文件中管理,例如 db.properties ,所以告知配置类去某个配置文件下找配置成了关键,引入 @PropertySource 注解:


@Configuration
@PropertySource("classpath:db.properties")
public class DBConfig {
    @Value("${db.url}")
    private String url;
    @Value("${db.user}")
    private String user;
    @Value("${db.password}")
    private String password;
    @Bean
    @Scope("prototype")
    // Spring 会自动从 IoC 容器中查找匹配 DataSource 的 Bean 作为参数
    public SqlTemplate sqlTemplate(DataSource dataSource) { 
        SqlTemplate sqlTemplate = new SqlTemplate(dataSource);
        return sqlTemplate;
    } 
    @Bean
    public DataSource dataSource() {
        DataSource dataSource = new DataSource();
        dataSource.setUrl(this.url);
        dataSource.setUser(this.user);
        dataSource.setPassword(this.password);
        return dataSource;
    } 
} 
复制代码


最后还剩下个问题,目前我们的 applicationContext.xml 只剩下一句扫描包的配置,这个能用注解代替吗?


答案是当然可以,我们可以使用 @ComponentScan 替代:


@Configuration
@ComponentScan("com.acoier")
public class Config {
    // 忽略业务代码...
}
复制代码


@ComponentScan 作用效果等同于 context:component-scan 标签 。一般该注解最好挂载在程序启动类或者主配置类上。


总结



Spring 的注解极大的提高了 Java Web 的开发效率,善用注解完全可以干掉以前的 applicationContext.xml 配置文件。


使用 XML 配置还有个致命弱点就是如果要找一个 Bean 还得需要先在配置文件中找到对应的全类名,再跳转到源文件中,当然团队良好的 Bean id 命名可以尽量避免这个问题,绝大多数能做到见 Bean id 就推测到具体实现类的类名。但这也是避免,不能消除。而使用注解则是完全解决了这个痛点。毫无疑问大家都应该尽快拥抱 Spring 注解。

另外 Spring 系列其实还有个 Spring Boot 框架,听说是进一步简化 Spring 的配置。之前用官网构建了一个 Spring Boot 的 Demo,无需任何配置就可以运行。有点 Python Web 中的 Django、Flask 的开箱即用的意思。如果说 Spring 的注解在 Java Web 和 Python Web、Node.js 和 Ruby Web 的开发效率对比上扳回一城的话,将来 Spring Boot 的成熟,开箱即用的实现可能基本上就是宣布后三者在 Web 开发领域的没落了。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
13天前
|
Java 测试技术 开发者
IoC容器有什么作用?
【4月更文挑战第30天】IoC容器有什么作用?
32 0
|
13天前
|
Java 测试技术 开发者
Spring IoC容器通过依赖注入机制实现控制反转
【4月更文挑战第30天】Spring IoC容器通过依赖注入机制实现控制反转
22 0
|
3天前
|
存储 安全 Java
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
Java容器类List、ArrayList、Vector及map、HashTable、HashMap
|
4天前
|
Kubernetes Java 调度
Java容器技术:Docker与Kubernetes
Java容器技术:Docker与Kubernetes
16 0
|
13天前
|
安全 Java 开发者
在Spring框架中,IoC和AOP是如何实现的?
【4月更文挑战第30天】在Spring框架中,IoC和AOP是如何实现的?
22 0
|
13天前
|
Java 开发者 容器
IoC容器如何实现依赖注入?
【4月更文挑战第30天】IoC容器如何实现依赖注入?
21 0
|
13天前
|
XML Java 数据格式
如何配置IoC容器?
【4月更文挑战第30天】如何配置IoC容器?
20 0
|
13天前
|
XML Java 程序员
什么是Spring的IoC容器?
【4月更文挑战第30天】什么是Spring的IoC容器?
20 0
|
5月前
|
XML Java 数据格式
④【Spring】IOC - 基于注解方式 管理bean
④【Spring】IOC - 基于注解方式 管理bean
49 0
|
5月前
|
XML Java 数据格式
②【Spring】一文精通:IOC - 基于XML方式管理Bean
②【Spring】一文精通:IOC - 基于XML方式管理Bean
146 0