【问题标题】:Spring choose bean implementation at runtimeSpring在运行时选择bean实现
【发布时间】:2015-12-18 07:56:30
【问题描述】:

我正在使用带有注释的 Spring Beans,我需要在运行时选择不同的实现。

@Service
public class MyService {
   public void test(){...}
}

比如windows平台需要MyServiceWin extending MyService,linux平台需要MyServiceLnx extending MyService

目前我只知道一个可怕的解决方案:

@Service
public class MyService {

    private MyService impl;

   @PostInit
   public void init(){
        if(windows) impl=new MyServiceWin();
        else impl=new MyServiceLnx();
   }

   public void test(){
        impl.test();
   }
}

请考虑我只使用注释而不是 XML 配置。

【问题讨论】:

  • 如果你所有的类名都不同,@Qualifier 有什么问题?
  • 嗯,如果我没记错的话,限定符不是运行时评估的。
  • 确实如此。您可能应该查看工厂模式。有关详细信息,请参阅下面的答案。

标签: java spring spring-bean


【解决方案1】:

1。实现自定义Condition

public class LinuxCondition implements Condition {
  @Override
  public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    return context.getEnvironment().getProperty("os.name").contains("Linux");  }
}

Windows 也一样。

2。在 Configuration 类中使用 @Conditional

@Configuration
public class MyConfiguration {
   @Bean
   @Conditional(LinuxCondition.class)
   public MyService getMyLinuxService() {
      return new LinuxService();
   }

   @Bean
   @Conditional(WindowsCondition.class)
   public MyService getMyWindowsService() {
      return new WindowsService();
   }
}

3。照常使用@Autowired

@Service
public class SomeOtherServiceUsingMyService {

    @Autowired    
    private MyService impl;

    // ... 
}

【讨论】:

  • 我尝试了@Profile 解决方案,它对我有用。我认为这也可以是一个很好的解决方案,但是使用@Profile 我不需要配置器。
  • @grep 你是对的。我修复了方法名称。请注意,您是基于类型 MyService 使用 Conditional 进行自动装配,因此 bean 定义的方法名称不优先,具有不同方法名称的单一配置应该可以工作。
  • 在这种情况下,如果两个条件都满足,你的自动装配会发生什么?
  • 如果我想在matches中使用一些变量怎么办?
  • 是否有不需要“new”的解决方案,因此您不必指定所有构造函数参数,而是使用依赖注入,例如通过Lombok.
【解决方案2】:

您可以将 bean 注入移动到配置中,如下所示:

@Configuration
public class AppConfig {

    @Bean
    public MyService getMyService() {
        if(windows) return new MyServiceWin();
        else return new MyServiceLnx();
    }
}

或者,您可以使用配置文件windowslinux,然后使用@Profile 注释来注释您的服务实现,例如@Profile("linux")@Profile("windows"),并为您的应用程序提供这些配置文件之一。

【讨论】:

  • 我可以通过 web.xml 设置 Spring 配置文件,但是否可以通过一些代码在 ContextListerner 中更改它?有了这个,我可以根据要求更改配置文件运行时。
  • 自 Spring 4(?) 以来,您可以使用 @Conditional 注释。 @Profile 可以 100% 工作,但 Conditional 应该更适合。
  • @demaniak 如果我正确阅读了 Conditional 的文档,听起来好像是在启动时评估的,而不是运行时?
  • @CharlesWood - 是的,你是对的。但是,通过添加“@RefreshScope”和执行器启动器,可以在运行时重新加载。您当然必须仔细考虑它对正在运行的系统的影响!
  • 为什么还要叫 AppConfig?从技术上讲,这不是工厂模式吗?
【解决方案3】:

让我们创建漂亮的配置。

假设我们有 Animal 接口,并且我们有 DogCat 实现。我们想写写:

@Autowired
Animal animal;

但是我们应该返回哪个实现呢?

那么什么是解决方案?有很多方法可以解决问题。我将编写如何一起使用 @Qualifier 和自定义条件。

首先让我们创建我们的自定义注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.TYPE})
public @interface AnimalType {
    String value() default "";
}

和配置:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class AnimalFactoryConfig {

    @Bean(name = "AnimalBean")
    @AnimalType("Dog")
    @Conditional(AnimalCondition.class)
    public Animal getDog() {
        return new Dog();
    }

    @Bean(name = "AnimalBean")
    @AnimalType("Cat")
    @Conditional(AnimalCondition.class)
    public Animal getCat() {
        return new Cat();
    }

}

注意我们的 bean 名称是 AnimalBean为什么我们需要这个 bean? 因为当我们注入 Animal 接口时,我们将只写 @Qualifier("AnimalBean")

我们还创建了 custom annotation 来将值传递给我们的custom Condition

现在我们的条件看起来像这样(假设“狗”名称来自配置文件或 JVM 参数或......)

   public class AnimalCondition implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        if (annotatedTypeMetadata.isAnnotated(AnimalType.class.getCanonicalName())){
           return annotatedTypeMetadata.getAnnotationAttributes(AnimalType.class.getCanonicalName())
                   .entrySet().stream().anyMatch(f -> f.getValue().equals("Dog"));
        }
        return false;
    }
}

最后是注入:

@Qualifier("AnimalBean")
@Autowired
Animal animal;

【讨论】:

  • 我认为 Qualifier 不应该与 Autowired 混合使用
  • 你为什么不认为它们应该混合在一起?
【解决方案4】:

使用@Qualifier 注释将所有实现自动装配到工厂中,然后从工厂返回您需要的服务类。

public class MyService {
    private void doStuff();
}

我的 Windows 服务:

@Service("myWindowsService")
public class MyWindowsService implements MyService {

    @Override
    private void doStuff() {
        //Windows specific stuff happens here.
    }
}

我的 Mac 服务:

@Service("myMacService")
public class MyMacService implements MyService {

    @Override
    private void doStuff() {
        //Mac specific stuff happens here
    }
}

我的工厂:

@Component
public class MyFactory {
    @Autowired
    @Qualifier("myWindowsService")
    private MyService windowsService;

    @Autowired
    @Qualifier("myMacService")
    private MyService macService;

    public MyService getService(String serviceNeeded){
        //This logic is ugly
        if(serviceNeeded == "Windows"){
            return windowsService;
        } else {
            return macService;
        }
    }
}

如果你想变得非常棘手,你可以使用枚举来存储你的实现类类型,然后使用枚举值来选择你想要返回的实现。

public enum ServiceStore {
    MAC("myMacService", MyMacService.class),
    WINDOWS("myWindowsService", MyWindowsService.class);

    private String serviceName;
    private Class<?> clazz;

    private static final Map<Class<?>, ServiceStore> mapOfClassTypes = new HashMap<Class<?>, ServiceStore>();

    static {
        //This little bit of black magic, basically sets up your 
        //static map and allows you to get an enum value based on a classtype
        ServiceStore[] namesArray = ServiceStore.values();
        for(ServiceStore name : namesArray){
            mapOfClassTypes.put(name.getClassType, name);
        }
    }

    private ServiceStore(String serviceName, Class<?> clazz){
        this.serviceName = serviceName;
        this.clazz = clazz;
    }

    public String getServiceBeanName() {
        return serviceName;
    }

    public static <T> ServiceStore getOrdinalFromValue(Class<?> clazz) {
        return mapOfClassTypes.get(clazz);
    }
}

然后您的工厂可以利用应用程序上下文并将实例拉入它自己的地图中。当您添加一个新的服务类时,只需在枚举中添加另一个条目,这就是您所要做的。

 public class ServiceFactory implements ApplicationContextAware {

     private final Map<String, MyService> myServices = new Hashmap<String, MyService>();

     public MyService getInstance(Class<?> clazz) {
         return myServices.get(ServiceStore.getOrdinalFromValue(clazz).getServiceName());
     }

      public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
          myServices.putAll(applicationContext.getBeansofType(MyService.class));
      }
 }

现在您只需将所需的类类型传递给工厂,它就会为您提供所需的实例。非常有帮助,特别是如果您想使服务通用。

【讨论】:

  • 委托模式的良好应用——理论上。在实践中,它在扩展 MyService 时引入了一些开销——添加的每个方法也应该由 MyFactory 实现(顺便说一句,它不是工厂,而是代理)。这没关系,虽然 MyService 只有一两个方法,但随着 MyService 的增长变得乏味。
【解决方案5】:

MyService.java:

public interface MyService {
  String message();
}

MyServiceConfig.java:

@Configuration
public class MyServiceConfig {

  @Value("${service-type}")
  MyServiceTypes myServiceType;

  @Bean
  public MyService getMyService() {
    if (myServiceType == MyServiceTypes.One) {
      return new MyServiceImp1();
    } else {
      return new MyServiceImp2();
    }
  }
}

application.properties:

service-type=one

MyServiceTypes.java

public enum MyServiceTypes {
  One,
  Two
}

在任何 Bean/组件/服务/等中使用。喜欢:

    @Autowired
    MyService myService;
    ...
    String message = myService.message()

【讨论】:

  • getMyService() 配置方法需要@Primary 以防止有太多实现可供选择。
【解决方案6】:

只需将 @Service 带注释的类设置为有条件的: 就是这样。不需要其他显式的@Bean 方法。

public enum Implementation {
    FOO, BAR
}

@Configuration
public class FooCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Implementation implementation = Implementation.valueOf(context.getEnvironment().getProperty("implementation"));
        return Implementation.FOO == implementation;
    }
}

@Configuration
public class BarCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Implementation implementation = Implementation.valueOf(context.getEnvironment().getProperty("implementation"));
        return Implementation.BAR == implementation;
    }
}

这里发生了魔法。 条件在它所属的地方是正确的:在实现类。

@Conditional(FooCondition.class)
@Service
class MyServiceFooImpl implements MyService {
    // ...
}

@Conditional(BarCondition.class)
@Service
class MyServiceBarImpl implements MyService {
    // ...
}

然后您可以像往常一样使用Dependency Injection,例如通过Lombok@RequiredArgsConstructor@Autowired

@Service
@RequiredArgsConstructor
public class MyApp {
    private final MyService myService;
    // ...
}

把它放在你的 application.yml 中:

implementation: FOO

? 只有用 FooCondition 注释的实现才会被实例化。没有幻像实例化。 ?

【讨论】:

  • Nice & clear 如何为这些编写集成测试?如果我对 Foo 进行了集成测试,如何设置 implementation property = foo.对于实现属性 = bar 的情况也是如此。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多