Tutorial: Spigot plugins & dependency injection

Discussion in 'Programming' started by MrDienns, Jan 6, 2018.

  1. Introduction
    In this simple guide I will show you the basics of using dependency injection in a Minecraft plugin. This can be used for both Spigot and BungeeCord, but I will use Spigot for the sake of this guide. Many starting developers are facing the problem of instantiating certain classes, mainly the plugin class. As we all (hopefully) know, doing this:
    Code (Java):
    Plugin plugin = new MainPluginClass();
    will crash the plugin, because we cannot instantiate it again due to how Bukkit/Spigot works. Most developers use static as an access modifier for getting around this, like so:
    Code (Java):
    public class MainPluginClass extends JavaPlugin {

        private static MainPluginClass i;
        public static MainPluginClass get() { return i; }

        @Override
        public void onEnable() {
            i = this;
        }

    }
    While it is generally acceptable to do in Minecraft plugins, it is considered to be very bad practise in the real world of developing software. This would be a singleton pattern. However, just because it's a design pattern doesn't mean it's a good one. Using static as an access modifier leaves many problems you'll one day face being a developer. To get around this, we can use dependency injection, which would be the correct way of solving this problem.

    What is dependency injection?
    Let say we want to make some tea. How would we do it and what do we need? We need some boiled water, a clean mug, teabags, sugar and milk (at least that’s what I put into mine). In order to make tea we need to provide all these ingredients and we need to manage all these ourselves. We have to fill the kettle with water and heat it up, get the milk from the refrigerator and get the rest.

    Now assume that we can make tea, by simply asking someone else to do it for us. Someone that knows how to do the tea just the way we want it. Would that be better than making the tea ourselves? If yes, then welcome to dependency injection.
    ~ Java Creed

    Turorial
    For this tutorial, we will make a simple plugin with a command executor and an event handler, with the use of dependency injection. I assume you know how basic plugin setups work, so I will not be explaining that. I will be using Maven for the sake of this guide, but Gradle would work very similar.

    First, into our pom.xml, it is important we add a dependency injection framework. I will be using Google's Guice for this, but there are plenty of other frameworks you can use, such as HK2. Add this to your pom:
    Code (XML):
    <dependencies>
        <dependency>
            <groupId>com.google.inject</groupId>
            <artifactId>guice</artifactId>
            <version>4.0</version>
        </dependency>
    </dependencies>
    We also need to shade this into our plugin once we export it, so add the following to make sure this is done:
    Code (XML):
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.4.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    Be careful to not shade Bukkit & Spigot into your plugin though. Make sure that those dependencies have
    Code (XML):
    <scope>provided</scope>
    added. Add this to every dependency you don't want to be shaded into your plugin, such as Bukkit and Spigot.

    Creating a basic plugin setup
    Let's move on to the actual coding part. We will create the following three classes for our plugin:
    Our main plugin class:
    Code (Java):
    public class DependencyInjectionPlugin extends JavaPlugin {

        @Override
        public void onEnable() {

        }

    }
    A simple command executor:
    Code (Java):
    public class SimpleCommand implements CommandExecutor {

        @Override
        public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {

        }

    }
    A simple event handler:
    Code (Java):
    public class SimpleListener implements Listener {
     
        @EventHandler
        public void onJoin(PlayerJoinEvent e) {
     
        }
     
    }
    You will now have your blank plugin setup. No command executors or listeners are registered so far. We will do that in a moment. First, you need to add a Binder. A binder is a helping class to tell our dependency injectino framework what to do in certain cases. For example, we need to specify that our main plugin class can not be instantiated again, as it would crash our plugin like explained at the top. What we can do in this binder is bind the instance to the class object. To do this, first create a simple binder class:
    Code (Java):
    public class SimpleBinderModule extends AbstractModule {

        private final DependencyInjectionPlugin plugin;

        public SimpleBinderModule(DependencyInjectionPlugin plugin) {
            this.plugin = plugin;
        }

        public Injector createInjector() {
            return Guice.createInjector(this);
        }

        @Override
        protected void configure() {
            // Here we tell Guice to use our plugin instance everytime we need it
            this.bind(DependencyInjectionPlugin.class).toInstance(this.plugin);
        }

    }
    Use the binder
    Now that we created the binder, we need to make use of it. We can change our main class into the following where we actually start making use of injection:
    Code (Java):
    public class DependencyInjectionPlugin extends JavaPlugin {

        @Inject private SimpleCommand command;

        @Override
        public void onEnable() {
            // Fetch dependencies. We only have to do it this way for our main class.
            SimpleBinderModule module = new SimpleBinderModule(this);
            Injector injector = module.createInjector();
            injector.injectMembers(this);

            // Now we can register our command executor
            // Be sure to register the command in your plugin.yml
            this.getCommand("test").setExecutor(this.command);
        }

    }
    You can add some very basic to code to your command handler to make sure the plugin works:
    Code (Java):
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        sender.sendMessage('command registered and working!');
        return true;
    }
    When you execute the command, it should result in:
    [​IMG]

    If you managed to keep up this far, congratulations! You have the first part working. You managed to inject our SimpleCommand dependency into our DependencyInjectionPlugin plugin class. Now we will do the opposite by injecting our DependencyInjectionPlugin plugin class into something else, which works very easily.

    Let's go to our SimpleListener class. Inject the plugin like so:
    Code (Java):
    public class SimpleListener implements Listener {

        @Inject
        private DependencyInjectionPlugin plugin;

        @EventHandler
        public void onJoin(PlayerJoinEvent e) {
            // Now you can use your plugin class in here. You can access
            // things like the configuration now (assuming you create one):
            String configValue = this.plugin.getConfig().getString("SomeCoolValue");
            e.getPlayer().sendMessage(configValue);
        }

    }
     
    Don't forgot to expand your DependencyInjectionPlugin class to register the listener:
    Code (Java):
    public class DependencyInjectionPlugin extends JavaPlugin {

        @Inject private SimpleCommand command;
        @Inject private SimpleListener listener;

        @Override
        public void onEnable() {
            // Fetch dependencies. We only have to do it this way for our main class.
            SimpleBinderModulemodule = new SimpleBinderModule(this);
            Injector injector = module.createInjector();
            injector.injectMembers(this);

            // Now we can register our command executor
            // Be sure to register the command in your plugin.yml
            this.getCommand("test").setExecutor(this.command);

            // Register the listener
            this.getServer().getPluginManager().registerEvents(this.listener, this);
        }

    }
    Liskov substitution principle
    Some developers here may have heard of, or are even using the Liskov Substitution principle. I won't explain what this principle is, but if you're using this principle, you may want to read these extra tips on using dependency injection with this principle.

    You can bind interfaces to class implementations in your binder class like so:
    Code (Java):
    @Override
    protected void configure() {
        this.bind(SomeInterface.class).to(SomeInterfaceImpl.class);
    }
    You can then inject your interface like so:
    Code (Java):
    @Inject private SomeInterface someInterface;
    Conclusion
    And there you have it! Dependency injection in your Minecraft plugin. No more will people complain about your static abuse. Credits to @MiniDigger for having an awesome open source project that helped me fix a few issues I ran into. This is the first guide that I've ever written, so chances are I forgot some things here and there. If you have any questions, you can always ask me.

    Additional notes:
    As pointed out by @MiniDigger, it is important to remember that once you use an @Inject annotation in your class, you should always instantiate it using an injector instead of creating a new instance using the new keyword. If you want classes to behave like a singleton, you can add the @Singleton annotation above your class definition to achieve this.
     
    #1 MrDienns, Jan 6, 2018
    Last edited: Feb 11, 2018
    • Like Like x 7
    • Useful Useful x 5
    • Optimistic Optimistic x 1
  2. MiniDigger

    Supporter

    doesn't matter, both work the same.

    good guide, more ppl should use proper DI using guice in their plugins, especially if they get large. DI is a fundamental requirement for large applications so that they stay decoupled and modular, so that the sideeffect of changes becomes minimal, your code stays maintainable and testable.

    one imporant part of guice you did forget is the @Singleton annotation. if you have a singleton, like this lang handler (https://github.com/VoxelGamesLib/Vo...lgameslib/voxelgameslib/lang/LangHandler.java), you mark it as @Singleton and guice will be able to create one instance of the class and reuse it every time you inject it. forgetting that @Singleton annotation causes serious pain. believe me.

    another thing to not forget is that as soon as you add a @Inject annotation to a class, you can never create a instance of that class just with the constructor, always use your injector and call getInstance(someclass.class). this is important since if you don't do that, guice can't inject your fields and you will get null pointer exceptions.

    for ppl who want to look into more complex usecase scenarios than just passing around your plugins main instance, take a look at my project VoxelGamesLib, which MrDienns used as a reference: https://github.com/VoxelGamesLib/VoxelGamesLibv2
    my guice module is located here https://github.com/VoxelGamesLib/Vo...meslib/voxelgameslib/VoxelGamesLibModule.java
    I do use some different guice functionalities, like
    injecting static util classes: https://github.com/VoxelGamesLib/Vo...ib/voxelgameslib/VoxelGamesLibModule.java#L82
    named annotations: https://github.com/VoxelGamesLib/Vo...oxelgameslib/VoxelGamesLibModule.java#L63-L77 (accessible via @Inject @Named("ConfigFolder") private File configFolder; (https://github.com/VoxelGamesLib/Vo...b/voxelgameslib/config/ConfigHandler.java#L32)
    providers: https://github.com/VoxelGamesLib/Vo...oxelgameslib/VoxelGamesLibModule.java#L88-L96 (work the same as toInstance, but using toProvider to provide a method thats called when a new instance is needed)
     
    • Informative Informative x 4
  3. For some reason I thought one would only work for constructor based injection annotations while the other one is for field annotations. Thanks for pointing that out.

    Yeah, I should probably mention that. Will add it.
     
  4. Isn't Dependency Injection just passing a instance variable in the constructor?
     
  5. Yes, but an approach like this is a lot easier to maintain. I would recommend this over using a bunch of constructors.
     
  6. Hi,

    I'm trying to implement this into my plugin but I think Guice is not being shaded into the jar file. The size of the .jar pretty much stays the same and when I try to run it it gives me a ClassNotFoundException. Am I doing something wrong?

    My pom.xml:
    Code (Text):
    <?xml version="1.0" encoding="UTF-8"?>
    <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/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>

        <groupId>com.matsg.battlegrounds</groupId>
        <artifactId>Battlegrounds</artifactId>
        <version>1.0-SNAPSHOT</version>

        <name>Battlegrounds</name>
        <url>http://maven.apache.org</url>

        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-shade-plugin</artifactId>
                    <version>2.4.3</version>
                    <executions>
                        <execution>
                            <phase>package</phase>
                            <goals>
                                <goal>shade</goal>
                            </goals>
                            <configuration>
                                <filters>
                                    <filter>
                                        <artifact>*:*</artifact>
                                        <excludes>
                                            <exclude>META-INF/*.SF</exclude>
                                            <exclude>META-INF/*.DSA</exclude>
                                            <exclude>META-INF/*.RSA</exclude>
                                        </excludes>
                                    </filter>
                                </filters>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>

        <repositories>
            <repository>
                <id>spigot-repo</id>
                <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
            </repository>
            <repository>
                <id>sk89q-snapshots</id>
                <url>http://maven.sk89q.com/artifactory/repo</url>
                <releases>
                    <enabled>true</enabled>
                </releases>
                <snapshots>
                    <enabled>true</enabled>
                </snapshots>
            </repository>
        </repositories>

        <dependencies>
            <dependency>
                <groupId>org.spigotmc</groupId>
                <artifactId>spigot-api</artifactId>
                <version>LATEST</version>
                <scope>provided</scope>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>org.bukkit</groupId>
                <artifactId>bukkit</artifactId>
                <version>LATEST</version>
                <scope>provided</scope>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>com.sk89q.worldedit</groupId>
                <artifactId>worldedit-bukkit</artifactId>
                <version>6.1.1-SNAPSHOT</version>
                <scope>provided</scope>
                <type>jar</type>
            </dependency>
            <dependency>
                <groupId>com.google.inject</groupId>
                <artifactId>guice</artifactId>
                <version>4.0</version>
            </dependency>
        </dependencies>
    </project>
     
  7. What maven command are you using to build your jar?
     
  8. I'm using Intellij to build the project for me (JAR artifact with module output)
     
  9. Wow great post, I am really interested right now in the DI pattern and this was interesting!
     
    • Like Like x 1
  10. MiniDigger

    Supporter

    make sure you actually use maven and not intellijs packaging mechanism. either use the terminal or the maven view
     
  11. Try executing:
    Code (Text):
    clean package
    See View > Tool Windows > Maven
    Then, in the left you'll see this commandline icon with an M which says "Execute Maven Goal". In there, put the command above. That works for me.

    I'm glad you find it informative. Hopefully it will improve your development.
     
  12. I have now built my jar via maven which appeared to work as the ClassNotFoundException doesn't show up anymore. The jar's file size has increased by 3MB though, is this normal?
     
  13. Well, you're basically shading the Guice framework in it, so yes, that's normal. Guice is roughly 3 MB, I have the same number (3.4 MB) in one of my small other plugins.
     
  14. MiniDigger

    Supporter

    Wait till you shade some more libs ^^
    * stares at his 23 mb jar *
     
  15. [​IMG]
     
  16. shade kotlin stdlib, kotlin reflect and kotlin embedded compiler too :)
     
  17. MiniDigger

    Supporter

    neither of that is included in the jar ;(
    [​IMG]
     
  18. Even better.

    Edit Configuration -> + -> Maven -> Name:"Install", Command:"clean install" -> Apply -> Done, now everytime you wanna compile, just click the green arrow. You can also use clean package (see https://stackoverflow.com/questions...clean-package-and-mvn-clean-install-different).
     
  19. No. You'd just setup the same configuration on that machine.
     

Share This Page