Question Pouvez-vous configurer la désérialisation de Jackson spécifique au contrôleur Spring?


Je dois ajouter un désérialiseur Jackson personnalisé pour java.lang.String à mon application Spring 4.1.x MVC. Cependant toutes les réponses (telles que ce) reportez-vous à la configuration d'ObjectMapper pour l'application Web complète et les modifications s'appliqueront à toutes les chaînes de tous les @RequestBody de tous les contrôleurs.

Je souhaite uniquement appliquer la désérialisation personnalisée aux arguments @RequestBody utilisés dans des contrôleurs particuliers. Notez que je n'ai pas la possibilité d'utiliser les annotations @JsonDeserialize pour les champs String spécifiques.

Pouvez-vous configurer la désérialisation personnalisée pour des contrôleurs spécifiques uniquement?


12
2018-06-15 19:24


origine


Réponses:


Pour avoir différentes configurations de désérialisation, vous devez avoir différentes ObjectMapper instances mais hors de la boîte utilise le printemps MappingJackson2HttpMessageConverter qui est conçu pour utiliser une seule instance.

Je vois au moins deux options ici:

S'éloigner de MessageConverter pour un ArgumentResolver

Créer un @CustomRequestBody une annotation et un résolveur d'argument:

public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {

  private final ObjectMapperResolver objectMapperResolver;

  public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
    this.objectMapperResolver = objectMapperResolver;
  }

  @Override
  public boolean supportsParameter(MethodParameter methodParameter) {
    return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null;
  }

  @Override
  public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    if (this.supportsParameter(methodParameter)) {
      ObjectMapper objectMapper = objectMapperResolver.getObjectMapper();
      HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
      return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType());
    } else {
      return WebArgumentResolver.UNRESOLVED;
    }
  }
}

ObjectMapperResolver est une interface que nous utiliserons pour résoudre les problèmes ObjectMapper exemple à utiliser, je vais en discuter ci-dessous. Bien sûr, si vous n’avez qu’un seul cas d’utilisation pour lequel vous avez besoin d’un mappage personnalisé, vous pouvez simplement initialiser votre mappeur ici.

Vous pouvez ajouter un résolveur d’argument personnalisé avec cette configuration:

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

  @Bean
  public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
    return new CustomRequestBodyArgumentResolver(objectMapperResolver)
  } 

  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {       
    argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver()));
  }
}

Remarque: Ne pas combiner @CustomRequestBody avec @RequestBody, il sera ignoré.

Emballage ObjectMapper dans un proxy qui cache plusieurs instances

MappingJackson2HttpMessageConverter est conçu pour fonctionner avec une seule instance de ObjectMapper. Nous pouvons faire de cette instance un délégué proxy. Cela rendra le travail avec plusieurs mappeurs transparents.

Tout d'abord, nous avons besoin d'un intercepteur qui traduira toutes les invocations de méthodes en un objet sous-jacent.

public abstract class ObjectMapperInterceptor implements MethodInterceptor {

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments());
  } 

  protected abstract ObjectMapper getObject();

}

Maintenant notre ObjectMapper Bean proxy ressemblera à ceci:

@Bean
public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) {
  ProxyFactory factory = new ProxyFactory();
  factory.setTargetClass(ObjectMapper.class);
  factory.addAdvice(new ObjectMapperInterceptor() {

      @Override
      protected ObjectMapper getObject() {
        return objectMapperResolver.getObjectMapper();
      }

  });

  return (ObjectMapper) factory.getProxy();
}

Remarque: J'ai eu des problèmes de chargement de classe avec ce proxy sur Wildfly, en raison de son chargement de classe modulaire, donc j'ai dû étendre ObjectMapper (sans rien changer) juste pour que je puisse utiliser la classe de mon module.

Tout est attaché ensemble en utilisant cette configuration:

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

  @Bean
  public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
    return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver()));
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(jackson2HttpMessageConverter());
  }
}

ObjectMapperResolver implémentations

La pièce finale est la logique qui détermine quel mappeur doit être utilisé, il sera contenu dans ObjectMapperResolver interface. Il ne contient qu'une seule méthode de recherche:

public interface ObjectMapperResolver {

  ObjectMapper getObjectMapper();

}

Si vous n'avez pas beaucoup de cas d'utilisation avec les mappeurs personnalisés, vous pouvez simplement créer une carte des instances préconfigurées avec ReqeustMatchers comme clés. Quelque chose comme ça:

public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver {

  private final ObjectMapper defaultMapper;
  private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>();

  public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) {
    this.defaultMapper = defaultMapper;
    this.mapping.putAll(mapping);
  }

  public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) {
    this.defaultMapper = defaultMapper;
  }

  @Override
  public ObjectMapper getObjectMapper() {
    ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = sra.getRequest();
    for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) {
      if (entry.getKey().matches(request)) {
        return entry.getValue();
      }
    }
    return defaultMapper;
  }

}

Vous pouvez également utiliser une requête ciblée ObjectMapper puis configurez-le sur demande. Utilisez cette configuration:

@Bean
public ObjectMapperResolver objectMapperResolver() {
  return new ObjectMapperResolver() {
    @Override
    public ObjectMapper getObjectMapper() {
      return requestScopedObjectMapper();
    }
  };
}


@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public ObjectMapper requestScopedObjectMapper() {
  return new ObjectMapper();
}

Ceci est le mieux adapté à la sérialisation des réponses personnalisées, car vous pouvez le configurer directement dans la méthode du contrôleur. Pour la désérialisation personnalisée, vous devez également utiliser Filter/HandlerInterceptor/ControllerAdvice configurer le mappeur actif pour la requête en cours avant le déclenchement de la méthode du contrôleur.

Vous pouvez créer une interface, similaire à ObjectMapperResolver:

public interface ObjectMapperConfigurer {

  void configureObjectMapper(ObjectMapper objectMapper);

}

Ensuite, faites une carte de ces instances avec RequstMatchers comme des clés et le mettre dans un Filter/HandlerInterceptor/ControllerAdvice semblable à RequestMatcherObjectMapperResolver.

P.S. Si vous voulez explorer la dynamique ObjectMapper configuration un peu plus loin, je peux suggérer ma vieille réponse ici. Il décrit comment rendre dynamique @JsonFilters au moment de l'exécution. Il contient également mon ancienne approche avec extension MappingJackson2HttpMessageConverter que j'ai suggéré dans les commentaires.


4
2017-09-14 07:30



Cela aiderait probablement, mais ce n'est pas joli. Il faudrait AOP. Aussi je ne l'ai pas validé. Créer un @CustomAnnotation.

Mettez à jour votre contrôleur:

void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity);

Alors implicite la partie AOP:

@Around("execution(* *(@CustomAnnotation (*)))")
public void advice(ProceedingJoinPoint proceedingJoinPoint) {
  // Here you would add custom ObjectMapper, I don't know another way around it
  HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
  String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator()));

  SomeEntity someEntity = /* deserialize */;
  // This could be cleaner, cause the method can accept multiple parameters
  proceedingJoinPoint.proceed(new Object[] {someEntity});
}

0
2017-09-08 20:15



Vous pouvez créer un désérialiseur personnalisé pour vos données String.

Désérialiseur personnalisé

public class CustomStringDeserializer extends JsonDeserializer<String> {

  @Override
  public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {

    String str = p.getText();

    //return processed String
  }

}

Supposons maintenant que String est présent dans un POJO, en utilisant l’annotation @JsonDeserialize au-dessus de la variable:

public class SamplePOJO{
  @JsonDeserialize(using=CustomStringDeserializer.class)
  private String str;
  //getter and setter
}

Maintenant, lorsque vous le renvoyez en tant que réponse, il sera désérialisé comme vous l’avez fait dans CustomDeserializer.

J'espère que cela aide.


0
2017-09-14 07:41



Tu pourrais essayer Convertisseurs de messages. Ils ont un contexte sur la requête d’entrée http (par exemple, docs see ici, JSON). Comment personnaliser vous pourriez voir ici. Idée que vous pouvez vérifier HttpInputMessage avec des URI spéciaux, utilisés dans vos contrôleurs et convertissant la chaîne comme vous le souhaitez. Vous pouvez créer une annotation spéciale pour cela, analyser les packages et le faire automatiquement.

Remarque

Vous n'avez probablement pas besoin d'implémenter ObjectMappers. Vous pouvez utiliser ObjectMapper par défaut simple pour analyser String puis convertir la chaîne comme vous le souhaitez. Dans ce cas, vous créez une fois RequestBody.


0
2017-09-14 06:11



Vous pouvez définir un POJO pour chaque type de paramètre de requête que vous souhaitez désérialiser. Ensuite, le code suivant extraira les valeurs du JSON dans l'objet que vous définissez, en supposant que les noms des champs de votre objet POJO correspondent aux noms du champ dans la requête JSON.

ObjectMapper mapper = new ObjectMapper(); 
YourPojo requestParams = null;

try {
    requestParams = mapper.readValue(JsonBody, YourPOJO.class);

} catch (IOException e) {
    throw new IOException(e);
}

-2
2018-06-15 19:51