【问题标题】:Dynamic dependency injection for multiple implementations of the same interface with Spring MVC使用 Spring MVC 对同一接口的多个实现进行动态依赖注入
【发布时间】:2018-11-13 04:39:48
【问题描述】:

我正在开发一个 REST API,其中我有一个接口,它定义了由 4 个不同类实现的方法列表,将来可能会添加更多。

当我收到来自客户端的 HTTP 请求时,URL 中包含一些信息,这些信息将确定需要使用哪个实现。

在我的控制器中,我希望端点方法包含一个 switch 语句,该语句检查 URL 路径变量,然后使用适当的实现。

我知道我可以定义具体实现并将其注入控制器,然后在 switch 语句中插入我想在每个特定情况下使用的实现,但这似乎不是很优雅或可扩展,原因有两个:

  1. 我现在必须实例化所有服务,即使我只需要使用一个。

  2. 1234563实现列表在增长……案例和冗余代码的数量也在增长。

有没有更好的解决方案来解决这种情况?我正在使用 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;
    }
}

【问题讨论】:

  • 我认为,如果来源可能性很小,您可以为每个来源提供单独的其余映射 api。或者如果有更多来源的可能性,那么检查是否有一种方法可以在服务层本身而不是在 api 级别合并处理多个来源的逻辑。
  • 我也想过这一点,实际上我最初是这样设置控制器的,但我遇到了同样的问题,因为我将实现自动装配到构造函数中。在这种特殊情况下,如果我要在同一个控制器中为每个源添加不同的映射,那么我会有更多的样板代码。这种方法的唯一选择是为每个源使用不同的控制器,这是可行的......但它似乎更像是一种解决方法而不是解决方案,但如果我找不到更好的方法,我可能会采用这种方法。跨度>
  • 检查您是否可以设计一种服务实现来处理不同来源的条件逻辑,从而通过将源参数传递给serviceImpl,您可以在一个类和一个相应的api中拥有逻辑。
  • 将检测委托给服务。注入您的服务列表(接口)。然后在您的方法中遍历您的服务列表,并调用一个方法以查看它们是否可以处理请求/源。当你找到正确的,使用它。这样你就有了一个可扩展的机制。

标签: java spring spring-mvc spring-boot dependency-injection


【解决方案1】:

在 Spring 中,您可以通过注入 List&lt;T&gt;Map&lt;String, T&gt; 字段来获取接口的所有实现(例如 T)。在第二种情况下,bean 的名称将成为映射的键。如果有很多可能的实现或它们经常更改,您可以考虑这一点。多亏了它,您可以在不更改控制器的情况下添加或删除实现。

在这种情况下,注入ListMap 都有一些优点和缺点。如果您注入List,您可能需要添加一些方法来映射名称和实现。类似的东西:

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

通过这种方式,您可以将其转换为 Map&lt;String, MyInterface&gt;,例如使用 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());
    }
}

【讨论】:

    【解决方案2】:

    关于您的两个疑问:
    1. 实例化服务对象应该不是问题,因为这是一次性工作,控制器需要它们来服务所有类型的请求。
    2.您可以使用确切的路径映射来摆脱开关盒。例如:

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

    以上所有映射都将在单独的方法上处理特定的源值并调用相应的服务方法。 希望这将有助于使代码更加简洁和优雅。

    还有另一种选择可以在服务层上分离它,并且只有一个端点来服务所有类型的源,但是正如您所说,每个源值都有不同的实现,然后它说源只是您的应用程序的资源,并且具有单独的URI/separate 方法在这里非常有意义。我在这里看到的几个优点是:

    1. 使编写测试用例变得容易。
    2. 在不影响任何其他源/服务的情况下进行扩展。
    3. 您的代码将每个来源视为与其他来源不同的实体。

    当您的源值有限时,上述方法应该没问题。如果您无法控制源值,那么我们需要在此处进一步重新设计,将源值区分为另一个值,例如 sourceType 等,然后为每个组类型的源设置单独的控制器。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-11
      • 2022-10-09
      • 1970-01-01
      • 2014-08-08
      • 1970-01-01
      相关资源
      最近更新 更多