Windows上のJavaでファイルを上書きリネームする

Javaでファイルの移動を行う File#renameTo メソッドは移動先にファイルが存在する場合にどういった動作をするかについて規定されておらず、プラットフォーム依存になっている。

そのため、UNIX系OSでは概ね他の言語のrename と同じように「ファイルが存在したら上書きする」という動作になるのに、Windowsでは「ファイルが存在すると移動失敗」になる、というクロスプラットフォームが聞いてあきれる状態になっており、しかもJavaでは他にファイルの「移動」ができる手段はなく、Javaの開発元にも対応する意思はない*1

こういう場合は、あきらめてさっさとネイティブコードを使ってしまえばいい。

Javaでネイティブコードの呼び出しが面倒だったのは昔の話で、今はJNA(Java Native Access)という素晴らしいライブラリがあり、.NETのP/Invoke並に簡単にネイディブコードを呼び出すことができる。

ということで、JNAを使い、Windows上でKernel32.dll の MoveFileEx 関数を呼び出して「上書きrename」するコード(エラーメッセージ表示付き)を書いてみる。

jarファイルのダウンロード

最新版の jna.jar と platform.jar をJNAのページからダウンロードしてクラスパス上に置く。

Mavenを使う場合は、pom.xml の中に以下を追加。

  <repositories>
    <repository>
      <id>Java.net Maven2 repository</id>
      <url>http://download.java.net/maven/2/</url>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>jna</artifactId>
      <version>3.2.7</version>
    </dependency>

    <dependency>
      <groupId>net.java.dev.jna</groupId>
      <artifactId>platform</artifactId>
      <version>3.2.7</version>
    </dependency>
  </dependencies>

ただし、platform.jar はリポジトリにないので、JNAのページから手動でダウンロードした上で、コマンドプロンプトから platform.jar を置いたフォルダに移動し、以下のようなコマンドでインストールする必要がある*2

mvn install:install-file ^
  -DgroupId=net.java.dev.jna -DartifactId=platform ^
  -DgeneratePom=true -DcreateChecksum=true -Dpackaging=jar ^
  -Dfile=platform.jar -Dversion=3.2.7

Javaソースを書く

package example;

import static example.MoveFile.Kernel32.*;

import com.sun.jna.Native;
import com.sun.jna.platform.win32.Kernel32Util;
import com.sun.jna.win32.StdCallLibrary;
import com.sun.jna.win32.W32APIOptions;

public class MoveFile {
    // 必要な関数を定義した interface を作成
    public interface Kernel32 extends StdCallLibrary {
        // 定数は Platform SDK などに入っているヘッダを参照
        // pinvoke.net に載っている場合もある
        //   http://www.pinvoke.net/default.aspx/Enums/MoveFileFlags.html
        int MOVEFILE_REPLACE_EXISTING           = 0x00000001;
        int MOVEFILE_COPY_ALLOWED               = 0x00000002;
        int MOVEFILE_WRITE_THROUGH              = 0x00000008;

        boolean MoveFileEx(
                String lpExistingFileName, String lpNewFileName, int dwFlags);
    }

    public static void main(String[] args) {
        // - kernel32.dll をロード
        // - interface 中で定数として定義してもいい
        // - UNICODE_OPTIONS を指定すると、以下のような動作になる
        //    - Unicode版の関数 "MoveFileExW" が選択される
        //    - JavaのStringが、WCHAR に変換される
        Kernel32 kernel32 = (Kernel32) Native.loadLibrary(
                    "kernel32", Kernel32.class, W32APIOptions.UNICODE_OPTIONS);

        if (kernel32.MoveFileEx(args[0], args[1],
                    MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED | MOVEFILE_WRITE_THROUGH)) {
            System.out.println("OK");
        } else {
            System.err.println(
                    // GetLastError() のエラーコードに対応するメッセージを取得
                    Kernel32Util.formatMessageFromLastErrorCode(Native.getLastError()));
        }
    }
}

これだけ。

テスト

> echo 1 > D:\tmp\1.txt
> echo 2 > D:\tmp\2.txt

> dir /b D:\tmp
1.txt
2.txt

> mvn -q exec:java -Dexec.mainClass=example.MoveFile -Dexec.args="D:\tmp\1.txt D:\tmp\2.txt"
OK

> dir /b D:\tmp
2.txt

> mvn -q exec:java -Dexec.mainClass=example.MoveFile -Dexec.args="D:\tmp\1.txt D:\tmp\2.txt"
指定されたファイルが見つかりません。

*1:追記:JDK7ではできるようになるらしい http://download.java.net/jdk7/docs/api/java/nio/file/Files.html

*2:-Dversion= で指定するバージョンはダウンロードしたものに合わせること