خوب دوستان توی این آموزش قصد داریم کل مبحث Exception رو ببندیم.
یکم طولانیه ولی باید گفتنیا رو گفت
اول میریم بگیم کلا Exception چیه و چرا اتفاق میفته و چطوری میشه مدیریتش کرد بعد میریم سمت اسپرینگ و مدیریت استثنا توی اسپرینگ .
منابع :
اول از همه بگم که اطلاعاتی که توی این آموزش هست از چنتا منبع خوب گرفتم :
1- مدیریت استثنا و انواع استثنا (فارسی)
2- مدیریت استثنا در Spring Rest (فارسی)
3- مدیریت استثنا و مفاهیم پایه (انگلیش) احتمال داره باز نشه پس VPN بزنید
4- مدیریت استثنا با استفاده از ControllerAdvice (انگلیش)
5- مدیریت استثنا با استفاده از ControllerAdvice (انگلیش)
6- قواعد Exception و دستورالعمل ها (فارسی)
استثنا یه رویداد ناخواسته یا غیر منتظره است که در حین اجرای یک برنامه ، یعنی در زمان اجرا رخ میدهد. و جریان عادی دستورالعمل های برنامه را مختل میکند.
هنگامی که یک استثنا در یک متد رخ میدهد یک شیء ایجاد میکند. این شیء را شیء استثنا مینامند.
این شیء شامل اطلاعاتی در مورد استثنا است مانند نام و شرح استثنا و وضعیت برنامه در هنگام وقوع استثنا.
اشتباه نکنید : یه کد یا درواقع یه عبارت ممکنه کامپایل و اجرا بشه اما در شرایط خاصی ممکنه مشکلاتی ایجاد شود و در این حالت میگیم استثنا اتفاق افتاده .
مثلا فرض کنید قرار است که دوتا عدد از ورودی بگیریم و حاصل تقسیم این دو عدد را به کاربر برگردانیم خوب اگر تقسیم بر صفر اتفاق بیفته اینجا استثنا ایجاد میشه پس باید برنامه نویس شروطی بزاره که این اتفاق نیفته در این مثال شما میتونید اجازه ندید استثنا اتفاق بیفته اما مواقعی هست که نمیشه کاریش کرد و باید یه استثنا برگردونید.
خطاها بیانگر شرایط غیرقابل جبرانی مانند اتمام حافظه ماشین مجازی جاوا (JVM)، نشت حافظه، خطاهای سرریز پشته، ناسازگاری کتابخانه، بازگشت بی نهایت و غیره هستند. خطاها معمولاً خارج از کنترل برنامه نویس هستند و ما نباید سعی کنیم خطاها را مدیریت کنیم. .
خطا: یک خطا نشان دهنده یک مشکل جدی است و نمیتونیم با try-catch اونو بگیریم و هندل کنیم .
استثنا: Exception شرایطی را نشان می دهد که در یک برنامه معقول ممکن است اتفاق بیفتد و میتوان با try-catch و یا روش های دیگه اون را هندل کرد.
چرا باید استثناء را مدیریت کنیم؟
به کمک مدیریت استثناء میتوانیم از قطع جریان برنامه توسط استثنای رخ داده جلوگیری کرده و به اجرا ادامه دهیم . در مواردی که امکان اجرای برنامه وجود ندارد نیز با پیغام مناسب کاربر را اگاه کنیم.
سلسله مراتب استثناء در جاوا :
تمام کلاس های استثناء در جاوا از کلاس Exception ارث بری دارند . کلاس های Error و زیر کلاس های ان مانند ThreadDeath, OutOfMemoryException,VirtualMachineError و... نشاندهنده ی حالات غیر عادی هستند که میتوانند در سطح ماشین مجازی جاوا (JVM) رخ دهند . این خطاها به ندرت اتفاق می افتند و توسط مکانیزم مدیریت استثناء نمیتوان آنها را مدیریت کرد .
استثناء های چک نشده در برابر چک شده:
تمایز مابین این دو از اهمیت ویژه ای برخوردار است چرا که رفتار کامپایلر جاوا با این دو متفاوت است. تمام کلاس هایی که از RuntimeExceptionارث بری میکنند چک نشده و تمام کلاس هایی که از Exception ارث بری میکنند چک شده(Checked) هستند
لازم به ذکر است زیر کلاس های Error همگی چک نشده هستند(Unchecked)
در استثناء های چک شده کامپایلر باید اطمینان یابد که استثناء چک شده در بلوک catch گرفتار میشود .
با استفاده از کلمه ی کلیدی throws در هنگام تعریف متد, استثناء های چک شده ای که ممکن است متد سبب انها شود اعلام میشوند.
در هنگام تعریف متد, استثناء های چک شده ای که ممکن است متد سبب انها شود اعلام میشوند
public class ExceptionExample{ public void method1() throws Exception{ } public void method2(){ try{ method1(); } catch(Exception e){ // handle } } }
رویکرد جاوا در استثناء های چک شده برنامه نویس را مجبور میکند تا در مورد مشکلی که ممکن است رخ دهد فکری کند
اما در استثناء های چک نشده کامپایلر چنین فرایندی را طی نمیکند چرا که عموما میتوان با کدنویسی مناسب و صحیح مانع رخ دادن استثناء های چک نشده شد
برای مثال هنگام تقسیم بر صفر میتوانیم قبل از تقسیم اطمینان یابیم که مقسوم علیه صفر نیست . و در این صورت استثنای ArithmeticException به راه نمی افتد .
همچنین در استثناء های چک نشده احتیاجی به استفاده از کلمه کلیدی throws در هنگام تعریف متد نیست:
public class ExceptionExample{ public double divide(int i1,int i2){ if(i2!=0) return i1/i2; } }
اما در حالت عادی اگر صفر میدادیم و متد را به صورت بالا ننوشته بودیم گرفتار استثنا میشدیم:
public class ExceptionExample{ public double divide(int i1,int i2){ return i1/i2; } }
try { // block of code to monitor for errors // the code you think can raise an exception } catch (ExceptionType1 exOb) { // exception handler for ExceptionType1 } catch (ExceptionType2 exOb) { // exception handler for ExceptionType2 } // optional finally { // block of code to be executed after try block ends }
فرض کنید میخواهید با منابع کار کنید مثلا یک فایل را بخوانید ، در این حالت وقتی کارتون با فایل سیستم تمام شد باید اون رو ببندید .
شاید بپرسید چرا در همان بلوک catch منابع را ازاد نکنیم؟ بخاطر اینکه اجرای catch وابسته به نوع استثنای رخ داده است و اگر فرضا ماIOException را هندل کردیم و NullpointerException رخ داده است دیگر catch ای که حاوی IOException است اجرا نمیشود و از این رو منبع ازاد نمیگردد:
public void method{ Scanner in = null; try{ in = new Scanner(System.in); }catch(IOException e){ System.out.println(e.getMessage()); } finally{ in.close(); } }
در نسخه ی java 7 به بعد میتوان از try-with-resources استفاده کرد . در این صورت دیگر نیازی به استفاده از finally نیست . کد بالا را به صورت زیر بازنویسی میکنیم:
try(Scanner in = new Scaner(System.in)){ // use the Scanner here } catch(IOException e){ // catch Exception that occur while using the resource System.out.println(e.getMessage()); }
گاهی برنامهنویسان به اشتباه، منابع را در انتهای بلوک try میبندند. این کد در زمانی که استثنایی اتفاق نیفتد، به خوبی کار میکند اما زمانی که خطایی پیش بیاید و بلوک try به آخر نرسد، کار آزادسازی هم انجام نمیشود.
اما اگر برنامهنویس بخواهد در بلوک finally کار بستن منابع را انجام دهد، کد مشابه و تکراری در سراسر پروژه خواهد بود.
با استفاده از Try-With-Resource که در جاوا ۷ اضافه شده، کافیست منابعی که احتیاج به بستهشدن دارند را در پرانتز جلوی try بنویسیم تا به صورت خودکار پس از اتمام کار try (چه موفق و چه با خطا) بسته شوند.
در استفاده از این روش، دیگر نیازی به بلوک finally نیست و کد خواناتر و کوتاهتر است.
catch (NoSuchMethodException e) { return null; }
شاید به نظر بیاید همینکه یک استثنا را دریافت کردیم و نمیتوانیم برای هندل کردن آن کار خاصی انجام دهیم میتوانیم با این کار مشکل را حل کنیم ، اما این شیوه نادرستی است. ما به جای اینکه خطا را از منبع شناسایی کنیم، علت آن را پنهان کردهایم و تنها باعث یک استثنای NullPointer در جای دیگری از کد شدهایم. این کار پیدا کردن و برطرف کردن مشکل را بسیار پیچیده و زمانبر میکند.
public void doNotDoThis() throws Exception { ... }
public void doThis() throws NumberFormatException { ... }
اگر متدِ ما استثنای چکشده پرتاب میکند، ممکن است تصمیم بگیریم که در امضای متد بنویسیم throws Exception و نوع استثناها را مشخص نکنیم. اینکار در کوتاه مدت به نظر سادهتر میآید ولی از نظر طراحی ایراد دارد، چرا که اگر همکار ما قرار باشد چند ماه بعد روی این کد کار کند و از این متد استفاده کند، دقیق نمیداند چه استثناهایی ممکن است پرتاب شود و مجبور است برای انواع حالتها و استثناها آمادگی داشته باشد. اما در صورتی که استثنا را دقیقا مشخص کنیم همکارمان (یا استفادهکننده از کدِ ما) میداند باید چه استثناهایی را دریافت کند.
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! } }
همانطور که گفته شد، Throwable کلاس مادرِ همه استثناها و ارورهاست. اگرچه به لحاظ قواعد زبان جاوا، میتوانیم همه ارورها و استثناها را با هم دریافت کنیم ولی باید از این کار اجتناب کنیم. ارور زمانی اتفاق میافتد که خود JVM قادر به رفع مشکل نیست. یعنی مثلا ارور سرریز پشته یا پر شدن حافظه رخ داده است و این حالتها چیزی نیست که خود برنامه بخواهد مدیریت کند و بعد از آن هم به کارش ادامه دهد. باید اجازه دهیم این ارورها برنامه را مختل کنند و برنامه بسته شود چون اصلا چاره دیگری نداریم. یادمان نرود که زمانی که ارور رخ داده یعنی به یک حالت غیرقابلبازگشت رسیدهایم.
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //Incorrect way }
گاهی برنامهنویسان، برای تغییر یک استثنا و معنیدار کردن آن، یک یا انواعی از استثناها را دریافت میکنند و به جای همه آنها، یک استثنای شخصیسازیشده (CusomException) پرتاب میکنند. این کار بسیار خوبی است ولی مشکل از جایی شروع میشود که مثل کد بالا استثنای جدید را تنها از روی پیغام استثنای قبلی میسازیم. این روش، اطلاعات مربوط به stack trace استثنای قبلی را از بین میبرد و کار غلطی است. کار بهتر این است که به شیوه زیر، استثنا را داخل استثنای جدید wrap کنیم. دقت کنید که در این روش، خودِ استثنای قبلی را به سازنده استثنای جدید پاس میدهیم.
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //Correct way }
زمانی که از این شیوه استفاده میکنیم، استثنای جدید، استثنای قبلی را هم در خودش دارد. همچنین میتوانیم با صدا زدن getCause روی آن، به استثنای قبلی هم دسترسی پیدا کنیم.
catch (NoSuchMethodException e) { LOGGER.error("Some information", e); throw e; }
در کد بالا، هم لاگ زدن و هم پرتاب مجدد استثنا، موجب میشود از یک خطای واحد، چندین لاگ داشته باشیم و کار را برای کسی که کد ما را اشکالزدایی میکند سخت میکند.
اگر کدی که داخل finally نوشتیم ممکن است استثنایی پرتاب کند، حتما آن را همان جا دریافت کنیم و اجازه ندهیم از بلوک finaly خارج شود.
همچنین دقت کنید که return کردن در بلوک finally نیز، هم مقدارِ برگشتی و هم استثنای پرتابشده از قسمت try catch را بیاثر میکند، پس هرگز از بلو ک finally چیزی return نکنیم.
هرگز هیچ استثنایی را نادیده نگیرید حتی استثنایی که الان به نظر میرسد هیچگاه اتفاق نخواهد افتاد ولی کد، ثابت نخواهد ماند و در طول زمان تغییر خواهد کرد. ممکن است در آینده کد به گونهای عوض شود که این اتفاقی که به نظر میآید هرگز رخ نخواهد داد، واقعا رخ دهد.
حداقل کاری که الان میتوانیم انجام دهیم این است که خطا را لاگ بزنیم.
public void logAnException() { try { // do something } catch (NumberFormatException e) { log.error("This should never happen: " + e); } }
نباید هدفِ استفاده از چارچوب مدیریت استثناها را فراموش کنیم. ما استثنا را پرتاب میکنیم چون نمیتوانیم در محلِ آن، خطا را برطرف کنیم و جایی دریافت میکنیم که بتوانیم مشکل را مدیریت کنیم. این که استثنا را جایی دریافت کنیم که نمیتوانیم مدیریتش کنیم، کار اشتباهی است. باید فقظ استثناهایی را دریافت کنیم که در همین مرحله میتوانیم مدیریت کنیم.
catch (NoSuchMethodException e) { throw e; //Avoid this as it doesn't help anything }
بعد از بلوک try، بلوک catch یا finally یا هردو میآیند. ترکیب try/catch را زیاد دیدهایم ولی try/finally هم کاربردهای خودش را دارد. گاهی ما واقعا هنوز نمیدانیم با استثنا چه کار کنیم، صرفا لازم است یک تمیزکاری انجام دهیم، پس اصلا قسمت دریافت استثنا را نمینویسیم و به بلوک finally بسنده می کنیم.
این احتمالا مهمترین و معروفترین بِهروش برای مدیریت استثناهاست. این قاعده میگوید که در زودترین زمانی که میتوان متوجه خطا شد، استثنا را پرتاب کنید. این اتفاق معمولا در متدهای با سطح انتزائیسازی (abstraction) پایین رخ میدهد.
در مقابل، زمانی استثنا را دریافت میکنیم که در پشته به سطح انتزائیسازی بالایی رسیدهایم و اطلاعات کافی داریم و قادر به هندل کردن استثنا هستیم.
اگر از منابعی مثل دیتابیس یا ارتباط شبکه استفاده میکنید، مطمئن شوید که حتی در صورت بروز استثنا هم آنها را close میکنید. در این موارد در صورتی که نخواهیم در محل، استثنا را مدیریت کنیم و هدف، بستن منابع باشد، استفاده از try/finally توصیه میشود.
فرض کنیم که متد شما قرار است با فایل کار کند، حالا اگر استثنای NullPointer پرتاب کند، برای دریافتکننده هیچ معنایی ندارد، اما اگر در عوض این NullPointer را به استثنای معنیدار و مرتبطی تبدیل کند (مثلا NoSuchFileFoundException)، معنیدارتر است و کار را برای استفادهکنندگان متد راحت میکند.
استفاده از استثناها برای عوض کردن روند برنامه مثل استفادهی if/else، کار صحیحی نیست و کد را زشت و ناخوانا و غیرقابل فهم میکند. از استثنا تنها در جایی استفاده میکنیم که واقعا خطایی رخ داده و نمی توانیم همینجا برطرف کنیم.
public void doSomething() { try { // bunch of code throw new MyException(); // second bunch of code } catch (MyException e) { // third bunch of code } }
گاهی برنامهنویسان به اشتباه، منابع را در انتهای بلوک try میبندند. این کد در زمانی که استثنایی اتفاق نیفتد، به خوبی کار میکند اما زمانی که خطایی پیش بیاید و بلوک try به آخر نرسد، کار آزادسازی هم انجام نمیشود.
اما اگر برنامهنویس بخواهد در بلوک finally کار بستن منابع را انجام دهد
با استفاده از Try-With-Resource که در جاوا ۷ اضافه شده، کافیست منابعی که احتیاج به بستهشدن دارند را در پرانتز جلوی try بنویسیم تا به صورت خودکار پس از اتمام کار try (چه موفق و چه با خطا) بسته شوند.
File file = new File("./tmp.txt"); try (FileInputStream inputStream = new FileInputStream(file);) { // use the inputStream to read a file } catch (FileNotFoundException e) { LOGGER.error(e.getMessage()); } catch (IOException e) { LOGGER.error(e.getMessage()); }
در استفاده از این روش، دیگر نیازی به بلوک finally نیست و کد خواناتر و کوتاهتر است.
خوب حالا که کلیاتی از Exception ها را یادآوری کردیم بهتر است برویم سراغ Spring REST. در وب سرویس ها یا REST API هایی که با فریمورک (Framework) اسپرینگ مینویسیم، مدیریت کردن استثنا ها به چهار روش قابل انجام است. که ما اینجا فقط اخریش رو میگیم .
قبل از Spring 3.2 راهکار های مدیریت کردن استثنا در اپلیکشن های Spring MVC به دو صورت کلی HandlerExceptionResolver و ExceptionHandler@ صورت می گرفت. هر دو این روش ها نقاط ضعف مشخصی دارند.
از Spring 3.2 به بعد با اضافه شدن ControllerAdvice@ تقریبا مشکلات دو روش قبل مرتفع شد و یک روش یکپارچه، برای مدیریت کردن استثنا ها در اپلیکیشن بدست آمد.
اخیرا نیز در Spring 5 کلاس ResponseStatusException معرفی شد که یک روش سریع برای مدیریت کردن خطا ها در REST API ها است.
در Spring 3.2 پشتیبانی از یک ExceptionHandler@ جامع با انوتیشن ControllerAdvice@ ممکن شد. این موضوع مکانیزمی جدیدی نسبت به مدل MVC بوجود می آورد.
این مکانیزم در عین سادگی، انعطاف پذیر نیز می باشد و امکانات زیر را به ما می دهد:
خوب بریم یه مثال بزینم و همه چیز رو تمام کنیم :
در مرحله اول ما یه مدل نیاز داریم که خطاهامون رو در قالب اون به کاربر برگردونیم :
public class ErrorMessage { private int statusCode; private Date timestamp; private String message; private String description; public ErrorMessage(int statusCode, Date timestamp, String message, String description) { this.statusCode = statusCode; this.timestamp = timestamp; this.message = message; this.description = description; } public int getStatusCode() { return statusCode; } public Date getTimestamp() { return timestamp; } public String getMessage() { return message; } public String getDescription() { return description; } }
در مرحله دوم خطایی که لازم داریم رو ایجاد میکنیم ، توجه کنید که در این پیاده سازی شما میتونید از Exception یا RuntimeException ارث ببرید.
public class DataNotFoundException extends Exception{ public DataNotFoundException() { } public DataNotFoundException(String message) { super(message); } public DataNotFoundException(String message, Throwable cause) { super(message, cause); } public DataNotFoundException(Throwable cause) { super(cause); } public DataNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
@ResponseStatus(value = HttpStatus.SERVICE_UNAVAILABLE) public class ServiceUnavailableException extends Exception{ public ServiceUnavailableException() { super(); } public ServiceUnavailableException(String message) { super(message); } public ServiceUnavailableException(String message, Throwable cause) { super(message, cause); } public ServiceUnavailableException(Throwable cause) { super(cause); } protected ServiceUnavailableException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } }
در این مرحله اخر میایم advice خودمون رو مینویسیم :
@ControllerAdvice public class DataNotFoundConfiguration extends ResponseEntityExceptionHandler { @ExceptionHandler({AccessDeniedException.class , DataNotFoundException.class}) @ResponseBody @ResponseStatus(HttpStatus.NOT_FOUND) public ResponseEntity<ErrorMessage> handleAccessDeniedException( Exception ex, WebRequest request) { ErrorMessage message = new ErrorMessage( HttpStatus.NOT_FOUND.value(), new Date(), ex.getMessage(), request.getDescription(false)); return new ResponseEntity<ErrorMessage>(message, new HttpHeaders() ,HttpStatus.NOT_FOUND); } @ResponseBody @ExceptionHandler(value = ServiceUnavailableException.class) @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) public ResponseEntity<ErrorMessage> serviceUnavailableNotFoundException( NotFoundException ex, WebRequest request) { ErrorMessage message = new ErrorMessage( HttpStatus.SERVICE_UNAVAILABLE.value(), new Date(), ex.getMessage(), request.getDescription(false)); return new ResponseEntity<ErrorMessage>(message, new HttpHeaders() , HttpStatus.SERVICE_UNAVAILABLE); } }
حالا فقط کافیه هر جا که لازم داشتیم exception مورد نظر رو پرتاب کنیم تا به صورت اتوماتیک Response مورد نظر به کاربر برگردانده شود.
try { this.Amount_TP_Car.amount_TP_Car_Method(TEST); } catch (InterruptedException | ServiceUnavailableException e) { throw new ServiceUnavailableException("sasasa" , e); }
نمونه خروجی :
{ "statusCode": 503, "timestamp": "2023-06-28T11:02:29.384+04:30", "message": "sasasa", "description": "uri=/api/v1/am_tp" }
خوب همه چیز تمام شد ، شما میتونید بسته به نیازتون Exception های مختلف ایجاد کنید.
یه کد هم نوشتم و توی گیت گذاشتم میتونید از لینک زیر به آموزش بربوط به این کد و همچنین به آدرس ریپازیتوری کد دسترسی داشته باشید.
Exception Handling - Sample Code (Spring Boot)
امیدوارم از آموزش لذت برده باشید . منتظر نگاه زیباتون هستم تا بتونم آموزش ها رو بهتر کنیم .
پایان