Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Jackson HttpMessageConverter no longer works in Spring 4.2

I am updating an application from Spring Platform version 1.1.3.RELEASE to 2.0.1.RELEASE, which bumps the Spring Framework version from 4.1.7 to 4.2.4, and Jackson from 2.4.6 to 2.6.4. There does not seem to have been any significant changes in Spring or Jackson's handling of custom HttpMessageConverter implementations, but my custom JSON serialization is failing to occur, and I have not been able to determine why. The following works fine in the previous Spring Platform release:

Model

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

Model wrapper

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

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

Custom 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

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

Now I am getting this exception (which is caught by Spring and logged) and a 500 error when making any sort of request:

[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])

The configureMessageConverters method executes, but it does not look like custom converter is ever utilized during requests. Is it possible that another message converter could be preventing this one from reaching my response? My understanding was that overriding configureMessageConverters would prevent converters other than the manually registered ones from being used.

No changes have been made between the working and non-working versions of this code, besides updating dependency versions via the Spring Platform. Has there been any change in the JSON serialization that I am just missing in the documentation?

Edit

Further testing yields strange results. I wanted to test to check the following things:

  1. Is my custom HttpMessageConverter actually being registered?
  2. Is another converter overriding/superseding it?
  3. Is this a problem with my test setup only?

So, I added an extra test and took a look at the output:

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

...and it works fine. My the envelope object and its contents are serialized and filtered correctly. So either there is an issue with the request handling before it reaches the message converters, or there has been a change in how MockMvc is testing requests.

like image 645
woemler Avatar asked Jan 22 '16 16:01

woemler


1 Answers

Your configuration is ok. The reason why writeInternal() is not called from your custom converter is because you are overriding the wrong method.

Looking at the source code of 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#write

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

The method writeInternal(...) called from within AbstractGenericHttpMessageConverter#write(...) has three arguments - (T t, Type type, HttpOutputMessage outputMessage). You are overriding the overloaded version of writeInternal(...) that has only 2 arguments - (T t, HttpOutputMessage outputMessage).

However, in version 4.1.7.RELEASE, it is not the case, hence the root cause of your problem. The writeInternal(...) used in this version is the other overloaded method (the method with 2 arguments) that you have overriden. This explains why it is working fine in 4.1.7.RELEASE.

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

So, to solve your problem, instead of overriding writeInternal(Object object, HttpOutputMessage outputMessage), override writeInternal(Object object, Type type, HttpOutputMessage outputMessage)

like image 58
Bnrdo Avatar answered Sep 24 '22 11:09

Bnrdo