Resource How to load external jar files into your plugin! (Guide)

Discussion in 'Spigot Plugin Development' started by burchard37, Jul 2, 2021.

  1. Hello, so I am sure we are all aware of plugins like Bento Box, Token Enchant being able to load external jars. I've seen a few different people ask me this in my few years of plugin development, and I had actually created a system that loads external jars that you can inject to your plugin as an Addon! Before I start this is going to be a major spoon feed because you should use this as a library, however I will explain what EVERYTHING does in this guide!

    Step 1:

    Firstly, you will need to create a new Project on your IDE, lay this out just like how you would a normal Spigot plugin, however instead of naming the .yml file "plugin.yml" you will name it "addon.yml" instead, and include these fields in your file (Create a Main class for the addon but just make it an empty class for now, we will come back to this):

    Code (Text):

    MainClass: "path.to.your.main.Class"
    Name: "NameOfYourAddon"
    Version: "1.0"
    Description: "Description here"
     
    Lets explain what we are doing here, we need the MainClass field because this system will need to open this jar file and find the main class, so this is how the system will find the main class and enable your plugin!

    Step 2:

    Now go back to your main plugin your working on (The one your wanting to be the "Addon" manager basically) You will need to create a abstract class for your Addons to extend, here we will do it just like this:

    https://hastebin.com/ukumeganoz.csharp (Like is said this is a GIANT spoon feed, but lets explain what's happening so at least someone can take the time and read through everything)

    IMPORTANT The objects named YourPlugin need to be changed to the name of whatever main class is extending JavaPlugin

    If you look at this abstract class, you will notice it actually has a lot of similarities with JavaPlugin. I'm not going to go over the setters and getters for addon name description (Also you should NOT use the setters as the AddonLoader sets these for you) etc.

    Addon#getDataFolder() - This does exactly what JavaPlugin#getDataFolder() does, it gets the folder for that addon so you can get custom configs and the main config.yml

    Addon#getResource(String s) - This does exactly what JavaPlugin#getResource(String s) does. This is where things get complicated really fast, since Jar files are a type of Zip file, we are able to open these very easily with ZipEntry and ZipFile, and from there we can just recall the ZipEntry's InputStream from the ZipFile we selected!

    Addon#saveResource(String s, boolean b) - Hey did you know things get more complicated? So firstly this will do the same thing as JavaPlugin#saveResource(String s, boolean b), Basically we runa few null checks and just make sure directories exist in the first few lines, then we use the InputStream we have and open it to a BufferedReader and BufferedWriter, the BufferedReader will be the String of the file your saving, and BufferedWriter is the file it will write to (basically the same file but actually inside /plugins folder)

    Addon#saveDefaultConfig() - Uhm, obviously by now you get the patter, it does the same as JavaPlugin#saveDefaultConfig() we basically use the saveResource and getResource methods to handle the default config now!

    Im not going over config getters and save config, those are very much self explainable.

    Step 3:

    Like I said this guide is a huge spoon feed but this is a very large thing to create, I will again try to explain EVERYTHING in this file, its your choice whether you copy paste or actually read this guide to understand what's happening

    Next up is the exciting part: Actually loading the addons. So before we even get started, you need to know where to actually place addons, this system will automatically create a directory called "addons" inside your plugins folder (EG: plugins/YourPlugin/addons), then this system will attempt to read every single .jar file in that directory

    Here is the AddonLoader class: https://hastebin.com/anozipanuy.java

    So first off, you see that YourPlugin variable at the top and inside the constructor? Change it to whatever class is extending JavaPlugin in your plugin and lets get on to explaining what happens

    The Constrcutor - Its here where we create the /addons folder and check if it exists, from there after its created, we check the contents of the folder by calling File#listFiles() and if there is no addons there is no need to load any addons so we will just called return. From there if there is any files we will loop through each File that ends with .jar as there's no need to use any other plugins

    Quick Explanation of another method: there is a method called getZipFile(args), this will simple just open the ZipFile into a InputStream for us, onto the rest of The Constructor:

    Once we have the input stream from one of the files in /addons, we call another method:

    Quick Explanation of another method: There is a method called getYamlFromStream(args) this pulls the addon.yml file from your addon jarfile so that we can get all the field (refer to step 1). onto the rest of The Constructor:

    Once we have the Addon .jar file and the Addons we finally start to actually dissect the .jar file with another method:

    QuickExplanation of another method: This is easily the hardest part of this project because many many people have never touched ClassLoader, I really cant explain much here but basically this method first pulls all the field from the addons addon.yml and null checks them. Once we do this we grab any ClassLoade for this one I just decided to use this once we have the ClassLoader we need to create a URLClassLoader, after we have this object, we pass it to a Class<?> what Class<?> means is an anonymous class, this can be any class in java but you cant really do to much with this alone. Next we have a Class<? extends Addon>, this means we have a Class (?) and we know that this class extends Addon (Errors will be thrown accordingly), and now, the most fun part: Actually getting the Addon instance! All you do is getDeclaredConstructor() from the Class<? extends Addon> and then call newInstance() which will return your Addon class! After we do this we set a few Addon variables we may need for later and return the instance of Addon we just created

    Now that we have a Addon object, you can see we call Addon#onEnable()? Your class extending Addon will need a onEnable and onDisable, and anything inside onEnable gets called right inside this class and the addon is now considered "Enabled" and is now running (see next step) please dont ditch this guide here because theres still MANY important aspects to this to actually make it fully functional

    Step 5

    Now, we have a Addon abstract class and a AddonLoader class inside your main plugin, and your Addon plugin should have your addon.yml and a empty class in your addon.

    Now its actually time to start writing the addon a little bit, go back to your Addon project, and your Main class needs to extend Addon, but your probably wondering "But how do I import this", well this is really up to YOU, when I had this plugin running on my server I had my AddonLoader plugin on github and I used JitPack (https://jitpack.io/) to get my maven dependencies, this is entirely up to you, you can even just shade in the main plugins jar into your Addon's plugin, or make a maven import from file (not recommended but its a last resort) If requested ill show you how to import your AddonLoader plugin from file with maven, just reply to this thread and ask for it and ill add it but im not putting it for now for time purposes.

    Your Addon class should look something like this:

    Code (Text):

    public class YourAddon extends Addon {


        @Override
        public void onEnable() {
           
        }

        @Override
        public void onDisable() {

        }
    }
     
    You can put a log in onEnable if you'd like to see this working, once you have all this compile your Addon plugin

    Step 6:

    Awesome, we have the Addon class made and all of our AddonLoaders created, now we need to go back to your main plugin (The one with AddonLoader) and we actually need to call the AddonLoader class to enable this plugin. In your main plugin go to your onEnabled method, we need to shcedule this task for when after all plugins initialize because it simply wont work when you don't, we can achieve this by creating a simple BukkitRunnable in the onEnable method.

    Code (Text):

    new BukkitRunnable() {
         @Override
         public void run() {
              new AddonLoader(core);
         }
    }.runTaskLater(this, 5);

     
    Step 7:

    VERY IMPORTANT!!!!!
    So, since this plugin needs to be enabled after the server is actually started, we cant just create normal commands, we actually need to create a custom command and then inject it into Bukkit's CommandMap (I will also explain Event Regsitering in this please bear with me)

    Any command handler will work that injects to the CommandMap, but of course im going to provide one and go over all the methods with you: https://hastebin.com/opuyeranin.java

    This class is now a Bukkit command, it contains 2 methods you can use onCommand, and they split up into one being ConsoleCommandSender or a Player that sent the command (pretty neat right?)

    To actually enable this you need a method in your plugins (the one extending JavaPlugin):
    Code (Text):

    public void registerCommand(@Nonnull final String commandName, @Nonnull final YourCommandClass command) {
        try {
            final Field commandMapField = Bukkit.getServer().getClass().getDeclaredField("commandMap");
            commandMapField.setAccessible(true);

            final CommandMap commandMap = ((CommandMap) commandMapField.get(Bukkit.getServer()));
            commandMap.register(commandName, command);
        } catch (@Nonnull final IllegalAccessException | NoSuchFieldException ex) {
            ex.printStackTrace();
        }
    }
     
    All this does it get the commandMap from Bukkit and injects it into it during runtime, do note i am not aware how tab completing works with this, I am sorry :(

    To actually use this Command class when creating command, you create these classes inside of your Addon plugin:

    Code (Text):

    public class Test extends YourCommandClass {


        protected Test(@Nonnull String command, @Nonnull String desc, @Nonnull String usage) {
            super(command, desc, usage);
        }

        @Override
        public void onCommand(@Nonnull ConsoleCommandSender console, @Nonnull List<String> args) {

        }

        @Override
        public void onCommand(@Nonnull Player player, @Nonnull List<String> args) {

        }
    }

     
    Step 8:

    Now im going to actually show you how to register command and events with your new addon! Go back to your Addon project and in the onEnable, you can access your ClassLoader's JavaPlugin's class by using this.getPlugin() You can register events the normal vanilla way, you don't need to change for that just make sure your using this.getPlugin(). When you want to register your commands, all you need to do is use this.getPlugin().registerCommand(String commandName, new CommandClass("commandName", "desc", "usage"); and voila your command is successfully registered!

    End of guide, more info:

    I apologize is this was confusing for many people, or if people don't like that this is kind of a giant spoon feed but its a fairly complex system that many people just don't know how it works or how to actually do this yet its a very useful feature for any developer. This is my first ever guide I wrote on SpigotMC so again apologies if things were hard to understand or I didn't explain something fully but I'm glad to answer any questions in the forums, and help if there's any errors, this comes from a old server I used to develop on called MineStar and I made this system to help clean up things and organize our plugins. The original system has many more features on it but this is the basic skeleton of the addon loader we used :)

    If there's error's (or I missed something) tell me and I will update this guide! This should work on most versions of Minecraft since its not using much of the Bukkit API
     
    • Winner Winner x 2
    • Useful Useful x 1
    • Friendly Friendly x 1

  2. This is mainly for things you don't want running as a full plugin and instead just an addon to your plugin (Like Bento Box does), it also allows for your plugins to be more organized instead of shoving a bunch of addons inside /plugins and just cluttering the folder, because you could make a plugin and make it have many many addons (Like TokenEnchant, there's hundreds and shoving all of those inside the /plugins is not very ideal)

    So not necessarily many advantages in terms where it comes to speed or efficiency I would assume there's not much of a difference but I've never used ServicesManager before so I'm not sure, this is mainly organization purposes and accessing Addons easier because you can save the Addon instances inside of an AddonsHolder and access methods from it directly, not to mention its very customizable in terms of what you can do, you can add gui's to control each addon if you like, access the Addons directly, even add in load orders and whatnot, this was just the bare minimum in the original plugin there's many advantages like this, I'm sure its achievable with ServicesManager but its extremely simple with this system. It would probably be more user friendly if this was in the form of a Library that way people don't have to copy all these classes over to they're project

    Not to mention, with ServicesManager it doesn't look like you can automatically load addons without hard coding them in to load in, this system automatically loads the jars inside the /addons folder inside of the plugins DataFolder
     
    • Informative Informative x 1
  3. What do you mean "hard coding them to load in"?
     
    • Friendly Friendly x 1
  4. By the looks of it, ServicesManager needs to call register to actually provide the plugin. So by the looks of it you'd need to import your addon into your main plugin, and then call it after grabbing the Addons main class (if the Addon plugin enabled of course), I could be wrong on this part but it looks like how that works
     
  5. The way I got it to work is as follows:
    1. Create an interface in the main plugin, which addons will implement.
    2. In the addon project, have the main project's jar file or online repository as a source in pom.xml, and implement the main plugin's interface in some class.
    3. In the addon project, register an instance of the class that interface the interface in the main plugin, and register it to the ServicesManager as a service provider for the interface it implements.
    4. In the main plugin, get the service manager, and call getRegistrations(Interface.class) (I've only ever done it in response to a command, but you could probably do it in repsonse to ServerLoadEvent).
    5. This returns a Collection of all the objects that have been registered as providers for that interface, which can then be cast to objects of that Interface and used as needed.
    I'm not sure if you meant import as in a java import, but I hope what I've written clears up any uncertainty. Thanks for the guide by the way, I may end up using it for a project I'm working on.
     
    #6 Sigong, Jul 2, 2021
    Last edited: Jul 2, 2021
    • Winner Winner x 1
  6. For the long term availability of the these pastes, I'd avoid hastebin because they expire after 30 days. GitHub Gists may be more suited for this, perhaps.
     
    • Friendly Friendly x 1
  7. Nice guide, when I was making addons for my plugin I kinda had to do everything on my own. This is nice for everyone that is starting with class loaders.
     
    • Friendly Friendly x 1
  8. HasteBin seems to have removed the links, are you able to post them again as I'd be interested in taking a look. :)

    Edit: Seemed to be a HB issue.
     
    #9 MrFishCakes, Jul 2, 2021
    Last edited: Jul 2, 2021
    • Friendly Friendly x 1

  9. Actually didnt consider that, I will get some githust gists up within the next day or 2 :) I may even consider adding source codes to a repo but for the moment I'm working on a rather big plugin so its consuming most of my time, the gists will be up first and ill update the post accordingly!
     
  10. You can also use module system, to activate/desactivate some features. It's very useful with this tutorial.