您现在的位置是:首页 >学无止境 >从零开始 Spring Boot 39:循环依赖网站首页学无止境

从零开始 Spring Boot 39:循环依赖

魔芋红茶 2024-10-20 00:01:03
简介从零开始 Spring Boot 39:循环依赖

从零开始 Spring Boot 39:循环依赖

spring boot

图源:简书 (jianshu.com)

什么是循环依赖

我们看一个例子:

@Component
public class Person {
    private Dog pet;

    public Person(Dog pet) {
        this.pet = pet;
    }
}

@Component
public class Dog {
    private Person owner;

    public Dog(Person owner) {
        this.owner = owner;
    }
}

这里定义了两个 Spring Bean:persondog。这两个 bean 都包含对另一个 bean 的依赖,并且这种依赖是通过构造器来完成注入的。

如果实际运行这样的示例,就会报错:

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  dog defined in file [D:workspacelearn_spring_bootch39cycle-dep	argetclassescomexamplecycledepDog.class]
↑     ↓
|  person defined in file [D:workspacelearn_spring_bootch39cycle-dep	argetclassescomexamplecycledepPerson.class]
└─────┘

错误提示告诉我们这两个 bean 之间出现了循环依赖的问题,因此程序无法正常启动。

这是因为 Spring 要创建Person对象,就必须先创建Dog对象以调用Person的构造器,但要创建Dog对象,同样需要先创建一个Person对象来调用Dog的构造器,这样就陷入了“先有鸡还是先有蛋”的问题,无法进行下去。

需要说明的是,这种循环依赖仅会在构造器注入的情况下出现,属性注入或者 Setter 注入都不会导致,因为后两者并不会影响到构造器的调用和对应 bean 实例的创建,它们都是在 bean 实例创建后的合适时间被初始化/调用的。

解决循环依赖

拆分代码

通常出现这种循环依赖说明代码结构有问题,我们可能需要重新设计代码。拆分其中相互依赖的部分,自然就可以解决循环依赖的问题。

延迟初始化

可以在构造器中产生循环引用的依赖注入上使用@Lazy来解决循环引用问题:

@Component
public class Person {
    private Dog pet;

    public Person(@Lazy Dog pet) {
        this.pet = pet;
    }
}

@Component
public class Dog {
    private Person owner;

    public Dog(Person owner) {
        this.owner = owner;
    }
}

此时,在 Spring 创建Person对象的时候,Spring 会使用一个Dog类型的代理,而不是真正创建一个Dog类型,真正的Dog类型会在之后需要的时候才被创建,而那时候Person类型的 bean 早已完成创建和初始化,因此再调用Dog的构造器进行注入时不会产生循环依赖的问题。

属性注入和 Setter 注入

就像之前说的,这种循环依赖仅会在使用构造器注入时出现,因此我们可以使用属性注入或 Setter 注入的方式来解决循环依赖问题:

@Component
public class Person {
    @Setter(onMethod = @__(@Autowired))
    private Dog pet;
}

@Component
public class Dog {
    @Setter(onMethod = @__(@Autowired))
    private Person owner;
}

除此之外我们还需要修改一个配置选项:

spring.main.allow-circular-references=true
  • 在早期的 Spring 版本中,spring.main.allow-circular-references默认为true,因此可以直接通过这种方式规避循环依赖,但后来 Spring 官方认为循环依赖是“代码异味”,所以将该选项默认设置为false
  • 在目前的版本中,无论spring.main.allow-circular-references的值是什么,构造器注入导致的循环依赖都会报错。
  • Lombok 和依赖注入的内容可以参考我的另一篇文章

当然属性注入也是同样的效果:

@Component
public class Person {
    @Autowired
    private Dog pet;
}

@Component
public class Dog {
    @Autowired
    private Person owner;
}

部分依赖注入

注意,下面的解决方案都不需要将spring.main.allow-circular-references配置设置为true

循环依赖实际上是因为两个互相存在依赖关系的类型都使用依赖注入实现依赖导致的,因此只要我们不完全使用依赖注入(部分使用依赖注入),就可以解决此类问题:

@Component
public class Person {
    @Autowired
    private Dog pet;

    @PostConstruct
    public void init(){
        this.pet.setOwner(this);
    }
}

@Component
public class Dog {
    @Setter
    private Person owner;
}

这里的关键在于,Person通过依赖注入来初始化pet属性,而Dog类中的owner属性并没有借助依赖注入进行初始化,所以这里并不存在循环依赖。但显然我们需要实现Dog类对Person类的依赖关系,这可以通过 bean 的生命周期回调来完成,比如这个示例中的@PostConstruct标记的回调方法,在这个方法中我们通过this.pet.setOwner(this)的方式创建了Dog实例对Person实例的依赖关系。

当然,类似的你可以使用任意方式的 bean 初始化回调,比如:

@Component
public class Person implements InitializingBean {
    // ...
    @Override
    public void afterPropertiesSet() throws Exception {
        this.pet.setOwner(this);
    }
}

效果是完全相同的。

关于更多 bean 生命周期回调的更多说明,可以参考我的这篇文章

不使用依赖注入

可以更激进一些,完全不使用依赖注入,自然也就不存在循环依赖的问题,比如:

@Component
public class Person implements InitializingBean, ApplicationContextAware {
    private Dog pet;
    private ApplicationContext ctx;

    @Override
    public void afterPropertiesSet() throws Exception {
        this.pet = ctx.getBean(Dog.class);
        this.pet.setOwner(this);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }
}

@Component
public class Dog {
    @Setter
    private Person owner;
}

在不使用依赖注入的情况下,我们就需要用ApplicationContext.getBean获取 bean 实例,因此需要一个ApplicationContext的引用,这里可以通过ApplicationContextAware接口实现。

当然,实际上我们并不需要这么“极端”,只需要不使用依赖注入处理存在循环依赖的属性即可,对于ApplicationContext可以通过依赖注入获取:

@Component
public class Person implements InitializingBean {
    private Dog pet;
    @Autowired
    private ApplicationContext ctx;

    @Override
    public void afterPropertiesSet() throws Exception {
        this.pet = ctx.getBean(Dog.class);
        this.pet.setOwner(this);
    }
}

总结

如果代码中存在依赖注入,在可能的情况下,最好进行重构,因为依赖注入往往说明存在“代码异味”。如果因为成本之类的原因无法重构,可以通过本文说明的几种方式进行处理。

The End,谢谢阅读。

本文的完整示例代码可以从这里获取。

参考资料

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。