【发布时间】:2018-06-07 23:04:39
【问题描述】:
我们有一个后端组件,通过 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<Child> children = new ArrayList<>();
@Transactional
public void addChild(Child child) {
child.setParent(this);
children.add(child);
}
@Transactional
public void removeChild(Child child) {
child.setParent(null);
children.remove(child);
}
public Long getId() {
return id;
}
@Transactional
public List<Child> 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;
@Entity
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Child implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "id")
private Parent parent;
public Long getId() {
return id;
}
public Parent getParent() {
return parent;
}
/**
* Only for usage in {@link Parent}
*/
void setParent(final Parent parent) {
this.parent = parent;
}
}
import org.springframework.data.repository.CrudRepository;
public interface ParentRepository extends CrudRepository<Parent, Long> {}
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;
@RestController
@RequestMapping("/api/v1.0/parents")
public class ParentController {
private final String hostPath;
private final ParentRepository parentRepository;
public ParentController(@Value("${app.hostPath}") final String hostPath,
final ParentRepository parentRepository) {
// in application.properties: app.hostPath=/api/v1.0/
this.hostPath = hostPath;
this.parentRepository = parentRepository;
}
@CrossOrigin(origins = "*")
@GetMapping("/{id}")
public ResponseEntity<?> getParent(@PathVariable(value = "id") long id) {
final Parent parent = parentRepository.findOne(id);
if (parent == null) {
return new ResponseEntity<>(new HttpHeaders(), HttpStatus.NOT_FOUND);
}
Link selfLink = linkTo(Parent.class)
.slash(hostPath + "parents")
.slash(parent.getId()).withRel("self");
Link updateLink = linkTo(Parent.class)
.slash(hostPath + "parents")
.slash(parent.getId()).withRel("update");
Link deleteLink = linkTo(Parent.class)
.slash(hostPath + "parents")
.slash(parent.getId()).withRel("delete");
Link syncLink = linkTo(Parent.class)
.slash(hostPath + "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,响应如下:
{
"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"
}
]
}
但我希望它不包含 children 或将其包含为空数组或 null -- 不从数据库中获取实际值。
该组件具有以下值得注意的 maven 依赖项:
- Spring Boot Starter 1.5.7.RELEASE - Spring Boot Starter Web 1.5.7.RELEASE(来自父版本) - 春季 HATEOAS 0.23.0.RELEASE - Jackson Databind 2.8.8(它是 web starter 中的 2.8.1,我不知道我们为什么要覆盖它) - Spring Boot Started Data JPA 1.5.7.RELEASE(来自父版本)--hibernate-core 5.0.12.Final目前尝试过
调试显示在序列化过程中有一个select 在Parent parentRepository.findOne(id) 和另一个Parent.children。
起初,我尝试将@JsonIgnore 应用于惰性集合,但即使集合实际上包含某些内容(已被获取),也会忽略该集合。
我发现了jackson-datatype-hibernate 声称的项目
构建 Jackson 模块 (jar) 以支持 JSON 序列化和 Hibernate (http://hibernate.org) 特定数据类型的反序列化 和属性; 尤其是延迟加载方面。
这样做的想法是将Hibernate5Module(如果使用第5版的hibernate)注册到ObjectMapper,并且应该这样做,因为模块的设置FORCE_LAZY_LOADING默认设置为false。
所以,我包含了这个依赖项jackson-datatype-hibernate5,版本 2.8.10(来自父级)。并用谷歌搜索了enable it in Spring Boot 的方法(我还找到了其他来源,但他们大多指的是这个)。
1.直接添加模块(特定于 Spring Boot):
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;
@Configuration
public class HibernateConfiguration {
@Bean
public Module disableForceLazyFetching() {
return new Hibernate5Module();
}
}
调试显示,Spring 在返回 Parent 时调用的 ObjectMapper 包含此模块,并且按预期将强制延迟设置设置为 false。但它仍然会获取children。
进一步调试显示:com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields 遍历属性 (com.fasterxml.jackson.databind.ser.BeanPropertyWriter) 并调用其方法 serializeAsField,其中第一行是:final Object value = (_accessorMethod == null) ? _field.get(bean) : _accessorMethod.invoke(bean); 触发延迟加载。我找不到代码真正关心该休眠模块的任何地方。
upd 还尝试启用 SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS,它应该包含惰性属性的实际 id,而不是 null(这是默认值)。
@Bean
public Module disableForceLazyFetching() {
Hibernate5Module module = new Hibernate5Module();
module.enable(Hibernate5Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS);
return module;
}
调试显示该选项已启用,但仍然无效。
2。指示 Spring MVC 添加模块:
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;
@Configuration
@EnableWebMvc
public class HibernateConfiguration extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.modulesToInstall(new Hibernate5Module());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}
这也成功地将模块添加到正在调用的ObjectMapper,但在我的情况下仍然没有效果。
3.将ObjectMapper 完全替换为新的:
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;
@Configuration
public class HibernateConfiguration {
@Primary
@Bean(name = "objectMapper")
public ObjectMapper hibernateAwareObjectMapper(){
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Hibernate5Module());
return mapper;
}
}
再次,我可以看到模块已添加,但这对我没有任何影响。
还有其他方法可以添加此模块,但我没有失败,因为添加了模块。
【问题讨论】:
-
你在休眠模块上启用
SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS功能了吗? -
@RobbyCornelissen 不,这有关系吗?我已经阅读了这方面的 javadoc,虽然
null没问题。我现在就试试这个! -
是的,就是这样。我已经成功使用这个模块有一段时间了,但不得不承认我自己扩展了它,因为我也无法让它按照我想要的方式运行。
-
@RobbyCornelissen 我已启用该选项,但没有效果。也许
@JsonIdentityInfo与它有关?我将更新答案以显示我是如何做到的 -
发现Spring Boot uses transactions in view by default。所以,如果我禁用那些我在序列化过程中得到
LazyInitializationException。
标签: java json spring hibernate jpa