1. OS におけるアクセス権
ファイルやフォルダを扱うときは、オペレーティングシステム(OS)がアクセス権の仕組みでそれらを保護していることを忘れてはいけません。つまり、どのプログラム(またはどのユーザー)でも任意のファイルを読み取り・変更・削除できるわけではありません。
POSIX(Linux、macOS など)
POSIX(Unix、Linux、macOS)では、各ファイルやフォルダに対し、次の3つの区分ごとに権限が設定されています。
- 所有者(user)
- グループ(group)
- その他(others)
各区分に対して3種類の権限が設定されます:
- r — read(読み取り)
- w — write(書き込み)
- x — execute(実行)
例:
-rw-r--r--
これは、所有者は読み書きでき、その他は読み取りのみという意味です。
Windows
Windows では、アクセス権は ACL(Access Control List)という仕組みにより設定されます。ユーザーやグループごとに、ファイルやフォルダで何ができるか(読み取り・書き込み・変更・実行など)を柔軟に制御できます。
重要:
Java プログラムは、起動したユーザーの権限の範囲内でファイルにアクセスします。ユーザーにそのファイルへの権限がなければ、プログラムも読み取りや変更はできません。
2. AccessDeniedException とその原因
Java(とくに NIO API)でファイル操作を行うと、次の例外に遭遇することがあります:
java.nio.file.AccessDeniedException
この例外は、プログラムにそのファイルまたはディレクトリでの操作を行うための権限がない場合に送出されます。
主な原因:
- 読み取り権限がない(例: ファイルが読み取りから保護されている)。
- 書き込み権限がファイルまたはフォルダにない(例: システムフォルダに書き込もうとしている)。
- 実行権限がない(プログラムの起動に関係)。
- フォルダやファイルが変更不可(読み取り専用)に設定されている。
- ファイルやフォルダが別のプロセスにより占有されている(特に Windows で発生しやすい)。
例:
Path path = Paths.get("/etc/shadow"); // Linux のシステムファイル
Files.readAllLines(path); // AccessDeniedException!
どうすればよいか?
- ファイル/フォルダのアクセス権を確認する。
- 必要な権限を持つユーザーとしてプログラムを実行する。
- 不要にシステムディレクトリに書き込もうとしない。
3. Java における権限の確認: メソッド Files.isReadable(), isWritable(), isExecutable()
Java にはファイルやフォルダの権限を確認する便利なメソッドがあります:
Path path = Paths.get("example.txt");
System.out.println(Files.isReadable(path)); // 読み取り可能なら true
System.out.println(Files.isWritable(path)); // 書き込み可能なら true
System.out.println(Files.isExecutable(path)); // 実行可能なら true
これらのメソッドは、現時点で OS があなたの権限をどのように認識しているかを示します。
ただし!
これらは保証にはなりません。理由はいくつかあります。ファイルが別のプログラムによりロックされているかもしれない、確認後に権限が変更されたかもしれない、ネットワークドライブでは結果がサーバーに依存する、OS が一つの情報を返しつつ別の制限を適用する場合がある、などです。
そのため、Java ではまず権限を確認しつつ、実際の読み取り/書き込み操作は try-catch に包んで実行するのがより確実です。
TOCTOU(Time Of Check To Time Of Use)の問題
権限の確認と実際の使用のあいだに状況が変わる TOCTOU に直接関係します。たとえば:
- isWritable でファイルが書き込み可能であると確認した。
- その間に別のプロセスやユーザーが権限を変更し、ファイルが保護された。
- データを書き込もうとすると、AccessDeniedException が発生する。
結論:
権限チェックは現状のヒントに過ぎず、成功を保証しません。ファイル操作では常に例外を処理しましょう。
4. 「安全な書き込み」(atomic write)の原則
なぜ安全(アトミック)な書き込みが必要か?
ファイルへの書き込み中に障害が起こることがあります。プログラムが落ちる、停電、ディスク容量不足…。結果としてファイルが破損したり、一部しか書き込まれない場合があります。これは重要なデータ(設定、データベース、ドキュメントなど)では特に危険です。
安全な書き込みとは、ファイルが完全に更新されるか、元の状態のまま残ることを保証する方法です。このアプローチはアトミック書き込み(atomic write)とも呼ばれます。
Java で安全な書き込みをどう実装する?
パターン:
- データを一時ファイルに書き込む(通常は同じフォルダ)。
- 書き込みが成功したら、アトミックに移動して一時ファイルで本体を置き換える。
なぜ有効なのか?
同一ファイルシステム内でのファイルのリネーム/移動(rename/move)は通常アトミックです。つまり、完全に置き換わるか、置き換わらないかのどちらかです。問題が起きても元のファイルは手つかずのままです。
コード例: 安全なファイル書き込み
import java.nio.file.*;
public class SafeWriteDemo {
public static void safeWrite(Path target, byte[] data) throws Exception {
// 1. 同じフォルダに一時ファイルを作成する
Path tempFile = Files.createTempFile(target.getParent(), "tmp_", ".tmp");
try {
// 2. 一時ファイルにデータを書き込む
Files.write(tempFile, data);
// 3. 一時ファイルを本来のファイルにアトミックに移動して置き換える
Files.move(
tempFile,
target,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
);
} finally {
// 失敗した場合は一時ファイルを削除する
Files.deleteIfExists(tempFile);
}
}
public static void main(String[] args) throws Exception {
Path file = Paths.get("important.txt");
byte[] content = "とても重要なデータ".getBytes();
safeWrite(file, content);
System.out.println("ファイルは安全に書き込まれました!");
}
}
注意:
- Files.createTempFile() を使って一時ファイルを作成します。
- 移動には ATOMIC_MOVE オプションを使用します。OS とファイルシステムが対応していればアトミック性を保証します。
- 問題が発生した場合は一時ファイルを削除します(Files.deleteIfExists)。
どんなときに特に重要か?
- 設定ファイル、データベース、ログを扱うとき。
- ファイルがいつでも別プロセスに読み取られる可能性があるとき。
- 書き込み失敗がデータ損失や破損につながる可能性があるとき。
5. ログ記録とアクセスエラーの処理
アクセスエラーを正しく処理するには?
ファイル操作では常に try-catch による例外処理を行いましょう。これにより次のことが可能になります。
- ユーザーに問題を適切に知らせる(例: 「フォルダへの書き込み権限がありません」)。
- 後で分析できるようエラーをログに記録する。
- 1 回の失敗でアプリケーション全体を落とさないようにする。
例: AccessDeniedException の処理
import java.nio.file.*;
public class FileAccessDemo {
public static void main(String[] args) {
Path file = Paths.get("/etc/shadow"); // Linux の例
try {
Files.readAllLines(file);
} catch (AccessDeniedException ade) {
System.err.println("アクセスエラー: ファイルを読み取る権限がありません: " + file);
// ログに記録する、またはユーザーに別のファイルを選ばせることができます
} catch (Exception e) {
System.err.println("その他のエラー: " + e.getMessage());
}
}
}
エラーのログ記録
実際のアプリケーションでは、java.util.logging、Log4j、SLF4J などのロギングを使用します。これにより次のことができます。
- 詳細(スタックトレース、時刻、ユーザーなど)とともにエラーを記録する。
- ログを分析して問題の発見と解決に役立てる。
- ユーザーに不親切なメッセージを表示せず、詳細はログのみに出力する。
ロギングの例:
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("ファイルにアクセスできません: " + file);
} catch (Exception e) {
logger.log(Level.SEVERE, "ファイル操作中のエラー", e);
}
}
}
6. よくある誤り
誤り1: ファイル操作時に例外を無視する。
Files.write(path, data) を try-catch なしで書かないでください。問題が起きるとプログラムが落ちます。
誤り2: TOCTOU を考慮せずに権限チェックだけに頼る。
Files.isWritable() などの結果だけに頼らないでください。「可能」と返っても、操作が失敗することがあります。常に例外(たとえば AccessDeniedException)を処理しましょう。
誤り3: 既存ファイルをバックアップなしで上書きする。
重要なファイルであれば、書き込み前にバックアップを作成するか、StandardCopyOption.ATOMIC_MOVE を用いたアトミック書き込みを使いましょう。
誤り4: 障害後に一時ファイルを削除しない。
アトミック書き込みで問題が発生した場合、一時ファイルが残ることがあります。finally と Files.deleteIfExists() を使用してください。
誤り5: アクセスエラーをログに記録しない。
プログラムがファイルの書き込みや読み取りに失敗した場合、ユーザーにはその旨を知らせ、開発側はログで詳細を確認できるようにすべきです。java.util.logging/SLF4J を使用し、スタックトレース付きで記録しましょう。
GO TO FULL VERSION