Dependency injection trong Spring
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Ở chương trước, danh sách sản phẩm là một field nằm ngay trong controller. Controller vừa xử lý request vừa giữ dữ liệu, nên muốn đổi sang database thì phải sửa controller. Bài này tách phần lưu trữ ra một service, rồi để Spring tự đưa service đó vào controller.
Khái niệm
🧰 Spring container: thành phần của Spring tự tạo object và truyền vào constructor của class cần nó.
🫘 Bean: object do Spring container tạo và quản lý, khai báo bằng cách đánh dấu class với @Service hoặc @Component.
⏱️ Bean scope: quy định container tạo object mới khi nào.
| Đánh dấu class bằng | Container tạo object |
|---|---|
@Scope("prototype") |
mới mỗi lần có nơi cần |
@RequestScope |
một object cho mỗi request |
| không ghi gì (singleton) | một object cho cả ứng dụng |
Ví dụ
// ProductStore.java
package com.shop.api;
import java.util.List;
interface ProductStore {
List<String> getAll();
}
// InMemoryProductStore.java
package com.shop.api;
import java.util.*;
import org.springframework.stereotype.*;
@Service
class InMemoryProductStore implements ProductStore {
private final List<String> names =
new ArrayList<>(List.of("Pen", "Notebook"));
@Override
public List<String> getAll() {
return names;
}
}
// ProductsController.java
package com.shop.api;
import java.util.List;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/products")
class ProductsController {
private final ProductStore store;
public ProductsController(ProductStore store) {
this.store = store;
}
@GetMapping
public List<String> getAll() {
return store.getAll();
}
}- Lúc khởi động, Spring quét package
com.shop.apivà tạo bean cho mọi class có@Service. Nơi nào cầnProductStorethì nhậnInMemoryProductStore. - Controller chỉ khai báo tham số
ProductStoretrong constructor. Ở bài DIP bạn tự tạo object rồi truyền vào constructor, ở đây container làm việc đó. - Dữ liệu trong bộ nhớ phải sống suốt ứng dụng. Scope mặc định là singleton nên không cần ghi thêm gì.
- Sang chương 4, controller vẫn nhận
ProductStorequa constructor như cũ, còn phần đọc ghi database nằm trong một class store mới.
Thử ngay
Giữ các file cũ, thêm class đếm và một controller mới. Counter đánh dấu
bằng @RequestScope:
// Counter.java
package com.shop.api;
import org.springframework.stereotype.*;
import org.springframework.web.context.annotation.*;
@Component
@RequestScope
class Counter {
private int count;
public int next() {
count++;
return count;
}
}
// CountController.java
package com.shop.api;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/count")
class CountController {
private final Counter counter;
public CountController(Counter counter) {
this.counter = counter;
}
@GetMapping
public int get() {
return counter.next();
}
}Chạy lại server, gọi curl -i http://localhost:5000/api/count ba lần. Sau
đó xoá dòng @RequestScope, chạy lại server và gọi thêm ba lần.
Đoán trước khi chạy: với @RequestScope, ba lần gọi trả về những số nào?
Xem kết quả
Status 200 cả sáu lần, body lần lượt là:
@RequestScope: 1, 1, 1
singleton: 1, 2, 3@RequestScope tạo Counter mới cho mỗi request, nên lần nào cũng bắt đầu
lại từ 0. Singleton dùng chung một Counter cho cả ứng dụng, nên số tăng
dần. Controller chỉ được tạo một lần, nhưng Spring đưa vào đó một object đại
diện, tự chuyển mỗi lần gọi tới Counter của request hiện tại.
Lỗi hay gặp
Quên đánh dấu @Service. Class không thành bean, Spring không biết lấy gì
cho ProductStore. Ứng dụng dừng ngay lúc khởi động:
Parameter 0 of constructor in com.shop.api.ProductsController
required a bean of type 'com.shop.api.ProductStore'
that could not be found.// SAI — thiếu @Service, không có bean ProductStore
// InMemoryProductStore.java
package com.shop.api;
import java.util.List;
class InMemoryProductStore implements ProductStore {
@Override
public List<String> getAll() {
return List.of("Pen", "Notebook");
}
}// ĐÚNG — @Service biến class thành bean
// InMemoryProductStore.java
package com.shop.api;
import java.util.List;
import org.springframework.stereotype.*;
@Service
class InMemoryProductStore implements ProductStore {
@Override
public List<String> getAll() {
return List.of("Pen", "Notebook");
}
}Singleton giữ bean prototype. Giả sử Counter đổi sang
@Scope("prototype") để mỗi nơi cần nhận object mới. Nhưng Report là
singleton, chỉ được tạo một lần, nên chỉ nhận đúng một Counter và dùng
mãi. Spring không báo lỗi gì, next() trả về 1, 2, 3 như singleton.
// SAI — Report chỉ nhận Counter một lần
// Report.java
package com.shop.api;
import org.springframework.stereotype.*;
@Service
class Report {
private final Counter counter;
public Report(Counter counter) {
this.counter = counter;
}
public int next() {
return counter.next();
}
}// ĐÚNG — xin Counter mới mỗi lần dùng
// Report.java
package com.shop.api;
import org.springframework.beans.factory.*;
import org.springframework.stereotype.*;
@Service
class Report {
private final ObjectProvider<Counter> counters;
public Report(ObjectProvider<Counter> counters) {
this.counters = counters;
}
public int next() {
return counters.getObject().next();
}
}ObjectProvider<Counter> nhờ container tạo Counter lúc gọi getObject(),
nên lần nào next() cũng trả về 1.
Tóm tắt
- Đánh dấu class bằng
@Servicehoặc@Component, nhận qua constructor của controller. prototypetạo mới mỗi lần,@RequestScopemỗi request một, singleton (mặc định) cả ứng dụng một.- Nhận bằng interface để đổi cách làm mà không sửa controller.
- Bean sống lâu giữ bean sống ngắn hơn thì bean đó cũng sống lâu theo.
Tự kiểm tra
0/3 câuService lưu cấu hình đọc một lần lúc khởi động và dùng chung cho mọi request. Nên khai báo thế nào?
Ứng dụng không khởi động được, log báo "required a bean of type 'com.shop.api.EmailSender' that could not be found". Nguyên nhân?
Bean đánh dấu @RequestScope được dùng ở controller và ở một service khác trong CÙNG một request. Có bao nhiêu object được tạo?