File.deleteOnExit() を使用していたライブラリ
トラブルは得てして古い環境で起こる。以下のエントリ:
File.createTempFile() と File.deleteOnExit() のコンボ罠の続き - HHeLiBeXの日記 正道編
を書いたのもそんなトラブルがきっかけだが、原因の1つはアプリケーションそのものではない‥いや、(古いライブラリを使っているという点では)ある意味アプリケーションが原因。
何かというと、Commons FileUpload v1.0 に含まれる問題。v1.1 で解決されているのだが、そんな古いライブラリを使っているんです、はい‥
再現させるのは、作業的にはすごく簡単。(ただマシンへの負荷がかかるだけ)
準備するものは次のとおり。
- Sun製の J2SDK v1.4.2 または JDK v5.0
- 今回は v1.4.2_18 を使用
- 使用する Java VM 上で稼動するWebアプリケーションサーバー
- Commons FileUpload v1.0 と v1.1
- 後述するWebアプリ
検証のためのWebアプリは次のとおり。
まずCommons FileUploadを使用する部分のコード。ファイル名は「FileUploadTest10.java」としておく。
import java.util.Iterator; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.apache.commons.fileupload.DiskFileUpload; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileUploadException; public class FileUploadTest10 { public static void parse(HttpServletRequest request) throws FileUploadException { DiskFileUpload fileUpload = new DiskFileUpload(); List items = fileUpload.parseRequest(request); for (Iterator it = items.iterator(); it.hasNext();) { FileItem item = (FileItem) it.next(); if (item.isFormField()) { System.out.println(item.getFieldName() + ":" + item.isFormField() + "=" + item.getString()); } } } }
これを呼び出すコードを含むJSPファイル。ファイル名は「action.jsp」としておく。
<%@ page contentType="text/html" pageEncoding="UTF-8" %> <%@ page session="false" %> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "DTD/xhtml11.dtd"> <% response.setHeader("pragma","no-cache"); response.setHeader("Cache-Control","no-cache"); %> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja"> <head> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8" /> <title>Commons FileUpload Test</title> </head> <body> <% FileUploadTest10.parse(request); %> <div> <a href="index.html">to index page</a> </div> </body> </html>
このaction.jspを呼び出すためのHTMLソース。ファイル名は「index.html」としておく。
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Commons FileUpload Test - index page</title> </head> <body> <form name="testForm" action="action.jsp" method="POST" enctype="multipart/form-data"> <input type="text" name="mytext" /> <input type="file" name="myfile" /> <input type="submit" /> </form> </body> </html>
最後に、「web.xml」。
<?xml version='1.0' encoding='UTF-8'?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd"> <web-app> <display-name>Commons FileUpload Test Application</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
で、このWebアプリをコンパイルし、アプリケーションサーバーにデプロイする。
その後、数十万回ほど「index.html」⇒「action.jsp」というアクセスを繰り返す。指定するファイルは何でもいい。また適切なPOSTパラメータを渡せるならば、「action.jsp」を直接呼び出してもよい。
実際、手作業で数十万回なんてやってられないので、JMeterを使用した。(詳しいやり方は省略)
今回は、Eclipse 3.5+Sysdeo Tomcat PluginでTomcatを起動。
30万回アクセスした場合の、Tomcatプロセスの使用メモリ量(起動直後は27MB程度)
- commons-fileupload-1.0.jar を使用
- ⇒356MB程度まで増加
- commons-fileupload-1.1.jar を使用 (Commons I/Oが別途必要)
- ⇒84MB程度で打ち止め(15万回辺りから一定)
ライブラリを置き換えられない場合は、最低限必要なコードを修正して置き換えるしかない。Commons FileUpload v1.0 のソースパッケージをダウンロードし、ソース「src/java/org/apache/commons/fileupload/DefaultFileItem.java」を見てみると、619〜621行目に次のようなコードがある。
File f = new File(tempDir, fileName); f.deleteOnExit(); return f;
この「f.deleteOnExit();」の行をコメントアウトしてコンパイルし、JARファイルを更新する。サーバーを長期間停止させない場合は「exit時に削除する」なんて意味がないので、大きな影響はない。同じソースの少し上を見るとわかるが、ファイルの削除処理がfinalize()メソッドの呼び出しに依存している。この点が少し気にはなるが、Java VMクラッシュ/OutOfMemoryErrorによるダウンに比べれば‥
ちなみに、Commons FileUpload v1.1 では、上記の「FileUploadTest10.java」で使用しているクラスは大部分が非推奨となっている。
v1.1 での推奨コードの例は次のとおり。
import java.util.Iterator; import java.util.List; import javax.servlet.http.HttpServletRequest; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.FileUpload; import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.servlet.ServletRequestContext; public class FileUploadTest11 { public static void parse(HttpServletRequest request) throws FileUploadException { FileItemFactory factory = new DiskFileItemFactory(); FileUpload fileUpload = new ServletFileUpload(factory); List items = fileUpload.parseRequest(new ServletRequestContext(request)); for (Iterator it = items.iterator(); it.hasNext();) { FileItem item = (FileItem) it.next(); if (item.isFormField()) { System.out.println(item.getFieldName() + ":" + item.isFormField() + "=" + item.getString()); } } } }