Site icon Voina Blog (a tech warrior's blog)

#SpringBoot : How to create multiple instances of the same Spring Bean type. #Java

Advertisements

Spring Boot assumes that all the created beans are singletons. That works OK for 90% of the cases when Spring Boot is used but sometimes we need several instances of the same bean type.

One example is when our bean is a kind of “interface service” that is reading or writing data to another external service of a given type. In case we have several connections to different external services of the same type it makes sense to have the same bean type but initialized with some different parameters.

For example we want to connect to several JMS external services of the same type.

In the bellow example basically we implement two different ways of being able to create multiple instances of the same Spring Bean type at the same time.

Method 1 : Define the beans using BeanDefinitionRegistryPostProcessor by dynamically register beans and tag them with different aliases.

Method 2 : Define the beans extending Thread and annotating them with scope prototype. This will allow us to create at runtime different Threads (Spring Beans) of the same type.

BeanDefinitionRegistryPostProcessor

Extension to the standard {@link BeanFactoryPostProcessor} SPI, allowing for the registration of further bean definitions before regular BeanFactoryPostProcessor detection kicks in. In particular, BeanDefinitionRegistryPostProcessor may register further bean definitions which in turn define BeanFactoryPostProcessor instances.

Spring Boot documentation

Our output AdapterJmsOutput bean is defined as:

public class AdapterJmsOutput extends Thread implements AdapterOutput {

	protected MessageBeanRepository messageBeanRepository;
	/**
	 * global id generator
	 */
	UUIDGenerator uuidGenerator;
	/**
	 * date format for the reference
	 */
	SimpleDateFormat sdf = new SimpleDateFormat("yyMMdd");
	
	public InterfaceAdapterJmsOutput(ContextConfiguration context, AdapterOutputConfiguration cfg) {
		try {
			config = cfg;
			configureJms(context);
			uuidGenerator = UUIDGenerator.getInstance(getName() + hashCode());
		} catch (Exception ex) {
			throw new RuntimeException("Interface adapter output failed to initialize!", ex);
		}
	}
...
}

We first need to define a @Configuration that will create our bean factory.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.example.messaging.AdapterConfiguration;


@Configuration
public class AdapterBeansConfigBeanConfiguration {
	@Bean
	public AdapterBeansConfig adapterOutputConfig(AdapterConfiguration config) {
		return new AdapterBeansConfig(config);
	}
}

Then we can define the class of our bean factory.

public class AdapterBeansConfig implements BeanDefinitionRegistryPostProcessor {

	protected AdapterConfiguration config;

	protected MessageBeanRepository messageBeanRepository;

	public AdapterBeansConfig(AdapterConfiguration config) {
		this.config = config;
	}

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		// do nothing
	}

	/**
	 * Dynamically register output beans, tag them with the entry key alias
	 */
	@Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
		createOutputBeans(registry);
	}

	protected void createOutputBeans(BeanDefinitionRegistry registry) {
		Map<String, AdapterOutputConfiguration> outputConfigurations = config.getOutputConfigurations();
		if (outputConfigurations != null) {
			for (Entry<String, AdapterOutputConfiguration> entry : outputConfigurations.entrySet()) {
				GenericBeanDefinition bd = new GenericBeanDefinition();
				bd.setBeanClass(AdapterJmsOutput.class);
				/**
				 * The bean scope must be “prototype“, so that each request will return a new instance, to run each individual thread.
				 */
				bd.setScope(GenericBeanDefinition.SCOPE_PROTOTYPE);
				ConstructorArgumentValues args = new ConstructorArgumentValues();
				args.addIndexedArgumentValue(0, config.getContextConfigurations().get(entry.getValue().getContextName()));
				args.addIndexedArgumentValue(1, entry.getValue());
				bd.setConstructorArgumentValues(args);
				registry.registerBeanDefinition("output" + entry.getKey(), bd);
			}
		}
	}

}

Several things to note:

  1. AdapterJmsOutput has a Repository type field messageBeanRepository. Note we cannot inject Repositories as they are not yet initialized. We need to add repositories or other beans that depend on repositories with setters on the beans after the application start.
  2. bd.setScope(GenericBeanDefinition.SCOPE_PROTOTYPE);
    This will set the scope of the bean definition to prototype. When using instead of a factory to create the beans direct @Bean annotation the equivalent annotation is @Scope(“prototype”)

Bellow is now an example of how we can create now several beans of type AdapterJmsOutput .

@SpringBootApplication
@EnableJpaRepositories(repositoryBaseClass = CustomRepositoryImpl.class)
public class AdapterSpringBootApplication {
...
@Autowired
public static AdapterConfiguration config;
...

public static void main(String[] args) {
SpringApplicationBuilder app = new SpringApplicationBuilder(AdapterSpringBootApplication.class).web(WebApplicationType.SERVLET);
		app.build().addListeners(new ApplicationPidFileWriter("shutdown.pid"));
		ConfigurableApplicationContext applicationContext = app.run();
... 
// for each output configuration start two Threads of the same type.  
Map<String, AdapterOutputConfiguration> outputConfigurations = config.getOutputConfigurations();
			if (outputConfigurations != null) {
				outputs = new TreeMap<>();
				for (Entry<String, AdapterOutputConfiguration> entry : outputConfigurations.entrySet()) {
					log.info("Starting output with name: " + entry.getKey() + " and context: " + entry.getValue());
					AdapterOutput ret = null;
					AdapterOutputConfiguration cfg = entry.getValue();
					try {
						// retrieve the bean tagged with name output+entry.getKey() from application context
						ret = (AdapterOutput) applicationContext.getBean("output" + entry.getKey());
						ret.setMessageBeanRepository(messageBeanRepository);
                                                ret.setName("Instance1" + "output" + entry.getKey());
					        ret.start();
                                                // retrieve the bean tagged with name output+entry.getKey() from application context
                                                // create a second thread of the same bean type
						ret = (AdapterOutput) applicationContext.getBean("output" + entry.getKey());
						ret.setMessageBeanRepository(messageBeanRepository);
                                                ret.setName("Instance2" + "output" + entry.getKey());
					        ret.start();
					} catch (Exception ex) {
						// if no bean exists then skip this output
						log.info("No registered output bean exists with name: " + entry.getKey());
					}
					
				}
			}
...

As a result if we have in out configurations a SWIFT_PROD and SWIFT_DR configured outputs with different configuration parameters the above code will start 4 threads of type AdapterJmsOutput tagged with names:

Instance1outputSWIFT_PROD

Instance2outputSWIFT_PROD

Instance1outputSWIFT_DR

Instance3outputSWIFT_DR

Exit mobile version