Программный запуск приложения Spring Boot

Опубликовано: 2022-03-11

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

Точно так же проект Spring Boot обычно создается в виде исполняемого файла JAR с помощью предоставленного плагина maven, который выполняет всю грязную работу. В результате получается удобный единый JAR-файл, которым легко поделиться с другими, развернуть на сервере и т. д.

Запустить приложение Spring Boot так же просто, как ввести java -jar mySpringProg.jar , и приложение выведет на консоль несколько хорошо отформатированных информационных сообщений.

Но что, если разработчик Spring Boot хочет запустить приложение из другой Java-программы без вмешательства человека?

Как работают вложенные JAR-файлы

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

«Затенение» — один из вариантов. Затенение зависимостей — это процесс включения и переименования зависимостей, перемещения классов и перезаписи затронутого байт-кода и ресурсов для создания копии, которая связана с собственным кодом приложения (проекта).

Затенение позволяет пользователям распаковывать все классы и ресурсы из зависимостей и упаковывать их обратно в исполняемый JAR-файл. Это может работать для простых сценариев, однако, если две зависимости содержат один и тот же файл ресурсов или класс с одинаковым именем и путем, они будут перекрываться, и программа может не работать.

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

 example.jar | +-META-INF | +-MANIFEST.MF +-org | +-springframework | +-boot | +-loader | +-<spring boot loader classes> +-BOOT-INF +-classes | +-mycompany | +-project | +-YourClasses.class +-lib +-dependency1.jar +-dependency2.jar

Архив JAR организован как стандартный JAR-файл, который может выполняться на Java. Классы загрузчика Spring Boot расположены по org/springframework/boot/loader , а пользовательские классы и зависимости — по пути BOOT-INF/classes и BOOT-INF/lib .

Примечание. Если вы новичок в Spring, вы также можете ознакомиться с нашей статьей «10 самых распространенных ошибок Spring Framework» .

Типичный JAR-файл Spring Boot содержит три типа записей:

  • Классы проекта
  • Вложенные библиотеки JAR
  • Классы загрузчика Spring Boot

Spring Boot Classloader сначала установит JAR-библиотеки в пути к классам, а затем классы проекта, что имеет небольшую разницу между запуском приложения Spring Boot из IDE (Eclipse, IntelliJ) и из консоли.

Дополнительные сведения о переопределениях классов и загрузчике классов см. в этой статье.

Запуск приложений Spring Boot

Запустить приложение Spring Boot вручную из командной строки или оболочки очень просто, набрав следующее:

 java -jar example.jar

Однако программный запуск приложения Spring Boot из другой программы Java требует больше усилий. Необходимо загрузить org/springframework/boot/loader/*.class , использовать немного отражения Java для создания экземпляров JarFileArchive , JarLauncher и вызвать метод launch(String[]) .

В следующих разделах мы более подробно рассмотрим, как это делается.

Загрузка классов загрузчика Spring Boot

Как мы уже указывали, JAR-файл Spring Boot ничем не отличается от любого JAR-архива. Можно загружать org/springframework/boot/loader/*.class , создавать объекты класса и использовать их для последующего запуска приложений Spring Boot.

 import java.net.URLClassLoader; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; . . . public static void loadJar(final String pathToJar) throws IOException . . . { // Class name to Class object mapping. final Map<String, Class<?>> classMap = new HashMap<>(); final JarFile jarFile = new JarFile(pathToJar); final Enumeration<JarEntry> jarEntryEnum = jarFile.entries(); final URL[] urls = { new URL("jar:file:" + pathToJar + "!/") }; final URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);

Здесь мы видим, что classMap будет содержать объекты класса, сопоставленные с соответствующими именами пакетов, например, строковое значение org.springframework.boot.loader.JarLauncher будет сопоставлено с объектом JarLauncher.class .

 while (jarEntryEnum.hasMoreElements()) { final JarEntry jarEntry = jarEntryEnum.nextElement(); if (jarEntry.getName().startsWith("org/springframework/boot") && jarEntry.getName().endsWith(".class") == true) { int endIndex = jarEntryName.lastIndexOf(".class"); className = jarEntryName.substring(0, endIndex).replace('/', '.'); try { final Class<?> loadedClass = urlClassLoader.loadClass(className); result.put(loadedClass.getName(), loadedClass); } catch (final ClassNotFoundException ex) { } } } jarFile.close();

Конечным результатом цикла while является карта, заполненная объектами класса загрузчика Spring Boot.

Автоматизация фактического запуска

После завершения загрузки мы можем приступить к завершению автоматического запуска и использовать его для фактического запуска нашего приложения.

Рефлексия Java позволяет создавать объекты из загруженных классов, что весьма полезно в контексте нашего руководства.

Первым шагом является создание объекта JarFileArchive .

 // Create JarFileArchive(File) object, needed for JarLauncher. final Class<?> jarFileArchiveClass = result.get("org.springframework.boot.loader.archive.JarFileArchive"); final Constructor<?> jarFileArchiveConstructor = jarFileArchiveClass.getConstructor(File.class); final Object jarFileArchive = jarFileArchiveConstructor.newInstance(new File(pathToJar));

Конструктор объекта JarFileArchive принимает объект File(String) в качестве аргумента, поэтому он должен быть предоставлен.

Следующим шагом является создание объекта JarLauncher , для которого требуется Archive в своем конструкторе.

 final Class<?> archiveClass = result.get("org.springframework.boot.loader.archive.Archive"); // Create JarLauncher object using JarLauncher(Archive) constructor. final Constructor<?> jarLauncherConstructor = mainClass.getDeclaredConstructor(archiveClass); jarLauncherConstructor.setAccessible(true); final Object jarLauncher = jarLauncherConstructor.newInstance(jarFileArchive);

Чтобы избежать путаницы, обратите внимание, что Archive на самом деле является интерфейсом, а JarFileArchive — одной из реализаций.

Последним шагом в этом процессе является вызов метода launch(String[]) для нашего только что созданного объекта jarLauncher . Это относительно просто и требует всего несколько строк кода.

 // Invoke JarLauncher#launch(String[]) method. final Class<?> launcherClass = result.get("org.springframework.boot.loader.Launcher"); final Method launchMethod = launcherClass.getDeclaredMethod("launch", String[].class); launchMethod.setAccessible(true); launchMethod.invoke(jarLauncher, new Object[]{new String[0]});

Метод invoke(jarLauncer, new Object[]{new String[0]}) окончательно запустит приложение Spring Boot. Обратите внимание, что основной поток остановится и будет ждать завершения работы приложения Spring Boot.

Несколько слов о загрузчике классов Spring Boot

Изучение нашего JAR-файла Spring Boot покажет следующую структуру:

 +--- mySpringApp1-0.0.1-SNAPSHOT.jar +--- META-INF +--- BOOT-INF | +--- classes # 1 - project classes | | | | | +--- com.example.mySpringApp1 | | \--- SpringBootLoaderApplication.class | | | +--- lib # 2 - nested jar libraries | +--- javax.annotation-api-1.3.1 | +--- spring-boot-2.0.0.M7.jar | \--- (...) | +--- org.springframework.boot.loader # 3 - Spring Boot loader classes +--- JarLauncher.class +--- LaunchedURLClassLoader.class \--- (...)

Обратите внимание на три типа записей:

  • Классы проекта
  • Вложенные библиотеки JAR
  • Классы загрузчика Spring Boot

Оба класса проекта ( BOOT-INF/classes ) и вложенные файлы JAR ( BOOT-INF/lib ) обрабатываются одним и тем же загрузчиком классов LaunchedURLClassLoader . Этот загрузчик находится в корневом каталоге JAR-приложения Spring Boot.

LaunchedURLClassLoader загрузит содержимое класса ( BOOT-INF/classes ) после содержимого библиотеки ( BOOT-INF/lib ), которое отличается от IDE. Например, Eclipse сначала поместит содержимое класса в путь к классам, а затем библиотеки (зависимости).

LaunchedURLClassLoader расширяет java.net.URLClassLoader , который создается с набором URL-адресов, которые будут использоваться для загрузки классов. URL-адрес может указывать на такое место, как архив JAR или папку классов. При выполнении загрузки класса все ресурсы, указанные URL-адресами, будут пройдены в том порядке, в котором были предоставлены URL-адреса, и будет использоваться первый ресурс, содержащий искомый класс.

Подведение итогов

Классическое Java-приложение требует перечисления всех зависимостей в аргументе пути к классам, что делает процедуру запуска несколько громоздкой и сложной.

Напротив, приложения Spring Boot удобны и легко запускаются из командной строки. Они управляют всеми зависимостями, и конечному пользователю не нужно беспокоиться о деталях.

Однако запуск приложения Spring Boot из другой программы Java усложняет процедуру, поскольку требует загрузки классов загрузчика Spring Boot, создания специализированных объектов, таких как JarFileArchive и JarLauncher , а затем использования отражения Java для вызова метода launch .

Итог : Spring Boot может выполнять множество рутинных задач, позволяя разработчикам высвободить время и сосредоточиться на более полезной работе, такой как создание новых функций, тестирование и так далее.