Phân tích cơ chế khởi tạo đối tượng trong Spring và mô phỏng thực tế để giảm độ kết dính

Chuyển giao quyền quản lý vòng đời đối tượng

Trong lập trình Java truyền thống, việc khởi tạo, cấu hình và huỷ các đối tượng thường được thực hiện trực tiếp bằng toán tử new. Cách tiếp cận này khiến các thành phần trong hệ thống phụ thuộc chặt chẽ vào nhau, dẫn đến độ kết dính cao. Khi cần thay đổi thuật toán hay triển khai khác, lập trình viên buộc phải sửa mã nguồn, biên dịch lại và phát hành bản cập nhật. Framework Spring giải quyết vấn đề này bằng cách chuyển toàn bộ quyền điều khiển sang một container trung tâm, cho phép định nghĩa quan hệ phụ thuộc thông qua file cấu hình hoặc annotation.

Mô phỏng container khởi tạo đối tượng bằng phản chiếu

Để hiểu rõ cách Spring vận hành, chúng ta có thể xây dựng một phiên bản rút gọn của container. Cơ chế này dựa trên việc ánh xạ tên dịch vụ đến lớp triển khai thực tế thông qua file thuộc tính, sau đó sử dụng tính năng phản chiếu (Reflection) để khởi tạo đối tượng động lúc chạy.

1. File ánh xạ cấu hình

mapping.properties
accountService=com.springlab.service.AccountServiceImplV1

2. Lớp quản lý registry

package com.springlab.core;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class BeanRegistry {
    private static final Properties configMap = new Properties();

    static {
        try (InputStream stream = BeanRegistry.class.getClassLoader().getResourceAsStream("mapping.properties")) {
            configMap.load(stream);
        } catch (IOException exception) {
            throw new RuntimeException("Không thể tải file cấu hình ánh xạ", exception);
        }
    }

    public static Object resolve(String componentKey) {
        String fullyQualifiedName = configMap.getProperty(componentKey);
        if (fullyQualifiedName == null) {
            return null;
        }
        try {
            Class targetClass = Class.forName(fullyQualifiedName);
            return targetClass.getDeclaredConstructor().newInstance();
        } catch (Exception exception) {
            throw new RuntimeException("Lỗi khởi tạo component: " + fullyQualifiedName, exception);
        }
    }
}

3. Giao diện và lớp triển khai

package com.springlab.service;

public interface AccountService {
    void registerNewAccount();
}

package com.springlab.service;

public class AccountServiceImplV1 implements AccountService {
    @Override
    public void registerNewAccount() {
        System.out.println("AccountServiceImplV1: Hoàn thành đăng ký tài khoản.");
    }
}

package com.springlab.service;

public class AccountServiceImplV2 implements AccountService {
    @Override
    public void registerNewAccount() {
        System.out.println("AccountServiceImplV2: Hoàn thành đăng ký tài khoản với bản vá mới.");
    }
}

4. Lớp điều khiển sử dụng registry

package com.springlab.web;

import com.springlab.core.BeanRegistry;
import com.springlab.service.AccountService;

public class AccountController {
    // Thay vì new AccountServiceImplV1(), chúng ta lấy đối tượng từ registry
    private final AccountService accountLogic = (AccountService) BeanRegistry.resolve("accountService");

    public void processRegistration() {
        accountLogic.registerNewAccount();
    }
}

Phương pháp này cho phép thay đổi phiên bản triển khai chỉ bằng cách sửa file mapping.properties mà không cần động đến mã nguồn Java, minh chứng cho nguyên lý Đảo ngược Điều khiển (Inversion of Control - IoC).

Áp dụng IoC sẵn có trong Spring Framework

Spring cung cấp container đầy đủ tính năng thông qua package spring-context. Thay vì tự viết logic phản chiếu, nhà phát triển khai báo các component trong file XML hoặc Java Config.

1. Cấu hình Maven

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.30</version>
</dependency>

2. Khai báo Bean trong XML

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountService" class="com.springlab.service.AccountServiceImplV2"/>
</beans>

3. Khởi tạo và lấy đối tượng từ container

package com.springlab.test;

import com.springlab.service.AccountService;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringContainerDemo {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
        AccountService service = context.getBean("accountService", AccountService.class);
        service.registerNewAccount();
        context.close();
    }
}

Quản lý phạm vi Bean: Singleton và Prototype

Spring không chỉ khởi tạo đối tượng mà còn kiểm soát chu kỳ sống của chúng thông qua thuộc tính scope. Hai phạm vi phổ biến nhất là singleton (mặc định) và prototype.

1. Lớp đối tượng dữ liệu

package com.springlab.model;

public class UserProfile {
    private String username;
    private int age;
    private UserSettings settings;

    public UserProfile() {}

    public UserProfile(String username, int age, UserSettings settings) {
        this.username = username;
        this.age = age;
        this.settings = settings;
    }

    // Getters và Setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public UserSettings getSettings() { return settings; }
    public void setSettings(UserSettings settings) { this.settings = settings; }

    @Override
    public String toString() {
        return "UserProfile [username=" + username + ", age=" + age + "]";
    }
}

package com.springlab.model;
public class UserSettings {
    // Lớp rỗng để minh hoạ quan hệ phụ thuộc
}

2. Cấu hình phạm vi Prototype

<bean id="userProfile" class="com.springlab.model.UserProfile" scope="prototype">
    <property name="username" value="NguyenVanA"/>
    <property name="age" value="25"/>
    <property name="settings" ref="defaultSettings"/>
</bean>
<bean id="defaultSettings" class="com.springlab.model.UserSettings"/>

3. Kiểm tra tính chất của phạm vi

UserProfile profileA = context.getBean("userProfile", UserProfile.class);
UserProfile profileB = context.getBean("userProfile", UserProfile.class);
System.out.println(profileA == profileB); // In ra false vì scope="prototype"

Khi sử dụng singleton, container chỉ khởi tạo một phiên bản duy nhất ngay khi khởi động (hoặc theo cơ chế lazy-init nếu được cấu hình). Ngược lại, prototype sẽ tạo ra một thể hiện mới mỗi khi phương thức getBean() được gọi. Việc lựa chọn phụ thuộc vào yêu cầu về trạng thái chia sẻ và khả năng an toàn trong môi trường đa luồng.

Cơ chế Inject phụ thuộc (Dependency Injection)

Sau khi container tạo ra đối tượng, bước tiếp theo là cung cấp các giá trị hoặc đối tượng phụ thuộc cần thiết. Spring hỗ trợ hai phương thức chính: Setter Injection và Constructor Injection.

1. Inject thông qua Setter

Phương pháp này yêu cầu lớp target phải có phương thức setXxx() tương ứng với thuộc tính. Container sẽ khởi tạo đối tượng mặc định, sau đó gọi các setter để điền giá trị.

<bean id="userProfile" class="com.springlab.model.UserProfile">
    <property name="username" value="TranThuH"/>
    <property name="age" value="30"/>
    <property name="settings" ref="defaultSettings"/>
</bean>

Lưu ý: Thuộc tính name trong thẻ property phải trùng với tên thuộc tính trong lớp (không tính tiền tố set). Đối với tham chiếu đối tượng khác, sử dụng ref thay vì value.

2. Inject thông qua Constructor

Setter Injection phù hợp với các thuộc tính tùy chọn, nhưng Constructor Injection đảm bảo đối tượng luôn ở trạng thái hoàn chỉnh ngay khi ra đời. Điều này đặc biệt quan trọng với các phụ thuộc bắt buộc.

<bean id="userProfile" class="com.springlab.model.UserProfile">
    <constructor-arg name="username" value="LeMinhD"/>
    <constructor-arg name="age" value="22"/>
    <constructor-arg name="settings" ref="defaultSettings"/>
</bean>

Trong thẻ constructor-arg, Spring sẽ tự động khớp tham số dựa trên tên hoặc kiểu dữ liệu. Nếu lớp có nhiều constructor, framework sẽ ưu tiên constructor có số lượng tham số lớn nhất hoặc khớp tên chính xác. Không nên kết hợp hai cách inject cho cùng một thuộc tính vì điều này sẽ gây ra xung đột và kích hoạt ngoại lệ BeanCreationException lúc khởi tạo container.

Việc nắm vững cách Spring khởi tạo đối tượng, quản lý phạm vi và inject phụ thuộc giúp lập trình viên thiết kế hệ thống mô-đun hóa cao, dễ dàng kiểm thử đơn vị và giảm thiểu rủi ro khi bảo trì mã nguồn về lâu dài.

Thẻ: Spring Framework ioc Dependency Injection Java Reflection Bean Scope

Đăng vào ngày 29 tháng 9 lúc 09:40