„Ich habe mir immer gewünscht, dass mein Computer so leicht zu bedienen ist wie mein Telefon; mein Wunsch ging in Erfüllung: Mein Telefon kann ich jetzt auch nicht mehr bedienen.“
Bjarne Stroustrup
Immer wieder kommt es vor, dass man im eigenen Spring Boot REST-Controller Klassen verwenden möchte, die nicht dafür konstruiert wurden. In der Regel trifft dies auf Klassen zu, die aus Dritt-Bibliotheken stammen.
Für die Vereinsverwaltung der Ahnenforscher sollen die Mitglieder mit ihrer Telefonnummer dargestellt werden. Dafür wird in der JSON Response ein Attribut phone
benötigt, dass die Telefonnummer als Text enthält.
{ "firstname": "Jens", "lastname": "Kaiser", "phone": "+49 521 112233", "member-id": "A-0001" }
In der Mitgliederverwaltung existiert das folgende DTO Member
.
@Getter @Setter public class Member { @NotBlank private String firstname; @NotBlank private String lastname; @NotNull @GermanCountryCode private InternationalPhoneNumber phone; @NotNull @MemberId private String memberId; @NotNull @Past private LocalDate dayOfBirth; }
In der Klasse ist das Attribute phone
vom Typ InternationalPhoneNumber
aus der Telephone Bibliothek. Diese Klasse wird aber leider nicht, wie gewünscht, in der JSON Antwort dargestellt.
Unter der Haube von Spring Boot arbeitet die Bibliothek Jackson, die für solche Fälle Serialisierer und Deserialisierer zur Verfügung stellt.
Der Serialisierer schreibt den Wert in die JSON Ausgabe und kann durch die Erweiterung der Klasse StdSerializer
implementiert werden.
final lass InternationalPhoneNumberSimpleSerializer extends StdSerializer<InternationalPhoneNumber> { InternationalPhoneNumberSimpleSerializer() { super(InternationalPhoneNumber.class); } @Override public void serialize(InternationalPhoneNumber value, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeString(value.toString()); } }
Die Implementierung des InternationalPhoneNumberSimpleSerializer
ist sehr einfach, weil die toString()
Methode der InternationalPhoneNumber
schon die korrekte Ausgabe produziert.
final class InternationalPhoneNumberSimpleDeserializer extends StdDeserializer<InternationalPhoneNumber> { InternationalPhoneNumberSimpleDeserializer() { super(InternationalPhoneNumber.class); } @Override public InternationalPhoneNumber deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { try { return InternationalPhoneNumber.parse(p.getValueAsString()); } catch (IllegalArgumentException e) { return (InternationalPhoneNumber) ctxt.handleWeirdStringValue(_valueClass, p.getValueAsString(), "not a valid representation (error: %s)", ClassUtil.exceptionMessage(e)); } } }
Die InternationalPhoneNumberSimpleDeserializer
Implementierung ist etwas komplizierter, da hier mögliche Fehler beim Auswerten der Textdarstellung abgefangen werden muss. Fehler, die beim Parsen der Telefonnummer auftreten, werden durch die Methode handleWeirdStringValue
zu einer Jackson spezifischen Fehlermeldung konvertiert.
Beide Implementierungen müssen im Jackson ObjectMapper
bekannt gemacht werden. Eine schöne Möglichkeit die Konfigurationen zusammenzuhalten, ist das Jackson Modul Konzept. Die Konfiguration wird in der setupModule()
Methode des eigenen TelephoneModuls
durchgeführt und das Modul seinerseits über die registerModule()
Methode am ObjectMapper
registriert.
ObjectMapper objectMapper = new ObjectMapper().registerModule(new TelephoneModule());
Die Implementierung des TelephoneModule
fügt jeweils eine Instanz des InternationalPhoneNumberSimpleSerializer
und InternationalPhoneNumberSimpleDeserializer
in den SetupContext
ein.
public class TelephoneModule extends SimpleModule { @Override public void setupModule(SetupContext context) { setUpModules(context, new InternationalPhoneNumberSimpleSerializer(), new InternationalPhoneNumberSimpleDeserializer()); } private void setUpModules(SetupContext context, JsonSerializer<?> ser, JsonDeserializer<?> deser) { context.addSerializers(new SimpleSerializers(List.of(ser))); context.addDeserializers(new SimpleDeserializers(Map.of(InternationalPhoneNumber.class, deser))); } }
Um das TelephoneModule
in der eigenen Spring Boot Anwendung zu nutzen, fehlt nur noch die Anpassung des dort verwendeten ObjectMappers
.
@Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> builder.modulesToInstall(new TelephoneModule()); }
Es sollte darauf geachtet werden modulesInstall()
und nicht modules()
zu verwenden. Ansonsten werden alle standardmäßig konfigurierten Module überschrieben.
Damit ist eine erste Version der Jackson Unterstützung für InternationalPhoneNumber
implementiert. Im nächsten Beitrag wird alles noch ein bisschen komfortabler.