简体   繁体   English

Spring IoC 如何在一个最小的例子中实际工作?

[英]How does Spring IoC actually work in a minimal example?

A vast majority of tutorials deal with the degenerate case that there is only exactly one Implementation for the interface to be injected.绝大多数教程都处理退化情况,即要注入的接口只有一个实现。 However, I am at a loss and so far have not found any lead on how to build an application whereof several specialised parts provide several different implementations of common interfaces to be injected into common parts (aka Strategy Pattern, aka Inversion of Control).但是,我不知所措,到目前为止还没有找到关于如何构建应用程序的任何线索,其中几个专门部分提供了要注入到公共部分(又名策略模式,又名控制反转)的公共接口的几种不同实现。

In my real-life situation I have a Tomcat server with one application deployed thereon, wherein several parts provide different interfaces to the outside world.在我的现实生活中,我有一个 Tomcat 服务器,上面部署了一个应用程序,其中几个部分为外部世界提供了不同的接口。 In this application, defining a @Bean for a common interface in one specialised @Configuration always results in other specialised parts receiving the same @Bean even though their (only seemingly?) independent @Configuration s define a different @Bean .在此应用程序中,在一个专门的@Configuration为公共interface定义@Bean总是会导致其他专门部分接收相同的@Bean即使它们(只是看似?)独立的@Configuration定义了不同的@Bean

For a minimal example, I have attempted to write a Spring-boot application which exhibits the same behaviour and has the same general architecture:作为一个最小的例子,我试图编写一个 Spring-boot 应用程序,它表现出相同的行为并具有相同的通用架构:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@FunctionalInterface
interface Service { boolean test(); }

class CommonProcess {
  @Autowired
  Service service;

  public boolean test() { return this.service.test(); }
}

@Configuration
class BaseConfig {
  @Bean
  CommonProcess commonProcess() { return new CommonProcess(); }
}

@Configuration
class ConfigA {
  @Bean
  CommandLineRunner processA() {
    return new CommandLineRunner() {
      @Autowired
      private CommonProcess process;

      @Override
      public void run(String... args) throws Exception {
        System.out.println(this.process.test());
      }
    };
  }

  @Bean
  Service service() { return () -> false; }
}

@Configuration
class ConfigB {
  @Bean
  CommandLineRunner processB() {
    return new CommandLineRunner() {
      @Autowired
      private CommonProcess process;

      @Override
      public void run(String... args) throws Exception {
        System.out.println(this.process.test());
      }
    };
  }

  @Bean
  Service service() { return () -> true; }
}

@SpringBootConfiguration
@Import(value = { BaseConfig.class, ConfigA.class, ConfigB.class })
class App {
  public static void main(String[] args) {
    System.exit(SpringApplication.exit(SpringApplication.run(App.class, args)));
  }
}

The intent behind this code is as follows:这段代码背后的意图如下:

  • Both ConfigA and ConfigB import BaseConfig, because their processes use the same CommonProcess . ConfigAConfigB导入 BaseConfig,因为它们的进程使用相同的CommonProcess
  • Both ConfigA and ConfigB define their specific, specialised implementation of Service to provide a common value from different sources ConfigAConfigB定义了它们特定的、专门的Service实现,以提供来自不同来源的通用值
    (eg one from XML and one from JSON). (例如一个来自 XML,一个来自 JSON)。
  • The class App here is a stand-in for the Servlet I would deploy on a Tomcat server.这里的类App是我将在 Tomcat 服务器上部署的 Servlet 的替代品。 Obviously, App has to know (provide) all the interfaces the Server should provide, so App has to @Import both ConfigA and ConfigB .显然,应用程序必须知道(提供)所有接口的服务器应该提供,所以应用程序必须@ImportConfigAConfigB
    It is my understanding, that such a "collection point" for "leaf nodes" of an application's abstraction layers needs to exist, in order to expose them all to the world我的理解是,应用程序抽象层的“叶节点”需要存在这样一个“收集点”,以便将它们全部暴露给世界
    (in this example by simply running them, in a Tomcat server by registering their Spring Controllers). (在这个例子中,通过在 Tomcat 服务器中注册它们的 Spring 控制器来简单地运行它们)。

Now, the following behaviours can be observed:现在,可以观察到以下行为:

  1. Starting the App as is will print false false or true true but never the expected false true or true false ;按原样启动应用程序将打印false falsetrue true但不会打印预期的false truetrue false
  2. removing the @Import from App will predictably result in the App not running anything.App删除@Import将可预见地导致 App 不运行任何东西。

Whereas the expected behaviour would be:而预期的行为是:

  1. where CommonProcess is called from ConfigA it uses the service of ConfigA其中CommonProcess从所谓ConfigA它使用的serviceConfigA
  2. where CommonProcess is called from ConfigB it uses the service of ConfigB其中CommonProcess从所谓ConfigB它使用的serviceConfigB

Question: What is the canonical way to produce the expected behaviour?问题:产生预期行为的规范方式是什么?
(annotation-based solution preferred) (首选基于注释的解决方案)


For reference a working example in plain Java:参考一个纯 Java 的工作示例:

import java.util.Arrays;
import java.util.List;

@FunctionalInterface
interface Service { boolean test(); }

class CommonProcess {
  public static final CommonProcess INSTANCE = new CommonProcess();

  public boolean test(Service service) { return service.test(); }
}

class ProcessA implements Runnable {
  // specific project knows generic project -> no need to inject
  private static final CommonProcess commonProcess = CommonProcess.INSTANCE;
  private static final Service service = () -> false;

  public void run() {
    // generic project does not know specific project -> specifics are injected
    System.out.println(this.commonProcess.test(this.service));
  }
}

class ProcessB implements Runnable {
  // specific project knows generic project -> no need to inject
  private static final CommonProcess commonProcess = CommonProcess.INSTANCE;
  private static final Service service = () -> true;

  public void run() {
    // generic project does not know specific project -> specifics are injected
    System.out.println(this.commonProcess.test(this.service));
  }
}

class PlainApp {
  private static final List<Runnable> processes = Arrays.asList(new ProcessA(), new ProcessB());

  public static void main(String[] args) {
    for (Runnable process : processes)
      process.run();
  }
}

Here the output is indeed as expected false true .这里的输出确实是预期的false true

You're overthinking Spring IoC and confusing @Configuration with ApplicationContext (the actual IoC container).您过度考虑 Spring IoC 并将@ConfigurationApplicationContext (实际的 IoC 容器)混淆。

@Configuration is processed in scope of an already existing container. @Configuration在现有容器的范围内处理。 And the docs once stated: 文档曾经说过:

@Import represents JavaConfig's equivalent of XML configuration's <import/> element. @Import表示 JavaConfig 相当于 XML 配置的<import/>元素。 One configuration class can import any number of other configuration classes, and their bean definitions will be processed as if locally defined.一个配置类可以导入任意数量的其他配置类,它们的 bean 定义将像本地定义一样进行处理。

That is, all imported and discovered @Configurations get loaded into the same container.也就是说,所有导入和发现的 @Configurations 都被加载到同一个容器中。

After that all singleton beans are created.之后创建所有单例bean。 Then they are wired together.然后将它们连接在一起。

Within a container you can have multiple beans of the same type but not with the same name .在一个容器中,您可以拥有多个相同类型但名称不同的bean。 In a JavaConfig, a bean name is derived from either the factory method name or the class name.在 JavaConfig 中,bean 名称源自工厂方法名称或类名称。 In case of Service there's just one name, service , and hence just one bean of type Service .Service的情况下,只有一个名称service ,因此只有一个Service类型的 bean。 If you look closely you'll see a startup message along the lines " Overriding bean definition for bean 'service' with a different definition: replacing [factoryBeanName=ConfigA; factoryMethodName=service; defined in ConfigA] with [factoryBeanName=ConfigB; factoryMethodName=service; defined in ConfigB] "如果您仔细观察,您会看到一条启动消息Overriding bean definition for bean 'service' with a different definition: replacing [factoryBeanName=ConfigA; factoryMethodName=service; defined in ConfigA] with [factoryBeanName=ConfigB; factoryMethodName=service; defined in ConfigB] "

The one and only service is then wired in everywhere it's needed (in commonProcess , configA and configB ).然后将唯一的service连接到需要它的任何地方(在commonProcessconfigAconfigB )。

In your specific case you can solve it by passing Service to CommonProcess.test() like in your plain Java version, and giving a unique name to each Service instance (eg serviceA and serviceB ):在您的特定情况下,您可以通过将Service传递给CommonProcess.test()来解决它,就像在您的纯 Java 版本中一样,并为每个Service实例(例如serviceAserviceB )提供一个唯一的名称:

@FunctionalInterface
interface Service {
  boolean test();
}

class CommonProcess {
  public boolean test(Service service) {
    return service.test();
  }
}

@Configuration
class BaseConfig {
  @Bean
  CommonProcess commonProcess() {
    return new CommonProcess();
  }
}

@Configuration
class ConfigA {
  @Bean
  CommandLineRunner processA(@Named("serviceA") Service service) {
    return new CommandLineRunner() {
      @Autowired
      private CommonProcess process;

      @Override
      public void run(String... args) throws Exception {
        System.out.println(this.process.test(service));
      }
    };
  }

  @Bean
  Service serviceA() {
    return () -> false;
  }
}

@Configuration
class ConfigB {
  @Bean
  CommandLineRunner processB(@Named("serviceB") Service service) {
    return new CommandLineRunner() {
      @Autowired
      private CommonProcess process;

      @Override
      public void run(String... args) throws Exception {
        System.out.println(this.process.test(service));
      }
      @Bean
      Service serviceB() {
        return () -> true;
      }
    };
  }

  @Autowired
  ApplicationContext applicationContext;

  @PostConstruct
  public void printBeans() {
    System.out.println(Arrays.asList(applicationContext.getBeanDefinitionNames()));
  }

  @Bean
  Service serviceB() {
    return () -> true;
  }
}

@SpringBootConfiguration
@Import(value = { BaseConfig.class, ConfigA.class, ConfigB.class })
class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }
}

I would also suggest looking into bean scopes , especially the factory scope.我还建议查看bean 范围,尤其是工厂范围。

And finally, Spring Boot supports a hierarchy of ApplicationContext's , which essentially allows you to create sub-applications within one executable.最后,Spring Boot 支持ApplicationContext 的层次结构,它本质上允许您在一个可执行文件中创建子应用程序。 This way ConfigA and ConfigB can each have it's own Service instance named service .这样ConfigAConfigB可以各自拥有自己的名为serviceService实例。 This functionality is rarely used.此功能很少使用。

@SpringBootConfiguration
@Import(value = { BaseConfig.class })
class App {
  public static void main(String[] args) {
    SpringApplicationBuilder app = new SpringApplicationBuilder(App.class);
    app.child(ConfigA.class).run(args);
    app.child(ConfigB.class).run(args);
  }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM