Today, I write about an annotation system called “Safe Reflection” which helps reduce runtime errors that can occur when using reflection.
Personally, I think reflection is a pretty crazy tool. It lets you perform action that go beyond the capabilities of a programming language. It’s basically the “evil” side of programming and hence, its use is discouraged.
The problem
One of the major issues with reflection arises with accessing fields and methods for various classes. For example, say we have some method getFieldA()
which retrieves the field named a
from some class MyClass
.
1
2
3
public Field getFieldA() {
return MyClass.class.getDeclaredField("a");
}
1
2
3
public class MyClass {
String a;
}
Now, let’s assume that MyClass
is actually from some imported library (so it’s not in your local project). Let’s say that the example above uses version 1.0 for this library.
Now let’s say that the developer of this library releases a new version 2.0 which has the following declaration:
1
2
3
public class MyClass {
String b;
}
The field named a
has been renamed to b
in version 2.0 of this library. Therefore, if we want to use a different version of this library, we’d have to ensure that our getFieldA
method actually has a field called a
, which is only the case in version 1.0 of this library.
Normally, this can simply be resolved by performing some check for the version of the library that is being used. Take the following pseudocode:
1
2
3
4
5
if (library.version == 1.0):
use field "a"
else if (library.version == 2.0):
use field "b"
end if
Now this is all well and good, but what happens when the creator of the library releases 3.0? Will the field name change? If you don’t have access to the source code for the library, this will mean you’d have to decompile the new library, manually check if there has been a change to the name of the field you want to access and hope that your code is compatible with that library.
Basically, the problem boils down to the following points:
- You have to take into account different library versions and method/field names that they use
- Those method/field names could change at any given update
- You don’t necessarily know which version of the library your clients are using, therefore you have to cover all cases
Creating the safe reflection annotation
So, this problem has been brought to my attention a number of times when creating plugins for Minecraft servers. I’ve been using a sneaky bit of reflection to bend the capabilities of what I can do when writing my Bukkit/Spigot plugins and then they release a new version which changes all of the variable and method names for basically no reason, which messes up my whole project.
The idea is simple. What if I have an annotation which states what fields or methods I want to access in what class and for what version. Then, I could have an annotation processor to handle that annotation at compile time which will perform the check to ensure that the field or method exists.
As usual, I go for the “write code first, plan stuff later” methodology and begin by creating an annotation that will handle the information that I would require. Luckily, that list is pretty well defined by what I want it to do overall.
- The class which contains the field/method
- The name of the field or method
- The version of the library that contains this class with this field/method name
Luckily, this is the easy part. I take a quick peek at Oracle’s documentation on declaring an annotation and begin constructing such an annotation. Luckily, it’s possible to declare certain parameters as optional, so I make the field name and method name optional with the intention that whatever developer is using it will choose to use one of the two parameters.
Now I wanted to make it repeatable. Repeatable annotations basically mean you can have multiple of the same annotation on one member. I want it to be repeatable so I could have safe reflection for different library versions, for example:
1
2
3
@SafeReflection(target = MyClass.class, field = "a", version = "1.0")
@SafeReflection(target = MyClass.class, field = "b", version = "2.0")
Field someField = //...
Since I have never created a repeatable annotation, I find out that you have to have some other annotation which is sort of like a collection of the first. So, my “main” annotation is @SafeReflection
and I made my repeating annotation SafeReflections
, which basically contains a SafeReflection[]
. This is basically the main setup that I required. To allow the code to be more condense and support multiple versions with the same mapping, I ensure that the version parameter is a String[]
, which leads to this effect, for example:
1
@SafeReflection(target = MyClass.class, field = "a", version = {"1.0", "1.1"})
Creating the annotation processor
Creating the annotation processor should be a breeze. I follow two generic tutorials, one from Baeldung, which I highly recommend1, and another from Medium to get an idea of what to do. From this, I create an annotation processor that processes the SafeReflection
annotation, performs the safe reflection check by finding the relevant .jar
file from the version number, finding the specific class (which is provided by the target
parameter) and then checks if the field or method exists. If so, everything is fine and the compilation continues as normal. Otherwise, compilation fails.
Or rather, that is how I expect it to go. Upon testing my annotation processor, I run into three problems that take me a lot longer to fix than I expect.
Type mirrors
In my SafeReflection
annotation, I have a parameter called target
which is of type Class<?>
. No problem there. Unfortunately, actually retrieving that class is a lot harder than you’d expect. Basically, trying to access a class throws a MirroredTypeException
and I have no clue why and instead, found this little trick to retrieving a class from an annotation, which goes like this:
1
2
3
4
5
try {
safeReflection.target();
} catch(MirroredTypeException e) {
e.getTypeMirror() // Do something with this
}
Luckily for me, all I require is the name of the class, so converting the type mirror to a String was sufficient for my purposes.
Dealing with LOCAL_VARIABLE
With annotations, you can restrict where they can be used, by using the meta-annotation (yeah, it’s literally called that. Meta-annotations are annotations for annotations) @Target
. I personally think it would be suitable to have the @SafeReflection
annotation directly next to a variable which executes the reflection call, for example:
1
2
3
4
public void someMethod() {
@SafeReflection(target = MyClass.class, field = "b", version = /* ... */)
Field field = MyClass.class.getDeclaredField("b");
}
To have this specific behaviour, I use the meta-annotation @Target(ElementType.LOCAL_VARIABLE)
which should have this effect. However! When I run the annotation processor to compile a test project, I find out quickly that the main method for processing annotations (called process
) was never being called! It takes me ages of searching the internet to find out that annoyingly, annotation processors just cannot process local variables annotations2.
To resolve this, I come to the compromise that I’ll just have all of the annotations declared at the top of the class where reflective calls are made, which would have this sort of result:
1
2
3
4
5
6
7
8
9
@SafeReflection(/**/)
@SafeReflection(/**/)
public class SomeClass {
public void someMethod() {
//Various reflective calls here
}
}
Again, not ideal, but a major improvement over using a Java compiler plugin (compiler plugins have the power to go beyond annotation processing, but it’s quite complex and not so easy to just implement in a project). I guess one of the pros is that all of the reflection declarations for that class are all in one place?
Repeatable annotations
So, you’d expect repeatable annotations to be the easiest thing to handle. Surely, since it has the repeatable tag, it’ll just convert all of the SafeReflection
annotations into a single SafeReflections
annotation which contains the array SafeReflection[]
. And indeed, it does do this. Only if you have more than one annotation.
This leads to me having to implement something which looks like this:
1
2
3
4
5
6
7
if (number of annotations == 1):
process the annotation
else if (number of annotations > 1):
for each annotation:
process the annotation
end for
end if
Like, WHY?! Couldn’t they have just implemented it such that if your annotation is repeatable, always make it in the form of the collection?! This would lead to the trivial code below, which doesn’t need this doopy if statement:
1
2
3
for each annotation:
process the annotation
end for
Conclusions
Overall, it works. Of course, it’s not fool proof - for example, you could just not use a @SafeReflection
declaration, or declare it and not use that in your code, but the overall goal that I want to achieve, that is checking for fields and methods that are accessed using reflection at compile time, is now doable!
The SafeReflection project that I made (which can be found on GitHub) is tailored for my other project, the CommandAPI, which uses a bit of reflection here and there, spanning over many versions of Minecraft. By using SafeReflection, I’ve been able to ensure that the code will not throw reflection-based errors at runtime.
Content wise, it’s not what I wanted, but the explanation on how to set up the development environment in maven, as well as include the annotation processor in another project is what makes it such a fantastic resource. ↩
According to this StackOverflow answer ↩