Sử lý Java Exception

Kiểm soát Exception trong source code là việc tất yếu để tạo ra những đoạn code có chất lượng tốt. Lập trình viên chúng ta cần hiểu về bản chất của exception, xác định được sớm phương án thích hợp để sử lý cho từng loại exception khác nhau. Trong bài viết này, tôi sẽ thảo luận vs các bạn về cách thức sử lý exception thế nào là hợp lý. Có một số bạn đã sử lý exception theo cách sau:

\\ Dấu exception đi, không quan tâm đến nó
public void consumeAndForgetAllExceptions(){
    try {
        doSomething();
    } catch (Exception ex){
        ex.printStacktrace();
    }
}

public void doSomething() throw SomeException{
}

Cá nhân tôi rất ghét khi thấy các đoạn code try catch theo cách dấu bug đi như thế này, vì các nguyên nhân sau:

  1. Đoạn code trên đã nuốt hết tất cả các bug có trong method doSomething và dấu nó đi. Nếu code trong method doSomething xảy ra bug do lập trình viên gây ra thì sao? Chúng ta có trách nhiệm fix lại các bug loại này. Nhưng bug đã bị dấu đi ở đây -> khiến cho việc phát hiện bug trở lên chậm hơn và ít bị lộ bug hơn.
  2. Lý do tại sao lại có exception xảy ra mà chúng ta lại vẫn có thể bỏ qua nó đi như vậy? Có một số ý kiến cho rằng việc log bug là đủ. Nhưng trong thực tế, bug trong logs file sẽ rất dễ bị bỏ qua, nếu chúng ta không có bất kỳ feedback nào lại cho user. Ví dụ: user nhập vào số không hợp lệ thì ta mặc định giá trị được sử lý là 0. User có thể không hiểu được logic như vậy trong code của bạn và họ vẫn nghĩ rằng chúng ta đang sử lý cho giá trị mà họ nhập vào -> chúng ta cần thông báo cho user biết để họ correct lại giá trị này. Hoặc trong TH data từ Database bị sai, chúng ta hiển thị giá trị mặc định.... Ít nhất chúng ta vẫn cần quản lý được dạng exception này thông báo vs user về hiện trạng đang được sử lý như thế nào.

Và một số bạn sử lý exception theo cách sau:

\\ không sử lý gì, tiếp tục ném exception ra bên ngoài
public void continueThrowException() throws SomeException{
    doSomething();
}

Thực sự thì vấn đề của cách code trên không quá lớn, vấn đề của nằm ở chỗ chúng ta sẽ rất khó chịu nếu tất cả các method sử dụng lại đoạn code này đều phải viết thêm đoạn "throws SomeException" nữa. Đến khi nào đấy, người ta sẽ có băn khoăn là không hiểu tại sao ở đây lại có thể xảy ra exception cái kiểu này, vì lúc đó logic code của method đấy đã cách quá xa logic của method xảy ra exception.

Thực ra vấn đề throws SomeException ở trong cách viết của Java, trong C#, C++ tôi thấy thì chúng ta thường không sử lý gì cả, nếu muốn exception được raise tiếp lên trên. Tuy nhiên, vấn đề này lại được bắt nguồn từ một ý tưởng rất hay của Java, đó là muốn chúng ta lưu ý và có biện pháp sử lý sớm cho các vấn đề ngoại lệ được raise lên.

Để có thể handle được exception, trước hết, chúng ta cần hiểu về bản chất của chúng:

Bản chất của Exception

Nói chung, có ba trường hợp khác nhau gây ra Exception:

Exception do lỗi lập trình: Trong loại này, ngoại lệ được tạo ra do lỗi lập trình (ví dụ, NullPointerExceptionvà IllegalArgumentException). Mã khách hàng thường không thể làm bất cứ điều gì về lỗi lập trình.

Exception do lỗi user: user thử một cái gì đó không được API cho phép, user cung cấp tài nguyên không chính xác cho API. Ví dụ: Exception được ném ra trong khi phân tích cú pháp một tài liệu XML không được định dạng tốt, exception lên chứa thông tin hữu ích về vị trí trong tài liệu XML gây ra sự cố. Khách hàng có thể sử dụng thông tin này để thực hiện các bước khôi phục.

Exception do lỗi hệ thống: exception được tạo ra khi hệ thống không được khởi tạo thành công, hoặc gặp các sự cố. Ví dụ: hệ thống hết bộ nhớ hoặc kết nối mạng không thành công. Phản ứng của user đối với lỗi hệ thống là theo ngữ cảnh. User có thể thử lại sau một thời gian hoặc chỉ đơn giản là đưa ra lỗi và thông báo ứng dụng ngừng hoạt động.

Java định nghĩa hai loại ngoại lệ:

Checked Exception: Là ngoại lệ thường xảy ra do người dùng mà không thể lường trước được bởi lập trình viên. Ví dụ, một file được mở, nhưng file đó không thể tìm thấy và ngoại lệ xảy ra. Những ngoại lệ này không thể được bỏ qua trong quá trình biên dịch. Checked Exception là các lớp mà kế thừa lớp Throwable ngoại trừ RuntimeException và Error. Ví dụ như IOException, SQLException, … Checked Exception được kiểm tra tại thời gian biên dịch compile-time.

Unchecked Exception: Một ngoại lệ xảy ra ở runtime là ngoại lệ có thể tránh được bởi lập trình viên. Unchecked Exception là các lớp kế thừa RuntimeException, ví dụ ArithmaticException, NullPointerException, ArrayIndexOutOfBoundsException, … Unchecked Exception không được kiểm tra tại compile-time, thay vào đó chúng được kiểm tra tại runtime.

Error: Nó không giống các exception, nhưng vấn đề xảy ra vượt quá tầm kiểm soát của lập trình viên hay người dùng. Error được bỏ qua trong code của bạn vì bạn hiếm khi có thể làm gì đó khi chương trình bị error. Ví dụ như OutOfMemoryError, VirtualMachineError, AssertionError, …

sơ đồ kế thừa của NullPointerException

Trong sơ đồ này, NullPointerException được kế thừa từ RuntimeException và do đó nó thuộc về Unchecked exception.

Checked exception được sử dụng một method sẽ bắt buộc method ở tầng trên sử dụng nó phải bắt exception trong try - catch hoặc tiếp tục ném exception này cho tầng trên nữa sử lý. Ràng buộc này đã thành một gánh nặng không mong muốn trong trường hợp source code tầng này không có khả năng đối phó với exception một cách hiệu quả. Vì chỉ đơn giản vì trách nhiệm tổng hợp và report exception không nằm trong phạm vi sử lý source code của method gọi ngay trên nó.

Ví dụ:

public List getAllAccounts() throws
    FileNotFoundException, SQLException{
    ...
}

method getAllAccounts() ném ra 2 checked exception, chúng ta phải giải quyết các exception này call method getAllAccounts(), ngay cả khi ta không biết method này sử dụng File nào và kết nối đến CSDL gì. Ngoài ra chúng ta cũng thắc mắc về lý do là tại sao lại có lỗi về file và CSDL ở đây? Tôi chỉ quan tâm đến việc lấy ra danh sách các account thôi mà. Đừng đưa ra cho tôi những thông tin mà tôi không muốn quan tâm như thế này.

Xử lý Exception như thế nào

1. Xác định loại Exception: checked hoặc unchecked.

Chúng ta cần trả lời câu hỏi "Hành động nào chúng ta có thể thực hiện khi exception xảy ra?" Nếu chúng ta có thể thực hiện một số hành động thay thế hoặc phục hồi, -> checked exception. Nếu không thể làm bất cứ điều gì -> uncheck exception.

Việc sử dụng exception hợp lý giúp chúng ta kiểm soát được các lỗi lập trình dễ dàng hơn. Uncheck exception có lợi ích là không buộc chúng ta phải đối phó, xử lý chúng. Chúng được truyền đến nơi mà ta muốn xử lý chúng một cách tập trung, hoặc chúng được ném ra bên ngoài và chúng ta có report về tình trạng của chúng. API Java cung cấp nhiều loại exception tiêu chuẩn chẳng hạn như NullPointerException, IllegalArgumentException, và IllegalStateException... , sử dụng các loại exception tiêu chuẩn này sẽ giúp code rõ ràng và dễ đọc hơn nhiều.

2. Sử dụng Exception Tunneling để đóng gói exception

Không để checked exception đã được kiểm tra leo thang lên trên tầng cao hơn. Ví dụ: không được truyền SQLException Code ở tầng data access lên tầng business logic. Lớp businesss logic không cần biết SQLException. Chúng ta có 2 cách:

Chuyển đổi SQLException sang một checked exception khác, nếu chúng ta có thể xử lý khác, đặc biệt giành cho nó.

Chuyển đổi SQLException sang RuntimeException, nếu không thể làm bất cứ điều gì.

Hầu hết chúng ta không thể làm bất cứ điều gì với SQLExceptions. Ví dụ:

public void dataAccessCode(){
    try{
        ..some code that throws SQLException
    }catch(SQLException ex){
        ex.printStacktrace();
    }
}

block try - catch này chỉ ngăn chặn exception và không làm gì cả, sẽ tốt hơn nếu chúng ta viết như sau:

public void dataAccessCode(){
    try{
        ..some code that throws SQLException
    }catch(SQLException ex){
        throw new RuntimeException(ex);
    }
}

Điều này chuyển SQLException thành RuntimeException. Nếu SQLException xảy ra, chúng ta ném ra RuntimeException, action bị chặn lại và exception được báo cáo.

Nếu chúng ta có thể xử lý để khắc phục được lỗi này ở tầng trên thì chúng ta có thể ném ra DataAccessException thay cho SqlException, nó sẽ đúng ý nghĩa vs logic đang được sử lý ở đây hơn là SqlException.

public void dataAccessCode(){
    try{
        ..some code that throws SQLException
    }catch(SQLException ex){
        throw new DataAccessException(ex);
    }
}

3. Tạo exception với thông tin hữu ích.

public class DuplicateUsernameException
    extends Exception {}

Đoạn code trên không đưa ra thông tin hữu ích nào cho chúng ta, trừ tên exception. Đừng quên rằng các class Exception của Java giống như các class khác, chúng ta có thể thêm các method mà ta nghĩ rằng source code có thể gọi để có thêm thông tin. Ví dụ:

public class DuplicateUsernameException
    extends Exception {
    public DuplicateUsernameException 
        (String username){....}
    public String requestedUsername(){...}
    public String[] availableNames(){...}
}

Đoạn trên cung cấp hai method hữu ích: requestedUsername() trả về tên được yêu cầu và availableNames() trả về một mảng tên người dùng có sẵn tương tự với tên yêu cầu. Ta có thể sử dụng các method này để thông báo rằng tên người dùng được yêu cầu không có sẵn và tên người dùng khác có sẵn.

Nhưng nếu ta không thêm thông tin, thì chỉ cần ném một exception như sau:

throw new Exception("Username already taken");

Hoặc nếu chúng ta không có bất kỳ hành động gì ngoài xác định lỗi trên đã xảy ra thì ta có thể ném ra RuntimeException như sau.

throw new RuntimeException("Username already taken");

Ta ưu tiên sử dụng RuntimeException đối với tất cả các lỗi chương trình, chúng làm cho source code dễ đọc hơn, lỗi ngoại lệ được xử lý tập trung hơn, và logic của các method được sử lý đơn giản, tập trung vào nghiệp vụ chính hơn.

4.Unit test cho exception

Chúng ta có thể sử dụng @Test(expected = IndexOutOfBoundsException.class) để kiểm tra các exception có được đưa ra đúng như ta mong muốn hay không. Các hàm unit test cho phép chúng ta xem các exception trong hành động và nó đóng vai trò như là tài liệu cho ta thấy exception có thể được thực hiện như thế nào. Đây là ví dụ unit test cho IndexOutOfBoundsException:

@Test(expected = IndexOutOfBoundsException.class) public void testIndexOutOfBoundsException() { ArrayList emptyList = new ArrayList(); Object o = emptyList.get(0); } Đoạn mã ở trên nên ném ra IndexOutOfBoundsExceptionkhi emptyList.get(0)được gọi. Bằng cách viết unit test cho các trường hợp exception, bạn không chỉ ghi lại các trường hợp exception, mà còn làm cho source code của bạn mạnh mẽ hơn, chắc chắn hơn bằng cách kiểm tra các trường hợp ngoại lệ.

Ví dụ sử dụng Exception

  1. Làm sạch resource sau khi sử dụng try-catch Nếu đang sử dụng tài nguyên như kết nối cơ sở dữ liệu hoặc kết nối mạng, hãy chắc chắn rằng chúng ta clear chúng sau khi sử dụng.
public void dataAccessCode(){
    Connection conn = null;
    try{
        conn = getConnection();
        ..some code that throws SQLException
    }catch(SQLException ex){
        ex.printStacktrace();
        throw new DataAccessException(ex);
    } finally{
        DBUtil.closeConnection(conn);
    }
}

class DBUtil{
    public static void closeConnection
        (Connection conn){
        try{
            conn.close();
        } catch(SQLException ex){
            logger.error("Cannot close connection");
            throw new RuntimeException(ex);
        }
    }
}

DBUtil là một lớp tiện ích đóng Connection. Điểm quan trọng là việc sử dụng finally, trong ví dụ này, việc finally sẽ luôn được thực hiện để đóng kết nối và ném một RuntimeException nếu có vấn đề với việc đóng kết nối.

  1. Không bỏ hoặc bỏ qua exception Khi một method từ API ném một check exception, nó đang cố gắng cho bạn biết rằng bạn nên thực hiện một hành động nào đó để xử lý thay thế. Nếu check exception không có ý nghĩa đối với bạn, đừng ngần ngại chuyển nó sang RuntimeException và throw nó, nhưng đừng bỏ qua nó bằng cách bắt nó {}và tiếp tục như thể nếu không có gì xảy ra.

  2. Không bắt Exception chung chung Unchecked exception được kế thừa từ RuntimeException lớp, do đó kế thừa từ Exception. Bằng cách bắt Exception class, bạn cũng bắt RuntimeException như trong đoạn mã sau:

try{
..
}catch(Exception ex){
}

Đoạn code trên bỏ qua những trường hợp Unchecked exception. 4. Tránh sử dụng try - catch exception bao các vòng lặp. 5. Log exception chỉ một lần Ghi lại cùng một exception nhiều hơn một lần có thể làm lập trình viên nhầm lẫn khi kiểm tra dấu vết, vì vậy, chỉ cần log exception một lần duy nhất thôi.

Kết lại trên đây là một số gợi ý để xử lý exception, chúng ta sẽ tìm cách tốt hơn để xử lý source code với các trường hợp exception.

Tài liệu tham khảo: https://viblo.asia/p/xu-ly-ngoai-le-trong-java-znVGLYEYvZOe https://www.tutorialspoint.com/java/java_exceptions.htm http://wiki.c2.com/?CheckedExceptionsAreOfDubiousValue http://www.onjava.com/pub/a/onjava/2003/11/19/exceptions.html https://www.artima.com/intv/handcuffs2.html http://www.mindview.net/Etc/Discussions/CheckedExceptions http://wiki.c2.com/?CheckedException