Динамическое внедрение зависимостей для нескольких реализаций одного и того же интерфейса с Spring MVC - PullRequest
0 голосов
/ 13 ноября 2018

Я работаю над REST API, где у меня есть интерфейс, который определяет список методов, которые реализуются 4 различными классами, с возможностью добавления еще многих в будущем.

Когда я получаю HTTP-запрос от клиента, в URL включается некоторая информация, которая определяет, какую реализацию необходимо использовать.

В моем контроллере я бы хотел, чтобы метод конечной точки содержал инструкцию switch, которая проверяет переменную пути URL-адреса и затем использует соответствующую реализацию.

Я знаю, что могу определить и внедрить конкретные реализации в контроллер, а затем вставить, какую из них я хотел бы использовать в каждом конкретном случае в операторе switch, но это не кажется очень элегантным или масштабируемым по двум причинам:

  1. Теперь мне нужно создать экземпляр всех сервисов, хотя мне нужно использовать только один.

  2. Код выглядит так, как будто он может быть намного проще, так как я буквально вызываю тот же метод, который определен в интерфейсе с теми же параметрами, и хотя в примере это на самом деле не проблема, но в том случае, если список реализаций растет ... увеличивается количество случаев и избыточный код.

Есть ли лучшее решение для разрешения ситуации такого типа? Я использую SpringBoot 2 и JDK 10, в идеале я бы хотел реализовать самое современное решение.

Мой текущий подход

@RequestMapping(Requests.MY_BASE_API_URL)
public class MyController {

    //== FIELDS ==
    private final ConcreteServiceImpl1 concreteService1;
    private final ConcreteServiceImpl2 concreteService2;
    private final ConcreteServiceImpl3 concreteService3;

    //== CONSTRUCTORS ==
    @Autowired
    public MyController(ConcreteServiceImpl1 concreteService1, ConcreteServiceImpl2 concreteService2,
                              ConcreteServiceImpl3 concreteService3){
      this.concreteService1 = concreteService1;
      this.concreteService2 = concreteService2;
      this.concreteService3 = concreteService3;
    }


    //== REQUEST MAPPINGS ==
    @GetMapping(Requests.SPECIFIC_REQUEST)
    public ResponseEntity<?> handleSpecificRequest(@PathVariable String source,
                                                       @RequestParam String start,
                                                       @RequestParam String end){

        source = source.toLowerCase();
        if(MyConstants.SOURCES.contains(source)){
            switch(source){
                case("value1"):
                    concreteService1.doSomething(start, end);
                    break;
                case("value2"):
                    concreteService2.doSomething(start, end);
                    break;
                case("value3"):
                    concreteService3.doSomething(start, end);
                    break;
            }
        }else{
            //An invalid source path variable was recieved
        }

        //Return something after additional processing
        return null;
    }
}

Ответы [ 2 ]

0 голосов
/ 18 ноября 2018

В Spring вы можете получить все реализации интерфейса (скажем, T), введя поле List<T> или Map<String, T>.Во втором случае имена бобов станут ключами карты.Вы можете рассмотреть это, если существует много возможных реализаций или если они часто меняются.Благодаря этому вы можете добавить или удалить реализацию, не меняя контроллер.

Обе инъекции List или Map в этом случае имеют некоторые преимущества и недостатки.Если вы введете List, вам, вероятно, потребуется добавить какой-либо метод для сопоставления имени и реализации.Что-то вроде:

interface MyInterface() {
    (...)
    String name()
}

Таким образом, вы можете преобразовать его в Map<String, MyInterface>, например, используя Streams API.Хотя это было бы более явно, это немного улучшило бы ваш интерфейс (зачем ему знать, что существует несколько реализаций?).

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

Упрощенная реализация в Spring Boot может выглядеть следующим образом:

@SpringBootApplication
public class DynamicDependencyInjectionForMultipleImplementationsApplication {

    public static void main(String[] args) {
        SpringApplication.run(DynamicDependencyInjectionForMultipleImplementationsApplication.class, args);
    }

    interface MyInterface {
        Object getStuff();
    }

    class Implementation1 implements MyInterface {
        @Override public Object getStuff() {
            return "foo";
        }
    }

    class Implementation2 implements MyInterface {
        @Override public Object getStuff() {
            return "bar";
        }
    }

    @Configuration
    class Config {

        @Bean("getFoo")
        Implementation1 implementation1() {
            return new Implementation1();
        }

        @Bean("getBar")
        Implementation2 implementation2() {
            return new Implementation2();
        }
    }



    @RestController
    class Controller {

        private final Map<String, MyInterface> implementations;

        Controller(Map<String, MyInterface> implementations) {
            this.implementations = implementations;
        }

        @GetMapping("/run/{beanName}")
        Object runSelectedImplementation(@PathVariable String beanName) {
            return Optional.ofNullable(implementations.get(beanName))
                           .orElseThrow(UnknownImplementation::new)
                           .getStuff();
        }

        @ResponseStatus(BAD_REQUEST)
        class UnknownImplementation extends RuntimeException {
        }

    }
}

Он проходит следующие тесты:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class DynamicDependencyInjectionForMultipleImplementationsApplicationTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void shouldCallImplementation1() throws Exception {
        mockMvc.perform(get("/run/getFoo"))
                    .andExpect(status().isOk())
                    .andExpect(content().string(containsString("foo")));
    }

    @Test
    public void shouldCallImplementation2() throws Exception {
        mockMvc.perform(get("/run/getBar"))
                    .andExpect(status().isOk())
                    .andExpect(content().string(containsString("bar")));
    }

    @Test
    public void shouldRejectUnknownImplementations() throws Exception {
        mockMvc.perform(get("/run/getSomethingElse"))
               .andExpect(status().isBadRequest());
    }
}
0 голосов
/ 13 ноября 2018

Относительно двух ваших сомнений:
1. Создание объекта службы не должно быть проблемой, так как это одноразовая работа, и диспетчеру понадобятся они для обслуживания всех типов запросов.
2. Вы можете использовать точное отображение пути, чтобы избавиться от случая переключения. Например, :

@GetMapping("/specificRequest/value1")
@GetMapping("/specificRequest/value2")
@GetMapping("/specificRequest/value3")

Все приведенное выше сопоставление будет осуществляться по отдельному методу, который будет работать с конкретным значением источника и вызывать соответствующий метод обслуживания. Надеюсь, это поможет сделать код более чистым и элегантным.

Есть еще одна возможность отделить это на сервисном уровне и иметь только одну конечную точку для обслуживания всех типов источников, но, как вы сказали, для каждого значения источника существует разная реализация, тогда в нем говорится, что источник - это не что иное, как ресурс для вашего приложения, и наличие отдельного URI / отдельный метод имеет здесь смысл. Вот некоторые преимущества, которые я вижу здесь:

  1. Упрощает написание тестовых случаев.
  2. То же масштабирование без влияния на любой другой источник / службу.
  3. Ваш код рассматривает каждый источник как отдельную сущность от других источников.

Приведенный выше подход должен подойти, если у вас ограниченные исходные значения. Если у вас нет контроля над исходным значением, тогда нам нужно дополнительно изменить его, сделав исходное значение дифференцированным еще на одно значение, например, sourceType и т. Д., И затем имея отдельный контроллер для каждого типа группы источника.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...