Dependency Injection và Autowiring

Xem thử

Một nhóm xây ứng dụng với field injection ở mọi nơi: @Autowired private PaymentService payment; trong hàng chục lớp, cho gọn. Nó chạy ổn cho tới khi hai việc xảy ra cùng tuần. Việc thứ nhất: một lập trình viên viết kiểm thử đơn vị cho OrderService, viết new OrderService() và nhận NullPointerException ở dòng đầu tiên dùng payment, vì không có container nào điền trường. Họ kết luận "không kiểm thử đơn vị được", chuyển mọi bài kiểm thử sang dựng cả context, và bộ kiểm thử chạy từ 20 giây lên 14 phút. Việc thứ hai: một đồng nghiệp thêm StripePayment cho cùng interface. Ở môi trường kiểm thử ứng dụng không còn khởi động, với thông báo expected single matching bean but found 2. Hai sự cố khác nhau, cùng gốc: phụ thuộc không được nói rõ ở nơi nó được dùng, và container không được chỉ dẫn cách chọn.

Bài này làm rõ cả hai. Bạn sẽ chạy ba kiểu tiêm và thấy chúng khác nhau thế nào ngoài container, chạy tình huống hai ứng viên và đo thứ tự ưu tiên của @Qualifier và @Primary, rồi xử lý phụ thuộc tùy chọn, tiêm cả danh sách bean, và tiêm giá trị cấu hình bằng @Value, kèm một cái bẫy im lặng của nó.

Sau bài này, bạn có thể

  • Viết ba kiểu tiêm (constructor, setter, trường) và chỉ ra bằng thí nghiệm kiểu nào kiểm thử được mà không cần container.
  • Giải thích cách container chọn bean khi có nhiều ứng viên, và đọc NoUniqueBeanDefinitionException.
  • Khai báo phụ thuộc tùy chọn bằng Optional, ObjectProvider hoặc required = false.
  • Tiêm List và Map bean, kiểm soát thứ tự bằng @Order.
  • Dùng @Value với giá trị mặc định, và biết khi nào thiếu giá trị bị bỏ qua im lặ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/03-dependency-injection, 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ầnPhiên bản đã kiểm chứngGhi chú
JDK17 trở lênjava -version
Maven3.9 trở lênmvn -v
Spring Framework7.0.9khai báo trong pom.xml
Dockerkhông cầnbài này không dùng cơ sở dữ liệu

Cấu trúc

text
03-dependency-injection/
  pom.xml
  src/main/java/dev/canhdominich/spring/di/
    PaymentService.java, PaypalPayment.java, StripePayment.java   interface và hai cài đặt
    InjectionStylesDemo.java     ba kiểu tiêm, trong và ngoài container
    ResolutionDemo.java          hai ứng viên, @Qualifier, @Primary
    OptionalDemo.java            phụ thuộc tùy chọn khi không có bean
    CollectionDemo.java          tiêm List và Map, thứ tự @Order
    ValueDemo.java               @Value, giá trị mặc định, thiếu giá trị
  src/main/resources/
    app.properties               hai thuộc tính cho @Value

Khai báo thư viện: pom.xml

pom.xml

Dự án Maven Java 17 chỉ khai báo spring-context 7.0.9, jakarta.annotation-api, 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; lớp mặc định là InjectionStylesDemo.

xml
<?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-dependency-injection</artifactId>
    <version>1.0.0</version>
    <name>Spring Core - Dependency Injection</name>

    <properties>
        <maven.compiler.release>17</maven.compiler.release>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <main.class>dev.canhdominich.spring.di.InjectionStylesDemo</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>

Ba kiểu tiêm và điều xảy ra ngoài container

Mô hình dùng cho cả bài rất nhỏ: một interface PaymentService với phương thức name() và hai cài đặt:

src/main/java/dev/canhdominich/spring/di/PaymentService.java

java
package dev.canhdominich.spring.di;

public interface PaymentService {

    String name();
}

src/main/java/dev/canhdominich/spring/di/PaypalPayment.java

java
package dev.canhdominich.spring.di;

public class PaypalPayment implements PaymentService {

    @Override
    public String name() {
        return "paypal";
    }
}

src/main/java/dev/canhdominich/spring/di/StripePayment.java

java
package dev.canhdominich.spring.di;

public class StripePayment implements PaymentService {

    @Override
    public String name() {
        return "stripe";
    }
}

InjectionStylesDemo có ba lớp cùng làm một việc (place() trả tên của cổng thanh toán) nhưng nhận PaymentService theo ba cách khác nhau, cùng một lớp cấu hình nhỏ để container dựng chúng. Chú ý vị trí của phụ thuộc trong mỗi lớp: tham số constructor (ConstructorOrder), phương thức setter có @Autowired (SetterOrder), và trường riêng tư có @Autowired (FieldOrder):

src/main/java/dev/canhdominich/spring/di/InjectionStylesDemo.java

java
package dev.canhdominich.spring.di;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class InjectionStylesDemo {

    public static void main(String[] args) {
        run();
    }

    static class ConstructorOrder {

        private final PaymentService payment;

        ConstructorOrder(PaymentService payment) {
            this.payment = payment;
        }

        String place() {
            return payment.name();
        }
    }

    static class SetterOrder {

        private PaymentService payment;

        @Autowired
        void setPayment(PaymentService payment) {
            this.payment = payment;
        }

        String place() {
            return payment.name();
        }
    }

    static class FieldOrder {

        @Autowired
        private PaymentService payment;

        String place() {
            return payment.name();
        }
    }

    @Configuration
    static class StylesConfig {

        @Bean
        PaymentService payment() {
            return new PaypalPayment();
        }

        @Bean
        ConstructorOrder constructorOrder(PaymentService payment) {
            return new ConstructorOrder(payment);
        }

        @Bean
        SetterOrder setterOrder() {
            return new SetterOrder();
        }

        @Bean
        FieldOrder fieldOrder() {
            return new FieldOrder();
        }
    }

    public static String[] run() {
        String[] results = new String[5];
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(StylesConfig.class)) {
            results[0] = context.getBean(ConstructorOrder.class).place();
            results[1] = context.getBean(SetterOrder.class).place();
            results[2] = context.getBean(FieldOrder.class).place();
        }
        System.out.println("inside the container: constructor=" + results[0] + ", setter=" + results[1] + ", field=" + results[2]);
        results[3] = outsideTheContainer("setter", () -> new SetterOrder().place());
        results[4] = outsideTheContainer("field", () -> new FieldOrder().place());
        System.out.println("constructor injection outside a container: new ConstructorOrder() does not compile without a PaymentService");
        return results;
    }

    private static String outsideTheContainer(String style, java.util.function.Supplier<String> action) {
        try {
            return action.get();
        } catch (NullPointerException e) {
            System.out.println("outside the container, " + style + " injection: NullPointerException on first use");
            return "NullPointerException";
        }
    }
}

Chạy bằng mvn -q compile exec:exec (lớp chính mặc định). Kết quả:

text
inside the container: constructor=paypal, setter=paypal, field=paypal
outside the container, setter injection: NullPointerException on first use
outside the container, field injection: NullPointerException on first use
constructor injection outside a container: new ConstructorOrder() does not compile without a PaymentService

Trong container cả ba cho cùng kết quả. Ngoài container, bài kiểm thử tự dựng đối tượng bằng new:

  • Setter: new SetterOrder().place() ném NullPointerException: không ai gọi setter, và trình biên dịch không ngăn bạn quên.
  • Trường: cũng NullPointerException, và tệ hơn, trường là private nên bài kiểm thử không có cách thường nào đặt giá trị (chỉ có reflection hoặc dựng cả container).
  • Constructor: new ConstructorOrder() không biên dịch nếu thiếu PaymentService. Người viết kiểm thử bị buộc truyền một giá trị, và truyền được bản giả như bài trước đã làm. Trường payment còn có thể là final: đối tượng không bao giờ ở trạng thái thiếu phụ thuộc.

Cách đọc hình: hàng trên: constructor injection buộc người tạo truyền phụ thuộc và cho phép trường final. Hàng dưới: setter hoặc trường, khi tự dựng bằng new, đều để phụ thuộc là null và lỗi chỉ lộ ra lúc dùng lần đầu.

KiểuPhụ thuộc bắt buộcTrường finalKiểm thử không cần containerKhi dùng
ConstructorCó, trình biên dịch bắtCóDễ, truyền bản giảMặc định cho phụ thuộc bắt buộc
SetterKhôngKhôngPhải nhớ gọi setterPhụ thuộc tùy chọn hoặc cấu hình lại được
TrườngKhôngKhôngCần reflection hoặc containerTránh trong code thật

Với một constructor duy nhất, Spring tự dùng nó: bạn không cần viết @Autowired (ở bài trước, OrderService đã như vậy).

Hai ứng viên: container chọn bean nào

Bây giờ cho PaymentService hai bean, paypal và stripe. ResolutionDemo chạy bốn tình huống bằng bốn lớp cấu hình nhỏ. Consumer nhận PaymentService thường; QualifiedConsumer nhận cùng kiểu nhưng ghi @Qualifier("stripe") ở tham số. TwoBeansConfig có hai bean và không có gợi ý nào; QualifierConfig thêm @Qualifier; PrimaryConfig đánh dấu paypal là @Primary và có cả hai consumer:

src/main/java/dev/canhdominich/spring/di/ResolutionDemo.java

java
package dev.canhdominich.spring.di;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

public class ResolutionDemo {

    public static void main(String[] args) {
        ambiguous();
        withQualifier();
        withPrimary();
        qualifierBeatsPrimary();
    }

    static class Consumer {

        private final PaymentService payment;

        Consumer(PaymentService payment) {
            this.payment = payment;
        }

        String uses() {
            return payment.name();
        }
    }

    static class QualifiedConsumer {

        private final PaymentService payment;

        QualifiedConsumer(@Qualifier("stripe") PaymentService payment) {
            this.payment = payment;
        }

        String uses() {
            return payment.name();
        }
    }

    @Configuration
    static class TwoBeansConfig {

        @Bean
        PaymentService paypal() {
            return new PaypalPayment();
        }

        @Bean
        PaymentService stripe() {
            return new StripePayment();
        }

        @Bean
        Consumer consumer(PaymentService payment) {
            return new Consumer(payment);
        }
    }

    @Configuration
    static class QualifierConfig {

        @Bean
        PaymentService paypal() {
            return new PaypalPayment();
        }

        @Bean
        PaymentService stripe() {
            return new StripePayment();
        }

        @Bean
        QualifiedConsumer qualifiedConsumer(@Qualifier("stripe") PaymentService payment) {
            return new QualifiedConsumer(payment);
        }
    }

    @Configuration
    static class PrimaryConfig {

        @Bean
        @Primary
        PaymentService paypal() {
            return new PaypalPayment();
        }

        @Bean
        PaymentService stripe() {
            return new StripePayment();
        }

        @Bean
        Consumer consumer(PaymentService payment) {
            return new Consumer(payment);
        }

        @Bean
        QualifiedConsumer qualifiedConsumer(@Qualifier("stripe") PaymentService payment) {
            return new QualifiedConsumer(payment);
        }
    }

    static String ambiguous() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(TwoBeansConfig.class)) {
            return "started";
        } catch (BeansException e) {
            Throwable cause = e;
            while (cause.getCause() != null) {
                cause = cause.getCause();
            }
            String message = cause.getClass().getSimpleName() + ": " + cause.getMessage();
            System.out.println("two candidates, nothing to choose with: " + message);
            return message;
        }
    }

    static String withQualifier() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(QualifierConfig.class)) {
            String uses = context.getBean(QualifiedConsumer.class).uses();
            System.out.println("@Qualifier(\"stripe\") selects: " + uses);
            return uses;
        }
    }

    static String withPrimary() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrimaryConfig.class)) {
            String uses = context.getBean(Consumer.class).uses();
            System.out.println("@Primary on paypal, plain parameter selects: " + uses);
            return uses;
        }
    }

    static String qualifierBeatsPrimary() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrimaryConfig.class)) {
            String uses = context.getBean(QualifiedConsumer.class).uses();
            System.out.println("@Primary on paypal and @Qualifier(\"stripe\") on the parameter selects: " + uses);
            return uses;
        }
    }
}

Chạy bằng mvn -q exec:exec -Dmain.class=dev.canhdominich.spring.di.ResolutionDemo:

text
two candidates, nothing to choose with: NoUniqueBeanDefinitionException: No qualifying bean of type 'dev.canhdominich.spring.di.PaymentService' available: expected single matching bean but found 2: paypal,stripe
@Qualifier("stripe") selects: stripe
@Primary on paypal, plain parameter selects: paypal
@Primary on paypal and @Qualifier("stripe") on the parameter selects: stripe
  • Không gợi ý: NoUniqueBeanDefinitionException ngay lúc khởi động, liệt kê đúng các ứng viên (paypal,stripe). Container không bao giờ đoán.
  • @Qualifier("stripe") ở tham số: chọn bean tên stripe.
  • @Primary ở bean paypal: tham số thường nhận paypal. @Primary là lựa chọn mặc định chung cho mọi chỗ không chỉ định gì.
  • Cả hai cùng có mặt: tham số có @Qualifier("stripe") nhận stripe dù paypal là @Primary. Chỗ nào nói rõ muốn gì thì thắng chỗ mặc định chung.

Đang tải Animation…

Cách đọc hình: hàng trên là các bước container đi qua: tìm các bean theo kiểu, xét @Qualifier ở tham số, rồi xét bean @Primary. Hàng dưới là các kết quả đo: không có gợi ý nào thì NoUniqueBeanDefinitionException khi khởi động; có cả hai thì @Qualifier thắng.

Phụ thuộc tùy chọn: khi có thể không có bean

Mặc định mọi phụ thuộc là bắt buộc (bài trước đã đo lỗi NoSuchBeanDefinitionException khi thiếu bean). Có ba cách nói "dùng nếu có". OptionalDemo thử cả ba trong một context không có bean PaymentService nào: Optional<PaymentService> trong constructor, ObjectProvider<PaymentService> (rồi getIfAvailable()), và @Autowired(required = false) trên setter:

src/main/java/dev/canhdominich/spring/di/OptionalDemo.java

java
package dev.canhdominich.spring.di;

import java.util.Optional;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class OptionalDemo {

    public static void main(String[] args) {
        run();
    }

    static class WithOptional {

        private final Optional<PaymentService> payment;

        WithOptional(Optional<PaymentService> payment) {
            this.payment = payment;
        }

        String describe() {
            return payment.map(PaymentService::name).orElse("none");
        }
    }

    static class WithProvider {

        private final ObjectProvider<PaymentService> payment;

        WithProvider(ObjectProvider<PaymentService> payment) {
            this.payment = payment;
        }

        String describe() {
            PaymentService found = payment.getIfAvailable();
            return found == null ? "none" : found.name();
        }
    }

    static class WithSetter {

        private PaymentService payment;

        @Autowired(required = false)
        void setPayment(PaymentService payment) {
            this.payment = payment;
        }

        String describe() {
            return payment == null ? "none" : payment.name();
        }
    }

    @Configuration
    static class NoPaymentConfig {

        @Bean
        WithOptional withOptional(Optional<PaymentService> payment) {
            return new WithOptional(payment);
        }

        @Bean
        WithProvider withProvider(ObjectProvider<PaymentService> payment) {
            return new WithProvider(payment);
        }

        @Bean
        WithSetter withSetter() {
            return new WithSetter();
        }
    }

    public static String[] run() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(NoPaymentConfig.class)) {
            String[] results = {
                    context.getBean(WithOptional.class).describe(),
                    context.getBean(WithProvider.class).describe(),
                    context.getBean(WithSetter.class).describe()};
            System.out.println("no PaymentService bean at all: Optional=" + results[0] + ", ObjectProvider=" + results[1] + ", required=false setter=" + results[2]);
            return results;
        }
    }
}

Chạy bằng mvn -q exec:exec -Dmain.class=dev.canhdominich.spring.di.OptionalDemo:

text
no PaymentService bean at all: Optional=none, ObjectProvider=none, required=false setter=none

Cả ba cách đều để context khởi động bình thường và cho đối tượng quyết định phải làm gì khi thiếu: Optional rỗng, getIfAvailable() trả null, setter không được gọi. ObjectProvider còn có ích ở chỗ nó chỉ tra cứu khi bạn gọi, nên cũng là cách lấy bean tạo muộn hoặc bean prototype mới mỗi lần (bài vòng đời sẽ dùng).

Cách đọc hình: tham số thường ở trên cùng làm khởi động thất bại; ba dạng tùy chọn đều dẫn tới một context khởi động bình thường, với đối tượng nhận none.

Tiêm cả danh sách và bản đồ bean

Khi bạn có nhiều cài đặt của một interface và muốn dùng hết (một danh sách chiến lược, một chuỗi bộ lọc), đừng tiêm từng cái: tiêm List<PaymentService> hoặc Map<String, PaymentService>. CollectionDemo khai báo hai bean với @Order ngược với thứ tự khai báo (stripe có @Order(1) nhưng được khai báo sau paypal), rồi tiêm cả danh sách lẫn bản đồ:

src/main/java/dev/canhdominich/spring/di/CollectionDemo.java

java
package dev.canhdominich.spring.di;

import java.util.List;
import java.util.Map;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

public class CollectionDemo {

    public static void main(String[] args) {
        run();
    }

    static class AllPayments {

        private final List<PaymentService> ordered;
        private final Map<String, PaymentService> byName;

        AllPayments(List<PaymentService> ordered, Map<String, PaymentService> byName) {
            this.ordered = ordered;
            this.byName = byName;
        }

        List<String> orderedNames() {
            return ordered.stream().map(PaymentService::name).toList();
        }

        List<String> keys() {
            return byName.keySet().stream().sorted().toList();
        }
    }

    @Configuration
    static class CollectionConfig {

        @Bean
        @Order(2)
        PaymentService paypal() {
            return new PaypalPayment();
        }

        @Bean
        @Order(1)
        PaymentService stripe() {
            return new StripePayment();
        }

        @Bean
        AllPayments allPayments(List<PaymentService> ordered, Map<String, PaymentService> byName) {
            return new AllPayments(ordered, byName);
        }
    }

    public static List<List<String>> run() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CollectionConfig.class)) {
            AllPayments all = context.getBean(AllPayments.class);
            System.out.println("List<PaymentService> in @Order: " + all.orderedNames());
            System.out.println("Map<String, PaymentService> keys: " + all.keys());
            return List.of(all.orderedNames(), all.keys());
        }
    }
}

Chạy bằng mvn -q exec:exec -Dmain.class=dev.canhdominich.spring.di.CollectionDemo:

text
List<PaymentService> in @Order: [stripe, paypal]
Map<String, PaymentService> keys: [paypal, stripe]

Danh sách chứa cả hai bean theo @Order (số nhỏ trước), không theo thứ tự khai báo; bản đồ có khóa là tên bean. Thêm một bean PaymentService thứ ba, không consumer nào phải sửa: danh sách tự dài ra.

Cách đọc hình: hai bean với @Order khác nhau đổ vào danh sách theo thứ tự đã đánh số và vào bản đồ theo tên; ô cuối nhắc lợi ích: thêm bean mới không đòi sửa consumer.

@Value: tiêm giá trị cấu hình và một cái bẫy im lặng

Cấu hình đơn giản (tên ứng dụng, giới hạn) được tiêm bằng @Value("${khóa}"); ${khóa:mặc định} cho giá trị khi thiếu. File thuộc tính nằm ở src/main/resources/app.properties và được nạp bằng @PropertySource:

src/main/resources/app.properties

properties
app.name=Course Platform
app.owner=Carnell

ValueDemo có ba thí nghiệm: Settings đọc ba giá trị (một có mặc định vì khóa không có trong file); BrokenSettings đọc một khóa không tồn tại và không có mặc định; và một cấu hình thứ ba thêm PropertySourcesPlaceholderConfigurer cho cùng BrokenSettings:

src/main/java/dev/canhdominich/spring/di/ValueDemo.java

java
package dev.canhdominich.spring.di;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

public class ValueDemo {

    public static void main(String[] args) {
        run();
        missing();
        strict();
    }

    static class Settings {

        @Value("${app.name}")
        String name;

        @Value("${app.max-size:100}")
        int maxSize;

        @Value("${app.owner}")
        String owner;
    }

    static class BrokenSettings {

        @Value("${app.missing}")
        String missing;
    }

    @Configuration
    @PropertySource("classpath:app.properties")
    static class SettingsConfig {

        @Bean
        Settings settings() {
            return new Settings();
        }
    }

    @Configuration
    @PropertySource("classpath:app.properties")
    static class BrokenConfig {

        @Bean
        BrokenSettings brokenSettings() {
            return new BrokenSettings();
        }
    }

    public static String run() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SettingsConfig.class)) {
            Settings settings = context.getBean(Settings.class);
            String line = "name=" + settings.name + ", owner=" + settings.owner + ", maxSize=" + settings.maxSize;
            System.out.println(line);
            return line;
        }
    }

    public static String missing() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BrokenConfig.class)) {
            String value = context.getBean(BrokenSettings.class).missing;
            System.out.println("placeholder without a value and without a default: context started, field = \"" + value + "\"");
            return value;
        } catch (BeansException e) {
            Throwable cause = e;
            while (cause.getCause() != null) {
                cause = cause.getCause();
            }
            String message = cause.getClass().getSimpleName() + ": " + cause.getMessage();
            System.out.println("placeholder without a value and without a default: " + message);
            return message;
        }
    }

    @Configuration
    @PropertySource("classpath:app.properties")
    static class StrictConfig {

        @Bean
        static PropertySourcesPlaceholderConfigurer placeholders() {
            return new PropertySourcesPlaceholderConfigurer();
        }

        @Bean
        BrokenSettings brokenSettings() {
            return new BrokenSettings();
        }
    }

    public static String strict() {
        try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(StrictConfig.class)) {
            return "started, field = " + context.getBean(BrokenSettings.class).missing;
        } catch (BeansException e) {
            Throwable cause = e;
            while (cause.getCause() != null) {
                cause = cause.getCause();
            }
            String message = cause.getClass().getSimpleName() + ": " + cause.getMessage();
            System.out.println("with PropertySourcesPlaceholderConfigurer: " + message);
            return message;
        }
    }
}

Chạy bằng mvn -q exec:exec -Dmain.class=dev.canhdominich.spring.di.ValueDemo:

text
name=Course Platform, owner=Carnell, maxSize=100
placeholder without a value and without a default: context started, field = "${app.missing}"
with PropertySourcesPlaceholderConfigurer: PlaceholderResolutionException: Could not resolve placeholder 'app.missing' in value "${app.missing}"
  • Có trong file thì được tiêm: name và owner lấy từ app.properties.
  • Có mặc định thì dùng mặc định: maxSize=100 vì khóa app.max-size không có trong file.
  • Thiếu khóa và không mặc định: trên Spring 7.0.9 chạy trần (không có PropertySourcesPlaceholderConfigurer), context khởi động bình thường và trường nhận đúng chuỗi thô ${app.missing}. Không lỗi, không cảnh báo: chuỗi đó sẽ đi tiếp vào code của bạn như một giá trị thật (một đường dẫn, một địa chỉ).
  • Thêm PropertySourcesPlaceholderConfigurer: cùng cấu hình giờ làm khởi động thất bại với PlaceholderResolutionException nêu rõ khóa thiếu. Đây là hành vi nghiêm ngặt bạn nên có (và là một lý do cấu hình tách riêng có lợi); Spring Boot ở chương sau tự đăng ký nó, nên bạn không phải nhớ.

Cách đọc hình: ba ô bên trái là ba dạng @Value: có giá trị, có mặc định, và không có gì. Dạng cuối cho chuỗi thô ở trường (ô giữa hàng dưới); thêm PropertySourcesPlaceholderConfigurer (ô cuối) biến nó thành lỗi lúc khởi động.

Quyết định thiết kế và đánh đổi

Quyết địnhLựa chọnĐánh đổi
Kiểu tiêmConstructorConstructor dài khi lớp có quá nhiều phụ thuộc, nhưng đó là tín hiệu nên tách lớp
Chọn bean khi có nhiều@Qualifier ở nơi cần, @Primary cho mặc địnhPhải đặt tên bean hoặc đánh dấu rõ ràng
Phụ thuộc không bắt buộcOptional hoặc ObjectProviderĐối tượng phải xử lý trường hợp thiếu
Nhiều cài đặt cùng kiểuTiêm List hoặc MapThứ tự phải khai báo bằng @Order
Giá trị cấu hình@Value với mặc định hợp lý, cộng cấu hình nghiêm ngặtCấu hình cần được kiểm tra lúc khởi động

Playbook: gặp sự cố tiêm phụ thuộc thì làm gì

Triệu chứngKiểm tra đầu tiênNguyên nhân hay gặpCách xử lý
NullPointerException trong bài kiểm thử đơn vịPhụ thuộc được tiêm kiểu nàoField hoặc setter injection, tự newChuyển sang constructor injection
expected single matching bean but found 2Danh sách ứng viên trong thông báoThêm một cài đặt thứ hai cùng interface@Qualifier ở chỗ cần, hoặc @Primary cho mặc định
Bean sai được tiêmCó @Primary và @Qualifier cùng mặt khôngQuên rằng @Qualifier thắngĐặt @Qualifier tường minh ở chỗ cần bean cụ thể
Thiếu bean làm ứng dụng không lênThông báo kiểu bean thiếuBean thực sự tùy chọnOptional hoặc ObjectProvider nếu đúng là tùy chọn
Giá trị cấu hình là chuỗi ${...}Khóa có trong file khôngPlaceholder không có giá trị trên Spring trầnThêm PropertySourcesPlaceholderConfigurer hoặc dùng Boot
Danh sách bean sai thứ tự@Order trên beanThiếu hoặc nhầm @OrderĐặt @Order rõ ràng

Lỗi thường gặp khi chạy lab

Triệu chứngNguyên nhânCách xử lý
Không thấy dòng nào của ValueDemo về thiếu giá trịChương trình in thẳng khi chạy mainChạy đúng lệnh ValueDemo ở trên, đọc cả ba dòng
mvn không tìm thấy lớp chínhQuên compile ở lệnh đầu tiên sau khi sửa codeChạy 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/03-dependency-injection). Sau mỗi bước, so kết quả với tiêu chí nghiệm thu.

  1. Chạy các thí nghiệm. Chạy InjectionStylesDemo, ResolutionDemo, OptionalDemo, CollectionDemo, ValueDemo. Tiêu chí: khớp từng dòng kết quả trong bài.
  2. Đổi thứ tự. Trong CollectionDemo hoán đổi hai số @Order (stripe thành 2, paypal thành 1). Tiêu chí: danh sách đổi thành [paypal, stripe].
  3. Cấu hình thêm khóa. Thêm dòng app.max-size=5 vào app.properties và chạy ValueDemo. Tiêu chí: maxSize=5.
  4. Làm cho Primary thắng. Trong ResolutionDemo.PrimaryConfig, bỏ @Qualifier("stripe") khỏi tham số của phương thức @Bean qualifiedConsumer, và sửa main chỉ còn gọi qualifierBeatsPrimary() (các dòng còn lại dùng cấu hình khác nên sẽ lỗi nếu bạn để nguyên). Tiêu chí: dòng kết quả cuối cùng in paypal (nhãn của dòng vẫn nhắc @Qualifier, hãy bỏ qua nhãn); giải thích vì sao.
  5. Viết kiểm thử không có container. Với ConstructorOrder, viết một bài kiểm thử đơn vị truyền một PaymentService giả (lớp ẩn danh trả "fake") và kiểm tra place(). Tiêu chí: test xanh, không có container, không có NullPointerException.
  6. 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 test cho Tests run: 6, Failures: 0, Errors: 0.

Hiểu lầm và giới hạn

  • "Field injection gọn nên cứ dùng." Nó che phụ thuộc và làm kiểm thử đơn vị cần container hoặc reflection.
  • "@Primary luôn thắng." Một @Qualifier tường minh ở nơi tiêm thắng @Primary.
  • "@Value thiếu khóa thì báo lỗi." Trên Spring trần thì không: chuỗi thô được tiêm im lặng.
  • Số đo thuộc về Spring Framework 7.0.9 (ví dụ hành vi lenient của @Value khi không có PropertySourcesPlaceholderConfigurer). Bài chưa đo việc ghép theo tên tham số khi không có @Qualifier, @Resource, @Inject, và @Autowired trên tham số của @Bean ở các cấu hình lồng phức tạp.

Tự kiểm tra

  1. Vì sao new SetterOrder().place() ném NullPointerException còn new ConstructorOrder() không biên dịch được nếu thiếu tham số?
  2. Container làm gì khi gặp hai bean cùng kiểu mà không có gợi ý nào?
  3. Bean paypal có @Primary và tham số có @Qualifier("stripe"): bean nào được tiêm?
  4. Ba cách khai báo phụ thuộc tùy chọn là gì?
  5. @Value("${app.missing}") không có khóa trong cấu hình nghiêm ngặt và không nghiêm ngặt thì khác nhau thế nào?

Gợi ý đáp án. (1) Setter không được gọi và không ai bắt buộc; constructor buộc người tạo truyền giá trị. (2) Ném NoUniqueBeanDefinitionException lúc khởi động, liệt kê ứng viên. (3) stripe, vì @Qualifier được xét trước @Primary. (4) Optional, ObjectProvider, và @Autowired(required = false). (5) Không nghiêm ngặt: chuỗi thô ${app.missing} được tiêm im lặng; nghiêm ngặt (có PropertySourcesPlaceholderConfigurer): khởi động thất bại với PlaceholderResolutionException.

Điều cần nhớ

  • Dùng constructor injection: phụ thuộc bắt buộc, trường final, kiểm thử bằng bản giả.
  • Hai ứng viên không gợi ý thì khởi động thất bại; @Qualifier chỉ rõ tại chỗ, @Primary là mặc định chung, và @Qualifier thắng.
  • Optional và ObjectProvider cho phụ thuộc tùy chọn; List và Map cho mọi cài đặt, thứ tự theo @Order.
  • @Value thiếu khóa không lỗi trên Spring trần; thêm cấu hình nghiêm ngặt hoặc dùng Boot.

Đọc thêm

Bài tiếp theo nói về cuộc đời của một bean: các bước khởi tạo và hủy, phạm vi singleton và prototype, cái bẫy prototype trong singleton, và SpEL.