Why Javacord over other Java Discord Wrappers?

8 minute read • Shindou Mihou

You may have come here to wonder over what library should I use to build a Discord bot on Java, and I will tell you this, it is neither Javacord nor JDA but rather the library that really suits your tastes the most. All the Discord wrappers on the Java community out there are all unique by their own from the style that they do stuff to how they perform but there will never be the best among them.

In this post, I'll be explaining why I chose Javacord over the many other choices like JDA, Discord4J, Catnip, and Kord to build Mana and explain the many parts that I really liked about the library over the others. Let's get started, shall we?

Simplicity is the key to speed.

This is the key reason for my choice with Javacord over the other Discord wrappers out there. It's the simplicity that Javacord gives that makes it really amazing to work on, everything from the event listeners to the method names is made with the developers' experience in mind and that itself is what really made me turn to Javacord over the others and also most likely the reason why many among the community within Javacord chose it.

First, to understand what makes Javacord simpler than the other libraries such a Discord4J and JDA, we have to first understand what Javacord uses for its asynchronous handling, Optional handling which really made it easier to learn.

CompletableFutures

CompletableFuture and Optionals are newer terms that people from JDK 7 and below may not have heard or tried out so far.

To summarize, CompletableFutures are more similar to Promises in Javascript in which they perform a task provided to them in another thread while allowing the current thread to continue on without blocking before returning the value and performing the other tasks that need to be done after. A simple example of how Futures in Java looks is as shown below.

private static CompletableFuture<String> someLongRunningTask() {
    return CompletableFuture.supplyAsync(() -> {
               // Imagine us doing a long task here.
              return "Hello World";
    });
}

public static void main(String[] args) {
    someLongRunningTask().thenAccept(str -> System.out.println(str));
}

As to make it easier for people unfamiliar with all of this to understand, what the code above does is basically run the long-running task which returned a CompletableFuture with the return value of a String in another thread before executing the attached thenAccept method which simply prints out the String returned from the future. This way, any other function inside the main function is executed while the long-running task performs its tasks at the same time. To see this more clearly, let's modify the code a bit and see.

private static CompletableFuture<String> someLongRunningTask() {
    return CompletableFuture.supplyAsync(() -> {
              Thread.sleep(2500);
              return "Hello World";
    });
}

public static void main(String[] args) {
    someLongRunningTask().thenAccept(str -> System.out.println(str));
    System.out.println("I should be second, right?");
}

Think about the code for a second, which among the two lines inside the main function would be executed first? Did you perhaps think it would be the Hello World? If so then you are completely wrong as the output of the code above prints the following:

I should be second, right?
Hello World

And that is how CompletableFutures work, they simply run the tasks asynchronously, or in other terms, non-blocking way.

Optionals

For a person learning Javacord, this is one of the more critical parts to learn in Java paired with CompletableFuture as the library heavily utilizes these two components to make what we know as Javacord right now. To understand Optional, we have to keep in mind the fact that a value can be either null or the value itself and it can be pretty annoying to check all the time whether a value is null or not and catching the exception if it is null, this is where Optionals comes in.

Introduced in JDK 8 together with CompletableFutures and Lambdas, Optionals is more of a replacement for null values. IN FACT, the language Rust has no such thing called null because they use something very close to Optional. You can think of Optional as a box with the possibility of being air inside or some kind of item, you can know if it is air or not by feeling the weight of the box. A simple example of Optional is as shown below.

public static Optional<String> thisHasAValue() {
    return Optional.of("Hello World");
}

public static Optional<String> thisPotentiallyHasAValue() {
    boolean shouldHaveAValue = ThreadLocalRandom.current().nextBoolean();
    return Optional.ofNullable(shouldHaveAValue ? "Hello World 2" : null);
}

public static Optional<String> thisNeverHasAValue() {
    return Optional.empty();
}

public static void main(String[] args) {
    thisHasAValue().ifPresent(str -> System.out.println(str));
    thisPotentiallyHasAValue().ifPresent(str -> System.out.println(str));
    thisNeverHasAValue().ifPresent(str -> System.out.println(str));
}

If you didn't skim the code, then you really would be able to understand Optionals as the concept of an Optional is very simple. Now, looking at the code, what do you think would be the output of this?

Hello World
Hello World 2
Hello World

These are the two possible outputs of this entire code. Did you notice how the second output doesn't print out its value and how all outputs never printed out a single line without having an if-check or an exception handling?

That is the job of the ifPresent function which only triggers the code inside IF the Optional has a value and simply ignores if it doesn't. There are also other methods such as get which is not recommended unless you are sure that the Optional has a value... which you can do by using the isPresent method and also ifPresentOrElse which was introduced in JDK 9 to handle both states but that is where we'll stop with this.

Javacord

Now that we all know what CompletableFutures and Optionals are, let us go into how Javacord utilizes these two components to produce a simple and quick-to-understand library that allows developers to get their stuff done very quickly. To start, how about we create a simple Discord bot?

public class Main {

    public static void main(String[] args) {
        DiscordApi api = new DiscordApiBuilder()
                      .setToken(System.getenv("DISCORD_TOKEN"))
                      .setAllNonPrivilegedIntents()
                      .login()
                      .join();
         System.out.println("The bot is now running...");
    }

}

Doesn't it look simple and easy to understand? If it doesn't then let me explain to you what the above does. The code that I showed above simply creates a new DiscordApi class which is practically the brain and heart of your entire Discord bot, we are telling the builder to use the token we have on our Environmental Variable which we took from Discord's Developer Portal and utilizing the non-privileged intents which exclude stuff such as Guild Member List and Presences.

After setting the above, we immediately tell Javacord to log the bot online before waiting for it to completely login then printing out the output which tells our console that the bot is now running. It's a very simple and easy-to-understand code, but this bot does nothing at all but stay online and that isn't what we want, so how about we add functionalities such as printing out "Pong" whenever a user says "Ping"?

public class Main {

    public static void main(String[] args) {
        DiscordApi api = new DiscordApiBuilder()
                      .setToken(System.getenv("DISCORD_TOKEN"))
                      .setAllNonPrivilegedIntents()
                      .login()
                      .join();

         api.addMessageCreateListener(event -> {
                 if (event.getMessageContent().equalsIgnoreCase("Ping")) {
                     event.getMessage().reply("PONG!");
                }
        });

         System.out.println("The bot is now running...");
    }

}

Now, the bot should always reply with PONG! whenever a user sends a message that equals to "Ping" with ignored casing. The code itself looks very easy to understand but somehow... don't you think it is a little bit too dirty, how about we move the event listening to another class?

public class PingListener implements MessageCreateListener {

      @Override
      public void onEvent(MessageCreateEvent event) {
           if (event.getMessageContent().equalsIgnoreCase("Ping")) {
                     event.getMessage().reply("PONG!");
           }
      }

}
public class Main {

    public static void main(String[] args) {
        DiscordApi api = new DiscordApiBuilder()
                      .setToken(System.getenv("DISCORD_TOKEN"))
                      .setAllNonPrivilegedIntents()
                      .addListener(new PingListener())
                      .login()
                      .join();

         System.out.println("The bot is now running...");
    }

}

Now that is a lot cleaner to look at, but we want to do something more than Ping Pong... what if I want to have the bot add a question mark reaction to the user's message if they ask "Tell me a question" and have the bot give the user a question if they press the reaction?

public class QuestionListener implements MessageCreateListener {

      @Override
      public void onMessageCreate(MessageCreateEvent event) {
           if (event.getMessageContent().equalsIgnoreCase("Tell me a question")) {
                     event.getMessage().addReaction("❓");
           }
      }

}
public class QuestionReactionListener implements ReactionAddListener {

      @Override
      public void onReactionAdd(ReactionAddEvent event) {
           if (!e.getEmoji().equalsEmoji("❓")) {
              return;
           }

           if (e.getUserId() != e.getApi().getYourself().getId()) {
                 e.requestMessage().thenAccept(message -> message.reply("I don't have a question!"));
           }
      }

}

You may be wondering, why are we requesting the message? It is because the message could have a chance to not be on your cache especially if you have message cache disabled which is why to guarantee that we can reply to the message, we are requesting the message from Discord instead and this is completely a Discord thing for them to not send the entire message.

But do you see how simple it is to work with Javacord, or is the code above a little bit too much for you? Let me explain the code a bit despite its very self-explanatory look.

The entire code practically checks if a user sent a message that says "Tell me a question" to which the bot reacts by adding a question mark to which then if a user reacts, the QuestionReactionListener class would then be triggered and first starts checking if the reaction is a question mark before ignoring if it isn't but if it is then it will check if the user who reacted is the bot or the user (because Discord also sends events that the bot performs) and if it isn't the bot then it requests the message and replies with "I don't have a question!"

Conclusion

Doesn't it all look simple and easy to understand?

I hope this post explained one of the reasons why I chose Javacord over the other libraries despite its flaws in aspects such as Voice handling but the simplicity of Javacord isn't the only part that really contributed to my adorable little Mana but it is the incredible community out there as well who have been giving help with parts that I was not familiar with but those times were pretty rare for me as Javacord is a library that tries its best to explain everything inside of it as much as it can.

To learn more about Javacord, please head to their GitHub repository and Discord server where I can also be found out there to help out as many people as possible with their questions as long as it is within the range of my knowledge. A big thanks to the Javacord maintainers and community for helping out in the development of my little Discord bot ❤️.

 


Written by Shindou Mihou

Developer of Mana.