前面介绍了Spring的一些基础知识和简单的用法,本篇开始分析Spring的IoC容器。BeanFactory是IoC容器的基础,所以接下来的分析都是基于BeanFactory的。
image.png
从图中看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; }
类加载器简介:
image.png
- 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容器的重点。