[spring-projects/spring-boot]默认禁止循环引用

2024-06-26 418 views
3

我们认为,应尽可能避免 bean 之间的循环引用。为了帮助用户避免意外创建此类依赖循环,我们应配置BeanFactory为默认禁止循环引用。我们将允许通过 API 和配置属性为真正需要它们的用户启用它们,就像我们在 2.1 中对 bean 定义覆盖所做的那样。我们也应该ApplicationContextRunner同时进行更新。

回答

5

@wilkinsona 我缺少一个针对现在检测到的自我引用(交易处理所需)的好的解决方案。

7

如果您需要帮助,请在 Stack Overflow 或 Gitter 上提问。试图帮助您的人需要查看包含循环引用的代码,以便他们可以就如何删除它们向您提供建议。

7

@wilkinsona 这不是真的。注入 bean 本身以便能够使用 spring 注释进行“内部”调用是一种常见模式。因此,如果默认情况下禁用此行为,那么应该有一个一般性建议来解决此类问题(无需更改 bean 的结构,因为有充分的理由这样设计)。

https://github.com/spring-projects/spring-framework/issues/13096 https://stackguides.com/questions/3037006/starting-new-transaction-in-spring-bean https://www.geekyhacker.com/2020/04/02/how-to-use-spring-boot-cacheable-on-self-invocation/

所以我并不是在寻找特殊情况下的解决方案,这些用例应该有一个可行的通用解决方案。

2

我还想知道推荐的解决方案。

这是另一个非常常见的用例:我有一个 repo 和一个自定义 repo,其中自定义 repo 使用内部的 repo。现在,我想到的唯一可以防止循环引用问题的方法就是使用@Lazy初始化:

interface DogRepo : DogRepoCustom, MongoRepository<Dog, String> { ... }

class DogRepoImpl(@Lazy private val dogRepo: DogRepo) : DogRepoCustom { ... }

interface DogRepoCustom { ... }

问题在于,比我更了解这个主题的人认为使用@Lazy此方法是一种解决方法。

8

@wilkinsona 谢谢!我认为 Spring 团队有充分的理由禁止循环依赖,我想遵循这一点。我完全愿意改变我的代码,但我不知道最好的方法是什么。以我上面的例子(repo 和自定义 repo)为例,使用循环依赖是@Lazy一个坏主意吗?如果是,推荐的方法是什么?我认为 Spring 应该在文档中添加一些内容来展示一些关于如何解决循环依赖的例子,既然你说存在循环依赖是一个坏主意。我显然想知道我的例子,但这个主题总体上可以在文档中解释。

1

关于存储库片段,这确实是 Spring Data 团队 (/cc @mp911de) 的问题。我不熟悉推荐自我注入的其他领域,而且我从未需要过它,所以我很难发表评论。

3

一般来说,如果您将存储库 bean 访问推迟到其创建之后(@LazyObjectProviderBeanFactory查找),那么您就不会遇到循环 bean 错误。请记住,存储库片段用于组装存储库本身,因此循环引用可能导致递归调用。

4

在我工作的项目中,我们经常使用自调用进行缓存。升级到 2.6.3 后,我正在寻找迁移策略。

我很好奇,想得到一些关于类似下面这样的解决方案的反馈。基本上,我会用对 ProxyExecutor bean 的调用替换自调用,并执行以下操作:

Function<UserService, UserConfiguration> fn = (userService) -> userService.getCurrentUserConfiguration(); UserConfiguration conf = proxyExecutor.execute(fn, UserService.class);

@Service
public class ProxyExecutor {

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * Allows to call a bean method forcing to go through the proxy.
     * 
     * @param <T>
     *            the class of the spring bean to be invoked
     * @param <R>
     *            the result of the function to invoke on that bean
     * @param fn
     *            the function that needs to be invoked via proxy
     * @param beanClazz
     *            the bean class
     * @return the same result of the given function
     */
    @Transactional(propagation = Propagation.REQUIRED)
    public <T, R> R execute(Function<T, R> fn, Class<T> beanClazz) {
        T bean = applicationContext.getBean(beanClazz);
        return fn.apply(bean);
    }
}

我认为这有点像黑客行为,但从总体角度来看,我没有发现任何问题。

4

我们可以在哪里得到更加详细和有条理的答案?

似乎每个人都认为循环依赖是糟糕设计的标志,但我不明白。当然,如果 bean 是通过构造函数注入的,那么循环依赖就会非常成问题。但简单的解决方案是通过 setter 进行注入,根据我的经验,这种方法更方便,也更简洁。在我的一个项目中,我们有一个接近图形的数据模型,其中包含一些 Node 和 Edge POJO。因此,要处理这些数据,我们需要两个 bean,一个 NodeProcessor 和一个 EdgeProcessor,并且自然而然地,每个 bean 都依赖于另一个:每个处理器都足够复杂,可以放在自己的类中,但要处理一个节点,您需要处理每个输出边,而要处理一条边,您必须处理关联的节点。在我看来,这种设计清晰易懂。每个建议的解决方案每次都会引入基于模糊类的黑客攻击,没有任何业务语义含义,唯一的目的是以某种方式打破循环。在我看来,这比我的两个循环 bean 更糟糕。那么,这种非常教条的“循环依赖很糟糕”的说法从何而来?有人有一篇带有虐待狂或恶魔般邪恶的循环依赖的 creepypasta 吗?

Spring Core 目前似乎禁止循环依赖。Spring Boot 引入了一种允许它的方法,但发行说明明确指出“强烈建议您更新配置以打破依赖循环”。每个 Spring 团队对这个问题的长期愿景是什么?该属性是否spring.main.allow-circular-references承认某些循环依赖是必要的(但仅限于 Spring Boot,Spring Core 否认这一点)或者它只是一个临时解决方案,以便开发人员有时间找到迁移路径?

1

我认为这有点像黑客行为,但从总体角度来看,我没有发现任何问题。

不,这不是黑客行为。自我注入完全可以允许重入调用遵守 AOP 建议,例如 @Cacheable、@Transactional、@Retryable

2

在我们的项目中,我们有很多对其他 bean 的引用,因为我们按逻辑上下文分离了代码。例如,有处理所有用户操作的 UserService,还有处理客户操作的 CustomerService。当然,如果我们想找到客户,UserService 会引用 CustomerService。例如,CustomerService 有 UserService 来检查用户的角色。我们不知道如何打破这种“依赖循环”,并建立更好的架构,而不是将所有东西都放在一个地方,造成服务混乱。

有没有什么好的实践可以让你更好地构建架构?如何开始解决这个问题?

7

您必须将业务逻辑分成连贯的部分。虽然我不知道您的 UserService 和 CustomerService 在做什么,但这种分离似乎很可疑,因为 User 和 Customer 是类似的东西。不要按实体(用户、客户)对业务逻辑进行分组,而是尝试按流程、用例等进行分组……如果您无法清楚地识别用户和客户的业务流程组,您可以尝试以下纯技术技巧:

  • 找到 UserService 中所有需要 CustomerService 的方法,并将它们移动到新的 UserCustomerService
  • 找到 CustomerService 中所有需要 UserService 的方法,并将它们移动到新的 CustomerUserService 中
  • 使 UserCustomerService 依赖于 CustomerService(而不是 CustomerUserService)
  • 让 CustomerUserService 依赖于 UserService(而不是 UserCustomerService)。如果您没有循环方法链(UserService.methodABC 调用 CustomerService.methodEDF,后者又调用 UserService.methodGHI...),这将有所帮助。如果您有这样的循环方法链,那么您将陷入一个非常混乱的境地,只有从头开始重写(和重构)所有内容才能解决。
0

@Majstr 一个非常有效的方法是将客户服务的低级操作(通常不需要用户服务)与更复杂的客户服务操作分开,并将其划分为不同的服务。用户服务也一样。

8

不,这不是黑客行为。自我注入完全可以允许重入调用遵守 AOP 建议,例如https://github.com/Cacheable、@transactional、@Retryable

针对这些合法用例是否有官方策略?对整个上下文使用 spring.main.allow-circular-references 只是为了允许一个服务自我注入似乎不正确。

9

针对这些合法用例是否有官方策略?

完全同意,应该有针对循环依赖的合法用例的官方策略,例如 self-ijnection。也许一个专门用于自我注入的特殊注释是一个可行的解决方案。或者其他一些明确的方式来处理可重入调用的 AOP 拦截器。

3

如果您希望看到这种情况,请向框架团队提出。Boot 可用的唯一工具是 BeanFactory 范围的配置选项,它spring.main.allow-circular-references映射到。