Tìm hiểu về spring core - Bài 3: Khởi tạo và sử dụng singleton

10 tháng 06, 2024 - 1586 lượt xem

Trong bài lần trước chúng ta đã cùng đi tìm hiểu về singleton và prototype, chúng ta đã cùng đi đo performace và thấy rằng với Spring sử dụng singleton vẫn là một lựa chọn tốt nhất, nếu bạn vẫn chưa xem bài trước bạn có thể tìm thấy nó tại đây nhé. Vậy có những cách nào để khởi tạo một singleton? Dũng sẽ cùng các bạn đi tìm hiểu trong bài này nhé.

Tạo module

Cũng giống như hai bài trước, lần này chúng ta cũng sẽ khởi tạo một module có tên singleton.

Khởi tạo ngay từ lớp

Thực ra trong hai bài trước chúng ta đã dùng phương pháp này rồi, nó chỉ đơn giản là bổ sung @Component ở phía trên khai báo lớp mà thôi.

@Component
public class UserService {

    public UserModel getUserByUsername(String username) {
        return new UserModel(username);
    }
}

Mặc định thì tên singleton sẽ có dạng chữ cái đầu viết thường ví dụ tên lớp là UserService thì tên singleton sẽ là userService, tuy nhiên bạn cũng có thể chỉ định tên cho singleton, ví dụ:

@Component("userService")
public class UserService {

    public UserModel getUserByUsername(String username) {
        return new UserModel(username);
    }
}

Tuy nhiên trong thực tế cũng hiếm khi nào bạn cần chỉ định tên vì đã có tên mặc định và các lớp thường có một tên duy nhất.
Để chuẩn bị cho các ví dụ bên dưới, tôi sẽ tạo thêm ra 1 interface và 2 lớp phục vụ cho nghiệp vụ thanh toán.

package vn.techmaster.singleton.service;

import java.math.BigDecimal;

public interface PaymentService {

    void processUserPayment(
        String username,
        BigDecimal amount
    );
}
package vn.techmaster.singleton.service;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class TechmasterPaymentService implements PaymentService {

    @Override
    public void processUserPayment(
        String username,
        BigDecimal amount
    ) {
        System.out.println(
            "User pay via Techmaster, amount: " + amount
        );
    }
}
package vn.techmaster.singleton.service;

import org.springframework.stereotype.Component;

import java.math.BigDecimal;

@Component
public class YoungMonkeysPaymentService implements PaymentService {

    @Override
    public void processUserPayment(
        String username,
        BigDecimal amount
    ) {
        System.out.println(
            "User pay via Young Monkeys, amount: " + amount
        );
    }
}

Ở đây chúng ta có một interface cơ sở cho nghiệp vụ thanh toán và có hai lớp cài đặt cho 2 dịch vụ thanh toán riêng biệt là Techmaster và Young Monkeys.

Khởi tạo từ lớp configuration

Trong dự án thực tế chúng ta thường sẽ phải sử dụng thư viện khác, lúc này chúng ta sẽ không thể bổ sung được annotation @Component vào trong các lớp của thư viện được. Vậy nên spring cũng cung cấp cho chúng ta cơ chế để chúng ta khởi tạo thông qua lớp configuration thế này:

package vn.techmaster.singleton.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Configuration
public class ApplicationConfiguration {

    @Bean
    public ExecutorService applicationExecutorService() {
        return Executors.newCachedThreadPool();
    }
}

Ở đây chúng ta đang khởi tạo một singleton có tên applicationExecutorService và kiểu là ExecutorService (một lớp cung cấp việc xử lý đa luồng).
Cũng tương tự như @Component, chúng ta có thể chỉ định tên cho @Bean, tuy nhiên cũng hiếm khi chúng ta cần làm việc này thì tên singleton có thể lấy theo tên hàm.

Sử dụng singleton

Chúng ta có thể sử dụng các singleton thông qua việc inject dependency (tiêm phụ thuộc) hoặc là lấy trực tiếp từ ApplicationContext.

Inject dependency

Chúng ta cũng có hai cách:

  1. Thông qua hàm tạo (khuyến khích).
  2. Thông qua @Autowired annotation.

Thông qua hàm tạo

Hãy nói chúng ta cần tạo ra một lớp có tên PaymentController để xử lý nghiệp vụ thanh toán, nó sẽ sử dụng:

  1. Lớp UserService để lấy ra thông tin người dùng để kiểm tra sự tồn tại.
  2. Lớp PaymentService để xử lý nghiệp vụ thanh toán.
  3. Lớp ExecutorService để gọi đa luồng nghiệp vụ xử lý thanh toán.
    Mã nguồn cho lớp PaymentController sẽ như sau:
package vn.techmaster.singleton.controller;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import vn.techmaster.singleton.model.UserModel;
import vn.techmaster.singleton.service.PaymentService;
import vn.techmaster.singleton.service.UserService;

import java.math.BigDecimal;
import java.util.concurrent.ExecutorService;

@Controller
public class PaymentController {

    private final UserService userService;
    private final PaymentService paymentService;
    private final ExecutorService applicationExecutorService;

    public PaymentController(
        UserService userService,
        @Qualifier("techmasterPaymentService")
        PaymentService paymentService,
        ExecutorService applicationExecutorService
    ) {
        this.userService = userService;
        this.paymentService = paymentService;
        this.applicationExecutorService = applicationExecutorService;
    }

    public void handleUserPayment(
        String username,
        BigDecimal amount
    ) {
        UserModel user = userService.getUserByUsername(username);
        if (user == null) {
            throw new IllegalArgumentException("user not found");
        }
        applicationExecutorService.execute(() ->
            paymentService.processUserPayment(
                username,
                amount
            )
        );
    }
}

Ở đây chúng ta có một điều đặc biệt đó là buộc phải sử dụng thêm @Qualifier(“techmasterPaymentService”) để chỉ định tên cụ thể của singleton, nếu chúng ta bỏ annotation này đi thì khi chạy chương trình chúng ta sẽ gặp lỗi:

Exception in thread "main"
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'paymentController'
defined in file [xxx/PaymentController.class]:
Unsatisfied dependency expressed through constructor parameter 1:
No qualifying bean of type 'vn.techmaster.singleton.service.PaymentService' available:
expected single matching bean but found 2:
techmasterPaymentService,youngMonkeysPaymentService

Nguyên nhân là vì chúng ta có 2 lớp cài đặt cho interface PaymentService mà nguyên tắc lấy singleton của spring là:

  1. Tìm xem có singleton nào có tên paymentService và kiểu là PaymentService không.
  2. Nếu không có thì tìm xem có singleton nào có kiểu là PaymentService không.
    Ở đây nó sẽ tìm thấy 2 singleton có cùng một kiểu là PaymentService, nó sẽ không biết phải chọn singleton nào nên sẽ báo lỗi.
    Đến đây mình đoán nhiều bạn sẽ đặt ra câu hỏi vậy tôi đặt tên tham số là techmasterPaymentService như thế này có được không?
@Controller
public class PaymentController {

    private final UserService userService;
    private final PaymentService paymentService;
    private final ExecutorService applicationExecutorService;

    public PaymentController(
        UserService userService,
        PaymentService techmasterPaymentService,
        ExecutorService applicationExecutorService
    ) {
        this.userService = userService;
        this.paymentService = techmasterPaymentService;
        this.applicationExecutorService = applicationExecutorService;
    }

Câu trả lời là không, bạn vẫn gặp lỗi như vậy, nguyên nhân là do khi biên dịch thì java sẽ không giữ được tên của tham số, bạn có thể sử dụng chương trình java reflection sau để kiểm chứng:

package vn.techmaster.singleton;

import vn.techmaster.singleton.controller.PaymentController;

import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;

public class Reflection {

    public static void main(String[] args) {
        Constructor<?> constructor = PaymentController.class.getConstructors()[0];
        for (Parameter parameter : constructor.getParameters()) {
            System.out.println(parameter.getName());
        }
    }
}

Kết quả bạn nhận được sẽ là:
arg0
arg1
arg2
Vậy nên spring sẽ vẫn tìm đến các bean có kiểu là PaymentService và lỗi tương tự vẫn xảy ra.

Thông qua @Autowired annotation

Ngoài phương pháp sử dụng hàm tạo chúng ta cũng có thể sử dụng @Autowired vào field (trường) hoặc phương thức (method) setter, ví dụ:

package vn.techmaster.singleton.controller;

import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import vn.techmaster.singleton.model.UserModel;
import vn.techmaster.singleton.service.PaymentService;
import vn.techmaster.singleton.service.UserService;

import java.math.BigDecimal;
import java.util.concurrent.ExecutorService;

@Setter
@Controller
public class PaymentController {

    @Autowired
    private UserService userService;

    @Autowired
    @Qualifier("techmasterPaymentService")
    private PaymentService paymentService;

    @Autowired
    private ExecutorService applicationExecutorService;

    public void handleUserPayment(
        String username,
        BigDecimal amount
    ) {
        UserModel user = userService.getUserByUsername(username);
        if (user == null) {
            throw new IllegalArgumentException("user not found");
        }
        applicationExecutorService.execute(() ->
            paymentService.processUserPayment(
                username,
                amount
            )
        );
    }
}

Ở đây chúng ta đang sử dụng @Setter của thư viện lombok để tạo hàm setter cho các trường, để cài đặt lombok bạn hãy mở tập tin mastering-spring-boot/pom.xml và cập nhật nó thành như sau:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>vn.techmaster</groupId>
    <artifactId>mastering-spring-boot</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>
    <modules>
        <module>hello-world</module>
        <module>singleton-prototype</module>
        <module>singleton</module>
    </modules>

    <properties>
        <lombok.version>1.18.32</lombok.version>
        <spring.version>6.1.8</spring.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Sau khi cập nhật xong, bạn hãy nhấn reload để intelliJ nhận thư viện lombok nhé.
Đến đây bạn có thắc mắc tại sao phải có thêm các phương thức setter, vì nếu bạn xoá đi @Setter annotation thì chương trình vẫn hoạt động bình thường? Trên thực tế thì có hay không có setter cũng không sao, vì spring có thể dùng java reflection để inject các singleton phụ thuộc, tuy nhiên ở phiên bản trước Java 17 thì có một lớp tên là SecurityManager, một số cài đặt sẽ chặn không cho phép truy cập thông qua reflection để tăng cường bảo mật nên có hàm setter thì sẽ đảm bảo được các phụ thuộc sẽ được inject khi hàm setter được gọi. Thú vị là cũng hiếm có ai dùng đến cái SecurityManager này lên từ phiên bản 17 nó đã được bỏ đi. Ngoài ra việc có thêm hàm setter cũng tiện khi chúng ta viết mã unit test, để đỡ phải dùng reflection inject các lớp giả lập vào.
Như bạn thấy việc sử dụng @Autowired lại sinh ra mấy cái hàm setter nó tương đối thừa thãi và có vẻ vi phạm vào nguyên tắc you don’t gonna need it, nên việc sử dụng qua hàm tạo được khuyến khích hơn.

Lấy trực tiếp từ ApplicationContext

Ở 2 bài trước chúng ta cũng đã sử dụng cách này rồi, và mã nguồn cũng không có gì đặc biệt hơn cả:

package vn.techmaster.singleton;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import vn.techmaster.singleton.controller.PaymentController;
import vn.techmaster.singleton.service.PaymentService;

public class SingletonStartUp {

    public static void main(String[] args) {
        ApplicationContext applicationContext =
            new AnnotationConfigApplicationContext(
                "vn.techmaster.singleton"
            );
        PaymentController paymentController = applicationContext
            .getBean(PaymentController.class);
        PaymentService paymentService = applicationContext
            .getBean("youngMonkeysPaymentService", PaymentService.class);
    }
}

Chỉ có hơi khách chút là chúng ta cần chỉ định rõ tên của singleton youngMonkeysPaymentService cho kiểu PaymentService để tránh gặp lỗi ở phía trên mà thôi

Tổng kết

Vậy là chúng ta đã:

  1. Tìm hiểu về cách khai báo và sử dụng singleton.
  2. Tìm hiểu sâu về nguyên nhân có thể gây ra lỗi nếu không chỉ định rõ tên của singleton.

Cám ơn bạn đã quan tâm đến bài viết này. Để nhận được thêm các kiến thức bổ ích bạn có thể:

  1. Đọc các bài viết của TechMaster trên facebook: https://www.facebook.com/techmastervn
  2. Xem các video của TechMaster qua Youtube: https://www.youtube.com/@TechMasterVietnam nếu bạn thấy video/bài viết hay bạn có thể theo dõi kênh của TechMaster để nhận được thông báo về các video mới nhất nhé.
  3. Chat với techmaster qua Discord: https://discord.gg/yQjRTFXb7a

Bình luận

avatar
DevSecOps Edu VN 2024-06-29 03:36:23.578023 +0000 UTC

hay

Avatar
* Vui lòng trước khi bình luận.
Ảnh đại diện
  0 Thích
0