it-swarm.com.de

Federbohne dynamisch einspritzen

In einer Java-Spring-Web-App möchte ich Bohnen dynamisch injizieren können. Zum Beispiel habe ich eine Schnittstelle mit 2 verschiedenen Implementierungen:

enter image description here

In meiner App verwende ich eine Eigenschaftendatei, um Injektionen zu konfigurieren:

#Determines the interface type the app uses. Possible values: implA, implB
myinterface.type=implA

Meine Injektionen wurden tatsächlich unter bestimmten Bedingungen unter Berücksichtigung der Eigenschaftswerte in der Eigenschaftendatei geladen. In diesem Fall ist beispielsweise "myinterface.type = implA" immer dann, wenn "MyInterface" injiziert wird, "ImplA" (dies wurde durch Erweitern der Bedingte Annotation erreicht).

Ich möchte das zur Laufzeit - sobald die Eigenschaften geändert wurden, passiert folgendes (ohne Neustart des Servers):

  1. Die richtige Implementierung wird eingespritzt. Zum Beispiel beim Einstellen von myinterface.type=implB ImplB wird immer dann injiziert, wenn MyInterface verwendet wird
  2. Spring Environment sollte mit den neuen Werten aktualisiert und den Bohnen erneut injiziert werden.

Ich dachte daran, meinen Kontext aufzufrischen, aber das schafft Probleme. Ich überlegte, Setter für Injektionszwecke zu verwenden und diese Setter wiederzuverwenden, sobald die Eigenschaften neu konfiguriert wurden. Gibt es eine Arbeitspraxis für eine solche Anforderung?

Irgendwelche Ideen?

[~ # ~] Update [~ # ~]

Wie einige vorgeschlagen haben, kann ich eine Factory/Registry verwenden, die beide Implementierungen (ImplA und ImplB) enthält und die richtige zurückgibt, indem die relevante Eigenschaft abgefragt wird. Wenn ich das tue, habe ich immer noch die zweite Herausforderung - die Umwelt. Zum Beispiel, wenn meine Registrierung so aussieht:

@Service
public class MyRegistry {

private String configurationValue;
private final MyInterface implA;
private final MyInterface implB;

@Inject
public MyRegistry(Environmant env, MyInterface implA, MyInterface ImplB) {
        this.implA = implA;
        this.implB = implB;
        this.configurationValue = env.getProperty("myinterface.type");
}

public MyInterface getMyInterface() {
        switch(configurationValue) {
        case "implA":
                return implA;
        case "implB":
                return implB;
        }
}
}

Sobald sich das Eigentum geändert hat, sollte ich meine Umgebung neu injizieren. irgendwelche Vorschläge dafür?

Ich weiß, dass ich diese env innerhalb der Methode anstelle des Konstruktors abfragen kann, aber dies ist eine Leistungsreduzierung und ich möchte auch an eine ID für das erneute Injizieren der Umgebung denken (wieder, vielleicht mit einer Setter-Injection?).

23
forhas

Ich würde diese Aufgabe so einfach wie möglich halten. Anstatt eine Implementierung der Schnittstelle MyInterface beim Start bedingt zu laden und dann ein Ereignis auszulösen, das das dynamische Laden einer anderen Implementierung derselben Schnittstelle auslöst, würde ich dieses Problem auf eine andere Weise angehen, die viel einfacher zu implementieren ist und pflegen.

Zunächst lade ich einfach alle möglichen Implementierungen:

@Component
public class MyInterfaceImplementationsHolder {

    @Autowired
    private Map<String, MyInterface> implementations;

    public MyInterface get(String impl) {
        return this.implementations.get(impl);
    }
}

Diese Bean ist nur ein Halter für alle Implementierungen der Schnittstelle MyInterface. Hier gibt es keine Magie, nur das übliche Verhalten von Spring Autowiring.

Nun, wo immer Sie eine bestimmte Implementierung von MyInterface einspeisen müssen, können Sie dies mit Hilfe einer Schnittstelle tun:

public interface MyInterfaceReloader {

    void changeImplementation(MyInterface impl);
}

Führen Sie dann für jede Klasse, die bei einer Änderung der Implementierung benachrichtigt werden muss, einfach die Implementierung der Schnittstelle MyInterfaceReloader durch. Zum Beispiel:

@Component
public class SomeBean implements MyInterfaceReloader {

    // Do not autowire
    private MyInterface myInterface;

    @Override
    public void changeImplementation(MyInterface impl) {
        this.myInterface = impl;
    }
}

Schließlich benötigen Sie eine Bean, die die Implementierung in jeder Bean ändert, die MyInterface als Attribut hat:

@Component
public class MyInterfaceImplementationUpdater {

    @Autowired
    private Map<String, MyInterfaceReloader> reloaders;

    @Autowired
    private MyInterfaceImplementationsHolder holder;

    public void updateImplementations(String implBeanName) {
        this.reloaders.forEach((k, v) -> 
            v.changeImplementation(this.holder.get(implBeanName)));
    }
}

Dadurch werden einfach alle Beans automatisch gestartet, die die Schnittstelle MyInterfaceReloader implementieren, und jede von ihnen wird mit der neuen Implementierung aktualisiert, die vom Inhaber abgerufen und als Argument übergeben wird. Auch hier gelten die allgemeinen Regeln für das automatische Verdrahten im Frühjahr.

Wann immer Sie die Implementierung ändern möchten, sollten Sie einfach die Methode updateImplementations mit dem Namen der Bean der neuen Implementierung aufrufen. Dies ist der einfache Name der Klasse in Kleinbuchstaben, dh myImplA oder myImplB für Klassen MyImplA und MyImplB.

Sie sollten diese Methode auch beim Start aufrufen, damit für jedes Bean, das die Schnittstelle MyInterfaceReloader implementiert, eine erste Implementierung festgelegt wird.

Ich habe ein ähnliches Problem mithilfe von org.Apache.commons.configuration.PropertiesConfiguration und org.springframework.beans.factory.config.ServiceLocatorFactoryBean gelöst:

Lassen Sie VehicleRepairService eine Schnittstelle sein:

public interface VehicleRepairService {
    void repair();
}

und CarRepairService und TruckRepairService zwei Klassen, die es implementieren:

public class CarRepairService implements VehicleRepairService {
    @Override
    public void repair() {
        System.out.println("repair a car");
    }
}

public class TruckRepairService implements VehicleRepairService {
    @Override
    public void repair() {
        System.out.println("repair a truck");
    }
}

Ich erstelle eine Schnittstelle für eine Servicefabrik:

public interface VehicleRepairServiceFactory {
    VehicleRepairService getRepairService(String serviceType);
}

Verwenden Sie Config als Konfigurationsklasse:

@Configuration()
@ComponentScan(basePackages = "config.test")
public class Config {
    @Bean 
    public PropertiesConfiguration configuration(){
        try {
            PropertiesConfiguration configuration = new PropertiesConfiguration("example.properties");
            configuration
                    .setReloadingStrategy(new FileChangedReloadingStrategy());
            return configuration;
        } catch (ConfigurationException e) {
            throw new IllegalStateException(e);
        }
    }

    @Bean
    public ServiceLocatorFactoryBean serviceLocatorFactoryBean() {
        ServiceLocatorFactoryBean serviceLocatorFactoryBean = new ServiceLocatorFactoryBean();
        serviceLocatorFactoryBean
                .setServiceLocatorInterface(VehicleRepairServiceFactory.class);
        return serviceLocatorFactoryBean;
    }

    @Bean
    public CarRepairService carRepairService() {
        return new CarRepairService();
    }

    @Bean
    public TruckRepairService truckRepairService() {
        return new TruckRepairService();
    }

    @Bean
    public SomeService someService(){
        return new SomeService();
    }
}

Mit FileChangedReloadingStrategy wird Ihre Konfiguration neu geladen, wenn Sie die Eigenschaftendatei ändern.

service=truckRepairService
#service=carRepairService

Wenn Sie die Konfiguration und das Werk in Ihrem Dienst haben, können Sie den entsprechenden Dienst vom Werk unter Verwendung des aktuellen Werts der Eigenschaft beziehen.

@Service
public class SomeService  {

    @Autowired
    private VehicleRepairServiceFactory factory;

    @Autowired 
    private PropertiesConfiguration configuration;


    public void doSomething() {
        String service = configuration.getString("service");

        VehicleRepairService vehicleRepairService = factory.getRepairService(service);
        vehicleRepairService.repair();
    }
}

Ich hoffe es hilft.

10
eltabo

Wenn ich Sie richtig verstehe, ist das Ziel, injizierte Objektinstanzen nicht zu ersetzen, sondern unterschiedliche Implementierungen während des Aufrufs der Schnittstellenmethode zu verwenden, abhängig von einer bestimmten Bedingung zur Laufzeit.

Wenn dies der Fall ist, können Sie versuchen, den SringTargetSource -Mechanismus in Kombination mit ProxyFactoryBean zu untersuchen. Der Punkt ist, dass Proxy-Objekte in Beans injiziert werden, die Ihre Schnittstelle verwenden, und alle Schnittstellenmethodenaufrufe an TargetSource target gesendet werden.

Nennen wir dies "Polymorpher Proxy".

Schauen Sie sich das folgende Beispiel an:

ConditionalTargetSource.Java

@Component
public class ConditionalTargetSource implements TargetSource {

    @Autowired
    private MyRegistry registry;

    @Override
    public Class<?> getTargetClass() {
        return MyInterface.class;
    }

    @Override
    public boolean isStatic() {
        return false;
    }

    @Override
    public Object getTarget() throws Exception {
        return registry.getMyInterface();
    }

    @Override
    public void releaseTarget(Object target) throws Exception {
        //Do some staff here if you want to release something related to interface instances that was created with MyRegistry.
    }

}

applicationContext.xml

<bean id="myInterfaceFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="MyInterface"/>
    <property name="targetSource" ref="conditionalTargetSource"/>
</bean>
<bean name="conditionalTargetSource" class="ConditionalTargetSource"/>

SomeService.Java

@Service
public class SomeService {

  @Autowired
  private MyInterface myInterfaceBean;

  public void foo(){
      //Here we have `myInterfaceBean` proxy that will do `conditionalTargetSource.getTarget().bar()`
      myInterfaceBean.bar();
  }

}

Auch wenn Sie möchten, dass beide MyInterface -Implementierungen Spring Beans sind und der Spring-Kontext nicht beide Instanzen gleichzeitig enthält, können Sie versuchen, ServiceLocatorFactoryBean mit prototype target beans scope und Conditional annotation auf Zielimplementierungsklassen. Dieser Ansatz kann anstelle von MyRegistry verwendet werden.

S. Möglicherweise kann der Vorgang zum Aktualisieren des Anwendungskontexts auch das tun, was Sie möchten, es kann jedoch auch andere Probleme verursachen, z.

5
Sergey Bespalov

Dies kann eine doppelte oder zumindest sehr ähnliche Frage sein, trotzdem habe ich diese Art von Frage hier beantwortet: Spring Bean Partial Autowire Prototyp Konstruktor

Wenn Sie zur Laufzeit eine andere Bean für eine Abhängigkeit benötigen, müssen Sie einen Prototypbereich verwenden. Anschließend können Sie eine Konfiguration verwenden, um verschiedene Implementierungen der Prototyp-Bean zurückzugeben. Sie müssen sich mit der Logik befassen, für welche Implementierung Sie sich selbst zurückgeben möchten (es können sogar 2 verschiedene Singleton-Beans zurückgegeben werden, es spielt keine Rolle). Angenommen, Sie möchten neue Beans und die Logik für die Rückgabe der Implementierung befindet sich in einem Bean mit dem Namen SomeBeanWithLogic.isSomeBooleanExpression(), dann können Sie eine Konfiguration vornehmen:

@Configuration
public class SpringConfiguration
{

    @Bean
    @Autowired
    @Scope("prototype")
    public MyInterface createBean(SomeBeanWithLogic someBeanWithLogic )
    {
        if (someBeanWithLogic .isSomeBooleanExpression())
        {
            return new ImplA(); // I could be a singleton bean
        }
        else
        {
            return new ImplB();  // I could also be a singleton bean
        }
    }
}

Es sollte niemals notwendig sein, den Kontext neu zu laden. Wenn Sie zum Beispiel möchten, dass sich die Implementierung eines Beans zur Laufzeit ändert, verwenden Sie die obigen Anweisungen. Wenn Sie Ihre Anwendung wirklich neu laden müssen, weil diese Bean in Konstruktoren einer Singleton-Bean oder etwas Seltsamem verwendet wurde, müssen Sie Ihr Design überdenken, und wenn diese Beans wirklich Singleton-Beans sind. Sie sollten den Kontext nicht neu laden, um Singleton-Beans neu zu erstellen, um ein anderes Laufzeitverhalten zu erzielen, das nicht benötigt wird.

Bearbeiten Im ersten Teil dieser Antwort wurde die Frage nach dem dynamischen Injizieren von Bohnen beantwortet. Wie gefragt, aber ich denke, die Frage ist eher eine: "Wie kann ich die Implementierung einer Singleton-Bean zur Laufzeit ändern?". Dies könnte mit einem Proxy-Entwurfsmuster erfolgen.

interface MyInterface 
{
    public String doStuff();
}

@Component
public class Bean implements MyInterface
{
    boolean todo = false; // change me as needed

    // autowire implementations or create instances within this class as needed
    @Qualifier("implA")
    @Autowired
    MyInterface implA;

    @Qualifier("implB")
    @Autowired
    MyInterface implB;

    public String doStuff()
    {
        if (todo)
        {
            return implA.doStuff();
        }
        else
        {
            return implB.doStuff();
        }
    }   
}
4
Snickers3192

Beachten Sie, dass FileChangedReloadingStrategy Ihr Projekt in hohem Maße von den Bereitstellungsbedingungen abhängig macht: Die WAR/EAR-Datei sollte nach Containern aufgelöst werden und Sie sollten direkten Zugriff auf das Dateisystem haben. Diese Bedingungen werden nicht immer in allen Situationen erfüllt und Umgebungen.

1
p3consulting

Sie können Spring @Conditional für einen Eigenschaftswert verwenden. Geben Sie beiden Beans den gleichen Namen und es sollte funktionieren, da nur eine Instanz erstellt wird.

Hier erfahren Sie, wie Sie @Conditional für Services und Komponenten verwenden: http://blog.codeleak.pl/2015/11/how-to-register-components-using.html

0
Dennis Ich