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,ObjectProviderhoặcrequired = false. - Tiêm
ListvàMapbean, kiểm soát thứ tự bằng@Order. - Dùng
@Valuevớ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ần | Phiên bản đã kiểm chứng | Ghi chú |
|---|---|---|
| JDK | 17 trở lên | java -version |
| Maven | 3.9 trở lên | mvn -v |
| Spring Framework | 7.0.9 | khai báo trong pom.xml |
| Docker | không cần | bài này không dùng cơ sở dữ liệu |
Cấu trúc
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 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
package dev.canhdominich.spring.di;
public interface PaymentService {
String name();
}
src/main/java/dev/canhdominich/spring/di/PaypalPayment.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
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
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ả:
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émNullPointerException: 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àprivatenê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ếuPaymentService. 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ườngpaymentcò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ểu | Phụ thuộc bắt buộc | Trường final | Kiểm thử không cần container | Khi dùng |
|---|---|---|---|---|
| Constructor | Có, trình biên dịch bắt | Có | Dễ, truyền bản giả | Mặc định cho phụ thuộc bắt buộc |
| Setter | Không | Không | Phải nhớ gọi setter | Phụ thuộc tùy chọn hoặc cấu hình lại được |
| Trường | Không | Không | Cần reflection hoặc container | Trá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
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:
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 ý:
NoUniqueBeanDefinitionExceptionngay 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ênstripe.@Primaryở beanpaypal: tham số thường nhậnpaypal.@Primarylà 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ậnstripedùpaypallà@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
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:
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
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:
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
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
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:
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:
namevàownerlấy từapp.properties. - Có mặc định thì dùng mặc định:
maxSize=100vì khóaapp.max-sizekhô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ớiPlaceholderResolutionExceptionnê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 định | Lựa chọn | Đánh đổi |
|---|---|---|
| Kiểu tiêm | Constructor | Constructor 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 định | Phải đặt tên bean hoặc đánh dấu rõ ràng |
| Phụ thuộc không bắt buộc | Optional hoặc ObjectProvider | Đối tượng phải xử lý trường hợp thiếu |
| Nhiều cài đặt cùng kiểu | Tiêm List hoặc Map | Thứ 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ặt | Cấ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ứng | Kiểm tra đầu tiên | Nguyên nhân hay gặp | Cách xử lý |
|---|---|---|---|
NullPointerException trong bài kiểm thử đơn vị | Phụ thuộc được tiêm kiểu nào | Field hoặc setter injection, tự new | Chuyển sang constructor injection |
expected single matching bean but found 2 | Danh sách ứng viên trong thông báo | Thê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êm | Có @Primary và @Qualifier cùng mặt không | Quê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ên | Thông báo kiểu bean thiếu | Bean thực sự tùy chọn | Optional 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ông | Placeholder không có giá trị trên Spring trần | Thêm PropertySourcesPlaceholderConfigurer hoặc dùng Boot |
| Danh sách bean sai thứ tự | @Order trên bean | Thiếu hoặc nhầm @Order | Đặt @Order rõ ràng |
Lỗi thường gặp khi chạy lab
| Triệu chứng | Nguyên nhân | Cá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 main | Chạy đúng lệnh ValueDemo ở trên, đọc cả ba dòng |
mvn không tìm thấy lớp chính | Quên compile ở lệnh đầu tiên sau khi sửa code | Chạ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.
- 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. - Đổi thứ tự. Trong
CollectionDemohoán đổi hai số@Order(stripe thành 2, paypal thành 1). Tiêu chí: danh sách đổi thành[paypal, stripe]. - Cấu hình thêm khóa. Thêm dòng
app.max-size=5vàoapp.propertiesvà chạyValueDemo. Tiêu chí:maxSize=5. - Làm cho Primary thắng. Trong
ResolutionDemo.PrimaryConfig, bỏ@Qualifier("stripe")khỏi tham số của phương thức@BeanqualifiedConsumer, và sửamainchỉ còn gọiqualifierBeatsPrimary()(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 inpaypal(nhãn của dòng vẫn nhắc@Qualifier, hãy bỏ qua nhãn); giải thích vì sao. - 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ộtPaymentServicegiả (lớp ẩn danh trả"fake") và kiểm traplace(). Tiêu chí: test xanh, không có container, không cóNullPointerException. - 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 testchoTests 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.
- "
@Primaryluôn thắng." Một@Qualifiertường minh ở nơi tiêm thắng@Primary. - "
@Valuethiế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
@Valuekhi 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à@Autowiredtrên tham số của@Beanở các cấu hình lồng phức tạp.
Tự kiểm tra
- Vì sao
new SetterOrder().place()némNullPointerExceptioncònnew ConstructorOrder()không biên dịch được nếu thiếu tham số? - Container làm gì khi gặp hai bean cùng kiểu mà không có gợi ý nào?
- Bean
paypalcó@Primaryvà tham số có@Qualifier("stripe"): bean nào được tiêm? - Ba cách khai báo phụ thuộc tùy chọn là gì?
@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;
@Qualifierchỉ rõ tại chỗ,@Primarylà mặc định chung, và@Qualifierthắng. OptionalvàObjectProvidercho phụ thuộc tùy chọn;ListvàMapcho mọi cài đặt, thứ tự theo@Order.@Valuethiế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
- Spring Framework Reference: Using @Autowired
- Spring Framework Reference: Fine-tuning Annotation-based Autowiring with @Primary or @Fallback
- Spring Framework Reference: Using @Value
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.