Un classeur de données personnalisé dans Spring MVC

1. Vue d'ensemble

Cet article montrera comment nous pouvons utiliser le mécanisme de liaison de données de Spring afin de rendre notre code plus clair et plus lisible en appliquant des primitives automatiques aux conversions d'objets.

Par défaut, Spring ne sait convertir que des types simples. En d'autres termes, une fois que nous soumettons les données au contrôleur de type Int , String ou Boolean , elles seront automatiquement liées aux types Java appropriés.

Mais dans les projets réels, cela ne suffira pas, car nous pourrions avoir besoin de lier des types d'objets plus complexes .

2. Lier des objets individuels à des paramètres de requête

Commençons par simple et lions d'abord un type simple; nous devrons fournir une implémentation personnalisée de l' interface ConverterS est le type à partir duquel nous convertissons et T est le type vers lequel nous convertissons:

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

Nous pouvons maintenant utiliser la syntaxe suivante dans notre contrôleur:

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

2.1. Utilisation d'énumérations comme paramètres de requête

Ensuite, nous verrons comment utiliser e num comme RequestParameter .

Ici, nous avons une simple énumération Modes :

public enum Modes { ALPHA, BETA; }

Nous allons créer un convertisseur String to enum comme suit:

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

Ensuite, nous devons enregistrer notre convertisseur :

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

Nous pouvons maintenant utiliser notre Enum comme RequestParameter :

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

Ou comme variable PathVariable :

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

3. Lier une hiérarchie d'objets

Parfois, nous devons convertir l'arborescence entière de la hiérarchie d'objets et il est logique d'avoir une liaison plus centralisée plutôt qu'un ensemble de convertisseurs individuels.

Dans cet exemple, nous avons AbstractEntity notre classe de base:

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

Et les sous-classes Foo et 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 }

Dans ce cas, nous pouvons implémenter ConverterFactory où S sera le type à partir duquel nous convertissons et R le type de base définissant la plage de classes vers lesquelles nous pouvons convertir:

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

Comme nous pouvons le voir, la seule méthode à implémenter est getConverter () qui renvoie le convertisseur pour le type nécessaire. Le processus de conversion est ensuite délégué à ce convertisseur.

Ensuite, nous devons enregistrer notre ConverterFactory :

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

Enfin, nous pouvons l'utiliser comme nous le souhaitons dans notre contrôleur:

@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. Objets de domaine de liaison

Il y a des cas où nous voulons lier des données à des objets, mais cela vient soit de manière non directe (par exemple, à partir de variables Session , Header ou Cookie ) ou même stocké dans une source de données. Dans ces cas, nous devons utiliser une solution différente.

4.1. Résolveur d'arguments personnalisés

Tout d'abord, nous définirons une annotation pour ces paramètres:

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

Ensuite, nous implémenterons un HandlerMethodArgumentResolver personnalisé :

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

La dernière chose à faire est d'indiquer à Spring où les rechercher:

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

C'est ça. Maintenant, nous pouvons l'utiliser dans un contrôleur:

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

Comme on peut le voir, HandlerMethodArgumentResolver de resolveArgument () méthode renvoie un Object. En d'autres termes, nous pourrions renvoyer n'importe quel objet, pas seulement String .

5. Conclusion

En conséquence, nous nous sommes débarrassés de nombreuses conversions de routine et avons laissé Spring faire la plupart des choses pour nous. À la fin, concluons:

  • Pour une conversion individuelle de type simple en objet, nous devons utiliser l' implémentation de Converter
  • Pour encapsuler la logique de conversion pour une gamme d'objets, nous pouvons essayer l' implémentation ConverterFactory
  • Pour toutes les données proviennent indirectement ou il est nécessaire d'appliquer une logique supplémentaire pour récupérer les données associées, il est préférable d'utiliser HandlerMethodArgumentResolver

Comme d'habitude, tous les exemples peuvent toujours être trouvés dans notre référentiel GitHub.