Spring Java Config: bagaimana Anda membuat @Bean dengan prototipe-lingkup dengan argumen runtime?

134

Menggunakan Spring's Java Config, saya perlu mendapatkan / membuat contoh prototipe-scoped bean dengan argumen konstruktor yang hanya dapat diperoleh saat runtime. Pertimbangkan contoh kode berikut (disederhanakan untuk singkatnya):

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

di mana kelas Thing didefinisikan sebagai berikut:

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

Perhatikan nameadalah final: itu hanya dapat diberikan melalui konstruktor, dan menjamin keabadian. Ketergantungan lain adalah dependensi implementasi-spesifik dari Thingkelas, dan tidak boleh diketahui (digabungkan erat dengan) implementasi penangan permintaan.

Kode ini berfungsi dengan baik dengan konfigurasi Spring XML, misalnya:

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

Bagaimana cara mencapai hal yang sama dengan konfigurasi Java? Berikut ini tidak berfungsi menggunakan Spring 3.x:

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

Sekarang, saya dapat membuat Pabrik, misalnya:

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

Tapi itu mengalahkan seluruh titik menggunakan Spring untuk mengganti pola desain ServiceLocator dan Factory , yang akan ideal untuk use case ini.

Jika Spring Java Config dapat melakukan ini, saya akan dapat menghindari:

  • mendefinisikan antarmuka pabrik
  • mendefinisikan implementasi Pabrik
  • tes tertulis untuk implementasi Pabrik

Itu satu ton pekerjaan (relatif berbicara) untuk sesuatu yang sangat sepele bahwa Spring sudah mendukung melalui konfigurasi XML.

Les Hazlewood
sumber
15
Pertanyaan yang sangat bagus
Sotirios Delimanolis
Namun, adakah alasan Anda tidak bisa hanya membuat kelas sendiri dan harus mendapatkannya dari Spring? Apakah ada ketergantungan pada kacang lain?
Sotirios Delimanolis
@ SotiriosDelimanolis ya, Thingimplementasi sebenarnya lebih kompleks dan memang memiliki ketergantungan pada kacang lainnya (saya hanya menghilangkannya untuk singkatnya). Karena itu, saya tidak ingin implementasi Request handler tahu tentang mereka, karena ini akan secara ketat memasangkan handler ke API / kacang yang tidak perlu. Saya akan memperbarui pertanyaan untuk mencerminkan pertanyaan Anda (luar biasa).
Les Hazlewood
Saya tidak yakin apakah Spring mengizinkan ini pada konstruktor, tapi saya tahu Anda bisa memakai @Qualifierparameter ke setter dengan @Autowiredsetter itu sendiri.
CodeChimp
2
Di Spring 4, contoh Anda dengan @Beankarya. The @BeanMetode dipanggil dengan argumen yang tepat Anda dilewatkan ke getBean(..).
Sotirios Delimanolis

Jawaban:

94

Di @Configurationkelas, @Beanmetode seperti itu

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

digunakan untuk mendaftarkan definisi kacang dan menyediakan pabrik untuk membuat kacang . Kacang yang didefinisikan hanya dipakai berdasarkan permintaan menggunakan argumen yang ditentukan secara langsung atau melalui pemindaian itu ApplicationContext.

Dalam kasus prototypekacang, objek baru dibuat setiap waktu dan karenanya @Beanmetode yang sesuai juga dieksekusi.

Anda dapat mengambil kacang dari ApplicationContextmelalui BeanFactory#getBean(String name, Object... args)metodenya yang menyatakan

Memungkinkan untuk menentukan argumen konstruktor eksplisit / argumen metode pabrik, mengesampingkan argumen default yang ditentukan (jika ada) dalam definisi bean.

Parameter:

args argumen untuk digunakan jika membuat prototipe menggunakan argumen eksplisit ke metode pabrik statis. Tidak valid untuk menggunakan nilai argumen non-null dalam kasus lain apa pun.

Dengan kata lain, untuk prototypekacang yang dicakup ini , Anda memberikan argumen yang akan digunakan, bukan dalam konstruktor dari kelas kacang, tetapi dalam @Beandoa metode.

Setidaknya ini berlaku untuk versi Spring 4+.

Sotirios Delimanolis
sumber
12
Masalah saya dengan pendekatan ini adalah bahwa Anda tidak dapat membatasi @Beanmetode untuk doa manual. Jika Anda pernah @Autowire Thingmenggunakan @Beanmetode ini kemungkinan akan mati karena tidak dapat menyuntikkan parameter. Sama jika Anda @Autowire List<Thing>. Saya menemukan ini agak rapuh.
Jan Zyka
@ Janzyka, apakah ada cara saya bisa autowire Hal selain yang diuraikan dalam jawaban ini (yang pada dasarnya semua sama jika Anda menyipitkan mata). Lebih khusus lagi, jika saya tahu argumen di muka (pada waktu kompilasi / konfigurasi), apakah ada cara saya dapat mengekspresikan argumen ini dalam beberapa anotasi yang saya dapat memenuhi syarat @Autowiredengan?
M. Prokhorov
52

Dengan Spring> 4.0 dan Java 8 Anda dapat melakukan ini dengan lebih aman:

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

}

Pemakaian:

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

    // ...
}

Jadi sekarang Anda bisa mendapatkan kacang Anda saat runtime. Ini adalah pola pabrik tentu saja, tetapi Anda dapat menghemat waktu untuk menulis kelas khusus seperti ThingFactory(namun Anda harus menulis kustom @FunctionalInterfaceuntuk melewati lebih dari dua parameter).

Roman Golyshev
sumber
1
Saya menemukan pendekatan ini sangat bermanfaat dan bersih. Terima kasih!
Alex Objelean
1
Apa itu kain? Saya mengerti penggunaan Anda .. tetapi bukan terminologi .. jangan berpikir saya pernah mendengar tentang "pola kain"
AnthonyJClink
1
@AnthonyJClink Saya kira saya hanya menggunakan fabricbukan factory, buruk saya :)
Roman Golyshev
1
@AbhijitSarkar oh, begitu. Tetapi Anda tidak dapat meneruskan parameter ke a Provideratau ke ObjectFactory, atau apakah saya salah? Dan dalam contoh saya, Anda dapat melewatkan parameter string ke sana (atau parameter apa pun)
Roman Golyshev
2
Jika Anda tidak ingin (atau tidak perlu) menggunakan metode siklus hidup kacang Spring (yang berbeda untuk kacang prototipe ...) Anda dapat melewati @Beandan Scopepenjelasan atas Thing thingmetode. Selain itu metode ini dapat dibuat pribadi untuk menyembunyikan dirinya sendiri dan hanya menyisakan pabrik.
m52509791
17

Sejak Musim Semi 4.3, ada cara baru untuk melakukannya, yang dijahit untuk masalah itu.

ObjectProvider - Ini memungkinkan Anda untuk menambahkannya sebagai ketergantungan pada kacang yang dicakup Prototipe "yang diperdebatkan" Anda dan untuk instantiate menggunakan argumen.

Berikut ini adalah contoh sederhana cara menggunakannya:

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

Ini tentu saja akan mencetak halo string ketika memanggil usePrototype.

David Barda
sumber
15

DIPERBARUI per komentar

Pertama, saya tidak yakin mengapa Anda mengatakan "ini tidak berhasil" untuk sesuatu yang berfungsi dengan baik di Spring 3.x. Saya menduga ada sesuatu yang salah dalam konfigurasi Anda di suatu tempat.

Ini bekerja:

- File Konfigurasi:

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

- Uji File untuk mengeksekusi:

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

Menggunakan Spring 3.2.8 dan Java 7, memberikan output ini:

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

Jadi 'Singleton' Bean diminta dua kali. Namun seperti yang kita harapkan, Spring hanya menciptakannya sekali. Kali kedua ia melihat bahwa ia memiliki kacang itu dan hanya mengembalikan objek yang ada. Konstruktor (metode @Bean) tidak dipanggil untuk kedua kalinya. Sehubungan dengan ini, ketika 'Prototipe' Bean diminta dari objek konteks yang sama dua kali kita melihat bahwa perubahan referensi dalam output DAN bahwa konstruktor (metode @Bean) IS dipanggil dua kali.

Jadi pertanyaannya adalah bagaimana menyuntikkan singleton ke dalam prototipe. Kelas konfigurasi di atas menunjukkan cara melakukannya juga! Anda harus meneruskan semua referensi tersebut ke konstruktor. Ini akan memungkinkan kelas yang dibuat menjadi POJO murni serta membuat objek referensi yang ada tidak berubah sebagaimana mestinya. Jadi layanan transfer mungkin terlihat seperti:

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

Jika Anda menulis Tes Unit Anda akan sangat senang Anda membuat kelas ini tanpa semua @Autowired. Jika Anda membutuhkan komponen autowired, simpan komponen lokal itu ke file konfigurasi java.

Ini akan memanggil metode di bawah ini di BeanFactory. Perhatikan dalam deskripsi bagaimana ini dimaksudkan untuk kasus penggunaan yang tepat.

/**
 * 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
sumber
3
Terima kasih balasannya! Namun, saya pikir Anda salah memahami pertanyaan itu. Bagian terpenting dari pertanyaan adalah bahwa nilai runtime harus diberikan sebagai argumen konstruktor ketika memperoleh (membuat contoh) prototipe.
Les Hazlewood
Saya memperbarui balasan saya. Sebenarnya sepertinya menangani nilai runtime dilakukan dengan benar sehingga saya tidak meninggalkan bagian itu. Meskipun secara eksplisit didukung seperti yang Anda lihat dari pembaruan dan keluaran dari program.
JoeG
0

Anda dapat mencapai efek serupa hanya dengan menggunakan kelas batin :

@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
sumber
0

dalam file xml kacang Anda gunakan atribut scope = "prototype"

sombong sopan
sumber
-1

Terlambat menjawab dengan pendekatan yang sedikit berbeda. Itu adalah tindak lanjut dari pertanyaan terbaru ini yang merujuk pertanyaan ini sendiri.

Ya, seperti yang dikatakan Anda dapat mendeklarasikan kacang prototipe yang menerima parameter di @Configurationkelas yang memungkinkan untuk membuat kacang baru di setiap injeksi.
Itu akan membuat @Configuration kelas ini menjadi pabrik dan untuk tidak memberikan pabrik ini terlalu banyak tanggung jawab, ini tidak termasuk kacang lainnya.

@Configuration    
public class ServiceFactory {

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

}

Tetapi Anda juga dapat menyuntikkan kacang konfigurasi itu untuk membuat 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
    // ...    
}

Jenisnya aman dan ringkas.

davidxxx
sumber
1
Terima kasih atas jawabannya, tetapi ini adalah pola anti Spring. Objek konfigurasi tidak boleh 'bocor' ke dalam kode aplikasi - mereka ada untuk mengonfigurasi grafik objek dan antarmuka aplikasi Anda dengan konstruksi Spring. Ini mirip dengan kelas XML dalam kacang aplikasi Anda (yaitu mekanisme konfigurasi lain). Yaitu, Jika Spring datang bersama mekanisme konfigurasi lain, Anda harus memperbaiki kode aplikasi Anda - indikator yang jelas ini melanggar pemisahan masalah. Lebih baik untuk membuat Config Anda membuat instance dari antarmuka Factory / Function dan menyuntikkan Factory - tidak ada kopling ketat untuk mengkonfigurasi.
Les Hazlewood
1) Saya sepenuhnya setuju bahwa dalam kasus umum, objek konfigurasi tidak bocor sebagai bidang. Tetapi dalam kasus khusus ini, menyuntikkan objek konfigurasi yang mendefinisikan satu dan hanya satu kacang untuk menghasilkan kacang prototipe, IHMO sangat masuk akal: kelas konfigurasi ini menjadi pabrik. Di mana pemisahan masalah menjadi masalah jika hanya itu yang terjadi? ...
davidxxx
... 2) Tentang "Yaitu, Jika Spring disertai dengan mekanisme konfigurasi lain,", itu adalah argumen yang salah karena ketika Anda memutuskan untuk menggunakan kerangka kerja dalam aplikasi Anda, Anda memasangkan aplikasi Anda dengan itu. Jadi dalam hal apa pun, Anda juga harus melakukan refactor pada aplikasi Spring yang bergantung @Configurationjika mekanisme itu berubah.
davidxxx
1
... 3) Jawaban yang Anda terima mengusulkan untuk digunakan BeanFactory#getBean(). Tapi itu jauh lebih buruk dalam hal penggandaan karena itu adalah pabrik yang memungkinkan untuk mendapatkan / membuat instance kacang aplikasi dan tidak hanya yang mana yang dibutuhkan kacang saat ini. Dengan penggunaan seperti itu Anda dapat mencampur tanggung jawab kelas Anda dengan sangat mudah karena dependensi yang mungkin ditarik tidak terbatas, yang sebenarnya tidak disarankan tetapi merupakan kasus luar biasa.
davidxxx
@ davidxxx - Saya menerima jawaban bertahun-tahun yang lalu, sebelum JDK 8 dan Spring 4 adalah de-facto. Jawaban Roman di atas lebih tepat untuk penggunaan Musim Semi modern. Berkenaan dengan pernyataan Anda "karena ketika Anda memutuskan untuk menggunakan kerangka kerja dalam aplikasi Anda, Anda memasangkan aplikasi Anda dengan itu" sangat bertentangan dengan rekomendasi tim Spring dan praktik terbaik Java Config - tanyakan pada Josh Long atau Jeurgen Hoeller jika Anda mendapatkan kesempatan untuk berbicara secara langsung dengan mereka (saya punya, dan saya dapat meyakinkan Anda bahwa mereka secara eksplisit menyarankan untuk tidak memasangkan kode aplikasi Anda ke Spring jika memungkinkan). Bersulang.
Les Hazlewood