Een aangepaste gegevensbinder in Spring MVC

1. Overzicht

Dit artikel laat zien hoe we het gegevensbindingsmechanisme van Spring kunnen gebruiken om onze code duidelijker en leesbaarder te maken door automatische primitieven toe te passen op objectconversies.

Standaard weet Spring alleen hoe eenvoudige typen moeten worden geconverteerd. Met andere woorden, zodra we gegevens aan de controller hebben verstrekt Int, Draad of Boolean type gegevens, wordt deze automatisch aan de juiste Java-typen gebonden.

Maar in echte projecten zal dat niet genoeg zijn, zoals we moeten mogelijk complexere typen objecten binden.

2. Afzonderlijke objecten binden aan verzoekparameters

Laten we eenvoudig beginnen en eerst een eenvoudig type binden; we zullen een aangepaste implementatie van het Converter interface waar S is het type waarvan we converteren, en T is het type waarnaar we converteren:

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

Nu kunnen we de volgende syntaxis gebruiken in onze controller:

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

2.1. Enums gebruiken als verzoekparameters

Vervolgens zullen we zien hoe te gebruiken enum als een RequestParameter.

Hier hebben we een simpele opsommingModi:

openbare opsomming Modi {ALPHA, BETA; }

We bouwen een Draad naar enum-omzetter als volgt:

openbare klasse StringToEnumConverter implementeert Converter {@Override public Modes convert (String from) {return Modes.valueOf (from); }}

Vervolgens moeten we onze Converter:

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

Nu kunnen we onze Enum als een RequestParameter:

@GetMapping openbare ResponseEntity getStringToMode (@RequestParam ("mode") Modes-modus) {// ...}

Of als een PadVariabele:

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

3. Een hiërarchie van objecten binden

Soms moeten we de hele structuur van de objecthiërarchie converteren en is het logisch om een ​​meer gecentraliseerde binding te hebben in plaats van een reeks individuele converters.

In dit voorbeeld hebben we AbstractEntity onze basisklasse:

openbare abstracte klasse AbstractEntity {lange id; openbare AbstractEntity (lange id) {this.id = id; }}

En de subklassen Foo en Bar:

openbare klasse Foo breidt AbstractEntity {private String-naam uit; // standaard constructeurs, getters, setters}
public class Bar breidt AbstractEntity {private int-waarde uit; // standaard constructeurs, getters, setters}

In dit geval, we kunnen implementeren ConverterFactory waarbij S het type is waarvan we converteren en R het basistype is het definiëren van het bereik van klassen waarnaar we kunnen converteren:

openbare klasse StringToAbstractEntityConverterFactory implementeert ConverterFactory {@Override openbare Converter getConverter (klasse targetClass) {retourneer nieuwe StringToAbstractEntityConverter (targetClass); } private statische klasse StringToAbstractEntityConverter implementeert Converter {private Class targetClass; openbare 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 {retourneer null; }}}}

Zoals we kunnen zien, is de enige methode die moet worden geïmplementeerd, is getConverter () die de converter voor het benodigde type retourneert. Het conversieproces wordt vervolgens gedelegeerd aan deze converter.

Vervolgens moeten we onze ConverterFactory:

@Configuration openbare klasse WebConfig implementeert WebMvcConfigurer {@Override openbare ongeldige addFormatters (FormatterRegistry-register) {registry.addConverterFactory (nieuwe StringToAbstractEntityConverterFactory ()); }}

Eindelijk kunnen we het gebruiken zoals we willen in onze controller:

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

4. Bindende domeinobjecten

Er zijn gevallen waarin we gegevens aan objecten willen binden, maar deze komen ofwel op een niet-directe manier (bijvoorbeeld van Sessie, Header of Koekje variabelen) of zelfs opgeslagen in een gegevensbron. In die gevallen moeten we een andere oplossing gebruiken.

4.1. Aangepaste argumentresolver

Allereerst zullen we een annotatie voor dergelijke parameters definiëren:

@Retention (RetentionPolicy.RUNTIME) @Target (ElementType.PARAMETER) openbare @interface-versie {}

Vervolgens implementeren we een maatwerk HandlerMethodArgumentResolver:

openbare klasse HeaderVersionArgumentResolver implementeert HandlerMethodArgumentResolver {@Override openbare boolean supportsParameter (MethodParameter methodParameter) {return methodParameter.getParameterAnnotation (Version.class)! = null; } @Override public Object resolArgument (MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) gooit Uitzondering {HttpServletRequest request = (HttpServletRequest) nativeWebRequest; return request.getHeader ("Versie"); }}

Het laatste is om Spring te laten weten waar ze naar moeten zoeken:

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

Dat is het. Nu kunnen we het in een controller gebruiken:

@GetMapping ("/ entity / {id}") openbare ResponseEntity findByVersion (@PathVariable Lange id, @Version String-versie) {return ...; }

Zoals we kunnen zien, HandlerMethodArgumentResolver‘S resolArgument () methode retourneert een Voorwerp. Met andere woorden, we zouden elk object kunnen retourneren, niet alleen Draad.

5. Conclusie

Als gevolg hiervan hebben we veel routinematige conversies verwijderd en hebben we Spring de meeste dingen voor ons laten doen. Laten we aan het einde concluderen:

  • Voor een individueel eenvoudig type om conversies tegen te gaan, moeten we gebruiken Converter implementatie
  • Voor het inkapselen van conversielogica voor een reeks objecten, kunnen we proberen ConverterFactory implementatie
  • Alle gegevens komen indirect of het is vereist om extra logica toe te passen om de bijbehorende gegevens op te halen die beter te gebruiken zijn HandlerMethodArgumentResolver

Zoals gewoonlijk zijn alle voorbeelden altijd te vinden in onze GitHub-repository.