Программирование arrow Microsoft .NET arrow Динамический поиск подключаемых модулей

Динамический поиск подключаемых модулей

Расширяет инфраструктуру для добавления поддержки подключаемых модулей в ваши .NET-приложения, чтобы вы также могли осуществлять динамический поиск подключаемых модулей в собственном каталоге приложения.
Прежде всего, эта статья — дополнение к моей предыдущей статье о подключаемых модулях. Я рекомендую вам, прежде чем погрузиться в эту статью, ознакомиться с первой. Основная цель этой статьи — избавить пользователя от файлов конфигурации. Основная мысль — обеспечить, чтобы при загрузке ваше приложение могло просматривать .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 не отмечен как сериализуемый.»)

Полезный инструмент отладки

При работе с AppDomain и отладке исключений, которые могут возникнуть при их загрузке и выгрузке, может возникнуть огромное количество ошибок. Полезный инструмент, который практически не задокументирован — fuslogvw.exe или Fusion Log Viewer. Fusion — это имя подсистемы загрузки. Вы можете использовать этот инструмент для регистрации сбоев. Если вы получаете ошибки во время загрузки сборок, обновите представление этого инструмента и получите протокол исключительных ситуаций.
 
« Предыдущая статья   Следующая статья »


  • .NET Reflection, Создание подключаемой инфраструктуры
    Люди, как правило, добавляют поддержку подключаемых модулей в свои приложения по следующим причинам:
    - Чтобы расширить функциональные возможности приложения без необходимости перекомпиляции или повторного распространения его среди заказчиков.
    - Чтобы добавить функциональные возможности без необходимости доступа к оригинальному исходному коду.
    - Бизнес-правила для приложения меняются часто или часто добавляются новые правила....
  • .NET Reflection, Прикладное применение рефлексии в .NET
    NET Reflection представляет собой классический пример некоторой низкоуровневой библиотеки, которая может быть использована при решении прикладных задач. Что же это такое?Рефлексия (ударение на последнем "и", синоним слова интроспекция), или, по-английски, reflection - система, предоставляющая выполняемому коду информацию о нем самом. Звучит немного запутанно, и, как всегда, намного проще понять суть на примере....
  • .NET Reflection, Применение рефлексии для создания плагинов
    Плагины стали неотъемлемой частью больших коммерческих приложений. С их помощью можно наращивать функциональность приложений без повторной компиляции или быстро изменять бизнес-правила, на основе которых работает приложение. Кроме того, для разработки плагинов не нужно иметь доступа к исходному коду приложения, поэтому они могут разрабатываться сторонними организациями. В .NET написание плагинов является простой задачей, которая решается с помощью рефлексии (reflection). Рефлексия позволяет дина...