jExt

Simple plug-in extension support for Java

License

License

GroupId

GroupId

io.github.luiinge
ArtifactId

ArtifactId

jext
Last Version

Last Version

1.0.0
Release Date

Release Date

Type

Type

jar
Description

Description

jExt
Simple plug-in extension support for Java
Project URL

Project URL

https://github.com/luiinge/jext
Project Organization

Project Organization

luiinge
Source Code Management

Source Code Management

https://github.com/luiinge/jext/tree/master

Download jext

How to add to project

<!-- https://jarcasting.com/artifacts/io.github.luiinge/jext/ -->
<dependency>
    <groupId>io.github.luiinge</groupId>
    <artifactId>jext</artifactId>
    <version>1.0.0</version>
</dependency>
// https://jarcasting.com/artifacts/io.github.luiinge/jext/
implementation 'io.github.luiinge:jext:1.0.0'
// https://jarcasting.com/artifacts/io.github.luiinge/jext/
implementation ("io.github.luiinge:jext:1.0.0")
'io.github.luiinge:jext:jar:1.0.0'
<dependency org="io.github.luiinge" name="jext" rev="1.0.0">
  <artifact name="jext" type="jar" />
</dependency>
@Grapes(
@Grab(group='io.github.luiinge', module='jext', version='1.0.0')
)
libraryDependencies += "io.github.luiinge" % "jext" % "1.0.0"
[io.github.luiinge/jext "1.0.0"]

Dependencies

compile (2)

Group / Artifact Type Version
javax.annotation : javax.annotation-api jar 1.3.2
org.slf4j : slf4j-api jar 1.7.30

test (5)

Group / Artifact Type Version
com.google.testing.compile : compile-testing jar 0.19
junit : junit jar 4.13.1
org.hamcrest : hamcrest jar 2.2
org.apache.logging.log4j : log4j-slf4j-impl jar 2.13.0
org.assertj : assertj-core jar 3.16.1

Project Modules

There are no modules declared in this project.

jExt

GitHub GitHub Workflow Status (branch) Maven Central

Quality Gate Status Lines of Code Coverage Bugs Code Smells Duplicated Lines (%) Technical Debt

jExt is a simple library that allows you to define a plug-in architecture detecting the Java classes that implements a specific interface. It mostly relies on the standard extension mechanism provided by Java by means of the ServiceLoader class, but it simplifies and enhances the usage.

The key concepts are:

  • Extension points (interfaces annotated with @ExtensionPoint) which define the contract
  • Extensions (classes annotated with @Extension and implementing the extension point class)
  • Extension loaders that are responsible of discovering extensions for an specific extension point

What is the extra value then?

  • Annotation processing of @Extension and @ExtensionPoint so the classes are automatically registered as services by the compiler
  • Capability of using multiple class loaders at the same time
  • Use the default ServiceLoader mechanism or provide your own discoverer from a IoC framework
  • Explicitly declaring the extension point version that implement each extension, avoiding using wrong jar versions
  • Custom control about creating new extension instances or reusing existing ones
  • When extending an extension via inheritance, you may either override the parent class or use both superclass and subclass independently

Usage

Maven dependency

Include the following within the <dependencies> section of your pom.xml file:

<dependency>
    <groupId>io.github.luiinge</groupId>
    <artifactId>jext</artifactId>
    <version>1.0.0</version>
</dependency>

Quick start

Declaring a extension point

Simply annotate an interface with @Extension:

@ExtensionPoint
public interface MyExtensionPoint {

    List<String> provideStuff();

}

Providing an extension

Annotate a class implementing the interface with @ExtensionPoint:

@Extension(provider="my.company", name="my.extension", version="1.0")
public class MyExtension implements MyExtensionPoint {

    @Override
    public List<String> provideStuff() {
        return List.of("apple","carrot","lemon");
    }

}

Consuming an extension point

Use a ExtensionManager instance to get one or many implementations of the extension point:

    ExtensionManager extensionManager = new ExtensionManager();
    // get one 
    Optional<MyExtensionPoint> extension = extensionManager.getExtension(MyExtensionPoint.class);
    // get many
    Stream<MyExtensionPoin> extensions = extensionManager.getExtensions(MyExtensionPoint.class);

You can apply several filters (predicates) when asking for an extension, such as provider or specific versions.

Scopes

Each extension can define a specific scope that will be used to decide whether it is necessary to create a new instance or reuse an existing one. The available scopes are:

  • GLOBAL : the same instance would be used along the lifetime of the application; this is the default scope when not specified
  • LOCAL : a new instance would be created each time the extension is requested
  • SESSION : the same instance would be used along the lifetime of the extension manager; if you create several managers, each one will reuse its own instance

Versioning

One major improvement over the regular ServiceLoader is avoiding version mismatches. Since Java does not have the concept of version at runtime, you can end up using an implementation version that is not aligned with the interface version. Thus, unexpected errors like NoSuchMethodError may occur without any further hint of what is happening.

Using jExt you can provide extra information in the annotations that helps to determine if an extension is suitable for an extension point. The ExtensionPoint annotation has the property version, used in the form of <major>.<minor>[.<patch>], and the Extension annotation has the property extensionPointVersion in the same manner. If the existing extension uses an extension point version that is not compatible with the actual extension point version, it will not be selected by the ExtensionManager, preventing this way potential errors.

If you do not care about versioning, just ignore it; version 1.0.0 will be used by default.

Use your own extension loader

The default behaviour of any declared extension is to be loaded by means of the java.util.ServiceLoader class. However, you can provide your own extension loading mechanism by creating a new class implementing the jext.ExtensionLoader interface.

Your custom extension loader have to be registered in the ServiceLoader registry, writing the fully qualified name of the class in the file META-INF/services/jext.ExtensionLoader. Package both files in a jar and include it in the classpath or modulepath along with the jext library in order to be accessible.

There are two constraints that external loaders should abide by:

  • Only retrieve extensions that are mark as externallyManaged
  • Honour the defined scope of the extension, returning new instances or reusing ones accordingly.

A common scenario where custom extension loaders are required is the use of an inversion of control framework that manage the lifecycle of components on its own. Check the jExt-Spring project for an example of this.

Other considerations

Finalizing instances

The internal extension loader keeps a cache of used extensions per session (that is, per instance of ExtensionManager). Due to the specifics of the Java finalization process and garbage collection, you must clear the cached objects manually once you are done ( by invoking extensionManager.clear() ). Otherwise, the cached objects would remain in memory indefinitely. That is not a problem when your extension manager is a singleton bound to the lifetime of the application, but if you are planning to use several instances of the extension manager, this becomes an issue to be aware of.

Java modules

When the Java Module System is present, extension points and extensions must be declared manually in your module-info.java file using provides directive. Although it partially defeats the purpose of fully automation, none straightforward solution can be applied to solve that, so the only outcome is live with it. Nonetheless, the compiler can detect the lack of the required directives in the module definition, and it will emit a warning message informing you of this situation, similar to the following:

[WARNING] [jext] at jext.MyExtensionV2_6 :: jext.MyExtensionV2_6 must be declared with the directive 'provides' in module-info.java in order to be used properly

Authors

Contributions

If you want to contribute to this project, visit the Github project. You can open a new issue / feature request, or make a pull request to consider. You will be added as a contributor in this very page.

Issue reporting

If you have found any defect in this software, please report it in Github project Issues. There is no guarantee that it would be fixed in the following version but it would be addressed as soon as possible.

License

    MIT License

    Copyright (c) 2020 Luis Iñesta Gelabert - [email protected]

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.

Versions

Version
1.0.0