たきるブログ

C#やOracleなどの情報を書いています。

【Java】tomcatを再起動しないでjarファイルからクラスをロードしたかった

システムとしては毎度クラスをロードするような形になってしまうのだろうから、実はあまり良くないのかもし れない。

でもやりたかった

だってさ、PHPとかJSPならファイル書き換えるだけで済むんだよ?
JSPってダメだったっけか?w でもHTMLとかCSSとかあれとかこれとか・・・)
FTPでアップするだけだぜ?
だったらJavaで書いたってそれを維持したいと思うのは当然だよな。

ってことでやってみた。
クラスローダーのことは全然深く知らないから、作法としてこれがいいものなのか
どうかが全く分からない。
というか、基本的にJava詳しくないから全く分からない。
けどやってみた。

構成はこんな感じでやってみた。もちろんEclipse上で作成したお話。

・Baseプロジェクト >動的Webプロジェクト。warファイルになるキモ。
  └Index.java

・PluginBaseプロジェクト >Javaプロジェクト。プラグイン用抽象クラスを持つjarファイル。
  └AbstractPlugin.java

・TestProjectプロジェクト >Javaプロジェクト。プラグイン用抽象クラスを利用した、プラグインjarファイル。
  ├META-INF
  │ └MANIFEST.MF
  └TestClass.java

まずはAbstractPlugin.java
あれこれ試してたら結果的にインターフェースでも良くなったんだけど、いずれインターフェースと抽象クラスが出来るだろうということから、とりあえず作る作る!

public abstract class AbstractPlugin {
    public abstract void dummyMethod();
}

PluginBaseプロジェクトはこれで完了。PluginBase.jarファイルを作っておしまい!

んでBaseプロジェクトのIndex.java
これにてこずった!!!!!まじで。
さっき作ったPluginBase.jarをビルドパスに入れる。
まあ、テスト的にはプロジェクト参照で追加してもいいけど、その時はwarファイルを作った時にPluginBase.jarが入らないっぽい。

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.apache.struts2.ServletActionContext;
import AbstractPlugin;

public class Index {
    private ArrayList plugins;
    private AbstractPlugin selectedPlugin;

    /**
     * 実行メソッド
     * @return
     */
    public String execute() {
        plugins = getPlugins();
        // デフォルトプラグインの準備
        if (plugins == null) {
            System.out.println("実行させるプラグインがなかった");
        } else if (plugins.size() > 0) {
            selectedPlugin = plugins.get(0);
            selectedPlugin.dummyMethod();
        }

        return "SUCCESS";
    }

    /**
    * プラグインクラスのインスタンスをArrayListにまとめて返す
    * @return プラグインリスト
    */
    public ArrayList getPlugins() {
        final String PLUGINS_DIRECTORY = "WEB-INF/lib";

        //WEB-INF/libを対象にする
        String cpath = ServletActionContext.getServletContext().
                getRealPath(File.separator + PLUGINS_DIRECTORY);

        try {
            plugins = new ArrayList();

            //ディレクトリ内にある全ファイルが対象
            File f = new File(cpath);
            String[] files = f.list();
            for (int i = 0; i < files.length; i++) {
                //jarファイルだけが対象
                if (files[i].endsWith(".jar")) {
                    //jarファイルからマニフェストを読み込んで、Plugin-Classに記されてる
                    //クラス名を取得
                    File file = new File(cpath + File.separator + files[i]);
                    JarFile jar = new JarFile(file);
                    Manifest mf = jar.getManifest();
                    Attributes att = mf.getMainAttributes();
                    String cname = att.getValue("Plugin-Class");

                    //Plugin-Classが指定されてるものだけが対象
                    if (cname != null) {
                        //jarファイルを、AbstractPluginがロードされてるクラスローダーに突っ込んで、
                        //クラスをロードする。
                        URL url = file.getCanonicalFile().toURI().toURL();
                        URLClassLoader loader = new URLClassLoader(
                                new URL[] { url }, AbstractPlugin.class.getClassLoader());
                        Class cls = loader.loadClass(cname);

                        //継承してるクラスがAbstractPluginの場合のみプラグインとして認識させる
                        Class sClsObj = cls.getSuperclass();
                        if (sClsObj.equals(AbstractPlugin.class)) {
                            AbstractPlugin plugin =
                                (AbstractPlugin)cls.newInstance();
                            plugins.add(plugin);
                        }
                    }
                }
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return plugins;
    }
}

struts2の機構をそのまま利用して作ったから、本質的にいらないものもある。
URLClassLoaderは、AbstractPluginクラスがロードされたローダーを親としないと、各プラグインクラスはロードできても、AbstractPluginクラスは継承してないよってことになるらしい。
execute()メソッドが、サーブレットではdoGet()になればいいと思う。
で、これでBase.warを作成。

最後にTestProjectプロジェクト内のTestClass.javaを作成。
これももちろん、PluginBase.jarにビルドバスを通す。

import AbstractPlugin;

public class TestClass extends AbstractPlugin {
    @Override
    public void dummyMethod() {
        // TODO 自動生成されたメソッド・スタブ
        System.out.println("ダミーメソッドが実行されました。");
    }

}

で、TestProjectプロジェクトのMANIFEST.MFにAbstractPluginクラスを継承してるクラス名を記述する。(パッケージ名から)

Manifest-Version: 1.0
Plugin-Class: TestClass
Class-Path: ./PluginBase.jar

これでTestProject.jarを作成!

で、tomcatのwebapps内に、Base.warを突っ込んで、配置させる。
この時、Base.warにPluginBase.jarが含まれていないなら、PluginBase.jarをlib内に突っ込んであげて、tomcatを再起動。

この状態が、プラグインの雛形ありますよ、その雛形を使ってプラグインを読み込みますよって状態だから、これが揃わないと

んなクラスねーよ!

しらねえええよ!

って言われる。

んで、TestProject.jarをlibの中に突っ込んで

Tomcatを再起動しない

そうすると、tomcatのログのstdout_yyyymmdd.log内に、

ダミーメソッドが実行されました。
って出てる!!

大大尾大おおおおおおおおおおおおおおおおお
きたああああああああああああああああああああああああああああああってなった。

lib内以外に存在するjarファイルからクラスをロードできるのかどうかは試してない。
本当はさ、lib/plugins/とかってディレクトリ内にプラグインのjarファイルを突っ込むような形を望んでたんだけど、今思い出した。

それ、試してない。

けど、出来るんじゃね?
結局tomcatの起動でlib内とかlib/ext内をロードしてるわけじゃないし、コード上はディレクトリを指定して、利用されてるローダー内でロードさせてるんだし。

とりあえずここまで出来たらかなり満足してしまったwww
これが出来たからっていって何かちゃちゃっとアプリが作れちゃうわけでもないんだけどね^p^