Spring Boot/IoC và DI — bóc tách 2 thuật ngữ bị lạm dụng nhất
~28 phútSpring là gì & nền tảng IoCMiễn phí

IoC và DI — bóc tách 2 thuật ngữ bị lạm dụng nhất

Inversion of Control là principle, Dependency Injection là 1 implementation cụ thể. Bài này phân biệt rõ qua code, đối chiếu với Martin Fowler 2004, so sánh @Autowired (Spring) vs @Inject (JSR-330), 3 hình thức inject, 4 autowire mode, generic-aware injection, và circular dependency 3 cách giải.

Bạn đọc 5 bài blog Spring sẽ thấy 5 cách giải thích IoC và DI khác nhau — đa phần lập lờ giữa "principle" và "tool". Khi bạn hỏi senior trong team "IoC khác DI ở đâu?", câu trả lời thường là một câu xa xôi kiểu "IoC nghĩa là Spring quản lý object thay bạn". Câu đó không sai, nhưng cũng không giúp bạn hiểu hơn.

Bài này phân biệt 2 thuật ngữ ở mức bản chất: IoC là principle thiết kế, DI là 1 trong nhiều cách implement IoC. Sau bài này, bạn nói được chính xác Spring làm gì khi gặp @Autowired, vì sao constructor injection được khuyến nghị thay setter, kể tên được vài kỹ thuật IoC ngoài DI, biết khác biệt giữa @Autowired (Spring) và @Inject (chuẩn JSR-330), và xử lý được 3 dạng circular dependency.

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

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

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

Cách B — đặt grab food: bạn nói "tôi muốn cơm tấm", grab gửi shipper đến đúng giờ. Bạn không quyết khi nào nấu, ai nấu, đi đường nào — flow đó được "đảo ngược" cho bên ngoài.

Đời thườngSoftware
Tự nấu cơmClass tự new dependency, tự gọi method theo thứ tự
Đặt grab foodClass khai báo "tôi cần X", framework đưa X vào
Bạn quyết flowTraditional flow — caller drives
Grab quyết flowInverted flow — framework drives
💡 Cách nhớ

IoC = ai-quyết-flow đảo ngược. DI = một cách cụ thể để framework "đưa dependency cho bạn", giống grab giao đồ ăn đến tận tay.

2. Inversion of Control — định nghĩa chính xác

IoC không phải khái niệm của Spring. Nó là design principle trong software engineering, được Martin Fowler hệ thống hoá năm 2004 trong bài viết "Inversion of Control Containers and the Dependency Injection pattern".

Định nghĩa đúng:

IoC là principle khi flow control của chương trình bị đảo ngược so với traditional procedural programming. Thay vì code bạn viết gọi vào framework/library, framework gọi vào code bạn viết.

Đây là điểm dễ nhầm: IoC nghĩa là "đảo ngược flow", không phải đảo ngược "object creation". Có nhiều cách đảo flow control:

Kỹ thuậtCách đảo flowVí dụ Spring
Dependency InjectionFramework tạo dependency và đưa vào constructor/setter của object@Autowired
Service LocatorObject hỏi locator "cho tôi service X" — locator trả về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 listener khi event xảy ra@EventListener, ApplicationListener
Strategy via lookupFramework load class qua reflection/SPIMETA-INF/services/, AutoConfiguration.imports

5 kỹ thuật trên đều "inverted control". DI chỉ là 1 trong 5.

flowchart TB
    IoC["IoC<br/>(principle - dao nguoc flow control)"]
    DI["Dependency Injection"]
    SL["Service Locator"]
    TM["Template Method"]
    EC["Event Callback"]
    SP["SPI / Reflection"]

    IoC --> DI
    IoC --> SL
    IoC --> TM
    IoC --> EC
    IoC --> SP

    style IoC fill:#fef3c7,stroke:#d97706
    style DI fill:#d1fae5,stroke:#059669

Hệ quả nói chính xác: "Spring là IoC container" đúng vì Spring đảo flow theo nhiều cách (DI cho bean, callback cho lifecycle, event publisher, AOP advice). "Spring là DI container" cũng đúng nhưng hẹp hơn — chỉ nói về phần inject dependency.

2.1 Tại sao thuật ngữ "IoC" gây nhầm lẫn

Trước Fowler, "IoC" được dùng cho mọi thứ "framework gọi code bạn" — từ Servlet doGet(), JUnit @Test, đến event listener Swing. Vì vậy nói "Spring là IoC container" không cho biết gì cụ thể.

Fowler 2004 đề xuất thuật ngữ mới rõ hơn: Dependency Injection. Từ đó, cộng đồng dùng:

  • DI khi nói về wiring dependency.
  • IoC khi nói về principle tổng quát.

Spring docs vẫn giữ "IoC container" vì lý do lịch sử — Rod Johnson 2003 dùng từ "IoC". Bạn đọc Spring docs gặp "IoC" hiểu là "DI + lifecycle management + event + lookup" tổng hợp.

3. Dependency Injection — 3 hình thức

DI có 3 cách inject dependency vào object. Cùng ví dụ OrderService cần PaymentGateway:

3.1 Constructor injection — KHUYẾN NGHỊ

@Service
public class OrderService {
    private final PaymentGateway payment;

    public OrderService(PaymentGateway payment) {
        this.payment = payment;
    }
}

Đặc điểm:

  • Field final → không thể thay đổi sau khi construct → thread-safe.
  • Dependency là bắt buộc — không tạo được object nếu thiếu payment.
  • Test dễ: new OrderService(mockPayment) không cần Spring.
  • Phát hiện circular dependency tại startup (Spring throw BeanCurrentlyInCreationException).

Từ Spring 4.3, không cần @Autowired trên constructor nếu class chỉ có 1 constructor — Spring tự nhận diện.

3.2 Setter injection — chỉ dùng cho dependency optional

@Service
public class ReportService {
    private NotificationService notifier;

    @Autowired(required = false)
    public void setNotifier(NotificationService notifier) {
        this.notifier = notifier;
    }
}

Đặc điểm:

  • Field không thể final.
  • Có thể set lại sau khi construct → mutable, khó reasoning.
  • Hợp khi dependency thực sự optional (có thì dùng, không có thì skip).

3.3 Field injection — KHÔNG KHUYẾN NGHỊ

@Service
public class BadService {
    @Autowired
    private PaymentGateway payment;
}

Đặc điểm:

  • Trông gọn nhưng hidden cost: không test được ngoài Spring (không có cách set field private từ ngoài, trừ khi reflection).
  • Field không final được.
  • Che giấu việc class có nhiều dependency — class với 12 @Autowired field vẫn "trông gọn", trong khi constructor 12 param ngay lập tức báo "class này quá lớn, cần tách".

Spring Team chính thức khuyến nghị constructor injection trong reference docs:

📚 Quote chính chủ

"The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null."

Nguồn: Spring Framework Reference — Constructor-based or setter-based DI

3.4 Bảng so sánh tổng

Tiêu chíConstructorSetterField
final field
Bắt buộc dep tại init
Test không cần SpringHơi khó❌ (cần reflection)
Phát hiện circular tại startup❌ (defer)❌ (defer)
Force class size discipline
Code "trông gọn"❌ (verbose)
Tương thích Lombok @RequiredArgsConstructor

4. @Autowired vs @Inject — Spring vs JSR-330

Spring có annotation riêng @Autowired, nhưng cũng support chuẩn JSR-330 @Inject:

// Spring native:
@Service
public class A {
    @Autowired
    private Foo foo;
}

// JSR-330 standard (jakarta.inject.Inject):
@Service
public class B {
    @Inject              // hoat dong y het @Autowired
    private Foo foo;
}
AnnotationPackagerequired = false?Default
@Autowiredorg.springframework.beans.factory.annotationCó (@Autowired(required = false))required
@Injectjakarta.inject (JSR-330)Không trực tiếp — phải dùng Optional<T>required
@Resourcejakarta.annotation (JSR-250)Khôngrequired, lookup byName

3 annotation hoạt động trên Spring; chọn @Autowired trong code Spring vì:

  • Là native, có nhiều option (required, @Qualifier, mode parameter).
  • Nhất quán với codebase Spring xung quanh.

Khi nào dùng @Inject? Khi viết library cần chạy trên cả Spring và CDI (Java EE), hoặc team có lý do chuẩn hoá. Hiếm.

@Resource đặc biệt: lookup theo name trước, type sau — ngược với @Autowired. Hữu ích khi nhiều bean cùng type:

@Resource(name = "stripePayment")
private PaymentGateway payment;

5. Spring làm gì khi gặp @Autowired — bóc tầng 1

Đoạn code này:

@Service
public class OrderService {
    private final PaymentGateway payment;

    public OrderService(PaymentGateway payment) {
        this.payment = payment;
    }
}

Khi container start, các bước theo thứ tự:

sequenceDiagram
    participant App as App startup
    participant Ctx as ApplicationContext
    participant BPP as BeanPostProcessor<br/>(AutowiredAnnotationBPP)
    participant OS as OrderService

    App->>Ctx: refresh()
    Ctx->>Ctx: scan @Component / @Service classes
    Note over Ctx: Tim thay OrderService class
    Ctx->>Ctx: doc constructor cua OrderService<br/>thay can PaymentGateway
    Ctx->>Ctx: tim bean ?Type=PaymentGateway
    Note over Ctx: Tim duoc StripePaymentGateway bean
    Ctx->>OS: new OrderService(stripeBean)
    Ctx->>BPP: post-process OrderService
    Ctx->>Ctx: register OrderService bean<br/>vao singleton cache

5 bước cụ thể:

  1. Component scan: container quét classpath theo @ComponentScan (Spring Boot mặc định scan từ package chứa @SpringBootApplication). Class có @Component/@Service/@Repository/@Controller được register thành bean definition.
  2. Bean definition giữ metadata: class, scope, dependencies, lifecycle. Chưa instantiate.
  3. Topological sort: container build dependency graph. Phát hiện cycle → throw error tại startup.
  4. Instantiation: container chọn constructor, resolve mỗi parameter (tìm bean cùng type, ambiguity → match theo @Qualifier/name), gọi new với bean đã resolve.
  5. Post-processing: BeanPostProcessor chạy callback trước/sau khi bean init (vd @PostConstruct, AOP proxy wrap). Bean cuối cùng được lưu vào singleton cache.

Spring không "đoán" — nó đọc bytecode/reflection để biết constructor nào, parameter type gì, từ đó match. Không có magic, chỉ là container reflection + topological sort + lifecycle callback.

5.1 Cụ thể về resolve dependency

Khi resolve param type PaymentGateway, Spring chạy thuật toán này:

flowchart TD
    Start[Resolve param: PaymentGateway]
    Q1{Co bean nao type PaymentGateway?}
    Err1[NoSuchBeanDefinitionException]
    Q2{Co bao nhieu bean?}
    Use[Use bean - DONE]
    Q3{Co @Qualifier tren param?}
    Match[Match by qualifier name]
    Q4{Co @Primary?}
    UsePrimary[Use @Primary bean]
    Q5{Co bean ten = paramName?}
    UseByName[Use bean by name]
    Err2[NoUniqueBeanDefinitionException]

    Start --> Q1
    Q1 -->|Khong| Err1
    Q1 -->|Co| Q2
    Q2 -->|1| Use
    Q2 -->|2+| Q3
    Q3 -->|Co| Match
    Q3 -->|Khong| Q4
    Q4 -->|Co| UsePrimary
    Q4 -->|Khong| Q5
    Q5 -->|Co| UseByName
    Q5 -->|Khong| Err2

    style Use fill:#d1fae5
    style UsePrimary fill:#d1fae5
    style UseByName fill:#d1fae5
    style Err1 fill:#fee
    style Err2 fill:#fee

Đây chính là thuật toán bạn cần nhớ. Đa phần bug "ambiguous" tại startup đều dừng ở Err2 — chưa thoả @Qualifier/@Primary/byName.

6. Autowire modes — 4 chế độ

Spring có 4 mode autowire (cấu hình qua @Bean(autowire = ...) hoặc XML cũ):

ModeCơ chếKhi nào dùng
no (default)Không tự động — phải @Autowired explicitJava config (default 2026)
byTypeMatch parameter theo typeDùng implicit khi @Autowired
byNameMatch theo tên parameter/setterKhi nhiều bean cùng type, không muốn @Qualifier
constructorbyType áp dụng cho constructor parameterTương đương @Autowired trên constructor

Mode no + @Autowired (default annotation-based) là cách 2026 dùng. Bạn ít gặp byName/constructor mode trừ khi đọc code XML legacy.

7. Generic-aware injection — Spring 4+

Một feature mạnh: Spring có thể inject theo generic type:

public interface Repository<T> { ... }

@Component
public class UserRepository implements Repository<User> { ... }

@Component
public class OrderRepository implements Repository<Order> { ... }

@Service
public class UserService {
    private final Repository<User> repo;        // Spring chon UserRepository

    public UserService(Repository<User> repo) {
        this.repo = repo;
    }
}

Spring đọc generic parameter <User> qua reflection (Java erasure tại runtime, nhưng signature giữ trong bytecode), match đúng UserRepository mặc dù cả 2 cùng implement Repository.

7.1 Inject Collection — gom tất cả implementation

public interface PaymentGateway {
    String name();
}

@Component class StripePayment implements PaymentGateway { ... }
@Component class MoMoPayment implements PaymentGateway { ... }
@Component class VnPayPayment implements PaymentGateway { ... }

@Service
public class PaymentRouter {
    private final List<PaymentGateway> gateways;
    private final Map<String, PaymentGateway> byName;

    public PaymentRouter(List<PaymentGateway> gateways,
                          Map<String, PaymentGateway> byName) {
        this.gateways = gateways;
        this.byName = byName;          // key = bean name (stripePayment, moMoPayment, ...)
    }

    public PaymentGateway pick(String code) {
        return gateways.stream()
            .filter(g -> g.name().equals(code))
            .findFirst()
            .orElseThrow();
    }
}

Spring inject tất cả bean match type vào collection. Map thì key = bean name. Đây là cách clean để implement strategy pattern — không cần switch-case, không cần factory.

Order quan trọng? Dùng @Order annotation hoặc implement Ordered:

@Component
@Order(1)
public class StripePayment implements PaymentGateway { ... }

@Component
@Order(2)
public class MoMoPayment implements PaymentGateway { ... }

Spring sort theo @Order ascending. Hữu ích khi cần thứ tự deterministic (vd chain of filter, fallback handler).

8. So sánh trực tiếp — không Spring vs có Spring

Cùng nghiệp vụ, viết 2 cách:

8.1 Không có Spring — caller drives

public class App {
    public static void main(String[] args) {
        // App chiu trach nhiem assemble graph
        var stripeKey = System.getenv("STRIPE_KEY");
        var smtpHost = System.getenv("SMTP_HOST");

        PaymentGateway payment = new StripePayment(stripeKey);
        EmailService email = new SmtpEmailService(smtpHost);
        InventoryClient inventory = new HttpInventoryClient();

        OrderService orderService = new OrderService(payment, email, inventory);
        UserService userService = new UserService(email);

        // 200 dong wiring tiep tuc...
    }
}

App phải biết tất cả implementation, đọc env, tạo graph theo đúng thứ tự, manage lifecycle. Đây là traditional flow — bạn drive everything.

8.2 Có Spring — framework drives

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}

@Service
public class OrderService {
    private final PaymentGateway payment;
    private final EmailService email;
    private final InventoryClient inventory;

    public OrderService(PaymentGateway payment, EmailService email, InventoryClient inventory) {
        this.payment = payment;
        this.email = email;
        this.inventory = inventory;
    }
}

@Service
public class StripePayment implements PaymentGateway {
    public StripePayment(@Value("${stripe.key}") String key) { /* ... */ }
}

Bạn không assemble graph. Bạn chỉ khai báo:

  • "Class này là service" (@Service).
  • "Class này cần các dependency này" (constructor parameter).
  • "Property này lấy từ config" (@Value).

Container đọc khai báo, làm phần wiring. Flow control đảo ngược — Spring drives.

9. Circular dependency — 3 dạng và cách giải

Circular dependency là khi A cần B, B cần A. Spring handle khác nhau tuỳ injection mode:

9.1 Circular qua constructor — FAIL tại startup

@Service public class A { A(B b) { ... } }
@Service public class B { B(A a) { ... } }

Spring throw BeanCurrentlyInCreationException tại startup. Đây là behavior tốt — phát hiện sớm, không để bug runtime.

9.2 Circular qua setter/field — Spring tự "giải" (nguy hiểm)

@Service
public class A {
    @Autowired private B b;       // field injection
}

@Service
public class B {
    @Autowired private A a;       // field injection
}

Spring giải bằng early reference: tạo A (chưa init xong), expose reference, tạo B với reference của A đó, populate field A.b sau. App start được — nhưng:

  • Trong constructor A, this.b còn null (chưa populate).
  • Trong @PostConstruct của A, this.b đã set — OK.
  • Race condition tinh tế: nếu A spawn thread trong constructor dùng this.b, thread đọc null.

Spring Boot 2.6+ tắt feature này mặc định (spring.main.allow-circular-references=false). Bật lại chỉ là band-aid — fix root cause: circular dep là dấu hiệu modeling sai.

9.3 3 cách fix circular dependency

Cách 1 — Refactor (tốt nhất): trích logic chung sang class C.

@Service public class C { /* shared logic */ }
@Service public class A { A(C c) { ... } }
@Service public class B { B(C c) { ... } }

90% case fix bằng cách này. Nếu A và B cùng cần nhau, có khả năng cao chúng nên là 1 class hoặc cần class trung gian.

Cách 2 — @Lazy (band-aid hợp lý):

@Service
public class A {
    private final B b;

    public A(@Lazy B b) {
        this.b = b;            // b la proxy, resolve khi method call
    }
}

@Lazy tạo proxy — bean B chỉ được resolve khi A gọi method trên proxy. Loại bỏ vòng tại construction time.

Tradeoff: lazy resolve = method call đầu chậm hơn, có overhead proxy nhỏ. Acceptable.

Cách 3 — ObjectProvider<T> (defer cao nhất):

@Service
public class A {
    private final ObjectProvider<B> bProvider;

    public A(ObjectProvider<B> bProvider) {
        this.bProvider = bProvider;
    }

    public void doStuff() {
        B b = bProvider.getObject();    // resolve moi lan goi
        b.work();
    }
}

Defer maximum — A không giữ reference nào với B tại construction. Phù hợp khi B chưa chắc tồn tại (optional bean) hoặc cần fresh instance (prototype).

10. Pitfall tổng hợp

Nhầm 1: Cho rằng IoC = DI. ✅ DI là 1 implementation của IoC. Còn callback, template method, event đều là IoC.

Nhầm 2: Field injection vì code "gọn hơn". ✅ Constructor injection — final, immutable, test không cần Spring, force phát hiện class quá lớn.

Nhầm 3: Dùng @Autowired trên constructor (Spring 4.3+ không cần).

@Service
public class OrderService {
    @Autowired   // thua nhung khong sai
    public OrderService(PaymentGateway p) { ... }
}

✅ Bỏ @Autowired đi — class chỉ có 1 constructor, Spring tự nhận.

Nhầm 4: Inject ApplicationContext rồi gọi getBean() — service locator anti-pattern.

@Service
public class BadService {
    @Autowired ApplicationContext ctx;

    public void doWork() {
        var p = ctx.getBean(PaymentGateway.class);   // anti-pattern
        p.charge(...);
    }
}

✅ Inject PaymentGateway thẳng vào constructor. Inject ApplicationContext chỉ khi cần dynamic lookup (rất hiếm).

Nhầm 5: Circular dependency bật lại allow-circular-references=true để app start.

spring.main.allow-circular-references: true   # band-aid

✅ Refactor — tách common logic ra service C, hoặc dùng @Lazy/ObjectProvider. Tốt nhất: refactor — circular dep thường là dấu hiệu modeling sai.

Nhầm 6: Inject Lombok @Autowired cùng constructor injection.

@RequiredArgsConstructor   // Lombok sinh constructor
public class OrderService {
    private final PaymentGateway payment;

    @Autowired                            // THUA - Lombok da sinh constructor
    public OrderService(PaymentGateway p, EmailService e) { ... }   // 2 constructor xung dot
}

✅ Chọn 1 cách: hoặc Lombok @RequiredArgsConstructor (sinh tự động), hoặc viết manual. Không trộn.

Nhầm 7: Nhầm @Resource với @Autowired.

@Resource
private List<PaymentGateway> gateways;   // @Resource khong inject Collection nhu @Autowired

✅ Dùng @Autowired cho Collection inject. @Resource chỉ lookup 1 bean theo name.

11. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Reference docs nên đọc song song bài này:

Bài viết kinh điển ngoài docs:

JSR chuẩn:

Source code để đọc khi muốn đi sâu:

  • org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor — BPP xử lý @Autowired.
  • org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency — algorithm resolve dependency. Search method doResolveDependency.

Ghi chú: đọc Fowler trước, rồi quay lại Spring docs sẽ thấy Spring docs gần như ánh xạ 1-1 từ ngôn ngữ Fowler — không phải trùng hợp, Spring team trực tiếp tham khảo bài đó.

12. Tóm tắt

  • IoC là design principle: flow control đảo ngược từ caller code sang framework. Có 5 kỹ thuật implement IoC: DI, Service Locator, Template Method, Callback, SPI.
  • DI là 1 trong 5 — framework tạo dependency và đưa vào object qua constructor/setter/field.
  • Martin Fowler 2004 hệ thống hoá thuật ngữ — đề xuất "DI" để rõ hơn "IoC" tổng quát.
  • Constructor injection là default khuyến nghị: final, immutable, test ngoài Spring, phát hiện circular dep tại startup, force class size hợp lý.
  • Setter injection chỉ dùng cho dependency optional. Field injection là anti-pattern.
  • Spring có @Autowired (native), @Inject (JSR-330), @Resource (JSR-250). Trong code Spring dùng @Autowired.
  • Spring resolve dep theo flowchart: type → qualifier → primary → name → fail. Hiểu thuật toán giúp debug nhanh.
  • Generic-aware injection: Repository<User> vs Repository<Order>. Collection injection: List<Strategy>, Map<String, Strategy> — implement strategy pattern không cần factory.
  • Spring xử lý @Autowired qua: component scan → bean definition → topological sort → instantiation → post-processing. Không magic.
  • Service locator (ctx.getBean() trong code) là anti-pattern đối lập với DI — tránh.
  • Circular dep qua constructor → fail startup (good). Qua field/setter → Spring tự giải bằng early reference, nhưng Boot 2.6+ tắt mặc định. Fix bằng refactor / @Lazy / ObjectProvider.

13. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn code sau hiện thực IoC theo cách nào — DI hay không phải DI? Tại sao?
@Component
public class OrderProcessor {
  @EventListener
  public void onOrderCreated(OrderCreatedEvent event) {
      // ...
  }
}

Đây là IoC nhưng KHÔNG phải DI. Cụ thể là event/callback pattern.

Lý do: Spring đảo flow bằng cách gọi vào method của bạn (onOrderCreated) khi event xảy ra. Bạn đăng ký listener qua @EventListener, framework tự khởi event loop và dispatch. Đây đúng định nghĩa IoC ("framework gọi code bạn"), nhưng không có dependency nào được inject — không có constructor parameter resolve.

Đây minh hoạ điểm bài học: IoC bao gồm nhiều kỹ thuật, DI chỉ là 1 trong số đó. Spring dùng cả DI lẫn event callback cùng lúc.

Q2
Constructor injection được khuyến nghị vì 4 lý do. Liệt kê và giải thích từng lý do bằng ví dụ ngược lại (trường hợp dùng setter/field sẽ tệ hơn).
  • Field final → immutable + thread-safe: Setter cho phép gọi service.setPayment(other) sau init → 2 thread đọc/ghi field cùng lúc gây race. Constructor + final loại bỏ rủi ro.
  • Bắt buộc dependency tại construct time: Field injection cho phép new object thiếu dep (test code có thể quên set), gây NPE runtime. Constructor không cho compile nếu thiếu argument.
  • Test ngoài Spring: Field injection field private → test phải dùng reflection hoặc @SpringBootTest nặng. Constructor: new OrderService(mockPayment) đủ.
  • Phát hiện class quá lớn: Class có 12 @Autowired field trông vẫn gọn. Constructor 12 parameter trông xấu ngay → developer biết phải tách class. IDE cũng cảnh báo "constructor too many parameters".

Rule chung: choose API style buộc developer làm điều đúng. Constructor injection là API đó.

Q3
Spring 4.3+ cho phép bỏ @Autowired trên constructor nếu class chỉ có 1 constructor. Vì sao Spring quyết định cho phép điều này? Lợi ích cụ thể là gì?

Lý do: @Autowired trên constructor duy nhất là thông tin thừa. Spring nhìn thấy class chỉ có 1 constructor → đương nhiên dùng constructor đó để inject, không có lựa chọn nào khác. Yêu cầu developer viết @Autowired mỗi lần là noise.

Lợi ích cụ thể:

  • Code sạch hơn: POJO trông như Java thuần, không có annotation Spring trên constructor.
  • Class business có thể chạy ngoài Spring: nếu bỏ tất cả annotation Spring đi (vd test với new), constructor không có @Autowired dư thừa.
  • Encourage constructor injection: bỏ "thuế annotation" làm constructor injection thuận tiện hơn field injection — push developer về phía best practice.
  • Tương thích Lombok @RequiredArgsConstructor: Lombok sinh constructor không có @Autowired, Spring vẫn inject được.

Class có nhiều constructor vẫn cần @Autowired để chỉ rõ constructor nào Spring dùng.

Q4
Service locator pattern là gì? Vì sao nó được coi là anti-pattern khi đã có DI? Cho code ví dụ minh hoạ vấn đề.

Service Locator: object hỏi 1 "locator" (registry) "cho tôi service X" — locator trả về. Implement trong Spring:

@Service
public class OrderService {
  private final ApplicationContext ctx;

  public OrderService(ApplicationContext ctx) { this.ctx = ctx; }

  public void place(Order o) {
      var payment = ctx.getBean(PaymentGateway.class);  // service locator
      payment.charge(o.total());
  }
}

Đây vẫn là IoC (Spring tạo OrderService và inject ctx), nhưng không phải DI cho PaymentGateway.

Vấn đề so với DI:

  • Hidden dependency: nhìn constructor không biết OrderService cần PaymentGateway. Phải đọc body method mới thấy getBean().
  • Test khó: phải mock ApplicationContext (object khổng lồ), không thể chỉ mock PaymentGateway.
  • Runtime failure: nếu không có bean PaymentGateway, lỗi xảy ra khi gọi place() chứ không phải startup. DI sẽ throw ngay startup.

Khi nào tạm chấp nhận: lookup động theo runtime data (vd "lấy bean tên = tenantId"). Trường hợp này dùng ObjectProvider hoặc factory bean rõ ràng hơn ApplicationContext trực tiếp.

Q5
Bạn có 2 implementation của PaymentGateway: StripePaymentMoMoPayment, đều annotate @Service. Constructor của OrderService nhận PaymentGateway. Spring sẽ làm gì? Có 2 cách fix — kể ra. Cách thứ 3 là gì?

Spring sẽ throw NoUniqueBeanDefinitionException tại startup — vì có 2 bean cùng type nhưng constructor không chỉ rõ chọn cái nào.

Fix cách 1 — @Qualifier theo tên:

public OrderService(@Qualifier("stripePayment") PaymentGateway p) { ... }

Tên mặc định của bean = tên class với chữ đầu lowercase (StripePaymentstripePayment).

Fix cách 2 — @Primary trên 1 bean:

@Service
@Primary
public class StripePayment implements PaymentGateway { ... }

@Service
public class MoMoPayment implements PaymentGateway { ... }   // khong primary

Spring chọn @Primary khi có ambiguity.

Fix cách 3 — Inject Collection (advanced, mạnh nhất):

public OrderService(List<PaymentGateway> all,
                  Map<String, PaymentGateway> byName) { ... }

Spring inject tất cả implementation. List theo thứ tự @Order (nếu có). Map với key = bean name. Đây là pattern strategy/chain-of-responsibility — runtime chọn implementation theo logic, không phải compile-time qualifier.

Khi nào dùng cái nào: @Primary khi có 1 default rõ ràng (90% case dùng Stripe). @Qualifier khi cần chỉ rõ từng vị trí inject. List/Map khi runtime chọn dynamically.

Q6
Đoạn sau khởi động thành công không? Nếu thành công, in gì? Nếu fail, vì sao?
@Service
public class A {
  @Autowired private B b;
  public void hello() { System.out.println("A says " + b.name()); }
}

@Service
public class B {
  @Autowired private A a;
  public String name() { return "B"; }
}

Tuỳ Spring Boot version:

  • Spring Boot < 2.6: khởi động thành công. Spring giải circular bằng early reference: tạo A, expose nó (chưa init xong), tạo B (field a = early A reference), populate field b của A = B đã tạo. Gọi a.hello() in "A says B".
  • Spring Boot 2.6+: FAIL tại startup với error "Requested bean is currently in creation: Is there an unresolvable circular reference?" — Boot tắt feature mặc định (spring.main.allow-circular-references=false) vì circular dep là design smell.

Cách đúng:

  • Refactor — extract logic chung sang class C.
  • Hoặc đổi field injection sang constructor injection — Spring throw thẳng BeanCurrentlyInCreationException tại startup, force fix.
  • Hoặc @Lazy trên 1 phía: @Autowired @Lazy private B b; — phá vòng tại construction.

Bài học: circular dep ở field injection là "im lặng nhưng nguy hiểm" — code chạy nhưng có thể bug subtle. Constructor injection làm vấn đề lộ ngay → fix sớm.

Q7
Đoạn sau làm gì? Có lợi ích gì so với switch-case theo type code?
public interface PaymentGateway { String code(); void charge(Money m); }

@Component class StripePayment implements PaymentGateway { public String code() { return "stripe"; } /* ... */ }
@Component class MoMoPayment implements PaymentGateway { public String code() { return "momo"; } /* ... */ }

@Service
public class PaymentRouter {
  private final Map<String, PaymentGateway> gateways;

  public PaymentRouter(List<PaymentGateway> all) {
      this.gateways = all.stream().collect(toMap(PaymentGateway::code, g -> g));
  }

  public void charge(String code, Money m) {
      PaymentGateway g = gateways.get(code);
      if (g == null) throw new IllegalArgumentException("Unknown: " + code);
      g.charge(m);
  }
}

Đây là strategy pattern + plugin architecture qua DI. Spring inject tất cả bean implement PaymentGateway vào List, PaymentRouter tự build Map<code, gateway> để route.

Lợi ích so với switch-case:

  • Open-closed principle: thêm VnPayPayment implement PaymentGateway + @Component → router tự pick up. Không cần sửa PaymentRouter.
  • Modular: mỗi gateway 1 class riêng, có dependency riêng (Stripe SDK, MoMo SDK), không lẫn lộn trong 1 class.
  • Testable: mock List<PaymentGateway> với 1 mock implementation cho test.
  • Plugin: nếu module Stripe và MoMo ở 2 jar khác nhau, conditional include via @ConditionalOnClass — deploy nhỏ hơn cho khách không cần Stripe.
  • Order: nếu cần thứ tự (vd fallback chain), thêm @Order trên class — Spring inject List đã sort.

Đây là pattern phổ biến trong Spring — chain of responsibility, validators, event handlers, security filters đều dùng cấu trúc này. Khi gặp "switch giữa 5+ case" trong Spring code, nghĩ tới pattern này trước.

Bài tiếp theo: ApplicationContext và BeanFactory — container hoạt động ra sao bên dưới

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...