簡體   English   中英

使用Spring MVC對同一接口的多個實現進行動態依賴注入

[英]Dynamic dependency injection for multiple implementations of the same interface with Spring MVC

我正在使用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;
    }
}

在Spring中,您可以通過注入List<T>Map<String, T>字段來獲得接口的所有實現(例如T )。 在第二種情況下,bean的名稱將成為地圖的鍵。 如果有很多可能的實現方式,或者它們經常更改,則可以考慮這一點。 多虧了它,您可以在不更改控制器的情況下添加或刪除實現。

在這種情況下,注入ListMap都具有一些優點和缺點。 如果注入List ,則可能需要添加一些方法來映射名稱和實現。 就像是 :

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

這樣,您可以將其轉換為Map<String, MyInterface> ,例如使用Streams API。 盡管這會更明確,但會稍微污染您的界面(為什么要知道有多個實現?)。

使用Map您可能應該顯式命名bean,甚至引入注釋以遵循最少驚訝的原則。 如果使用配置類的類名或方法名來命名Bean,則可以通過重命名它們(實際上是更改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());
    }
}

關於您的兩個疑問:
1.實例化服務對象應該不是問題,因為這是一項工作,控制器將需要它們來滿足所有類型的請求。
2.您可以使用精確的路徑映射來擺脫開關情況。 例如:

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

以上所有映射都將在單獨的方法上,該方法將處理特定的源值並調用相應的服務方法。 希望這將有助於使代碼更簡潔明了。

還有另一種選擇,可以在服務層上將其分開,並且只有一個終結點來服務所有類型的源,但是正如您所說的,每個源值都有不同的實現,然后它說源不過是您的應用程序的資源,並且具有單獨的資源URI /單獨方法在這里很有意義。 我在這里看到的一些優點是:

  1. 使編寫測試用例變得容易。
  2. 在不影響任何其他源/服務的情況下進行縮放。
  3. 您的代碼將每個源作為獨立於其他源的實體進行處理。

當您的源值有限時,上述方法應該很好。 如果您無法控制源值,那么我們需要在此處進行進一步的重新設計,方法是使源值相差一個以上的值(如sourceType等),然后為每個組的源類型分別設置控制器。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM