Tổng quan
Disease Common là hệ thống cung các dịch vụ đăng ký ban đầu, kiểm tra triệu chứng, hỗ trợ theo dõi, tư vấn và cảnh báo về sức khoẻ cho người dùng thông qua các OA trên ứng dụng LINE Chat.
Các sản phẩm tiêu biểu của Disease Common có thể kể đến như Diabetes OA (theo dõi và cảnh báo bệnh đau đầu), Rheumatoid arthritis OA (cung cấp thông tin về bệnh viêm khớp dạng thấp), Psoriasis OA (cung cấp thông tin về bệnh vẩy nến) và còn nhiều hơn nữa.
Để làm được điều này, hệ thống cung cấp các tính năng cho hệ thống LINE OA thông qua các Webhook. Hiện tại, hệ thống đang được triển khai với những công nghệ như sau:
- Spring Boot 2.7.10: hệ thống sử dụng Spring Boot 2.7 và được triển khai bằng Java 11.
- Spring Ecosystem: Spring Security cho việc xác thực giữa các service, Spring Batch cho các tác vụ gửi thông báo cho người dùng và tổng hợp báo cáo.
- MySQL: cơ sở dữ liệu lưu trữ chính của hệ thống.
- Redis: sử dụng với mục đích giảm tải cho hệ thống thông qua cache.
- Kafka: sử dụng cho việc truyền tải dữ liệu giữa các service và các tác vụ bất đồng bộ (asynchronous).
Hiện trạng
Sau 5 năm, Spring Boot đã triển khai major update tiếp theo với phiên bản 3. Với thực tế phiên bản tiền nhiệm đã kết thúc EOL, đội ngũ phát triển quyết định thực hiện quá trình nâng cấp từ Spring Boot 2 lên 3. Việc nâng cấp này đảm bảo hệ thống nằm trong trạng thái an toàn về security. Bên cạnh đó, các kĩ sư có thể tận dụng được những tính năng mới của JDK 17 trong quá trình phát triển và nâng cao hiệu suất sản phẩm.
Disease Common gồm nhiều service vận hành cùng nhau. Chúng có thể được phân chia vào 3 khu vực lớn bao gồm:
- Public Zone: bao gồm các service thực hiện tương tác với dịch vụ bên ngoài, như LINE OA Platform, các nhà cung cấp nội dung thứ ba (Content Provider) và hệ thống Admin.
- Private Zone - Disease Private: bao gồm các service thực hiện xử lý core business.
- Private Zone - Postman Private: bao gồm các service thực hiện xử lý các tác vụ gửi message/notification đồng bộ và tổng hợp báo cáo.
Khi so sánh những thay đổi của Spring Boot 3 với hệ thống hiện tại, chúng tôi kết luận hiện trạng của quá trình nâng cấp như sau:
Thay đổi/Nâng cấp chính của Spring Boot 3 | Đánh giá thay đổi với Disease Common | |
---|---|---|
1 | Sử dụng JDK 17 trở lên |
Thay đổi lớn |
2 | Nâng cấp Spring Framework 6.x |
Thay đổi lớn |
3 | Thay đổi về CRUD repository của Spring Data 2022.0 |
Không ảnh hưởng |
4 | Thay đổi về kiểu trả về của Spring Kafka 3 |
Thay đổi nhỏ |
5 | Thay đổi cách triển khai của Spring Batch |
Thay đổi lớn |
6 | Thay đổi config của Hibernate và Spring Data Redis |
Thay đổi nhỏ |
Chiến lược nâng cấp
Chúng tôi tiến hành nâng cấp theo hai giai đoạn nhỏ:
Giai đoạn | Tác vụ | |
---|---|---|
1 | Trước khi nâng cấp lên Spring Boot 3 |
|
2 | Trong khi nâng cấp lên Spring Boot 3 |
|
Quá trình nâng cấp
1. Nâng cấp Java 17
Chúng tôi chọn phiên bản JDK 17 vì đây là phiên bản Long-Term-Support (LTS), và cũng ổn định hơn phiên bản 21. Những thay đổi của phiên bản 17 so với phiên bản 11 được liệt kê tại JDK Enhancement-Proposal.
Quá trình nâng cấp này gồm 3 bước: nâng cấp source code, nâng cấp Gradle và nâng cấp trên cloud.
- Nâng cấp Java 17
build.gradle
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
- Nâng cấp phiên bản Gradle
$ ./gradlew wrapper --gradle-version 8.5
- Nâng cấp JDK trên deployment: chúng tôi thực hiện việc nâng cấp JDK thông qua Ansible script. Một điều cần lưu ý l à môi trường Production thường cài đặt các config về network hay tường lửa cho mục đích bảo mật. Vậy nên chúng ta cần rà soát các config này và cập nhật script để quá trình nâng cấp được suôn sẻ nhất.
- name: Update CA certificates
yum:
name: ca-certificates
state: latest
remote_user: irteamsu
become: true
- name: Import the Eclipse Temurin GPG key
rpm_key:
state: present
key: https://packages.adoptium.net/artifactory/api/gpg/key/public
remote_user: irteamsu
become: true
- name: Add Eclipse Temurin repository
yum_repository:
name: Adoptium
description: Eclipse Temurin YUM Repository
baseurl: https://packages.adoptium.net/artifactory/rpm/centos/7/$basearch
gpgcheck: yes
gpgkey: https://packages.adoptium.net/artifactory/api/gpg/key/public
enabled: yes
remote_user: irteamsu
become: true
- name: java install
yum:
name: temurin-17-jdk.x86_64
state: present
remote_user: irteamsu
become: true
- name: change default java
shell: "/usr/sbin/alternatives --set java /usr/lib/jvm/temurin-17-jdk/bin/java"
remote_user: irteamsu
become: true
# other configurations
2. Thay thế những deprecation của Spring Boot 2.7
Auto-Configuration Registration
Spring Boot 2.7 giới thiệu một cách triển khai mới cho file spring.factories trước đây.
Auto-configuration Files Spring Boot 2.7 introduced a new META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports file for registering auto-configurations, while maintaining backwards compatibility with registration in spring.factories. With this release, support for registering auto-configurations in spring.factories using the org.springframework.boot.autoconfigure.EnableAutoConfiguration key has been removed in favor of the imports file. Other entries in spring.factories under other keys are unaffected. |
Hiện tại, hệ thống Disease Common đang ứng dụng cách triển khai này với những common service, ví dụ như Authentication service dưới đây:
- Một service trong hệ thống import authentication-client và sử dụng bean AuthenticationClient được định nghĩa bởi AutoConfiguration trong module.
- Nếu nâng cấp hệ thống lên Spring Boot 3, file spring.factories sẽ gây ra lỗi, từ đó tạo ra lỗi runtime vì bean AuthenticationClient không hề tồn tại.
Cách triển khai cũ của file này được xoá khỏi Spring Boot 3.0. Để xử lý, chúng ta sẽ triển khai thêm một file import như sau:
com.linecorp.<sub_packages>.AuthenticationClientAutoConfiguration
Spring Security
Tại phiên bản mới của Spring Security, chúng tôi thực hiện những thay đổi sau:
- Sử dụng SecurityFilterChain để thay thế WebSecurityConfigurerAdapter.
- Sử dụng method requestMatchers(antMatcher()) để thay thế antMatchers().
- Sử dụng authorizeHttpRequests() để thay thế authorizeRequests().
Spring WebMVC
Tại phiên bản mới của Spring WebMVC, chúng tôi thực hiện hai thay đổi lớn sau:
- Cập nhật trailing slash matching: Một thay đổi lớn của Spring WebMVC là việc không còn hỗ trợ trailing slash matching. Ví dụ, hai api GET /users và GET /users/ sẽ không đồng nhất với nhau như trước. Để có được behavior như cũ, chúng tôi thiết lập như sau:
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void configurePathMatching(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
}
- Sử dụng HandlerInterceptor để thay thế HandlerInterceptorAdapter.
3. Nâng cấp phiên bản Spring Boot 3
Bây giờ, chúng ta thực hiện nâng cấp phiên bản của Spring Boot lên phiên bản 3:
build.gradle
plugins {
id 'org.springframework.boot' version '3.2.0' apply false
id 'io.spring.dependency-management' version '1.1.4' apply false
}
Jakarta EE 10
Ngay khi thực hiện thay đổi trên, codebase không thể compile được và trả về lỗi như sau:
Execution failed for task ':batch:compileJava'.
> java.lang.NoClassDefFoundError: javax/persistence/Entity
Lỗi này xảy ra do Java EE 10 có sự thay đổi về namespace, chuyển từ javax.* sang jakarta.* cho hầu hết các package.
Sử dụng migration tool của IDEA, chúng ta có thể nhanh chóng convert namespace cũ sang namespace mới.
Chúng tôi đã thực hiện migrate các package của Java Persistence và đạt được kết quả như sau:
- import javax.persistence.Entity;
- import javax.persistence.Id;
- import javax.persistence.Table;
+ import jakarta.persistence.Entity;
+ import jakarta.persistence.Id;
+ import jakarta.persistence.Table;
@ConstructorBinding annotation
Loại bỏ annotation @ConfigurationBinding: Ở phiên bản Spring Boot 3, những ConfigurationProperties class sẽ không cần được thêm annotation này.
- @ConstructorBinding
@ConfigurationProperties(prefix = "obs.callback")
data class ObsCallbackProperties(
val authToken: String,
)
MySQL JDBC Driver và Spring Data JPA
Sử dụng dependency com.mysql:mysql-connector-j thay thế cho mysql:mysql-connector-java.
Ngoài việc thay thế tên thư viện, chúng ta không cần cập nhật gì thêm trong codebase.
- dependency "mysql:mysql-connector-java:$mysqlVersion"
+ dependency "com.mysql:mysql-connector-j:$mysqlVersion"
Sử dụng config CamelCaseUnderscoresNamingStrategy thay cho SpringPhysicalNamingStategy do phiên bản mới của Hibernate sẽ không còn hỗ trợ config.
spring:
jpa:
hibernate:
- naming.physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
+ naming.physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
Spring Kafka
Ở phiên bản mới, KafkaTemplate sẽ trả về CompletableFuture thay vì ListenableFuture như trước đây.
Vì vậy, chúng tôi thực hiện migrate từ phương thức addCallback() sang phương thức whenComplete() để xử lý kết quả của KafkaTemplate:
- Phiên bản trước
kafkaTemplate.send(record)
.addCallback(successCallback(), failureCallback());
- Phiên bản mới
kafkaTemplate.send(record).whenComplete((result, ex) -> {
// handle accordingly
});
Spring Redis
Ở phiên bản mới, config của Spring Redis sẽ được chuyển từ spring.redis.* vào spring.data.redis.* trong application config:
- Phiên bản trước
application.yml
spring:
redis:
# Redis configurations
- Phiên bản mới
application.yml
spring:
data:
redis:
# Redis configurations
Thay đổi Spring Cloud Sleuth sang Micrometer Tracing
Thư viện Spring Cloud Sleuth sẽ hoàn toàn được thay thế bởi Micrometer Tracing. Các config liên quan sẽ được chuyển sang thư viện Spring Boot Actuator.
Với Disease Common, chúng tôi loại bỏ thư viện này và sử dụng Micrometer / Spring Boot Actuator cho việc monitor các metrics.
Apache HTTP Client
Từ phiên bản Spring Framework 6, Apache HttpClient sẽ được thay thế hoàn toàn bởi Apache HttpClient 5.
Chúng tôi thực hiện loại bỏ thư viện cũ và thay thế bằng thư viện mới.
- dependency "org.apache.httpcomponents:httpclient"
+ dependency "org.apache.httpcomponents.client5:httpclient5"
Spring Batch
Disease Common sử dụng Spring Batch để triển khai hai service Disease Batch và Postman Batch.
Spring Boot 3 đã nâng cấp gói Spring Batch lên phiên bản 5 bao gồm nhiều thay đổi lớn nhỏ. Trong số đó, hai thay đổi lớn nhất của phiên bản này là việc loại bỏ annotation @EnableBatchProcessing và cách triển khai mới cho JobBuilderFactory và StepBuilderFactory.
- Với @EnableBatchProcessing: thực hiện loại bỏ @EnableBatchProcessing khỏi Application
- @EnableBatchProcessing
@SpringBootApplication
public class BatchApplication {
public static void main(String[] args) {
System.exit(SpringApplication.exit(new SpringApplicationBuilder().sources(BatchApplication.class).run(args)));
}
}
- Với JobBuilderFactory và StepBuilderFactory: sử dụng JobRepository và PlatformTransactionManager để thay thế cách triển khai cũ
-
- Phiên bản trước
// Phiên bản Spring Batch 4
@Configuration
@RequiredArgsConstructor
public class SendingReservationConfig {
private final JobBuilderFactory jobBuilderFactory;
private final StepBuilderFactory stepBuilderFactory;
@Bean
public Job sendingReservationJob(Step sendingReservationStep) {
return jobBuilderFactory.get(SENDING_RESERVATION_JOB)
.start(sendingReservationStep)
.build();
}
@Bean
@JobScope
public Step sendingReservationStep(SendingReservationTasklet tasklet) {
return stepBuilderFactory.get(SENDING_RESERVATION_STEP)
.tasklet(tasklet)
.build();
}
}
-
- Phiên bản mới
// Phiên bản Spring Batch 5
@Configuration
@RequiredArgsConstructor
public class SendingReservationConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager platformTransactionManager;
@Bean
public Job sendingReservationJob(Step sendingReservationStep) {
return new JobBuilder(SENDING_RESERVATION_JOB, jobRepository)
.start(sendingReservationStep)
.build();
}
@Bean
@JobScope
public Step sendingReservationStep(SendingReservationTasklet tasklet) {
return new StepBuilder(SENDING_RESERVATION_STEP, jobRepository)
.tasklet(tasklet, platformTransactionManager)
.build();
}
}
- Với deprecated API khác: chúng tôi cũng thực hiện các cập nhật liên quan đến các deprecated API được mô tả tại documentation của Spring Batch 5.
Thay đổi các thư viện khác
Song song với việc nâng cấp các thư viện thuộc hệ sinh thái của Spring, đội ngũ phát triển đã tiến hành nâng cấp các thư viện khác được sử dụng trong codebase.
- LINE Internal Service: nâng cấp nClavis (thư viện hỗ trợ mã hoá), L-station (thư viện tương tác với in-house service), etc.
- Thư viện common: nâng cấp Swagger 2 lên SpringDoc OpenAPI, lombok, logback, etc.
Với mỗi thư viện, chúng ta cần lưu ý cách triển khai mới và cách thiết lập config mới. Ví dụ, thư viện SpringDoc đã có sự thay đổi về config so với phiên bản Swagger trước đó:
application-apidoc.yml
- springfox.documentation.enabled: false
+ springdoc.api-docs.enabled: false
+ springdoc.swagger-ui.enabled: false
Vấn đề phát sinh
Trong quá trình nâng cấp, do code hiện tại không tương thích với phiên bản mới của Spring Data JPA, một vài @Bean trong ApplicationContext tạo ra một vòng kín. Điều này khiến codebase compile không thành công:
authenticationController defined in file [/disease/microservice/authentication/auth/controller/AuthenticationController.class]
↓
authenticationService defined in file [/disease/microservice/authentication/auth/service/AuthenticationService.class]
↓
profileService defined in file [/disease/microservice/authentication/profile/common/service/ProfileService.class]
┌─────┐
| diseaseProfileItemRepository defined in disease.repository.item.DiseaseProfileItemRepository defined in @EnableJpaRepositories declared on AuthenticationApplication
↑ ↓
| jpa.DiseaseProfileItemRepository.fragments#0
↑ ↓
| diseaseProfileRepositoryImplFragment
↑ ↓
| diseaseProfileRepositoryImpl defined in file [/disease/microservice/authentication/profile/disease/repository/DiseaseProfileRepositoryImpl.class]
└─────┘
Chúng tôi đã thực hiện refactor để sửa lỗi này như sau:
- Thêm @Component annotation cho class DiseaseProfileRepositoryImpl
- Loại bỏ extends DiseaseProfileRepository trong DiseaseProfileItemRepository
- Thay thế diseaseProfileItemRepository.findDiseaseProfileByDiseaseId() bởi diseaseProfileRepository.findDiseaseProfileByDiseaseId() tại toàn bộ codebase.
Kết quả
Dưới đây bảng tổng hợp những cập nhật mà chúng tôi đã thực hiện trong quá trình nâng cấp này.
Tên | Phiên bản trước | Phiên bản mới |
---|---|---|
JDK | 11 | 17 |
Spring Boot | 2.7.10 | 3.2.0 |
Gradle | 7.0.2 | 8.5.0 |
Spring Batch | 4.x | 5.x |
Spring Kafka | 2.x | 3.x |
Spring Data Redis | 2.x | 3.x |
Spring Cloud Sleuth | 2021.0.5 | (removed) |
Springdoc OpenAPI | 2.x | 3.x |
Thư viện internal của LINE | 2.x | 3.x |
Tổng kết
Trong bài viết này, chúng tôi đã chia sẻ quá trình nâng cấp hệ thống Disease Common từ Spring Boot 2 lên Spring Boot 3. Có thể thấy được rõ ràng rằng việc nâng cấp này không chỉ dừng lại ở việc cập nhật version của Spring Boot, mà còn cần nhiều xử lý cho các thư viện xung quanh để codebase có thể vận hành được.
Dành cho các kĩ sư và các đội phát triển đang chuẩn bị cho quá trình nâng cấp tương tự, chúng tôi có một số gợi ý như sau:
- Ưu tiên hàng đầu là nắm được những thay đổi giữa hai phiên bản Spring Boot 2.7 và 3.0, do đây là nơi xảy ra nhiều thay đổi lớn nhất. Chúng sẽ rất hữu ích trong việc lên kế hoạch hay điều tra khi xảy ra lỗi.
- Trong quá trình nâng cấp, chúng ta nên đánh giá nguyên nhân vấn đề dựa vào những câu hỏi sau, theo thứ tự:
- Có phải lỗi này do việc nâng cấp JDK 17 không? Liệu có phải do một feature của Java bị deprecate?
- Có phải lỗi này do thay đổi của Spring Framework không?
- Có phải lỗi này do thư viện hay không? Liệu có phải do chưa nâng cấp version mới nhất của thư viện hoặc do thư viện chưa hỗ trợ không?
- Cuối cùng, chúng ta cần có kịch bản rollback cho các trường hợp rủi ro.
Chúng tôi tin rằng với một kế hoạch kèm các bước chi tiết cho quá trình triển khai, chúng ta có thể nâng cấp hệ thống hiện tại một cách an toàn và tiết kiệm nhất.
Từ đó, sản phẩm sẽ trở nên tốt hơn, bảo mật hơn và đem lại những trải nghiệm tốt nhất cho người dùng.