Spring Boot වල Exception Handling | Global Exception Handling SC Guide

Spring Boot වල Exception Handling | Global Exception Handling SC Guide

ආයුබෝවන් කට්ටියට! අද අපි කතා කරන්න යන්නේ ඔයාලා හැමෝම ෆීල්ඩ් එකේදි අනිවාර්යයෙන්ම මූණ දෙන, ඒත් ගොඩක් අය හරියට තේරුම් නොගෙන වැඩ කරන ටොපික් එකක් ගැන – ඒ තමයි Exception Handling. සරලව කිව්වොත්, අපේ සොෆ්ට්වෙයාර් එක වැඩ කරනකොට අනපේක්ෂිත දේවල් වුණොත් ඒවා පාලනය කරන්නේ කොහොමද කියන එක.

හිතන්නකෝ ඔයාලා API එකක් හැදුවා කියලා. ඒකෙන් ඩේටාබේස් එකට කනෙක්ට් වෙලා ඩේටා ගන්නවා. හදිස්සියේ ඩේටාබේස් එක ඩවුන් වුණොත් මොකද වෙන්නේ? නැත්නම් කෙනෙක් ඉල්ලන ID එකට අදාළ රෙකෝඩ් එකක් නැත්නම්? සාමාන්‍යයෙන් අපිට එන්නේ කැත error messages ටිකක්. ඒවා දැක්කම user ට හිතෙන්නේ "අනේ මන්දා, මේ මොකක්ද මේ වෙන්නේ" කියලා. ඒ වගේ වෙලාවට තමයි මේ Exception Handling කියන එක අපිට පිහිටට එන්නේ. අපි මේක හරියට කළොත්, user ට තේරෙන විදිහට, ලස්සනට error messages පෙන්නන්න පුළුවන්. ඒ වගේම අපේ system එක stable කරගන්නත් පුළුවන්.

Exception Handling කියන්නේ මොකක්ද?

සරලව කිව්වොත්, Exception එකක් කියන්නේ program එකක් run වෙනකොට වෙන අනපේක්ෂිත සිද්ධියක් (unexpected event) නැත්නම් වරදක් (error) කියලා. උදාහරණයක් විදිහට, ඩිවිෂන් බයි සීරෝ (division by zero), නොතිබෙන ෆයිල් එකක් ඕපන් කරන්න හදන එක, නැත්නම් වැරදි ඩේටා ටයිප් එකක් පාස් කරන එක වගේ දේවල් වෙන්න පුළුවන්.

ගොඩක් වෙලාවට අපි පොඩි program වලදි මේවට try-catch බ්ලොක්ස් පාවිච්චි කරනවා. ඒක හොඳයි පොඩි තැන්වලට. හැබැයි ලොකු Spring Boot application එකක, controller method සීයක් විතර තියෙනකොට, හැම එකටම try-catch දැම්මොත් කොහොමද? කෝඩ් එක මාරම අවුල් වෙනවා නේද? එක exception එකක් ආවම, ඒක හැන්ඩ්ල් කරන්න තැන් දහයකට යන්න වෙනවා. එතකොට තමයි අපිට මේ Spring Boot වලින් දෙන ෆිචර්ස් ටික ගොඩක් වැදගත් වෙන්නේ.

@ExceptionHandler: Local Errors හසුරුවමු

මුලින්ම අපි බලමු @ExceptionHandler කියන annotation එක ගැන. මේක අපිට පුළුවන් එක controller එකක් ඇතුළේ, එතන විතරක් වෙන specific exceptions ටිකක් handle කරන්න. ඒ කියන්නේ, ඔයාගේ controller එකේ තියෙන method එකකදී යම් exception එකක් ආවොත්, ඒ controller එක ඇතුළේම ඒකට අදාළ logic එක ලියන්න පුළුවන්.

හිතමුකෝ අපි user කෙනෙක්ගේ ID එකක් අරගෙන එයාගේ ඩීටේල්ස් ගන්නවා කියලා. කෙනෙක් වැරදි ID එකක් (උදාහරණයක් විදිහට, ස්ට්‍රින් එකක්) දුන්නොත් NumberFormatException එකක් එන්න පුළුවන්. ඒක handle කරන්නේ මෙහෙමයි:


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ExceptionHandler;

@RestController
public class UserController {

    @GetMapping("/users/{id}")
    public String getUserById(@PathVariable String id) {
        try {
            Long userId = Long.parseLong(id);
            // Simulate fetching user from DB
            if (userId < 1) {
                throw new IllegalArgumentException("User ID cannot be less than 1");
            }
            return "User details for ID: " + userId;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Invalid user ID format. Please provide a number.", e);
        }
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>("Error: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }
}

මේ කෝඩ් එකේදි අපි IllegalArgumentException එකක් ආවොත් ඒක හැන්ඩ්ල් කරන්න handleIllegalArgumentException කියන method එක පාවිච්චි කරලා තියෙනවා. ඒකෙන් HTTP 400 Bad Request ස්ටේටස් එකත් එක්ක ලස්සන message එකක් යවනවා. හැබැයි මේක වැඩ කරන්නේ UserController එක ඇතුළේ විතරයි. වෙන controller එකක මේ වගේ exception එකක් ආවොත් මේක වැඩ කරන්නේ නෑ.

@ControllerAdvice: Global Errors හසුරුවමු

අපි දැක්කා @ExceptionHandler එක Controller එකකට සීමා වෙනවා කියලා. එතකොට අපේ application එකේ තියෙන හැම controller එකකම පොදු exception ටිකක් (DataIntegrityViolationException, IOException වගේ) හැන්ඩ්ල් කරන්න ඕන වුණොත් අපි හැම controller එකකම @ExceptionHandler ලියන්න ඕනද? ඒක එච්චර හොඳ විදිහක් නෙවෙයි. කෝඩ් එක duplication වෙනවා, maintain කරන්න අමාරු වෙනවා.

අන්න ඒකට තමයි @ControllerAdvice කියන annotation එක අපිට උදව් කරන්නේ. මේක පාවිච්චි කරලා අපිට පුළුවන් එක තැනකදී, අපේ මුළු application එකටම පොදු exception handling logic එකක් ලියන්න. මේක මාරම පහසුයි. අලුතෙන් controller එකක් හැදුවත්, ඒකේ exception handling ගැන හිතන්න ඕන නෑ, මොකද ඒක automatically මේ global handler එකෙන් හැන්ඩ්ල් වෙනවා.

හදමු අපි GlobalExceptionHandler කියලා class එකක්:


import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NumberFormatException.class)
    public ResponseEntity<String> handleNumberFormatException(NumberFormatException ex) {
        return new ResponseEntity<>("Invalid number format provided. Please check your input.", HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
        return new ResponseEntity<>("Request error: " + ex.getMessage(), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleAllUncaughtExceptions(Exception ex) {
        // Log the exception for debugging
        System.err.println("An unexpected error occurred: " + ex.getMessage());
        return new ResponseEntity<>("An unexpected error occurred. Please try again later.", HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

දැන් අපිට UserController එකේ @ExceptionHandler එක අයින් කරන්න පුළුවන්. මොකද NumberFormatException සහ IllegalArgumentException කියන දෙකම දැන් GlobalExceptionHandler එකෙන් හැන්ඩ්ල් කරනවා. මේකෙදි අපි Exception.class කියන generic exception එකත් handle කරලා තියෙනවා. ඒකෙන් වෙන්නේ, අපි නිශ්චිතව handle කරලා නැති ඕනෑම exception එකක් ආවොත්, මේ generic handler එකෙන් ඒක අල්ලගෙන, INTERNAL_SERVER_ERROR එකක් යවන එක.

Custom Exceptions හසුරුවමු

සමහර වෙලාවට අපිට ඕනේ වෙනවා අපේ application එකටම විශේෂිත වූ errors ටිකක් හදන්න. උදාහරණයක් විදිහට, "User Not Found", "Product Out Of Stock" වගේ දේවල්. මේවට අපි Custom Exceptions හදනවා. මේකෙන් අපේ කෝඩ් එක තවත් කියවන්න පහසු වෙනවා, වගේම business logic එකත් පැහැදිලි වෙනවා. ඒ වගේම, මේවා අපේ global exception handler එකෙන් ලස්සනට හැන්ඩ්ල් කරන්නත් පුළුවන්.

මුලින්ම, හදමු Custom Exception class එකක්:


// src/main/java/com/example/demo/exception/ResourceNotFoundException.java
package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND) // This sets the HTTP status code directly
public class ResourceNotFoundException extends RuntimeException {

    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

දැන් මේක අපේ Service layer එකෙන් throw කරමු. අපි හිතමු UserService එකක් තියෙනවා කියලා:


// src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;

import com.example.demo.exception.ResourceNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public String getUserDetails(Long id) {
        // Simulate checking if user exists in DB
        if (id > 100) { // For example, IDs greater than 100 don't exist
            throw new ResourceNotFoundException("User with ID " + id + " not found.");
        }
        return "Details for user " + id + " retrieved successfully.";
    }
}

දැන් මේක Controller එකෙන් පාවිච්චි කරමු:


// src/main/java/com/example/demo/controller/UserApiController.java
package com.example.demo.controller;

import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserApiController {

    @Autowired
    private UserService userService;

    @GetMapping("/api/users/{id}")
    public String getUser(@PathVariable Long id) {
        return userService.getUserDetails(id);
    }
}

අපේ GlobalExceptionHandler එකට මේ ResourceNotFoundException එක හැන්ඩ්ල් කරන්න අමුතුවෙන් කෝඩ් ලියන්න ඕන නෑ. මොකද අපි ResourceNotFoundException class එකටම @ResponseStatus(HttpStatus.NOT_FOUND) කියන annotation එක දාලා තියෙනවා. ඒක නිසා Spring Boot එක automaticly HttpStatus.NOT_FOUND (404) status එකත් එක්ක response එකක් යවනවා.

හැබැයි, ඔයාට තවත් detailed response එකක් දෙන්න ඕන නම්, GlobalExceptionHandler එකටම මේ වගේ method එකක් දාන්නත් පුළුවන්:


// In GlobalExceptionHandler.java
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
    ErrorResponse error = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis());
    return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}

// You might define a custom ErrorResponse class
public class ErrorResponse {
    private int status;
    private String message;
    private long timestamp;

    public ErrorResponse(int status, String message, long timestamp) {
        this.status = status;
        this.message = message;
        this.timestamp = timestamp;
    }

    // Getters and setters
    public int getStatus() { return status; }
    public void setStatus(int status) { this.status = status; }
    public String getMessage() { return message; }
    public void setMessage(String message) { this.message = message; }
    public long getTimestamp() { return timestamp; }
    public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
}

දැන් ඔයා /api/users/101 වගේ request එකක් යැව්වොත්, ඔයාට 404 Not Found ස්ටේටස් එකත් එක්ක, ඔයා දුන්න message එකත් එක්ක ලස්සන JSON response එකක් එයි. පට්ට නේද?

වස්සන කතාව (Conclusion)

අද අපි කතා කළේ Spring Boot වල Exception Handling කියන මාරම වැදගත් ටොපික් එකක් ගැන. @ExceptionHandler පාවිච්චි කරලා local level එකේ exceptions handle කරන විදිහයි, ඊටත් වඩා වැදගත් වෙන @ControllerAdvice පාවිච්චි කරලා global level එකේ හැම exception එකක්ම එක තැනකින් handle කරන විදිහයි. ඒ වගේම අපේ application එකේ business logic එකට විශේෂිත වූ Custom Exceptions හදලා ඒවත් handle කරන විදිහත් අපි බැලුවා.

මේ ක්‍රමවේදයන් පාවිච්චි කිරීමෙන් ඔයාගේ application එක තව කල් පවතින, user-friendly, maintain කරන්න පහසු එකක් වෙනවා. ඉතින්, තව දුරටත් try-catch ගොඩගහන්නේ නැතුව, මේවා ඔයාගේ project වලටත් implement කරලා බලන්න. මොකද, හොඳ exception handling එකක් කියන්නේ හොඳ software එකක වැදගත් ලක්ෂණයක්.

මේ ගැන ඔයාලට මොනවා හරි ප්‍රශ්න තියෙනවා නම්, නැත්නම් ඔයාලා මේ වගේ දේවල් කරපු විදිහ ගැන අදහස් තියෙනවා නම්, පහලින් කමෙන්ට් එකක් දාගෙන යන්න. තව අලුත් article එකකින් හමුවෙමු! Happy Coding!