مساله Inject کردن prototype bean درsingleton bean در Spring

وقتی شما با فریم ورک هایی چون Spring کار می کنید سعی می کنید تا جایی که می تونید از کلیدواژه (اپراتور) new کمتر استفاده کنید برای ساخت آبجکت جدید و این کار رو می سپارید به دست Big Brother یعنی فریم ورک اعظم !چرا ؟ برای اینکه اکثر فریم ورک های مدرن براساس یکسری ویژگی ها ساخته و پیاده سازی می شوند تا کار برنامه نویس و توسعه کد را ساده کنند . البته به شرطی که برنامه نویس به اون اصول و ویژگی ها وفادار باشه و ذهنشو در همون قالب ببره جلو .مثل چاقوی دو لبه هس هم می تونه خوب باشه هم بد . برای برنامه نویس مبتدی استفاده از فریم ورک ها به نظرم حکم سم و زهر رو داره و جلوی یادگیری و درک عمیق تکنولوژی ها و زبانها رو میگیره و نمیزاره پوست شما در این راه کنده و یادگیری تون عمیق بشه :)))) باری ، یکی از اون ویژگیها / اصول ، اصل Dependency Injection می باشد. اصل / ویژگی Dependency Injection می گوید :برنامه نویس جان ! تو نوع ، نحوه کانفیگ ، زمان ایجاد و تعداد Bean هایی که می خواهی بهم بگو من خودم اونارو مدیریت می کنم . درضمن هر جایی دیدی یک bean / object به object/bean دیگری نیاز دارد . کافیه اسم و نوع اون رو داخل کدت اعلام کنی ، اینکه اون bean/object چطوری و کی ساخته میشه و به کد تو تزریق میشه کاری نداشته باش اونا رو من هندل می کنم .در Spring معمولا از هر bean به صورت پیش فرض یک دونه ساخته میشه به اصطلاح Scope پیش فرض ساخت bean درفریم ورک Singleton ست . scope های دیگری هم در این فریم ورک تعریف شده که بر حسب نیاز پروژه مون می تونیم ازشون استفاده کنیم این scope ها عبارتند از :singleton(default)prototyperequestsessionglobalموقع کد زدن و تعریف bean ها معمولا پیش فرض ذهنی ما همون singleton scope هس مگه اینکه موقعیت /سناریویی برامون پیش بیاد که ببینیم نه باید بریم سراغ scope دیگه . یکی از سناریوها این است که bean ما stateful است و به ازای هر بار مراجعه بهش حالت و وضعیتش قراره تغییر کنه و مهم تر اینکه اکثر این bean ها قراره توی یه محیط multi-threading استفاده بشه واین می تونه یه تهدید و باگ برای برنامه ما در محیط عملیاتی باشه .. در اینجا ست که scope اون bean رو تغییر می دیم . باز این مشکل نیس مشکل موقعی پیش میاد که می خواهیم این bean ها رو بهمدیگه اصطلاحا سیم بندی (wire) کنیم یعنی چی؟یعنی اینکه یک bean با scope ی از نوع prototype رو به یک bean با scopeی از نوع singleton سیم بندی کنیم . خب مشکل اینجا چیه ؟مشکل اینکه spring به عنوان big brother و مدیر موقع راه اندازی پروژه همه این bean ها رو در کل پروژه جستجو کرده و سعی می کند از هر کدوم یک دونه بسازه و هر جایی لازم شد همون رو به داخل کد موردنظر inject کنه . وقتی همه bean ها از نوع singleton باشند مشکلی نیس از هر bean یه دونه ساخته میشه و تموم . اما اگه bean از نوع prototype باشه و هر موقع جایی از کد به اون نیاز میشه لازمه فریم ورک یک نمونه جدید ازش بسازد و در اختیارش قرار دهد و معمولا اون قسمت از کد که درخواست این bean را دارد همون جا هم میگه من یه bean می خوام با این مشخصات و ویژگی ها . مشکل اینجاست وقتی beanی از نوع singleton باشه یه بار ازش ساخته میشه و حین ساختش ، هر bean دیگه ای بهش نیاز داره اونم باید ساخته بشه و به این beanی singleton تزریق بشه حالا فرض کنیم اون bean از نوع prototype باشه چیکار کنیم ؟ راه حل روی کاغذ :راه حلش اینه همون جا یه جوری به spring بگیم ببین beanی singletone ی من الان نیاز به beanی از نوع prototype داره ولی تو الان اون beanی prototype ی رو نساز بزار هر موقع بهش ارجاع واقعی دادم بساز . حالا بزار پروژه رو کامپایل کنم بعد...راه حل واقعی همراه با یک مثال کاربردی :بعد از اینکه صورت مساله رو بیان کردیم می رویم سراغ یک مثال کاربردی برای ارایه یک راه حل مناسب برای این مساله . مثال رو از یک پروژه ای که خودم باهاش برخورد کردم بیان می کنم . توی پروژه من یک Rest Controller ی بود که یک درخواست از مشتری می گرفت مشتری رو از نظر هویتی بررسی می کرد و در صورت تایید هویت و چک کردن مقدار اعتبار شارژش خدمتی بهش ارایه میداد. حین دادن خدمت بهش همزمان نیاز بود مقداری از اعتبار شارژش کم بشه و این کم شدن از اعتبارش رو به صورت یک درخواست http به میکروسرویس دیگری میداد. در پایین این کدهای مربوط به عمل کم کردن اعتبار مشتری مشاهده می کنید (خلاصه شده جهت درک بهتر موضوع )public class UserConsumptionLots implements Runnable{ private final RestTemplate restTemplate; private final Environment env; private String deviceToken; private String userToken; public void setDeviceToken(String deviceToken) { this.deviceToken = deviceToken; } public void setUserToken(String userToken) { this.userToken = userToken; } public UserConsumptionLots(RestTemplate restTemplate, Environment env) { this.restTemplate = restTemplate; this.env = env; } @Override public void run() { // conntec to microservice and send http request }}در واقع در داخل این کلاس یک task تعریف شده که قراره به صورت async توسط یه thread انجام بشه .در ادامه اون Rest Controller رو داریم که قراره از این کلاس ما استفاده کنه همون طور که میدونید معمولا اغلب Rest controller ها به صورت singletone bean ها تعریف می شوند و باید هم اینطوری باشند : @PostMapping(&quot/path&quot) public ResponseEntity<Item> recognize() { UserConsumptionLots userConsumptionLots=getUserConsumptionLots(); userConsumptionLots.setDeviceToken(deviceToken); userConsumptionLots.setUserToken(userToken); taskExecutor.execute(userConsumptionLots); return ResponseEntity.ok(); } این restController ما از دو bean بنام های TaskExecutor و UserConsumption استفاده می کند . bean دومی یعنی UserConsumption از نوع prototype است چرا که به ازای هر درخواست کاربر باید مقداری درش ست بشود و برحسب اونا کاری صورت بگیرد .(به کد بالایی و دو پارامتر deviceToken و userTokenتوجه کنید ) نحوه کانفیگ این دو bean به صورت زیر می باشد :@Configuration @EnableAsyncpublic class AsyncConfiguration { @Bean(&quotthreadPoolTaskExecutor&quot) public TaskExecutor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); executor.setMaxPoolSize(100); // executor.setWaitForTasksToCompleteOnShutdown(true); executor.setThreadNamePrefix(&quotAsync-consume-user-limit&quot); executor.initialize(); return executor; } @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public UserConsumptionLots userConsumptionLots(RestTemplate restTemplate, Environment env) { return new UserConsumptionLots(restTemplate, env); }}حالا می رویم سراغ راه حل اینکه چطوری این دو bean یعنی RestController و UserConsumptionLots رو بهم سیم بندی کنیم . یک روش خیلی رایج استفاده از @Lookup annotation است . به این صورت که در داخل همون کلاس restController یک متدی به صورت زیر تعریف می کنیم :@Lookup public UserConsumptionLots getUserConsumptionLots() { return null; } در این روش Spring متد getPrototypeBean() که با @Lookup علامت گذاری شده را override می کند. ولی به اصطلاح عامیانه اسم این bean رو گوشه ذهنش ثبت می کند . و هرموقع نیاز بهش شد یک نمونه جدید می سازد و تحویل میدهد. در این روش spring از CGLIB استفاده می کند برای تولید بایت کد که وظیفه اش ساخت یک نمونه جدید از bean است .روشهای دیگری هم برای این مساله هست ولی رایج ترین روش همینی بود که سعی کردم توضیح بدم . امیدوارم مفید بوده باشه برای شما . اگه علاقه مند به استفاده از روشهای دیگر هستید مقاله زیر را می تونید مطالعه کنید https://www.baeldung.com/spring-inject-prototype-bean-into-singleton