Spring · اسپرینگ پایهBeginner ~39 دقیقه مطالعه~32 min read
هستهٔ Spring: IoC، DI، Bean و AOPSpring Core: IoC, DI, Beans & AOP
از صفر یاد میگیری کانتینر Spring چطور اشیایت را میسازد و سیمکشی میکند، چرا constructor injection برنده است، چرخهٔ حیات bean دقیقاً چه ترتیبی دارد، و پراکسیهای CGLIB و AOP چطور کار میکنند — با تشبیه، کد و پرسشهای مصاحبه.Learn from scratch how Spring's container builds and wires your objects, why constructor injection wins, the exact bean lifecycle order, and how CGLIB and AOP proxies really work — through analogies, code, and interview questions.
بیا با یک تصویر ساده شروع کنیم. فرض کن یک رستوران راه انداختهای. در روش قدیمی، هر آشپز باید خودش برود بازار، گوشت بخرد، سبزی بشوید، چاقو تیز کند و اجاق روشن کند تا بتواند یک غذا بپزد. آشوب میشود. در یک رستوران حرفهای اما یک نفر (یا یک سیستم) مسئول تهیهٔ همهچیز است؛ آشپز فقط اعلام میکند «من گوشت و سبزی و اجاق میخواهم» و همهچیز آماده جلویش میآید. آشپز روی کاری تمرکز میکند که فقط از او برمیآید: پختن.
هستهٔ Spring دقیقاً همان «سیستم تدارکات» است. این درس همهچیز را از پایه میسازد.
در این فصل اینها را یاد میگیری و هر کدام را از صفر باز میکنیم:
- کانتینر، IoC و DI — چرا دیگر خودت
newنمیزنی و این چه سودی دارد. - BeanFactory در برابر ApplicationContext — دو نوع کانتینر و اینکه در عمل کدام را استفاده میکنی.
- سه سبک تزریق (constructor، setter، field) و چرا constructor برندهٔ بیچونوچراست.
- چرخهٔ حیات bean — ترتیب دقیقِ تولد تا مرگ یک شیء.
- Scopeها و تلهٔ کلاسیک «تزریق prototype در singleton».
- پراکسی CGLIB برای @Configuration و دام حالت «lite».
- وابستگیهای چرخهای و اینکه چه زمانی Spring میبُرد.
- AOP، پراکسی JDK/CGLIB و دام self-invocation.
- یک بخش کامل پرسشوپاسخ مصاحبه در انتها.
بخش صفر — واژههایی که باید بلد باشی
قبل از هر چیز، چند واژه را با تشبیه جا بیندازیم تا در ادامه هیچجا گیر نکنی.
تصور کن یک کارخانهٔ مبلمان داری. کانتینر (container) خودِ کارخانه است: جایی که اشیاء ساخته، مونتاژ و در پایان عمر اوراق میشوند. هر مبلی که از خط تولید بیرون میآید یک bean است — شیئی که کارخانه مسئول ساخت و نگهداریاش است. و BeanDefinition همان نقشهٔ فنی روی کاغذ است: از چه چوبی، با چه اندازهای، به کدام پیچها نیاز دارد، چطور مونتاژ و چطور اوراق شود. کارخانه اول نقشه را میخواند، بعد میسازد.
- کانتینر (container): موتوری که ساخت، سیمکشی، پیکربندی و نابودی اشیایت را در دست میگیرد.
- bean: هر شیئی که چرخهٔ حیاتش را کانتینر مدیریت میکند.
- BeanDefinition: فراداده (metadata) دربارهٔ یک bean — کلاسش، scope، وابستگیهایش، متدهای init/destroy و پرچم lazy. کانتینر این نقشه را میخواند تا bean را بسازد.
- پراکسی (proxy): یک شیء «بدل» که جلوی شیء اصلی میایستد و پیش/پس از فراخوانی متدها کاری اضافه انجام میدهد. کمی جلوتر مفصل بازش میکنیم؛ فعلاً بدان که «نگهبانِ دمِ در» است.
هرچیز جذابی که در Spring میبینی — تراکنشها، امنیت، caching، @Async — روی همین دو ستون سوار است: کانتینر بهعلاوهٔ AOP. اگر این دو را عمیق بفهمی، بقیهٔ Spring دیگر «جادو» نیست، مهندسی قابلپیشبینی است.
کانتینر واقعاً چه کار میکند؟ (IoC و DI)
بیا به همان رستوران برگردیم. وقتی آشپز خودش میرفت بازار خرید میکرد، کنترل در دست آشپز بود. وقتی سیستم تدارکات همهچیز را جلویش میگذارد، کنترلِ «تهیهٔ ملزومات» از آشپز گرفته و به سیستم داده شده. به این وارونهشدنِ مسئولیت میگویند وارونگی کنترل.
«وارونگی کنترل» (Inversion of Control – IoC) یعنی دیگر خودت روی همکارها (collaborators — یعنی اشیائی که کلاست به آنها نیاز دارد) new نمیزنی و چرخهٔ حیاتشان را مدیریت نمیکنی؛ فقط اعلام میکنی به چه چیزی نیاز داری و کانتینر آن را به تو تحویل میدهد.
تزریق وابستگی (Dependency Injection – DI) مکانیزم مشخصی است که IoC را پیاده میکند: وابستگیها به داخل شیء هل داده میشوند (push)، نه اینکه شیء خودش برود آنها را بکشد (pull).
فرقِ «کشیدن» و «هلدادن» را اینطور ببین: در حالت pull، تو تشنه میشوی، بلند میشوی، میروی سرِ چاه و آب میکشی — خودت مسئول پیداکردن و آوردن آبی. در حالت push (تزریق)، پیشخدمت لیوان آب را میآورد و جلویت میگذارد؛ تو فقط اعلام کردهای که آب میخواهی. کلاست هم در DI فقط میگوید «من به یک PaymentGateway نیاز دارم» و کانتینر آن را میآورد و در دستش میگذارد.
فایدهٔ اصلی این کار جادو نیست، کاهش وابستگی (decoupling) است. کلاست به یک اینترفیس وابسته است، کانتینر تصمیم میگیرد کدام پیادهسازی را بدهد، و تو میتوانی بدون دستزدن به مصرفکننده، پیادهسازی را عوض، mock (نمونهٔ ساختگی برای تست) یا بازپیکربندی کنی.
تو چهچیزی میخواهی را اعلام میکنی؛ کانتینر چطور و چهوقتِ ساختن و تحویلدادنش را برعهده میگیرد. همین جدایی «چه» از «چطور» است که کدت را قابلتست و قابلتغییر میکند.
دو نوع کانتینر: BeanFactory و ApplicationContext
Spring دو اینترفیس کانتینر دارد و دانستن فرقشان مهم است.
BeanFactory— قرارداد کمینه و تنبل (lazy) برای IoC. تنبل یعنی beanها را تا لحظهای که برای اولینبارgetBean()صدا نزنی نمیسازد. بهندرت مستقیم استفاده میشود.ApplicationContext— یک اَبَرمجموعه از BeanFactory با امکانات بیشتر:- ساخت زودهنگام (eager) singletonها هنگام بالاآمدن برنامه (نه لحظهٔ درخواست).
- انتشار رویداد با
ApplicationEventPublisher. - بینالمللیسازی (i18n) با
MessageSource. - بارگذاری منابع (resource loading).
- کشف خودکار
BeanPostProcessorوBeanFactoryPostProcessor(اینها را جلوتر باز میکنیم).
در عمل تقریباً همیشه ApplicationContext را استفاده میکنی — مثل AnnotationConfigApplicationContext یا GenericWebApplicationContext.
// ساخت یک کانتینر از روی کلاس پیکربندی جاوایی
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
OrderService svc = ctx.getBean(OrderService.class);
svc.place(new Order());
} // close() فراخوانیهای نابودی singletonها را trigger میکند
تفاوت lazy و eager را اینطور بهخاطر بسپار: BeanFactory مثل رستورانی است که فقط وقتی سفارش میدهی غذا را میپزد؛ ApplicationContext مثل بوفهای است که همهچیز را از اول صبح آماده کرده تا بهمحض ورودت آماده باشد. زودهنگامبودن یعنی اگر جایی در سیمکشی خطا باشد، همان لحظهٔ استارتاپ میفهمی، نه وسط کار کاربر.
یک نکتهٔ ظریف: post-processorها میتوانند BeanDefinitionها را پیش از ساختهشدن هر beanی تغییر دهند — بخش بزرگی از آن چیزی که «جادوی Spring» مینامیم دقیقاً همینگونه پیاده شده است.
سه سبک تزریق وابستگی
سه نقطه برای تزریق وجود دارد: constructor (سازنده)، setter (متد ستکننده) و field (مستقیم روی فیلد). اینها معادل هم نیستند و انتخاب بینشان مهم است.
@Service
class ConstructorInjected {
private final PaymentGateway gateway; // میتواند final باشد
private final InventoryClient inventory;
// از Spring 4.3 به بعد، اگر یک constructor واحد باشد @Autowired اختیاری است
ConstructorInjected(PaymentGateway gateway, InventoryClient inventory) {
this.gateway = gateway;
this.inventory = inventory;
}
}
@Service
class FieldInjected {
@Autowired private PaymentGateway gateway; // نمیتواند final باشد؛ با reflection ست میشود
}
@Service
class SetterInjected {
private PaymentGateway gateway;
@Autowired void setGateway(PaymentGateway g) { this.gateway = g; }
}
- constructor injection مثل ماشینی است که موتور، فرمان و چرخهایش سرِ خط تولید نصب شده؛ وقتی از کارخانه بیرون میآید، کامل است و راه میافتد.
- setter injection مثل ماشینی است که بدون رادیو تحویلت میدهند و بعداً خودت رادیو را نصب میکنی — برای چیزهای اختیاری خوب است.
- field injection مثل این است که کسی از پشت، مخفیانه و بدون در، دست کند داخل داشبورد و سیمها را وصل کند (reflection). کار میکند، اما نه شفاف است، نه قابلکنترل.
چرا constructor injection پاسخ پیشفرض است؟
۱. تغییرناپذیری (immutability) و امنیت در نخ (thread-safety). فیلدها میتوانند final باشند؛ شیء وقتی از constructor برمیگردد کاملاً مقداردهی شده است. هیچ بازهای نیست که bean موجود باشد اما نیمی از وابستگیهایش null. («تغییرناپذیر» یعنی بعد از ساخت دیگر تغییر نمیکند؛ همین باعث میشود چند نخ بتوانند بیخطر همزمان استفادهاش کنند.)
۲. شکست سریع (fail-fast) در نبود یا ابهام وابستگی. آرگومان اجباریِ constructor که قابل resolve نباشد، همان لحظهٔ بالاآمدن context شکست میخورد، نه بعداً وسط کار با یک NPE (NullPointerException) مرموز.
۳. قابلیت تست بدون Spring. میتوانی در یک تست ساده JUnit بنویسی new ConstructorInjected(mock1, mock2). اما field injection مجبورت میکند یا reflection بزنی یا کل context را بالا بیاوری.
۴. آشکارکردن بوی بد طراحی (design smell). constructor با ۸ پارامتر فریاد میزند که این کلاس کار زیادی میکند. field injection این درد را پنهان میکند و کلاس بیسروصدا به یک god object (شیئی که همهکاره و بیشازحد بزرگ است) تبدیل میشود.
۵. جلوگیری از فرارِ bean نیمهساخته. setter/field injection اجازه میدهند شیء پیش از تکمیل سیمکشی استفاده شود (مثلاً درون یک callback چرخهٔ حیات).
setter injection برای وابستگیهای اختیاری یا حالت قابل بازپیکربندی بهدرستی مفید است. اما تنها مزیت واقعی field injection «کوتاهی کد» بود — و Lombok مدرن با @RequiredArgsConstructor همان مزیت را میدهد بیآنکه ضررهایش را داشته باشد. خودِ مستندات Spring Framework و راهنمای Spring Boot برای وابستگیهای اجباری، constructor injection را توصیه میکنند.
یک مزیت ظریف دیگر: constructor injection مخفیکردن وابستگی چرخهای را ناممکن میکند. Spring نمیتواند A را که به B نیاز دارد بسازد در حالی که هنوز مشغول ساخت B است که به A نیاز دارد؛ پس بهجای کارکردن مرموز، سرِ استارتاپ فریاد میزند (در بخش وابستگی چرخهای مفصل میبینی).
چرخهٔ حیات bean
درک ترتیب دقیقِ تولد تا مرگ یک bean، اغلب همان چیزی است که یک برنامهنویس ارشد را از میانی جدا میکند.
تولد یک bean مثل استخدام و راهانداختن یک کارمند است: اول استخدام میشود (ساخت شیء)، بعد کارت و لپتاپ و دسترسیها را میگیرد (مقداردهی وابستگیها)، بعد معرفی میشود که کجا کار میکند و رئیسش کیست (callbackهای Aware)، بعد دورهٔ آموزشی میبیند (@PostConstruct و init)، بعد شروع به کار میکند (در سرویس)، و در پایان تسویهحساب و خروج (@PreDestroy و destroy). هر مرحله ترتیب مشخصی دارد.
ساخت نمونه (constructor)
↓
مقداردهی propertyها (field/setter DI)
↓
callbackهای Aware (BeanNameAware، BeanFactoryAware، ApplicationContextAware)
↓
BeanPostProcessor.postProcessBeforeInitialization
↓
@PostConstruct
↓
InitializingBean.afterPropertiesSet()
↓
init-method سفارشی (@Bean(initMethod=...))
↓
BeanPostProcessor.postProcessAfterInitialization ← پراکسیهای AOP اینجا ساخته میشوند
↓
[ bean در حال سرویسدهی ]
↓ (هنگام خاموشی context، فقط singletonها)
@PreDestroy
↓
DisposableBean.destroy()
↓
destroy-method سفارشی
@Component
class LifecycleDemo implements InitializingBean, DisposableBean, BeanNameAware {
@Override public void setBeanName(String name) { /* سوم */ }
@PostConstruct void warmUp() { /* پنجم — hook ترجیحی برای init */ }
@Override public void afterPropertiesSet() { /* ششم */ }
@PreDestroy void flush() { /* هنگام خاموشی */ }
@Override public void destroy() { /* بعد از @PreDestroy */ }
}
@PostConstruct و @PreDestroy از استاندارد JSR-250 میآیند (در Spring Boot 3 / Jakarta در پکیج jakarta.annotation قرار دارند). اینها را به اینترفیسهای Spring مثل InitializingBean و DisposableBean ترجیح بده، چون annotationها کدت را به Spring گره نمیزنند — همان کلاس در یک پروژهٔ غیرSpring هم معنا دارد. کد کموابسته، کد سالم است.
BeanPostProcessor در برابر BeanFactoryPostProcessor
این دو اسم شبیهاند اما دو کار کاملاً متفاوت میکنند.
BeanFactoryPostProcessor مثل کسی است که پیش از شروع تولید، روی نقشهها ور میرود: هنوز هیچ محصولی ساخته نشده، او فقط نقشهٔ فنی (BeanDefinition) را ویرایش میکند. اما BeanPostProcessor مثل بازرسی سرِ خط تولید است که هر محصول را که رد میشود برمیدارد، رویش کاری انجام میدهد (مثلاً درون یک جعبهٔ محافظ میگذاردش) و برمیگرداند.
BeanFactoryPostProcessorیکبار اجرا میشود، پس از بارگذاری تعریفهای bean اما پیش از ساخت هر beanی. فرادادهٔ پیکربندی را تغییر میدهد. مثال:PropertySourcesPlaceholderConfigurerکه مقادیر${...}را resolve میکند.BeanPostProcessorبهازای هر bean اجرا میشود و دورِ مقداردهی اولیه را میپیچد. این همان نقطهٔ توسعهای است که Spring در آن پراکسی میسازد. مثالها:AutowiredAnnotationBeanPostProcessor(که@Autowiredرا پردازش میکند) وAnnotationAwareAspectJAutoProxyCreator(که پراکسیهای AOP را میسازد).
چون پراکسیها در postProcessAfterInitialization ساخته میشوند، مرجعی که پس از پایان کار کانتینر در دست میگیری اغلب یک پراکسی است، نه bean خامِ خودت. این یک جمله را نگه دار؛ در بخش AOP میبینی که ریشهٔ اصلی معروفترین باگ Spring دقیقاً همین است.
Scopeهای bean
Scope یعنی «دامنهٔ زندگی» یک bean: کانتینر چند نمونه از آن میسازد و هر نمونه چقدر عمر میکند.
| Scope | معنا | تعداد نمونه |
|---|---|---|
singleton (پیشفرض) |
یک نمونه بهازای هر کانتینر | ۱ |
prototype |
نمونهٔ جدید در هر تزریق/getBean |
N |
request |
یکی بهازای هر درخواست HTTP | بهازای هر request |
session |
یکی بهازای هر session HTTP | بهازای هر session |
application |
یکی بهازای هر ServletContext |
۱ |
websocket |
یکی بهازای هر session وبسوکت | بهازای هر socket |
اول: singleton یعنی «یکی بهازای هر کانتینر Spring»، نه الگوی Singleton در GoF (که یکی بهازای کل JVM است). اگر دو کانتینر داشته باشی، دو نمونه داری.
دوم: Spring چرخهٔ حیات کاملِ beanهای prototype را مدیریت نمیکند. آن را میسازد، سیمکشی میکند و callbackهای init را اجرا میکند، اما بعد bean را فراموش میکند. یعنی @PreDestroy روی یک prototype هرگز فراخوانی نمیشود. اگر prototype منبعی (مثلاً یک فایل یا کانکشن) نگه دارد، باید خودت آزادش کنی.
تلهٔ کلاسیک: تزریق prototype در singleton
singleton فقط یکبار و هنگام ساختش سیمکشی میشود. حالا فرض کن یک آبدارخانه (singleton) داری که قرار است هر بار یک لیوان یکبارمصرفِ تازه (prototype) بدهد. اما چون آبدارخانه فقط یکبار مبله شده، همان لیوان اولی برای همیشه به دیوارش میخ میشود — هر بار همان لیوان کهنه را میگیری. کل ایدهٔ «یکبارمصرف» نقض شده است.
@Component @Scope("prototype")
class Task { /* هربار یک نمونهٔ جدید میخواهیم */ }
@Component
class Worker {
@Autowired private Task task; // باگ: یکبار resolve میشود، برای همیشه همان Task
}
راهحلها، از بهترین به پایین:
۱. ObjectProvider<T> / Provider<T> (JSR-330) — تمیزترین راه. بهجای خودِ prototype، یک «کارخانهٔ تنبل» تزریق میکنی و هر بار که واقعاً لازم داری یک نمونهٔ تازه میسازی. صریح، قابلتست و بدون جادوی پراکسی.
@Component
class Worker {
private final ObjectProvider<Task> taskProvider; // کارخانهٔ تنبل (lazy factory)
Worker(ObjectProvider<Task> taskProvider) { this.taskProvider = taskProvider; }
void run() {
Task fresh = taskProvider.getObject(); // هر فراخوانی یک prototype جدید
fresh.execute();
}
}
۲. پراکسی scopeدار (scoped proxy) — بهجای prototype یک پراکسی میگذاری تا هر فراخوانی متد یک نمونهٔ تازه resolve کند:
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
class Task { }
۳. تزریق متدی با @Lookup — Spring یک متد abstract/lookup را با CGLIB (کتابخانهای که در زمان اجرا subclass میسازد) override میکند تا هر بار bean تازه برگرداند:
@Component
abstract class Worker {
void run() { createTask().execute(); }
@Lookup protected abstract Task createTask(); // توسط کانتینر override میشود
}
دقیقاً همین دام «مرجع کهنه (stale reference)» هنگام تزریق یک bean با scope از نوع request یا session در یک singleton هم رخ میدهد — با این تفاوت که آنجا مجبوری از پراکسی scopeدار (یا ObjectProvider) استفاده کنی، چون در لحظهٔ ساخت singleton اصلاً هیچ requestِ فعالی وجود ندارد که نمونهاش را بدهد.
@Configuration، @Bean و پراکسی CGLIB
این یکی از محبوبترین دامهای مصاحبه است. با دقت بخوانش.
@Configuration
class AppConfig {
@Bean ClientA clientA() { return new ClientA(shared()); }
@Bean ClientB clientB() { return new ClientB(shared()); }
@Bean Shared shared() { return new Shared(); }
}
سؤال: چند نمونهٔ Shared ساخته میشود؟ در نگاه اول انگار دو بار shared() صدا زده شده، پس دو نمونه. اما جواب یکی است.
با @Configuration، Spring کلاس پیکربندی تو را با CGLIB بهارث میبرد (یک subclass مخفی میسازد) و مثل یک منشی هوشمند جلوی هر فراخوانی بینبینیِ متد میایستد. بار اول که shared() صدا زده میشود منشی میگذارد شیء ساخته و در کانتینر ذخیره شود. بار دوم، منشی میگوید «صبر کن، این را قبلاً ساختهایم» و همان نمونهٔ آماده را برمیگرداند — نه یک شیء نو. به این حالت «full» mode برای @Configuration میگویند.
حالا خودِ دام — حالت «lite»:
@Component // نه @Configuration
class LiteConfig {
@Bean ClientA clientA() { return new ClientA(shared()); }
@Bean Shared shared() { return new Shared(); } // مثل یک متد ساده فراخوانی میشود
}
متدهای @Bean که روی کلاسی اعلام شوند که @Configuration نیست (مثلاً روی @Component، یا با @Configuration(proxyBeanMethods = false)) توسط CGLIB رهگیری نمیشوند. یعنی منشی هوشمندی در کار نیست. هر فراخوانی مستقیم shared() متد واقعی را دوباره اجرا میکند → چند نمونهٔ Shared. توجه: beanهایی که خودِ کانتینر ثبت میکند همچنان singleton هستند، اما فراخوانی مستقیم متد به متد، کانتینر را دور میزند.
proxyBeanMethods = false (از Spring 5.2 معرفی شد) یک بهینهسازی کارایی است: از subclassکردن با CGLIB و هزینهٔ استارتاپِ ساختِ پراکسی صرفنظر میکند. فقط زمانی استفادهاش کن که متدهای @Bean همدیگر را صدا نمیزنند. کلاسهای auto-configuration در Spring Boot برای استارتاپ سریعتر بهشدت از آن استفاده میکنند.
@Configuration(proxyBeanMethods = false) // سریعتر، اما بدون رهگیری بینبینی
class FastConfig { /* متدهای @Bean نباید یکدیگر را صدا بزنند */ }
اسکن کامپوننتها و stereotypeها
@ComponentScan (که بهطور ضمنی داخل @SpringBootApplication هست) پکیجها را میپیماید، کلاسهای دارای annotationِ stereotype را پیدا میکند و آنها را بهعنوان bean ثبت میکند. «stereotype» یعنی برچسبی که نقش کلاس را اعلام میکند.
@Component— برچسب عمومی.@Service،@Repository،@Controller،@RestController— تخصصیهای@Component. از نظر معنایی برچسب نقشاند، اما دو تایشان کار اضافه هم میکنند:@Repositoryعلاوه بر برچسب، ترجمهٔ استثنای persistence را فعال میکند: استثناهای وابسته به vendor در JDBC/JPA به سلسلهمراتب یکدستDataAccessExceptionدر Spring تبدیل میشوند.@Controller/@RestControllerتوسط Spring MVC مدیریت و به مسیرهای HTTP وصل میشوند.
وقتی چند کاندیدا با یک نوع match میشوند، Spring باید تصمیم بگیرد کدام را تزریق کند:
@Autowired
public Notifier(@Qualifier("sms") Notifier n) { ... } // انتخاب با qualifier
@Component @Primary // برندهٔ پیشفرض در حالت ابهام
class EmailNotifier implements Notifier {}
سه لایه را بهخاطر بسپار: @Primary یک پیشفرض تعیین میکند («اگر مشخص نکردی، این را بردار»)؛ @Qualifier در هر نقطهٔ تزریق مشخصتر است و @Primary را override میکند؛ و اگر هیچکدام نبود، تطبیق نام آخرین تایبریک است — فیلدی به نام smsNotifier با beanی به نام smsNotifier match میشود. از Spring 6 / Boot 3 میتوانی از annotationهای qualifier سفارشی یا @Fallback (نسخهٔ 6.2) هم استفاده کنی.
وابستگیهای چرخهای (circular dependencies)
فرض کن A به B نیاز دارد و B هم به A. این یک معمای مرغوتخممرغ است: کدام اول ساخته شود؟
تصور کن دو کارگر داری. کارگر A میگوید «تا آچار کاملِ B را نگیرم شروع نمیکنم» و کارگر B هم میگوید «تا آچار کاملِ A را نگیرم شروع نمیکنم». اگر هر دو تا لحظهٔ تولد به دیگری وابسته باشند (constructor)، هیچکدام هرگز شروع نمیشود — بنبست. اما اگر هرکدام بتواند اول بهدنیا بیاید و ابزار طرف مقابل را بعداً بگیرد (setter)، میشود کلاف را باز کرد: اول هر دو را میسازی، بعد ابزارها را رد و بدل میکنی.
setter/field injection: Spring گاهی میتواند این را با کش سهسطحی (three-level cache) و «مرجعهای زودهنگام bean» حل کند. A را میسازد، یک مرجع زودهنگامِ خام از A را در کش singleton قرار میدهد، همان A زودهنگام را در B تزریق میکند، B را کامل میکند و سپس A را. این کار میکند چون ساخت و مقداردهی دو فاز جدا هستند.
constructor injection: Spring نمیتواند آن را حل کند — constructor مربوط به A به یک B کاملاً ساختهشده نیاز دارد و بالعکس؛ مرغوتخممرغ بدون هیچ مرجع زودهنگامی برای تحویل. نتیجه: BeanCurrentlyInCreationException هنگام بالاآمدن.
چرخهٔ constructor → BeanCurrentlyInCreationException (شکست استارتاپ — خوب، fail fast)
چرخهٔ setter/field → با کش مرجع زودهنگام حل میشود (کار میکند — اما بوی بد طراحی است)
از Spring Boot 2.6 به بعد، مراجع چرخهای بهطور پیشفرض ممنوعاند؛ حتی حالت setter/field هم شکست میخورد مگر اینکه spring.main.allow-circular-references=true را ست کنی. اما راهحل درست تقریباً هرگز روشنکردن این پرچم نیست — بلکه شکستن چرخه است: یک همکار سوم استخراج کن، از اینترفیس/رویداد استفاده کن، یا یک مرجع @Lazy/ObjectProvider تزریق کن تا یک طرف در نخستین استفاده resolve شود.
@Service
class A {
private final B b;
A(@Lazy B b) { this.b = b; } // @Lazy یک پراکسی تزریق میکند → چرخهٔ ساخت را میشکند
}
AOP: دغدغههای عرضی (cross-cutting concerns)
بعضی دغدغهها در دهها کلاس مختلف تکرار میشوند: لاگگرفتن، شروع/پایان تراکنش، بررسی دسترسی، اندازهگیری زمان، caching. اگر اینها را داخل هر متد بنویسی، کدت پر از تکرار میشود.
تصور کن هر اتاق در یک ساختمان کاری تخصصی انجام میدهد. حالا میخواهی ورود و خروج به هر اتاق ثبت شود و هر ورود بررسی امنیتی شود. راه احمقانه این است که داخل هر اتاق یک دفترچه و یک نگهبان بگذاری. راه هوشمندانه: یک نگهبان واحد دمِ در اصلی بگذاری که پیش و پس از هر رفتوآمد کارش را انجام دهد. اتاقها اصلاً نمیدانند نگهبانی وجود دارد. AOP دقیقاً همین نگهبانِ دمِ در است.
AOP این دغدغههای مشترک را به aspectها استخراج میکند که بهصورت اعلانی (declarative) — یعنی با یک annotation، نه با کد دستی داخل هر متد — اعمال میشوند. اصطلاحات کلیدی:
- Join point — نقطهای در اجرا که advice میتواند اعمال شود (در Spring AOP همیشه یک اجرای متد).
- Pointcut — یک گزاره (predicate) که join pointها را انتخاب میکند؛ مثلاً
execution(* com.acme.service..*(..))یعنی «هر متدی در پکیج service». - Advice — خودِ عملی که انجام میشود:
@Before(قبل)،@AfterReturning(بعد از بازگشت موفق)،@AfterThrowing(بعد از استثنا)،@After(در هر حال)،@Around(دورتادور، قدرتمندترین). - Aspect — کلاسی که pointcutها و adviceها را بستهبندی میکند (خودِ «نگهبان»).
- Weaving — عملِ چسباندن aspectها به کد. Spring از weaving مبتنی بر پراکسی در زمان اجرا استفاده میکند، نه weaving در زمان کامپایل/بارگذاریِ AspectJ (مگر خودت انتخابش کنی).
@Aspect @Component
class TimingAspect {
@Around("execution(* com.acme..*Service.*(..))")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long t0 = System.nanoTime();
try {
return pjp.proceed(); // متد واقعی را فراخوانی کن
} finally {
long us = (System.nanoTime() - t0) / 1000;
System.out.println(pjp.getSignature() + " took " + us + "us");
}
}
}
پراکسی داینامیک JDK در برابر CGLIB
حالا وقتش است بفهمیم آن «نگهبان» چطور ساخته میشود. Spring پراکسی را در زمان post-processing مربوط به bean میسازد و دو راه دارد:
| پراکسی داینامیک JDK | CGLIB | |
|---|---|---|
| نیازمند | target یک اینترفیس پیادهسازی کند | هر کلاس concrete |
| نحوه | java.lang.reflect.Proxy که اینترفیس را پیاده میکند |
subclass از target در زمان اجرا |
| محدودیت | فقط متدهای اینترفیس advise میشوند | کلاس/متد final قابل پراکسی نیست |
| پیشفرض | وقتی اینترفیس موجود باشد (کلاسیک) | Spring Boot بهطور پیشفرض این را اجبار میکند |
پراکسی JDK مثل بدلکاری است که همان قراردادِ نقش (اینترفیس) را امضا میکند: فقط کارهایی را میتواند انجام دهد که در قرارداد نوشته شده. CGLIB مثل بدلکاری است که گریمِ کاملِ خودِ بازیگر را میزند (subclass میشود): میتواند جای شخصِ concrete بنشیند، اما اگر بازیگر «قفلشده» باشد (final)، گریم غیرممکن است.
از نظر تاریخی Spring اگر bean دستکم یک اینترفیس پیاده میکرد از پراکسی JDK استفاده میکرد، وگرنه CGLIB. اما Spring Boot پیشفرض را به proxyTargetClass = true تغییر داد (یعنی همیشه CGLIB)، چون پراکسی مبتنی بر اینترفیس وقتی کد انتظار نوع concrete را داشت ClassCastException غیرمنتظره میساخت. هر دو را بدان، چون تعیین میکند bean تزریقشده را به چه نوعی میتوانی cast کنی.
دام self-invocation (محبوب سطح ارشد)
یادت هست نگهبان فقط دمِ در اصلی بود؟ حالا فرض کن داخل اتاق، یک کارمند از درِ پشتیِ داخلی به اتاق بغلی برود. نگهبان اصلاً او را نمیبیند، چون از در اصلی رد نشد. در Spring، advice روی پراکسی (در اصلی) نشسته. وقتی متدی از داخلِ همان شیء متد دیگری را با this.method() صدا میزند، از درِ پشتی رفته و کاملاً پراکسی را دور زده — پس هیچ adviceای اجرا نمیشود.
@Service
class OrderService {
@Transactional
public void outer() {
inner(); // باگ: 'this.inner()' — پراکسی نیست → @Transactional نادیده گرفته میشود
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* انتظار تراکنش جدید؛ چیزی نمیگیرد/تراکنش outer را میگیرد */ }
}
outer() روی پراکسی صدا زده میشود (پس advise میشود)، اما inner() بهصورت this.inner() فراخوانی میشود — یک فراخوانی مستقیم روی نمونهٔ خامِ target — پس advice تراکنش برای inner هرگز فعال نمیشود. همین باگ عیناً روی @Cacheable، @Async، @Retryable و هر annotationِ AOP دیگری هم رخ میدهد.
راهحلها:
۱. inner() را به یک bean جدا منتقل کن و تزریقش کن؛ حالا فراخوانی از پراکسیِ آن bean عبور میکند. تمیزترین راه.
۲. پراکسی را به خودت تزریق کن (self-inject) و self.inner() را صدا بزن (از @Lazy یا ObjectProvider استفاده کن تا چرخهٔ ساخت پیش نیاید).
۳. از AopContext.currentProxy() با @EnableAspectJAutoProxy(exposeProxy = true) استفاده کن — کار میکند اما کد را به Spring AOP گره میزند؛ تا جای ممکن اجتنابش کن.
AOP پراکسیشده در پراکسیهای CGLIBبنیانِ Spring فقط روی متدهای public اعمال میشود — متدهای private/protected برای پراکسی نامرئیاند. و گذاشتن @Transactional روی متد private یا final بیسروصدا هیچ کاری نمیکند؛ نه خطایی، نه هشداری، فقط کار نمیکند. این دقیقاً همان نوع باگی است که ساعتها وقت میگیرد.
دامها و نکات ظریف رایج
@AutowiredرویOptional<T>/@Nullable/ObjectProvider<T>یک وابستگی اختیاری را نشان میدهد؛ اما یک نوع اجباریِ ساده در نبودِ bean،NoSuchBeanDefinitionExceptionپرتاب میکند.- field injection +
finalناممکن است؛ field injection همچنین تضمینهای تغییرناپذیریِ مبتنی بر constructor را میشکند. @Value("${prop}")بهPropertySourcesPlaceholderConfigurerنیاز دارد (Boot خودکار فراهمش میکند). اگر property موجود نباشد → شکست استارتاپ، مگر اینکه از${prop:default}(مقدار پیشفرض) استفاده کنی.@PreDestroyروی prototype هرگز اجرا نمیشود. پاکسازی prototype را دستی، یا باDisposableBeanای که خودت صدا میزنی، مدیریت کن.- دو کلاس
@Configurationکه هرکدام@Beanای با نام یکسان اعلام کنند → ثبتِ دیرتر جایگزین میشود؛ اما Boot بهطور پیشفرض override را ممنوع میکند (spring.main.allow-bean-definition-overriding=false). @Async/@Transactionalروی همان کلاسِ فراخواننده → self-invocation، بیسروصدا بیاثر.- ترتیب
@PostConstructبین beanهای مختلف بهطور سراسری تضمین نمیشود، مگر با@DependsOnصریح یا لبههای واقعیِ وابستگی.
بهترین شیوهها
۱. از constructor injection با فیلدهای final استفاده کن؛ بگذار Lombok @RequiredArgsConstructor یا یک constructor دستنویس سیمکشی کند.
۲. پیکربندی را در کلاسهای @Configuration نگه دار؛ به proxyBeanMethods = false فقط زمانی دست بزن که متدها مستقلاند و سرعت استارتاپ اهمیت دارد.
۳. هرگز به مراجع چرخهای متکی نباش؛ آنها را ساختاری بشکن.
۴. برای تزریق beanهای با scope باریکتر، ObjectProvider را به پراکسی scopeدار ترجیح بده — صریح و دوستدار تست است.
۵. متدهای دارای annotationِ AOP را public نگه دار و آنها را از طریق bean صدا بزن، هرگز با this..
۶. @Transactional را در لایهٔ service بگذار، نه روی controllerها یا repositoryها، و همیشه بهیاد داشته باش که این یک پراکسی است — برای آن طراحی کن.
سؤالات مصاحبه
فیلدهای final تغییرناپذیر، شیء کاملاً مقداردهیشده هنگام بازگشت از constructor، شکست سریع (fail-fast) هنگام استارتاپ برای وابستگیهای غیرقابلresolve، قابلیت تست بدون بالاآوردن context مربوط به Spring، و آشکارکردن بوی بدِ «وابستگیهای بیشازحد». field injection همهٔ اینها را پنهان میکند و برای تست به reflection نیاز دارد.
BeanFactory کانتینر IoCِ تنبل و کمینه است. ApplicationContext آن را با ساخت زودهنگام singletonها، انتشار رویداد، MessageSource برای i18n، بارگذاری منابع و کشف خودکار post-processorها گسترش میدهد. در عمل تقریباً همیشه ApplicationContext را استفاده میکنی.
ساخت نمونه → مقداردهی وابستگیها → callbackهای *Aware → BPP.postProcessBeforeInitialization → @PostConstruct → afterPropertiesSet() → init-method سفارشی → BPP.postProcessAfterInitialization (پراکسیها اینجا ساخته میشوند) → در سرویس → @PreDestroy → destroy() → destroy-method سفارشی.
@Configuration
class C {
@Bean A a() { return new A(shared()); }
@Bean B b() { return new B(shared()); }
@Bean Shared shared() { return new Shared(); }
}
یکی. @Configuration با CGLIB پراکسی میشود (حالت «full»)؛ فراخوانیهای رهگیریشدهٔ shared() همان singletonِ مدیریتشده توسط کانتینر را برمیگردانند. اگر @Configuration را به @Component تغییر دهی یا proxyBeanMethods = false بگذاری، دو نمونه میگیری، چون فراخوانیها به فراخوانی متد سادهٔ جاوا تبدیل میشوند.
@Transactional public void outer() { inner(); }
@Transactional(propagation = REQUIRES_NEW) public void inner() {}
self-invocation: inner() همان this.inner() است، یک فراخوانی مستقیم روی targetِ خام که پراکسیِ محلِ advice تراکنش را دور میزند. با انتقال inner به bean دیگر، تزریق پراکسی به خود (self-inject)، یا AopContext.currentProxy() رفعش کن.
singleton یکبار سیمکشی میشود، پس یک prototype که مستقیم تزریق شده بهعنوان یک نمونهٔ واحد برای همیشه ثابت میماند. راهحلها: ObjectProvider.getObject() (ترجیحی)، پراکسی scopeدار (proxyMode = TARGET_CLASS)، یا تزریق متدی با @Lookup.
Spring کلاسیک: پراکسی داینامیک JDK اگر bean اینترفیسی پیاده کند، وگرنه CGLIB. Spring Boot پیشفرض را proxyTargetClass = true گذاشت، پس همیشه CGLIB را استفاده میکند تا از ClassCastException هنگام انتظار نوع concrete اجتناب شود.
چرخههای setter/field از طریق کش سهسطحیِ singleton و افشای مرجع زودهنگام bean قابل حلاند. چرخههای constructor نه — پیش از ساخت هیچ مرجع زودهنگامی وجود ندارد، پس BeanCurrentlyInCreationException میگیری. از Boot 2.6 حتی چرخههای قابلحل هم بهطور پیشفرض ممنوعاند (allow-circular-references=false).
@PostConstruct بله؛ @PreDestroy خیر. Spring پس از تحویل نمونههای prototype آنها را ردیابی نمیکند، پس باید خودت منابع prototype را آزاد کنی.
یک تخصصیسازی @Component بهعلاوهٔ ترجمهٔ استثنای persistence: استثناهای وابسته به vendor در JDBC/JPA از طریق PersistenceExceptionTranslationPostProcessor به سلسلهمراتب DataAccessExceptionِ غیرچکشدهٔ Spring تبدیل میشوند.
هیچ. AOP مبتنی بر پراکسی فقط میتواند متدهای قابلدسترس از بیرون را advise کند؛ برای پراکسیهای subclass با CGLIB یعنی متدهای public (غیر final). متدهای private/final برای پراکسی نامرئیاند، پس annotation بیسروصدا نادیده گرفته میشود.
subclassکردن با CGLIB برای یک کلاس @Configuration را غیرفعال میکند و رهگیری متد بینبینی را برای استارتاپ سریعتر و مصرف حافظهٔ کمتر رد میکند. فقط زمانی استفاده کن که متدهای @Bean همدیگر را صدا نمیزنند. auto-configهای Spring Boot بهشدت از آن استفاده میکنند.
در زمان ساخت singleton هیچ requestی وجود ندارد، پس تزریق مستقیم شکست میخورد یا یک نمونهٔ کهنه را ثابت میکند. یک پراکسی scopeدار یا ObjectProvider تزریق کن؛ پراکسی، bean واقعیِ request-scoped را بهصورت تنبل در هر فراخوانی متد درون یک request حل میکند.
BeanFactoryPostProcessor یکبار پیش از ساخت هر beanی اجرا میشود و تعریفهای bean (فراداده) را ویرایش میکند. BeanPostProcessor بهازای هر bean دورِ مقداردهی اولیه اجرا میشود و میتواند نمونهها را بپیچد/جایگزین کند — پراکسیهای AOP اینجا ساخته میشوند.
@Primary یک کاندیدای پیشفرض را برای حالت ابهام علامت میزند. @Qualifier در نقطهٔ تزریق مشخصتر است و @Primary را override میکند. بهعنوان آخرین راه، تطبیق نام bean با نام فیلد/پارامتر، تای را میشکند.
- هستهٔ Spring یک کانتینر است که ساخت و سیمکشی و نابودی اشیایت را در دست میگیرد؛ IoC یعنی تو دیگر
newنمیزنی و DI یعنی وابستگیها به تو هل داده میشوند. - در عمل
ApplicationContext(زودهنگام، پرامکانات) را استفاده میکنی، نهBeanFactory(تنبل، کمینه). - constructor injection بهخاطر immutability، fail-fast، تستپذیری و آشکارکردن design smell برنده است.
- چرخهٔ حیات ترتیب دقیق دارد و پراکسیهای AOP در
postProcessAfterInitializationساخته میشوند — ریشهٔ خیلی از دامها همینجاست. - singleton یعنی یکی بهازای کانتینر؛ prototype را Spring پس از تحویل فراموش میکند و
@PreDestroyاش را صدا نمیزند. تلهٔ «prototype در singleton» را باObjectProviderبشکن. @Configurationبا CGLIB پراکسی میشود (حالت full) و فراخوانی بینبینی را یکتا نگه میدارد؛ حالت lite این را از دست میدهد.- چرخهٔ constructor حلنشدنی است (fail fast)؛ چرخهٔ setter/field با کش سهسطحی حل میشود اما از Boot 2.6 پیشفرض ممنوع است.
- AOP با پراکسی کار میکند؛ دام self-invocation یعنی
this.method()پراکسی را دور میزند و annotationهای AOP بیاثر میشوند.
Let's start with a simple picture. Imagine you're running a restaurant. In the naive setup, every cook has to go to the market, buy meat, wash vegetables, sharpen knives, and light the stove before they can cook a single dish. Chaos. In a professional restaurant, one person (or system) is responsible for supplying everything; the cook simply declares "I need meat, vegetables, and a hot stove," and it all arrives ready. The cook focuses on the one thing only they can do: cooking.
Spring's core is exactly that "supply system." This lesson builds everything from the ground up.
Here's what you'll learn, each concept unpacked from scratch:
- The container, IoC, and DI — why you stop calling
new, and what it buys you. - BeanFactory vs ApplicationContext — the two container types and which you actually use.
- The three injection styles (constructor, setter, field) and why constructor wins hands down.
- The bean lifecycle — the exact birth-to-death ordering of an object.
- Scopes and the classic "singleton-injects-prototype" trap.
- The CGLIB proxy behind @Configuration and the "lite" mode gotcha.
- Circular dependencies and when Spring breaks.
- AOP, JDK/CGLIB proxies, and the self-invocation pitfall.
- A full interview Q&A section at the end.
Part 0 — words you must know
Before anything else, let's plant a few terms with analogies so nothing trips you up later.
Picture a furniture factory. The container is the factory itself: where objects are built, assembled, and finally scrapped at end of life. Each piece coming off the line is a bean — an object the factory is responsible for creating and maintaining. And a BeanDefinition is the blueprint on paper: what wood, what dimensions, which screws it needs, how to assemble it, how to dismantle it. The factory reads the blueprint first, then builds.
- container: the engine that owns the creation, wiring, configuration, and destruction of your objects.
- bean: any object whose lifecycle the container manages.
- BeanDefinition: metadata about a bean — its class, scope, dependencies, init/destroy methods, and lazy flag. The container reads this blueprint to build the bean.
- proxy: a "stand-in" object that stands in front of the real object and does something extra before/after method calls. We'll open it up fully later; for now, think "gatekeeper at the door."
Everything glamorous in Spring — transactions, security, caching, @Async — rides on two pillars: the container plus AOP. Understand these two deeply and the rest of Spring stops being "magic" and becomes predictable engineering.
What does the container actually do? (IoC and DI)
Back to the restaurant. When the cook did their own shopping, control was in the cook's hands. When the supply system puts everything in front of them, control over "gathering supplies" has been taken from the cook and handed to the system. This inversion of responsibility is called Inversion of Control.
Inversion of Control (IoC) means you no longer call new on your collaborators (the objects your class needs) and manage their lifecycles; you merely declare what you need and the container hands it to you.
Dependency Injection (DI) is the concrete mechanism that implements IoC: dependencies are pushed into the object, rather than the object going out to pull them.
See the difference between "pull" and "push" like this: in pull mode, you get thirsty, stand up, walk to the well, and draw water — you're responsible for finding and fetching. In push mode (injection), a waiter brings the glass and sets it in front of you; you only declared that you wanted water. In DI, your class just says "I need a PaymentGateway" and the container brings it and places it in its hand.
The real payoff isn't magic, it's decoupling. Your class depends on an interface, the container decides which implementation to give, and you can swap, mock (a fake instance for tests), or reconfigure without touching the consumer.
You declare what you want; the container takes over how and when to build and deliver it. It's this separation of "what" from "how" that makes your code testable and changeable.
Two container types: BeanFactory and ApplicationContext
Spring has two container interfaces, and knowing the difference matters.
BeanFactory— the minimal, lazy IoC contract. Lazy means it doesn't build a bean until you callgetBean()for it the first time. Rarely used directly.ApplicationContext— a superset of BeanFactory with more features:- eager instantiation of singletons at application startup (not at request time).
- event publishing via
ApplicationEventPublisher. - internationalization (i18n) via
MessageSource. - resource loading.
- automatic detection of
BeanPostProcessorandBeanFactoryPostProcessor(we'll unpack these shortly).
In practice you almost always use an ApplicationContext — like AnnotationConfigApplicationContext or GenericWebApplicationContext.
// A container built from a Java config class
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
OrderService svc = ctx.getBean(OrderService.class);
svc.place(new Order());
} // close() triggers singleton destruction callbacks
Remember the difference this way: BeanFactory is like a restaurant that only cooks a dish when you order it; ApplicationContext is like a buffet that has everything ready from early morning so it's waiting the moment you walk in. Being eager means that if something in your wiring is broken, you find out at startup, not mid-way through a user's operation.
One subtle point: post-processors can mutate BeanDefinitions before any bean is instantiated — much of what we call "Spring magic" is implemented exactly this way.
The three dependency-injection styles
There are three injection points: constructor, setter, and field (directly onto the field). They are not equivalent, and choosing between them matters.
@Service
class ConstructorInjected {
private final PaymentGateway gateway; // can be final
private final InventoryClient inventory;
// Since Spring 4.3, @Autowired is optional on a single constructor
ConstructorInjected(PaymentGateway gateway, InventoryClient inventory) {
this.gateway = gateway;
this.inventory = inventory;
}
}
@Service
class FieldInjected {
@Autowired private PaymentGateway gateway; // cannot be final; reflection-set
}
@Service
class SetterInjected {
private PaymentGateway gateway;
@Autowired void setGateway(PaymentGateway g) { this.gateway = g; }
}
- constructor injection is like a car whose engine, steering wheel, and wheels were fitted on the production line; when it leaves the factory it's complete and drives.
- setter injection is like a car delivered without a radio that you install later yourself — good for optional things.
- field injection is like someone reaching in from behind, secretly, without a door, into the dashboard to connect wires (reflection). It works, but it's neither transparent nor controllable.
Why constructor injection is the default answer
Immutability & thread-safety. Fields can be
final; the object is fully initialized when the constructor returns. There's no window where the bean exists but half its dependencies arenull. ("Immutable" means it can't change after construction; that's exactly what lets many threads use it safely at once.)Fail-fast on missing or ambiguous dependencies. A required constructor argument that can't be resolved fails at context startup, not later, mid-operation, with a cryptic NPE (NullPointerException).
Testability without Spring. You can write
new ConstructorInjected(mock1, mock2)in a plain JUnit test. Field injection forces you to either use reflection or bring up the whole context.Reveals design smells. A constructor with 8 parameters screams that this class does too much. Field injection hides that pain, so the class silently rots into a god object (an all-doing, oversized object).
No partially-constructed bean escaping. Setter/field injection allow the object to be used before wiring completes (e.g., inside a lifecycle callback).
Setter injection is legitimately useful for optional dependencies or reconfigurable state. But field injection's only real advantage was "brevity" — and modern Lombok's @RequiredArgsConstructor gives you that brevity without the downsides. The Spring Framework docs themselves and the Spring Boot guidance recommend constructor injection for mandatory dependencies.
One more subtle benefit: constructor injection makes it impossible to hide a circular dependency. Spring can't construct A that needs B while it's still constructing B that needs A; so instead of working mysteriously, it shouts at startup (you'll see this in detail in the circular-dependency section).
The bean lifecycle
Understanding the exact birth-to-death ordering of a bean is often what separates a senior engineer from a mid-level one.
A bean's birth is like hiring and onboarding an employee: first they're hired (object construction), then they get their badge, laptop, and access (dependency population), then they're shown where they work and who their manager is (Aware callbacks), then they go through training (@PostConstruct and init), then they start working (in service), and finally offboarding and exit (@PreDestroy and destroy). Each step has a defined order.
Instantiation (constructor)
↓
Populate properties (field/setter DI)
↓
Aware callbacks (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
↓
BeanPostProcessor.postProcessBeforeInitialization
↓
@PostConstruct
↓
InitializingBean.afterPropertiesSet()
↓
custom init-method (@Bean(initMethod=...))
↓
BeanPostProcessor.postProcessAfterInitialization ← AOP proxies created HERE
↓
[ bean is in service ]
↓ (on context shutdown, singletons only)
@PreDestroy
↓
DisposableBean.destroy()
↓
custom destroy-method
@Component
class LifecycleDemo implements InitializingBean, DisposableBean, BeanNameAware {
@Override public void setBeanName(String name) { /* 3rd */ }
@PostConstruct void warmUp() { /* 5th — preferred init hook */ }
@Override public void afterPropertiesSet() { /* 6th */ }
@PreDestroy void flush() { /* on shutdown */ }
@Override public void destroy() { /* after @PreDestroy */ }
}
@PostConstruct and @PreDestroy come from the JSR-250 standard (in Spring Boot 3 / Jakarta they live in the jakarta.annotation package). Prefer them over Spring interfaces like InitializingBean and DisposableBean, because the annotations don't couple your code to Spring — the same class still makes sense in a non-Spring project. Low-coupling code is healthy code.
BeanPostProcessor vs BeanFactoryPostProcessor
These two names look alike but do two completely different jobs.
BeanFactoryPostProcessor is like someone who tinkers with the blueprints before production starts: no product has been built yet, they only edit the technical drawing (the BeanDefinition). BeanPostProcessor, by contrast, is like an inspector on the assembly line who picks up each product as it passes, does something to it (say, wraps it in a protective case), and puts it back.
BeanFactoryPostProcessorruns once, after bean definitions are loaded but before any bean is instantiated. It mutates configuration metadata. Example:PropertySourcesPlaceholderConfigurer, which resolves${...}values.BeanPostProcessorruns per bean, wrapping around initialization. This is the extension point where Spring creates proxies. Examples:AutowiredAnnotationBeanPostProcessor(processes@Autowired) andAnnotationAwareAspectJAutoProxyCreator(creates AOP proxies).
Because proxies are created in postProcessAfterInitialization, the reference you get after the container is done is often a proxy, not your raw bean. Hold onto that one sentence; in the AOP section you'll see it's the root cause of Spring's most famous bug.
Bean scopes
Scope means the "lifetime domain" of a bean: how many instances the container makes and how long each lives.
| Scope | Meaning | Instances |
|---|---|---|
singleton (default) |
one instance per container | 1 |
prototype |
new instance on every injection/getBean |
N |
request |
one per HTTP request | per request |
session |
one per HTTP session | per session |
application |
one per ServletContext |
1 |
websocket |
one per WebSocket session | per socket |
First: singleton means "one per Spring container," not the GoF Singleton pattern (which is one per JVM). If you have two containers, you have two instances.
Second: Spring does not manage the full lifecycle of prototype beans. It instantiates, wires, and runs init callbacks, but then forgets the bean. That means @PreDestroy is never called on a prototype. If a prototype holds a resource (a file, a connection), you must release it yourself.
The classic trap: injecting a prototype into a singleton
A singleton is wired once, at creation time. Now suppose you have a break room (singleton) that's supposed to hand you a fresh disposable cup (prototype) each time. But since the break room was furnished only once, that very first cup gets nailed to the wall forever — you keep getting the same stale cup. The whole "disposable" idea is defeated.
@Component @Scope("prototype")
class Task { /* new one desired each time */ }
@Component
class Worker {
@Autowired private Task task; // BUG: resolved once, same Task forever
}
Fixes, best first:
1. ObjectProvider<T> / Provider<T> (JSR-330) — the cleanest. Instead of the prototype itself, you inject a "lazy factory" and build a fresh instance each time you actually need one. Explicit, testable, no proxy magic.
@Component
class Worker {
private final ObjectProvider<Task> taskProvider; // lazy factory
Worker(ObjectProvider<Task> taskProvider) { this.taskProvider = taskProvider; }
void run() {
Task fresh = taskProvider.getObject(); // NEW prototype each call
fresh.execute();
}
}
2. Scoped proxy — put a proxy in place of the prototype so each method call resolves a fresh instance:
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
class Task { }
3. @Lookup method injection — Spring overrides an abstract/lookup method with CGLIB (a library that subclasses at runtime) to return a fresh bean each time:
@Component
abstract class Worker {
void run() { createTask().execute(); }
@Lookup protected abstract Task createTask(); // container-overridden
}
This exact "stale reference" trap also occurs when you inject a request- or session-scoped bean into a singleton — except there you must use a scoped proxy (or ObjectProvider), because at singleton-creation time there is no active request at all to hand you an instance.
@Configuration, @Bean, and the CGLIB proxy
This is one of the most popular interview gotchas. Read it carefully.
@Configuration
class AppConfig {
@Bean ClientA clientA() { return new ClientA(shared()); }
@Bean ClientB clientB() { return new ClientB(shared()); }
@Bean Shared shared() { return new Shared(); }
}
Question: how many Shared instances get built? At first glance shared() is called twice, so two. But the answer is one.
With @Configuration, Spring subclasses your config class with CGLIB (it builds a hidden subclass) and, like a smart secretary, intercepts every inter-bean method call. The first time shared() is called, the secretary lets the object be built and stored in the container. The second time, the secretary says "hold on, we already made this" and returns that same ready instance — not a new object. This is called "full" @Configuration mode.
Now the trap itself — "lite" mode:
@Component // NOT @Configuration
class LiteConfig {
@Bean ClientA clientA() { return new ClientA(shared()); }
@Bean Shared shared() { return new Shared(); } // called as a PLAIN method
}
@Bean methods declared on a class that is not @Configuration (e.g., on @Component, or with @Configuration(proxyBeanMethods = false)) are not CGLIB-intercepted. There's no smart secretary. Each direct call to shared() runs the real method again → multiple Shared instances. Note: beans the container itself registers are still singletons, but direct method-to-method calls bypass the container.
proxyBeanMethods = false (introduced in Spring 5.2) is a performance optimization: it skips CGLIB subclassing and the startup cost of building the proxy. Use it only when your @Bean methods don't call each other. Spring Boot's auto-configuration classes use it heavily for faster startup.
@Configuration(proxyBeanMethods = false) // faster, but no inter-bean interception
class FastConfig { /* @Bean methods must not invoke one another */ }
Component scanning & stereotypes
@ComponentScan (implicit inside @SpringBootApplication) walks packages, finds classes annotated with a stereotype, and registers them as beans. "Stereotype" means a label that declares the class's role.
@Component— a generic label.@Service,@Repository,@Controller,@RestController— specializations of@Component. Semantically they're role labels, but two of them also do extra work:@Repositoryadditionally enables persistence-exception translation: vendor-specific JDBC/JPA exceptions are converted into Spring's uniformDataAccessExceptionhierarchy.@Controller/@RestControllerare handled by Spring MVC and wired to HTTP routes.
When multiple candidates match a type, Spring has to decide which to inject:
@Autowired
public Notifier(@Qualifier("sms") Notifier n) { ... } // pick by qualifier
@Component @Primary // default winner when ambiguous
class EmailNotifier implements Notifier {}
Remember three layers: @Primary sets a default ("if you didn't specify, pick this one"); @Qualifier at an injection point is more specific and overrides @Primary; and if neither is present, name matching is the last-resort tiebreak — a field named smsNotifier matches a bean named smsNotifier. Since Spring 6 / Boot 3 you can also use custom qualifier annotations or @Fallback (6.2).
Circular dependencies
Suppose A needs B and B needs A. It's a chicken-and-egg puzzle: which gets built first?
Imagine two workers. Worker A says "I won't start until I have B's finished wrench," and Worker B says "I won't start until I have A's finished wrench." If both depend on each other at birth (constructor), neither ever starts — deadlock. But if each can first be born and receive the other's tool later (setter), the knot can be untied: build both first, then exchange the tools.
Setter/field injection: Spring can sometimes resolve this using its three-level cache and "early bean references." It instantiates A, exposes a raw early reference of A into the singleton cache, injects that early A into B, finishes B, then finishes A. This works because construction and population are separate phases.
Constructor injection: Spring cannot resolve it — A's constructor needs a fully-built B and vice versa; chicken-and-egg with no early reference to hand out. Result: BeanCurrentlyInCreationException at startup.
Constructor cycle → BeanCurrentlyInCreationException (startup failure — good, fail fast)
Setter/field cycle → resolved via early-reference cache (works — but is a design smell)
Since Spring Boot 2.6, circular references are prohibited by default; even the setter/field case fails unless you set spring.main.allow-circular-references=true. But the correct fix is almost never to flip that flag — it's to break the cycle: extract a third collaborator, use an interface/event, or inject a @Lazy/ObjectProvider reference so one side is resolved on first use.
@Service
class A {
private final B b;
A(@Lazy B b) { this.b = b; } // @Lazy injects a proxy → breaks the construction cycle
}
AOP: cross-cutting concerns
Some concerns repeat across dozens of different classes: logging, starting/ending a transaction, access checks, timing, caching. Write these inside every method and your code drowns in duplication.
Picture a building where each room does its own specialized work. Now you want every entry to and exit from every room logged, and every entry security-checked. The dumb way is to put a logbook and a guard inside each room. The smart way: put a single guard at the main door who does the work before and after every passage. The rooms don't even know a guard exists. AOP is exactly that guard at the door.
AOP factors these shared concerns out into aspects, applied declaratively — via an annotation, not hand-written code inside each method. Key terms:
- Join point — a point in execution where advice can apply (in Spring AOP, always a method execution).
- Pointcut — a predicate selecting join points; e.g.,
execution(* com.acme.service..*(..))means "any method in the service package." - Advice — the actual action:
@Before,@AfterReturning(after a successful return),@AfterThrowing(after an exception),@After(in any case),@Around(all the way around, the most powerful). - Aspect — a class bundling pointcuts + advice (the "guard" itself).
- Weaving — the act of applying aspects to code. Spring uses runtime proxy-based weaving, not AspectJ's compile-/load-time bytecode weaving (unless you opt in).
@Aspect @Component
class TimingAspect {
@Around("execution(* com.acme..*Service.*(..))")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
long t0 = System.nanoTime();
try {
return pjp.proceed(); // invoke the real method
} finally {
long us = (System.nanoTime() - t0) / 1000;
System.out.println(pjp.getSignature() + " took " + us + "us");
}
}
}
JDK dynamic proxy vs CGLIB
Now let's understand how that "guard" is actually built. Spring creates the proxy at bean post-processing time, and it has two options:
| JDK dynamic proxy | CGLIB | |
|---|---|---|
| Requires | target implements an interface | any concrete class |
| How | java.lang.reflect.Proxy implementing the interface |
runtime subclass of the target |
| Limitation | only interface methods advised | final classes/methods can't be proxied |
| Default | used when interfaces present (classic) | Spring Boot forces this by default |
A JDK proxy is like a stunt double who signs the same role contract (interface): they can only do what the contract lists. CGLIB is like a stunt double who puts on a full makeup of the actor themselves (subclassing): they can sit in for the concrete person — but if the actor is "locked" (final), the makeup is impossible.
Historically Spring used a JDK proxy if the bean implemented at least one interface, else CGLIB. But Spring Boot changed the default to proxyTargetClass = true (always CGLIB), because interface-based proxies caused surprising ClassCastExceptions when code expected the concrete type. Know both, because it determines what type you can cast the injected bean to.
The self-invocation pitfall (a senior favorite)
Remember the guard stood only at the main door? Now imagine an employee inside the room going through an internal back door into the next room. The guard never sees them, because they didn't pass the main door. In Spring, advice sits on the proxy (the main door). When one method calls another method on the same object via this.method(), it went through the back door and completely bypassed the proxy — so no advice runs.
@Service
class OrderService {
@Transactional
public void outer() {
inner(); // BUG: 'this.inner()' — NOT the proxy → @Transactional IGNORED
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void inner() { /* expected a new transaction; gets none/outer's */ }
}
outer() is called on the proxy (so it's advised), but inner() is invoked as this.inner() — a direct call on the raw target instance — so the transaction advice for inner never fires. This exact bug also hits @Cacheable, @Async, @Retryable, and any other AOP annotation.
Fixes:
Move
inner()to a separate bean and inject it; now the call goes through that bean's proxy. Cleanest.Self-inject the proxy and call
self.inner()(use@LazyorObjectProviderto avoid a construction cycle).Use
AopContext.currentProxy()with@EnableAspectJAutoProxy(exposeProxy = true)— it works but couples code to Spring AOP; avoid where possible.
Proxied AOP on Spring's CGLIB-based proxies applies only to public methods — private/protected methods are invisible to the proxy. And putting @Transactional on a private or final method silently does nothing: no error, no warning, it just doesn't work. This is exactly the kind of bug that eats hours.
Common pitfalls & gotchas
@AutowiredonOptional<T>/@Nullable/ObjectProvider<T>signals an optional dependency; but a plain required type throwsNoSuchBeanDefinitionExceptionwhen the bean is absent.- Field injection +
finalis impossible; field injection also breaks constructor-based immutability guarantees. @Value("${prop}")needs aPropertySourcesPlaceholderConfigurer(auto-provided by Boot). A missing property → startup failure, unless you use${prop:default}(a default value).- Prototype
@PreDestroynever runs. Manage prototype cleanup manually, or via aDisposableBeanyou call yourself. - Two
@Configurationclasses each declaring a@Beanof the same name → the later registration overrides; but Boot forbids overriding by default (spring.main.allow-bean-definition-overriding=false). @Async/@Transactionalon the same class as the caller → self-invocation, silently no-ops.- Ordering of
@PostConstructacross different beans is not globally guaranteed, except through explicit@DependsOnor actual dependency edges.
Best practices
- Use constructor injection with
finalfields; let Lombok@RequiredArgsConstructoror a hand-written constructor do the wiring. - Keep configuration in
@Configurationclasses; reach forproxyBeanMethods = falseonly when the methods are independent and startup speed matters. - Never rely on circular references; break them structurally.
- Prefer
ObjectProviderover scoped proxies for injecting narrower-scoped beans — it's explicit and test-friendly. - Keep AOP-annotated methods
publicand call them through the bean, never viathis.. - Put
@Transactionalat the service layer, not on controllers or repositories, and always remember it's a proxy — design for that.
Interview Questions
Immutable final fields, a fully-initialized object when the constructor returns, fail-fast at startup for unresolvable deps, testability without bringing up a Spring context, and surfacing the "too many dependencies" design smell. Field injection hides all of that and needs reflection to test.
BeanFactory is the lazy, minimal IoC container. ApplicationContext extends it with eager singleton instantiation, event publishing, i18n MessageSource, resource loading, and automatic post-processor detection. You almost always use ApplicationContext.
Instantiate → populate deps → *Aware callbacks → BPP.postProcessBeforeInitialization → @PostConstruct → afterPropertiesSet() → custom init-method → BPP.postProcessAfterInitialization (proxies created here) → in service → @PreDestroy → destroy() → custom destroy-method.
@Configuration
class C {
@Bean A a() { return new A(shared()); }
@Bean B b() { return new B(shared()); }
@Bean Shared shared() { return new Shared(); }
}
One. @Configuration is CGLIB-proxied ("full" mode); the intercepted shared() calls return the same container-managed singleton. Change @Configuration to @Component or add proxyBeanMethods = false and you'd get two, because the calls become plain Java method invocations.
@Transactional public void outer() { inner(); }
@Transactional(propagation = REQUIRES_NEW) public void inner() {}
Self-invocation: inner() is this.inner(), a direct call on the raw target that bypasses the proxy where the transaction advice lives. Fix by moving inner to another bean, self-injecting the proxy, or AopContext.currentProxy().
A singleton is wired once, so a directly injected prototype is captured as a single instance forever. Fixes: ObjectProvider.getObject() (preferred), a scoped proxy (proxyMode = TARGET_CLASS), or @Lookup method injection.
Classic Spring: JDK dynamic proxy if the bean implements an interface, else CGLIB. Spring Boot defaults proxyTargetClass = true, so it always uses CGLIB to avoid ClassCastException when code expects the concrete type.
Setter/field cycles can be resolved via the three-level singleton cache exposing an early bean reference. Constructor cycles cannot — there's no early reference before construction, so you get BeanCurrentlyInCreationException. Since Boot 2.6, even resolvable cycles are forbidden by default (allow-circular-references=false).
@PostConstruct yes; @PreDestroy no. Spring doesn't track prototype instances after handing them out, so you must clean up prototype resources yourself.
It's a @Component specialization plus persistence-exception translation: vendor-specific JDBC/JPA exceptions are converted to Spring's unchecked DataAccessException hierarchy via a PersistenceExceptionTranslationPostProcessor.
Nothing. Proxy-based AOP can only advise externally-reachable methods; for CGLIB subclass proxies that means public (non-final) methods. Private/final methods are invisible to the proxy, so the annotation is silently ignored.
It disables CGLIB subclassing of a @Configuration class, skipping inter-bean method interception for faster startup and lower memory. Use it only when @Bean methods don't call each other. Spring Boot auto-configs use it heavily.
At singleton-creation time no request exists, so a direct injection fails or captures a stale instance. Inject a scoped proxy or an ObjectProvider; the proxy resolves the real request-scoped bean lazily on each method call within a request.
BeanFactoryPostProcessor runs once before any bean is instantiated and edits bean definitions (metadata). BeanPostProcessor runs per bean around initialization and can wrap/replace instances — this is where AOP proxies are created.
@Primary marks a default candidate used when the injection point is ambiguous. @Qualifier at an injection point is more specific and overrides @Primary. As a last resort, bean name matching the field/parameter name breaks ties.
- Spring's core is a container that owns building, wiring, and destroying your objects; IoC means you stop calling
new, and DI means dependencies are pushed into you. - In practice you use
ApplicationContext(eager, feature-rich), notBeanFactory(lazy, minimal). - Constructor injection wins for immutability, fail-fast, testability, and surfacing design smells.
- The lifecycle has an exact order, and AOP proxies are created in
postProcessAfterInitialization— the root of many pitfalls lives here. - singleton means one per container; Spring forgets a prototype after handing it out and never calls its
@PreDestroy. Break the "prototype-in-singleton" trap withObjectProvider. @Configurationis CGLIB-proxied (full mode) and keeps inter-bean calls unique; lite mode loses that.- The constructor cycle is unresolvable (fail fast); the setter/field cycle is resolved via the three-level cache but is forbidden by default since Boot 2.6.
- AOP works via a proxy; the self-invocation pitfall means
this.method()bypasses the proxy and AOP annotations silently do nothing.