Spring:如何在运行时更改接口实现

8
作为一名Java开发者,我经常需要在不同的接口实现中进行选择。有时,这个选择只需进行一次,而其他时候,根据程序接收到的不同输入,我需要不同的实现。换句话说,我需要能够在运行时更改实现。这可以通过一个辅助对象轻松实现,该对象将某个关键字(基于用户输入)转换为适当的接口实现的引用。
使用Spring,我可以将这样一个对象设计为bean,并在需要的地方注入它:
public class MyClass {

    @Autowired
    private MyHelper helper;

    public void someMethod(String someKey) {
        AnInterface i = helper.giveMeTheRightImplementation(someKey);
        i.doYourjob();
    }

}

现在,我应该如何实现这个助手呢?让我们从这里开始:
@Service
public class MyHelper {

    public AnInterface giveMeTheRightImplementation(String key) {
        if (key.equals("foo")) return new Foo();
        else if (key.equals("bar")) return new Bar();
        else ...
    }

}

这样的解决方案有几个缺陷。其中最严重的一个是助手返回的实例对容器来说是未知的,因此无法从依赖注入中受益。换句话说,即使我像这样定义Foo类:
@Service
public class Foo {

    @Autowired
    private VeryCoolService coolService;

    ...

}

由于MyHelper返回的Foo实例没有正确初始化coolService字段,为避免这种情况,经常建议的解决方法是在helper中注入每个可能的实现。
@Service
public class MyHelper {

    @Autowired
    private Foo foo;

    @Autowired
    private Bar bar;

    ...

    public AnInterface giveMeTheRightImplementation(String key) {
        if (key.equals("foo")) return foo;
        else if (key.equals("bar")) return bar;
        else ...
    }

}

但我并不是这样解决方案的铁粉。我觉得更优雅和可维护的是像这样的东西:
@Service
public class MyHelper {

    @Autowired
    private ApplicationContext app;

    public AnInterface giveMeTheRightImplementation(String key) {
        return (AnInterface) app.getBean(key);
    }

}

这是基于 Spring 的 ApplicationContext
另一种类似的解决方案是使用 ServiceLocatorFactoryBean 类:
public interface MyHelper {

    AnInterface giveMeTheRightImplementation(String key);

}

// Somewhere else, in Java config

@Bean
ServiceLocatorFactoryBean myHelper() {
    ServiceLocatorFactoryBean bean = new ServiceLocatorFactoryBean();
    bean.setServiceLocatorInterface(MyHelper.class);
    return bean;
}

但由于我不是Spring专家,我想知道是否有更好的方法。

你可以在方法中传递一个类,而不是一个字符串,比如Foo.class - Turtle
@Nathan 我不确定你在提出什么。请注意,选择正确的类是问题,而不是解决方案。 - Marco Liceti
它必须是按键查找吗?如果您的解决方案只是解决区分相同接口实现的问题,那么使用@Qualifier进行自动装配并按名称引用bean可能会有所帮助。 - msp
@msparer 的目标是能够将某种输入转换为适当的实现。在我的示例中,我使用了一个名为 someKey 的字符串,它被视为 bean 名称,但这并不一定是这样的。它甚至可以是一个数字,并且可以根据该数字的大小选择实现,其中某些实现对小数字更有效,而其他一些实现对大数字更有效。此外,@Qualifier 也没有帮助,因为它允许您选择特定的 bean,但一旦它被连接,您就无法切换到另一个 :-/ - Marco Liceti
似乎问题与这个类似。 - Sergey Bespalov
可能是重复问题:动态注入Spring Bean - Sergey Bespalov
3个回答

6
我会尽力进行翻译,以下是您需要翻译的内容:

在我的项目中,我采用这种方法。这并非万无一失,但在添加新实现时,只需编写很少的配置代码即可有效服务。

我创建一个类似于这样的枚举

enum Mapper{
    KEY1("key1", "foo"),
    KEY2("key2", "bar")
    ;

    private String key;
    private String beanName;

    public static getBeanNameForKey(String key){
       // traverse through enums using values() and return beanName for key
    }
}

假设Foo和Bar都从同一个接口实现。我们称其为AnInterface。
class ImplFactory{

    @Autowired
    Map<String, AnInterface> implMap; // This will autowire all the implementations of AnInterface with the bean name as the key

    public AnInterface getImpl(string beanName){
            implMap.get(beanName);
    }
  }

你的辅助类将如下所示:
@Service
public class MyHelper {

@Autowired
ImplFactory factory;

    public AnInterface giveMeTheRightImplementation(String key) {

        String beanName = Mapper.getBeanNameForKey(key);  
        factory.getImpl(beanName);
    }  
}

这种方法的一些优点是:
1. 它避免了在选择正确的实现时使用冗长的if else或switch语句。
2. 如果您希望添加新的实现。你所要做的就是在枚举中添加一个Mapper(除了添加你的新Impl类)。
3. 你甚至可以配置你想要的impl类的Bean名称(如果你不想使用Spring给出的默认Bean名称)。这些名称将是工厂类中映射的键。你必须在你的枚举中使用它们。

编辑: 如果您希望为您的bean自定义名称,您可以使用其中一个构造型注释的value属性。例如。如果您已经将您的Impl注释为@Component或@Service,则执行@Component("myBeanName1")@Service("myBeanName2")


如果您想让键不是bean名称而是其他内容,您可以在setter上使用@autowire,并基于其中一个bean属性创建键。 - Tomer A
将字符串映射到bean名称对于简单的情况是可以的,但更普遍的情况下,选择正确(甚至是最佳)实现所需的逻辑可能很复杂。当然,这个逻辑应该封装在一个专用对象中(比如说class MyHelper)。当然,这个对象可以(有时应该)与其他对象协作(这基本上就是您使用的enum Mapperclass ImplFactory所建议的)。但这些对我来说只是实现细节。我的实际目标是使框架更加了解我正在尝试做什么。 - Marco Liceti

3
您想要做的标准方法应该是这样的:
interface YourInterface {
    void doSomething();
}

public class YourClass {

    @Inject @Any Instance<YourInterface> anImplementation;

    public void yourMethod(String someInput) {
        Annotation qualifier = turnInputIntoQualifier(someInput);
        anImplementation.select(qualifier).get().doSomething();
    }

    private Annotation turnInputIntoQualifier(String input) {
        ...
    }

}

然而目前,Spring不支持它(尽管计划在v5.x中支持)。 它应该可以在应用服务器上运行。

如果您想继续使用Spring,则基于ServiceLocatorFactoryBean的解决方案可能是最佳选择。


0

在声明bean时,您可以为其命名,然后您的助手可以请求应用程序上下文返回给定类型的bean。根据bean上声明的范围,应用程序上下文可以创建新实例(如果需要)或重用单例或其他可用范围,以基于上下文的方式。通过这种方式,您可以充分利用Spring功能。

例如:

@Service
public class MyHelper {
   @Autowired
   ApplicationContext applicationContext;

   public AnInterface giveMeTheRightImplementation(String key) {

   return context.getBean(key);
}

}

@Service("foo")
public class Foo implements AnInterface {
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接