【SpringBoot】服务 Jar 包的启动过程原理
1 前言
到现在我碰到的微服务,大多都是打的 Jar包,然后打镜像,推镜像,发布。当然也有 War 包的,但是还是比较少。我们这节主要看看 Jar包。 不知道大家有没有看过 SpringBoot 打好的 Jar 包的内容,以及它是如何启动的,这节我们就来看看。
2 Jar 包启动
2.1 单Java Jar 包启动
有关 java jar包的一些官方说明,我们都知道 Java 能直接执行 Jar 包的,java -jar xxx.jar,那么我们先看看一个最简单的 Jar 包。
(1)我这里有一个简单的类 JarTest如下:
public class JarTest { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println(i); } } }
编译后的 class:
然后我们新建个清单文件:META-INF/MANIFEST.MF,内容如下:
Main-Class: JarTest
然后把两者放到一个压缩包里,如下:
然后我们直接执行 java -jar test.jar 可以看到我们的程序执行了。
这个我们的启动类是在最外层,当我把它放到一个目录下里,再启动,也是可以的,这个是我又测试的一个:
那我们再试一个 jar 里边套 jar 的,因为我们的 SpringBoot 打包后是不是有很多的第三方依赖的 jar包,所以我们这里试试这样的场景再:
看结果貌似是不行,也就是没有被加载。那我们猜猜是不是要自定义一个类加载,加载像这种第三方依赖的 jar 的呢?那我们下边看看 SpringBoot 的启动。
另外,你可能会遇到这个报错:
其中的一个原因可能就是:你的manifest.mf文件的格式是否正确。确保每行都以换行符结尾,每个属性都以“属性名: 属性值”格式表示,并且每个属性之间用换行符分隔。我的就是因为有几次尝试的时候,没有换行一直报清单的错误......
2.2 SpringBoot Jar 包启动
2.2.1 SpringBoot Jar 结构
先看下我们服务平时打好的 Jar 包的内容:
大概分三块:
(1)BOOT-INF:classes 存放的是我们服务本身的代码编译后的 .class文件以及 resources 下的一些资源配置文件,lib存放的是我们服务以来的一些第三方 jar 包(依赖的越多 jar 包越大)
(2)META-INF:这个是 jar 包必备的,java 本身要求的
(3)org:这个里边存放的是一些 SpringBoot 启动需要的类 比如类加载器
至于为什么需要类加载器?像刚才上边的我们的例子,我们的 main 类就找不到。对于 Java 标准的 jar 文件来说,规定在一个 jar 文件中,我们必须要将指定 main.class 的类直接放置在文件的顶层目录中(也就是说,它不予许被嵌套),否则将无法加载,因此 Spring 要想启动加载,就需要自定义实现自己的类加载器去加载。
2.2.2 SpringBoot Jar 启动过程
那我们从哪里开始看起呢?是不是就从清单文件 MANIFEST.MF 的 Main-Class 开始看起,这个是 Jar 包的启动入口。接下来我们就从 JarLauncher 看起,先看看它的类图,其实 Java 就是类,哪哪都是类,你知道的类越多你越牛逼,你在知道类的基础上还能理清楚它的上下关系也就是类关系那你更牛逼,你能在理清楚上下关系的基础上用到自己的代码里学以致用加总结其实就 prefect 了,是不是呢?哈哈哈,继续看我们的 JarLauncher :
看这个图,是不是我们设计模式里典型的模板模式,我们这里主要看 Jar 的哈:
// JarLauncher public class JarLauncher extends ExecutableArchiveLauncher { ... // main 方法 public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } }
作为启动类,main 方法里很简单就一句,实例化然后调用 launch 方法。
在实例化的时候,首先会执行父类的实例化 ExecutableArchiveLauncher ,就会开始构建我们的文档类 Archive ,也就是我们的 jar里边都有哪些内容,都会封装进 Archive :
// ExecutableArchiveLauncher 看表面类名 可执行的文档启动器 public abstract class ExecutableArchiveLauncher extends Launcher { // 重要的属性文档 比我们的这里的 jar 启动 我们 jar 包的所有内容是不是都会包装到 Archive 类里 private final Archive archive; public ExecutableArchiveLauncher() { try { this.archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); } } ... } // Launcher 类的 createArchive // createArchive 方法就是根据当前的启动类的位置,来寻找和封装 Archive 我们的这里的 jar 就会是 JarFileArchive protected final Archive createArchive() throws Exception { // ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; String path = (location != null) ? location.getSchemeSpecificPart() : null; 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); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); }
那看到这里,我们知道的是 JarLauncher 实例化的时候会把我们的 jar 里的信息都包进了 Archive 这个类里,那我们这里简单了解下这个类。
2.2.2.1 Archive
Archive 即归档文件,这个概念在linux下比较常见,通常就是一个tar/zip格式的压缩包,jar 就是 zip 格式,SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层,关于Spring Boot中Archive的源码如下:
public interface Archive extends Iterable<Archive.Entry> { // 获取该归档的url URL getUrl() throws MalformedURLException; // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF Manifest getManifest() throws IOException; // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar List<Archive> getNestedArchives(EntryFilter filter) throws IOException; }
该接口有两个实现,分别是
- org.springframework.boot.loader.archive.ExplodedArchive
- org.springframework.boot.loader.archive.JarFileArchive。
前者用于在文件夹目录下寻找资源,后者用于在jar包环境下寻找资源。而在SpringBoot打包的 jar 中,则是使用后者JarFileArchive。
大家也可以打开 SpringBoot 的源码,就有一个专门的 JarLauncherTest 大家可以写一个测试方法,来用 JarFileArchive 打开一个平时我们的 jar包看看效果,这是我的:
可以看到对 jar包 的封装,每个JarFileArchive都会对应一个JarFile。JarFile被构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹,这些文件或文件夹会被封装到Entry中,也存储在JarFileArchive中。如果Entry是个jar,会解析成JarFileArchive。
比如一个JarFileArchive对应的URL为:
jar:file:/D:/JetBrains/yanjiu/spring-boot-2.1.8.RELEASE/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/demo-0.0.1-SNAPSHOT.jar!/
它对应的JarFile为:
D:\JetBrains\yanjiu\spring-boot-2.1.8.RELEASE\spring-boot-project\spring-boot-tools\spring-boot-loader\src\test\resources\demo-0.0.1-SNAPSHOT.jar
这个JarFile有很多Entry,就是我们 Jar包 里的所有内容:
JarFileArchive内部的一些依赖jar对应的URL(SpringBoot使用org.springframework.boot.loader.jar.Handler
处理器来处理这些URL),并且如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,那么会使用 !/ 分隔开,这种方式只有org.springframework.boot.loader.jar.Handler
能处理,它是SpringBoot内部扩展出来的一种URL协议。
构造JarFileArchive对象,获取其中所有的资源目标,取得其所有的Url,那我们继续回到启动过程:
// Launcher 类的 launch 方法 protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 创建类加载器 因为jar in jar的java 是不会加载的,所以这里创建自己的类加载器进行加载 ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 获取 start-class 执行其 main方法 也就是我们服务的 SpringApplication的main方法 launch(args, getMainClass(), classLoader); } // 创建类加载器 protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL[0])); } protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } // 启动 protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 将创建的类加载器放到线程上下文中 Thread.currentThread().setContextClassLoader(classLoader); createMainMethodRunner(mainClass, args, classLoader).run(); } // ExecutableArchiveLauncher 根据 jar 里的清单文件找 start-class @Override protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); } return mainClass; } // 封装 MainMethodRunner 对象执行 run方法 protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); } public class MainMethodRunner { private final String mainClassName; private final String[] args; /** * Create a new {@link MainMethodRunner} instance. * @param mainClass the main class * @param args incoming arguments */ public MainMethodRunner(String mainClass, String[] args) { this.mainClassName = mainClass; this.args = (args != null) ? args.clone() : null; } 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 }); } }
在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader
,并将其设置为线程上下文类加载器,启动应用。
至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。
3 小结
好啦,本节主要看下 SpringBoot 的 jar 大体的启动过程,我们可能还有一些小细节,比如 JarFileArchive 详细去收集每个目录下的文件的过程(可能是递归或者某种循环收集)以及 SpringBoot 的类加载器实际加载每个类的过程(类加载器加载过程)没去细看了哈,我们知道的是它把创建的类加载已经放到上下文中了,可以通过上下文中加载我们的类,本节就暂时了解到这里,有理解不对的地方欢迎指正哈。