LY Corporation Tech Blog

Quảng bá công nghệ và văn hoá phát triển hỗ trợ cho các dịch vụ cung cấp bởi LY Corporation và LY Corporation Group (LINE Plus, LINE Taiwan, LINE Vietnam).

Nâng cấp hệ thống Disease Common từ Spring Boot 2 lên 3

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).
Ảnh 1. Tổng quan dịch vụ Disease Common

Hiện trạng

Ảnh 2. Timeline vòng đời của Spring Boot

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
1Sử dụng JDK 17 trở lên

Thay đổi lớn

2Nâng cấp Spring Framework 6.x

Thay đổi lớn

3Thay đổi về CRUD repository của Spring Data 2022.0

Không ảnh hưởng

4Thay đổi về kiểu trả về của Spring Kafka 3

Thay đổi nhỏ

5Thay đổi cách triển khai của Spring Batch

Thay đổi lớn

6Thay đổ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ạnTác vụ
1Trước khi nâng cấp lên Spring Boot 3
  • Nâng cấp Spring Boot lên phiên bản mới nhất (2.7.x)
  • Nâng cấp JDK 17
  • Nâng cấp Spring Security lên phiên bản 5.8
  • Kiểm tra các thư viện deprecate của Spring Boot 2.x
  • Kiểm tra các thay đổi của các thư viện khác
2Trong khi nâng cấp lên Spring Boot 3
  • Thay đổi về Maven / Gradle
  • Thay đổi Jakarta EE
  • Thay đổi Spring Framework 6
  • Thay đổi về config properties
  • Thay đổi của các thư viện khác

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:

Ảnh 3. Cách triển khai sử dụng Auto-Configuration Registration

  • 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:

resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
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  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ênPhiên bản trướcPhiên bản mới
JDK1117
Spring Boot2.7.103.2.0
Gradle7.0.28.5.0
Spring Batch4.x5.x
Spring Kafka2.x3.x
Spring Data Redis2.x3.x
Spring Cloud Sleuth2021.0.5(removed)
Springdoc OpenAPI2.x3.x
Thư viện internal của LINE2.x3.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.