Как работает Spring IoC в минимальном примере? - PullRequest
0 голосов
/ 26 сентября 2018

Подавляющее большинство учебных пособий посвящено вырожденному случаю, когда существует только одна Реализация для интерфейса, который нужно внедрить.Тем не менее, я в растерянности и до сих пор не нашел какого-либо руководства по созданию приложения, в котором несколько специализированных частей предоставляют несколько различных реализаций общих интерфейсов для внедрения в общие части (так называемый шаблон стратегии, или инверсия управления).

В моей реальной ситуации у меня есть сервер Tomcat, на котором развернуто одно приложение, в котором несколько частей предоставляют различные интерфейсы для внешнего мира.В этом приложении определение @Bean для общего interface в одном специализированном @Configuration всегда приводит к тому, что другие специализированные части получают одинаковые @Bean, даже если их (только на первый взгляд?) Независимые @Configuration определяют другое @Bean.

В качестве минимального примера я попытался написать приложение 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)));
  }
}

Цель этого кодавыглядит следующим образом:

  • Оба ConfigA и ConfigB импортируют BaseConfig, поскольку их процессы используют одинаковые CommonProcess.
  • Оба ConfigA и ConfigB определяют ихспециальная специализированная реализация Service для предоставления общего значения из разных источников
    (например, один из XML и один из JSON).
  • Класс App здесь является заменой сервлетаЯ бы развернул на сервере Tomcat.Очевидно, что приложение должно знать (предоставлять) все интерфейсы, которые должен предоставлять сервер, поэтому приложение должно @Import и ConfigA и ConfigB.
    Насколько я понимаю, такая "точка сбора" для "листовые узлы "уровней абстракции приложения должны существовать, чтобы представить их всем миру
    (в этом примере просто запустив их на сервере Tomcat, зарегистрировав их контроллеры Spring).

Теперь можно наблюдать следующее поведение:

  1. При запуске приложения как есть будет напечатано false false или true true, но никогда не ожидается false true или true false;
  2. удаление @Import из App, как и ожидалось, приведет к тому, что приложение ничего не будет запускать.

В то время как ожидаемое поведение будет:

  1. где CommonProcess вызывается из ConfigA используется service из ConfigA
  2. , где CommonProcess вызывается из ConfigB используется service из ConfigB

Вопрос: Каков канонический способ получения ожидаемого поведения?r?
(предпочтительнее решение на основе аннотаций)


Для справки рабочий пример на простом 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();
  }
}

Здесь вывод действительно такой, как ожидалось false true.

1 Ответ

0 голосов
/ 27 сентября 2018

Вы переосмысливаете Spring IoC и путаете @Configuration с ApplicationContext (фактическим контейнером IoC).

@Configuration обрабатывается в рамках уже существующего контейнера.И документы однажды заявленные:

@Import представляет JavaConfig-эквивалент элемента XML-конфигурации <import/>.Один класс конфигурации может импортировать любое количество других классов конфигурации, и их определения bean-компонентов будут обрабатываться так, как если бы они были определены локально.

То есть все импортированные и обнаруженные @Configurations загружаются в один и тот же контейнер.

После этого создаются все одиночные бины.Затем они соединяются вместе.

Внутри контейнера вы можете иметь несколько бинов одного типа, но не с одинаковым именем .В JavaConfig имя бина получается из имени метода фабрики или имени класса.В случае Service есть только одно имя, service, и, следовательно, только один компонент типа Service.Если вы присмотритесь, вы увидите сообщение запуска по линии "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]"

Один-единственный service подключается везде, где это необходимо (в commonProcess, configA и configB).

В вашем конкретном случае вы можете решить эту проблему, передав Service в CommonProcess.test(), как в простой версии Java, и присвоив уникальное имя каждому Service экземпляру (например, serviceA).и serviceB):

@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);
  }
}

Я бы также посоветовал изучить область действия боба , особенно заводскую область применения.

И, наконец, Spring Boot поддерживает иерархия ApplicationContext , которая, по сути, позволяет создавать подпрограммы в одном исполняемом файле.Таким образом, ConfigA и ConfigB могут иметь свой экземпляр Service с именем service.Эта функциональность используется редко.

@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);
  }
}
...