Spring Boot/Mini-challenge: tự build mini IoC container 80 dòng
~30 phútSpring là gì & nền tảng IoCMiễn phí

Mini-challenge: tự build mini IoC container 80 dòng

Bài thực hành khép lại Module 1 — code 1 IoC container đơn giản đủ tính năng: scan @Component, resolve dependency qua constructor, lifecycle @PostConstruct. Chứng minh Spring không phải magic — chỉ là reflection + topological sort + map cache.

Module 01 đã bóc tách đủ tầng: IoC principle, DI implementation, ApplicationContext nội tại, lifecycle 9 bước, scope, 3 cách khai báo bean. Bài cuối này không thêm khái niệm mới — bạn tự code 1 IoC container đơn giản từ scratch để chứng minh: Spring không phải magic, chỉ là reflection + topological sort + map.

Kết thúc bài, bạn có 1 mini container ~80-100 dòng Java thuần. Nó scan classpath tìm @Component, instantiate với constructor injection, gọi @PostConstruct, expose qua getBean(). So sánh với Spring source thật, bạn sẽ thấy Spring làm chính xác việc này — chỉ với nhiều edge case hơn, BPP, AOP, scope, profile.

🎯 Đề bài

Build class MiniContainer với 3 method public:

public class MiniContainer {
    public MiniContainer(String basePackage) { /* scan + register definitions */ }
    public void start() { /* instantiate all + call @PostConstruct */ }
    public <T> T getBean(Class<T> type) { /* lookup by type */ }
}

Yêu cầu cụ thể:

1. Annotation tự định nghĩa

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PostConstruct {}

(Không dùng org.springframework.* hay jakarta.annotation.* — viết lại từ đầu để hiểu cơ chế.)

2. Component scan

new MiniContainer("com.demo") quét classpath, tìm class có @Component trong package com.demo và sub-package. Lưu metadata vào Map<Class<?>, BeanDefinition>.

3. Constructor injection

Chỉ hỗ trợ 1 constructor public — Spring có thể nhiều constructor + @Autowired, mini container đơn giản hoá. Mỗi parameter resolve theo type (tìm bean cùng type trong registry).

4. Topological sort

Khi start(), container tính thứ tự instantiate sao cho dependency của 1 bean được tạo trước nó. Phát hiện circular dependency → throw IllegalStateException("Circular dependency detected").

5. @PostConstruct

Sau khi instantiate xong tất cả bean, gọi method có @PostConstruct (không cần void/0-param check phức tạp — giả định method đúng signature).

6. Singleton scope

Tất cả bean là singleton — getBean() return cached instance.

7. Test case

@Component
class GreetingService {
    public String greet(String name) { return "Hello, " + name; }
}

@Component
class UserService {
    private final GreetingService greeting;
    private boolean initialized;

    public UserService(GreetingService greeting) {
        this.greeting = greeting;
    }

    @PostConstruct
    void init() { initialized = true; }

    public String welcome(String name) {
        if (!initialized) throw new IllegalStateException("not initialized");
        return greeting.greet(name);
    }
}

public class App {
    public static void main(String[] args) throws Exception {
        var container = new MiniContainer("com.demo");
        container.start();

        UserService us = container.getBean(UserService.class);
        System.out.println(us.welcome("OLHub"));   // Hello, OLHub
    }
}

Output mẫu:

Scanned 2 components
Instantiated GreetingService
Instantiated UserService
Called @PostConstruct on UserService
Hello, OLHub

🔍 Phân tích I-P-O

Input: Annotation @Component, @PostConstruct trên class trong package được scan.

Processing:

  1. Scan — đọc all class file trong package, lọc class có @Component annotation.
  2. Build dependency graph — mỗi class node, edge từ class A sang class B nếu A's constructor có parameter type B.
  3. Topological sort — DFS/BFS từ leaf node. Phát hiện cycle.
  4. Instantiate theo thứ tự — gọi constructor với bean cache của dep. Lưu vào registry.
  5. Lifecycle — gọi @PostConstruct sau khi instantiate xong.

Output: Method getBean(Class) trả instance.

📦 Concept dùng trong bài

ConceptBàiDùng ở đây
IoC principleBài 02Container "drives", code đăng ký intent
Constructor injectionBài 02Resolve dep qua param type
ApplicationContextBài 03Mini ApplicationContext = MiniContainer
Bean lifecycleBài 04Instantiate → populate (qua constructor) → @PostConstruct
Singleton scopeBài 05Map<Class, Object> cache 1 instance / type
@Component scanBài 06Quét annotation tự định nghĩa

▶️ Starter code

import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.*;
import java.util.stream.*;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface PostConstruct {}

public class MiniContainer {

    private final String basePackage;
    private final Map<Class<?>, Object> beans = new LinkedHashMap<>();
    private final List<Class<?>> definitions = new ArrayList<>();

    public MiniContainer(String basePackage) throws Exception {
        this.basePackage = basePackage;
        scan();
    }

    private void scan() throws Exception {
        // TODO: doc class file trong basePackage, loc class co @Component, them vao definitions
    }

    public void start() throws Exception {
        // TODO:
        // 1. topological sort definitions
        // 2. instantiate theo thu tu, resolve dep tu beans cache
        // 3. goi @PostConstruct method
    }

    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> type) {
        // TODO: tim trong beans, return instance
        return null;
    }
}

Dành 30 phút tự làm. Hint ở dưới khi kẹt.

💡 Gợi ý

💡 Gợi ý — đọc khi bị kẹt

Scan classpath — đọc tất cả class file trong package qua ClassLoader:

private void scan() throws Exception {
    String path = basePackage.replace('.', '/');
    URL root = Thread.currentThread().getContextClassLoader().getResource(path);
    if (root == null) throw new IllegalStateException("Package not found: " + basePackage);

    File dir = new File(root.toURI());
    for (File file : Objects.requireNonNull(dir.listFiles())) {
        if (file.getName().endsWith(".class")) {
            String className = basePackage + "." + file.getName().replace(".class", "");
            Class<?> clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Component.class)) {
                definitions.add(clazz);
            }
        }
    }
    System.out.println("Scanned " + definitions.size() + " components");
}

(Đơn giản hoá — không recurse sub-package. Bài này chỉ scan 1 level. Spring làm full recursive với ASM bytecode reader.)

Topological sort — Kahn's algorithm:

private List<Class<?>> topologicalSort() {
    Map<Class<?>, Set<Class<?>>> deps = new LinkedHashMap<>();
    for (Class<?> c : definitions) {
        Constructor<?> ctor = c.getConstructors()[0];
        deps.put(c, new HashSet<>(Arrays.asList(ctor.getParameterTypes())));
    }

    List<Class<?>> sorted = new ArrayList<>();
    while (!deps.isEmpty()) {
        Class<?> ready = deps.entrySet().stream()
            .filter(e -> e.getValue().stream().allMatch(t -> sorted.contains(t) || !definitions.contains(t)))
            .map(Map.Entry::getKey)
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("Circular dependency detected: " + deps.keySet()));
        sorted.add(ready);
        deps.remove(ready);
    }
    return sorted;
}

Instantiate — chọn constructor đầu, resolve param từ cache:

public void start() throws Exception {
    for (Class<?> c : topologicalSort()) {
        Constructor<?> ctor = c.getConstructors()[0];
        Object[] args = Arrays.stream(ctor.getParameterTypes())
            .map(beans::get)
            .toArray();
        Object instance = ctor.newInstance(args);
        beans.put(c, instance);
        System.out.println("Instantiated " + c.getSimpleName());

        invokePostConstruct(c, instance);
    }
}

private void invokePostConstruct(Class<?> c, Object instance) throws Exception {
    for (Method m : c.getDeclaredMethods()) {
        if (m.isAnnotationPresent(PostConstruct.class)) {
            m.setAccessible(true);
            m.invoke(instance);
            System.out.println("Called @PostConstruct on " + c.getSimpleName());
        }
    }
}

getBean — direct lookup:

@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> type) {
    Object bean = beans.get(type);
    if (bean == null) throw new IllegalStateException("No bean of type " + type.getName());
    return (T) bean;
}

✅ Lời giải

✅ Lời giải đầy đủ — xem sau khi đã thử
import java.io.*;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.net.URL;
import java.util.*;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface PostConstruct {}

public class MiniContainer {

    private final String basePackage;
    private final Map<Class<?>, Object> beans = new LinkedHashMap<>();
    private final List<Class<?>> definitions = new ArrayList<>();

    public MiniContainer(String basePackage) throws Exception {
        this.basePackage = basePackage;
        scan();
    }

    private void scan() throws Exception {
        String path = basePackage.replace('.', '/');
        URL root = Thread.currentThread().getContextClassLoader().getResource(path);
        if (root == null) {
            throw new IllegalStateException("Package not found: " + basePackage);
        }
        File dir = new File(root.toURI());
        File[] files = Objects.requireNonNull(dir.listFiles());
        for (File f : files) {
            if (!f.getName().endsWith(".class")) continue;
            String className = basePackage + "." + f.getName().replace(".class", "");
            Class<?> clazz = Class.forName(className);
            if (clazz.isAnnotationPresent(Component.class)) {
                definitions.add(clazz);
            }
        }
        System.out.println("Scanned " + definitions.size() + " components");
    }

    public void start() throws Exception {
        for (Class<?> c : topologicalSort()) {
            Constructor<?> ctor = c.getConstructors()[0];
            Class<?>[] paramTypes = ctor.getParameterTypes();
            Object[] args = new Object[paramTypes.length];
            for (int i = 0; i < paramTypes.length; i++) {
                args[i] = beans.get(paramTypes[i]);
                if (args[i] == null) {
                    throw new IllegalStateException(
                        "Cannot resolve dependency " + paramTypes[i].getName() +
                        " for " + c.getName());
                }
            }
            Object instance = ctor.newInstance(args);
            beans.put(c, instance);
            System.out.println("Instantiated " + c.getSimpleName());
            invokePostConstruct(c, instance);
        }
    }

    private List<Class<?>> topologicalSort() {
        Map<Class<?>, Set<Class<?>>> remaining = new LinkedHashMap<>();
        for (Class<?> c : definitions) {
            Constructor<?> ctor = c.getConstructors()[0];
            Set<Class<?>> deps = new HashSet<>();
            for (Class<?> p : ctor.getParameterTypes()) {
                if (definitions.contains(p)) deps.add(p);
            }
            remaining.put(c, deps);
        }

        List<Class<?>> sorted = new ArrayList<>();
        while (!remaining.isEmpty()) {
            Class<?> ready = null;
            for (Map.Entry<Class<?>, Set<Class<?>>> e : remaining.entrySet()) {
                if (sorted.containsAll(e.getValue())) {
                    ready = e.getKey();
                    break;
                }
            }
            if (ready == null) {
                throw new IllegalStateException(
                    "Circular dependency detected among: " + remaining.keySet());
            }
            sorted.add(ready);
            remaining.remove(ready);
        }
        return sorted;
    }

    private void invokePostConstruct(Class<?> c, Object instance) throws Exception {
        for (Method m : c.getDeclaredMethods()) {
            if (m.isAnnotationPresent(PostConstruct.class)) {
                m.setAccessible(true);
                m.invoke(instance);
                System.out.println("Called @PostConstruct on " + c.getSimpleName());
            }
        }
    }

    @SuppressWarnings("unchecked")
    public <T> T getBean(Class<T> type) {
        Object bean = beans.get(type);
        if (bean == null) {
            throw new IllegalStateException("No bean of type " + type.getName());
        }
        return (T) bean;
    }
}

Test driver:

package com.demo;

@Component
class GreetingService {
    public String greet(String name) { return "Hello, " + name; }
}

@Component
class UserService {
    private final GreetingService greeting;
    private boolean initialized;

    public UserService(GreetingService greeting) {
        this.greeting = greeting;
    }

    @PostConstruct
    void init() { this.initialized = true; }

    public String welcome(String name) {
        if (!initialized) throw new IllegalStateException("not initialized");
        return greeting.greet(name);
    }
}

public class App {
    public static void main(String[] args) throws Exception {
        var container = new MiniContainer("com.demo");
        container.start();
        UserService us = container.getBean(UserService.class);
        System.out.println(us.welcome("OLHub"));
    }
}

Output:

Scanned 2 components
Instantiated GreetingService
Instantiated UserService
Called @PostConstruct on UserService
Hello, OLHub

Giải thích từng phần:

  • scan() — dùng ClassLoader.getResource(path) để lấy URL của package, list file .class, load qua Class.forName, kiểm tra annotation. Đơn giản hoá so với Spring (Spring dùng ASM bytecode reader để không phải Class.forName toàn bộ — hiệu năng tốt hơn cho classpath lớn).

  • topologicalSort() — Kahn's algorithm: tìm node có tất cả dependency đã sorted (in-degree = 0 trong sub-graph còn lại), thêm vào sorted, loại khỏi remaining. Lặp đến khi cạn hoặc không tìm được node ready (= cycle).

  • start() — duyệt sorted, lấy constructor đầu tiên, resolve param từ beans cache, gọi newInstance. Hậu init: tìm method có @PostConstruct và invoke.

  • getBean() — direct map lookup. Spring có overload theo name, qualifier, phức tạp hơn nhiều.

🎓 Mở rộng

Mức 1 — Field injection với @Autowired:

Thêm annotation @Autowired, sau khi newInstance thì duyệt field có annotation, set value từ cache. Lưu ý: field có thể private → cần setAccessible(true).

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Autowired {}

private void injectFields(Class<?> c, Object instance) throws Exception {
    for (Field f : c.getDeclaredFields()) {
        if (f.isAnnotationPresent(Autowired.class)) {
            f.setAccessible(true);
            f.set(instance, beans.get(f.getType()));
        }
    }
}

Mức 2 — @PreDestroy và shutdown:

Thêm method close(), gọi method @PreDestroy theo thứ tự ngược dependency (LIFO). Wrap container trong AutoCloseable để dùng try-with-resources.

Mức 3 — Recursive scan sub-package:

Hiện tại chỉ scan 1 level. Convert scan() thành scanRecursive(File dir, String currentPackage) để duyệt sub-folder.

Mức 4 — Resolve theo interface:

Hiện tại bean lookup theo class concrete. Spring resolve theo interface — vd @Autowired PaymentGateway tìm bean implement PaymentGateway. Thêm logic: khi resolve param type là interface, scan registry tìm bean class implement interface đó.

private Object resolve(Class<?> type) {
    Object exact = beans.get(type);
    if (exact != null) return exact;
    // Tim bean implement interface
    return beans.entrySet().stream()
        .filter(e -> type.isAssignableFrom(e.getKey()))
        .map(Map.Entry::getValue)
        .findFirst()
        .orElseThrow(() -> new IllegalStateException("No bean for " + type.getName()));
}

Pitfall: nhiều bean implement cùng interface → ambiguity. Spring giải bằng @Primary/@Qualifier. Mức này tự thêm.

Mức 5 — @Configuration + @Bean method:

Hỗ trợ class @Configuration chứa method @Bean. Container scan thấy @Configuration, instantiate config, invoke từng @Bean method, register kết quả. Không cần CGLIB enhancement (đơn giản hoá — chấp nhận lite mode).

✨ Điều bạn vừa làm được

Hoàn thành mini-challenge này, bạn đã:

  • Bóc lớp magic của Spring: container chỉ là classpath scanner + reflection + topological sort + map cache. Không có gì huyền bí.
  • Hiểu vì sao circular dependency phát hiện được tại startup: topological sort không complete được.
  • Hiểu vì sao constructor injection ưu việt: container có thể tính graph từ constructor signature ngay từ class load — không cần "lazy resolve".
  • Hiểu lifecycle order: instantiate xong toàn bộ rồi mới gọi @PostConstruct. Có lý do — @PostConstruct có thể gọi method của bean khác (đã được populate field), nếu chưa init xong tất cả → NPE.
  • Đo được khoảng cách đến Spring thật: ~80-100 dòng vs hàng trăm nghìn dòng. Spring thêm: BPP, AOP proxy, scope multi-tier, qualifier, conditional, profile, externalized config, event publisher, ... Toàn lớp xây trên cùng nền tảng đơn giản này.

Chúc mừng — bạn đã hoàn thành Module 01! Bạn đã có nền tảng IoC vững chắc. Module 02 sẽ bóc tách Spring Boot foundations: auto-configuration làm gì, starter đóng gói cái gì, profile và externalized config — toàn bộ "công thức nấu sẵn" mà Spring Boot thêm vào Spring Framework.

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...