Pluginsite using Maven and Eclipse Aether

When the end users of your product are the developers who can not only offer you ideas for improving it, but also burning with desire to fully participate in its expansion, you start to think about how to provide them with such opportunity. You do not want to give full freedom, exactly as to access the repository with the source code. In this case, allowing third-party developers to make changes eliminating the need to change the source code, compile, and priviledge system?


the

Prehistory


We are developing a system running automated tests and monitoring for different services of the company. The tests, developed by our programmers is, in fact, a full-fledged program that has its own life cycle. A set of events related to their execution is logged in our system and, if necessary (e.g. if monitoring indicates a problem), the system shall notify all interested parties. Our testers creative people, so often the standard notification methods (email and SMS) is not enough for them, and I want to have the ability to customize the action when a problem is detected. For example, to light a red light or siren.
Notifications is just one example of needs in a dynamic extension of the functionality of the product. Such tasks in our work there is quite a lot. In this regard, we decided to consider options which would allow our programmers to expand the functionality of the system without the need to change something in herself.

the

implementation variations


To plan think of a few options.
the
    the
  • Most common variant of the possible behaviors of the system — interaction via the API. Most often it is the interface of external influence on the system, for example, SOAP, REST API, etc. This approach is quite limited and often does not allow time to react to internal events of the system.
  • the
  • a More complicated but more interesting is the approach of the internal API that allows you to get feedback from processes occurring in the nucleus. If the first interface has the most modern services, the latter is often implemented via a feedback mechanism (e.g., Push notifications or hooks). But it is not always convenient and reliable, besides I want to find a more General approach to the problem, while hooky is good for solving a specific narrow task.
  • the
  • Another way to expand the system "from within" is the mechanism of plug-ins (plug-ins). It allows you to get feedback from the system directly from inside a running process. This approach is widely known and well established in many known applications. It came to us when we needed to add a mechanism to dynamically extend the functionality of the main application.

Our system is completely built on Java. There are many great examples of extensible applications built on this platform. One of the most famous libraries that allow you to create a flexible architecture of a Java application is OSGi. However, the use of this library creates additional complexity in the implementation of the application itself imposes a number of restrictions and adds a large number of often unnecessary features. In addition, we are actively using Maven infrastructure to build and release services, run automated tests, build reports and download them to the remote repository, etc. So it was decided to try to implement a plugin system using Maven. Maven allows you to collect the artifact that contains the compiled version of the plugin to specify all the dependencies needed to run it, and install it in a remote repository, where it will be available for download. This is very useful when a plugin is a small piece of functionality that actively use third-party libraries in their work.
the

From words to action


We will need 2 artifacts: first containing our plugin interface (Plugin API), and the second containing an implementation of this interface. Create a simple Maven project for an API:
the
mvn archetype:generate-DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin-DartifactId=plugin-api

Now let's add a simple plugin interface with a single method host 2 integer arguments and returns an integer:
Hidden text
package com.my.plugin;

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


You need to set the artifact in any repository. Temporarily use a local repository:
the
mvn clean install

Now, let's create an implementation created by API
the
mvn archetype:generate-DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin-DartifactId=sum-plugin

We need to add the dependence of our API:
Hidden text
 <dependency>
<groupId>com.my.plugin</groupId>
<artifactId>plugin-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

And, accordingly, the implementation of the Plugin interface:
Hidden text
package com.my.plugin.impl;

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


To demonstrate the implementation of the extensible application generate another project:
the
mvn archetype:generate-DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin-DartifactId=app

We need to register dependencies from the Eclipse Aether. To do this, first add a few properties pom.xml:
Hidden text
<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>

Now write yourself dependencies:
Hidden text
<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>

Now you need to implement the logic for resolving dependencies. Eclipse Aether is quite simple and intuitive API. To find all dependencies of an artifact, you must invoke method resolveDependencies for an object of type RepositorySystem. First we need to create an instance of this class, and before that you also need to instantiate an object of type RepositorySystemSession. Then the dependency resolution could be implemented as follows:
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();

To initialize an object of type RepositorySystem, we will need to implement the interface WagonProvider, which is used for pumping artifacts from any source, be it http or ftp repository, or, for example, ssh. The simplest implementation of this interface might look like the following:
Hidden text
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
}

So get the implementation of the DependencyResolver class, which will allow us to download and receive a list of all dependencies of any artifact of the repositories:
Hidden text
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));
}

synchronized public 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();
}
}

As you can see in the above code, we used ManualWagonProvider created as a service for an object of type ServiceLocator, which initialisere an object of type RepositorySystem. You can now use this class to resolve dependencies of any artifact in any repository. It is enough to instantiate it and pass the path to the local repository and to the list of remote repositories to search (the last argument is optional).
You can now test the performance of our code with the following example.
Hidden text
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());
}

The above code will collect all dependencies of the artifact plugin-sum and display them on the screen. The result should be something like:
file:/home/smecsia/.m2/repository/com/my/plugin/plugin-api/1.0-SNAPSHOT/plugin-api-1.0-SNAPSHOT.jar
Now that we have a way to list the jar files needed for the implementation of our plugin. To create an instance of the class SumPlugin, we need to load all of the artifacts found by using a separate class loader the parent loader which will do the system.
Hidden text
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());

Using a separate loader we can initialize a new instance of the loaded class:
the
Class<?> clazz = urlClassLoader.loadClass("com.my.plugin.SumPlugin");
final Plugin pluginInstance = (Plugin) clazz.newInstance();
System.out.println("Result:" + pluginInstance.perform(2, 3));

Now we can execute the method "perform" for dynamically loaded class. In our example, this will not be difficult, since we use a simple argument types. However, if the arguments are objects, we have to use reflection mechanism, because in fact within our individual loader can find other copies of the same classes that we will not be able to bring to the interface Plugin.
To refuse the use of reflection and work with an instance of a plugin in the same way as if it were loaded with our current class loader, you can use the proxy mechanism, the consideration of which is beyond the scope of this article. One of the implementations of this mechanism can be found in the library cglib.
The source code for all examples available in github.
Article based on information from habrahabr.ru

Комментарии