Un raccoglitore di dati personalizzato in Spring MVC

1. Panoramica

Questo articolo mostrerà come possiamo usare il meccanismo di Data Binding di Spring per rendere il nostro codice più chiaro e leggibile applicando primitive automatiche alle conversioni di oggetti.

Per impostazione predefinita, Spring sa solo come convertire i tipi semplici. In altre parole, una volta inviati i dati al tipo di dati Int , String o Boolean del controller, verranno associati automaticamente ai tipi Java appropriati.

Ma nei progetti del mondo reale, ciò non sarà sufficiente, poiché potrebbe essere necessario legare tipi di oggetti più complessi .

2. Associazione di singoli oggetti ai parametri di richiesta

Iniziamo semplice e prima leghiamo un tipo semplice; dovremo fornire un'implementazione personalizzata dell'interfaccia del convertitore in cui S è il tipo da cui stiamo convertendo e T è il tipo in cui stiamo convertendo:

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

Ora possiamo usare la seguente sintassi nel nostro controller:

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

2.1. Utilizzo di enumerazioni come parametri di richiesta

Successivamente, vedremo come utilizzare e num come RequestParameter .

Qui abbiamo una semplice modalità enum :

public enum Modes { ALPHA, BETA; }

Costruiremo un convertitore String to enum come segue:

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

Quindi, dobbiamo registrare il nostro convertitore :

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

Ora possiamo usare il nostro Enum come RequestParameter :

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

O come PathVariable :

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

3. Associazione di una gerarchia di oggetti

A volte è necessario convertire l'intero albero della gerarchia degli oggetti e ha senso avere un'associazione più centralizzata anziché un insieme di convertitori individuali.

In questo esempio, abbiamo AbstractEntity la nostra classe base:

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

E le sottoclassi Foo e 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 questo caso, possiamo implementare ConverterFactory dove S sarà il tipo da cui stiamo convertendo e R sarà il tipo di base che definisce l'intervallo di classi in cui possiamo convertire:

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; } } } }

Come possiamo vedere, l'unico metodo che deve implementare è getConverter () che restituisce il convertitore per il tipo necessario. Il processo di conversione viene quindi delegato a questo convertitore.

Quindi, dobbiamo registrare la nostra ConverterFactory :

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

Infine, possiamo usarlo come preferiamo nel nostro controller:

@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. Binding di oggetti di dominio

Ci sono casi in cui vogliamo associare i dati agli oggetti, ma provengono o in modo non diretto (ad esempio, dalle variabili Session , Header o Cookie ) o anche memorizzati in un'origine dati. In questi casi, dobbiamo utilizzare una soluzione diversa.

4.1. Risolutore di argomenti personalizzato

Prima di tutto, definiremo un'annotazione per tali parametri:

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

Quindi, implementeremo un HandlerMethodArgumentResolver personalizzato :

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"); } }

L'ultima cosa è far sapere a Spring dove cercarli:

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

Questo è tutto. Ora possiamo usarlo in un controller:

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

Come possiamo vedere, HandlerMethodArgumentResolver s' resolveArgument () restituisce un oggetto. In altre parole, potremmo restituire qualsiasi oggetto, non solo String .

5. conclusione

Di conseguenza, ci siamo liberati di molte conversioni di routine e abbiamo lasciato che Spring facesse la maggior parte delle cose per noi. Alla fine, concludiamo:

  • Per un singolo tipo semplice per conversioni di oggetti dovremmo usare l' implementazione di Converter
  • Per incapsulare la logica di conversione per una serie di oggetti, possiamo provare l' implementazione di ConverterFactory
  • Per tutti i dati che arrivano indirettamente o è necessario applicare una logica aggiuntiva per recuperare i dati associati è meglio usare HandlerMethodArgumentResolver

Come al solito, tutti gli esempi possono essere sempre trovati nel nostro repository GitHub.