Spring Java Config: come si crea un @Bean con ambito prototipo con argomenti di runtime?


134

Utilizzando Spring Config di Spring, ho bisogno di acquisire / creare un'istanza di un bean con ambito prototipo con argomenti di costruzione ottenibili solo in fase di esecuzione. Considera il seguente esempio di codice (semplificato per brevità):

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

dove la classe Thing è definita come segue:

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

Avviso nameè final: può essere fornito solo tramite un costruttore, e garantisce l'immutabilità. Le altre dipendenze sono dipendenze della Thingclasse specifiche dell'implementazione e non dovrebbero essere note all'implementazione del gestore richieste (strettamente accoppiato a).

Questo codice funziona perfettamente con la configurazione Spring XML, ad esempio:

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

Come ottengo la stessa cosa con la configurazione Java? Quanto segue non funziona utilizzando Spring 3.x:

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

Ora potrei creare una fabbrica, ad esempio:

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

Ma questo sconfigge l'intero punto dell'uso di Spring per sostituire il modello di progettazione ServiceLocator e Factory , che sarebbe l'ideale per questo caso d'uso.

Se Spring Java Config potesse fare questo, sarei in grado di evitare:

  • definire un'interfaccia Factory
  • definire un'implementazione in fabbrica
  • scrivere test per l'implementazione in fabbrica

È un sacco di lavoro (relativamente parlando) per qualcosa di così banale che Spring supporta già tramite la configurazione XML.


15
Ottima domanda
Sotirios Delimanolis,

Tuttavia, c'è un motivo per cui non puoi semplicemente istanziare la classe da solo e doverla prendere da Spring? Ha dipendenze da altri bean?
Sotirios Delimanolis,

@SotiriosDelimanolis sì, l' Thingimplementazione è in realtà più complessa e ha dipendenze da altri bean (li ho appena omessi per brevità). Pertanto, non voglio che l'implementazione del gestore richieste ne sia a conoscenza, poiché ciò accoppierebbe strettamente il gestore alle API / bean che non è necessario. Aggiornerò la domanda per riflettere la tua (eccellente) domanda.
Les Hazlewood

Non sono sicuro che Spring lo consenta su un costruttore, ma so che puoi impostare @Qualifierparametri su un setter con @Autowiredsul setter stesso.
CodeChimp

2
Nella primavera 4, il tuo esempio con le @Beanopere. Il @Beanmetodo viene chiamato con gli argomenti appropriati passati getBean(..).
Sotirios Delimanolis,

Risposte:


94

In una @Configurationclasse, un @Beanmetodo simile

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

viene utilizzato per registrare una definizione di bean e fornire la factory per la creazione del bean . Il bean che definisce viene istanziato solo su richiesta usando argomenti determinati direttamente o tramite la sua scansione ApplicationContext.

Nel caso di un prototypebean, ogni volta viene creato un nuovo oggetto e quindi @Beanviene eseguito anche il metodo corrispondente .

È possibile recuperare un bean dal ApplicationContextsuo BeanFactory#getBean(String name, Object... args)metodo che indica

Consente di specificare argomenti espliciti del costruttore / argomenti del metodo factory, sovrascrivendo gli argomenti predefiniti specificati (se presenti) nella definizione del bean.

parametri:

Arg argomenti da usare se si crea un prototipo usando argomenti espliciti in un metodo factory statico. Non è valido utilizzare un valore args non nullo in nessun altro caso.

In altre parole, per questo prototypebean con ambito, si forniscono gli argomenti che verranno utilizzati, non nel costruttore della classe bean, ma nella @Beanchiamata del metodo.

Questo è vero almeno per le versioni Spring 4+.


12
Il mio problema con questo approccio è che non puoi limitare il @Beanmetodo all'invocazione manuale. Se mai @Autowire Thingil @Beanmetodo verrà chiamato probabilmente morire per non essere in grado di iniettare il parametro. Lo stesso se tu @Autowire List<Thing>. L'ho trovato un po 'fragile.
Jan Zyka,

@JanZyka, c'è un modo in cui posso autorizzare Cosa diversa da quella descritta in queste risposte (che sono essenzialmente le stesse se strizzi gli occhi). Più specificamente, se conosco gli argomenti in anticipo (in fase di compilazione / configurazione), c'è un modo per esprimere questi argomenti in alcune annotazioni con cui posso qualificarmi @Autowire?
M. Prokhorov,

52

Con Spring> 4.0 e Java 8 puoi farlo in modo più sicuro:

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

}

Uso:

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

    // ...
}

Quindi ora puoi ottenere il tuo bean in fase di esecuzione. Questo è ovviamente un modello di fabbrica, ma puoi risparmiare un po 'di tempo sulla scrittura di classi specifiche come ThingFactory(tuttavia dovrai scrivere personalizzato @FunctionalInterfaceper passare più di due parametri).


1
Trovo questo approccio molto utile e pulito. Grazie!
Alex Objelean,

1
Che cos'è un tessuto? Capisco il tuo utilizzo .. ma non la terminologia .. non credo di aver sentito parlare del "modello di tessuto"
AnthonyJClink

1
@AnthonyJClink Immagino di aver appena usato, fabricinvece factory, il mio male :)
Roman Golyshev,

1
@AbhijitSarkar oh, capisco. Ma non puoi passare un parametro a Providero a un ObjectFactory, o sbaglio? E nel mio esempio puoi passarci un parametro stringa (o qualsiasi altro parametro)
Roman Golyshev,

2
Se non si desidera (o non è necessario) utilizzare i metodi del ciclo di vita dei bean Spring (che sono diversi per i bean prototipo ...), è possibile saltare @Beane Scopeannotare il Thing thingmetodo. Inoltre questo metodo può essere privato per nascondersi e lasciare solo la fabbrica.
m52509791,

17

Dalla primavera 4.3, c'è un nuovo modo di farlo, che è stato cucito per quel problema.

ObjectProvider : consente semplicemente di aggiungerlo come dipendenza al bean con ambito prototipo "argomentato" e di istanziarlo utilizzando l'argomento.

Ecco un semplice esempio di come usarlo:

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

Questo ovviamente stamperà ciao stringa quando si chiama usePrototype.


15

AGGIORNATO per commento

In primo luogo, non sono sicuro del motivo per cui dici "questo non funziona" per qualcosa che funziona bene nella primavera 3.x. Ho il sospetto che qualcosa debba essere sbagliato nella tua configurazione da qualche parte.

Questo funziona:

- File di configurazione:

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

- File di test da eseguire:

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

Utilizzando Spring 3.2.8 e Java 7, si ottiene questo risultato:

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

Quindi il fagiolo "Singleton" è richiesto due volte. Tuttavia, come ci aspetteremmo, Spring lo crea solo una volta. La seconda volta vede che ha quel bean e restituisce semplicemente l'oggetto esistente. Il costruttore (metodo @Bean) non viene richiamato una seconda volta. In riferimento a ciò, quando il bean "Prototype" viene richiesto due volte dallo stesso oggetto di contesto, vediamo che il riferimento cambia nell'output E che il costruttore (metodo @Bean) viene invocato due volte.

Quindi la domanda è come iniettare un singleton in un prototipo. La classe di configurazione sopra mostra come farlo anche! Dovresti passare tutti questi riferimenti nel costruttore. Ciò consentirà alla classe creata di essere un POJO puro e di rendere immutabili gli oggetti di riferimento contenuti come dovrebbero essere. Quindi il servizio di trasferimento potrebbe assomigliare a:

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

Se scrivi Unit Test, sarai così felice di aver creato le classi senza tutto il @Autowired. Se hai bisogno di componenti autowired, mantieni quelli locali nei file di configurazione java.

Questo chiamerà il metodo seguente in BeanFactory. Nota nella descrizione come questo è destinato al tuo caso d'uso esatto.

/**
 * 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;

3
Grazie per la risposta! Tuttavia, penso che tu abbia frainteso la domanda. La parte più importante della domanda è che un valore di runtime deve essere fornito come argomento del costruttore durante l'acquisizione (istanza) del prototipo.
Les Hazlewood

Ho aggiornato la mia risposta. In realtà sembrava che la gestione del valore di runtime fosse stata eseguita correttamente, quindi ho lasciato fuori quella parte. È esplicitamente supportato, tuttavia, come puoi vedere dagli aggiornamenti e dall'output del programma.
JoeG

0

Puoi ottenere un effetto simile semplicemente usando una classe interna :

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


-1

Risposta in ritardo con un approccio leggermente diverso. Questo è il seguito di questa recente domanda che fa riferimento a questa domanda stessa.

Sì, come detto, è possibile dichiarare il bean prototipo che accetta un parametro in una @Configurationclasse che consente di creare un nuovo bean ad ogni iniezione.
Ciò renderà questa @Configuration classe una fabbrica e per non dare troppe responsabilità a questa fabbrica, ciò non dovrebbe includere altri fagioli.

@Configuration    
public class ServiceFactory {

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

}

Ma puoi anche iniettare quel bean di configurazione per creare Things:

@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
    // ...    
}

È sia sicuro che conciso.


1
Grazie per la risposta, ma questo è un modello anti-primavera. Gli oggetti di configurazione non devono "infiltrarsi" nel codice dell'applicazione: esistono per configurare il grafico dell'oggetto dell'applicazione e l'interfaccia con i costrutti Spring. È simile alle classi XML nei bean delle applicazioni (ovvero un altro meccanismo di configurazione). Cioè, se Spring arrivasse con un altro meccanismo di configurazione, avresti un refactoring del codice dell'applicazione - un chiaro indicatore che viola la separazione delle preoccupazioni. È meglio che Config crei istanze di un'interfaccia Factory / Function e inietti Factory - nessun accoppiamento stretto da configurare.
Les Hazlewood,

1) Concordo pienamente sul fatto che, nel caso generale, gli oggetti di configurazione non debbano perdere come campo. Ma in questo caso specifico, iniettando un oggetto di configurazione che definisce uno e un solo bean per produrre bean prototipo, IHMO ha perfettamente senso: questa classe di configurazione diventa una fabbrica. Dov'è il problema della separazione delle preoccupazioni se lo fa solo? ...
davidxxx il

... 2) Informazioni su "Cioè, se Spring arriva con un altro meccanismo di configurazione", è un argomento sbagliato perché quando decidi di utilizzare un framework nella tua applicazione, accoppi la tua applicazione con quella. Quindi, in ogni caso, dovrai anche riformattare qualsiasi applicazione Spring su cui fare affidamento @Configurationse tale meccanismo è cambiato.
davidxxx,

1
... 3) La risposta che hai accettato si propone di utilizzare BeanFactory#getBean(). Ma questo è molto peggio in termini di accoppiamento poiché è una fabbrica che consente di ottenere / istanziare qualsiasi bean dell'applicazione e non solo quello di cui ha bisogno il bean corrente. Con un tale utilizzo è possibile mescolare le responsabilità della propria classe molto facilmente poiché le dipendenze che può derivare sono illimitate, il che non è in realtà consigliato ma caso eccezionale.
davidxxx,

@ davidxxx - Ho accettato la risposta anni fa, prima che JDK 8 e Spring 4 fossero di fatto. La risposta di Roman sopra è più corretta per i moderni usi della primavera. Per quanto riguarda la tua affermazione "perché quando decidi di utilizzare un framework nella tua applicazione, accoppi la tua applicazione con quella" è abbastanza antitetico ai consigli del team Spring e alle migliori pratiche di Java Config - chiedi a Josh Long o Jeurgen Hoeller se ottieni un possibilità di parlare con loro di persona (ho, e posso assicurarti che consigliano esplicitamente di non accoppiare il codice dell'app a Spring ogni volta che è possibile). Saluti.
Les Hazlewood,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.