Đừng ép tôi phải bắt!

Trong Java có hai loại exception, 1 loại thừa kế trực tiếp từ Exception, và một loại thừa kế từ RuntimeException, loại thừa kế từ Exception yêu cầu chúng ta phải catch hoặc re-throws ngay trong giai đoạn code nếu không IDE sẽ báo lỗi compile, còn loại thừa kế từ RuntimeException thì không.

Những tưởng câu chuyện này chẳng có gì, nhưng hoá ra nó lại tạo ra rất nhiều tranh cãi rằng là nên dùng cái nào, vậy nên mình muốn có một bài chia sẻ quan điểm của mình về việc này nhé.

Các lĩnh vực sử dụng

Có 2 lĩnh vực mà chúng ta sẽ cần quan tâm, đó là lĩnh vực viết thư viện và lĩnh vực viết ứng dụng.

Lĩnh vực viết thư viện

Đối với lĩnh vực viết thư viện: như mình đang làm đây chẳng hạn thì có rất nhiều loại Exception phải bắt vì mình dùng rất nhiều reflection, ví dụ: ClassNotFoundException, NoSuchMethodException, IllegalAccessException, ExceptionInInitializerError. Câu hỏi đặt ra là phải làm gì với những Exception này? Một ví dụ thực tế là khi khởi tạo 1 bean, mình phải dùng reflection thế này:

Object bean = beanClass.newInstance();

Mình sẽ có 4 lựa chọn

  1. Catch Exception và log ra lỗi: cái này không ổn, vì là viết thư viện nên mình phải hạn chế dùng các thư viện ngoài nên ở đây mình sẽ không có đối tượng logger, không thể dùng System.out vì nó sẽ in ra console, và khi deploy rất có thể nó sẽ không in ra file và chúng ta sẽ không biết lỗi là gì
  2. Trả về giá trị null: cái này không ổn vì nếu có lỗi xảy ra, người sử dụng thư viện sẽ bị NullPointerException và họ sẽ không hiểu tại sao
  3. Re-throw tất cả các Exception: cái này không ổn, vì người dùng thư viện sẽ không hiểu được tại sao lại ném ra và phải làm gì với các Exception này, họ sẽ thấy thư viện này quá phức tạp và không muốn dùng hoặc họ sẽ làm thêm 1 bước nữa là #wrap lại để làm gì đó với các Exception, và điều này làm tốn thời gian, công sức của họ
  4. Wrap lại Exception và ném ra RuntimeException: Hợp lý bởi vì rất hiếm khi xảy ra Exception trong trường hợp này, và trong trường hợp có lỗi cũng sẽ có đủ thông tin để chúng ta debug
try {
    Object bean = beanClass.newInstance();
}
catch(Exception e) {
    throw new RuntimeException("can not create bean of: " + beanClass);
}

Lĩnh vực viết ứng dụng

Đối với lĩnh vực viết ứng dụng: như chúng ta vẫn đang làm. Giả sử chúng ta tổ chức source code thành 3 tầng:

  1. Repository: truy xuất cơ sở dữ liệu
  2. Service: Xử lý dữ liệu
  3. Controller: Tiếp nhận request và trả lại response

Và code của chúng ta thế này:

class UserRepository {
    public User findUserByName(String username) throws SQLException, IOException;
}
class UserService {
    public User getUser(String username) throws SQLException, IOException;
}

Đến tầng controller chúng ta sẽ có 2 lựa chọn:

  1. Xử lý Exception: không hợp lý, bởi vì đây chỉ là một nghiệp vụ lấy thông tin User theo name, còn rất nhiều nghiệp vụ khác thì sao, nó sẽ khiến source code của chúng ta rất dài, phức tạp và đội chi phí unitest
class UserController {
    public UserResponse getUser(String username) {
        try {
            return new UserResponse(userService.getUser(username));
        }
        catch(IOException e) {
        }
        catch(SQLException e) {
        }
    }
}
  1. Re-throws Exception và tạo ra lớp xử lý Exception tập trung: hợp lý và thường chúng ta sẽ làm vậy, vì rốt cuộc chúng ta cũng chẳng thể biết chúng ta cần làm gì với những Exception này vì nó không liên quan đến người dùng. Tuy nhiên câu hỏi đặt ra là tại sao chúng ta không sử dụng RuntimeException ngay ở tầng repository để đỡ phải ném đi ném lại giữa các tầng? Đúng vậy, chúng ta hãy làm như vậy để source code trở lên ngắn gọn và clean hơn nhé.
class UserRepository {
    public User findUserByName(String username);
    }
}

class UserService {
    public User getUser(String username);
}

class UserController {
    public UserResponse getUser(String username);
}

@ControllerAdvice
class GlobalExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity handleRuntimeException(RuntimeException e) {
        Throwable cause = e.getCause();
        if(cause == null) {
            // trả về kết quả
        }
        else if(cause instanceof IOException) {
            // trả về kết quả
        }
        else if(cause instanceof SQLException) {
            // trả về kết quả
        }
        else {
            // trả về kết quả
        }
    }
}

Lập trình qua nhiều ngôn ngữ như CSharp và framework như Spring hay apache thì đa phần đều sử dụng RuntimeException, nghĩa là không ép chúng ta phải bắt Exception, và gần đây nhất thì Kotlin ra đời và đã keep silent tất cả các Exception của Java, và đó cũng là một trong những lý do mà mình rất thích ở kotlin

Kết luận

Theo quan điểm của mình, chúng ta hãy nên sử dụng dạng RuntimeException và xử lý Exception tập trung ở một nơi nào đó. Hiện nay các framework như Spring đều hỗ trợ việc xử lý Exception tập trung, điều này rất tiện lợi, hãy tận dụng sức mạnh này để tiết kiệm thời gian và công sức cho mình nhé, 🙂