Spring Dependency Injection Püf Noktaları

3 Haziran 2026 · netologist · 6 dakika, 1093 kelime ·

Not: “Spring 4.1” ifadesi Spring Boot 4.1.0 (Haziran 2026, Spring Framework 7.0.8 tabanlı, JDK 25 tam destekli) olarak yorumlanmıştır.

1. Constructor Injection’ı Varsayılan Yapın

Zorunlu bağımlılıklar için her zaman constructor injection kullanın. Tek constructor’lı sınıflarda @Autowired gereksizdir.

@Service
public class OrderService {
    private final PaymentClient paymentClient;
    public OrderService(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }
}

2. Opsiyonel Tekil Bağımlılık için Optional<T>

@Autowired(required=false) yerine constructor parametresinde Optional<T> kullanın; Spring bean bulamazsa Optional.empty() enjekte eder.

public NotificationService(Optional<SmsGateway> smsGateway) {
    this.smsGateway = smsGateway;
}

3. Lazy / Çoklu Bean Senaryoları için ObjectProvider<T>

Bean’in gerçekten kullanılana kadar resolve edilmesini istediğinizde veya birden fazla aday olabileceğinde:

public ReportGenerator(ObjectProvider<PdfExporter> provider) {
    this.exporter = provider.getIfAvailable(DefaultPdfExporter::new);
}

4. Çoklu Implementasyonları List<T> ile Toplu Enjekte Edin

Strategy pattern uygularken tüm implementasyonları liste olarak alın, sıralama için @Order kullanın.

@Order(1) @Component class CreditCardValidator implements PaymentValidator {}
@Order(2) @Component class FraudCheckValidator implements PaymentValidator {}

@Service
public class PaymentPipeline {
    private final List<PaymentValidator> validators;
    public PaymentPipeline(List<PaymentValidator> validators) {
        this.validators = validators; // Spring @Order sırasına göre enjekte eder
    }
}

5. İsimlendirilmiş Bean Koleksiyonları için Map<String, T>

Bean adına göre ayrım gereken durumlarda (örn. birden çok CacheManager):

public CacheRouter(Map<String, CacheManager> cacheManagers) {
    this.redisCache = cacheManagers.get("redisCacheManager");
}

6. Belirsizlik Varsa @Qualifier veya @Primary Kullanın

Aynı tipten birden fazla bean varsa hangisinin enjekte edileceğini açıkça belirtin.

@Service
public class ShippingService {
    public ShippingService(@Qualifier("fedexClient") CourierClient client) { ... }
}

7. @Autowired(required = false) Kullanımından Tamamen Kaçının

Field/setter injection’a dayandığı ve tip güvenliğini bozduğu için deprecated pratik olarak görün; Optional<T>‘a migrate edin.

// ❌ Kaçının
@Autowired(required = false)
private MetricsCollector metricsCollector;

// ✅ Tercih edin
public MyService(Optional<MetricsCollector> metricsCollector) { ... }

8. Immutability için Alanları final Yapın

Constructor injection’ı final field’larla birleştirerek nesnenin oluşturulduktan sonra değişmemesini garanti edin.

private final InventoryClient inventoryClient; // final => thread-safe, test edilebilir

9. Circular Dependency’yi @Lazy ile Değil, Tasarımla Çözün

@Lazy bir kaçış yoludur, kök sebep genelde kötü sorumluluk ayrımıdır. Önce servisleri ayırmayı deneyin.

// Geçici çözüm (önerilmez, sadece son çare):
public ServiceA(@Lazy ServiceB serviceB) { this.serviceB = serviceB; }

// Doğru çözüm: ortak mantığı üçüncü bir servise çıkarın
public ServiceA(SharedLogic sharedLogic) { ... }

10. @ConfigurationProperties için Record Kullanın (Spring Boot 4.x)

JDK 25 record’ları immutable configuration binding için birinci sınıf desteklenir.

@ConfigurationProperties(prefix = "payment")
public record PaymentProperties(String apiKey, Duration timeout, boolean sandboxMode) {}

11. Null-Safety Sözleşmeleri için JSpecify @Nullable/@NonNull

Spring Framework 7.x ile gelen standart null-safety anotasyonlarını API sınırlarında (dönüş tipi, parametre) kullanın; bunu DI opsiyonelliği ile karıştırmayın.

public @Nullable Customer findByEmail(String email) { ... } // dönebilir veya null olabilir

12. Koşullu Bean Tanımları için @ConditionalOnProperty Kullanın

Manuel null-check yerine bean’in var olup olmayacağını konfigürasyon seviyesinde belirleyin.

@Bean
@ConditionalOnProperty(name = "features.sms.enabled", havingValue = "true")
public SmsGateway smsGateway() { return new TwilioSmsGateway(); }

13. Prototype Scope Bean’ler için ObjectProvider.getObject()

Singleton bir servis içinde her seferinde yeni bir prototype instance almak için ObjectProvider kullanın.

public BatchJobRunner(ObjectProvider<JobContext> jobContextProvider) {
    this.jobContextProvider = jobContextProvider;
}
public void run() {
    JobContext ctx = jobContextProvider.getObject(); // her çağrıda yeni instance
}

14. Sealed Interface + Pattern Matching ile DI Dostu Tasarım (JDK 25)

Strategy/Visitor pattern’lerde sealed interface kullanarak derleme zamanında eksiksizlik garantisi alın; implementasyonlar yine constructor injection ile bağlanır.

public sealed interface DiscountPolicy permits PercentageDiscount, FlatDiscount {}
public record PercentageDiscount(double rate) implements DiscountPolicy {}
public record FlatDiscount(BigDecimal amount) implements DiscountPolicy {}

BigDecimal apply(DiscountPolicy policy, BigDecimal price) {
    return switch (policy) {
        case PercentageDiscount p -> price.multiply(BigDecimal.valueOf(1 - p.rate()));
        case FlatDiscount f -> price.subtract(f.amount());
    };
}

15. Virtual Thread Tabanlı TaskExecutor‘ı Opsiyonel Enjekte Edin

Spring Framework 7.x’in native virtual thread desteğini, executor bean’i olmayan ortamlarda düşmeyecek şekilde Optional/ObjectProvider ile alın.

@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
    return new VirtualThreadTaskExecutor("app-vt-");
}

public JobDispatcher(Optional<AsyncTaskExecutor> executor) {
    this.executor = executor.orElseGet(SyncTaskExecutor::new);
}

16. Constructor Injection’ı Test Edilebilirlik için Kullanın

Constructor injection sayesinde Spring context’i başlatmadan, sade new ile birim test yazabilirsiniz.

@Test
void discountAppliedCorrectly() {
    OrderService service = new OrderService(new FakePaymentClient()); // Spring gerekmiyor
    assertEquals(..., service.process(order));
}

17. Manuel new ile Bean Oluşturmaktan Kaçının (Bean Lifecycle Bütünlüğü)

Servis içinde başka bir Spring-managed bean’i new ile oluşturmak, AOP proxy’lerini, transaction yönetimini ve DI grafiğini bozar.

// ❌ Kaçının
PaymentClient client = new StripePaymentClient(); // @Transactional, proxy vs. çalışmaz

// ✅ DI'a bırakın
public OrderService(PaymentClient client) { this.client = client; }

18. Field Injection’ı Statik Analiz Kurallarıyla (ArchUnit / Checkstyle) Engelleyin

Ekip disiplinini korumak için @Autowired field kullanımını CI/CD pipeline’ında otomatik reddedin.

@ArchTest
static final ArchRule noFieldInjection = noFields()
    .that().areDeclaredInClassesThat().resideInAPackage("..service..")
    .should().beAnnotatedWith(Autowired.class);

19. @Primary ile Varsayılan Implementasyonu Netleştirin

Birden fazla bean varsa ve çoğu enjeksiyon noktasında biri varsayılan olacaksa @Qualifier yaymak yerine @Primary kullanın.

@Primary
@Service
public class DefaultPricingEngine implements PricingEngine { ... }

20. Structured Concurrency ile Enjekte Edilmiş Client’ları Paralel Çağırın (JDK 25)

JDK 25’in structured concurrency API’si ile bir servis içinde birden fazla enjekte edilmiş client’ı paralel çağırın; her client hâlâ constructor injection ile gelir.

public class DashboardService {
    private final OrdersClient ordersClient;
    private final InventoryClient inventoryClient;

    public DashboardService(OrdersClient ordersClient, InventoryClient inventoryClient) {
        this.ordersClient = ordersClient;
        this.inventoryClient = inventoryClient;
    }

    public Dashboard load() throws Exception {
        try (var scope = StructuredTaskScope.open()) {
            var orders = scope.fork(ordersClient::fetchAll);
            var stock = scope.fork(inventoryClient::fetchAll);
            scope.join();
            return new Dashboard(orders.get(), stock.get());
        }
    }
}

21. Record DTO’ları DI ile Taşınan Servislerde Value Object Olarak Kullanın

Enjekte edilen servislerin metod imzalarında mutable class yerine record kullanarak yan etkisiz, immutable veri akışı sağlayın.

public record OrderSummary(String orderId, BigDecimal total, Instant createdAt) {}

public OrderSummary summarize(Order order) { // servis enjekte edilmiş, dönüş tipi record
    return new OrderSummary(order.id(), order.total(), order.createdAt());
}

22. Setter Injection’ı Yalnızca Framework Zorunluluğu Varsa Kullanın

Bazı legacy entegrasyonlar (örn. JAX-WS, bazı test framework’leri) setter injection gerektirebilir; bunun dışında constructor injection’dan sapmayın.

// Sadece framework zorunlu kılıyorsa:
@Autowired
public void setLegacyHandler(LegacyHandler handler) { this.handler = handler; }

Özet Tablo

#PratikNe Zaman Kullanılır
1Constructor injectionHer zaman, zorunlu bağımlılıklar
2Optional<T>Tekil opsiyonel bağımlılık
3ObjectProvider<T>Lazy / çoklu / prototype senaryolar
4List<T> + @OrderStrategy pattern, çoklu implementasyon
5Map<String,T>İsimlendirilmiş bean seçimi
6@Qualifier/@PrimaryBelirsizlik giderme
7Optional<T>‘a migrate@Autowired(required=false) yerine
8final fieldImmutability
9Tasarımla çözümCircular dependency
10Record + @ConfigurationPropertiesImmutable config
11JSpecify @NullableAPI null-safety sözleşmesi
12@ConditionalOnPropertyKoşullu bean varlığı
13ObjectProvider.getObject()Prototype scope
14Sealed interface + switchDerleme zamanı eksiksizlik
15Virtual thread executorPerformans/eşzamanlılık
16Plain new ile testTestability
17DI’a bırak, manuel new yapmaBean lifecycle bütünlüğü
18ArchUnit/Checkstyle kurallarıEkip disiplini
19@PrimaryVarsayılan implementasyon
20Structured concurrencyParalel bağımlılık çağrıları
21Record DTO/Value ObjectImmutable veri taşıma
22Setter injection (istisna)Framework zorunluluğu

Kaynaklar: Spring Framework 7.0 GA duyurusu (spring.io), Spring Boot/Framework resmi dokümantasyonu ve sürüm notları (Haziran 2026 itibarıyla güncel), OpenJDK JEP notları (Records, Structured Concurrency, Sealed Classes).