среда, 23 апреля 2008 г.

Пишем плагинную шину с использованием Guice


Сейчас очень популярны так называемые расширяемые приложения, ведь согласитесь - это очень удобно, когда пользователь может изменить возможности Вашего приложения не влезая в его ядро (что впрочем не отменяет пользу опен-сорца). Но когда первый раз создаешь приложение, расширяемое с помощью плагинов возникает множество проблем и вопросов как при проектировании, так и при реализации с использованием той или иной технологии.

Сегодня мы поговорим о реализации примитивной плагинной шины с помощью IoC-контейнера от гугл - Guice. Реализация плагинной шины с помощью IoC-контейнера дает нам все преимущества паттерна Inversion Of Control. Мы легко можем реализовывать с использованием этого паттерна как базовые какие-то механизмы (API приложения, доступное плагинам), так и сами плагины, что очень удобно. Так-же использование Guice облегчает сам процесс подключения плагинов к системе, как именно - об этом поговорим чуть позже.


Начем с того, что такое вообще плагин, и какая особенность создания плагинов в приложениях, написаных на Java. Впрочем думаю, что ни для кого не секрет, что плагин - модуль приложения, который разрабатывается отдельно от основной программы, компилируется тоже отдельно. Взаимодействие плагина с программой строго ограничено набором определенных классов и интерфейсов (в терминах Java). Очень часто такой набор называют API. В самом примитивном случае - приложение выставляет некий набор интерфейсов, которые должны быть реализованы плагином. При реализации этих интерфейсов плагин может и должен использовать некий набор классов, предоставляемых приложением (например классы работы с БД, классы работы с GUI и т.д.). По реализации плагинной архитектуры на Java есть замечательная статья, пересказывать которую не имеет смысла, я лишь коротко опишу основные проблемы, которые необходимо решить при создании плагинной шины:

1. Загрузка классов из jar-файлов, не входящих в classpath-приложения. Дело в том, что зачастую плагины устанавливаются просто копированием в некий каталог, например plugins, при этом наше приложение не должно знать до своего запуска, какие там лежат плагины и в каких файлах. Эта информация должна быть доступна только в рантайме. Собственно дальше я расскажу как написать такой загрузчик и оформить его в виде guice-провайдера.

2. Разделение областей видимости классов плагина и классов основного прииложения. Плагин пишет сторонний разработчик и понятно, что хитрость человеческая безмерна. Поэтому плагин не должен влиять на приложение ни при каких условиях. Взаимодействие плагина и приложения ограничено рамками API, предоставляемого приложением плагину. В рамках Java данная проблема решается применением отдельных загрузчиков классов для общедоступного API, основного приложения и плагинов.

3. Разделение областей видимости между плагинами. В общем случае плагины пишутся разными людьми и поэтому возможно использование одинаковых имен для классов и даже пакетов. Решается данная проблема использованием для каждого плагина своего загрузчика классов.

С проблемами определились, теперь попробуем разобраться на примере как их решать, используя Guice для построения расширяемой плагинной шины на базе IoC. Рекомендую сразу набраться терпения, потому что проблемы весьма нетривиальны.

Сначала определимся с API для работы с плагинами. Как я уже писал ранее в основе Guice лежит понятия модуля, а что такое плагин, если не модуль. Соответственно каждый наш плагин должен реализовывать некий интерфейс (для примера возьмем очень простой интерфейс с одним методом invoke) и предоставлять свой контекст вызывающему приложению. Контекст плагина это фактически и есть модуль Guice, который описывает зависимости в плагине. Такой подход позволяет очень упростить как сам код плагина (вводя патерн IoC в плагин), так и взаимодействие плагина и приложения.

Итак, код интерфейса, который должен реализовывать плагин очень прост:
package ru.caffeineim.api.plugins;



public interface IPlugin { 

    public void invoke();

}

 


Вообще давайте сразу определимся с тем, что значит "плагин должен реализовывать интерфейс". Любой плагин должен иметь так называемый основной класс. Именно этот класс будет отвечать со стороны плагина за взаимодействие с вызывающим приложением. Так вот, этот основной класс должен реализовывать интерфейс IPlugin, через который будет осуществлятся взаимодействие приложение - плагин.

Контекст плагина тоже не должен вызывать сложностей при реализации. Это просто интерфейс с одним методов, возвращающим модуль (в терминах Guice). Данный модуль будет использоваться для инъекции зависимостей, а значит для корректного построения плагина. Код контекста плагина:
package ru.caffeineim.api.plugins;



import com.google.inject.Module;



public interface IPluginContext {

    public Module getModule()

}

 


Таким образом мы отделили использование плагина (IPlugin) и построение плагина (IPluginContext).

Теперь разберемся как раз таки с самым главным - загрузкой классов плагина. Чем хорош Guice - он позволяет создавать свои фабрики классов (в терминах Guice - провайдеры), которые будут использоваться для разрешения зависимостей (т.е. для создания классов, реализующих интерфейсы, описанные как зависимости). Напишем свой загрузчик классов плагинов и оформим его как Guice-провайдер. Приведу код провайдера:

package ru.caffeineim.guice.plugincontainer;



import java.io.File;

import java.io.FileFilter;

import java.net.URL;

import java.net.URLClassLoader;

import java.util.ArrayList;

import java.util.List;

import java.util.jar.JarFile;

import java.util.jar.Manifest;



import ru.caffeineim.api.plugins.IPluginContext;

import ru.caffeineim.core.manifest.IManifestParser;

import ru.caffeineim.core.manifest.ManifestParser;

import ru.caffeineim.core.manifest.Manifestor;



import com.google.inject.Inject;

import com.google.inject.Provider;

import com.google.inject.name.Named;



public class PluginsProvider<T extends List<IPluginContext>> implements Provider<T> {



    private static final String JAR = ".jar";

   

    private String folder;

   

    @Inject

    public void injectFolder(@Named("folder") String folder) {

        this.folder = folder;

    }

   

    @SuppressWarnings("unchecked")

    public T get() {

        List<IPluginContext> contexts = new ArrayList<IPluginContext>();

       

        File[] jars = getJarsFromFolder();

       

        for (int i = 0; i < jars.length; i++) {

            contexts.add(getContextInstance(folder + "/" + jars[i].getName()));

        }      

       

        return (T) contexts;

    }

       

    private File[] getJarsFromFolder() {

        File pluginFolder = new File(folder);   

   

        return pluginFolder.listFiles(new FileFilter() {

            public boolean accept(File file) {

                return file.isFile() && file.getName().endsWith(JAR);

            }

        });

    }

   

    private IPluginContext getContextInstance(String jar) {

        try {

            Manifestor mf = getManifestor(new JarFile(jar).getManifest());

           

            URL jarURL = (new File(jar)).toURI().toURL();

            URLClassLoader classLoader = new URLClassLoader(new URL[]{jarURL});                                                    

            Class<?> clazz = classLoader.loadClass(mf.getMainclass());

           

            return (IPluginContext) clazz.newInstance();

        }

        catch (Exception e) {

            e.printStackTrace();

            return null;

        }

    }

   

    private Manifestor getManifestor(Manifest mf) {

        IManifestParser parser = new ManifestParser();

        parser.parse(mf);

        return parser.getManifest();

    }

   

    public static <T extends List<IPluginContext>> Provider<T> pluginsProvider() {

        return new PluginsProvider<T>();

    }

}

 


Наш PluginsProvider реализует интерфейс Provider, переопределяя его метод
public T get()


Прошу обратить, что класс параметризуется списком интерфейсов IPluginContext. Это очень важная особенность - мы пишем провайдер, который загружает список классов - контекстов плагинов (классов, реализующих интерфейс IPluginContext).

Рассмотрим как работает PluginProvider. Есть каталог (который мы инъектируем в PluginProvider как строковую константу), PluginProvider получает список jar-файлов (с помощью метода private File[] getJarsFromFolder()), лежащих в этом каталоге. Каждый Jar-файл считается плагином. С помощью метода private Manifestor getManifestor(Manifest mf) загружаем манифест jar-файла (реализация класса Manifestor и собственно парсера манифестов - ManifestParser - приведена ниже). Метод private IPluginContext getContextInstance(String jar) загружает контекст плагина из Jar-файла. Имя этого класса вместе с остальной информации хранится в манифесте jar-файла.

Собственно больше по работе провайдера сказать нечего, кроме того что статический метод public static <T extends List<IPluginContext>> Provider<T> pluginsProvider() нужен просто для облегчения процесса создания провайдера.

Код класса Manifestor:
package ru.caffeineim.core.manifest;



public class Manifestor {

    private String version;

    private String author;

    private String plugin;

    private String mainclass;

           

    public String getVersion() {

        return version;

    }

   

    public void setVersion(String version) {

        this.version = version;

    }

   

    public String getAuthor() {

        return author;

    }

   

    public void setAuthor(String author) {

        this.author = author;

    }

   

    public String getPlugin() {

        return plugin;

    }

   

    public void setPlugin(String plugin) {

        this.plugin = plugin;

    }

   

    public String getMainclass() {

        return mainclass;

    }

   

    public void setMainclass(String mainclass) {

        this.mainclass = mainclass;

    }   

}

 


Код интерфейса IManifestParser:

package ru.caffeineim.core.manifest;



import java.util.jar.Manifest;



public interface IManifestParser {

    public static final String MANIFEST_VERSION = "Manifest-Version";

    public static final String CREATED_BY = "Created-By";

    public static final String EXTENSION_NAME = "Extension-Name";

    public static final String MAIN_CLASS = "Main-Class";

   

    public void parse(Manifest manifest);

    public Manifestor getManifest();

}

 


Код класса ManifestParser:
package ru.caffeineim.core.manifest;



import java.util.jar.Attributes;

import java.util.jar.Manifest;



public class ManifestParser implements IManifestParser {

   

    private Manifestor manifestor = new Manifestor();

   

    public Manifestor getManifest() {

        return manifestor;

    }



    public void parse(Manifest manifest) {

        Attributes mainAttrs = manifest.getMainAttributes();

        manifestor.setAuthor(mainAttrs.getValue(IManifestParser.CREATED_BY));

        manifestor.setMainclass(mainAttrs.getValue(IManifestParser.MAIN_CLASS));

        manifestor.setPlugin(mainAttrs.getValue(IManifestParser.EXTENSION_NAME));

        manifestor.setVersion(mainAttrs.getValue(IManifestParser.MANIFEST_VERSION));

    }

}


Все, с загрузкой контекстов плагинов закончили. Теперь осталось только разобраться как все это работает вместе - загрузчик контекста плагина, контекст, ну и сам плагин.

Алгоритм работы в данном случае предельно прост. Получаем список контекстов плагинов (это реализует как раз таки наш провайдер). Далее, используя контексты, строим сами плагины (напомню, что плагин это просто модуль Guice, а контекст как раз возвращает этот модуль). Ну и затем вызываем метод invoke() для каждого плагина. Как видим все просто.

Итак, создадим интерфейс IPluginContainerDemo с помощью которого будем демонстрировать работу нашей плагинной шины. Как видим код данного интерфейса тривиален:
package ru.caffeineim.guice.plugincontainer;



public interface IPluginContainerDemo {

    public void run();

}

 


Весь алгоритм работы с плагинами, описаный выше укладывается в реализацию метода run() данного интерфейса. Рассмотрим класс PluginContainerDemo, демонстрирующий данный алгоритм:
package ru.caffeineim.guice.plugincontainer;



import java.util.List;



import com.google.inject.Guice;

import com.google.inject.Inject;

import com.google.inject.Injector;



import ru.caffeineim.api.plugins.IPlugin;

import ru.caffeineim.api.plugins.IPluginContext;



public class PluginContainerDemo implements IPluginContainerDemo {



    private final List<IPluginContext> contexts;



    @Inject

    public PluginContainerDemo(List<IPluginContext> contexts) {

        this.contexts = contexts;

    }

   

    public void run() {

        for (IPluginContext context : contexts) {

            Injector pluginjector = Guice.createInjector(context.getModule());

            IPlugin plugin = pluginjector.getInstance(IPlugin.class);                                          

            plugin.invoke();

        }

    }

}

 


Собственно метод run() не сложен - в нем мы обходим список загруженых контекстов плагинов, получаем из каждого контекста модуль, с помощью Guice'вского Injector'а разрешаем зависимости в данном модуле, далее получаем из модуля реализацию интерфейса IPlugin. Ну и вызываем метод invoke(). В этот класс также осуществляется инъекция списка плагинов (через конструктор).

Все - плагинная шина написана. Осталась самая малость - разрешить зависимости в самой плагинной шине, т.е. описать ее саму в виде Guice-модуля. Напомню что зависимости у нас такие: название каталога, в котором лежат плагины, список контекстов плагинов и реализация интерфейса IPluginContainerDemo. Описываются данные зависимости в классе PluginContainerModule:

package ru.caffeineim.guice.plugincontainer;



import java.util.List;



import ru.caffeineim.api.plugins.IPluginContext;



import com.google.inject.AbstractModule;

import com.google.inject.Scopes;

import com.google.inject.TypeLiteral;

import com.google.inject.name.Names;



public class PluginContainerModule extends AbstractModule {

   

    public static final String PLUGINS_FOLDER = "plugins";

   

    @Override

    protected void configure() {

        bindConstant()

            .annotatedWith(Names.named("folder"))

            .to(PLUGINS_FOLDER);

       

        bind(new TypeLiteral<List<IPluginContext>>(){})

            .toProvider(PluginsProvider.<List<IPluginContext>>pluginsProvider());

       

        bind(IPluginContainerDemo.class)

            .to(PluginContainerDemo.class)

            .in(Scopes.SINGLETON);

    }

}

 


Прошу обратить внимание на такую особенность Guice. Если вам надо инстанцировать некий параметр не объектом класса, а списком - необходимо использовать конструкцию вида:
bind(new TypeLiteral<List<IPluginContext>>(){})

            .toProvider(PluginsProvider.<List<IPluginContext>>pluginsProvider());


Все, теперь можно точно так же через Injector загружать плагинную шину и демонстрировать работу приложения:

package ru.caffeineim.guice.plugincontainer;



import com.google.inject.Guice;

import com.google.inject.Injector;



public class Main {

    public static void main(String[] args) {

        Injector injector = Guice.createInjector(new PluginContainerModule());

        IPluginContainerDemo demo = injector.getInstance(IPluginContainerDemo.class);                              

   

        demo.run();

    }

}


Я отдаю себе отчет в том, что данная плагинная шина весьма примитивна, но она демонстрирует такие моменты, как собственно создание некоторого контейнера для работы с плагинами и то, как использование паттерна IoC облегчает создание этого контейнера. Основной идеей является то, что каждый плагин выставляет реализацию некоторых интерфейсов, и свой контекст - файтически модуль Guice. В этом модуле плагин описывает свою внутреннюю реализацию, которая может быть очень сложной, но вызывающему приложению она абсолютно неинтересна. Построит эту реализацию за нас Guice.

Хочу сделать небольшое замечание. В приведенной реализации PluginContainerDemo не разделены во времени процессы загрузки плагинов и их вызова, однако данная возможность поддерживается даже нашей примитивной шиной.

Буду рад любым вашим вопросам, замечаниям и конструктивной критике. Ведь самое главное - как можно полнее понять суть вопроса. Безбажных проектов вам.

Понравилось сообщение - подпишись на блог

Комментариев нет:

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!