Halo semuanya!
Musim gugur telah berakhir, musim dingin telah menjadi hak hukumnya, dedaunan telah lama berguguran dan cabang semak yang membingungkan membuat saya berpikir tentang repositori Git saya yang berfungsi ... Tetapi sebuah proyek baru telah dimulai: sebuah tim baru, sebuah repositori yang bersih seperti yang baru saja turun salju. "Semuanya akan berbeda di sini" - Saya pikir dan mulai "google" tentang Pengembangan Berbasis Batang.
Jika Anda tidak dapat mendukung aliran git, Anda bosan dengan tumpukan cabang dan aturan yang tidak dapat dipahami ini, jika cabang seperti "develop / ivanov" muncul di proyek Anda, selamat datang di subcat! Di sana saya akan membahas highlight dari Trunk Based Development dan menunjukkan kepada Anda bagaimana menerapkan pendekatan ini menggunakan Spring Boot.
pengantar
Trunk Based Development ( TBD ) adalah pendekatan di mana semua pengembangan didasarkan pada satu trunk. Untuk menghidupkan pendekatan ini, kita perlu mengikuti tiga aturan dasar:
1) Komitmen apa pun ke trunk tidak boleh merusak build.
2) Komitmen apa pun ke trunk harus kecil, sehingga tidak perlu lebih dari 10 menit untuk meninjau kode baru.
3) Pelepasan hanya dirilis pada basis trunk.
Apakah kamu setuju Sekarang mari kita lihat contohnya.
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, , .
, , !