Spring'in iç yüzü: @SpringBootApplication, reflection, component scan ve ApplicationContext
Bir Spring Boot uygulamasının main metodundan trafiğe açılana kadar geçen yolu kaynak kodu seviyesinde izler: annotation'ların nasıl okunduğunu, reflection'ın nerede ve ne pahasına kullanıldığını, component scan'in sınıfları yüklemeden nasıl bulduğunu ve ApplicationContext'in aslında ne olduğunu öğretir.
Bu bölümde
Bölüm 6 container'ın bean'leri nasıl bağladığını ve yaşam döngüsünü, Bölüm 7 auto-configuration'ın koşullarla nasıl geri çekildiğini anlattı. Bu bölüm bir kat aşağı iniyor: SpringApplication.run(...) satırından embedded Tomcat'in port açmasına kadar hangi sınıfın hangi metodu hangi sırayla çağırdığını, bir annotation'ın nasıl "okunduğunu", reflection'ın bu süreçte nerede devreye girdiğini ve component scan'in sınıfları JVM'e yüklemeden nasıl bulduğunu göreceksin.
Bu dört soru (@SpringBootApplication nasıl çalışır, reflection nedir, @ComponentScan nasıl çalışır, Spring context nedir) gerçek bir mülakatta art arda soruldu. Hepsi aynı mekanizmanın parçalarıdır. Mülakatçı tanım istemiyor, "sihir" dediğimiz şeyin altındaki mekanizmayı anlatıp anlatamadığına bakıyor. Bu bölümün sonunda dördünü tek bir hikaye olarak anlatabilmelisin: annotation bir metadata'dır, onu reflection (veya bytecode okuyucu) ile okuyan kod iş yapar, component scan ve auto-configuration bu metadata'dan bean tanımları üretir, ApplicationContext de bu tanımları nesnelere dönüştürüp yöneten container'dır.
Konu anlatımı
Annotation'lar kendi başına hiçbir şey yapmaz
Bir annotation, sınıfa, metoda, alana veya parametreye iliştirilmiş metadata'dır. Derleyici onu class dosyasına (veya sadece kaynak koda) yazar ve orada durur. @Transactional transaction açmaz, @Component bean oluşturmaz, @Test testi çalıştırmaz. İşi her zaman annotation'ı okuyan başka bir kod yapar: Spring'in post-processor'ları, JUnit'in test motoru, Hibernate'in metadata okuyucusu, bir annotation processor.
Annotation'ı kimin, ne zaman okuyabileceğini @Retention belirler:
@Retention | Nerede yaşar | Kim okur | Örnek |
|---|---|---|---|
SOURCE | Sadece kaynak kodda; class dosyasına yazılmaz | Derleyici ve annotation processor'lar (derleme zamanı) | @Override, @SuppressWarnings, Lombok'un @Getter'ı |
CLASS (belirtilmezse varsayılan) | Class dosyasında RuntimeInvisibleAnnotations attribute'u olarak | Bytecode okuyan araçlar; reflection göremez | Bazı statik analiz anotasyonları |
RUNTIME | Class dosyasında RuntimeVisibleAnnotations olarak, JVM yükler | Reflection ile çalışma anında | @Component, @Transactional, @Entity, @Test |
Bunu kendin görebilirsin. İki annotation tanımla, birini RUNTIME, birini CLASS yap, aynı sınıfa koy, derle ve javap -v ile class dosyasına bak:
RuntimeVisibleAnnotations:
0: #26()
ReflectionTour$Audited
RuntimeInvisibleAnnotations:
0: #28()
ReflectionTour$BuildOnly
İkisi de class dosyasında. Ama InMemoryStock.class.isAnnotationPresent(BuildOnly.class) false döner, çünkü JVM RuntimeInvisibleAnnotations'ı reflection'a sunmaz. Kendi annotation'ını yazıp @Retention koymayı unutursan Spring onu hiçbir zaman görmez ve hata da vermez.
@Target annotation'ın nereye konabileceğini sınırlar (TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, ANNOTATION_TYPE...). TYPE annotation tiplerini de kapsadığı için @Target(ElementType.TYPE) olan bir annotation başka bir annotation'ın üzerine konabilir. @Inherited ise bir sınıf annotation'ının alt sınıflara "miras" kalmasını sağlar; sadece üst sınıftan miras alır, interface'lerden değil. @SpringBootApplication @Inherited'dır.
Meta-annotation ve composed annotation. Bir annotation'ın üzerine konan annotation'a meta-annotation denir. @Service, @Repository, @Controller ve @Configuration, @Component ile meta-annotated'dır. Birden çok meta-annotation'ı birleştiren annotation'a composed annotation denir: @RestController = @Controller + @ResponseBody; @GetMapping = @RequestMapping(method = GET); @SpringBootApplication üç annotation'ın birleşimidir.
Burada mülakatta fark yaratan bir ayrıntı var: Java'nın reflection API'si meta-annotation'ı anlamaz. @Service ile işaretli bir sınıf için OrderService.class.isAnnotationPresent(Component.class) false döner. JDK, sınıfın üzerinde doğrudan duran annotation'lara bakar; "bu annotation'ın üzerindeki annotation"ı takip etmez. Meta-annotation hiyerarşisini dolaşan ve "bu sınıf bir çeşit @Component mı?" sorusunu cevaplayan kod Spring'e aittir.
Spring bunu org.springframework.core.annotation paketindeki MergedAnnotations API'siyle yapar (Spring 5.2+). Bu API bir elemandaki tüm annotation'ları, meta-annotation'larıyla birlikte, istenen arama stratejisine göre (DIRECT, INHERITED_ANNOTATIONS, SUPERCLASS, TYPE_HIERARCHY) toplar ve her birine "kökten uzaklık" (getDistance()) bilgisi ekler:
// Spring API'si; derleme için spring-core gerekir
MergedAnnotation<Component> component = MergedAnnotations
.from(OrderService.class, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY)
.get(Component.class);
component.isPresent(); // true: @Service -> @Component
component.getDistance(); // 1: doğrudan değil, bir meta seviyesinde
component.getString("value"); // @Service("orders") verildiyse "orders"
@AliasFor meta-annotation'lara değer taşımanın yoludur. İki kullanımı var:
- Aynı annotation içinde eşanlamlı attribute:
@RequestMapping'devaluevepathbirbirinin alias'ıdır; hangisini yazarsan yaz diğeri de aynı değeri döner. - Meta-annotation'ın attribute'unu ezmek: Composed annotation, kendi attribute'unu meta-annotation'ın attribute'una bağlar.
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
}
public @interface SpringBootApplication {
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
}
@SpringBootApplication(scanBasePackages = "com.example.checkout") yazdığında JDK bu değeri @ComponentScan'e iletmez; JDK açısından @ComponentScan'in basePackages'i hâlâ boştur. Spring, MergedAnnotations ile hiyerarşiyi birleştirirken @AliasFor kurallarını uygular ve "birleştirilmiş" (merged) bir @ComponentScan görünümü üretir. Gerekirse bunu gerçek bir annotation nesnesine çevirir (synthesize()).
İlginç bir yan bilgi: Java'da annotation nesnelerinin kendisi de dynamic proxy'dir. getAnnotation(...) ile aldığın nesnenin sınıfı $Proxy1 gibi bir isimdir ve Proxy.isProxyClass(...) true döner. Spring'in synthesize ettiği annotation'lar da aynı yöntemle üretilir.
Spring Framework 6.1'den beri kendi stereotype annotation'ında bean adını taşıyan attribute'u @AliasFor(annotation = Component.class) ile açıkça bağlaman gerekir. Sadece adı value olan bir attribute'a güvenen "convention-based" isimlendirme deprecated'dır ve Spring açılışta uyarı loglar.
Reflection: çalışma anında sınıfları okumak ve çağırmak
Reflection, çalışan bir programın kendi sınıflarının yapısını (alanlar, metotlar, constructor'lar, annotation'lar, generic imzalar) çalışma anında okuması ve bu bilgiyle derleme zamanında bilmediği kodu çağırmasıdır. Kapısı java.lang.Class nesnesidir: JVM her yüklediği sınıf için heap'te bir Class nesnesi tutar (sınıf metadata'sı Metaspace'te durur, bkz. Bölüm 3).
Temel API'ler:
| İşlem | API | Not |
|---|---|---|
| Sınıfa ulaşmak | Order.class, obj.getClass(), Class.forName("...") | forName sınıfı yükler ve varsayılan olarak initialize eder (static bloklar çalışır) |
| Üyeleri listelemek | getDeclaredFields/Methods/Constructors() | getDeclared* sadece o sınıfta tanımlı tüm üyeler (private dahil); getFields/getMethods public üyeler, miras alınanlar dahil |
| Nesne oluşturmak | Constructor.newInstance(args) | Spring'in constructor injection'ı sonunda budur |
| Alan okumak/yazmak | Field.get/set | Field injection budur |
| Metot çağırmak | Method.invoke(target, args) | Argümanlar Object[], primitive'ler boxing'e uğrar |
| Erişim kontrolünü kapatmak | setAccessible(true) | Private üyelere erişim; JPMS sınırlarına tabi |
| Annotation okumak | getAnnotation, isAnnotationPresent | Sadece RUNTIME retention, meta-annotation yok |
| Generic bilgi | getGenericType, getGenericReturnType | Bildirimdeki generic bilgi Signature attribute'undan gelir (bkz. Bölüm 2) |
Aşağıdaki program bunların hepsini çalıştırır (JDK 21 ile java ReflectionTour.java diye derleyip çalıştırıldı):
import java.lang.annotation.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.*;
import java.util.Arrays;
public class ReflectionTour {
@Retention(RetentionPolicy.RUNTIME) @interface Audited {}
@Retention(RetentionPolicy.CLASS) @interface BuildOnly {}
interface StockService { int reserve(String sku, int qty); }
@Audited @BuildOnly
static class InMemoryStock implements StockService {
private int available = 10;
public int reserve(String sku, int qty) { available -= qty; return available; }
private String secret() { return "internal"; }
}
public static void main(String[] args) throws Throwable {
Class<?> type = InMemoryStock.class;
System.out.println("@Audited : " + type.isAnnotationPresent(Audited.class));
System.out.println("@BuildOnly : " + type.isAnnotationPresent(BuildOnly.class));
System.out.println("fields : " + Arrays.toString(type.getDeclaredFields()));
Object stock = type.getDeclaredConstructor().newInstance();
Field available = type.getDeclaredField("available");
available.setAccessible(true);
available.setInt(stock, 3);
Method reserve = type.getMethod("reserve", String.class, int.class);
System.out.println("invoke : " + reserve.invoke(stock, "SKU-1", 1));
Method secret = type.getDeclaredMethod("secret");
secret.setAccessible(true);
System.out.println("private : " + secret.invoke(stock));
try {
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
} catch (InaccessibleObjectException e) {
System.out.println("JPMS : " + e.getClass().getSimpleName());
}
MethodHandle mh = MethodHandles.lookup().findVirtual(InMemoryStock.class, "reserve",
MethodType.methodType(int.class, String.class, int.class));
System.out.println("handle : " + (int) mh.invokeExact((InMemoryStock) stock, "SKU-1", 1));
StockService proxy = (StockService) Proxy.newProxyInstance(
StockService.class.getClassLoader(),
new Class<?>[] {StockService.class},
(p, m, a) -> {
long start = System.nanoTime();
try { return m.invoke(stock, a); }
finally { System.out.println("proxy : " + m.getName() + " took "
+ (System.nanoTime() - start) / 1_000 + " us"); }
});
System.out.println("via proxy : " + proxy.reserve("SKU-1", 1));
System.out.println("proxy class: " + proxy.getClass().getName()
+ " instanceof InMemoryStock? " + (proxy instanceof InMemoryStock));
}
}
@Audited : true
@BuildOnly : false
fields : [private int ReflectionTour$InMemoryStock.available]
invoke : 2
private : internal
JPMS : InaccessibleObjectException
handle : 1
proxy : reserve took ... us
via proxy : 0
proxy class: $Proxy2 instanceof InMemoryStock? false
Son satır, Bölüm 6'daki "JDK proxy ile somut sınıf enjeksiyonu neden BeanNotOfRequiredTypeException verir" sorusunun en çıplak halidir: JDK proxy'si sadece interface'i implemente eden yeni bir sınıftır, hedef sınıfın alt tipi değildir.
setAccessible ve modül sistemi (JPMS). Java 9 ile gelen modül sistemi, bir modülün iç paketlerini dışarıya kapatır. setAccessible(true), hedef üyenin bulunduğu paket çağıran koda açık (opened) değilse InaccessibleObjectException fırlatır. JDK 16'da (JEP 396) JDK iç paketleri varsayılan olarak kapatıldı, JDK 17'de (JEP 403) bunu gevşeten --illegal-access seçeneği etkisiz hale getirildi (verilirse sadece uyarı basılır). Yukarıdaki örnekte String.value alanına erişim bu yüzden reddedildi. java --add-opens java.base/java.lang=ALL-UNNAMED ile çalıştırınca aynı satır hata vermez.
Spring Boot uygulamaları genelde classpath'te, yani isimsiz (unnamed) modülde çalışır; kendi sınıflarının private üyelerine erişim serbesttir. Sorun JDK'nın içine girmeye çalışan eski kütüphanelerde çıkar: Java 17 geçişinde görülen module java.base does not "opens java.lang" to unnamed module hatası budur. Kalıcı çözüm kütüphaneyi güncellemek, --add-opens ise belgelenmiş geçici bir yamadır.
Maliyet. Reflection üç açıdan pahalıdır:
- Arama maliyeti.
getDeclaredMethods()her çağrıda yeni bir dizi kopyası döner, aramalar erişim kontrolü yapar. Bu yüzden framework'ler sonuçları önbelleğe alır: Spring'deReflectionUtilsdeclared-methods cache'i tutar,AutowiredAnnotationBeanPostProcessorher sınıfın enjeksiyon noktalarını bir kez çıkarıpInjectionMetadataolarak saklar. - Çağrı maliyeti.
Method.invokeargümanlarıObject[]'e koyar, primitive'leri boxing'e uğratır, erişim kontrolü yapar. Eski JDK'larda ilk çağrılar native kodla yapılır, bir eşikten sonra JVM bytecode üreten bir accessor oluştururdu ("inflation"). JDK 18'de JEP 416 ileMethod,ConstructorveFieldmethod handle'lar üzerinde yeniden yazıldı. JEP'in ölçümüne görestatic finalalanda tutulan birMethod'u JIT sabit kabul edip optimize edebildiği için çağrı belirgin şekilde hızlanır; sabit olmayan durumda ise belirgin yavaşlama görülebilir (JEP'in mikro benchmark'larında sabit olmayanFielderişimi eski implementasyona göre %51-77 yavaş). Eski implementasyon-Djdk.reflect.useDirectMethodHandle=falseile geri açılabiliyordu; JDK 22'de tamamen kaldırıldı. - Optimizasyon kaybı. Çağrılacak metot çalışma anında belirlendiği için JIT inlining'i sınırlı uygular. Oracle'ın rehberi de reflection'ı sık çağrılan, performansa duyarlı yollardan uzak tutmayı önerir.
Spring'de maliyetin büyük kısmı açılışta ödenir (metadata çıkarmak, enjeksiyon noktalarını bulmak). İstek başına kısım küçüktür: Spring MVC controller metodunu InvocableHandlerMethod.doInvoke içinde method.invoke(getBean(), args) ile çağırır; JIT ısındıktan sonra bu, I/O ve JSON maliyetinin yanında gürültüdür. Açılış süresi ise Kubernetes'te ölçeklenme hızını etkilediği için AOT'un konusudur (aşağıda).
Kapsülleme. setAccessible(true) private ve final alanları değiştirip nesnenin invariant'larını constructor'ı atlayarak bozabilir. Framework için gerekli, uygulama kodunda bir kokudur: testte ReflectionTestUtils.setField görmek, sınıfın constructor injection'a geçmesi gerektiğini söyler.
Reflection, MethodHandle ve bytecode üretimi. Aynı amaca giden dört araç var:
| Araç | Ne yapar | Kim kullanır |
|---|---|---|
Core reflection (java.lang.reflect) | Üyeleri bul, invoke/set/newInstance | Spring DI, Spring MVC handler çağrısı, JUnit, Jackson'ın keşif aşaması |
MethodHandle (java.lang.invoke, Java 7+) | Tipli, doğrudan çağrılabilir metot referansı; erişim kontrolü handle Lookup üzerinden (findVirtual vb.) oluşturulurken bir kez yapılır; JIT tarafından inline edilebilir | JDK 18+ core reflection'ın kendisi, lambda'ların altyapısı (LambdaMetafactory), yüksek performanslı kütüphaneler |
JDK dynamic proxy (java.lang.reflect.Proxy) | Çalışma anında verilen interface'leri implemente eden bir sınıf üretir; her çağrı InvocationHandler.invoke'a gider | Spring AOP (interface tabanlı), Spring Data repository'leri, @HttpExchange client'ları, Java annotation nesneleri |
| Bytecode üretimi (CGLIB, ByteBuddy, ASM) | Hedef sınıfın alt sınıfını veya tamamen yeni sınıflar üretir | Spring'in CGLIB proxy'leri ve @Configuration sınıfları (Spring, CGLIB ve Objenesis'in kendi içine gömülü kopyalarını taşır), Hibernate'in lazy proxy'leri ve bytecode enhancement'ı (ByteBuddy), Mockito (ByteBuddy) |
CGLIB ile ByteBuddy aynı işi yapar: çalışma anında alt sınıf üretir. CGLIB'in bağımsız projesi aktif geliştirilmiyor; Spring kendi içindeki kopyanın bakımını üstlenmiş durumda. Mülakat özeti: "Spring CGLIB, Hibernate ve Mockito ByteBuddy kullanır; alt sınıf üreten proxy'ler (Spring'in CGLIB proxy'leri, Hibernate'in lazy proxy'leri) final sınıf ve metotlarda çalışmaz." İstisna Mockito'dur: Mockito 5'ten beri varsayılan olan inline mock maker alt sınıf üretmek yerine sınıfın bytecode'unu instrumentation ile yerinde değiştirdiği için final sınıf ve metotları da mock'layabilir.
Reflection'ı kim kullanır?
- Spring: Constructor'ı
BeanUtils.instantiateClassiçindector.newInstance(args)ile çağırır; field injectionAutowiredAnnotationBeanPostProcessoriçindefield.set(bean, value), metot injectionmethod.invoke(bean, args)ile yapılır.@PostConstruct,@EventListenermetotları da böyle çağrılır. - Jackson: DTO'nun constructor, getter, alan ve annotation'larını keşfeder, sonucu tip başına bir serializer olarak önbelleğe alır.
- Hibernate: Entity metadata'sını okur, alanları reflection ile okuyup yazar, lazy ilişkiler için ByteBuddy proxy'si üretir. No-arg constructor istemesinin sebebi budur: önce boş nesneyi oluşturur, sonra alanları doldurur.
- JUnit 5:
@Testmetotlarını reflection ile bulup çağırır; test sınıflarınınpublicolması gerekmez çünkü erişim kontrolünü kendisi gevşetir.
Kendi mini DI container'ını yazmak
Spring'in yaptığını anlamanın en hızlı yolu, en küçük halini yazmaktır. Aşağıdaki container:
- Bir paketi ve alt paketlerini tarar (component scan),
@MyComponentile doğrudan veya meta-annotation olarak (@MyService) işaretli sınıfları "bean definition" olarak toplar,- Bağımlılıkları constructor parametrelerinden çıkarır, tek constructor varsa onu, birden çoksa
@MyInjectolanı seçer, - Interface ile istenen bağımlılık için tek implementasyonu bulur; sıfır veya birden çok aday varsa hata verir,
- Singleton cache tutar, döngüyü tespit eder ve tüm bean'leri açılışta eager oluşturur.
// Üç ayrı dosya: MyComponent.java, MyService.java, MyInject.java (paket: minidi)
package minidi;
import java.lang.annotation.*;
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {}
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
@MyComponent // meta-annotation
public @interface MyService {}
@Target(ElementType.CONSTRUCTOR) @Retention(RetentionPolicy.RUNTIME)
public @interface MyInject {}
package minidi;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.*;
public final class MiniContainer {
private final List<Class<?>> components = new ArrayList<>(); // "bean definition"lar
private final Map<Class<?>, Object> singletons = new HashMap<>(); // singleton cache
private final Set<Class<?>> inCreation = new HashSet<>(); // döngü tespiti
public MiniContainer(String basePackage) throws Exception {
var root = Thread.currentThread().getContextClassLoader().getResource(basePackage.replace('.', '/'));
scan(new File(Objects.requireNonNull(root, basePackage).toURI()), basePackage);
for (Class<?> type : components) getBean(type); // eager, ApplicationContext gibi
}
private void scan(File dir, String pkg) throws ClassNotFoundException {
for (File f : Objects.requireNonNull(dir.listFiles())) {
String name = pkg + "." + f.getName();
if (f.isDirectory()) scan(f, name);
else if (name.endsWith(".class")) {
// Spring'den farkı: sınıfı JVM'e YÜKLÜYORUZ (initialize=false ile static blok çalışmaz)
Class<?> c = Class.forName(name.substring(0, name.length() - 6), false, getClass().getClassLoader());
if (!c.isInterface() && !c.isAnnotation() && isComponent(c.getAnnotations(), new HashSet<>())) components.add(c);
}
}
}
private static boolean isComponent(Annotation[] annotations, Set<Class<?>> visited) {
for (Annotation a : annotations) { // meta-annotation hiyerarşisinde yukarı çık
Class<? extends Annotation> t = a.annotationType();
if (t == MyComponent.class || (visited.add(t) && isComponent(t.getAnnotations(), visited))) return true;
}
return false;
}
public <T> T getBean(Class<T> requested) {
List<Class<?>> candidates = components.stream().filter(requested::isAssignableFrom).toList();
if (candidates.size() != 1)
throw new IllegalStateException((candidates.isEmpty() ? "No bean" : "No unique bean") + " of type " + requested.getName() + " " + candidates);
Class<?> impl = candidates.get(0);
Object bean = singletons.get(impl);
if (bean != null) return requested.cast(bean);
if (!inCreation.add(impl)) throw new IllegalStateException("Circular dependency at " + impl.getSimpleName());
try {
Constructor<?> ctor = chooseConstructor(impl);
Object[] args = Arrays.stream(ctor.getParameterTypes()).map(this::getBean).toArray(); // önce bağımlılıklar
ctor.setAccessible(true); // package-private constructor'lar için
bean = ctor.newInstance(args); // reflection ile "new"
singletons.put(impl, bean);
return requested.cast(bean);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot create " + impl.getName(), e);
} finally {
inCreation.remove(impl);
}
}
private static Constructor<?> chooseConstructor(Class<?> impl) {
Constructor<?>[] ctors = impl.getDeclaredConstructors();
if (ctors.length == 1) return ctors[0]; // Spring 4.3+ kuralı: tek constructor'da @Autowired gerekmez
return Arrays.stream(ctors).filter(c -> c.isAnnotationPresent(MyInject.class)).findFirst()
.orElseThrow(() -> new IllegalStateException("Ambiguous constructors in " + impl.getName()));
}
}
Uygulama tarafı: shop.payment paketinde PaymentGateway interface'i ve package-private @MyComponent class IyzicoGateway, shop.order paketinde iki constructor'lı @MyComponent OrderRepository (biri @MyInject) ve @MyService OrderService(OrderRepository, PaymentGateway).
package app;
import java.math.BigDecimal;
import minidi.MiniContainer;
import shop.order.OrderRepository;
import shop.order.OrderService;
public class Main {
public static void main(String[] args) throws Exception {
var context = new MiniContainer("shop"); // ana sınıf kök pakette
OrderService orders = context.getBean(OrderService.class);
System.out.println(orders.place("ORD-1", new BigDecimal("249.90")));
System.out.println("same singleton: "
+ (context.getBean(OrderRepository.class) == context.getBean(OrderRepository.class)));
try {
new MiniContainer("shop.order"); // ana sınıf alt pakette
} catch (IllegalStateException e) {
System.out.println("FAIL: " + e.getMessage());
}
}
}
$ javac -d out $(find src -name '*.java') && java -cp out app.Main
iyzico:ORD-1:249.90
same singleton: true
FAIL: No bean of type shop.payment.PaymentGateway []
Son satır, Spring Boot'ta ana sınıfı alt bir pakete koyduğunda aldığın hatanın birebir aynısıdır: tarama shop.order'dan başladığı için shop.payment hiç görülmedi.
Bu 60 satırlık container ile Spring arasındaki farklar, Spring'in neden karmaşık olduğunu da açıklar:
| Mini container | Spring |
|---|---|
Sınıfları Class.forName ile JVM'e yükleyip annotation'larına bakıyor | Class dosyasını bytecode olarak okur (ASM veya Java 24+ ClassFile API), aday olmayan sınıfı hiç yüklemez |
| Sadece dosya sistemi dizinini tarıyor | classpath*: ile tüm jar'ları ve dizinleri, Boot'un nested jar'ları dahil tarar |
Bean tanımı = Class nesnesi | BeanDefinition: scope, lazy, primary, qualifier, init/destroy metodu, factory metodu, bağımlılıklar |
Tip eşleşmesi isAssignableFrom | Generic tip eşleşmesi (ResolvableType), @Qualifier, @Primary, @Fallback, List/Map/ObjectProvider enjeksiyonu |
| Döngüde direkt hata | Constructor döngüsünde hata, field/setter döngüsünde üç seviyeli cache ile erken referans (Boot 2.6+ varsayılan kapalı) |
| Proxy yok | BeanPostProcessor zinciri ve AOP proxy'leri |
| Yaşam döngüsü yok | Aware, @PostConstruct, SmartLifecycle, kapanış sırası, event'ler |
@SpringBootApplication'ı açmak
Spring Boot 4'ün kaynak kodundaki tanım (yorumlar çıkarılmış):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
@AliasFor(annotation = Configuration.class)
boolean proxyBeanMethods() default true;
}
Yani @SpringBootApplication kendi başına hiçbir şey yapmayan bir composed annotation'dır. Tüm attribute'ları @AliasFor ile alttaki annotation'lara yönlendirilir. Parçalar:
1) @SpringBootConfiguration = @Configuration + @Indexed. Ana sınıfı bir konfigürasyon sınıfı yapar: içine @Bean metodu yazabilirsin ve proxyBeanMethods varsayılan true olduğu için ana sınıf da CGLIB ile alt sınıflanır (Bölüm 6, full mode). Ayrıca @SpringBootTest, konfigürasyon vermediğinde test paketinden yukarı doğru çıkarak @SpringBootConfiguration taşıyan sınıfı arar; bir uygulamada tek bir @SpringBootConfiguration olmasının sebebi budur.
2) @EnableAutoConfiguration iki meta-annotation taşır:
@AutoConfigurationPackage→@Import(AutoConfigurationPackages.Registrar.class). Bu registrar, ana sınıfın paketiniAutoConfigurationPackagesadlı özel bir bean'e yazar. Component scan ile ilgisi yoktur; bu paketi başka tarayıcılar kullanır: Spring Data repository auto-configuration'ları (@EnableJpaRepositoriesyazmadıysan repository arayüzlerini bu paketten arar) ve JPA entity taraması (@EntityScanyoksa). Ana sınıfı taşıdığında repository'lerin "kaybolmasının" ikinci sebebi budur.@Import(AutoConfigurationImportSelector.class)→ Auto-configuration'ları yükleyenDeferredImportSelector.getAutoConfigurationEntrymetodu sırasıyla:spring.boot.enableautoconfigurationproperty'si ile kapatılmış mı bakar;ImportCandidates.load(AutoConfiguration.class, ...)ile classpath'teki tümMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsdosyalarını okur; tekrarları çıkarır;exclude/excludeName/spring.autoconfigure.excludeile verilenleri düşer; ardındanAutoConfigurationImportFilter'ları (OnClassCondition,OnBeanCondition,OnWebApplicationCondition) uygular. Bu ön filtre, derleme zamanındaspring-boot-autoconfigure-processor'ın ürettiğiMETA-INF/spring-autoconfigure-metadata.propertiesdosyasını kullanır; yani classpath'te Redis olmayan bir projedeRedisAutoConfigurationsınıfı hiç okunmadan elenir. Kalan sınıflarAutoConfigurationSorterilebefore/after/@AutoConfigureOrderkurallarına göre sıralanır.
3) @ComponentScan, basePackages verilmezse annotation'ın bulunduğu sınıfın paketini kök alır (aşağıda ayrıntılı). İki exclude filtresi taşır:
TypeExcludeFilter: Kendisi hiçbir şeyi dışlamaz; context'tekiTypeExcludeFiltertipindeki bean'leri bulup kararı onlara devreder. Spring Boot'un test slice'ları bu kancayı kullanır:@WebMvcTestbirWebMvcTypeExcludeFilterkaydeder ve controller dışındaki@Service,@Repositorygibi bileşenleri taramadan çıkarır.@WebMvcTest'te servis bean'inin "bulunamamasının" mekanizması budur.AutoConfigurationExcludeFilter: Taranan pakette hem@Configurationhem@AutoConfigurationolan veyaAutoConfiguration.importslistesinde yer alan bir sınıf bulursa onu dışlar. Auto-configuration'lar component scan ile değil, sadece imports dosyasıyla ve doğru sırada yüklenmelidir; kendi starter'ını aynı repo'da, ana uygulamanın paket ağacında geliştirirken bile çift kayıt olmasın diye bu filtre var.
@SpringBootApplication'a ek olarak ana sınıfa ayrıca @ComponentScan("com.example") koymak yaygın bir hatadır. Ana sınıf üzerinde doğrudan bir @ComponentScan bulunduğunda Spring, meta-annotation olarak gelen @ComponentScan'i (ve onun TypeExcludeFilter / AutoConfigurationExcludeFilter filtrelerini) kullanmaz; ConfigurationClassParser önce doğrudan tanımlı @ComponentScan'e bakar, yoksa meta olana düşer. Sonuç: test slice'ları bozulur ve auto-configuration sınıfları tarama ile ikinci kez kaydedilebilir. Tarama kökünü değiştirmek için @SpringBootApplication(scanBasePackages = ...) kullan.
SpringApplication.run adım adım
SpringApplication.run(CheckoutApplication.class, args) statik metodu iki iş yapar: new SpringApplication(CheckoutApplication.class) ve .run(args).
Constructor (güncel Boot 4 kaynağından; 4.0.0'da deduce() yerine hâlâ deduceFromClasspath() çağrılıyordu):
public SpringApplication(@Nullable ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.properties.setWebApplicationType(WebApplicationType.deduce());
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
- Primary source kaydedilir:
CheckoutApplication.class. Context'e ilk kaydedilecek bean tanımı budur; geri kalan her şey ondan "büyür". - Web uygulama tipi classpath'ten çıkarılır. Boot 3.x'te
WebApplicationType.deduceFromClasspath():DispatcherHandlervar amaDispatcherServletve Jersey yoksaREACTIVE;jakarta.servlet.ServletveyaConfigurableWebApplicationContext'ten biri yoksaNONE; aksi haldeSERVLET. Boot 4'te (4.0.1+) karar modüllerinspring.factories'te kaydettiğiWebApplicationType.Deducer'lara dağıtıldı (spring-boot-webflux,MonoveDispatcherHandlervarsaREACTIVEder); hiçbiri karar vermezse servlet sınıflarına bakılır. Ezmek içinspring.main.web-application-type. spring.factories'ten yükleme.SpringFactoriesLoadertümMETA-INF/spring.factoriesdosyalarını okur veBootstrapRegistryInitializer,ApplicationContextInitializer,ApplicationListenerimplementasyonlarını reflection ile oluşturur. Bu listener'lar context'ten önce yaşar;LoggingApplicationListenerveEnvironmentPostProcessorApplicationListeneren önemlileridir.- Main sınıfı
StackWalkerile çağrı yığınındakimainframe'inden bulunur; açılış log'undaki sınıf adı ve AOT'un ürettiğiCheckoutApplication__ApplicationContextInitializeradı buradan gelir.
run() (Boot 4, kısaltılmış):
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.started(context, startup.started());
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
return context;
}
Süre ölçümü Boot 3.2+'da Startup nesnesiyle yapılır (eski sürümlerde StopWatch veya System.nanoTime()); "Started CheckoutApplication in 4.2 seconds (process running for 4.9)" satırındaki iki sayı buradan gelir.
flowchart TD
A["new SpringApplication: web tipi, spring.factories, main class"] --> B["starting: ApplicationStartingEvent"]
B --> C["prepareEnvironment: property source'lar, ConfigData, profiller"]
C --> D["environmentPrepared: ApplicationEnvironmentPreparedEvent"]
D --> E["printBanner"]
E --> F["createApplicationContext: AnnotationConfigServletWebServerApplicationContext"]
F --> G["prepareContext: initializer'lar, contextPrepared, ana sınıfı BeanDefinition olarak kaydet, contextLoaded"]
G --> H["refreshContext: AbstractApplicationContext.refresh()"]
H --> I["started: ApplicationStartedEvent, LivenessState.CORRECT"]
I --> J["callRunners: ApplicationRunner ve CommandLineRunner"]
J --> K["ready: ApplicationReadyEvent, ReadinessState.ACCEPTING_TRAFFIC"]SpringApplicationRunListeners, spring.factories'ten gelen EventPublishingRunListener üzerinden her adımı bir event'e çevirir:
| Run listener çağrısı | Yayınlanan event | O anda ne var? |
|---|---|---|
starting | ApplicationStartingEvent | Sadece listener'lar; Environment yok |
environmentPrepared | ApplicationEnvironmentPreparedEvent | Environment var, context yok. application.yml bu event'te yüklenir |
contextPrepared | ApplicationContextInitializedEvent | Context oluştu, initializer'lar çalıştı; altyapı tanımları dışında uygulama bean tanımı yok |
contextLoaded | ApplicationPreparedEvent | Ana sınıf kayıtlı, refresh henüz başlamadı |
| (refresh içinde) | WebServerInitializedEvent, ContextRefreshedEvent | Tüm singleton'lar hazır, port açık |
started | ApplicationStartedEvent | Runner'lar henüz çalışmadı |
ready | ApplicationReadyEvent | Runner'lar bitti, trafik kabul ediliyor |
failed | ApplicationFailedEvent | Açılış hata ile bitti |
İlk iki event context oluşmadan yayınlandığı için bunları bir @EventListener bean'i ile dinleyemezsin; bean henüz yoktur. Bu event'leri dinlemek için listener'ı spring.factories'e veya SpringApplication.addListeners(...) ile kaydetmek gerekir.
prepareEnvironment doğru Environment tipini oluşturur, komut satırı argümanlarını property source olarak ekler ve environmentPrepared yayınlar. Bu event'i dinleyen EnvironmentPostProcessorApplicationListener, EnvironmentPostProcessor'ları çalıştırır; içlerindeki ConfigDataEnvironmentPostProcessor, application.yml, profil dosyaları ve spring.config.import kaynaklarını bu noktada yükler (öncelik sırası için bkz. Bölüm 7). Sonuç: @PropertySource dosyaları ancak refresh'te, konfigürasyon parse edilirken eklenir; logging.* ve spring.main.* gibi daha önce okunan ayarlar için çok geçtir.
createApplicationContext web tipine göre context sınıfını seçer: SERVLET için AnnotationConfigServletWebServerApplicationContext, REACTIVE için AnnotationConfigReactiveWebServerApplicationContext, NONE için AnnotationConfigApplicationContext (Boot 4'te servlet context spring-boot-web-server modülüne, org.springframework.boot.web.server.servlet.context paketine taşındı). Constructor'daki AnnotatedBeanDefinitionReader, AnnotationConfigUtils.registerAnnotationConfigProcessors ile altyapı tanımlarını kaydeder: ConfigurationClassPostProcessor, AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, EventListenerMethodProcessor. Henüz kullanıcı bean'i yoktur; sadece "annotation'ları okuyacak kodun" tanımları vardır.
prepareContext environment'ı bağlar, ApplicationContextInitializer'ları çalıştırır, allow-circular-references ve allow-bean-definition-overriding ayarlarını bean factory'ye uygular, lazy initialization açıksa LazyInitializationBeanFactoryPostProcessor ekler ve load() ile primary source'u BeanDefinitionLoader üzerinden bir BeanDefinition olarak kaydeder. Refresh başladığında registry'deki tek uygulama tanımı ana sınıftır; scan, auto-configuration ve her şey bu tanımın annotation'larının işlenmesiyle ortaya çıkar.
refreshContext shutdown hook'u kaydeder (SIGTERM'de context.close()) ve context.refresh()'i çağırır. callRunners runner'ları @Order'a göre çalıştırır; biri exception fırlatırsa açılış başarısız sayılır ve context kapatılır.
main metodu run() döndükten sonra biter ama uygulama kapanmaz: TomcatWebServer, container-0 adlı daemon olmayan bir thread başlatır ve JVM daemon olmayan thread kalmadıkça kapanmaz. Virtual thread'ler daemon olduğu için Boot 3.2+ ayrıca spring.main.keep-alive ayarını sunar.
refresh(): context'in kalbi
AbstractApplicationContext.refresh() Spring Framework'ün en önemli metodudur. Spring Framework 7 kaynağındaki iskelet:
public void refresh() throws BeansException, IllegalStateException {
this.startupShutdownLock.lock();
try {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
prepareRefresh();
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
prepareBeanFactory(beanFactory);
try {
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
invokeBeanFactoryPostProcessors(beanFactory);
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
initMessageSource();
initApplicationEventMulticaster();
onRefresh();
registerListeners();
finishBeanFactoryInitialization(beanFactory);
finishRefresh();
}
catch (RuntimeException | Error ex) {
// oluşmuş singleton'ları yok et (destroy callback'leri çalışır), 'active' bayrağını sıfırla
destroyBeans();
cancelRefresh(ex);
throw ex;
}
finally {
contextRefresh.end();
}
}
finally {
this.startupShutdownLock.unlock();
}
}
Adım adım:
| # | Metot | Ne yapar |
|---|---|---|
| 1 | prepareRefresh | Context'i aktif işaretler, property source'ları başlatır, zorunlu property'leri doğrular |
| 2 | obtainFreshBeanFactory | Bean factory'yi verir. GenericApplicationContext (Boot'un context'leri bundan türer) tek bir DefaultListableBeanFactory tutar ve yalnızca bir kez refresh edilebilir; ikinci çağrı IllegalStateException verir. Eski XML tabanlı AbstractRefreshableApplicationContext ise her refresh'te yeni factory kurar |
| 3 | prepareBeanFactory | Class loader, SpEL çözümleyici, ApplicationContextAwareProcessor BPP'si eklenir. BeanFactory, ApplicationContext, ApplicationEventPublisher, ResourceLoader "resolvable dependency" olarak kaydedilir: bunlar bean değildir ama enjekte edilebilir. environment, systemProperties gibi singleton'lar kaydedilir |
| 4 | postProcessBeanFactory | Alt sınıf kancası. Servlet context burada request/session scope'larını kaydeder |
| 5 | invokeBeanFactoryPostProcessors | Bean tanımlarının neredeyse tamamı burada oluşur (aşağıda) |
| 6 | registerBeanPostProcessors | BeanPostProcessor bean'lerini önce PriorityOrdered, sonra Ordered, sonra diğerleri sırasıyla oluşturup factory'ye ekler |
| 7 | initMessageSource | i18n için messageSource bean'i, yoksa boş bir delegating implementasyon |
| 8 | initApplicationEventMulticaster | Event yayını için multicaster (varsayılan SimpleApplicationEventMulticaster, senkron) |
| 9 | onRefresh | Alt sınıf kancası. ServletWebServerApplicationContext burada embedded web server'ı oluşturur |
| 10 | registerListeners | ApplicationListener bean'lerini multicaster'a kaydeder, bekleyen erken event'leri yayınlar |
| 11 | finishBeanFactoryInitialization | Tanımları dondurur (freezeConfiguration) ve tüm lazy olmayan singleton'ları oluşturur (preInstantiateSingletons) |
| 12 | finishRefresh | LifecycleProcessor.onRefresh() ile SmartLifecycle bean'lerini başlatır (web server portu burada açılır, Kafka listener container'ları burada başlar), ContextRefreshedEvent yayınlar |
Adım 5 ayrıntısı: ConfigurationClassPostProcessor. invokeBeanFactoryPostProcessors, işi PostProcessorRegistrationDelegate'e devreder. Önce BeanDefinitionRegistryPostProcessor'lar (yeni tanım ekleyebilen özel BFPP'ler), sonra sıradan BeanFactoryPostProcessor'lar çalışır; her grup kendi içinde PriorityOrdered → Ordered → diğerleri sırasıyla. ConfigurationClassPostProcessor PriorityOrdered bir BeanDefinitionRegistryPostProcessor'dır, yani kullanıcı tarafındaki ilk iş odur. Yaptığı iş:
- Registry'deki tanımlar arasından konfigürasyon sınıfı adaylarını bulur. Başlangıçta tek aday ana sınıftır.
ConfigurationClassParser.parse(...)her adayıdoProcessConfigurationClassile işler. Sıra kaynak kodda açıkça yazılıdır: iç içe (member) sınıflar →@PropertySource→@ComponentScan→@Import→@ImportResource→@Beanmetotları → interface'lerdeki default@Beanmetotları → üst sınıf.@ComponentScanhemen çalıştırılır.ComponentScanAnnotationParsertarayıcıyı kurar, bulunan sınıflar anındaBeanDefinitionolarak kaydedilir ve bunların içinde@Configuration(veya@Component+@Beangibi "lite" konfigürasyon) olanlar aynı parse işlemine özyinelemeli sokulur. Senin@Configurationsınıfların ve onların@Import'ları bu sırada işlenir.@Importüç (Spring 7'de dört) tür şeyi kabul eder: düz bir konfigürasyon/component sınıfı; birImportSelector(sınıf adları döner); birImportBeanDefinitionRegistrar(registry'ye programatik tanım yazar,AutoConfigurationPackages.Registrargibi); Spring Framework 7 ile gelenBeanRegistrar(programatik bean kaydının yeni, AOT uyumlu sözleşmesi).ImportSelectorbirDeferredImportSelectorise hemen değil, bir tutamağa (handler) konur.parsemetodunun en sonundadeferredImportSelectorHandler.process()çalışır.AutoConfigurationImportSelectorbirDeferredImportSelectorolduğu için auto-configuration sınıfları ancak senin bütün konfigürasyonların ve taranan bileşenlerin parse edildikten sonra listeye eklenir.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(...)parse edilen sınıfların@Beanmetotlarını,@Importile gelen registrar'ları ve imported sınıfları sırayla registry'ye yazar.@Conditionalkoşulları iki fazda değerlendirilir:PARSE_CONFIGURATIONfazındaki koşullar (@ConditionalOnClass,@Profile) sınıfı parse etmeden önce;REGISTER_BEANfazındaki koşullar (Boot'unOnBeanCondition'ı, yani@ConditionalOnBean/@ConditionalOnMissingBean) tanımlar kaydedilirken. Senin tanımların auto-configuration'lardan önce kaydedildiği için@ConditionalOnMissingBean"kullanıcı bu bean'i tanımladı mı?" sorusuna doğru cevap verir.ConfigurationClassPostProcessor'ın kendipostProcessBeanFactorymetodunda (refresh'in 4. adımı değil, yine adım 5 içinde) full mode@ConfigurationsınıflarıenhanceConfigurationClassesile CGLIB alt sınıflarına çevrilir (Bölüm 6).
Bu adımın sonunda registry'de yüzlerce BeanDefinition vardır ama tek bir uygulama nesnesi bile oluşmamıştır. Ardından PropertySourcesPlaceholderConfigurer gibi diğer BFPP'ler tanımlardaki ${...}'leri çözer.
Adım 9 ayrıntısı: Tomcat ne zaman başlar? ServletWebServerApplicationContext.onRefresh() → createWebServer() context'te tek bir ServletWebServerFactory bean'i arar (yoksa MissingWebServerFactoryBeanException), factory.getWebServer(...) ile TomcatWebServer'ı kurar ve iki SmartLifecycle singleton'ı kaydeder: webServerGracefulShutdown ve webServerStartStop. Tomcat başlatılır ama connector'lar bilerek çıkarılır: port dinlenmez, bean'ler hazır olmadan istek kabul edilmez. Port, adım 12'de WebServerStartStopLifecycle.start() connector'ları geri ekleyince açılır ve ServletWebServerInitializedEvent yayınlanır.
Adım 11 ayrıntısı: bir bean nasıl doğar? preInstantiateSingletons her lazy olmayan singleton tanımı için getBean(name) çağırır. Zincir (AbstractBeanFactory → AbstractAutowireCapableBeanFactory):
getBean -> doGetBean
-> getSingleton(name) // cache'te var mı? (erken referans dahil)
-> getMergedBeanDefinition // parent tanımlarla birleştirilmiş RootBeanDefinition
-> dependsOn bean'lerini önce oluştur
-> getSingleton(name, () -> createBean(...))
createBean
-> resolveBeforeInstantiation // InstantiationAwareBPP nesnenin yerine proxy dönebilir (nadir)
-> doCreateBean
-> createBeanInstance // factory method (@Bean) veya autowireConstructor:
// constructor seçimi + BeanUtils.instantiateClass -> ctor.newInstance(args)
-> applyMergedBeanDefinitionPostProcessors // @Autowired/@PostConstruct metadata'sı çıkarılır ve cache'lenir
-> addSingletonFactory(name, () -> getEarlyBeanReference(...)) // döngüler için erken referans
-> populateBean // field/setter injection: field.set / method.invoke
-> initializeBean // Aware -> BPP before (@PostConstruct) -> init -> BPP after (AOP proxy)
-> addSingleton // singletonObjects'e konur
Callback sırası ve proxy'nin postProcessAfterInitialization'da doğması Bölüm 6'da anlatıldı. Buradaki nokta: Spring'in bir nesneyi oluşturması, sonunda mini container'daki gibi Constructor.newInstance'tır; fark, etrafındaki BPP zinciri ve önbelleklemedir. Spring 6.2+'da @Bean(bootstrap = Bean.Bootstrap.BACKGROUND) ile ağır bir bean arka planda da oluşturulabilir (context'te bootstrapExecutor adlı bir Executor varsa).
Hata olursa refresh oluşmuş singleton'ları destroyBeans() ile yok eder (@PreDestroy'lar çalışır) ve exception'ı fırlatır (hata lifecycle başlatma sırasında çıktıysa DefaultLifecycleProcessor.onRefresh o ana kadar başlamış Lifecycle bean'lerini ayrıca durdurur); Boot'un FailureAnalyzer'ları "APPLICATION FAILED TO START" raporunu basar.
@ComponentScan: içeride ne olur
Tarama kökü. ComponentScanAnnotationParser, basePackages ve basePackageClasses boşsa ClassUtils.getPackageName(declaringClass) ile annotation'ı taşıyan sınıfın paketini kök alır. @SpringBootApplication ana sınıfta olduğu için kök, ana sınıfın paketidir ve tarama tüm alt paketleri kapsar. Spring Boot dokümantasyonu bu yüzden ana sınıfı diğer sınıfların üstündeki bir kök pakete koymayı önerir. Ana sınıf com.example.checkout.api paketindeyse com.example.checkout.payment taranmaz. Aynı parser ayrıca taramayı başlatan sınıfın kendisini exclude filtresine ekler (zaten kayıtlı olduğu için). Ana sınıfı paketsiz (default package) bırakmak ise en kötüsüdür: Boot dokümantasyonuna göre bu durumda tarama tüm jar'lardaki tüm sınıfları okur.
Akış. ClassPathBeanDefinitionScanner.doScan(basePackages), her paket için üst sınıfı ClassPathScanningCandidateComponentProvider'ın findCandidateComponents metodunu çağırır:
"com.example.checkout"
-> "classpath*:com/example/checkout/**/*.class" // CLASSPATH_ALL_URL_PREFIX + paket yolu + "**/*.class"
-> PathMatchingResourcePatternResolver.getResources(...) // tüm classpath kökleri: dizinler, jar'lar, Boot nested jar'ları
-> her Resource için:
CGLIB üretimi sınıfları atla ("$$" içerenler)
MetadataReader = metadataReaderFactory.getMetadataReader(resource) // bytecode'u oku, sınıfı YÜKLEME
isCandidateComponent(metadataReader) // exclude filtreleri -> include filtreleri -> @Conditional
ScannedGenericBeanDefinition oluştur
isCandidateComponent(beanDefinition) // bağımsız (top-level veya static nested) ve somut mu?
-> her aday için: scope çöz, bean adı üret, ortak annotation'ları uygula (@Lazy, @Primary, @DependsOn...)
-> checkCandidate: aynı isimde uyumsuz tanım var mı?
-> registry.registerBeanDefinition(name, definition)
classpath*: öneki önemlidir: classpath: ilk bulunan kaynağı döner, classpath*: aynı yoldaki tüm classpath girdilerini (her jar'daki aynı paket dahil) döner. Çok modüllü bir projede com.example.checkout paketinin hem uygulama jar'ında hem paylaşılan bir kütüphane jar'ında sınıfları olabilir; ikisi de taranır.
Sınıfları neden yüklemeden okur? Spring her .class dosyası için bir MetadataReader oluşturur. Bu okuyucu dosyayı bayt olarak okuyup sınıf adını, üst sınıfı, interface'leri, annotation'ları (meta-annotation'ları da çözerek) ve metot metadata'sını çıkarır. Sınıf ClassLoader'a verilmez. Üç sebebi var:
- Hız ve bellek. Taranan paketteki her sınıfı yüklemek; doğrulama, bağlama ve Metaspace maliyeti demektir. Sınıfların çoğu (DTO, entity, util, exception) bean değildir ve hiç yüklenmesine gerek yoktur.
- Yan etkisizlik. Sınıf initialize edilirse static blokları çalışır. Taramanın, bean olmayan bir sınıfın static initializer'ını tetiklemesi istenmez.
- Eksik bağımlılıklara dayanıklılık. Bir sınıfı yüklemek, imzasındaki tiplerin de çözülmesini gerektirebilir; classpath'te olmayan bir tipe referans
NoClassDefFoundErrorverir. Spring Boot dokümantasyonu@ConditionalOnClass(SomeLibrary.class)yazılabilmesini tam olarak buna bağlar: annotation metadata'sı ASM ile okunduğu için, değer olarak verilen sınıf classpath'te olmasa bile koşul güvenle değerlendirilir.
Bytecode okuyucu tarihsel olarak Spring'in içine gömülü, sadeleştirilmiş bir ASM kopyasıdır (org.springframework.asm; SimpleMetadataReaderFactory). Spring Framework 7.0 ile MetadataReaderFactory.create(...) çalışılan JDK'ya göre implementasyon seçer: Java 24+'ta JDK'nın standart Class-File API'sini (java.lang.classfile, JEP 484 ile Java 24'te final) kullanan ClassFileMetadataReaderFactory, daha eski JDK'larda ASM tabanlı SimpleMetadataReaderFactory. Sebep, ASM'in yeni class dosyası sürümlerini okuyabilmesi için her JDK sürümünde güncellenmesi gerekmesidir; Class-File API JDK ile birlikte geldiği için her zaman güncel class formatını tanır. Taramada kullanılan CachingMetadataReaderFactory okunan metadata'yı önbelleğe alır; Boot, SharedMetadataReaderFactoryContextInitializer ile tek bir CachingMetadataReaderFactory'yi ConfigurationClassPostProcessor ile Boot'un kendi bileşenleri arasında paylaştırır; context'in resource loader'ıyla kurulan factory'ler ayrıca aynı resource cache'ini kullanır, böylece aynı sınıf tekrar tekrar okunmaz.
Filtreler. Varsayılan include filtresi new AnnotationTypeFilter(Component.class)'tır ve meta-annotation'ları da kabul eder: @Service, @Repository, @Controller, @RestController, @Configuration hepsi bu tek filtreden geçer. Classpath'te jakarta.inject.Named varsa o da eklenir. Kendi filtrelerini includeFilters/excludeFilters ile verebilirsin; türler ANNOTATION, ASSIGNABLE_TYPE, ASPECTJ, REGEX, CUSTOM (kendi TypeFilter implementasyonun; MetadataReader alır, yani yine sınıf yüklemeden karar verir). useDefaultFilters = false ile @Component filtresi kapatılır.
@Configuration
@ComponentScan(
basePackages = "com.example.checkout",
excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*\\.legacy\\..*"))
class CheckoutScanConfig { }
İkinci aday kontrolü (isCandidateComponent(AnnotatedBeanDefinition)) sınıfın bağımsız (top-level veya static nested) ve somut olmasını ister; abstract sınıf ancak @Lookup metodu varsa kabul edilir. Interface'e @Component koymak bu yüzden bir şey üretmez. (Spring Data repository arayüzleri component scan ile değil, kendi tarayıcılarıyla ve FactoryBean ile bean olur, bkz. Bölüm 6.) @Component olan bir sınıfta @Conditional / @Profile da bu aşamada ConditionEvaluator ile değerlendirilir; koşul sağlanmazsa tanım hiç kaydedilmez.
Bean adı. AnnotationBeanNameGenerator, stereotype annotation'da açık bir ad verilmişse onu kullanır (@Service("orderService")), yoksa sınıfın kısa adını JavaBeans kuralıyla küçük harfle başlatır: OrderService → orderService; ama ilk iki harfi büyük olanlar olduğu gibi kalır: URLResolver → URLResolver. İki farklı paketteki aynı kısa adlı sınıf (payment.OrderMapper ve shipping.OrderMapper) aynı bean adını üretir ve ClassPathBeanDefinitionScanner.checkCandidate ConflictingBeanDefinitionException fırlatır. Çözüm: birine açık ad vermek veya tarayıcıya FullyQualifiedAnnotationBeanNameGenerator vermek (@SpringBootApplication(nameGenerator = ...)).
Component index. Spring 5.0, büyük projelerde taramayı hızlandırmak için spring-context-indexer annotation processor'ını getirmişti: derleme zamanında META-INF/spring.components dosyasına aday sınıfları yazar, Spring de classpath'i taramak yerine bu dosyayı okurdu. Bu processor Spring Framework 6.1'de deprecated edildi (forRemoval = true) ve gerekçe olarak AOT motoru ve AOT'un üreteceği bir component index gösterildi. Spring 7'de CandidateComponentsIndex programatik olarak (çalışma anında) doldurulabilen bir yapıya genişletildi; AOT'un üreteceği component index ise processor'ın deprecation notunda hâlâ "gelecek" bir özellik olarak anılıyor. Yeni projede spring-context-indexer ekleme; açılış süresi gerçekten sorunsa önce ölç, sonra AOT/CDS seçeneklerine bak.
@ComponentScan, @Import ve @Bean farkı:
@ComponentScan | @Import | @Bean | |
|---|---|---|---|
| Neyi kaydeder | Paketteki @Component (meta) sınıfları | Adı verilen konfigürasyon/component sınıflarını, selector ve registrar'ların ürettiklerini | Metodun döndürdüğü nesneyi |
| Keşif | Örtük, paket ağacına göre | Açık, sınıf adıyla | Açık, kodla |
| Kontrol | Sınıfa annotation eklemen gerekir | Sınıfın annotation'ı olmasa da import edilebilir | Kaynak koduna dokunamadığın sınıflar, kurulum mantığı |
| Tipik kullanım | Uygulamanın kendi bileşenleri | Kütüphane ve auto-configuration'lar, modüller arası bağlama | Üçüncü parti client'lar, Clock, farklı yapılandırılmış aynı tipte nesneler |
Kütüphane ve starter yazarken component scan kullanılmaz; Boot dokümantasyonu auto-configuration'ların asla scan hedefi olmamasını ve kendileri de scan açmamalarını, gereken sınıfları @Import ile almalarını söyler. Sebep: scan, kullananın paket yapısına ve sırasına bağımlıdır; import ise deterministiktir.
Spring context nedir?
Spring context (ApplicationContext), bean tanımlarını tutan, bu tanımlardan nesneleri oluşturup bağlayan, yaşam döngülerini yöneten ve uygulamaya ortam, event, kaynak ve mesaj hizmetleri sunan container'dır. Bir Spring uygulamasında "Spring'in yönettiği" her şey bir context'in içinde yaşar.
İçinden bakınca bir Boot web uygulamasının context'i şöyle katmanlanır:
AnnotationConfigServletWebServerApplicationContext
extends ServletWebServerApplicationContext // embedded server, web scope'ları
extends GenericWebApplicationContext
extends GenericApplicationContext // tek DefaultListableBeanFactory, tek refresh
extends AbstractApplicationContext // refresh() şablonu, event'ler, lifecycle, MessageSource
implements ConfigurableApplicationContext, BeanDefinitionRegistry, ...
beanFactory: DefaultListableBeanFactory
- beanDefinitionMap (bean adı -> BeanDefinition) // tarif
- singletonObjects / earlySingletonObjects / singletonFactories // nesneler
- beanPostProcessors listesi
environment: ConfigurableEnvironment (PropertySource listesi, profiller)
applicationEventMulticaster, messageSource, lifecycleProcessor
BeanFactory ile ApplicationContext farkı (tembel/eager oluşturma, BPP'lerin otomatik kaydı, event, i18n, Environment) Bölüm 6'da tablo olarak var. Burada üç kavramı netleştirmek gerekir.
BeanDefinition (tarif) ve bean (nesne) farkı. BeanDefinition, bir bean'in nasıl oluşturulacağını anlatan metadata'dır: sınıf adı veya factory bean + factory metodu, scope, lazy-init, primary, autowire candidate olup olmadığı, constructor argümanları, property değerleri, init/destroy metot adları, dependsOn. Component scan ScannedGenericBeanDefinition, @Bean metodu ConfigurationClassBeanDefinition üretir; container bunları oluşturmadan önce RootBeanDefinition olarak birleştirir (merged definition). BeanDefinitionRegistry bu tanımları kaydetme/silme sözleşmesidir; DefaultListableBeanFactory hem registry hem factory'dir. BeanFactoryPostProcessor'lar tarifleri, BeanPostProcessor'lar nesneleri değiştirir. Prototype bir bean'in tek bir tarifi ama sayısız nesnesi vardır; abstract bir parent tanımın ise hiç nesnesi yoktur.
Singleton cache ve erken referans. DefaultSingletonBeanRegistry üç map tutar: singletonObjects (hazır bean'ler), earlySingletonObjects (oluşturulmuş ama tamamlanmamış, erken referansı birine verilmiş bean'ler) ve singletonFactories (erken referansı ihtiyaç olursa üretecek factory'ler). Mekanizma Bölüm 6'da anlatıldı; mülakatta sorulan ince nokta "neden iki değil üç seviye?" sorusudur. Cevap: erken referansın proxy mi ham nesne mi olacağı ancak birisi gerçekten erken referans istediğinde belli olur. doCreateBean, nesneyi oluşturur oluşturmaz addSingletonFactory(name, () -> getEarlyBeanReference(...)) ile bir factory koyar; bu factory çağrıldığında SmartInstantiationAwareBeanPostProcessor'lar (AOP auto-proxy creator dahil) gerekiyorsa proxy'yi erkenden üretir. Döngü yoksa factory hiç çağrılmaz ve proxy normal yerinde, postProcessAfterInitialization'da oluşur. Factory bir kez çağrılınca sonucu earlySingletonObjects'e taşınır ki ikinci isteyen aynı erken nesneyi alsın. Spring 6.2+'da bu erken referans yalnızca oluşturmayı yapan thread tarafından üretilir (singletonLock.tryLock() başarısızsa null döner); arka planda bean oluşturma desteği bunu gerektirdi.
Context hiyerarşisi. Bir context'in parent'ı olabilir. Child, kendi bulamadığı bean'i parent'ta arar; parent child'ı görmez. Yerleşik örnekler:
- Klasik Spring MVC (Boot öncesi,
web.xmldönemi):ContextLoaderListenerservisleri ve repository'leri tutan bir root context kurar, herDispatcherServletcontroller'ları tutan bir child context kurardı. "@Transactionalneden controller'da çalışıyor da serviste çalışmıyor?" veya "@EnableWebMvcneden etkisiz?" tarzı eski hataların kaynağı, BPP'lerin ve konfigürasyonun yanlış context'e konmasıydı; BPP'ler sadece kendi context'lerindeki bean'leri işler. - Spring Boot: Varsayılan olarak tek context vardır;
DispatcherServletana context'i kullanır. İstisna:management.server.portana porttan farklıysa Actuator endpoint'leri için ayrı bir child management context (kendi web server'ıyla) kurulur.SpringApplicationBuilder().parent(...).child(...)ile kendi hiyerarşini de kurabilirsin. - Testler:
@ContextHierarchyile parent/child test context'leri tanımlanabilir.
Environment ve PropertySource'lar. Context'in Environment'ı sıralı bir PropertySource listesidir (komut satırı, SPRING_APPLICATION_JSON, system property, env, config data dosyaları, @PropertySource, varsayılanlar) ve aktif profilleri tutar. @Value("${...}") ve @ConfigurationProperties bu listeden çözülür; öncelik sırası Bölüm 7'de.
Event'ler. Context bir ApplicationEventPublisher'dır. Kendi event'lerinin yanında yaşam döngüsü event'leri yayınlar: ContextRefreshedEvent, ContextClosedEvent, (Boot'ta) ApplicationStartedEvent, ApplicationReadyEvent, AvailabilityChangeEvent. Senkron davranış ve @TransactionalEventListener için bkz. Bölüm 6.
Kapanış. context.close() (SIGTERM'de Boot'un shutdown hook'u çağırır) → doClose(): Boot'un servlet context'i önce ReadinessState.REFUSING_TRAFFIC yayınlar; ContextClosedEvent yayınlanır; LifecycleProcessor.onClose() SmartLifecycle bean'lerini yüksek fazdan düşüğe durdurur (graceful shutdown burada; bkz. Bölüm 7); sonra singleton'lar oluşturulma sırasının tersine yok edilir (@PreDestroy → DisposableBean → destroy metodu); en son web server destroy edilir.
Test context cache'i. Spring TestContext framework, aynı konfigürasyona sahip test sınıfları için context'i bir kez kurar ve paylaşır. Cache anahtarı konfigürasyon sınıfları, aktif profiller, @TestPropertySource değerleri, context initializer'lar ve "context customizer"lardır; @MockitoBean/@MockitoSpyBean/@TestBean gibi bean override'ları ve @DynamicPropertySource metotları bu customizer'lara dahildir. Yani her farklı @MockitoBean kombinasyonu yeni bir context açar (bkz. Bölüm 16). Cache varsayılan en fazla 32 context tutar (spring.test.context.cache.maxSize), dolunca LRU ile kapatır. @DirtiesContext bir context'i bilinçli olarak cache'ten atar. Spring Framework 7.0 ile kullanılmayan cache'teki context'ler "pause" edilir: SmartLifecycle bean'leri (Kafka/JMS listener'ları, scheduler'lar) durdurulur ve context tekrar kullanılınca yeniden başlatılır; böylece arka planda bekleyen eski bir context'in listener'ı başka testin mesajını tüketmez. 7.0.3'ten beri varsayılan mod ON_CONTEXT_SWITCH'tir (sadece sıradaki test farklı bir context kullanınca pause eder), spring.test.context.cache.pause=never ile kapatılır.
AOT ve native image: reflection'ı build zamanına taşımak
Buraya kadar anlatılan her şey açılışta, çalışma anında olur: classpath taranır, annotation'lar okunur, koşullar değerlendirilir, constructor'lar reflection ile çağrılır. GraalVM native image bu modeli sevmez çünkü closed-world varsayımıyla çalışır: build zamanında main'den başlayan statik analiz yapılır, ulaşılamayan kod binary'den atılır, classpath sabittir ve sonradan sınıf yüklenmez. Reflection ile hangi sınıfın çağrılacağı çalışma anındaki veriye bağlı olabildiği için analiz bunu göremez; native image'a "şu sınıfların constructor'ları ve metotları reflection ile çağrılacak" bilgisinin reachability metadata olarak verilmesi gerekir. Aynısı dynamic proxy'ler, kaynak dosyaları ve serileştirme için de geçerlidir. Runtime'da sınıf üretmek (CGLIB) de mümkün değildir.
Spring'in cevabı AOT (ahead-of-time) işlemedir (Spring Framework 6 / Boot 3.0+):
- Build sırasında uygulama context'i özel bir modda refresh edilir (
refreshForAotProcessing): bean tanımları oluşturulur (scan, auto-configuration, koşullar), ama hiçbir bean nesnesi oluşturulmaz. - Bu tanımlar Java kaynak koduna çevrilir. Her konfigürasyon sınıfı için
__BeanDefinitionssınıfları üretilir; reflection yerine düz Java çağrıları kullanılır, örneğinbeanDefinition.setInstanceSupplier(DataSourceConfiguration::new). Giriş noktasıCheckoutApplication__ApplicationContextInitializer'dır;SpringApplication.prepareContext, AOT modunda ana sınıfı yüklemek ve scan yapmak yerine bu initializer'ı kullanır (SpringApplicationkaynağındakiAotDetector.useGeneratedArtifacts()kontrolü). - CGLIB proxy'leri ve
@Configurationalt sınıfları build zamanında bytecode olarak üretilir. - Hâlâ gereken reflection, kaynak ve proxy ihtiyaçları
RuntimeHintsolarak toplanır ve GraalVM metadata dosyalarına yazılır. Kendi ihtiyaçların içinRuntimeHintsRegistrar+@ImportRuntimeHints,@Reflective,@RegisterReflectionve Jackson ile serileştirilen DTO'lar için@RegisterReflectionForBindingkullanılır.
@Configuration(proxyBeanMethods = false)
@ImportRuntimeHints(CargoClientConfig.CargoHints.class)
class CargoClientConfig {
static class CargoHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("cargo/tariffs.json"); // classpath kaynağı
}
}
@Bean
@RegisterReflectionForBinding(ShipmentQuote.class) // Jackson bu tipi reflection ile okuyacak
CargoClient cargoClient(RestClient.Builder builder) {
return new CargoClient(builder.build());
}
}
AOT'un bedeli, Spring dokümantasyonunun açıkça listelediği kısıtlardır: classpath build zamanında sabittir; bean tanımları çalışma anında değişemez; @Profile ve bean varlığını etkileyen @Conditional property'leri (ör. @ConditionalOnProperty, ...enabled anahtarları) build zamanında değerlendirilir; @Bean metotlarının dönüş tipi mümkün olduğunca kesin (somut sınıf) olmalıdır, çünkü AOT nesneye değil tipe bakar; döngüsel bağımlılıklar AOT ile optimize edilmiş context'i başlatamaz. Yani "prod'da feature.x.enabled=false verip bean'i kapatırım" alışkanlığı native image'da çalışmaz.
AOT sadece native image için değildir: aynı üretilmiş kod JVM'de de kullanılabilir (-Dspring.aot.enabled=true); scan ve koşul değerlendirmesi atlandığı için açılış kısalır. JVM tarafında başka bir yol da JDK'nın sınıf yükleme/bağlama sonuçlarını önbelleğe alan CDS ve Java 24+ AOT cache'idir (JEP 483); bunlar Spring'in yaptığı işi değil, JVM'in sınıf yükleme işini hızlandırır.
Spring Framework 7 / Boot 4 ile değişenler (bu bölümle ilgili olanlar):
- Spring Boot 4 ile native image için GraalVM 25 veya üstü gerekir; Spring Framework 7 GraalVM'in yeni, tek dosyalık birleşik reachability metadata formatına (
reachability-metadata.json) geçti. Eski sürümlerde ayrıreflect-config.json,resource-config.json,proxy-config.jsondosyaları üretiliyordu. Bir tip için reflection hint kaydetmek artık constructor, metot ve alan introspection'ını kapsıyor;MemberCategory'ninINVOKE_*dışındaki değerleri deprecated. BeanRegistrar(Spring 7):@Importile alınan,register(BeanRegistry, Environment)metodunda koşullu ve programatik bean kaydı yapan yeni sözleşme. Lambda supplier'lı kayıtlar dahil AOT ve native image ile destekleniyor;ImportBeanDefinitionRegistrar'a göre daha okunur ve AOT dostu.- ClassFile API ile metadata okuma (Java 24+, yukarıda).
- Proxy tipi: Spring Boot'un "varsayılan CGLIB" davranışı artık Framework genelindeki tüm proxy işleyicilerine tutarlı uygulanıyor; tek bir bean için
@Proxyableile çıkılabiliyor. - Boot 4'ün modülerleşmesiyle
WebApplicationTypeçıkarımı modüllere dağıtıldı,BootstrapRegistryInitializergibi tiplerorg.springframework.boot.bootstrappaketine taşındı veEnvironmentPostProcessorarayüzüorg.springframework.bootpaketine geçti (eskisi deprecated olarak duruyor).spring.factories'e kendi uzantını yazan şirket içi kütüphanelerin bu taşımaları takip etmesi gerekir.
Mülakattaki sorular: adım adım cevap
Soru 1: @SpringBootApplication anotasyonu nasıl çalışır?
Soru: "@SpringBootApplication anotasyonu nasıl çalışır?"
Mülakatta beklenen: Üç annotation'ın birleşimi olduğunu söylemek yetmez; her birinin hangi mekanizmayı tetiklediğini ve annotation'ın kendisinin değil, SpringApplication.run ile başlayan sürecin onu okuyarak iş yaptığını anlatabilmek.
60 saniyelik cevap:
"@SpringBootApplication kendi başına bir şey yapmayan composed bir annotation; üç annotation'ı birleştiriyor ve attribute'larını @AliasFor ile onlara yönlendiriyor. @SpringBootConfiguration ana sınıfı bir @Configuration yapıyor. @ComponentScan ana sınıfın paketinden başlayarak alt paketlerdeki @Component ve türevlerini tarıyor; bu yüzden ana sınıfı kök pakete koyuyoruz. @EnableAutoConfiguration ise iki şey import ediyor: ana paketi Spring Data ve entity taraması için kaydeden @AutoConfigurationPackage ve AutoConfiguration.imports dosyalarındaki auto-configuration'ları yükleyen AutoConfigurationImportSelector. Bu selector bir DeferredImportSelector; yani auto-configuration'lar benim bean tanımlarımdan sonra işleniyor, @ConditionalOnMissingBean de bu sayede benim bean'imi görüp geri çekiliyor. Bütün bunları yapan annotation değil: SpringApplication.run ana sınıfı bir bean tanımı olarak kaydediyor, refresh sırasında ConfigurationClassPostProcessor bu sınıfın annotation'larını okuyup scan'i, import'ları ve @Bean metotlarını işliyor."
Mülakatçı "devam et" derse: @ComponentScan'deki iki exclude filtresini (test slice'ları ve auto-config'lerin çift kaydı), auto-configuration hattını (imports dosyası → exclude → metadata ile ön filtre → sıralama → REGISTER_BEAN koşulları) ve ana sınıfa ikinci bir @ComponentScan koymanın neyi bozduğunu anlat; kanıt olarak --debug conditions report'u ve /actuator/conditions'ı göster.
Soru 2: Reflection nedir?
Soru: "Reflection nedir?"
Mülakatta beklenen: Tanımın ötesinde, Spring/Jackson/Hibernate'in reflection'ı nerede kullandığını, maliyetini, modül sistemiyle ilişkisini ve native image'da neden sorun olduğunu bağlayabilmek. Bu soru genelde "Spring bean'i nasıl oluşturuyor?" sorusuna köprüdür.
60 saniyelik cevap:
"Reflection, programın çalışma anında kendi sınıflarının yapısını okuyup derleme zamanında bilmediği kodu çağırabilmesi. Class nesnesinden alanları, metotları, constructor'ları ve RUNTIME retention'lı annotation'ları okuyorum; Constructor.newInstance, Method.invoke, Field.set ile nesne oluşturup çağrı yapıyorum; setAccessible ile private üyelere erişiyorum. Spring'in DI'ı bunun üzerine kurulu: bean'i constructor'ı reflection ile çağırarak oluşturuyor, @Autowired alanları Field.set ile dolduruyor, MVC controller metodunu Method.invoke ile çağırıyor. Jackson DTO'ları, Hibernate entity'leri, JUnit test metotlarını aynı yolla buluyor. Bedeli var: lookup ve çağrı direkt koddan pahalı, JIT optimizasyonlarını sınırlıyor, kapsüllemeyi deliyor. Bu yüzden framework'ler sonuçları cache'liyor ve maliyetin çoğu açılışta ödeniyor. Java 16'dan beri modül sistemi kapalı paketlere setAccessible'ı varsayılan olarak engelliyor, InaccessibleObjectException alıyoruz. GraalVM native image'da da reflection önceden bildirilmek zorunda; Spring'in AOT motoru bu yüzden bean tanımlarını build zamanında koda çeviriyor."
Mülakatçı "devam et" derse: Tahtaya mini DI container'ın akışını çiz (getDeclaredConstructors() → parametre tiplerini çöz → newInstance(args)); JDK'nın meta-annotation çözmediğini ve Spring'in bunu MergedAnnotations ile yaptığını, JEP 416'yı ve proxy ailesini (JDK proxy, CGLIB/ByteBuddy, MethodHandle) ekle; uygulama kodunda reflection yerine interface/strategy veya derleme zamanı kod üretimini tercih ettiğini söyle.
Soru 3: @ComponentScan nasıl çalışır, ne işe yarar?
Soru: "@ComponentScan nasıl çalışır, ne işe yarar?"
Mülakatta beklenen: Tarama kökünün nasıl belirlendiğini, sınıfların nasıl bulunduğunu (classpath kaynakları ve bytecode okuma), hangi filtrelerin uygulandığını, sonucun bir BeanDefinition olduğunu ve "bean bulunamadı" hatalarıyla ilişkisini anlatabilmek.
60 saniyelik cevap:
"@ComponentScan, bir paketin ve alt paketlerinin altındaki @Component ile doğrudan veya meta-annotation olarak işaretli sınıfları bulup container'a bean tanımı olarak kaydeder; @Service, @Repository, @Controller, @Configuration hep @Component'in meta-annotation'lı halleri. Paket verilmezse annotation'ın durduğu sınıfın paketi kök alınır; Boot'ta bu ana sınıfın paketi. İşlem refresh sırasında ConfigurationClassPostProcessor tarafından tetikleniyor: ClassPathBeanDefinitionScanner paket adını classpath*:com/example/**/*.class desenine çevirip tüm jar ve dizinlerdeki class dosyalarını buluyor, her birini sınıfı yüklemeden bytecode olarak okuyor (ASM, Spring 7 ve Java 24+'ta ClassFile API), include/exclude filtrelerini ve @Conditional'ları uyguluyor, somut ve bağımsız sınıflar için ScannedGenericBeanDefinition oluşturup orderService gibi bir isimle registry'ye yazıyor. Bu noktada nesne yok, sadece tarif var; nesneler refresh'in sonunda oluşuyor. Yüklemeden okumanın sebebi hız, static initializer'ları tetiklememek ve classpath'te olmayan tiplere referans veren sınıflarda patlamamak."
Mülakatçı "devam et" derse: Hata modlarını say (kök paket dışında kalan bean, aynı kısa adlı iki sınıf, interface'e @Component, @WebMvcTest'te taranmayan servis), kütüphanede scan yerine @Import kullanma gerekçesini ver ve debug yolunu göster: logging.level.org.springframework.context.annotation=DEBUG "Identified candidate component class" satırlarını basar.
Soru 4: Spring context nedir?
Soru: "Spring context nedir?"
Mülakatta beklenen: "Bean'lerin tutulduğu yer" demekle kalmayıp BeanFactory/ApplicationContext ayrımını, bean tanımı ile bean nesnesi farkını, refresh sürecini ve context'in sunduğu ek hizmetleri (environment, event, lifecycle, hiyerarşi) anlatabilmek.
60 saniyelik cevap:
"Spring context, yani ApplicationContext, Spring'in IoC container'ı. İki katmanı var: altta BeanFactory, Boot'ta DefaultListableBeanFactory; bean tanımlarını (BeanDefinition: sınıf, scope, bağımlılıklar, init/destroy) bir map'te tutuyor, bunlardan nesneleri oluşturup bağımlılıkları enjekte ediyor ve singleton'ları cache'liyor. Üstte ApplicationContext buna uygulama hizmetlerini ekliyor: Environment ve property kaynakları, profiller, event yayını, i18n, kaynak yükleme, BeanPostProcessor'ların otomatik kaydı ve SmartLifecycle ile başlatma/durdurma. Hayatı refresh() metodunda başlıyor: önce BeanFactoryPostProcessor'lar tanımları üretiyor ve değiştiriyor (component scan ve auto-configuration burada), sonra BeanPostProcessor'lar kaydediliyor, embedded server kuruluyor, tüm lazy olmayan singleton'lar eager oluşturuluyor ve lifecycle bean'leri başlatılıyor. Boot web uygulamasında bu AnnotationConfigServletWebServerApplicationContext. Eager oluşturma sayesinde eksik bean veya hatalı konfigürasyon uygulama açılırken patlıyor."
Mülakatçı "devam et" derse: Tarif ile nesne ayrımını ve iki post-processor ailesini (BFPP tarife, BPP nesneye dokunur), singleton cache'in neden üç seviyeli olduğunu, context hiyerarşisini (klasik MVC'de root + child, Boot'ta ayrı management portu) ve "singleton context başınadır" sonucunun test context cache'ine etkisini anlat.
Üç seviyede cevap
Soru: "@SpringBootApplication nasıl çalışır?"
seviye: Orta etiketler: [spring-boot, auto-configuration] kaynak: Gerçek mülakat
@SpringBootApplication, Spring Boot uygulamasını başlatan anotasyon. İçinde @Configuration, @EnableAutoConfiguration ve @ComponentScan var. Bu anotasyonu main sınıfına koyunca Spring her şeyi otomatik ayarlıyor, bean'leri buluyor ve Tomcat'i başlatıyor.
Üç annotation'ın birleşimi. @SpringBootConfiguration ana sınıfı konfigürasyon sınıfı yapıyor. @ComponentScan ana sınıfın paketinden itibaren @Component, @Service gibi sınıfları tarıyor; bu yüzden ana sınıfı kök pakete koyuyoruz. @EnableAutoConfiguration, jar'lardaki AutoConfiguration.imports dosyalarında listelenen auto-configuration sınıflarını yüklüyor; bunlar @ConditionalOnClass, @ConditionalOnMissingBean gibi koşullarla korunuyor. Belirli bir auto-config'i exclude ile kapatabiliyorum, tarama kökünü scanBasePackages ile değiştirebiliyorum. Asıl işi SpringApplication.run başlatıyor.
Annotation metadata; işi yapan SpringApplication.run. Run ana sınıfı tek bir BeanDefinition olarak kaydediyor; refresh'te ConfigurationClassPostProcessor onu parse ederken MergedAnnotations ile meta-annotation'ları ve @AliasFor'ları çözüyor. @ComponentScan anında çalışıyor; filtrelerinden TypeExcludeFilter test slice'larının kancası, AutoConfigurationExcludeFilter auto-config'lerin scan ile çift yüklenmesini engelliyor. @AutoConfigurationPackage ana paketi Spring Data ve entity taraması için kaydediyor; ana sınıfı taşımak repository keşfini de bozar. AutoConfigurationImportSelector bir DeferredImportSelector: adaylar metadata ile sınıf okunmadan ön filtreleniyor, sıralanıyor ve bean koşulları benim tanımlarım kaydedildikten sonra, REGISTER_BEAN fazında değerlendiriliyor. Bundan iki kural çıkarıyorum: ana sınıfa ayrıca @ComponentScan koymam, çünkü meta olanı ve filtrelerini devre dışı bırakır; kendi @Configuration'ımda @ConditionalOnMissingBean kullanmam, çünkü sıra garantisi sadece auto-config'lerde var. "Bu bean neden geldi" sorusunu tahminle değil conditions report ile cevaplarım.
Junior "otomatik ayarlıyor" diyerek mekanizmayı atlıyor ve annotation'ın kendisinin iş yaptığını ima ediyor; bu, "annotation'lar metadata'dır" farkındalığının olmadığını gösterir. Ayrıca içinde doğrudan @Configuration değil, onu meta-annotation olarak taşıyan @SpringBootConfiguration vardır. Mid parçaları ve işlevlerini doğru sayıyor. Senior annotation'ı okuyan kodu (run, ConfigurationClassPostProcessor, MergedAnnotations) adlandırıyor, deferred import ile @ConditionalOnMissingBean arasındaki nedensellik bağını kuruyor, @AutoConfigurationPackage'ın gözden kaçan etkisini biliyor ve mekanizmadan pratik kurallar ile doğrulama yöntemi çıkarıyor.
Takip sorusu: "Ana sınıfı com.example.checkout.api paketine taşıdık; controller'lar çalışıyor ama OrderRepository bulunamıyor. Neden ve nasıl düzeltirsin?"
Soru: "Reflection nedir, Spring nerede kullanır, bedeli ne?"
seviye: Orta etiketler: [reflection, spring] kaynak: Gerçek mülakat
Reflection, Java'da çalışma anında sınıfların metotlarına ve alanlarına erişmeyi sağlayan bir API. Private alanlara bile erişebiliyoruz. Spring de @Autowired için reflection kullanıyor. Yavaş olduğu için çok kullanılmaması gerekiyor.
Class nesnesi üzerinden getDeclaredFields, getDeclaredMethods, getDeclaredConstructors ile yapıyı okuyorum; Constructor.newInstance, Method.invoke, Field.set ile çalıştırıyorum; private üyeler için setAccessible(true) gerekiyor. Spring bean'leri constructor'ı reflection ile çağırarak oluşturuyor, field injection'da Field.set kullanıyor, @PostConstruct ve controller metotlarını Method.invoke ile çağırıyor. Jackson ve Hibernate de DTO ve entity'leri reflection ile okuyor. Java 16'dan beri modül sistemi yüzünden JDK iç paketlerine reflection ile erişim varsayılan olarak InaccessibleObjectException veriyor, eski kütüphanelerde --add-opens gerekebiliyor. Annotation'ları okumak için retention'ın RUNTIME olması gerekiyor.
Çalışma anında tip metadata'sını okuyup derleme zamanında bilinmeyen kodu çağırmak; framework'lerin "convention over configuration" yapabilmesinin temeli. Spring'de üç yerde: keşif (enjeksiyon noktaları; scan'de ise reflection değil bytecode okuma var), oluşturma (BeanUtils.instantiateClass → ctor.newInstance) ve çağırma (field injection, @EventListener, MVC'de InvocableHandlerMethod.doInvoke). Bedeli: lookup ve erişim kontrolü, boxing ve dolaylılık, JIT'in sınırlı inline edebilmesi. JEP 416 ile JDK 18'de reflection method handle üzerine taşındı, eski yol JDK 22'de kaldırıldı. Spring metadata'yı sınıf başına bir kez çıkarıp cache'lediği için maliyetin çoğu açılışta; onu /actuator/startup ile ölçerim, çünkü açılış Kubernetes'te scale-out hızı demek. Kapsülleme tarafında ReflectionTestUtils.setField görürsem sınıfı constructor injection'a çeviririm. Örneğin bir projede Java 17 geçişinde InaccessibleObjectException veren kütüphaneler çıkarsa bunları --add-opens yerine sürüm yükselterek çözerim; native image'da da reflection'ı RuntimeHints ile bildirmek gerekiyor. Uygulama kodunda reflection kullanmam; önce interface/strategy, performans gerekiyorsa MethodHandle, boilerplate için derleme zamanı kod üretimi (MapStruct) tercih ederim.
Junior tanımı ve "yavaştır" ezberini veriyor ama ne kadar, nerede ve neden yavaş olduğunu bilmiyor. Mid API'yi ve Spring'deki kullanım yerlerini somut sayıyor, JPMS'i biliyor. Senior maliyeti kalemlere ayırıyor, nerede ödendiğini (açılış vs istek) ayırt ediyor, JDK sürümündeki değişikliği biliyor, kapsülleme ve native image boyutunu ekliyor, alternatifleri sıralıyor ve "ne zaman kullanmam" diyor. Ayrıca scan'in reflection değil bytecode okuma olduğunu belirtmesi, mekanizmayı gerçekten bildiğini gösteriyor.
Takip sorusu: "Spring'in @Transactional'ı uygulamak için kullandığı proxy reflection mı? JDK proxy ile CGLIB arasında bu açıdan ne fark var?"
Soru: "@ComponentScan nasıl çalışır?"
seviye: Orta etiketler: [spring, component-scan] kaynak: Gerçek mülakat
@ComponentScan verilen paketteki @Component, @Service, @Repository ve @Controller anotasyonlu sınıfları bulup bean olarak ekliyor. Spring Boot'ta @SpringBootApplication içinde olduğu için ayrıca yazmaya gerek yok.
Paket verilmezse annotation'ın bulunduğu sınıfın paketinden başlayıp alt paketleri tarar, bu yüzden Boot'ta ana sınıf kök pakette olmalı. @Service, @Repository gibi anotasyonlar @Component ile meta-annotated olduğu için hepsi bulunur. includeFilters ve excludeFilters ile tarama daraltılabilir. Bulunan sınıflar bean tanımı olarak kaydedilir; bean adı sınıf adının küçük harfle başlayan hali olur. Kütüphanelerde scan yerine @Import veya auto-configuration kullanılır.
Scan, refresh'te ConfigurationClassPostProcessor'ın ana sınıfı parse ederken anında çalıştırdığı bir adım. ClassPathBeanDefinitionScanner paketi classpath*:paket/**/*.class desenine çevirir; classpath*: olduğu için aynı paket birden çok jar'daysa hepsi taranır. Her class dosyası MetadataReader ile bytecode olarak okunur, sınıf yüklenmez (ASM; Spring 7 ve Java 24+'ta ClassFile API): bean olmayan binlerce DTO yüklenmez, static blok çalışmaz, classpath'te olmayan tipe referans veren sınıf patlamaz. Sonra exclude ve include filtreleri (varsayılan include meta-annotation'ları da kabul eden bir @Component filtresi), @Conditional, somut/bağımsız sınıf kontrolü, AnnotationBeanNameGenerator ile isim ve kayıt; aynı adla uyumsuz tanım ConflictingBeanDefinitionException verir. Taranan @Configuration'lar özyinelemeli parse edilir. Örneğin büyük bir monolitte açılış yavaşsa önce /actuator/startup ile ölçerim; asıl maliyet çoğu zaman scan değil, açılıştaki uzak çağrılar gibi işler çıkar; spring-context-indexer zaten 6.1'den beri deprecated. Kuralım: uygulamada scan, kütüphanede @Import ve AutoConfiguration.imports; modüller arasını da açık @Import ile bağlarım ki hangi modülün hangi bean'i getirdiği görünsün.
Junior "ne yapar"ı söylüyor. Mid kök paket kuralını, meta-annotation'ı, filtreleri ve isimlendirmeyi biliyor. Senior tarama hattını uçtan uca (deseni, kaynak çözümlemeyi, bytecode okumayı, filtre sırasını, koşulları, isimlendirmeyi, kaydı) ve her adımın nedenini anlatıyor, hata modlarını ve ölçümü ekliyor, kütüphane ile uygulama arasında bilinçli bir tasarım kuralı koyuyor.
Takip sorusu: "Classpath'te ojdbc olmayan bir projede, Oracle'a referans veren bir sınıf taranan pakette duruyor. Spring açılışta neden patlamıyor, hangi durumda patlar?"
Soru: "Spring context nedir? BeanFactory'den farkı ne?"
seviye: Başlangıç etiketler: [spring, application-context] kaynak: Gerçek mülakat
Spring context, Spring'in bean'leri tuttuğu yer. ApplicationContext ile bean'lere erişebiliyoruz. BeanFactory daha eski ve basit bir versiyonu, artık ApplicationContext kullanılıyor.
ApplicationContext, Spring'in IoC container'ı ve BeanFactory'yi genişletiyor. BeanFactory bean tanımlarını tutar ve getBean ile nesne verir; ApplicationContext buna event yayını, Environment ve profiller, i18n, kaynak yükleme ve post-processor'ların otomatik kaydını ekler. Ayrıca singleton'ları açılışta eager oluşturur, bu yüzden konfigürasyon hataları açılışta görünür. Boot'ta web uygulamasında context AnnotationConfigServletWebServerApplicationContext oluyor ve refresh() sırasında bean'ler oluşup Tomcat başlıyor.
Context bir tarif deposu, bir nesne fabrikası ve uygulama hizmetlerinin birleşimi. Altta DefaultListableBeanFactory: beanDefinitionMap'te tarifler, üç seviyeli singleton cache'te nesneler, bir de BPP listesi. ApplicationContext bunun etrafına refresh() şablonunu kuruyor: BFPP'ler tanımları üretip değiştiriyor, BPP'ler sıralı kaydediliyor, onRefresh'te Boot Tomcat'i kuruyor, singleton'lar eager oluşuyor, port finishRefresh'teki lifecycle başlangıcında açılıyor. ApplicationContext'in kendisi bean olmadığı halde enjekte edilebiliyor çünkü prepareBeanFactory onu resolvable dependency olarak kaydediyor. Operasyonel sonuçlarını önemsiyorum: eager oluşturma sayesinde hatalı sürüm readiness'a ulaşmıyor; refresh hata verirse oluşmuş singleton'lar destroy edildiği için yarım context kaynak sızdırmıyor; SIGTERM → close() → SmartLifecycle fazları → destroy callback'leri graceful shutdown'ın temeli. "Singleton" context başına: testte her farklı @MockitoBean kombinasyonu ayrı bir context demek, bu yüzden override'ları ortak bir taban sınıfta toplarım. Klasik MVC'deki root/child ayrımı BPP'lerin neden yanlış context'te "çalışmadığını" açıklıyordu; Boot'ta tek context var, ama management portunu ayırınca Actuator için child context açılıyor.
Junior BeanFactory'yi "eski versiyon" sanıyor; oysa ApplicationContext onu kullanıyor, yerine geçmiyor. Mid ayrımı ve eklenen hizmetleri doğru biliyor. Senior context'i iç yapısıyla (tarif deposu, cache, BPP listesi) tarif ediyor, refresh şablonunu operasyonel sonuçlarına (readiness, kaynak sızıntısı, graceful shutdown, test süresi) bağlıyor ve hiyerarşinin pratik etkisini biliyor.
Takip sorusu: "Bir BeanFactoryPostProcessor'ı @Bean metoduyla tanımladın ve aynı @Configuration sınıfındaki @Value alanları null geldi. Neden?"
Soru: "Uygulama açılırken bir bean bulunamadı hatası alıyorsun. Nasıl çözersin?"
seviye: Başlangıç etiketler: [spring, dependency-injection, debugging] kaynak: Tahmini
Hata mesajında hangi bean'in bulunamadığını görürüm. Genelde o sınıfa @Service veya @Component eklemeyi unutmuşuzdur, eklerim. Olmazsa @ComponentScan ile paketi eklerim.
Boot'un "APPLICATION FAILED TO START" raporu hangi sınıfın hangi constructor parametresinin hangi tipi istediğini söyler. Önce o tipin implementasyonunun bean olup olmadığına bakarım: stereotype annotation var mı, ana sınıfın paketinin altında mı, bir @Profile veya @ConditionalOnProperty yüzünden mi elenmiş. Auto-configuration'dan gelmesi gereken bir bean'se --debug ile conditions report'a bakarım; rapor hangi koşulun tutmadığını yazar. Interface için birden çok implementasyon varsa hata farklı olur, NoUniqueBeanDefinitionException alırım.
Önce hatanın türünü ayırırım, çünkü her biri farklı bir aşamada doğar: NoSuchBeanDefinitionException (tanım yok), NoUniqueBeanDefinitionException (birden çok aday), BeanNotOfRequiredTypeException (bean var, proxy tipi uymuyor). Boot'un failure analyzer'ı hangi constructor parametresinin hangi tipi istediğini, auto-configuration'dan gelmesi gerekiyorsa hangi koşulun tutmadığını yazar. Sonra "tanım neden yok" sorusunu katman katman elerim: (1) scan: sınıf ana paketin altında mı, ana sınıfa ikinci bir @ComponentScan eklenmiş mi; org.springframework.context.annotation DEBUG log'unda "Identified candidate component class" var mı. (2) Koşul: @Profile, @ConditionalOnProperty; aktif profili açılış log'undan doğrularım. (3) Auto-configuration: conditions report; exclude edilmiş veya classpath'te sınıf eksik olabilir. (4) Özel tarayıcılar: repository ise @AutoConfigurationPackage paketinin dışında mı, entity ise @EntityScan gerekiyor mu. (5) Test: @WebMvcTest servisleri TypeExcludeFilter ile dışlar, çözüm @MockitoBean. (6) Tip: generic parametre farkı (Repository<Order> istenip Repository<Cart> tanımlı). Kalıcı düzeltme olarak modülden gelen bean'i açık @Import ile bağlarım ve context'in açıldığını doğrulayan bir smoke test (auto-config için ApplicationContextRunner) eklerim ki hata CI'da yakalansın.
Junior semptomu tek bir nedene bağlıyor ve tahminle @ComponentScan ekliyor; bu, ana sınıftaki meta scan'i bozarak daha büyük bir soruna yol açabilir. Mid doğru teşhis araçlarını biliyor. Senior exception türlerini oluştukları aşamaya göre ayırıyor, nedenleri mekanizma katmanlarına göre sistematik eliyor (scan, koşul, auto-config, özel tarayıcılar, test slice, tip), her katman için bir kanıt aracı veriyor ve hatanın tekrarını önleyen test stratejisiyle kapatıyor.
Takip sorusu: "Lokalde açılıyor, CI'daki @SpringBootTest'te aynı bean bulunamıyor. Nerelere bakarsın?"
Sorulmadı ama sorulabilir
spring.factories ve AutoConfiguration.imports farkı
META-INF/spring.factories, Spring'in genel amaçlı "service loader" dosyasıdır: anahtar bir arayüz adı, değer implementasyon listesidir ve SpringFactoriesLoader bunları reflection ile oluşturur. Boot bunu ApplicationListener, ApplicationContextInitializer, EnvironmentPostProcessor, FailureAnalyzer, SpringApplicationRunListener, AutoConfigurationImportFilter ve Boot 4'te WebApplicationType.Deducer gibi açılış uzantıları için hâlâ kullanır. Auto-configuration kaydı ise Boot 2.7'de ayrı bir dosyaya (META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, satır başına bir sınıf) taşındı ve Boot 3.0'da spring.factories üzerinden auto-configuration desteği kaldırıldı. Ayırmanın sebebi, auto-configuration'ın ayrı bir yaşam döngüsü (deferred import, ön filtre, sıralama) ve araç desteği (metadata processor) olmasıdır.
Mülakatta tek cümle: "spring.factories açılış uzantıları için genel bir SPI dosyası olarak yaşıyor; auto-configuration Boot 3'ten beri yalnızca AutoConfiguration.imports ile kaydediliyor."
@Conditional nasıl değerlendirilir?
@Conditional(SomeCondition.class) bir Condition implementasyonuna işaret eder; matches(ConditionContext, AnnotatedTypeMetadata) true dönerse bileşen kaydedilir. ConditionEvaluator bir elemandaki tüm koşulları toplar, @Order'a göre sıralar ve biri bile false dönerse elemanı atlar. Koşul bir ConfigurationCondition ise fazını bildirir: PARSE_CONFIGURATION (sınıf parse edilmeden önce; @ConditionalOnClass, @Profile) veya REGISTER_BEAN (tanımlar kaydedilirken; @ConditionalOnBean/@ConditionalOnMissingBean). Koşul metadata üzerinden çalışır, yani sınıfı yüklemeden karar verebilir. Boot'un koşulları ayrıca sonucu ConditionEvaluationReport'a yazar; --debug raporu buradan gelir. Bean koşulları "o ana kadar kaydedilmiş tanımlara" bakar; sıra garantisi sadece auto-configuration'larda olduğu için Boot dokümantasyonu bean koşullarını sadece orada kullanmayı önerir.
Mülakatta tek cümle: "Koşullar bean oluşmadan, tanım aşamasında ve iki fazda değerlendirilir; bean koşulları kayıt sırasına bağlı olduğu için onları sadece auto-configuration'da kullanırım."
BeanFactoryPostProcessor ve BeanPostProcessor
BFPP tarifleri (BeanDefinition) değiştirir ve refresh'in 5. adımında, hiçbir uygulama bean'i yokken çalışır: ConfigurationClassPostProcessor (scan, import, @Bean), PropertySourcesPlaceholderConfigurer (${...}). BPP nesneleri işler ve her bean'in oluşturulmasında çalışır: AutowiredAnnotationBeanPostProcessor (enjeksiyon), CommonAnnotationBeanPostProcessor (@PostConstruct), AOP auto-proxy creator (proxy). Sonuçları: BFPP'yi tanımlayan @Bean metodu static olmalı (yoksa içeren konfigürasyon sınıfı çok erken oluşur ve kendi @Value'ları işlenmez); BPP'ler ve bağımlılıkları diğer BPP'lerden önce oluştuğu için proxy'lenmez ("not eligible for getting processed by all BeanPostProcessors" uyarısı, bkz. Bölüm 6).
Mülakatta tek cümle: "BFPP tarife, BPP nesneye dokunur; scan ve placeholder çözümü BFPP, enjeksiyon ve proxy BPP işidir."
@Lazy ve lazy initialization
@Lazy bir bean'in tanımını değil oluşturulma anını değiştirir: preInstantiateSingletons onu atlar, ilk getBean'de oluşur. Enjeksiyon noktasında @Lazy ise gerçek bean yerine, ilk metot çağrısında hedefi çözen bir proxy enjekte eder. spring.main.lazy-initialization=true Boot'ta LazyInitializationBeanFactoryPostProcessor ekleyerek tüm tanımları lazy işaretler (SmartInitializingSingleton gibi bazı tipler hariç). Bedeli fail-fast'in kaybıdır (bkz. Bölüm 6): hatalı bean ilk istekte patlar ve ilk istek yavaşlar. Ayrıca bir bean'i lazy yapsan bile onu eager bir bean'e doğrudan enjekte edersen yine açılışta oluşur.
Mülakatta tek cümle: "Lazy, açılış süresini hatanın ve gecikmenin ilk isteğe taşınmasıyla takas eder; production'da fail-fast'i tercih ederim."
Açılış süresini ölçmek
Spring Framework, refresh'in adımlarını ApplicationStartup arayüzüyle işaretler: spring.context.refresh, spring.context.beans.post-process, spring.beans.instantiate, Boot'ta spring.boot.webserver.create gibi StartupStep'ler. Varsayılan implementasyon hiçbir şey kaydetmez. SpringApplication.setApplicationStartup(new BufferingApplicationStartup(2048)) ile adımlar bellekte tutulur ve Actuator'ın /actuator/startup endpoint'i her bean'in oluşturulma süresini ve hangi bean için oluşturulduğunu JSON olarak verir. FlightRecorderApplicationStartup ise aynı adımları JFR event'i olarak yazar; JDK Mission Control'de diğer JVM olaylarıyla (sınıf yükleme, GC) birlikte görülür. Tipik bulgu: süreyi Spring değil, bir @PostConstruct'taki uzak çağrı veya Hibernate'in metadata kurulumu harcar.
public static void main(String[] args) {
var app = new SpringApplication(CheckoutApplication.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
}
Mülakatta tek cümle: "Açılışı optimize etmeden önce BufferingApplicationStartup + /actuator/startup veya JFR ile bean bazında ölçerim."
Custom starter yazmak: içeride ne olur?
Bölüm 7 iskeleti verdi; bu bölümün diliyle akış şöyledir: starter jar'ı classpath'e girer → AutoConfigurationImportSelector onun AutoConfiguration.imports dosyasını ImportCandidates ile okur → spring-boot-autoconfigure-processor eklendiyse derleme zamanında üretilen spring-autoconfigure-metadata.properties ile @ConditionalOnClass sınıfı okumadan değerlendirilir → kalan sınıf kullanıcının tanımlarından sonra parse edilir → @ConditionalOnMissingBean korumalı @Bean'ler REGISTER_BEAN fazında kaydedilir. Tasarım kuralları bu mekanizmadan çıkar: component scan kullanma (kullanıcının paketine bağımlı olursun), sınıf koşullarını @Bean metodunun dönüş tipine değil ayrı bir nested @Configuration sınıfına koy (Boot dokümantasyonuna göre JVM metot imzasındaki sınıfı koşul değerlendirilmeden yükleyebilir), her şeyi ApplicationContextRunner ile test et.
Mülakatta tek cümle: "Starter'ım imports dosyasıyla, deferred olarak ve koşullarla yüklenir; scan kullanmam, her bean'i @ConditionalOnMissingBean ile ezilebilir bırakırım."
Spring proxy'yi ne zaman oluşturur?
Üç an var. (1) Normal yol: initializeBean'in sonunda, postProcessAfterInitialization'da AbstractAutoProxyCreator bean'i sarar (@Transactional, @Cacheable, aspect'ler); @Async ve @Validated için de aynı fazda kendi BPP'leri. (2) Erken referans: Döngüsel bir referans varsa ve birisi bean'i tamamlanmadan isterse, singletonFactories'teki factory getEarlyBeanReference ile proxy'yi önceden üretir; sonra normal yolda aynı proxy'nin tekrar üretilmemesi için kayıt tutulur. (3) Oluşturmadan önce: resolveBeforeInstantiation'da bir InstantiationAwareBeanPostProcessor (ör. özel bir TargetSource) gerçek nesne yerine proxy dönebilir; nadirdir. Bunlardan ayrı olarak @Configuration sınıflarının CGLIB alt sınıfı BFPP aşamasında, nesne oluşmadan üretilir; @Lazy enjeksiyon noktası ve scoped proxy'ler (@RequestScope) ise enjeksiyon anında oluşturulan ayrı proxy'lerdir.
Mülakatta tek cümle: "AOP proxy'si normalde tüm init callback'lerinden sonra, postProcessAfterInitialization'da doğar; döngü varsa erken referans için daha önce üretilebilir."
Sık yapılan hatalar
- Ana sınıfı bir alt pakete koymak →
@ComponentScanve@AutoConfigurationPackageana sınıfın paketini kök alır; kardeş paketlerdeki bileşenler, repository'ler ve entity'ler bulunmaz → ana sınıfı tüm uygulama paketlerinin üstündeki kök pakete koy; zorunluysascanBasePackages,@EnableJpaRepositories,@EntityScanile açıkça belirt. @SpringBootApplication'ın yanına ayrıca@ComponentScaneklemek → doğrudan tanımlı scan, meta olanı veTypeExcludeFilter/AutoConfigurationExcludeFilter'ı devre dışı bırakır; test slice'ları ve auto-config kaydı bozulur →@SpringBootApplication(scanBasePackages = ...)kullan.- Kendi annotation'ına
@Retention(RUNTIME)koymamak → varsayılanCLASSretention reflection'a görünmez; aspect veya BPP annotation'ı sessizce görmez → runtime'da okunacak her annotation@Retention(RetentionPolicy.RUNTIME)taşımalı. isAnnotationPresent(Component.class)ile "bu bir bean mi" diye bakmak → JDK meta-annotation çözmez,@Serviceiçin false döner →AnnotatedElementUtils.hasAnnotationveyaMergedAnnotationskullan.- Composed annotation'da attribute'u
@AliasForolmadan tanımlamak → değer meta-annotation'a geçmez; Spring 6.1+'da convention-based stereotype adı deprecated →@AliasFor(annotation = Component.class)ile açıkça bağla. - Auto-configuration'ı component scan ile yüklemek veya auto-config içinde
@ComponentScanaçmak → sıra ve koşul garantisi kaybolur, bean'ler iki kez kaydolabilir → sadeceAutoConfiguration.importsve@Import. - Kendi
@Configurationsınıfında@ConditionalOnMissingBeankullanmak → bean koşulu o ana kadar kaydedilen tanımlara bakar, sonuç parse sırasına bağlıdır → bu koşulu sadece auto-configuration'da kullan. - Farklı paketlerde aynı kısa adlı iki
@Componentsınıfı → ikisi deorderMapperadını alır,ConflictingBeanDefinitionException→ birine açık bean adı ver veyaFullyQualifiedAnnotationBeanNameGeneratorkullan. ApplicationStartingEventveyaApplicationEnvironmentPreparedEvent'i@EventListenerbean'i ile dinlemeye çalışmak → bu event'ler context oluşmadan yayınlanır, bean yoktur, listener hiç çağrılmaz → listener'ıspring.factoriesveyaSpringApplication.addListenersile kaydet.@PropertySourceilelogging.*veyaspring.main.*ayarlamaya çalışmak →@PropertySourcerefresh'te, konfigürasyon parse edilirken eklenir; bu ayarlar daha önce okunmuştur →application.yml, env veyaspring.config.importkullan.- Java 17+ geçişinde
InaccessibleObjectException'ı kalıcı--add-opensile kapatmak → kapsülleme deliği kalıcı olur, bir sonraki JDK'da yine kırılabilir → kütüphaneyi güncelle,--add-opensgerekiyorsa geçici ve belgelenmiş olsun. - Testte private alanı
ReflectionTestUtils.setFieldile doldurmak → sınıfın tasarımındaki field injection kokusunu gizler, refactoring'de sessizce kırılır → constructor injection'a geç, testtenewile kur. - Native image hedeflerken
@ConditionalOnPropertyile çalışma anında bean açıp kapatmayı beklemek → AOT koşulları build zamanında değerlendirir → farklı bean setleri için farklı build veya çalışma anı mantığı (feature flag) kullan. - Yeni projeye
spring-context-indexereklemek → 6.1'den beri deprecated (forRemoval) → önce açılışı ölç; gerekiyorsa AOT, CDS/AOT cache. @WebMvcTest'te servis bean'i bulunamayınca@ComponentScanveya@Importile servisi eklemek → slice'ın amacı bozulur, test yavaşlar ve gerçek bağımlılıklara bağlanır → servisi@MockitoBeanile ver.
Hızlı tekrar
S: Annotation'lar kendi başına iş yapar mı?
C: Hayır. Annotation metadata'dır; işi onu okuyan kod yapar (Spring post-processor'ları, JUnit motoru, annotation processor'lar).
S: @Retention'ın üç değeri nedir, reflection hangisini görür?
C: SOURCE (sadece kaynak kod, derleyici ve annotation processor'lar), CLASS (class dosyasında ama runtime'da görünmez, varsayılan), RUNTIME (reflection ile okunabilir). Reflection sadece RUNTIME'ı görür.
S: @Service ile işaretli bir sınıfta isAnnotationPresent(Component.class) ne döner, Spring bunu nasıl çözer?
C: false; JDK meta-annotation çözmez. Spring MergedAnnotations ile meta-annotation hiyerarşisini dolaşır ve @AliasFor kurallarını uygular.
S: JEP 416 neyi değiştirdi?
C: JDK 18'de Method, Constructor, Field method handle'lar üzerinde yeniden yazıldı; eski native/bytecode accessor yolu JDK 22'de kaldırıldı. static final tutulan reflection nesneleri JIT ile sabit kabul edilip hızlanabilir.
S: setAccessible(true) hangi durumda InaccessibleObjectException fırlatır?
C: Hedef üyenin paketi çağıran koda açık (opened) değilse; ör. classpath'teki koddan java.base iç alanlarına erişim. JDK 16'dan beri varsayılan kapalı, JDK 17'de --illegal-access etkisiz hale getirildi; geçici çözüm --add-opens.
S: @SpringBootApplication hangi annotation'lardan oluşur, @EnableAutoConfiguration ne import eder?
C: @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan (iki exclude filtresiyle). @EnableAutoConfiguration = @AutoConfigurationPackage (ana paketi kaydeder) + @Import(AutoConfigurationImportSelector).
S: Auto-configuration'lar neden kullanıcının bean'lerinden sonra işlenir?
C: AutoConfigurationImportSelector bir DeferredImportSelector'dır; ConfigurationClassParser onu tüm kullanıcı konfigürasyonları parse edildikten sonra çalıştırır. Bean koşulları da REGISTER_BEAN fazında değerlendirildiği için @ConditionalOnMissingBean kullanıcı tanımını görür.
S: SpringApplication constructor'ı ne yapar?
C: Primary source'u saklar, web tipini classpath'ten çıkarır (SERVLET/REACTIVE/NONE), spring.factories'ten BootstrapRegistryInitializer, ApplicationContextInitializer ve ApplicationListener'ları yükler, main sınıfını StackWalker ile bulur.
S: refresh()'in hangi adımında bean tanımları, hangisinde bean nesneleri oluşur?
C: Tanımlar invokeBeanFactoryPostProcessors'ta (ConfigurationClassPostProcessor: scan, import, @Bean, auto-config); lazy olmayan singleton nesneleri finishBeanFactoryInitialization'da (preInstantiateSingletons).
S: Embedded Tomcat ne zaman oluşturulur, port ne zaman açılır?
C: onRefresh'te ServletWebServerApplicationContext.createWebServer() ile oluşturulur ama connector'lar çıkarılır; port finishRefresh'te WebServerStartStopLifecycle.start() ile, singleton'lar hazır olduktan sonra açılır.
S: Component scan sınıfları neden JVM'e yüklemeden okur?
C: Hız ve bellek (bean olmayan sınıfları yüklememek), yan etkisizlik (static initializer çalışmaz) ve classpath'te olmayan tiplere referans veren sınıflarda NoClassDefFoundError almamak. ASM, Spring 7 + Java 24+'ta ClassFile API kullanılır.
S: Component scan'de tarama kökü ve bean adı nasıl belirlenir?
C: Kök: basePackages yoksa annotation'ı taşıyan sınıfın paketi. Ad: annotation'da açık ad yoksa kısa sınıf adının küçük harfle başlayan hali (OrderService → orderService, URLResolver aynen kalır).
S: BeanDefinition ile bean arasındaki fark nedir?
C: BeanDefinition tariftir (sınıf/factory metodu, scope, lazy, bağımlılıklar, init/destroy); bean bu tariften oluşturulan nesnedir. BFPP tarifi, BPP nesneyi değiştirir.
S: Singleton cache'te neden üç seviye var?
C: singletonFactories erken referansı (gerekirse proxy'yi) sadece döngüde biri isterse üretir; üretilen erken nesne earlySingletonObjects'e taşınır ki herkes aynısını alsın; tamamlanan bean singletonObjects'e geçer.
S: AOT işleme ne yapar ve en önemli kısıtı nedir?
C: Build zamanında context'i nesne oluşturmadan refresh eder, bean tanımlarını Java koduna çevirir, proxy'leri önceden üretir ve reflection/kaynak ihtiyaçlarını RuntimeHints olarak yazar. Kısıt: classpath ve bean seti sabittir; @Profile ve bean varlığını etkileyen koşullar build zamanında değerlendirilir.
Kaynaklar
- Spring Framework: Classpath Scanning and Managed Components: Meta-annotation, composed annotation,
@AliasForile stereotype adı, filtreler ve bean isimlendirmenin resmi anlatımı. - Spring Framework: Ahead of Time Optimizations:
refreshForAotProcessing, üretilen__BeanDefinitionskodu, AOT kısıtları veRuntimeHintsAPI'si. - Spring Framework: Programmatic Bean Registration: Spring 7 ile gelen
BeanRegistrarve AOT desteği. - Spring Boot: Creating Your Own Auto-configuration:
AutoConfiguration.imports, scan yasağı, sıralama, bean koşullarının sırası ve ASM ile okunan@ConditionalOnClass. - Spring Boot: Structuring Your Code: Ana sınıfın kök pakette olması gerekçesi ve default package uyarısı.
- AbstractApplicationContext.java (Spring Framework, GitHub):
refresh(),prepareBeanFactoryvefinishRefresh'in kaynağı; bu bölümdeki adım tablosunun birincil kaynağı. - ClassPathScanningCandidateComponentProvider.java (Spring Framework, GitHub):
classpath*:deseni,MetadataReaderile okuma, filtreler ve index kullanımı. - SpringApplication.java (Spring Boot, GitHub): Constructor,
run(),prepareContextve runner'ların çağrılması; Boot 4 kaynağı. - The Java Tutorials: The Reflection API: Reflection API'sinin temelleri ve resmi "drawbacks" listesi (JDK 8 dönemi yazılmıştır, temel kavramlar geçerli).
- JEP 416: Reimplement Core Reflection with Method Handles: Reflection'ın JDK 18'deki iç değişikliği ve performans ölçümleri.
- GraalVM: Reachability Metadata: Native image'da reflection'ın neden önceden bildirilmesi gerektiği ve metadata dosyalarının yeri.