Spring Framework và IoC
Xem thửMột công ty chuyển từ cổng thanh toán cũ sang cổng mới. Việc tưởng đơn giản: đổi một dòng. Nhưng 41 lớp trong dự án đều có đoạn private PaymentGateway gateway = new OldGateway(); ngay trong trường của chúng, và mỗi lớp phải được sửa, biên dịch và kiểm thử lại. Tệ hơn, bộ kiểm thử đơn vị vốn chạy trong 20 giây bắt đầu mất 15 phút, vì mỗi bài kiểm thử của dịch vụ đơn hàng thật sự gọi môi trường sandbox của cổng thanh toán. Hôm sau, một bài kiểm thử gửi nhầm lệnh tới cổng thật. Không có dòng code nào sai về mặt cú pháp: mọi lớp đều biên dịch và đều chạy. Cái sai là chỗ đặt quyết định "dùng bản cài đặt nào" trong chính lớp cần nó.
Bài này mở đầu chương Spring Core bằng đúng vấn đề đó. Inversion of Control (IoC) là đảo ngược quyền quyết định này: lớp chỉ khai báo nó cần gì, còn việc tạo và truyền vào do bên ngoài đảm nhận, ở ứng dụng là container Spring. Bạn sẽ chạy hai bản của cùng một dịch vụ đơn hàng, đo thứ tự container tạo các bean, và viết một bài kiểm thử không cần container nhờ đối tượng giả.
Sau bài này, bạn có thể
- Giải thích IoC và Dependency Injection (DI) bằng hai bản code chạy được của cùng một dịch vụ.
- Chỉ ra vì sao
newtrong lớp làm kiểm thử và thay thế khó, bằng một sự kiện đo được. - Mô tả thứ tự container tạo bean và đọc đồ thị phụ thuộc của nó bằng mã.
- Kiểm thử một lớp dùng constructor injection mà không cần dựng container.
- Kể tên các module của Spring Core thật sự có trên classpath và vai trò của chúng.
Chuẩn bị môi trường và dự án
Mọi thí nghiệm của bài chạy trong một dự án Maven nhỏ. Mục này nói rõ cần cài gì, khai báo thư viện nào và cấu hình ra sao; mã của từng lớp được trình bày ngay tại chỗ nó được dùng, kèm giải thích, nên bạn không cần mở nơi nào khác. Số liệu và thông báo lỗi trong bài được đo từ chính mã đó (JDK 17, Spring Framework 7.0.9). Bản sao lưu cả dự án nằm ở kho mã của khóa học, thư mục 06-spring-core/01-ioc-va-di, chỉ để bạn clone cho nhanh nếu không muốn tự gõ lại.
Yêu cầu môi trường
| Thành phần | Phiên bản đã kiểm chứng | Ghi chú |
|---|---|---|
| JDK | 17 trở lên | java -version |
| Maven | 3.9 trở lên | mvn -v |
| Spring Framework | 7.0.9 | khai báo trong pom.xml |
| Docker | không cần | bài này không dùng cơ sở dữ liệu |
Cấu trúc
01-ioc-va-di/
pom.xml
src/main/java/dev/canhdominich/spring/ioc/
Events.java ghi lại và in các sự kiện (tạo bean, gọi thanh toán)
PaymentService.java interface thanh toán
PaypalPayment.java cài đặt thật, là một @Component
FakePayment.java bản giả dùng khi kiểm thử, không phải bean
HardWiredOrderService.java dịch vụ tự tạo phụ thuộc bằng new
OrderService.java dịch vụ nhận phụ thuộc qua constructor, là một @Service
AppConfig.java lớp cấu hình bật quét component
IocDemo.java ba thí nghiệm: tự tạo, container, bản giả
ModulesDemo.java liệt kê các jar spring trên classpath
Khai báo thư viện: pom.xml
pom.xml
Dự án Maven Java 17 chỉ khai báo spring-context 7.0.9 (kéo theo spring-core, spring-beans, spring-aop và spring-expression), jakarta.annotation-api cho các annotation vòng đời, cùng JUnit và AssertJ cho test. Không cần cơ sở dữ liệu hay Docker. Plugin exec-maven-plugin chạy lớp chính bằng mvn -q compile exec:exec.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.canhdominich.javabackend</groupId>
<artifactId>spring-ioc-va-di</artifactId>
<version>1.0.0</version>
<name>Spring Core - IoC va Dependency Injection</name>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.class>dev.canhdominich.spring.ioc.IocDemo</main.class>
<spring.version>7.0.9</spring.version>
<junit.version>6.0.3</junit.version>
<assertj.version>3.27.7</assertj.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>${main.class}</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
Khi lớp tự tạo phụ thuộc bằng new
Mọi thứ bắt đầu từ một interface PaymentService chỉ có một phương thức charge:
src/main/java/dev/canhdominich/spring/ioc/PaymentService.java
package dev.canhdominich.spring.ioc;
public interface PaymentService {
String charge(int amountVnd);
}
Lớp Events ghi và in các sự kiện (tạo bean, gọi thanh toán) để các thí nghiệm có thứ tự đo được thay vì phỏng đoán; nó là công cụ của bài, không phải một phần của mẫu thiết kế:
src/main/java/dev/canhdominich/spring/ioc/Events.java
package dev.canhdominich.spring.ioc;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Events {
private static final List<String> LOG = Collections.synchronizedList(new ArrayList<>());
private Events() {
}
public static void record(String event) {
LOG.add(event);
System.out.println(" event: " + event);
}
public static void clear() {
LOG.clear();
}
public static List<String> all() {
synchronized (LOG) {
return new ArrayList<>(LOG);
}
}
}
PaypalPayment là cài đặt "thật" của cổng thanh toán. Mỗi lần được tạo và mỗi lần charge đều ghi một sự kiện. Annotation @Component đánh dấu nó là ứng viên để container quản lý; chưa cần quan tâm tới nó ở thí nghiệm đầu:
src/main/java/dev/canhdominich/spring/ioc/PaypalPayment.java
package dev.canhdominich.spring.ioc;
import org.springframework.stereotype.Component;
@Component
public class PaypalPayment implements PaymentService {
public PaypalPayment() {
Events.record("constructing PaypalPayment");
}
@Override
public String charge(int amountVnd) {
Events.record("PAYPAL charged " + amountVnd);
return "paypal:" + amountVnd;
}
}
HardWiredOrderService là kiểu viết ở đầu bài: dịch vụ tự gọi new PaypalPayment() trong trường của nó:
src/main/java/dev/canhdominich/spring/ioc/HardWiredOrderService.java
package dev.canhdominich.spring.ioc;
public class HardWiredOrderService {
private final PaymentService payment = new PaypalPayment();
public String place(int amountVnd) {
return payment.charge(amountVnd);
}
}
IocDemo chạy ba thí nghiệm liên tiếp; thí nghiệm đầu dùng HardWiredOrderService như trên:
src/main/java/dev/canhdominich/spring/ioc/IocDemo.java
package dev.canhdominich.spring.ioc;
import java.util.Arrays;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class IocDemo {
public static void main(String[] args) {
run();
}
public static void run() {
withoutContainer();
withContainer();
withFake();
}
static String withoutContainer() {
System.out.println("-- without a container: OrderService creates its own payment");
Events.clear();
String result = new HardWiredOrderService().place(100);
System.out.println("result: " + result);
return result;
}
static List<String> withContainer() {
System.out.println("-- with a container: the container creates and wires the beans");
Events.clear();
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {
String[] userBeans = Arrays.stream(context.getBeanDefinitionNames())
.filter(name -> !name.startsWith("org.springframework"))
.sorted()
.toArray(String[]::new);
System.out.println("bean names: " + Arrays.toString(userBeans));
System.out.println("orderService depends on: " + Arrays.toString(context.getBeanFactory().getDependenciesForBean("orderService")));
System.out.println("paypalPayment is needed by: " + Arrays.toString(context.getBeanFactory().getDependentBeans("paypalPayment")));
OrderService first = context.getBean(OrderService.class);
OrderService second = context.getBean(OrderService.class);
System.out.println("same instance twice: " + (first == second));
System.out.println("result: " + first.place(100));
}
return Events.all();
}
static List<Integer> withFake() {
System.out.println("-- no container at all: a fake payment goes through the constructor");
FakePayment fake = new FakePayment();
String result = new OrderService(fake).place(100);
System.out.println("result: " + result + ", charges recorded by the fake: " + fake.charges());
return fake.charges();
}
}
Chạy bằng mvn -q compile exec:exec (lớp chính mặc định là IocDemo). Với thí nghiệm đầu (các dòng đầu của kết quả):
-- without a container: OrderService creates its own payment
event: constructing PaypalPayment
event: PAYPAL charged 100
result: paypal:100
Ngay cả khi bạn chỉ muốn kiểm thử logic của dịch vụ, nó vẫn tạo cổng thật và gọi cổng thật (hai sự kiện). Không có chỗ nào để người gọi (một bài kiểm thử, một ứng dụng khác) nói "lần này hãy dùng thứ khác".
Cách đọc hình: hàng trên: HardWiredOrderService tự gọi new, nên mọi lần chạy đều chạm cổng thật và đổi cổng nghĩa là sửa lớp. Hàng dưới: OrderService chỉ khai báo cần một PaymentService qua constructor; ai lắp ráp (container hay bài kiểm thử) quyết định bản cài đặt.
Khi container tạo và truyền phụ thuộc vào
OrderService làm cùng việc nhưng khai báo phụ thuộc ở constructor và chỉ biết interface PaymentService. Annotation @Service đánh dấu nó là một bean ở tầng nghiệp vụ. Với một constructor duy nhất, Spring tự dùng nó để tiêm phụ thuộc mà không cần @Autowired:
src/main/java/dev/canhdominich/spring/ioc/OrderService.java
package dev.canhdominich.spring.ioc;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final PaymentService payment;
public OrderService(PaymentService payment) {
this.payment = payment;
Events.record("constructing OrderService with " + payment.getClass().getSimpleName());
}
public String place(int amountVnd) {
return payment.charge(amountVnd);
}
}
AppConfig là lớp cấu hình tối thiểu: @Configuration báo đây là nguồn định nghĩa bean, và @ComponentScan bảo container quét package của lớp này, tìm các lớp có annotation như @Component và @Service:
src/main/java/dev/canhdominich/spring/ioc/AppConfig.java
package dev.canhdominich.spring.ioc;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackageClasses = AppConfig.class)
public class AppConfig {
}
Phương thức withContainer của IocDemo dựng AnnotationConfigApplicationContext từ AppConfig, in tên các bean, hỏi đồ thị phụ thuộc, kiểm tra getBean hai lần có trả cùng một đối tượng không, rồi gọi dịch vụ. Kết quả của thí nghiệm thứ hai:
-- with a container: the container creates and wires the beans
event: constructing PaypalPayment
event: constructing OrderService with PaypalPayment
bean names: [appConfig, orderService, paypalPayment]
orderService depends on: [paypalPayment]
paypalPayment is needed by: [orderService]
same instance twice: true
event: PAYPAL charged 100
result: paypal:100
Đọc kết quả theo từng điểm:
- Thứ tự tạo.
PaypalPaymentđược tạo trướcOrderService, dù bạn không viết dòng nào nói thế: container đọc constructor củaOrderService, thấy nó cần mộtPaymentService, nên tạo bean đó trước rồi mới tạoOrderServicevà truyền vào (dòngconstructing OrderService with PaypalPayment). - Ba bean của bạn.
appConfig(chính lớp cấu hình cũng là một bean),orderServicevàpaypalPayment. Danh sách đã lọc bỏ các bean nội bộ của Spring (tên bắt đầu bằngorg.springframework) mà container tự đăng ký để xử lý annotation. - Đồ thị phụ thuộc có thể hỏi được.
getDependenciesForBean("orderService")trả[paypalPayment], và chiều ngược lạigetDependentBeans("paypalPayment")trả[orderService]. Đây là bản đồ mà container dùng để biết thứ tự tạo và thứ tự hủy. - Mặc định là một đối tượng duy nhất.
getBean(OrderService.class)hai lần trả cùng một đối tượng (same instance twice: true); phần phạm vi (scope) sẽ nói kỹ ở bài vòng đời.
Cách đọc hình: từ trái sang phải rồi xuống hàng dưới: quét package tìm lớp, đọc constructor của OrderService, tạo PaypalPayment trước, truyền nó vào OrderService, giữ một bản của mỗi bean, ghi lại đồ thị phụ thuộc. Mỗi bước có sự kiện hoặc kết quả đo được tương ứng trong kết quả chạy.
Đang tải Animation…
Kiểm thử không cần container
Lợi ích đầu tiên của constructor injection nằm ở kiểm thử. FakePayment là một bản cài đặt PaymentService chỉ ghi lại các lần gọi vào một danh sách, không chạm hệ thống ngoài. Nó không có annotation nên không phải bean:
src/main/java/dev/canhdominich/spring/ioc/FakePayment.java
package dev.canhdominich.spring.ioc;
import java.util.ArrayList;
import java.util.List;
public class FakePayment implements PaymentService {
private final List<Integer> charges = new ArrayList<>();
@Override
public String charge(int amountVnd) {
charges.add(amountVnd);
return "fake:" + amountVnd;
}
public List<Integer> charges() {
return charges;
}
}
Thí nghiệm thứ ba của IocDemo (phương thức withFake) không dựng container: nó gọi new OrderService(fake) thẳng, rồi kiểm tra điều bản giả ghi lại:
-- no container at all: a fake payment goes through the constructor
event: constructing OrderService with FakePayment
result: fake:100, charges recorded by the fake: [100]
Dịch vụ chạy đúng logic với bản giả, ghi lại lần tính tiền 100, và không có dòng PAYPAL charged nào: không cổng thật, không container, không cấu hình. Một bài kiểm thử như vậy chạy trong vài mili giây. Đây là lý do thường nói constructor injection làm code "dễ kiểm thử": không cần phép màu nào của framework, chỉ cần hàm tạo nhận phụ thuộc là một tham số.
Spring gồm những module nào
Spring Framework chia thành nhiều artifact, và bạn chỉ thêm những gì cần. ModulesDemo đọc classpath của chính dự án và in các jar có tên bắt đầu bằng spring-:
src/main/java/dev/canhdominich/spring/ioc/ModulesDemo.java
package dev.canhdominich.spring.ioc;
import java.io.File;
import java.util.Arrays;
import java.util.List;
public class ModulesDemo {
public static void main(String[] args) {
run();
}
public static List<String> run() {
List<String> jars = Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator))
.map(path -> new File(path).getName())
.filter(name -> name.startsWith("spring-"))
.sorted()
.toList();
System.out.println("spring jars on the classpath:");
jars.forEach(jar -> System.out.println(" " + jar));
return jars;
}
}
Dự án chỉ khai báo duy nhất spring-context (và jakarta.annotation-api cho các annotation vòng đời ở bài sau), vậy mà kết quả:
spring jars on the classpath:
spring-aop-7.0.9.jar
spring-beans-7.0.9.jar
spring-context-7.0.9.jar
spring-core-7.0.9.jar
spring-expression-7.0.9.jar
| Jar | Vai trò |
|---|---|
spring-core | Tiện ích nền tảng mà mọi module khác dùng |
spring-beans | Định nghĩa bean (công thức tạo đối tượng) và nhà máy tạo bean |
spring-context | ApplicationContext, quét component, các annotation như @Component, @Configuration |
spring-aop | Nền tảng proxy; được kéo theo vì container dùng nó cho các tính năng nâng cao |
spring-expression | Ngôn ngữ biểu thức SpEL, bài vòng đời sẽ dùng |
Web (MVC), truy cập dữ liệu và bảo mật là những artifact riêng không có trong danh sách trên, bạn thêm chúng khi chương tương ứng cần. Bốn jar spring-core, spring-beans, spring-context và spring-expression thường được gọi chung là Core Container. Danh sách này đo trên bản 7.0.9; các bản khác có thể có tập jar khác, nên hãy đo lại bằng chính ModulesDemo khi nâng cấp.
Cách đọc hình: spring-context (hàng trên, bên phải) phụ thuộc spring-beans và spring-core; hai ô nét đứt là các jar kéo theo thêm (spring-aop, spring-expression). Ô cuối nhắc rằng MVC, Data, Security không nằm trên classpath cho tới khi bạn thêm chúng.
Quyết định thiết kế và đánh đổi
| Quyết định | Lựa chọn | Đánh đổi |
|---|---|---|
| Nơi tạo phụ thuộc | Bên ngoài lớp (constructor nhận interface) | Cần một chỗ lắp ráp (container hoặc test) |
| Kiểu phụ thuộc | Interface, không phải lớp cụ thể | Thêm một interface cho mỗi phụ thuộc cần thay thế |
| Kiểm thử đơn vị | Truyền đối tượng giả qua constructor | Phải viết hoặc dựng đối tượng giả |
| Số lượng module | Chỉ thêm module cần dùng | Phải biết module nào cho tính năng nào |
| Dùng container | Cho ứng dụng thật, còn kiểm thử đơn vị thì không cần | Hai cách dựng cùng một đối tượng cần được giữ nhất quán |
Playbook: gặp sự cố ở lớp nền IoC thì làm gì
| Triệu chứng | Kiểm tra đầu tiên | Nguyên nhân hay gặp | Cách xử lý |
|---|---|---|---|
| Bài kiểm thử đơn vị gọi dịch vụ thật | Có new cho phụ thuộc trong lớp không | Phụ thuộc bị gắn cứng | Chuyển thành tham số constructor kiểu interface |
| Đổi cài đặt phải sửa nhiều lớp | Tìm các chỗ new của lớp cài đặt | Quyết định nằm rải rác | Tập trung việc lắp ráp ở một chỗ (cấu hình) |
Khởi động báo expected single matching bean but found 2 | Có hai bean cùng kiểu | Thêm một @Component cùng interface | Bài DI sau dạy @Qualifier và @Primary |
| Bean không được tạo | Lớp có annotation stereotype và nằm trong package được quét không | Sai package hoặc thiếu @ComponentScan | Kiểm tra basePackageClasses hoặc package của lớp cấu hình |
| Không biết bean nào cần bean nào | Đồ thị phụ thuộc | Nhiều bean, nhiều tầng | getDependenciesForBean, getDependentBeans |
Lỗi thường gặp khi chạy lab
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
mvn báo không tìm thấy jakarta.annotation | Thiếu thư viện khai báo trong pom.xml | Chạy lại với mạng, hoặc xóa ~/.m2/repository/jakarta/annotation rồi chạy lại |
Không thấy dòng event: nào | Chạy sai lớp chính | Dùng đúng lệnh mvn -q compile exec:exec |
Bài thực hành
Làm trong dự án của bài (thư mục 06-spring-core/01-ioc-va-di). Sau mỗi bước, so kết quả với tiêu chí nghiệm thu.
- Chạy thí nghiệm.
mvn -q compile exec:exec. Tiêu chí: bạn thấyconstructing PaypalPaymenttrướcconstructing OrderService with PaypalPayment, vàsame instance twice: true. - Thêm một cổng thứ hai. Tạo lớp
StripePayment implements PaymentServicecó@Component(trả"stripe:" + amountVnd) trong cùng package và chạy lại. Tiêu chí: container từ chối khởi động với lỗiNoUniqueBeanDefinitionException: ... expected single matching bean but found 2: paypalPayment,stripePayment; ghi lại lỗi xảy ra ở bước nào (khi khởi động, hay khi gọiplace) và giải thích vì sao. - Viết một bài kiểm thử không có container. Thêm một test gọi
new OrderService(new FakePayment())rồiplace(250)và kiểm tra danh sáchcharges()bằng[250]. Tiêu chí: test xanh và không có sự kiệnPAYPAL chargednào. - Vẽ đồ thị. Thêm một lớp
ReceiptService(@Service) nhậnOrderServicequa constructor, rồi ingetDependenciesForBean("receiptService")vàgetDependentBeans("orderService"). Tiêu chí: hai danh sách khớp với các constructor bạn viết. - Chạy bộ test (tùy chọn). Bộ test nằm trong bản sao lưu ở kho mã của khóa học; sau khi clone,
mvn testchoTests run: 4, Failures: 0, Errors: 0.
Hiểu lầm và giới hạn
- "Dùng Spring nghĩa là dùng DI." DI là kỹ thuật: bạn có thể làm nó bằng tay (như
withFake); container chỉ tự động hóa việc lắp ráp. - "IoC cần container." Constructor injection cho phép kiểm thử mà không có container nào.
- "
@Autowiredbắt buộc trên constructor." Với một constructor duy nhất Spring tự dùng nó, nhưOrderServiceở trên không có@Autowired. - Số đo thuộc về Spring Framework 7.0.9 trên JDK 17. Bài này chưa nói về các kiểu cấu hình khác (bài sau), các kiểu tiêm khác ngoài constructor (bài DI), vòng đời và phạm vi (bài sau nữa), và mọi tính năng web hay dữ liệu.
Tự kiểm tra
- Vì sao một bài kiểm thử của
HardWiredOrderServicevẫn gọi cổng thanh toán thật? - Trong kết quả, vì sao
constructing PaypalPaymentđứng trướcconstructing OrderService? - Bạn kiểm thử
OrderServicekhông cần container bằng cách nào? getDependenciesForBeanvàgetDependentBeanscho biết điều gì, theo chiều nào?- Thêm một lớp
@Componentthứ hai cùng implementPaymentServicethì chuyện gì xảy ra, và vào lúc nào?
Gợi ý đáp án. (1) Lớp tự gọi new PaypalPayment() trong trường final, người gọi không thay được. (2) Constructor của OrderService cần một PaymentService, nên container tạo phụ thuộc đó trước. (3) Truyền một đối tượng giả vào constructor: new OrderService(new FakePayment()). (4) Cái đầu: các bean mà một bean cần; cái sau: các bean cần bean đó. (5) Container báo NoUniqueBeanDefinitionException ngay khi khởi động vì có hai ứng viên và không có chỉ dẫn chọn.
Điều cần nhớ
- IoC chuyển quyết định "dùng bản cài đặt nào" ra khỏi lớp; DI là cách làm: nhận phụ thuộc qua constructor.
- Container đọc constructor, tạo phụ thuộc trước, rồi truyền vào; đồ thị phụ thuộc hỏi được bằng mã.
- Constructor injection cho phép kiểm thử bằng đối tượng giả, không cần container.
spring-contextkéo theo năm jar trên bản 7.0.9; module web, dữ liệu và bảo mật là artifact riêng.
Đọc thêm
- Spring Framework Reference: Core, the IoC Container
- Spring Framework Reference: Dependency Injection
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
Bài tiếp theo đi sâu vào chính container: BeanFactory so với ApplicationContext, ba cách khai báo bean, và vì sao một số lỗi cấu hình chỉ lộ ra lúc khởi động.