Testing the Consistency of Reflection and Annotation processing
Intro
Java SE and the Java ecosystem offer a multitude of Metaprogramming APIs.
They serve the same purpose just in different contexts.
Annotation processing to analyse classes in the during compilation process, or Reflection to do the same at runtime.
They have context dependent differences.
Type erasure for example.
Some Type information is lost at the end of the compilation and no longer present at runtime.
Map<Long, List<String>>
at compiletime becomes just Map
at runtime.
With these differences acounted for, there is still a lot of API surface left that should be consistent.
But is it?
This project is an Abstraction for Metaprogramming
The Api
The Api is request based. As a caller you can request for example a field of a class. Accessing fields may or may not be supported.
@Test
void request()
{
//adapter for the reflection api
C_Class systemClass = R_Adapter.generalize(System.class);
//request the field "out" for the class java.lang.System
Response<C_Field> out = Provider.request(systemClass,
Operations.DECLARED_GET_FIELD,
"out");
switch (out)
{
//the implementation may not support this operation
//e.g. it's impossible to access fields with reflection
case Response.Unsupported<C_Field> unsupported -> Assertions.fail();
//the implementation may support this operation, but there is no
//result for this instance
//e.g. the class java.lang.System does not have a field called "out"
case Response.Empty<C_Field> empty -> Assertions.fail();
//accessing fields via reflection is possible and java.lang.System
//does have a field called "out" therefore a result is expected
case Response.Result<C_Field> result -> assertNotNull(result.value());
}
}
Or use a convenience method if Optional.empty()
or throwing an Exception
is a fitting default behavior.
@Test
void requestOrEmpty()
{
//adapter for the reflection api
C_Class systemClass = R_Adapter.generalize(System.class);
//request the field "out" for the class java.lang.System.
//If its unsupported an Empty Optional is returned
Optional<C_Field> out = Provider.requestOrEmpty(systemClass,
Operations.DECLARED_GET_FIELD,
"out");
assertTrue(out.isPresent());
}
@Test
void requestOrThrow()
{
//adapter for the reflection api
C_Class systemClass = R_Adapter.generalize(System.class);
//request the field "out" for the class java.lang.System.
//If its unsupported an Exception is thrown
C_Field out = Provider.requestOrThrow(systemClass,
Operations.DECLARED_GET_FIELD,
"out");
assertNotNull(out);
}
Consistency Tests
Consistency Tests are written in the Style of a Technology Compatibility Kit (TCK).
Testing consistency works like this.
We start with a String representation of some code.
This code gets compiled using the javax.tools.JavaCompiler
and the Annotation processing part is executed and tested with the consumer supplied to the method "test".
The compiled code is intercepted and loaded with a custom Classloader into the runtime.
There the reflection part is executed and tested.
@Test
void classDeclarationRendering()
{
String expected =
"public class InterpolateGenericsExample<A extends Comparable<B>, B extends Comparable<A>> {}\n";
String name = "InterpolateGenericsExample.java";
String content =
"public class InterpolateGenericsExample<A extends Comparable<B>, B extends Comparable<A>> {}";
TckTest.withSource(name, content)
.test(implementation ->
{
C_Class cClass = Provider.requestOrThrow(implementation,
Operations. GET_CLASS,
"InterpolateGenericsExample");
assertEquals(expected, Renderer.render(RenderingContext.DEFAULT, cClass)
.declaration());
});
}
This tests:
-
Feature
-
class rendering
-
-
Api
-
Provider.request(Operations.NAMEABLE_GET_NAME, aClass)
and others used internally by the render
-
-
Adapters
-
ReflectionAdapter#generalize
-
LangModelAdapter#generalize
-
-
Java
-
java.lang.Class#getSimpleName
-
javax.lang.model.element.TypeElement#getSimpleName
-
Findings
1. Provides directive duplicates
With the java platform module system you can enforce encapsulation. One way is to declare the implementations of service provider interfaces (Spi) using "provides [Spi-Name] with [Implementation-Name]".
For the module java.desktop this looks like this:
module java.desktop {
//...
provides javax.sound.midi.spi.MidiDeviceProvider with
com.sun.media.sound.MidiInDeviceProvider,
com.sun.media.sound.MidiOutDeviceProvider,
com.sun.media.sound.RealTimeSequencerProvider,
com.sun.media.sound.SoftProvider;
provides javax.sound.midi.spi.MidiFileReader with
com.sun.media.sound.StandardMidiFileReader;
provides javax.sound.midi.spi.MidiFileWriter with
com.sun.media.sound.StandardMidiFileWriter;
provides javax.sound.midi.spi.SoundbankReader with
com.sun.media.sound.AudioFileSoundbankReader,
com.sun.media.sound.DLSSoundbankReader,
com.sun.media.sound.JARSoundbankReader,
com.sun.media.sound.SF2SoundbankReader;
//...
}
Reflection
Using Reflection we can list the provides directives like this.
ModuleFinder.ofSystem()
.find("java.desktop")
.orElseThrow()
.descriptor()
.provides()
.toList();
resulting in
ModuleDescriptor.Provides service javax.sound.midi.spi.MidiDeviceProvider providers com.sun.media.sound.MidiInDeviceProvider com.sun.media.sound.MidiOutDeviceProvider com.sun.media.sound.RealTimeSequencerProvider com.sun.media.sound.SoftProvider ModuleDescriptor.Provides service javax.sound.midi.spi.MidiFileReader providers com.sun.media.sound.StandardMidiFileReader ModuleDescriptor.Provides service javax.sound.midi.spi.MidiFileWriter providers com.sun.media.sound.StandardMidiFileWriter ModuleDescriptor.Provides service javax.sound.midi.spi.SoundbankReader providers com.sun.media.sound.AudioFileSoundbankReader com.sun.media.sound.DLSSoundbankReader com.sun.media.sound.JARSoundbankReader com.sun.media.sound.SF2SoundbankReader
Annotation Processing
Accessing the same information with Annotation Processing is a bit more work.
@SupportedSourceVersion(SourceVersion.RELEASE_21)
@SupportedAnnotationTypes("*")
public class Processor extends AbstractProcessor
{
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
ModuleElement javaDesktop = processingEnv.getElementUtils().getModuleElement("java.desktop");
javaDesktop.getDirectives()
.stream()
.filter(directive -> directive.getKind().equals(ModuleElement.DirectiveKind.PROVIDES))
.map(ModuleElement.ProvidesDirective.class::cast)
.toList();
return false;
}
}
And the results contain duplicates.
ModuleElement.ProvidesDirective service javax.sound.midi.spi.MidiDeviceProvider impls com.sun.media.sound.MidiInDeviceProvider com.sun.media.sound.MidiOutDeviceProvider ModuleElement.ProvidesDirective service javax.sound.midi.spi.MidiDeviceProvider impls com.sun.media.sound.MidiInDeviceProvider com.sun.media.sound.MidiOutDeviceProvider com.sun.media.sound.RealTimeSequencerProvider ModuleElement.ProvidesDirective service javax.sound.midi.spi.MidiDeviceProvider impls com.sun.media.sound.MidiInDeviceProvider com.sun.media.sound.MidiOutDeviceProvider com.sun.media.sound.RealTimeSequencerProvider com.sun.media.sound.SoftProvider ModuleElement.ProvidesDirective service javax.sound.midi.spi.MidiFileReader impls com.sun.media.sound.StandardMidiFileReader ModuleElement.ProvidesDirective service javax.sound.midi.spi.MidiFileWriter impls com.sun.media.sound.StandardMidiFileWriter ModuleElement.ProvidesDirective service javax.sound.midi.spi.SoundbankReader impls com.sun.media.sound.AudioFileSoundbankReader com.sun.media.sound.DLSSoundbankReader com.sun.media.sound.JARSoundbankReader com.sun.media.sound.SF2SoundbankReader ModuleElement.ProvidesDirective service javax.sound.midi.spi.SoundbankReader impls com.sun.media.sound.AudioFileSoundbankReader com.sun.media.sound.DLSSoundbankReader com.sun.media.sound.JARSoundbankReader ModuleElement.ProvidesDirective service javax.sound.midi.spi.SoundbankReader impls com.sun.media.sound.AudioFileSoundbankReader com.sun.media.sound.DLSSoundbankReader ModuleElement.ProvidesDirective service javax.sound.midi.spi.SoundbankReader impls com.sun.media.sound.AudioFileSoundbankReader
2. Receiver Parameter
Receiver parameter got introduced with java 8 and JSR 308 but are very rarely used. To the point that I have never seen them in the wild.
The receiver parameter is an optional syntactic device for an instance method or an inner class’s constructor. For an instance method, the receiver parameter represents the object for which the method is invoked. For an inner class’s constructor, the receiver parameter represents the immediately enclosing instance of the newly constructed object. In both cases, the receiver parameter exists solely to allow the type of the represented object to be denoted in source code, so that the type may be annotated (§9.7.4). The receiver parameter is not a formal parameter; more precisely, it is not a declaration of any kind of variable (§4.12.3), it is never bound to any value passed as an argument in a method invocation expression or class instance creation expression, and it has no effect whatsoever at run time.
The points we will focus on are, that they can be used in two places, and are a special kind of parameter.
The Handling is inconsistent in Reflection.
Method
If we get the parameter for a method like this with Executable#getParameters()
the result does not contain the Receiver.
public class MethodExample {
public void foo(@MyAnnotation MethodExample MethodExample.this) {}
}
@Test
void methodReceiver()
{
Executable method = Arrays.stream(MethodExample.class.getMethods())
.filter(method1 -> method1.getName().equals("foo"))
.findFirst()
.get();
Parameter[] methodParameters = method.getParameters();
Assertions.assertEquals(0, methodParameters.length);
}
Constructor
If we get the parameter for a constructor with Executable#getParameters()
the result does contain the Receiver.
public class ConstructorExample {
public class Inner {
public Inner(@MyAnnotation ConstructorExample ConstructorExample.this) {}
}
}
@Test
void constructorReceiver()
{
Executable constructor = ConstructorExample.Inner.class.getConstructors()[0];
Parameter[] constructorParameters = constructor.getParameters();
Assertions.assertEquals(1, constructorParameters.length);
Assertions.assertEquals(ConstructorExample.class, constructorParameters[0].getType());
}