Um fichário de dados personalizado no Spring MVC

Um fichário de dados personalizado no Spring MVC

1. Visão geral

Este artigo mostrará como podemos usar o mecanismo Data Binding do Spring para tornar nosso código mais claro e legível aplicando primitivas automáticas para conversões de objetos.

Por padrão, o Spring sabe apenas como converter tipos simples. Em outras palavras, assim que enviarmos os dados para o tipo de dadosInt,String ouBoolean do controlador, eles serão vinculados aos tipos Java apropriados automaticamente.

Mas em projetos do mundo real, isso não será suficiente, poiswe might need to bind more complex types of objects.

2. Vinculando objetos individuais a parâmetros de solicitação

Vamos começar simples e primeiro vincular um tipo simples; teremos que fornecer uma implementação personalizada da interfaceConverter<S, T> em queS é o tipo do qual estamos convertendo eT é o tipo para o qual estamos convertendo:

@Component
public class StringToLocalDateTimeConverter
  implements Converter {

    @Override
    public LocalDateTime convert(String source) {
        return LocalDateTime.parse(
          source, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}

Agora podemos usar a seguinte sintaxe em nosso controlador:

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

2.1. Usando Enums como Parâmetros de Solicitação

A seguir, veremoshow to use enum as a RequestParameter.

Aqui, temos um simplesenumModes:

public enum Modes {
    ALPHA, BETA;
}

Vamos construir umString paraenum Converter da seguinte forma:

public class StringToEnumConverter implements Converter {

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

Então, precisamos registrar nossoConverter:

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Agora podemos usar nossoEnum comoRequestParameter:

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


Ou comoPathVariable:

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

3. Vinculando uma hierarquia de objetos

Às vezes, precisamos converter a árvore inteira da hierarquia de objetos e faz sentido ter uma ligação mais centralizada em vez de um conjunto de conversores individuais.

Neste exemplo, temosAbstractEntity nossa classe base:

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

E as subclassesFoo eBar:

public class Foo extends AbstractEntity {
    private String name;

    // standard constructors, getters, setters
}
public class Bar extends AbstractEntity {
    private int value;

    // standard constructors, getters, setters
}

Neste caso,we can implement ConverterFactory<S, R> where S will be the type we are converting from and R to be the base type definindo o intervalo de classes para o qual podemos converter:

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

Como podemos ver, o único método que deve ser implementado égetConverter(), que retorna o conversor para o tipo necessário. O processo de conversão é delegado a este conversor.

Então, precisamos registrar nossoConverterFactory:

@Configuration
public class WebConfig implements WebMvcConfigurer {

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

Finalmente, podemos usá-lo como quisermos em nosso controlador:

@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 Domain Objects

Há casos em que queremos vincular dados a objetos, mas isso vem de forma não direta (por exemplo, das variáveisSession,Header ouCookie) ou mesmo armazenado em uma fonte de dados. Nesses casos, precisamos usar uma solução diferente.

4.1. Resolvedor de argumento personalizado

Primeiro, definiremos uma anotação para esses parâmetros:

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

Então, vamos implementar umHandlerMethodArgumentResolver: personalizado

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

A última coisa é deixar o Spring saber onde procurá-los:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    //...

    @Override
    public void addArgumentResolvers(
      List argumentResolvers) {
        argumentResolvers.add(new HeaderVersionArgumentResolver());
    }
}

É isso aí. Agora podemos usá-lo em um controlador:

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

Como podemos ver, o métodoHandlerMethodArgumentResolver'sresolveArgument() retorna umObject.. Em outras palavras, poderíamos retornar qualquer objeto, não apenasString.

5. Conclusão

Como resultado, nos livramos de muitas conversões de rotina e deixamos o Spring fazer a maioria das coisas para nós. No final, vamos concluir:

  • Para um tipo simples individual para conversões de objeto, devemos usar a implementaçãoConverter

  • Para encapsular a lógica de conversão para uma variedade de objetos, podemos tentar a implementação deConverterFactory

  • Para todos os dados vindos indiretamente ou é necessário aplicar lógica adicional para recuperar os dados associados, é melhor usarHandlerMethodArgumentResolver

Como de costume, todos os exemplos podem ser sempre encontrados em nossoGitHub repository.