Ein alternativer Blick mit Jackson Views

„Willst du dich am Ganzen erquicken, so musst du das Ganze im Kleinsten erblicken.“

Johann Wolfgang von Goethe

Liest ein Entwickler seine Source ein zweites Mal, dann findet er mindestens drei Dinge die ihm nicht gefallen. Es gibt immer ein Design-Pattern, das hätte verwendet werden können, eine Bibliothek, die den Code kompakter gestaltet hätte oder ein Algorithmus, der alles vereinfachen würde. Die tatsächliche Implementierung eines Features ergibt sich aus Erfordernissen, Erfahrung und Eingebung des Moments. So kann es immer wieder passieren, dass dem Entwickler die ein oder andere gute Idee nicht in den Sinn kommt.

Als ich den Beispiel Code für meinen Spring Boot Controller schrieb, wollte ich eine Liste alle Ahnen produzieren, die nur die Kerninformationen der Personen liefert. Die Liste sollte Namen, Geschlecht, Geburts- und Todesdatum enthalten. Außerdem natürlich einen Link auf die vollständigen Informationen zur Person.

{
  "person": {
    "sex": "♂",
    "name": "Johann /Wisloh/",
    "birth": { "place": "Jardinghausen" },
  },
  "_links": {
    "self": { "href": "http://localhost:8080/ancestor/gebdas/1151432013" }
  }
}

Damit nur die notwendigen Informationen in der Bean stehen, wurden diese Informationen in eine Kopie der Person gespeichert und alle anderen Felder frei gelassen.

@RequestMapping(path = "/ancestors", method = GET, produces = APPLICATION_JSON_VALUE)
public Resources<PersonResource> allAnchestor() throws IOException {
  List<PersonResource> allPersons = new ArrayList<>();
  for (Person person : service.getAll()) {
    Person copy = new Person();
    copy.setName(person.getName());
    copy.setBirth(person.getBirth());
    copy.setDeath(person.getDeath());
    copy.setSex(person.getSex());
    PersonResource resource = new PersonResource(copy);
    String[] parts = getUrnPart(resource.getPerson().getUrn());
    resource.add(linkTo(methodOn(AncestorController.class).anchestor(parts[0], parts[1])).withSelfRel());
    allPersons.add(resource);
  }
  Link link = linkTo(methodOn(AncestorController.class).allAnchestor()).withSelfRel();
  return new Resources<PersonResource>(allPersons, link);
}

Das Kopieren der Informationen ist nicht besonders schön. Es wäre viel eleganter, die Json Attribute zu definieren, die ausgegeben werden sollen.  Diese Möglichkeit bietet sich mit Jackson Views. Alle Attribute, die zu einem View gehören sollen, werden mit @JsonView annotiert. Als Wert der Annotation wird eine Klasse angegeben, mit der dieser View adressiert wird.

public class Person { 
  @JsonView(Views.Core.class) 
  private Sex sex; 
  @JsonView(Views.Core.class) 
  private String name; 
  @JsonView(Views.Short.class) 
  private String description; 
  private String submitterId; 
  // ...
}

Im obigen Beispiel gehören das Attribute sex und name zum View Views.Core und das Attribute description augenscheinlich zum View View.Short. Schauen wir uns die Definition dieser Klassen an, dann verstehen wir auch sofort, warum die @JsonView Annotation eine Klasse als Wert verwendet.

public class Views { 
  public static class Core { } 
  public static class Short extends Core {}
}

Die Klasse Short erweitert die Klasse Core und somit gehören alle Attribute, die zum View Views.Core gehören, auch zum View Views.Short. Nun bleibt also nur noch die Frage offen, wie wir Spring Boot dazu bewegen können, für die Methode allAnchestors unseren View Views.Core zu verwenden. Damit alles reibungslos funktioniert, müssen wir das default-view-inclusion Feature des verwendeten ObjectMapper auf true setzen.

@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
  return new Jackson2ObjectMapperBuilder().indentOutput(true)
    .dateFormat(new SimpleDateFormat("yyyy-MM-dd")).defaultViewInclusion(true);
}

Ansonsten benötigen wir nur noch die @JsonView Annotation an unserer Controller Methode und können den unnötigen Code zum Erstellen einer Kopie löschen.

@JsonView(Views.Core.class)
@RequestMapping(path = "/ancestors", method = GET, produces = APPLICATION_JSON_VALUE)
public Resources<PersonResource> allAnchestor() throws IOException {
  List<PersonResource> allPersons = new ArrayList<>();
  for (Person person : service.getAll()) {
    PersonResource resource = new PersonResource(person);
    String[] parts = getUrnPart(resource.getPerson().getUrn());
    resource.add(linkTo(methodOn(getClass()).byUrn(parts[0], parts[1])).withSelfRel());
    allPersons.add(resource);
  }
  return new Resources<PersonResource>(allPersons, linkTo(methodOn(getClass()).allAnchestor()).withSelfRel());

Nach diesem Refactoring ist unser Code  kompakter und einfacher zu verstehen und es gibt zwei Dinge, die dem Entwickler jetzt noch nicht so richtig gefallen.