ClassLoader格闘24時間

 現在Struts上で開発しているシステムにプラグイン拡張機構を作ろうとして、ClassLoaderを使おうとしたのだが、これが非常に苦労したので、開発メモとして保存しておく。
 
 Struts上でプラグイン機構を実現するために、まずStrutsのPlugin機能を使う。Sturts上のPlugin機能は、Struts上で用意されているPlugInインターフェースに従うことにより、Struts起動時に、自動ロードしてくれる機能である。この機能を用いてシステム上にプラグイン管理クラスをロードする。
 


public class PluginManagerFactory implements PlugIn

 
 これにより管理クラスがロードされ、中でPluginのインスタンスが生成される。あとはstaticメソッドでこれらのインスタンスを返すメソッドを用意すれば、外部からPluginへのアクセスが可能となる。
 
 内部ではJarで登録されたプラグインをClassLoaderがロードしていく機能を実装しているのだが、ここで泥沼にはまることになる。プラグインを登録している部分は以下のような実装をしている。
 

if(pluginDir==null) return;
// プラグインリストの初期化
pluginList=new HashMap();
class OutputPluginFileFilter implements FileFilter{
public boolean accept(File arg0) {
if(arg0.isFile() && arg0.getName().endsWith("jar")) return true;
else return false;
}
}
// プラグインタイプを設定
this.pluginManagedType=pluginType;
// Jarファイルの一覧を取得する
File pathName=new File(pluginDir);
File pluginFileList=pathName.listFiles(new OutputPluginFileFilter());
for(int i=0;i try{
JarFile jar=new JarFile(pluginFileList[i]);
Manifest manifest=jar.getManifest();
Attributes attributes=manifest.getMainAttributes();
if(attributes==null){
continue;
}
String className=attributes.getValue("Plugin-Class-Name");
URL
url=new URL[1];
url[0]=pluginFileList[i].toURL();
PluginLoader loader=new PluginLoader(url,SuperPluginInterface.class.getClassLoader());
SuperPluginInterface spi=(SuperPluginInterface)loader.loadClass(className).newInstance();
if(spi.getPluginId()==null || spi.getVersion()==null){
// プラグインID・バージョンがnullならばプラグインの登録をスキップする
continue;
}
// プラグインタイプの判別
if(spi.getPluginType().equals(this.pluginManagedType)){
setPlugin(spi);
}

 
 このプログラムの示す一連の流れはこのようになっている。

  1. 指定されたディレクトリ以下のjarという拡張子を持つファイルを検索(ディレクトリが存在しなければ、そのままリターン)
  2. URLClassLoaderを継承したPluginLoaderのインスタンスを作成して、jarをロードする。
  3. Jarの中にあるManifestファイルをロードして、中にある項目からPlugin-Class-Nameを取り出す。
  4. Plugin-Class-Nameに対応したオブジェクトをloadClassでロードして、SuperPluginInterfaceにキャストする。
  5. キャストしたインターフェースからプラグイン情報を取り出し、異常が無ければプラグインリストに登録する。

 
 まずJarの注意点としては、MANIFESTファイルに書いた情報は、getAttributesで取るのではなく、Manifest.getMainAttributes()メソッドを取らなければならない。余談だが、この問題を調べるに当たってGoogleで「Manifest」だけで調べると、「民主党」のページが大量に出てきた。
 またJar内で定義された独自のクラスは全てJar内にパッケージングしなければならない。

 ここまでは、他のサイトでも時々解説されているのだが、Tomcat・Struts上でClassLoaderを使うとき注意すべき点がある。ClassLoaderを引数なしでインスタンスを生成すると、デフォルトのクラスローダを用いてObjectが生成される。
 
 ここで、SuperPluginInterface型はTomcat上でロードされたものなので、クラスローダはCatarinaのものが使われる。対して引数なしで宣言するとJavaVMが用意したデフォルトのクラスローダを親とするため、別の空間でObjectが生成される。このため、SuperPluginInterfaceに従っているはずのObjectが同系列のオブジェクトとみなされなくなり、ClassCastExceptionをスローしてしまう。
 
 そこで、クラスローダの引数にSuperPluginInterfaceがロードされているクラスローダを渡すことにより、この問題が解決する。


PluginLoader loader=new PluginLoader(url,SuperPluginInterface.class.getClassLoader());

 
 これで、ClassCastExceptionの問題は解決し、無事にクラスがロードされる。