Spring Webflux中的HATEOAS PagedModel反序列化问题

问题描述

我正在尝试在Spring Boot中测试Reactive Controller,但即使接收的字节流正常,也无法反序列化返回的对象,如以下代码所示...

    @Test
    public void testListTemp(@Autowired HypermediaWebTestClientConfigurer configurer) {

        Page<T> page = new PageImpl<T>(Arrays.asList(getEntity()),PageRequest.of(0,7),1);
        when(getRepository().findAll(any(SearchQuery.class),any(Pageable.class))).thenReturn(Mono.just(page));

        //@formatter:off
        WebTestClient
            .bindToController(getController())
            .argumentResolvers((argsConfigurer) -> {
                Integer MAX_PAGE_SIZE = 100;
                ReactivePageableHandlerMethodArgumentResolver pageableResolver = new ReactivePageableHandlerMethodArgumentResolver();
                
                pageableResolver.setMaxPageSize(MAX_PAGE_SIZE);
                pageableResolver.setFallbackPageable(SearchQuery.getDefaultPageable());
                
                argsConfigurer.addCustomresolver(pageableResolver);
            }).build()
            .mutateWith(configurer)
            .get()
            .uri(getBaseUri()+"/")
            .accept(MediaTypes.HAL_JSON)
            .exchange()
            .expectBody(String.class)
            .consumeWith(result -> {
                String model = result.getResponseBody();
                System.err.println("model: " + model);
        });

        //@formatter:on
    }

...其中的型号:

{
  "links":[
    {
      "rel":"self","href":"/test/exampleRoot/example/?includedDeleted=false&langCode=en-US&page=0&size=7","title":"Misma página"
    },{
      "rel":"first","title":"Primera página"
    },{
      "rel":"prev","title":"Página previa"
    },{
      "rel":"next","title":"Siguiente página"
    },{
      "rel":"last","title":"Última página"
    },{
      "rel":"page-1","title":"1"
    },{
      "rel":"create","href":"/test/exampleRoot/example/?langCode=en-US"
    }
  ],"content": [
  {
    "createdBy":null,"createdDate":null,"lastModifiedBy":null,"lastModifiedDate":null,"deletedBy":null,"deletedDate":null,"businessId":2,"field01":"FieldE2_01","field02":2,"links":[
    {
      "rel":"self","href":"/test/exampleRoot/example/2?langCode=en-US&includeDeleted=false"
    },{
      "rel":"all","href":"/test/exampleRoot/example/"
    },{
      "rel":"delete","href":"/test/exampleRoot/example/2"
    },{
      "rel":"update","href":"/test/exampleRoot/example/2?langCode=en-US"
    }]
  }],"page":
  {
    "size":7,"totalElements":1,"totalPages":1,"number":0
  }
}

但是在以下代码中不起作用:

    @Test
    public void testListTemp(@Autowired HypermediaWebTestClientConfigurer configurer) {

        Page<T> page = new PageImpl<T>(Arrays.asList(getEntity()),any(Pageable.class))).thenReturn(Mono.just(page));

        WebTestClient
            .bindToController(getController())
            .argumentResolvers((argsConfigurer) -> {
                Integer MAX_PAGE_SIZE = 100;
                ReactivePageableHandlerMethodArgumentResolver pageableResolver = new ReactivePageableHandlerMethodArgumentResolver();
                
                pageableResolver.setMaxPageSize(MAX_PAGE_SIZE);
                pageableResolver.setFallbackPageable(SearchQuery.getDefaultPageable());
                
                argsConfigurer.addCustomresolver(pageableResolver);
            }).build()
            .mutateWith(configurer)
            .get()
            .uri(getBaseUri()+"/")
            .accept(MediaTypes.HAL_JSON)
            .exchange()
            .expectBody(new TypeReferences.PagedModelType<ExampleDto>() {})
            .consumeWith(result -> {
                PagedModel<ExampleDto> model = result.getResponseBody();
                System.err.println("model: " + model);
        });

        //@formatter:on
    }

返回的对象在哪里:

PagedResource { content: [],Metadata: Metadata { number: 0,total pages: 1,total elements: 1,size: 7 },links:  }

因此没有反序列化内容链接

关于您的信息,这里有ExampleDto类和层次结构的根

package net.crezco.api.example;

import javax.validation.constraints.Positive;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonTypeInfo;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.crezco.api.CzcDto;

@Getter
@Setter
@NoArgsConstructor
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public class ExampleDto extends CzcDto<ExampleDto> {

    @Size(max = 20)
    private String field01;

    @Positive
    private Integer field02;

}

以及层次结构的根:

package net.crezco.api;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

import javax.persistence.Transient;
import javax.validation.constraints.Null;
import javax.validation.constraints.Positive;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.util.ReflectionUtils;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import net.crezco.validation.onCreate;
import net.crezco.validation.onUpdate;

@JsonIgnoreProperties(ignoreUnkNown = true)
public abstract class CzcDto<T extends RepresentationModel<? extends T>> extends RepresentationModel<T> {
    
    @Transient
    @JsonIgnore
    private final List<String> reserved = Arrays.asList("reserved","id","version","createdBy","createdDate","lastModifiedBy","lastModifiedDate","deletedBy","deletedDate","_log");

    /**
     * Created by
     */
    @JsonProperty
    private String createdBy;

    /**
     * Created date
     */
    @JsonProperty
    private Date createdDate;

    /**
     * Last modified by
     */
    @JsonProperty
    private String lastModifiedBy;

    /**
     * Last modified date
     */
    @JsonProperty
    private Date lastModifiedDate;

    /**
     * Deleted by
     */
    @JsonProperty
    private String deletedBy;

    /**
     * Deleted date
     */
    @JsonProperty
    private Date deletedDate;

    /**
     * ID of the entity
     */
    @Null(groups = onCreate.class)
    @Positive(groups = onUpdate.class)
    @JsonProperty
    private Long businessId;

    public CzcDto() {
        super();
    }

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public Date getCreatedDate() {
        return createdDate;
    }

    public void setCreatedDate(Date createdDate) {
        this.createdDate = createdDate;
    }

    public String getLastModifiedBy() {
        return lastModifiedBy;
    }

    public void setLastModifiedBy(String lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public Date getLastModifiedDate() {
        return lastModifiedDate;
    }

    public void setLastModifiedDate(Date lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }

    public String getDeletedBy() {
        return deletedBy;
    }

    public void setDeletedBy(String deletedBy) {
        this.deletedBy = deletedBy;
    }

    public Date getDeletedDate() {
        return deletedDate;
    }

    public void setDeletedDate(Date deletedDate) {
        this.deletedDate = deletedDate;
    }

    public Long getBusinessId() {
        return businessId;
    }

    public void setBusinessId(Long businessId) {
        this.businessId = businessId;
    }

    /************************************
     * 
     * UTILITY NETHODS
     * 
     ************************************/

    @Override
    public String toString() {
        return ReflectionToStringBuilder.toStringExclude(this,"reserved","_log");
    }

    @Override
    public int hashCode() {
        HashCodeBuilder hcb = new HashCodeBuilder(17,37);
        //@formatter:off
        FieldUtils.getAllFieldsList(getClass()).stream()
            .forEach(field -> {
                if (!reserved.contains(field.getName())) 
                    hcb.append(field);
                });
        //@formatter:on
        return hcb.build();
    }

    @Override
    public boolean equals(Object that) {
        if (that == this)
            return true;
        if (!that.getClass().isinstance(this))
            return false;

        EqualsBuilder eb = new EqualsBuilder();
        //@formatter:off
        FieldUtils.getAllFieldsList(getClass()).stream()
            .forEach(field -> {
                field.setAccessible(true);
                if (!reserved.contains(field.getName()))  {
                    eb.append(ReflectionUtils.getField(field,this),ReflectionUtils.getField(field,that));
                }
            });
        //@formatter:on
        return eb.isEquals();
    }
}

解决方法

有一个类似的问题,这是为我解决的问题:

spring.hateoas:
    use-hal-as-default-json-media-type: false