Тщательно проанализируйте исполняемый принцип SpringBoot jar

Spring Boot

Статья длинная, но в ней от начала и до конца описан принцип исполняемого jar-пакета SpringBoot, терпеливо смотрите. В то же время статья основана наSpringBoot-2.1.3анализ. Задействованные точки знаний в основном включаютЖизненный цикл Maven и настраиваемые плагины, JDK предоставляет классы инструментов для пакетов jar и способы расширения Springboot, и, наконец, настраиваемые загрузчики классов..

spring-boot-maven-plugin

Исполняемый jar-пакет SpringBoot также известен какfat jar,ДаОн включает в себя все сторонние зависимые банки, Пакет JAR встраивает все зависимости, кроме виртуальной машины Java, которая является All-In-One JAR Package. Общий плагинmaven-jar-pluginсгенерированный пакет иspring-boot-maven-pluginПрямая разница между сгенерированными пакетамиfat jarВ основном добавлены две части, первая частькаталог библиотеки, в котором хранятся файлы пакетов jar, от которых зависит Maven. Вторая частьspring boot loaderродственные классы.

fat jar 目录结构
├─BOOT-INF
│  ├─classes
│  └─lib
├─META-INF
│  ├─maven
│  ├─app.properties
│  ├─MANIFEST.MF      
└─org
    └─springframework
        └─boot
            └─loader
                ├─archive
                ├─data
                ├─jar
                └─util

Что вы хотите знатьfat jarКак это сгенерируется, нужно знатьspring-boot-maven-pluginрабочий механизм иspring-boot-maven-pluginЭто настраиваемый плагин, поэтому мы должны знать,Как работают пользовательские плагины Maven

Пользовательский плагин Maven

Maven имеет три независимых жизненных цикла:clean,defaultиsite, а каждый жизненный цикл содержит некоторыеphaseСтадии, стадии последовательны, а более поздние стадии зависят от более ранних стадий. стадия жизненного циклаphaseЦели с плагинамиgoalСвяжите друг друга, чтобы выполнить фактическую задачу сборки.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

repackageВ соответствии с целью будет выполненоorg.springframework.boot.maven.RepackageMojo#execute, основная логика этого метода заключается в вызовеorg.springframework.boot.maven.RepackageMojo#repackage

private void repackage() throws MojoExecutionException {
     //获取使用maven-jar-plugin生成的jar,最终的命名将加上.orignal后缀
   Artifact source = getSourceArtifact();
    //最终文件,即Fat jar
   File target = getTargetFile();
    //获取重新打包器,将重新打包成可执行jar文件
   Repackager repackager = getRepackager(source.getFile());
    //查找并过滤项目运行时依赖的jar
   Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
         getFilters(getAdditionalFilters()));
    //将artifacts转换成libraries
   Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
         getLog());
   try {
       //提供Spring Boot启动脚本
      LaunchScript launchScript = getLaunchScript();
       //执行重新打包逻辑,生成最后fat jar
      repackager.repackage(target, libraries, launchScript);
   }
   catch (IOException ex) {
      throw new MojoExecutionException(ex.getMessage(), ex);
   }
    //将source更新成 xxx.jar.orignal文件
   updateArtifact(source, target, repackager.getBackupFile());
}

давайте позаботимсяorg.springframework.boot.maven.RepackageMojo#getRepackagerзнаю этот методRepackagerКак он генерируется, можно примерно предположить внутреннюю логику упаковки.

private Repackager getRepackager(File source) {
   Repackager repackager = new Repackager(source, this.layoutFactory);
   repackager.addMainClassTimeoutWarningListener(
         new LoggingMainClassTimeoutWarningListener());
    //设置main class的名称,如果不指定的话则会查找第一个包含main方法的类,repacke最后将会设置org.springframework.boot.loader.JarLauncher
   repackager.setMainClass(this.mainClass);
   if (this.layout != null) {
      getLog().info("Layout: " + this.layout);
       //重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
      repackager.setLayout(this.layout.layout());
   }
   return repackager;
}
/**
 * Executable JAR layout.
 */
public static class Jar implements RepackagingLayout {
   @Override
   public String getLauncherClassName() {
      return "org.springframework.boot.loader.JarLauncher";
   }
   @Override
   public String getLibraryDestination(String libraryName, LibraryScope scope) {
      return "BOOT-INF/lib/";
   }
   @Override
   public String getClassesLocation() {
      return "";
   }
   @Override
   public String getRepackagedClassesLocation() {
      return "BOOT-INF/classes/";
   }
   @Override
   public boolean isExecutable() {
      return true;
   }
}

layoutМы можем перевести его в макет файла или макет каталога, код ясен и ясен, и нам нужно обратить внимание, что также является следующим фокусом внимания.org.springframework.boot.loader.JarLauncher, исходя из имени, которое, скорее всего, вернет исполняемый файлjarКласс запуска для файла.

Содержимое файла MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: oneday-auth-server
Implementation-Version: 1.0.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: oneday
Implementation-Vendor-Id: com.oneday
Spring-Boot-Version: 2.1.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.oneday.auth.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_171

repackagerСгенерированный файл MANIFEST.MF представляет собой приведенную выше информацию, вы можете увидеть две ключевые сведения.Main-ClassиStart-Class. Мы можем пойти дальше, запись запуска программы не определена в нашем SpringBootmain, ноJarLauncher#main, а затем используйте отражение для вызова определенногоStart-Classизmainметод

JarLauncher

Знакомство с ключевыми классами
  • java.util.jar.JarFileЧтение предоставлено классами инструментов JDKjarдокумент
  • org.springframework.boot.loader.jar.JarFileСпрингбот-погрузчик наследует от JDKJarFileсвоего рода
  • java.util.jar.JarEntryЗапись файла ``jar``, предоставляемая классом инструментов DK.
  • org.springframework.boot.loader.jar.JarEntrySpringboot-loader наследует предоставленный JDKJarEntryсвоего рода
  • org.springframework.boot.loader.archive.ArchiveУнифицированный уровень ресурсов доступа, абстрагированный Springboot
    • JarFileArchiveАбстракция файлов пакетов jar
    • ExplodedArchiveКаталог файлов

Вот бы описатьJarFileроль, каждыйJarFileArchiveбудет соответствовать одномуJarFile. Во время строительства внутренняя структура будет разобрана для полученияjarкаждый в сумкедокументилипапкасвоего рода. Мы можем взглянуть на аннотации для этого класса.

/* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
* offers the following additional functionality.
* <ul>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
* on any directory entry.</li>
* <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
* embedded JAR files (as long as their entry is not compressed).</li>
**/ </ul>

jarРазделитель ресурсов в!/, представленный в JDKJarFileURL-адрес поддерживает только один '!/', а Spring boot расширяет этот протокол для поддержки нескольких '!/', которые могут представлять jar в jar, jar в каталоге и толстые ресурсы jar.

Пользовательский механизм загрузки классов
  • Самый простой: Bootstrap ClassLoader (загружает классы в каталог /lib JDK)
  • ​ Sub-basic: Extension ClassLoader (загружает классы в каталог /lib/ext JDK)
  • Обычный: Application ClassLoader (классы в собственном пути к классам программы)

Первое вниманиеМеханизм родительского делегированияОчень важным моментом является то, что если класс может быть делегирован для загрузки с помощью самого простого ClassLoader, он не может быть загружен с помощью ClassLoader более высокого уровня, который должен ввести не-JDK-класс с тем же именем класса для неправильной области. Во-вторых, если в рамках этого механизма за счетfat jarРазличные третьи стороны, на которые полагались вjarфайл, а не в самой программеclasspathdown, то есть если мы используем механизм родительского делегирования,Мы не можем получить пакет jar, от которого мы вообще зависим, поэтому нам нужно изменить метод поиска классов в родительском механизме делегирования и настроить механизм загрузки классов..

Сначала краткое введение в Springboot2.LaunchedURLClassLoader, класс наследуетjava.net.URLClassLoader, переписаноjava.lang.ClassLoader#loadClass(java.lang.String, boolean), а затем мы обсуждаем, как он модифицирует механизм родительского делегирования.

Выше мы упоминали, что Spring boot поддерживает несколько '!/' для представления нескольких jar-файлов, и наша проблема заключается в том, как найти эти несколько jar-пакетов. Давайте взглянемLaunchedURLClassLoaderметод построения.

public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
   super(urls, parent);
}

urlsПримечания объясняютthe URLs from which to load classes and resources, то есть все классы и ресурсы, от которых зависит пакет fat jar,urlsПараметры, переданные в родительский классjava.net.URLClassLoader, по родительскому классуjava.net.URLClassLoader#findClassВыполните метод класса поиска, источником поиска класса является параметр urls, переданный конструктором.

//LaunchedURLClassLoader的实现
protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException {
   Handler.setUseFastConnectionExceptions(true);
   try {
      try {
          //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联	           //的package关联起来
         definePackageIfNecessary(name);
      }
      catch (IllegalArgumentException ex) {
         // Tolerate race condition due to being parallel capable
         if (getPackage(name) == null) {
            // This should never happen as the IllegalArgumentException indicates
            // that the package has already been defined and, therefore,
            // getPackage(name) should not return null.
             
            //这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
            throw new AssertionError("Package " + name + " has already been "
                  + "defined but it could not be found");
         }
      }
      return super.loadClass(name, resolve);
   }
   finally {
      Handler.setUseFastConnectionExceptions(false);
   }
}

методsuper.loadClass(name, resolve)на самом деле вернетсяjava.lang.ClassLoader#loadClass(java.lang.String, boolean), следуйте механизму родительского делегирования для классов поиска иBootstrap ClassLoaderиExtension ClassLoaderне найдет классов, от которых зависит толстая банка, и в конце концов придет кApplication ClassLoader,перечислитьjava.net.URLClassLoader#findClass

Как на самом деле начать

Самая большая разница между Springboot2 и Springboot1 заключается в том, что Springboo1 запускает новый поток для выполнения соответствующегологика вызова отражения, в то время как SpringBoot2 удаляет этап создания нового потока. путьorg.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)Логика размышлений вызывает относительно проста, поэтому мы не проанализируем ее здесь. Основная точка в том, что при вызовеmainПеред методом настроен загрузчик класса контекста текущего потока.LaunchedURLClassLoader

protected void launch(String[] args, String mainClass, ClassLoader classLoader)
      throws Exception {
   Thread.currentThread().setContextClassLoader(classLoader);
   createMainMethodRunner(mainClass, args, classLoader).run();
}

Demo

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
        JarFile.registerUrlProtocolHandler();
// 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理
        LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
                new URL[] {
                        new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")
                        , new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")
                },
                Application.class.getClassLoader());
// 加载类
// 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)
        classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
        classLoader.loadClass("org.springframework.boot.SpringApplication");
// 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出
   classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");

//        SpringApplication.run(Application.class, args);
    }
       <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-loader</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.1.3.RELEASE</version>

        </dependency>

Суммировать

​ Для анализа исходного кода самым большим преимуществом на этот раз является то, что я не могу сразу же понять логику каждого шага кода в исходном коде, даже если я знаю функцию метода. Что нам нужно понять, так это ключевой код и задействованные точки знаний. Я начал отслеживать с пользовательского плагина Maven и закрепил свои знания о Maven.В процессе я даже узнал, что JDK предоставляет соответствующие классы инструментов для чтения jar-файлов. Последний и самый важный пункт знаний — это пользовательский загрузчик классов. Весь код предназначен не для того, чтобы сказать, насколько он хорош, а для того, чтобы понять, почему он хорош.

​ Автор: пожалуйста, называйте меня красным шарфом

Источник:Наггетс.Личное сообщение/пост/5 из 15 из 4…

Этот блог приветствуется для перепечатки, но это заявление должно быть сохранено без согласия автора, а ссылка на исходный текст дана в явном месте на странице статьи, в противном случае сохраняется право привлечения к юридической ответственности. Кодировать слова непросто, ваши лайки — самая большая мотивация для моего письма.

Использованная литература:Анализ исполняемого файла SpringBoot анализа исходного кода SpringBoot