最近维护公司的产品时,我碰到了两个头痛的Java异常。未免以后忘记了,所以写篇blog记录下这些问题和解决方法。
由于不能展示公司的代码,我就用书店、书、作者这些对象来说明。书店与作者之间是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
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
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
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
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
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
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
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
authors) { - this.authors.clear();
- if (authors != null) {
- this.authors.addAll(authors);
- }
- }
以防万一,按同样的方式修改Author.setBooks():
- public void setBooks(Set
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
authors) { - this.authors =
- Optional.ofNullable(authors)
- .map(s -> s.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
- .orElse(new HashSet<>());
- }
-
- public void setBooks(Set
books) { - this.books =
- Optional.ofNullable(books)
- .map(s -> s.stream().filter(Objects::nonNull).collect(Collectors.toSet()))
- .orElse(new HashSet<>());
- }