Eine benutzerdefinierte Datenbindung im Frühjahr MVC
1. Überblick
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 Datentyp des ControllersInt,String oderBoolean senden, werden sie automatisch an die entsprechenden Java-Typen gebunden.
In realen Projekten reicht dies jedoch nicht aus, dawe might need to bind more complex types of objects.
2. Einzelne Objekte an Anforderungsparameter binden
Beginnen wir einfach und binden zuerst einen einfachen Typ. Wir müssen eine benutzerdefinierte Implementierung derConverter<S, T>-Schnittstelle bereitstellen, wobeiS der Typ ist, von dem wir konvertieren, undT 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 in unserem Controller die folgende Syntax verwenden:
@GetMapping("/findbydate/{date}")
public GenericEntity findByDate(@PathVariable("date") LocalDateTime date) {
return ...;
}
2.1. Verwenden von Aufzählungen als Anforderungsparameter
Als nächstes sehen wirhow to use enum as a RequestParameter.
Hier haben wir ein einfachesenumModes:
public enum Modes {
ALPHA, BETA;
}
Wir erstellen einString bisenum Converter wie folgt:
public class StringToEnumConverter implements Converter {
@Override
public Modes convert(String from) {
return Modes.valueOf(from);
}
}
Dann müssen wir unsereConverter registrieren:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
}
Jetzt können wir unsereEnum alsRequestParameter verwenden:
@GetMapping
public ResponseEntity
Oder alsPathVariable:
@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 als eine Reihe einzelner Konverter zu verwenden.
In diesem Beispiel haben wirAbstractEntity unsere Basisklasse:
public abstract class AbstractEntity {
long id;
public AbstractEntity(long id){
this.id = id;
}
}
Und die UnterklassenFoo undBar:
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 definiertwe can implement ConverterFactory<S, R> where S will be the type we are converting from and R to be the base typeden Bereich der Klassen, in die konvertiert werden kann:
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 nurgetConverter() implementiert werden, die den Konverter für den benötigten Typ zurückgibt. Der Konvertierungsprozess wird dann an diesen Konverter delegiert.
Dann müssen wir unsereConverterFactory registrieren:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
}
}
Schließlich können wir es verwenden, wie wir es in unserem Controller mögen:
@RestController
@RequestMapping("/string-to-abstract")
public class AbstractEntityController {
@GetMapping("/foo/{foo}")
public ResponseEntity
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 den VariablenSession,Header oderCookie) oder sogar in eine Datenquelle. In diesen Fällen müssen wir eine andere Lösung verwenden.
4.1. Resolver für benutzerdefinierte Argumente
Zunächst definieren wir eine Annotation für solche Parameter:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Version {
}
Dann implementieren wir ein benutzerdefiniertesHandlerMethodArgumentResolver:
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 dieresolveArgument()-Methode vonHandlerMethodArgumentResolverObject. zurück. Mit anderen Worten, wir können jedes Objekt zurückgeben, nicht nurString.
5. Fazit
Infolgedessen haben wir viele Routinekonvertierungen aufgegeben 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 Implementierung vonConverterverwenden
-
Um die Konvertierungslogik für eine Reihe von Objekten zu kapseln, können Sie die Implementierung vonConverterFactoryversuchen
-
Wenn Daten indirekt eingehen oder zusätzliche Logik zum Abrufen der zugehörigen Daten angewendet werden muss, ist es besser,HandlerMethodArgumentResolver zu verwenden
Wie üblich finden Sie alle Beispiele immer bei unserenGitHub repository.