转载

Spring学习笔记(1)----Spring IOC学习和理解

Spring IOC (Inversion of Control, 控制反转 )是Spring的一个核心思想,其包括两层含义:

控制
反转

在Spring中, 控制反转 也可以简单地理解为将对象的创建和管理交由IOC容器来完成。

1.2 依赖注入(Dependency Injection)

IOC将依赖对象的控制权转移到外部,但当前对象还是需要依赖对象的,这时候就需要使用 依赖注入 将所需要的依赖对象从外部注入进来。

1.3 控制反转与依赖注入的关系

控制反转是一种思想,而依赖注入是一种实现方式。

  • 有些文章有提到,实现控制反转的方式还有依赖查找。

1.4 Spring IOC的优点

为什么需要使用Spring IOC?从IOC思想来看,主要有两大优点:

  • 资源集中管理 ,实现资源可配置和易管理;
  • 通过IOC实现当前对象和依赖对象之间的 松耦合

从Spring IOC的实际实现来看,还有如下好处:

  • 享受单例的好处,效率高,节约空间;
  • 便于单元测试,方便切换mock组件;
  • 便于进行AOP操作;

1.4.1 Spring IOC实现松耦合理解

没有IOC的情况下,使用依赖的对象需要 手动new一个对象 出来,根据构造器是否需要参数,可以分为有参对象和无参对象。而Spring只需要在xml文件中集中配置一次,或者使用注解就可以实现依赖注入,不需要手动new对象出来。

接下来考虑几种需要修改具体对象实现的情况下 代码重构的成本 ,来理解Spring IOC如何实现松耦合。

(1)无参对象,需要修改类名

比如需要将类名从Message修改为News,那么此时可以借助开发工具的重构功能实现代码重构。

重构难度:1星。

(2)无参对象,需要修改对象类型

比如需要将对象类型名从Student修改为Teacher,那么此时可以借助查找替换功能实现代码重构。 无重写方法情况下,重构难度:2星; 有重写方法情况下,重构难度:3星;

(3)有参对象,需要修改参数;

比如需要在创建Student对象时,增加年龄参数,此时借助查找功能也需要一个个手动修改代码,增加参数。

重构难度:5星。

(4)Spring IOC重构

如果使用Spring IOC,即便对难度最大的第三种情况,也只需要在xml文件中修改下注入的参数,或者在对应对象中增加一个属性,并使用注解自动注入即可。

重构难度:1星。

2. Spring IOC容器简介

IOC容器 是Spring提供的一个 工厂 ,用于管理所有的bean、以及bean之间的关系。

2.1 Spring IOC容器原理

Java反射机制

2.2 常见IOC容器

Spring中主要提供两类IOC容器,分别是 BeanFactoryApplicationContext

  • BeanFactory: 最基础的IOC容器,提供完整的IOC服务支持。
  • ApplicationContext: ApplicationContext是在 BeanFactory的基础之上构建的,是相对比较高级的容器实现。

两者间继承关系如下图,可见ApplicationContext间接继承自BeanFactory。

Spring学习笔记(1)----Spring IOC学习和理解

2.2.1 BeanFactory

BeanFactory是Spring提供的最基础的IOC容器,用于帮助完成bean的注册和bean之间关系的绑定。

特点:

  • 默认采用 延加载 策略。当容器中bean被访问时,才进行bean的初始化和依赖注入工作。

2.2.1.1 实现类

BeanFactory最基础的实现类是 DefaultListableBeanFactory

旧版本中BeanFactory最常见实现类是XmlBeanFactory(现已被废弃),新版中使用 XmlBeanDefinitionReader + DefaultListableBeanFactory 替代 XmlBeanFactory

2.2.1.2 代码示例

(1)建立一个工程,引入响应jar包

出于方便,可直接建立了一个SpringBoot工程。

(2)建立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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    
</beans>
复制代码

(3)创建对象类

这里使用@Data注解省略了getter和setter方法。

@Data
public class User {
    private Integer age;
    private Nation nation;

    public User() {
    }
    public User(Integer age) {
        this.age = age;
    }
    public User(Nation nation) {
        this.nation = nation;
    }
}
复制代码

(4)在配置文件中注册一个bean

<bean id="user" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
复制代码

(5)实例化容器、加载配置文件、获取bean

public static void main(String[] args) {
    // 1. XmlBeanFactory方式,已废弃
    // XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));

    // 2. XmlBeanDefinitionReader + DefaultListableBeanFactory 替代XmlBeanFactory
    Resource resource = new ClassPathResource("application.xml");
    BeanFactory factory = new DefaultListableBeanFactory();
    BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
    bdr.loadBeanDefinitions(resource);
    // 根据bean名从容器中获取bean
    User user1 = (User) factory.getBean("user");
    System.out.println(user1.getAge());
}
复制代码

2.2.2 ApplicationContext

ApplicationContext 是在BeanFactory的基础上构建的,是相对比较高级的容器实现,除了拥有 BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如:

  • 统一资源加载策略
  • 国际化信息支持
  • 容器内部事件发布机制

2.2.2.1 实现类

ApplicationContext常见实习类有:FileSystemXmlApplicationContext、ClassPathXmlApplicationContext和XmlWebApplicationContext。

2.2.2.2 代码示例

仍然使用BeanFactory示例中的配置文件,同时使用 ClassPathXmlApplicationContext 来读取xml文件。

// 3. ApplicationContext拥有比BeanFactory更高级的特性
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
User user3 = (User) applicationContext.getBean("user");
System.out.println(user3.getAge());
复制代码

3. Spring IOC容器装配Bean详解

3.1 依赖注入方式

依赖注入方式 主要包括:

构造器注入
属性注入

其中常用的两种方式是 构造器注入属性注入 。虽然官方文档建议能用构造器注入就用构造器注入,因为这样可以使得依赖关系明确,并且如果缺少依赖的话在初始化阶段就可以发现问题。但 属性注入 更加 灵活 ,并且 构造器注入 方式无法解决循环依赖问题,所以一般使用 属性注入强制依赖 建议使用 构造器方式 注入。

3.1.1 构造器注入

基于构造器的依赖注入是指,在bean的构造器中指明依赖的bean,并在初始化时完成注入。

代码示例

(1)bean类

User类见2.2.1.2中,存在如下构造器:

public User(Integer age) {
    this.age = age;
}
复制代码

(2)在xml中注册bean,并通过构造器方式进行依赖注入

<!-- 构造器方式注入 -->
<bean id="user" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
复制代码

3.1.2 属性注入

基于属性的依赖注入是指,容器通过无参构造器初始化bean,再通过调用setter方法来完成注入。

  • 不一定是无参构造器,只要后续通过setter方法进行注入就行。

(1)bean类

User类如上,有属性age(通过@Data注解省略了getter/setter方法):

private Integer age;
复制代码

(2)注册bean,并通过属性方式进行依赖注入

<!-- 属性注入 -->
<bean id="user2" class="com.example.springdemo.xml.model.User">
    <property name="age" value="27"></property>
</bean>
复制代码

(3)获取bean,并输出其依赖

// user使用了构造器注入
User user1 = (User) factory.getBean("user");
System.out.println("DI by constructor:" + user1.getAge());
// user2使用了属性注入
User user2 = (User) factory.getBean("user2");
System.out.println("DI by setter:" + user2.getAge());
复制代码

输出:

Spring学习笔记(1)----Spring IOC学习和理解

3.2 自动装配

3.2.1 手动注入依赖bean

在3.1小节中,User bean注入了int型的age属性,同样可以注入其它由IOC容器管理的bean。

(1)bean类

User类还存在如下构造器,表示User bean依赖Nation bean。

public User(Nation nation) {
    this.nation = nation;
}
复制代码

Nation类:

public class Nation {
    public String name;
    public Nation(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
复制代码

(2)注册bean,并声明依赖关系

注册Nation bean,同时注册User bean,并使用构造器方式对User bean注入Nation bean。

<bean id="nation" class="com.example.springdemo.xml.model.Nation">
    <constructor-arg name="name" type="java.lang.String" value="China"></constructor-arg>
</bean>

<!-- 手动注入依赖bean -->
<bean id="user3" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="nation" type="com.example.springdemo.xml.model.Nation" ref="nation"></constructor-arg>
</bean>
复制代码

(3)获取bean,并输出其依赖bean的属性

// 注入IOC容器管理的bean,user注入时需要nation。
// 这时候也能明显体现Spring Ioc解耦的优点,如果不使用依赖注入,将对象的创建交由bean来做的话,代码如下。
// 如果需要修改传入参数,项目中所有地方都需要修改
Nation nation = new Nation("China");
User user4 = new User(nation);
System.out.println("Name of nation in user3 by code: " + user4.getNation().getName());
// 而使用依赖注入的话,只需要在xml中一个地方集中配置管理,在配置文件中注入取值或者内部bean,而不需要每个创建对象的地方都要修改
User user5 = (User) factory.getBean("user3");
System.out.println("Name of nation in user3 by IOC: " + user5.getNation().getName());
复制代码

输出:

Spring学习笔记(1)----Spring IOC学习和理解

3.2.2 自动装配

在实际项目中,一般对于Dao、Service都需要单例,不会创建过多的bean,那么很多时候这些bean并不存在name、type的冲突,这时候是不是可以根据特定的规则,来简化bean的装配。

Spring IOC容器还提供了 自动装配 ,可以根据bean的type、name进行自动装配,而不需要显示地声明bean间依赖关系。

(1)xml中声明自动装配

<!-- 自动装配bean,这里是byType方式。自动装配时会将满足条件的bean注入到构造器和setter方法中 -->
<!-- 自动装配缺点:(1)不适用构造器重写的情况;(2)不能装配基本类型和字符串 -->
<bean id="user4" class="com.example.springdemo.xml.model.User" autowire="byType">
    <constructor-arg name="age" type="java.lang.Integer" value="28"></constructor-arg>
</bean>
复制代码

(2)获取bean,并输出其依赖bean

// 自动装配bean
User user6 = (User) factory.getBean("user4");
System.out.println("自动装配:" + user6.getAge());
System.out.println("自动装配:" + user6.getNation().getName());
复制代码

输出:

Spring学习笔记(1)----Spring IOC学习和理解

3.3 读取依赖关系方式

Spring IOC容器 读取依赖关系方式 有:

xml配置文件
注解

相比较而言, xml配置文件 方式拥有 集中配置 的优点,而 注解 方式则拥有 简化配置 的优点。

3.3.1 xml配置文件

Spring IOC容器可以通过加载xml配置文件,来读取依赖关系。

  • 3.2示例的代码都是通过此种方式来读取bean间依赖关系。

3.3.2 注解

由于xml配置文件拥有繁杂、不易配置的缺点,Spring IOC容器还同时支持 注解 来简化配置。

使用注解来读取依赖关系的步骤如下:

(1)声明注解

在bean类上使用注解@Component声明bean,使用注解@Autowired来声明依赖关系:

@Component
@Data
public class User {
    private Integer age;
    
    @Autowired
    private Nation nation;
}
复制代码

(2)实例化容器、设置要扫描注解类

创建ApplicationContext类型的IOC容器(BeanFactory实测不支持),并设置要扫描注解的包路径。

// 可以传入要扫描的包路径动态数组,也可以传入Class动态数组
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example.springdemo.annation");

User user1 = (User) applicationContext.getBean("user");
System.out.println(user1.getNation().getName());
复制代码

3.3.3 xml配置文件和注解混用

Spring IOC容器其实还支持xml配置文件和注解混用的方式来读取bean间依赖关系。

(1)在User类的nation的属性上添加@Autowired

@Autowired
private Nation nation;
复制代码

(2)配置文件中注册bean

使用构造器方式注入age属性,但没有注入nation字段

<!-- 注解与xml混用方式,这里并没有注入nation,也没有使用自动装配 -->
<bean id="user5" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="30"></constructor-arg>
</bean>
复制代码

(3)获取bean

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
// xml与注解混用,通过@Autowired注入nation bean(实际测试BeanFactory不支持注解,需要使用ApplicationContext)
User user7 = (User) applicationContext.getBean("user5");
System.out.println(user7.getAge());
System.out.println(user7.getNation().getName());
复制代码

从输出可以发现age和nation属性都被注入了。

Spring学习笔记(1)----Spring IOC学习和理解

3.4 注解

在3.3.2中介绍了注解的简单使用,由于注解已经成为现在主流的使用方式,接下来详细介绍些常用注解知识。

3.4.1 bean注册

@Bean@Component 等注解用于将对象注册为bean,交由Spring容器管理。

  • @Component 注解在 上使用,将类的定义与bean的声明绑定,@Bean则将两者分离。一般@Component更适合声明service、dao等单例对象,而@Bean更适合用于在不同场景下创建不同对象的情况。
  • @Controller、@Service和@Repository属于@Component的一种,一般分别用于controller层、service层和持久化层。个人理解@Service注解和@Component没有什么区别。
  • @Bean注解使用在方法上。

(1)@Bean

  • 可使用 @Bean 注解注册同一类的多个bean,该注解使用在方法上。
  • 可在被 @Componet@Configuration 注解声明的类里面使用声明里面使用;
  • 默认bean名为首字母小写的方法名,也可以使用name字段设置bean名。
@Bean
// @Primary表示默认bean,当有多个相同类型的bean时,可以使用@Primary来表示默认bean,其它地方注入依赖时就不需要都指明具体bean名。
@Primary
public Nation nation1() {
    return new Nation("China");
}

@Bean
public Nation nation2() {
    return new Nation("England");
}

// 使用name属性为bean命名
@Bean(name = "nation3")
public Nation nation() {
    return new Nation("America");
}
复制代码

(2)@Primary

当有多个相同类型的bean时,可以使用 @Primary 来表示默认bean,其它地方注入默认bean时就不需要指明具体bean名。

(3)@Value和@ConfigurationProperties

可以使用 @Value 注解来注入外部属性,而 @ConfigurationProperties 则可用来注入一整个对象的所有属性,或者说批量注入外部属性。

@Configuration
@PropertySource("classpath:application.properties")
public class JavaConfiguration {}
复制代码
@Bean
// 使用@Value来注入外部属性,需要使用@PropertySource引入资源文件。还可以使用@ConfigurationProperties来批量注入外部属性
public Nation nation4(@Value("${nation4.name:#{null}}") String nation4name) {
    return new Nation(nation4name);
}
复制代码

3.4.2 bean注入

@Autowired@Required 用来注入依赖的bean。

(1)@Autowired

  • @Autowired 注解可以在 字段 上使用,也可以在 setter方法 上使用,同时可以在 构造器 上使用。
  • @Autowired 默认采用了 自动装配 ,且优先根据 类型 自动装配,同类型则优先注入primary bean,然后再依据bean名(与属性同名)进行注入。
  • @Autowired 还可以结合 @Qualifier 来指定要注入的bean名。例如:@Qualifier("nation2")。

(2)@Required

@Required 注解也可以用来注入依赖,但已经被废弃。

(3)@Resource

Spring同时支持JSR-250的注解,同样可以使用 @Resource 来注入依赖(字段或者setter方法上),并使用name字段来指定bean名。例如:@Resource(name = "nation2")。

(4)@Inject

Spring3.0还支持JSR-330注解,可以使用 @Inject 来注入依赖。

3.4.3 Java config

@Configuration 注解表明该类是一个Java配置类,用于声明bean,在该类中可以使用@Bean注解来声明bean。一般情况下会将同一功能或用途的bean在一个配置累中声明。

3.5 Bean作用域(scope)

Spring IOC中Bean有多种不同作用域,主要有:

  • singleton
  • prototype

接下来主要介绍下singleton和prototype作用域。

3.5.1 singleton模式

singleton 模式(单例模式)是默认模式,该模式的bean有以下特点:

  • 容器中该bean只存在一个实例;
  • 实例存活时间:从容器启动,到第一次因请求而实例化,只要容器不销毁或退出,该实例一直存在。

代码示例

(1)注册bean 注册一个bean名为nation1的单例bean。

@Bean
public Nation nation1() {
    return new Nation("China");
}
复制代码

(2)从容器获取bean 两次从容器中获取同一个bean,是同一对象地址相同。

// 创建的nation1和nation2对象是相同bean
Nation nation1 = (Nation) applicationContext.getBean("nation1");
Nation nation2 = (Nation) applicationContext.getBean("nation1");
System.out.println("nation1 == nation2: " + (nation1 == nation2));
复制代码

输出:

Spring学习笔记(1)----Spring IOC学习和理解

3.5.2 prototype模式

prototype 模式(原型模式、多例模式)的bean有以下特点:

  • 每次从容器中请求bean都会重新生成一个实例;

代码示例

(1)注册bean 注册一个名为nation7的多例bean。

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Nation nation7() {
    return new Nation();
}
复制代码

(2)从容器获取bean 两次从容器获取同一个bean,是不同的对象,地址不同。

// nation3和nation4是不同bean。
// 此处需要注意,如果bean类上使用了lombok注解,不要被输出所迷惑,因为lombok重写了toString(),使得两者看起来像是一个对象,实际应该比较对象地址
Nation nation3 = (Nation) applicationContext.getBean("nation7");
Nation nation4 = (Nation) applicationContext.getBean("nation7");
System.out.println("nation3 == nation4: " + (nation3 == nation4));
System.out.println("nation3:" + nation3);
System.out.println("nation4:" + nation4);
复制代码

输出:

Spring学习笔记(1)----Spring IOC学习和理解

3.6 循环依赖

未完待续。。。。。

小结

Spring IOC可以说是Spring全家桶的基石,AOP等都依赖IOC的实现,要理解学习Spring应该先掌握Spring IOC的知识。

本文先是简单介绍了Spring IOC的思想和一些基本概念,然后针对Spring IOC容器实现依赖注入相关的知识点,并且分别给出了xml形式和注解形式依赖注入代码实现。

参考网址

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html
https://juejin.im/post/5bf51d4c5188256d9832b0d3
https://segmentfault.com/a/1190000013700859
https://segmentfault.com/a/1190000014979704
https://juejin.im/post/5b399eb1e51d4553156c0525
复制代码
原文  https://juejin.im/post/5edf8e0d518825431845b276
正文到此结束
Loading...