SpringBoot的jar包如何啟動


SpringBoot的jar包如何啟動

一、簡介

​ 使用過SprongBoot打過jar包的都應該知道,目標文件一般都會生成兩個文件,一個是以.jar的包,一個是.jar.original文件。那么使用SpringBoot會打出兩個包,而.jar.original的作用是什么呢?還有就是java -jar是如何將一個SpringBoot項目啟動,之間都進行了那些的操作?

​ 其實.jar.originalmavenSpringBoot重新打包之前的原始jar包,內部只包含了項目的用戶類,不包含其他的依賴jar包,生成之后,SpringBoot重新打包之后,最后生成.jar包,內部包含了原始jar包以及其他的引用依賴。以下提及的jar包都是SpringBoot二次加工打的包。

二、jar包的內部結構

SpringBoot打出的jar包,可以直接通過解壓的方式查看內部的構造。一般情況下有三個目錄。

  • BOOT-INF:這個文件夾下有兩個文件夾classes用來存放用戶類,也就是原始jar.original里的類;還有一個是lib,就是這個原始jar.original引用的依賴。
  • META-INF:這里是通過java -jar啟動的入口信息,記錄了入口類的位置等信息。
  • org:Springboot loader的代碼,通過它來啟動。

這里主要介紹一下/BOOT-INF/MANIFEST.MF文件

Manifest-Version: 1.0
Implementation-Title: springboot-server
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: Administrator
Implementation-Vendor-Id: cn.com.springboot
Spring-Boot-Version: 1.5.13.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: cn.com.springboot.center.AuthEenterBootstrap
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_241
Implementation-URL: http://projects.spring.io/spring-boot/auth-server/

Main-Class:記錄了java -jar的啟動入口,當使用該命令啟動時就會調用這個入口類的main方法,顯然可以看出,Springboot轉移了啟動的入口,不是用戶編寫的xxx.xxx.BootStrap的那個入口類。

Start-Class:記錄了用戶編寫的xxx.xxx.BootStrap的那個入口類,當內嵌的jar包加載完成之后,會使用LaunchedURLClassLoader線程加載類來加載這個用戶編寫的入口類。

三、加載過程

1.使用到的一些類

3.1.1 Archive

​ 歸檔文件接口,實現迭代器接口,它有兩個子類,一個是JarFileArchivejar包文件使用,提供了返回這個jar文件對應的url、或者這個jar文件的MANIFEST文件數據信息等操作。是ExplodedArchive是文件目錄的使用也有獲取這個目錄url的方法,以及獲取這個目錄下的所有Archive文件方法。

3.1.2 Launcher

​ 啟動程序的基類,這邊最后是通過JarLauncher#main()來啟動。ExecutableArchiveLauncher是抽象類,提供了獲取Start-Class類路徑的方法,以及是否還有內嵌對應文件的判斷方法和獲取到內嵌對應文件集合的后置處理方法的抽象,由子類JarLauncherWarLauncher自行實現。

3.1.3 Spring.loader下的JarFile和JarEntry

jarFile繼承於jar.util.jar.JarFileJarEntry繼承於java.util.jar.JarEntry,對原始的一些方法進行重寫覆蓋。每一個JarFileArchive都擁有一個JarFile方法,用於存儲這個jar包對應的文件,而每一個JarFile都有一個JarFileEntries,JarFileEntries是一個迭代器。總的來說,在解析jar包時,會將jar包內的文件封裝成JarEntry對象后由JarFile對象保存文件列表的迭代器。所以JarFileArchiveJarFileEntries之間是通過JarFile連接,二者都可以獲取到JarFile對象。

2.過程分析

MANIFEST.MF文件中的Main-class指向入口開始。

創建JarLauncher並且通過它的launch()方法開始加載jar包內部信息。

public static void main(String[] args) throws Exception {
  new JarLauncher().launch(args);
}

JarLauncher的空構造方法時一個空實現,剛開始看的時候還懵了一下,以為是在后續的操作中去加載的文件,其實不然,在創建時由父類ExecutableArchiveLauncher的構造方法去加載的文件。

public ExecutableArchiveLauncher() {
  try {
    // 加載為歸檔文件對象
    this.archive = createArchive();
  }
  catch (Exception ex) {
    throw new IllegalStateException(ex);
  }
}


/**
	 * 具體的加載方法
	 */
protected final Archive createArchive() throws Exception {
  ProtectionDomain protectionDomain = getClass().getProtectionDomain();
  CodeSource codeSource = protectionDomain.getCodeSource();
  URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
  String path = (location == null ? null : location.getSchemeSpecificPart());
  if (path == null) {
    throw new IllegalStateException("Unable to determine code source archive");
  }
  File root = new File(path);
  if (!root.exists()) {
    throw new IllegalStateException(
      "Unable to determine code source archive from " + root);
  }
  // 判斷路徑是否是一個文件夾,是則返回ExplodedArchive對象,否則返回JarFileArchive
  return (root.isDirectory() ? new ExplodedArchive(root): new JarFileArchive(root));
}

// ============== JarFileArchive =============

public class JarFileArchive implements Archive {

  public JarFileArchive(File file) throws IOException {
    this(file, null);
  }

  public JarFileArchive(File file, URL url) throws IOException {
    // 通過這個new方法創建JarFile對象
    this(new JarFile(file));
    this.url = url;
  }
}

// ============== JarFile =============

public class JarFile extends java.util.jar.JarFile {

  public JarFile(File file) throws IOException {
    // 通過RandomAccessDataFile讀取文件信息
    this(new RandomAccessDataFile(file));
  }
}

jarLauncher#launch()方法:

protected void launch(String[] args) throws Exception {
  // 注冊URL協議的處理器,沒有指定時,默認指向org.springframework.boot.loader包路徑
  JarFile.registerUrlProtocolHandler();
  // 獲取類路徑下的歸檔文件Archive並通過這些歸檔文件的URL,創建線程上下文類加載器LaunchedURLClassLoader
  ClassLoader classLoader = createClassLoader(getClassPathArchives());
  // 使用類加載器和用戶編寫的啟動入口類,通過反射調用它的main方法。
  launch(args, getMainClass(), classLoader);
}

JarLaunchergetClassPathArchives()是在ExecutableArchiveLauncher中實現:

@Override
protected List<Archive> getClassPathArchives() throws Exception {
  List<Archive> archives = new ArrayList<Archive>(
    // 獲取歸檔文件中滿足EntryFilterg過濾器的項,isNestedArchive()方法由具體
    // 的之類實現。
    this.archive.getNestedArchives(new EntryFilter() {

      @Override
      public boolean matches(Entry entry) {
        return isNestedArchive(entry);
      }

    }));
  // 獲取到當前歸檔文件下的所有子歸檔文件之后的后置操作,是一個擴展點。在JarLauncher
  // 中是一個空實現。
  postProcessClassPathArchives(archives);
  return archives;
}

/**
 * JarLauncher的具體實現,這里通過判斷是否在BOOT-INF/lib/包下返回true
 * 也就是說只會把jar包下的BOOT-INF/lib/下的文件加載為Archive對象
 */
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
  if (entry.isDirectory()) {
    return entry.getName().equals(BOOT_INF_CLASSES);
  }
  return entry.getName().startsWith(BOOT_INF_LIB);
}

JarFileArchivegetNestedArchives方法

@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
	List<Archive> nestedArchives = new ArrayList<Archive>();
		for (Entry entry : this) {
		  // 若匹配器匹配獲取內嵌歸檔文件
			if (filter.matches(entry)) {
				nestedArchives.add(getNestedArchive(entry));
			}
		}
	return Collections.unmodifiableList(nestedArchives);
}

protected Archive getNestedArchive(Entry entry) throws IOException {
	JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
	if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
		return getUnpackedNestedArchive(jarEntry);
	}
	try {
		// 根據具體的Entry對象,創建JarFile對象
		JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
		// 封裝成歸檔文件對象后返回
		return new JarFileArchive(jarFile);
	}
	catch (Exception ex) {
		throw new IllegalStateException("Failed to get nested  entry"+entry.getName(),ex);
	}
}

public synchronized JarFile getNestedJarFile(final ZipEntry entry)throws IOException {
	return getNestedJarFile((JarEntry) entry);
}

public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
	try {
		 // 根據具體的Entry對象,創建JarFile對象
		 return createJarFileFromEntry(entry);
	}
	catch (Exception ex) {
		throw new IOException( "Unable to open nested jar file'"+entry.getName()+"'",ex);
	}
}

private JarFile createJarFileFromEntry(JarEntry entry) throws IOException {
	if (entry.isDirectory()) {
		return createJarFileFromDirectoryEntry(entry);
	}
	return createJarFileFromFileEntry(entry);
}

private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
	if (entry.getMethod() != ZipEntry.STORED) {
	  throw new IllegalStateException("Unable to open nested entry '"
	  + entry.getName() + "'. It has been compressed and nested "
	  + "jar files must be stored without compression. Please check the "
	   + "mechanism used to create your executable jar file");
	}
	// 獲取到參數entry對應的RandomAccessData對象
	RandomAccessData entryData = this.entries.getEntryData(entry.getName());
	// 這里根據springboot擴展的url協議,在父路徑的基礎上添加!/來標記子包
	return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
	                   entryData, JarFileType.NESTED_JAR);
}

到這基本上讀取jar內部信息,加載為對應歸檔文件對象的大概過程已經講完了,接下來分析一下在獲取到了整個jar的歸檔文件對象后的處理。


/**
 * 通過歸檔文件對象列表,獲取對應的url信息,並通過url信息創建LaunchedURLClassLoader
 */
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
	List<URL> urls = new ArrayList<URL>(archives.size());
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	return createClassLoader(urls.toArray(new URL[urls.size()]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
	return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

獲取到對應的LaunchedUrlClassLoader類加載器之后,設置線程的上下文類加載器為該加載器。

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
	Thread.currentThread().setContextClassLoader(classLoader);
	// 根據MANIFI.MF文件中的start-classs信息創建項目啟動入口主類對象,並通過run方法啟動
	createMainMethodRunner(mainClass, args, classLoader).run();
}

// =========== MainMethodRunner ================
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	public MainMethodRunner(String mainClass, String[] args) {
		this.mainClassName = mainClass;
		this.args = (args == null ? null : args.clone());
	}

	public void run() throws Exception {
		Class<?> mainClass = Thread.currentThread().getContextClassLoader()
				.loadClass(this.mainClassName);
    // 通過反射調用啟動項目啟動類的main方法
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.invoke(null, new Object[] { this.args });
	}
}

最后來說一下這個LaunchedURLClassLoader,它繼承於URLClassLoader,並重寫了loadClass方法

LaunchedClassLoaderloadClass方法

@Override
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
   Handler.setUseFastConnectionExceptions(true);
   try {
      try {
         definePackageIfNecessary(name);
      }
      catch (IllegalArgumentException ex) {
         if (getPackage(name) == null) {
            throw new AssertionError("Package " + name + " has already been "
                  + "defined but it could not be found");
         }
      }
     // 調用父類loadClass方法,走正常委派流程,最終會被LaunchURLClassLoader
      return super.loadClass(name, resolve);
   }
   finally {
      Handler.setUseFastConnectionExceptions(false);
   }
}

// ============URLClassLoader =====================

protected Class<?> findClass(final String name)throws ClassNotFoundException
{
  final Class<?> result;
  try {
    result = AccessController.doPrivileged(
      new PrivilegedExceptionAction<Class<?>>() {
        public Class<?> run() throws ClassNotFoundException {
          // 根據name,將路徑轉化為以.class結尾的/分隔的格式。
          String path = name.replace('.', '/').concat(".class");
          // 通過UrlClassPath對象根據路徑獲取資源類文件
          Resource res = ucp.getResource(path, false);
          if (res != null) {
            try {
              return defineClass(name, res);
            } catch (IOException e) {
              throw new ClassNotFoundException(name, e);
            }
          } else {
            return null;
          }
        }
      }, acc);
  } catch (java.security.PrivilegedActionException pae) {
    throw (ClassNotFoundException) pae.getException();
  }
  if (result == null) {
    throw new ClassNotFoundException(name);
  }
  return result;
}

四、總結

Springboot主要實現了對URL加載方式進行了擴展,並且對一些對象ArchiveJarFileEntry等進行了抽象和擴展,最后使用LaunchedUrlClassLoader來進行處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM