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());
}