Search code examples
javaspring-bootjdbispring-autoconfigurationjdbi3

Spring Boot Custom Bean Loader


I am using JDBI in tandem with Spring Boot. I followed this guide which results in having to create a class: JdbiConfig in which, for every dao wanted in the application context, you must add:

@Bean
public SomeDao someDao(Jdbi jdbi) {
    return jdbi.onDemand(SomeDao.class);
}

I was wondering if there is some way within Spring Boot to create a custom processor to create beans and put them in the application context. I have two ideas on how this could work:

  1. Annotate the DAOs with a custom annotation @JdbiDao and write something to pick those up. I have tried just manually injecting these into the application start up, but the problem is they may not load in time to be injected as they are not recognized during the class scan.
  2. Create a class JdbiDao that every repository interface could extend. Then annotate the interfaces with the standard @Repository and create a custom processor to load them by way of Jdbi#onDemand

Those are my two ideas, but I don't know of any way to accomplish that. I am stuck with manually creating a bean? Has this been solved before?


Solution

  • The strategy is to scan your classpath for dao interface, then register them as bean.

    We need: BeanDefinitionRegistryPostProcessor to register additional bean definition and a FactoryBean to create the jdbi dao bean instance.

    1. Mark your dao intercface with @JdbiDao
    @JdbiDao
    public interface SomeDao {
    }
    
    1. Define a FactoryBean to create jdbi dao
    public class JdbiDaoBeanFactory implements FactoryBean<Object>, InitializingBean {
    
        private final Jdbi jdbi;
        private final Class<?> jdbiDaoClass;
        private volatile Object jdbiDaoBean;
    
        public JdbiDaoBeanFactory(Jdbi jdbi, Class<?> jdbiDaoClass) {
            this.jdbi = jdbi;
            this.jdbiDaoClass = jdbiDaoClass;
        }
    
        @Override
        public Object getObject() throws Exception {
            return jdbiDaoBean;
        }
    
        @Override
        public Class<?> getObjectType() {
            return jdbiDaoClass;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            jdbiDaoBean = jdbi.onDemand(jdbiDaoClass);
        }
    }
    
    1. Scan classpath for @JdbiDao annotated interfaces:
    public class JdbiBeanFactoryPostProcessor
            implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware, EnvironmentAware, BeanClassLoaderAware, BeanFactoryAware {
    
        private BeanFactory beanFactory;
        private ResourceLoader resourceLoader;
        private Environment environment;
        private ClassLoader classLoader;
    
        @Override
        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    
        @Override
        public void setEnvironment(Environment environment) {
            this.environment = environment;
        }
    
        @Override
        public void setBeanClassLoader(ClassLoader classLoader) {
            this.classLoader = classLoader;
        }
    
        @Override
        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            this.beanFactory = beanFactory;
        }
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    
        }
    
        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
            ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false) {
                @Override
                protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                    // By default, scanner does not accept regular interface without @Lookup method, bypass this
                    return true;
                }
            };
            scanner.setEnvironment(environment);
            scanner.setResourceLoader(resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(JdbiDao.class));
            List<String> basePackages = AutoConfigurationPackages.get(beanFactory);
            basePackages.stream()
                    .map(scanner::findCandidateComponents)
                    .flatMap(Collection::stream)
                    .forEach(bd -> registerJdbiDaoBeanFactory(registry, bd));
        }
    
        private void registerJdbiDaoBeanFactory(BeanDefinitionRegistry registry, BeanDefinition bd) {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition) bd;
            Class<?> jdbiDaoClass;
            try {
                jdbiDaoClass = beanDefinition.resolveBeanClass(classLoader);
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
            beanDefinition.setBeanClass(JdbiDaoBeanFactory.class);
            // Add dependency to your `Jdbi` bean by name
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("jdbi"));
            beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(Objects.requireNonNull(jdbiDaoClass));
    
            registry.registerBeanDefinition(jdbiDaoClass.getName(), beanDefinition);
        }
    }
    
    1. Import our JdbiBeanFactoryPostProcessor
    @SpringBootApplication
    @Import(JdbiBeanFactoryPostProcessor.class)
    public class Application {
    }