Spring Java Config: Wie erstellt man eine @Bean mit Prototypbereich mit Laufzeitargumenten?

134

Mit der Java-Konfiguration von Spring muss ich eine Bean mit Prototypbereich mit Konstruktorargumenten erwerben / instanziieren, die nur zur Laufzeit verfügbar sind. Betrachten Sie das folgende Codebeispiel (der Kürze halber vereinfacht):

@Autowired
private ApplicationContext appCtx;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = appCtx.getBean(Thing.class, name);

    //System.out.println(thing.getName()); //prints name
}

wobei die Thing-Klasse wie folgt definiert ist:

public class Thing {

    private final String name;

    @Autowired
    private SomeComponent someComponent;

    @Autowired
    private AnotherComponent anotherComponent;

    public Thing(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

Hinweis nameist final: Es kann nur über einen Konstruktor geliefert werden und garantiert Unveränderlichkeit. Die anderen Abhängigkeiten sind implementierungsspezifische Abhängigkeiten der ThingKlasse und sollten nicht bekannt (eng mit der Implementierung des Anforderungshandlers gekoppelt) sein.

Dieser Code funktioniert perfekt mit der Spring XML-Konfiguration, zum Beispiel:

<bean id="thing", class="com.whatever.Thing" scope="prototype">
    <!-- other post-instantiation properties omitted -->
</bean>

Wie erreiche ich dasselbe mit der Java-Konfiguration? Folgendes funktioniert mit Spring 3.x nicht:

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

Jetzt könnte ich eine Fabrik erstellen, zB:

public interface ThingFactory {
    public Thing createThing(String name);
}

Dies macht jedoch die Verwendung von Spring zum Ersetzen des ServiceLocator- und Factory-Entwurfsmusters zunichte , was für diesen Anwendungsfall ideal wäre.

Wenn Spring Java Config dies könnte, könnte ich Folgendes vermeiden:

  • Definieren einer Factory-Schnittstelle
  • Definieren einer Factory-Implementierung
  • Schreiben von Tests für die Factory-Implementierung

Das ist eine Menge Arbeit (relativ gesehen) für etwas so Triviales, dass Spring es bereits über die XML-Konfiguration unterstützt.

Les Hazlewood
quelle
15
Ausgezeichnete Frage.
Sotirios Delimanolis
Gibt es jedoch einen Grund, warum Sie die Klasse nicht einfach selbst instanziieren können und sie aus dem Frühling beziehen müssen? Hat es Abhängigkeiten von anderen Beans?
Sotirios Delimanolis
@SotiriosDelimanolis Ja, die ThingImplementierung ist tatsächlich komplexer und hat Abhängigkeiten von anderen Beans (ich habe sie der Kürze halber einfach weggelassen). Daher möchte ich nicht, dass die Implementierung des Anforderungshandlers davon erfährt, da dies den Handler eng mit APIs / Beans koppeln würde, die er nicht benötigt. Ich werde die Frage aktualisieren, um Ihre (ausgezeichnete) Frage widerzuspiegeln.
Les Hazlewood
Ich bin nicht sicher, ob Spring dies für einen Konstruktor zulässt, aber ich weiß, dass Sie @Qualifiereinem Setter mit @Autowireddem Setter selbst Parameter zuweisen können.
CodeChimp
2
In Spring 4 @Beanfunktioniert Ihr Beispiel mit . Die @BeanMethode wird mit den entsprechenden Argumenten aufgerufen, an die Sie übergeben haben getBean(..).
Sotirios Delimanolis

Antworten:

94

In einer @ConfigurationKlasse eine @Beansolche Methode

@Bean
@Scope("prototype")
public Thing thing(String name) {
    return new Thing(name);
}

wird verwendet, um eine Bean-Definition zu registrieren und die Factory zum Erstellen der Bean bereitzustellen . Die von ihr definierte Bean wird nur auf Anfrage mithilfe von Argumenten instanziiert, die entweder direkt oder durch Scannen dieser Argumente ermittelt werden ApplicationContext.

Bei einer prototypeBean wird jedes Mal ein neues Objekt erstellt und daher auch die entsprechende @BeanMethode ausgeführt.

Sie können eine Bean ApplicationContextüber die BeanFactory#getBean(String name, Object... args)Methode abrufen , die angibt

Ermöglicht die Angabe expliziter Konstruktorargumente / Factory-Methodenargumente, wobei die angegebenen Standardargumente (falls vorhanden) in der Bean-Definition überschrieben werden.

Parameter:

args Argumente , wenn die Erstellung eines Prototyps mit expliziten Argumente auf eine statische Factory - Methode zu verwenden. In jedem anderen Fall ist es ungültig, einen Argumentwert ungleich Null zu verwenden.

Mit anderen Worten, für diese prototypeBean mit Gültigkeitsbereich geben Sie die Argumente an, die nicht im Konstruktor der Bean-Klasse, sondern im @BeanMethodenaufruf verwendet werden.

Dies gilt zumindest für die Spring-Versionen 4+.

Sotirios Delimanolis
quelle
12
Mein Problem bei diesem Ansatz ist, dass Sie die @BeanMethode nicht auf den manuellen Aufruf beschränken können. Wenn Sie jemals @Autowire Thingdie @BeanMethode als wahrscheinlich sterbend bezeichnen, wenn Sie den Parameter nicht injizieren können. Das Gleiche gilt, wenn Sie @Autowire List<Thing>. Ich fand das etwas zerbrechlich.
Jan Zyka
@ JanZyka, gibt es eine andere Möglichkeit, Dinge automatisch zu verdrahten, als in diesen Antworten beschrieben (die alle im Wesentlichen gleich sind, wenn Sie blinzeln). Wenn ich Argumente im Voraus kenne (zum Zeitpunkt der Kompilierung / Konfiguration), gibt es eine Möglichkeit, diese Argumente in einer Anmerkung auszudrücken, mit der ich sie qualifizieren kann @Autowire?
M. Prokhorov
51

Mit Spring> 4.0 und Java 8 können Sie dies typsicherer machen:

@Configuration    
public class ServiceConfig {

    @Bean
    public Function<String, Thing> thingFactory() {
        return name -> thing(name); // or this::thing
    } 

    @Bean
    @Scope(value = "prototype")
    public Thing thing(String name) {
       return new Thing(name);
    }

}

Verwendung:

@Autowired
private Function<String, Thing> thingFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = thingFactory.apply(name);

    // ...
}

Jetzt können Sie Ihre Bean zur Laufzeit erhalten. Dies ist natürlich ein Factory-Muster, aber Sie können beim Schreiben bestimmter Klassen wie z. B. Zeit sparen ThingFactory(Sie müssen jedoch benutzerdefiniert schreiben @FunctionalInterface, um mehr als zwei Parameter zu übergeben).

Roman Golyshev
quelle
1
Ich finde diesen Ansatz sehr nützlich und sauber. Vielen Dank!
Alex Objelean
1
Was ist ein Stoff? Ich verstehe Ihre Verwendung .. aber nicht die Terminologie .. glaube nicht, dass ich von dem "Stoffmuster" gehört habe
AnthonyJClink
1
@AnthonyJClink Ich denke, ich habe gerade fabricstatt factorymeiner schlechten verwendet :)
Roman Golyshev
1
@ AbhijitSarkar oh, ich verstehe. Aber Sie können einen Parameter nicht an a Provideroder an a übergeben ObjectFactory, oder irre ich mich? Und in meinem Beispiel können Sie ihm einen String-Parameter (oder einen beliebigen Parameter) übergeben
Roman Golyshev
2
Wenn Sie keine Spring-Bean-Lebenszyklusmethoden verwenden möchten (oder müssen) (die sich für Prototyp-Beans unterscheiden ...), können Sie die Methode überspringen @Beanund mit ScopeAnmerkungen versehen Thing thing. Darüber hinaus kann diese Methode privat gemacht werden, um sich zu verstecken und nur die Fabrik zu verlassen.
m52509791
17

Seit Frühjahr 4.3 gibt es eine neue Möglichkeit, die für dieses Problem genäht wurde.

ObjectProvider - Hiermit können Sie es einfach als Abhängigkeit zu Ihrer "argumentierten" Prototype-Scoped-Bean hinzufügen und mithilfe des Arguments instanziieren.

Hier ist ein einfaches Beispiel für die Verwendung:

@Configuration
public class MyConf {
    @Bean
    @Scope(BeanDefinition.SCOPE_PROTOTYPE)
    public MyPrototype createPrototype(String arg) {
        return new MyPrototype(arg);
    }
}

public class MyPrototype {
    private String arg;

    public MyPrototype(String arg) {
        this.arg = arg;
    }

    public void action() {
        System.out.println(arg);
    }
}


@Component
public class UsingMyPrototype {
    private ObjectProvider<MyPrototype> myPrototypeProvider;

    @Autowired
    public UsingMyPrototype(ObjectProvider<MyPrototype> myPrototypeProvider) {
        this.myPrototypeProvider = myPrototypeProvider;
    }

    public void usePrototype() {
        final MyPrototype myPrototype = myPrototypeProvider.getObject("hello");
        myPrototype.action();
    }
}

Dies wird natürlich Hallo-Zeichenfolge drucken, wenn usePrototype aufgerufen wird.

David Barda
quelle
15

AKTUALISIERT pro Kommentar

Erstens bin ich mir nicht sicher, warum Sie sagen, dass "das nicht funktioniert" für etwas, das in Spring 3.x einwandfrei funktioniert. Ich vermute, dass irgendwo in Ihrer Konfiguration etwas nicht stimmt.

Das funktioniert:

-- Konfigurationsdatei:

@Configuration
public class ServiceConfig {
    // only here to demo execution order
    private int count = 1;

    @Bean
    @Scope(value = "prototype")
    public TransferService myFirstService(String param) {
       System.out.println("value of count:" + count++);
       return new TransferServiceImpl(aSingletonBean(), param);
    }

    @Bean
    public AccountRepository aSingletonBean() {
        System.out.println("value of count:" + count++);
        return new InMemoryAccountRepository();
    }
}

- Auszuführende Testdatei:

@Test
public void prototypeTest() {
    // create the spring container using the ServiceConfig @Configuration class
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
    Object singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    singleton = ctx.getBean("aSingletonBean");
    System.out.println(singleton.toString());
    TransferService transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter One");
    System.out.println(transferService.toString());
    transferService = ctx.getBean("myFirstService", "simulated Dynamic Parameter Two");
    System.out.println(transferService.toString());
}

Wenn Sie Spring 3.2.8 und Java 7 verwenden, erhalten Sie folgende Ausgabe:

value of count:1
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
com.spring3demo.account.repository.InMemoryAccountRepository@4da8692d
value of count:2
Using name value of: simulated Dynamic Parameter One
com.spring3demo.account.service.TransferServiceImpl@634d6f2c
value of count:3
Using name value of: simulated Dynamic Parameter Two
com.spring3demo.account.service.TransferServiceImpl@70bde4a2

Die 'Singleton'-Bohne wird also zweimal angefordert. Wie zu erwarten, erstellt Spring es jedoch nur einmal. Das zweite Mal sieht es, dass es diese Bean hat und gibt nur das vorhandene Objekt zurück. Der Konstruktor (@ Bean-Methode) wird kein zweites Mal aufgerufen. Wenn die 'Prototype'-Bean zweimal von demselben Kontextobjekt angefordert wird, sehen wir, dass sich die Referenz in der Ausgabe ändert UND dass der Konstruktor (@ Bean-Methode) zweimal aufgerufen wird.

Die Frage ist also, wie ein Singleton in einen Prototyp eingefügt werden kann. Die Konfigurationsklasse oben zeigt, wie das auch geht! Sie sollten alle diese Referenzen an den Konstruktor übergeben. Auf diese Weise kann die erstellte Klasse ein reines POJO sein und die enthaltenen Referenzobjekte unveränderlich machen, wie sie sein sollten. Der Transferservice könnte also ungefähr so ​​aussehen:

public class TransferServiceImpl implements TransferService {

    private final String name;

    private final AccountRepository accountRepository;

    public TransferServiceImpl(AccountRepository accountRepository, String name) {
        this.name = name;
        // system out here is only because this is a dumb test usage
        System.out.println("Using name value of: " + this.name);

        this.accountRepository = accountRepository;
    }
    ....
}

Wenn Sie Unit Tests schreiben, werden Sie sehr froh sein, dass Sie die Klassen ohne @Autowired erstellt haben. Wenn Sie automatisch verdrahtete Komponenten benötigen, behalten Sie diese lokal in den Java-Konfigurationsdateien.

Dadurch wird die folgende Methode in der BeanFactory aufgerufen. Beachten Sie in der Beschreibung, wie dies für Ihren genauen Anwendungsfall vorgesehen ist.

/**
 * Return an instance, which may be shared or independent, of the specified bean.
 * <p>Allows for specifying explicit constructor arguments / factory method arguments,
 * overriding the specified default arguments (if any) in the bean definition.
 * @param name the name of the bean to retrieve
 * @param args arguments to use if creating a prototype using explicit arguments to a
 * static factory method. It is invalid to use a non-null args value in any other case.
 * @return an instance of the bean
 * @throws NoSuchBeanDefinitionException if there is no such bean definition
 * @throws BeanDefinitionStoreException if arguments have been given but
 * the affected bean isn't a prototype
 * @throws BeansException if the bean could not be created
 * @since 2.5
 */
Object getBean(String name, Object... args) throws BeansException;
JoeG
quelle
3
Danke für die Antwort! Ich denke jedoch, dass Sie die Frage falsch verstanden haben. Der wichtigste Teil der Frage ist, dass beim Erfassen (Instanziieren) des Prototyps ein Laufzeitwert als Konstruktorargument angegeben werden muss.
Les Hazlewood
Ich habe meine Antwort aktualisiert. Eigentlich schien es so, als ob der Umgang mit dem Laufzeitwert korrekt war, also habe ich diesen Teil weggelassen. Es wird jedoch explizit unterstützt, wie Sie aus den Updates und der Ausgabe des Programms ersehen können.
JoeG
0

Sie können einen ähnlichen Effekt erzielen, indem Sie eine innere Klasse verwenden :

@Component
class ThingFactory {
    private final SomeBean someBean;

    ThingFactory(SomeBean someBean) {
        this.someBean = someBean;
    }

    Thing getInstance(String name) {
        return new Thing(name);
    }

    class Thing {
        private final String name;

        Thing(String name) {
            this.name = name;
        }

        void foo() {
            System.out.format("My name is %s and I can " +
                    "access bean from outer class %s", name, someBean);
        }
    }
}
pmartycz
quelle
0

Verwenden Sie in Ihrer Beans-XML-Datei das Attribut scope = "prototype".

manpreet singh
quelle
-1

Späte Antwort mit einem etwas anderen Ansatz. Dies ist eine Fortsetzung dieser jüngsten Frage , die sich auf diese Frage selbst bezieht.

Ja, wie gesagt, Sie können die Prototyp-Bean deklarieren, die einen Parameter in einer @ConfigurationKlasse akzeptiert , mit der bei jeder Injektion eine neue Bean erstellt werden kann.
Das macht diese @Configuration Klasse zu einer Fabrik und um dieser Fabrik nicht zu viel Verantwortung zu übertragen, sollte dies keine anderen Bohnen einschließen.

@Configuration    
public class ServiceFactory {

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public Thing thing(String name) {
       return new Thing(name);
   }

}

Sie können diese Konfigurations-Bean aber auch injizieren, um Things zu erstellen :

@Autowired
private ServiceFactory serviceFactory;

public void onRequest(Request request) {
    //request is already validated
    String name = request.getParameter("name");
    Thing thing = serviceFactory.thing(name); // create a new bean at each invocation
    // ...    
}

Es ist sowohl typsicher als auch prägnant.

davidxxx
quelle
1
Vielen Dank für die Antwort, aber dies ist ein Frühlings-Anti-Muster. Konfigurationsobjekte sollten nicht in Anwendungscode "lecken" - sie sind vorhanden, um Ihr Anwendungsobjektdiagramm und die Schnittstelle mit Spring-Konstrukten zu konfigurieren. Dies ähnelt XML-Klassen in Ihren Application Beans (dh einem anderen Konfigurationsmechanismus). Das heißt, wenn Spring mit einem anderen Konfigurationsmechanismus geliefert wird, müssen Sie Ihren Anwendungscode umgestalten - ein klarer Indikator, der die Trennung von Bedenken verletzt. Es ist besser, wenn Ihre Konfiguration Instanzen einer Factory / Function-Schnittstelle erstellt und die Factory einfügt - keine enge Kopplung an die Konfiguration.
Les Hazlewood
1) Ich stimme voll und ganz zu, dass Konfigurationsobjekte im allgemeinen Fall nicht als Feld auslaufen müssen. In diesem speziellen Fall ist es jedoch völlig sinnvoll, ein Konfigurationsobjekt zu injizieren, das eine und nur eine Bean definiert, um Prototyp-Beans zu erzeugen. IHMO: Diese Konfigurationsklasse wird zur Factory. Wo ist die Frage der Trennung von Bedenken, wenn es nur darum geht? ...
Davidxxx
... 2) Über "Das heißt, wenn Spring mit einem anderen Konfigurationsmechanismus geliefert wird" ist dies ein falsches Argument, da Sie Ihre Anwendung damit koppeln, wenn Sie sich für die Verwendung eines Frameworks in Ihrer Anwendung entscheiden. In jedem Fall müssen Sie auch alle Spring-Anwendungen umgestalten, die darauf angewiesen sind, @Configurationwenn sich dieser Mechanismus ändert.
Davidxxx
1
... 3) Die Antwort, die Sie akzeptiert haben, schlägt vor, sie zu verwenden BeanFactory#getBean(). In Bezug auf die Kopplung ist dies jedoch weitaus schlimmer, da dies eine Fabrik ist, die es ermöglicht, alle Beans der Anwendung abzurufen / zu instanziieren und nicht nur die, die die aktuelle Bean benötigt. Mit einer solchen Verwendung können Sie die Verantwortlichkeiten Ihrer Klasse sehr einfach mischen, da die Abhängigkeiten, die sie ziehen kann, unbegrenzt sind, was wirklich nicht empfohlen wird, aber ein Ausnahmefall.
Davidxxx
@ davidxxx - Ich habe die Antwort vor Jahren akzeptiert, bevor JDK 8 und Spring 4 de facto waren. Die Antwort von Roman oben ist korrekter für moderne Frühlingsverwendungen. In Bezug auf Ihre Aussage "Denn wenn Sie sich entscheiden, ein Framework in Ihrer Anwendung zu verwenden, koppeln Sie Ihre Anwendung damit" widerspricht dies den Empfehlungen des Spring-Teams und den Best Practices für Java Config - fragen Sie Josh Long oder Jeurgen Hoeller, ob Sie ein Framework erhalten Gelegenheit, persönlich mit ihnen zu sprechen (ich habe und ich kann Ihnen versichern, dass sie ausdrücklich davon abraten, Ihren App-Code nach Möglichkeit nicht mit Spring zu koppeln). Prost.
Les Hazlewood