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

Posted

技术标签:

【中文标题】Spring:如何在运行时更改接口实现【英文标题】:Spring: how to change interface implementations at runtime 【发布时间】:2017-10-15 11:43:55 【问题描述】:

作为一名 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 字段。

为避免这种情况,经常建议的解决方法是在帮助器中注入每个可能的实现

@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 @Nathan 我不确定你的提议。请注意,选择正确的课程是问题,而不是解决方案。 一定要按键查找吗?如果您的解决方案只是解决该问题以区分同一接口的实现,那么使用 @Qualifier 自动装配并按名称引用 bean 可能会对您有所帮助。 @msparer 目标是能够将某种输入转化为合适的实现。在我的示例中,我使用了一个名为 someKey 的字符串,它被视为 bean 名称,但不一定是这种情况。它甚至可以是一个数字,并且可以根据这个数字的大小来选择实现,其中一些实现对小数字更有效,而另一些对更大的数字更有效。此外,@Qualifier 也无济于事,因为它可以让您选择特定的 bean,但是一旦连接好,您就无法切换到另一个 :-/ 看来问题和this类似 【参考方案1】:

我在我的项目中采用这种方法。这不是万无一失的,但它在使用非常少的配置代码添加新实现方面非常有用。

我用这样的东西创建了一个枚举

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 都从一个命令接口实现。我们称它为接口

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's or switch case。 2. 如果你想添加一个新的实现。您所要做的就是在枚举中添加一个 Mapper(除了添加新的 Impl 类)。 3. 您甚至可以为您想要的 impl 类配置 Bean 名称(如果您不想要 spring 给出的默认 bean 名称)。这些名称将是您工厂类中映射的键。您必须在枚举中使用它。

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

【讨论】:

如果您希望密钥不是 bean 名称,您可以在 setter 上使用 @autowire 并根据 bean 属性之一创建密钥。 将字符串映射到 bean 名称对于简单的场景来说很好,但更一般地说,选择正确(甚至是 最佳)实现所需的逻辑可能很复杂。当然,这个逻辑应该封装在一个专用对象中(比如class MyHelper)。当然,这个对象可以(有时应该)与其他对象协作(这基本上就是您对enum Mapperclass ImplFactory 的建议)。但这些对我来说只是实现细节。我的实际目标是让框架更清楚我想要做什么。【参考方案2】:

做你想做的事的标准方式应该是这样的:

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 does not support it(虽然 它计划用于 v5.x)。它应该工作 application servers.

如果您想坚持使用 Spring,请使用基于 ServiceLocatorFactoryBean 的解决方案 可能是最好的。

【讨论】:

【参考方案3】:

您可以在声明 bean 时为其命名,并且您的助手可以要求应用程序上下文返回给定类型的 bean。根据 bean 上声明的范围,如果您需要或重用单例或其他基于上下文可用的范围,应用程序上下文可以创建一个新实例。这样您就可以充分利用弹簧功能。

例如

@Service
public class MyHelper 
   @Autowired
   ApplicationContext applicationContext;

   public AnInterface giveMeTheRightImplementation(String key) 

   return context.getBean(key);




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

【讨论】:

以上是关于Spring:如何在运行时更改接口实现的主要内容,如果未能解决你的问题,请参考以下文章

一个类没有接口,如何实现Spring管理

Spring 同一接口注入多个bean实现

企业级spring-boot案例-Spring Boot 启动时的运行方法

企业级spring-boot案例-Spring Boot 启动时的运行方法

企业级spring-boot案例-Spring Boot 启动时的运行方法

企业级spring-boot案例-Spring Boot 启动时的运行方法