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 دقیقاً همان «سیستم تدارکات» است. این درس همه‌چیز را از پایه می‌سازد.

نقشهٔ راه این درس

در این فصل این‌ها را یاد می‌گیری و هر کدام را از صفر باز می‌کنیم:

  1. کانتینر، IoC و DI — چرا دیگر خودت new نمی‌زنی و این چه سودی دارد.
  2. BeanFactory در برابر ApplicationContext — دو نوع کانتینر و اینکه در عمل کدام را استفاده می‌کنی.
  3. سه سبک تزریق (constructor، setter، field) و چرا constructor برندهٔ بی‌چون‌وچراست.
  4. چرخهٔ حیات bean — ترتیب دقیقِ تولد تا مرگ یک شیء.
  5. Scopeها و تلهٔ کلاسیک «تزریق prototype در singleton».
  6. پراکسی CGLIB برای ‎@Configuration و دام حالت «lite».
  7. وابستگی‌های چرخه‌ای و اینکه چه زمانی Spring می‌بُرد.
  8. AOP، پراکسی JDK/CGLIB و دام self-invocation.
  9. یک بخش کامل پرسش‌و‌پاسخ مصاحبه در انتها.

بخش صفر — واژه‌هایی که باید بلد باشی

قبل از هر چیز، چند واژه را با تشبیه جا بیندازیم تا در ادامه هیچ‌جا گیر نکنی.

کانتینر، bean و BeanDefinition

تصور کن یک کارخانهٔ مبلمان داری. کانتینر (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).

تفاوت 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 چرخهٔ حیات).

تنها مزیت field injection را مدرن‌سازی نابود کرده

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 */ }
}
چرا annotationها بهتر از اینترفیس‌ها هستند

@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 را می‌سازد).
ریشهٔ دام self-invocation همین‌جاست

چون پراکسی‌ها در 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 و prototype

اول: 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 می‌شود
}
همین مشکل برای request/session هم هست

دقیقاً همین دام «مرجع کهنه (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 برای چیست؟

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 {}
ترتیب تصمیم‌گیری Spring در ابهام

سه لایه را به‌خاطر بسپار: @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 → با کش مرجع زودهنگام حل می‌شود (کار می‌کند — اما بوی بد طراحی است)
از Boot 2.6 حتی چرخهٔ قابل‌حل هم ممنوع است

از 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ها، و همیشه به‌یاد داشته باش که این یک پراکسی است — برای آن طراحی کن.

سؤالات مصاحبه

۱. چرا constructor injection بر field injection ترجیح دارد؟

فیلدهای final تغییرناپذیر، شیء کاملاً مقداردهی‌شده هنگام بازگشت از constructor، شکست سریع (fail-fast) هنگام استارتاپ برای وابستگی‌های غیرقابل‌resolve، قابلیت تست بدون بالا‌آوردن context مربوط به Spring، و آشکارکردن بوی بدِ «وابستگی‌های بیش‌ازحد». field injection همهٔ این‌ها را پنهان می‌کند و برای تست به reflection نیاز دارد.

۲. تفاوت BeanFactory و ApplicationContext چیست؟

BeanFactory کانتینر IoCِ تنبل و کمینه است. ApplicationContext آن را با ساخت زودهنگام singletonها، انتشار رویداد، MessageSource برای i18n، بارگذاری منابع و کشف خودکار post-processorها گسترش می‌دهد. در عمل تقریباً همیشه ApplicationContext را استفاده می‌کنی.

۳. ترتیب دقیق چرخهٔ حیات bean را همراه با @PostConstruct، InitializingBean و BeanPostProcessor بگو.

ساخت نمونه → مقداردهی وابستگی‌ها → callbackهای *AwareBPP.postProcessBeforeInitialization@PostConstructafterPropertiesSet() → init-method سفارشی → BPP.postProcessAfterInitialization (پراکسی‌ها اینجا ساخته می‌شوند) → در سرویس → @PreDestroydestroy() → destroy-method سفارشی.

۴. (دام) این کد چند نمونهٔ Shared می‌سازد و چرا؟
@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 هیچ کاری نمی‌کند؟
@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() رفعش کن.

۶. مشکل تزریق prototype در singleton و سه راه‌حل را توضیح بده.

singleton یک‌بار سیم‌کشی می‌شود، پس یک prototype که مستقیم تزریق شده به‌عنوان یک نمونهٔ واحد برای همیشه ثابت می‌ماند. راه‌حل‌ها: ObjectProvider.getObject() (ترجیحی)، پراکسی scope‌دار (proxyMode = TARGET_CLASS)، یا تزریق متدی با @Lookup.

۷. Spring چه زمانی از پراکسی JDK و چه زمانی از CGLIB استفاده می‌کند و Spring Boot چه چیزی را تغییر داد؟

Spring کلاسیک: پراکسی داینامیک JDK اگر bean اینترفیسی پیاده کند، وگرنه CGLIB. Spring Boot پیش‌فرض را proxyTargetClass = true گذاشت، پس همیشه CGLIB را استفاده می‌کند تا از ClassCastException هنگام انتظار نوع concrete اجتناب شود.

۸. (سخت) آیا Spring می‌تواند وابستگی چرخه‌ای را حل کند؟ در چه شرایطی شکست می‌خورد؟

چرخه‌های setter/field از طریق کش سه‌سطحیِ singleton و افشای مرجع زودهنگام bean قابل حل‌اند. چرخه‌های constructor نه — پیش از ساخت هیچ مرجع زودهنگامی وجود ندارد، پس BeanCurrentlyInCreationException می‌گیری. از Boot 2.6 حتی چرخه‌های قابل‌حل هم به‌طور پیش‌فرض ممنوع‌اند (allow-circular-references=false).

۹. آیا @PreDestroy یک bean از نوع prototype فراخوانی می‌شود؟ @PostConstruct چطور؟

@PostConstruct بله؛ @PreDestroy خیر. Spring پس از تحویل نمونه‌های prototype آن‌ها را ردیابی نمی‌کند، پس باید خودت منابع prototype را آزاد کنی.

۱۰. @Repository علاوه بر @Component چه چیزی اضافه می‌کند؟

یک تخصصی‌سازی @Component به‌علاوهٔ ترجمهٔ استثنای persistence: استثناهای وابسته به vendor در JDBC/JPA از طریق PersistenceExceptionTranslationPostProcessor به سلسله‌مراتب DataAccessExceptionِ غیرچک‌شدهٔ Spring تبدیل می‌شوند.

۱۱. (دام) @Transactional روی یک متد private — چه اتفاقی می‌افتد؟

هیچ. AOP مبتنی بر پراکسی فقط می‌تواند متدهای قابل‌دسترس از بیرون را advise کند؛ برای پراکسی‌های subclass با CGLIB یعنی متدهای public (غیر final). متدهای private/final برای پراکسی نامرئی‌اند، پس annotation بی‌سروصدا نادیده گرفته می‌شود.

۱۲. proxyBeanMethods = false چیست و چه زمانی باید استفاده‌اش کرد؟

subclass‌کردن با CGLIB برای یک کلاس @Configuration را غیرفعال می‌کند و رهگیری متد بین‌بینی را برای استارتاپ سریع‌تر و مصرف حافظهٔ کمتر رد می‌کند. فقط زمانی استفاده کن که متدهای @Bean همدیگر را صدا نمی‌زنند. auto-configهای Spring Boot به‌شدت از آن استفاده می‌کنند.

۱۳. (سخت) تزریق یک bean با scope از نوع request در یک singleton — چه چیزی می‌شکند و چطور رفعش می‌کنی؟

در زمان ساخت singleton هیچ requestی وجود ندارد، پس تزریق مستقیم شکست می‌خورد یا یک نمونهٔ کهنه را ثابت می‌کند. یک پراکسی scope‌دار یا ObjectProvider تزریق کن؛ پراکسی، bean واقعیِ request-scoped را به‌صورت تنبل در هر فراخوانی متد درون یک request حل می‌کند.

۱۴. تفاوت BeanPostProcessor و BeanFactoryPostProcessor؟

BeanFactoryPostProcessor یک‌بار پیش از ساخت هر beanی اجرا می‌شود و تعریف‌های bean (فراداده) را ویرایش می‌کند. BeanPostProcessor به‌ازای هر bean دورِ مقداردهی اولیه اجرا می‌شود و می‌تواند نمونه‌ها را بپیچد/جایگزین کند — پراکسی‌های AOP اینجا ساخته می‌شوند.

۱۵. @Primary در برابر @Qualifier — تقدم با کدام است؟

@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.

Roadmap for this lesson

Here's what you'll learn, each concept unpacked from scratch:

  1. The container, IoC, and DI — why you stop calling new, and what it buys you.
  2. BeanFactory vs ApplicationContext — the two container types and which you actually use.
  3. The three injection styles (constructor, setter, field) and why constructor wins hands down.
  4. The bean lifecycle — the exact birth-to-death ordering of an object.
  5. Scopes and the classic "singleton-injects-prototype" trap.
  6. The CGLIB proxy behind @Configuration and the "lite" mode gotcha.
  7. Circular dependencies and when Spring breaks.
  8. AOP, JDK/CGLIB proxies, and the self-invocation pitfall.
  9. 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.

Container, bean, and BeanDefinition

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."
Why should we even care about these?

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.

The difference between push and pull

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.

The heart of it in one sentence

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 call getBean() 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 BeanPostProcessor and BeanFactoryPostProcessor (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
Lazy versus eager

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; }
}
The three styles like taking delivery of a car
  • 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

  1. 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 are null. ("Immutable" means it can't change after construction; that's exactly what lets many threads use it safely at once.)

  2. 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).

  3. 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.

  4. 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).

  5. No partially-constructed bean escaping. Setter/field injection allow the object to be used before wiring completes (e.g., inside a lifecycle callback).

Field injection's only advantage has been killed off by modernization

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.

Onboarding a new employee

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 */ }
}
Why annotations beat the interfaces

@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.

Blueprint editor vs assembly-line inspector

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.

  • BeanFactoryPostProcessor runs once, after bean definitions are loaded but before any bean is instantiated. It mutates configuration metadata. Example: PropertySourcesPlaceholderConfigurer, which resolves ${...} values.
  • BeanPostProcessor runs per bean, wrapping around initialization. This is the extension point where Spring creates proxies. Examples: AutowiredAnnotationBeanPostProcessor (processes @Autowired) and AnnotationAwareAspectJAutoProxyCreator (creates AOP proxies).
This is the root of the self-invocation pitfall

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
Two common misconceptions about singleton and prototype

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 disposable cup nailed to the wall as if it were a permanent plate

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
}
The same problem hits request/session too

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.

A smart secretary who prevents duplicate orders

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.

What is proxyBeanMethods = false for?

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:
    • @Repository additionally enables persistence-exception translation: vendor-specific JDBC/JPA exceptions are converted into Spring's uniform DataAccessException hierarchy.
    • @Controller/@RestController are 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 {}
Spring's tie-breaking order under ambiguity

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?

Two workers each waiting for the other's tool

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 Boot 2.6, even a resolvable cycle is forbidden

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.

A guard and the building's entry/exit log

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
Two kinds of stunt double

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)

A guard who only stands at the main door

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:

  1. Move inner() to a separate bean and inject it; now the call goes through that bean's proxy. Cleanest.

  2. Self-inject the proxy and call self.inner() (use @Lazy or ObjectProvider to avoid a construction cycle).

  3. Use AopContext.currentProxy() with @EnableAspectJAutoProxy(exposeProxy = true) — it works but couples code to Spring AOP; avoid where possible.

Related proxy pitfalls

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

  • @Autowired on Optional<T> / @Nullable / ObjectProvider<T> signals an optional dependency; but a plain required type throws NoSuchBeanDefinitionException when the bean is absent.
  • Field injection + final is impossible; field injection also breaks constructor-based immutability guarantees.
  • @Value("${prop}") needs a PropertySourcesPlaceholderConfigurer (auto-provided by Boot). A missing property → startup failure, unless you use ${prop:default} (a default value).
  • Prototype @PreDestroy never runs. Manage prototype cleanup manually, or via a DisposableBean you call yourself.
  • Two @Configuration classes each declaring a @Bean of the same name → the later registration overrides; but Boot forbids overriding by default (spring.main.allow-bean-definition-overriding=false).
  • @Async/@Transactional on the same class as the caller → self-invocation, silently no-ops.
  • Ordering of @PostConstruct across different beans is not globally guaranteed, except through explicit @DependsOn or actual dependency edges.

Best practices

  1. Use constructor injection with final fields; let Lombok @RequiredArgsConstructor or a hand-written constructor do the wiring.
  2. Keep configuration in @Configuration classes; reach for proxyBeanMethods = false only when the methods are independent and startup speed matters.
  3. Never rely on circular references; break them structurally.
  4. Prefer ObjectProvider over scoped proxies for injecting narrower-scoped beans — it's explicit and test-friendly.
  5. Keep AOP-annotated methods public and call them through the bean, never via this..
  6. Put @Transactional at the service layer, not on controllers or repositories, and always remember it's a proxy — design for that.

Interview Questions

1. Why is constructor injection preferred over field injection?

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.

2. What is the difference between BeanFactory and ApplicationContext?

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.

3. Give the exact bean lifecycle order including @PostConstruct, InitializingBean, and BeanPostProcessor.

Instantiate → populate deps → *Aware callbacks → BPP.postProcessBeforeInitialization@PostConstructafterPropertiesSet() → custom init-method → BPP.postProcessAfterInitialization (proxies created here) → in service → @PreDestroydestroy() → custom destroy-method.

4. (Gotcha) How many Shared instances does this print, and why?
@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.

5. (Gotcha) Why does this @Transactional do nothing?
@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().

6. Explain the singleton-injects-prototype problem and three fixes.

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.

7. When does Spring use a JDK proxy vs CGLIB, and what did Spring Boot change?

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.

8. (Hard) Can Spring resolve a circular dependency? Under what conditions does it fail?

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).

9. Is a prototype bean's @PreDestroy called? What about @PostConstruct?

@PostConstruct yes; @PreDestroy no. Spring doesn't track prototype instances after handing them out, so you must clean up prototype resources yourself.

10. What does @Repository add beyond @Component?

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.

11. (Gotcha) @Transactional on a private method — what happens?

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.

12. What is proxyBeanMethods = false and when should you use it?

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.

13. (Hard) Injecting a request-scoped bean into a singleton — what breaks and how do you fix it?

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.

14. Difference between BeanPostProcessor and BeanFactoryPostProcessor?

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.

15. @Primary vs @Qualifier — which takes precedence?

@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.

In a nutshell
  • 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), not BeanFactory (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 with ObjectProvider.
  • @Configuration is 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.