Spring Core & Boot/Inversion of Control — đảo ngược quyền kiểm soát
3/41
Bài 3 / 41~10 phútNhập môn & IoC/DIMiễn phí lượt xem

Inversion of Control — đảo ngược quyền kiểm soát

IoC là design principle: quyền tạo và nối dependency bị đảo từ code bạn viết sang container. Bài này giải thích bản chất IoC, lý do cần IoC (testability, decoupling), và cơ chế trước/sau khi áp dụng.

TL;DR: IoC (Inversion of Control) là design principle — quyền điều khiển việc tạo và nối dependency bị đảo ngược từ code bạn viết sang một bên ngoài (container/framework). Trước IoC, class tự new dependency; sau IoC, container quyết định tạo cái gì, khi nào, nối với ai. Lợi ích cốt lõi: class không còn biết implementation cụ thể của dependency → testable và decoupled. Dependency Injection là hình thức IoC phổ biến nhất Spring dùng để truyền dependency vào object.

Bài trước (Spring là gì) cho thấy Spring là IoC container. Bài này trả lời đúng một câu hỏi: IoC là gì và vì sao cần nó? Cơ chế container bên dưới (BeanFactory, singletonObjects map) được mổ tách ở BeanFactory vs ApplicationContext.

1. Analogy — đặt cơm vs tự nấu cơm

Bạn có hai cách ăn trưa:

Cách A — tự nấu: đi chợ, chọn rau thịt, sơ chế, nấu, dọn bàn. Mọi bước bạn quyết định và thực hiện — bạn kiểm soát toàn bộ flow.

Cách B — đặt GrabFood: bạn chỉ nói "tôi muốn cơm tấm sườn". Grab tìm quán, điều shipper, giao đúng giờ. Bạn không quyết định khi nào nấu, ai nấu, đi đường nào — quyền điều phối đó bị "đảo" sang Grab.

Trong software, tình huống tương tự:

Đời thườngSoftware
Tự nấu cơmClass tự new PaymentGateway(), tự gọi từng bước
Đặt GrabFoodClass khai báo "tôi cần PaymentGateway", container tạo và đưa vào
Bạn quyết toàn bộ flowTraditional: caller drives, code bạn khởi tạo mọi thứ
Grab quyết flowInverted: container drives, framework tạo và nối object cho bạn

Hai cột đối chiếu chiều mũi tên: bên trái OrderService tự gọi new nên mũi tên đi ra từ code bạn viết, bên phải Spring Container tạo sẵn các dependency rồi tiêm vào OrderService nên mũi tên đi vào

Cach nho nhanh

IoC = ai-quyết-flow bị đảo ngược. Trước: code bạn drive mọi thứ. Sau: container drive, code bạn chỉ khai báo nhu cầu.

2. Định nghĩa IoC chính xác

IoC (Inversion of Control) là design principle phát biểu: quyền kiểm soát việc tạo object và nối dependency được đảo ngược từ code bạn viết sang một thành phần bên ngoài (container, framework).

Thuật ngữ này không phải của Spring. Martin Fowler hệ thống hoá IoC năm 2004 trong bài viết kinh điển "Inversion of Control Containers and the Dependency Injection pattern". Fowler chỉ ra rằng "IoC" quá rộng — nó bao gồm nhiều kỹ thuật:

Kỹ thuậtCách đảo flowVí dụ
Dependency InjectionContainer tạo dependency và đưa vào constructor/setter@Autowired, Spring container
Service LocatorObject hỏi locator "cho tôi service X"ApplicationContext.getBean()
Template MethodFramework định nghĩa skeleton, gọi vào hook bạn overrideJdbcTemplate, RestTemplate
Event / CallbackBạn đăng ký listener, framework gọi khi event xảy ra@EventListener

DI chỉ là một trong nhiều hình thức IoC. Spring dùng cả bốn — nhưng Dependency Injection là hình thức chủ yếu để quản lý bean.

Trong docs Spring, "IoC container" tức là container làm tất cả bốn việc trên. Khi bạn nói "Spring là DI container" — đúng nhưng hẹp hơn.

3. Tại sao cần IoC — vấn đề khi tự quản lý dependency

Giả sử bạn có OrderService cần PaymentGatewayInventoryClient:

// Truoc IoC — OrderService tu quyet moi thu
public class OrderService {

    private final PaymentGateway payment;
    private final InventoryClient inventory;

    public OrderService() {
        // Kho khan 1: bi rang buoc vao implementation cu the
        this.payment = new StripePayment(System.getenv("STRIPE_KEY"));
        this.inventory = new HttpInventoryClient("http://inventory-svc");
    }

    public void placeOrder(Order order) {
        inventory.reserve(order);
        payment.charge(order.total());
    }
}

Ba vấn đề cụ thể:

Vấn đề 1 — Testability bằng 0: Để unit test placeOrder, bạn cần chạy thật StripePayment (gọi API Stripe thật) và HttpInventoryClient (cần server inventory thật). Không có cách nào thay bằng mock mà không sửa source code.

Vấn đề 2 — Coupling chặt: OrderService phụ thuộc trực tiếp vào StripePayment — muốn đổi sang MoMoPayment phải sửa bên trong OrderService. Mỗi lần đổi implementation là mỗi lần sửa code business.

Vấn đề 3 — Lifecycle không kiểm soát được: Mỗi lần tạo OrderService là tạo mới StripePaymentHttpInventoryClient. Không có cơ chế reuse, không có cách biết bao nhiêu instance đang tồn tại, không clean up đúng lúc.

4. Sau IoC — quyền tạo dependency chuyển sang container

Cùng nghiệp vụ, viết lại theo IoC:

// Sau IoC — OrderService chi khai bao nhu cau
@Service
public class OrderService {

    private final PaymentGateway payment;
    private final InventoryClient inventory;

    // Khai bao "toi can 2 dependency nay"
    // Container quyet tao gi, khi nao, inject vao day
    public OrderService(PaymentGateway payment, InventoryClient inventory) {
        this.payment = payment;
        this.inventory = inventory;
    }

    public void placeOrder(Order order) {
        inventory.reserve(order);
        payment.charge(order.total());
    }
}

OrderService không biết StripePayment tồn tại. Nó chỉ biết "tôi cần một PaymentGateway". Container quyết implementation nào được đưa vào.

Vấn đề 1 giải quyết — test dễ: Vì dependency đến từ ngoài, unit test có thể tạo mock:

// Test khong can Spring, khong can server that
@Test
void placeOrder_should_reserve_then_charge() {
    var mockPayment = mock(PaymentGateway.class);
    var mockInventory = mock(InventoryClient.class);

    var service = new OrderService(mockPayment, mockInventory);
    service.placeOrder(sampleOrder());

    verify(mockInventory).reserve(sampleOrder());
    verify(mockPayment).charge(sampleOrder().total());
}

Vấn đề 2 giải quyết — swap implementation không đụng OrderService: Đổi từ Stripe sang MoMo chỉ cần đăng ký MoMoPayment implements PaymentGateway — container inject đúng bean, OrderService không hay biết.

Vấn đề 3 giải quyết — container quản lý lifecycle: Spring quyết singleton hay prototype, khởi tạo khi nào, destroy khi nào. Code business không liên quan.

5. Cơ chế — ai giữ quyền control trước và sau

Đây là phần bóc rõ "inversion" thật sự diễn ra ở đâu:

Trước IoC: code bạn viết (hàm main, App.java) nắm quyền: gọi new, quyết thứ tự, quản lý instance, truyền config vào. Caller drives.

Sau IoC: quyền đó chuyển hoàn toàn sang container. Bạn chỉ làm 2 việc:

  1. Khai báo class là bean (@Service, @Component).
  2. Khai báo nhu cầu (constructor parameter).

Container đọc khai báo, xây dựng dependency graph, quyết thứ tự tạo, inject, quản lý lifecycle. Code bạn không gọi new một lần nào cho bean.

"Inversion" chính là: trước đây bạn gọi vào library/class khác; bây giờ container gọi vào code của bạn — bạn cung cấp logic, framework orchestrate.

IoC va Service Locator

Service Locator cũng là IoC — code hỏi container "cho tôi bean X" (ctx.getBean(PaymentGateway.class)). Nhưng dependency vẫn ẩn bên trong method, không hiện ở constructor → khó test, khó trace. Đây là lý do Dependency Injection được ưa hơn Service Locator — dependency rõ ràng tại constructor.

6. Pitfall — nhầm IoC với DI

Nhầm thường gặp: Cho rằng IoC và DI là một.

// Day la IoC nhung KHONG phai DI:
@Component
public class OrderProcessor {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        // Container goi vao method nay khi event xay ra
        // Flow bi dao nguoc (IoC), nhung khong co dependency nao duoc inject
    }
}

Spring gọi vào onOrderCreated khi event xảy ra — đây là IoC kiểu callback. Không có dependency nào được inject → không phải DI.

Hiểu đúng: IoC là principle. DI là một cách implement IoC (cách phổ biến nhất trong Spring). Container Spring thực hiện IoC theo nhiều hình thức cùng lúc.

Nhầm thứ hai: Inject ApplicationContext rồi gọi getBean() trong code business.

@Service
public class BadService {
    @Autowired
    private ApplicationContext ctx;   // anti-pattern

    public void doWork() {
        var p = ctx.getBean(PaymentGateway.class);  // service locator
        p.charge(amount);
    }
}

✅ Inject PaymentGateway thẳng vào constructor. ctx.getBean() ẩn dependency, làm hỏng testability — phủ nhận lợi ích chính của IoC. Xem chi tiết 3 hình thức inject ở bài Dependency Injection.

Liên hệ các bài khác

Bài này là một mảnh trong chuỗi container. Ghép với các bài xung quanh:

  • Spring là gì: bài trước đặt context "Spring là IoC container" — bài này giải thích IoC nghĩa là gì để câu đó không còn mơ hồ.
  • Dependency Injection: bài tiếp theo mổ cụ thể hình thức IoC Spring dùng chính — 3 cách inject (constructor/setter/field), thuật toán resolve dependency, và vì sao constructor được khuyến nghị.
  • BeanFactory vs ApplicationContext: sau khi hiểu IoC về mặt principle, bài đó bóc cơ chế bên dưới container — singletonObjects map, beanDefinitionMap, và tại sao getBean() lần hai nhanh hơn lần đầu.

Tóm tắt

  • IoC là design principle: quyền tạo và nối dependency đảo từ code bạn viết sang container/framework.
  • Trước IoC: class tự new dependency → coupling chặt, không testable, lifecycle không kiểm soát.
  • Sau IoC: class khai báo nhu cầu, container quyết implementation, inject, lifecycle — code business không gọi new.
  • DI là một hình thức IoC, cùng với Service Locator, Template Method, Event/Callback.
  • Lợi ích cốt lõi: testability (mock dễ qua constructor), decoupling (swap implementation không đụng business code), lifecycle tập trung.
  • "Inversion" thật sự: trước đây bạn gọi vào library; sau IoC container gọi vào code bạn.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Phát biểu nào mô tả đúng bản chất của IoC? (a) Framework tạo object thay bạn. (b) Quyền kiểm soát việc tạo và nối dependency chuyển từ code bạn viết sang container. (c) Code bạn không được gọi new nữa. (d) Spring quản lý database connection.
  2. Q2
    Đoạn code sau có áp dụng IoC không? Nếu có, đây là hình thức nào — DI hay không phải DI? Giải thích.
    @Component
    public class AuditService {
      @EventListener
      public void onLogin(UserLoginEvent event) {
          log.info("User logged in: " + event.userId());
      }
    }
  3. Q3
    Trước IoC, class OrderService tự new StripePayment() trong constructor. Điều này gây ra hai vấn đề cụ thể nào? Giải thích cách IoC giải quyết từng vấn đề.
  4. Q4
    Đoạn code dưới inject ApplicationContext để gọi getBean(). Đây có phải IoC không? Đây có phải DI không? Vấn đề là gì?
    @Service
    public class ReportService {
      @Autowired
      private ApplicationContext ctx;
    
      public void generate(String type) {
          var formatter = ctx.getBean(type, ReportFormatter.class);
          formatter.format(getData());
      }
    }
  5. Q5
    Fowler 2004 đề xuất tên "Dependency Injection" để thay "IoC" khi nói về wiring dependency. Lý do là gì? Tên "IoC" có vấn đề gì khi dùng để mô tả Spring?

Bài tiếp theo: Dependency Injection — 3 hình thức

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Dependency Injection — 3 hình thức, cơ chế resolve, và generic-aware injection