Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

dependency injection

Tung Bui Thanh (TungDaDev)11 mins read

Spring dependency injection.webp

# giới thiệu

Dependency Injection (DI) là pattern mà class không tự tạo dependency bên trong, mà nhận từ bên ngoài vào. Nghe thì đơn giản, nhưng nó đổi hẳn cách bạn viết và test code.

Không có DI thì OrderService tự new OrderRepository(): dính chặt (tight coupling), không mock được, muốn đổi implementation cũng không được. Có DI thì Spring "tiêm" repository vào service, các class tách rời nhau, dễ test, dễ cấu hình.

Package org.springframework.beans.factory.annotation chứa mấy annotation quyết định Spring inject thế nào: inject cái gì (@Autowired), chọn bean nào (@Qualifier, @Primary), lấy config ở đâu (@Value). Dev Spring nào cũng đụng tới chúng hằng ngày.

# @Autowired: tự động inject dependency

@Autowired bảo Spring: "Tìm bean hợp trong container rồi inject vào đây." Spring so theo type trước, có nhiều bean trùng type thì mới xét tới name.

Có 3 cách inject: constructor, field, setter. Nhưng với code production thì gần như chỉ nên dùng constructor injection, vì:

  • Field là final → immutable, thread-safe
  • Dependency rõ ràng (nhìn constructor là biết class cần gì)
  • Dễ test (truyền mock qua constructor, không cần Spring context)
  • Fail fast: thiếu bean là app báo lỗi ngay lúc start (UnsatisfiedDependencyException), chứ không đợi tới lúc gọi method mới văng NullPointerException. Còn trong unit test, quên truyền dependency vào constructor thì compiler báo luôn

Xài Lombok @RequiredArgsConstructor thì khỏi viết constructor luôn, Lombok tự generate từ các field final.

@Service
public class OrderService {
 
   private final OrderRepository orderRepository;
   private final ProductService productService;
   private final NotificationService notificationService;
 
   // @Autowired không cần khi chỉ có 1 constructor (Spring Framework 4.3+)
   public OrderService(OrderRepository orderRepository,
                       ProductService productService,
                       NotificationService notificationService) {
       this.orderRepository = orderRepository;
       this.productService = productService;
       this.notificationService = notificationService;
   }
}
 
// Lombok equivalent (RECOMMENDED trong CSP)
@Service
@RequiredArgsConstructor
public class OrderService {
   private final OrderRepository orderRepository;
   private final ProductService productService;
   private final NotificationService notificationService;
}

# field injection (nên tránh, khó test)

@Service
public class OrderService {
 
   @Autowired  // Inject trực tiếp vào field
   private OrderRepository orderRepository;
 
   @Autowired(required = false)  // Optional — null nếu bean không tồn tại
   private CacheService cacheService;
}

# setter injection (optional dependencies)

@Service
public class OrderService {
 
   private NotificationService notificationService;
 
   @Autowired(required = false)
   public void setNotificationService(NotificationService notificationService) {
       this.notificationService = notificationService;
   }
 
   public void createOrder(OrderRequest request) {
       // ... create order
       if (notificationService != null) {
           notificationService.notify(order);
       }
   }
}

# collection injection

@Service
public class NotificationDispatcher {
 
   // Inject TẤT CẢ beans implement NotificationSender
   private final List<NotificationSender> senders;
 
   @Autowired
   public NotificationDispatcher(List<NotificationSender> senders) {
       this.senders = senders;
       // senders = [EmailSender, SmsSender, PushSender] (tất cả impl)
   }
 
   public void dispatch(Notification notification) {
       senders.forEach(sender -> sender.send(notification));
   }
}
 
// Map injection — key = bean name (mặc định là tên class viết thường chữ đầu)
@Autowired
private Map<String, PaymentProcessor> processors;
// processors = {"stripeProcessor": StripeProcessor, "vnPayProcessor": VnPayProcessor}

# @Qualifier: chọn đúng bean khi có nhiều ứng viên

Container có nhiều bean cùng type (2 DataSource, 3 PaymentProcessor...) thì Spring không biết inject cái nào → NoUniqueBeanDefinitionException. @Qualifier gỡ chuyện này bằng cách chỉ đích danh tên bean.

Trong CSP, ví dụ hay gặp nhất là primary database (write) với replica database (read). Service đọc nhiều thì dùng replica, service cần ghi thì dùng primary. Cùng là type DataSource, nhưng nhờ @Qualifier mà inject đúng instance.

Xịn hơn chút thì tự tạo qualifier annotation (@PrimaryDatabase, @ReplicaDatabase): dễ đọc hơn qualifier dạng string, lại được compiler bắt lỗi gõ sai.

// Nhiều beans cùng type
@Configuration
public class DataSourceConfig {
 
   @Bean("primaryDs")
   public DataSource primaryDataSource() {
       return DataSourceBuilder.create().url("jdbc:postgresql://primary:5432/db").build();
   }
 
   @Bean("replicaDs")
   public DataSource replicaDataSource() {
       return DataSourceBuilder.create().url("jdbc:postgresql://replica:5432/db").build();
   }
}
 
// Inject với @Qualifier
// ⚠️ Lombok mặc định KHÔNG chép @Qualifier từ field sang tham số constructor.
// Cần thêm vào lombok.config:
//   lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
@Service
@RequiredArgsConstructor
public class ReportService {
 
   @Qualifier("replicaDs")  // Chọn replica cho read-heavy operations
   private final DataSource dataSource;
}
 
// Hoặc với constructor
@Service
public class ReportService {
   private final DataSource readDataSource;
 
   public ReportService(@Qualifier("replicaDs") DataSource readDataSource) {
       this.readDataSource = readDataSource;
   }
}

# custom qualifier annotation

// Tạo custom qualifier
// METHOD là bắt buộc để gắn được lên method @Bean bên dưới
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface PrimaryDatabase {}
 
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface ReplicaDatabase {}
 
// Đánh dấu beans
@Bean
@PrimaryDatabase
public DataSource primaryDs() { ... }
 
@Bean
@ReplicaDatabase
public DataSource replicaDs() { ... }
 
// Inject
@Service
public class OrderService {
   public OrderService(@PrimaryDatabase DataSource ds) { ... }
}
 
@Service
public class ReportService {
   public ReportService(@ReplicaDatabase DataSource ds) { ... }
}

# @Value: inject giá trị property

@Value là cách nhanh nhất để đưa giá trị config vào code. Đi kèm Spring Cloud Config Server (như CSP đang dùng) thì config được quản lý tập trung một chỗ. Có điều @Value chỉ được inject một lần lúc bean khởi tạo: đổi config trên server thì app vẫn giữ giá trị cũ cho tới khi restart, trừ khi bean được đánh dấu @RefreshScope và bạn gọi /actuator/refresh (hoặc dùng Spring Cloud Bus). Config nhiều trường liên quan nhau thì @ConfigurationProperties gọn và an toàn kiểu hơn là rải @Value khắp nơi.

Cú pháp ${property.name:defaultValue}: phần sau dấu : là giá trị dự phòng khi property không có. Config nào không bắt buộc thì nhớ đặt default value, kẻo app start không lên.

Dùng SpEL (Spring Expression Language) trong @Value thì làm được nhiều hơn là inject string: gọi method, tính toán, đọc system properties. Nhưng đừng lạm dụng, logic phức tạp thì để trong code, đừng nhét vào annotation.

@Service
public class FileService {
 
   @Value("${app.upload.max-size:10485760}")
   private long maxFileSize;
 
   @Value("${app.upload.path}")
   private String uploadPath;
 
   @Value("${app.upload.allowed-types:pdf,png,jpg}")
   private List<String> allowedTypes;
 
   @Value("#{${app.feature-flags:{:}}}")  // SpEL → Map (trong SpEL, {:} là map rỗng, còn {} là LIST rỗng)
   private Map<String, Boolean> featureFlags;
 
   @Value("${app.name:#{null}}")  // null default
   private String appName;
 
   // SpEL expressions
   @Value("#{systemEnvironment['JAVA_HOME']}")
   private String javaHome;
 
   @Value("#{T(java.time.LocalDate).now()}")
   private LocalDate today;
 
   @Value("#{@someBean.computeValue()}")  // Call another bean's method
   private String computedValue;
 
   // Hoặc constructor injection with @Value (chọn MỘT trong hai cách, đừng khai báo cả field lẫn constructor như minh hoạ này)
   public FileService(
           @Value("${app.upload.max-size:10485760}") long maxFileSize,
           @Value("${app.upload.path}") String uploadPath) {
       this.maxFileSize = maxFileSize;
       this.uploadPath = uploadPath;
   }
}

# @Lookup: method injection cho prototype bean

Cái này dành cho một tình huống ít người để ý: singleton bean cần một prototype bean mới tinh mỗi lần gọi.

Inject prototype vào singleton qua constructor thì prototype chỉ được tạo đúng 1 lần (lúc singleton khởi tạo), coi như mất luôn ý nghĩa prototype. Với @Lookup, Spring override method đó để mỗi lần gọi là trả về một instance mới.

Ngoài đời không gặp nhiều, nhưng có ích khi làm report builder cần giữ state, xử lý theo từng request trong singleton service, hay mấy object kiểu command pattern.

// Problem: Singleton service cần mỗi lần 1 prototype bean mới
@Service
public abstract class ReportService {
 
   // Spring override method này → trả về new prototype instance mỗi lần gọi
   @Lookup
   protected abstract ReportGenerator createGenerator();
 
   public String generateReport(ReportRequest request) {
       ReportGenerator generator = createGenerator(); // New instance mỗi lần
       generator.addHeader(request.getTitle());
       generator.addData(request.getData());
       return generator.generate();
   }
}
 
@Component
@Scope("prototype")
public class ReportGenerator {
   private final StringBuilder content = new StringBuilder();
 
   public void addHeader(String title) { content.append("# ").append(title).append("\n"); }
   public void addData(Object data) { content.append(data.toString()); }
   public String generate() { return content.toString(); }
}

# @Primary: bean mặc định khi có nhiều ứng viên

@Primary là kiểu "mặc định mềm": không ghi @Qualifier thì Spring lấy bean có @Primary. Còn khi cần bean khác thì @Qualifier luôn thắng @Primary.

Cách dùng hay gặp: Redis cache để @Primary (dùng mặc định), local cache làm phương án phụ (cho test hoặc fallback). Hoặc DataSource production để @Primary, còn DataSource test thì gọi bằng @Qualifier("testDb").

@Configuration
public class CacheConfig {
 
   @Bean
   @Primary  // Default khi inject CacheManager không chỉ định @Qualifier
   public CacheManager redisCacheManager(RedisConnectionFactory factory) {
       return RedisCacheManager.builder(factory).build();
   }
 
   @Bean("localCache")
   public CacheManager localCacheManager() {
       return new ConcurrentMapCacheManager("products", "users");
   }
}
 
// Inject — không cần @Qualifier, tự động chọn @Primary
@Service
@RequiredArgsConstructor
public class ProductService {
   private final CacheManager cacheManager; // → redisCacheManager
}
 
// Override khi cần
@Service
public class TestService {
   public TestService(@Qualifier("localCache") CacheManager cache) { ... }
}

# @Required (deprecated) & alternatives

@Required hồi trước gắn lên setter để báo "property này bắt buộc phải inject". Từ Spring 5.1 nó bị deprecated, và tới Spring Framework 6.0 thì bị xoá hẳn (trong spring-beans 6.x không còn class này), vì constructor injection làm việc này gọn hơn: thiếu dependency là app không start được.

Rút ra: constructor injection không chỉ là "best practice", nó còn khiến mấy annotation kiểu này trở nên thừa.

// @Required deprecated từ Spring 5.1, bị xoá ở Spring 6.0 — dùng constructor injection thay thế
 
// ❌ Old way
@Component
public class OldService {
   private Repository repo;
 
   @Required  // Deprecated (5.1), không còn ở Spring 6+
   public void setRepo(Repository repo) { this.repo = repo; }
}
 
// ✅ Modern way — constructor injection (required by default)
@Service
@RequiredArgsConstructor
public class ModernService {
   private final Repository repo;  // Bắt buộc phải inject
}

# objectprovider & lazy resolution

ObjectProvider<T> là cách "an toàn" để inject một bean có thể không tồn tại. @Autowired(required=false) thì trả null (dễ dính NPE), còn ObjectProvider có API rõ ràng: getIfAvailable(), getIfUnique(), stream().

Mấy chỗ hay dùng:

  • Kiến trúc plugin: quét hết các implementation rồi xử lý động
  • Tính năng tuỳ chọn: bean không có thì service vẫn chạy bình thường
  • Phá circular dependency: lazy resolution để khỏi inject vòng tròn
@Service
public class FlexibleService {
 
   private final ObjectProvider<ExpensiveService> expensiveServiceProvider;
   private final ObjectProvider<List<Plugin>> pluginsProvider;
 
   @Autowired
   public FlexibleService(
           ObjectProvider<ExpensiveService> expensiveServiceProvider,
           ObjectProvider<List<Plugin>> pluginsProvider) {
       this.expensiveServiceProvider = expensiveServiceProvider;
       this.pluginsProvider = pluginsProvider;
   }
 
   public void doWork() {
       // Lazy resolution — chỉ tạo khi cần
       ExpensiveService service = expensiveServiceProvider.getIfAvailable();
       if (service != null) {
           service.process();
       }
 
       // Với default fallback
       ExpensiveService serviceOrDefault = expensiveServiceProvider
           .getIfAvailable(NoOpExpensiveService::new);
 
       // Stream tất cả beans
       expensiveServiceProvider.stream().forEach(s -> s.process());
 
       // Ordered
       expensiveServiceProvider.orderedStream().forEach(s -> s.process());
   }
}

# injection patterns: vài cách dùng hay

Tới đây thì mấy annotation DI bắt đầu thành design pattern. Bạn không chỉ inject dependency nữa, mà đang thiết kế cách các component nói chuyện với nhau qua DI.

Strategy Pattern qua DI là ví dụ quen thuộc: khai báo interface, viết nhiều implementation, inject List<Interface> → runtime tự chọn. Không cần factory class, không cần switch/case, Spring gom hết implementation cho bạn.

# pattern 1: strategy pattern via di

public interface PaymentProcessor {
   boolean supports(PaymentMethod method);
   PaymentResult process(PaymentRequest request);
}
 
@Component
public class StripeProcessor implements PaymentProcessor {
   public boolean supports(PaymentMethod m) { return m == PaymentMethod.CREDIT_CARD; }
   public PaymentResult process(PaymentRequest req) { ... }
}
 
@Component
public class VnPayProcessor implements PaymentProcessor {
   public boolean supports(PaymentMethod m) { return m == PaymentMethod.BANK_TRANSFER; }
   public PaymentResult process(PaymentRequest req) { ... }
}
 
@Service
@RequiredArgsConstructor
public class PaymentService {
   private final List<PaymentProcessor> processors; // Auto-inject all implementations
 
   public PaymentResult pay(PaymentRequest request) {
       return processors.stream()
           .filter(p -> p.supports(request.getMethod()))
           .findFirst()
           .orElseThrow(() -> new UnsupportedPaymentException(request.getMethod()))
           .process(request);
   }
}

# pattern 2: conditional dependencies

@Service
public class NotificationService {
 
   private final Optional<SmsGateway> smsGateway;       // May not exist
   private final Optional<PushService> pushService;     // May not exist
   private final EmailService emailService;             // Always exists
 
   public NotificationService(
           EmailService emailService,
           Optional<SmsGateway> smsGateway,
           Optional<PushService> pushService) {
       this.emailService = emailService;
       this.smsGateway = smsGateway;
       this.pushService = pushService;
   }
 
   public void notify(User user, String message) {
       emailService.send(user.getEmail(), message);
       smsGateway.ifPresent(gw -> gw.send(user.getPhone(), message));
       pushService.ifPresent(ps -> ps.send(user.getDeviceToken(), message));
   }
}

# quick reference

AnnotationMục đíchKhi nào dùng
@AutowiredAuto inject dependencySetter/field injection (prefer constructor)
@QualifierChọn bean cụ thểNhiều beans cùng type
@PrimaryDefault bean1 bean ưu tiên hơn
@ValueInject propertyConfig values, SpEL
@LookupMethod injectionSingleton cần prototype
@RequiredBắt buộc inject (đã xoá ở 6)Dùng constructor thay thế

# so sánh injection types

TypeProsConsKhi nào
ConstructorImmutable, testable, required depsVerbose (Lombok fix)DEFAULT, luôn dùng
SetterOptional depsMutable, hiddenOptional dependencies
FieldConciseUntestable, hidden, mutableTRÁNH, chỉ cho test class

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.