Skip to content

Commit 25dae3a

Browse files
committed
Part 25: update СreateSpringBootStarter.md
1 parent 78517a4 commit 25dae3a

File tree

2 files changed

+219
-250
lines changed

2 files changed

+219
-250
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
См. исходники и дополнения (RUS): https://otus.ru/nest/post/1384/
2+
________________________________________________________________________________________________________________________
3+
### Создаём свой Spring Boot Starter
4+
5+
Современный Spring, а если быть точнее — специальный фреймворк Spring Boot, позволяют с минимальными усилиями подключать
6+
ту или иную технологию. Необходимость создавать десятки служебных бинов ушла в прошлое. Для этого имеются всевозможные
7+
starter-ы и специальные Maven/Gradle зависимости, которые необходимо только подключить в проект.
8+
9+
Например, подключив в проект всего одну зависимость:
10+
11+
<dependency>
12+
<groupId>org.springframework.boot</groupId>
13+
<artifactId>spring-boot-starter-jdbc</artifactId>
14+
</dependency>
15+
16+
Мы имеем уже созданный в контексте DataSource, созданный по свойствам в application.properties и другими классами, вроде
17+
NamedParameterJdbcTemplate. Подобные starter основаны на двух специальных функциональностях:
18+
- Spring Boot — AutoConfigurations ;
19+
- Spring Boot — Conditional ;
20+
21+
Покажем, как использовать данные функциональности на примере создания собственного Spring Boot Starter-a.
22+
23+
________________________________________________________________________________________________________________________
24+
#### Наш Spring Boot Starter
25+
26+
Для начала представим, что мы работаем в компании “Марсианская Почта” (com.martianpost). В нашей компании написано
27+
множество приложений, для простоты, ровно два — консольное app-example и веб-приложение web-app-example.
28+
29+
Создадим эти приложения с [помощью Spring Initializer](https://start.spring.io/);
30+
31+
Исходный код их [можно найти на GitHub](https://github.com/ydvorzhetskiy/starter-example);
32+
33+
Т.к. мы “Марсианская Почта”, то нам очень важно в каждом приложении знать точное марсианское время (а точнее MSD — Mars
34+
Sol Date) для всех приложений.
35+
36+
Выпустим Spring Boot Starter, решающий эту задачу. В соответствии [с документацией (см. Док.)](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.custom-starter.naming)
37+
выдадим следующие Maven-координаты:
38+
39+
<project ...>
40+
<groupId>com.martianpost</groupId>
41+
<artifactId>martian-time-spring-boot-starter</artifactId>
42+
<version>1.0.0</version>
43+
44+
...
45+
</project>
46+
47+
Для больших проектов и технологий, можно вынести отдельно, как саму технологию — martian-time, так и авто-конфигурацию —
48+
martian-time-autoconfigure и сам стартер — martian-time-spring-boot-starter. Но для педагогических целей мы просто всё
49+
напишем в starter-е.
50+
51+
Из зависимостей мы оставим нам необходим только spring-boot-starter, правда, добавим его с optional-параметром — наш
52+
starter не является starter-ом уровнем всего приложения (как, например, spring-boot-starter-web), поэтому
53+
spring-boot-starter уже будет добавлен в приложение.
54+
55+
<dependency>
56+
<groupId>org.springframework.boot</groupId>
57+
<artifactId>spring-boot-starter</artifactId>
58+
<optional>true</optional>
59+
</dependency>
60+
61+
Ну и реализуем наш сервис:
62+
63+
package com.martianpost.martiantime.service;
64+
65+
import org.springframework.stereotype.Service;
66+
67+
import java.time.Duration;
68+
import java.time.ZonedDateTime;
69+
70+
@Service
71+
public class MartianTimeService {
72+
73+
private static final ZonedDateTime MID_DAY
74+
= ZonedDateTime.parse("2000-01-06T00:00:00Z");
75+
76+
public double toMarsSolDate(ZonedDateTime zonedDateTime) {
77+
double secondsFromMidDay
78+
= (double) Duration.between(MID_DAY, zonedDateTime).getSeconds();
79+
return secondsFromMidDay / 88775.244 + 44795.9998;
80+
}
81+
}
82+
83+
Не забудем про тест:
84+
85+
package com.martianpost.martiantime.service;
86+
87+
...
88+
89+
@DisplayName("Сервис MartianTimeService")
90+
class MartianTimeServiceTest {
91+
92+
private final MartianTimeService service = new MartianTimeService();
93+
94+
@DisplayName("должен конвертировать совпадение полночей в 06.01.2000")
95+
@Test
96+
void shouldConvertZeroDay() {
97+
ZonedDateTime zeroDayUtc = ZonedDateTime.parse("2000-01-06T00:00:00Z");
98+
double result = service.toMarsSolDate(zeroDayUtc);
99+
assertEquals(44_795.9998, result, 1e-3);
100+
}
101+
102+
@DisplayName("Должен конвертировать пример с http://jtauber.github.io/mars-clock/")
103+
@Test
104+
void shouldConvertExampleFromGithub() {
105+
ZonedDateTime time = ZonedDateTime.parse("2020-05-01T09:44:43Z");
106+
double result = service.toMarsSolDate(time);
107+
assertEquals(52_018.84093, result, 1e-3);
108+
}
109+
}
110+
111+
Обратим внимание, что если мы подключим данную библиотеку, то сервис не создастcя автоматически (хотя аннотация
112+
@Service) — для этого как раз и нужны авто-конфигурации.
113+
114+
Создадим авто-конфигурацию для нашего модуля:
115+
116+
package com.martianpost.martiantime;
117+
118+
...
119+
120+
@Configuration
121+
@ComponentScan
122+
public class MartianTimeAutoConfiguration {
123+
}
124+
125+
Данный класс ничем не отличается от обычного класса конфигурации. Его главная задача — найти класс, помеченный @Service
126+
и создать его бин. Но кто найдёт этот класс авто-конфигурации? Никто, и нужно дополнительно указать эту авто-конфигурацию,
127+
чтобы Spring Boot нашёл её:
128+
129+
# resources/META_INF/spring.factories
130+
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
131+
com.martianpost.martiantime.MartianTimeAutoConfiguration
132+
133+
Да, теперь можно попробовать подключить данный модуль в наше консольное приложение:
134+
135+
<dependency>
136+
<groupId>com.martianpost</groupId>
137+
<artifactId>martian-time-spring-boot-starter</artifactId>
138+
<version>1.0.0</version>
139+
</dependency>
140+
141+
И больше ничего!
142+
143+
@Autowired
144+
private MartianTimeService martianTimeService;
145+
146+
@PostConstruct
147+
public void printCurrentTime() {
148+
double currentMarsSolDate = martianTimeService.toMarsSolDate(ZonedDateTime.now());
149+
System.out.println("MSD: " + currentMarsSolDate);
150+
}
151+
152+
И получаем:
153+
154+
MSD: 52018.87995339051
155+
156+
Допустим, нам в каждом веб-приложении необходимо сделать, чтобы это время возвращалось RestController-ом. Но проблема в
157+
том, что наш стартер может использоваться как в консольных приложениях, так и в веб-приложениях. ОК, мы это можем
158+
сделать с помощью @Conditional.
159+
160+
Сначала изменим зависимости нашего starter-а:
161+
162+
<dependency>
163+
<groupId>org.springframework.boot</groupId>
164+
<artifactId>spring-boot-starter-web</artifactId>
165+
<optional>true</optional>
166+
</dependency>
167+
168+
Добавим контроллер, который будет создаваться только в веб-приложениях:
169+
170+
package com.martianpost.martiantime.rest;
171+
172+
...
173+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
174+
175+
@ConditionalOnWebApplication
176+
@RestController
177+
public class MartianTimeController {
178+
179+
private final MartianTimeService martianTimeService;
180+
181+
public MartianTimeController(MartianTimeService martianTimeService) {
182+
this.martianTimeService = martianTimeService;
183+
}
184+
185+
@GetMapping("/mds/current")
186+
public double getMds() {
187+
return martianTimeService.toMarsSolDate(ZonedDateTime.now());
188+
}
189+
}
190+
191+
Обратите внимание, что за магию включения/выключения бина отвечает аннотация @ConditionalOnWebApplication. Помимо неё
192+
существует множество других @Conditional аннотаций - Condition Annotations:
193+
https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.developing-auto-configuration.condition-annotations
194+
195+
Проверим, что наше консольное приложение работает:
196+
197+
MSD: 52018.896467003244
198+
199+
Если вывести список зависимостей консольного приложения, то увидим, что spring-mvc там и не присутствует. А вот
200+
подключив в веб-приложение:
201+
202+
<dependency>
203+
<groupId>org.springframework.boot</groupId>
204+
<artifactId>spring-boot-starter</artifactId>
205+
</dependency>
206+
<dependency>
207+
<groupId>com.martianpost</groupId>
208+
<artifactId>martian-time-spring-boot-starter</artifactId>
209+
<version>1.0.0</version>
210+
</dependency>
211+
212+
И запустив приложение, мы получим:
213+
214+
GET http://localhost:8080/msd/current
215+
52018.90259483771
216+
217+
Магия доступная каждому !!!
218+
219+
Код [примера целиком на GitHub](https://github.com/ydvorzhetskiy/starter-example);

0 commit comments

Comments
 (0)