使用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等,然后为每组类型的源设置单独的控制器。

最新更新