Spring Framework uygulamalarında request kimliği, güvenlik bağlamı ve trace bilgisini async ve reactive akışlarda kaybetmeden taşıyın. Java backend geliştirme için MDC, Reactor Context, JPA sınırları ve ölçüm adımlarını inceleyin.
Spring Framework'te Async Context Propagation ve MDC İzlenebilirliği
Java backend geliştirme için context sözleşmesini request sınırında kurun
java eğitimi ve java programlama eğitimi içeriklerinde ThreadLocal genellikle request boyunca sabitmiş gibi anlatılır; gerçekte Servlet işçi thread'i sadece controller çağrısının sahibidir. java backend geliştirme servisinde ilk yapılacak iş, dışarıdan gelen kimliği güvenilir kabul etmeden sınırda üretmek ve MDC'yi mutlaka temizlemektir. Aşağıdaki OncePerRequestFilter, 64 karakteri aşan veya beklenen karakter kümesine uymayan X-Correlation-Id değerini reddeder; böylece log forging ve sınırsız etiket kardinalitesi oluşmaz.
@Component
final class CorrelationFilter extends OncePerRequestFilter {
private static final Pattern SAFE_ID = Pattern.compile("[A-Za-z0-9_-]{8,64}");
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String supplied = request.getHeader("X-Correlation-Id");
String correlationId = supplied != null && SAFE_ID.matcher(supplied).matches()
? supplied
: UUID.randomUUID().toString();
try (MDC.MDCCloseable ignored = MDC.putCloseable("correlationId", correlationId)) {
response.setHeader("X-Correlation-Id", correlationId);
chain.doFilter(request, response);
}
}
}Bu filtre request thread'inde çalışır, dolayısıyla @Async, Executor veya CompletableFuture geçişinden sonra MDC'nin bulunacağını garanti etmez. Bir java kursu projesinde bu hatayı yakalamanın pratik testi şudur: controller içinde correlationId loglanır, sonra asenkron task içinde tekrar loglanır; ikinci satırda null görülüyorsa bağlam thread havuzu geçişinde kopmuştur. java microservices ortamında X-Correlation-Id uygulama loglarını birleştirmek için yararlıdır, ancak dağıtık trace için W3C traceparent üretmeye çalışmak yerine Micrometer Tracing ve seçilen tracer'ın propagator'ü kullanılmalıdır.
Spring Boot eğitimi: @Async executor'unda MDC ve SecurityContext kopyalama
spring boot eğitimi sırasında sık yapılan yapılandırma, yalnızca ThreadPoolTaskExecutor bean'i tanımlamaktır. Bu executor task'ı başka bir thread'de çalıştırdığı için MDC ThreadLocal'ını taşımaz. Spring Framework'ün TaskDecorator uzantısı submission anındaki değerleri yakalar, worker thread'inde önceki değerleri saklar ve finally bloğunda geri yükler. Geri yükleme kritik bir ayrıntıdır: havuz thread'i yeniden kullanıldığı için temizlenmeyen tenantId sonraki kullanıcının loguna veya yetki kararına sızabilir.
@Configuration
@EnableAsync
class AsyncConfig {
@Bean(name = "applicationExecutor")
ThreadPoolTaskExecutor applicationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(64);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("app-async-");
executor.setTaskDecorator(runnable -> {
Map<String, String> capturedMdc = MDC.getCopyOfContextMap();
Authentication capturedAuth = SecurityContextHolder.getContext().getAuthentication();
return () -> {
Map<String, String> previousMdc = MDC.getCopyOfContextMap();
SecurityContext previousSecurity = SecurityContextHolder.getContext();
try {
if (capturedMdc == null) MDC.clear(); else MDC.setContextMap(capturedMdc);
SecurityContext copiedSecurity = SecurityContextHolder.createEmptyContext();
copiedSecurity.setAuthentication(capturedAuth);
SecurityContextHolder.setContext(copiedSecurity);
runnable.run();
} finally {
if (previousMdc == null) MDC.clear(); else MDC.setContextMap(previousMdc);
SecurityContextHolder.setContext(previousSecurity);
}
};
});
executor.initialize();
return executor;
}
}SecurityContext nesnesini doğrudan parent thread'den atamak yerine boş context içine Authentication koymak kasıtsız paylaşılan mutable state riskini azaltır. Buna rağmen Authentication içindeki custom principal mutable ise immutable DTO kullanın. Ayrıca @Async proxy tabanlıdır: aynı bean içinden this.send() çağrısı proxy'yi atlar ve metot senkron çalışır. CompletableFuture.supplyAsync() da executor verilmezse common pool kullanır; doğru çağrı aşağıdaki gibi açık executor seçmelidir.
@Service
class InvoiceService {
private final Executor applicationExecutor;
InvoiceService(@Qualifier("applicationExecutor") Executor applicationExecutor) {
this.applicationExecutor = applicationExecutor;
}
CompletableFuture<Void> send(String invoiceId) {
return CompletableFuture.runAsync(() -> {
LoggerFactory.getLogger(getClass()).info("sending invoice={}", invoiceId);
}, applicationExecutor);
}
}Reactor Context ile reactive akışlarda MDC yanılgısını önleyin
WebFlux veya WebClient kullanan kodda MDC'yi temel taşıma mekanizması yapmak hatalıdır; Reactor bir sinyali farklı scheduler thread'lerinde işleyebilir. Request kimliğini Reactor Context'e koyun ve log yazılacak sinyalde ContextView'den okuyun. Bu yaklaşım scheduler değişse bile değerle birlikte taşınır; ThreadLocal'ın aksine belirli bir işletim sistemi thread'ine bağlı değildir.
@Bean
WebFilter correlationContextFilter() {
return (exchange, chain) -> {
String id = Optional.ofNullable(exchange.getRequest().getHeaders()
.getFirst("X-Correlation-Id"))
.filter(value -> value.matches("[A-Za-z0-9_-]{8,64}"))
.orElseGet(() -> UUID.randomUUID().toString());
exchange.getResponse().getHeaders().set("X-Correlation-Id", id);
return chain.filter(exchange).contextWrite(ctx -> ctx.put("correlationId", id));
};
}
Mono<Customer> findCustomer(String id) {
return repository.findById(id)
.switchIfEmpty(Mono.error(new NoSuchElementException(id)))
.doOnEach(signal -> {
if (signal.isOnNext() || signal.isOnError()) {
String correlationId = signal.getContextView()
.getOrDefault("correlationId", "missing");
log.info("correlationId={} signal={}", correlationId, signal.getType());
}
});
}Micrometer Context Propagation ile ThreadLocal accessor kaydedip Reactor'ın otomatik context propagation desteğini etkinleştirmek mümkündür, fakat bunu sadece MDC için küresel olarak açmadan önce allocation maliyetini ölçün. Özellikle çok sayıda kısa Mono zincirinde snapshot alma ek nesne üretir. Reactive loglarda doğrudan ContextView kullanmak, yalnızca log için ThreadLocal restore etme ihtiyacını ortadan kaldırır. Downstream HTTP çağrısında custom kimlik gerekiyorsa WebClient filter'ında request attribute veya Reactor Context'ten alınarak X-Correlation-Id eklenmelidir; traceparent için ise Micrometer Tracing'in instrumented WebClient'ı tercih edilmelidir.
Hibernate ORM ve Spring Data JPA sınırında transaction taşımayın
hibernate orm Session ve Spring Data JPA tarafından bağlanan EntityManager, request bağlamı gibi başka threade taşınacak veri değildir. Transaction synchronization ve persistence context thread'e bağlıdır. Bir entity'yi @Async metoda verip lazy ilişki okumak LazyInitializationException üretebilir; açık session'ı paylaşmaya çalışmak ise eşzamanlı persistence context erişimine yol açar. Güvenli model, transaction içinde değişikliği yazmak, commit sonrasında sadece kimlik taşıyan immutable event yayınlamak ve yeni task'ta yeni transaction açmaktır.
@Service
class OrderService {
private final ApplicationEventPublisher events;
@Transactional
void confirm(UUID orderId) {
Order order = orderRepository.getReferenceById(orderId);
order.confirm();
events.publishEvent(new OrderConfirmed(order.getId()));
}
}
@Component
class OrderProjectionListener {
@Async("applicationExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void project(OrderConfirmed event) {
projectionWriter.writeInNewTransaction(event.orderId());
}
}
record OrderConfirmed(UUID orderId) {}AFTER_COMMIT seçimi, rollback edilen transaction için iş başlatılmasını engeller; ancak process commit'ten sonra düşerse event kaybolabilir. Bu nedenle işlem finansal veya tekrar üretilemez ise transactional outbox kullanın, ama async context propagation ile outbox güvenilirliğini birbirine karıştırmayın. Context propagation log ve observability bilgisini taşır; veritabanı atomikliğini sağlamaz. java fullstack eğitimi projelerinde frontend'e dönen correlationId'yi hata raporunda göstermek faydalıdır, fakat JPA entity'sini JSON'a serileştirmek yerine yalnızca DTO ve kimlik döndürün.
Spring AI ve Spring MCP çağrılarında trace ile yetkiyi ayırın
spring ai kullanan bir servis, model çağrısını veya araç yürütmesini executor'a devrediyorsa correlationId'nin loglarda kalması teşhis süresini kısaltır; fakat correlationId asla authorization girdisi olmamalıdır. spring mcp ile model context protocol üzerinden bir araca çağrı yapıldığında süreç sınırı aşılabilir ve ThreadLocal otomatik taşınmaz. Süreçler arası trace bağlamı için instrumented HTTP istemcisi kullanın, iş yetkisi için ise kısa ömürlü servis token'ı veya kullanıcının doğrulanmış claim'lerini ayrı bir kanal olarak gönderin.
@Component
class CustomerTools {
@Tool(description = "Returns a masked customer summary for an authorized operator")
CustomerSummary customerSummary(String customerId) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("CUSTOMER_READ"))) {
throw new AccessDeniedException("CUSTOMER_READ is required");
}
return customerService.findMaskedSummary(UUID.fromString(customerId));
}
}
record CustomerSummary(UUID id, String maskedEmail, String status) {}Araç metoduna tüm SecurityContext'i prompt veya tool sonucu olarak koymayın. Sadece araç tarafında gereken izin kontrolünü yapın ve CustomerSummary gibi whitelist edilmiş bir sonuç döndürün. Bir model context protocol sunucusuna ağ üzerinden çağrı yapılıyorsa requestId uygulama korelasyonu içindir; OpenTelemetry span context ise tracer'ın propagator'ü ile taşınmalıdır. java microservices içinde bu ayrım, aynı requestId'ye sahip iki isteğin birbirinin yetkisini kazanmasını önler.
Context propagation maliyetini JFR ve async-profiler ile karşılaştırın
MDC map kopyalama her task submission'ında allocation yapar; 100 bin küçük task/saniye üreten bir işte bunun etkisini tahmin etmeyin. Önce TaskDecorator kapalı, sonra açık olacak şekilde aynı k6 senaryosunu çalıştırın ve p50, p95, p99 gecikmesi ile allocation profilini aynı yükte karşılaştırın. Test boyunca aynı JVM heap ayarını, executor queueCapacity değerini ve downstream stub gecikmesini sabit tutun; aksi halde sonuç decorator yerine kuyruklanma farkını ölçer.
# Uygulama PID'sini bulun ve 120 saniyelik JFR kaydı alın
jcmd $PID JFR.start name=context-propagation settings=profile duration=120s filename=before-or-after.jfr
# Aynı yükte allocation hotspot'larını HTML olarak çıkarın
./profiler.sh -e alloc -d 120 -f alloc-before-or-after.html $PID
# k6 ile iki koşulda aynı sabit oranlı yükü uygulayın
k6 run --vus 80 --duration 2m context-load.jsJFR'de jdk.ObjectAllocationInNewTLAB ve jdk.ObjectAllocationOutsideTLAB olaylarında java.util.HashMap ile MDC adapter çağrılarını, async-profiler alloc görünümünde ise TaskDecorator lambda ve map kopyalarını inceleyin. Sonra yalnızca gerekli anahtarları taşıyan immutable bir RequestContext kaydına geçmeyi deneyin. Örneğin tenantId ve correlationId dışında MDC'de 20 anahtar varsa, decorator içinde tam map yerine iki değeri set etmek allocation'ı azaltabilir. Bu değişikliği ancak before ve after p99 ile allocation oranı birlikte doğrulandıysa kabul edin; context kaybı kontrolü için async task loglarında correlationId'nin boş gelme sayısını Micrometer Counter ile ayrıca sayın.
İlgili Eğitim
Sık Sorulan Sorular
Spring Framework @Async içinde MDC neden null olur?
@Async çağrısı işi ThreadPoolTaskExecutor worker thread'ine verir, MDC ise ThreadLocal tabanlıdır ve yeni threade kopyalanmaz. Executor'a TaskDecorator ekleyin, MDC.getCopyOfContextMap() ile submission anındaki haritayı alın ve worker thread'inde try/finally ile önceki haritayı geri yükleyin. CompletableFuture.supplyAsync için de applicationExecutor parametresini açıkça verin.
Spring Boot eğitimi projesinde Reactor Context mi MDC mi kullanmalıyım?
Servlet tabanlı, senkron request akışında MDC uygundur. WebFlux akışında request kimliğini contextWrite ile Reactor Context'e koyun ve doOnEach içindeki signal.getContextView() üzerinden okuyun. MDC'yi reactive zincirde ana veri kaynağı kabul etmek scheduler değişiminde yanlış correlation üretir.
Spring Data JPA entity nesnesi @Async metoda gönderilebilir mi?
Göndermeyin. Entity'nin lazy alanları async thread'de bağlı EntityManager olmadığı için LazyInitializationException verebilir. Transaction içinde sadece entity kimliğini içeren event yayınlayın, AFTER_COMMIT listener içinde yeni transaction ile repository'den tekrar yükleyin. Bu yaklaşım Hibernate ORM persistence context'ini threadler arasında paylaşmaz.
Spring AI ve Spring MCP araçlarında model context protocol için correlationId yeterli mi?
Hayır. correlationId log korelasyonu içindir ve yetki kanıtı değildir. Model context protocol ile süreç dışına çıkan çağrılarda dağıtık trace için OpenTelemetry veya Micrometer Tracing propagator'ünü, yetkilendirme için doğrulanmış kısa ömürlü token veya servis kimliğini kullanın. Tool metodunda gerekli authority kontrolünü tekrar yapın ve modele sadece maskelenmiş DTO döndürün.
AI / LLM Discovery
Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.



