📜 ⬆️ ⬇️

Plugging with Maven and Eclipse Aether

When the end users of your product are developers who can not only offer you ideas for improving it, but are also eager to participate in its expansion in every way, you begin to think about how to give them a similar opportunity. In this case, you do not want to give full freedom, exactly like access to the repository with the source code. How, in this case, allow third-party developers to make changes eliminating the need to change the source code, compile and re-insert the system?


Prehistory


We develop a system for performing automatic tests and monitoring for various services of the company. Tests developed by our automatists are, in fact, full-fledged programs that have their own life cycle. A set of events related to their execution is recorded in our system, and, if necessary (for example, if monitoring signals a problem), the system alerts all interested parties. Our testers are creative people, which is why they often have few standard methods of notification (mail and SMS), and I want to be able to customize the actions that are performed when a problem is detected. For example, light a red traffic light or turn on the siren.
Notifications are just one example of the need to dynamically extend the functionality of a product. There are quite a lot of similar problems in our work. In this regard, we decided to consider options that would allow our automators to expand the functionality of the system without having to change something in it.

Implementation options


For the implementation of the planned comes to mind several options.

Our system is completely built in Java. There are many excellent examples of extensible applications built on this platform. One of the most well-known libraries that allows you to create a flexible Java application architecture is OSGi . However, the use of this library creates additional complexity in the implementation of the application itself, imposes a number of limitations, and adds a large number of often unnecessary features. In addition, we very actively use the Maven-infrastructure for building and releasing services, running autotests, building reports and uploading them to remote storage, etc. Therefore, it was decided to try to implement a plugin system using Maven. Maven allows you to collect an artifact containing the compiled version of the plugin, specify all the dependencies necessary for its operation, and also install it in a remote repository, from where it will be available for download. This is very convenient when the plugin is a small piece of functionality that actively uses third-party libraries in its work.
')

From words to deeds


We will need 2 artifacts: the first, which contains the interface of our plugin (Plugin API), and the second - which contains the implementation of this interface. Let's create the simplest Maven project for the API:
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin -DartifactId=plugin-api 

Now we add the simplest interface of the plug-in with a single method that takes 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 install the created artifact in any repository. Temporarily use the local repository:
 mvn clean install 

Now let's start creating the implementation of the created API:
 mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=com.my.plugin -DartifactId=sum-plugin 

We need to add dependency on 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 SumPlugin implements Plugin { @Override public Integer perform(Integer param1, Integer param2){ return param1 + param2; } } 


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

We need to write dependencies on Eclipse Aether. To do this, first add some properties to 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 we write the dependencies themselves:
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 of dependency resolution. Eclipse Aether has a fairly simple and straightforward API. To find all the dependencies of an artifact, you must call the method resolveDependencies for an object of type RepositorySystem. First you need to create an instance of this class, and before that you also need to instantiate an object of type RepositorySystemSession. Then the resolution of dependencies can be implemented as follows:
Hidden text
 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(); 

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

Thus, we obtain the implementation of the DependencyResolver class, which will allow us to download and get a list of all dependencies of any artifact from the specified 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)); } 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(); } } 

As you can see in the above code, we used the created ManualWagonProvider as a service for an object of type ServiceLocator, which initializes an object of type RepositorySystem. Now you can use this class to resolve dependencies of any artifact located in any repository. It is enough to instantiate it and pass it the path to the local repository and to the list of remote repositories for the search (the last argument is optional).
Now you can 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 of its execution should be something like:
 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 

Now we have got the paths to the list of jar-files that are necessary for the work of implementing our plugin. To create an instance of the SumPlugin class, we need to load all the artifacts found using a separate class loader, whose parent loader we will make the system one.
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:
 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 perform the “perform” method for a dynamically loaded class. In our example, this is not difficult, since we use simple types of arguments. However, if the arguments are objects, we will have to use the reflection mechanism, since in fact inside our separate loader there are other copies of the same classes that we cannot bring to the Plugin interface.
To stop using reflection and work with a plug-in instance in the same way as if it were loaded by our current class loader, you can use the proxy mechanism, which is beyond the scope of this article. One of the implementations of this mechanism can be found in the cglib library.
The source code for all the examples provided is available on github .

Source: https://habr.com/ru/post/218323/


All Articles