前面介绍了Spring的一些基础知识和简单的用法,本篇开始分析Spring的IoC容器。BeanFactory是IoC容器的基础,所以接下来的分析都是基于BeanFactory的。
从图中看IoC容器的启动可分为三步,加载资源文件、解析资源文件、注册BeanDefinition。本篇分析资源文件加载过程。
Spring中资源文件加载主要有两个接口,Resource和ResourceLoader,前者提供了对资源文件的定位、是否存在、是否可读、是否打开、是否文件、获取URL,获取File、获取FileName等一系列功能。后者提供了Resource对象获取,自定义资源文件协议解析等功能。加载资源文件的过程会涉及到类加载机制,且不是我们分析IoC容器的重点,所以本篇不会做太多深入的分析。
- 测试类
package com.lyc.cn.v2.day03;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import java.io.IOException;
/**
* @author: LiYanChao
* @create: 2018-09-07 23:40
*/
public class MyTest {
@Test
public void test1() {
// 从资源文件夹下加载
Resource resource = new ClassPathResource("v2/day01.xml");
print(resource);
}
@Test
public void test2() {
// 使用类信息加载
Resource resource = new ClassPathResource("day01.xml", MyTest.class);
print(resource);
}
@Test
public void test3() {
// 使用类加载器从资源文件夹下加载
Resource resource = new ClassPathResource("v2/day01.xml", MyTest.class.getClassLoader());
print(resource);
}
@Test
public void test4() {
// 使用DefaultResourceLoader加载
Resource resource = new DefaultResourceLoader().getResource("v2/day01.xml");
print(resource);
}
// 打印资源文件内容
public void print(Resource resource) {
byte[] read = new byte[10000];
try {
resource.getInputStream().read(read, 0, read.length);
System.out.println(new String(read));
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行测试类,将会打印day01.xml的内容。注意资源文件的路径
[图片上传失败...(image-8c8c1e-1539135472409)]
先分析ClassPathResource的加载过程,再DefaultResourceLoader的加载过程
1. ClassPathResource对象创建过程
- 创建ClassPathResource对象
public ClassPathResource(String path, @Nullable ClassLoader classLoader) {
//规范路径
String pathToUse = StringUtils.cleanPath(path);
//如果路径以"/"开头,则截取开头"/"以后字符做为路径
if (pathToUse.startsWith("/")) {
pathToUse = pathToUse.substring(1);
}
//将处理后的路径赋给this.path
this.path = pathToUse;
//获取classLoader并赋给this.classLoader
this.classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
}
该构造函数比较简单,重点来看一下类加载器的获取过程。
- 获取类加载器
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
//优先获取线程上下文类加载器
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
// 获取当前类的类加载器
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
//获取SystemClassLoader
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
}
类加载器简介:
- bootstrap class loader:主要负责main方法启动的时候,加载JAVA_HOME/lib下的jar包
- extension class loader:主要负责加载JAVA_HOME/ext/lib下的jar包
- system class loader:主要负责加载classpath下的jar包或者类
2.使用ClassPathResource获取InputStream
创建了ClassPathResource对象实例之后,就可以使用该对象来获取InputStream。
public InputStream getInputStream() throws IOException {
InputStream is;
// ①如果类对象不为null,则使用类对象信息的getResourceAsStream获取输入流
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
// ②如果类加载器不为null,则使用类加载器的getResourceAsStream获取输入流
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
// ③否则使用ClassLoader类的getSystemResourceAsStream方法获取输入流
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
//以上三种方法都无法获取到输入流的话,那么说明文件不存在,抛出异常
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
Spring对InputStream的获取过程进行了分情况处理
- ①如果类对象不为null,则使用类对象信息的getResourceAsStream获取输入流
- ②如果类加载器不为null,则使用类加载器的getResourceAsStream获取输入流
- ③否则使用ClassLoader类的getSystemResourceAsStream方法获取输入流
- 使用类对象信息的getResourceAsStream获取输入流
public InputStream getResourceAsStream(String name) {
// 解析资源文件名称
// 如果名称不是绝对的,则添加包名称前缀。如果名称是绝对的,则删除前面的“/”
// 例如:相对路径:解析前name=day01.xml;解析后name=com/lyc/cn/v2/day03/day01.xml
// 绝对路径:解析前name=/day01.xml;解析后name=day01.xml
name = resolveName(name);
// 获取类加载器并返回InputStream
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
- 使用类加载器的getResourceAsStream获取输入流
public InputStream getResourceAsStream(String name) {
// 将资源文件路径转换为URL统一资源定位符
URL url = getResource(name);
try {
if (url == null) {
return null;
}
URLConnection urlc = url.openConnection();
// 获取InputStream
InputStream is = urlc.getInputStream();
// 判断URLConnection类型并根据其类型加入到closeables对象中
// closeables对象通过WeakHashMap维护了Closeable对象信息
if (urlc instanceof JarURLConnection) {
JarURLConnection juc = (JarURLConnection)urlc;
JarFile jar = juc.getJarFile();
synchronized (closeables) {
if (!closeables.containsKey(jar)) {
closeables.put(jar, null);
}
}
} else if (urlc instanceof sun.net.www.protocol.file.FileURLConnection) {
synchronized (closeables) {
closeables.put(is, null);
}
}
return is;
} catch (IOException e) {
return null;
}
}
- 使用ClassLoader类的getSystemResourceAsStream方法获取输入流
public static InputStream getSystemResourceAsStream(String name) {
// 将资源文件路径转换为URL统一资源定位符
URL url = getSystemResource(name);
try {
// 判断URL对象是否为空,并返回InputStream
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
DefaultResourceLoader的加载过程
3.创建DefaultResourceLoader对象
该过程主要就是获取ClassLoader对象,与上面的分析是相同的。
public DefaultResourceLoader() {
this.classLoader = ClassUtils.getDefaultClassLoader();
}
public static ClassLoader getDefaultClassLoader() {
ClassLoader cl = null;
try {
//优先获取线程上下文类加载器
cl = Thread.currentThread().getContextClassLoader();
}
catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
// 获取当前类的类加载器
cl = ClassUtils.class.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
//获取SystemClassLoader
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
}
4.使用DefaultResourceLoader对象获取Resource
public Resource getResource(String location) {
// ①优先遍历协议解决器集,如果可以解决,则返回位置相应的资源
for (ProtocolResolver protocolResolver : this.protocolResolvers) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// ②如果资源位置以"/"开头,则获取路径资源
if (location.startsWith("/")) {
return getResourceByPath(location);
}
// ③如果资源位置以"classpath:"开头,创建路径位置的的类路径资源ClassPathResource
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
// ④尝试将路径转换为URL资源
URL url = new URL(location);
return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// No URL -> resolve as resource path.
// ⑤没有成功转换为URL资源,则将location视为资源路径并返回对应解析资源
return getResourceByPath(location);
}
}
}
该过程涉及到的步骤比较多,但是资源文件加载并不是我们分析IoC容器的重点,我们只分析自定义解析协议,其他的不再赘述,感兴趣的同学可以自己debug跟踪下代码。
5. 自定义协议解析器
- MyProtocolResolver
package com.lyc.cn.v2.day03;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
/**
* @author: LiYanChao
* @create: 2018-10-08 16:42
*/
public class MyProtocolResolver implements ProtocolResolver {
@Override
public Resource resolve(String location, ResourceLoader resourceLoader) {
if (location.startsWith("my")) {
return resourceLoader.getResource(location.replace("my", "classpath"));
}
return null;
}
}
继承并实现ProtocolResolver接口的方法,将自定义的协议前缀转换为classpath即可
- 使用方法
@Test
public void test5() {
// 使用自定义协议解析器加载
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
resourceLoader.addProtocolResolver(new MyProtocolResolver());
Resource resource = resourceLoader.getResource("my:/v2/day01.xml");
print(resource);
}
本篇就分析到这里了,大家只需要了解一下Spring是如何加载资源即可,如不感兴趣则无需做过深入的了解,毕竟这不是IoC容器的重点。