HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-03-10
1 <p>Современный Spring, а если быть точнее - специальный фреймворк Spring Boot, позволяют с минимальными усилиями подключать ту или иную технологию. Необходимость создавать десятки служебных бинов ушла в прошлое.</p>
1 <p>Современный Spring, а если быть точнее - специальный фреймворк Spring Boot, позволяют с минимальными усилиями подключать ту или иную технологию. Необходимость создавать десятки служебных бинов ушла в прошлое.</p>
2 <p>Для этого имеются всевозможные starter-ы - специальные Maven/Gradle зависимости, которые необходимо только подключить в проект.</p>
2 <p>Для этого имеются всевозможные starter-ы - специальные Maven/Gradle зависимости, которые необходимо только подключить в проект.</p>
3 <p>Например, подключив в проект всего одну зависимость:</p>
3 <p>Например, подключив в проект всего одну зависимость:</p>
4 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt; &lt;/dependency&gt;<p>мы имеем уже созданный в контексте DataSource, созданный по свойствам в application.properties и другими классами, вроде NamedParameterJdbcTemplate.</p>
4 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-jdbc&lt;/artifactId&gt; &lt;/dependency&gt;<p>мы имеем уже созданный в контексте DataSource, созданный по свойствам в application.properties и другими классами, вроде NamedParameterJdbcTemplate.</p>
5 <p>Подобные starter основаны на двух специальных функциональностях Spring Boot - AutoConfigurations и Conditional. Покажем, как использовать данные функциональности на примере<strong>создания собственного Spring Boot Starter</strong>.</p>
5 <p>Подобные starter основаны на двух специальных функциональностях Spring Boot - AutoConfigurations и Conditional. Покажем, как использовать данные функциональности на примере<strong>создания собственного Spring Boot Starter</strong>.</p>
6 <p>Для начала представим, что мы работаем в компании “Марсианская Почта” (com.martianpost). В нашей компании написано множество приложений, для простоты, ровно два - консольное app-example и веб-приложение web-app-example.</p>
6 <p>Для начала представим, что мы работаем в компании “Марсианская Почта” (com.martianpost). В нашей компании написано множество приложений, для простоты, ровно два - консольное app-example и веб-приложение web-app-example.</p>
7 <p>Создадим эти приложения с помощью<a>Spring Initializr</a>.</p>
7 <p>Создадим эти приложения с помощью<a>Spring Initializr</a>.</p>
8 <p>Исходные коды их можно найти вот<a>здесь</a>.</p>
8 <p>Исходные коды их можно найти вот<a>здесь</a>.</p>
9 <p>Т. к. мы “Марсианская Почта”, то нам очень важно в каждом приложении знать точное марсианское время (а точнее MSD - Mars Sol Date) для всех приложений.</p>
9 <p>Т. к. мы “Марсианская Почта”, то нам очень важно в каждом приложении знать точное марсианское время (а точнее MSD - Mars Sol Date) для всех приложений.</p>
10 <p>Выпустим Spring Boot Starter, решающий эту задачу. В соответствии с<a>документацией</a>выдадим следующие Maven-координаты:</p>
10 <p>Выпустим Spring Boot Starter, решающий эту задачу. В соответствии с<a>документацией</a>выдадим следующие Maven-координаты:</p>
11 &lt;project ...&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; ... &lt;/project&gt;<p>Для больших проектов и технологий, можно вынести отдельно, как саму технологию - martian-time, так и автоконфигурацию - martian-time-autoconfigure и сам стартер - martian-time-spring-boot-starter. Но для педагогических целей мы просто всё напишем в starter-е.</p>
11 &lt;project ...&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; ... &lt;/project&gt;<p>Для больших проектов и технологий, можно вынести отдельно, как саму технологию - martian-time, так и автоконфигурацию - martian-time-autoconfigure и сам стартер - martian-time-spring-boot-starter. Но для педагогических целей мы просто всё напишем в starter-е.</p>
12 <p>Из зависимостей мы оставим нам необходим только spring-boot-starter, правда, добавим его с optional-параметром - наш starter не является starter-ом уровнем всего приложения (как, например, spring-boot-starter-web), поэтому spring-boot-starter starter уже будет добавлен в приложение.</p>
12 <p>Из зависимостей мы оставим нам необходим только spring-boot-starter, правда, добавим его с optional-параметром - наш starter не является starter-ом уровнем всего приложения (как, например, spring-boot-starter-web), поэтому spring-boot-starter starter уже будет добавлен в приложение.</p>
13 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt;<p>Ну и реализуем наш сервис:</p>
13 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt;<p>Ну и реализуем наш сервис:</p>
14 package com.martianpost.martiantime.service; import org.springframework.stereotype.Service; import java.time.Duration; import java.time.ZonedDateTime; @Service public class MartianTimeService { private static final ZonedDateTime MID_DAY = ZonedDateTime.parse("2000-01-06T00:00:00Z"); public double toMarsSolDate(ZonedDateTime zonedDateTime) { double secondsFromMidDay = (double) Duration.between(MID_DAY, zonedDateTime).getSeconds(); return secondsFromMidDay / 88775.244 + 44795.9998; } }<p>Не забудем про тест:</p>
14 package com.martianpost.martiantime.service; import org.springframework.stereotype.Service; import java.time.Duration; import java.time.ZonedDateTime; @Service public class MartianTimeService { private static final ZonedDateTime MID_DAY = ZonedDateTime.parse("2000-01-06T00:00:00Z"); public double toMarsSolDate(ZonedDateTime zonedDateTime) { double secondsFromMidDay = (double) Duration.between(MID_DAY, zonedDateTime).getSeconds(); return secondsFromMidDay / 88775.244 + 44795.9998; } }<p>Не забудем про тест:</p>
15 package com.martianpost.martiantime.service; ... @DisplayName("Сервис MartianTimeService") class MartianTimeServiceTest { private final MartianTimeService service = new MartianTimeService(); @DisplayName("должен конвертировать совпадение полночей в 06.01.2000") @Test void shouldConvertZeroDay() { ZonedDateTime zeroDayUtc = ZonedDateTime.parse("2000-01-06T00:00:00Z"); double result = service.toMarsSolDate(zeroDayUtc); assertEquals(44_795.9998, result, 1e-3); } @DisplayName("Должен конвертировать пример с http://jtauber.github.io/mars-clock/") @Test void shouldConvertExampleFromGithub() { ZonedDateTime time = ZonedDateTime.parse("2020-05-01T09:44:43Z"); double result = service.toMarsSolDate(time); assertEquals(52_018.84093, result, 1e-3); } }<p>Обратим внимание, что если мы подключим данную библиотеку, то сервис не создастcя автоматически (хотя аннотация @Service) - для этого как раз и нужны автоконфигурации.</p>
15 package com.martianpost.martiantime.service; ... @DisplayName("Сервис MartianTimeService") class MartianTimeServiceTest { private final MartianTimeService service = new MartianTimeService(); @DisplayName("должен конвертировать совпадение полночей в 06.01.2000") @Test void shouldConvertZeroDay() { ZonedDateTime zeroDayUtc = ZonedDateTime.parse("2000-01-06T00:00:00Z"); double result = service.toMarsSolDate(zeroDayUtc); assertEquals(44_795.9998, result, 1e-3); } @DisplayName("Должен конвертировать пример с http://jtauber.github.io/mars-clock/") @Test void shouldConvertExampleFromGithub() { ZonedDateTime time = ZonedDateTime.parse("2020-05-01T09:44:43Z"); double result = service.toMarsSolDate(time); assertEquals(52_018.84093, result, 1e-3); } }<p>Обратим внимание, что если мы подключим данную библиотеку, то сервис не создастcя автоматически (хотя аннотация @Service) - для этого как раз и нужны автоконфигурации.</p>
16 <p>Создадим автоконфигурацию для нашего модуля:</p>
16 <p>Создадим автоконфигурацию для нашего модуля:</p>
17 package com.martianpost.martiantime; ... @Configuration @ComponentScan public class MartianTimeAutoConfiguration { }<p>Данный класс ничем не отличается от обычного класса конфигурации. Его главная задача - найти класс, помеченный @Service и создать его бин.</p>
17 package com.martianpost.martiantime; ... @Configuration @ComponentScan public class MartianTimeAutoConfiguration { }<p>Данный класс ничем не отличается от обычного класса конфигурации. Его главная задача - найти класс, помеченный @Service и создать его бин.</p>
18 <p>Но кто найдёт этот класс автоконфигурации? Никто, и нужно дополнительно указать эту автоконфигурацию, чтобы Spring Boot нашёл её:</p>
18 <p>Но кто найдёт этот класс автоконфигурации? Никто, и нужно дополнительно указать эту автоконфигурацию, чтобы Spring Boot нашёл её:</p>
19 # resources/META_INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.martianpost.martiantime.MartianTimeAutoConfiguration<p>Да, теперь можно попробовать подключить данный модуль в наше консольное приложение:</p>
19 # resources/META_INF/spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.martianpost.martiantime.MartianTimeAutoConfiguration<p>Да, теперь можно попробовать подключить данный модуль в наше консольное приложение:</p>
20 &lt;dependency&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; &lt;/dependency&gt;<p>и больше ничего!</p>
20 &lt;dependency&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; &lt;/dependency&gt;<p>и больше ничего!</p>
21 @Autowired private MartianTimeService martianTimeService; @PostConstruct public void printCurrentTime() { double currentMarsSolDate = martianTimeService.toMarsSolDate(ZonedDateTime.now()); System.out.println("MSD: " + currentMarsSolDate); }<p>И получаем:</p>
21 @Autowired private MartianTimeService martianTimeService; @PostConstruct public void printCurrentTime() { double currentMarsSolDate = martianTimeService.toMarsSolDate(ZonedDateTime.now()); System.out.println("MSD: " + currentMarsSolDate); }<p>И получаем:</p>
22 <p>Допустим, нам в каждом веб-приложении необходимо сделать, чтобы это время возвращалось RestController-ом. Но проблема в том, что наш стартер может использоваться как в консольных приложениях, так и в веб-приложениях.</p>
22 <p>Допустим, нам в каждом веб-приложении необходимо сделать, чтобы это время возвращалось RestController-ом. Но проблема в том, что наш стартер может использоваться как в консольных приложениях, так и в веб-приложениях.</p>
23 <p>ОК, мы это можем сделать с помощью @Conditional. Сначала изменим зависимости нашего starter-а:</p>
23 <p>ОК, мы это можем сделать с помощью @Conditional. Сначала изменим зависимости нашего starter-а:</p>
24 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt;<p>Добавим контроллер, который будет создаваться только в веб-приложениях:</p>
24 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt;<p>Добавим контроллер, который будет создаваться только в веб-приложениях:</p>
25 package com.martianpost.martiantime.rest; ... import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @ConditionalOnWebApplication @RestController public class MartianTimeController { private final MartianTimeService martianTimeService; public MartianTimeController(MartianTimeService martianTimeService) { this.martianTimeService = martianTimeService; } @GetMapping("/mds/current") public double getMds() { return martianTimeService.toMarsSolDate(ZonedDateTime.now()); } }<p>Обратите внимание, что за магию включения/выключения бина отвечает аннотация @ConditionalOnWebApplication. Помимо неё существует множество других @Conditional аннотаций -<a>ссылка</a>.</p>
25 package com.martianpost.martiantime.rest; ... import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @ConditionalOnWebApplication @RestController public class MartianTimeController { private final MartianTimeService martianTimeService; public MartianTimeController(MartianTimeService martianTimeService) { this.martianTimeService = martianTimeService; } @GetMapping("/mds/current") public double getMds() { return martianTimeService.toMarsSolDate(ZonedDateTime.now()); } }<p>Обратите внимание, что за магию включения/выключения бина отвечает аннотация @ConditionalOnWebApplication. Помимо неё существует множество других @Conditional аннотаций -<a>ссылка</a>.</p>
26 <p>Проверим, что наше консольное приложение работает:</p>
26 <p>Проверим, что наше консольное приложение работает:</p>
27 <p>Если вывести список зависимостей консольного приложения, то увидим, что spring-mvc там и не присутствует.</p>
27 <p>Если вывести список зависимостей консольного приложения, то увидим, что spring-mvc там и не присутствует.</p>
28 <p>А вот подключив в веб-приложение:</p>
28 <p>А вот подключив в веб-приложение:</p>
29 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; &lt;/dependency&gt;<p>И запустив, мы получим:</p>
29 &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.martianpost&lt;/groupId&gt; &lt;artifactId&gt;martian-time-spring-boot-starter&lt;/artifactId&gt; &lt;version&gt;1.0.0&lt;/version&gt; &lt;/dependency&gt;<p>И запустив, мы получим:</p>
30 GET http://localhost:8080/msd/current 52018.90259483771<p>Магия :-) Но доступная каждому :-)</p>
30 GET http://localhost:8080/msd/current 52018.90259483771<p>Магия :-) Но доступная каждому :-)</p>
31 <p>Целиком пример Вы можете посмотреть на<a>GitHub</a>.</p>
31 <p>Целиком пример Вы можете посмотреть на<a>GitHub</a>.</p>
32  
32