Spring Boot Custom Bean Loader
Asked Answered
K

1

6

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?

Kissable answered 30/4, 2020 at 15:18 Comment(2)
BeanFactoryPostProcessor?Onesided
Did you check the spring-data-jdbc project already and does it not cover the things you would use with jdbi? It's perhaps easier (development+testing wise) to stick to the Spring ecosystem.Kesley
R
4

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 {
}
Rowen answered 29/5, 2021 at 15:18 Comment(2)
This works! I was narrowing in on this, but was unaware of how to add the JDBI bean as an argument within the factory registration. Thank you!Kissable
@Kissable it's late, but glad it helpsPreussen

© 2022 - 2024 — McMap. All rights reserved.