ظرف IoC – قسمت 3 از 3 – Spring MVC

مدیریت چرخه حیات یک bean

با اتفاقاتی که می تواند برای یک bean موجود در کارخانه بیافتد، چرخه حیات آن می تواند بسیار ساده و یا نسبتا پیچیده ای باشد. از آنجایی که ما در مورد POJO[1] صحبت می کنیم، چرخه حیات bean به چیزی بیش از ایجاد و استفاده از آن شی وابسته نیست. با این حال، روش هایی وجود دارند که به کمک آن ها می توان چرخه حیات را مدیریت و کنترل کرد. این روش ها عمدتا بر روی متدهای callback متمرکز هستند. این فراخوانی ها توسط اشیاء bean و همچنین اشیاء ناظری که به پساپردازنده های bean معروف هستند، در مراحل مختلف مقداردهی اولیه و تخریب[۲] دریافت می شود. در جدول زیر اقدامات وابسته به ظرف که در چرخه حیات یک bean رخ می دهد قرار گرفته است.

عمل انجام شده توضیحات
هنگاهی که یک نمونه از bean ساخته شده، مقداردهی اولیه آغاز می شود یک bean جدید از طریق یک سازنده و یا متد کارخانه ایجاد می شود. همانطور که دیده شد این دو روش معادل هستند. این رویداد زمانی اتفاق می افتد که ()getBean کارخانه فراخوانی شده و یا bean دیگری که در حال ساخت است به عنوان یک وابستگی به این bean نیاز داشته باشد.
وابستگی ها ترزیق می شوند همانطور که قبلا گفته شد، وابستگی ها در این نمونه تزریق می شوند.
متد ()setBeanName فراخوانی می شود اگر این bean رابط اختیاری BeanNameAware را پیاده سازی کرده باشد، متد ()setBeanName این رابط برای ارسال شناسه اصلی از تعریف bean به این شی فراخوانی می شود.
متد ()setBeanFactory فراخوانی می شود اگر این bean رابط اختیاری BeanFactoryAware را پیاده سازی کرده باشد، متد ()setBeanFactory این رابط فراخوانی می شود تا یک ارجاع به کارخانه ای که bean در آن قرار گرفته است، در اختیار آن قرار دهد. توجه کنید که چون بسترهای برنامه، کارخانه bean هستند، این متد برای اشیاء bean مستقر در بستر برنامه نیز فراخوانی شده و به عنوان آرگومان، کارخانه bean داخل بستر برنامه به متد ارسال می شود.
متد ()setResourceLoader فراخوانی می شود اگر این bean رابط اختیاری ResourceLoader را پیاده سازی کرده و در یک بستر برنامه مستقر شده باشد، در این صورت متد ()setResourceLoader این رابط، به همراه بستر برنامه تحت عنوان آرگومان ResourceLoader فراخوانی می شود.
متد ()setApplicationEventPublisher فراخوانی می شود اگر این bean رابط اختیاری ApplicationEventPublisherAware را پیاده سازی کرده و در یک بستر برنامه مستقر شده باشد، در این صورت متد ()setApplicationEventPublisher این رابط، همراه با بستر برنامه تحت عنوان ApplicationEventPublisher فراخوانی می شود.
متد ()setMessageSource فراخوانی می شود اگر این bean رابط اختیاری MessageSourceAware را پیاده سازی کرده و در یک بستر برنامه مستقر شده باشد، در این صورت متد ()setMessageSource این رابط، همراه با بستر برنامه تحت عنوان MessageSource فراخوانی می شود.
متد ()setApplicationContext فراخوانی می شود اگر این bean رابط اختیاری ApplicationContext را پیاده سازی کرده و در یک بستر برنامه مستقر شده باشد، در این صورت متد ()setApplicationContext این رابط برای تهیه یک ارجاع به بستر فراخوانی می شود.
روال “before-initialization” پساپردازنده های bean با مقدار شی bean جدید فراخوانی می شود پساپردازنده های bean، که در ادامه مطالعه خواهند شد، کنترلرهای ویژه ای هستند که برنامه های کاربردی می توانند آن ها را در کارخانه ثبت کنند. پساپردازنده ها یک callback با عنوان before-initialization دارند که bean را به عنوان آرگومان دریافت کرده و در صورت نیاز آن را تغییر می دهد.
متد ()afterPropertiesSet فراخوانی می شود اگر این bean رابط نشانگر[۳] InitializingBean را پیاده سازی کرده باشد، در این صورت متد ()afterPropertiesSet این رابط فراخوانی شده تا به bean اجازه دهد که خود را مقداردهی اولیه کند.
متد init تعیین شده از bean فراخوانی می شود اگر در تعریف bean یک متد مقداردهی اولیه از طریق خصیصه init-method تعریف شده باشد، در این صورت این متد فراخوانی شده تا bean بتواند از این طریق خود را مقداردهی اولیه کند.
روال “after-initialization” پساپردازنده های bean با مقدار شی bean جدید فراخوانی می شود تمام پساپردازنده ها یک callback با عنوان after-initialization دارند که bean را به عنوان آرگومان دریافت کرده و در صورت نیاز آن را تغییر می دهد.
شی bean ایجاد شده استفاده می شود در این مرحله این نمونه bean می تواند عملا استفاده شود. به این معنی که این bean می تواند به فراخواننده ()getBean برگردانده شده و یا به عنوان یک وابستگی برای تنظیم یک خصیصه در bean دیگری که در حال ایجاد است مورد استفاده قرار گیرد. نکته مهم: تنها اشیاء bean یگانه هستند که از این لحظه ردیابی[۴] می شوند. زیرا فرض بر این است که اشیاء bean غیریگانه توسط مالک آن ها کنترل می شوند. بنابراین ظرف، تنها چرخه حیات یک bean یگانه را کنترل خواهد کرد. از این قدم به بعد تمام اشیاء bean غیریگانه باید به طور کامل توسط مالک آن ها مدیریت شوند و در صورتی که نیاز به تخریب این نوع bean وجود دارد، این کار باید توسط مالک آن انجام شود.
عملیات تخریب bean آغاز می شود به عنوان بخشی از فرایند اتمام کار کارخانه bean یا بستر برنامه، تمام اشیاء bean یگانه باید از بین بروند. این فرایند از گام های زیر تشکیل شده است. توجه کنید که اشیاء bean متناسب با رابطه وابستگی خود، به ترتیب از بین می روند. معمولا این روند عکس مرحله ایجاد است.
روال “destroy” پساپردازنده های bean با مقدار شی bean جدید فراخوانی می شود هر پساپردازنده ای که رابط DestructionAwareBeanPostProcessors را پیاده سازی کرده باشد برای دستکاری bean یگانه به منظور تخریب آن فراخوانی می شود.
متد ()destroy فراخوانی می شود اگر این bean یگانه رابط نشانگر DisposableBean را پیاده سازی کرده باشد، در این صورت متد ()destroy این رابط فراخوانی شده تا به bean اجازه دهد تا تمام پاکسازی های مورد نیاز را بر روی منابع خود انجام دهد.
متد تعیین شده تخریب شی bean فراخوانی می شود اگر از طریق خصیصه destroy-method تعریف bean یک متد مشخص شده باشد، در این صورت این متد فراخوانی شده تا به bean اجازه دهد که تمام منابعی که باید آزاد شوند را برگرداند.

 

متدهای مقداردهی اولیه و تخریب

هدف متدهای مقداردهی اولیه و تخریب که در بالا به آن ها اشاره شداین است که به bean اجازه دهد تا هرگونه تخصیص منابع مورد نیاز و یا آزاد سازی آن ها را انجام دهد. در صورت استفاده از متدهایی با نام های مشخص برای مقداردهی اولیه و آزادسازی منابع، باید این متدها را جهت فراخوانی در زمان مناسب به ظرف معرفی کنیم. برای این کار از صفات init-method و destroy-method استفاده می کنیم. مثال زیر متد close() را برای آزادسازی منبع DataSource در پایان کار، معرفی می کند:

 

 

در کل، توصیه می شود حتی برای پیاده سازی یک bean جدید از صفات init-method و destroy-method استفاده شده تا از این طریق، ظرف در مورد متدهای سازنده و مخرب آگاهی یابد. به جای این روش، می توان از روش های دیگری مثل پیاده سازی رابط های خاص اسپرینگ InitializingBean و DisposableBean به همراه متدهای ()afterPropertiesSet و ()destroy موجود در آن ها استفاده کرد. این رویکرد زمانی راحت تر است که این رابط ها توسط اسپرینگ شناخته و متدهای آن ها به طور خودکار فراخوانی شوند، اما دقت کنید که این کار باعث گره خوردن غیر ضروری کد به ظرف اسپرینگ می شود. اگر کد به دلیل دیگری به اسپرینگ گره خورده باشد، در این صورت این روش مشکل چندانی ایجاد نمی کند و استفاده از رابط می تواند کار برنامه نویس را ساده تر کند. کد چارچوب اسپرینگ به گونه ای نوشته شده است که اشیاء bean درون ظرف بتوانند از این رابط ها استفاده زیادی بکنند.

همانطور که گفته شد، اشیاء bean غیریگانه، پس از مرحله مقداردهی اولیه، توسط ظرف نگه داری نمی شوند. به این ترتیب، این امکان برای اسپرینگ وجود ندارد که متد مخرب اشیاء bean غیریگانه را فراخوانی کرده و یا اعمال مربوط به کنترل چرخه حیات موجود در ظرف را بر روی آن ها اعمال کرد. این متدها باید توسط کد کاربر فراخوانی شود. علاوه بر این، پساپردازنده های bean امکان دستکاری این اشیاء bean را در مرحله تخریب نخواهند داشت.

رابط های callback به نام BeanFactoryAware و ApplicationContextAware

اگر شی bean بخواهد از کارخانه bean یا بستر برنامه خود اطلاع و به آن دسترسی داشته باشد، باید به ترتیب رابط BeanFactoryAware و ApplicationContextAware را پیاده سازی کند. در لیست اعمال چرخه حیات، که بالاتر به آن اشاره شد، ظرف، متدهای ()setBeanFactory و ()setApplicationContext این رابط ها را که در bean پیاده سازی شده اند، فراخوانی کرده و یک ارجاع از خود به آن ها می فرستد. در کل اغلب برنامه ها به دسترسی به ظرف نیازی ندارند. اما گاهی اوقات ممکن است این ارجاع مفید باشد. این فراخوانی های callback در بسیاری از کلاس های خود اسپرینگ استفاده می شود. از جمله زمان هایی که ممکن است کد برنامه به ارجاع به کارخانه نیاز داشته باشد زمانی است که bean یگانه به اشیاء bean غیریگانه نیاز دارد. از آنجا که وابستگی های bean یگانه تنها یک بار تزریق می شود، به نظر می رسد مکانیسم تزریق وابستگی، اجازه دریافت نمونه bean مورد نیاز و جدید را نمی دهد. بنابراین، دسترسی مستقیم به کارخانه به این bean اجازه می دهد که این کار انجام پذیر باشد. با این حال، احساس می شود که تزریق متد Lookup، مکانیسم بهتری برای رسیدن به این مورد باشد. چرا که با در این روش کلاس bean هیچ اطلاعی از اسپرینگ و یا نام bean غیریگانه نخواهد داشت.

مجرد سازی دسترسی به سرویس ها و منابع

با وجود این که هنوز تعدادی از ویژگی های پیشرفته تر کارخانه bean و بستر برنامه مورد بررسی قرار نگرفته اند، اما تا این قسمت تقریبا تمام امکانات سطح پایین برنامه نویسی و استقرار IoC مطالعه شده است. ما دیدیم که چگونه اشیاء برنامه کاربردی کار خود را در کنار سایر اشیاءای که توسط ظرف تامین شده اند انجام می دهند. همچنین دیدیم که این اشیاء چگونه با رابط ها و یا کلاس های انتزاعی کار می کنند و دیگر نگران پیاده سازی این کلاس ها نیستند.

و حال این سوال پیش می آید که در شرایطی که سایر اشیاء bean مورد نیاز برای یک bean می توانند به صور مختلفی پیکربندی شده و در دسترس قرار گیرند، چگونه باید این اشیاء را تهیه کرد. در ادامه در قالب یک مثال بررسی می کنیم که اسپرینگ چگونه از برخی از پیچیدگی های احتمالی جلوگیری کرده و مدیریت ها را به صورت شفاف انجام می دهد.

شکل متفاوتی از مثال DAO آب و هوا را در نظر بگیرید که به جای کار با داده های ایستا، از JDBC برای دسترسی به داده های درون یک پایگاه داده استفاده می کند. در پیاده سازی اولیه، برای ایجاد یک اتصال از رویکرد DriverManager موجود در JDBC استفاده شده است؛ این رویکرد در متد find() نشان داده شده است:

 

 

اگر این DAO به عنوان یک bean در ظرف اسپرینگ قرار می گیرد، می تواند از برخی از مزایای ظرف استفاده کند. از جمله می توان به درج مقدار مورد نظر برای url موجود در درایور JDBC، نام کاربری و رمز عبور مورد نیاز، از طریق تزریق قراردهنده و یا سازنده اشاره کرد.

راه مناسب برای ایجاد ارتباط استفاده از JDBC DataSource است. هنگامی که DAO ایجاد شده، یک DataSource داشته باشد به راحتی می تواند یک ارتباط را از آن درخواست کرده و در مورد چگونگی ایجاد این ارتباط نگران نباشد. بنابراین از دیدگاه نظری مشکل، وجود و قابلیت در دسترس بودن DataSource نیست. به عنوان مثال پیاده سازی های مستقل مجموعه ارتباطات، مانند DBCP از Apache Jakarta Commons وجود دارد که می تواند در محیط J2SE یا J2EE استفاده شود و توسط رابط DataSource مورد استفاده قرار گیرد. همچنین در بسیاری از محیط های ظرف J2EE، یک مجموعه ارتباطات تحت کنترل ظرف نیز موجود است که در تراکنش های تحت مدیریت ظرف همکاری می کند. این مجموعه نیز می تواند توسط اشیاء DataSource استفاده شود.

با این حال، ایجاد ارتباط از طریق رابط DataSource پیچیدگی های خود را دارد، چرا که روش های مختلفی برای ایجاد و تهیه یک DataSource وجود دارد. یک DataSource از نوع DBCP می تواند به سادگی به عنوان یک JavaBean ایجاد و برخی از خصیصه های پیکربندی به آن داده شود. در حالی که همانند توالی زیر، در اغلب محیط های ظرف J2EE، یک DataSource تحت کنترل ظرف، از طریق JNDI به دست آمده و استفاده می شود:

 

 

سایر اشیاء DataSource می توانند با یک استراتژی کاملا متفاوت ایجاد شده و در دسترس قرار گیرند.

DAO مثال بالا می توانست خودش در مورد طریقه ایجاد و یا تامین هر نوع DataSource مورد نیاز اطلاعات کافی داشته و برای استفاده از یکی از آن ها پیکربندی شود. اما با توجه به اطلاعاتی که در مورد اسپرینگ و IoC اطلاعات داریم، می دانیم که این روش، روش مناسبی نیست. چرا که این روش، DAO را به پیاده سازی های غیر ضروری DataSource گره زده، انجام تنظیمات را سخت تر کرده و باعث می شود تست نرم افزار پیچیده تر شود. راه حل مناسبی که IoC ارائه می دهد این است که DataSource را به عنوان یک خصیصه از DAO در نظر بگیریم و در نتیجه بتوانیم آن را از طریق ظرف اسپرینگ به DAO تزریق کنیم. این روش برای پیاده سازی DBCP به خوبی کار می کند، زیرا می تواند DataSource را به عنوان یک bean ایجاد و به DAO تزریق کند:

 

 

حال این سوال مطرح می شود که چگونه می توان به جای استفاده از DataSource از نوع DBCP، از یک DataSource به دست آمده از JNDI استفاده شود؟ به نظر می رسد کار ساده ای نباشد که مقدار گرفته شده از JNDI را در خصیصه ای از نوع DataSource درون DAO قرار داد؛ تنها چیزی که ما تا این لحظه می دانیم این است که چگونه باید در پیکربندی ظرف، خصیصه های JavaBean را به عنوان یک مقدار تعریف کرده و آن را به bean دیگری ارجاع دهیم. در واقع، این دقیقا همان کاری است که باید انجام دهیم. در مثال زیر این کار به همراه یک کلاس کمکی به نام JndiObjectFactoryBean انجام می شود.

 

مطالعه و بررسی اشیاء bean کارخانه

JndiObjectFactoryBean یکی از انواع نمونه های bean کارخانه[۵] است. یک bean کارخانه بر پایه یک مفهوم بسیار ساده بنا می شود: یک bean کارخانه دقیقا یک bean است که در صورت درخواست از آن، bean دیگری تولید می کند. همه اشیاء bean کارخانه رابط مخصوص org.springframework.beans.factory.FactoryBean را پیاده سازی می کنند. از آنجایی که به وسیله این نوع bean یک سطح غیر مستقیم معرفی می شود، بنابراین می توان گفت که این، یک نوع bean خاص است. یک bean کارخانه یک JavaBean عادی است. هنگامی که یک bean کارخانه تعریف می شود، باید همانند هر JavaBean دیگر، اعضاء داده ای و آرگومان های مورد نیاز برای انجام کار آن مشخص شوند. هنگامی که bean دیگری در ظرف، از طریق عنصر <ref> به bean کارخانه اشاره کند، و یا زمانی که از طریق ()getBean، یک درخواست برای bean کارخانه ارسال شود، ظرف، خود bean کارخانه را بر نمی گرداند؛ از آنجایی که ظرف از طریق رابط bean متوجه می شود که این، یک bean کارخانه است بنابراین، خروجی این bean کارخانه را به جای خود آن برمی گرداند. بنابراین هر جایی که از bean کارخانه برای ایجاد وابستگی های مورد نیاز استفاده شود، می توان bean کارخانه را همان شی ای نگریست که عملا تولید می گردد. برای مثال، در صورت استفاده از JndiObjectFactoryBean، نتیجه ای JNDI را بازمی گرداند، که در مثال ما این شیء یک DataSource است.

رابط FactoryBean بسیار ساده است:

 

 

با فراخوانی متد ()getObject توسط ظرف، شی خروجی مورد نظر به دست می آید. متد ()isSingleton نشان می دهد که آیا در هربار فراخوانی، نمونه شی مشابه به عنوان خروجی بازمی گردد. در نهایت، متد ()getObjectType به نوع شی بازگشتی اشاره می کند و یا در صورتی که نوع شی بازگشتی شناخته شده نباشد، مقدار null برمی گرداند. از آنجایی که اشیاء bean کارخانه به طور معمول در ظرف پیکربندی و مستقر می شوند، بنابراین این نوع اشیاء، یک JavaBean بوده و در صورت نیاز می توانند از طریق برنامه نویسی استفاده شوند.

نکته مهم: سوالی که ایجاد می شود این است که در صورتی که با درخواست از یک bean کارخانه، نتیجه خروجی آن بازمی گردد، چگونه می توان از طریق فراخوانی ()getBean خود این bean کارخانه را به دست آورد. این کار با استفاده از مکانیسمی به نام escaping قابل انجام است. شما از این طریق به ظرف می گویید که می خواهید خود bean کارخانه را برگرداند و نه خروجی آن را. برای این کار باید همانند زیر یک علامت & را قبل از شناسه bean قرار دهید:

 

 

شما می توانید به راحتی برای خود اشیاء bean کارخانه مورد نیاز را بسازید. با این حال، اسپرینگ تعدادی bean کارخانه مفید را پیاده سازی کرده است که در بسیاری از منابع و سرویس های مشترک، تجردسازی را به وجود آورده اند و کنترل آن ها از این روش سودمندتر است. بخشی از این اشیاء عبارتند از:

  • JndiObjectFactoryBean: شی ای را که نتیجه JNDI است، برمی گرداند.
  • ProxyFactoryBean: شی موجود را در یک پروکسی قرار داده و برمی گرداند. آن چه پروکسی باید انجام دهد توسط کاربر پیکربندی می شود و می تواند یک interception برای تغییر رفتار شی، یک شی برای بررسی عملکرد امنیتی و غیره باشد.
  • TransactionProxyFactoryBean: حالت خاص ProxyFactoryBean است که شی را با یک پروکسی تراکنشی می پوشاند.
  • RmiProxyFactoryBean: برای دسترسی یک شی از راه دور به صورت شفاف و از طریق RMI، یک پروکسی ایجاد می کند. HttpInvokerProxyFactoryBean، JaxRpcPortProxyFactoryBean، HessianProxyFactoryBean و BurlapProxyFactoryBean پروکسی های مشابهی برای دسترسی به اشیاء راه دور، به ترتیب از طرق HTTP، JAX-RPC، Hessian و پروتکل های Burlap ایجاد می کنند. در تمام موارد، استفاده کنندگان از پروکسی بی اطلاع بوده و تنها با رابط مربوطه کار می کند.
  • LocalSessionFactoryBean: یک شی Hibernate SessionFactory پیکربندی کرده و بازمی گرداند. کلاس های مشابهی برای مدیران منابع JDO و iBatis وجود دارند.
  • LocalStatelessSessionProxyFactoryBean و SimpleRemoteStatelessSessionProxyFactoryBean: برای دسترسی به اشیاء bean نشست های محلی و یا راه دور و به صورت stateless، یک شی پروکسی ایجاد می کند. سرویس گیرنده تنها با رابط مربوطه ارتباط داشته و در مورد رابط های JNDI و یا EJB نگرانی ندارد.
  • MethodInvokingFactoryBean: نتیجه فراخوانی یک متد از یک bean دیگر را برمی گرداند. FieldRetrievingFactoryBean مقدار یک فیلد از یک bean دیگر را برمی گرداند.
  • تعدادی از اشیاء bean کارخانه مرتبط با JMS، منابع JMS را برمی گردانند.

چند نکته کلیدی

ما در بخش های قبل دیدیم که ایجاد ارتباط بین اشیاء در روش IoC به چه میزان ساده است. با این حال، قبل از این نوع ایجاد ارتباط با یکدیگر، اشیاء باید ایجاد شده و یا در اختیار قرار گیرند. برخی از این اشیاء، حتی اگر بخواهند یک بار در اختیار قرار گرفته و پیکربندی شوند در نهایت باید از طریق یک رابط استاندارد در دسترس قرار بگیرند. این واقعیت که معمولا ایجاد و یا به دست آوردن این اشیا از طریق مکانیسم های پیچیده و یا غیر استاندارد انجام می شود، حتی برای دریافت آن ها در ابتدای کار نیز مانعی به وجود می آورد. اشیاء bean کارخانه می توانند این مانع را از بین ببرند. خروجی های اشیاء bean کارخانه، پس از طی مراحل ایجاد اولیه اشیاء و ایجاد ارتباطات، با قرار گرفتن در پروکسی ها و اشیاء پوشاننده مشابه، می توانند در نقش یک آداپتور خدمت کرده، به ایجاد انتزاع برای استفاده از منابع و سرویس های واقعی کمک کرده و باعث شوند بتوان از طریق یک رابط مشابه به سرویس های متفاوتی دسترسی یافت.

همانطور که دیدید، بدون این که نیاز به آگاه سازی و یا تغییر در مشتری، یعنی weatherDao داشته باشیم، به راحتی DataSource اولیه مبتنی بر DBCP را با یک bean محلی که از JNDI دریافت شد جایگزین کردیم. IoC توانست این جایگزینی را انجام دهد، اما برای این کار لازم بود که دسترسی به منابع را از کد برنامه خارج کرده، تا بتوانند توسط IoC کنترل شوند.

نکته مهم: مباحث گفته شده، ما را به این نتیجه می رساند که زمانی که بتوان دسترسی به سرویس و منابع را مانند بالا انتزاعی کرد و در صورت نیاز از یک مکانیسم به دیگری منتقل شد، دلیلی وجود ندارد که از سناریوها و فن آوری های زمان استقرار در زمان تولید کد استفاده شود. در هر صورت، سرویس گیرنده نباید به سناریوی پیاده سازی و استقرار وابسته باشد.

اگر این امکان وجود داشته باشد که دسترسی به خدمات راه دور به صورت شفاف و از طریق RMI، RPC بر روی HTTP یا EJB انجام شود، بهتر است از این راه حل استفاده شود و دلیلی وجود ندارد که سرویس گیرنده را به یکی از پیاده سازی ها متصل کنیم. J2EE به رسم عادت، منابعی مانند DataSource، منابع JMS، رابط های JavaMail و غیره را از طریق JNDI ارائه می دهد. حتی اگر ارائه این منابع از این طریق منطقی به نظر آید، اما دلیل وجود ندارد که سرویس گیرنده نیز مستقیما به JNDI دسترسی داشته باشد. تجرد سازی از طریق ابزاری شبیه JndiObjectFactoryBean این امکان را می دهد که در صورت لزوم بتوانیم به سادگی، به جای تغییر کد سرویس گیرنده، با تغییر پیکربندی کارخانه bean به یک محیط بدون JNDI تغییر مکان دهیم. حتی زمانی که شما نیاز ندارید محیط استقرار و یا فن آوری پیاده سازی را تغییر دهید، این انتزاع به ساده سازی تست unit و یکپارچه[۶] کمک بسیاری می کند، زیرا امکان استقرار پیکربندی های مختلف را برای سناریوهای استقرار و آزمون ایجاد می کند.

استفاده مجدد از تعاریف bean

در صورت توجه به مقدار کد جاوایی که برای پیکربندی و ایجاد ارتباط بین اشیاء، جایگزین کد اصلی می شود، مشخص می شود که تعاریف bean در XML نسبتا فشرده شده است. اما هنوز هم در برخی قسمت ها می توان کد را فشرده تر کرد. به عنوان مثال گاهی اوقات شما نیاز دارید تعدادی bean تعریف کنید که کاملا شبیه به یکدیگر هستند.

به مثال WeatherService بازمی گردیم:

 

در یک سناریوی معمولی برنامه که یک پایگاه داده در پس زمینه قرار دارد، باید از اشیاء سرویس به صورت تراکنشی استفاده کرد. یکی از ساده ترین راه ها برای این کار این است که توسط یک bean کارخانه اسپرینگ به نام TransactionProxyFactoryBean، پوششی برای شی ایجاد کرده که به صورت یک تراکنش درآید:

 

نکته مهم این مثال این است که TransactionProxyFactoryBean یک FactoryBean است که بر اساس bean هدف داده شده، یک شی پروکسی تراکنشی را به عنوان خروجی تولید کرده و این شی، همان رابط bean هدف را، این بار در کنار مفهوم تراکنشی پیاده سازی می کند. از آنجا که ما می خواهیم سرویس گیرندگان از شی پوشانده شده استفاده کنند، شی bean اصلی و بدون پوشش، یعنی همان شی بدون تراکنش، را به weatherServiceTarget تغییر نام داده و نام شی پروکسی را weatherService می گذاریم. به این ترتیب تمام سرویس گیرندگان سرویس آب و هوا، از این موضوع که هم اکنون از یک سرویس تراکنشی استفاده می کنند بی اطلاع هستند.

امکان پوشش اشیاء به صورت تعریفی، به خصوص در مقایسه با روش برنامه نویسی در سطح کد بسیار مفید است، اما در یک برنامه بزرگ با ده ها یا صدها رابط سرویس که باید به روش مشابهی پوشش داده شوند، به نظر می رسد نیاز باشد فایل های XML مشابه زیادی را تولید کرد و برای این کار باید زمان زیادی صرف کرد. در واقع، توانایی ظرف برای تعریف bean والد و فرزند دقیقا همان چیزی است که برای این شرایط می تواند مفید باشد. این امکان می تواند از یک والد انتزاعی یا یک قالب برای تعریف پروکسی تراکنش استفاده کند:

 

 

سپس باید هر یک از پروکسی های واقعی را به عنوان یک فرزند تعریف و تنها خصیصه هایی را مقداردهی کرد که نسبت به والد خود مقدار متفاوتی دارند:

 

 

در واقع، این امکان وجود دارد که حتی یک فرم فشرده تر و مشخص تری را تعریف کرد. از آنجا که هیچ سرویس گیرنده ای به استفاده از bean بدون پوشش سرویس آب و هوا نیاز ندارد، می توان آن را به عنوان یک bean داخلی پروکسی پوشش دهنده تعریف کرد:

 

اگر نیاز باشد سرویس دیگری پوشش داده شود، تنها کافی است مشابه مثال بالا از یک تعریف bean استفاده کرد که از یک قالب والد مشتق شده باشد. در مثال زیر، خصیصه transactionAttributes نیز سربارگذاری شده تا بتوان تنظیمات انتشار تراکنش را که خاص این پروکسی است به این خصیصه اضافه کرد:

 

 

خصیصه های تعریف bean فرزند

تعریف bean فرزند مقادیر خصیصه ها و آرگومان های تعریف شده در تعریف bean والد خود را به ارث می برد. همچنین در صورتی که تعدادی صفت اختیاری درون والد تعریف شده باشد، توسط فرزند ارث بری می شود. همانطور که در مثال قبل دیده شد، خصیصه parent در تعریف bean فرزند برای اشاره به والد استفاده می شود.

در تعریف bean فرزند می تواند یک کلاس مشخص شود. در صورتی که این مقدار مشخص نشده باشد، از والد به ارث خواهد رسید. اگر در تعریف فرزند، نام کلاس مشخص شده باشد –کلاسی که متفاوت از کلاس والد است – آن کلاس نیز باید بتواند آرگومان های سازنده یا مشخصات تعیین شده در تعریف والد را بپذیرد، زیرا این آرگومان ها به ارث برده و در نتیجه استفاده می شوند.

تعریف فرزند تمام آرگومان های سازنده، مقادیر خصیصه ها و متدهای سربارگذاری شده را به ارث می برد، اما در صورت نیاز می توان مقادیر جدیدی را نیز به این تعریف افزود. از سوی دیگر، مقادیر init-method، destroy-method و factory-method نیز از والد به ارث می رسد، که می تواند با تعیین مقادیر متناظر در فرزند به طور کامل سربارگذاری شود.

برخی از تنظیمات پیکربندی bean هرگز به ارث نرسیده و در صورت نیاز باید در تعریف فرزند قرار داشته باشند. این مقادیر عبارتند از: depends-on، autowire، dependency-check، singleton و lazy-init.

به کمک صفت abstract، همانند مثال بالا، می توان تعاریف bean را انتزاعی کرد. از تعاریف bean انتزاعی نمی توان یک نمونه ایجاد کرد. در صورتی که نیاز نیست یک نمونه از bean والد ایجاد شود، بهتر است همیشه آن را به عنوان bean انتزاعی تعریف کرد. از این بابت بهتر است این کار انجام شود که احتمال دارد یک نمونه از bean والد غیر انتزاعی توسط ظرف ایجاد شود، حتی اگر مشخصا از ظرف خواسته نشده باشد که این کار را انجام دهد و یا به عنوان یک وابستگی به آن مراجعه نشده باشد. بسترهای کاربرد، و نه کارخانه های bean، به طور پیش فرض سعی می کنند در ابتدای بارگذاری، یک نمونه از تعاریف bean یگانه غیر انتزاعی را ایجاد کنند.

نکته مهم: اگر در تعریف یک bean، یک کلاس و کارخانه bean مشخص نشده باشد، حتی بدون تعیین صریح خصیصه abstract، به طور ضمنی انتزاعی در نظر گرفته می شود. زیرا این تعریف اطلاعات کافی را برای ایجاد یک نمونه در خود ندارد. هر گونه تلاش صریح یا ضمنی برای ایجاد یک نمونه از روی تعریف bean انتزاعی به یک خطا منجر خواهد شد.

استفاده از پساپردازنده ها برای کنترل اشیاء bean سفارشی و کنترل ظرف ها

پساپردازنده های bean، اشیاء خاصی هستند که به طور صریح یا ضمنی درون ظرف ثبت شده و هنگامی که یک نمونه bean ایجاد شد فراخوانی می شوند. پساپردازنده های کارخانه bean نیز شنوندگان مشابهی هستند که زمانی که خود ظرف ایجاد شود فراخوانی خواهند شد. در زمان های متناوبی نیاز است که یک bean، یک گروه از اشیاء bean و یا تمام تنظیمات ظرف، سفارشی سازی شوند. این کار با ایجاد یک پساپردازنده سفارشی و یا استفاده از یکی از پساپردازنده های اسپرینگ به راحتی انجام پذیر است.

پساپردازنده های bean

پساپردازنده های bean رابط BeanPostProcessor را پیاده سازی می کنند:

 

رابط DestructionAwareBeanPostProcessor این رابط را گسترش می دهد:

 

 

جدول چرخه حیات bean که در بالاتر آورده شده است، لیستی از فراخوانی های پساپردازنده را نشان می دهد که در چرخه حیات bean اتفاق می افتند، در مثال زیر یک پساپردازنده bean ایجاد کرده ایم که با فراخوانی postProcessAfterInitialization، تمام اشیاء bean را که در ظرف مقداردهی اولیه می شوند در خروجی لیست می کند.

 

 

 

نکته: در این مثال کار زیادی انجام نمی شود. معمولا در روال یک پساپردازنده واقعی، در نمونه bean به وجود آمده تغییراتی اعمال شده و اطلاعات، درون یک فایل و نه در کنسول ثبت می شوند. همچنین در یک بستر برنامه، همزمان با سایر اشیاء bean، پساپردازنده های bean نیز به صورت خودکار توسط ظرف شناخته و استفاده می شوند:

 

 

هنگامی که بستری که توسط این پیکربندی مشخص شده است بارگذاری شود، پساپردازنده دو بار فراخوانی شده و خروجی زیر را تولید می کند:

 

 

اگر بخواهیم پساپردازنده های bean را در یک کارخانه bean ساده استفاده کنیم و پیکره بندی آن ها را در فایل XML قرار ندهیم، در این صورت کمی پیچیدگی بیشتر می شود. چرا که به جای این که این پساپردازنده ها فقط به عنوان یک bean در پیکربندی XML افزوده شوند، باید به صورت دستی در کارخانه ثبت گردند:

 

 

همانطور که می بینید، ()BeanFactory.preInstantiateSingletons در ابتدا کار، یک نمونه از تعاریف bean یگانه را در کارخانه ایجاد می کند. زیرا بستر برنامه به صورت پیش فرض اشیاء یگانه را تنها در زمان بارگذاری ایجاد می کند و بنابراین پس از بارگذاری، باید این کار به صورت دستی انجام شود. در هر صورت، هنگامی که یک نمونه bean ساخته شد، پساپردازنده فراخوانی می شود. در صورت انجام مرحله نمونه سازی اولیه در ابتدای کار، فراخوانی پساپردازنده می تواند در ابتدا رخ دهد، و در صورتی که مرحله نمونه سازی اولیه حذف شود، فراخوانی پساپردازنده زمانی رخ می دهد که این bean درخواست شود.

پساپردازنده های کارخانه bean

پساپردازنده های کارخانه bean رابط BeanFactoryPostProcessor را پیاده سازی می کنند:

 

 

مثال زیر یک نمونه پساپردازنده کارخانه bean را پیاده سازی کرده است که تنها لیست نام تمام اشیاء bean موجود در کارخانه را گرفته و چاپ می کند:

 

 

استفاده از پساپردازنده های کارخانه bean نسبتا شبیه استفاده از پساپردازنده های bean است. آن ها باید مانند هر bean دیگر، در یک بستر برنامه مستقر شوند، و در این صورت به طور خودکار شناخته شده و توسط بستر برنامه استفاده خواهند شد. از طرف دیگر در صورت استفاده از یک کارخانه bean ساده، این اشیاء باید به صورت دستی در کارخانه اجرا شوند:

 

 

در ادامه با برخی از پساپردازنده های مفید bean و کارخانه bean موجود در اسپرینگ آشنا خواهیم شد.

PropertyPlaceholderConfigurer

معمولا زمانی که یک برنامه ی مبتنی بر اسپرینگ مستقر[۷] می شود، به تغییر تنظیمات درون پیکربندی ظرف نیاز چندانی وجود ندارد. شاید چندان خوشایند نباشد که برای تغییر تعداد کمی از پارامترها، یک پیکربندی پیچیده بررسی شود. علاوه بر این، این کار می تواند بالقوه مشکل ساز باشد، چرا که ممکن است با تغییر اشتباه پیکربندی مقدار نادرستی در آن قرار گیرد.

PropertyPlaceholderConfigurer یک پساپردازنده کارخانه bean است که زمانی که در تعریف یک کارخانه bean یا بستر برنامه استفاده شود، اجازه می دهد که برخی مقادیر را از طریق رشته های ویژه ای به نام placeholder مشخص کنید. سپس این رشته ها با مقادیری که از یک فایل خارجی و در فرمت Java Properties گرفته می شوند، جایگزین خواهند شد. همچنین به طور پیش فرض، این تنظیم کننده[۸] برای رشته هایی که با مقادیر موجود در فایل های Properties خارجی انطباق پیدا نکنند، در خصیصه های سیستم جاوا[۹] به دنبال مقدار متناظر می گردد. به کمک خصیصه systemPropertiesMode می توان این حالت پیش فرض را تغییر داد. در این صورت تنظیم کننده در خصیصه های سیستم جاوا به دنبال یک مقدار نخواهد بود، و یا اولویت این خصیصه ها را بالا خواهد برد.

از جمله مقادیری که می تواند برای قرار دادن در فایل های خارجی مناسب باشد، رشته هایی هستند که برای پیکربندی ارتباطات پایگاه داده استفاده می شوند. در زیر، مجددا تعریف DataSource مبتنی بر DBCP آمده ، اما این بار از متغیرهای placeholder برای مقادیر واقعی استفاده شده است:

 

 

مقادیر واقعی در یک فایل با فرمت Properties قرار گرفته اند. نام این فایل را jdbc.properties می گذاریم.

 

 

 

برای استفاده از یک نمونه از PropertyPlaceholderConfigurer برای دریافت مقادیر مناسب در بستر برنامه، باید این تنظیم کننده را همانند سایر اشیاء bean دیگر مستقر کرد:

 

 

در یک کارخانه bean ساده، برای استفاده از این تنظیم کننده باید آن را به صورت دستی اجرا کرد:

 

 

 

PropertyOverrideConfigurer

PropertyOverrideConfigurer، یک پساپردازنده کارخانه bean است که تا حدودی شبیه به PropertyPlaceholderConfigurer عمل می کند. اما زمانی که مقادیر تنظیم کننده PropertyPlaceholderConfigurer باید از یک فایل خصیصه خارجی دریافت شود، تنظیم کننده PropertyOverrideConfigurer اجازه می دهد مقادیر خصیصه های خارجی بتوانند بر روی مقادیر خصیصه های bean موجود در کارخانه bean یا بستر برنامه بنشینند، ولی اجباری برای حضور تمام مقادیر در فایل های خارجی نیست.

خطوط فایل Properties باید به فرمت زیر تعریف شوند:

 

 

که در آن beanName شناسه یک bean در ظرف و property یکی از خصیصه های آن است. یک نمونه از فایل Properties می تواند مشابه زیر باشد:

 

چهار خصیصه dataSource توسط این فایل سربارگذاری خواهند شد. تمام خصیصه ها، از هر bean موجود در ظرف که با یک مقدار از فایل Properties سربارگذاری نشده باشند، به سادگی برابر همان مقداری قرار می گیرند که پیکربندی ظرف برای آن مشخص کرده باشد، و اگر پیکربندی ظرف مقداری برای آن تنظیم نکرده باشد برابر مقدار پیش فرض خود باقی خواهند ماند. از آنجا که تنها مشاهده پیکربندی ظرف به شما نشان نخواهد داد که قرار است یک مقدار سربارگذاری شود و باید برای این کار به فایل Properties هم مراجعه کرد، بنابراین بهتر است از این قابلیت با کمی دقت بیشتر استفاده شود.

CustomEditorConfigurer

CustomEditorConfigurer یک پساپردازنده کارخانه bean است که به شما اجازه می دهد تا در صورت نیاز یک JavaBeans سفارشی از نوع PropertyEditors را برای تبدیل مقادیر رشته ای به مقادیری مناسب برای آرگومان های سازنده و یا خصیصه های یک شی خاص، ثبت کنید.

ایجاد یک PropertyEditor سفارشی شده

یکی از دلایلی که ممکن است شما بر اساس آن به استفاده از یک PropertyEditor سفارشی نیاز پیدا کنید، امکان مقداردهی خصیصه هایی از نوع java.util.Date است. همانطور که گفته شد مقادیر به صورت رشته ای مشخص می شوند. از آنجا که فرمت های تاریخ به مکان وابسته هستند، استفاده از PropertyEditor، که به یک فرمت رشته خاص نیاز دارد، ساده ترین راه برای پاسخ دادن به این نیاز است. از آنجا که این یک مشکل برای بسیاری از کاربران به وجود می آید، به جای استفاده از یک مثال ساده و انتزاعی، به بررسی CustomDateEditor موجود در اسپرینگ می پردازیم. این شی باید به عنوان یک PropertyEditor برای تاریخ ثبت و سپس استفاده شود. اشیاء PropertyEditor سفارشی دیگر نیز به روش مشابه پیاده سازی و ثبت خواهند شد:

 

 

ساده ترین راه برای پیاده سازی یک PropertyEditor این است که، همانند مثال بالا از ارث بری کلاس پایه java.beans.PropertyEditorSupport شروع کنیم. این کلاس بخشی از کتابخانه استاندارد جاوا است. این کلاس تمام متدهای مورد نیاز یک ویرایشگر خصیصه[۱۰] را، به جز ()setAsText و ()getAsText پیاده سازی می کند. این دو متد باید توسط شما پیاده سازی شوند. توجه داشته باشید از آن جایی که اشیاء PropertyEditor درون خود اطلاعاتی را نگه داری می کنند، بنابراین به طور معمول threadsafe نیستند، اما اسپرینگ با هماهنگ سازی توالی فراخوانی های مورد نیاز متد برای انجام یک تبدیل، این اطمینان را ایجاد کرده که این اشیاء threadsafe باشند.

CustomDateEditor می تواند از هر نوع پیاده سازی رابط java.text.DateFormat که به عنوان یک آرگومان به سازنده منتقل می شود، برای عمل تبدیل خود استفاده کند. به عنوان مثال شما می توانید در هنگام استقرار از یک java.text.SimpleDateFormat استفاده کنید. همچنین این کلاس می تواند به گونه ای پیکربندی شود که رشته های خالی را به عنوان مقادیر null و یا خطا در نظر بگیرد.

ثبت و استفاده از PropertyEditor سفارشی

حال نگاهی به یک تعریف بستر برنامه می اندازیم که در آن از CustomEditorConfigurer برای ثبت CustomDateEditor به عنوان یک PropertyEditor استفاده شده است تا بتواند رشته ها را به اشیاء java.util.Date تبدیل کند. در این مثال از یک فرمت رشته تاریخ خاص استفاده شده است:

 

 

 

CustomEditorConfigurer می تواند یک یا چند PropertyEditor سفارشی را ثبت کند. در این مثال تنها یک نمونه PropertyEditor ثبت شده است. PropertyEditorهای سفارشی شده برای انواع دیگر نیز می توانند بدون هیچ گونه تنظیمات خاصی استفاده شوند. CustomDateEditor، که در این مثال دو آرگومان سازنده دارد، به عنوان اولین آرگومان خود یک نمونه SimpleDateFormat را با یک فرمت رشته تاریخ خاص و به عنوان دومی آرگومان یک مقدار بولین[۱۱] را می گیرد. این مقدار نشان می دهد که در تفسیر یک رشته خالی باید مقادیر null تولید شود.

این مثال همچنین نشان می دهد که چگونه یک bean خاص به testBean می تواند دو خصیصه از نوع Date را از طریق مقادیر رشته ای تاریخ مقداردهی کند.

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator یک پساپردازنده bean است. این پساپردازنده با دریافت یک لیست نام bean، می تواند در هنگام نمونه سازی از تعاریف bean کارخانه که نام آن ها در این لیست آمده باشد، یک پوشش پروکسی شی ایجاد کند که دسترسی به آن bean را intercept کرده و یا رفتار آن را تغییر دهد.

DefaultAdvisorAutoProxyCreator

این پساپردازنده bean مشابه BeanNameAutoProxyCreator است، با این تفاوت که در یک رویکرد عمومی تر، تعاریف bean که باید پوشش داده شوند را همراه با اطلاعات نحوه پوشش آن ها – Advice –دریافت می کند.

[۱] Plain Old Java Object

[۲] Destruction Phase

[۳] Marker

[۴] Track

[۵] Factory Bean

[۶] Integration Test

[۷] Deploy

[۸] Configurer

[۹] Java System Properties

[۱۰] Property Editor

[۱۱] Boolean

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *