Thu. Jun 24th, 2021

Recently a project I was working on required the ability to load up a number of unknown services that implemented a Calendar Provider Interface. As this was a Spring server, I started looking around trying to find the right way to do this. I began by looking at being able to add an annotation to a class to identify the class as a Calendar Service Provider.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CalendarServiceProvider {
}

Nothing fancy about the annotation, just a flag on the class at runtime. Next was a way to find the instances with the annotation.

ClassPathScanningCandidateComponentProvider provider
                = new ClassPathScanningCandidateComponentProvider(false);
        provider.addIncludeFilter(new AnnotationTypeFilter(CalendarServiceProvider.class));

        Set<BeanDefinition> providers = provider.findCandidateComponents("com.blue.project");

Now this was much easier than I had thought it would be. I scan com.blue.project for any classes with the annotation type of CalendarServiceProvider.class. However this gives me a BeanDefinition for the class. After a little trial and error I came up with the following:

providers.stream().map(BeanDefinition::getBeanClassName).map(name -> {
            return name.substring(name.lastIndexOf('.')+1);
        }).map(beanName -> {
            return Character.toLowerCase(beanName.charAt(0)) + beanName.substring(1);
        }).map(beanName -> {
            return applicationContext.getBean(beanName);
        }).map(bean -> {
            if(bean instanceof CalendarServiceProviderInterface) {
                return (CalendarServiceProviderInterface)bean;
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());

Couple of things to point out I take the simple name of the class, then I convert the first character to lowercase, and this allows me to load an existing bean of that type. Then for good measure I run it through another map to check verify it has the Interface I want implemented on the bean, if it does I pass it along, else I return a null. Then a final filter to remove the nulls, and then collect everything else to a list.

My completed method looks like this.

@Autowired
    private ApplicationContext applicationContext;

private List<CalendarServiceProviderInterface> findCalendarServiceProviders() {
        ClassPathScanningCandidateComponentProvider provider
                = new ClassPathScanningCandidateComponentProvider(false);
        provider.addIncludeFilter(new AnnotationTypeFilter(CalendarServiceProvider.class));

        Set<BeanDefinition> providers = provider.findCandidateComponents("com.blue.project");

        return providers.stream().map(BeanDefinition::getBeanClassName).map(name -> {
            return name.substring(name.lastIndexOf('.')+1);
        }).map(beanName -> {
            return Character.toLowerCase(beanName.charAt(0)) + beanName.substring(1);
        }).map(beanName -> {
            return applicationContext.getBean(beanName);
        }).map(bean -> {
            if(bean instanceof CalendarServiceProviderInterface) {
                return (CalendarServiceProviderInterface)bean;
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

You will need to autowire in the ApplicationContext. Simple enough of a process to implement.

By Jeffery Miller

I am known for being able to quickly decipher difficult problems to assist development teams in producing a solution. I have been called upon to be the Team Lead for multiple large-scale projects. I have a keen interest in learning new technologies, always ready for a new challenge.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.