Динамический поиск подключаемых модулей
Прежде всего, эта статья — дополнение к моей предыдущей статье о подключаемых модулях. Я рекомендую вам, прежде чем погрузиться в эту статью, ознакомиться с первой. Основная цель этой статьи — избавить пользователя от файлов конфигурации. Основная мысль — обеспечить, чтобы при загрузке ваше приложение могло просматривать .DLL-файлы своего каталога, находить те, которые содержат типы, поддерживающие интерфейс IPlugin, и создавать экземпляры этих подключаемых модулей. Никакого вмешательства пользователя, за исключением копирования .DLL-файла в каталог приложения, не должно быть.
Использование System.Reflection — путь к спасению
Одно из наиболее мощных пространств имен в Microsoft® .NET Framework — System.Reflection. Как следует из имени, оно позволяет коду «отбрасывать свою тень», раскрывая любые свойства, члены (как открытые, так и закрытые), методы, интерфейсы, цепочки интерфейсов — практически все, что вы хотели знать о Типе Х, но никогда не осмеливались спросить.
Используя это могущественное пространство имен, вы будете проходить по каждому файлу, обнаруживая все находящиеся в нем типы, и для каждого типа будете выяснять, поддерживает ли он интерфейс IPlugin. Класс, который вам надо использовать, чтобы извлечь все типы, входящие в .NET-сборку, называется System.Reflection.Assembly. Вот простой метод, используемый этим классом именно для того, что мы только что обсуждали:
private void TryLoadingPlugin(string path)
{
Assembly asm = AppDomain.CurrentDomain.Load(path);
foreach(Type t in asm.GetTypes())
{
foreach(Type iface in t.GetInterfaces())
{
if(iface.Equals(typeof(IPlugin)))
{
AddToGoodTypesCollection(t);
break;
}
}
}
}
Как видите, с помощью пространства имен System.Reflection очень просто извлечь большое количество информации о любом заданном файле сборки. В приведенном выше методе вы вызываете метод для GetInterfaces() для каждого Type, существующего в заданном файле. Затем вы проверяете, является ли какой-нибудь интерфейс этого типа интерфейсом IPlugin. Если да, это означает, что вы можете загружать его в ваше приложение; поместите его в список массивов (Array List) для хранения. Позже вы можете вернуться к этому списку массивов и использовать Activator.CreateInstance(Type) этих типов и, таким образом, создать экземпляр любого из обнаруженных вами подключаемых модулей.
Небольшая проблема
Этот код, безусловно, работоспособен и мог бы быть приемлемым, если бы не существовало одной маленькой проблемы. Чтобы объяснить ее суть, вам понадобится узнать о AppDomain. Я избавлю вас от собственных объяснений, что такое AppDomain, и приведу цитаты из документации по этому поводу:Домены приложений, которые представлены объектами AppDomain, обеспечивают изолированные, выгружаемые и безопасные границы для выполнения управляемого кода.В одном процессе могут выполняться несколько доменов приложений; однако не существует взаимно-однозначного соответствия между доменами приложений и потоками. Одному домену приложения могут принадлежать несколько потоков, и, пока данный поток не ограничен отдельным доменом приложения, в любой момент времени поток выполняется в одном домене приложения.Домены приложений создаются методом CreateDomain. Экземпляры AppDomain используются для загрузки и выполнения сборок. Если AppDomain больше не используется, он может быть выгружен.
Я бы добавил следующее: любая сборка, загруженная в приложении, по умолчанию загружается в AppDomain приложения. Само по себе это неплохо, если бы не тот факт, что вы не можете напрямую выгрузить сборку, если загрузили ее в AppDomain. Единственный способ выгрузить ее — выгрузить сам AppDomain.
Отсюда несколько следствий:
1. Любой .DLL-файл, проверяемый на наличие IPlugin, будет с момента проверки загружен в ваше приложение на все оставшееся время существования AppDomain.
2. Проверка множества .DLL-файлов может привести к серьезным перерасходам памяти для приложения.
Итак, теперь вы столкнулись с проблемой, как пройти по всем файлам каталога, загрузить сборки, но при этом иметь возможность выгрузить их. Решение намного проще, чем вы могли бы ожидать:
1. Вы создадите новый AppDomain и загрузите все проверяемые в данный момент сборки в этот AppDomain.
2. Завершив проверку и обнаружив только те типы, экземпляры которых могут быть созданы, вы выгрузите отдельный AppDomain.
3. Затем вы загрузите «хорошие» типы в ваш AppDomain, таким образом, вы избавите себя от мусора в памяти вашего приложения.
Создать новый AppDomain просто:
AppDomain domain = AppDomain.CreateDomain("PluginLoader");
PluginFinder finder = (PluginFinder)domain.CreateInstanceFromAndUnwrap(
Application.ExecutablePath,"Royo.PluggableApp.PluginFinder");
ArrayList FoundPluginTypes = finder.SearchPath(Environment.CurrentDirectory);
AppDomain.Unload(domain);
4. Вы создаете новый экземпляр объекта AppDomain, используя статический метод AppDomain. Вы передаете в него удобное для пользователя имя этого нового AppDomain.
5. Вы создаете экземпляр класса PluginFinder (в котором есть метод SearchPath()) в AppDomain. Для этого вы передаете в него (очень похоже на использование Activator) имя сборки, в которой находится класс, и полное имя класса, экземпляр которого надо создать.
6. В результате последней операции вы получаете Proxy, который выглядит и ведет себя так же, как ваш класс PluginLoader, но на самом деле является посредником между AppDomain вашего приложения и только что созданным вами новым AppDomain. Из вышесказанного вы знаете, что с этого момента любые загружаемые PluginLoader сборки будут на самом деле загружаться в ваш новый AppDomain, а не в AppDomain вашего приложения. Это означает, что после того, как этот класс выполнит свою работу, вы сможете выгрузить новый AppDomain, избавляясь, таким образом, от засорения памяти.
7. Вы вызываете метод SearchPath() в Proxy вашего реального класса PluginLoader находящегося в другом AppDomain. Назад вы получаете список массивов, содержащий только типы, использующие интерфейс IPlugin.
8. Вы выгружаете другой AppDomain, поскольку он вам больше не нужен.
9. Теперь вы можете двигаться дальше и создавать экземпляры подключаемых модулей, как описано в моей предыдущей статье («Создание подключаемого модуля»), используя класс Activator.
Важно!
Т.к. вы используете Proxy при сообщении между AppDomain, любой объект, экземпляр которого будет создан в этом прокси (в данном случае, PluginLoader), должен быть сериализуемым. Вы должны или унаследовать PluginLoader от MarshalByRefObject, или применить атрибут [Serializable] к этому классу. В противном случае, вы получите исключение:"Additional information: The type Royo.PluggableApp.PluginFinder in Assembly PluggableApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null is not marked as serializable."(«Дополнительная информация: тип Royo.PluggableApp.PluginFinder в сборке PluggableApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null не отмечен как сериализуемый.»)