Chúc mừng năm mới toàn thể mọi người !.

Ngày làm việc đầu tiên của năm con Heo, tranh thủ viết 1 bài ngắn ngắn trong vài phút lấy tinh thần phát triển tiếp, rồi còn phải đi liên hoan nữa :v Bài này mình sẽ nhắc đến 1 lỗi tiềm ẩn khá hiểm trong lập trình Java, cái này ít khi gặp nhưng đã gặp thì cũng ko dễ gì tìm ra nó nếu chưa từng trải qua 😀

Trước tiên hãy đi vào 1 ví dụ

Trong các hệ thống lớn nhỏ thường xuất hiện và cần sử dụng đến 1 class dạng khai báo các constant cần sử dụng lại ở nhiều nơi, để khi cần thay đổi chỉ cần sửa 1 chỗ là ngon. Ví dụ như này:

package net.tunghuynh.constant;

public class Constants {

    static class STATUS{
        static int ACTIVE = 1;
        static final int INACTIVE = 0;
    }

    interface DAY{
        String MON = "MON";
        String TUE = "TUE";
        String WED = "WED";
        String THU = "THU";
        String FRI = "FRI";
        String SAT = "SAT";
        String SUN = "SUN";
    }
}

Và khi sử dụng thay vì gán giá trị cứng (hard-code) thì ta sẽ gán các constant kia

package net.tunghuynh.constant;

public class Main {
    public static void main(String[] args) {
        int active = Constants.STATUS.ACTIVE;
        int inactive = Constants.STATUS.INACTIVE;
        String day = Constants.DAY.MON;
        String str = String.format("Active: %d, Inactive: %d, Day: %s", active, inactive, day);
        System.out.println(str);
    }
}

Nhìn qua rất bình thường và không có vấn đề gì, và mình cũng không quan tâm đến kết quả của cái out.print kia là gì cả vì nhìn cũng biết ngay rồi.

Nhưng thử build 2 class trên rồi decompile lại thằng Main.class xem nó ra cái gì 🙂

Ồ, trình biên dịch của Java đã thay đổi mất source ban đầu của mình. STATUS.INACTIVE đã bị đổi luôn thành giá trị 0, DAY.MON cũng bị đổi thành "MON". Nhưng riêng STATUS.ACTIVE lại được giữ nguyên?

Ở ví dụ này mình đã cố tình tạo ra 1 class Constant không đồng nhất. CONSTANT.DAY mình để là interface, CONSTANT.STATUS thì mình để là static class, INACTIVEfinal, còn ACTIVE lại không có final. Mục đích để nhìn vào có thể rút được kết luận luôn rằng

Khi khởi tạo giá trị cho 1 biến bằng 1 constant final thì java sẽ tự động lấy giá trị của constant để khởi tạo cho biến khi compile.

Quy trình thực hiện phép gán int inactive = Constants.STATUS.INACTIVE là truy xuất đến vùng nhớ của class Constants, tiếp theo tìm đến STATUS, rồi cuối cùng tìm đến INACTIVE và đọc giá trị gán cho biến inactive. Đây là 1 cái khá thông minh của java nhằm làm tối ưu performance khi runtime, việc truy xuất vào các biến final sẽ thực hiện 1 lần duy nhất ngay từ lúc compile.

Các bạn có thể test thêm 1 trường hợp khác tương tự. Khi ta thực hiện phép gán cho 1 biểu thức toán học, trình biên dịch cũng sẽ tự động tính toán và gán luôn giá trị sau khi compile.

int num = 123 + 456;

Sẽ được chuyển thành sau khi compile

int num = 579;

Như 1 số tài liệu cũ đã out-of-date có nói rằng việc viết phép gán như trên sẽ làm ảnh hưởng đến hiệu năng khi chạy chương trình, nhưng thực ra không đúng vì với các phiên bản java sau này (mình ko rõ từ bản bao nhiêu) việc đó chỉ được xử lý khi compile.

Thế tại sao thằng interface DAY có khai báo final gì đâu mà cũng bị chuyển thành giá trị? Nếu bạn đang thắc mắc câu này thì có thể xem lại bài [Java] Phần 4: Interface mình có nhắc đến

Mặc định các biến của interface là static và final

Vậy tóm lại việc này nó ảnh hưởng gì nhỉ?

Có một câu chuyện thực tế rằng ở khá nhiều project lớn thường xuyên phải thực hiện công việc maintain sản phẩm, mà sản phẩm đó đã chạy từ chục năm về trước qua tay tới vài chục vài trăm ông dev, từ thời mà teamwork lười hoặc ko đc tiếp cận các tool quản lý source như svn/git,…, ngày này qua năm khác mỗi ông sửa 1 phần lớn nhỏ mà chẳng ai dám khẳng định hay kiểm soát được source mình đang code có phải là bản chuẩn so với bản đã build trên server product của khách hàng hay không, nên khi deploy maintain chả ai deploy hẳn 1 gói war hay jar hay là all classes lên product mà chỉ deploy riêng lẻ từng class thay đổi. Việc này càng gây mông lung và tạo thói quen cho các dev tiếp quản sau này, cứ sửa cái gì là chỉ build mỗi class mình sửa để đẩy lên server product deploy. Rủi ro phát sinh từ đây khi sửa code trên máy local chạy ro ro, có khi deploy lên server test cũng ngon vì chả sợ gì toàn kéo all class lên server test, nhưng lại không kiểm soát được hết các class thay đổi để deploy dẫn đến lỗi logic mà chạy mãi mới gặp lỗi, gặp lỗi rồi cũng ko biết tại sao.

Cụ thể, nếu ta thử sửa giá trị của 1 biến constant final thì tất nhiên khi compile giá trị mới đó cũng sẽ được update vào Main.class. Nhưng giả sử có vài chục class khác cũng sử dụng biến đó mà dev không hề kiểm tra cũng không biết được các class ảnh hưởng mà chỉ copy mỗi cái Constants.class đi deploy thì hiển nhiên là lỗi logic code cũ rồi.

Một trường hợp khác tương tự đó là với các class có inner class kiểu như class STATUSinterface DAY nằm trong class Constants thì khi compile sẽ tự động sinh ra 2 file class nữa là Constants$STATUS.classConstants$DAY.class cho riêng từng inner class. Nếu sửa STATUS hoặc DAY mà chỉ copy Constants.class đi deploy thì cũng sẽ gặp lỗi tương tự.

Vậy cách khắc phục hay hạn chế việc này là gì?

Với các dự án mới xây dựng thì việc này ít gặp hơn do tool quản lý source ngon rồi, deploy full gói luôn rồi chứ không deploy lẻ từng class thay đổi. Còn các dự án vẫn dùng cách deploy cổ truyền kia thì chỉ còn cách kiểm soát class thay đổi bằng cách dựa dẫm vào IDE để search xem biến constant đó được sử dụng ở những đâu thôi. Hoặc như mình ngày xưa thì làm luôn 1 cái tool nhỏ nhỏ quét các class thay đổi bằng cách kiểm tra thời gian “Date modified” của file class đó, nếu thời gian thay đổi sau khi build thì nghĩa là class bị ảnh hưởng và cần deploy cùng 🙂

Hy vọng sau khi đọc xong bài viết này, các bạn có thể lưu giữ thêm 1 chút hình ảnh về rủi ro tiềm ẩn liên quan đến Constant. Chúc các bạn sang năm mới làm việc hiệu quả và thành công hơn 😀

Bình luận