web-dev-qa-db-fra.com

Implémentez un modèle d'usine simple avec des annotations Spring 3

Je me demandais comment je pouvais implémenter le modèle d'usine simple avec les annotations Spring 3. J'ai vu dans la documentation que vous pouvez créer des beans qui appellent la classe d'usine et exécutez une méthode d'usine. Je me demandais si cela était possible en utilisant uniquement des annotations.

J'ai un contrôleur qui appelle actuellement

MyService myService = myServiceFactory.getMyService(test);
result = myService.checkStatus();

MyService est une interface avec une méthode appelée checkStatus ().

Ma classe d'usine ressemble à ceci:

@Component
public class MyServiceFactory {

    public static MyService getMyService(String service) {
        MyService myService;

        service = service.toLowerCase();

        if (service.equals("one")) {
            myService = new MyServiceOne();
        } else if (service.equals("two")) {
            myService = new MyServiceTwo();
        } else if (service.equals("three")) {
            myService = new MyServiceThree();
        } else {
            myService = new MyServiceDefault();
        }

        return myService;
    }
}

La classe MyServiceOne ressemble à ceci:

@Autowired
private LocationService locationService;

public boolean checkStatus() {
      //do stuff
}

Lorsque j'exécute ce code, la variable locationService est toujours nulle. Je crois que c'est parce que je crée moi-même les objets à l'intérieur de l'usine et que le câblage automatique n'a pas lieu. Existe-t-il un moyen d'ajouter des annotations pour que cela fonctionne correctement?

Merci

42
blong824

Vous avez raison, en créant manuellement un objet, vous ne laissez pas Spring effectuer le câblage automatique. Pensez également à gérer vos services d'ici le printemps:

@Component
public class MyServiceFactory {

    @Autowired
    private MyServiceOne myServiceOne;

    @Autowired
    private MyServiceTwo myServiceTwo;

    @Autowired
    private MyServiceThree myServiceThree;

    @Autowired
    private MyServiceDefault myServiceDefault;

    public static MyService getMyService(String service) {
        service = service.toLowerCase();

        if (service.equals("one")) {
            return myServiceOne;
        } else if (service.equals("two")) {
            return myServiceTwo;
        } else if (service.equals("three")) {
            return myServiceThree;
        } else {
            return myServiceDefault;
        }
    }
}

Mais je considérerais la conception globale comme plutôt médiocre. Ne serait-il pas préférable d'avoir une implémentation générale de MyService et de passer la chaîne one/two/three comme paramètre supplémentaire à checkStatus() ? Que veux-tu accomplir?

@Component
public class MyServiceAdapter implements MyService {

    @Autowired
    private MyServiceOne myServiceOne;

    @Autowired
    private MyServiceTwo myServiceTwo;

    @Autowired
    private MyServiceThree myServiceThree;

    @Autowired
    private MyServiceDefault myServiceDefault;

    public boolean checkStatus(String service) {
        service = service.toLowerCase();

        if (service.equals("one")) {
            return myServiceOne.checkStatus();
        } else if (service.equals("two")) {
            return myServiceTwo.checkStatus();
        } else if (service.equals("three")) {
            return myServiceThree.checkStatus();
        } else {
            return myServiceDefault.checkStatus();
        }
    }
}

Ceci est toujours mal conçu car l'ajout d'une nouvelle implémentation de MyService nécessite également une modification de MyServiceAdapter (violation SRP). Mais c'est en fait un bon point de départ (indice: carte et modèle de stratégie).

32
Tomasz Nurkiewicz

Ce qui suit a fonctionné pour moi:

L'interface comprend des méthodes logiques et une méthode d'identité supplémentaire:

public interface MyService {
    String getType();
    void checkStatus();
}

Quelques implémentations:

@Component
public class MyServiceOne implements MyService {
    @Override
    public String getType() {
        return "one";
    }

    @Override
    public void checkStatus() {
      // Your code
    }
}

@Component
public class MyServiceTwo implements MyService {
    @Override
    public String getType() {
        return "two";
    }

    @Override
    public void checkStatus() {
      // Your code
    }
}

@Component
public class MyServiceThree implements MyService {
    @Override
    public String getType() {
        return "three";
    }

    @Override
    public void checkStatus() {
      // Your code
    }
}

Et l'usine elle-même comme suit:

@Service
public class MyServiceFactory {

    @Autowired
    private List<MyService> services;

    private static final Map<String, MyService> myServiceCache = new HashMap<>();

    @PostConstruct
    public void initMyServiceCache() {
        for(MyService service : services) {
            myServiceCache.put(service.getType(), service);
        }
    }

    public static MyService getService(String type) {
        MyService service = myServiceCache.get(type);
        if(service == null) throw new RuntimeException("Unknown service type: " + type);
        return service;
    }
}

J'ai trouvé une telle implémentation plus facile, plus propre et beaucoup plus extensible. Ajouter un nouveau MyService est aussi simple que de créer un autre bean Spring implémentant la même interface sans apporter de modifications à d'autres endroits.

63
DruidKuma

Pourquoi ne pas ajouter l'interface FactoryBean à MyServiceFactory (pour dire à Spring que c'est une fabrique), ajouter un registre (service String, instance MyService) puis demander à chacun des services d'appeler:

@Autowired
MyServiceFactory serviceFactory;

@PostConstruct
public void postConstruct() {
    serviceFactory.register(myName, this);
}

De cette façon, vous pouvez séparer chaque fournisseur de services en modules si nécessaire, et Spring récupérera automatiquement tous les fournisseurs de services déployés et disponibles.

9
Jeff Wang

Vous pouvez demander manuellement à Spring de le câbler automatiquement.

Demandez à votre usine d'implémenter ApplicationContextAware. Fournissez ensuite l'implémentation suivante dans votre usine:

@Override
public void setApplicationContext(final ApplicationContext applicationContext {
    this.applicationContext = applicationContext;
}

puis procédez comme suit après avoir créé votre bean:

YourBean bean = new YourBean();
applicationContext.getAutowireCapableBeanFactory().autowireBean(bean);
bean.init(); //If it has an init() method.

Cela permettra de câbler parfaitement votre LocationService.

8
rogermenezes

Vous pouvez également définir de manière déclarative un bean de type ServiceLocatorFactoryBean qui agira comme une classe Factory. il a soutenu par Spring 3.

Une implémentation FactoryBean qui prend une interface qui doit avoir une ou plusieurs méthodes avec les signatures (généralement MyService getService () ou MyService getService (String id)) et crée un proxy dynamique qui implémente cette interface

Voici n exemple d'implémentation du modèle Factory à l'aide de Spring

n exemple plus clair

6
kyiu

Réponse suivante de DruidKuma

Litte refactor de son usine avec constructeur auto-câblé:

@Service
public class MyServiceFactory {

    private static final Map<String, MyService> myServiceCache = new HashMap<>();

    @Autowired
    public MyServiceFactory(List<MyService> services) {
        for(MyService service : services) {
            myServiceCache.put(service.getType(), service);
        }
    }

    public static MyService getService(String type) {
        MyService service = myServiceCache.get(type);
        if(service == null) throw new RuntimeException("Unknown service type: " + type);
        return service;
    }
}
3
Pavel Černý

Je suppose que vous utilisez org.springframework.beans.factory.config.ServiceLocatorFactoryBean. Cela simplifiera beaucoup votre code. Sauf MyServiceAdapter u ne peut créer que l'interface MyServiceAdapter avec la méthode MyService getMyService et avec des alliés pour enregistrer vos classes

Code

bean id="printStrategyFactory" class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean">
        <property name="YourInterface" value="factory.MyServiceAdapter" />
    </bean>

    <alias name="myServiceOne" alias="one" />
    <alias name="myServiceTwo" alias="two" />
2
Mikhail

Vous pouvez instancier "AnnotationConfigApplicationContext" en passant toutes vos classes de service en tant que paramètres.

@Component
public class MyServiceFactory {

    private ApplicationContext applicationContext;

    public MyServiceFactory() {
        applicationContext = new AnnotationConfigApplicationContext(
                MyServiceOne.class,
                MyServiceTwo.class,
                MyServiceThree.class,
                MyServiceDefault.class,
                LocationService.class 
        );
        /* I have added LocationService.class because this component is also autowired */
    }

    public MyService getMyService(String service) {

        if ("one".equalsIgnoreCase(service)) {
            return applicationContext.getBean(MyServiceOne.class);
        } 

        if ("two".equalsIgnoreCase(service)) {
            return applicationContext.getBean(MyServiceTwo.class);
        } 

        if ("three".equalsIgnoreCase(service)) {
            return applicationContext.getBean(MyServiceThree.class);
        } 

        return applicationContext.getBean(MyServiceDefault.class);
    }
}
1
OlivierTerrien

Essaye ça:

public interface MyService {
 //Code
}

@Component("One")
public class MyServiceOne implements MyService {
 //Code
}

@Component("Two")
public class MyServiceTwo implements MyService {
 //Code
}
0
Viren Dholakia

Sur la base d'une solution de Pavel Černý ici nous pouvons faire une implémentation typée universelle de ce modèle. Pour cela, nous devons introduire l'interface NamedService:

    public interface NamedService {
       String name();
    }

et ajoutez une classe abstraite:

public abstract class AbstractFactory<T extends NamedService> {

    private final Map<String, T> map;

    protected AbstractFactory(List<T> list) {
        this.map = list
                .stream()
                .collect(Collectors.toMap(NamedService::name, Function.identity()));
    }

    /**
     * Factory method for getting an appropriate implementation of a service
     * @param name name of service impl.
     * @return concrete service impl.

     */
    public T getInstance(@NonNull final String name) {
        T t = map.get(name);
        if(t == null)
            throw new RuntimeException("Unknown service name: " + name);
        return t;
    }
}

Ensuite, nous créons une usine concrète d'objets spécifiques comme MyService:

 public interface MyService extends NamedService {
           String name();
           void doJob();
 }

@Component
public class MyServiceFactory extends AbstractFactory<MyService> {

    @Autowired
    protected MyServiceFactory(List<MyService> list) {
        super(list);
    }
}

où Liste la liste des implémentations de l'interface MyService au moment de la compilation.

Cette approche fonctionne très bien si vous avez plusieurs usines similaires dans l'application qui produisent des objets par leur nom (si la production d'objets par un nom vous suffit bien sûr). Ici, map fonctionne bien avec String comme clé et contient toutes les implémentations existantes de vos services.

si vous avez une logique différente pour produire des objets, cette logique supplémentaire peut être déplacée vers un autre endroit et fonctionner en combinaison avec ces usines (qui obtiennent des objets par leur nom).

0
Andreas Gelever