web-dev-qa-db-fra.com

Spring choisit l'implémentation du bean au moment de l'exécution

J'utilise Spring Beans avec des annotations et je dois choisir une implémentation différente au moment de l'exécution.

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

Par exemple, pour la plate-forme Windows, j'ai besoin de MyServiceWin extending MyService, pour Linux, il me faut MyServiceLnx extending MyService.

Pour l'instant je ne connais qu'une seule solution horrible:

@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();
   }
}

Veuillez considérer que j'utilise uniquement des annotations et non une configuration XML.

37
Tobia

Vous pouvez déplacer l'injection de haricot dans la configuration, comme suit:

@Configuration
public class AppConfig {

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

Vous pouvez également utiliser les profils windows et linux, puis annoter vos implémentations de service avec l'annotation @Profile, Comme @Profile("linux") ou @Profile("windows"), et fournissez l'un de ces profils pour votre application.

14
Stanislav

1. Implémenter une personnalisation Condition

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

Même chose pour Windows.

2. Utilisez @Conditional dans votre classe Configuration

@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. Utilisez @Autowired comme d'habitude

@Service
public class SomeOtherServiceUsingMyService {

    @Autowired    
    private MyService impl;

    // ... 
}
60
nobeh

Créons une belle configuration.

Imaginez que nous ayons une interface Animal et que nous ayons un chien et Cat implémentation. Nous voulons écrire écrire:

@Autowired
Animal animal;

mais quelle implémentation devons-nous retourner?

enter image description here

Alors, quelle est la solution? Il y a plusieurs façons de résoudre le problème. Je vais écrire comment utiliser @ Qualifier et les conditions personnalisées ensemble.

Alors tout d'abord, créons notre annotation personnalisée:

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

et config:

@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();
    }

}

Remarque notre nom de bean est AnimalBean . pourquoi avons-nous besoin de ce haricot? car lorsque nous injectons l'interface Animal, nous n'écrirons que @ Qualifier ("AnimalBean") )

Nous avons également créé une annotation personnalisée pour transmettre la valeur à notre condition personnalisée .

Maintenant, nos conditions ressemblent à ceci (imaginez que le nom "Dog" vienne du fichier de configuration ou du paramètre JVM ou ...)

   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;
    }
}

et enfin l'injection:

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

Autowire toutes vos implémentations dans une usine avec @Qualifier annotations, puis renvoyez la classe de service dont vous avez besoin depuis l’usine.

public class MyService {
    private void doStuff();
}

Mon service Windows:

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

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

Mon service Mac:

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

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

Mon usine:

@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;
        }
    }
}

Si vous voulez devenir vraiment délicat, vous pouvez utiliser une énumération pour stocker vos types de classe d'implémentation, puis utiliser la valeur enum pour choisir l'implémentation que vous voulez renvoyer.

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);
    }
}

Ensuite, votre usine peut accéder au contexte de l'application et extraire des instances dans sa propre carte. Lorsque vous ajoutez une nouvelle classe de service, ajoutez simplement une autre entrée à l'énumération, et c'est tout ce que vous avez à faire.

 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));
      }
 }

Maintenant, vous pouvez simplement passer le type de classe que vous voulez dans l’usine et cela vous fournira l’instance dont vous avez besoin. Très utile surtout si vous voulez rendre les services génériques.

4
JamesENL