Resource [Tutorial] Reflection

Discussion in 'Spigot Plugin Development' started by Stef, May 14, 2016.

  1. Please don't use this tutorial. I had no idea what I was doing at the time of writing this and (may) contain bad code.

    What is reflection?

    Reflection can do a couple of things. It can make your plugin version independent (until minecraft changes his method or classes at least) when using nms (net.minecraft.server classes) or obc (org.bukkit.craftbukkit classes) code. It also can give you access to private fields and methods so you can use those in your project. You can also use it to use methods that should exist, but don't have to (like is done with the getHanderList in events). I'm going to show how to do the basic stuff and this tutorial will not contain everything reflection can do, however this should be enough to send packets or get values from nms classes with reflection. In this tutorial we will mainly be focusing on making plugins version independent, however the other examples are included in this tutorial as well.

    Introduction to reflection
    When making your plugin version independent the one thing we want to achieve is not using any nms or obc imports. When java reads your code and wants to know where for example EntityPlayer (EntityPlayer is an nms class representing the player) is located, it's going to look at your imports and looking for this class. When you're using v1.9R1 for example and your user is using v1.9.4R0.1 your user is going to get errors, because net.minecraft.server.v1.9R1.EntityPlayer doesn't exist. That's why we have to remove these imports. How, you ask? It sounds pretty straightforward, but can be quite complicated sometimes, we want java to look up the classes, methods, fields etc. That way we can guarantee that it will find the class with the version the user is using.

    Getting the version
    Let's start with programming. So the first thing you want to get is the version your player is running. With this version we can get other classes too, so it's important to have this setup. To get the version used in packages we're going to get the version which is inside the packages. To do this we want to first get an obc or nms class. We will be getting the package from the class this returns.
    Code (Text):
    Bukkit.getServer()
    You might be thinking this doesn't work because getServer() returns a Server which isn't an obc or nms class. You're right, however Server is an interface, and the classes which implement Server are CraftServer classes. CraftServer is an obc class. This is an important thing when working with reflection, the classes which will be returned will not be the interface, but the actual class itself. You can also get other obc or nms classes too of course, but this one is easy to use, because we can access it anywhere.

    So how are we going to get the version from the packet of this class? First we want to get the class. By doing:
    Code (Text):
    Bukkit.getServer().getClass()
    When we have the class of an object we can do loads of things and almost everything starts by getting the class from an object. Once we have the class we can do whatever we want. In this case it will be getting the package. To get the package just call getPackage() on the class.
    Code (Text):
    Bukkit.getServer().getClass().getPackage()
    Where almost there, but getPackage() returns a Package class and not a string. To turn this into a string call getName() on the package (do not call toString() on the package, because this will give a different result). Now that we have the actual package as a string we want to get the part where the version is. When we have this package, let's count at which part the version is (with a part I mean, after each dot).
    Code (Text):

    org.bukkit.craftbukkit.v1_9R2.CraftServer
    ^    ^        ^         ^                  ^
    1    2        3          4                   5
    It's number four, however Java is zero-indexed so we would specify this with a 3.
    So to finish the previous code, it'll look like this.
    Code (Text):
    Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
    Don't forget to escape your dot. Your IDE will not give any errors about this, but when you forget to escape it, this won't work.

    Let's make a variable containing this information, so we don't have to type this out all the time.
    Code (Text):
    private final String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
    Good job, you've just written your first reflection! Let's see what other things we can do with reflection.

    Getting classes
    As said in the previous paragraph most reflection starts off by getting the class. This is really easy, because every object has a getClass() method. And if you're in a static method and you can't use getClass() you can always use the .class field which is in every single object. So to get the class of a Player we would do
    Code (Text):
    Player.class
    or if you have a variable which is a player you can do
    Code (Text):
    player.getClass()
    One problem though, how can we get classes which have versions inside their package? That's where we can use Class.forName(); This takes in a string containing the full package and class. So to get the EntityPlayer class we can do.
    Code (Text):
    Class.forName("net.minecraft.server." + version + ".EntityPlayer");
    Note: The version variable was from the previous paragraph.
    Note 2: The forName method can throw a ClassNotFoundException and wants you to catch it. Let your IDE make this for you, or do it yourself if you prefer that.

    This will return the class from the specified package and class name. Let's turn this into a nice method so we don't have to write this out all the time.
    Code (Text):
    private Class<?> getNMSClass(String name) {
      try {
        return Class.forName("net.minecraft.server." + version + "." + name);
      } catch(ClassNotFoundException e) {
        e.printStackTrace();
        return null;
      }
    }
    Great now we can start focusing on some more useful stuff. When working with reflection make sure to have this method already setup, because you're probably going to need it.

    Getting nested classes
    We can now get classes, but what about nested classes? Let's try this out. We want to get the ChatSerializer class from the IChatBaseComponent class (net.minecraft.server.v1.9.4-R0.1.IChatBaseComponent, don't worry if you don't know what this class does, it doesn't matter for now). We can get all the classes inside a class by calling getClasses(). This returns an array containing all the nested classes (which are public). Let's get the nested classes from the IChatBaseComponent class.
    Code (Text):
    getNMSClass("IChatBaseComponent").getClasses();
    The array contains the classes from top to bottom. If you don't know where a class is located I recommend using a decompiler to view the class. The ChatSerializer class is the first one inside the IChatBaseComponent class, so we want to get the 0th element from the array.
    Code (Text):
    getNMSClass("IChatBaseComponent").getClasses()[0];
    Now we have the ChatSerializer class and we can do anything we want with it. You may have seen that there also is a getDeclaredClasses() method which also returns these classes. This difference between these is explained at the end of this tutorial.

    The above are ways to get a nested class, however when the order of nested classes change, you'll have a problem. Luckily there is a way to avoid that. By using a '$' sign between your class and the nested class you can get the nested class without having to worry about the correct order.
    In this case you would do something like this:
    Code (Text):
    getNMSClass("IChatBaseComponent$ChatSerializer");
    This way is preferred, because it is likely to be more stable than using the order of nested classes. However when you want to get all nested classes you should use the example shown above.

    Making new instance of classes
    Creating new instances of a class is an important aspect when for example working with packets. Let's try to create a new instance of the PacketPlayOutChat class. To create a new instance we first have to get the constructor of the class. To get the constructor we can call getConstructor(). As parameters we have to specify all classes from the constructor. For the PacketPlayOutChat that would be IChatBaseComponent and byte. To get the IChatBaseComponent class we can call the method we've created to get classes and to get the class of the byte we can call .class on the byte, like this.
    Code (Text):
    getNMSClass("PacketPlayOutChat").getConstructor(getNMSClass("PacketPlayOutChat"), byte.class);
    To create a new instance of this class with this constructor we can call newInstance on the constructor. As parameters we have to insert all the objects as specified in the constructor like this:
    Code (Text):
    getNMSClass("PacketPlayOutChat").getConstructor(getNMSClass("IChatBaseComponent"), byte.class).newInstance(iChatBaseComponent, (byte) 0);
    Getting and invoking methods
    Getting a method is important, because usually it returns data which we want to know in order to make an awesome plugin. Let's try to get the sendMessage method from the player class with reflection (of course you shouldn't do this, because you can just do player.sendMessage()). To get a method we can use the getMethod() method. This takes one or more parameters. The first one is the name of the method as a string. The others are the parameters of the method we want to get (as a class). In this case that would be a String.
    Code (Text):
    player.getClass().getMethod("sendMessage", String.class)
    To invoke this method we can simply call invoke on the method. This also takes one or more parameters. The first one is the object where you're invoking from. The others are the parameters as specified in the getMethod method.
    Code (Text):
    player.getClass().getMethod("sendMessage", String.class).invoke(player, "Welcome to my server!");
    Getting and setting fields
    Most of the fields are editable through methods, however some are not. That's why it's useful to get these as well. We can get a field with the getField method. This takes in one parameter; the name of the field. Let's try it on the playerConnection from the EntityPlayer class.
    Code (Text):
    entityPlayer.getClass().getField("playerConnection");
    To get the value of this field we can call get on the field with as parameter the source (in this case the entityPlayer).
    Code (Text):
    entityPlayer.getClass().getField("playerConnection").get(entityPlayer);
    This will return an object containing the value of the field. To change the value of the field we can call set on the field with the source and the new value. Let's change the playerConnection to null.
    Code (Text):
    entityPlayer.getClass().getField("playerConnection").set(entityPlayer, null);
    We now have changed the playerConnection to null. Remember to not do this yourself on a plugin, because this can lead to weird errors.

    Making fields or method accessible
    We can do a lot already, however to use private fields or method one extra step has to be done. When we got our method or field save it in a variable.
    Code (Text):
    Field field = entityPlayer.getClass().getDeclaredField("lastHealthSent");
    We can't access this field right away, because it ks private. We first have to make it accessible by calling setAccessible(true) on the field.
    Code (Text):
    Field field = entityPlayer.getClass().getDeclaredField("lastHealthSent");
    field.setAccessible(true);
    We can now use the field to do whatever we want.

    Static fields or methods
    Static fields and methods work just a little bit different than the normal ones. When we want to get/set or invoke the field/method we have to specify a source for which instance of the class we want our values to be changed or invoked. With static fields there is no instance. In that case the source will be null. Let's try invoking the a method of ChatSerializer (which is static).
    Code (Text):
    getNMSClass("IChatBaseComponent").getClasses()[0].getMethod("a", String.class).invoke(null, "Amaxing text");
    As you can see the source is null, because the method is static.

    getDeclaredFields() vs getFields()
    Note: This counts for every method that has a declared variant.

    The difference between these two may not be very important at first, but you can get weird errors when you use the wrong one. There are two main differences when using the getFields method instead of the getDeclaredFields.
    • getFields only returns the public fields and not the others.
    • getFields gets all fields from all super classes
    So when trying to use private fields don't forget to use getDeclaredField() instead of getField()!

    Caching
    Reflection is an heavy task and isn't very good for performance. Saving methods, classes etc that you are going to use multiple times to call them later without having to get them again can reduce this. Whenever you're using reflection, try to cache as many things which otherwise had to be looked up (including, but not limited to, getting classes, methods, fields).

    Test
    Here is a small test to see if you can change nms code to a version which uses reflection. This is the code you have to change. This is code that shows an actionbar.
    Code (Text):
    public void sendActionbar(Player player, String message) {
      IChatBaseComponent icbc = ChatSerializer.a("{\"text\":\"" + message + "\"}");
      PacketPlayOutChat packet = new PacketPlayOutChat(icbc, (byte) 2);
      ((CraftPlayer) player).getHandle().playerConnection.sendPacket(packet);
    }
     
    First we want our IChatBaseComponent to change. So change the IChatBaseComponent in front of the variable name to Object so we don't have to import it. Than we need to get the ChatSerializer class. This class is inside the IChatBaseComponent class and is the first one. This will result in this.
    Code (Text):
    Object icbc = getNMSClass("IChatBaseComponent").getClasses()[0].getMethod("a", String.class).invoke(null, "{\"text\":\"" + message + "\"}");
    Then we want to create our packet variable. Again change the PacketPlayOutChat in front of the variable to Object. Then get the class and get the constructor with the IChatBaseComponent class and the byte class. Make a new instance of it and you have you're packet
    Code (Text):
    Object packet = getNMSClass("PacketPlayOutChat").getConstructor(getNMSClass("IChatBaseComponent"), byte.class).newInstance(icbc, (byte) 2);
    Now we first want to get the entityPlayer from the player and save it in a variable (remember that you don't have to cast to a CraftPlayer).
    Code (Text):
    Object entityPlayer = player.getClass().getMethod("getHandle").invoke(player);
    Now from the entityPlayer we want to make another variable containing the playerConnection field.
    Code (Text):
    Object playerConnection = entityPlayer.getClass().getField("playerConnection").get(entityPlayer);
    Only one part left. We want to send the packet. Call the sendPacket method on the playerConnection with as parameter the packet class. Then invoke it with the packet.
    Code (Text):
    playerConnection.getClass().getMethod("sendPacket", getNMSClass("Packet")).invoke(playerConnection, packet);
    Good job!

    Final word
    Hopefully you'll understand a bit more about reflection. If the test was too hard, don't worry, you'll get better over time. Try to change nms code to one with reflection whenever you see nms code somewhere. Try for example to change these (, , , most of those are currently included in spigot, but you can still try this to enhance your reflection skills) video examples (credit to PogoStick29Dev) to one which use reflection. If you don't understand something don't hesitate to ask in the reactions of this thread. Also the examples in this tutorial aren't all things you should do. When you can always call methods and fields the way you're used to and never set any important fields to null, because it might break your plugin or server.

    This is a basic tutorial on reflection and doesn't include all features of it. If you see any errors in this tutorial please let me know so I can fix them.
     
    #1 Stef, May 14, 2016
    Last edited: Sep 14, 2016
    • Useful Useful x 17
    • Like Like x 5
    • Informative Informative x 2
    • Creative Creative x 1
  2. Nice tutorial, it is very well redacted this will help a lot of developers to get into NMS.
     
  3. Thank you :)
     
  4. To be honest, I don't see why I should use reflection unless I have to change final or private fields. It's a pain in the ass to maintain it and it's very inefficient if you use it to a significant extent.

    http://docs.oracle.com/javase/tutorial/reflect/index.html
    Well explained, though :)
     
    • Agree Agree x 2
  5. Once you have the reflection setup you don't have to do too much to it anymore. Most classes and methods stay the same and you don't need to update those. And reflection is (in spigot) mostly used to ensure your plugin doesn't break whenever a new update comes out. There are ways to make classes for every single version and than call the right one, however this is only reliable for past updates. You don't know what the next version will be called and how many there will be. Will there be a 1.9.5? Or does 1.10 come after 1.9.4. That's why I prefer reflection.
     
    • Agree Agree x 1
  6. As I've explained before, some method names are not guaranteed to stay the same and you should not be relying on reflection. You will still have to maintain it in the end. There's a reason the version barrier was created. What you should do instead is make a PR to Spigot for the unimplemented features you need.
     
    • Agree Agree x 1
  7. But..but, i only want to use 1.8 to craft plugins since most of Vietnam server dont use 1.9
     
  8. Good tutorial this will help allot of new developers understand what Reflection is about
     
  9. I think you're missing a couple return statements there ;)
     
    • Agree Agree x 1
  10. We can't just add everything to spigot within one hour. That's why you could use reflection. With that pull requests don't have to be added to spigot. Also not everyone is using the latest version.
    Thank you.
    I see, should be fixed now.
     
  11. Great, now read the other half that I said.
     
  12. Hex

    Hex
    IRC Staff

    Reflection has a place, but to completely and arbitrarily ignore version restrictions isn't it. If you want to create a different implementation for each version, that's both fine and sane. However, if you want to just assume that nothing changes between versions, you're going to make much more trouble than it's worth. Also, yes, PR things to Spigot.
     
    • Agree Agree x 5
  13. "As I've explained before, some method names are not guaranteed to stay the same and you should not be relying on reflection." Of course if something changes it breaks. It's up to the developer to fix those things. But people don't want to create a ton of different classes to make sure your plugin works on all versions from 1.7 to 1.9. With that if something changes there is a chance normal spigot methods won't function either.
    "You will still have to maintain it in the end." Isn't that what developers are supposed to do? Maintain their plugin? On bukkit you have to maintain your plugin as stated in the rules (source).
    "There's a reason the version barrier was created." Of course they're is, however not everyone has time to make 20 different versions just to make sure everyone has a version which they can use. And it's going to be hard for users to find the correct version. Even with two versions I see people struggle finding the correct one.
    "What you should do instead is make a PR to Spigot for the unimplemented features you need." Not everybody has time to make a PR to Spigot and when I want to try and make one I found it being incredibly hard. To make a pr you have to sign up to do this. I have no problem with that however why do they need your phone number? Are people going to call me, congratulating me that I can now make pr's? With that to set this up I have to do all sort of stuff I don't have time for. The requirements for this: Maven, SpecialSource, Fernflower, Jacobe, Minecraft Server Jar, etc. That's quite a lot, only setting up Maven took me five hours and apparently I have to install SpecialSource, Fernflower and Jacobe as well? Making pull requests should be easier, because this is not fun. If they would make it easier, I would definitely contribute to spigot. But I have three days no school currently so maybe I'll try again.
     
  14. Please use quote next time tl;dr
     
  15. You prove my point exactly. If you have to maintain it in the end, reflection is useless and pointless.
     
    • Like Like x 2
    • Agree Agree x 1
  16. This can be argued though. If a variable or method name is remapped, you probably can just use reflection.
     
  17. And you can also make a pull request to implement it in the actual API as well.
     
  18. Hex

    Hex
    IRC Staff

    Fun fact, you can develop straight out of your BuildTools directory. :) Also, we don't use Jacobe anymore. Something a lot of people do is to make it so the correct implementation for the current version is automatically selected (my example is https://github.com/0x277F/hexlib). If I had to choose between a quick workaround that makes it harder to maintain in the end or a reasonable, sane, slightly more complex system, I feel the choice is obvious.
     
    • Like Like x 1
    • Agree Agree x 1
  19. I get the feeling people on this forum just say "You prove my point", just because they want to. Anyway reflection is not useless, it is not guaranteed that you have to maintain it in the end, while if you don't use it you have to maintain it.
    Of course a reliable system is better, however it is more complex making it less fun for people who just want to set this up and have fun creating pull requests.
     
  20. Hex

    Hex
    IRC Staff

    If you're going to put effort into maintaining and correcting on version updates your reflection-based system, you might as well not use it. The only time in which it's actually not going to be a waste of time is if you're not going to maintain it.
     
    • Agree Agree x 1