+4

Thử tạo một Dependency Injection Framework đơn giản viết bằng Java

Mayfest2023

Hello folks!

Chắc hẳn thuật ngữ "Dependency Injection" đã quá quen thuộc đối với dân lập trình, đặc biệt là những bạn học kiến thức nền tảng của một Software Engineer, nếu bạn đọc còn mơ hồ về thuật ngữ này thì đây là một lí thuyết giản đơn: Dependency Injection (DI) là một design pattern giúp việc giảm sự phụ thuộc giữa các lớp, nó giúp chúng ta truyền các phụ thuộc của một lớp từ bên ngoài lớp đó thay vì để lớp đó tự tạo chúng. Trong bài viết này mình sẽ tạo một Framework đơn giản implement DI sử dụng ngôn ngữ Java để giúp mình và bạn đọc ôn lại về design pattern này nhé.

(Source code tham khảo trong bài nằm ở cuối bài viết)

1.Tạo các annotations

Đầu tiên mình sẽ tạo 2 annotation chính, và rất thường gặp nếu bạn đang làm với ngôn ngữ Java, đó là @Component@Inject

package com.phatng.di.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}

@Component giúp mình đánh dấu lên các classes sẽ được quản lí bởi DI.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR})
public @interface Inject {
}

@Inject giúp mình đánh dấu các field (hoặc constructor) cần phải inject các phụ thuộc của lớp đó vào.

2. Tạo các class cơ bản, đánh dấu annotation

Mình sẽ có 5 packages:

  • anntation (gồm 2 annotation vừa tạo bên trên)
  • component (chứa các service class)
  • container (chứa class DI container)
  • dbengine (chứa class quản lí database)
  • entity (chứa các entities)
  • repository (chứ các repositories)

Mình sẽ đi qua 4 classes quan trọng trong framework

2.1 ProductService


import com.phatng.di.annotation.Component;
import com.phatng.di.annotation.Inject;
import com.phatng.di.entity.Product;
import com.phatng.di.repository.ProductRepository;

import java.util.List;

@Component
public class ProductService {

    @Inject
    protected ProductRepository productRepository;

    public List<Product> getAllProducts() {
        return productRepository.getAllProducts();
    };
}

Là một service đơn giản, chứa 1 method giản đơn trả về danh sách Product, mình sẽ đánh dấu @Component cho class này để DI container quản lí việc tạo class. Ngoài ra, field producRepository được đánh dấu @Inject để DI container khởi tạo instance cho field này (đây chính là "truyền các phụ thuộc của một lớp từ bên ngoài lớp")

2.2 ProductRepository

package com.phatng.di.repository;

import com.phatng.di.annotation.Component;
import com.phatng.di.annotation.Inject;
import com.phatng.di.dbengine.ConsoleDatabaseManager;
import com.phatng.di.entity.Product;

import java.util.List;

@Component
public class ProductRepository {

    @Inject
    protected ConsoleDatabaseManager<Product> consoleDatabaseManager;

    public List<Product> getAllProducts() {
        return consoleDatabaseManager.query(Product.class);
    }

}

Là một class repository, giúp tương tác với DB manager để lấy data cần thiết cho service. Mình sẽ đánh dấu class này bằng @Component vì DI manager cần phải quản lí được class này thì mới tạo và truyền instance của nó vào trong ProductService. Tương tự service, trong repository sẽ có field consoleDatabaseManager được đánh dấu @Inject để DI container tự động tạo instance cho field này.

2.3 ConsoleDatabaseManager

package com.phatng.di.dbengine;

import com.phatng.di.annotation.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class ConsoleDatabaseManager<T> {
    public List<T> query(Class<T> clazz) {
        try {
            return List.of(
                    clazz.newInstance(),
                    clazz.newInstance(),
                    clazz.newInstance()
            );
        } catch (Exception e) {
            return new ArrayList<>();
        }

    }
}

Ở đây mình giả lập việc query vào DB bằng method query, kết quả sẽ trả về 3 Product(s). Được đánh dấu @Component

2.4 DIContainer

package com.phatng.di.container;


import com.phatng.di.annotation.Component;
import com.phatng.di.annotation.Inject;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;

import java.lang.reflect.Field;
import java.util.*;

public class DIContainer {

    private final Map<Class<?>, Object> components = new HashMap<>();

    public DIContainer() {
        // scan all classes in the classpath for @Component annotation
        List<Object> highLevelComponents = new ArrayList<>();
        for (Class<?> clazz : getAllClasses()) {
            if (clazz.isAnnotationPresent(Component.class)) {
                // create an instance of the component and store it in the map
                Object component = createComponent(clazz);
                components.put(clazz, component);
                if (isHighLevelComponent(clazz)) {
                    highLevelComponents.add(component);
                }
            }
        }

        // inject dependencies into all high level components
        for (Object highLevelComponent : highLevelComponents) {
            injectDependencies(highLevelComponent);
        }
    }

    public <T> T getInstance(Class<T> type) {
        if (!components.containsKey(type)) {
            throw new RuntimeException("No instance found for type " + type.getName());
        }
        return type.cast(components.get(type));
    }

    private Set<Class> getAllClasses() {
        Reflections reflections = new Reflections("com.phatng.di", new SubTypesScanner(false));
        return new HashSet<>(reflections.getSubTypesOf(Object.class));
    }

    private boolean isHighLevelComponent(Class<?> clazz) {
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Inject.class)) {
                return true;
            }
        }
        return false;
    }

    private Object createComponent(Class<?> clazz) {
        Object instance;
        try {
            instance = clazz.newInstance();
        } catch (Exception e) {
            System.out.println("Cannot instantiate class " + clazz.getName());
            instance = null;
        }
        return instance;
    }

    private void injectDependencies(Object component) {
        Field[] fields = component.getClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(Inject.class)) {
                Class<?> type = field.getType();
                Object dependency = getInstance(type);
                field.setAccessible(true);
                try {
                    field.set(component, dependency);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("Error injecting dependency of type " + type.getName() + " into field " + field.getName() + " of component " + component.getClass().getName(), e);
                }
            }
        }
    }

}

Quản lí toàn bộ việc tạo ra instance cho các Component (class được đánh dấu @Component) và sẽ inject các instance này vào field được đánh dấu @Inject. Mình có sử dụng thêm thư viện Reflection để giúp dễ dàng thao tác với class, field.

Để sử dụng DI Container, mình dùng method getInstance, truyền vào class cần khởi tạo instance và để DI container lo phần còn lại 😄 Cách dùng cụ thể

        DIContainer diContainer = new DIContainer();

        ProductService productService = diContainer.getInstance(ProductService.class);
        System.out.println(productService.getAllProducts());

Nếu không có DI container, nếu muốn tạo ra instance của ProductService, mình sẽ phải tạo 2 instance của DB manager và Repository trước và phải lồng nhau, cụ thể: trong class Repository tạo instance DB manager, trong class Service tạo instance Repository,.. rất rườm rà và quá phụ thuộc nhau.

3. Phát triển thêm

Vẫn còn vài vấn đề có thể giải quyết được bằng DI, chẳn hạn đánh dấu @Component lên interface thay vì class trực tiếp, làm vậy sẽ cho phép mình ở mỗi class có thể tạo ra nhiều Component khác nhau cùng 1 loại interface, code sẽ flexible hơn. Một số Framework với DI container mạnh mẽ như Spring Boot sẽ có rất nhiều annotation (@Autowired, @Qualifier,...) để giải quyết bài toán DI.

4. Tổng kết

Qua bài này mình đã tự tạo ra một mini DI framework sử dụng ngôn ngữ Java, mặc dù còn quá đơn giản so với một DI framework đang được vận hành trong các enterprise applications nhưng mình mong sẽ giúp được các bạn hiểu rõ hơn về cách sử dụng DI trong thực tế. Hẹn gặp lại các bạn trong những bài sau.

Github source: https://github.com/phatnt99/DependencyInjectionExample


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.