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:
- Scan — đọc all class file trong package, lọc class có
@Componentannotation. - Build dependency graph — mỗi class node, edge từ class A sang class B nếu A's constructor có parameter type B.
- Topological sort — DFS/BFS từ leaf node. Phát hiện cycle.
- Instantiate theo thứ tự — gọi constructor với bean cache của dep. Lưu vào registry.
- Lifecycle — gọi
@PostConstructsau khi instantiate xong.
Output: Method getBean(Class) trả instance.
📦 Concept dùng trong bài
| Concept | Bài | Dùng ở đây |
|---|---|---|
| IoC principle | Bài 02 | Container "drives", code đăng ký intent |
| Constructor injection | Bài 02 | Resolve dep qua param type |
| ApplicationContext | Bài 03 | Mini ApplicationContext = MiniContainer |
| Bean lifecycle | Bài 04 | Instantiate → populate (qua constructor) → @PostConstruct |
| Singleton scope | Bài 05 | Map<Class, Object> cache 1 instance / type |
@Component scan | Bài 06 | Qué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 ý
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
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ùngClassLoader.getResource(path)để lấy URL của package, list file.class, load quaClass.forName, kiểm tra annotation. Đơn giản hoá so với Spring (Spring dùng ASM bytecode reader để không phảiClass.forNametoà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ừbeanscache, gọinewInstance. Hậu init: tìm method có@PostConstructvà 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 —@PostConstructcó 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...