Пользовательский 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
тестирует запросы.
Ответы
Ответ 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)