SpringBoot打包后启动分析
在 SpringBoot项目打包结构 中的清单文件(META-INF 文件夹中的 MANIFEST.MF 文件)中定义 jar 包的入口类(Main-Class)指向了 org.springframework.boot.loader.JarLauncher 类。
该类在 org.springframework.boot:spring-boot-loader 包中,要查看需要额外引入,一般开发是通过 SpringBoot 插件生成的,没必要引入。
JarLauncher
继承 ExecutableArchiveLauncher 类
jar包 的 Launcher。此 Launcher 假设项目依赖的 jar包 放在 “/BOOT-INF/lib” 目录中,项目代码的文件放在 “/BOOT-INF/classes” 目录中。
通过 main 方法启动,内容只有一行:
1 new JarLauncher().launch(args);
1. 首先是创建 JarLauncher 示例,而 JarLauncher 的构造方法为空,默认调用父类的构造方法,将成员变量 archive 初始化:
1 public ExecutableArchiveLauncher() { 2 try { 3 this.archive = createArchive(); 4 } 5 catch (Exception ex) { 6 throw new IllegalStateException(ex); 7 } 8 }
createArchive() 方法定义在其父类 Launcher 中,创建并返回一个持有指向当前 jar包 的 JarFileArchive对象:
1 protected final Archive createArchive() throws Exception { 2 ProtectionDomain protectionDomain = getClass().getProtectionDomain(); 3 CodeSource codeSource = protectionDomain.getCodeSource(); 4 URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; 5 String path = (location != null) ? location.getSchemeSpecificPart() : null; 6 if (path == null) { 7 throw new IllegalStateException("Unable to determine code source archive"); 8 } 9 File root = new File(path); 10 if (!root.exists()) { 11 throw new IllegalStateException("Unable to determine code source archive from " + root); 12 } 13 return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); 14 }
- 2 ~ 5 行代码用于获取当前 jar文件 的绝对路径
- 创建指向当前 jar文件 的 File对象
- jar文件 非文件夹,传入上面的 File对象,创建并返回一个 JarFileArchive 对象
2. 创建了 JarLauncher对象 后,调用 launch() 方法(定义在 Launcher 类中)
1 protected void launch(String[] args) throws Exception { 2 JarFile.registerUrlProtocolHandler(); // 注册一个 “java.protocol.handler.pkgs” 属性 3 ClassLoader classLoader = createClassLoader(getClassPathArchives()); 4 launch(args, getMainClass(), classLoader); 5 }
- 启动程序的方法,此方法是初始化入口,必须在子类的 main 方法中调用
第2行跳过,分析第3行,首先是 getClassPathArchives() 方法,定义在 Launcher 类中,实现在 ExecutableArchiveLauncher 类中:
1 protected List<Archive> getClassPathArchives() throws Exception { 2 List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive)); 3 postProcessClassPathArchives(archives); 4 return archives; 5 }
- 首先是 getNestedArchives(EntryFilter) 方法,EntryFilter 接收一个 Archive.Entry 类型的参数,返回一个 boolean 值
- 然后是 this::isNestedArchive,在 JarLauncher/WarLauncher 中实现,结合类中的常量定义筛选的规则(在 jar包中,该规则为:名称为 “BOOT-INF/classes/” 的文件,或者名称以 “BOOT-INF/lib/” 开头的文件,参考 SpringBoot项目打包结构 ,也就是筛选出依赖包)
- getNestedArchives(EntryFilter) 在上面提到的 JarFileArchive 中实现,根据对应给定的过滤器,筛选出当前 jar文件 中相关的依赖包等内容,放到一个 list 中,并返回
- 第4行用于对 list 中的 Archive 进行处理,具体实现为空
- 返回 list
然后是 createClassLoader 方法:
1 protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { 2 List<URL> urls = new ArrayList<>(archives.size()); 3 for (Archive archive : archives) { 4 urls.add(archive.getUrl()); 5 } 6 return createClassLoader(urls.toArray(new URL[0])); 7 } 8 protected ClassLoader createClassLoader(URL[] urls) throws Exception { 9 return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); 10 }
- 将 List<Archive> 转换成一个 URL数组
- 传入 URL数组 和指定加载当前类的类加载器作为父加载器,通过 JDK 提供的 URLClassLoader 的构造方法,构造一个 LaunchedURLClassLoader ,并返回
最后是 launch 方法:
1 protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { 2 Thread.currentThread().setContextClassLoader(classLoader); 3 createMainMethodRunner(mainClass, args, classLoader).run(); 4 }
- 将线程上下文类加载器设置为 LaunchedURLClassLoader (为了打破双亲委托机制)
- 然后创建 MainMethodRunner 类,并调用其 run 方法,实际上就是通过反射去运行项目中自定义的启动方法
至此,启动工作完成!