4

I have a question regarding Spring Boot and using a setup with multiple DataSources using JpaRepositories.

[4 Edits below]

The structure of the project looks like this: com/mycompany/schema/AbstractJpaConfig com/mycompany/schema/domain_A/AJpaConfiguration com/mycompany/schema/domain_A/entity/AEntity com/mycompany/schema/domain_A/repository/ARepository com/mycompany/schema/domain_B/BJpaConfiguration com/mycompany/schema/domain_B/entity/BEntity com/mycompany/schema/domain_B/repository/BRepository

So I have two domains (A and B), with the DataSource setup being handled separately.

The abstract JPA configuration class is used to reduce redundancy and uses a custom DataSourceManager which handles the DataSources:

public abstract class AbstractJpaConfiguration {

    private final DataSourceManager dataSources;

    public AbstractJpaConfiguration(DataSourceManager dataSources) {
        this.dataSources = dataSources;
    }

    protected abstract String persistenceUnitName();

    protected abstract Class<?> packageEntityClass();

    protected abstract DataSource useDataSource(DataSourceManager dataSources);

    public abstract LocalContainerEntityManagerFactoryBean entityManagerFactoryBean();

    public abstract PlatformTransactionManager transactionManagerBean();

    public abstract LocalContainerEntityManagerFactoryBean getEntityManagerFactoryBean();

    protected LocalContainerEntityManagerFactoryBean buildEntityManagerFactory() {
        LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(useDataSource(dataSources));
        em.setPersistenceUnitName(persistenceUnitName() + "PU");
        em.setBeanName(persistenceUnitName() + "EntityManager");
        em.setPackagesToScan(packageEntityClass().getPackage().getName());
        em.setJpaPropertyMap(persistenceProperties());
        em.setJpaVendorAdapter(jpaVendorAdapter());
        return em;
    }

    protected Map<String, String> persistenceProperties() {
        Map<String, String> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "validate");
        return properties;
    }

    protected JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
        adapter.setShowSql(false);
        adapter.setGenerateDdl(false);
        return adapter;
    }

    protected PlatformTransactionManager buildTransactionManager() {
        LocalContainerEntityManagerFactoryBean emfBean = getEntityManagerFactoryBean();
        EntityManagerFactory emf = emfBean.getObject();
        return new JpaTransactionManager(emf);
    }
}

An implementation of the configuration, located in the domain A package, looks like this:

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "com.mycompany.schema.domain_A",
        entityManagerFactoryRef = "AEntityManagerFactory",
        transactionManagerRef = "ATransactionManager")
@EntityScan(basePackages = "com.mycompany.schema.domain_A")
@DependsOn("flywayMigrationInitializer")
public class AJpaConfiguration extends AbstractJpaConfiguration {

    @Autowired
    public AJpaConfiguration(DataSourceManager dataSources) {
        super(dataSources);
    }

    @Override
    protected Class<?> packageEntityClass() {
        return getClass(); // This class is located in the entity class package
    }

    @Override
    protected String persistenceUnitName() {
        return "a";
    }

    @Override
    protected DataSource useDataSource(DataSourceManager dataSources) {
        return dataSources.domainADataSource();
    }

    @Bean("aEntityManagerFactory")
    @Override
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
        return buildEntityManagerFactory();
    }

    @Bean("aTransactionManager")
    @Override
    public PlatformTransactionManager transactionManagerBean() {
        return buildTransactionManager();
    }

    @Override
    public LocalContainerEntityManagerFactoryBean getEntityManagerFactoryBean() {
        return entityManagerFactoryBean();
    }
}

Then, the actual repository is defined as a JpaRepository:

@Repository
public interface ARepository extends JpaRepository<AEntity, Long> {
}

This seems to work, according to the application logs:

2018-12-14 09:45:02.997  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2018-12-14 09:45:02.997  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-12-14 09:45:03.012  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 9ms. Found 1 repository interface.
2018-12-14 09:45:03.029  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode!
2018-12-14 09:45:03.029  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data repositories in DEFAULT mode.
2018-12-14 09:45:03.085  INFO 13867 --- [  restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 56ms. Found 1 repository interface.

And after that, and the successful Flyway migration, the Persistence Units are started:

2018-12-14 09:45:06.459  INFO 13867 --- [  restartedMain] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: aPU
    ...]
2018-12-14 09:45:06.544  INFO 13867 --- [  restartedMain] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.7.Final}
2018-12-14 09:45:06.546  INFO 13867 --- [  restartedMain] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2018-12-14 09:45:06.746  INFO 13867 --- [  restartedMain] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2018-12-14 09:45:06.922  INFO 13867 --- [  restartedMain] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.MySQL57Dialect
2018-12-14 09:45:07.976  INFO 13867 --- [  restartedMain] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
2018-12-14 09:45:08.066  INFO 13867 --- [  restartedMain] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'aPU'

But, when I try to autowire the repository into a service:

@Service
public class MyService {
    private final ARepository repository;

    @Autowired
    public MyService(ARepository repository) {
        this.repository = repository;
    }

    // ...
}

This is the error message I recieve in the logs:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'MyService' defined in URL [jar:file:...]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.mycompany.schema.domain_A.repository.ARepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:767) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:218) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1308) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1154) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:273) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1239) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1166) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:855) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:758) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 106 common frames omitted
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.mycompany.schema.domain_A.repository.ARepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1646) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1205) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1166) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:855) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:758) ~[spring-beans-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 120 common frames omitted

I hope the information provided can help solve this problem.
If not, I'll be happy to provide additional information.

Thank you for your time, and have a nice day
- Alexander

--- Edit ---

I put two breakpoints into the configuration classes, and got some new information:

The method for creating the LocalContainerEntityManagerFactoryBean is called, but the application stops due to the missing bean before the PlatformTransactionManager bean method is called.

What I overlooked was that the Service in question, which requires the JpaRepository subtype, is implementing the Spring Security UserDetailsManager interface. It seems like the Spring Security system is trying to instantiate the UserDetailsManager Service before the JpaRepository Beans picked up by @EnableJpaRepositories can be created.

Is there any solution for that?

--- Edit 2 ---

I tried to @Import the AJpaConfiguration class, which didn't change anything. However, looking closer at the bean instantiation logs I found the following messages:

2018-12-14 11:52:08.395 DEBUG 22417 --- [  restartedMain] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'aEntityManagerFactory'
2018-12-14 11:52:08.395 DEBUG 22417 --- [  restartedMain] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'aJpaConfig'

So Spring Boot is picking up the @Bean annotation for the LocalContainerEntityManagerFactoryBean, but neither creating EntityManagerFactory instances nor picking up the @Bean annotation for the PlatformTransactionManager.

--- Edit 3 ---

I set the @Autowired option required = false, and now it's picking up the beans and instantiating the repositories - but only after instantiating the services that require them, and thus not injecting them.

2018-12-14 13:25:30.484 DEBUG 31932 --- [  restartedMain] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'aEntityManager'
2018-12-14 13:25:33.035 DEBUG 31932 --- [  restartedMain] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'aTransactionManager'
2018-12-14 13:25:34.852 DEBUG 31932 --- [  restartedMain] o.s.b.f.s.DefaultListableBeanFactory     : Creating shared instance of singleton bean 'aRepository'

How can I create the beans before everything else? That would seem to solve the problem.

--- Edit 4 ---

Thanks to the tip by @RobertNiestroj, I added @Lazy to the @Autowired annotations, which causes them to be initialized after then repositories.

But now, a different error appears:
java.lang.IllegalArgumentException: interface com.mycompany.schema.domain_A.repository.ARepository is not visible from class loader

I should also mention this Application is built out of Maven Modules: - Schema module (JPA configuration, repositories, and entity classes) - Core module (Application configuration and property classes) - [Other modules using Core and Schema] - Main module (Spring-Boot Application class)

Alexander D.
  • 91
  • 1
  • 5
  • You might want to post a [Minimal, Complete, and Verifiable Examples](http://stackoverflow.com/help/mcve). Please try your best to avoid posting too much code, it makes the question harder to read. Thanks – Dang Nguyen Dec 14 '18 at 09:29
  • Hi Alexander, do you see the `ARepository` being created in the logs? Set your logs to `logging.level.org.springframework.beans.factory=DEBUG` in your `application.properties` and look for the line which says "Creating shared instance of singleton bean 'ARepository'". If you do not see the line it means that there is something wrong in creating your bean. i.e. component scan didn't pick it up. – Rentius2407 Dec 14 '18 at 09:36
  • Thanks @Rentius2407, I checked that and the repository is indeed not instantiated. I will check why that is the case, and how to get it to be created. – Alexander D. Dec 14 '18 at 10:05
  • Edit after debugging with @Rentius2407 hint – Alexander D. Dec 14 '18 at 10:35
  • Try to add `@DependsOn("aRepository")` to the service bean – Vüsal Dec 14 '18 at 12:55
  • I'm concerned with word "shared" in logs, please check this also: https://stackoverflow.com/questions/11547240/spring-creating-multiple-instances-of-a-singleton [Child context(s) can reinstantiate the same singleton beans if you are not careful with context:component-scan annotations] - check your `componentScan` annotation property in both JPA configurations – Vüsal Dec 14 '18 at 12:58
  • Try adding `@Lazy` to `@Autowired` where you want to have the repos injected. – Robert Niestroj Dec 14 '18 at 13:07
  • @RobertNiestroj Oh yeah, that _almost_ worked: The bean is being created with "Creating shared instance of singleton bean 'aRepository'" ... But when the Service is created, if fails with the message: `java.lang.IllegalArgumentException: interface com.mycompany.schema.domain_A.repository.ARepository is not visible from class loader` – Alexander D. Dec 14 '18 at 13:36

2 Answers2

0

Have you tried @Repository(name = "repository")? IIRC, by not providing a name, the component gets created with a name matching the class, in this case "aRepository". And thus when autowiring the repository variable, there's no component named "repository", resulting in the no qualifying bean exception.

SvenskNavi
  • 117
  • 1
  • 12
-1

Please try autowiring as below and check

@Service
public class MyService {

    @Autowired
    private ARepository repository;

    // ...
}
TheSprinter
  • 1,523
  • 17
  • 30
  • I tried that (without final, because with final it needs to be set in the constructor), and it didn't work - the bean does not exist, and the dependency is unsatisfied. The error is still `No qualifying bean of type 'com.mycompany..schema.domain_A.repository.ARepository'`. – Alexander D. Dec 14 '18 at 11:49
  • Remove the annotation `@Repository` from interface `ARepository` and try – TheSprinter Dec 14 '18 at 11:55
  • Still no bean, sorry. :( It is creating an `aEntityManagerFactory` and `aJpaConfig`, but no TransactionManager and no Repositories. – Alexander D. Dec 14 '18 at 12:04
  • Hold on - I set the option `@Autowired(required = false)`, and now it's creating the Repository beans!! - but only after all other beans, so they are not injected into the services that need them. I'll edit the original question. – Alexander D. Dec 14 '18 at 12:28