|
مراقب خطرات استثنائات عمومي باشيد
نويسنده:
Paul Philion چكيده جاوا چهارچوب غني براي بكارگيري استثنائات را فراهم ميآورد، اما بسياري از برنامهنويسان ترجيح ميدهند تا از آن غفلت ورزند و از Exceptionهاي عمومي استفاده كنند. اين مقاله خطرات throwing، catching و غفلت از Exceptionهاي عمومي را توضيح ميدهد و عملكردهاي مناسب جهت استفاده از exception پيچيده را در پروژههاي نرمافزاري بزرگ و پيچيده نشان ميدهد. هنگام كار بر روي پروژه اخير، قطعه كدي را يافتم كه cleanup (مرتبسازي) منبع را انجام ميداد. از آنجائيكه اين كد فراخوانيهای متنوعي داشت، ميتوانست به طور بالقوه شش استثنا مختلف را throw كند. برنامهنويس اصلي براي سادهسازي كد، متدي را براي throw و exception تهيه نمود تا شش exception مختلف بتوانند throw شوند. اين امر سبب ميشود كه فراخواني در بلوك try/catch و wrap و exception، catch شود. برنامهنويس فكر ميكند از آنجاييكه كد به منظور مرتبسازي است، اين مشكل چندان مهم نيست، لذا بلوك catch خالي ميماند و سيستم متوقف ميشود. به طور حتم، اينها عملكردهاي مناسب برنامهنويسي نيستند اما به نظر ميرسد اشتباهي رخ نداده است، به جز يك مشكل كوچك منطقي در خط سوم كد اصلي.
ليست 1- كد clean up اصلي
private
void cleanupConnections() throws ExceptionOne, ExceptionTwo {
آرايه connection در بخش ديگر كد هنگامي آغاز ميشود كه اولين connection ايجاد گردد. اما اگر connection هيچگاه ايجاد نشود، آرايه connection به صورت null خواهد بود. لذا در بعضي از موارد، فراخواني connection[I].release() در NullPointer Exception نتيجه ميدهد. رفع اين مشكل نسبتا ساده است. به سادگي فرمان ميتوان connection != null را قرار داد. با اين وجود، exception هيچگاه گزارش داده نميشود. Exception با cleanupConnection()، cleanupEverything()، throw و در نهايت با done()، catch ميشود. متد done() كاري با exception انجام نميدهد. Exception هيچگاه ديده نميشود. لذا كد هرگز اصلاح نخواهد شد. بنابراين، در اين سناريوي خطا، هرگز متدهاي removeListeners()، cleanupfiles() فراخوانده نميشوند (لذا منابعشان نيز هرگز آزاد نخواهد شد)، متد doMorestuff() نيز فراخوانده نميشود، لذا آخرين پردازش در done() هرگز كامل نميشود. بدتر اينكه با خاموش شدن سيستم، done() فراخوانده نميشود. در عوض براي كامل نمودن هر گونه ارتباط صدا زده ميشود. اين مشكل بسيار حائز اهميت است، زيرا خطاها گزارش داده نميشوند. اما به نظر ميرسد با توجه به نحوه كدنويسي، رديابي اين مشكل دشوار باشد. با اين وجود، با بكارگيري چند راهنمايي ميتوان مشكل را يافت و آن را رفع كرد: · از exception غفلت نكنيد. · Exceptionهاي عمومي را Catch نكنيد. · Exceptionهاي عمومي را throw نكنيد.
از Exception غفلت نكنيد مهمترين مشكل كد ليست 1 اين است كه خطاي موجود در برنامه، به طور كامل به فراموشي سپرده شده است. يك exception غير منتظره (exceptionها به طور طبيعي غير منتظره هستند)، دور انداخته شده و كد آمادگي پذيرش exception را ندارد. اين exception حتي گزارش داده نشده، زيرا فرض كد بر اين است كه exceptionها، پيامدهايي در پي نخواهند داشت. در اكثر موارد، حداقل بايد exception، log شود. بستههاي log متعددي ميتوانند بدون اينكه بر كارآيي سيستم تاثير بگذارند، از خطاها و exceptionهاي سيستم، log تهيه كنند. اكثر سيستمهاي تهيه كننده log امكان چاپ رديابيها را فراهم ميآورند، لذا اطلاعات ارزشمندي درباره مكان و علت روي دادن exception در اختيارمان قرار ميدهند. سرانجام، از آنجائيكه logها در فايلها نوشته ميشوند و ميتوان سوابق exceptionها را مورد بازنگري و تجزيه و تحليل قرار داد. مثالي از رديابيهاي پشته logging در ليست 11 آمده است.
Exceptionها در ليست 1، بعضي از دادهها از فايل خوانده ميشوند و بايد فايل را صرفنظر از اينكه exception، دادهها را ميخواند، بست. لذا متد close() در عبارت آخر بكار ميرود. اما اگر خطا سبب بسته شدن فايل شود، نميتوان كاري كرد:
public
void loadFile(String fileName) throws IOException {
توجه نماييد در صورتي كه دادههاي واقعي بارگذاري به دليل مشكل I/O (ورودي/خروجي) با شكست مواجه شوند. باز هم loadFile()، IOException را به متد فراخوانده شده گزارش ميدهد. همچنين اگر از exception متد close() غفلت شود، كد اين مطلب را به فردي كه برنامهنويسي مربوطه را انجام ميدهد، گوشزد ميكند. شما ميتوانيد همين رويه را براي cleanup همه جريانات I/O بكار ببريد و Connectionهاي، JDB و سوكتها را مسدود نماييد. نكته مهمي كه بايد توجه نماييد اين است كه از قرار گرفتن يك متد منفرد در بلوك try/catch، Catch يك exception خاص، اطمينان حاصل كنيد. اين شرط ويژه با Catch يك Exception عمومي تفاوت دارد. در ساير موارد، excepton بايد حداقل log شود.
Exceptionهاي عمومي را Catch نكنيد اغلب بلوكي از كد در يك نرمافزار پيچيده، متدهايي را اجرا ميكند كه exceptionهاي متعددي را throw ميكنند. بارگذاري يك كلاس و استفاده از آبجكت آني ميتواند، exceptionهاي متعدد و مختلفي را نظير Class Not Found Exception، Instantiation Exception، Illegal Access Exception، Class Cast Exception بوجود آورد. برنامهنويس ممكن است به جاي افزودن چهار بلوك مختلف Catch به بلوك try، فراخوانيهاي متد را در بلوك try/catch قرار دهد. اين بلوك Exceptionهاي عمومي را Catch ميكند (رجوع به ليست 3). در حاليكه اين اقدام بيضرر به نظر ميرسد، پيامدهاي غيرمنتظرهاي را در پي خواهد داشت. براي نمونه، اگر className()، null باشد، Class.forName()، NullPointerException را throw ميكند و اين امر سبب ميشود توسط متد، catch شود. در اين حالت، بلوك catch، exceptionهايي را Catch ميكند كه هرگز نبايد Catch ميكرد، زيرا NullPointerException، زير كلاسي از RuntimeException است كه خود نيز زير كلاسي از Exception ميباشد. لذا Catchهاي (Exception e)، همه زيركلاسهاي RuntimeException شامل NullPointerException، IndexOutOfBoundsException، ArrayStoreException را Catch ميكند. NullClassName در ليست 3 در NullPointerException نتيجه ميدهد كه بيانگر متدي است كه نام كلاس آن غير مجاز است.
ليست 3:
public
SomeInterface buildInstance(String className) { پيامد ديگر عبارت Catch عمومي اين است كه Logging محدود است، زيرا Catch از Catching يك Exception خاص اطلاعي ندارد. بعضي از برنامهنويسان هنگامي كه با اين مشكل مواجه ميشوند، براي مشاهده نوع exception، عبارتي را براي بررسي (رجوع به ليست 4) به برنامه ميافزايند.
ليست 4:
catch
(Exception e) { ليست 5 مثال كاملي از exception catchingهاي ويژه است. اپراتور instanceof لازم نيست، زيرا exceptionهاي خاص Catch ميشوند. هر يك از exceptionهاي بررسي شده (AccessException، IllegalInstantiationException، ClassNotFoundException)، Catch مي شوند. حالت ويژهاي كه ClassCostException را توليد ميكند (كلاس به طور صحيح بارگذاري ميشود، اما واسط someInterface را پيادهسازي نميكند) نيز با بررسي آن exception، شناسايي ميشود.
ليست 5:
public
SomeInterface buildInstance(String className) {
در بعضي از موارد، بهتر است كه exception شناخته شده را به جاي استفاده در متد، rethrow كرد (يا exception جديدي را ساخت). بدين ترتيب متد فراخوانده شده با قرار دادن exception در يك زمينه شناخته شده، با شرايط خطا را كنترل ميكند. ليست 6 نسخه انتخابي متد buildInterface() را نشان ميدهد اين نسخه ClassNotFoundException را در صورت بروز مشكل، throw و كلاس را بارگذاري ميكند. متد فراخواني شده در اين مثال آبجكت آني يا exception را دريافت ميكند، لذا نيازي به بررسي اينكه آيا آبجكت بازگشتي null است يا خير، نيست. توجه نماييد كه اين مثال از متد جاوا 1.4 براي ساخت exception جديد كه پيرامون exception ديگري اطلاعات رديابي پشته اصلي را نگهداري ميكند، استفاده مينمايد. در غير اينصورت رديابي پشته متد buildInstance() را به عنوان متدي تلقي ميكند كه exception نشات گرفته نه يك exception كه توسط متد newInstance()، throw شده است.
ليست 6:
public
SomeInterface buildInstance(String className) در بعضي از موارد، ممكن است كه بتواند از شرايط خطاي خاصي اصلاح شود. در اينگونه موارد، Catching يك exception خاص مهم است، لذا كد ميتواند دريابد كه آيا شرايط اصلاحپذير است يا خير. با توجه به اين نكته به مثال instantiation در ليست 6 نگاه كنيد. كد ليست 7، آبجكت پيش فرضي را براي ClassName غير مجاز باز ميگرداند، اما به دليل وجود عمليات غير قانوني نظير نقض امنيت، exception را throw ميكند.
تذكر IllegalClassException يك كلاس exception دامين است كه براي تشريح اهداف خاصي بكار ميرود.
ليست 7:
public
SomeInterface buildInstance(String className) هنگام catch يك exception عمومي صورت ميگيرد Catch اينگونه exceptionها در موارد خاصي صورت مي گيرد.
اين موارد بسيار ويژه هستند، اما براي سيستمهاي تلرانس خطا حائز اهميتند. در
ليست 8 درخواستها از صفي از درخواست به نوبت خوانده و پردازش ميشوند. اما اگر
exception
در هنگام پردازش درخواست رخ دهد
ليست 8:
public
void processAllRequests() {
راه بهتر اين است كه دو تغيير عمده در منطق همانند آنچه در ليست 9 آمده است، صورت گيرد: ابتدا بلوك try/catch درون حلقه پردازش درخواست را منتقل كنيد (move). بدين ترتيب خطاها درون حلقه پردازش گرفته ميشوند و نميتوانند break حلقه را موجب شوند. لذا حلقه به پردازش درخواستها هنگامي كه يك درخواست با شكست مواجه ميشود، ادامه ميدهد. دوم، بلوك try/catch را به گونهاي تغيير دهيد كه Exception عمومي Catch شود، لذا Catch يك exception درون حلقه صورت ميگيرد و پردازش درخواستها ادامه مييابد.
ليست 9 :
public
void processAllRequests() { به نظر ميرسد كه Catching يك exception عمومي صحيح نباشد، البته همينطور است. اما مثال ما تحت شرايط خاص است. در اين حالت، catch يك exception صورت ميگيرد تا از متوقف شدن كل سيستم توسط exception ممانعت بعمل آيد. در شرايطي كه درخواستها، ارتباطات يا پيشامدها در يك حلقه پردازش ميشوند، پردازش حلقه بايد ادامه يابد حتي اگر exceptionها در حين پردازش throw شوند. بلوك try/catch در حلقه پردازش ليست 9 را ميتوان به عنوان top-level exception handler مورد بررسي قرار داد، top-level exception handler بايد exceptionهايي كه در اين سطح از كد وجود دارند، catch نمايد. بدين ترتيب از exceptionها غفلت نميشود، به علاوه اينكه exceptionها سبب ايجاد وقفه در پردازش ساير درخواستها نيز نخواهند شد. هر سيستم پيچيده بزرگ يك exception handler سطح بالا (احتمالا يكي در هر زير سيستم، بسته به نحوه پردازش دارد. Exception handler، مشكلي كه سبب بروز exception ميشود را رفع نميكند، اما ميتواند عمل لاگ و catch سيستم را بدون اينكه پردازش متوقف شود، انجام دهد. Throw همه exceptionها در اين سطح ضروري نيست. Exception در جايي بايد مديريت شود كه منطق برنامه از شرايطي كه مشكلي بروز ميكند، اطلاع بيشتري داشته باشد. اما اگر exception نتواند با سطح پايينتر كار كند، throw بايد انجام شود. بدين ترتيب همه خطاهاي غير قابل اصلاح و يكجا (در exception handler سطح بالا) مديريت ميشوند، در حاليكه بايد اين امر در كل سيستم انجام گيرد.
Exceptionهاي عمومي را throw نكنيد مشكل مثال 1 هنگامي آغاز ميشود كه برنامهنويس تصميم ميگيرد تا Exception عمومي را از متد CleanupEverything()، throw كند. در اين حالت متد، شش exception مختلف را throw ميكند. در چنين وضعيتي اعلان متد غير قابل خواندن خواهد بود و فراخواني متد سعي دارد همانند آنچه در ليست 10 آمده است، شش exception را catch كند.
ليست 10:
public
void cleanupEverything() throws
بكارگيري يك exception خاص از بروز مشكلات جدي جلوگيري ميكند. Throw يك exception عمومي جزئيات مشكلات زيرساختاري را مخفي ميسازد، لذا نميتوان با مشكل واقعي مواجه شد. بعلاوه، throw يك exception عمومي سبب ميشد كه متد Exception را Catch يا با throw آن، مشكل را انتشار دهد. هنگاميكه متدي يك Exception عمومي را throw ميكند، اينكار به دو دليل صورت ميگيرد: در يك حالت متد چند متد ديگر را كه ممكن است exceptionهاي مختلفي را throw كنند، فرا ميخواند (مانند الگوهاي طراحي Mediator يا Facade) و جرئيات شرايط exception را مخفي ميسازد. متد به جاي ايجاد و throw نمودن exceptionهاي سطح دامين، فقط throw شدن Exception را اعلان ميكند. وضعيت ديگر هنگامي است كه متد بلافاصله Exception عمومي (throw newException()) را throw ميكند زيرا برنامهنويس درباره اينكه كدام exception بيانگر وضعيت است، تدبيري نينديشيده است. مسائل فوق را با كمي تفكر و طراحي ميتوان برطرف كرد. چگونه بايد exception سطح دامين را throw نمود؟ در طراحي بايد متدي اعلان شود كه exceptionها را throw كند. گزينه دوم اين است كه exception سطح دامين ايجاد كنيد تا exceptionهاي throw شده را دربرگيرد. در اكثر موارد، exception (يا مجموعهاي از exceptionهايي) كه متد، throw ميكند، بايد به تفصيل عنوان شود. Exceptionهاي جزئي اطلاعات بيشتري را درباره شرايط خطا در اختيارمان قرار ميدهند و ميتوان وضعيت را تحت كنترل درآورديا حداقل به تفضيل log نمود.
در هنگام بكارگيري Exceptionهاي عمومي دقت كنيد اين مقاله به جوانب مختلفي از Exceptionهاي عمومي اشاره نمود. هرگز نبايد آنها را throw يا ignore كرد و بايد تحت شرايط خاصي catch شوند و اطلاعات زيادي براي مديريت صحيح و اثربخششان در اختيارتان قرار نميدهند. Exceptionها بخش قدرتمند جاوا هستند كه اگر به طور صحيح بكار روند به برنامهنويس اثربخش تبديل ميشويد و چرخه توسعه به ويژه هنگام آزمايش و اشكالزدايي كوتاه ميشود. اگر به طور موثر از exceptionها استفاده شود، بر ضد شما كار ميكنند و مشكلات سيستم را مخفي ميسازند.
Logging exception حتي بهترين كد نيز به سادگي نميتواند شرايط exception (استثنايي) را اصلاح كند: خطا ممكن است منتج از نوشتن در فايل يا در دسترس نبودن بانك اطلاعاتي و عدم وجود حافظه باشد. در بعضي از موارد، كد ميتواند دوباره سعي كند يا از دادههاي پيش فرض استفاده كند، شما ميتوانيد كاري را انجام ندهيد، اما از exception، log (لاگ) تهيه كنيد و سعي نماييد مرتبسازي را انجام دهيد. Log از exceptionها براي هر گونه سيستمي حائز اهميت است. بدين ترتيب ميتوانيد اشكالات برنامهنويسي را رديابي نموده و خطاهاي سيستم را شناسايي كنيد و كاربر ميتواند دريابد كه چرا فايلي ذخيره نميشود يا چرا سيستم بقيه درخواستها را پردازش نميكند. هر سيستمي ممكن است به طرق مختلفي دچار مشكل شود و log گرفتن در درك و يافتن exceptionها مفيد است. بعضي از سيستمها براي تهيه log از خطاها و exceptionها از system.err استفاده ميكنند. از آنجائيكه system.err سربار زيادي دارد و انعطافپذير نيست، راهكار مناسبي براي سيستمهاي بزرگ نخواهد بود. پلاتفرم (J2SE)Java2، بسته java.util.logging را معرفي ميكند. اين بسته يك API خوب تهيه log دارد. اين APIها مختص J2SE 1.4 به بعد هستند. Log4J بهترين لاگگيري در جاواست و مجموعهاي غني از قابليتها و پيكربنديها براي اشكالزدايي دقيق اطلاعات و رديابي پشتهها (stack) دارد و ميتواند در سطوح مختلف لاگگيري پيكربندي شود. Log4J با Java 1.1 سازگار است. پروژه the Jakarta Commons بسته لاگگيري دارد كه واسط استاندارد و قابل پيكربندي براي لاگگيري جاوا و ساير سيستمهاي لاگگيري فراهم ميكند. اين انعطافپذيري سبب ميشود تا به سهولت از يك API استفاده نمود و ساير بستههاي تهيه log را در صورت نياز بكار گرفت. همه اين سيستمهاي لاگ، از پيامها و exceptionها log تهيه كرده (به همراه رديابي پشته) و سطح ذخيره log را مشخص ميكنند. در مثال 11، بعضي از مقادير از فايل خوانده ميشود. اگر IoException، throw شود، مقادير پيشفرض استفاده خواهند شد و پيام لاگگيري نيز چاپ ميشود. خطوط 5 و 16 تا 18 بخشهاي مهمي دارند: logger LOG در خط 5 به صورت static، final اعلان ميشود. LOG، static است لذا مرتبط با يك كلاس است و find است، لذا قابل تغيير نيست. ساخت logger مذكور، سربارهايي نيز به همراه دارد، اما اين سربار يكبار در سيستم روي ميدهد. سطح لاگ در خط 15 پيش از لاگ پيام بررسي ميشود، لذا هر گونه اتصال رشته يا ساير پردازشها (نظير بررسي مقادير) تا پيش از چاپ لاگ انجام نميشود. VM بدون بررسي سطح لاگ، اتصال رشته را پيش از اينكه دريابد كه رشته چاپ نخواهد شد، پردازش خواهد كرد. در مثال 11 اين بررسي چندان تاثيرگذار نيست، اما چنين بررسيهايي در سيستمهاي بزرگ با لاگهاي اشكالزدايي، براي كارآيي سيستم بسيار حياتي است. پارامتر دوم در خط 16 (LOG.warn)، يك exception را نشان ميدهد. عبور exception اصلي به سيستم لاگگيري به اين سيستم امكان ميدهد تا رديابي كامل پشته را براي exception، چاپ كند. توجه نماييد كه چاپ يا عدم چاپ رديابي پشته را ميتوان در پيكربندي چارچوب لاگگيري، فعال يا غير فعال نمود.
كد 11:
01.
import org.apache.commons.logging.Log;
من از بسته Commons تحت پشتيباني log4j در پروژههايم استفاده و براي پروژهها بستههايي مشابه آن را توصيه ميكنم.
Copyright
2004 IDG News Service.All right reserved. |