پياده‌سازی يك ESB قابل سفارشي سازی با جاوا

يكپارچه‌سازی برنامه‌های نامتجانس با استفاده از

Enterprise Service Bus

 

نويسنده:Balwinder Sodhi
مترجم: مريم پويان‌پور

 

خلاصه

يكپارچه ساختن سرويس‌ها و كامپوننت‌هاي مختلف موجود در يك سازمان، بدون توجه به پروتكل پيغام‌گذاري و يا فرمت پيغامي كه آنها استفاده مي‌كنند به يك backbone نياز دارد تا بتواند اين سرويس‌ها و كامپوننت‌ها را مرتبط سازد. يك گذرگاه سرويس سازماني (ESB)، زيرساخت- Service Messaging است كه اين backbone را فراهم مي‌سازد. اين مقاله چگونگي پياده‌سازي يك ESB مبتني‌ بر جاواي ساده، اما قابل سفارشي ساختن را كه رايج‌ترين ملزومات كاربردي يك زيرساخت توزيع پيغام ESB را برآورده سازد، توضيح مي‌دهد. يكي از مزاياي اين پياده‌سازي اين است كه كاملا بر پايه استانداردهاي Java/J2EE بوده و هيچ ويژگي J2EE خاص فروشنده را براي فراهم نمودن كارآيي ESB مورد استفاده قرار نمي‌دهد. اين پياده‌سازي مي‌تواند روي هر كانتينر J2EE سازگار انجام شود. يك سازمان را در نظر بگيريد كه در آنجا برنامه‌هاي نامتجانس داريد (احتمالا توسط گروه‌هاي مختلف توزيع شده‌اند) كه به تعامل با يكديگر نياز دارند، اما از محدوديت‌هايي برخوردارند. اين محدوديت‌ها عبارتند از:

برنامه‌ها لزوما با استفاده از تكنولوژي مشابه ساخته نشده‌اند، بنابراين ممكن است با استفاده از مكانيزم invocation محلي خود با هم ارتباط برقرار نكنند، به عنوان مثال يك برنامه J2EE و برنامه .Net

ترجيحا هر برنامه نبايد در خواستهايش را به فرمتي كه برنامه مورد نظر آن را مي‌شناسد، تغيير دهد، به علاوه سازمان برنامه‌هاي زيادي دارد كه برنامه مورد نظر را مورد استفاده قرار مي‌دهند.

كامپوننت‌هاي سرويس بايد يك مكانيزم درخواست يا invocation را استفاده كنند كه براي آنها طبيعي مي‌باشد. به عنوان مثال برنامه J2EE موجود مي‌تواند درخواست‌ها را فقط از طريق (Java Message Service) JMS بگيرد.

سازمان به سمت معماري حركت مي‌كند كه در آنجا يك برنامه خودش را با آنچه كه مي‌شناسد و آنچه بايد در هنگام به دست آوردن سرويس‌هاي ديگر در داخل سازمان به عنوان پارامتر پاس كند، مرتبط مي‌سازد.

محدوديت‌هاي ديگر ممكن است شما را به داشتن يك زيرساخت كه برنامه‌هاي نامتجانس را بدون تغيير طرح آنها قادر به يكپارچه سازي مي‌نمايد، ملزم سازند. ESB يكي از روشهاي شناخت چنين معماري يكپارچه‌سازي است.

گرچه هر سازمان احتمالا ESB خود را به روش منحصر به ‌فرد خود ايجاد مي‌كند، اما در نظر گرفتن انعطاف‌پذيري در تعريف يك ESB از اهميت خاصي برخوردار است. هيچ روش ثابتي براي ساختن يك ESB وجود ندارد. ايده واقعي، داشتن يك لايه ارتباطي است كه تعاملات بين فراهم‌كنندگان سرويس و مصرف كنندگان سرويس را بهبود بخشد و بتواند به بافت سازگار با سرويس، پيام يا رويداد پاسخ دهد.

در اينجا روشي براي ساختن يك ESB مبتني‌بر جاواي قابل گسترش كه از رايج‌ترين ملزومات كاربردي ESB پشتيباني مي‌كند، ارائه شده است.

 

ملزومات رايج ESB

ملزومات رايج ESB پر استفاده‌ترين ويژگي‌هاي آن نيز مي‌باشد.

1. ESB: Routing بايد يك مكانيزم Routing انعطاف‌پذير و كارآمد فراهم كند.

2. Transformation (تغيير): يك كامپوننت سرويس نبايد به دانستن معرفت درخواست سرويسي كه ممكن است استفاده نمايد، نياز داشته باشد. براساس درخواست كننده و هدف، ESB بايد بتواند تغيير مناسب را در مورد درخواست اعمال كند تا هدف بتواند آن را درك نمايد.

3. يك پياده‌سازي ESB كه فقط JMS يا سرويس‌هاي وب را پشتيباني كند، ارزش چنداني ندارد، بلكه بايد بتواند آنقدر قابل گسترش باشد كه چند پروتكل پيام را بسته به نيازهاي سازمان پشتيباني نمايد.

4. امنيت: در صورت لزوم، ESB بايد احراز هويت و مجوز را براي دستيابي به كامپوننت‌هاي سرويس مختلف اعمال كند.

 

شكل 1- كامپوننت‌هاي معماري اصلي يك ESB

 

شكل 1 كامپوننت‌هاي معماري اصلي يك ESB را نشان مي‌دهند كه داراي 3 بخش هستند:

1. Receiver (گيرنده): يك ESB براي اينكه به برنامه‌هاي كلاينت اجازه دهد تا پيامها را به ESB بفرستند، اينترفيس‌هاي مختلفي را در دسترس مي‌گذارد. به عنوان مثال يك Servlet مي‌تواند درخواست‌هاي HTTP براي ESB را دريافت كند. در همين زمان شما مي‌توانيد يك (message-driven bean)MDB داشته باشيد كه به يك مقصد JMS يعني جايي كه برنامه‌هاي كلاينت‌ مي‌توانند پيام بفرستند، گوش دهيد.

2. Core (هسته): Core بخش اصلي پياده‌سازي ESB است و روتينگ و تغيير را مديريت نموده و امنيت را برقرار مي‌سازد. عموما Core از يك MDB تشكيل شده كه درخواستهاي رسيده را دريافت نموده و سپس براساس بافت متن تغييرات، روتينگ و امنيت مناسب را اعمال مي‌كند. اطلاعات مشروح درباره روتينگ، انتقال، تغيير و امنيت در يك سند XML خاص قرار مي‌گيرد.

3. Dispatcher (توزيع كننده): تمام اداره كننده‌هاي انتقال outbound (به طرف بيرون) در اين بخش ESB قرار مي‌گيرند. شما مي‌توانيد هر گونه اداره كننده انتقال (مثل e-mail، فكس، FTP و غيره) را به ESB متصل كنيد.

تمام اين بخش‌هاي ESB توسط يك سند XML كه كل مسيرهايي را كه ESB بر روي آنها عمل مي‌نمايد را فهرست بندي مي‌كند، به يكديگر متصل مي‌شوند. اداره كننده‌هاي انتقالات، مبدل‌هاي مختلف، سياست‌هاي اعمال شده و اتصالات آنها به مسيرهاي مختلف از طريق اين سند XML مرتبط مي‌شوند.

 

ESBConfiguration.xml

ليست XML كه در زير آورده شده اطلاعاتي درباره عملكرد ESB به ما مي‌دهد. عناصر اصلي به شرح زير مي‌باشند:

1. Beans: اين عنصر شامل صفر عنصر Bean يا بيشتر مي‌باشد.

2. Bean: اين عنصر روش ايجاد و پيكربندي يك كلاس Bean را تعريف مي‌كند و ويژگي‌هاي زير را دارد:

name: نام منحصربه‌فردي كه براي اشاره به اين bean استفاده مي‌شود.

Class Name: نام كاملا مشروط كلاس bean

هر bean مي‌تواند صفر عنصر پراپرتي يا تعداد بيشتري از آنها را به عنوان Children داشته باشد. هر عنصر پراپرتي يك name ويژگي دارد كه آن را مشخص مي‌كند و نيز يك عنصر Child كه نوع Value دارد و مقدار پراپرتي را نگاه مي‌دارد. اين پراپرتي‌ها واقعا اعضاي مدل Java-Beans اين كلاس هستند كه مي‌توانند كلاس bean را پيكربندي كنند.

3. RetryPolices: اين عنصر حامل صفر يا تعداد بيشترRetry Policy Children مي‌باشد.

4. RetryPolicy: اين عنصر سياست retry براي يك مسير خاص را تعريف مي‌كند و داراي يك نام ويژگي است كه براي ارجاع به آن مورد استفاده قرار مي‌گيرد. اين عنصر دو عنصر Child به نام‌هاي MaxRetries وRetry Interval دارد.

5. Route: عنصر ريشه EsbConfiguration مي‌تواند داراي صفر يا تعداد بيشتر عنصرهاي Child از اين نوع باشد. اين عنصر مسيري را براي ESB نشان مي‌دهد و ويژگي‌هاي ذيل را دارا مي‌باشد:

name: يك نام منحصربه‌فرد كه براي ارجاع به آن مورد استفاده قرار مي‌گيرد.

retryPolicyRef: ارجاع به سياست retry. اين بايد با ويژگي name عنصر RetryPolicy مطابقت كند.

TransformerRef: ارجاع به يك bean كه مبدل را نشان مي‌دهد. اين بايد با ويژگي name عنصر Bean مطابقت نمايد.

عنصر Route مي‌تواند يك يا چند عنصر  Child از نوع Transport HandlerRef داشته باشد.

اين Child اساسا به يك bean ( يك اداره كننده انتقال مناسب كه بايد براي اين مسير مورد استفاده قرار گيرد را نشان مي‌دهد) و نام متد عمومي اين bean كه براي ارسال پيغام فراخواني مي‌شود، اشاره مي‌كند. عنصر Route به شكل اختياري، مي‌تواند يك Child DeadletterDestination داشته باشد كه به مسير ديگري كه نشان دهنده يك مقصد dead-Letter است، اشاره مي‌كند.

سند XML نمونه EsbConfiguration.xml، به شكل زير ظاهر مي‌شود:

 

<?xml version="1.0" encoding="UTF-8"?>
<EsbConfiguration xmlns="http://www.bss.org/esb/xml" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <Route name="creditService" retryPolicyRef="100-sec-retry" transformerRef="creditServiceTransform">
<TransportHandler beanName="creditJMSTransport"/>
 <DeadLetterDestination routeName="DeadLetter"/>
      <AuthConstraint principals="app-1"/>
   </Route>
   <Route name="taxCalculationService" retryPolicyRef="500-sec-retry" transformerRef="taxCalcServiceTransform">
      <TransportHandler beanName="taxCalcWS"/>
      <DeadLetterDestination routeName="DeadLetter"/>
      <AuthConstraint principals="app-2"/>
   </Route>
   <Route name="RedeliveryRequest" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="redeliveryRequestJMSTransport"/>
      <DeadLetterDestination routeName="DeadLetter"/>
   </Route>
   <Route name="DeadLetter" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="deadLetterJMSTransport"/>
   </Route>
   <Route name="Redelivery" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="redeliveryJMSTransport"/>
  <DeadLetterDestination routeName="DeadLetter"/>
   </Route>
   <Route name="Error" retryPolicyRef="500-sec-retry">
      <TransportHandler beanName="errorJMSTransport"/>
   </Route>
   <Beans>
      <!-- Transport handlers for the service components. -->
      <Bean name="creditJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>

         <Property name="Destination" type="java.lang.String">
            <Value>myCreditQueue</Value>
         </Property>
      </Bean>
      <Bean name="taxCalcWS" className="org.bss.esb.transport.webservice.WebServiceHandler">
         <Property name="WsdlUrl" type="java.lang.String">
       <Value>http://www.tax.com/calc</Value>
         </Property>
      </Bean>
<!-- Transformer beans for the service components -->
      <Bean name="creditServiceTransform" className="org.bss.esb.transform.XSLTransform">
         <Property name="XslUrl" type="java.lang.String">
            <Value>file:///C:/temp/esb/transform/xsl/credit.xsl</Value>
         </Property>
      </Bean>
      <Bean name="taxCalcServiceTransform" className="org.bss.esb.transform.CustomTransform">
         <Property name="ConfigFileUrl" type="java.lang.String">
            <Value>file:///C:/temp/esb/transform/custom/configManager.properties</Value>
         </Property>
      </Bean>
<!-- Transport handlers for the system queues -->
      <Bean name="redeliveryJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>Redelivery.Queue</Value>
         </Property>
      </Bean>
      <Bean name="deadLetterJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>System.DL.Queue</Value>
         </Property>
      </Bean>
      <Bean name="errorJMSTransport" className="org.bss.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>System.Error.Queue</Value>
         </Property>
      </Bean>
      <Bean name="redeliveryRequestJMSTransport" className="org.bss.esb.transport.jms.EsbRedeliveryHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>qcf-1</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
         <Value>Redelivery.Request.Topic</Value>
         </Property>
      </Bean>
   </Beans>
   <!-- Defines the retry policies that can be used by various route definitions. -->
   <RetryPolicies>
      <RetryPolicy name="100-sec-retry">
         <MaxRetries>10</MaxRetries>
         <RetryInterval>100</RetryInterval>
      </RetryPolicy>
      <RetryPolicy name="500-sec-retry">
         <MaxRetries>10</MaxRetries>
         <RetryInterval>500</RetryInterval>
      </RetryPolicy>
   </RetryPolicies>
</EsbConfiguration>

 

رفتار ESB

سند ESBConfiguration.xml رفتار ESB ما را تعيين مي‌كند. EsbRouterMDB اين XML را از محلي كه در توصيف‌گر deployment آن مشخص شده، بارگذاري مي‌كند، سپس اطلاعات آن در يك ساختار داده كه در شكل 2 نشان داده شده، سازماندهي مي‌شود.

 

شكل 2- اطلاعات ESB در يك ساختار داده

 

EsbRouter از اين اطلاعات (از طريق EsbConfigManager) براي خواندن مسير درست، تغييرات اعمال شده، مجوز امنيت و غيره استفاده مي‌كند. نكته مهمي كه بايد به‌ خاطر سپرد، روش مورد استفاده قرار گرفتن تكنيك Dependency Injection به همراه وراثت براي پايان دادن به ارتباط عملكردهاي مختلف ESB مي‌باشد. اين روش امكان قابل گسترش بودن و قابل سفارشي بودن ESB را فراهم مي‌سازد.

همانطور كه الگوي كلاس نشان مي‌دهد، 2 اينترفيس مهم در طراحي ESB وجود دارد: TransformHandler و TransportHandler. اين دو اينترفيس امكان نوشتن يك پياده‌سازي انتقال و تغيير خاص براي پيامهاي راهبري شده را در اختيار شما قرار مي‌دهند. اين كلاس‌هاي پياده‌سازي مي‌توانند از طريق عناصر Bean در EsbConfigureration با مسيرها مرتبط شوند (Wire). به عنوان مثال در سند نمونه ESBConfiguration.xml، تعريف Bean زير اداره كننده انتقال را مشخص مي‌كند.

 

<Bean name="creditJMSTransport" className="com.foo.esb.transport.jms.JmsHandler">
         <Property name="ConnectionFactory" type="java.lang.String">
            <Value>myQCF</Value>
         </Property>
         <Property name="Destination" type="java.lang.String">
            <Value>myCreditQueue</Value>
         </Property>
      </Bean>

 

سپس مي‌توان با درج يك TransportHandler Child مثل

<TransportHandler BeanName = “Credit JMST transport”/>

به اين اداره كننده انتقال در يك گره Route رجوع كرد.

نكته: روشي كه در اين مقاله شرح داده شده است از اينترفيس‌هاي جاوا براي تعريف اداره كننده‌هاي انتقال و تغيير استفاده مي‌كند، بنابراين هر اداره كننده جديد بايد اينترفيس مورد نياز را پياده‌سازي نمايد كه بسيار آزار دهنده به نظر مي‌رسد. شما مي‌توانيد EsbConfigManager را براي استفاده از Dependency Injection به منظور فراخواني هر متد اختياري كلاس پياده‌سازي اصلاح كنيد و نياز به پياده‌سازي يك اينترفيس را برطرف سازيد، اما چونEsbRouter هميشه يك مورد javax.jms.Message را عبور مي‌دهد (Pass)، كلاس پياده‌سازي اداره كننده شما بايد به هر حال از نوع javax.jms.Message استفاده كند.

اكنون بياييد اين فرآيند را به همان شكلي كه در يك ESB اتفاق مي‌افتد، براساس دريافت يك پيام كه عازم يك مسير خاص (سرويس يا برنامه) است، مرحله به مرحله مرور كنيم. براي سهولت بيشتر فرض مي‌كنيم دو برنامه از طريق JMS تعامل دارند.

1. برطبق توصيف‌گر deployment اولين تعداد نمونه‌هاي MDBهاي EsbRouter ايجاد مي‌شوند.

2. متد ejbcrate() از EsbRouter فراخواني شده و وظايف ذيل را كامل مي‌كند:

- فايل EnvConfig.xml را از آدرس (URL) تعيين شده در ورودي محيط به نام java:Comp/env/EnvconfigURL بارگذاري مي‌كند.

- از EnvConfig.xml، آدرس محل EsbConfiguration.xml را مي‌خواند و آن را به يك Xml Bean تجزيه مي‌كند.

- نمونه EsbConfigManager را ايجاد و آغاز مي‌نمايد كه اين مرحله ايجاد و آغاز تمام beanهاي پيكربندي شده در EsbConfiguration.xml و سپس پركردن (Populate) ساختارهاي ديتا مشخص شده در شكل 2 را در برمي‌گيرد.

- نمونه EsbRouterMonitorMBean را آغاز مي‌كند (جزئيات اين مرحله شرح داده خواهد شد. فقط بدانيد كه به ساخت ESB مجهز به        (Java Management Extension) JMX كمك مي‌كند.

- وقتي كامپوننت سرويس به ارسال يك پيام (مثلا M) به يك كامپوننت ديگر از طريق ESB نياز پيدا مي‌كند، اين متد پيام JMS را در صف ورودي ESB قرار مي‌دهد و منجر به فراخواني متد OnMessage() از ESBRouter مي‌شود. مراحل ذيل از طريق OnMessage() انجام مي‌شوند:

1. مسيري با نام R كه در يك پراپرتي پيام رسيده M مشخص شده، خوانده مي‌شود. نام اين پراپرتي خاص همراه با نامهاي ديگري كه بعدا معرفي خواهند شد از طريق EnvConfig.xml قابل پيكربندي مي‌باشند.

2. Route Info مطابق با مسير R از نمونه ESBConfigManager جستجو مي‌شود.

3. درخواست كننده كنترل مي‌شود تا مجاز يا غيرمجاز بودن آن براي ارسال پيام به مسير انتخاب شده مشخص شود. اگر درخواست كننده مجاز نباشد پيام كنار گذاشته مي‌شود. شما مي‌توانيد اين رفتار را به دلخواه تنظيم نماييد. به عنوان مثال مي‌توانيد به عنوان مدير شبكه پيام را به يك صف يا صفحه خاص وارد كنيد.

4. اگر يك TransformHandler bean قرينه پيكربندي شود از طريق الگوي EsbConfigManager جستجو مي‌شود و متد TransformMessage() آن با عبور دادن پيام M به عنوان يك آرگومان فراخواني مي‌گردد.

5. در هر TransportHandler به دست آمده براي مسير R، EsbRouter متد TransportMessage() را فرا مي‌خواند و پيام M ورودي به عنوان يك پارامتر عبور داده مي‌شود.

6. اگر هيچ ورودي در EsbConfigManager براي مسير R پيدا نشود، TransportHandler براي مسير dead-letter سيستم جستجو مي‌شود، سپس پيام M به مسير dead-letter فرستاده مي‌گردد.

7. اگر اداره كننده مسير خاص پيدا شود، اما TransportHandler نتواند پيام را ارسال كند و يا اينكه خطاي سيستم وجود داشته باشد، پس پيام براي ارسال مجدد در صف قرار مي‌گيرد.

قرار گرفتن در صف براي ارسال مجدد شامل مراحل زير است:

1. سياست retry از EsbConfigManager براي مسير خاص جستجو مي‌شود.

2. زمان بعدي ارسال براساس وقفه retry در سياست retry محاسبه مي‌گردد.

3. اين زمان به عنوان پراپرتي MessageRedeliveryTime تنظيم شده و به صف ارسال مجدد فرستاده مي‌شود.

4. يك پيام درخواست ارسال مجدد جداگانه به موضوع درخواست ارسال مجدد فرستاده مي‌گردد. درخواست ارسال مجدد يك javax.jms.ObjectMessage است كه يك آبجكت Long دارد. آبجكت فوق زمان ارسال مجدد محاسبه شده بعدي را به عنوان Payload نشان مي‌دهد.

 

ارسال مجدد پيام: زمانبندي و پردازش

RedeliveryRequestorProcessorMDB براساس دريافت يك پيام، درخواست ارسال مجدد يك نمونه RedeliveryTask را ايجاد مي‌كند. RedeliveryTask با زمان ارسال مجدد (كه در پيام درخواست ارسال مجدد با عنوان Long مشخص شده) آغاز و از طريق RedeliveryScheduler زمانبندي مي‌شود.

RedeliveryScheduler يك كلاس تك است كه كار زمانبندي را به java.Util.Timer مربوط به خود تفويض مي‌كند. متد run() از RedeliveryTask در زمان تعيين شده فراخواني مي‌شود و سعي مي‌كند در اين زمان يك پيام از صف ارسال مجدد دريافت نمايد. گيرنده‌اي كه اين متد را مورد استفاده قرار مي‌دهد يك انتخابگر پيام دارد كه فقط پيامي را كه پراپرتي MessageRedeliveryTime آن با زمان تعيين شده مطابقت داشته باشد، انتخاب مي‌كند، سپس اين پيام به صف ورودي ESB فرستاده مي‌شود.

يك پيام مجددا ارسال شده M كه براي مسير خاص R در نظر گرفته شده بدين شكل پردازش مي‌شود:

1. سياست retry از EsbConfigManager<