Building an OSGi-like Plugin System in Java
Tags: java, tutorial, osgi, showdev
Published: 2020-01-05 15:09:14

---
title: Building an OSGi-like Plugin System in Java
published: true
description: How to implement a basic OSGi-like plugin system in Java
tags: java, tutorial, osgi, showdev
---

If you are not familiar with what OSGi is, you can find additional details [here](https://en.wikipedia.org/wiki/OSGi) and [here](https://www.osgi.org/developer/what-is-osgi/).

The complete project can be found on github here: https://github.com/Larisho/osgi

# The Spec
By the end of this tutorial, we will have an MVP that should:
- accept a path to a plugin (`jar file`)
- creates an instance of the plugin
- runs the plugin

# Requirements
To follow along, you will only need the following:
- Java 8+
- Maven 3+

# Phase 1: Boilerplate!
Let's get right into it. Open up a terminal and follow along.  
```bash
mkdir ~/osgi
cd ~/osgi

mvn archetype:generate -DgroupId=com.gabdavid.osgi                       \
                       -DartifactId=engine                               \
                       -DarchetypeArtifactId=maven-archetype-quickstart  \
                       -DinteractiveMode=false
mvn archetype:generate -DgroupId=com.gabdavid.osgi                       \
                       -DartifactId=sample-plugin                        \
                       -DarchetypeArtifactId=maven-archetype-quickstart  \
                       -DinteractiveMode=false
```

Make sure that your `pom.xml` files look like the following:
`~/osgi/engine/pom.xml`
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.gabdavid.osgi</groupId>
  <artifactId>engine</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>engine</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>

        <configuration>
          <archive>
            <manifest>
              <mainClass>com.gabdavid.osgi.engine.Engine</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```
`~/osgi/sample-plugin/pom.xml`
```xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.gabdavid.osgi</groupId>
  <artifactId>sample-plugin</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>sample-plugin</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.2.0</version>

        <configuration>
          <archive>
            <manifest>
              <mainClass>com.gabdavid.osgi.sampleplugin.App</mainClass>
            </manifest>
          </archive>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
```
*NOTE*: I've moved and renamed where the main source file is in each project. Feel free to not do that, just make sure that the `mainClass` tags reflect where the actual main classes are.

Now we're ready to roll! Next up, a little theory.

# Phase 2: The Theory
## Interfacing with the Plugin
The first thing we need to do is figure out a way for the engine to interface with the plugin.

One option would be to rely on a class in the Java standard library. A benefit of this option is that we do not need a third project that contains classes that both the engine and the plugins can depend on. A downside is that we have no way of specifying "plugin system specific" methods in a type-safe way. For the sake of simplicity of our MVP, this is the option that we're going to go with.

The class from the standard library we will be using is the `Runnable` class. We will be able to make use of its `run()` method to run the plugin itself.

## Reading a `jar` file
The second issue we need to address is how to read the `jar` file. For those not "in the know", I'll let you in to a little secret: `jar` files are just `zip` files organized in a certain way! Which means that you could unzip a `jar` and read the `class` files somehow (we'll get to that in a second) and create instances of the classes defined. Luckily, we don't need to go through all of that work because Java's standard library comes with a `JarInputStream` that will iterate through all of the "entries" (files) in the `jar` (`zip`) file and allow you to access them.

## Finding the right `class` file to load
The next issue is finding the right file to read. For the sake of simplicity, we will require that the plugin include the entry class's canonical name (`<package>.<classname>`) in the `MANIFEST.MF`. This simplifies things because the `JarInputStream` has a `getManifest` method which will give us a `Manifest` object containing all of the properties defined.

## Loading the `class`
The final issue is actually reading the `class` file and running it! We will be using a `UrlClassLoader` to dynamically load the classes. We will be using its `loadClass` method to load our class from byte code. The `loadClass` method will return a `Class` object which we will use to create instances of the class using the reflection API.

### Class Loading 101
If you already know how class loading works in Java, you can skip this section. **Huge caveat: this is a quick explanation for the sake of context and is not a 100% complete explanation!**

Java source code is compiled into byte code (the content of a `class` file) which is then run by the JVM. All references to objects outside of the `class` file are resolved at runtime, as opposed to a language like C which resolves all `include` macros at compilation time. The way that these classes are resolved at runtime is with a `ClassLoader`. If a `ClassLoader` cannot find a particular `class` it will throw a `ClassNotFoundException`. The class loader is able to take byte code and turn it into an actual `Class` object.

# Phase 3: Writing the Engine
Here is our MVP for the engine:
`~/osgi/engine/src/main/java/com/gabdavid/osgi/engine/Engine.java`
```java
package com.gabdavid.osgi.engine;

import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.Attributes;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

public class Engine {

  public static void main(String[] args) throws Exception {

    // Have to check that a path has been passed in
    if (args.length != 1) {
      System.out.println("Path must be passed in");
      return;
    }

    String path = args[0];
    File jarFile = new File(path);

    String className;  
    try (JarInputStream jarStream = new JarInputStream(new FileInputStream(jarFile))) {
      Manifest manifest = jarStream.getManifest();

      // Get Main-Class attribute from MANIFEST.MF
      className = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
    }

    System.out.println("Class name found in manifest is: " + className);

    // Here, we initialize the class loader with the path to the jar file.
    //
    // We use the jar protocol here. The !/ is used to separate between the URL to the actual
    // jar resource and the path within the jar. For example, I could download a whole jar by
    // calling jar:http://something.com/myjar!/ or I could download only 1 class file by calling
    // jar:http://something.com/myjar!/com/gabdavid/myjar/File.class
    // See https://docs.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html for more details
    URLClassLoader classLoader = new URLClassLoader(
        new URL[]{new URL("jar:" + jarFile.toURI().toURL() + "!/")}
    );

    Class<?> pluginClass = classLoader.loadClass(className); // Load the class

    System.out.println("Package of plugin class: " + pluginClass.getPackage());
    System.out.println("Interface of plugin class is: " + pluginClass.getInterfaces()[0].getName());
    System.out.println("Creating a new instance now....!");

    // Create an instance of the loaded class and cast it from an Object
    // to a Runnable. We assume that this will succeed because that is the
    // contract we've agreed to (see theory section).
    Runnable plugin = (Runnable) pluginClass.newInstance();
    System.out.println("Running instance now....!");
    plugin.run();

    System.out.println("Exiting successfully!");
  }
}
```

# Phase 4: Writing the Plugin
Here is our super simple plugin:
`~/osgi/sample-plugin/com/gabdavid/osgi/sampleplugin/App.java`
```java
package com.gabdavid.osgi.sampleplugin;

public class App implements Runnable {

  // This is here for the sake of completeness, really. Wouldn't want to tell
  // Maven that this class has a main method when it doesn't actually have one!
  public static void main(String[] args) {
    new App().run();
  }

  public void run() {
    System.out.println("Hello from the Sample Plugin!");
  }
}
```

# Phase 5: Let's Run the Thing Already!
Let's run it!
```bash
cd ~/osgi/engine
mvn package

cd ~/osgi/sample-plugin
mvn package

java -jar ~/osgi/engine/target/engine-1.0-SNAPSHOT.jar ~/osgi/sample-plugin/target/sample-plugin-1.0-SNAPSHOT.jar

# Output:
# Class name found in manifest is: com.gabdavid.osgi.sampleplugin.App
# Package of plugin class: package com.gabdavid.osgi.sampleplugin
# Interface of plugin class is: java.lang.Runnable
# Creating a new instance now....!
# Hello from the Sample Plugin!
# Exiting successfully!
```

There you have it! You've now dynamically loaded a different `jar` file and run their code! From here, the possibilities are endless.

# Conclusion

## Drawbacks of our current solution
### Using `Runnable`
As we discussed in the theory section, using the `Runnable` interface as our "adapter", if you will, limits us in the number of operations that we can call in a type-safe way. We may want to have lifecycle hooks or something as well as the main `run` method. I keep saying "type-safe" because we could always just cast the plugin to an `Object` and use reflection to call whatever methods that we want but that leaves a lot of room for error and could lead to (fictional) plugin-writers becoming frustrated.

### Doesn't Really Do Anything
This really doesn't do anything! A future post will be to expand on this project so that it could actually _do_ something. 

## Future Plans?
I plan on writing a part 2 to this post where I expand on the engine. If you have any suggestions of features to be added, please comment!

Here's a short list of things that I'm thinking of improving on:
- Turn the engine into a `repl` of sorts where we can load and run different plugins
- Have the ability to reload a plugin
- Create a library where we can define an actual contract specific to our engine. That way, we can specify lifecycle hooks and get a little more creative with our plugins.