前言
本人在稀土掘金上的文章,搬到CSDN,非抄袭。
以下文字均为手敲,引用字段我会进行说明,拒绝抄袭、复制从我做起。
在Spring学习过程中,IOC是重中之重,而其中又以Bean的加载为最,故学好Bean的加载十分重要。其重要性主要体现在以下三点:
(1)有助于程序员理解策略模式、设计原则、算法应用;
(2)能更深层次的理解Spring家族成员的“家规”;
(3)为后续的SpringBoot、SpringCloud等框架的学习开辟道路;
前人对Bean的加载已经有所总结,其中蚂蚁P8大佬爱撒谎的男孩写了spring的Bean加载过程 - 云+社区 - 腾讯云 (tencent.com),石玉军在微信公众号Java学习录中发表了SpringIOC源码解析(上) (qq.com)、SpringIOC源码解析(下) (qq.com),Java技术驿站发布了[死磕 Spring] — IOC 之深入理解 Spring IoC - Java 技术驿站 (cmsblogs.com)。这些文章内容详细, 能从refresh()出发鞭辟入里的解答Bean的加载全流程,在一定程度上推动了知识的传递。但这些文章仍不可避免的有言语啰嗦、部分问题表达不清、源码解释不够深入的问题。
故本文针对网络上关于Bean的加载资料繁多杂乱、表达不清、不够深入的特点,整合了基于AplicationContext的Bean加载流程,重新梳理了Java8下的Bean加载源码体系。本文是第一次在掘金上写文章,以期能助力知识的传递及掘金论坛的发展。
注:写的菜别骂我,请帮助我成为大佬,之后我再改。
作者介绍
姓名:比卷帘门还能卷
职业:学生(23年应届)
爱好:摸鱼、装大佬讨论技术
座右铭:保持真实,保持清醒
正文
1. IOC是什么?
大佬:IOC都不知道你说精通Spring?IOC就是创建一个IOC容器,由这个容器统一管理对象的生命周期和对象之间的关系。也可以叫控制反转或者依赖注入(DI)。
我:???啥玩意?
大佬:滚
…
于是经过我自己的琢磨发现,IOC就是饿了么(免费版)嘛~~
别急,看我举例:
今天午饭我想吃土豆炖龙肉,我要去选土豆,选鲜嫩小龙,自己做。
耗时3天。
我,我默认是吃土的,今天自己做一个土豆龙肉,后来发现冰箱里还有点辣椒肉,又炒了个辣椒回锅肉。这是我今天的操作。
我类:
package Test;
//我
public class me {
//我的午饭
private myLunch mylunch;
//默认我的午饭(构造方法注入)
me(){
mylunch=new myLunch("土","土");
mylunch.cook();
}
//指定午饭款式(Setter方法注入)
public void setMylunch(myLunch mylunch) {
this.mylunch = mylunch;
}
public static void main(String[] args) {
//冰箱里的剩菜(提前有的模板)
myLunch fryMeetWithPepper=new myLunch("青椒","回锅肉");
//我先创建一个我自己,默认吃土
me i=new me();
//自己做个土豆龙肉
myLunch potatostewedDragon=new myLunch("土豆","龙肉");
potatostewedDragon.cook();
//昨晚剩下的青椒艹回锅肉
i.setMylunch(fryMeetWithPepper);
fryMeetWithPepper.cook();
}
}
午饭类:
package Test;
//午饭类
public class myLunch {
//食材1
private String material1;
//食材2
private String material2;
//默认的午饭
myLunch(String material1,String material2){
this.material1=material1;
this.material2=material2;
System.out.println("食材:"+material1+"和"+material2);
}
//放入食材1
public void setMaterial1(String material1) {
this.material1 = material1;
}
//放入食材2
public void setMaterial2(String material2) {
this.material2 = material2;
}
//自己做
public void cook() {
System.out.println("我给自己做了午饭:"+this.material1+"炒"+this.material2);
}
}
结果:
做完了吃饱了,干净又卫生,只是本卷王要累死了。不知道各位读者老爷有没有发现什么?没关系,我下载了一个饿了么(免费版),各位老爷再来看看。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
</dependencies>
<?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="mylunch" class="Test.myLunch">
<!--不用你构造方法了,我知道你爱吃土,给你放锅里了-->
<!--默认食材1-->
<constructor-arg index="0" value="土"/>
<!--默认食材2-->
<constructor-arg index="1" value="土"/>
</bean>
</beans>
package Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class me2 {
public static void main(String[] args) {
//尊敬的少爷,您的饿了么(免费版)下载好了
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application-ioc.xml");
myLunch mylunch=context.getBean(myLunch.class);
//什么也不选,下单只能吃土
mylunch.cook();
//来个龙肉,再来个土豆,再下单
mylunch.setMaterial1("龙肉");
mylunch.setMaterial2("土豆");
mylunch.cook();
//来个海参,再来个鲍鱼,再下单
mylunch.setMaterial1("海参");
mylunch.setMaterial2("鲍鱼");
mylunch.cook();
//来个千年人参,再来个万年灵芝,再下单
mylunch.setMaterial1("千年人参");
mylunch.setMaterial2("万年灵芝");
mylunch.cook();
}
}
结果:
通过自己做饭和使用饿了么对比可以看出,自己做饭需要买菜、洗菜、做菜、刷碗…(需要负责午餐对象创建、赋值、销毁等,耦合性较高),饿了么只需要下载APP(容器)之后选择你想要的菜就可以了,这个菜(对象)会由饿了么分配给你。
为什么叫控制反转?
原来你自己做菜,现在由饿了么做菜,菜的控制权由你转移到了饿了么(IOC容器)。也就是对象不再由开发者管理,变成由虚拟容器来管理。这个控制发生了反转,即由人转移到了程序。
为什么叫依赖注入?
我依赖菜,有才可以吃午饭,然而我需要的午饭是由饿了么(IOC容器)注入到我这个类里面的。所以也叫依赖注入,注入方式为反射,反射允许程序在运行时动态的生成对象、执行对象的方法、改变的对象的属性。
2.IOC如何加载Bean?
饿了么里面为什么有这么多做好的菜?为了搞清楚这一点我们有必要从刚才的APP说起。
//尊敬的少爷,您的饿了么(免费版)下载好了
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application-ioc.xml");
myLunch mylunch=context.getBean(myLunch.class);
进入classPathxmlApplcationContext中我们会发现:
其中只有一个成员变量就是Resource类型的数组??也许你会有疑问这个Resource是干什么的?(虽然我们这个例子没有用到,但是我还是建议你看一下)
我们检索configResource会发现,他在下面的代码里面十分关键。
这里的代码是另一个构造方法,与我们的代码不同的是,这里传入的是一个数组,也就是多个字符串,而我们的例子只传了一个字符串,这里还传入了Class对象。在此构造方法中先执行了super,通过追踪可以发现,这个方法一直到
这里则是对AbstractApplicationContext进行赋初始值。先不继续深入,回头看。经过连续两个判空之后,创建了一个Resource数组configResource并通过Class参数和Path数组参数创建CassPathResource对象给数组赋值。后执行refresh,refresh十分重要在此先不说。
CassPathResource对象是什么?通过追溯可以找到
至此,我们引出一个重要的接口,Resource。
[死磕 Spring] — IOC 之 Spring 统一资源加载策略 - Java 技术驿站 (cmsblogs.com)
在学 Java SE 的时候我们学习了一个标准类
java.net.URL
,该类在 Java SE 中的定位为统一资源定位器(Uniform Resource Locator),但是我们知道它的实现基本只限于网络形式发布的资源的查找和定位。然而,实际上资源的定义比较广泛,除了网络形式的资源,还有以二进制形式存在的、以文件形式存在的、以字节流形式存在的等等。而且它可以存在于任何场所,比如网络、文件系统、应用程序中。所以java.net.URL
的局限性迫使 Spring 必须实现自己的资源加载策略,该资源加载策略需要满足如下要求:
- 职能划分清楚。资源的定义和资源的加载应该要有一个清晰的界限;
- 统一的抽象。统一的资源定义和资源加载策略。资源加载后要返回统一的抽象给客户端,客户端要对资源进行怎样的处理,应该由抽象资源接口来界定。
这位大佬是这样形容的Resource,Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义。
OK ,那么Resource做了什么呢?看看它提供的几大方法。
这些方法大部分由抽象实现类AbstractResource进行了实现,所以需要重写的话,重写AbstractResource类就好。
告诉我你们看到了什么?接口,里面有方法的实现了,看到了吗?自从Java8开始,接口里面的方法就可以实现了,目的是为了解决一些接口中不常用的方法导致每个实现类都必须要去实现这类方法,但是也不可避免的带来了冲突问题,感兴趣的同学可以自行搜索。(题外话)
观察AbstracResource的子类
不难发现这么常用的五类资源,其中classPathResource正是我们开头所看到的
为什么默认是classPathResource?原因在下图。
所以真相大白了,ClassPathXmlApplicationContext是要搭配classPathResource使用的。
那么研究一下ClassPathXmlApplicationContext,这各类一直追到最底层会发现其实源自于一个叫ResourceLoader的类。
classpath:前缀正是在这里设置的规则。其中getResource是根据所提供资源的路径 location 返回 Resource 实例。而getClassLoader则是直接返回返回 ClassLoader 实例。实现类为
说明了三种情况:
“/”-classPathContextResource
“classpath”-classPathResource
“URL”-调用 getResourceByPath(),主要为FileSystemResourceLoader使用。
FileSystemResourceLoader为FileSystemXmlApplicationContext提供服务。
而FileSystemXmlApplicationContext是ClassPathXmlApplicationContext兄弟。
其中ResourcePatternResolver 是 ResourceLoader 的扩展,它支持根据指定的资源路径匹配模式每次返回多个 Resource 实例。
到这里终于说完了说完了Spring的类加载策略,接下来可以说说类的加载方式了。
回到刚刚的构造方法
值得一提的是这个configLocations中只有一个值,“classpath:application-ioc.xml”。
第一行super在上面已经讲过了,主要任务是执行父类AbstractApplicationContext的构造方法。
第二行setConfigLocations可以详细说说,传入[“classpath:application-ioc.xml”]。
可以看到先对locations进行了判空,当然不为空了,接下来,通过resolvePath方法将值赋值给configLocations。接下来看看这个resolvePath是什么来路。
上图可以看出,先执行了getEnvironment()
接着看createEnvironment()方法,发现它返回了一个StandardEnvironment类,而这个类中的customizePropertySources方法就会往资源列表中添加Java进程中的变量和系统的环境变量。
返回resolvePath继续看resolveRequiredPlaceholders(path)方法。
由于strictHelper默认是空的,所以会执行createPlacholdHelper。
继续创建并返回一个propertyPlaceholderHelper,并传入下图参数作为构造参数。
可以看的出,正是在这里传入了占位符。
这个类中定义了三种括号。
在此方法内判断了括号是否符合规则,并返回对象。
回头看这个方法
将刚刚生成的占位符对象以及"classpath:application-ioc.xml"传入。
继续往下看
判空后执行parseStringValue,看来这里就是最终的处理逻辑了。
将递归结果(括号中间的内容)输入到resolvwPlaceholder。
这么大的方法!这么多细节要处理!他怎么敢递归的啊!真的膜拜大佬!!
不得不说,parseStringValue这个方法写的太秀了,看傻了。
回到这里,爷爷类AbstractRefreshableConfigApplicationContext的configLocation已经复制完毕,返回孙子类ClassPathXmlApplicationContext。
累了,改天好好写一下refresh方法,今天就到这里了。
本人研二学生一枚,理解有限,欢迎指正。谢谢大家。