JSR330 Support for Plugins

JSR-330 component model for plugins (aka Smoothie).

Some content of this page may be slightly inaccurate wrt specific details of classes and dependencies, as things have been moved around.
The general concepts however are still valid.

Overview

Smoothie is a replacement for the classic Hudson plugin strategy, and extension finding mechanism, that allows Hudson components to be implemented using JSR-330 annotations and injection.

This integration is done via standard documented Hudson features:

What is a Smoothie component?

A smoothie component is similar to a standard Hudson extension, except that instead of being adorned by @hudson.Extension is adorned by an annotation which is adorned by @javax.inject.Qualifier, such as @javax.inject.Named.

Smoothie components can live along-side standard Hudson extensions. Any Hudson hudson.ExtensionPoint may be defined as a Smoothie component. Standard extensions can discover Smoothie components just as they would for their standard counter-parts.

The significant difference, aside for the annotation semantics, is that Smoothie components are created by the container, where possible, and will be subject to JSR-330 injection. Some components are not able to be created by the container, for cases that uses Stapler for construction, and will not be able to use constructor-injection, but will benefit from member injection after the instance has been constructed.

A Simple Example

To implement a new link on the Manage Hudson page, a plugin needs to define a sub-class of hudson.model.ManagementLink.

In standard Hudson, this could look like:

@Extension
public class MyLink
    extends ManagementLink
{
    private MyLinkService getService() {
        // Do static-singleton-lookup
    }
}

The Smoothie equivalent component would look like:

@Named
@Singleton
public class MyLink
    extends ManagementLink
{
    private final MyLinkService service;

    @Inject
    public MyLink(MyLinkService service) {
        this.service = service;
    }
}

The major advantage to the Smoothie version of this component, is that if the implementation depends on other components, those components can be injected dynamically, instead of having to use the static-singleton-lookup method which is all that is available to standard extensions.

ExtensionPoint instances in Hudson are singletons, discovered by the ExtensionFinder and constructed once. To provide the same functionality for Smoothie components, @Singleton is used. It is not required specifically, but if the component has some shared state it should be annotated as such, so that if another component needs a reference, that it will use the same instance and not get a new instanced constructed by the container.

Configure the Plugin Strategy

When starting the augmented hudson.war, configure the strategy (This is the default as of 2.0.0)

-Dhudson.PluginStrategy=com.sonatype.matrix.smoothie.internal.plugin.DelegatingPluginStrategy

The DelegatingPluginStrategy here might look odd here. But what it does is provide a simple delegate model for the real strategy instance that will be used and uses the container to construct the delegate instance.

Developing Smoothie Components

Code  up your components, using @Named instead of @Extension.

For some types, where the container can not figure out what extension type you are trying to declare, additional @javax.enterprise.inject.Typed annotation is required.

For example, using Hudson's common Descriptor/Describable pattern requires that the Descriptor implementation be explicitly typed:

public class MyBuilder
    extends Builder
{
    // impl omitted

    @Named
    @Singleton
    @Typed(Descriptor.class)
    public static class DescriptorImpl
        extends BuildStepDescriptor<Builder>
    {
        @Override
        public boolean isApplicable(final Class<? extends AbstractProject> type) {
            return true;
        }

        @Override
        public String getDisplayName() {
            return "My Builder";
        }
    }
}

Features

Compatibility

When using the Smoothie plugin strategy, other non-smoothie extensions/plugins should function 100% the same as they did with the classic plugin strategy.

So... any existing plugin can work in a smoothie-enabled Hudson, but smoothie-enabled plugins will not work in a standard Hudson.

Describable Injection

Describable instances created by Descriptor components are not directly constructed by the container. They can be constructed via new or by using Stapler's @DataBoundConstructor facility. In any case, Smoothie will perform member injection after the instance is created, or deserialized from disk.

Deserialization from disk works with the standard XStream instances that Hudson uses, such as:

  • Hudson.XSTREAM
  • Items.XSTREAM
  • ...

Priority

Hudson's @Extension provides an ordinal member to indicate the sorting-order for extension points of the same type when used in a list context. Smoothie provides the @Priority annotation which provides the same mechanism.

The following are equivalent with respect to sorting order:

@Extension(ordinal=57)
public class MyLink
    extends ManagementLink
{
    // impl omitted
}
@Named
@Singleton
@Priority(57)
public class MyLink
    extends ManagementLink
{
    // impl omitted
}

Advanced Features

Injectomatic

Some components instances currently must be created outside of the container, such as Describable instances created via Stapler using @DataBoundConstructor. These instances can not have constructor-based injection, but will instead receive member injection via the magical Injectomatic component.

InjectomaticAware

Allows any object to become aware of the Injectomatic component when its instance is created, regardless of how the instance was created (by the container or otherwise).

Injectable

Allows any object to receive member injection after the instance was created, regardless of how the instance was created (by the container or otherwise).

XStreamInjectoHandler

This is where the magic for handling injection on deserialization occurs. The default set of XStream instances are automatically registered. To add inject-on-deserialize behavior for custom streams, register the stream with this handler.

Aspects

Aspects are required currently to implement a few of the more magically features, like post Describable instance creation member injection. Use of aspects is limited ATM, but as a side-effect, all plugins (Smoothie or classic) are subject to load-time-weaving.

The performance impact is negligible, costing a few parts of a second more when the plugin is loaded.

References

Labels:

Enter labels to add to this page:
Wait Image 
Looking for a label? Just start typing.