Motivation
The project that I work on is a vast net of 40+ microservices, mainly divided into
- Apps (those with UI code)
- Services (all the heavyweight logic and interfacing downstream/upstream services, persistence etc), and
- SCDF streams/tasks.
Obviously, there are lots of similar functionalities within each of these categories, such as
- WebClients to interface different microservices
- Interfaces for messaging queues/databases
- Authentication code
- Metrics/Logging
and the list goes on. Usually these code share the exact same logic except for subtle differences (e.g. Database schemas, Rest Endpoint contracts). Without a proper mechanism to factor out common code would mean having duplicate codes scattered around numerous repositories, and keeping everything updated and consistent will become a true challenge.
Project Structure
I've simplified a lot, but this is how the project look like:
- a
common
repository (though I've made it a module here) with template code and a registrar app
repositories which are users of commons, they have minimal code except for the annotation to specify app-specific stuff
Common
Let's say our common code is in BeanTemplate. It has a distinct beanId, a reference to some other dependencies and a common function. Note that this is not annotated as a bean as we will create this dynamically:
@RequiredArgsConstructor
public class BeanTemplate {
private final String beanId;
private final TestDependency testDependency;
public String someCommonFunction() {
System.out.println("Welcome to the common routine");
return beanId;
}
}
TestDependency
is nothing really interesting, I've just made an empty class here:
@Component
public class TestDependency {
}
Now make an annotation which the app will eventually use.
@Retention(RetentionPolicy.RUNTIME)
@Import(value = {
DynamicBeanConfiguration.class, DynamicBeanRegistrar.class
})
public @interface EnableDynamicBean {
String value() default "defaultBeanName";
}
A few things to note here:
@Retention
is mandatory and must be set to runtime! Otherwise by default, annotations will evaporate after compilation (which Spring will not be able to read at startup)- You usually want to import a
Configuration
and aRegistrar
for scanning dependencies and doing the registration work respectively - You can define as many methods as you want here to include any app-specific configuration. The other way (for example in the case of dynamic WebClients) you would put app-specific methods in the (usually) Interface you put this annotation in, and then use reflection to scan through the methods in the registrar.
As stated above, DynamicBeanConfiguration
is no more than an import of dependencies:
@Configuration
@ComponentScan(basePackageClasses = {TestDependency.class, DynamicBeanRegistrar.class})
public class DynamicBeanConfiguration {
}
The meat is in DynamicBeanRegistrar
, which implements ImportBeanDefinitionRegistrar
. With this, it implements a method which gets
- the
BeanDefinitionRegistry
to register the bean, plus - the
AnnotationMetadata
to get the app-specific annotated data
The function looks roughly like the following:
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
BeanNameGenerator importBeanNameGenerator) {
String beanName = importingClassMetadata.getAnnotations()
.get(EnableDynamicBean.class)
.getString("value");
final BeanDefinition dynamicBean = BeanDefinitionBuilder
// root is needed for non-static methods
// if you set factory method, then this class doesn't matter.
// .rootBeanDefinition(BeanTemplate.class)
.rootBeanDefinition(Object.class)
.setFactoryMethodOnBean("createInstance", "dynamicBeanRegistrar")
.setScope(SCOPE_PROTOTYPE)
.addConstructorArgValue(beanName)
.addDependsOn("testDependency")
.addConstructorArgReference("testDependency")
.getBeanDefinition();
registry.registerBeanDefinition(beanName, dynamicBean);
}
Now the tricky part is to make a BeanDefinition
and get it registered via the BeanDefinitionRegistry
. There are two favours of it:
- put
.rootBeanDefinition(BeanTemplate.class)
directly and it will instantiate aBeanTemplate
making it into a bean - use
.setFactoryMethodOnBean("createInstance", "dynamicBeanRegistrar")
to create the bean using an instance method. You still need.rootBeanDefinition(Object.class)
, but whatever class you put in the parameter doesn't matter anymore.
The factory method could be as simple as this:
BeanTemplate createInstance(String beanId, TestDependency testDependency) {
return new BeanTemplate(beanId, testDependency);
}
though obviously things can get really complicated.
For the other arguments, you can either look at the official doc (which frankly I found not pretty useful) or Google some of the resources. This is one of the sources which I referenced from (thanks mate).
App
Now finally onto the App. The only interesting part would be ApplicationConfiguration
, which is a one-liner using the annotation:
@EnableDynamicBean("dynamicBeanName")
@Configuration
public class ApplicationConfiguration {
}
and here you go! TestRunner
magically autowires the bean and uses it with ease.
@Component
public class TestRunner implements CommandLineRunner {
@Autowired
private BeanTemplate dynamicBeanName;
@Override
public void run(String... args) {
System.out.println(Optional.ofNullable(dynamicBeanName)
.map(BeanTemplate::someCommonFunction)
.orElse("NO BEAN"));
}
}
And finally the output
Welcome to the common routine
dynamicBeanName
The full source code is in Github. Till next time, happy coding!
Comments (0)