Desenvolvimento baseado em tronco e Spring Boot, ou ramificação, tudo por abstração

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. "" , .





, , , . Git , , .





:





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, , .





, , !





GitHub





TBD





spring initializr





ConfigurationProperties





Spring profiles





Feature flags









trunk





Branch by abstraction




















All Articles