DLLの動的読み込み

折角、Seasar Conferenceに来たのだから、Javaのセッションも1つぐらいは・・・と、ひがさんのを。Hot Deployの実現方法がリクエスト毎に新しいクラスローダを作成して云々(良く分かってない)というのを聞いて、.NETだとリクエスト毎にAppDomainを作成するようなものかな?とか考えたり。ってことで、サンプルコード。
動的に読み込むクラス用のインタフェース。たとえば、BaseLibrary.dllとかで作成。

namespace BaseLibrary
{
    public interface IGreeting
    {
        string Hello (string name);
    }
}

その実装。ClassLibrary1.dll。

using System;
using BaseLibrary;

namespace ClassLibrary1
{
    public class Greeting : MarshalByRefObject, IGreeting
    {
        public string Hello (string name)
        {
            return String.Format ("Hello {0}!", name);
        }
    }
}

で、DLLを呼び出すプログラム。

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using BaseLibrary;

namespace ConsoleSample
{
    class Program
    {
        private const string asm = "ClassLibrary1";
        private const string dll = asm + ".dll";
        private const string cls = "ClassLibrary1.Greeting";
        static readonly object syncObj = new object ();
        private static readonly List<AppDomain> old = new List<AppDomain> ();
        private static AppDomain current;

        static IGreeting GetComponent()
        {
            lock(syncObj)
            {
                if (current == null)
                    return null;
                return current.CreateInstanceAndUnwrap (asm, cls) as IGreeting;
            }
        }

        static void SetupAppDomain()
        {
            lock (syncObj)
            {
                if (File.Exists (dll))
                {
                    AppDomainSetup setup = new AppDomainSetup ();
                    setup.ApplicationBase = AppDomain.CurrentDomain.SetupInformation.ApplicationBase;
                    // DLLは自由に削除したいのでシャドーコピーを使う
                    setup.ShadowCopyFiles = "true";
                    // 古いAppDomainはどこかでUnloadさせる(今回は手抜き)
                    if (current != null)
                        old.Add (current);
                    current = AppDomain.CreateDomain ("AD#1", null, setup);
                }
            }
        }

        static void Main (string[] args)
        {
            // DLLを監視
            FileSystemWatcher fsw = new FileSystemWatcher (AppDomain.CurrentDomain.SetupInformation.ApplicationBase);
            fsw.Filter = "*.dll";
            fsw.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime;
            fsw.Changed += Changed;
            fsw.Created += Changed;
            fsw.EnableRaisingEvents = true;

            while (true)
            {
                IGreeting greeting = GetComponent ();
                if (greeting != null)
                    Console.WriteLine (greeting.Hello ("太郎"));
                else
                    Console.WriteLine ("need " + dll + ".");
                Thread.Sleep (1000);
            }
        }

        static void Changed (object sender,FileSystemEventArgs e)
        {
            SetupAppDomain ();
        }
    }
}

実行すると、

need ClassLibrary1.
need ClassLibrary1.
...

と表示され、実行ディレクトリにClassLibrary1.dllをコピーすると、

Hello 太郎!
Hello 太郎!
...

となります。シャドーコピーを使っているので、プログラムを実行したままでもファイルはロックされず、DLLは削除可能です。ここで、ClassLibrary1.dllを変更して、「Hello」を「こんにちは」に変えてビルドし、DLLを上書きすると、

こんにちは 太郎!
こんにちは 太郎!
...

となります。実行中のプログラムに変更が即座に反映されるところが面白いですね。

ポイントはシャドーコピーを使っていることと、ファイルシステムを監視してDLLが変更されるたびにAppDomainを作成してDLLを読み込んでいるところ。ぶっちゃけASP.NETがやっていることの超手抜き版。Seasar.NETのようなDIコンテナの場合、DLLだけでなく設定ファイルも監視すれば良いですね。