Pull to refresh

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

Reading time 10 min
Views 4.3K
Когда конечными пользователями вашего продукта являются разработчики, способные не только предложить вам идеи для его улучшения, но и горящие желанием всячески поучаствовать в его расширении, вы начинаете задумываться о том, как бы предоставить им подобную возможность. При этом вы не хотите давать полную свободу, ровно как и доступ к репозиторию с исходным кодом. Как в этом случае позволить сторонним разработчикам вносить изменения исключая необходимость изменения исходников, компиляции и перевыкладки системы?


Предыстория


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

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


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

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

От слов к делу


Нам понадобится 2 артефакта: первый, содержащий интерфейс нашего плагина (Plugin API), и второй — содержащий реализацию этого интерфейса. Создадим простейший 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.
Tags:
Hubs:
+6
Comments 0
Comments Leave a comment

Articles