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 type

  • be 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 don’t want to 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 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:

PlantUmlExample1 UML
Figure 1. Representation of the PlantUML syntax as a graphic.

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
    Cloneable <|.. Point2D
    Serializable <|.. Point

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:

PlantUmlExample2 UML
Figure 2. Representation of PlantUML syntax as a graphic.

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) object 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 object variables, the object 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 very important, because many configurations developers express declaratively today and leave the actual execution to the framework.

1.2.1. Create CSV documents from annotated object 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 object variables.

  • Allow a string attribute format at @CsvColumn, for a pattern that controls the formatting of the number using a DecimalFormat pattern.

  • Create a class CsvWriter with a constructor that stores a Class object as a type token and also a Writer, where the CSV rows will be written later. The class CsvWriter can be AutoCloseable.

  • Create CsvWriter as a generic type CsvWriter<T>.

  • Write two new methods

    • void writeObject(T object): Write an object

    • void 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 method delimiter(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 );

1.3. Suggested solutions

1.3.1. Create UML class diagram with inheritance relationships

com/tutego/exercise/lang/reflect/PlantUmlTypeHierarchy.java
  public static void visitType( Class<?> clazz ) {

    if ( clazz.getSuperclass() != null ) {
      System.out.printf( "%s <|-- %s%n",
                         clazz.getSuperclass().getSimpleName(),
                         clazz.getSimpleName() );
      visitType( clazz.getSuperclass() );
    }

    for ( Class<?> interfaze : clazz.getInterfaces() ) {
      System.out.printf( "%s <|.. %s%n",
                         interfaze.getSimpleName(),
                         clazz.getSimpleName() );
      visitType( interfaze );
    }
  }

  public static void main( String[] args ) throws ClassNotFoundException {
//    Class<?> clazz = Class.forName( "javax.swing.JButton" );
    Class<?> clazz = Class.forName( "java.awt.Point" );
    visitType( clazz );
    System.out.println( "hide members" );
  }

At the heart of the solution is the custom method visitType(Class<?> clazz), which is called recursively. We don’t have to care about the object/class variables, methods and constructors, but only about the possible superclass and implemented interfaces.

The first case distinction takes care of the possible superclass. There can be at most one of these. On the Class object we call getSuperclass() and get either null, if we have already landed at java.lang.Object in the inheritance hierarchy, or just the superclass. If we have a superclass, the program generates an arrow.

And if we found a superclass, it will have a superclass again, so we call the visitType(…​) method recursively.

The second part generates the arrows for the implemented interfaces. The extended for loop traverses all interfaces. If the class does not implement an interface, the loop is not executed. If there is an interface, we ask for the name as well as the name of our own class and generate the arrow. Since this interface can extend other interfaces, we again call visitType(…​) recursively.

1.3.2. Create UML class Diagram with Properties

com/tutego/exercise/lang/PlantUmlClassMembers.java
private static String plantUml( Class<?> clazz ) {

  StringWriter result = new StringWriter( 1024 );
  PrintWriter body = new PrintWriter( result );
  body.printf( "@startuml%nclass %s {%n", clazz.getSimpleName() );

  for ( Field field : clazz.getDeclaredFields() ) {
    String visibility = formatUmlVisibility( field );
    String type = field.getType().getSimpleName();
    body.printf( "   %s %s: %s%n", visibility, field.getName(), type );
  }

  for ( Constructor<?> method : clazz.getConstructors() ) {
    body.printf( "   %s %s(%s)%n",
                 formatUmlVisibility( method ),
                 clazz.getSimpleName(),
                 formatParameters( method.getParameters() ) );
  }

  for ( Method method : clazz.getDeclaredMethods() ) {
    String visibility = formatUmlVisibility( method );
    String parameters = formatParameters( method.getParameters() );
    String returnType = method.getReturnType().getSimpleName();
    body.printf( "   %s %s(%s): %s%n",
                 visibility, method.getName(), parameters, returnType );
  }

  body.println( "}\n@enduml" );
  return result.toString();
}

private static String formatParameters( Parameter[] parameters ) {
  return Arrays.stream( parameters )
               .map( p -> p.getName() + ": " + p.getType().getSimpleName() )
               .collect( Collectors.joining( ", " ) );
}

private static String formatUmlVisibility( Member field ) {
  return Modifier.isPrivate( field.getModifiers() )   ? "-" :
         Modifier.isPublic( field.getModifiers() )    ? "+" :
         Modifier.isProtected( field.getModifiers() ) ? "#" :
         "~";
}

The focus is on the plantUml(Class) &#8212 method; it generates the text for the UML diagram for a Class object. Since we need to build a string, we have several options. We could use the + operator to concatenate strings, or we could use the StringBuilder and use the append(…​) methods, however it would be handy if we could use a format string. Here we can use the PrintWriter class, which we already know in an API-like form from System.out. A PrintWriter offers us the nice print(…​), println(…​) and printf(…​) methods. A PrintWriter is an adapter that needs a target into which it writes the generated string. We want to collect the result in a StringWriter.

Then we start to write the single segments of a UML diagram. First comes the class name, then we put in the object/class variables, then the constructors, and finally the methods. The Class object provides us with the data via corresponding methods. There is one thing in common for the object/class variables, constructors, and methods, and that is their visibility. This can be queried via the base type Member. The own method formatUmlVisibility(Member) translates the modifier into a string, which we include in the PlantUML code for the visibility.

Constructors and methods have another thing in common, a parameter list. Therefore there is another method formatParameters(Parameter[]) which generates a string for PlantUML from an array of parameter objects. It is array because a method or constructor can have multiple parameters, and consequently we need to ask for the name of each parameter, and the return type. This can be seen as a step-by-step transformation, and this is where the Stream API comes in handy. First, we generate a Stream from the array. In the next step we transfer the Parameter object to a string. Here we proceed as follows: The string consists of the name of the parameter, a colon and a data type. After the map(…​) operation a Stream<String> is created. This string is comma-separated to a large result and returned.

1.3.3. Generate CSV files from list entries

com/tutego/exercise/lang/reflect/ReflectionCsvExporter.java
public static void writeAsCsv( List<?> objects, Appendable out )
    throws IOException {
  for ( Object object : objects ) {
    String line =
        Arrays.stream( object.getClass().getFields() )
              .filter( f -> ! Modifier.isTransient( f.getModifiers() ) )
              .map( f -> accessField( f, object ) )
              .collect( Collectors.joining( ";" ) );
    out.append( line ).append( "\n" );
  }
}

private static String accessField( Field field, Object object ) {
  try {
    return field.get( object ).toString();
  }
  catch ( IllegalAccessException e ) {
    throw new RuntimeException( e );
  }
}

The solution consists of the desired method writeAsCsv(List<?> objects, Appendable out) and an helper method accessField(…​). Since we have a list of arbitrary elements, we must first traverse the list. This is where the extended for loop comes in handy.

When we consider each item from the list, we need to generate a line for each item. This sequence of operations, from an object to the CSV line, is realized by the Stream API. Taking an object as a starting point, we first query the Class object and then all object/class variables with getFields(). The result is an array that we want to enhance to a stream. The stream thus consists of Field objects, and since we do not want to consider transient fields, we use the filter method from the stream to leave only the object/class variables in the stream that are not transient.

In the next step, we need to read the object variable. For this we rely on a separate method. The reason is that lambda expressions and checked exceptions become syntactically cluttered, because a checked exception must be caught within the lambda expression. However, the Reflection API uses checked exceptions frequently.The purpose of the custom method String accessField(Field, Object) is to read an object variable for a given object, and convert it to a string. The method catches a possible checked exception and encapsulates it into an unchecked exception. The stream’s terminal operation collects all partial strings and concatenates them with a semicolon. Finally, the line, including an end-of-line character, is written to the writer.

1.3.4. Create CSV documents from annotated object variables

Two annotation types must be declared for the solution. The first type Csv is @Target(ElementType.TYPE) because it is only allowed to be at type declarations, and of course this annotation must be read at runtime, so the RetentionPolicy.RUNTIME:

com/tutego/exercise/annotation/Csv.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.TYPE )
public @interface Csv {
}

The second annotation type CsvColumn is only attached to class/object variables, so @Target(ElementType.FIELD). However, it is not possible to restrict the application to object variables alone.

This annotation is also read at runtime. CsvColumn has a format attribute for the formatting string, which is empty by default. We will not evaluate empty strings later.

com/tutego/exercise/annotation/CsvColumn.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.FIELD )
public @interface CsvColumn {
  String format() default "";
}

Let’s move on to the comprehensive CsvWriter:

com/tutego/exercise/annotation/CsvWriter.java
public class CsvWriter<T> implements AutoCloseable {

  private final List<Field> fields;
  private final Class<?> clazz;
  private final Writer writer;
  private char delimiter = ';';

  public CsvWriter( Class<T> clazz, Writer writer ) {
    if ( ! clazz.isAnnotationPresent( Csv.class ) )
      throw new IllegalArgumentException(
          "Given class is not annotated with @Csv" );

    fields = Arrays.stream( clazz.getDeclaredFields() )
                   .filter( field -> field.isAnnotationPresent( CsvColumn.class ) )
                   .collect( Collectors.toList() );

    if ( fields.isEmpty() )
      throw new IllegalArgumentException(
          "Class does not contain any @CsvColumn" );

    this.clazz = clazz;
    this.writer = Objects.requireNonNull( writer );
  }

  public CsvWriter<T> delimiter( char delimiter ) {
    this.delimiter = delimiter;
    return this;
  }

  public void write( Iterable<? extends T> iterable ) {
    iterable.forEach( this::writeObject );
  }

  public void writeObject( T object ) {

    if ( ! clazz.isInstance( object ) )
      throw new IllegalArgumentException(
            "Argument is of type " + object.getClass().getSimpleName()
          + " but must be of type " + clazz.getSimpleName() );

    String line = fields.stream()
                        .map( field -> getFieldValue( object, field ) )
                        .collect( Collectors.joining( Character.toString( delimiter ),
                                                      "", "\n" ) );
    try {
      writer.write( line );
    }
    catch ( IOException e ) {
      throw new UncheckedIOException( e );
    }
  }

  private String getFieldValue( Object object, Field field ) {
    try {
      Object fieldValue = field.get( object );

      if ( fieldValue == null )
        return "";

      String format = field.getAnnotation( CsvColumn.class ).format();
      if ( format.trim().isEmpty() )
        return Objects.toString( fieldValue );

      if ( isNumericType( fieldValue ) )
        return new DecimalFormat( format ).format( fieldValue );

      throw new IllegalStateException( "Only numeric types can be formatted, but type was "
                                         + fieldValue.getClass().getSimpleName() );
    }
    catch ( IllegalAccessException e ) {
      throw new RuntimeException( e );
    }
  }

  private static boolean isNumericType( Object value ) {
    return Stream.of( Integer.class, Long.class, Double.class,
                      BigInteger.class, BigDecimal.class )
                 .anyMatch( clazz -> clazz.isInstance( value ) );
  }

  @Override public void close() {
    try {
      writer.close();
    }
    catch ( IOException e ) {
      throw new UncheckedIOException( e );
    }
  }
}

The constructor takes a Class object and a Writer and performs checks and preprocessing.

  1. The constructor first performs a test whether what is to be written later has the marking annotation Csv; if not, there is a runtime exception. This way, this test does not have to be repeated later when writing. Since the types will also always be the same when writing later, and reflection at runtime should be avoided for performance reasons, the constructor fetches the corresponding Field objects and stores them. Only the Field objects annotated with @CsvColumn are placed in the internal list. If this list is empty, there is an exception, because this would mean that there is nothing to write. Also the constructor remembers the Class object for a later test, because generics are just a trick of the compiler and at runtime wrong types could be foisted after all.

  2. The Writer is stored internally, and as usual an early test ensures that this Writer is not null. Our class implements AutoCloseable, and the implemented close() method closes the underlying Writer. Thus CsvWriter can be used well in a try-with-resources block.

The delimiter, that is the CSV separator for the columns, can be reassigned by delimiter(char). The method returns the current CsvWriter, so that calls cascade well in a fluent API way. Whether the delimiter is reasonable or not is not tested by the method. In principle, the delimiter could be a 'a', '\n' or '\u000'.

writeObject(T) writes a single object that the write(Iterable<? extends T>) method can well access. The forEach(…​) method provided by Iterable runs over all the data and calls writeObject(T) on each individual element.

First, writeObject(T) performs a type check to see if the argument is type-compatible with the one declared in the constructor via the Class object. clazz.isInstance(…​) is the dynamic variant of instanceof. If the type does not match, there is an exception. In the next step the Stream walks over all Field elements, gets the value of the object variable converted to a string for each Field with getFieldValue(…​) and finally assembles all strings with the delimiter for the CSV output. At the end of the line there is a line break. The resulting string is written to Writer and a possible IOException is caught and translated into an unchecked exception. getFieldValue(…​) is a separate method that hides the complexity for accessing and formatting numeric values.

The getFieldValue(…​) method gets the object with the data and the Field object for the object variable. The Field method get(…​) returns the value stored in the object variable. If it is null, there is nothing to write and we return the empty string.

Now there are two possibilities: At the CsvColumn there is the format attribute with a formatting pattern or not.

  • If there is no formatting pattern, or the format consists only of blanks, then the string representation of the read value is returned.

  • If there is a formatting pattern, isNumericType(…​) checks whether the value in the attribute is numeric. This property could have been tested already in the constructor, but this would have a disadvantage: If the type is Object for example and there is a Double behind it at runtime, this is perfectly fine. Only at runtime this can be tested. For a numeric attribute, the format(…​) method of DecimalFormat returns the string representation. If a formatting pattern is given but the type is not numeric, an IllegalStateException follows.

DecimalFormat can format different types automatically. The valid types are tested via the own method isNumericType(…​). These include Integer, Long, Double, BigInteger, BigDecimal.