Ein benutzerdefinierter Datenordner in Spring MVC

1. Übersicht

Dieser Artikel zeigt, wie wir den Datenbindungsmechanismus von Spring verwenden können, um unseren Code durch Anwenden automatischer Grundelemente auf Objektkonvertierungen klarer und lesbarer zu machen.

Standardmäßig kann Spring nur einfache Typen konvertieren. Mit anderen Worten, sobald wir Daten an den Controller- Typ Int , String oder Boolean senden , werden sie automatisch an die entsprechenden Java-Typen gebunden.

In realen Projekten reicht dies jedoch nicht aus, da möglicherweise komplexere Objekttypen gebunden werden müssen .

2. Einzelne Objekte an Anforderungsparameter binden

Beginnen wir einfach und binden zuerst einen einfachen Typ. Wir müssen eine benutzerdefinierte Implementierung der Converter- Schnittstelle bereitstellen, wobei S der Typ ist, von dem wir konvertieren, und T der Typ ist, in den wir konvertieren:

@Component public class StringToLocalDateTimeConverter implements Converter { @Override public LocalDateTime convert(String source) { return LocalDateTime.parse( source, DateTimeFormatter.ISO_LOCAL_DATE_TIME); } }

Jetzt können wir die folgende Syntax in unserem Controller verwenden:

@GetMapping("/findbydate/{date}") public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) { return ...; }

2.1. Verwenden von Aufzählungen als Anforderungsparameter

Als nächstes werden wir sehen, wie e num als RequestParameter verwendet wird .

Hier haben wir eine einfache Aufzählung Modi :

public enum Modes { ALPHA, BETA; }

Wir werden einen String to Enum Converter wie folgt erstellen :

public class StringToEnumConverter implements Converter { @Override public Modes convert(String from) { return Modes.valueOf(from); } }

Dann müssen wir unseren Konverter registrieren :

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new StringToEnumConverter()); } }

Jetzt können wir unsere Aufzählung als RequestParameter verwenden :

@GetMapping public ResponseEntity getStringToMode(@RequestParam("mode") Modes mode) { // ... }

Oder als PathVariable :

@GetMapping("/entity/findbymode/{mode}") public GenericEntity findByEnum(@PathVariable("mode") Modes mode) { // ... }

3. Binden einer Hierarchie von Objekten

Manchmal müssen wir den gesamten Baum der Objekthierarchie konvertieren, und es ist sinnvoll, eine zentralere Bindung anstelle einer Reihe einzelner Konverter zu verwenden.

In diesem Beispiel haben wir AbstractEntity als Basisklasse:

public abstract class AbstractEntity { long id; public AbstractEntity(long id){ this.id = id; } }

Und die Unterklassen Foo und Bar :

public class Foo extends AbstractEntity { private String name; // standard constructors, getters, setters }
public class Bar extends AbstractEntity { private int value; // standard constructors, getters, setters }

In diesem Fall können wir ConverterFactory implementieren, wobei S der Typ ist, von dem wir konvertieren, und R der Basistyp ist , der den Bereich der Klassen definiert, in die wir konvertieren können:

public class StringToAbstractEntityConverterFactory implements ConverterFactory{ @Override public  Converter getConverter(Class targetClass) { return new StringToAbstractEntityConverter(targetClass); } private static class StringToAbstractEntityConverter implements Converter { private Class targetClass; public StringToAbstractEntityConverter(Class targetClass) { this.targetClass = targetClass; } @Override public T convert(String source) { long id = Long.parseLong(source); if(this.targetClass == Foo.class) { return (T) new Foo(id); } else if(this.targetClass == Bar.class) { return (T) new Bar(id); } else { return null; } } } }

Wie wir sehen können, muss nur getConverter () implementiert werden, das den Konverter für den benötigten Typ zurückgibt. Der Konvertierungsprozess wird dann an diesen Konverter delegiert.

Dann müssen wir unsere ConverterFactory registrieren :

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToAbstractEntityConverterFactory()); } }

Schließlich können wir es in unserem Controller verwenden, wie wir möchten:

@RestController @RequestMapping("/string-to-abstract") public class AbstractEntityController { @GetMapping("/foo/{foo}") public ResponseEntity getStringToFoo(@PathVariable Foo foo) { return ResponseEntity.ok(foo); } @GetMapping("/bar/{bar}") public ResponseEntity getStringToBar(@PathVariable Bar bar) { return ResponseEntity.ok(bar); } }

4. Domänenobjekte binden

Es gibt Fälle, in denen wir Daten an Objekte binden möchten, diese jedoch entweder nicht direkt (z. B. aus Sitzungs- , Header- oder Cookie- Variablen) oder sogar in einer Datenquelle gespeichert sind. In diesen Fällen müssen wir eine andere Lösung verwenden.

4.1. Resolver für benutzerdefinierte Argumente

Zunächst definieren wir eine Anmerkung für solche Parameter:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface Version { }

Anschließend implementieren wir einen benutzerdefinierten HandlerMethodArgumentResolver:

public class HeaderVersionArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter methodParameter) { return methodParameter.getParameterAnnotation(Version.class) != null; } @Override public Object resolveArgument( MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest(); return request.getHeader("Version"); } }

Das Letzte ist, Spring wissen zu lassen, wo er nach ihnen suchen soll:

@Configuration public class WebConfig implements WebMvcConfigurer { //... @Override public void addArgumentResolvers( List argumentResolvers) { argumentResolvers.add(new HeaderVersionArgumentResolver()); } }

Das ist es. Jetzt können wir es in einem Controller verwenden:

@GetMapping("/entity/{id}") public ResponseEntity findByVersion( @PathVariable Long id, @Version String version) { return ...; }

Wie wir sehen können, gibt die resolveArgument () -Methode von HandlerMethodArgumentResolver ein Objekt zurück. Mit anderen Worten, wir könnten jedes Objekt zurückgeben, nicht nur String .

5. Schlussfolgerung

Infolgedessen haben wir viele Routinekonvertierungen beseitigt und Spring die meisten Dinge für uns erledigen lassen. Lassen Sie uns am Ende schließen:

  • Für eine einzelne einfache Konvertierung von Typ zu Objekt sollten wir die Converter- Implementierung verwenden
  • Um die Konvertierungslogik für eine Reihe von Objekten zu kapseln, können Sie die Implementierung von ConverterFactory ausprobieren
  • Wenn Daten indirekt eingehen oder zusätzliche Logik zum Abrufen der zugehörigen Daten angewendet werden muss, ist es besser, HandlerMethodArgumentResolver zu verwenden

Alle Beispiele finden Sie wie immer in unserem GitHub-Repository.