我有三个实体(取自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 以便:
我尝试使用Jackson的JsonSerializer和PropertyFilters进行地址和配置文件,但我想要一个序列化程序,只有当它们处于关联时才将它们序列化为资源URI 。
任何提示或替代解决方案都会有所帮助。
答案 0 :(得分:0)
没有正确配置。
您不应该使用POST HAL格式化数据来使其工作,序列化为JSON的普通旧POJO应该可以正常使用默认配置。
我建议使用代理拦截请求并确认结构。
答案 1 :(得分:0)
我遇到了同样的问题,并尝试用几种技术解决它。 实施工作解决方案 - 这是一个肮脏的解决方法,所以不要责怪我的代码质量,可能我会在以后清理它:) 我想测试Spring Data REST API,并意识到MappingJackson2HttpMessageConverter忽略了@Entity关系。 设置序列化修饰符没有正常工作:空值序列化器没有工作,关系序列化与深度属性序列化。
解决方法的想法是提供CustomSerializerModifier,它返回项目@Entities的CustomSerializer(在本例中继承自BaseEntity)。 CustomSerializer执行以下操作:
我不喜欢这个怪物,但它有效,遗憾的是我没有找到任何解决方案:/
工作解决方案:
<强> 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