英文:
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) {}
}
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创建了TransferServicebean - 随后,
Spring"知道"TransferServicebean 也实现了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
> public static void main(String[] args) {
>   TransferService ts = new BankTransferServiceImpl();
>   BankingClient bs = new BankingClient(ts);
>}
>
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<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);
}
In your case bean initialisation order actually makes sense:
- at first 
springcreatesTransferServicebean - after that 
spring"does know" thatTransferServicebean also implementsBankServiceinterface, and thus it can constructBankingClientinstance 
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 'BankService' 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, 
springinitially 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, sincespringdoes see the actual bean type it has more information about beans to be created. 
You may also check AbstractBeanFactory#isTypeMatch method.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。



评论