Un classeur de données personnalisé dans Spring MVC

Un classeur de données personnalisé dans Spring MVC

1. Vue d'ensemble

Cet article montre comment utiliser le mécanisme de liaison de données de Spring pour 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 type de données du contrôleurInt,String ouBoolean, elles seront automatiquement liées aux types Java appropriés.

Mais dans les projets du monde réel, cela ne suffira pas, commewe might need to bind more complex types of objects.

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

Commençons simplement et lions d'abord un type simple; nous devrons fournir une implémentation personnalisée de l'interfaceConverter<S, T>S est le type à partir duquel nous effectuons la conversion etT 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 verronshow to use enum as a RequestParameter.

Ici, nous avons un simpleenumModes:

public enum Modes {
    ALPHA, BETA;
}

Nous allons construire unString enenum Converter comme suit:

public class StringToEnumConverter implements Converter {

    @Override
    public Modes convert(String from) {
        return Modes.valueOf(from);
    }
}

Ensuite, nous devons enregistrer nosConverter:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToEnumConverter());
    }
}

Nous pouvons maintenant utiliser nosEnum comme desRequestParameter:

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


Ou enPathVariable:

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

3. Lier une hiérarchie d'objets

Parfois, nous avons besoin de convertir l’arbre complet de la hiérarchie des objets et il est logique d’avoir une liaison plus centralisée plutôt qu’un ensemble de convertisseurs individuels.

Dans cet exemple, nous avonsAbstractEntity notre classe de base:

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

Et les sous-classesFoo etBar:

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,we can implement ConverterFactory<S, R> where S will be the type we are converting from and R to be the base type définit la plage de classes que nous pouvons convertir en:

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 qui doit implémenter estgetConverter() qui renvoie le convertisseur pour le type nécessaire. Le processus de conversion est ensuite délégué à ce convertisseur.

Ensuite, nous devons enregistrer nosConverterFactory:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverterFactory(new StringToAbstractEntityConverterFactory());
    }
}

Enfin, nous pouvons l’utiliser comme bon nous semble 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ù l'on souhaite lier des données à des objets, mais cela vient soit de manière non directe (par exemple, à partir des variablesSession,Header ouCookie) ou même stockée 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 allons définir une annotation pour de tels paramètres:

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

Ensuite, nous implémenterons unHandlerMethodArgumentResolver: 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 est de faire savoir au printemps 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 nous pouvons le voir, la méthoderesolveArgument() deHandlerMethodArgumentResolver renvoie unObject. En d'autres termes, nous pourrions renvoyer n'importe quel objet, pas seulementString.

5. Conclusion

En conséquence, nous nous sommes débarrassés de nombreuses conversions de routine et avons laissé Spring s'occuper de la plupart des tâches pour nous. À la fin, concluons:

  • Pour une conversion individuelle de type simple en objet, nous devrions utiliser l'implémentationConverter

  • Pour encapsuler la logique de conversion pour une plage d'objets, nous pouvons essayer l'implémentation deConverterFactory

  • 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'utiliserHandlerMethodArgumentResolver

Comme d'habitude, tous les exemples se trouvent toujours dans nosGitHub repository.