Andrey Listopadov

PF4J and Clojure

@programming clojure lisp ~15 minutes read

Today we’ll take a look at an interesting Java library, called PF4J, which describes itself as a Plugin Framework for Java. The main purpose of this library is to provide a way of detecting, initializing, and using plugins to extend your Java application with new features without the need to modify the code. The official repository has examples of how to use this library with Java, and Kotlin, but there is no Clojure example.

Recently I had to find a way how to use this library with Clojure, and it has turned a bit hard to achieve, because this library is very Javaish, if you know what I mean :). Classes, interfaces, and Java annotations are not the most common thing to see in Clojure, as we usually have no classes, interfaces, or annotations, but functions, data abstractions, and metadata. I’m not very experienced in Java bytecode inspection, and Clojure class generation, and I was unable to find any information on how to integrate PF4J and Clojure, so this post will explain what I’ve found so far. You know, so I would be able to refresh this if I ever again will need to use this library, or will find myself in a similar situation where I need to work with annotations or class generation. And hopefully, this information will be useful for others as well.

So here we will write an example plugin, in the same way how Java example plugins are written in the pf4j demo application. But first, let’s look at how the Java plugin is structured.

Anatomy of a plugin

The complete plugin consists of two main components - the plugin itself, and the extension point. We will take the .jar approach for storing and distributing plugins, as it is the easiest one to reach, and we can directly compare the bytecode of the Java plugin, and Clojure plugin. So let’s first create an extension point in Java.

Create project Greeting in your Java IDE of choice. I’m creating a maven project, so the first thing we’ll need is to add pf4j dependency to our pom.xml:

<!-- snip -->
<dependencies>
  <dependency>
    <groupId>org.pf4j</groupId>
    <artifactId>pf4j</artifactId>
    <version>3.6.0</version>
  </dependency>
</dependencies>
<!-- snap -->

Now we can create extension point class org.example.Greeting, and add this code into it:

package org.example;

import org.pf4j.ExtensionPoint;

public interface Greeting extends ExtensionPoint {
    String getGreeting();
}

This is all we need for our small extension point, and we can install it in our local repo with mvn install. AFAIU, extension point’s main purpose is to help PF4J detect particular plugins, and not all plugins.

Now we need the plugin itself. This plugin depends on both pf4j and this extension point we’ve just created and installed. Create a new project JavaWelcomePlugin, and add there pf4j dependency as explained above, and the org.example.Greeting dependency:

<!-- snip -->
<dependency>
  <groupId>org.example</groupId>
  <artifactId>Greeting</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
<!-- snap -->

Again, create a new class GreetingsPlugin:

package org.example;

import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.example.Greeting;

public class GreetingsPlugin extends Plugin {
    public GreetingsPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }

    @Override
    public void start() { System.out.println("Java plugin start"); }

    @Override
    public void stop() { System.out.println("Java plugin stop"); }

    @Extension
    public static class Welcome implements Greeting {
        public String getGreeting() {
            return "Hi from Java!";
        }
    }
}

This plugin has start and stop methods that pf4j will call during initialization, and deinitialization of the plugin, and the main part here is the nested Welcome class. This class implements our Greeting extension point, and is annotated with @Extension, which helps pf4j discover this class during the build process.

One more thing we need to add to our pom.xml are new manifest entries. Pf4j expects each plugin to have at least two entries in MANIFEST.mf file in the .jar archive: Plugin-Version, and Plugin-ID. In our case, we will also need to add Plugin-Class entry, as we’ve created a wrapper class GreetingsPlugin. Add this to the pom.xml:

<!-- snip -->
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifestEntries>
            <Plugin-ID>java-welcome-plugin</Plugin-ID>
            <Plugin-Class>org.example.GreetingsPlugin</Plugin-Class>
            <Plugin-Version>1.0</Plugin-Version>
          </manifestEntries>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>
<!-- snap -->

It’s a bit too verbose (well, it is XML after all), but here we’re just adding these three entries to our manifest file during the build process. Now let’s package this plugin with the mvn package command.

This makes the complete example plugin in Java - much like the one in the demo application from PF4J examples. Both the interface and the plugin are written, and we can plug this code into any application, that looks for our Greeting interface. Now let’s test this plugin in a small Clojure program.

Writing Clojure program that uses plugins

Let’s create a small project based on the app template, that uses pf4j library, and loads this plugin we’ve just written in Java. I’m using Leiningen for project automation, but you may use deps or other tools if you prefer. But we will later need to edit project.clj and do some tweaks other than adding dependencies, so if you’re using deps you’re on your own here. For me, the command is:

$ lein new app pf4j-clj-app

A new project is created and we need to add org.pf4j/pf4j and org.example.Greeting dependencies to the :dependencies section in project.clj:

:dependencies [[org.clojure/clojure "1.10.1"]
               [org.pf4j/pf4j "3.6.0"]
               [org.example/Greeting "1.0-SNAPSHOT"]]

Rest of the project.clj can be left as is. Now, let’s write our -main function in the core.clj:

(ns pf4j-clj-app.core
  (:import [org.pf4j JarPluginManager]
           [org.example Greeting])
  (:gen-class))

(defn -main
  [& _]
  (let [plugin-manager (JarPluginManager.)]
    (doto plugin-manager
      (.loadPlugins)
      (.startPlugins))
    (let [greets (.getExtensions plugin-manager Greeting)]
      (println "Found" (count greets) "extensions for extension point" (.getName Greeting))
      (doseq [greet greets]
        (println (.getGreeting greet))))
    (doto plugin-manager
      (.stopPlugins)
      (.unloadPlugins))))

I’ll keep this brief - we’re importing our Greeting extension point, and the pf4j library, creating plugin-manager, loading and starting plugins. Then we print how many plugins we’ve found for the Greeting extension point, and execute the .getGreeting method for each plugin. And after that, we’re stopping and unloading plugins to keep things clean.

By default pf4j searches for jar plugins in the plugin directory at the root of the project. Let’s create this directory, copy our Java plugin there, and start our application with lein run:

$ mkdir plugins
$ cp path/to/JavaWelcomePlugin/target/JavaWelcomePlugin-1.0-SNAPSHOT.jar ./plugins/
$ lein run
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Java plugin start
Found 1 extensions for extension point 'org.example.Greeting'
Hi from Java!
Java plugin stop

It works! We’ve seen the message from the start method, the getGreeting call result, and then the stop method message. Now let’s do everything in Clojure!

Clojure ExtensionPoint implementation

To make an extension point in Clojure, let’s create a new library project with lein new clj-greeting, and add needed changes to our project.clj:

(defproject org.example/Greeting "1.0-SNAPSHOT"
  :description "pf4j extension poin example in Clojure"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.pf4j/pf4j "3.6.0"]]
  :aot :all)

I’ve mainly changed the project name from clj-greeting to org.example/Greeting so we could hot-swap our Java extension point to the Clojure one with simple lein install. Similarly, the version was changed to 1.0-SNAPSHOT to match the version of our Java extension point. The pf4j dependency was added to the :dependencies key. Lastly, :aot :all entry was added to make sure we AOT compile every file.

Now we can create our interface. To do so, first we need to create needed directories with src/org/example directories, and move core.clj to src/org/example/greeting.clj:

$ mkdir -p src/org/example
$ mv src/clj_greeting/core.clj src/org/example/greeting.clj
$ rmdir src/clj_greeting

Next, we add this code to greeting.clj:

(ns org.example.greeting
  (:import org.pf4j.ExtensionPoint))

(gen-interface
 :name org.example.Greeting
 :extends [org.pf4j.ExtensionPoint]
 :methods [[getGreeting [] String]])

Here, we’re creating an interface with gen-inteface and naming it to org.example.Greeting. This interface extends the org.pf4j.ExtensionPoint class, and exposes one method getGreeting which takes zero arguments, and returns a String.

Let’s package this as a jar archive with lein jar command, and see its contents with zipinfo. If you don’t have zipinfo you can use any other way to view archives. Here are the contents:

lein jar && zipinfo -1 target/Greeting-1.0-SNAPSHOT.jar
Compiling clj-greetings.core
Created /home/alistopadov/Git/clj-greetings/target/Greeting-1.0-SNAPSHOT.jar
META-INF/MANIFEST.MF
META-INF/maven/org.example/Greeting/pom.xml
META-INF/leiningen/org.example/Greeting/project.clj
META-INF/leiningen/org.example/Greeting/README.md
META-INF/leiningen/org.example/Greeting/LICENSE
META-INF/
META-INF/maven/
META-INF/maven/org.example/
META-INF/maven/org.example/Greeting/
META-INF/maven/org.example/Greeting/pom.properties
org/
org/example/
org/example/Greeting.class
clj_greetings/
clj_greetings/core__init.class
clj_greetings/core$loading__6721__auto____171.class
clj_greetings/core$fn__173.class
clj_greetings/core.clj

We’re mainly interested in the org/example/Greeting.class entry. Let’s decompile it. This can be either done with Intellij Idea or by using fernflower directly. The decompiled bytecode looks like this:

package org.example;

import org.pf4j.ExtensionPoint;

public interface Greeting extends ExtensionPoint {
   String getGreeting();
}

And this looks exactly like the Java example we’ve written before. Great, let’s test it!

We do this by installing this extension point in place of our Java one, by using the lein install command (you can remove the old extension from the .m2 directory for extra safety). Then we need to repackage our Java plugin and copy it again to our Clojure application. When we run it, we should see the same output:

Java plugin start
Found 1 extension for extension point org. example.Greeting
Hi from Java!
Java plugin stop

So, our extension point is now implemented in Clojure. Now we can write the plugin itself!

PF4J plugin in Clojure

To write a plugin let’s create another project with lein new clj-welcome-plugin. Again, we need to alter project.clj a bit:

(defproject clj-welcome-plugin "0.1.0-SNAPSHOT"
  :description "pf4j plugin example in Clojure"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [org.pf4j/pf4j "3.6.0"]
                 [org.example/Greeting "1.0-SNAPSHOT"]]
  :manifest {"Plugin-Id" "clojure-welcome-plugin"
             "Plugin-Version" "0.1.0"
             "Plugin-Class" "clj_welcome_plugin.GreetingsPlugin"}
  :aot :all)

Again, we’re adding :aot :all entry and the :manifest key with a map of keys and values of manifest entries we need to add. Thankfully, Leiningen makes it really easy to deal with this, compared to pom.xml we’ve had to modify previously.

Now, we need to create a wrapper class, and the @Extension class, and here’s a problem. To my knowledge, there’s no way to write nested classes in Clojure. And annotations are also a bit finicky. So let’s approach this step by step.

The first thing we’ve done in Java code was import all needed libraries, and create a class that extended Plugin class:

package org.example;

import org.pf4j.Extension;
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;
import org.example.Greeting;

public class GreetingsPlugin extends Plugin {
    public GreetingsPlugin(PluginWrapper wrapper) {
        super(wrapper);
    }
    /* ... */
}

Let’s do the same in Clojure. We start by renaming the core.clj to greetings_plugin.clj, and importing all needed libraries, also saying that our :gen-class has no main and :extends org.pf4j.Plugin:

(ns clj-welcome-plugin.greetings-plugin
  (:import [org.pf4j Plugin PluginWrapper Extension]
           [org.example Greeting])
  (:gen-class
   :name clj_welcome_plugin.GreetingsPlugin
   :main false
   :extends org.pf4j.Plugin))

So far so good. Now we need a constructor, but in our case, Clojure has already generated one, which we can see by compiling our code into the jar file, and looking through the bytecode:

package clj_welcome_plugin;

/* Clojure imports */
import org.pf4j.Plugin;
import org.pf4j.PluginWrapper;

public class GreetingsPlugin extends Plugin {
   /* some initialization Clojure stuff */

   public GreetingsPlugin(PluginWrapper var1) {
      super(var1);
   }

   /* rest of the Clojure stuff */
}

A custom constructor can be provided by using the :init key in :gen-class, but I’ll leave it out to the reader.

Now we can add start and stop methods:

(defn -start [_]
  (println "Clojure plugin start"))

(defn -stop [_]
  (println "Clojure plugin stop"))

And this basically completes our wrapper class, so how do we add a nested class? Well, actually, from a bytecode standpoint this is not a nested class, and the outer class simply acts as a namespace. We can actually see that in the contents of the jar file which contains compiled code of our Java plugin:

org/example/GreetingsPlugin.class
org/example/GreetingsPlugin$Welcome.class

So the inner public static Welcome class is actually a separate .class file, and we can replicate this in Clojure pretty easily. Let’s add another call to gen-class in the same file:

(gen-class
 :name ^{org.pf4j.Extension {}} clj_welcome_plugin.GreetingsPlugin$Welcome
 :prefix "impl-"
 :implements [org.example.Greeting])

(defn impl-getGreeting [_] "Hi from Clojure!")

Or we can create another file, and use :gen-class there, but to match Java plugin source code layout, I’m using the single file.

The most important thing here is the :name key. You can see that we’re providing a metadata entry ^{org.pf4j.Extension {}} which is a way to add Java annotation to a class. And we’re naming our class as clj_welcome_plugin.GreetingsPlugin$Welcome. Lastly, the :implements key is used to tell, that our class implements our extension point. And because we’re using the same file, we change :prefix to "impl-", and the getGreeting method is defined as impl-getGreeting.

Let’s compile our code with lein jar again, and see if we’ve managed to replicate the .class layout in the jar archive:

$ lein jar && zipinfo -1 target/clj-welcome-plugin-0.1.0-SNAPSHOT.jar
...
META-INF/MANIFEST.MF
META-INF/leiningen/...
META-INF/maven/...
clj_welcome_plugin/GreetingsPlugin.class
clj_welcome_plugin/GreetingsPlugin$Welcome.class
...

Great! Let’s see if our GreetingsPlugin$Welcome.class actually decompiles to correct class:

package clj_welcome_plugin;

/* Clojure imports */
import org.example.Greeting;
import org.pf4j.Extension;

@Extension
public class GreetingsPlugin$Welcome implements Greeting {
   /* Clojure stuff */
   public String getGreeting() {
      Var var10000 = getGreeting__var;
      Object var1 = getGreeting__var.isBound() ? var10000.get() : null;
      if (var1 != null) {
         return (String)((IFn)var1).invoke(this);
      } else {
         throw new UnsupportedOperationException("getGreeting (clj-welcome-plugin.greetings-plugin/impl-getGreeting not defined?)");
      }
   }
}

And it is! We can see the @Extension annotation, and public class GreetingsPlugin$Welcome that implements Greeting extension point. Later, after some Clojure internal code, we can see our getGreeting method definition.

However, if we try to use this plugin, it will not work just yet:

$ cp path/to/clj-welcome-plugin/target/clj-welcome-plugin-0.1.0-SNAPSHOT.jar ./plugins/
$ lein run
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Java plugin start
Found 1 extensions for extension point 'org.example.Greeting'
Hi from Java!
Java plugin stop

Still, only the Java plugin runs. This took me some time to figure out why it doesn’t work - the .class files layout of the resulting .jar archives are the same. Decompiled classes, except for the Clojure stuff, look the same as Java classes.

The catch is, that when we’re building our Java plugin with mvn package, the extra file extensions.idx is created in the META-INF directory of the archive. Let’s look at this file:

# Generated by PF4J
org.example.GreetingsPlugin$Welcome

Aha! This file contains the necessary info to detect the @Extension annotated class in the jar archive and is generated by processing Java annotation during compilation. Unfortunately, the Clojure compiler doesn’t try to process our metadata as Java annotation, and hence no file is created. But we can add this file by hand, by creating META-INF/extensions.idx in the resources directory with the following contents:

clj_welcome_plugin.GreetingsPlugin$Welcome

Here, we’re indicating that our @Extension class is clj_welcome_plugin.GreetingsPlugin$Welcome. Let’s re-package our plugin with lein jar, and re-run our app:

$ cp path/to/clj-welcome-plugin/target/clj-welcome-plugin-0.1.0-SNAPSHOT.jar ./plugins/
$ lein run
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Clojure plugin start
Java plugin start
Found 2 extensions for extension point 'org.example.Greeting'
Hi from Clojure!
Hi from Java!
Java plugin stop
Clojure plugin stop

It works! Now we can make a more complex app, and extend it with plugins written both in Java and Clojure. Of course, more complex plugins will be bigger and have more code, but the general idea will be the same.

Some ending notes

You don’t have to put both the wrapper class and the Extension class in the same file. I mainly did this because it is in line with the Java code, so sources could be compared mostly directly. But a separate file, that sets the class name in a similar way is a more clean approach and should be preferred.

There’s also no need to rename most of the classes, except for the @Extension one, as we have to explicitly make it into a nested class via $ notation. Again, I’ve mainly done this to keep more or less the same code structure with the Java examples, for easier direct comparison.

Lastly, some automation may be needed to generate extensions.idx file based on source code. Unfortunately, I’ve not found any info on how to make Java annotation processing work with Clojure metadata annotations. So I’ve done this manually, but I’ve also written this script, that analyzes Clojure source code as EDN, and searches for given extension point class, printing class name from gen-class form if present:

#!/usr/bin/env bb

(require '[clojure.edn :as edn])
(require '[clojure.string :as str])

(defn analyze-gen-class
  "Tests if `form` is a `gen-class` and gets the `:name` or uses the
  `default` if `:name` is not set, but `:implements` matches the
  `query`.  Replaces all dashes with underscores in case the `default`
  name came from the `ns` form."
  ([form query]
   (analyze-gen-class form query nil))
  ([form query default]
   (when (and (list? form)
              (#{:gen-class 'gen-class} (first form))
              (some #{:implements} form))
     (let [gencl (->> form
                      (drop 1)
                      (partition 2)
                      (into {} (map vec)))]
       (when-let [name (and (= (:implements gencl) query)
                            (get gencl :name default))]
         (str/replace name #"-" "_"))))))

(defn analyze-ns
  "The `ns` form already contains class name as a namespace name,
  but we check inner `:gen-class` for a `:name` override."
  [form query]
  (let [name (second form)]
    (some #(analyze-gen-class % query name) form)))

(defn analyze-form
  "Check whether current `form` is a `gen-class` or `:gen-class` expression.
  If it is, check if gen-class has `:implements` key that is equal to
  `query` and gets the value of the `:name` key.  If `form` is an `ns`
  form finds `:gen-class` in it and checks it as above.  Recursively
  analyzes nested lists.  Terminates at first successful analysis."
  [query form]
  (when (list? form)
    (cond (= 'ns (first form)) (analyze-ns form query)
          (some list? form) (some #(analyze-form query %) form)
          :else (analyze-gen-class form query))))

(let [[query] *command-line-args*]
  (doseq [form *input*]
    (when-let [name (analyze-form
                     (edn/read-string query)
                     form)]
      (println name))))

This is a Babashka script, which analyzes S-expressions, and can be used together with another tool for parsing EDN - Jet (huge thanks to Michiel Borkent for awesome Clojure tooling!):

$ jet -c < src/clj_welcome_plugin/greetings_plugin.clj | ~/script.clj "[org.example.Greeting]"
clj_welcome_plugin.GreetingsPlugin$Welcome

Or, similarly for every .clj file in the project:

$ find -name '*.clj' -exec sh -c 'jet -c < "{}" | ~/script.clj "[org.example.Greeting]"' \;
clj_welcome_plugin.GreetingsPlugin$Welcome

For this project, the script finds the extension class name correctly from the source code we’ve given to it, but it is more a proof of concept, and by far is not a well-tested solution.

One particular point to improve is the fact that we have to pass "[org.example.Greeting]" to the script, to help it find gen-class entries that implement the interface we’re interested in. A much better solution would be to parse metadata, but unfortunately, I was not able to make Jet keep metadata in the resulting EDN, and I’m not sure how to properly read Clojure source code files otherwise. Although, it’s not really hard to check if all classes are present, as I highly doubt that there will be more than 5 extension classes presented in a single plugin, so this solution is good enough for now.

Other than that, the process of using the PF4J library is pretty straightforward, and we can fully utilize this library from Clojure projects with some hand tweaking. Thanks for reading!