CodeGym /Các khóa học /JAVA 25 SELF /Best practices khi làm việc với tệp

Best practices khi làm việc với tệp

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Xử lý lỗi: đừng bỏ qua ngoại lệ!

Làm việc với tệp luôn là tương tác với thế giới bên ngoài, vốn có thể rất khó lường. Ổ đĩa có thể đầy, tệp — biến mất, quyền — thay đổi, và người dùng — làm những điều khó ngờ (ví dụ, chạy chương trình của bạn từ thư mục có khoảng trắng trong tên hoặc từ USB sắp bị rút ra). Nếu mã của bạn không sẵn sàng cho những tình huống đó, nó không chỉ có nguy cơ “đổ sập”, mà còn có thể khiến người dùng mất dữ liệu cần thiết.

Best practices không chỉ là “mẹo thời thượng” mà là tập hợp các phương pháp đã được thời gian kiểm chứng, giúp tránh những kịch bản tệ nhất: mất dữ liệu, rò rỉ bộ nhớ, lộ thông tin riêng tư và những lỗi ngớ ngẩn khiến bạn khó nhìn mặt đồng nghiệp (và đặc biệt — người dùng).

Vì sao không được viết catch rỗng?

Trong Java (và không chỉ Java) rất dễ bị cám dỗ viết như sau:

try {
    // thao tác với tệp
} catch (IOException e) {
    // thôi, không được — cũng kệ!
}

Đây là điều tệ hại nhất bạn có thể làm. Mã như vậy không chỉ “nuốt” lỗi: nó còn khiến lỗi trở nên vô hình với người dùng và chính bạn. Kết quả là, nếu có gì đó trục trặc, bạn sẽ không bao giờ biết chính xác điều gì đã xảy ra và khi nào.

Làm thế nào cho đúng?

  • Ghi log lỗi: ít nhất hãy in thông báo ra console hoặc ghi vào file log.
  • Thông báo cho người dùng: nếu lỗi nghiêm trọng, hãy hiển thị thông điệp thân thiện.
  • Đừng tiết lộ quá mức: đừng hiển thị cho người dùng chi tiết nội bộ của hệ thống (ví dụ, toàn bộ stack trace — thứ dành cho nhà phát triển).

Ví dụ:

try {
    List<String> lines = Files.readAllLines(Path.of("data.txt"));
    // xử lý dữ liệu
} catch (IOException e) {
    System.err.println("Lỗi khi đọc tệp: " + e.getMessage());
    // Có thể ghi chi tiết vào file log
    e.printStackTrace(System.err);
}

Tại sao cần bắt các ngoại lệ cụ thể?
Vì các lỗi khác nhau cần phản ứng khác nhau. Ví dụ, nếu không tìm thấy tệp — có thể đề nghị người dùng chọn tệp khác (NoSuchFileException hoặc FileNotFoundException). Nếu thiếu quyền — yêu cầu chạy chương trình với quyền phù hợp (AccessDeniedException). Nếu ổ đĩa đầy — đề nghị giải phóng dung lượng (IOException khi ghi).

2. Quyền truy cập và bảo mật

Kiểm tra quyền truy cập trước khi thao tác

Trước khi đọc hoặc ghi tệp, nên chắc chắn rằng bạn có quyền làm điều đó. Trong Java có các phương thức:

  • File.canRead()
  • File.canWrite()

Ngay cả khi các phương thức này trả về true, điều đó không bảo đảm thành công — quyền có thể thay đổi bất cứ lúc nào (ví dụ, một tiến trình khác thay đổi quyền). Vì vậy hãy luôn sẵn sàng xử lý ngoại lệ.

Ví dụ:

File file = new File("config.properties");
if (!file.canRead()) {
    System.err.println("Không có quyền đọc tệp!");
    return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
    // đọc tệp
} catch (IOException e) {
    System.err.println("Lỗi khi đọc: " + e.getMessage());
}

Đừng tiết lộ chi tiết nội bộ

Nếu chương trình của bạn làm việc với các tệp nhạy cảm (ví dụ: mật khẩu), đừng in các đường dẫn tới những tệp này hoặc nội dung của chúng trong các lỗi hiển thị cho người dùng.

3. Đừng dùng đường dẫn tương đối cho các thao tác quan trọng

Đường dẫn tương đối (new File("data.txt")) là đường dẫn tính từ thư mục làm việc hiện tại, vốn có thể khác nhau tùy cách chương trình được chạy (ví dụ, từ IDE hoặc từ dòng lệnh). Điều này có thể dẫn đến nhầm lẫn và lỗi.

Best practice: với các tệp quan trọng, hãy dùng đường dẫn tuyệt đối hoặc xác định rõ thư mục làm việc.

Ví dụ:

String userHome = System.getProperty("user.home");
Path configPath = Path.of(userHome, "myapp", "config.properties");

4. Làm việc với tệp và thư mục tạm

Tệp tạm dùng để làm gì?

Các tệp tạm được dùng cho nhiều nhiệm vụ. Đôi khi chúng phục vụ cho các thao tác trung gian: chẳng hạn, trước hết dữ liệu được ghi vào tệp tạm, sau đó tệp này thay thế tệp chính. Trường hợp khác — tệp tạm giúp lưu trữ thông tin không cần sau khi chương trình kết thúc và có thể xóa an toàn.

Tạo tệp tạm an toàn như thế nào?

Hãy dùng các phương thức trong java.nio.file.Files:

Path tempFile = Files.createTempFile("myapp_", ".tmp");
// ... làm việc với tệp
Files.deleteIfExists(tempFile);

Thư mục tạm thời

Path tempDir = Files.createTempDirectory("myapp_");

5. Độ tin cậy: bản sao dự phòng và kiểm tra tính toàn vẹn

Hãy dùng bản sao dự phòng khi thay đổi các tệp quan trọng

Trước khi ghi đè một tệp quan trọng (ví dụ: cấu hình), hãy tạo bản sao của nó:

Path config = Path.of("config.properties");
Path backup = Path.of("config.properties.bak");
if (Files.exists(config)) {
    Files.copy(config, backup, StandardCopyOption.REPLACE_EXISTING);
}

Nếu có sự cố khi ghi — bạn luôn có thể khôi phục từ bản sao dự phòng.

Kiểm tra tính toàn vẹn dữ liệu

Với dữ liệu đặc biệt quan trọng, có thể dùng checksum (ví dụ, MD5 hoặc SHA-256). Sau khi ghi tệp — tính checksum và lưu cạnh đó. Khi đọc — kiểm tra xem tệp có bị thay đổi không.

Ví dụ tính SHA-256 (cho những ai yêu thích mật mã):

import java.security.MessageDigest;
import java.nio.file.Files;
import java.nio.file.Path;

byte[] data = Files.readAllBytes(Path.of("important.dat"));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
// Lưu hash vào tệp riêng hoặc so sánh khi đọc

6. Giảm thiểu khoảng thời gian giữa kiểm tra và sử dụng tệp

Đây là vấn đề cổ điển TOCTOU (Time Of Check To Time Of Use): giữa lúc bạn kiểm tra rằng tệp tồn tại và lúc bắt đầu đọc, tệp có thể biến mất hoặc bị thay đổi.

Vì vậy hãy cố gắng thực hiện kiểm tra và sử dụng trong cùng một khối try. Và nhất định phải xử lý ngoại lệ, ngay cả khi bạn vừa mới kiểm tra tệp.

Ví dụ:

Path filePath = Path.of("data.txt");
if (Files.exists(filePath)) {
    try (BufferedReader reader = Files.newBufferedReader(filePath)) {
        // đọc tệp
    } catch (IOException e) {
        System.err.println("Lỗi khi đọc tệp (có thể tệp đã biến mất): " + e.getMessage());
    }
}

7. Một vài lời khuyên hữu ích khác

Hãy dùng try-with-resources cho mọi tài nguyên
Mọi lớp triển khai giao diện AutoCloseable (gần như tất cả các stream của Java IO/NIO) đều có thể dùng trong try-with-resources. Cách này giúp tránh rò rỉ tài nguyên.

try (BufferedReader reader = Files.newBufferedReader(Path.of("data.txt"))) {
    // đọc
}

Đừng quên xóa các tệp tạm

Files.deleteIfExists(tempFile);

Đừng đóng tài nguyên hai lần
Nếu bạn dùng try-with-resources, đừng gọi close() thủ công — điều này có thể dẫn tới lỗi và việc đóng lặp lại.

8. Các lỗi thường gặp khi làm việc với tệp

Lỗi số 1: Bỏ qua ngoại lệ.
Viết catch rỗng chẳng khác nào bắt ruồi bằng tay rồi thả chúng ra. Hãy luôn ghi log hoặc ít nhất thông báo cho người dùng biết điều gì đã xảy ra.

Lỗi số 2: Không đóng stream.
Nếu quên đóng stream, tệp có thể bị khóa và hệ thống có thể hết descriptor. Hãy dùng try-with-resources.

Lỗi số 3: Dùng đường dẫn tương đối cho tệp quan trọng.
Đừng trông mong thư mục làm việc luôn đúng như bạn nghĩ. Tốt hơn hãy chỉ rõ đường dẫn hoặc dùng các thư mục đặc biệt (user.home, java.io.tmpdir).

Lỗi số 4: Ghi đè tệp quan trọng mà không có bản sao dự phòng.
Trước khi xóa hay ghi đè thứ gì đó quan trọng, hãy tạo backup. Điều này sẽ cứu bạn và dữ liệu của người dùng.

Lỗi số 5: Không kiểm tra quyền truy cập.
Hãy kiểm tra rằng người dùng có quyền đọc/ghi các tệp hoặc thư mục cần thiết — nếu không bạn sẽ gặp AccessDeniedException bất ngờ.

Lỗi số 6: Cửa sổ TOCTOU.
Giữa kiểm tra và sử dụng tệp, tệp có thể bị người khác thay đổi hoặc xóa. Luôn xử lý ngoại lệ, ngay cả sau khi đã kiểm tra.

Lỗi số 7: Để lại tệp tạm và rác.
Sau các lần dừng đột ngột của chương trình hoặc lỗi, tệp tạm có thể còn sót lại. Đừng quên xóa chúng, đặc biệt nếu đó là dữ liệu nhạy cảm.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Lỗi khi làm việc với file
Lỗi khi làm việc với file
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION