Spring依赖注入意外地推断了正确的bean。

huangapple go评论44阅读模式
英文:

Spring dependency injection infers unexpectly the right bean

问题

拥有这些接口/类:

interface BankService {}

interface TransferService {}

class BankTransferServiceImpl implements BankService, TransferService {}

class BankingClient {
  public BankingClient(BankService bs) {}
}

如预期,此主方法将无法编译,因为BankingClient构造函数需要一个BankService类型,而不是TransferService类型:

public static void main(String[] args) {
  TransferService ts = new BankTransferServiceImpl();
  BankingClient bs = new BankingClient(ts);
}

然后我们有这个Spring配置文件:

@Configuration
public class MyConfig {

  @Bean
  TransferService transferService() {
    return new BankTransferServiceImpl();
  }

  @Bean
  BankingClient bankingService(BankService bs) {
    return new BankingClient(bs);
  }
}

类似地,我预期在bankingService方法中会出现错误,因为我们已经有的唯一bean是TransferService,而TransferService不扩展BankService。但是我的JUnit测试运行并成功实例化了bankingService。我漏掉了什么?

英文:

Having this interfaces/classes:

interface BankService {}

interface TransferService {}
 
class BankTransferServiceImpl implements BankService, TransferService {}

class BankingClient {
  public BankingClient(BankService bs) {}
}

Spring依赖注入意外地推断了正确的bean。

As expected this main method will not compiled, because BankingClient constructor expects a BankService, not a TransferService type:

public static void main(String[] args) {
  TransferService ts = new BankTransferServiceImpl();
  BankingClient bs = new BankingClient(ts);
}

Then we have this Spring configuration file:

@Configuration
public class MyConfig {

  @Bean
  TransferService transferService() {
    return new BankTransferServiceImpl();
  }

  @Bean
  BankingClient bankingService(BankService bs) {
    return new BankingClient(bs);
  }
}

Similarly, I was expecting to get an error in bankingService method, because the only bean we have already is the TransferService, and TransferService does not extends BankService. But my junit runs and instantiate bankingService successfully. What am I missing?

答案1

得分: 1

public static void main(String[] args) {
    List<Object> objects = new ArrayList<>();
    Map<Class, Supplier> suppliers = new HashMap<>();
    suppliers.put(TransferService.class, BankTransferServiceImpl::new);
    TransferService ts = new BankTransferServiceImpl();
    objects.add(ts);
    BankService bs = objects.stream()
            .filter(BankService.class::isInstance)
            .map(BankService.class::cast)
            .reduce((a, b) -> {
                throw new IllegalStateException("Multiple beans found");
            })
            .orElseGet(() -> {
                Supplier<?> supplier = suppliers.get(BankService.class);
                if (supplier == null) {
                    throw new IllegalStateException("No bean supplier found");
                }
                Object result = supplier.get();
                objects.add(result);
                return (BankService) result;
            });
    BankingClient client = new BankingClient(bs);
}

在您的情况下,bean初始化顺序实际上是有意义的:

  • 首先,Spring 创建了 TransferService bean
  • 随后,Spring "知道" TransferService bean 也实现了 BankService 接口,因此它可以构造 BankingClient 实例。

然而,如果您更改 MyConfig 配置类中的bean定义顺序:

@Configuration
public class MyConfig {

    @Bean
    BankingClient bankingService(BankService bs) {
        return new BankingClient(bs);
    }

    @Bean
    TransferService transferService() {
        return new BankTransferServiceImpl();
    }

}

您可能会得到另一种行为:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type 'BankService' available: 
     expected at least 1 bean which qualifies as autowire candidate.

这是因为 Spring 知道如何创建 TransferService bean,但不知道如何创建 BankService bean,而 TransferService bean 还没有被创建。

从工厂方法(使用 @Bean 注解的方法)切换到 @ComponentScan/@Component 也会改变 Spring 的行为:

  • 对于工厂方法,Spring 最初从工厂方法的返回类型中推断出bean类型,因此它可能没有足够的信息来确定实际的bean类型。
  • 对于 @ComponentScan/@Component,由于 Spring 确实看到了实际的bean类型,因此它有关于要创建的bean的更多信息。

您还可以查看 AbstractBeanFactory#isTypeMatch 方法。

英文:

> java
&gt; public static void main(String[] args) {
&gt; TransferService ts = new BankTransferServiceImpl();
&gt; BankingClient bs = new BankingClient(ts);
&gt;}
&gt;

Spring creates beans in a different way:

  • it stores information about beans have been created
  • it stores information about how to create particular bean

basically, that could be represented by the following oversimplified code:

public static void main(String[] args) {
    List&lt;Object&gt; objects = new ArrayList&lt;&gt;();
    Map&lt;Class, Supplier&gt; suppliers = new HashMap&lt;&gt;();
    suppliers.put(TransferService.class, BankTransferServiceImpl::new);
    TransferService ts = new BankTransferServiceImpl();
    objects.add(ts);
    BankService bs = objects.stream()
            .filter(BankService.class::isInstance)
            .map(BankService.class::cast)
            .reduce((a, b) -&gt; {
                throw new IllegalStateException(&quot;Multiple beans found&quot;);
            })
            .orElseGet(() -&gt; {
                Supplier&lt;?&gt; supplier = suppliers.get(BankService.class);
                if (supplier == null) {
                    throw new IllegalStateException(&quot;No bean supplier found&quot;);
                }
                Object result = supplier.get();
                objects.add(result);
                return (BankService) result;
            });
    BankingClient client = new BankingClient(bs);
}

In your case bean initialisation order actually makes sense:

  • at first spring creates TransferService bean
  • after that spring "does know" that TransferService bean also implements BankService interface, and thus it can construct BankingClient instance

However, if you change definition order of beans in MyConfig configuration class:

@Configuration
public class MyConfig {

    @Bean
    BankingClient bankingService(BankService bs) {
        return new BankingClient(bs);
    }

    @Bean
    TransferService transferService() {
        return new BankTransferServiceImpl();
    }

}

you may get another behaviour:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type &#39;BankService&#39; available: 
     expected at least 1 bean which qualifies as autowire candidate.

just because spring does know how to create TransferService bean, but has no idea about how to create BankService bean, and TransferService bean has not been created yet.

Switching from factory methods (methods annotated by @Bean) to @ComponentScan/@Component changes behaviour of spring as well:

  • in case of factory methods, spring initially infers bean type from factory method return type, and thus it may not have enough information about actual bean type
  • in case of @ComponentScan/@Component, since spring does see the actual bean type it has more information about beans to be created.

You may also check AbstractBeanFactory#isTypeMatch method.

huangapple
  • 本文由 发表于 2023年6月12日 07:27:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/76452892.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定