Skip to content

Spring IoC (Inversion of Control)

12 min read
On this page

Spring IoC là cơ chế trong đó Spring Container đảm nhận việc tạo, cấu hình, kết nối và quản lý vòng đời các object (bean), thay vì để ứng dụng tự khởi tạo và quản lý dependency một cách thủ công.

1. Tóm tắt nhanh#

  • IoC (Inversion of Control): đảo ngược quyền kiểm soát việc tạo và kết nối object. Thay vì code ứng dụng tự new dependency, container quản lý việc đó.
  • DI (Dependency Injection): kỹ thuật phổ biến để hiện thực IoC; dependency được cung cấp từ bên ngoài vào object.
  • Spring IoC Container: thành phần đọc cấu hình, tạo bean, resolve dependency và quản lý lifecycle.
  • Bean: object được Spring IoC Container khởi tạo, cấu hình và quản lý.
  • BeanDefinition: metadata mô tả cách tạo và cấu hình một bean.
  • ApplicationContext: container cấp cao, cung cấp DI cùng các tính năng như event, resource access, internationalization và tích hợp enterprise.
  • Constructor injection: thường là lựa chọn ưu tiên cho dependency bắt buộc vì thể hiện rõ yêu cầu và hỗ trợ tạo object bất biến.

Sơ đồ khái quát#

flowchart LR
    A[Application configuration<br/>Annotations / Java config / XML] --> B[Spring IoC Container]
    B --> C[Read BeanDefinitions]
    C --> D[Create beans]
    D --> E[Resolve and inject dependencies]
    E --> F[Initialize and manage lifecycle]
    F --> G[Application uses managed beans]

Ý chính: ứng dụng khai báo cần những thành phần nào; Spring chịu trách nhiệm tạo chúng và kết nối chúng như thế nào.


2. Vấn đề trước khi có IoC#

Giả sử OrderService cần OrderRepository để lưu đơn hàng. Nếu tự tạo dependency bên trong service:

public class OrderService {
    private final OrderRepository repository = new OrderRepository();

    public void createOrder() {
        repository.save();
    }
}

Cách này có một số hạn chế:

  • OrderService tự quyết định implementation cụ thể cần dùng.
  • Khó thay thế repository bằng implementation khác, chẳng hạn mock khi test.
  • Nếu constructor của repository thay đổi hoặc repository có dependency riêng, việc khởi tạo có thể lan rộng khắp codebase.
  • Vòng đời và cấu hình object không có một nơi quản lý tập trung.

Có thể tách việc khởi tạo ra ngoài và truyền dependency vào:

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void createOrder() {
        repository.save();
    }
}

Giờ OrderService không còn tự tạo repository. Nó chỉ khai báo dependency qua constructor. Một thành phần bên ngoài sẽ cung cấp repository đó. Đây là Dependency Injection.


3. IoC và DI khác nhau thế nào?#

Hai khái niệm liên quan nhưng không đồng nghĩa hoàn toàn.


Khái niệm Ý nghĩa


IoC Nguyên lý đảo quyền kiểm soát khỏi code ứng dụng sang một cơ chế/framework quản lý

DI Một cách triển khai IoC, trong đó dependency được truyền vào object

Spring IoC Container Hạ tầng của Spring thực hiện việc tạo, cấu hình, kết nối và quản lý bean#

Ví dụ: Spring quản lý OrderService và cung cấp OrderRepository vào constructor của nó. Đây là DI được dùng để thực hiện IoC.

Không phải mọi IoC đều bắt buộc dùng DI; framework có thể đảo quyền kiểm soát qua các cơ chế khác như callback hoặc event. Tuy nhiên, DI là mô hình trung tâm trong Spring.


4. Spring IoC Container là gì?#

Spring IoC Container là thành phần chịu trách nhiệm quản lý bean. Hai interface container thường được nhắc đến:

  • BeanFactory: nền tảng container cơ bản, cung cấp cơ chế quản lý và lấy bean.
  • ApplicationContext: mở rộng các khả năng của BeanFactory, bổ sung nhiều dịch vụ ứng dụng và là lựa chọn phổ biến trong ứng dụng Spring.

Trong Spring Boot, ApplicationContext thường được khởi tạo tự động khi ứng dụng chạy.

Sơ đồ vai trò#

flowchart TD
    A[Spring Application] --> B[ApplicationContext]
    B --> C[BeanFactory capabilities]
    B --> D[Application events]
    B --> E[Resource loading]
    B --> F[MessageSource / i18n]
    B --> G[Environment and profiles]
    C --> H[Bean creation, lookup, dependency wiring]

Container sử dụng cấu hình ứng dụng để biết bean nào cần tồn tại và dependency của chúng là gì.


5. Bean và BeanDefinition#

5.1. Bean là gì?#

Bean là một object được Spring Container tạo hoặc tiếp nhận, sau đó quản lý theo cấu hình. Ví dụ:

import org.springframework.stereotype.Service;

@Service
public class PaymentService {
    public void pay() {
        System.out.println("Payment processed");
    }
}

Khi component scanning được bật và package được quét, Spring phát hiện PaymentService và đăng ký nó làm bean.

5.2. BeanDefinition là gì?#

BeanDefinition là metadata mô tả bean, không phải chính object bean. Nó có thể chứa:

  • Class hoặc factory method dùng để tạo bean.
  • Scope.
  • Constructor arguments và property values.
  • Các thông tin init/destroy.
  • Những cấu hình khác liên quan đến bean.

Có thể hình dung: BeanDefinition là bản hướng dẫn; bean là object được tạo theo bản hướng dẫn đó.


6. Dependency Injection trong Spring#

Spring hỗ trợ nhiều cách inject dependency.

6.1. Constructor injection#

@Repository
public class OrderRepository {
    public void save() {
        System.out.println("Order saved");
    }
}

@Service
public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    public void createOrder() {
        repository.save();
    }
}

Nếu class chỉ có một constructor, Spring thường có thể sử dụng constructor đó mà không cần @Autowired.

Ưu điểm:

  • Dependency bắt buộc được thể hiện rõ.
  • Có thể dùng final để giữ tham chiếu không đổi.
  • Dễ unit test vì có thể truyền mock/fake trực tiếp.
  • Tránh object được tạo ở trạng thái thiếu dependency bắt buộc.

6.2. Setter injection#

@Service
public class ReportService {
    private ReportExporter exporter;

    @Autowired
    public void setExporter(ReportExporter exporter) {
        this.exporter = exporter;
    }
}

Setter injection phù hợp hơn với dependency tùy chọn hoặc có thể thay đổi sau khi object được tạo. Cần cân nhắc để tránh bean có trạng thái chưa hoàn chỉnh trước khi setter được gọi.

6.3. Field injection#

@Service
public class NotificationService {
    @Autowired
    private MailSender mailSender;
}

Field injection ngắn gọn nhưng thường ít được khuyến nghị cho dependency ứng dụng vì khó khởi tạo độc lập trong unit test, khó thể hiện dependency bắt buộc và không thuận tiện cho object bất biến.

So sánh nhanh#


Cách inject Khi phù hợp Điểm cần lưu ý


Constructor Dependency bắt buộc Thường là lựa chọn mặc định

Setter Dependency tùy chọn Có thể tạo trạng thái hoặc có thể cấu hình chưa hoàn chỉnh lại

Field Code ngắn, một số Khó test độc lập và khó trường hợp đặc thù thấy dependency#


7. Khai báo bean để Spring quản lý#

Có nhiều cách đăng ký bean; thường gặp nhất là component scanning và Java configuration.

7.1. Component scanning với stereotype annotations#

@Component
public class FileStorage {
}

@Service
public class DocumentService {
    private final FileStorage storage;

    public DocumentService(FileStorage storage) {
        this.storage = storage;
    }
}

Các annotation phổ biến:


Annotation Ý nghĩa thường dùng


@Component Component tổng quát

@Service Component ở tầng service

@Repository Component tầng truy cập dữ liệu; còn hỗ trợ chuyển đổi một số exception persistence

@Controller Spring MVC controller

@RestController Controller REST, kết hợp semantics của @Controller và @ResponseBody#

Các stereotype annotation giúp phân loại vai trò component và cho phép component scanning phát hiện bean.

7.2. Java configuration với @Configuration và @Bean#

@Configuration
public class AppConfig {

    @Bean
    public Clock systemClock() {
        return Clock.systemUTC();
    }

    @Bean
    public AuditService auditService(Clock systemClock) {
        return new AuditService(systemClock);
    }
}

@Bean đánh dấu phương thức tạo bean. Spring gọi factory method và quản lý object được trả về theo cấu hình. Các tham số phương thức @Bean có thể được Spring resolve thành dependency.

Cách này hữu ích khi:

  • Cấu hình bean từ thư viện bên thứ ba mà không thể thêm annotation vào class.
  • Muốn kiểm soát rõ cách tạo object.
  • Muốn gom cấu hình theo module hoặc môi trường.

7.3. XML configuration#

Spring cũng hỗ trợ khai báo bean bằng XML, dù Java configuration và annotation phổ biến hơn trong các ứng dụng mới.

Ví dụ minh họa:

<bean id="orderRepository" class="com.example.OrderRepository" />
<bean id="orderService" class="com.example.OrderService">
    <constructor-arg ref="orderRepository" />
</bean>

Các kiểu cấu hình có thể được kết hợp tùy nhu cầu dự án.


8. Spring resolve dependency như thế nào?#

Khi tạo bean, container cần tìm bean phù hợp cho từng dependency. Việc chọn phụ thuộc vào kiểu, qualifier, tên và cấu hình liên quan.

Ví dụ có hai implementation:

public interface PaymentGateway {
    void charge();
}

@Component
public class CardPaymentGateway implements PaymentGateway {
    @Override
    public void charge() {
        System.out.println("Card payment");
    }
}

@Component
public class WalletPaymentGateway implements PaymentGateway {
    @Override
    public void charge() {
        System.out.println("Wallet payment");
    }
}

Nếu một bean yêu cầu PaymentGateway mà có nhiều candidate cùng phù hợp, Spring có thể báo lỗi do không xác định được bean cần inject.

Có thể chỉ định rõ bằng @Qualifier:

@Service
public class CheckoutService {
    private final PaymentGateway gateway;

    public CheckoutService(
            @Qualifier("cardPaymentGateway") PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

Hoặc dùng @Primary trên implementation được ưu tiên khi không có qualifier cụ thể:

@Primary
@Component
public class CardPaymentGateway implements PaymentGateway {
    // ...
}

Nên dùng qualifier có chủ đích, đặc biệt khi có nhiều implementation theo provider, môi trường hoặc chiến lược nghiệp vụ.


9. Bean scope: một bean có bao nhiêu instance?#

Scope xác định phạm vi và cách Spring tạo/lưu instance của bean.


Scope Ý nghĩa


singleton Một instance cho mỗi bean definition trong một container; đây là scope mặc định

prototype Tạo instance mới mỗi lần container được yêu cầu cấp bean

request Một instance trong phạm vi HTTP request

session Một instance trong phạm vi HTTP session

application Một instance trong phạm vi ServletContext

websocket Một instance trong phạm vi WebSocket session#

Ví dụ:

@Component
@Scope("prototype")
public class ReportBuilder {
}

Lưu ý quan trọng: singleton của Spring không đồng nghĩa với singleton toàn JVM. Hai ApplicationContext độc lập có thể có hai instance singleton riêng. Ngoài ra, Spring không tự động quản lý đầy đủ callback hủy cho bean prototype sau khi đã giao instance cho client.


10. IoC liên quan gì đến Bean Lifecycle?#

IoC Container không chỉ tạo object mà còn quản lý lifecycle của bean. Với một bean thông thường, chuỗi khái quát gồm:

flowchart TD
    A[Đọc cấu hình / BeanDefinition] --> B[Tạo instance]
    B --> C[Inject dependency]
    C --> D[Aware callbacks]
    D --> E[BeanPostProcessor: before initialization]
    E --> F[Initialization callbacks]
    F --> G[BeanPostProcessor: after initialization]
    G --> H[Bean sẵn sàng sử dụng]
    H --> I[ApplicationContext đóng]
    I --> J[Destruction callbacks]

Các callback khởi tạo thường gặp:

  • @PostConstruct
  • InitializingBean.afterPropertiesSet()
  • Custom init method

Các callback hủy thường gặp:

  • @PreDestroy
  • DisposableBean.destroy()
  • Custom destroy method

Ví dụ:

@Component
public class WorkerService {

    @PostConstruct
    public void initialize() {
        System.out.println("Worker initialized");
    }

    @PreDestroy
    public void cleanup() {
        System.out.println("Worker cleanup");
    }
}

Nên xem lifecycle như một phần trách nhiệm của container: ứng dụng khai báo bean và các callback; Spring điều phối những giai đoạn tương ứng. Callback hủy không được bảo đảm khi tiến trình bị dừng đột ngột.


11. IoC giúp gì cho kiến trúc và kiểm thử?#

Giảm coupling#

Service phụ thuộc vào abstraction thay vì tự khởi tạo implementation cụ thể:

public interface UserRepository {
    Optional<User> findById(Long id);
}

@Service
public class UserService {
    private final UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public Optional<User> findUser(Long id) {
        return repository.findById(id);
    }
}

UserService không cần biết repository được triển khai bằng JPA, JDBC hay một adapter khác, miễn là implementation đáp ứng contract.

Dễ unit test#

class UserServiceTest {
    @Test
    void returnsUserFromRepository() {
        UserRepository fakeRepository = id ->
                Optional.of(new User(id, "Linh"));

        UserService service = new UserService(fakeRepository);

        assertTrue(service.findUser(1L).isPresent());
    }
}

Test có thể tự tạo dependency giả mà không cần khởi động toàn bộ Spring context. Với integration test, có thể dùng Spring Test để kiểm tra cấu hình và wiring thực tế.

Quản lý cấu hình tập trung#

Container cung cấp một nơi để khai báo bean, dependency và scope. Kết hợp với profiles và configuration properties, ứng dụng có thể thay đổi cấu hình theo môi trường mà không phải rải logic khởi tạo khắp codebase.


12. Một số nhầm lẫn thường gặp#

  1. IoC và DI là một: DI là một cách thực hiện IoC, nhưng IoC là nguyên lý rộng hơn.
  2. Spring chỉ là nơi chứa object: container còn resolve dependency, áp dụng cấu hình, quản lý scope/lifecycle và phối hợp các extension point.
  3. @Autowired là IoC: @Autowired là một annotation hỗ trợ wiring; IoC là nguyên lý và container là hạ tầng thực thi.
  4. Mọi object trong ứng dụng đều là bean: chỉ những object được đăng ký hoặc quản lý theo cơ chế tương ứng mới là Spring bean. Object tạo bằng new trong code thông thường không tự động trở thành bean.
  5. Singleton là thread-safe: singleton chỉ mô tả số instance theo container; nó không tự bảo đảm an toàn đa luồng. Trạng thái mutable dùng chung cần được thiết kế cẩn thận.
  6. @Component và @Bean giống hệt nhau: cả hai có thể đăng ký bean, nhưng @Component đánh dấu class để scanning phát hiện, còn @Bean đánh dấu factory method trong cấu hình.
  7. Inject bằng interface là bắt buộc: không bắt buộc trong mọi trường hợp, nhưng phụ thuộc abstraction thường giúp giảm coupling và tăng khả năng thay thế implementation.

13. Câu hỏi phỏng vấn thường gặp#

Q1. Spring IoC là gì?#

Là cơ chế container quản lý việc tạo, cấu hình, kết nối và lifecycle của các object được đăng ký làm bean, thay vì để từng class tự tạo và quản lý dependency.

Q2. IoC khác DI như thế nào?#

IoC là nguyên lý đảo quyền kiểm soát; DI là một kỹ thuật thực hiện nguyên lý đó bằng cách cung cấp dependency từ bên ngoài vào object.

Q3. BeanFactory và ApplicationContext khác nhau thế nào?#

BeanFactory cung cấp các khả năng container cơ bản. ApplicationContext mở rộng nó với các dịch vụ ứng dụng như event publication, resource loading, message resolution và tích hợp môi trường.

Q4. Tại sao constructor injection thường được ưu tiên?#

Nó thể hiện dependency bắt buộc ngay tại constructor, hỗ trợ final, giúp object tránh trạng thái thiếu dependency và làm unit test đơn giản hơn.

Q5. @Component và @Bean khác nhau thế nào?#

@Component đánh dấu class để component scanning phát hiện. @Bean đánh dấu phương thức factory trong cấu hình; Spring quản lý object mà phương thức trả về.

Q6. Nếu có hai bean cùng implement một interface thì sao?#

Nếu không có thông tin phân biệt phù hợp, Spring có thể báo lỗi do có nhiều candidate. Có thể dùng @Qualifier, @Primary hoặc điều chỉnh cấu hình để chọn đúng bean.

Q7. Spring singleton có phải singleton toàn hệ thống không?#

Không. Nó là một instance cho mỗi bean definition trong một Spring container. Nhiều container có thể quản lý các instance riêng.

Q8. Object tạo bằng new có phải Spring bean không?#

Không tự động. Object đó chỉ trở thành bean nếu được đăng ký hoặc đưa vào container theo một cơ chế phù hợp.


14. Kết luận#

Có thể ghi nhớ Spring IoC bằng ba ý:

  1. Container quản lý object: Spring tạo và cấu hình bean theo metadata.
  2. DI kết nối dependency: bean nhận những thành phần nó cần từ container thay vì tự khởi tạo chúng.
  3. Lifecycle được điều phối: Spring quản lý các giai đoạn khởi tạo, sử dụng theo scope và hủy bean trong phạm vi container.

Trong ứng dụng Spring, hãy ưu tiên constructor injection cho dependency bắt buộc, dùng abstraction khi cần thay thế implementation, và chọn @Component hoặc @Bean dựa trên cách bạn muốn khai báo và kiểm soát việc tạo object.