Подтвердить что ты не робот

Пользовательский Jackson HttpMessageConverter больше не работает в Spring 4.2

Я обновляю приложение из Spring Платформа версии 1.1.3.RELEASE до версии 2.0.1.RELEASE, которая сталкивается с версией Spring Framework с 4.1.7 до 4.2.4, а Jackson - с 2.4.6 до 2.6.4. По-видимому, не было каких-либо существенных изменений в Spring или обработке Джексона пользовательских реализаций HttpMessageConverter, но моя пользовательская сериализация JSON не выполняется, и я не смог определить, почему. В предыдущей версии платформы Spring работает отлично:

Model

@JsonFilter("fieldFilter")
public class MyModel { 
    /*model fields and methods*/ 
}

Оболочка модели

public class ResponseEnvelope {

    private Set<String> fieldSet;
    private Set<String> exclude;
    private Object entity;

    public ResponseEnvelope(Object entity) {
        this.entity = entity;
    }

    public ResponseEnvelope(Object entity, Set<String> fieldSet, Set<String> exclude) {
        this.fieldSet = fieldSet;
        this.exclude = exclude;
        this.entity = entity;
    }

    public Object getEntity() {
        return entity;
    }

    @JsonIgnore
    public Set<String> getFieldSet() {
        return fieldSet;
    }

    @JsonIgnore
    public Set<String> getExclude() {
        return exclude;
    }

    public void setExclude(Set<String> exclude) {
        this.exclude = exclude;
    }

    public void setFieldSet(Set<String> fieldSet) {
        this.fieldSet = fieldSet;
    }

    public void setFields(String fields) {
        Set<String> fieldSet = new HashSet<String>();
        if (fields != null) {
            for (String field : fields.split(",")) {
                fieldSet.add(field);
            }
        }
        this.fieldSet = fieldSet;
    }
}

контроллер

@Controller
public class MyModelController {

    @Autowired MyModelRepository myModelRepository;

    @RequestMapping(value = "/model", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
    public HttpEntity find(@RequestParam(required=false) Set<String> fields, @RequestParam(required=false) Set<String> exclude){
        List<MyModel> objects = myModelRepository.findAll();
        ResponseEnvelope envelope = new ResponseEnvelope(objects, fields, exclude);
        return new ResponseEntity<>(envelope, HttpStatus.OK);
    }
}

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

public class FilteringJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {

    private boolean prefixJson = false;

    @Override
    public void setPrefixJson(boolean prefixJson) {
        this.prefixJson = prefixJson;
        super.setPrefixJson(prefixJson);
    }

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {

        ObjectMapper objectMapper = getObjectMapper();
        JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody());

        try {

            if (this.prefixJson) {
                jsonGenerator.writeRaw(")]}', ");
            }

            if (object instanceof ResponseEnvelope) {

                ResponseEnvelope envelope = (ResponseEnvelope) object;
                Object entity = envelope.getEntity();
                Set<String> fieldSet = envelope.getFieldSet();
                Set<String> exclude = envelope.getExclude();
                FilterProvider filters = null;

                if (fieldSet != null && !fieldSet.isEmpty()) {
                    filters = new SimpleFilterProvider()
                            .addFilter("fieldFilter", SimpleBeanPropertyFilter.filterOutAllExcept(fieldSet))
                                .setFailOnUnknownId(false);
                } else if (exclude != null && !exclude.isEmpty()) {
                    filters = new SimpleFilterProvider()
                            .addFilter("fieldFilter", SimpleBeanPropertyFilter.serializeAllExcept(exclude))
                                .setFailOnUnknownId(false);
                } else {
                    filters = new SimpleFilterProvider()
                            .addFilter("fieldFilter", SimpleBeanPropertyFilter.serializeAllExcept())
                                .setFailOnUnknownId(false);
                }

                objectMapper.setFilterProvider(filters);
                objectMapper.writeValue(jsonGenerator, entity);

            } else if (object == null){
                jsonGenerator.writeNull();
            } else {
                FilterProvider filters = new SimpleFilterProvider().setFailOnUnknownId(false);
                objectMapper.setFilterProvider(filters);
                objectMapper.writeValue(jsonGenerator, object);
            }

        } catch (JsonProcessingException e){
            e.printStackTrace();
            throw new HttpMessageNotWritableException("Could not write JSON: " + e.getMessage());
        }

    }
}

Конфигурация

@Configuration
@EnableWebMvc
public class WebServicesConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FilteringJackson2HttpMessageConverter jsonConverter = new FilteringJackson2HttpMessageConverter();
        jsonConverter.setSupportedMediaTypes(MediaTypes.APPLICATION_JSON);
        converters.add(jsonConverter);
    }

    // Other configurations
}

Теперь я получаю это исключение (которое поймано Spring и зарегистрировано) и 500 ошибок при выполнении любого запроса:

[main] WARN  o.s.w.s.m.s.DefaultHandlerExceptionResolver - Failed to write HTTP message: 
  org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: 
  Can not resolve PropertyFilter with id 'fieldFilter'; 
  no FilterProvider configured (through reference chain:
  org.oncoblocks.centromere.web.controller.ResponseEnvelope["entity"]->java.util.ArrayList[0]); 
  nested exception is com.fasterxml.jackson.databind.JsonMappingException: 
  Can not resolve PropertyFilter with id 'fieldFilter'; 
  no FilterProvider configured (through reference chain: 
  org.oncoblocks.centromere.web.controller.ResponseEnvelope["entity"]->java.util.ArrayList[0])

Выполняется метод configureMessageConverters, но он не похож на то, что пользовательский конвертер когда-либо используется во время запросов. Возможно ли, что другой конвертер сообщений может помешать этому человеку достичь моего ответа? Я понимаю, что переопределение configureMessageConverters предотвратит использование преобразователей, отличных от зарегистрированных вручную.

Никакие изменения не были сделаны между рабочими и нерабочими версиями этого кода, помимо обновления версий зависимостей через платформу Spring. Были ли какие-либо изменения в сериализации JSON, которые я просто отсутствую в документации?

Edit

Дальнейшее тестирование дает странные результаты. Я хотел протестировать, чтобы проверить следующие вещи:

  • Является ли мой пользовательский HttpMessageConverter фактически зарегистрированным?
  • Другой преобразователь, переопределяющий/заменяющий его?
  • Это проблема только с моей тестовой установкой?

Итак, я добавил дополнительный тест и посмотрел на результат:

@Autowired WebApplicationContext webApplicationContext;

@Before
public void setup(){
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

@Test
public void test() throws Exception {
    RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) webApplicationContext.getBean("requestMappingHandlerAdapter");
    List<EntrezGene> genes = EntrezGene.createDummyData();
    Set<String> exclude = new HashSet<>();
    exclude.add("entrezGeneId");
    ResponseEnvelope envelope = new ResponseEnvelope(genes, new HashSet<String>(), exclude);
    for (HttpMessageConverter converter: adapter.getMessageConverters()){
        System.out.println(converter.getClass().getName());
        if (converter.canWrite(ResponseEnvelope.class, MediaType.APPLICATION_JSON)){
            MockHttpOutputMessage message =  new MockHttpOutputMessage();
            converter.write((Object) envelope, MediaType.APPLICATION_JSON, message);    
            System.out.println(message.getBodyAsString());
        }
    }
}

... и он отлично работает. Мой объект envelope и его содержимое сериализуются и фильтруются правильно. Поэтому либо возникает проблема с обработкой запроса до того, как он достигнет конвертеров сообщений, либо произошел сбой в том, как MockMvc тестирует запросы.

4b9b3361

Ответ 1

Ваша конфигурация в порядке. Причина, по которой writeInternal() не вызывается из вашего настраиваемого конвертера, заключается в том, что вы переопределяете неправильный метод.

Глядя на исходный код 4.2.4.RELEASE

AbstractMessageConverterMethodProcessor # writeWithMessageConverters

protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
            ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
            throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    ...
    ((GenericHttpMessageConverter<T>) messageConverter).write(returnValue, returnValueType, selectedMediaType, outputMessage);
    ...
}

AbstractGenericHttpMessageConverter # написать

public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
    ...
    writeInternal(t, type, outputMessage);
    ...
}

Метод writeInternal(...), вызванный из AbstractGenericHttpMessageConverter#write(...), имеет три аргумента - (T t, Type type, HttpOutputMessage outputMessage). Вы переопределяете перегруженную версию writeInternal(...), которая имеет только 2 аргумента - (T t, HttpOutputMessage outputMessage).

Однако в версии 4.1.7.RELEASE это не так, поэтому основная причина вашей проблемы. writeInternal(...), используемый в этой версии, - это другой перегруженный метод (метод с двумя аргументами), который вы переопределили. Это объясняет, почему он отлично работает в 4.1.7.RELEASE.

@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
    ...
    writeInternal(t, outputMessage);
    ...
}

Итак, чтобы решить вашу проблему, вместо переопределения writeInternal(Object object, HttpOutputMessage outputMessage), переопределить writeInternal(Object object, Type type, HttpOutputMessage outputMessage)