Углубленный анализ принципа запуска SpringBoot java-jar из командной строки

Spring Boot

При весенней загрузке очень привлекательной особенностью является то, что приложение может быть упаковано непосредственно в jar/war, а затем jar/war можно запустить напрямую без необходимости настройки веб-сервера. Итак, как начинается весенняя загрузка? Сегодня давайте рассмотрим, как это работает. Во-первых, давайте создадим базовый проект весенней загрузки, который поможет нам проанализировать.Эта версия весенней загрузки — 2.2.5.RELEASE.

// SpringBootDemo.java
@SpringBootApplication
public class SpringBootDemo {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemo.class);
    }

}

Вот зависимости pom:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<build>
    <finalName>springboot-demo</finalName>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

После создания проекта выполните команду упаковки maven, чтобы сгенерировать два файла jar:

springboot-demo.jar

springboot-demo.jar.original

Где springboot-demo.jar.original — это пакет, сгенерированный стандартным плагином maven-jar. springboot-demo.jar — это пакет jar, сгенерированный подключаемым модулем maven spring boot, который содержит зависимости приложений и классы, связанные с загрузкой spring. В дальнейшем именуемый исполняемой банкой или толстой банкой. Последний содержит только скомпилированные локальные ресурсы приложения, а первый вводит соответствующие сторонние зависимости, что также видно по размеру файла.

图1
фигура 1

Что касается исполняемой банки,официальная документация весенней загрузкиобъясняется таким образом.

Исполняемые jar-файлы (иногда называемые «толстыми jar-файлами») — это архивы, содержащие ваши скомпилированные классы вместе со всеми зависимостями jar-файлов, которые необходимы вашему коду для запуска.

Исполняемые банки (иногда называемые «толстыми банками») — это те, которые содержатСкомпилированные классы и все jar-зависимости, необходимые для запуска кода.архивный файл.

Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained within a jar). This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking.

Java не предоставляет стандартного способа загрузки вложенных файлов jar (т. е. файлов jar, которые сами содержатся внутри jar).. Проблемы могут возникнуть, если вам нужно распространять самодостаточное приложение, которое можно запустить из командной строки без распаковки.

Чтобы решить эту проблему, многие разработчики используют "затененные" jar-файлы. Затененный jar-файл упаковывает все классы из всех jar-файлов в один "убер-jar". Проблема с затененными jar-файлами заключается в том, что трудно увидеть, какие библиотеки на самом деле находятся в вашем Это также может быть проблематично, если одно и то же имя файла используется (но с разным содержимым) в нескольких банках.

Чтобы решить эту проблему, многие разработчики используют затененные банки. Затененная банка упаковывает все классы из всех банок в одну убер (супер) банку. Проблема с затененными jar-файлами заключается в том, что трудно увидеть, какие библиотеки на самом деле включены в приложение. Это также может создать проблемы, если одно и то же имя файла (но с разным содержимым) используется в нескольких банках.

Spring Boot takes a different approach and lets you actually nest jars directly.

Spring Boot использует другой подход и фактически позволяет напрямую вкладывать jar-файлы.

Проще говоря, в стандарте Java нет возможности загружать вложенные файлы jar, что является способом использования jar в jar.Для решения этой проблемы многие разработчики используют затененные jar-файлы, но этот метод будет иметь некоторые проблемы, и Spring Boot использует другой подход, чем затененные банки.

Структура исполняемого файла Jar

Итак, как именно реализована весенняя загрузка? Имея в виду этот вопрос, давайте сначала посмотрим на структуру каталогов загрузочного пакета spring (опуская неважное):

图6
Изображение 6

Можно обнаружить, что каталог файлов соответствует следующим спецификациям:

Application classes should be placed in a nested BOOT-INF/classes directory. Dependencies should be placed in a nested BOOT-INF/lib directory.

Классы приложений следует размещать во вложенной директории BOOT-INF/classes. Зависимости должны быть размещены во вложенной директории BOOT-INF/lib.

Мы обычно используем на сервереjava -jarкоманда для запуска нашего приложения, вОфициальная документация по JavaЭто описывается так:

Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.

Выполнять программы, инкапсулированные в файлы JAR. Аргумент имени файла — это имя JAR-файла с манифестом, содержащим строку вида Main-Class:classname, определяющую класс с помощью общедоступного метода static void main(String[] args), который выступает в качестве отправной точки для приложения. .

When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are ignored.

При использовании параметра -jar указанный файл JAR является источником для всех пользовательских классов, а другие параметры пути к классам игнорируются.

Проще говоря, конкретный класс запуска, управляемый командой java -jar, должен быть настроен в атрибуте основного класса файла манифеста MANIFEST.MF.Эта команда используется для управления стандартным исполняемым файлом jar и считывает основной класс файла Файл MANIFEST.MF.-Class Значение атрибута, Main-Class, то есть класс, определяющий класс, содержащий основной метод, представляет класс входа для выполнения приложения.

Затем вернитесь и просмотрите ранее упакованный и распакованный каталог файлов, найдите файл /META-INF/MANIFEST.MF и просмотрите метаданные:

Manifest-Version: 1.0 Implementation-Title: spring-boot-demo Implementation-Version: 1.0-SNAPSHOT Start-Class: com.example.spring.boot.demo.SpringBootDemo Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.5.RELEASE Created-By: Maven Archiver 3.4.0 Main-Class: org.springframework.boot.loader.JarLauncher

Вы можете видеть, что основной классorg.springframework.boot.loader.JarLauncher, указывающий, что стартовая запись проекта — это не наш собственный определенный класс запуска, а JarLauncher. Наш собственный класс загрузки проекта, com.example.spring.boot.demo.SpringBootDemo, определен в свойстве Start-Class, которое не является стандартным свойством файла MANIFEST.MF Java.

Процесс упаковки spring-boot-maven-plugin

Мы не добавляли зависимости этих классов в org.springframework.boot.loader, так как же они упакованы в FatJar? Здесь должен быть упомянут рабочий механизм плагина spring-boot-maven-plugin. Для каждого нового проекта весенней загрузки вы можете увидеть следующие плагины в его файле pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Это официальный подключаемый модуль, предоставляемый SpringBoot для упаковки FatJar. Классы в org.springframework.boot.loader фактически вводятся через этот подключаемый модуль;

Когда мы выполним команду пакета, мы увидим следующий журнал:

[INFO] --- spring-boot-maven-plugin:2.2.5.RELEASE:repackage (repackage) @ spring-boot-demo ---
[INFO] Replacing main artifact with repackaged archive

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

// RepackageMojo.java
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());
}

// 继续跟踪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);
        repackager.setLayout(this.layout.layout());
    }
    return repackager;
}

Переупаковщик устанавливает возвращаемый объект метода макета, который является org.springframework.boot.loader.tools.Layouts.Jar.

/**
 * 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;
    }

}

макет мы можем перевести в макет файла, или макет каталога, код ясен и понятен, и в то же время мы нашли свойство Main-Class org.springframework.boot.loader.JarLauncher, определенное в файле MANIFEST.MF, это кажется, что мы сосредоточены на изучении этого JarLauncher.

Процесс сборки JarLauncher

Поскольку класс org.springframework.boot.loader.JarLauncher находится в spring-boot-loader, о spring-boot-loader, github весенней загрузки представлен следующим образом:

Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using java -jar. Generally you will not need to use spring-boot-loader directly, but instead work with the Gradle or Maven plugin.

Spring Boot Loader предоставляет секретные инструменты, позволяющие создавать отдельные файлы jar, которые можно запускать с помощью java -jar. Обычно вам не нужно напрямую использовать spring-boot-loader, вы можете использовать плагин Gradle или Maven.

Но если вы хотите увидеть исходный код в IDEA, вам нужно ввести следующую конфигурацию в файле pom:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-loader</artifactId>
 <scope>provided</scope>
</dependency>

Найдите класс org.springframework.boot.loader.JarLauncher.

// JarLauncher.java
public class JarLauncher extends ExecutableArchiveLauncher {

    // BOOT-INF/classes/
 static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
 // BOOT-INF/lib/
 static final String BOOT_INF_LIB = "BOOT-INF/lib/";

 public JarLauncher() {
 }

 protected JarLauncher(Archive archive) {
  super(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);
 }
 // main方法
 public static void main(String[] args) throws Exception {
  new JarLauncher().launch(args);
 }

}

Можно обнаружить, что JarLauncher определяет две константы, BOOT_INF_CLASSES и BOOT_INF_LIB, которые точно соответствуют двум файловым каталогам, которые мы распаковали ранее. JarLauncher содержит метод main в качестве начальной записи для приложения.

С основной точки зрения он просто создает объект JarLauncher, а затем выполняет его метод запуска. Давайте посмотрим на структуру наследования JarLauncher:

图2
фигура 2

При построении объекта JarLauncherd вызывается конструктор родительского класса ExecutableArchiveLauncher:

// ExecutableArchiveLauncher.java
public ExecutableArchiveLauncher() {
    try {
        // 构造 archive 对象
        this.archive = createArchive();
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
}
// 构造 archive 对象
protected final Archive createArchive() throws Exception {
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    // 这里就是拿到当前的 classpath 的绝对路径
    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);
    }
    // 将构造的archive 对象返回
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}

Archive

Здесь нам нужно сначала понять концепцию Архива.

  • Архив — это файл архива, это понятие более распространено в Linux.
  • Обычно это сжатый пакет в формате tar/zip.
  • jar в формате zip
public abstract class Archive {
 public abstract URL getUrl();
 public String getMainClass();
 public abstract Collection<Entry> getEntries();
 public abstract List<Archive> getNestedArchives(EntryFilter filter);
}

Архив — это интерфейс, абстрагированный в весенней загрузке для унифицированного доступа к ресурсам. Существует две реализации этого интерфейса: ExplodedArchive и JarFileArchive. Первый — это файловый каталог, а второй — jar, оба из которых используются для поиска ресурсов в файловом каталоге и jar.Здесь мы видим, что JarLauncher поддерживает как запуск jar, так и запуск файловой системы.На самом деле, мы находимся в директория с распакованными файлами.Выполнение команды java org.springframework.boot.loader.JarLauncher также может запуститься нормально.

图3
изображение 3

В FatJar используется последний. Архив имеет собственный URL-адрес, например

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!

У класса Archive также есть метод getNestedArchives, который будет использоваться ниже.Этот метод фактически возвращает список архивов jar-файлов в springboot-demo.jar/lib. Их URL-адреса:

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-2.2.5.RELEASE.jar!

jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-autoconfigure-2.2.5.RELEASE.jar!/

пропускать...

запуск() процесса выполнения

После того, как архив построен, пришло время выполнить метод запуска JarLauncher, который определен в Launcher родительского класса:

// Launcher.java
protected void launch(String[] args) throws Exception {
 /*
     * 利用 java.net.URLStreamHandler 的扩展机制注册了SpringBoot的自定义的可以解析嵌套jar的协议。
     * 因为SpringBoot FatJar除包含传统Java Jar中的资源外还包含依赖的第三方Jar文件
     * 当SpringBoot FatJar被java -jar命令引导时,其内部的Jar文件是无法被JDK的默认实现
     * sun.net.www.protocol.jar.Handler当做classpath的,这就是SpringBoot的自定义协议的原因。
  */
    JarFile.registerUrlProtocolHandler();
    // 通过 classpath 来构建一个 ClassLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 1
    launch(args, getMainClass(), classLoader); // 2
}

Сосредоточьтесь на логике createClassLoader(getClassPathArchives()) для создания ClassLoader. Сначала вызовите метод getClassPathArchives(), чтобы вернуть значение в качестве параметра. Этот метод является абстрактным методом, который специально реализован в подклассе ExecutableArchiveLauncher:

// ExecutableArchiveLauncher.java
@Override
protected List<Archive> getClassPathArchives() throws Exception {
    List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
}

Этот метод выполняет список вложенных архивов, возвращаемый методом getNestedArchives, определенным интерфейсом Archive для записей, соответствующих заданному фильтру. Как видно из вышесказанного, архив здесь на самом деле JarFileArchive, а входящий фильтр — ссылка на метод JarLauncher#isNestedArchive

// JarLauncher.java
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    // entry是文件目录时,必须是我们自己的业务类所在的目录 BOOT-INF/classes/
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    // entry是Jar文件时,需要在依赖的文件目录 BOOT-INF/lib/下面
    return entry.getName().startsWith(BOOT_INF_LIB);
}

Метод getClassPathArchives передает вложенные архивы в BOOT-INF/classes/ и BOOT-INF/lib/ в качестве возвращаемого параметра List в метод createClassLoader через фильтр.

// Launcher.java
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        // 前面说到,archive有一个自己的URL的,获得archive的URL放到list中
        urls.add(archive.getUrl());
    }
    // 调用下面的重载方法
    return createClassLoader(urls.toArray(new URL[0]));
}

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

Целью метода createClassLoader() является создание загрузчика класса LaunchedURLClassLoader для полученных URL-адресов.Во время построения загрузчик класса текущего Launcher передается в качестве его родительского загрузчика, обычно системного загрузчика классов. Давайте сосредоточимся на процессе построения LaunchedURLClassLoader:

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

LaunchedURLClassLoader — это загрузчик классов, определенный Spring Boot.Он наследует JDK URLClassLoader и переписывает метод loadClass, что означает, что он изменяет метод загрузки классов по умолчанию и определяет свои собственные правила загрузки классов, которые можно получить из предыдущего списка. зависимого пакета загружается в Архив>.

После создания LaunchedURLClassLoader возвращаемся в Launcher, и следующим шагом является выполнение перегруженного метода запуска.

// Launcher.java
launch(args, getMainClass(), classLoader);

Перед этим вызывается метод getMainClass с возвращаемым значением в качестве параметра.

Реализация getMainClass находится в ExecutableArchiveLauncher, подклассе Launcher:

// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
    // 从 archive 中拿到 Manifest文件
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        // 就是MANIFEST.MF 文件中定义的Start-Class属性,也就是我们自己写的com.example.spring.boot.demo.SpringBootDemo这个类
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
        throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
    }
    // 返回mainClass
    return mainClass;
}

После получения mainClass выполняем перегруженный метод запуска:

// Launcher.java
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    // 将自定义的LaunchedURLClassLoader设置为当前线程上下文类加载器
    Thread.currentThread().setContextClassLoader(classLoader);
    // 构建一个 MainMethodRunner 实例对象来启动应用
    createMainMethodRunner(mainClass, args, classLoader).run();
}

// Launcher.java
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
    return new MainMethodRunner(mainClass, args);
}

После создания объекта MainMethodRunner вызывается его метод запуска:

// MainMethodRunner.java
public void run() throws Exception {
    // 使用当前线程上下文类加载器也就是自定义的LaunchedURLClassLoader来加载我们自己写的com.example.spring.boot.demo.SpringBootDemo这个类
    Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
    // 找到SpringBootDemo的main方法
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    // 最后,通过反射的方式调用main方法
    mainMethod.invoke(null, new Object[] { this.args });
}

В этот момент начинает вызываться наш собственный основной метод, все файлы классов собственного приложения можно загрузить через /BOOT-INF/classes, все зависимые сторонние jar-файлы можно загрузить через /BOOT-INF/lib, а затем запустить Процесс запуска весенней загрузки завершен.

советы по отладке

Вышеизложенный принцип весенней загрузки с помощью команды java -jar.После понимания принципа, можем ли мы еще больше углубить наше понимание с помощью отладки? Обычно, когда мы начинаем в IDEA, мы запускаем основной метод напрямую, потому что зависимые Jars помещаются в IDEA в пути к классам, поэтому весенняя загрузка завершается напрямую, и она не будет запущена вышеуказанным методом. Однако мы можем начать с Jar, настроив конфигурации запуска/отладки IDEA для настройки приложения JAR.

图4
Рисунок 4

Когда мы сделали вышеуказанные настройки, мы можем легко отлаживать исходный код в IDEA.

图5
Рисунок 5

резюме

В этой статье JarLauncher используется в качестве отправной точки для ознакомления с методом запуска Spring Boot java -jar, излагается основной принцип работы запуска JarLauncher и кратко представляются связанные подключаемые модули spring-boot-maven-plugin и связанные с ними концепции, такие как архив, LaunchedURLClassLoader и т. д. Надеюсь, это поможет всем понять.