Spring MVC: Wie man ein Custom Converter für Typsicherheit in Controller verwendet
Autor
Marcus HeldSind deine Controller andauernd mit Primitiven voll? Im ArticleController
lautet die erste Zeile jeder Methode: articleRepository.findById(articleId)
. Immer wieder machen wir die gleichen Konvertierungen. In kaum einem anderen Layer beobachte ich so viele Primitive wie im API Layer. Das führt zu viel Boilerplate. Der Code wird schwerer zu testen. Und er ist schwieriger zu lesen. Dabei wissen wir wie viel einfacher es ist, wenn wir typsicher arbeiten. In diesem Artikel zeige ich dir, wie du das Converter
Interface nutzen kannst, um deine primitiven automatisch in den korrekten Typ zu überführen.
Ein Converter in Spring ist ein einfaches Interface, das eine Transformation zwischen Typen definiert. Das Interface sieht folgendermaßen aus:
@FunctionalInterface
public interface Converter<S, T> {
/**
* Convert the source object of type {@code S} to target type {@code T}.
* @param source the source object to convert, which must be an instance of {@code S} (never {@code null})
* @return the converted object, which must be an instance of {@code T} (potentially {@code null})
* @throws IllegalArgumentException if the source cannot be converted to the desired target type
*/
@Nullable
T convert(S source);
}
Ein Converter sorgt für Typsicherheit
Die Typsicherheit ermöglicht es, deinen Code robuster und weniger fehleranfällig zu gestalten. Durch die Implementierung eines Converters kannst du sicherstellen, dass die Daten, die durch die verschiedenen Schichten deiner Anwendung fließen, stets den erwarteten Typen entsprechen. Das trägt nicht nur zur Codequalität bei, sondern auch zur Wartbarkeit und Verständlichkeit des Codes.
Zum Beispiel kann ein Converter in Kombination mit @PathVariable
genutzt werden
Nehmen wir an, du hast eine API, die unter GET /api/v2/article/myArticleId die Details eines Artikels liefert. Mithilfe von @PathVariable
kannst du auf myArticleId
zugreifen:
@RestController
@RequestMapping("/api/v2/article")
public class UserController {
private final ArticleRepository articleRepository;
public UserController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@GetMapping("/{myArticleId}")
public ArticleDTO getArticle(@PathVariable long myArticleId) {
Article article = articleRepository.findById(myArticleId);
return ArticleDTO.from(article);
}
}
In dieser Implementierung werden wir immer wieder die gleiche Konvertierung machen. In allen weiteren Operationen rufen wir zuerst articleRepository#findById
auf. Was in diesem kleinen Beispiel kein wirkliches Problem ist, wird zu einem, sobald unsere API komplexer wird.
Mit einem eigenen Converter können wir diese Komplexität verringern
Und das ist gar nicht kompliziert. Um obige Konvertierung zentral zu machen reicht folgende Implementierung:
@Component
public class LongToArticleConverter implements Converter<Long, Article> {
private final ArticleRepository articleRepository;
public LongToArticleConverter(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@Override
public Article convert(Long articleId) {
return articleRepository.findById(articleId);
}
}
Durch @Component
haben wir den Converter automatisch dem Context bekannt gemacht
Converter
als Bean registrieren, dann nutzen auch andere Module von Spring diesen. Beispielsweise Spring Integration oder Spring Data. Wenn du das nicht möchtest, dann kannst du einen Converter exklusiv in Spring MVC registrieren, in dem du WebMvcConfigurer
implementierst und addFormatters
überschreibst.Jetzt kannst du Article
direkt im Controller verwenden
Der schwierige Teil ist bereits geschafft. Wir können unsere Implementierung von oben durch die folgende ersetzen:
@RestController
@RequestMapping("/api/v2/article")
public class UserController {
@GetMapping("/{myArticleId}")
public ArticleDTO getArticle(@PathVariable Article article) {
return ArticleDTO.from(article);
}
}
Ist das nicht viel simpler? Unser Controller beschreibt eindeutig seinen Purpose. Und diese Methode ist nicht nur leicht zu lesen. Sie ist auch leicht zu testen. Du kannst jetzt einen Unit-Test schreiben, der ohne einen mock auskommt. Wir haben sauberen, modularen und leicht verständlichen Code geschrieben.