Menyuntikkan Kacang Prototipe ke dalam Instans Singleton di Musim Semi

1. Ikhtisar

Dalam artikel singkat ini, kami akan menunjukkan pendekatan berbeda untuk memasukkan kacang prototipe ke dalam contoh tunggal . Kami akan membahas kasus penggunaan dan keuntungan / kerugian dari setiap skenario.

Secara default, kacang musim semi adalah lajang. Masalah muncul saat kami mencoba memasang biji dengan cakupan yang berbeda. Misalnya, prototipe kacang menjadi singleton. Ini dikenal sebagai masalah injeksi kacang polong .

Untuk mempelajari lebih lanjut tentang lingkup kacang, artikel ini adalah tempat yang baik untuk memulai.

2. Masalah Injeksi Kacang Prototipe

Untuk menjelaskan masalahnya, mari kita konfigurasikan kacang berikut:

@Configuration public class AppConfig { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public PrototypeBean prototypeBean() { return new PrototypeBean(); } @Bean public SingletonBean singletonBean() { return new SingletonBean(); } }

Perhatikan bahwa kacang pertama memiliki lingkup prototipe, yang lainnya adalah tunggal.

Sekarang, mari menyuntikkan kacang cakupan-prototipe ke dalam singleton - dan kemudian mengekspos if melalui metode getPrototypeBean () :

public class SingletonBean { // .. @Autowired private PrototypeBean prototypeBean; public SingletonBean() { logger.info("Singleton instance created"); } public PrototypeBean getPrototypeBean() { logger.info(String.valueOf(LocalTime.now())); return prototypeBean; } }

Kemudian, mari muat ApplicationContext dan dapatkan kacang tunggal dua kali:

public static void main(String[] args) throws InterruptedException { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); SingletonBean firstSingleton = context.getBean(SingletonBean.class); PrototypeBean firstPrototype = firstSingleton.getPrototypeBean(); // get singleton bean instance one more time SingletonBean secondSingleton = context.getBean(SingletonBean.class); PrototypeBean secondPrototype = secondSingleton.getPrototypeBean(); isTrue(firstPrototype.equals(secondPrototype), "The same instance should be returned"); }

Berikut keluaran dari konsol:

Singleton Bean created Prototype Bean created 11:06:57.894 // should create another prototype bean instance here 11:06:58.895

Kedua kacang diinisialisasi hanya sekali, pada permulaan konteks aplikasi.

3. Memasukkan ApplicationContext

Kami juga dapat memasukkan ApplicationContext langsung ke dalam kacang.

Untuk melakukannya, gunakan anotasi @Autowire atau terapkan antarmuka ApplicationContextAware :

public class SingletonAppContextBean implements ApplicationContextAware { private ApplicationContext applicationContext; public PrototypeBean getPrototypeBean() { return applicationContext.getBean(PrototypeBean.class); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }

Setiap kali metode getPrototypeBean () dipanggil, instance baru PrototypeBean akan dikembalikan dari ApplicationContext .

Namun, pendekatan ini memiliki kekurangan yang serius. Ini bertentangan dengan prinsip inversi kontrol, karena kami meminta dependensi dari container secara langsung.

Juga, kami mengambil prototipe kacang dari applicationContext di dalam kelas SingletonAppcontextBean . Ini berarti menggabungkan kode ke Spring Framework .

4. Metode Injeksi

Cara lain untuk mengatasi masalah ini adalah injeksi metode dengan anotasi @Lookup :

@Component public class SingletonLookupBean { @Lookup public PrototypeBean getPrototypeBean() { return null; } }

Spring akan menimpa metode getPrototypeBean () yang dianotasi dengan @Lookup. Ini kemudian mendaftarkan kacang ke dalam konteks aplikasi. Setiap kali kita meminta metode getPrototypeBean () , ia mengembalikan instance PrototypeBean baru .

Ini akan menggunakan CGLIB untuk menghasilkan bytecode yang bertanggung jawab untuk mengambil PrototypeBean dari konteks aplikasi.

5. API javax.inject

Setup bersama dengan dependensi yang diperlukan dijelaskan dalam artikel kabel Spring ini.

Inilah kacang tunggal:

public class SingletonProviderBean { @Autowired private Provider myPrototypeBeanProvider; public PrototypeBean getPrototypeInstance() { return myPrototypeBeanProvider.get(); } }

Kami menggunakan antarmuka Penyedia untuk menyuntikkan kacang prototipe. Untuk setiap panggilan metode getPrototypeInstance () , myPrototypeBeanProvider. g et () mengembalikan contoh baru PrototypeBean .

6. Proxy Tercakup

Secara default, Spring menyimpan referensi ke objek nyata untuk melakukan injeksi. Di sini, kami membuat objek proxy untuk menghubungkan objek nyata dengan objek dependen.

Setiap kali metode pada objek proxy dipanggil, proxy memutuskan sendiri apakah akan membuat contoh baru dari objek nyata atau menggunakan kembali yang sudah ada.

Untuk menyiapkan ini, kami memodifikasi kelas Appconfig untuk menambahkan anotasi @Scope baru :

@Scope( value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)

By default, Spring uses CGLIB library to directly subclass the objects. To avoid CGLIB usage, we can configure the proxy mode with ScopedProxyMode.INTERFACES, to use the JDK dynamic proxy instead.

7. ObjectFactory Interface

Spring provides the ObjectFactory interface to produce on demand objects of the given type:

public class SingletonObjectFactoryBean { @Autowired private ObjectFactory prototypeBeanObjectFactory; public PrototypeBean getPrototypeInstance() { return prototypeBeanObjectFactory.getObject(); } }

Let's have a look at getPrototypeInstance() method; getObject() returns a brand new instance of PrototypeBean for each request. Here, we have more control over initialization of the prototype.

Also, the ObjectFactory is a part of the framework; this means avoiding additional setup in order to use this option.

8. Create a Bean at Runtime Using java.util.Function

Another option is to create the prototype bean instances at runtime, which also allows us to add parameters to the instances.

To see an example of this, let's add a name field to our PrototypeBean class:

public class PrototypeBean { private String name; public PrototypeBean(String name) { this.name = name; logger.info("Prototype instance " + name + " created"); } //... }

Next, we'll inject a bean factory into our singleton bean by making use of the java.util.Function interface:

public class SingletonFunctionBean { @Autowired private Function beanFactory; public PrototypeBean getPrototypeInstance(String name) { PrototypeBean bean = beanFactory.apply(name); return bean; } }

Finally, we have to define the factory bean, prototype and singleton beans in our configuration:

@Configuration public class AppConfig { @Bean public Function beanFactory() { return name -> prototypeBeanWithParam(name); } @Bean @Scope(value = "prototype") public PrototypeBean prototypeBeanWithParam(String name) { return new PrototypeBean(name); } @Bean public SingletonFunctionBean singletonFunctionBean() { return new SingletonFunctionBean(); } //... }

9. Testing

Let's now write a simple JUnit test to exercise the case with ObjectFactory interface:

@Test public void givenPrototypeInjection_WhenObjectFactory_ThenNewInstanceReturn() { AbstractApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); SingletonObjectFactoryBean firstContext = context.getBean(SingletonObjectFactoryBean.class); SingletonObjectFactoryBean secondContext = context.getBean(SingletonObjectFactoryBean.class); PrototypeBean firstInstance = firstContext.getPrototypeInstance(); PrototypeBean secondInstance = secondContext.getPrototypeInstance(); assertTrue("New instance expected", firstInstance != secondInstance); }

Setelah berhasil meluncurkan pengujian, kita dapat melihat bahwa setiap kali metode getPrototypeInstance () dipanggil, instance kacang prototipe baru dibuat.

10. Kesimpulan

Dalam tutorial singkat ini, kami mempelajari beberapa cara untuk memasukkan prototipe bean ke dalam instance tunggal.

Seperti biasa, kode lengkap untuk tutorial ini dapat ditemukan di proyek GitHub.