Spring源码阅读目录
第一章 Spring之最熟悉的陌生人——IOC
第二章 Spring之假如让你来写IOC容器——加载资源篇
第三章 Spring之假如让你来写IOC容器——解析配置文件篇
第四章 Spring之假如让你来写IOC容器——XML配置文件篇
第五章 Spring之假如让你来写IOC容器——BeanFactory和FactoryBean
第六章 Spring之假如让你来写IOC容器——Scope和属性填充
第七章 Spring之假如让你来写IOC容器——属性填充特别篇:SpEL表达式
第八章 Spring之假如让你来写IOC容器——拓展篇
第九章 Spring之源码阅读——环境搭建篇
第十章 Spring之源码阅读——IOC篇
前言
对于Spring一直都是既熟悉又陌生,说对它熟悉吧,平时用用没啥问题,但面试的时候被问的一脸懵逼,就很尴尬,都不好意思在简历上写着熟悉Spring了
所以决定花点时间研究研究Spring的源码。主要参考的书籍是:《Spring源码深度解析(第2版)》、《Spring揭秘》、《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》
一些废话
一说到IOC,相信大家脑子里都会浮现出很多面试题,诸如:Spring如何解决循环依赖、BeanFactory和FactoryBean的区别、Spring的生命周期等等。不过这里先不贴代码直接看了,之前我也试过debug一步步跟,几分钟过后:我是谁?我在那里?我在干什么?
不妨换个思路,如果让我们自己写个IOC容器,我们会这么写?会从哪些方面考虑?完整项目
尝试动手写IOC容器
出场人物:A君(苦逼的开发)、老大(项目经理)
背景:老大要求A君在一周内开发个简单的IOC容器
现在老大突然要让A君自己来开发一个IOC容器,A君接到任务后,不禁心里暗骂:不是已经有现成的IOC容器了吗?为啥还要自己写@#¥%
骂归骂,活还是要干的,A君先用idea新建个maven项目,这个就不细说了,在引入一下依赖,pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hqd</groupId>
<artifactId>spring-imitation</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.0.0</version>
</dependency>
<!-- BeanUtils的依赖 -->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
</project>
定义一个简单的User对象用来测试,如下:
import lombok.Data;
@Data
public class User {
private String name;
private int age;
}
对于Bean的描述,只需要最简单的几个属性就行了,定义一个简单的bean.xml,如下:
<beans>
<bean id="user" class="com.hqd.ch03.bean.User">
<property name="name" value="zs"/>
<property name="age" value="14"/>
</bean>
</beans>
第一版 面向过程实现
由于是上头要求的,任务重,工期紧,A君不管三七二十一,先按面向过程的思路来做,要实现一个IOC容器,大致可以分为以下几步:
- 加载配置文件
- 解析配置文件
- 根据配置生成对象
- 将对象放在map里
分析完步骤,A君直接撸袖子干,代码如下:
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
*/
public class SpringImitationV1 {
/**
* 对象缓存
*/
private Map<String, Object> bdMap = new HashMap<>();
public SpringImitationV1(String path) {
this.init(path);
}
public <T> T getBean(String name, Class<T> clazz) {
return (T) bdMap.get(name);
}
/**
* 初始化bean
*
* @param bean
* @param properties
*/
private void initBean(Object bean, List<Element> properties) {
try {
properties.forEach(property -> {
String name = property.attributeValue("name");
String value = property.attributeValue("value");
try {
BeanUtils.setProperty(bean, name, value);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建bean对象
*
* @param beanEles
*/
private void createBeans(List<Element> beanEles) {
beanEles.forEach(beanEle -> {
String className = beanEle.attributeValue("class");
Object bean = createBean(className);
List<Element> properties = beanEle.elements("property");
if (CollectionUtils.isNotEmpty(properties)) {
initBean(bean, properties);
}
bdMap.put(beanEle.attributeValue("id"), bean);
});
}
private Object createBean(String className) {
try {
return Class.forName(className).getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 加载配置
*
* @param path
* @return
*/
private Document loadConfig(String path) {
SAXReader reader = new SAXReader();
try {
return reader.read(SpringImitationV1.class.getClassLoader().getResource(path));
} catch (DocumentException e) {
throw new RuntimeException(e);
}
}
/**
* 解析配置文件
*
* @param document
* @return
*/
private List<Element> parseConfig(Document document) {
Element rootElement = document.getRootElement();
return rootElement.elements("bean");
}
private void init(String path) {
Document document = loadConfig(path);
List<Element> beanEles = parseConfig(document);
createBeans(beanEles);
}
}
A君三下五除二,几下就写完了,接下来做个简单的测试,代码如下:
@Test
public void v1() {
System.out.println("############# 第一版 #############");
SpringImitationV1 bean = new SpringImitationV1("v1/bean.xml");
User user = bean.getBean("user", User.class);
System.out.println(user);
}
输入如下:
测试完之后,A君准备收工跑路,就把代码提交上去给老大过目
老大看了下,说:嗯,A君,还不错。但是你这个加载配置文件只能在类路径下,如果有些用户想把配置文件放在项目外,方便修改。这时候要怎么办呢?
第二版 统一资源加载
第一版被老大打回来之后,A君只能再修改一版,不过这时候A君留了个心眼:现在用户想要把配置放在项目外边,回头可能就放在类路径下甚至其他地方,不可能天天陪着用户改来改去
想到了这里,A君就设计一个Resource接口,这个接口只负责获取流对象,如下:
import java.io.IOException;
import java.io.InputStream;
/**
* 配置文件存放路径的接口定义
*/
public interface Resource {
/**
* 获取流对象
*
* @return
*/
InputStream getInputStream() throws IOException;
}
有了接口之后,就可以提供不同的实现方式来应对用户不同的需求了,这也是多态有魅力的地方,首先是基于类路径的实现,如下:
import java.io.IOException;
import java.io.InputStream;
/**
* 类路径下的资源
*/
public class ClassPathResource implements Resource {
private String path;
public ClassPathResource(String path) {
this.path = path;
}
@Override
public InputStream getInputStream() throws IOException {
return this.getClass().getClassLoader().getResourceAsStream(path);
}
}
接下来是基于文件系统的实现,这个就更简单了,如下:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 文件系统路径下的资源
*/
public class FileSystemResource implements Resource {
private String path;
public FileSystemResource(String path) {
this.path = path;
}
@Override
public InputStream getInputStream() throws IOException {
return new FileInputStream(path);
}
}
做完以上操作后,用户只要传入个Resource接口的实现就可以了,再来修改下SpringImitation类,改造如下:
init方法,如下:
loadConfig方法,如下:
完整代码:
import com.hqd.ch03.v2.io.Resource;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SpringImitationV2 {
/**
* 对象缓存
*/
private Map<String, Object> bdMap = new HashMap<>();
public SpringImitationV2(Resource resource) {
this.init(resource);
}
public <T> T getBean(String name, Class<T> clazz) {
return (T) bdMap.get(name);
}
/**
* 初始化bean
*
* @param bean
* @param properties
*/
private void initBean(Object bean, List<Element> properties) {
try {
properties.forEach(property -> {
String name = property.attributeValue("name");
String value = property.attributeValue("value");
try {
BeanUtils.setProperty(bean, name, value);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建bean对象
*
* @param beanEles
*/
private void createBeans(List<Element> beanEles) {
beanEles.forEach(beanEle -> {
String className = beanEle.attributeValue("class");
Object bean = createBean(className);
List<Element> properties = beanEle.elements("property");
if (CollectionUtils.isNotEmpty(properties)) {
initBean(bean, properties);
}
bdMap.put(beanEle.attributeValue("id"), bean);
});
}
private Object createBean(String className) {
try {
return Class.forName(className).getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 加载配置
*
* @param resource
* @return
*/
private Document loadConfig(Resource resource) {
SAXReader reader = new SAXReader();
try {
return reader.read(resource.getInputStream());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
resource.getInputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 解析配置文件
*
* @param document
* @return
*/
private List<Element> parseConfig(Document document) {
Element rootElement = document.getRootElement();
return rootElement.elements("bean");
}
private void init(Resource resource) {
Document document = loadConfig(resource);
List<Element> beanEles = parseConfig(document);
createBeans(beanEles);
}
}
再来编写下测试代码,如下:
/**
* 抽象资源文件
*/
@Test
public void v2() {
System.out.println("############# 第二版:抽象资源 #############");
SpringImitationV2 bean = new SpringImitationV2(new ClassPathResource("v2/bean.xml"));
User user = bean.getBean("user", User.class);
System.out.println("基于类路径加载:" + user);
SpringImitationV2 bean1 = new SpringImitationV2(new FileSystemResource("E:/代码/java/JavaEE/spring-imitation/src/main/resources/v2/bean.xml"));
User user1 = bean1.getBean("user", User.class);
System.out.println("基于文件系统路径加载:" + user1);
}
输入如下:
有了Resource接口之后,变化是显而易见,可以提供不同的Resource实现来获取各种形式存在的配置文件了。想到这里,A君兴高采烈的把代码提交给老大
老大再次看了下,又说:A君啊,这个用户还需要知道你Resource接口的具体实现,如果再简单一点就好了,毕竟提供给别人用的东西,越简单就越好,最好是开箱即用的那种
第三版 简化资源接口使用
老大既然发话了,为了方便用户使用,继续改呗
A君经过一番研究,发现URL可以描述很多协议:file://、http:// 等。可以参考URL协议,定义以下协议:classpath:,file:,分别用来描述不同的读取方式
有了想法之后,说干就干,老样子,A君还是先定义一个ResourceLoader接口,用来加载定义的协议。接口只有一个方法,就是通过传入的协议串来获取不同的资源,接口如下:
/**
* 配置文件加载接口
*/
public interface ResourceLoader {
String CLASSPATH_URL_PREFIX = "classpath:";
String FILE_URL_PREFIX = "file:";
/**
* 获取资源
*
* @param location
* @return
*/
Resource getResource(String location);
}
有了接口之后,需要在提供一个默认的实现,实现也很简单,根据字符串的开头进行判断,返回具体的资源即可,代码如下:
/**
* 默认资源加载器
*/
public class DefaultResourceLoader implements ResourceLoader {
@Override
public Resource getResource(String location) {
if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.replace(CLASSPATH_URL_PREFIX, ""));
} else if (location.startsWith(FILE_URL_PREFIX)) {
return new FileSystemResource(location.replace(FILE_URL_PREFIX, ""));
}
return null;
}
}
做好上边这些事之后,还需要修改下SpringImitation类,SpringImitation类需要提供个ResourceLoader的实现,用来解析用户传入的字符串,改造如下:
init方法如下:
loadConfig方法如下:
完整代码:
import com.hqd.ch03.v3.io.DefaultResourceLoader;
import com.hqd.ch03.v3.io.Resource;
import com.hqd.ch03.v3.io.ResourceLoader;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.collections.CollectionUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SpringImitationV3 {
/**
* 对象缓存
*/
private Map<String, Object> bdMap = new HashMap<>();
/**
* 资源加载器
*/
private ResourceLoader resourceLoader = new DefaultResourceLoader();
public SpringImitationV3(String path) {
this.init(path);
}
/**
* 初始化bean
*
* @param bean
* @param properties
*/
private void initBean(Object bean, List<Element> properties) {
try {
properties.forEach(property -> {
String name = property.attributeValue("name");
String value = property.attributeValue("value");
try {
BeanUtils.setProperty(bean, name, value);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 创建bean对象
*
* @param beanEles
*/
private void createBeans(List<Element> beanEles) {
beanEles.forEach(beanEle -> {
String className = beanEle.attributeValue("class");
Object bean = createBean(className);
List<Element> properties = beanEle.elements("property");
if (CollectionUtils.isNotEmpty(properties)) {
initBean(bean, properties);
}
bdMap.put(beanEle.attributeValue("id"), bean);
});
}
private Object createBean(String className) {
try {
return Class.forName(className).getConstructor().newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 加载配置
*
* @param path
* @return
*/
private Document loadConfig(String path) {
Resource resource = resourceLoader.getResource(path);
try {
SAXReader reader = new SAXReader();
return reader.read(resource.getInputStream());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
resource.getInputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public <T> T getBean(String name, Class<T> clazz) {
return (T) bdMap.get(name);
}
/**
* 解析配置文件
*
* @param document
* @return
*/
private List<Element> parseConfig(Document document) {
Element rootElement = document.getRootElement();
return rootElement.elements("bean");
}
private void init(String path) {
Document document = loadConfig(path);
List<Element> beanEles = parseConfig(document);
createBeans(beanEles);
}
}
修改完SpringImitation类,接下来在做个简单的测试,如下:
/**
* 添加资源加载器
*/
@Test
public void v3() {
System.out.println("############# 第三版:简化资源接口使用 #############");
SpringImitationV3 bean = new SpringImitationV3("classpath:v3/bean.xml");
User user = bean.getBean("user", User.class);
System.out.println("基于类路径加载:" + user);
SpringImitationV3 bean1 = new SpringImitationV3("file:E:/代码/java/JavaEE/spring-imitation/src/main/resources/v3/bean.xml");
User user1 = bean1.getBean("user", User.class);
System.out.println("基于文件系统路径加载:" + user1);
}
输出如下:
问题不大,今天又可以准时下班了,接着A君美滋滋的的把新版的代码提交给了老大,这次总没问题了吧?
老大看了看代码,瞟A君了一眼,说:寻找资源是没啥问题了,但是配置文件有问题,配置文件可是有多种多样的,目前这个只支持XML,如果是Properties、YML、JSON的呢?
总结
正所谓树欲静而风不止,欲知后事如何,请看下回分解(✪ω✪)