-
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模式的项目结构和代码细节:
现在关注后台的代码—》
src
下面的代码,主要关注dao
,service
,controller
三个包下面的结构,
controller主要控制页面的跳转交互等 service主要是业务的实现的逻辑 dao主要是和数据库交互的层次(如果是mybatis,那就是mapper层) 在controller层,会调用service,实现具体的业务逻辑,而service层在完成相关的业务时,会调用dao层,进行数据库的各种操作(CURD,事务等)。
在controller层中:(调用了service层的接口,创建了对象)
image-20221025134923762 而在service层,调用了dao层的接口,创建了dao层的对象:
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中,一个模块就是一个项目。


创建完成之后,你发现没有项目名?
可以关闭这个项目再重新打开,
当然你也可以自己先创建好一个空的文件夹,用IDEA大家这个文件夹,这个和上面的效果时一样的。
创建一个maven项目

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

创建完成之后,就是一个典型的maven的结构:
检查maven版本,配置文件,仓库
要养成一个习惯:每一次创建maven模块之后,建议检查模块的maven的版本等信息。
settings—》Build,Execution,Deploymnet
—>Build Tools
–>Maven

不建议idea内置的maven,建议使用自己本地的maven,因为有时候你需要配置镜像,便于下载相应的依赖。
修改
Maven home path
,User settings file
,有时候需要还修改Local repository
.
添加依赖
根据spring的框架图,可以看到Core Contrainer
这一层,需要四个依赖。

在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
中修改image-20221025142900010
查看添加的依赖:
依赖之间也相互依赖,所以引入依赖的时候可能会重复引入依赖
还可以图形化看依赖的引入:
上面的pom.xml依赖引入明显出现了冗余,其实只需要引入一个context依赖即可:
初次之外,为了方便测试建议引入junit依赖:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
准备代码
现在准备dao层,将测试类模拟为service层作为测试。
项目结构:
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();
}
}
执行测试方法,控制台输出:

注意:
1.
spring.xml
的创建:image-20221025161110501 创建完成后,只需要添加
bean
标签即可2.关于bean标签:
<bean id="empDao" class="com.bones.dao.impl.EmpDaoImpl"/>
id就是在创建对象的时候需要指定的:
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 实现的原理
这里只是简单聊一下。不涉及很详细深入的源码阅读,但是要读源码
先上一张图易于理解:

先看代码
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
EmpDao empDao = (EmpDao) applicationContext.getBean("empDao");
empDao.deleteEmpByEmpno();

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
的构造方法:

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

上面这个是创建容器的主线(主要思路)
分为几个部分:
【1】调用父类AbstractXmlApplicationContext
的构造方法:

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

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

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

终于调用到源头了(所以说读Spring源码确实也是一件非常困难的事情)
这里分为2步:
第一步是: this()
image-20221026152531700 而
getResourcePatternResolver
方法会创建一个PathMatchingResourcePatternResolver
对象:image-20221026152625732 这个
PathMatchingResourcePatternResolver
就是为了创建一个资源加载器的:image-20221026152650374
第二步是: setParent(parent);
image-20221026153022460
setParent
方法将父容器的environment
合并到当前容器中。
再回到主线:

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

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

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

再看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… 看到一张不错的图:
image-20221026155120036 摘自:https://pdai.tech/md/spring/spring-x-framework-ioc-source-2.html

回顾一下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的子接口,提供更多更强大的功能,研发人员一般在写业务时使用的接口image-20221026160526662
【2】最后还要看一下目前代码中用的ClassPathXmlApplicationContext
和ApplicationContext
之间的关系

两个类:
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