springIOC第1弹


  • 0x01_控制反转的概念

  • 0x02_Spring解耦合的原理

  • 0x03_Spring项目搭建

    • 创建一个空的project

    • 创建一个maven项目

    • 检查maven版本,配置文件,仓库

    • 添加依赖

    • 准备代码

  • 0x04_IOC 实现的原理

    • XML 解析技术DOM4J

    • 反射技术实例化对象,放到容器中

    • 工厂模式返回Bean对象:getBean方法


控制反转spring IOC应用及原理浅析

Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。IoC不是一个技术,而是一个设计思想

0x01_控制反转的概念

先看看比较晦涩的说法:(看不懂直接看0x02,怎样用IOC解耦的原理解释的很清楚)

控制容器的反转(依赖注入)- (Inversion of control container (dependency injection))Spring Framework 的核心是它的控制反转(IoC) 容器,它提供了使用反射配置和管理 Java 对象的一致方法。容器负责管理特定对象的对象生命周期:创建这些对象,调用它们的初始化方法,并通过将它们连接在一起来配置这些对象。

由容器创建的对象也称为托管对象bean。可以通过加载XML(可扩展标记语言)文件或检测配置类上的特定Java 注释来配置容器。这些数据源包含提供创建bean所需信息的 bean 定义。

可以通过依赖查找依赖注入来获取对象。依赖查找是一种模式,调用者向容器对象询问具有特定名称或特定类型的对象(可以理解为有一个map,根据id获取相应的对象)。依赖注入是一种模式,其中容器通过构造函数、属性或工厂方法按名称将对象传递给其他对象。

在许多情况下,在使用 Spring 框架的其他部分时不需要使用容器,尽管使用它可能会使应用程序更易于配置和定制。Spring 容器提供了一致的机制来配置应用程序并与几乎所有 Java 环境集成,从小型应用程序到大型企业应用程序。

通过 Pitchfork 项目,该容器可以变成部分兼容的EJB(Enterprise JavaBeans)3.0容器。一些人批评 Spring Framework 不符合标准。然而,SpringSource 并不将 EJB 3 合规性视为主要目标,并声称Spring框架和容器允许更强大的编程模型。程序员不会直接创建一个对象,而是通过在 Spring 配置文件中定义它来描述它应该如何创建。同样,服务和组件也不是直接调用的;相反,Spring 配置文件定义了必须调用哪些服务和组件。该 IoC 旨在提高维护和测试的便利性。


简单的说就是,创建对象的权利,或者是控制的位置,由JAVA代码(new 一个对象)转移到spring容器,由spring的容器控制对象的创建,就是控制反转,spring创建对象时,会读取配置文件(xml文件,其实可以简单理解为一个map集合)中的信息,然后使用反射给我们创建好对象之后在容器中存储起来,当我们需要某个对象时,通过id获取对象即可,不需要我们自己去new.

一句话:创建对象交给容器 (或者说转移到了配置文件中)

为什么要这么做?因为不这么做,耦合度太高。—》怎么理解这个问题?看看下面的分析

0x02_Spring解耦合的原理

看看我之前的用原生的的Servlet,JSP,JDBC搭建的MVC模式的项目结构和代码细节:

springIOC第1弹

现在关注后台的代码—》src下面的代码,主要关注dao,service,controller三个包下面的结构,

  • controller主要控制页面的跳转交互等
  • service主要是业务的实现的逻辑
  • dao主要是和数据库交互的层次(如果是mybatis,那就是mapper层)

在controller层,会调用service,实现具体的业务逻辑,而service层在完成相关的业务时,会调用dao层,进行数据库的各种操作(CURD,事务等)。

在controller层中:(调用了service层的接口,创建了对象)

springIOC第1弹
image-20221025134923762

而在service层,调用了dao层的接口,创建了dao层的对象:

springIOC第1弹
image-20221025135133521

对于上面的例子,看出来,一层一层的耦合度非常高,因为只要一层发生改变,那么相邻的那一层就一定会发生连锁的变化,后期维护的时候,很有可能牵一发动全身,代码会到处需要修改,耦合度非常高。

软件内部不同模块之间相互联系以及紧密的程度就是耦合。如果模块之间联系得越紧密,那么就说明耦合性越高,模块的独立性就越差。 那么在开发中,如果对象之间的耦合度很高的话,也就是说对象之间的依赖性越强的话,维护的成本自然就越高,因此在设计时应使对象之间的耦合变到最小,尽量达到低耦合。

因为需要开发者自己去new 一个对象。而Spring将这些都交给容器,开发只需要修改配置文件,容器就会相应创建对象,开发者根本不需要修改代码,这样相对来说,就降低了耦合度。

解耦也就是字面上的意思解除耦合关系。 在软件开发中,把降低模块之间的耦合程度即可以理解为解耦操作,我们总是听到别人说高内聚、低耦合,也是软件设计衡量模块独立程度的标准。但实际上要想达到理论上的绝对零耦合是做不到的。所以最好能达到松耦合的状态,即在保证实现功能的前提下尽量减少类和类之间的耦合。我们可以利用接口的多态达到这一目标,通过不同的实现类让调用方对具体实现不去关心,从而达到松耦合的目的。

严格来说,IOC不是一种技术,而是一种设计思想。IOC的出现主要作用就是解耦。

在平时的JavaWeb应用程序开发中,我们要完成某一个模块功能时,可能会需要两个或以上的对象来一起协作完成,在以前没有使用Spring框架的时候,每个对象在需要别的对象协助时,都需要自己通过new的方式将协作的对象实例化出来,创建协作对象的权限在自己手上,自己需要哪个,就主动去创建就可以了,而这样做呢,会使得对象之间的耦合变高,A对象需要B对象时,A对象来实例化B对象,这样A对象就对B对象产生了依赖,也就是A对象和B对象之间存在一种紧密耦合关系,而使用了Spring框架之后就不一样了,创建B对象的工作交由Spring框架来帮我们完成,Spring创建好这个对象以后,会存储到一个容器里面,当A对象需要时,Spring就会从容器中注入进来,至于Spring是如何创建这个对象的,A对象其实并不需要关心,A对象需要B对象时,Spring来创建B对象,然后给A对象就可以,从而达到松耦合的目的。

所以控制反转IOC是说创建对象的控制权发生了转移,由以前主动创建的权限,到现在把权限转移交给了IOC容器它是创建对象的工厂,你需要什么对象,它就给你创建出来,然后你需要的时候,注入进来就可以了,有了IOC容器之后,原来的依赖关系就没有了,它们都依赖IOC容器来帮我们建立它们之间的关系。

如果你还是不太理解IOC,那么举个例子就比如说人饿了想要吃饭。如果不使用IOC的话,你就得自己去菜市场买菜、做饭才能吃上饭。用了IOC以后,你可以到一家饭店,想吃什么菜你点好就可以了,具体怎么做你不用关心,饭店做好了,服务员端上来你负责吃就可以了,其它的交给饭店来做。

最后再看几个问题:

  • 说了半天控制,到底控制什么?谁控制谁呢?

传统的java SE程序,是在一个对象内部手动new来创建对象,是由开发者写的程序主动去创建对象;而IoC的思想是由一个专门的容器去创建对象。谁控制谁?那就是IoC容器控制对象。控制什么?容器不止对象,而且控制了其他资源(比如文件)。

  • 反转?

什么是正转?由开发者主动控制对象的获取,一个对象依赖一个对象,也需要开发者自己去创建;而反转就是由容器去创建对象,并且由容器去控制对象的依赖(依赖注入),对象只是被动的接受依赖对象。哪些方面反转了?依赖对象的获取被反转了。

传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是 松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

0x03_Spring项目搭建

上面解释了原理,对于Spring如何做到用容器创建对象,开发者只需要修改配置文件,用项目来了解最好不过。

创建一个空的project

这是我的一个习惯,在每次学习一个技术的时候,我比较习惯先创建一个空的project,每一次需要重新创建新的项目的时候,其实只需要在这个空的project中创建一个模块就可以了,方便学习的时候建立联系。每一次只需要在一堆具有联系的模块之间找自己需要的模块即可,不用idea频繁的切换项目。IDEA中,一个模块就是一个项目

springIOC第1弹
image-20221025140543515
springIOC第1弹
image-20221025141014444

创建完成之后,你发现没有项目名?

springIOC第1弹

可以关闭这个项目再重新打开,

springIOC第1弹

当然你也可以自己先创建好一个空的文件夹,用IDEA大家这个文件夹,这个和上面的效果时一样的。

创建一个maven项目

springIOC第1弹

springIOC第1弹
image-20221025141338476

创建maven项目的好处就是,方便灵活的添加依赖。

springIOC第1弹
image-20221025141440283

创建完成之后,就是一个典型的maven的结构:

springIOC第1弹

检查maven版本,配置文件,仓库

要养成一个习惯:每一次创建maven模块之后,建议检查模块的maven的版本等信息。

settings—》Build,Execution,Deploymnet—>Build Tools–>Maven

springIOC第1弹
image-20221025141731229

不建议idea内置的maven,建议使用自己本地的maven,因为有时候你需要配置镜像,便于下载相应的依赖。

修改Maven home path,User settings file,有时候需要还修改Local repository.

添加依赖

根据spring的框架图,可以看到Core Contrainer这一层,需要四个依赖。

springIOC第1弹
image-20221025142118315

在maven中心仓库分别搜索下面这4个依赖:(中心仓库网址:https://mvnrepository.com/)

  • Spring Beans
  • spring-core
  • spring-context
  • spring-spel

pom.xml中添加依赖,我以spring5.3.23(目前稳定的最新版本,虽然现在spring6的版本都有了,但是6还是内测阶段)

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.23</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.23</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.23</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.23</version>
</dependency>

</dependencies>

如果想要切换当前模块运行的JDK版本,可以去Project Structrue中修改

springIOC第1弹
image-20221025142900010

查看添加的依赖:

springIOC第1弹

依赖之间也相互依赖,所以引入依赖的时候可能会重复引入依赖

还可以图形化看依赖的引入:

springIOC第1弹springIOC第1弹

上面的pom.xml依赖引入明显出现了冗余,其实只需要引入一个context依赖即可:

springIOC第1弹

初次之外,为了方便测试建议引入junit依赖:

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

准备代码

现在准备dao层,将测试类模拟为service层作为测试。

项目结构:

springIOC第1弹

EmpDao

package com.bones.dao;

/**
* @author : bones
* @version : 1.0
*/

public interface EmpDao {
int deleteEmpByEmpno();
}

EmpDaoImpl

package com.bones.dao.impl;

import com.bones.dao.EmpDao;

/**
* @author : bones
* @version : 1.0
*/

public class EmpDaoImpl implements EmpDao {
@Override
public int deleteEmpByEmpno() {
System.out.println("EmpDaoImpl deleteEmpByEmpno invoked");
return 0;
}
}

spring.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="empDao" class="com.bones.dao.impl.EmpDaoImpl"/>
</beans>

TestContainer

package com.bones.test01;

import com.bones.dao.EmpDao;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
* @author : bones
* @version : 1.0
*/

public class TestContainer {
@Test
public void testContainer1(){
//获取容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
EmpDao empDao = applicationContext.getBean("empDao", EmpDao.class);
empDao.deleteEmpByEmpno();
}
}

执行测试方法,控制台输出:

springIOC第1弹
image-20221025160737816

注意:

1.spring.xml的创建:

springIOC第1弹
image-20221025161110501

创建完成后,只需要添加bean标签即可

2.关于bean标签:

<bean id="empDao" class="com.bones.dao.impl.EmpDaoImpl"/>

id就是在创建对象的时候需要指定的:

springIOC第1弹
image-20221025161310559

class属性就是改对象的全路径名,容器会根据反射创建对象,所以只需要写全路径名。

3.测试类中:

【1】获取容器:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");

applicationContext是容器上下文,可以简单理解为就是容器,他是一个接口,其实现可以调用实现类ClassPathXmlApplicationContext,创建时需要传入一个Spring的配置文件,这个配置文件中就写清楚了容器需要创建的id和全路径名。

【2】获取bean:

EmpDao empDao = applicationContext.getBean("empDao", EmpDao.class);

通过容器创建对象,只需要调用getBean方法,这个方法重载有很多,这里只关注2个:

Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;

上面这个方法,只需要传入要实例化对象的id值empDao,但是返回对象是Object,后期还需要强制转换,就像这样:

EmpDao empDao = (EmpDao) applicationContext.getBean("empDao");

后一个方法就是上面用到的,传入要实例化对象的字节码。(推荐使用这个方法)

以上就是spring IOC 实现的一个简单的版本。(可以基本理解IOC的思想了)

0x04_IOC 实现的原理

这里只是简单聊一下。不涉及很详细深入的源码阅读,但是要读源码

先上一张图易于理解:

springIOC第1弹
image-20221025202031252

先看代码

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
EmpDao empDao = (EmpDao) applicationContext.getBean("empDao");
empDao.deleteEmpByEmpno();
springIOC第1弹
image-20221025210339582

XML 解析技术DOM4J

ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("spring.xml");

首先需要创建容器,读取XML配置文件(后面spring开发基本都是基于注解,但是学习基于XML的开发有助于基于注解的学习)

这里面就涉及到XML的解析技术DOM4J

dom4j is an open source framework for processing XML which is integrated with XPath and fully supports DOM, SAX, JAXP and the Java platform such as Java 2 Collections.

dom4j是一个十分优秀的JavaXML API,具有性能优异、功能强大和极其易使用的特点,它的性能超过sun公司官方的dom技术,同时它也是一个开放源代码的软件,可以在SourceForge上找到它。

可以简单创建一个项目应用一下dom4j,这个是后话,空了再研究。

上面构造方法调用的是ClassPathXmlApplicationContext的构造方法:

springIOC第1弹
image-20221026151631164

上面的this是另外一个重构的构造方法:

springIOC第1弹
image-20221026151835640

上面这个是创建容器的主线(主要思路)

分为几个部分:

【1】调用父类AbstractXmlApplicationContext的构造方法:

springIOC第1弹
image-20221026152009971

AbstractXmlApplicationContext调用的是其父类AbstractRefreshableConfigApplicationContext的构造方法

springIOC第1弹
image-20221026152148254

这个super还需要再往上调用一层:AbstractRefreshableApplicationContext的构造方法:

springIOC第1弹
image-20221026152259887

super再往上调用一层:AbstractApplicationContext的构造方法:

springIOC第1弹
image-20221026152335756

终于调用到源头了(所以说读Spring源码确实也是一件非常困难的事情)

这里分为2步:

  • 第一步是:this()
springIOC第1弹
image-20221026152531700

getResourcePatternResolver方法会创建一个PathMatchingResourcePatternResolver对象:

springIOC第1弹
image-20221026152625732

这个PathMatchingResourcePatternResolver就是为了创建一个资源加载器的:

springIOC第1弹
image-20221026152650374
  • 第二步是:setParent(parent);
springIOC第1弹
image-20221026153022460

setParent方法将父容器的environment合并到当前容器中。

再回到主线:

springIOC第1弹
image-20221026153211200

接下来来看下一步setConfigLocations(configLocations);

【2】setConfigLocations(configLocations);这里的参数configLocations就是当前传进来的配置文件的路径spring.xml

springIOC第1弹
image-20221026153842491

再次回到主线,前两步super(parent);setConfigLocations(configLocations);都了解了,再看第三步refresh

springIOC第1弹
image-20221026154126667

refresh是调用这个构造方法传入的值,传入是true

springIOC第1弹
image-20221026154221254

再看refresh()方法:(方法实现代码比较多)

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");

// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}

Spring IoC容器对Bean定义资源的载入是从refresh()函数开始的,refresh()是一个模板方法,refresh()方法的作用是:在创建IoC容器前,如果已经有容器存在,则需要把已有的容器销毁和关闭,以保证在refresh之后使用的是新建立起来的IoC容器。refresh的作用类似于对IoC容器的重启,在新建立好的容器中对容器进行初始化,对Bean定义资源进行载入。

这里的设计上是一个非常典型的资源类加载处理型的思路,头脑中需要形成如下图的顶层思路(而不是只停留在流水式的方法上面):

  • 模板方法设计模式,模板方法中使用典型的钩子方法
  • 具体的初始化加载方法插入到钩子方法之间
  • 将初始化的阶段封装,用来记录当前初始化到什么阶段;常见的设计是xxxPhase/xxxStage;
  • 资源加载初始化有失败等处理,必然是try/catch/finally

看到一张不错的图:

springIOC第1弹
image-20221026155120036

摘自:https://pdai.tech/md/spring/spring-x-framework-ioc-source-2.html

springIOC第1弹
image-20221026151502551

回顾一下new ClassPathXmlApplicationContext("spring.xml");做的事情

1.一层一层往上调用,创建一个资源加载器ResourceLoader,并且将父容器的environment合并到当前容器中。

2.setConfigLocations(configLocations);解析配置路径

3.refresh将资源加载到容器中。

反射技术实例化对象,放到容器中

EmpDao empDao =applicationContext.getBean("empDao",EmpDao.class);

首先在getBean方法中,需要传入字节码文件EmpDao.class。因为要利用反射来创建对象,所以需要字节码文件来调用Class.forName()方法,得到一个class对象,由得到的class对象调用newInstance方法得到要实例化的bean对象,然后将实例化得到的bean对象,放入容器,实际上可以简单理解为容器中的map会维护,map的键就是getBean()方法传入的idempDao,值即为实例化得到的对象。

工厂模式返回Bean对象:getBean方法

下面简单说一下getBean方法:(关于源码的详细深入的阅读,后面的文章详细说,因为这个工厂模式比较复杂,这里为了不偏离学习的方向,暂且不介绍;一开始读大量的源码不利于学习技术的使用)

简单理解就是首先判断有没有这个id的对象,有的话就类似于调用map.get()返回对象即可。

【1】再提一下IOC思想的两个重要的接口:

  • BeanFactory接口: IOC容器基本功能接口,是spring内部使用的接口,我们在处理业务时一般不直接使用该接口
  • ApplicationContext接口: BeanFactory的子接口,提供更多更强大的功能,研发人员一般在写业务时使用的接口
springIOC第1弹
image-20221026160526662

【2】最后还要看一下目前代码中用的ClassPathXmlApplicationContextApplicationContext之间的关系

springIOC第1弹
image-20221026161218615

两个类:

FileSystemXmlApplicationContext:用来读取系统文件。

默认获取的是项目路径,默认文件路径是项目名下一级,与src同级。 如果前边加了file:则说明后边的路径就要写全路径了,就是绝对路径 file:D:/workspace/applicationContext.xml

ClassPathXmlApplicationContext

默认文件路径是src下那一级classpath:classpath*:的区别:classpath: 只能加载一个配置文件,如果配置了多个,则只加载第一个classpath*: 可以加载多个配置文件,如果有多个配置文件,就用这个


原文始发于微信公众号(小东方不败):springIOC第1弹

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/47098.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!