具有与Spring Data REST服务器

时间:2015-09-15 15:28:56

标签: json rest jackson resttemplate spring-data-rest

问题

我有三个实体(取自Spring Data REST Exporter Example):人物,地址和个人资料。一个人可以拥有地址和个人资料。

@Entity
public class Person {

    @Id
    @GeneratedValue
    private Long id;
    private String name;
    @Version
    private Long version;
    @OneToMany
    private List<Address> addresses;
    @OneToMany
    private Map<String, Profile> profiles;

    // getters and setters
}

在客户端,我使用Spring的RestTemplate。我将Jackson2HalModule添加到我的RestTemplate使用的MappingJackson2HttpMessageConverter使用的ObjectMapper中。

由于Address和Profile没有对其他实体的引用,我可以将它们发布到我的Spring Data REST服务器,并且它们已成功保存:

final ResponseEntity<Resource<Address>> response = restTemplate.postForEntity("http://localhost:8080/addresses",
                addressInstance, AddressResource.class);

其中AddressResource extends org.springframework.hateoas.Resource<Address>

但是当我尝试POST一个Person实例时

final ResponseEntity<Resource<Person>> response = restTemplate.postForEntity("http://localhost:8080/people",
                personInstance, PersonResource.class);

我收到了org.springframework.web.client.HttpClientErrorException: 400 Bad Request,我认为原因是关联的Address es和Profile s  被序列化为普通的POJO而不是它们的资源URI。

以下是POST请求的实际正文:

{
   "id":null,
   "name":"Jongjin Han",
   "version":null,
   "addresses":[
      {
         "id":1,
         "lines":[
            "1111",
            "coder's street"
         ],
         "city":"San Diego",
         "province":"California",
         "postalCode":"60707"
      },
      {
         "id":2,
         "lines":[
            "1111",
            "coder's street"
         ],
         "city":"San Diego",
         "province":"California",
         "postalCode":"60707"
      }
   ],
   "profiles":{
      "key1":{
         "type":"a type of profile",
         "url":"http://www.profileurl.com"
      },
      "key2":{
         "type":"a type of profile",
         "url":"http://www.profileurl.com"
      }
   }
}

我认为应该是 - &gt; 编辑:应该是

{
   "id":null,
   "name":"Jongjin Han",
   "version":null,
   "addresses":[
      "http://localhost:8080/addresses/1",
      "http://localhost:8080/addresses/2"
   ],
   "profiles":{
      "key1":"http://localhost:8080/profiles/1",
      "key2":"http://localhost:8080/profiles/2"
   }
}

实际上服务器的响应主体是

{
  "cause" : {
    "cause" : {
      "cause" : {
        "cause" : null,
        "message" : "Cannot resolve URI id. Is it local or remote? Only local URIs are resolvable."
      },
      "message" : "Failed to convert from type java.net.URI to type org.springframework.data.rest.example.model.Address for value 'id'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI id. Is it local or remote? Only local URIs are resolvable."
    },
    "message" : "Failed to convert from type java.net.URI to type org.springframework.data.rest.example.model.Address for value 'id'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI id. Is it local or remote? Only local URIs are resolvable. (through reference chain: org.springframework.data.rest.example.model.Person[\"addresses\"]->java.util.ArrayList[1])"
  },
  "message" : "Could not read document: Failed to convert from type java.net.URI to type org.springframework.data.rest.example.model.Address for value 'id'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI id. Is it local or remote? Only local URIs are resolvable. (through reference chain: org.springframework.data.rest.example.model.Person[\"addresses\"]->java.util.ArrayList[1]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to convert from type java.net.URI to type org.springframework.data.rest.example.model.Address for value 'id'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI id. Is it local or remote? Only local URIs are resolvable. (through reference chain: org.springframework.data.rest.example.model.Person[\"addresses\"]->java.util.ArrayList[1])"
}

我想实施的可能解决方案

由于我可以从客户端访问REST存储库,我正在寻找一种方法来自定义Jackson Json Serializer 以便:

  • 检查我序列化的对象是否是REST导出的实体(带反射的简单,如果我只知道将代码放在哪里)和
  • 如果我正在序列化一个实体,像往常一样序列化非关联字段(例如人名)和关联字段作为其资源URI(例如人的地址)(带反射它应该很容易从一个转换实体到它的资源URI,但我不知道在哪里再次放置代码

我尝试使用Jackson的JsonSerializer和PropertyFilters进行地址和配置文件,但我想要一个序列化程序,只有当它们处于关联时才将它们序列化为资源URI

任何提示或替代解决方案都会有所帮助。

2 个答案:

答案 0 :(得分:0)

没有正确配置。

您不应该使用POST HAL格式化数据来使其工作,序列化为JSON的普通旧POJO应该可以正常使用默认配置。

我建议使用代理拦截请求并确认结构。

答案 1 :(得分:0)

我遇到了同样的问题,并尝试用几种技术解决它。 实施工作解决方案 - 这是一个肮脏的解决方法,所以不要责怪我的代码质量,可能我会在以后清理它:) 我想测试Spring Data REST API,并意识到MappingJackson2HttpMessageConverter忽略了@Entity关系。 设置序列化修饰符没有正常工作:空值序列化器没有工作,关系序列化与深度属性序列化。

解决方法的想法是提供CustomSerializerModifier,它返回项目@Entities的CustomSerializer(在本例中继承自BaseEntity)。 CustomSerializer执行以下操作:

  1. 写入空值(因为省略它们)
  2. 在Spring Data REST样式(//)
  3. 中提供相关的@Entities数组作为List
  4. 执行默认MappingJackson2HttpMessageConverter的序列化(...),但提供NameTransformer,它重命名关系键(添加&#39; _ @&#39;)然后应用过滤器,该过滤器排除以&#39;开头的所有字段。 _ @&#39;
  5. 我不喜欢这个怪物,但它有效,遗憾的是我没有找到任何解决方案:/

    工作解决方案:

    <强> BasicRestTest

    import com.fasterxml.jackson.databind.BeanDescription;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializationConfig;
    import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
    import com.meddis.model.BaseEntity;
    
    public class CustomSerializerModifier extends BeanSerializerModifier {
    
        private final String springDataRestBasePath;
    
        public CustomSerializerModifier(final String springDataRestBasePath) {
            this.springDataRestBasePath = springDataRestBasePath;
        }
    
        @Override
        public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
            if (BaseEntity.class.isAssignableFrom(beanDesc.getBeanClass())) {
                return new CustomSerializer((JsonSerializer<Object>) serializer, springDataRestBasePath);
            }
            return serializer;
        }
    }
    

    <强> CustomSerializerModifier

    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.util.NameTransformer;
    import com.google.common.base.Preconditions;
    import com.meddis.model.BaseEntity;
    
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.util.*;
    
    public class CustomSerializer extends JsonSerializer<Object> {
    
        private final JsonSerializer<Object> defaultSerializer;
    
        private final String springDataRestBasePath;
    
        public CustomSerializer(JsonSerializer<Object> defaultSerializer, final String springDataRestBasePath) {
            this.defaultSerializer = Preconditions.checkNotNull(defaultSerializer);
            this.springDataRestBasePath = springDataRestBasePath;
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public void serialize(Object baseEntity, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
                throws IOException, JsonProcessingException {
    
            jsonGenerator.writeStartObject();
    
            Set<String> nestedEntityKeys = new HashSet<>();
    
            Arrays.asList(baseEntity.getClass().getMethods()).stream()
                    .filter(field -> field.getName().startsWith("get"))
                    .filter(field -> !Arrays.asList("getClass", "getVersion").contains(field.getName()))
                    .forEach(field -> {
                        try {
                            Object value = field.invoke(baseEntity, new Object[]{});
                            String fieldName = field.getName().replaceAll("^get", "");
                            fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
                            if (value == null) {
                                jsonGenerator.writeObjectField(fieldName, null);
                            } else if (Iterable.class.isAssignableFrom(value.getClass())) {
                                Iterator it = ((Iterable) value).iterator();
                                // System.out.println(field.getName() + field.invoke(baseEntity, new Object[]{}));
                                List<String> nestedUris = new ArrayList<>();
                                it.forEachRemaining(nestedValue -> {
                                    if (BaseEntity.class.isAssignableFrom(nestedValue.getClass())) {
                                        try {
                                            String nestedEntityStringDataName = nestedValue.getClass().getSimpleName() + "s";
                                            nestedEntityStringDataName = nestedEntityStringDataName.substring(0, 1).toLowerCase() + nestedEntityStringDataName.substring(1);
                                            Long nestedId = (long) nestedValue.getClass().getMethod("getId").invoke(nestedValue, new Object[]{});
                                            String nestedEntitySpringDataPath = springDataRestBasePath + "/" + nestedEntityStringDataName + "/" + Long.toString(nestedId);
                                            nestedUris.add(nestedEntitySpringDataPath);
                                        } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ignored) {
                                        }
                                    }
                                });
                                nestedEntityKeys.add(fieldName);
                                jsonGenerator.writeObjectField(fieldName, nestedUris);
                            }
                        } catch (Throwable ignored) {
                        }
                    });
    
            // Apply default serializer
            ((JsonSerializer<Object>) defaultSerializer.unwrappingSerializer(new NameTransformer() {
                @Override
                public String transform(String s) {
                    if (nestedEntityKeys.contains(s)) {
                        return "_@" + s;
                    }
                    return s;
                }
    
                @Override
                public String reverse(String s) {
                    if (nestedEntityKeys.contains(s.substring(2))) {
                        return s.substring(2);
                    }
                    return s;
                }
            }).withFilterId("CUSTOM")).serialize(baseEntity, jsonGenerator, serializerProvider);
    
            jsonGenerator.writeEndObject();
        }
    }
    

    <强> CustomSerializer

    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.JsonMappingException;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import com.fasterxml.jackson.databind.ser.PropertyWriter;
    import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
    
    public class CustomIgnorePropertyFilter extends SimpleBeanPropertyFilter {
    
        @Override
        public void serializeAsField(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider, PropertyWriter propertyWriter) throws Exception {
            if (propertyWriter.getName().startsWith("_@")) {
                return;
            }
            super.serializeAsField(o, jsonGenerator, serializerProvider, propertyWriter);
        }
    
        @Override
        public void serializeAsElement(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider, PropertyWriter propertyWriter) throws Exception {
            if (propertyWriter.getName().startsWith("_@")) {
                return;
            }
            super.serializeAsElement(o, jsonGenerator, serializerProvider, propertyWriter);
        }
    
        @Override
        public void depositSchemaProperty(PropertyWriter propertyWriter, ObjectNode objectNode, SerializerProvider serializerProvider) throws JsonMappingException {
            if (propertyWriter.getName().startsWith("_@")) {
                return;
            }
            super.depositSchemaProperty(propertyWriter, objectNode, serializerProvider);
        }
    
        @Override
        public void depositSchemaProperty(PropertyWriter propertyWriter, JsonObjectFormatVisitor jsonObjectFormatVisitor, SerializerProvider serializerProvider) throws JsonMappingException {
            if (propertyWriter.getName().startsWith("_@")) {
                return;
            }
            super.depositSchemaProperty(propertyWriter, jsonObjectFormatVisitor, serializerProvider);
        }
    }
    

    <强> CustomIgnorePropertyFilter

    import com.meddis.AdminApiTest;
    import com.meddis.model.VideoStream;
    import com.meddis.repository.SpecialistRepository;
    import com.meddis.repository.VideoStreamTagRepository;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.web.servlet.MvcResult;
    
    import java.util.stream.Collectors;
    import java.util.stream.StreamSupport;
    
    import static org.hamcrest.Matchers.*;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
    /**
     * <a href="https://spring.io/guides/tutorials/bookmarks/">example</a>
     */
    public class VideoStreamRestTest extends AdminApiTest {
    
        @Autowired
        private SpecialistRepository specialistRepository;
    
        @Autowired
        private VideoStreamTagRepository videoStreamTagRepository;
    
        @Test
        public void springDataRestVideoStreams() throws Exception {
            String requestBody;
            String newEntityTitle = md5("VIDEO_STREAM_");
            MvcResult create = mockMvc.perform(post(springDataRestBasePath + "/videoStreams").headers(authenticationHeader)
                    .content(requestBody = json(new VideoStream()
                            .setTitle(newEntityTitle)
                            .setType(VideoStream.Type.BROADCAST)
                            .setPrice(10.0)
                            .setDurationInMinutes(70)
                            .setDescription("broadcast description")
                            .setPreviewUrl("http://example.com")
                            .setSpecialists(StreamSupport.stream(specialistRepository.findAll().spliterator(), false).collect(Collectors.toList()))
                            .setTags(StreamSupport.stream(videoStreamTagRepository.findAll().spliterator(), false).collect(Collectors.toList())))))
                    .andExpect(status().isCreated())
                    .andReturn();
            String createdLocation = create.getResponse().getHeader("Location");
            logger.info("Created new entity: {}", createdLocation);
            logger.info("Sent: {}", requestBody);
    
            MvcResult list = mockMvc.perform(get(springDataRestBasePath + "/videoStreams").headers(authenticationHeader))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType(contentType))
                    .andExpect(jsonPath("$._embedded.videoStreams", hasSize(greaterThanOrEqualTo(1))))
                    .andExpect(jsonPath("$._embedded.videoStreams[*].title", hasItem(newEntityTitle)))
                    .andExpect(jsonPath("$._embedded.videoStreams[*]._links.self.href", hasItem(createdLocation)))
                    .andReturn();
            logger.info("Got list containing new entity:\n{}", list.getResponse().getContentAsString());
    
            MvcResult createdEntity = mockMvc.perform(get(createdLocation).headers(authenticationHeader))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$._links.self.href", equalTo(createdLocation)))
                    .andExpect(jsonPath("$.title", equalTo(newEntityTitle)))
                    .andReturn();
            logger.info("Got new entity:\n{}", createdEntity.getResponse().getContentAsString());
        }
    
    }
    

    <强> VideoStreamRestTest

    import com.fasterxml.jackson.databind.JsonNode;
    import org.junit.Before;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.test.web.client.TestRestTemplate;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.test.web.servlet.MvcResult;
    
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;
    
    import static org.junit.Assert.assertEquals;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    public abstract class AdminApiTest extends BasicRestTest {
    
        protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    
        protected HttpHeaders authenticationHeader;
    
        @Before
        @Override
        public void setup() throws Exception {
            super.setup();
            this.authenticationHeader = createHeaderWithAuthentication();
        }
    
        protected HttpHeaders createHeaderWithAuthentication() throws IOException {
            String user = "pasha@pasha.ru";
            String password = "pasha";
            ResponseEntity<String> response = new TestRestTemplate()
                    .postForEntity(
                            "http://" + host + ":" + port
                                    + "login?"
                                    + "&username=" + user
                                    + "&password=" + password,
                            null,
                            String.class
                    );
            assertEquals(HttpStatus.FOUND, response.getStatusCode());
            List<String> authenticationCookie = response.getHeaders().get("Set-Cookie");
            assertEquals(1, authenticationCookie.size());
            HttpHeaders headers = new HttpHeaders();
            headers.set("Cookie", authenticationCookie.get(0));
            return headers;
        }
    
    }
    

    <强> AdminApiTest

    ng cli