最近维护公司的产品时,我碰到了两个头痛的Java异常。未免以后忘记了,所以写篇blog记录下这些问题和解决方法。
Entity定义
由于不能展示公司的代码,我就用书店、书、作者这些对象来说明。书店与作者之间是m:n的关系,作者与书之间是1:n的关系。各个对象定义如下:
import jakarta.persistence.*;
import java.io.Serializable;
import java.util.*;
import org.hibernate.annotations.*;
import org.hibernate.envers.Audited;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
@Audited
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Entity
@EntityListeners({AuditingEntityListener.class, AuditingEntityListener.class})
@MappedSuperclass
@Table(name = "book_store")
public class BookStore implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@org.springframework.data.annotation.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name") private String name;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@JoinTable(name = "book_store_author",
joinColumns =
@JoinColumn(name = "book_stores_id", referencedColumnName = "id"),
inverseJoinColumns =
@JoinColumn(name = "authors_id", referencedColumnName = "id"))
private Set<Author> authors = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Author> getAuthors() {
return authors;
}
public Author addAuthor(Author author) {
this.authors.add(author);
return this;
}
public Author removeAuthor(Author author) {
this.authors.remove(author);
return this;
}
public void setAuthors(Set<Author> authors) {
this.authors = authors;
}
}
@Audited
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Entity
@EntityListeners({AuditingEntityListener.class, AuditingEntityListener.class})
@MappedSuperclass
@Table(name = "author")
public class Author implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@org.springframework.data.annotation.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name") private String name;
@OneToMany(
cascade = {CascadeType.ALL, CascadeType.PERSIST, CascadeType.MERGE},
fetch = FetchType.LAZY, orphanRemoval = true)
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@JoinColumn(name = "author_id")
private Set<Book> books = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Book> getBooks() {
return books;
}
public Book addBook(Book book) {
this.books.add(book);
book.setAuthor(this);
return this;
}
public Book removeBook(Book book) {
this.books.remove(book);
book.setAuthor(null);
return this;
}
public void setBooks(Set<Book> books) {
this.books = books
}
}
@Audited
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Entity
@EntityListeners({AuditingEntityListener.class, AuditingEntityListener.class})
@MappedSuperclass
@Table(name = "book")
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@org.springframework.data.annotation.Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name") private String name;
@ManyToOne(optional = false)
@JoinColumn(nullable = false)
private Author author;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Author getAuthor() {
return author;
}
public void setAuthor(Author author) {
this.author = author;
}
}
错误及其修正
当尝试创建一个书店,内含两个作者,每个作者三本书的时候,程序抛出了第一个错误:
A collection with cascade=”all-delete-orphan” was no longer referenced by the owning entity instance ...
Google了半天,不少人碰到了同样的问题,但没有人讲清楚根本原因是什么,只是建议将方法setAuthors()做如下修改:
public void setAuthors(Set<Author> authors) {
this.authors.clear();
this.authors.addAll(authors);
}
我照着修改了下,果然有效。但为啥直接给成员authors赋一个新的实例要出错,而修改其内容却不会?完全搞不清楚Spring Boot在背后检查了什么,以后有空还是要深挖一下Spring Boot的代码。
然而,在后续测试时,又碰到了第二个错误:
JSON parse error: Cannot invoke "java.util.Collection.iterator()" because "c" is null
又Google了一天半,终于发现了原因:setAuthors方法里,没有检查参数authors。若它为null,就会引发这个错误。遂继续修改setAuthors():
public void setAuthors(Set<Author> authors) {
this.authors.clear();
if (authors != null) {
this.authors.addAll(authors);
}
}
以防万一,按同样的方式修改Author.setBooks():
public void setBooks(Set<Book> books) {
this.books.clear();
if (books != null) {
this.books.addAll(books);
}
}
至此,终于解决了上述两个错误。
解决这两个错误的难点在于,哪怕我把log leve调到最低,程序都只是输出了一行错误信息,没有任何相关的stacktrace,导致我无法迅速定位相关代码。而且这两个错误都是在执行RESTful API callback函数之前就触发了,只能在Spring Boot的代码里打断点,调试起来很不方便。
后续
在后续调试代码的时候,我终于发现并不是不能给成员authors赋一个新的实例,问题还是在于传入的参数,参数本身可能为null,或其内部含有null元素。所以最终,setAuthors()和setBooks()应该修改成:
public void setAuthors(Set<Author> authors) {
this.authors =
Optional.ofNullable(authors)
.map(s -> s.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
.orElse(new HashSet<>());
}
public void setBooks(Set<Book> books) {
this.books =
Optional.ofNullable(books)
.map(s -> s.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
.orElse(new HashSet<>());
}