Nexus is a Plexus application, it uses Plexus as a container. As we are well aware, Plexus isn't the only Depedency Injection framework currently available. We decided to design the Nexus plugin mechanism to allow for extensibility in a container independent way. If you want to write a plugin for Nexus (and possibly all other Sonatype products) you don't need to pick up a book on Plexus, we're trying to make it easier for someone to innovate on the Nexus platform without having to adopt a whole series of technologies.
This post covers some of the initial steps, that are required to write a plugin for Nexus. Although Nexus is a Plexus application, we want to give 3rd party developers ability to extend Nexus, without forcing them to know Plexus. We also want to give 3rd party developers the ability to extend Nexus, without burying themselves into Nexus internals. Clearly if someone wants to add complex, highly custom behavior to Nexus they will need to dive into the internals, but it should be easy to add a simple extension to Nexus without having a PhD in Nexus Internals. We are also committed to adopting 3rd party "specs/suggestions/APIs" that look promising, even if it comes from another IoC provider.
Now, let's replace the "Nexus" in above sentences with "Sonatype Application" in the sentences above. It's not so different, right? At Sonatype, we're convinced that providing an intuitive plugin and extension mechanism is critical for adoption and we want to make it as straightforward as possible. To start this discussion, we need first a look at how Plexus works.
Plexus in 3 minutes
Plexus "managed beans" are called "components". In contrast to the way most people use Spring, where each bean has a unique name or "ID", Plexus components are addressed with **two** coordinates: *role* and *hint*.
Plexus encourages the use of interfaces when defining components. Generally spoken, when implementing one component in Plexus, you will almost always end up with two compilation units: a Java interface, that sets the contract that your component fulfills, and an implementation (or multiple implementations) that implements the contract interface. Again, this is not enforced at all, but is considered as "best practice".
While these two coordinates are internally represented as plain strings, the role is usually a FQN of the interface that component "provides" (or implements). For example, the role might be something like "com.sonatype.component.TestComponent" which is the FQN of an interface used to define the contract of the component. This is not enforced at all in Plexus and it's tooling (for example, the plexus-component-metadata Maven plugin), but again, is considered a "best practice". The "hint" is actually a free string qualifier, to be able to differentiate amongst multiple implementors (components) with same role, if needed. There is one special hint used internally by Plexus: "default", which is used when no hint is supplied.
Why is this important? Well, usually you have two major situations with your components (managed beans). For one component contract interface you either have:
- one "default" implementation across your application (singular case),
- or you may have multiple different implementations of same component contract (plural case)
Do not confuse the singular case with "singleton" pattern: in both cases the components (managed beans) **are singletons**. We are just providing **one or multiple implementations** for same interface. But in both cases, the actual classes being created by container are singletons (unless you say different in your Plexus annotations, but that's another story, and I don't want to complicate existing examples).
Another case is how the "consumer" of that component -- which is probably some other component in your application -- decides which implementation it wants to use. Or maybe it doesn't care at all?
The simplest example of "singular" case, the component contract interface and it's implementation:
public interface MyComponent
{
String getHello();
}
@Component( role=MyComponent.class )
public class DefaultMyComponent
implements MyComponent
{
public String getHello() {
return "hello";
}
}
That's all you need. We just created a MyComponent component contract (interface), and provided one implementation. The implementation is even annotated using Plexus annotation, that states "this class is a component with role MyComponent". Since we say nothing about the hint of component, Plexus implicitly manages it as "default" implementation (one-and-only).
The consumer of this component does something like this:
@Component( role=SomeOtherComponent.class )
public class AComponentConsumer
implements SomeOtherComponent
{
@Requirement
private MyComponent myComponent;
...
}
Here, with @Requirement annotation we declared a "requirement" (we need it injected) to the MyComponent interface. The Plexus will try to fulfill this requirement by looking up a component with role MyComponent, and hint "default" (implicit hint), since we did not say anything about needed hint.
For the second case (one contract, multiple implementations), the most simpler example is this:
public interface Archiver
{
void archive( File source, File archiveFile )
throws IOException;
void unarchive( File archiveFile, File destination )
throws IOException;
}
and having component implementations like these:
@Component( role=Archiver.class, hint-"zip" )
public class ZipArchiver
implements Archiver
{
...
}
@Component( role=Archiver.class, hint-"tgz" )
public class TgzArchiver
implements Archiver
{
...
}
@Component( role=Archiver.class, hint-"7z" )
public class SevenZArchiver
implements Archiver
{
...
}
The consumer of Archiver does something like this:
@Component( role=SomeOtherComponent.class )
public class AComponentConsumer
implements SomeOtherComponent
{
@Requirement( hint="zip" )
private Archiver zipArchiver;
...
}
In case above we "wired" the AComponentConsumer to support "zip" archives only. But we may do something like this:
@Component( role=SomeOtherComponent.class )
public class AComponentConsumer
implements SomeOtherComponent
{
@Requirement( role=Archiver.class )
private List archivers;
...
}
or
@Component( role=SomeOtherComponent.class )
public class AComponentConsumer
implements SomeOtherComponent
{
@Requirement( role=Archiver.class )
private Map archivers;
public void archive( File src, File dest )
throws IOException
{
String destExt = getFileExtension( dest );
if ( archivers.containsKey( destExt ) )
{
Archiver archiver = archivers.get( destExt );
archiver.archive( src, dest );
...
}
else
{
throw new IOException( "Archive format unsupported!" );
}
}
}
It is obvious, that in this example, Plexus will inject a Map<String, Archiver> into AComponentConsumer, where the String keys will be hints of components in the Map, and values will be Archiver instances with given key.
So?
So, why is this important? -- you ask. Well, in Nexus, the "extension points" are simply taken, "marked" interfaces (roles), that will be automatically pulled by Plexus collection lookups similar to this last example. Marked by Nexus developers, and pointed out as "extension point" to the 3rd party plugin developers.
Actually, up to now, this was how Nexus supported "plugins". "Plugins" and not plugins, up to now, the job of the developer was to code Plexus components, build them, provide Plexus descriptors for the plugin (usually by annotating the classes as above and using the plexus-component-metadata plugin, which embeds the descriptor into resulting JAR), and "drop it" into Nexus lib.
Since the developers implemented and marked their classes as @Component with roles that were looked up, after booting up Nexus with new JAR in it's lib folder, Nexus was aware of these and would start using them as if they were a part of the "core". Simple as that.
The downside of this approach is twofold:
- 3rd party developers are forced to use Plexus, plexus tooling, etc. Also, they must be aware of Nexus internals, for example that Repository components should be **prototypes**, and not **singletons**.
- the plugins are more like "extensions", since they total and uncontrolled access to all of the Nexus internals, not just extension points.
The Idea
In short:
- let the "host application developers" mark and parametrize the extension points (@ExtensionPoint)
- let the "3rd party plugin developers" create extensions by implementing interfaces marked with @ExtensionPoint.
- provide IoC abilities for "3rd party pluging developers"
- let the "3rd party plugin developers" create their own managed beans, components with all the IoC benefits
- load the plugin in separate classloader
Okay, let's draft the idea: having "extension points", which are all simple Java interfaces (component contracts) and are provided by "host application" developers (in this case Nexus developers). This is simple to understand: the **host application developers** are the one who know the what, where, and how about what is extensible within the application. Thus, we need to provide some means for developers, to mark their interfaces as @ExtensionPoint. Almost always, the case with components here is the "plural" case (one component interface, with multiple implementations, 3rd party developers just adding new ones to the pool of existing). But again, usually the host application developers are the ones who knos, and should provide information on extension points about instantiation details of the component implementations (singletons versus prototypes for example).
When the host application API has decorated interfaces (API), the **3rd party plugin developers** may kick in, by implementing @ExtensionPoints. All they have to do is pick a component interface marked as @ExtensionPoint, and implement it. That's all. And what about IoC?
We decided to accept the @Inject project proposal at code.google.com, and use "meta-metadata" to decorate the components. Since @Inject already proposes a meta-metadata to make possible to annotate classes and use them across Guice and Spring, we thought making it work with Plexus would be nice too.
Thus, in short, 3rd party Nexus plugin developers should use @Inject annotations only. Naturally, when deployed to Nexus, their components will be "powered by Plexus" under the hud, but it's not their problem and they should not care about it.
The Nexus Plugin Manager will have another great feature: **class loader separation**. Right now, since as I said, the plugin JAR was just dropped into Nexus lib folder, that implicitly means that plugins were doomed to use the same versions of dependencies that were already present in Nexus (newly introduced dependencies are fine, though). This led to the proliferation and over user of the Maven Shade plugin. Don't get me wrong, I think this plugin is very-very useful, but it must be used with care (not to mention it's flaw, that it "shades" stuff always under hidden package, instead to somewhere module specific place. Two jars with same shaded dependency may again clash).
One more thing: the @Inject proposal does not mention how to create new managed beans, they have no means to annotate a new component (well, in case of Guice it happens implicitly with @Inject, making the interface needed in binding). This is where another new annotation comes in: @Managed. The @Managed annotation will be used on Java **interfaces only**, to mark them as component contracts.
So, in short:
- Host application developers should point out the "extension point" interfaces in the apps. Also, they should provide as much "metadata" as possible about the instances of those components.
- plugin developers should only care about implementing those marked interfaces, and not fiddle with annotating them with some container-specific stuff
- use @Inject project annotations
- add ability to plugin developers to enjoy the benefits of IoC containers, and let them defined their own components (again, no Plexus needed).
Teaser
Here is an example of a theoretical implementation of artifact virus scanner for Nexus. In short: we want to make sure no virus infected artifact will come in our Nexus instance. Internal deployments are considered "safe", thus proxy requests are checked only.
These is what we need to implement this plugin:
- A Virus scanner implementation (not shown here for example's brevity's sake)
- A Nexus RequestProcessor (extension point) implementation. RequstProcessor components are able to interact with every Nexus request, and stop their execution at different stages
- A Nexus RepositoryCustomizer (extension point) implementation, to inject the RequestProcessor from above to repositories we want
We start with creating a managed component contract for Virus Scanner:
package org.sample.plugin;
import java.io.InputStream;
import org.sonatype.nexus.plugins.Managed;
@Managed
public interface VirusScanner
{
boolean hasVirus( InputStream is );
}
Then we implement it. This is a "singular" case, the newly introduced component contract interface has only one default implementation:
package org.sample.plugin;
import java.io.InputStream;
import javax.inject.Named;
public class XYVirusScanner
implements VirusScanner
{
public boolean hasVirus( InputStream is )
{
// DO THE JOB HERE
return resultOfTheScanning;
}
}
Next, a RequestProcessor. It requires VirusScanner to do the work. We are not making a decision until we get to the content in shouldCache(). We are using the @Inject annotations to inject our newly created component. Since VirusScannerRequestProcessor implements a Nexus extension point (they are "plural" components), we need to reference it later (see below in VirusScannerRepositoryCustomizer), we are "naming it" with the @Named annotation.
package org.sample.plugin;
import java.io.IOException;
import javax.inject.Inject;
import javax.inject.Named;
import org.sonatype.nexus.proxy.ResourceStoreRequest;
import org.sonatype.nexus.proxy.access.Action;
import org.sonatype.nexus.proxy.item.AbstractStorageItem;
import org.sonatype.nexus.proxy.item.StorageFileItem;
import org.sonatype.nexus.proxy.repository.ProxyRepository;
import org.sonatype.nexus.proxy.repository.Repository;
import org.sonatype.nexus.proxy.repository.RequestProcessor;
@Named( "virusScanner" )
public class VirusScannerRequestProcessor
implements RequestProcessor
{
@Inject
private VirusScanner virusScanner;
public boolean process( Repository repository, ResourceStoreRequest request, Action action )
{
// don't decide until have content
return true;
}
public boolean shouldProxy( ProxyRepository repository, ResourceStoreRequest request )
{
// don't decide until have content
return true;
}
public boolean shouldCache( ProxyRepository repository, AbstractStorageItem item )
{
if ( item instanceof StorageFileItem )
{
StorageFileItem file = (StorageFileItem) item;
// do a virus scan
try
{
return virusScanner.hasVirus( file.getInputStream() );
}
catch ( IOException e )
{
// handle it
return false;
}
}
else
{
return true;
}
}
}
Finally, we are creating a RepositoryCustomizer (also a "plural" component, but we don't care about naming it), that will inject our RequestProcessor into repository instances we need. In this case, those are proxy repositories (the only ones able to fetch artifacts from remote repositories).
package org.sample.plugin;
import javax.inject.Inject;
import javax.inject.Named;
import org.sonatype.nexus.configuration.ConfigurationException;
import org.sonatype.nexus.plugins.RepositoryCustomizer;
import org.sonatype.nexus.proxy.repository.ProxyRepository;
import org.sonatype.nexus.proxy.repository.Repository;
import org.sonatype.nexus.proxy.repository.RequestProcessor;
public class VirusScannerRepositoryCustomizer
implements RepositoryCustomizer
{
@Inject
private @Named( "virusScanner" )
RequestProcessor virusScannerRequestProcessor;
public boolean isHandledRepository( Repository repository )
{
// handle proxy reposes only
return repository.getRepositoryKind().isFacetAvailable( ProxyRepository.class );
}
public void configureRepository( Repository repository )
throws ConfigurationException
{
repository.getRequestProcessors().put( "virusScanner", virusScannerRequestProcessor );
}
}
That's all for now. In the next part, we will make more in-depth explanations of the plugin API, and explain what is happening "behind the curtains". Have fun!
Written by Tamas Cservenak
Tamas is a former Senior Develoepr at Sonatype. He has over 15 years of experience developing software systems in Public Services, Telco and Publishing industries.
Tags