Inversion of Control và Dependency Injection trong Spring – Học Spring Boot

1. Tổng quan

Trong bài viết này, tôi sẽ giới thiệu các khái niệm về IoC (Inversion of Control) và DI (Dependency Injection), cũng như xem qua cách chúng được triển khai trong Spring Framework.

2. Inversion of Control là gì?

IoC còn có tên gọi đầy đủ là Inversion of Control, được hiểu là một nguyên lý thiết kế ứng dụng trong công nghệ phần mềm. Kiến trúc phần mềm khi áp dụng nguyên lý thiết kế này sẽ đảo ngược quyền điều khiển so với kiểu lập trình hướng thủ tục. Nếu như trong lập trình hướng thủ tục, các đoạn mã được thêm vào sẽ gọi các thư viện thì ở IoC lại hoàn toàn khác. Những IoC container sẽ chích những dependencies khi khởi tạo bean.

3. Dependency Injection là gì?

Đây là một pattern dùng để implement IoC, các dependencies sẽ được inject vào module trong quá trình khởi tạo. 

Việc kết nối giữa các đối tượng với các đối tượng khác, hoặc inject các đối tượng vào đối tượng khác được thực hiện bằng quá trình lắp ráp chứ không phải bởi chính các đối tượng.

Tổng hợp 200+ tài liệu, sách, bài thực hành, video hướng dẫn lập trình… từ cơ bản đến nâng cao

Đây là cách tôi tạo ra một đối tượng dependency trong lập trình truyền thống:

public

class

Store

{

private

Item item;

public

Store() { item =

new

ItemImpl1(); } }

Code language:

PHP

(

php

)

Còn đây là sau khi sử dụng DI:

public

class

Store

{

private

Item item;

public

Store(Item item) { this.item = item; } }

Code language:

PHP

(

php

)

4. Spring IoC Container

IoC Container là đặc điểm chung của các framework implement IoC.

Trong Spring Framework, interface ApplicationContext đại diện cho IoC Container. Spring Container chịu trách nhiệm khởi tạo, config và lắp ráp các đối tượng gọi là bean, cũng như quản lý vòng đời của chúng.

Spring framework cung cấp một số implement của ApplicationContext: ClassPathXmlApplicationContext và FileSystemXmlApplicationContext cho các ứng dụng độc lập, và WebApplicationContext cho ứng dụng web.

Để tập hợp các bean, container sử dụng config siêu dữ liệu, có thể ở dạng cấu hình XML hoặc chú thích

Đây là một cách để khởi tạo vùng chứa theo cách thủ công:

ApplicationContext context =

new

ClassPathXmlApplicationContext(

"applicationContext.xml"

);

Code language:

JavaScript

(

javascript

)

Để đặt thuộc tính item trong ví dụ trên, chúng ta có thể sử dụng siêu dữ liệu. Sau đó, vùng chứa sẽ đọc siêu dữ liệu này và sử dụng nó để tập hợp các bean trong thời gian chạy.

Dependency Injection trong Spring có thể được thực hiện thông qua các constructor, setters hoặc các trường.

5. Constructor-Based Dependency Injection

Trong trường hợp Constructor-Based Dependency Injection, container sẽ gọi một method khởi tạo với các đối số đại diện cho một dependency mà chúng ta muốn đặt.

TÀI LIỆU HỌC LẬP TRÌNH

Spring giải quyết từng đối số chủ yếu theo kiểu, theo sau là tên của thuộc tính và index để định hướng. Hãy xem cấu hình của bean và các phụ thuộc của nó bằng cách sử dụng chú thích:

@Configuration

public

class

AppConfig

{ @Bean

public

Item item1() {

return

new

ItemImpl1(); } @Bean

public

Store store() {

return

new

Store(item1()); } }

Code language:

PHP

(

php

)

Chú thích @Configuration chỉ ra rằng lớp là nguồn định nghĩa bean. Chúng tôi cũng có thể thêm nó vào nhiều lớp cấu hình.

Chúng tôi sử dụng chú thích @Bean trên một phương thức để xác định bean. Nếu chúng ta không chỉ định tên tùy chỉnh, thì tên bean sẽ mặc định là tên phương thức.

Đối với một bean có phạm vi singleton mặc định, trước tiên Spring sẽ kiểm tra xem một phiên bản được lưu trong bộ nhớ cache của bean đã tồn tại hay chưa và chỉ tạo một phiên bản mới nếu nó chưa tồn tại. Nếu chúng ta đang sử dụng phạm vi nguyên mẫu, container sẽ trả về một cá thể bean mới cho mỗi lần gọi phương thức.

Một cách khác để tạo cấu hình của bean là thông qua cấu hình XML:

<bean id=

"item1"

class

=

"org.baeldung.store.ItemImpl1"

/>

<

bean

id

=

"store"

class

=

"org.baeldung.store.Store"

>

<

constructor-arg

type

=

"ItemImpl1"

index

=

"0"

name

=

"item"

ref

=

"item1"

/>

</

bean

>

Code language:

JavaScript

(

javascript

)

6. Setter-Based Dependency Injection

Đối với DI dựa trên Setter, container sẽ gọi method setter của class sau khi gọi contructor không tham số hoặc phương thức nhà máy tĩnh không tham số để khởi tạo bean.

Hãy tạo cấu hình này bằng cách sử dụng chú thích:

@Bean

public

Store store() { Store store =

new

Store(); store.setItem(item1());

return

store; }

Code language:

PHP

(

php

)

Chúng ta cũng có thể sử dụng XML cho cùng một cấu hình bean:

<

bean

id

=

"store"

class

=

"org.baeldung.store.Store"

>

<

property

name

=

"item"

ref

=

"item1"

/>

</

bean

>

Code language:

HTML, XML

(

xml

)

7. Field-Based Dependency Injection

Trong trường hợp Field-Based DI, chúng ta có thể inject dependency bằng cách gắn @Autowired:

public

class

Store

{ @Autowired

private

Item item; }

Code language:

PHP

(

php

)

Trong khi xây dụng đối tượng Store, nếu không có hàm tạo hoặc phương thức setter nào để đưa vào Item bean, container sẽ sử dụng phản chiếu để đưa Item vào Store.

THAM GIA KHÓA HỌC LẬP TRÌNH

Chúng ta cũng có thể làm điều này bằng cách sử dụng cấu hình XML.

Cách tiếp cận này có thể trong đơn giản và gọn gàng hơn, nhưng tôi không khuyên bạn nên sử dụng vì có những nhược điểm sau:

  • Phương pháp này sử dụng phản xạ để đưa các dependency vào, tốn kém hơn so với inject dựa trên phương thức khởi tạo hoặc dựa trên setter.
  • Thực sự dễ dàng để tiếp tục thêm nhiều dependency bằng cách sử dụng phương pháp này. Nếu chúng ta đang sử dụng hàm tạo chèn, có nhiều đối số sẽ khiến chúng ta nghĩ rằng lớp làm nhiều hơn một việc, điều này có thể vi phạm Single Responsibility Principle.

8. Autowiring Dependencies

Wiring cho phép Spring container tự động giải quyết các dependency giữa các bean cộng tác bằng cách kiểm tra bean đã được xác định.

Có bốn chế độ tự động nạp một bean bằng cách sử dụng cấu hình XML:

  • no: default value – có nghĩa là không có autowiring nào được sử dụng cho bean và chúng ta phải đặt tên rõ ràng cho các dependency.
  • byName: autowiring được thực hiện dựa trên tên của thuộc tính, do đó Spring sẽ tìm một bean có cùng tên với thuộc tính cần đặt.
  • byType: tương tự như byName autowiring, chỉ dựa trên type của property. Điều này có nghĩa là Spring sẽ tìm một bean có cùng loại thuộc tính để đặt. Nếu có nhiều hơn một bean thuộc loại đó, thì framework sẽ đưa ra một ngoại lệ.
  • constructor: autowiring được thực hiện dựa trên các đối số của hàm tạo, có nghĩa là Spring sẽ tìm kiếm các bean có cùng kiểu với các đối số của hàm tạo.

Ví dụ: hãy autowire bean item1 được xác định ở trên theo loại vào store bean

@Bean(autowire = Autowire.BY_TYPE)

public

class

Store

{

private

Item item;

public

setItem(Item item){ this.item = item; } }

Code language:

PHP

(

php

)

Chúng ta có thể inject bean bằng cách sử dụng @Autowired

public

class

Store

{ @Autowired

private

Item item; }

Code language:

PHP

(

php

)

Nếu có nhiều hơn một bean cùng loại, chúng ta có thể sử dụng chú thích @Qualifier để tham chiếu đến một bean theo tên:

public

class

Store

{ @Autowired @Qualifier(

"item1"

)

private

Item item; }

Code language:

PHP

(

php

)

Bây giờ hãy autowire bean theo loại thông qua cấu hình XML:

<

bean

id

=

"store"

class

=

"org.baeldung.store.Store"

autowire

=

"byType"

>

</

bean

>

Code language:

HTML, XML

(

xml

)

Tiếp theo, hãy đưa một mục có tên bean vào thuộc tính item của store bean theo tên thông qua XML:

<bean id=

"item"

class

=

"org.baeldung.store.ItemImpl1"

/>

<

bean

id

=

"store"

class

=

"org.baeldung.store.Store"

autowire

=

"byName"

>

</

bean

>

Code language:

JavaScript

(

javascript

)

Chúng ta cũng có thể ghi đè tự động bằng cách xác định các phụ thuộc một cách rõ ràng thông qua các đối số của phương thức khởi tạo hoặc bộ thiết lập.

9. Lazy Initialized Beans

Theo mặc định, vùng chứa tạo và cấu hình tất cả các hạt đậu đơn trong quá trình khởi tạo. Để tránh điều này, chúng ta có thể sử dụng thuộc tính lazy-init với giá trị true trên cấu hình bean:

<bean id=

"item1"

class

=

"org.baeldung.store.ItemImpl1"

lazy-init=

"true"

/>

Code language:

JavaScript

(

javascript

)

Do đó, bean item1 sẽ chỉ được khởi tạo khi nó được yêu cầu lần đầu tiên và không phải khi khởi động. Ưu điểm của việc này là thời gian khởi tạo nhanh hơn, nhưng sự cân bằng là chúng ta sẽ không phát hiện ra bất kỳ lỗi cấu hình nào cho đến khi bean được yêu cầu, có thể là vài giờ hoặc thậm chí vài ngày sau khi ứng dụng đã chạy.

10. Kết luận

Trong bài viết này, chúng tôi đã trình bày các khái niệm về Inversion of ControlDependency Injection, và ví dụ chúng trong Spring Framework.

Chúc bạn thành công.

Tham khảo một số bài viết khác:

Hướng dẫn sử dụng Spring Data JPA