1. Quyền truy cập trong HĐH
Khi bạn làm việc với tệp và thư mục, cần nhớ rằng hệ điều hành (HĐH) bảo vệ chúng bằng hệ thống quyền truy cập. Điều này có nghĩa là không phải chương trình nào (và không phải người dùng nào) cũng có thể đọc, sửa hoặc xóa mọi tệp.
POSIX (Linux, macOS, v.v.)
Trong các hệ thống POSIX (Unix, Linux, macOS), mỗi tệp và thư mục có quyền cho ba nhóm:
- Chủ sở hữu (user)
- Nhóm (group)
- Người khác (others)
Với mỗi nhóm, có ba loại quyền:
- r — read (đọc)
- w — write (ghi)
- x — execute (thực thi)
Ví dụ:
-rw-r--r--
Nghĩa là: chủ sở hữu có thể đọc và ghi, những người còn lại — chỉ được đọc.
Windows
Trên Windows, quyền truy cập được thiết lập qua hệ thống ACL (Access Control List) — danh sách quyền cho người dùng và nhóm. Ở đây có thể cấu hình linh hoạt ai có thể làm gì với tệp hoặc thư mục (đọc, ghi, sửa, chạy, v.v.).
Quan trọng:
Chương trình Java làm việc với tệp trong phạm vi quyền của người dùng mà nó đang chạy. Nếu người dùng không có quyền với tệp — chương trình cũng sẽ không thể đọc hoặc sửa tệp đó.
2. Ngoại lệ AccessDeniedException và các nguyên nhân
Khi bạn làm việc với tệp trong Java (đặc biệt qua NIO API), bạn có thể gặp ngoại lệ:
java.nio.file.AccessDeniedException
Ngoại lệ này được ném ra nếu chương trình của bạn không có quyền để thực hiện thao tác với tệp hoặc thư mục.
Các nguyên nhân chính:
- Không có quyền đọc tệp (ví dụ, tệp bị bảo vệ khỏi đọc).
- Không có quyền ghi vào tệp hoặc thư mục (ví dụ, đang cố ghi vào thư mục hệ thống).
- Không có quyền thực thi tệp (liên quan đến việc chạy chương trình).
- Thư mục hoặc tệp được bảo vệ khỏi thay đổi (ví dụ, chỉ đọc).
- Tệp hoặc thư mục đang bị tiến trình khác chiếm dụng (đặc biệt hay gặp trên Windows).
Ví dụ:
Path path = Paths.get("/etc/shadow"); // tệp hệ thống của Linux
Files.readAllLines(path); // AccessDeniedException!
Nên làm gì?
- Kiểm tra quyền truy cập với tệp/thư mục.
- Chạy chương trình dưới tài khoản người dùng có quyền phù hợp.
- Đừng cố ghi vào các thư mục hệ thống nếu không thật sự cần.
3. Kiểm tra quyền trong Java: các phương thức Files.isReadable(), isWritable(), isExecutable()
Java cung cấp các phương thức tiện dụng để kiểm tra quyền trên tệp hoặc thư mục:
Path path = Paths.get("example.txt");
System.out.println(Files.isReadable(path)); // true nếu có thể đọc
System.out.println(Files.isWritable(path)); // true nếu có thể ghi
System.out.println(Files.isExecutable(path)); // true nếu có thể chạy
Những phương thức này cho biết hệ thống nhìn nhận quyền của bạn tại thời điểm hiện tại.
Nhưng!
Chúng không bảo đảm rằng thao tác thực sự sẽ thành công. Lý do có thể khác nhau: tệp có thể bị khóa bởi chương trình khác, quyền có thể thay đổi sau khi kiểm tra, trên ổ đĩa mạng kết quả phụ thuộc vào máy chủ, và đôi khi HĐH báo một đằng nhưng áp dụng giới hạn khác.
Vì thế trong Java, tốt hơn là vừa kiểm tra quyền, vừa bao bọc thao tác đọc/ghi thực bằng try-catch — đáng tin cậy hơn.
Vấn đề TOCTOU (Time Of Check To Time Of Use)
Điều này liên quan trực tiếp đến tình huống TOCTOU, khi giữa lúc kiểm tra quyền và lúc thực thi thao tác có sự thay đổi. Ví dụ:
- Bạn kiểm tra rằng tệp có thể ghi (isWritable).
- Ngay lúc đó, tiến trình hoặc người dùng khác thay đổi quyền — giờ tệp đã được bảo vệ.
- Bạn cố ghi dữ liệu — nhận AccessDeniedException.
Kết luận:
Kiểm tra quyền chỉ đưa ra gợi ý về trạng thái hiện tại, chứ không bảo đảm thao tác thành công. Luôn xử lý ngoại lệ khi làm việc với tệp.
4. Nguyên tắc “ghi an toàn” (atomic write)
Tại sao cần cách ghi an toàn (nguyên tử)?
Đôi khi trong lúc ghi tệp có thể xảy ra sự cố: chương trình bị sập, mất điện, hết dung lượng đĩa... Kết quả là tệp có thể bị hỏng hoặc chỉ ghi một phần. Điều này đặc biệt nguy hiểm với dữ liệu quan trọng (ví dụ, cấu hình, cơ sở dữ liệu, tài liệu).
Ghi an toàn là cách đảm bảo rằng tệp hoặc được cập nhật hoàn toàn, hoặc vẫn giữ nguyên. Cách tiếp cận này gọi là ghi nguyên tử (atomic write).
Cách hiện thực ghi an toàn trong Java?
Mẫu:
- Ghi dữ liệu vào tệp tạm (thường trong cùng thư mục).
- Nếu ghi thành công — di chuyển nguyên tử tệp tạm sang vị trí tệp chính (thay thế nó).
Vì sao điều này hoạt động?
Thao tác di chuyển tệp (rename/move) trong cùng một hệ thống tệp thường là nguyên tử: hoặc tệp được thay thế hoàn toàn, hoặc không. Nếu có sự cố — tệp chính không bị đụng đến.
Ví dụ mã: ghi an toàn tệp
import java.nio.file.*;
public class SafeWriteDemo {
public static void safeWrite(Path target, byte[] data) throws Exception {
// 1. Tạo tệp tạm trong cùng thư mục
Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");
try {
// 2. Ghi dữ liệu vào tệp tạm
Files.write(tempFile, data);
// 3. Di chuyển nguyên tử tệp tạm sang vị trí tệp chính
Files.move(
tempFile,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} finally {
// Nếu có sự cố — xóa tệp tạm
Files.deleteIfExists(tempFile);
}
}
public static void main(String[] args) throws Exception {
Path file = Paths.get("important.txt");
byte[] content = "Du lieu rat quan trong".getBytes();
safeWrite(file, content);
System.out.println("Tep duoc ghi an toan!");
}
}
Lưu ý:
- Dùng Files.createTempFile() để tạo tệp tạm.
- Khi di chuyển, dùng tùy chọn ATOMIC_MOVE — điều này đảm bảo tính nguyên tử (nếu HĐH và hệ thống tệp hỗ trợ).
- Nếu có sự cố — tệp tạm được xóa (Files.deleteIfExists).
Khi nào điều này đặc biệt quan trọng?
- Khi làm việc với tệp cấu hình, cơ sở dữ liệu, nhật ký.
- Nếu tệp có thể được tiến trình khác đọc ở bất cứ thời điểm nào.
- Nếu lỗi khi ghi có thể dẫn đến mất mát hoặc hỏng dữ liệu.
5. Ghi log và xử lý lỗi truy cập
Xử lý lỗi truy cập đúng cách như thế nào?
Khi làm việc với tệp, luôn sử dụng xử lý ngoại lệ (try-catch). Điều này cho phép:
- Thông báo đúng cách cho người dùng về vấn đề (ví dụ, “Không có quyền ghi vào thư mục”).
- Ghi lỗi vào log để phân tích sau.
- Không “làm sập” cả chương trình chỉ vì một thao tác thất bại.
Ví dụ: xử lý AccessDeniedException
import java.nio.file.*;
public class FileAccessDemo {
public static void main(String[] args) {
Path file = Paths.get("/etc/shadow"); // ví dụ cho Linux
try {
Files.readAllLines(file);
} catch (AccessDeniedException ade) {
System.err.println("Loi truy cap: khong co quyen doc tep " + file);
// Co the ghi vao log hoac de xuat nguoi dung chon tep khac
} catch (Exception e) {
System.err.println("Loi khac: " + e.getMessage());
}
}
}
Ghi log lỗi
Trong các ứng dụng thực tế, hãy sử dụng hệ thống ghi log (ví dụ, java.util.logging, Log4j, SLF4J). Điều này cho phép:
- Ghi lỗi với chi tiết (stack trace, thời gian, người dùng).
- Phân tích log để tìm và khắc phục vấn đề.
- Không hiển thị các thông báo “đáng sợ” cho người dùng, chỉ đưa chúng vào log.
Ví dụ với ghi log:
import java.nio.file.*;
import java.util.logging.*;
public class FileLoggerDemo {
private static final Logger logger = Logger.getLogger(FileLoggerDemo.class.getName());
public static void main(String[] args) {
Path file = Paths.get("data.txt");
try {
Files.readAllLines(file);
} catch (AccessDeniedException ade) {
logger.severe("Khong co quyen truy cap tep: " + file);
} catch (Exception e) {
logger.log(Level.SEVERE, "Loi khi lam viec voi tep", e);
}
}
}
6. Lỗi thường gặp
Lỗi số 1: Bỏ qua ngoại lệ khi làm việc với tệp.
Đừng bao giờ chỉ gọi Files.write(path, data) mà không có try-catch — nếu có sự cố, chương trình sẽ sập.
Lỗi số 2: Chỉ kiểm tra quyền mà không xử lý TOCTOU.
Đừng chỉ dựa vào Files.isWritable() và các phương thức tương tự. Dù chúng trả về “có thể”, thao tác vẫn có thể thất bại. Luôn xử lý ngoại lệ (ví dụ, AccessDeniedException).
Lỗi số 3: Ghi “đè” lên tệp hiện có mà không có bản sao dự phòng.
Nếu tệp quan trọng — hãy sao lưu trước khi ghi hoặc dùng ghi nguyên tử với StandardCopyOption.ATOMIC_MOVE.
Lỗi số 4: Không xóa tệp tạm sau khi có sự cố.
Nếu trong quá trình ghi nguyên tử có sự cố — tệp tạm có thể còn lại. Hãy dùng finally và Files.deleteIfExists().
Lỗi số 5: Không ghi log lỗi truy cập.
Nếu chương trình không thể ghi hoặc đọc tệp — người dùng cần biết, và bạn cần xem chi tiết trong log. Hãy dùng java.util.logging/SLF4J và ghi lại ngoại lệ kèm stack trace.
GO TO FULL VERSION