Boot yourself, Spring is coming (Часть 2)

в 9:14, , рубрики: java, joker, spring boot, Блог компании JUG.ru Group, конференции, Программирование, стартер

Евгений EvgenyBorisov Борисов (NAYA Technologies) и Кирилл tolkkv Толкачев (Циан.Финанс, Твиттер) продолжают рассказывать о применении Spring Boot к решению задач воображаемого Железного банка Браавоса. Во второй части речь пойдет о профилях и тонкостях запуска приложения.

Boot yourself, Spring is coming (Часть 2) - 1

Первую часть статьи можно найти тут.

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

Во-первых, меняется принцип выдачи кредитов. Если раньше с вероятностью 50% выдавали всем, кроме Старков, то теперь выдают исключительно тем, кто возвращает долги. Поэтому мы меняем правила выдачи кредитов в нашей бизнес-логике. Но только для филиалов банка, которые находятся там, где зима уже пришла, во всех остальных всё остаётся по-старому. Напоминаю, речь идет о сервисе, который решает, выдавать кредит или нет. Мы просто сделаем еще один сервис, который будет работать только зимой.

Идем в нашу бизнес-логику:

public class WhiteListBasedProphetService implements ProphetService {
  @Override
  public boolean willSurvive(String name) {
    return false;
  }
}

У нас уже есть перечень тех, кто возвращает долги.

spring:
  application.name: money-raven
  jpa.hibernate.ddl-auto: validate

ironbank:
  те-кто-возвращают-долги:
    - Ланистеры

ворон:
  куда-лететь: браавос, главный банк
  вкл: true

И есть класс, который уже связан с property — теКтоВозвращаютДолги.

public class ProphetProperties {
  List<String> теКтоВозвращаютДолги;
}

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

public class WhiteListBasedProphetService implements ProphetService {  
  private final ProphetProperties prophetProperties;
  @Override
  public boolean willSurvive(String name) {
    return false;
  }
}

Помним про constructor injection (про волшебные аннотации):

@Service
@RequiredArgsConstructor
public class WhiteListBasedProphetService implements ProphetService {
  private final ProphetProperties prophetProperties;
  @Override
  public boolean willSurvive(String name) {
    return false;
  }
}

Почти готово.

Теперь мы должны выдать только тем, кто возвращает долги:

@Service
@RequiredArgsConstructor
public class WhiteListBasedProphetService implements ProphetService {
  private final ProphetProperties prophetProperties;
  @Override
  public boolean willSurvive(String name) {
  return prophetProperties.getТеКтоВозвращаютДолги().contains(name);
  }
}

Но тут у нас есть небольшая проблема. Теперь у нас две реализации: старый и новый сервисы.

Description

Parameter 1 of constructor in com.ironbank.moneyraven.service.TransferMoneyProphecyBackend…
  - nameBasedProphetService: defined in file [/Users/tolkv/git/conferences/spring-boot-ripper…
  - WhileListBackendProphetService: defined in file [/Users/tolkv/git/conferences/spring-boot-ripper...

Логично разделить эти бины по разным профилям. Профиль зимаТута и профиль зимаБлизко. Пусть наш новый сервис запускается только в профиле зимаТута:

@Service
@Profile(ProfileConstants.зимаТута)
@RequiredArgsConstructor
public class WhiteListBasedProphetService implements ProphetService {
  private final ProphetProperties prophetProperties;
  @Override
  public boolean willSurvive(String name) {
    return prophetProperties.getТеКтоВозвращаютДолги().contains(name);
  }
}

А старый сервис — в зимаБлизко:

@Service
@Profile(ProfileConstants.зимаБлизко)
public class NameBasedProphetService implements ProphetService {
  @Override
  public boolean willSurvive(String name) {
    return !name.contains("Stark") && ThreadLocalRandom.current().nextBoolean();
  }
}

Но зима приходит медленно. В королевствах, находящихся рядом со сломанной стеной, уже зима. А вот где-то на юге — еще нет. Т.е. приложения, которые находятся в разных филиалах и тайм-зонах, должны работать по-разному. По условиям нашей задачи мы не можем стереть старую имплементацию там, где зима наступила, и использовать новый класс. Мы хотим, чтобы банковские работники не делали вообще ничего: поставим им приложение, которое будет работать в режиме лета до момента прихода зимы. А когда придет зима, они просто перезапустят его и все. Они не должны будут менять код, стирать какие-то классы. Поэтому у нас изначально есть два профиля: часть бинов создается, когда лето, а часть бинов создается, когда зима.

Но появляется другая проблема:

Boot yourself, Spring is coming (Часть 2) - 2

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

Так у нас появляется новое требование от заказчика.

Железный закон 2. Без профиля нельзя

Boot yourself, Spring is coming (Часть 2) - 3

Мы не хотим поднимать контекст, если не активизирован профиль, потому что зима уже пришла, все стало очень плохо. Есть определенные вещи, которые должны происходить или нет, в зависимости от того, зимаТута или зимаБлизко. Кроме того, посмотрите на exception, текст которого приведен выше. Он ничего не объясняет. Профиль не задан, поэтому нет ни одной имплементации ProphetService. В то же время никто не сказал о том, что надо обязательно задавать профиль.

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

Можем ли мы это сделать с помощью нашего application listener? Нет. И этому есть три причины:

  • Single responsibility listener отвечает за то, чтобы ворон летел. Listener не должен проверять, был ли активирован профиль, потому что активизация профиля влияет не только на сам listener, но и на многое другое.
  • Когда строится контекст, происходят разные вещи. И мы не хотим, чтобы они начали происходить, если не был задан профиль.
  • Listener работает в самом конце, когда рефрешится контекст. А то, что профиля нет, мы знаем намного раньше. Зачем ждать эти условные пять минут, пока сервис почти поднимется, а потом все упадет.

Кроме того, я еще не знаю, какие баги появятся из-за того, что мы начали подниматься без профиля (предположим, мне неизвестна бизнес-логика). Поэтому при отсутствии профиля надо валить контекст на очень раннем этапе. Кстати, если вы используете какой-нибудь Spring Cloud, это для вас становится еще более актуальным, потому что приложение на раннем этапе делает довольно много всего.

Для реализации нового требования существует ApplicationContextInitializer. Это еще один интерфейс, который позволяет нам расширить какую-то точку Spring, указав его в spring.factories.

Boot yourself, Spring is coming (Часть 2) - 4

Мы имплементируем этот интерфейс, и у нас появляется Context Initializer, в котором есть ConfigurableApplicationContext:

public class ProfileCheckAppInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
  }
}

С помощью него мы можем достать environment — штуку, которую нам подготовил SpringApplication. Туда попали все property, которые мы ему передали. Среди прочего там содержатся и профили.

Если профилей там нет, то мы должны кинуть exception о том, что нельзя так работать.

public class ProfileCheckAppInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
  @Override
  public void initialize(ConfigurableApplicationContext applicationContext) {
  if applicationContext.getEnvironment().getActiveProfiles().length == 0 {
      throw new RuntimeException("Нельзя без профиля!");
    }
  }
}

Теперь нужно это добро прописать в spring.factories.

org.springframework.boot.context.properties.EnableConfigurationProperties=com.ironbank.moneyraven.starter.IronConfiguration
org.springframework.context.ApplicationContextInitializer=com.ironbank.moneyraven.starter.ProfileCheckAppInitializer

Из вышесказанного можно догадаться, что ApplicationContextInitializer — это некая точка расширения. ApplicationContextInitializer работает, когда контекст только начинает строиться, еще нет никаких бинов.

Возникает вопрос: если мы написали ApplicationContextInitializer, почему его, как listener, не прописать в конфигурации, которая тянется в любом случае? Ответ прост: потому что он должен работать намного раньше, когда нет контекста и никаких конфигураций. Т.е. его еще нельзя заинжектить. Поэтому мы его прописываем как отдельную штуку.

Попытка запуска показала: все повалилось достаточно быстро и сообщило, что мы запускаемся без профиля. А теперь попробуем указать какой-нибудь профиль, и все работает — ворон отправляется.

ApplicationContextInitializer —  отрабатывает, когда контекст уже создан, но еще в нем нет ничего, кроме environment.

Boot yourself, Spring is coming (Часть 2) - 5

Кто создает environment? Карлсон — SpringBootApplication. Он же ее наполняет разной мета-информацией, которую потом из контекста можно будет дергать. Большинство вещей можно инжектить через @value, что-то можно получить из environment, как мы только что получили профили.

Например, сюда заходят разные property:

  • которые умеет собирать Spring Boot;
  • которые при запуске передаются через командную строку;
  • системные;
  • прописанные как environment variables;
  • прописанные в application properties;
  • прописанные в каких-то других property-файлах.

Всё это собирается и сетится в объект environment. Туда же попадает информация о том, какие профили активны. Объект environment — это единственное, что существует на момент, когда Spring Boot начинает строить контекст.

Хотелось бы угадать автоматически, какой будет профиль, если люди забыли задать его руками (мы делаем все, чтобы банковские работники, которые достаточно беспомощны без программистов, могли запустить приложение — чтобы у них все заработало, независимо ни от чего). Для этого мы допишем в наш стартер штуку, которая будет угадывать профиль — зимаТута или нет — в зависимости от температуры на улице. И в этом всем нам поможет еще один новый волшебный интерфейс — EnvironmentPostProcessor, потому что нам нужно сделать это до того, как работает ApplicationContextInitializer. А до ApplicationContextInitializer есть только EnvironmentPostProcessor.

Мы опять реализуем новый интерфейс. Тут всего один метод, который точно так же ConfigurableEnvironment прокидывает в SpringApplication, потому что ConfigurableContext у нас еще нет (в SpringInitializer он уже есть, то здесь его нет; есть только environment).

public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
  }
}

В этом environment мы можем установить профиль. Но сначала надо проверить, что никто его не установил ранее. Поэтому проверка getActiveProfiles нам в любом случае нужна. Если люди знают, что они делают, и они установили профиль, то мы не будем за них пытаться угадать. А вот если профиля нет, тогда мы попытаемся понять по погоде.

И второе — мы должны понять, зимняя или летняя у нас сейчас погода. Будем возвращать температуру -300.

public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
if (environment.getActivePrifiles().length == 0 && getTemperature() < -272) {
    }
  }
public int getTemperature() {
  return -300;
}
}

При таком условии у нас зима, и мы можем установить новый профиль. Мы помним, что профиль называется зимаТута:

public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    if (environment.getActivePrifiles().length == 0 && getTemperature() < -272) {
environment.setActiveProfiles("зимаТута");
    } else {
environment.setActiveProfiles("зимаБлизко");
    }
  }
public int getTemperature() {
  return -300;
}
}

Теперь надо указать EnvironmentPostProcessor в spring.factories.

org.springframework.boot.context.properties.EnableConfigurationProperties=com.ironbank.moneyraven.starter.IronConfiguration

org.springframework.context.ApplicationContextInitializer=com.ironbank.moneyraven.starter.ProfileCheckAppInitializer

org.springframework.boot.env.EnvironmentPostProcessor=com.ironbank.moneyraven.starter.ResolveProfileEnvironmentPostProcessor

В итоге приложение запускается без профиля, мы говорим, что это продакшн, и проверяем, в каком же профиле он у нас запустился. Волшебным образом мы поняли, что профиль у нас зимаТута. И приложение не упало, потому что ApplicationContextInitializer, который проверяет, есть ли профиль, идет следом.
Итог:

public class ResolveProfileEnvironmentPostProcessor implements EnvironmentPostProcessor {
  @Override
  public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
    if (getTemperature() < -272) {
      environment.setActiveProfiles("зимаТута");
    } else {
      environment.setActiveProfiles("зимаБлизко");
    }
  }
  private int getTemperature() {
    return -300;
  }
}

Мы говорили про EnvironmentPostProcessor, который работает до ApplicationContextInitializer. Но кто его запускает?

Boot yourself, Spring is coming (Часть 2) - 6

Запускает его вот такой чудик, который, видимо, является внебрачным сыном ApplicationListener и EnvironmentPostProcessor, потому что наследуется и от ApplicationListener, и от EnvironmentPostProcessor. Называется он ConfigFileApplicationListener (почему "ConfigFile" — никто не знает).

Ему наш Карлсон, т.е. Spring Application, дает подготовленный environment, чтобы тот слушал два event: ApplicationPreparedEvent и ApplicationEnvironmentPreparedEvent. Мы сейчас не будем разбирать, кто кидает эти event-ы. Там есть еще одна прослойка (на мой взгляд уже совершенно лишняя, по крайней мере, на данном этапе развития Spring), которая кидает event о том, что сейчас начинает строиться environment (парсятся Application.yml, properties, environment переменные и т.д.).
Получив ApplicationEnvironmentPreparedEvent, listener понимает, что надо настроить environment — найти все EnvironmentPostProcessor и дать им отработать.

Boot yourself, Spring is coming (Часть 2) - 7

После этого он говорит SpringFactoriesLoader доставить все, что вы прописали, а именно все EnvironmentPostProcessor, в spring.factories. Потом он запихивает все EnvironmentPostProcessor в один List

Boot yourself, Spring is coming (Часть 2) - 8

и понимает, что он тоже EnvironmentPostProcessor (по совместительству), поэтому и себя туда запихивает,
Boot yourself, Spring is coming (Часть 2) - 9
при этом сортирует их, едет вместе с ними и вызывает у каждого метод postProcessEnvironment.

Таким образом все postProcessEnvironment запускаются на раннем этапе еще до SpringApplicationInitializer. При этом непонятный EnvironmentPostProcessor под названием ConfigFileApplicationListener тоже запускается.

Когда environment настроился, все опять возвращается к Карлсону.

Если environment готов, можно строить контекст. И Карлсон начинает строить контекст с помощью ApplicationInitializer. Тут у нас есть собственный кусок, который проверяет, что в контексте есть environment, в котором есть активные профили. Если нет, мы падаем, потому что иначе у нас потом все равно будут проблемы. Дальше работают стартеры, со всеми уже обычными конфигурациями.

Картинка выше отражает, что в Spring тоже не все хорошо. Там периодически встречаются такие инопланетяне, single responsibility не соблюдается и лезть туда нужно аккуратно.

Теперь мы хотим немного поговорить о другой стороне этого странного существа, которое с одной стороны listener, а с другой стороны — EnvironmentPostProcessor.

Boot yourself, Spring is coming (Часть 2) - 10

Как EnvironmentPostProcessor он умеет подгружать application.yml, application properties, всякие environment variable, command аргументы и т.д. А как listener, он умеет слушать два event-а:

  • ApplicationPreparedEvent
  • ApplicationEnvironmentPreparedEvent

Возникает вопрос:

Boot yourself, Spring is coming (Часть 2) - 11

Все эти event-ы были в старом Spring. А те, о которых мы говорили выше — event из Spring Boot (специальные event-ы, которые он добавил для своего жизненного цикла). И их еще целая пачка. Это основные:

  • ApplicationStartingEvent
  • ApplicationEnvironmentPreparedEvent
  • ApplicationPreparedEvent
  • ContextRefreshedEvent
  • EmbeddedServletContainerInitializedEvent
  • ApplicationReadyEvent
  • ApplicationFailedEvent

В этом списке присутствует далеко не все. Но важно, что часть из них относится к Spring Boot, а часть — к Spring (старый-добрый ContextRefreshedEvent и т.д.).

Нюанс заключается в том, что не все из этих event-ов мы можем получить в приложении (простые смертные — разные бабушки — не могут просто слушать сложные event-ы, которые кидает Spring Boot). Но если вы знаете про тайные механизмы spring.factories и определяете свой Application Listener на уровне spring.factories, то эти event-ы самого раннего этапа старта приложения попадают к вам.

Boot yourself, Spring is coming (Часть 2) - 12

В итоге вы можете влиять на старт своего приложения на довольно раннем этапе. Прикол, правда, в том, что часть этой работы вынесена в другие сущности — такие, как EnvironmentPostProcessor и ApplicationContextInitializer.

Можно было всё делать на listener-ах, но это было бы неудобно и некрасиво. Если вы хотите слушать все event-ы, которые кидает Spring, а не только ContextRefreshedEvent и ContextStartedEvent, то не надо прописывать listener, как бин, обычным путем (иначе он создается слишком поздно). Его надо тоже прописать через spring.factories, тогда его создадут намного раньше.

Кстати, когда мы смотрели этот список, нам было непонятно, когда вообще срабатывают ContextStartedEvent и ContextStoppedEvent?

Boot yourself, Spring is coming (Часть 2) - 13

Оказалось, что эти event-ы вообще никогда не работают. Мы долго ломали голову над тем, какие же event-ы надо ловить, чтобы понять, что приложение действительно стартовало. И оказалось, что event-ы, о которых мы сейчас говорили, появляются, когда вы принудительно дергаете методы у контекста:

  • ctx.start(); -> ContextStartedEvent
  • ctx.stop(); -> ContextStoppedEvent

Т.е. event-ы будут приходить, только если мы запустим SpringApplication.run, получим контекст, дернем у него ctx.start(); или ctx.stop();. Не очень понятно, зачем это нужно. Но вам, опять же, дали точку расширения.

Имеет ли Spring к этому какое-то отношение? Если да, то где-то должен быть exception:

  • ctx.stop();  (1)
  • ctx.start(); (2)
  • ctx.close(); (3)
  • ctx.start(); (4)

По факту он будет на последней строчке, потому что после ctx.close(); с контекстом нельзя делать ничего. Но вызывать ctx.stop(); перед ctx.start(); — можно (просто Spring игнорирует эти event-ы  - они только для вас).

Пишите свои listener-ы, слушайте сами, придумайте ваши законы, что делать на ctx.stop();, а что делать на ctx.start();.

Итого схема взаимодействия и жизненного цикла приложения выглядит примерно так:

Boot yourself, Spring is coming (Часть 2) - 14

Цветами здесь показаны разные периоды жизни.

  • Синий — это Spring Boot, приложение уже стартовало. Это значит, что обрабатываются Tomcat service запросы, которые приходят к нему от клиентов, весь контекст точно поднят, все бины работают, базы данных подключены и т.д.
  • Зеленый — прилетел event ContextRefreshedEvent и контекст построен. С этого момента начинают работать, например, application listener-ы, которые вы имплементируете либо путем установки аннотации ApplicationListener, либо через одноименный интерфейс с дженериком, который слушает определенные event-ы. Если же вы хотите получать больше event-ов, нужно прописывать этот же ApplicationListener в spring.factories (здесь работает обычный Spring). Полосой отмечено, где начинается доклад Spring Ripper.
  • На более ранней стадии работает SpringApplication, который подготавливает нам контекст. Это та работа по подготовке приложения, которую мы делали, когда были обычными Spring-разработчиками. Например, конфигурировали WebXML.
  • Но есть еще более ранние этапы. Здесь указано, кто, где и за кем работает.
  • Есть еще серый этап, в которой никак нельзя вклиниться. Это этап, на котором "из коробки" работает SpringApplication (только в код лезть).

Если вы обратили внимание, в процессе доклада из двух частей мы шли справа налево: начали с самого конца, прикрутили конфигурацию, которая прилетала из стартера, потом добавили следующее и т.п. Теперь давайте быстренько всю эту цепочку проговорим в обратную сторону.
Вы пишите в вашем main SpringApplication.run. Он находит разных listener-ов, кидает им event, что начал строиться. После этого listener-ы находят EnvironmentPostProcessor, дают им настроить environment. Как только environment настроен, мы начинаем строить контекст (вступает Карлсон). Карлсон строит контекст и дает возможность всем Application Initializer-ам что-то с этим контекстом сделать. У нас есть точка расширения. После этого контекст уже настроен и дальше начинает происходить то же самое, что и в обычном Spring-овом приложении, когда строиться контекст — BeanFactoryPostProcessor, BeanPostProcessor, бины настраиваются. Этим занимается обычный Spring.

Как запускать

Мы закончили обсуждать процесс написания приложения.

Но у нас была еще одна вещь, которую не любят разработчики. Они не любят думать, как же в итоге их приложение запустится. Будет ли админ его запускать в Tomcat, JBoss или в WebLogic? Оно просто должно работать. Если оно не работает, в худшем случае разработчику приходится опять что-то настраивать

Итак, какие у нас есть способы запуска?

  • tomcat war;
  • idea;
  • java -jar/war.

Tomcat — не массовый тренд, про него подробно рассказывать не будем.

Idea — тоже, в принципе, не очень интересно. Там просто чуть хитрее, чем я расскажу ниже. Но в Idea в принципе не должно быть проблем. Она же видит, какие зависимости стартер принесет.
Если мы делаем java -jar, основная проблема — построить classpath перед тем, как мы запускаем приложение.

Что люди делали в 2001 году? Они писали java -jar, какой jar надо запустить, потом пробел, classpath=... и там указывали скрипты. В нашем случае там 150 Мб разных зависимостей, которые добавили стартеры. И все это надо было бы делать вручную. Естественно, никто так не делает. Мы просто пишем: java -jar, какой jar надо запустить и все. Каким-то образом classpath все равно строится. Про это мы сейчас будем разговаривать.

Начнем с этапа подготовки jar-файла, чтобы его вообще можно было запустить без Tomcat. Перед тем, как сделать java -jar, надо jar построить. Этот jar явно должен быть необычный, какой-то аналог war-а, где будет внутри все, включая embedded Tomcat.

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

Когда мы скачали проект, у нас в POM-е уже кто-то прописал плагин. Тут еще, кстати, можно конфигурации накидывать, но об этом чуть позже. В итоге кроме обычного jar, который строит Maven или Gradle вашего приложения, строится еще jar необычный. С одной стороны он выглядит нормально:

Boot yourself, Spring is coming (Часть 2) - 15

А вот если посмотреть сбоку:

Boot yourself, Spring is coming (Часть 2) - 16

Это в принципе аналог war-а.

Посмотрим, что он вообще собой представляет.

Тут есть явно стандартные части, как в обычном jar. Когда мы пишем java -jar, подтягиваются все классы, которые находятся в корне, например, org.springframework.boot. Но это же не наши классы. Мы вряд ли писали org.springframework.boot package. Поэтому в первую очередь для нас знакомым является META-INF

Boot yourself, Spring is coming (Часть 2) - 17

Spring Boot тоже делает MANIFEST (через тот самый плагин Maven или Gradle), кастомизирует его и прописывает main class, который запуститься в jar-е.

По сути, все jar-ы делятся на два вида: запускаемые и простые зависимости чего-то, не имеющие собственного main-а. И мы можем сделать java -jar только на -jar, у которого прописано, кто является main-class-ом.

Логично предположить, что этот плагин, который делает нам упаковку и создает этот MANIFEST, в main-class прописывает тот класс, который у нас main (который мы запускаем из Idea). Но если погрузиться чуть глубже, этого никак не может быть. Кто же тогда построит class path? Если мы возьмем java -jar и скажем, что main, который надо запустить, — это наш главный main, он не найдет никаких зависимостей. Поэтому плагин в MANIFEST прописывает в качестве основного класса JarLauncher.

Boot yourself, Spring is coming (Часть 2) - 18

Т.е. помимо вашего кода и зависимостей, он упаковывает еще такую штуку, в которой есть JarLauncher. Его задача в том, чтобы запустить наш main, но перед этим построить class path.
А как он знает, какой у нас main? Тут есть property — Start-class.

Т.е. инициализация вашего приложения происходит в два шага. На первом шаге есть class path стандартного jar. Все, что там лежит — org.springframework.boot — будет в class path. Поэтому мы совершенно валидно пишем  org.springframework.boot.loader.JarLauncher в main-class. И он запускается, main-class идет дальше. Он формирует class path, который находится в BOOT-INF (там есть папочка lib с зависимостями и папочка class с классами, которые вы написали в своем приложении).

Когда мы писали RavenApplication, properties падает в class в BOOT-INF, а все зависимости, типа Tomcat и библиотек, падают в BOOT-INF/lib/. Когда JarLauncher сформировал classpath, он совершает второй шаг — запускает класс, который указан в start-class. Там уже работает реальный Spring, ContextSpringApplication — ровно тот flow, о котором мы рассказывали ранее.

Откуда плагин знает, кто является start-class-ом? Мы можем указать явно, какой класс должен быть запускным. И тогда этот плагин пропишет его, как стартовый.

Кстати, тут получается такой парадокс имен. Мы прописываем его через property, которое называется mainClass, а в MANIFEST это будет Start-Class, потомучто mainClass — это всегда JarLauncher.

Но если мы не рассказали явно, кто является mainClass, как он разберется? У него есть следующая логика. Spring boot plugin сканирует проект – ищет mainClass:

  • если есть только один – то это точно он. Плагин его сам выставляет — не нужно принудительно ставить main class;
  • если больше одного – смотрит, над каким mainClass стоит аннотация @SpringBootApplication и выбирает его, если, конечно, нет второго;
  • если есть второй — будет exception с указанием того, что нужно прописать main class, иначе непонятно, с кем бандлить ваш jar-ник с приложением. Т.е. приложение просто не запустится, а точнее, даже не соберется. Скажет, что там больше, чем один кандидат на main class.
  • если @SpringBootApplication нет — тоже будет ошибка.

JarLauncher есть не всегда. Некоторые используют Tomcat и для них есть WarLauncher, благодаря которому есть возможность запустить war-ник так же, как jar-ник.

Но есть такие люди, для которых сложно даже java -jar. Можно ли упростить? Можно. Сейчас посмотрим как.

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

Мы можем указать опцию <configuration> а в ней <executable>true</executable> ну или в Gradle так же, только проще:

springBoot {
 executable = true
}

Мы указываем эту опцию и jar превращается в executable jar. Наше приложение уже было сконфигурировано на это.

Давайте посмотрим, как это вообще происходит. Все пользователи Windows помнят, что можно кликнуть по exe-шнику, и он запустится. То же происходит и с приложением Spring Boot, т.е. с вашим jar, когда вы делаете так. Я просто условно кликаю по нему, и он пошел запускаться.
Как это произошло?

Мы можем открыть наш файлик (jar — это zip-архив, открываем его в текстовом редакторе):

Boot yourself, Spring is coming (Часть 2) - 19

Spring Boot совершил какую-то магию.

Это баш-скрипт, который запускает jar-ник. Прикол в том, что в начале здесь указан интерпретатор, которым надо запускать скрипт — #!/bin/bash. И в этом половина магии.

Вторая половина магии находится в конце скрипта. Там есть exit 0 и дальше какая-то белиберда — с этого места пошел наш zip-архив.

Boot yourself, Spring is coming (Часть 2) - 20

Магия в том, что любой zip-архив имеет специальную метку — 0xf4ra. Если архиватор видит эту метку, он понимает, что нашел старт архива.

Boot yourself, Spring is coming (Часть 2) - 21

До этого может быть что угодно (картинки, текст и т.п.).

Запуск jar превращается в следующую операцию:

  • система считывает первую строчку — потому что это просто файл;
  • она видит, что там написано "запускай меня через bash" (#!/bin/bash);
  • bash запускает этот файл;
  • он выполняет скрипт и доходит exit 0;
  • сам скрипт делает java -jar на себя — на тот же самый jar-ник, которым он является;
  • после java -jar стандартный zip-архиватор идет по jar-у, пропускает скрипт, видит метку начала архива, и с этого момента идет запуск приложения.

Выводы

Выводы в первую очередь связаны с мнением о том, что Spring Boot — это большая магия, что там делается много всего, с чем потом невозможно разобраться.

Во-первых, разобраться можно. Это сложнее, чем разобраться со Spring, потому что Spring — это маленькая часть жизненного цикла Spring Boot. Это связано с тем, что он на себя взял ответственность за те проблемы, которые не любит брать на себя разработчик — конфигурации, зависимости, версии и запуск, сняв головную боль с нас. Кстати, часть проблем, связанных с неправильным использованием Spring, Spring Boot тоже забрал.

Во-вторых, мы видим аннотацию @SpringBootApplication, в которой собраны те best practice, которые были в хороших Spring-приложениях.

И третье — это новая инфраструктура, которая решает проблему внешних зависимостей, типа различных конфигураций. Мы можем прописать property как environment variable, можем прописать как var arg в нашем приложении, можем прописать ее как некий внешний ресурс, а можем использовать JSON. В любом случае все загрузится в приложение и будет доступно через @value или даже через более новый безопасный способ, как мы это демонстрировали. Наши configuration properties комплитились, мы предоставляли стартер как сервис, люди его брали и комплитили, и у них не было никаких проблем. Более того, сам Spring предоставляет огромное количество этих стартеров. Мы уже показали, что написаны они весьма странно, но на это есть свои причины.

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

Boot yourself, Spring is coming (Часть 2) - 22

Минутка рекламы. 19-20 октября состоится конференция Joker 2018, на которой Евгений Борисов вместе с Барухом Садогурским выступят с докладом «Приключения Сеньора Холмса и Джуниора Ватсона в мире разработки ПО [Joker Edition]», а Кирилл Толкачев с Максимом Гореликовым представят доклад «Micronaut vs Spring Boot, или Кто тут самый маленький?». В целом, на Joker будет ещё множество интересных и заслуживающих пристального внимания докладов. Приобрести билеты можно на официальном сайте конференции.

Автор: olegchir

Источник


* - обязательные к заполнению поля