Я написал статью раньше, чтобы представить файл JAR и файл MENIFEST.MF, см.:Расскажите о файлах JAR и MANIFEST.MF, который описывает внутреннюю структуру файла JAR в этой статье. Эта статья продолжит предыдущий ритм и расскажет, как запустить FatJar в SpringBoot.
Каталог файлов после распаковки FatJar
отофициальный сайт весныИли создайте новый проект SpringBoot через Idea.Для удобства рекомендуется не добавлять никаких зависимостей.Достаточно привезенного по умолчанию пустого проекта SpringBoot.
Упаковано через команду maven, скриншот продукта сборки, полученный после успешной упаковки, выглядит следующим образом:
Как упоминалось в предыдущей статье, пакет jar является вариантом пакета zip, поэтому его также можно распаковать с помощью unzip.
unzip -q guides-for-jarlaunch-0.0.1-SNAPSHOT.jar -d mock
В распакованном фиктивном каталоге используйте команду дерева, чтобы увидеть, что вся распакованная структура каталога FatJar выглядит следующим образом (частично опущена):
.
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties # 用户-配置文件
│ │ └── com
│ │ └── glmapper
│ │ └── bridge
│ │ └── boot
│ │ └── BootStrap.class # 用户-启动类
│ └── lib
│ ├── jakarta.annotation-api-1.3.5.jar
│ ├── jul-to-slf4j-1.7.28.jar
│ ├── log4j-xxx.jar # 表示 log4j 相关的依赖简写
│ ├── logback-xxx.jar # 表示 logback 相关的依赖简写
│ ├── slf4j-api-1.7.28.jar
│ ├── snakeyaml-1.25.jar
│ ├── spring-xxx.jar # 表示 spring 相关的依赖简写
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.glmapper.bridge.boot
│ └── guides-for-jarlaunch
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── # 省略
├── data
│ ├── # 省略
├── jar
│ ├── # 省略
└── util
└── SystemPropertyUtils.class
В двух словах, FatJar включает в себя три папки после распаковки:
├── BOOT-INF # 存放的是业务相关的,包括业务开发的类和配置文件,以及依赖的jar
│ ├── classes
│ └── lib
├── META-INF # 包括 MANIFEST.MF 描述文件和 maven 的构建信息
│ ├── MANIFEST.MF
│ └── maven
└── org # SpringBoot 相关的类
└── springframework
Обычно мы начинаем с метода SpringApplication#run при отладке процесса запуска проекта SpringBoot.
@SpringBootApplication
public class BootStrap {
public static void main(String[] args) {
// 入口
SpringApplication.run(BootStrap.class,args);
}
}
Для программы Java мы знаем, что запись запуска должна иметь основную функцию, которая, похоже, именно здесь, но одна точка - это то, что когда класс с основной функцией выполняется через команду Java, нет необходимости в Параметр jar. Например, создайте новый файл Bootstrap.java со следующим содержимым:
public class BootStrap {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Скомпилируйте этот файл через javac:
javac BootStrap.java
Затем вы можете получить скомпилированный файл .class BootStrap.class, который можно выполнить непосредственно через команду java:
java BootStrap # 输出 Hello World
А как насчет java -jar? Это на самом делеОфициальная документация по java.Это четко описано в:
- -jar filename
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.
When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are ignored.
Проще говоря, конкретный класс запуска, управляемый командой java -jar, должен быть настроен в свойстве Main-Class ресурса MANIFEST.MF.
Что обратно и посмотрите еще раз перед упакованным, файловым каталогом после декомпрессии, найдите файл /мета-inf/manifest.mf, посмотрите метаданные:
Manifest-Version: 1.0
Implementation-Title: guides-for-jarlaunch
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.glmapper.bridge.boot.BootStrap
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.2.0.RELEASE
Created-By: Maven Archiver 3.4.0
# Main-Class 在这里,指向的是 JarLauncher
Main-Class: org.springframework.boot.loader.JarLauncher
Класс org.springframework.boot.loader.JarLauncher хранится в org/springframework/boot/loader:
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class # JarLauncher
├── # 省略
Таким образом, основы ясны.В FatJar классы ниже org.springframework.boot.loader отвечают за загрузку проекта SpringBoot.Как запись, бизнес-код и зависимости хранятся в BOOT-INF, и есть метаданные описание в META-INF.
JarLaunch - лаунчер для FatJar
Прежде чем анализировать JarLaunch, вот как эти классы в org.springframework.boot.loader упакованы в FatJar.
spring-boot-maven-plugin упаковывает процесс spring-boot-loader
Поскольку нет места для импорта или записи, связанных с родственниками в недавно созданном пустом проекте Springboot. Фактически, для каждого вновь созданного проекта SpringBoot вы можете увидеть следующие плагины в его файле Pom.xml:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Это подключаемый модуль, предоставляемый Springboot, org.springframework.boot.loader фактически воспроизводится через этот подключаемый модуль;
Ниже приведен поток выполнения этого плагина для помещения классов, связанных с загрузчиком, в FatJar:
org.springframework.boot.maven#execute-> org.springframework.boot.maven#repackage -> org.springframework.boot.loader.tools.Repackager#repackage-> org.springframework.boot.loader.tools.Repackager#writeLoaderClasses-> org.springframework.boot.loader.tools.JarWriter#writeLoaderClasses
Окончательным методом выполнения является следующий метод, из комментариев видно, что функция этого метода заключается в записи классов spring-boot-loader в FatJar.
/**
* Write the required spring-boot-loader classes to the JAR.
* @throws IOException if the classes cannot be written
*/
@Override
public void writeLoaderClasses() throws IOException {
writeLoaderClasses(NESTED_LOADER_JAR);
}
Основы JarLaunch
Исходя из предыдущего анализа, рассмотрим здесь вопрос, можно ли запустить проект SpringBoot напрямую через java BootStrap? Таким образом, теоретически можно напрямую использовать самые примитивные java-инструкции без параметра -jar и руководства JarLaunch, потому что есть метод main.
пройти черезjava BootStrap
Режим запуска
Класс BootStrap выглядит следующим образом:
@SpringBootApplication
public class BootStrap {
public static void main(String[] args) {
SpringApplication.run(BootStrap.class,args);
}
}
После компиляции выполнитьjava com.glmapper.bridge.boot.BootStrap
, а затем выдает исключение:
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
at com.glmapper.bridge.boot.BootStrap.main(BootStrap.java:13)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 1 more
С точки зрения стека исключений это связано с тем, что класс SpringApplication не может быть найден, здесь его на самом деле легче понять. тоже запускается при старте.путь к классу не указан.
Я не буду вдаваться в подробности здесь. Я пробовал через -classpath + -Xbootclasspath, но это, похоже, не работает. Если вы успешно запустите его через команду java, пожалуйста, оставьте сообщение для связи.
пройти черезjava JarLaunch 启动
пройти сноваjava org.springframework.boot.loader.JarLauncher
способ начать, вы можете видеть, что это возможно.
Здесь можно в основном догадаться, что при запуске JarLauncher файл JAR, от которого нужно зависеть, будет каким-то образом представлен как зависимость от BootStrap. Давайте кратко проанализируем, что делает JarLauncher в качестве загрузочного класса при запуске.
Фундаментальный анализ
Класс JarLaunch определяется следующим образом:
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() {
}
// 带有指定 Archive 的构造函数
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);
}
}
Сквозь код мы можем ясно увидеть несколько ключевых информационных моментов:
-
BOOT_INF_CLASSES
иBOOT_INF_LIB
Две константы соответствуют двум файловым каталогам после предыдущей распаковки. - JarLaunch содержит основную функцию в качестве записи запуска
Но с основной точки зрения он просто создает объект JarLaunch, а затем выполняет его метод запуска, и нет места для зависимостей, которые мы ожидаем увидеть. На самом деле эта часть выполняется в конструкторе родительского класса JarLaunch ExecutableArchiveLauncher.
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
// /Users/xxx/Documents/test/glmapper-springboot-study-guides/guides-for-jarlaunch/target/mock/
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));
}
PS: Концепция Архива здесь не объясняется из-за ограниченного места.
Создайте Архив из приведенного выше, а затем приступайте к выполнению метода запуска:
protected void launch(String[] args) throws Exception {
// 注册协议,利用了 java.net.URLStreamHandler 的扩展机制,SpringBoot
// 扩展出了一种可以解析 jar in jar 的协议
JarFile.registerUrlProtocolHandler();
// 通过 classpath 来构建一个 ClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// launch
launch(args, getMainClass(), classLoader);
}
Следующие значения необходимо обратить внимание на метод getMainClass(), Здесь нужно получить Start-Class, указанный в MENIFEST.MF, который на самом деле является классом BootStrap в нашем проекте:
@Override
protected String getMainClass() throws Exception {
// 从 archive 中拿到 Manifest
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 获取 Start-Class
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException(
"No 'Start-Class' manifest entry specified in " + this);
}
// 返回 mainClass
return mainClass;
}
Наконец, объект экземпляра MainMethodRunner создается, а затем метод main в классе BootStrap вызывается путем отражения:
резюме
Эта статья в основном анализирует метод запуска SpringBoot с точки зрения JarLaunch и кратко демонстрирует такие методы запуска, как обычный метод Java и java -jar; в то же время кратко объясняется основной принцип работы запуска JarLaunch. Конструкция архива, пользовательского обработчика протоколов и т. д. не была глубоко изучена, и позже будет проведен отдельный анализ соответствующих моментов.