Spring Data Pagination returns no results with JSO

2020-06-03 07:11发布

问题:

I am using Spring data pagination in my REST Controller and returning Paged entity. I would like to control the data returned as JSON with the help of JSONViews.

I am able to achieve the result when I return a single object. But when I return Page, I am receiving blank JSON as response.

Following is my method signature.

@JsonView(TravelRequestView.MyRequests.class)
@RequestMapping("/travel/requests")
public Page<TravelRequest> getUserTravelRequests(
            @RequestParam("ps") int pageSize, @RequestParam("p") int page,
            @RequestParam(defaultValue = "", value = "q") String searchString)

I am able to receive response when I remove @JsonView annotation.

回答1:

Try with below piece of code,

@Configuration
public class MyInterceptorConfig extends WebMvcConfigurerAdapter{

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
      MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
      ObjectMapper mapper = new ObjectMapper() {
        private static final long serialVersionUID = 1L;
        @Override
        protected DefaultSerializerProvider _serializerProvider(SerializationConfig config) {
          // replace the configuration with my modified configuration.
          // calling "withView" should keep previous config and just add my changes.
          return super._serializerProvider(config.withView(TravelRequestView.MyRequests.class));
        }        
      };
      mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
      converter.setObjectMapper(mapper);
      converters.add(converter);
    }

Although I don't want to take credit for this, It was a reference from

Jackson JsonView not being applied

It would retrieve all the variables of an entity which are annotated with jsonview (TravelRequestView.MyRequests.class) along with all the variables which are not annotated with jsonview. If you don't want certain properties of an object, annotated with different view.



回答2:

If you are using spring-boot, then another simpler solution would be add the following to application.yml

spring:
  jackson:
    mapper:
      DEFAULT_VIEW_INCLUSION: true

or application.properties

spring.jackson.mapper.DEFAULT_VIEW_INCLUSION=true

With this approach, we have the advantage of retaining the ObjectMapper managed by Spring Container and not creating a new ObjectMapper. So once we use the spring managed ObjectMapper, then any Custom Serialzers we define will still continue to work e.g CustomDateSerialzer

Reference: http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html



回答3:

Setting DEFAULT_VIEW_INCLUSION has a global effect, while all we need is to be able to serialize a Page object. The following code will register a serializer for Page, and is a simple change to your code:

@Bean
public Module springDataPageModule() {
    return new SimpleModule().addSerializer(Page.class, new JsonSerializer<Page>() {
        @Override
        public void serialize(Page value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            gen.writeStartObject();
            gen.writeNumberField("totalElements",value.getTotalElements());
            gen.writeNumberField("totalPages", value.getTotalPages());
            gen.writeNumberField("number", value.getNumber());
            gen.writeNumberField("size", value.getSize());
            gen.writeBooleanField("first", value.isFirst());
            gen.writeBooleanField("last", value.isLast());
            gen.writeFieldName("content");
            serializers.defaultSerializeValue(value.getContent(),gen);
            gen.writeEndObject();
        }
    });
}

Another (arguably more elegant) solution is to register the following ResponseBodyAdvice. It will make sure your REST endpoint will still return a JSON array, and set a HTTP header 'X-Has-Next-Page' to indicate whether there is more data. The advantages are: 1) No extra count(*) query to your DB (single query) 2) Response is more elegant, since it returns a JSON array

/**
 * ResponseBodyAdvice to support Spring data Slice object in JSON responses.
 * If the value is a slice, we'll write the List as an array, and add a header to the HTTP response
 *
 * @author blagerweij
 */
@ControllerAdvice
public class SliceResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof Slice) {
            Slice slice = (Slice) body;
            response.getHeaders().add("X-Has-Next-Page", String.valueOf(slice.hasNext()));
            return slice.getContent();
        }
        return body;
    }
}


回答4:

I actually found a simpler and better way of doing this. The problem I had was with the fact that I cannot set @JsonView annotations on the Page object I was receiving. So I created an implementation of the Page interface and added my JsonViews to it. And instead of returning Page, I am now returning MyPage

public class MyPage<T> implements Page<T> {

    private Page<T> pageObj;

    public MyPage(Page<T> pageObj) {
        this.pageObj = pageObj;
    }

    @JsonView(PaginatedResult.class)
    @Override
    public int getNumber() {
        return pageObj.getNumber();
    }

    @Override
    public int getSize() {
        return pageObj.getSize();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public int getNumberOfElements() {
        return pageObj.getNumberOfElements();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public List<T> getContent() {
        return pageObj.getContent();
    }

    @Override
    public boolean hasContent() {
        return pageObj.hasContent();
    }

    @Override
    public Sort getSort() {
        return pageObj.getSort();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public boolean isFirst() {
        return pageObj.isFirst();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public boolean isLast() {
        return pageObj.isLast();
    }

    @Override
    public boolean hasNext() {
        return pageObj.hasNext();
    }

    @Override
    public boolean hasPrevious() {
        return pageObj.hasPrevious();
    }

    @Override
    public Pageable nextPageable() {
        return pageObj.nextPageable();
    }

    @Override
    public Pageable previousPageable() {
        return pageObj.previousPageable();
    }

    @Override
    public Iterator<T> iterator() {
        return pageObj.iterator();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public int getTotalPages() {
        return pageObj.getTotalPages();
    }

    @JsonView(PaginatedResult.class)
    @Override
    public long getTotalElements() {
        return pageObj.getTotalElements();
    }

}


回答5:

you need to add annotation @JsonView(TravelRequestView.MyRequests.class) recursively. Add it to the field you want to see in Page class.

public class Page<T> {
    @JsonView(TravelRequestView.MyRequests.class)
    private T view;
 ...
}

or enable DEFAULT_VIEW_INCLUSION for ObjectMapper:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="org.springframework.http.converter.StringHttpMessageConverter" />
        <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
            <property name="objectMapper">
                <bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean">
                    <property name="defaultViewInclusion" value="true"/>
                </bean>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

or use dto objects for your responses where you can control all your views



回答6:

Still cleaner is to tell Jackson to Serialize all props for a bean of type Page. To do that, you just have to declare to adapt slighly the Jackson BeanSerializer : com.fasterxml.jackson.databind.ser.BeanSerializer Create a class that extends that BeanSerializer called PageSerialier and if the bean is of type Page<> DO NOT apply the property filtering.

As show in the code below, i just removed the filtering for Page instances :

public class MyPageSerializer extends BeanSerializer {

/**
 * MODIFIED By Gauthier PEEL
 */
@Override
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider)
        throws IOException, JsonGenerationException {
    final BeanPropertyWriter[] props;
    // ADDED
    // ADDED
    // ADDED
    if (bean instanceof Page) {
        // for Page DO NOT filter anything so that @JsonView is passthrough at this level
        props = _props;
    } else {
        // ADDED
        // ADDED
        if (_filteredProps != null && provider.getActiveView() != null) {
            props = _filteredProps;
        } else {
            props = _props;
        }
    }

        // rest of the method unchanged
    }

    // inherited constructor removed for concision
}

Then you need to declare it to Jackson with a Module :

public class MyPageModule extends SimpleModule {

    @Override
    public void setupModule(SetupContext context) {

        context.addBeanSerializerModifier(new BeanSerializerModifier() {

            @Override
            public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc,
                    JsonSerializer<?> serializer) {
                if (serializer instanceof BeanSerializerBase) {
                    return new MyPageSerializer ((BeanSerializerBase) serializer);
                }
                return serializer;

            }
        });
    }
}

Spring conf now : in your @Configuration create a new @Bean of the MyPageModule

@Configuration
public class WebConfigPage extends WebMvcConfigurerAdapter {

    /**
     * To enable Jackson @JsonView to work with Page<T>
     */
    @Bean
    public MyPageModule myPageModule() {
        return new MyPageModule();
    }

}

And you are done.