PlaceholderAPI

API designdevJava

A system designed to make replacing variables in text a breeze.

PlaceholderAPI is an API for Sponge that was designed to replace variables in complex text objects. The API’s goal was to serve as a simple yet powerful bridge between plugins that had information and plugins which wished to use said information to display to the user. This project is built using Java on Sponge API v6 and v7.

The initial purpose of this project was to replace a similar project for a different platform. Sponge, in the grand scheme of Minecraft server backends, is relatively young, and so to help catch it up to the other platforms, namely Spigot in this case, one of the tasks was to create such a placeholder system. From this need, PlaceholderAPI was born.

As development work on PlaceholderAPI proceeded, a number of new needs would come up that would require complete overhauls or rewrites, especially regarding performance and ease of use. One of the key pieces of the API is being able to operate with little overhead while doing a lot of work, a very important task for Minecraft servers whose average compute and memory space is very low. In order to achieve this, many attempts were made to interface more powerfully with Java’s APIs. The end result is a bit hacky, unfortunately, but it yields the best performance given the task at hand.

The API flow is fairly straightforwards: Firstly, the API registers itself into Sponge. Any placeholders are then added afterwards, completing the intial steps of the API’s lifecycle, though in theory placeholders could be added at any time. Next, any plugins which wish to use the replacers would then get a reference to the API, which they could simply call with text and the resulting text would have the variables replaced.

In order to register placeholders, a straightforward system built on annotations was devised, and looked something like the following:


    @Placeholder("player_name")
    public String playerName(@Source CommandSender player) {
      // ... logic goes here
      return output;
    }

Then, to register, simply calling the API with the file containing that function would suffice to add it. Then, when it would be used, calling the api with a string similar to Hello, %player_name% would result in the text being replaced, something like Hello, Sam.

In order to transform any random function, like the above, into a usable API, there are a number of approaches that could be taken.

  1. Firstly, Java’s base Reflection API could be used, which allows you to simply find and call invoke on any methods matching a filter. The caveat of this method is that it is extremely slow, resulting in code execution times upwards of 3 times slower than direct calls in some cases.
  2. The next, more improved method would be to use MethodHandles to call the code using invokeExact, which would perform fewer runtime checks. However, this was still slower and results in at least one more level of indirection at best, and would be 1.1 to 2 times slower than direct calls.
  3. The third approach was to use Lambda Factories, since those were surprisingly very performant (near native at 1.0-1.1 times slower), however limitations on the types of functions they could call would rule out this option.
  4. The last option, the one that was settled upon, was to compile code at runtime to reference these functions. This is possible thanks to the JVM’s ability to compile .class files at runtime, and so if we inject our own, the JVM would be none the wiser. A transformer was created to, when a placeholder was registered, convert the method from a reflected Method type into a class, which matched a known interface, that would directly call the method after performing some logic to transform and cast types. This technique resulted in very performant code, though with slightly higher memory usage.

Most of the code in the API that doesn’t deal with the above situation simply exists to store placeholders, parse text and populate placeholders in that text, which is much simpler.

Going forward, there is likely no work that makes sense to do for this project; Sponge has since integrated a native version of the API, and it covers enough of the use case that there is no longer a niche for this plugin to fill.

I learned a lot of really valuable lessons over the years of working on this project, from performance to security. This project served as a valuable experimentation ground to really dig deep into how the JVM works, how to architect stable and sensible APIs, and how to design large scale and popular projects.