Можете ли вы сконфигурировать дескрипцию Jackson для дескрипции дескриптора контроллера Spring?

Мне нужно добавить пользовательский десериализатор Jackson для java.lang.String в мое приложение Spring 4.1.x MVC. Однако все ответы (например, this) относятся к настройке ObjectMapper для полного веб-приложения, и изменения будут применяться ко всем строкам во всех @RequestBody во всех контроллерах.

Я только хочу применить настраиваемую десериализацию к аргументам @RequestBody, используемым в определенных контроллерах. Обратите внимание, что у меня нет возможности использовать аннотации @JsonDeserialize для конкретных полей String.

Можно ли настроить индивидуальную десериализацию только для определенных контроллеров?

Ответ 1

Чтобы иметь разные конфигурации десериализации, у вас должны быть разные экземпляры ObjectMapper, но из окна Spring используется MappingJackson2HttpMessageConverter, который предназначен для использования только одного экземпляра.

Я вижу здесь как минимум два варианта:

Отведите MessageConverter от ArgumentResolver

Создайте аннотацию @CustomRequestBody и преобразователь аргументов:

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 - это интерфейс, который мы будем использовать для разрешения фактического экземпляра ObjectMapper для использования, я расскажу об этом ниже. Конечно, если у вас есть только один вариант использования, где вам нужно настраивать сопоставление, вы можете просто инициализировать свой картограф здесь.

Вы можете добавить настраиваемый аргумент аргумента с этой конфигурацией:

@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()));
  }
}

Примечание. Не комбинируйте @CustomRequestBody с @RequestBody, он будет проигнорирован.

Оберните ObjectMapper в прокси-сервер, который скрывает несколько экземпляров

MappingJackson2HttpMessageConverter предназначен для работы только с одним экземпляром ObjectMapper. Мы можем сделать этот экземпляр доверенным делегатом. Это упростит работу с несколькими картографами.

Прежде всего нам нужен перехватчик, который будет переводить все вызовы метода в базовый объект.

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();

}

Теперь наш ObjectMapper прокси bean будет выглядеть следующим образом:

@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();
}

Примечание. У меня были проблемы с загрузкой классов с помощью этого прокси на Wildfly из-за его модульной загрузки классов, поэтому мне пришлось расширять ObjectMapper (без изменения чего-либо), чтобы я мог использовать класс из моего модуля.

Все это связано с использованием этой конфигурации:

@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 реализация

Заключительная часть - это логика, определяющая, какой из карт следует использовать, она будет содержаться в интерфейсе ObjectMapperResolver. Он содержит только один метод поиска:

public interface ObjectMapperResolver {

  ObjectMapper getObjectMapper();

}

Если у вас нет много вариантов использования с настраиваемыми карточками, вы можете просто сделать карту предварительно сконфигурированных экземпляров с помощью ReqeustMatcher в качестве ключей. Что-то вроде этого:

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

}

Вы также можете использовать область запроса ObjectMapper, а затем настроить ее для каждого запроса. Используйте эту конфигурацию:

@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();
}

Это лучше всего подходит для пользовательской сериализации ответа, так как вы можете настроить его прямо в методе контроллера. Для пользовательской десериализации вы также должны использовать Filter/HandlerInterceptor/ControllerAdvice для настройки активного Mapper для текущего запроса до запуска метода контроллера.

Вы можете создать интерфейс, похожий на ObjectMapperResolver:

public interface ObjectMapperConfigurer {

  void configureObjectMapper(ObjectMapper objectMapper);

}

Затем создайте карту этих экземпляров с помощью RequstMatcher в качестве ключей и поместите ее в Filter/HandlerInterceptor/ControllerAdvice, аналогичный RequestMatcherObjectMapperResolver.

P.S. Если вы хотите более подробно изучить динамическую конфигурацию ObjectMapper, я могу предложить свой старый ответ здесь. В нем описывается, как вы можете сделать динамический @JsonFilter во время выполнения. Он также содержит мой более старый подход с расширенным MappingJackson2HttpMessageConverter, который я предложил в комментариях.

Ответ 2

Возможно, это поможет, но это некрасиво. Это потребует АОП. Также я не подтвердил это. Создайте @CustomAnnotation.

Обновите свой контроллер:

void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity);

Затем выполните часть 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});
}

Ответ 3

Вы можете попробовать Message Converters. У них есть контекст о запросе на ввод http (например, docs см. здесь, JSON). Как настроить, вы можете увидеть здесь. Идея, что вы можете проверить HttpInputMessage со специальными URI, которые используются в ваших контроллерах и конвертировать строку по вашему желанию. Вы можете создать специальную аннотацию для этого, сканировать пакеты и делать это автоматически.

Примечание

Вероятно, вам не нужна реализация ObjectMappers. Вы можете использовать простой ObjectMapper по умолчанию для синтаксического анализа String и затем преобразовать строку по своему усмотрению. В этом случае вы однажды создадите RequestBody.

Ответ 4

Вы можете создать собственный десериализатор для ваших данных String.

Пользовательский десериализатор

public class CustomStringDeserializer extends JsonDeserializer<String> {

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

    String str = p.getText();

    //return processed String
  }

}

Теперь предположим, что String присутствует внутри POJO, используя @JsonDeserialize аннотацию над переменной:

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

Теперь, когда вы возвращаете его в качестве ответа, он будет десериализован так, как вы это сделали в CustomDeserializer.

Надеюсь, что это поможет.

Ответ 5

Вы можете определить POJO для каждого параметра типа запроса, который вы хотите десериализовать. Затем следующий код вытащит значения из JSON в объект, который вы определяете, считая, что имена полей в вашем POJO совпадают с именами поля в запросе JSON.

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

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

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