Spring IoC基础

3/25/2022 SpringIoC

# Spring IoC

在Spring 中,它会认为一切Java 类都是资源,而资源都是Bean,容纳这些Bean 的是Spring 所提供的IoC 容器,所以Spring 是一种基于Bean的编程。

为了实现一个功能,你需要调用一个接口会对象的方法,在潜意识里你会觉得对象应该由你主动创建,但是事实上这并不是你真实的需要,因为也许你对某一领域并不精通,这个时候可以把创建对象的主动权转交给别人,这就是控制反转的概念。

控制反转是一种通过描述(在Java中可以是XML 或者注解)并通过第三方去产生或获取特定对象的方式。在Spring 中实现控制反转的是IoC 容器,其实现方法是依赖注入( DI )

它最大的好处在于降低对象之间的耦合,在一个系统中有些类,具体如何实现并不需要去理解,只需要知道它有什么用就可以了。只是这里对象的产生依靠于IoC 容器,而不是开发者主动的行为。

# IoC配置的三种方式

# xml 配置

顾名思义,就是将bean的信息配置.xml文件里,通过Spring加载文件为我们创建bean。这种方式出现很多早前的SSM项目中,将第三方类库或者一些配置工具类都以这种方式进行配置,主要原因是由于第三方类不支持Spring注解。

  • 优点: 可以使用于任何场景,结构清晰,通俗易懂
  • 缺点: 配置繁琐,不易维护,枯燥无味,扩展性差

举例

  1. 配置xx.xml文件
  2. 声明命名空间和配置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="userService" class="tech.pdai.springframework.service.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        
    </bean>
    
</beans>

# Java 配置

将类的创建交给我们配置的JavcConfig类来完成,Spring只负责维护和管理,采用纯Java创建方式。其本质上就是把在XML上的配置声明转移到Java配置类中

  • 优点:适用于任何场景,配置方便,因为是纯Java代码,扩展性高,十分灵活
  • 缺点:由于是采用Java类的方式,声明不明显,如果大量配置,可读性比较差

举例

  1. 创建一个配置类, 添加@Configuration注解声明为配置类
  2. 创建方法,方法上加上@bean,该方法用于创建实例并返回,该实例创建后会交给spring管理,方法名建议与实例名相同(首字母小写)。注:实例类不需要加任何注解
 @Configuration
public class BeansConfig {

    @Bean("userDao")
    public UserDaoImpl userDao() {
        return new UserDaoImpl();
    }
    
    @Bean("userService")
    public UserServiceImpl userService() {
        UserServiceImpl userService = new UserServiceImpl();
        userService.setUserDao(userDao());
        return userService;
    }
}

# 注解配置

通过在类上加注解的方式,来声明一个类交给Spring管理,Spring会自动扫描带有@Component,@Controller,@Service,@Repository这四个注解的类,然后帮我们创建并管理,前提是需要先配置Spring的注解扫描器。

  • 优点:开发便捷,通俗易懂,方便维护。
  • 缺点:具有局限性,对于一些第三方资源,无法添加注解。只能采用XML或JavaConfig的方式配置

举例

  1. 对类添加@Component相关的注解,比如@Controller,@Service,@Repository
  2. 设置ComponentScan的basePackage, 比如<context:component-scan base-package='tech.pdai.springframework'>, 或者@ComponentScan("tech.pdai.springframework")注解,或者 new AnnotationConfigApplicationContext("tech.pdai.springframework")指定扫描的basePackage.
@Service
public class UserServiceImpl {
    
    @Autowired
    private UserDaoImpl userDao;

    public List<User> findUserList() {
        return userDao.findUserList();
    }
}

# Spring IoC容器

Spring IoC 容器的设计主要是基于BeanFactory 和ApplicationContext 两个接口,其中ApplicationContext 是BeanFactory 的子接口之一,换句话说BeanFactory 是Spring IoC 容器所定义的最底层接口,而ApplicationContext 是其高级接口之一,并且对BeanFactory 功能做了许多有用的扩展,所以在绝大部分的工作场景下, 都会使用ApplicationContext 作为Spring IoC 容器。ApplicationContext用来对Bean做初始化工作,根据context也可以获取到bean的实例。

Bean的初始化过程

  1. Resource 定位,这步是Spring IoC 容器根据开发者的配置,进行资源定位,在Spring的开发中,通过XML 或者注解都是十分常见的方式,定位的内容是由开发者所提供的。

  2. BeanDefinition 的载入,这个过程就是Spring 根据开发者的配置获取对应的POJO,用以生成对应实例的过程

  3. BeanDefinition 的注册,这个步骤就相当于把之前通过BeanDefinition 载入的POJO往Spring IoC 容器中注册,这样就可以使得开发和测试人员都可以通过描述从中得到Spring IoC 容器的Bean 了。

# 依赖注入

就 IOC 本身而言,其并不是什么新技术,只是一种思想理念。IOC 的核心就是原先创建一个对象,我们需要自己直接通过 new 来创建,而 IOC 就相当于有人帮们创建好了对象,需要使用的时候直接去拿就行,IOC 主要有两种实现方式:

DL(Dependency Lookup):依赖查找。==这种就是说容器帮我们创建好了对象,我们需要使用的时候自己再主动去容器中查找==,如:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml"); 
Object bean = applicationContext.getBean("object");

DI(Dependency Inject):依赖注入。依赖注入相比较依赖查找又是一种优化,也就是我们==不需要自己去查找,只需要告诉容器当前需要注入的对象,容器就会自动将创建好的对象进行注入(赋值)==。

Bean的初始化结束之后,还需要完成依赖注入。如果没有完成依赖注入,也就是没有注入其配置的资源给Bean ,那么它还不能完全使用。

对于依赖注入, Spring Bean还有一个配置选项一一lazy-init(延迟注入) , 其含义就是是否初始化Spring Bean 。在没有任何配置的情况下,它的默认值为default ,实际值为false ,也就是Spring IoC 默认会自动初始化Bean 。如果将其设置为true ,那么只有当我们使用Spring IoC 容器的getBean 方法获取它时,它才会进行初始化, 完成依赖注入。

# 依赖注入的方式

# setter方式

  • 在XML配置方式中,property都是setter方式注入,比如下面的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.xsd">
    
    <bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
        <property name="userDao" ref="userDao"/>
        
    </bean>
    
</beans>

本质上包含两步:

  1. 第一步,需要new UserServiceImpl()创建对象, 所以需要默认构造函数
  2. 第二步,调用setUserDao()函数注入userDao的值, 所以需要setUserDao()函数

所以对应的service类是这样的:

 public class UserServiceImpl {

    private UserDaoImpl userDao;

    public UserServiceImpl() {
    }

    public List<User> findUserList() {
        return this.userDao.findUserList();
    }

    public void setUserDao(UserDaoImpl userDao) {
        this.userDao = userDao;
    }
}
  • 在注解和Java配置方式下
@Service
public class UserServiceImpl {

    private UserDaoImpl userDao;

    public List<User> findUserList() {
        return this.userDao.findUserList();
    }

    @Autowired
    public void setUserDao(UserDaoImpl userDao) {
        this.userDao = userDao;
    }
}

在Spring3.x刚推出的时候,推荐使用注入的就是这种, 但是这种方式比较麻烦,所以在Spring4.x版本中推荐构造函数注入。

# 构造函数

  • 在XML配置方式中<constructor-arg>是通过构造函数参数注入,比如下面的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.xsd">
    
    <bean id="userService" class="tech.pdai.springframework.service.UserServiceImpl">
        <constructor-arg name="userDao" ref="userDao"/>
        
    </bean>
    
</beans>

本质上是new UserServiceImpl(userDao)创建对象, 所以对应的service类是这样的:

 public class UserServiceImpl {

    private final UserDaoImpl userDao;

    public UserServiceImpl(UserDaoImpl userDaoImpl) {
        this.userDao = userDaoImpl;
    }

    public List<User> findUserList() {
        return this.userDao.findUserList();
    }

}
  • 在注解和Java配置方式下
@Service
public class UserServiceImpl {

    private final UserDaoImpl userDao;

    @Autowired 
    public UserServiceImpl(final UserDaoImpl userDaoImpl) {
        this.userDao = userDaoImpl;
    }

    public List<User> findUserList() {
        return this.userDao.findUserList();
    }
}

在Spring4.x版本中推荐的注入方式就是这种, 具体原因看后续章节。

# 注解注入

以@Autowired(自动注入)注解注入为例,修饰符有三个属性:Constructor,byType,byName。==默认按照byType注入==。

  • constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。

  • byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。

  • byType:查找所有的set方法,将符合符合参数类型的bean注入。

比如:

@Service
public class UserServiceImpl {
    
    @Autowired
    private UserDaoImpl userDao;
    
    public List<User> findUserList() {
        return userDao.findUserList();
    }
}
  1. 构造器注入:构造器注入依赖于构造方法实现,而构造方法可以是有参数的或者是无参数的。在大部分的情况下,我们都是通过类的构造方法来创建类对象, Spring 也可以采用反射的方式,通过使用构造方法来完成注入,这就是构造器注入的原理。

img

  1. setter注入:setter 注入是Spring 中最主流的注入方式,它利用Java Bean 规范所定义的setter 方法来完成注入,灵活且可读性高。它消除了使用构造器注入时出现多个参数的可能性,首先可以把构造方法声明为无参数的,然后使用setter 注入为其设置对应的值,其实也是通过Java 反射技术得以实现的

img

  1. 接口注入:有些时候资源并非来自于自身系统,而是来自于外界,比如数据库连接资源完全可以在Tomcat 下配置,然后通过刑DI 的形式去获取它,这样数据库连接资源是属于开发工程外的资源,这个时候我们可以采用接口注入的形式来获取它。

img

# 依赖注入的注解

  1. @Autowired
  2. @Resource

# 依赖倒置/控制反转/依赖注入

img

# 依赖倒置 DIP

依赖倒置(Dependency inversion principle,缩写为 DIP)是面向对象六大基本原则之一。它是指一种特定的解耦形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。

该原则规定:

  • 高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
  • 抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。

上面这两句话很抽象,需要细细品味才能发现其中奥秘,如果暂时理解不了也没关系,下文会结合具体案例帮助大家理解。

# 控制反转 IOC

控制反转(Inversion of Control,缩写为 IOC)是面向对象编程中的一种设计原则,用来降低计算机代码之间的耦合度。是实现依赖倒置原则的一种代码设计思路。其中最常见的方式叫做依赖注入,还有一种方式叫依赖查找。

# 依赖注入 DI

依赖注入(Dependency Injection,缩写为 DI)是实现控制反转的一种方式。常用的依赖注入方法有 3 种:

  • 接口注入:
  • **构造函数注入:**指 IoC 容器使用构造方法注入被依赖的实例。基于构造器的 DI 通过调用带参数的构造方法实现,每个参数代表一个依赖。
  • **属性注入:**指 IoC 容器使用 setter 方法注入被依赖的实例。通过调用无参构造器或无参 static 工厂方法实例化 bean 后,调用该 bean 的 setter 方法,即可实现基于 setter 的 DI。

# IoC与DI的关系

控制反转是通过依赖注入实现的,其实它们是同一个概念的不同角度描述。通俗来说就是IoC是设计思想,DI是实现方式

DI—Dependency Injection,即依赖注入:组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

我们来深入分析一下:

  • 谁依赖于谁

当然是应用程序依赖于IoC容器;

  • 为什么需要依赖

应用程序需要IoC容器来提供对象需要的外部资源;

  • 谁注入谁

很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;

  • 注入了什么

就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

  • IoC和DI由什么关系呢

其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以2004年大师级人物Martin Fowler又给出了一个新的名字:“依赖注入”,相对IoC 而言,“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。通俗来说就是IoC是设计思想,DI是实现方式

# 参考

https://pdai.tech/md/spring/spring-x-framework-ioc.html

# Spring IoC如何解决循环依赖问题?

Spring IOC只能解决属性注入之间的循环依赖问题,如果是构造器之间的循环依赖,只会抛出BeanCurrentlyInCreationException异常。

Spring使用了3个Map来保存Bean,俗称为三级依赖:

  1. singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例,可以使用的。
  2. earlySingletonObjects 二级缓存,bean刚刚构造完成,但是还没有进行属性填充。
  3. singletonFactories 三级缓存,用于保存正在创建中的bean,以便于后面扩展有机会创建代理对象,此时的bean是没有完成属性填充的。

假设A类和B类相互依赖,A中有一个B类的属性,B中有一个A类的属性。那么在初始化A的Bean时,首先会依次去一级依赖,去二级依赖,三级依赖中去找,都没有就调用创建方法创建实例A,将A添加到三级依赖中,然后对A的属性进行依赖注入,填充属性时,发现B的Bean在各级依赖中都没有,就创建B的bean添加到三级依赖,然后对B的属性进行填充,填充B的属性A时,会从三级依赖中取出A,填充完放到二级依赖,然后对B进行初始化,初始化完成添加到一级依赖。B初始化完成后,将B从一级依赖中,填充到实例A,A可以进入到二级依赖,完全初始化完成后,A进入到一级依赖,供用户代码使用。

img

# @Autowired

# @Autowired原理

举例:@Autowired private BookService bookService;

1)先按照类型去容器中找到对应的组件;bookService = ioc.getBean(BookService.class)

① 找到一个:找到就赋值

② 没找到就报异常

③ 按照类型可以找到多个?找到多个如何装配上?

​ a、类型一样就按照变量名为ID继续匹配

​ Ⅰ、匹配上就装配

​ Ⅱ、没有匹配?报错

​ 原因:因为我们是按照变量名作为id继续匹配的

​ 解决:使用@Qualifier指定一个新的id,找到就匹配

# Bean有多个实现类的装配原理

# 现象1
@Autowired
private BookService bookService;

Autowired 为组件自动赋值,自动查找ioc容器,找到就赋值,查找的方式是按照类型寻找,有可能找到多个,比如说有一个继承于要查找的类,如果是多个就按照变量名作为id继续匹配。

举例说:查找到多个相同的类是BookService 和 BookServiceExtend,这 个 时候会根据你要查找的bookservice 来查找id,BookService 的id默认是bookservice,BookServiceExtend的id默认是bookServiceExtend,一查找就只有BookService匹配上了

如果现在把例子改成bookService2

@Autowired
private BookService bookService2;

Spring容器先查找BookService类,一找就找到两个类(BookService 和BookServiceExtend),然后查找id,发现都不匹配,会直接报错

这个时候诞生了@Qualifier注解,让spring不使用bookService2作为id去查找,而是使用一个比如说bookService去查找,作为新的id去查找。@Qualifier注解找不到就报错,找到就装配

@Qualifier"bookService"@Autowired
private BookService bookService2;

可以在Autowired(request = false)默认是true,找不到就会报错,置为false后,找不到会置为null

@Qualifier"bookService"@Autowired(request=falseprivate BookService bookService2;
# 现象2
public interface IUser {
    void say();
}

@Service
public class User1 implements IUser{
    @Override
    public void say() {
    }
}

@Service
public class User2 implements IUser{
    @Override
    public void say() {
    }
}

@Service
public class UserService {

    @Autowired
    private IUser user;
}

报错:testService1是单例的,却找到两个对象。

img

# @Autowire 和 @Resource 注解的区别

@Autowired功能虽说非常强大,但是也有些不足之处。比如:比如它跟spring强耦合了,如果换成了JFinal等其他框架,功能就会失效。而@Resource是JSR-250提供的,它是Java标准,绝大部分框架都支持

除此之外,有些场景使用@Autowired无法满足的要求,改成@Resource却能解决问题。接下来,我们重点看看@Autowired和@Resource的区别。

    • @Autowired默认按byType自动装配,而@Resource默认byName自动装配
  • @Autowired只包含一个参数:required,表示是否开启自动准入,默认是true。而**@Resource包含七个参数,其中最重要的两个参数是:name 和 type**。

  • @Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。

  • @Autowired能够用在:构造器、方法、参数、成员变量和注解上,而@Resource能用在:类、成员变量和方法上。

  • @Autowired是spring定义的注解,而@Resource是JSR-250定义的注解。

# @Autowired的装配顺序

img

# @Resource的装配顺序

如果同时指定了name和type:

img

如果指定了name:

img

如果指定了type:

img

如果既没有指定name,也没有指定type:

img

Last Updated: 3/29/2022, 6:01:02 PM