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
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency><p>мы имеем уже созданный в контексте DataSource, созданный по свойствам в application.properties и другими классами, вроде NamedParameterJdbcTemplate.</p>
4
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency><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
<project ...> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> ... </project><p>Для больших проектов и технологий, можно вынести отдельно, как саму технологию - martian-time, так и автоконфигурацию - martian-time-autoconfigure и сам стартер - martian-time-spring-boot-starter. Но для педагогических целей мы просто всё напишем в starter-е.</p>
11
<project ...> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> ... </project><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
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <optional>true</optional> </dependency><p>Ну и реализуем наш сервис:</p>
13
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <optional>true</optional> </dependency><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
<dependency> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency><p>и больше ничего!</p>
20
<dependency> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency><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
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <optional>true</optional> </dependency><p>Добавим контроллер, который будет создаваться только в веб-приложениях:</p>
24
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <optional>true</optional> </dependency><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
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency><p>И запустив, мы получим:</p>
29
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.martianpost</groupId> <artifactId>martian-time-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency><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