1. Reflection, Annotations and JavaBeans
Reflection gives us the ability to look inside a running Java program. We can ask a class what properties it has, and later call methods on arbitrary objects and read and modify object or class variables. Many frameworks make use of the Reflection API, such as JPA for object-relational mapping or JAXB for mapping Java objects to XML structures. We will program some examples ourselves that would not be possible without Reflection.
Annotations are a kind of self-programmed modifiers. They allow us to enrich the source code with metadata that can later be read via reflection or another tool. Often we are just users of other people’s annotations, but in this chapter, we will also practice how to write our own annotation types.
Prerequisites
know
Class
typebe able to read type relationships at runtime
be able to address object properties at runtime
be able to read annotations
be able to declare new annotation types
Data types used in this chapter:
1.1. Reflection API
The Reflection API can be used to examine arbitrary objects, and the following tasks use that to generate UML diagrams of arbitrary data types. The tasks focus on practical applications; you can also do a lot of nonsense with the Reflection API, such as changing characters from immutable strings, but that’s silly, and we would rather not do that.
1.1.1. Create UML class diagram with inheritance relationships ⭐
UML diagrams are very handy in documenting systems. Some UML diagrams can also be generated automatically by tools. We want to write such a tool from scratch. The starting point is an arbitrary class, which is examined by reflection. We can read all properties of this class and generate a UML diagram.
Since UML diagrams are graphical, the question naturally arises of how we can draw graphics in Java. We do not want to solve this problem, but use the description language PlantUML (https://plantuml.com/). PlantUML is for UML diagrams, what HTML is for web pages and SVG is for vector graphics. Example:
interface Serializable << interface >> Radio ..|> Serializable ElectronicDevice --|> Radio
The arrows --|>
or <|--
are represented regularly, ..|>
or <|..
are stippled.
PlantUML generates from these text documents a representation of the following type:
PlantUML is open source, and you can install a command-line program that converts the textual description into a graph with the UML diagram. There are also websites like https://www.planttext.com that can display live UML diagrams.
Task:
For any class, of which only the fully qualified name is given, generate a PlantUML diagram text, and output the text to the console.
The diagram should show the type and its base types (superclasses and implemented interfaces).
The diagram should also recursively list the types of the superclasses.
Example:
For
Class.forName("java.awt.Point")
, the output might look like this:Point2D <|-- Point Object <|-- Point2D interface Cloneable <<interface>> Cloneable <|.. Point2D interface Serializable <<interface>> Serializable <|.. Point hide members
1.1.2. Create UML class Diagram with Properties ⭐
In PlantUML, not only type relationships — such as inheritance, implementation of interfaces and associations — can be described, but also object/static variables and methods:
class Radio { isOn: boolean isOn() : boolean {static} format(number: int): String }
The result will look something like this:
Task:
Write a method that retrieves any
Class
object and returns a multi-line string in PlantUML syntax as the result.It is sufficient to include only the object/static variables, constructors, and methods, not the type relationships.
Example:
For type
java.awt.Dimension
the output might look like this:@startuml class Dimension { + width: int + height: int - serialVersionUID: long + Dimension(arg0: Dimension) + Dimension() + Dimension(arg0: int, arg1: int) + equals(arg0: Object): boolean + toString(): String + hashCode(): int + getSize(): Dimension - initIDs(): void + setSize(arg0: Dimension): void + setSize(arg0: double, arg1: double): void + setSize(arg0: int, arg1: int): void + getWidth(): double + getHeight(): double } @enduml
1.1.3. Generate CSV files from list entries ⭐⭐
In a CSV file, the entries are separated by comma or semicolon, it looks like this:
1;2;3 4;5;6
Task:
Write a static method
writeAsCsv(List<?> objects, Appendable out)
that traverses all objects in the list, extracts all information via reflection, and then writes the results in CSV format to the given output stream.To extract, we can either call the public JavaBean getters (if we want to go via properties) or access the (internal) instance variables — the solution can use one of the two variants.
Example usage:
Point p = new Point( 1, 2 );
Point q = new Point( 3, 4 );
List<?> list = Arrays.asList( p, q );
Writer out = new StringWriter();
writeAsCsv( list, out );
System.out.println( out );
Bonus: If you use accesses to instance variables, the instance variables marked with the modifier transient
should not be written.
1.2. Annotations
Annotations allow us to introduce metadata into Java code that can later read — usually via Reflection. Annotations have become essential because many developers express configurations declaratively and leave the actual execution to the framework.
1.2.1. Create CSV documents from annotated instance variables ⭐⭐
Given a class with annotations:
@Csv
class Pirate {
@CsvColumn String name;
@CsvColumn String profession;
@CsvColumn int height;
@CsvColumn( format = "### €" ) double income;
@CsvColumn( format = "###.00" ) Object weight;
String secrets;
}
Task:
Declare the annotation
@Csv
, which can only be set on type declarations.Declare the annotation
@CsvColumn
, which can only be set on instance variables.Allow a string attribute
format
at@CsvColumn
, for a pattern that controls the formatting of the number using aDecimalFormat
pattern.Create a class
CsvWriter
with a constructor that stores aClass
object as a type-token and also aWriter
, where the CSV rows will be written later. The classCsvWriter
can beAutoCloseable
.Create
CsvWriter
as a generic typeCsvWriter<T>
.Write two new methods
void writeObject(T object)
: Write an objectvoid write(Iterable<? extends T> iterable)
: Write multiple objects
The separator for the CSV columns is
';'
by default, but should be able to be changed via a methoddelimiter(char)
.Consider what error cases may occur and report them as an unchecked exception.
Example usage:
Pirate p1 = new Pirate();
p1.name = "Hotzenplotz";
p1.profession = null;
p1.height = 192;
p1.income = 124234.3234;
p1.weight = 89.10;
p1.secrets = "kinky";
StringWriter writer = new StringWriter();
try ( CsvWriter<Pirate> csvWriter =
new CsvWriter<>( Pirate.class, writer ).delimiter( ',' ) ) {
csvWriter.writeObject( p1 );
csvWriter.writeObject( p1 );
}
System.out.println( writer );
Solution: Csv, CsvColumn, ReflectionCsvExporter, CsvPirateWriter