我们有一个后端组件,它通过JPA将数据库(PostgreSQL)数据暴露给RESTful API.
问题是,当发送JPA实体作为REST响应时,我可以看到Jackson触发所有Lazy JPA关系.
代码示例(简化):
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")//for resolving this bidirectional relationship, otherwise StackOverFlow due to infinite recursion
public class Parent extends ResourceSupport implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
//we actually use Set and override hashcode&equals
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List children &#61; new ArrayList<>();
&#64;Transactional
public void addChild(Child child) {
child.setParent(this);
children.add(child);
}
&#64;Transactional
public void removeChild(Child child) {
child.setParent(null);
children.remove(child);
}
public Long getId() {
return id;
}
&#64;Transactional
public List getReadOnlyChildren() {
return Collections.unmodifiableList(children);
}
}
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import java.io.Serializable;
&#64;Entity
&#64;JsonIdentityInfo(generator &#61; ObjectIdGenerators.PropertyGenerator.class, property &#61; "id")
public class Child implements Serializable {
private static final long serialVersionUID &#61; 1L;
&#64;Id
&#64;GeneratedValue
private Long id;
&#64;ManyToOne
&#64;JoinColumn(name &#61; "id")
private Parent parent;
public Long getId() {
return id;
}
public Parent getParent() {
return parent;
}
/**
* Only for usage in {&#64;link Parent}
*/
void setParent(final Parent parent) {
this.parent &#61; parent;
}
}
import org.springframework.data.repository.CrudRepository;
public interface ParentRepository extends CrudRepository {}
import com.avaya.adw.db.repo.ParentRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
&#64;RestController
&#64;RequestMapping("/api/v1.0/parents")
public class ParentController {
private final String hostPath;
private final ParentRepository parentRepository;
public ParentController(&#64;Value("${app.hostPath}") final String hostPath,
final ParentRepository parentRepository) {
// in application.properties: app.hostPath&#61;/api/v1.0/
this.hostPath &#61; hostPath;
this.parentRepository &#61; parentRepository;
}
&#64;CrossOrigin(origins &#61; "*")
&#64;GetMapping("/{id}")
public ResponseEntity> getParent(&#64;PathVariable(value &#61; "id") long id) {
final Parent parent &#61; parentRepository.findOne(id);
if (parent &#61;&#61; null) {
return new ResponseEntity<>(new HttpHeaders(), HttpStatus.NOT_FOUND);
}
Link selfLink &#61; linkTo(Parent.class)
.slash(hostPath &#43; "parents")
.slash(parent.getId()).withRel("self");
Link updateLink &#61; linkTo(Parent.class)
.slash(hostPath &#43; "parents")
.slash(parent.getId()).withRel("update");
Link deleteLink &#61; linkTo(Parent.class)
.slash(hostPath &#43; "parents")
.slash(parent.getId()).withRel("delete");
Link syncLink &#61; linkTo(Parent.class)
.slash(hostPath &#43; "parents")
.slash(parent.getId())
.slash("sync").withRel("sync");
parent.add(selfLink);
parent.add(updateLink);
parent.add(deleteLink);
parent.add(syncLink);
return new ResponseEntity<>(adDataSource, new HttpHeaders(), HttpStatus.OK);
}
}
所以,如果我发送GET … / api / v1.0 / parents / 1,响应如下&#xff1a;
{
"id": 1,
"children": [
{
"id": 1,
"parent": 1
},
{
"id": 2,
"parent": 1
},
{
"id": 3,
"parent": 1
}
],
"links": [
{
"rel": "self",
"href": "http://.../api/v1.0/parents/1"
},
{
"rel": "update",
"href": "http://.../api/v1.0/parents/1"
},
{
"rel": "delete",
"href": "http://.../api/v1.0/parents/1"
},
{
"rel": "sync",
"href": "http://.../api/v1.0/parents/1/sync"
}
]
}
但我希望它不包含子节点或包含它作为空数组或null – 不从数据库中获取实际值.
该组件具有以下值得注意的maven依赖项&#xff1a;
- Spring Boot Starter 1.5.7.RELEASE
- Spring Boot Starter Web 1.5.7.RELEASE (version from parent)
- Spring HATEOAS 0.23.0.RELEASE
- Jackson Databind 2.8.8 (it&#39;s 2.8.1 in web starter, I don&#39;t know why we overrode that)
- Spring Boot Started Data JPA 1.5.7.RELEASE (version from parent) -- hibernate-core 5.0.12.Final
到目前为止尝试过
调试显示在parentRepository.findOne(id)的Parent上有一个select,在序列化期间在Parent.children上有另一个select.
起初,我尝试将&#64;JsonIgnore应用于延迟集合,但即使它实际上包含某些内容(已经被提取),它也会忽略该集合.
build Jackson module (jar) to support JSON serialization and
deserialization of Hibernate (07001) specific datatypes
and properties; especially lazy-loading aspects.
这个想法是将Hibernate5Module(如果使用了hibernate的第5版)注册到ObjectMapper,并且应该这样做,因为默认情况下模块的设置FORCE_LAZY_LOADING设置为false.
所以,我包含了这个依赖项jackson-datatype-hibernate5,版本2.8.10(来自父级).用谷歌搜索到enable it in Spring Boot(我也找到了其他来源,但他们大多指的是这个).
1.直接添加模块(特定于Spring Boot)&#xff1a;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
&#64;Configuration
public class HibernateConfiguration {
&#64;Bean
public Module disableForceLazyFetching() {
return new Hibernate5Module();
}
}
调试显示,Spring返回Parent时调用的ObjectMapper包含此模块,并且强制延迟设置设置为false,正如预期的那样.但它仍然带走了孩子.
进一步调试显示&#xff1a;com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields遍历属性(com.fasterxml.jackson.databind.ser.BeanPropertyWriter)并调用其方法serializeAsField,其中第一行是&#xff1a;final Object value &#61;(_ accesscessMethod &#61;&#61; null)&#xff1f; _field.get(bean)&#xff1a;_ accessorMethod.invoke(bean);这是触发延迟加载.我无法找到代码实际上关心hibernate模块的任何地方.
upd还尝试启用SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS,它应该包含惰性属性的实际id,而不是null(这是默认值).
&#64;Bean
public Module disableForceLazyFetching() {
Hibernate5Module module &#61; new Hibernate5Module();
module.enable(Hibernate5Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);
return module;
}
调试显示该选项已启用但仍然没有效果.
2.指示Spring MVC添加模块&#xff1a;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
&#64;Configuration
&#64;EnableWebMvc
public class HibernateConfiguration extends WebMvcConfigurerAdapter {
&#64;Override
public void configureMessageConverters(List> converters) {
Jackson2ObjectMapperBuilder builder &#61; new Jackson2ObjectMapperBuilder()
.modulesToInstall(new Hibernate5Module());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
这也成功地将模块添加到正在调用的ObjectMapper中,但在我的情况下它仍然没有效果.
3.用一个新的完全替换ObjectMapper&#xff1a;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
&#64;Configuration
public class HibernateConfiguration {
&#64;Primary
&#64;Bean(name &#61; "objectMapper")
public ObjectMapper hibernateAwareObjectMapper(){
ObjectMapper mapper &#61; new ObjectMapper();
mapper.registerModule(new Hibernate5Module());
return mapper;
}
}
再次,我可以看到添加了模块,但这对我没有任何影响.
还有其他方法可以添加此模块,但我没有失败,因为添加了模块.