Bean Scopes (Kapsamlar)
Giriş
Spring'de bir bean oluşturulduğunda, o bean'in ne kadar yaşayacağı ve kimlerle paylaşılacağı scope (kapsam) ile belirlenir. Varsayılan olarak tüm bean'ler singleton'dır — yani container'da tek bir instance vardır ve herkes aynı nesneyi kullanır. Ama her senaryo singleton'a uygun değildir.
Gerçek Dünya Analojisi
Bir ofisi düşünün:
Singleton = Ofisteki yazıcı. Herkes aynı yazıcıyı kullanır. Bir tane yeterli, herkes sırayla.
Prototype = Post-it kağıt bloğu. Herkes kendi bloğunu alır, birbirinin notlarını görmez.
Request = Toplantı odası. Her toplantı (HTTP request) için ayrı oda tahsis edilir, toplantı bitince oda boşalır.
Session = Kişisel çekmece. Her çalışanın (kullanıcı oturumu) kendi çekmecesi var, şirket terk edilene kadar kullanılır.
Neden Scope Önemli?
Yanlış scope seçimi ciddi hatalara yol açar:
Singleton bean'de mutable state → race condition (veri yarışı)
Prototype olması gereken bean singleton yapılırsa → veri kirlenmesi
Her request'te yeni instance gereken bean singleton yapılırsa → kullanıcılar arası veri sızıntısı
Singleton Scope — Varsayılan ve En Yaygın
Container'da bean'in tek bir instance'ı vardır. Tüm injection noktalarına aynı nesne verilir:
@Service // Varsayılan scope: singleton
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findById(Long id) {
return userRepository.findById(id).orElseThrow();
}
}Bu UserService tüm uygulama ömrü boyunca tek instance olarak yaşar. 100 controller, 50 diğer service — hepsi aynı UserService nesnesini paylaşır.
Singleton ve Thread Safety
Singleton bean'ler birden fazla thread tarafından eşzamanlı kullanılır. Bu yüzden mutable state tutmak tehlikelidir:
// ❌ TEHLİKELİ — Singleton'da mutable state
@Service
public class CounterService {
private int count = 0; // Tüm thread'ler aynı değişkeni paylaşır
public int increment() {
return ++count; // Race condition! 2 thread aynı anda artırabilir
}
}
// ❌ TEHLİKELİ — Kullanıcıya özel veri singleton'da
@Service
public class CartService {
private List<Item> items = new ArrayList<>(); // Herkesin sepeti aynı!
public void addItem(Item item) {
items.add(item); // Ali'nin eklediğini Ayşe de görür!
}
}
// ✅ GÜVENLİ — Stateless (durumsuz) singleton
@Service
public class PricingService {
private final BigDecimal taxRate = new BigDecimal("0.20"); // immutable, final
public BigDecimal calculatePrice(BigDecimal base) {
return base.add(base.multiply(taxRate)); // Her çağrı bağımsız
}
}💡 İpucu: Singleton bean'lerde asla mutable instance variable tutmayın. Tüm veriler metot parametreleri veya local variable'lar üzerinden akmalıdır. Singleton = stateless.
Prototype Scope — Her Seferinde Yeni Instance
Her injection noktasında veya her getBean() çağrısında yeni bir instance oluşturulur:
@Component
@Scope("prototype") // veya @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
private final String cartId = UUID.randomUUID().toString();
public void addItem(CartItem item) {
items.add(item);
}
public List<CartItem> getItems() {
return Collections.unmodifiableList(items);
}
public BigDecimal getTotal() {
return items.stream()
.map(CartItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
public String getCartId() {
return cartId;
}
}Prototype Dikkat Noktaları
⚠️ Dikkat 1: Spring, prototype bean'lerin @PreDestroy metodunu çağırmaz! Bean oluşturulduktan sonra Spring onu "unutur". Yaşam döngüsü yönetimi sizin sorumluluğunuzdadır.
@Component
@Scope("prototype")
public class TempFileHandler {
private File tempFile;
@PostConstruct
public void init() {
tempFile = File.createTempFile("upload_", ".tmp");
}
@PreDestroy
public void cleanup() {
tempFile.delete(); // ⚠️ BU ASLA ÇAĞRILMAZ!
}
}⚠️ Dikkat 2: Prototype bean'i singleton'a inject ettiğinizde, prototype bir kez oluşturulur ve singleton onu sonsuza dek kullanır — yani prototype de fiilen singleton olur:
@Service // Singleton
public class OrderService {
private final ShoppingCart cart; // Prototype
public OrderService(ShoppingCart cart) {
// ⚠️ Bu cart bir kez oluşturulup sonsuza dek kullanılır!
// Her request'te yeni cart OLUŞMAZ!
this.cart = cart;
}
}Çözüm: Scope Proxy veya ObjectFactory/Provider kullanın.
Scope Proxy — Farklı Scope'ları Birlikte Çalıştırma
Singleton bean'e kısa ömürlü (prototype, request, session) bir bean enjekte etmek istediğinizde proxy kullanmanız gerekir:
// Proxy ile — her erişimde yeni prototype instance
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
private final List<CartItem> items = new ArrayList<>();
// ...
}
@Service // Singleton
public class OrderService {
private final ShoppingCart cart; // Proxy nesnesi enjekte edilir
public OrderService(ShoppingCart cart) {
this.cart = cart;
// Bu cart aslında bir PROXY
// Her cart.addItem() çağrısında proxy, yeni prototype instance alır
}
public void addToCart(CartItem item) {
cart.addItem(item); // Her çağrıda gerçek prototype'a yönlendirilir
}
}ObjectFactory / ObjectProvider Alternatifi
@Service
public class OrderService {
private final ObjectFactory<ShoppingCart> cartFactory;
public OrderService(ObjectFactory<ShoppingCart> cartFactory) {
this.cartFactory = cartFactory;
}
public void processOrder() {
ShoppingCart cart = cartFactory.getObject(); // Her çağrıda yeni instance
cart.addItem(new CartItem("Laptop", 1, new BigDecimal("15000")));
// İşlem bittikten sonra cart garbage collect olur
}
}
// veya Provider kullanarak
@Service
public class OrderService {
private final Provider<ShoppingCart> cartProvider; // Jakarta inject Provider
public OrderService(Provider<ShoppingCart> cartProvider) {
this.cartProvider = cartProvider;
}
public void processOrder() {
ShoppingCart cart = cartProvider.get(); // Her çağrıda yeni instance
}
}Web Scopes — HTTP'ye Özel Kapsamlar
Web uygulamalarında kullanılan özel scope'lar. spring-boot-starter-web dependency'si gerektirir.
Request Scope — Her HTTP İsteğinde Yeni
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String correlationId = UUID.randomUUID().toString();
private Instant startTime = Instant.now();
private String clientIp;
private String userId;
public String getCorrelationId() { return correlationId; }
public Instant getStartTime() { return startTime; }
public void setClientIp(String ip) { this.clientIp = ip; }
public String getClientIp() { return clientIp; }
public void setUserId(String userId) { this.userId = userId; }
public String getUserId() { return userId; }
public long getElapsedMs() {
return Duration.between(startTime, Instant.now()).toMillis();
}
}// Kullanım — her request kendi RequestContext'ini alır
@RestController
@RequiredArgsConstructor
public class ApiController {
private final RequestContext requestContext; // Proxy enjekte edilir
private final UserService userService;
@GetMapping("/api/data")
public ResponseEntity<DataDto> getData() {
System.out.println("Request ID: " + requestContext.getCorrelationId());
System.out.println("Elapsed: " + requestContext.getElapsedMs() + "ms");
// Her HTTP request için farklı correlationId
return ResponseEntity.ok(userService.getData());
}
}Session Scope — Kullanıcı Oturumu Boyunca
@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserPreferences {
private String language = "tr";
private String theme = "light";
private String currency = "TRY";
private int pageSize = 20;
// Getter/Setter
public String getLanguage() { return language; }
public void setLanguage(String language) { this.language = language; }
public String getTheme() { return theme; }
public void setTheme(String theme) { this.theme = theme; }
}Application Scope
Tüm ServletContext boyunca tek instance (singleton'a benzer ama servlet context başına):
@Component
@Scope("application")
public class AppMetrics {
private final AtomicLong totalRequests = new AtomicLong(0);
private final AtomicLong totalErrors = new AtomicLong(0);
public void incrementRequests() { totalRequests.incrementAndGet(); }
public void incrementErrors() { totalErrors.incrementAndGet(); }
public long getTotalRequests() { return totalRequests.get(); }
public long getTotalErrors() { return totalErrors.get(); }
}Scope Karşılaştırma Tablosu
| Scope | Instance Sayısı | Yaşam Süresi | Kullanım Alanı | @PreDestroy |
|---|---|---|---|---|
| singleton | 1 | Container ömrü | Stateless servisler | ✅ Çalışır |
| prototype | Sınırsız | Kullanıcıya bağlı | Stateful nesneler | ❌ Çalışmaz |
| request | İstek başına 1 | HTTP isteği | İstek bazlı context | ✅ Çalışır |
| session | Session başına 1 | Kullanıcı oturumu | Kullanıcı tercihleri | ✅ Çalışır |
| application | 1 | ServletContext | Uygulama metrikleri | ✅ Çalışır |
| websocket | WS session başına 1 | WebSocket bağlantısı | WS oturum verisi | ✅ Çalışır |
Yaygın Hatalar ve Çözümleri
Hata 1: Singleton'da Mutable State
// ❌ Thread-unsafe singleton
@Service
public class ReportService {
private List<String> currentReport = new ArrayList<>(); // Paylaşılıyor!
public void addLine(String line) {
currentReport.add(line); // 2 kullanıcı aynı anda eklerse → veri karışır
}
}
// ✅ Stateless singleton
@Service
public class ReportService {
public Report generate(ReportRequest request) {
List<String> lines = new ArrayList<>(); // Local variable — thread-safe
lines.add("Header: " + request.getTitle());
// ...
return new Report(lines);
}
}Hata 2: Prototype Bean'i Singleton'a Proxy'siz Inject Etmek
// ❌ Prototype fiilen singleton gibi davranır
@Component
@Scope("prototype")
public class TempProcessor { }
@Service
public class ProcessService {
private final TempProcessor processor; // Bir kez oluşturulur, hep aynı!
public ProcessService(TempProcessor processor) {
this.processor = processor;
}
}
// ✅ Scope proxy veya ObjectFactory kullanın
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class TempProcessor { }Hata 3: Web Scope Bean'i Test'te Kullanmak
// ❌ Web scope bean'i unit test'te çalışmaz
// (active HTTP request yok)
@Autowired
private RequestContext requestContext; // Hata!
// ✅ Test'te mock scope kullanın
@TestConfiguration
public class TestScopeConfig {
@Bean
@Primary
public RequestContext testRequestContext() {
return new RequestContext(); // Normal nesne
}
}Scope Seçim Rehberi — Karar Tablosu
Hangi scope'u ne zaman kullanmalısınız? Aşağıdaki tablo pratik kılavuzdur:
| Senaryo | Scope | Neden |
|---|---|---|
| Service katmanı (UserService, OrderService) | Singleton | Stateless — state tutmaz, thread-safe |
| Repository katmanı | Singleton | JPA EntityManager zaten thread-safe proxy |
| Controller | Singleton | Request bilgisi method parametrelerinde gelir |
| Geçici hesaplama nesnesi (ReportBuilder) | Prototype | Her kullanımda sıfır state ile başlamalı |
| Request bazlı audit context | Request | Hangi kullanıcı, hangi IP, hangi endpoint |
| Kullanıcı tercihleri (tema, dil, sepet) | Session | Oturum boyunca korunmalı |
| WebSocket bağlantı state'i | WebSocket | Her WebSocket session için ayrı |
@Scope ile @Bean Kullanımı
@Component tabanlı tarama yerine @Configuration sınıfında @Bean ile de scope belirlenebilir:
@Configuration
public class ProcessorConfig {
// Her injection'da yeni instance
@Bean
@Scope("prototype")
public ReportBuilder reportBuilder() {
return new ReportBuilder();
}
// Her HTTP request'te yeni instance
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public AuditContext auditContext() {
return new AuditContext();
}
}Bu yaklaşım özellikle 3rd party kütüphane sınıflarına scope vermek istediğinizde kullanışlıdır — onlara @Component ekleyemezsiniz çünkü kaynak kodları sizde değildir.
Thread Safety ve Scope İlişkisi
// ❌ Singleton bean'de mutable field — RACE CONDITION
@Service
public class CounterService {
private int count = 0; // Tüm thread'ler aynı değişkeni paylaşır!
public int increment() {
return ++count; // Thread-safe DEĞİL
}
}
// ✅ Seçenek 1: AtomicInteger ile thread-safe
@Service
public class CounterService {
private final AtomicInteger count = new AtomicInteger(0);
public int increment() {
return count.incrementAndGet(); // Thread-safe
}
}
// ✅ Seçenek 2: Prototype scope — her çağıran kendi instance'ını alır
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CounterService {
private int count = 0;
public int increment() {
return ++count; // Her instance bağımsız, thread-safe
}
}Kural: Singleton scope'ta mutable instance variable tutmaktan kaçının. Ya
AtomicInteger/ConcurrentHashMapgibi thread-safe yapılar kullanın, ya da scope'u değiştirin. Ancak %95+ durumda stateless singleton en iyi seçenektir.
Application Scope (Servlet Context)
Nadir kullanılan bir scope daha vardır: application. Bu scope, ServletContext ömrü boyunca tek instance tutar — singleton'a benzer ama farkı, Spring container yerine Servlet container yaşam döngüsüne bağlı olmasıdır:
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class GlobalRateLimiter {
private final Map<String, AtomicInteger> counters = new ConcurrentHashMap<>();
public boolean isAllowed(String clientId, int maxPerMinute) {
AtomicInteger counter = counters.computeIfAbsent(
clientId, k -> new AtomicInteger(0)
);
return counter.incrementAndGet() <= maxPerMinute;
}
}Custom Scope Oluşturma
Spring'in yerleşik scope'ları yetmezse kendi scope'unuzu tanımlayabilirsiniz:
public class TenantScope implements Scope {
private final ThreadLocal<Map<String, Object>> scopedObjects =
ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = scopedObjects.get();
return scope.computeIfAbsent(name, k -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return scopedObjects.get().remove(name);
}
@Override
public String getConversationId() {
return TenantContext.getCurrentTenant();
}
// registerDestructionCallback, resolveContextualObject — genelde no-op
}
// Custom scope'u kaydedin
@Configuration
public class ScopeConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer configurer = new CustomScopeConfigurer();
configurer.addScope("tenant", new TenantScope());
return configurer;
}
}
// Kullanım
@Component
@Scope("tenant")
public class TenantCacheManager {
// Her tenant için ayrı cache instance
}Multi-tenant uygulamalarda bu yaklaşım çok güçlüdür — her kiracı (tenant) kendi bean instance'larını alır.
Özet
Singleton (varsayılan) → %95 durumda yeterli, stateless servisler için
Prototype → her kullanımda yeni instance, stateful nesneler için —
@PreDestroyçalışmaz!Request → HTTP isteği başına bir instance — request bazlı context bilgisi
Session → kullanıcı oturumu boyunca — tercihler, sepet (geleneksel web app)
Application → ServletContext ömrü boyunca — global limiter gibi nadir senaryolar
Singleton'a kısa ömürlü bean inject ederken proxyMode veya ObjectFactory kullanın
Singleton bean'de mutable state tutmayın — race condition kaçınılmaz
Custom scope ile tenant, conversation veya workflow bazlı bean yönetimi mümkün
@Bean+@Scopeile 3rd party sınıflara da scope verilebilirThread safety singleton scope'un en kritik konusu —
AtomicInteger,ConcurrentHashMapkullanınScope seçiminde şüpheye düştüğünüzde singleton ile başlayın; gerçekten state tutmanız gerektiğinde prototype veya request'e geçin
AI Asistan
Sorularını yanıtlamaya hazır