はじめに
私は子供のころから、「部屋を出るときは、入ったときより綺麗に片づけておきなさい」と教えられてきたので、このスタイルは私の作成するアプリケーションにも反映されています。しかし、先日携わったJavaプロジェクトはその正反対で、ユーザーのシステム上にテンポラリファイルを作成するだけして、片づけずに放置していました。このままリリースすれば、ユーザーのハードディスクを満杯にしてしまい、テクニカルサポートに苦情が殺到するのは目に見えていたので、この問題を解決する必要がありました。
問題のアプリケーションにおけるテンポラリファイルの扱いは、ごく単純なもので、他の多くのアプリケーションの場合と同様、実行中にユーザーのシステム上に作成しておき、終了時にこれらを処分するだけというものでした。テンポラリファイル(ごく短期間使用するだけのファイルで、通常は特定の一時ディレクトリに作成しておく)の扱い方についての要件は、プロジェクトごとに異なるのが普通です。アプリケーションによっては、長大なバッチ処理を行う際の中間処理に使用する場合もあれば、ネットワークや周辺装置からの入出力(I/O)バッファとして利用する場合もあるでしょう。私のケースで扱ったテンポラリファイルは、実行時に作成されてアプリケーションにダイナミックにロードされるJava Archive(JAR)でした(後になってわかったのですが、この点が問題をさらに難しくしていました)。プロジェクトの期日が近づいていたので、私は問題の原因を把握し、具体的な解決法を講じなければなりませんでした。
問題
Javaのよく知られた2つのバグにより、JVMデザインとWin32オペレーティングシステムを組み合わせて利用するJavaアプリケーションは、終了時にオープン状態になっているテンポラリファイルを削除することができません。そのため、ユーザーのシステム上にテンポラリファイルを放置することになります。
解決法
JVMが起動時に短時間のクリーンアップ処理を行って、アプリケーションを前回実行したときに残されたテンポラリファイルをすべて削除するようにします。ただし、テンポラリファイルは作成時にロック指定する必要があるので、アプリケーションの特定インスタンスに関するテンポラリファイルをすべて1つのディレクトリに入れて、そのディレクトリをロックするようにします。
ささいな原因が引き起こす大きな問題
Java Foundation Classes(JFC)には、アプリケーションからテンポラリファイルを作成および破棄するための機能が用意されています。下記のコードのようにjava.io.File
クラスを使用すると、アプリケーションからテンポラリファイルを作成したり、削除対象としてマークしたりできます。
File tempFile = File.createTempFile("myApp", ".tmp"); tempFile.deleteOnExit();
deleteOnExit()
メソッドは、Java Virtual Machine(JVM)の終了時にファイルを削除するために設計されたものです。すべてが理想どおりに進めば、これで必要な処理がすべて行われるはずでした。ところが私のアプリケーションでは、この行われるべき処理が実行されませんでした。ファイルは削除対象として指定されてはいましたが、実際には削除されませんでした。さらに複雑なことに、このアプリケーションをLinux上で実行した場合には、テンポラリファイルが削除されました。こうして私は問題の調査を始めました。
調査を進めた結果、ようやくこの問題の原因を発見しました。犯人は、Javaのよく知られた2つのバグでした。1つ目のバグは、バグ番号4171239「java.io.File.deleteOnExitは、オープン状態のファイルに対しては動作しません(win32)」です。つまり、JVMデザインとWin32オペレーティングシステムの組み合わせでは、オープン状態のファイルを終了時に削除できないのです。では、終了前にすべてのファイルを閉じておけばいい、ということでしょうか? そうはいきません。これはバグ番号4950148「ClassLoaderの使用にあたっては、明示的な廃棄処理を設ける必要があります」のバグに関係してきます。これはつまり、URLClassLoader
(JARファイルからクラスをロードする際に使用するクラス)は、JVMのライフタイム中はJARファイルへのストリームをオープンにし続けるということです。ここで行き詰まりました。Win32 JVMはオープン状態のファイルを削除できず、URLClassLoader
は常にファイルをオープン状態にしているのですから。先に進むには、何らかの迂回路を探す必要があります。
1つの単純な解決法としては、アプリケーションの作成するテンポラリファイルに共通の接頭辞を付けるようにしておき、特定のタイミングでこの接頭辞を持つすべてのファイルを削除する、という方式が考えられます。しかしこの方式には、同一アプリケーションの複数のインスタンスを同時実行すると競合状態が生じて、一方のインスタンスが使用しているテンポラリファイルを他方が削除してしまう危険性があるので、複数インスタンスを使用できなくなるという欠点があります。また、何か別のアプリケーションで同じ接頭辞を使用していると、この場合もテンポラリファイルが想定外のシナリオで削除される可能性が生じます。しかし、私が必要としているのは、堅牢性と再利用性に優れたソリューションであり、それを使えばどのプロジェクトでも適切なクリーンアップ処理を実現できるというものです。
デザイン:より適切なファイルトラップの設計
この問題を解決するには、テンポラリファイルマネージャを作成する必要がありました。ここでいうテンポラリファイルマネージャとは、テンポラリファイルを作成し、将来の何らかの時点でこれらを確実に廃棄するためのクラスです。そもそもの原因がJVMのバグなので、アプリケーションの次回実行時までテンポラリファイルが残留するとしても、それは容認することにしました。このように条件を少し緩めましたが、それでも、「アプリケーションの実行と実行の間にシステム上に残されるテンポラリファイルを1セットだけにする」ことを保証しなければなりません。
たいていの場合、最善のソリューションとは最も簡単なものです。JVMは、オープン状態のファイルでなければファイルを削除できるのですから、可能な限りこの機能を活用することにしました。また私は、JVMの初回起動時にはオープン状態のテンポラリファイルは存在しない、ということを知っていました。したがって、前回実行時から放置されているファイルが存在しても、これらを削除するクリーンアップ処理は即座に実行できるはずです。さらに、テンポラリファイルは作成時にロック指定される必要があるので、これと同じテンポラリファイル管理スキームを使う他のアプリケーションがあったとしても、使用中のファイルを削除しようとはしないはずです。私は、ロックファイルの数を減らすために、アプリケーションの特定インスタンスに属するすべてのテンポラリファイルを1つのディレクトリに入れて、このディレクトリごとロックするようにしました。
ソリューションの設計はこれで完了したので、次にこれをコード化する必要があります。
実装:問題部分の修正
シンプルさを確保するためには、JFCのFile
クラスに似たAPIが必要でした。そこで、テンポラリファイルの作成と既存ファイルの削除を扱わせるために、TempFileManager
というマネージャクラスを作成しました。
このクラスには、テンポラリファイルのクリーンアップを容易にするための静的メソッドが用意されています。この静的メソッドは、JVMが次回このクラスをロードしたときにクリーンアップされる特別なディレクトリ内にテンポラリファイルを作成します。
package com.devx.io; import java.io.*; import java.util.logging.Level; import java.util.logging.Logger; /** * Generates and properly cleans up temporary files. Similar to {@link * File#createTempFile(java.lang.String, java.lang.String)}, this class * provides a static method to create temporary files. The temporary files will * be created in a special directory to be cleaned up the next time this class * is loaded by the JVM. This functionality is required because Win32 platforms * will not allow the JVM to delete files that are open. This causes problems * with items such as JARs that get opened by a URLClassLoader and can * therefore not be deleted by the JVM (including deleteOnExit). * * The caller should not need to create an instance of this class, although it * is possible. Simply use the static methods to perform the required * operations. Note that all files created by this class should be * considered as deleted at JVM exit (although the actual deletion may be * delayed). If persistent temporary files are required, use {@link * java.io.File} instead. * * Refer to Sun bugs 4171239 and 4950148 for more details. */ public class TempFileManager { /** * Creates a temporary file in the proper directory to allow for cleanup * after execution. This method delegates to {@link * File#createTempFile(java.lang.String, java.lang.String, java.io.File)} so * refer to it for more documentation. Any file created using this method * should be considered as deleted at JVM exit; therefore, do not use this * method to create files that need to be persistent between application * runs. * * @param prefix the prefix string used in generating the file name; * must be at least three characters long * @param suffix the suffix string to be used in generating the file's * name; may be null, in which case the suffix ".tmp" will be used * @return an abstract pathname denoting a newly created empty * file * @throws IOException if a file could not be created */ public static File createTempFile(String prefix, String suffix) throws IOException { // Check to see if you have already initialized a temp directory // for this class. if (sTmpDir == null) { // Initialize your temp directory. You use the java temp directory // property, so you are sure to find the files on the next run. String tmpDirName = System.getProperty("java.io.tmpdir"); File tmpDir = File.createTempFile(TEMP_DIR_PREFIX, ".tmp", new File(tmpDirName)); // Delete the file if one was automatically created by the JVM. // You are going to use the name of the file as a directory name, // so you do not want the file laying around. tmpDir.delete(); // Create a lock before creating the directory so // there is no race condition with another application trying // to clean your temp dir. File lockFile = new File(tmpDirName, tmpDir.getName() + ".lck"); lockFile.createNewFile(); // Set the lock file to delete on exit so it is properly cleaned // by the JVM. This will allow the TempFileManager to clean // the overall temp directory next time. lockFile.deleteOnExit(); // Make a temp directory that you will use for all future requests. if (!tmpDir.mkdirs()) { throw new IOException("Unable to create temporary directory:" + tmpDir.getAbsolutePath()); } sTmpDir = tmpDir; } // Generate a temp file for the user in your temp directory // and return it. return File.createTempFile(prefix, suffix, sTmpDir); } /** * Utility method to load the TempFileManager at any time and allow it to * clean the temporary files that may be left from previous instances * * @param args command line arguments are currently not supported */ public static void main(String[] args) { // Although the JVM will load the class in order to // run the main method, this gives a little clarity to // what is happening and why we want the main method. try { // This will load the TempFileManager, which will // cause the static block to execute, cleaning // any old temp files. Class.forName(TempFileManager.class.getName()); } catch (ClassNotFoundException ex) { ex.printStackTrace(); } } /** * Deletes all of the files in the given directory, recursing into any sub * directories found. Also deletes the root directory. * * @param rootDir the root directory to be recursively deleted * @throws IOException if any file or directory could not be deleted */ private static void recursiveDelete(File rootDir) throws IOException { // Select all the files File[] files = rootDir.listFiles(); for (int i = 0; i < files.length; i++) { // If the file is a directory, we will // recursively call delete on it. if (files[i].isDirectory()) { recursiveDelete(rootDir); } else { // It is just a file so we are safe to // delete it if (!files[i].delete()) { throw new IOException("Could not delete: " + files[i].getAbsolutePath()); } } } // Finally, delete the root directory now // that all of the files in the directory have // been properly deleted. if (!rootDir.delete()) { throw new IOException("Could not delete: " + rootDir.getAbsolutePath()); } } /** * The prefix for the temp directory in the system temp directory */ private final static String TEMP_DIR_PREFIX = "tmp-mgr-"; /** * The temp directory to generate all files in */ private static File sTmpDir = null; /** * Static block used to clean up any old temp directories found -- the JVM * will run this block when a class loader loads the class. */ static { // Clean up any old temp directories by listing // all of the files, using a filter that will // return only directories that start with your // prefix. FileFilter tmpDirFilter = new FileFilter() { public boolean accept(File pathname) { return (pathname.isDirectory() && pathname.getName().startsWith(TEMP_DIR_PREFIX)); } }; // Get the system temp directory and filter the files. String tmpDirName = System.getProperty("java.io.tmpdir"); File tmpDir = new File(tmpDirName); File[] tmpFiles = tmpDir.listFiles(tmpDirFilter); // Find all the files that do not have a lock by // checking if the lock file exists. for (int i = 0; i < tmpFiles.length; i++) { File tmpFile = tmpFiles[i]; // Create a file to represent the lock and test. File lockFile = new File(tmpFile.getParent(), tmpFile.getName() + ".lck"); if (!lockFile.exists()) { // Delete the contents of the directory since // it is no longer locked. Logger.getLogger("default").log(Level.FINE, "TempFileManager::deleting old temp directory " + tmpFile); try { recursiveDelete(tmpFile); } catch (IOException ex) { // You log at a fine level since not being able to delete // the temp directory should not stop the application // from performing correctly. However, if the application // generates a lot of temp files, this could become // a disk space problem and the level should be raised. Logger.getLogger("default").log(Level.INFO, "TempFileManager::unable to delete " + tmpFile.getAbsolutePath()); // Print the exception. ByteArrayOutputStream ostream = new ByteArrayOutputStream(); ex.printStackTrace(new PrintStream(ostream)); Logger.getLogger("default").log(Level.FINE, ostream.toString()); } } } } }
リスト1の最初に出てくるcreateTempFile(String prefix, String suffix)
メソッドは、呼び出し元にとってはFile
クラスのメソッドと同様の動作をするだけのものですが、内部の処理は少し異なっています。このマネージャは最初に、初期化されているかどうかのチェックをします。初期化されていない場合、マネージャはシステムのテンポラリディレクトリに基づいてディレクトリ名を生成し、そのロックファイルを作成します。最後に、テンポラリディレクトリを作成します。こうした順序を踏むのは、ロックの処理前に、別のマネージャによってテンポラリディレクトリが削除されるような競合状態を回避するためです。ロックファイルは、終了時に削除対象として選ばれますが、JVMの中にはこれを参照しているものが存在しないので、これですべてうまく処理されるはずです。このメソッドは最後に、ユーザーが要請したテンポラリファイルを、新しいディレクトリに作成します。
TempFileManager
の次に重要なポイントは、静的な初期化ブロックです。このブロックは、クラスが初めてロードされる際にJVMによって実行されます。これはアプリケーションからのクラス要求に応じて、マネージャが即座に残留テンポラリファイルを削除できるようにするためのものです。このクリーンアップを実行するために、アプリケーションからマネージャに要求する必要はありません。
リスト1のstatic {}
ブロック内で、マネージャはテンポラリディレクトリのファイルリストを取得しますが、その際にファイルフィルタを用いて、このマネージャ用の接頭辞で始まるディレクトリだけを選択します。検出された個々のディレクトリについては、ロックファイルが存在するかどうかのチェックが実行されます。ロックファイルが存在しないディレクトリについては、ユーティリティがrecursiveDelete(File rootDir)
メソッドを用いて再帰的に削除します。このマネージャを実装した場合、1つのインスタンスが存在すれば、共通のマネージャ実装を使っている他のアプリケーションについても、それらのテンポラリファイルを削除します。
個々のアプリケーションからTempFileManager
を使用する手順はごく簡単です。前回の実行後に放置されているファイルをこのテンポラリファイルマネージャで確実に削除するには、このクラスをJVMに強制的にロードさせます。
Class.forName(TempFileManager.class.getName());
新しいテンポラリファイルを生成するとマネージャが自動的にロードされるので、これは必須のステップではありません。しかし、明示的にマネージャをロードしておくと、ドキュメンテーションの意味では役に立ちます。新しくテンポラリファイルを作成する手順は、JFC APIを呼び出す場合とほぼ同一です。
File myTmp = TempFileManager.createTempFile("foo", ".bar");
改良の成果
このソリューションを設計・実装したおかげで、すべてのjava.io.File#createTempFile(...)
呼び出しをTempFileManager#createTempFile(...)
呼び出しに置き換えるだけで、ファイル削除機構の問題を解決することができました。必要なコードの変更はこれだけであり、Sunのバグ修正を待つ必要もありませんでした。このソリューションは完璧なものではありませんが、ユーザーのシステム上にテンポラリファイルがあふれかえるような事態は回避できるはずです。
シェルスクリプトやバッチファイルから実行するアプリケーションでこのソリューションを利用するには、TempFileManager
の実装に単純なmain(...)
メソッドを用意して、アプリケーションのJVMの終了直後にそれを呼び出せばいいでしょう。これによりテンポラリファイルを速やかに削除すると同時に、必要なロックファイルを安全に保持することができます。
これは、ごくささいなバグであっても、プロジェクトのリリース前に解決できなかったら、厄介な問題を引き起こしただろう、という事例の1つです。きっとユーザーはあなたに感謝するでしょうし、あなた自身も、ハードディスクが満杯になったというクレームの電話が殺到することはないので安心して眠れるはずです。少なくとも、ここで紹介したアプリケーションではそんな問題は起こりませんから。