@Data注解导致的StackOverflowError

2024-01-25 10:53:07 浏览数 (1)

场景

Springboot项目中使用Lombok,实体采用@Data注解。运行过程中报Caused by: java.lang.StackOverflowError。

@Data到底做了啥?

1、帮助我们生成Get/Set方法,简化javabean的代码冗余 2、帮助我们重写equals方法, 3、帮助我们重写hashCode 4、大大提高了JavaBean的执行效率(?)

StackOverflowError是哪里抛出的异常?

先来看StackOverflowError和OutOfMemoryError。 在《Java虚拟机规范》中描述了这两种异常: 1)如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError。 2)如果虚拟机的栈内存允许动态扩展,当扩展容量无法申请到足够的内存时,将抛出OutOfMemoryError。 也就是说,由于JVM规定了栈的最大深度,因无法容纳新的栈帧而抛出StackOverflowError异常;这种情况通常预示着代码可能有出现死循环等问题。 通过查看执行log,发现TreeDTO.hashCode()方法循环抛出异常,也即出现了死循环。

代码语言:javascript复制
@Data
public class TreeDTO implements Serializable {

    private Integer id;
    private String name;
    private Integer pid;
    private String checked;

    private List<TreeDTO> children;

}

由于该类使用了@Data注解,所以该hashCode()方法由注解自动生成,所以将范围缩小至@Data上,而且这里出现了集合间包含自身的递归引用。

什么是hashCode?

equals():是用来判断两个对象是否相同,在Object类中是通过判断对象间的内存地址来决定是否相同。 hashCode():是获取哈希码,也称为散列码,返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。如果两个对象equals()方法是相等的,那么调用二者各自的hashCode()方法必须产生同一个int结果。

为什么会出现该异常?

@Data注解编译后的Entity:

代码语言:javascript复制
public class TreeDTO implements Serializable {
    private Integer id;
    private String name;
    private Integer pid;
    private String checked;
    private List<TreeDTO> children;

    public TreeDTO() {
    }

    public Integer getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public Integer getPid() {
        return this.pid;
    }

    public String getChecked() {
        return this.checked;
    }

    public List<TreeDTO> getChildren() {
        return this.children;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setPid(Integer pid) {
        this.pid = pid;
    }

    public void setChecked(String checked) {
        this.checked = checked;
    }

    public void setChildren(List<TreeDTO> children) {
        this.children = children;
    }

    public boolean equals(Object o) {
        if(o == this) {
            return true;
        } else if(!(o instanceof TreeDTO)) {
            return false;
        } else {
            TreeDTO other = (TreeDTO)o;
            if(!other.canEqual(this)) {
                return false;
            } else {
                label71: {
                    Object this$id = this.getId();
                    Object other$id = other.getId();
                    if(this$id == null) {
                        if(other$id == null) {
                            break label71;
                        }
                    } else if(this$id.equals(other$id)) {
                        break label71;
                    }

                    return false;
                }

                Object this$name = this.getName();
                Object other$name = other.getName();
                if(this$name == null) {
                    if(other$name != null) {
                        return false;
                    }
                } else if(!this$name.equals(other$name)) {
                    return false;
                }

                label57: {
                    Object this$pid = this.getPid();
                    Object other$pid = other.getPid();
                    if(this$pid == null) {
                        if(other$pid == null) {
                            break label57;
                        }
                    } else if(this$pid.equals(other$pid)) {
                        break label57;
                    }

                    return false;
                }

                Object this$checked = this.getChecked();
                Object other$checked = other.getChecked();
                if(this$checked == null) {
                    if(other$checked != null) {
                        return false;
                    }
                } else if(!this$checked.equals(other$checked)) {
                    return false;
                }

                Object this$children = this.getChildren();
                Object other$children = other.getChildren();
                if(this$children == null) {
                    if(other$children == null) {
                        return true;
                    }
                } else if(this$children.equals(other$children)) {
                    return true;
                }

                return false;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof TreeDTO;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $id = this.getId();
        int result = result * 59   ($id == null?43:$id.hashCode());
        Object $name = this.getName();
        result = result * 59   ($name == null?43:$name.hashCode());
        Object $pid = this.getPid();
        result = result * 59   ($pid == null?43:$pid.hashCode());
        Object $checked = this.getChecked();
        result = result * 59   ($checked == null?43:$checked.hashCode());
        Object $children = this.getChildren();
        result = result * 59   ($children == null?43:$children.hashCode());
        return result;
    }

    public String toString() {
        return "TreeDTO(id="   this.getId()   ", name="   this.getName()   ", pid="   this.getPid()   ", checked="   this.getChecked()   ", children="   this.getChildren()   ")";
    }
}

由于TreeDTO中包含有自身对象的集合List,对于AbstractList的hashCode其实是把每一个子元素的hashCode经过迭代计算得到的,也就是说,要计算AbstractList的hashCode,就要把每一个子元素的hashCode先计算一遍,如果这些子元素中的某一个或子元素的子元素引用到上级对象,那么hashCode方法就会出现无限递归调用,最终出现StackOverflowError错误。 不仅仅是List集合,Set、Map、Stack也有同样的问题。

如何解决?

1、尽量不要出现集合间的递归引用。 2、使用@Getter、@Setter来替代@Data 3、@Data配合@EqualsAndHashCode(callSuper=true)一起使用,让其生成的方法中调用父类的方法。注:使用EqualsAndHashCode时,实体类必须要有继承父类,因为设置true默认是要调用父类的方法,如果没有继承,则无法使用@EqualsAndHashCode(callSuper=true),默认callSuper=false.

虽然出现这种问题的概率比较小,线上项目也是正常运行一段时间后才出现。这里不知道较高版本的JDK或者较高版本的Lombok会不会修复次问题。这里使用的是JDK-1.8以及Lombok-1.16.10。

0 人点赞