Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Плагинизация с применением Maven и Eclipse Aether

Anna | 2.06.2014 | нет комментариев
Когда финальными пользователями вашего продукта являются разработчики, способные не только предложить вам идеи для его совершенствования, но и горящие желанием различно поучаствовать в его растяжении, вы начинаете задумываться о том, как бы предоставить им сходственную вероятность. При этом вы не хотите давать полную волю, ровно как и доступ к репозиторию с начальным кодом. Как в этом случае дозволить сторонним разработчикам вносить метаморфозы исключая надобность метаморфозы исходников, компиляции и перевыкладки системы?

Предыстория

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

Варианты реализации

Для осуществления замышленного в голову приходит несколько вариантов.

  • Самый распространённый вариант потенциального растяжения функциональности системы — взаимодействие через API. Почаще каждого это интерфейс внешнего воздействия на систему, скажем, SOAP, REST API, и т.д. Данный подход достаточно лимитирован и нередко не разрешает своевременно среагировать на внутренние события системы.
  • Больше трудным, но и больше увлекательным является подход реализации внутреннего API, разрешающий получить отклик от процессов, протекающих в ядре. Если 1-й интерфейс имеет множество современных сервисов, то 2-й Зачастую реализуется через механизм обратной связи (скажем, Push-нотификации либо хуки). Но это не неизменно комфортно и надёжно, к тому же хочется обнаружить больше всеобщий подход к задаче, тогда как хуки отменны для решения определенных тесных задач.
  • Ещё одним методом растяжения системы «изнутри» является механизм плагинов (подключаемых модулей). Он разрешает получить отклик от системы непринужденно находясь внутри работающего процесса. Данный подход обширно знаменит и отлично зарекомендовал себя во множестве знаменитых приложений. К нему пришли и мы, когда нам потребовалось добавить механизм динамического растяжения функциональности основного приложения.

Наша система всецело построена на Java. Существует уйма хороших примеров расширяемых приложений, построенных на данной платформе. Одной из самых знаменитых библиотек, разрешающих сделать эластичную архитектуру Java-приложения является OSGi. Впрочем, применение данной библиотеки создаёт дополнительную трудность в реализации самого приложения, накладывает ряд ограничений, и добавляет огромное число нередко лишних вероятностей. К тому же, мы дюже энергично используем Maven-инфраструктуру для сборки и релиза сервисов, прогонки автотестов, построения отчётов и загрузки их в удалённое хранилище и т.д. Следственно было решено испробовать реализовать систему плагинов с применением Maven. Maven разрешает собрать артефакт, содержащий скомпилированную версию плагина, указать все зависимости, нужные для его работы, а также установить его в удалённый репозиторий, откуда он будет доступен для загрузки. Это дюже комфортно, когда плагин представляет собой небольшую часть функциональности, энергично использующую сторонние библиотеки в своей работе.

От слов к делу

Нам потребуется 2 артефакта: 1-й, содержащий интерфейс нашего плагина (Plugin API), и 2-й — содержащий реализацию этого интерфейса. Сотворим примитивный Maven-план для API:

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin -DartifactId=plugin-api

Сейчас добавим примитивный интерфейс плагина с исключительным способом принимающим 2 целочисленных довода и возвращающим целое число:

Спрятанный текст

package com.my.plugin;

public interface Plugin
{
  public Integer perform(Integer param1, Integer param2);
}

Необходимо установить сделанный артефакт в какой-либо репозиторий. Временно воспользуемся локальным репозиторием:

mvn clean install

Сейчас приступим к созданию реализации сделанного API:

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin -DartifactId=sum-plugin

Нам нужно добавить связанность от нашего API:

Спрятанный текст

  <dependency>
     <groupId>com.my.plugin</groupId>
     <artifactId>plugin-api</artifactId>
     <version>1.0-SNAPSHOT</version>
   </dependency>

И соответственно реализацию интерфейса Plugin:

Спрятанный текст

package com.my.plugin.impl;

public class SumPlugin implements Plugin
{
  @Override
  public Integer perform(Integer param1, Integer param2){
     return param1   param2;
  }
}

Дабы продемонстрировать реализацию расширяемого приложения сгенерируем ещё один план:

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin -DartifactId=app

Нам нужно прописать зависимости от Eclipse Aether. Для этого сперва добавим несколько свойств в pom.xml:

Спрятанный текст

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.compiler.version>1.6</project.compiler.version>
    <aetherVersion>0.9.0.M2</aetherVersion>
    <mavenVersion>3.1.0</mavenVersion>
    <wagonVersion>1.0</wagonVersion>
  </properties>

Сейчас пропишем сами зависимости:

Спрятанный текст

<dependency>
        <groupId>com.my.plugin</groupId>
        <artifactId>plugin-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <!-- COMMONS -->
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>

    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>

    <!-- AETHER -->
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-api</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-util</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-impl</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-connector-file</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-connector-asynchttpclient</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.eclipse.aether</groupId>
        <artifactId>aether-connector-wagon</artifactId>
        <version>${aetherVersion}</version>
    </dependency>
    <dependency>
        <groupId>io.tesla.maven</groupId>
        <artifactId>maven-aether-provider</artifactId>
        <version>${mavenVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.maven.wagon</groupId>
        <artifactId>wagon-ssh</artifactId>
        <version>${wagonVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.maven.wagon</groupId>
        <artifactId>wagon-file</artifactId>
        <version>${wagonVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.maven.wagon</groupId>
        <artifactId>wagon-http</artifactId>
        <version>${wagonVersion}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.maven.wagon</groupId>
        <artifactId>wagon-http-lightweight</artifactId>
        <version>${wagonVersion}</version>
    </dependency>

Сейчас нужно реализовать логику разрешения зависимостей. Eclipse Aether имеет довольно примитивный и внятный API. Дабы обнаружить все зависимости артефакта, нужно вызвать способ resolveDependencies для объекта типа RepositorySystem. Для начала нужно сделать инстанс этого класса, а перед этим также необходимо инстанциировать объект типа RepositorySystemSession. Тогда разрешение зависимостей дозволено будет осуществить дальнейшим образом:

Спрятанный текст

Dependency dependency = new Dependency(new DefaultArtifact("com.my.plugin:plugin-sum:jar:1.0-SNAPSHOT"), "compile");
CollectRequest collectRequest = new CollectRequest();
collectRequest.setRoot(dependency);

// this will collect the transitive dependencies of an artifact and build a dependency graph
DependencyNode node = repositorySystem.collectDependencies(repoSession, collectRequest).getRoot();
DependencyRequest dependencyRequest = new DependencyRequest();
dependencyRequest.setRoot(node);

// this will collect and resolve the transitive dependencies of an artifact
DependencyResult depRes = repositorySystem.resolveDependencies(repoSession, dependencyRequest);

List<ArtifactResult> result = depRes.getArtifactResults();

Дабы инициализировать объект типа RepositorySystem, нам потребуется реализация интерфейса WagonProvider, тот, что применяется для выкачивания артефактов из какого-либо источника, будь то http либо ftp репозиторий, либо, скажем, ssh. Простейшая реализация данного интерфейса может выглядеть дальнейшим образом:

Спрятанный текст

public class ManualWagonProvider implements WagonProvider {
    public Wagon lookup(String roleHint)
            throws Exception {
        if ("file".equals(roleHint)) {
            return new FileWagon();
        } else if (roleHint != null && roleHint.startsWith("http")) { // http and https
            return new HttpWagon();
        }
        return null;
    }

    public void release(Wagon wagon) {
        // no-op
    }

Таким образом, получим реализацию класса DependencyResolver, тот, что дозволит нам скачать и получить список всех зависимостей всякого артефакта из указанных репозиториев:

Спрятанный текст

public class DependencyResolver {

    public static final String MAVEN_CENTRAL_URL = "http://repo1.maven.org/maven2";
    public static class ResolveResult {
        public String classPath;
        public List<ArtifactResult> artifactResults;

        public ResolveResult(String classPath, List<ArtifactResult> artifactResults) {
            this.classPath = classPath;
            this.artifactResults = artifactResults;
        }
    }

    final RepositorySystemSession session;
    final RepositorySystem repositorySystem;
    final List<String> repositories = new ArrayList<String>();

    public DependencyResolver(File localRepoDir, String... repos) throws IOException {
        repositorySystem = newRepositorySystem();
        session = newSession(repositorySystem, localRepoDir);
        repositories.addAll(Arrays.asList(repos));
    }

    public synchronized ResolveResult resolve(String artifactCoords) throws Exception {
        Dependency dependency = new Dependency(new DefaultArtifact(artifactCoords), "compile");

        CollectRequest collectRequest = new CollectRequest();
        collectRequest.setRoot(dependency);

        for (int i = 0; i < repositories.size();   i) {
            final String repoUrl = repositories.get(i);
            collectRequest.addRepository(i > 0 ? repo(repoUrl, null, "default") : repo(repoUrl, "central", "default"));
        }

        DependencyNode node =repositorySystem.collectDependencies(session, collectRequest).getRoot();

        DependencyRequest dependencyRequest = new DependencyRequest();
        dependencyRequest.setRoot(node);

        DependencyResult res = repositorySystem.resolveDependencies(session, dependencyRequest);

        PreorderNodeListGenerator nlg = new PreorderNodeListGenerator();
        node.accept(nlg);
        return new ResolveResult(nlg.getClassPath(), res.getArtifactResults());
    }

    private RepositorySystemSession newSession(RepositorySystem system, File localRepoDir) throws IOException {
        DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();

        LocalRepository localRepo = new LocalRepository(localRepoDir.getAbsolutePath());
        session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));

        return session;
    }

    private RepositorySystem newRepositorySystem() {
        DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
        locator.setServices(WagonProvider.class, new ManualWagonProvider());
        locator.addService(RepositoryConnectorFactory.class, WagonRepositoryConnectorFactory.class);
        return locator.getService(RepositorySystem.class);
    }

    private RemoteRepository repo(String repoUrl) {
        return new RemoteRepository.Builder(null, null, repoUrl).build();
    }

    private RemoteRepository repo(String repoUrl, String repoName, String repoType) {
        return new RemoteRepository.Builder(repoName, repoType, repoUrl).build();
    }
}

Как дозволено подметить в приведённом выше коде, мы применяли сделанный ManualWagonProvider как сервис для объекта типа ServiceLocator, тот, что и инициализирует объект типа RepositorySystem. Сейчас дозволено применять данный класс для разрешения зависимостей всякого артефакта, находящегося в любом репозитории. Довольно инстанциировать его и передать ему путь к локальному репозиторию и к списку удалённых репозиториев для поиска (конечный довод необязателен).
Сейчас дозволено проверить работоспособность нашего кода дальнейшим примером.

Спрятанный текст

DependencyResolver resolver = new DependencyResolver(new File(System.getProperty("user.home")   "/.m2/repository"));
DependencyResolver.ResolveResult result = resolver.resolve("com.my.plugin:plugin-sum:jar:1.0-SNAPSHOT");

for (ArtifactResult res : result.artifactResults) {
     System.out.println(res.getArtifact().getFile().toURI().toURL());
}

Приведённый код соберёт все зависимости артефакта plugin-sum и выведет их на экран. Итогом его выполнения должно быть что-то как бы:

file:/home/smecsia/.m2/repository/com/my/plugin/plugin-sum/1.0-SNAPSHOT/plugin-sum-1.0-SNAPSHOT.jar
file:/home/smecsia/.m2/repository/com/my/plugin/plugin-api/1.0-SNAPSHOT/plugin-api-1.0-SNAPSHOT.jar

Сейчас мы получили пути к списку jar-файлов, которые нужны для работы реализации нашего плагина. Для создания экземпляра класса SumPlugin, нам необходимо подгрузить все обнаруженные артефакты с поддержкой отдельного загрузчика классов, родительским загрузчиком которого сделаем системный.

Спрятанный текст

List<URL> artifactUrls = new ArrayList<URL>();
for (ArtifactResult artRes : resolveResult.artifactResults) {
    artifactUrls.add(artRes.getArtifact().getFile().toURI().toURL());
}
final URLClassLoader urlClassLoader = new URLClassLoader(artifactUrls.toArray(new URL[artifactUrls.size()]), getSystemClassLoader());

С поддержкой отдельного загрузчика мы можем инициализировать новейший экземпляр загруженного класса:

Class<?> clazz = urlClassLoader.loadClass("com.my.plugin.SumPlugin");
final Plugin pluginInstance  = (Plugin) clazz.newInstance();
System.out.println("Result: "   pluginInstance.perform(2, 3));

Сейчас мы можем исполнить способ «perform» для динамически загруженного класса. В нашем примере это не составит труда, от того что мы используем примитивные типы доводов. Впрочем, если доводы будут объектами, нам придётся применять механизм рефлексии, от того что реально внутри нашего отдельного загрузчика находятся другие копии тех же классов, которые мы не сумеем привести к интерфейсу Plugin.
Дабы отказаться от применения рефлексии и трудиться с экземпляром плагина так же, как если бы он был загружен нашим нынешним загрузчиком классов, дозволено воспользоваться механизмом проксирования, рассмотрение которого выходит за рамки данной статьи. Одну из реализаций данного механизма дозволено обнаружить в библиотеке cglib.
Начальный код всех приведённых примеров доступен на github.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB