VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Spring Boot Web APICấu trúc ứng dụng
Bài 9/19
5 phút

Dependency injection trong Spring

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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.api và tạo bean cho mọi class có @Service. Nơi nào cần ProductStore thì nhận InMemoryProductStore.
  • Controller chỉ khai báo tham số ProductStore trong 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 ProductStore qua constructor như cũ, còn phần đọc ghi database nằm trong một class store mới.
tạo một lần, dùng lại truyền vào constructor GET /api/products Spring container InMemoryProductStore ProductsController
Container tạo store rồi đưa vào constructor controller

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 @Service hoặc @Component, nhận qua constructor của controller.
  • prototype tạo mới mỗi lần, @RequestScope mỗ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âu
Câu 1

Service 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?

Câu 2

Ứ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?

Câu 3

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?

CRUD hoàn chỉnhCấu hình với application.properties

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt