## 简述
> 自己写了一篇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
