About Plugins

Javalin’s plugin system enforces a consistent API so that end users can rely on the same patterns across all plugins.

This how-to guide will walk you through the plugin system and how to write your own plugins.

What is a plugin?

A plugin is a piece of code that can be added to Javalin to extend Javalin’s functionality. Typically, this means adding some handlers to the Javalin instance, or modifying the configuration, or adding some functionality to the Javalin Context. Examples of plugins could be an integration with a database, some code which does authentication, an integration with a rate-limiting service, template rendering, etc.

How does the Plugin API look?

To write a plugin in Javalin, you need to extend the Plugin class. This class can be a bit intimidating, but we will walk you through it step by step. There are also several examples in the later sections.

The Plugin class

abstract class Plugin<CONFIG>(userConfig: Consumer<CONFIG>? = null, defaultConfig: CONFIG? = null) {

    /** Initialize properties and access configuration before any handler is registered. */
    open fun onInitialize(state: JavalinState) {}

    /** Called when the plugin is applied to the Javalin instance. */
    open fun onStart(state: JavalinState) {}

    /** Checks if plugin can be registered multiple times. */
    open fun repeatable(): Boolean = false

    /** The priority of the plugin that determines when it should be started. */
    open fun priority(): PluginPriority = PluginPriority.NORMAL

    /** The name of this plugin. */
    open fun name(): String = this.javaClass.simpleName

    /** The combined config of the plugin. */
    @JvmField
    protected var pluginConfig: CONFIG = defaultConfig?.also { userConfig?.accept(it) } as CONFIG
}

Every function in the Plugin class has a default implementation, so you only need to override the functions that you actually want to use. The Plugin class also accepts a generic type parameter CONFIG, which is the type of the configuration object that you want to use. If you don’t want to use a configuration object, you can use Void/Unit as the type parameter.

The ContextPlugin class

If you want to add functionality to the Context class, you can extend the ContextPlugin class, which in turn extends the Plugin class. This class has a generic type parameter EXTENSION, in addition to the CONFIG type parameter from the Plugin class:

abstract class ContextPlugin<CONFIG, EXTENSION>(
    userConfig: Consumer<CONFIG>? = null,
    defaultConfig: CONFIG? = null
) : Plugin<CONFIG>(userConfig, defaultConfig) {
    /** Context extending plugins cannot be repeatable, as they are keyed by class */
    final override fun repeatable(): Boolean = false
    abstract fun createExtension(context: Context): EXTENSION
}

The ContextPlugin class has a function createExtension, which is called when the user calls ctx.with(Plugin::class). This function should return an instance of the extension class. It also overrides the repeatable function to always returns false. This is necessary because context extensions are keyed by class, so you can only have one instance of each context extension.

This is all a bit complicated, but it will become clearer when we look at some examples. We will be creating a rate limiting plugin, which we will name Ratey.
Typically, a plugin will be named after the functionality that it provides, or after the library that it integrates.

Plugin examples

This section will walk you through the process of creating a plugin. We will start with a simple plugin, and then we will add some more functionality to it in each section.

Creating a simple plugin (no config, no extension)

The simplest plugin is one that doesn’t have a config, and doesn’t create a Context extension. All you have to do in this case is extend the Plugin class, and override any functions that you want to use. We will use Void as the config type, since we don’t need a config.

Let’s create a plugin named Ratey that does rate limiting:

class Ratey extends Plugin<Void> {
    int counter;
    @Override
    public void onStart(JavalinState state) {
        state.routes.before(ctx -> {
            if (counter++ > 100) {
                throw new TooManyRequestsResponse();
            }
        });
    }
}
class Ratey : Plugin<Void>() {
    var counter = 0

    override fun onStart(state: JavalinState) {
        state.routes.before { ctx ->
            if (counter++ > 100) {
                throw TooManyRequestsResponse()
            }
        }
    }
}

In order to use this plugin, you need to call JavalinConfig#registerPlugin:

var app = Javalin.create(config -> {
    config.registerPlugin(new Ratey());
});
val app = Javalin.create { config ->
    config.registerPlugin(Ratey())
}

This will register the plugin. The onStart function will be called when Javalin starts, so our rate-limiting code will be executed for every request. This plugin is currently quite terrible, as it will rate-limit all requests, and it has a hardcoded limit of 100 requests. Let’s make it a bit more flexible by adding a config.

Creating a plugin with config (and no extension)

Let’s say that we want to be able to configure the rate-limiter limit. We can do this by adding a config to our plugin. We have to use Ratey.Config as our config type, and add a Consumer<Config> to the constructor of our plugin:

class Ratey extends Plugin<Ratey.Config> { // the Ratey.Config class is the config type
    int counter;
    public Ratey(Consumer<Config> userConfig) {
        super(userConfig, new Config()); // we pass config + default config to the super constructor
    }
    public static class Config {
        public int limit = 1;
    }
    @Override
    public void onStart(JavalinState state) {
        state.routes.before(ctx -> {
            if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field
                throw new TooManyRequestsResponse();
            }
        });
    }
}
class Ratey(userConfig: Consumer<Config>) : Plugin<Ratey.Config>(userConfig, Config()) {
    // the Ratey.Config class is the config type
    // we need to pass the config + default config to the super constructor
    // this will merge the user config with the default config
    var counter = 0
    class Config {
        var limit = 1
    }

    override fun onStart(state: JavalinState) {
        state.routes.before { ctx ->
            if (counter++ > pluginConfig.limit) { // we can access the config through the pluginConfig field
                throw TooManyRequestsResponse()
            }
        }
    }
}

Now, we can configure our plugin when we register it:

var app = Javalin.create(config -> {
    config.registerPlugin(new Ratey(rateyConfig -> {
        rateyConfig.limit = 100_000;
    }));
});
val app = Javalin.create { config ->
    config.registerPlugin(Ratey { rateyConfig ->
        rateyConfig.limit = 100_000
    })
}

Note: The reason why Javalin requires a Consumer for the config is to enforce consistency across different plugins. Most of Javalin's built-in functionality uses a Consumer for the config, and we want to make sure that all plugins follow the same pattern.

While our plugin is better now, it’s still not good. We are only able to limit the number of requests, but ideally we want to be able to do this per user and have different costs per endpoint. Let’s add a Context extension to our plugin.

Creating a plugin with config and extension

For this example, we want to be able to limit the number of requests per user, and have different costs per endpoint. Instead of a before-handler, we will create an extension to the Context class, which we will let users call with ctx.with(Ratey::class). This will return an instance of our extension class.

Let’s extend the ContextPlugin class in our Ratey plugin and add a createExtension function:

class Ratey extends ContextPlugin<Ratey.Config, Ratey.Extension> {
    public Ratey(Consumer<Config> userConfig) {
        super(userConfig, new Config());
    }

    // map of ip to counter, to keep track of the number of requests per ip
    Map<String, Integer> ipToCounter = new HashMap<>();

    // called when the user calls ctx.with(Ratey.class), should return an instance of the extension class
    @Override
    public Extension createExtension(@NotNull Context context) {  
        return new Extension(context);
    }

    // the config class that is used in JavalinConfig#registerPlugin
    public static class Config {
        public int limit = 1;
    }
    // this is an inner class, so it has access to the ipToCounter property of the outer class
    public class Extension {
        private final Context context;

        public Extension(Context context) {
            this.context = context;
        }

        public void tryConsume(int cost) {
            String ip = context.ip();
            int counter = ipToCounter.compute(ip, (k, v) -> v == null ? cost : v + cost);
            if (counter > pluginConfig.limit) {
                throw new TooManyRequestsResponse();
            }
        }
    }
}
class Ratey(userConfig: Consumer<Config>) : ContextPlugin<Ratey.Config, Ratey.Extension>(userConfig, Config()) {
    // map of ip to counter, to keep track of the number of requests per ip
    val ipToCounter = mutableMapOf<String, Int>()
    // this function is called when the user calls ctx.with(Ratey::class), 
    // and should return an instance of the extension class
    override fun createExtension(context: Context) = Extension(context)
    // the config class that is used in JavalinConfig#registerPlugin
    class Config(var limit: Int = 0)
    // this is an inner class, so it has access to the ipToCounter property of the outer class
    inner class Extension(var context: Context) {
        fun tryConsume(cost: Int = 1) {
            val ip = context.ip()
            val counter = ipToCounter.compute(ip) { _, v -> v?.plus(cost) ?: cost }!!
            if (counter > pluginConfig.limit) {
                throw TooManyRequestsResponse()
            }
        }
    }
}

Now, we can use our plugin like this:

var app = Javalin.create(config -> {
    config.registerPlugin(new Ratey(rateyConfig -> {
        rateyConfig.limit = 100_000;
    }));
    config.routes.get("/cheap-endpoint", ctx -> {
        ctx.with(Ratey.class).tryConsume(1);
        ctx.result("Hello cheap world!");
    });
    config.routes.get("/expensive-endpoint", ctx -> {
        ctx.with(Ratey.class).tryConsume(100);
        ctx.result("Hello expensive world!");
    });
}).start(7070);
val app = Javalin.create { config ->
    config.registerPlugin(Ratey { rateyConfig ->
        rateyConfig.limit = 100_000
    })
    config.routes.get("/cheap-endpoint") { ctx ->
        ctx.with(Ratey::class).tryConsume(1)
        ctx.result("Hello cheap world!")
    }
    config.routes.get("/expensive-endpoint") { ctx ->
        ctx.with(Ratey::class).tryConsume(100)
        ctx.result("Hello expensive world!")
    }
}.start(7070)

Now each IP can make 100_000 requests, and the endpoints have different costs associated with them. This is a much better plugin, but it still has some issue. This plugin is not thread-safe, and it never resets the counters or removes old entries from the map. This is not relevant for this guide though, the main purpose was to show the interaction between the Plugin, ContextPlugin and pluginConfig.

Like Javalin?
Star us 😊