Olá a todos!
O outono acabou, o inverno chegou aos seus direitos legais, as folhas já caíram há muito tempo e os galhos confusos dos arbustos me fazem pensar no meu repositório Git em funcionamento ... Mas um novo projeto começou: uma nova equipe, um repositório limpo que acabou de nevar. "Tudo será diferente aqui" - penso e começo a "pesquisar" no Google sobre Desenvolvimento Baseado em Troncos.
Se você não pode suportar git flow de nenhuma forma, você está cansado de montes desses branches incompreensíveis e regras para eles, se branches como "desenvolver / ivanov" aparecerem em seu projeto, então seja bem-vindo ao subcat! Lá, examinarei os destaques do desenvolvimento baseado em tronco e mostrarei como implementar essa abordagem usando Spring Boot.
Introdução
O Trunk Based Development ( TBD ) é uma abordagem em que todo o desenvolvimento é baseado em um único tronco. Para dar vida a essa abordagem, precisamos seguir três regras básicas:
1) Qualquer commit no tronco não deve interromper a construção.
2) Qualquer commit no tronco deve ser pequeno o suficiente para que não leve mais de 10 minutos para revisar o novo código.
3) A liberação é liberada apenas em uma base tronco.
Você concorda? Agora vamos ver um exemplo.
Initial commit
"", REST json, . spring initializr. Maven Project, Java 8, Spring Boot 2.4.0. :
|
|
|
|
|---|---|---|
Spring Configuration Processor |
DEVELOPER TOOLS |
Generate metadata for developers to offer contextual help and "code completion" when working with custom configuration keys (ex.application.properties/.yml files). |
Validation |
I/O |
JSR-303 validation with Hibernate validator. |
Spring Web |
WEB |
Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container. |
Lombok |
DEVELOPER TOOLS |
Java annotation library which helps to reduce boilerplate code. |
git GitHub . : main, master - trunk, . . . .
. ConfigurationProperties. : sender-email - email-subject - .
NotificationProperties
@Getter
@Setter
@Component
@Validated //,
@ConfigurationProperties(prefix = "notification")
public class NotificationProperties {
@Email //
@NotBlank //
private String senderEmail;
@NotBlank
private String emailSubject;
}
, , .
.
EmailSender
@Slf4j
@Component
public class EmailSender {
/**
*
*/
public void sendEmail(String from, String to, String subject, String text){
log.info("Send email\nfrom: {}\nto: {}\nwith subject: {}\nwith\n text: {}", from, to, subject, text);
}
}
:
Notification
@Getter
@Setter
@Builder
@AllArgsConstructor
public class Notification {
private String text;
private String recipient;
}
:
NotificationService
@Service
@RequiredArgsConstructor
public class NotificationService {
private final EmailSender emailSender;
private final NotificationProperties notificationProperties;
public void notify(Notification notification){
String from = notificationProperties.getNotificationSenderEmail();
String to = notification.getRecipient();
String subject = notificationProperties.getNotificationEmailSubject();
String text = notification.getText();
emailSender.sendEmail(from, to, subject, text);
}
}
:
NotificationController
@RestController
@RequiredArgsConstructor
public class NotificationController {
private final NotificationService notificationService;
@PostMapping("/notification/notify")
public void notify(Notification notification){
notificationService.sendNotification(notification);
}
}
, TBD . NotificationService:
NotificationServiceTest
@SpringBootTest
class NotificationServiceTest {
@Autowired
NotificationService notificationService;
@Autowired
NotificationProperties properties;
@MockBean
EmailSender emailSender;
@Test
void emailNotification() {
Notification notification = Notification.builder()
.recipient("test@email.com")
.text("some text")
.build();
notificationService.notify(notification);
ArgumentCaptor<string> emailCapture = ArgumentCaptor.forClass(String.class);
verify(emailSender, times(1))
.sendEmail(emailCapture.capture(),emailCapture.capture(),emailCapture.capture(),emailCapture.capture());
assertThat(emailCapture.getAllValues())
.containsExactly(properties.getSenderEmail(),
notification.getRecipient(),
properties.getEmailSubject(),
notification.getText()
);
}
}
NotificationController
NotificationControllerTest
@WebMvcTest(controllers = NotificationController.class)
class NotificationControllerTest {
@Autowired
MockMvc mockMvc;
@Autowired
ObjectMapper objectMapper;
@MockBean
NotificationService notificationService;
@SneakyThrows
@Test
void testNotify() {
ArgumentCaptor<notification> notificationArgumentCaptor = ArgumentCaptor.forClass(Notification.class);
Notification notification = Notification.builder()
.recipient("test@email.com")
.text("some text")
.build();
mockMvc.perform(post("/notification/notify")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(notification)))
.andExpect(status().isOk());
verify(notificationService, times(1)).notify(notificationArgumentCaptor.capture());
assertThat(notificationArgumentCaptor.getValue())
.usingRecursiveComparison()
.isEqualTo(notification);
}
}
, rebase, trunk - .
, - code review 10 .
NotificationTask
@Component
@EnableScheduling
@RequiredArgsConstructor
public class NotificationTask {
private final NotificationService notificationService;
private final NotificationProperties notificationProperties;
@Scheduled(fixedDelay = 1000)
public void notifySubscriber(){
notificationService.notify(Notification.builder()
.recipient(notificationProperties.getSubscriberEmail())
.text("Notification is worked")
.build());
}
}
:
"org.mockito.exceptions.verification.TooManyActualInvocations".
, sendEmail, , .
. initialDelay, , . . @EnableScheduling @Profile , , "test".
SchedulingConfig
@Profile("!test")
@Configuration
@EnableScheduling
public class SchedulingConfig {}
, application.yaml :
application.yaml
spring:
profiles:
active: test
notification:
email-subject: Auto notification
sender-email: robot@somecompany.com
, , , main , .
, , , .
, - , , .. - . : , .
Feature flags, . rebase, trunk.
, . TBD : , trunk. .
, trunk, , , .
git :
git checkout <hash>
, c , .
git checkout -b Release_1.0.0 git tag 1.0.0 git push -u origin Release_1.0.0 git push origin 1.0.0
! staging, production.
, :
1) -
2) trunk
3) Hotfix, Cherry-pick trunk
"" , . .
Feature flags
: . , production . , , , , feature flag.
, , , , , - , .
. production oracle ( ), h2.
()
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-data-jpa</artifactid>
</dependency>
<dependency>
<groupid>com.oracle.ojdbc</groupid>
<artifactid>ojdbc10</artifactid>
</dependency>
<dependency>
<groupid>com.h2database</groupid>
<artifactid>h2</artifactid>
</dependency>
, . , boolean. "persistence", .
FeatureProperties
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "features.active")
public class FeatureProperties {
boolean persistence;
}
application.yaml features.active.persistence: on (spring , on==true).
, .
Entity.
!
Notification (Entity)
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Notification {
@Id
@GeneratedValue
private Long id;
private String text;
private String recipient;
@CreationTimestamp
private LocalDateTime time;
}
NotificationRepository
public interface NotificationRepository extends CrudRepository<notification, long=""> {
}
NotificationService NotificationRepository FeatureProperties , notify save, if.
NotificationService (Feature flag)
@Service
@RequiredArgsConstructor
public class NotificationService {
private final EmailSender emailSender;
private final NotificationProperties notificationProperties;
private final FeatureProperties featureProperties;
@Nullable
private final NotificationRepository notificationRepository;
public void notify(Notification notification){
String from = notificationProperties.getSenderEmail();
String to = notification.getRecipient();
String subject = notificationProperties.getEmailSubject();
String text = notification.getText();
emailSender.sendEmail(from, to, subject, text);
if(featureProperties.isPersistence()){
notificationRepository.save(notification);
}
}
}
, @Nullable NotificationRepository , Spring UnsatisfiedDependencyException, .
, , , url .
. , , features.active.persistence: off (spring , off==false).
DataJpaConfig
@Configuration
@ConditionalOnProperty(prefix = "features.active", name = "persistence",
havingValue = "false", matchIfMissing = true)
@EnableAutoConfiguration(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
public class DataJpaConfig {
}
features.active.persistence: off . , .
, spring , :
--spring.config.additional-location=file:/etc/config/features.yaml
VM, :
-Dfeatures.active.persistence=true
:
1) ,
2) , feature ,
. , feature , : "if (flag) {…}" , , " ", .
Branch by Abstraction
, .
, : EMAIL, SMS PUSH. "" , .
:
1)
2)
3)
4)
5)
NotificationService , EmailNotificationService. Inellij IDEA :
1) , Refactor/Extract interface…
2) "Rename original class and use interface where possible"
3) "Rename implementation class to" "EmailNotificationService"
4) "Members to from interface" "notify"
5) "Refactor"
NotificationService, EmailNotificationService .
rebase, trunk.
. , Enum.
NotificationType
public enum NotificationType {
EMAIL, SMS, PUSH, UNKNOWN
}
"":
SmsSender PushSender.
Senders
@Slf4j
@Component
public class SmsSender {
/**
*
*/
public void sendSms(String phoneNumber, String text){
log.info("Send sms {}\nto: {}\nwith text: {}", phoneNumber, text);
}
}
@Slf4j
@Component
public class PushSender {
/**
* push
*/
public void push(String id, String text){
log.info("Push {}\nto: {}\nwith text: {}", id, text);
}
}
MultipleNotificationService, " ".
MultipleNotificationService - switch case
@Service
@RequiredArgsConstructor
public class MultipleNotificationService implements NotificationService {
private final EmailSender emailSender;
private final PushSender pushSender;
private final SmsSender smsSender;
private final NotificationProperties notificationProperties;
private final NotificationRepository notificationRepository;
@Override
public void notify(Notification notification) {
String from = notificationProperties.getSenderEmail();
String to = notification.getRecipient();
String subject = notificationProperties.getEmailSubject();
String text = notification.getText();
NotificationType notificationType = notification.getNotificationType();
switch (notificationType!=null ? notificationType : NotificationType.UNKNOWN) {
case PUSH:
pushSender.push(to, text);
break;
case SMS:
smsSender.sendSms(to, text);
break;
case EMAIL:
emailSender.sendEmail(from, to, subject, text);
break;
default:
throw new UnsupportedOperationException("Unknown notification type: " + notification.getNotificationType());
}
notificationRepository.save(notification);
}
}
, , NotificationServiceTest :
"expected single matching bean but found 2: emailNotificationService, multipleNotificationService".
@Primary - EmailNotificationService.
@Primary - , .
- @Service , Spring , unit , "new".
Spring .
MultipleNotificationServiceTest
@SpringBootTest
class MultipleNotificationServiceTest {
@Autowired
MultipleNotificationService multipleNotificationService;
@Autowired
NotificationProperties properties;
@MockBean
EmailSender emailSender;
@MockBean
PushSender pushSender;
@MockBean
SmsSender smsSender;
@Test
void emailNotification() {
Notification notification = Notification.builder()
.recipient("test@email.com")
.text("some text")
.notificationType(NotificationType.EMAIL)
.build();
multipleNotificationService.notify(notification);
ArgumentCaptor<string> emailCapture = ArgumentCaptor.forClass(String.class);
verify(emailSender, times(1))
.sendEmail(emailCapture.capture(),emailCapture.capture(),emailCapture.capture(),emailCapture.capture());
assertThat(emailCapture.getAllValues())
.containsExactly(properties.getSenderEmail(),
notification.getRecipient(),
properties.getEmailSubject(),
notification.getText()
);
}
@Test
void pushNotification() {
Notification notification = Notification.builder()
.recipient("id:1171110")
.text("some text")
.notificationType(NotificationType.PUSH)
.build();
multipleNotificationService.notify(notification);
ArgumentCaptor<string> captor = ArgumentCaptor.forClass(String.class);
verify(pushSender, times(1))
.push(captor.capture(),captor.capture());
assertThat(captor.getAllValues())
.containsExactly(notification.getRecipient(), notification.getText());
}
@Test
void smsNotification() {
Notification notification = Notification.builder()
.recipient("+79157775522")
.text("some text")
.notificationType(NotificationType.SMS)
.build();
multipleNotificationService.notify(notification);
ArgumentCaptor<string> captor = ArgumentCaptor.forClass(String.class);
verify(smsSender, times(1))
.sendSms(captor.capture(),captor.capture());
assertThat(captor.getAllValues())
.containsExactly(notification.getRecipient(), notification.getText());
}
@Test
void unsupportedNotification() {
Notification notification = Notification.builder()
.recipient("+79157775522")
.text("some text")
.build();
assertThrows(UnsupportedOperationException.class, () -> {
multipleNotificationService.notify(notification);
});
}
}
rebase, , trunk, switch-case.
"", , "" , , . "". , , GitHub.
, : rebase, , trunk.
, . feature .
:
boolean multipleSenders;
EmailNotificationService ( @Primary):
", , features.active.multiple-senders (matchIfMissing) false"
@ConditionalOnProperty(prefix = "features.active",
name = "multiple-senders",
havingValue = "false",
matchIfMissing = true)
MultipleNotificationService "" :
", , features.active.multiple-senders (matchIfMissing) true"
@ConditionalOnProperty(prefix = "features.active",
name = "multiple-senders",
havingValue = "true",
matchIfMissing = true)
, .
, feature , .
rebase, , trunk. , , .
production , , feature .
, , Hotfix, code review , … , .
Trunk Based Development - , - , , , " " .
Trunk Based Development - , , Spring Boot, , .
, , !