PLATFORM
  • Tails

    Create websites with TailwindCSS

  • Wave

    Start building the next great SAAS

  • Pines

    Alpine & Tailwind UI Library

  • Auth

    Plug'n Play Authentication for Laravel

  • Designer comingsoon

    Create website designs with AI

  • DevBlog comingsoon

    Blog platform for developers

  • Static

    Build a simple static website

  • SaaS Adventure

    21-day program to build a SAAS

Written By

Organising Common Code via Spring Annotations and Bean Registry

Organising Common Code via Spring Annotations and Bean Registry

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

Project dir

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 a Registrar 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:

  1. put .rootBeanDefinition(BeanTemplate.class) directly and it will instantiate a BeanTemplate making it into a bean
  2. 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)

loading comments