[TOC]
## 简述
> 自己写了一篇Springboot的启动流程,然后我发现还有http://blog.csdn.net/hengyunabc/article/details/50120001 这一篇写的比较好,我就继续的把加载里的细节给描述清楚。
### 如何读取到资源文件?
我们从上一篇《SpringBoot应用启动流程》这一篇知道了LaunchedURLClassLoader这个类,但是没有具体说。LaunchedURLClassLoader继承了URLClassLoader,然后URLCLassLoader又继承了SecureClassLoader,最后SecureClassLoader继承ClassLoader。LaunchedURLClassLoader其实 就是通过Url的方式来加载类。
在 LaunchedURLClassLoader被执行前需要静态块里的方法,这是使用java7的并行加载机制,所以要在静态方法里将ClassLoader注册为可并行加载。
```
static {
performParallelCapableRegistration();
} @UsesJava7
private static void performParallelCapableRegistration() {
try {
ClassLoader.registerAsParallelCapable();
}
catch (NoSuchMethodError ex) {
// Running on Java 6. Continue.
}
}```
我们重新把思路整理一下,SpringBoot在启动的时候构造LaunchedURLClassLoader,这个类要求是执行并行加载,然后LaunchedURLClassLoader构造函数通过拿到的URL数组(该数组就是Lib底下的jar路径),来加载Jar包。上面的继承关系我们也知道了,在 LaunchedURLClassLoader重写了URLClassLoader的 findResource方法 findResources方法,这两个方法,代码如下:
```
//LaunchedURLClassLoader重写的方法
@Override
public URL findResource(String name) {
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResource(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
@Override
public Enumeration<URL> findResources(String name) throws IOException {
Handler.setUseFastConnectionExceptions(true);
try {
return super.findResources(name);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}```
从LaunchedURLClassLoader重写查找资源的方法上只是添加的异常处理,具体还是调用的是URLClassLoader查找资源的方法。然后就是读取资源。
```
public URL findResource(final String name) {
/*
* The same restriction to finding classes applies to resources
*/
URL url = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
return ucp.findResource(name, true);
}
}, acc);
return url != null ? ucp.checkURL(url) : null;
} public Enumeration<URL> findResources(final String name)
throws IOException
{
final Enumeration<URL> e = ucp.findResources(name, true);
return new Enumeration<URL>() {
private URL url = null;
private boolean next() {
if (url != null) {
return true;
}
do {
URL u = AccessController.doPrivileged(
new PrivilegedAction<URL>() {
public URL run() {
if (!e.hasMoreElements())
return null;
return e.nextElement();
}
}, acc);
if (u == null)
break;
url = ucp.checkURL(u);
} while (url == null);
return url != null;
}
public URL nextElement() {
if (!next()) {
throw new NoSuchElementException();
}
URL u = url;
url = null;
return u;
}
public boolean hasMoreElements() {
return next();
}
};
} public InputStream getResourceAsStream(String name) {
URL url = getResource(name);
try {
if (url == null) {
return null;
}
URLConnection urlc = url.openConnection();
InputStream is = urlc.getInputStream();
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;
}
}```
### 资源获取总结
资源的获取,就是使用的是URLClassLoader的查找资源与读取资源的方法。
### 对于一个URL,JDK或者ClassLoader如何知道怎么读取到里面的内容的?
实际上流程是这样子的:
* LaunchedURLClassLoader.loadClass
* URL.getContent()
* URL.openConnection()
* Handler.openConnection(URL)
最终调用的是JarURLConnection的getInputStream()函数。
```
public InputStream getInputStream() throws IOException {
if (this.jarFile == null) {
throw FILE_NOT_FOUND_EXCEPTION;
}
if (this.jarEntryName.isEmpty()
&& this.jarFile.getType() == JarFile.JarFileType.DIRECT) {
throw new IOException("no entry name specified");
}
connect();
InputStream inputStream = (this.jarEntryName.isEmpty()
? this.jarFile.getData().getInputStream(ResourceAccess.ONCE)
: this.jarFile.getInputStream(this.jarEntry));
if (inputStream == null) {
throwFileNotFound(this.jarEntryName, this.jarFile);
}
return inputStream;
}```
从一个URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:
* spring boot注册了一个Handler来处理”jar:”这种协议的URL
* spring boot扩展了JarFile和JarURLConnection,内部处理jar in jar的情况
* 在处理多重jar in jar的URL时,spring boot会循环处理,并缓存已经加载到的JarFile
* 对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考JarFileArchive里的代码
* 在获取URL的InputStream时,最终获取到的是JarFile里的JarEntryData
### URLClassLoader是如何getResource的呢?
URLClassLoader在构造时,有URL[]数组参数,它内部会用这个数组来构造一个URLClassPath:
```
/* The search path for classes and resources */
private final URLClassPath ucp; public URLClassLoader(URL[] urls, ClassLoader parent) {
super(parent);
// this is to make the stack depth consistent with 1.1
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
ucp = new URLClassPath(urls); //构造成一个URLClassPath
this.acc = AccessController.getContext();
}
```
在 URLClassPath 内部会为这些URLS 都构造一个Loader,然后在getResource时,会从这些Loader里一个个去尝试获取。
如果获取成功的话,就像下面那样包装为一个Resource。
如果获取成功的话,就像下面那样包装为一个Resource。
```
Resource getResource(final String name, boolean check) {
final URL url;
try {
url = new URL(base, ParseUtil.encodePath(name, false));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("name");
}
final URLConnection uc;
try {
if (check) {
URLClassPath.check(url);
}
uc = url.openConnection();
InputStream in = uc.getInputStream();
if (uc instanceof JarURLConnection) {
/* Need to remember the jar file so it can be closed
* in a hurry.
*/
JarURLConnection juc = (JarURLConnection)uc;
jarfile = JarLoader.checkJar(juc.getJarFile());
}
} catch (Exception e) {
return null;
}
return new Resource() {
public String getName() { return name; }
public URL getURL() { return url; }
public URL getCodeSourceURL() { return base; }
public InputStream getInputStream() throws IOException {
return uc.getInputStream();
}
public int getContentLength() throws IOException {
return uc.getContentLength();
}
};
}
```
从代码里可以看到,实际上是调用了url.openConnection()。这样完整的链条就可以连接起来了。
注意,URLClassPath这个类的代码在JDK里没有自带,在这里看到 http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/misc/URLClassPath.java#506
知识碎片,重在整理,路很长,一步一个脚印,就好。