## 簡述
> 自己寫了一篇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