javaspring-bootspring-boot-starter

spring boot custom starter, define entities in it, without using @EntityScan. Is it possible?


I'm making some tests using custom starters for spring boot. I managed to configure everything except the entities. I've tryed using @Import to load entities in the @AutoConfiguration class but this does not work. Instead if we use @EntityScan in the starter the entities are loaded, but if you import this starter and have entities in the project that depends on the starter you are forced to use @EntityScan also in it, and in my opinion this breaks the autoconfiguration meaning of the starter because when you import a starter you should do nothing in order to use it, yes you can override the default configuration but not forced to do anything maybe to declare some properties.

Example of autoconfiguration class in the starter:

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
@EnableJpaRepositories(basePackages = "com.example.springbootstarterexample.repository")
@Import({SomeServiceImpl.class, SomeEntityController.class /*, SomeEntity.class NOT WORKING*/})
@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}

and then if you have entities in the consumer of the starter you have to do this if you have entities in it:

@SpringBootApplication
@EntityScan(basePackages = "com.example.springbootconsumer.model")
public class SpringBootConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootConsumerApplication.class, args);
    }

}

Otherwise we can remove @EntityScan from the starter and do this in the consumer:

@SpringBootApplication
@EntityScan(basePackages = {"com.example.springbootconsumer.model", "com.example.springbootstarterexample.domain"})
public class SpringBootConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootConsumerApplication.class, args);
    }

}

but this totaly brakes the autoconfiguration, because you have to know where the entities are in the starter in order to start the application. I've write an example if interested.

EDIT tryed with @AutoConfigurationPackage

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
//@EnableJpaRepositories(basePackages = {"com.example.springbootstarterexample.repository"})
@AutoConfigurationPackage(basePackageClasses = {SomeEntity.class, SomeEntityRepository.class})
@Import({SomeServiceImpl.class, SomeEntityController.class /*, SomeEntity.class NOT WORKING*/})
//@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}

In this way the repository is not istantiated

Description:

Parameter 0 of constructor in com.example.springbootstarterexample.service.SomeServiceImpl required a bean of type 'com.example.springbootstarterexample.repository.SomeEntityRepository' that could not be found.


Action:

Consider defining a bean of type 'com.example.springbootstarterexample.repository.SomeEntityRepository' in your configuration.

If I use @EnableJpaRepositories the repository is find for injection but not the entity

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
@EnableJpaRepositories(basePackages = {"com.example.springbootstarterexample.repository"})
@AutoConfigurationPackage(basePackageClasses = {SomeEntity.class})
@Import({SomeServiceImpl.class, SomeEntityController.class /*, SomeEntity.class NOT WORKING*/})
//@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}

Error:

Caused by: java.lang.IllegalArgumentException: Not a managed type: class com.example.springbootstarterexample.domain.SomeEntity

Using the name of the package I have the same result

EDIT 2 The @AutoConfiguration class is loaded by META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports removed @Import:

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
//@EnableJpaRepositories(basePackages = {"com.example.springbootstarterexample.repository"})
@AutoConfigurationPackage(basePackageClasses = {SomeEntity.class, SomeServiceImpl.class, SomeEntityController.class, SomeEntityRepository.class})
//@Import({SomeServiceImpl.class, SomeEntityController.class /*, SomeEntity.class NOT WORKING*/})
//@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}

trying to inject something in the consumer:

Description:

Parameter 0 of constructor in com.example.springbootconsumer.SpringBootConsumerApplication required a bean of type 'com.example.springbootstarterexample.service.SomeService' that could not be found.


Action:

Consider defining a bean of type 'com.example.springbootstarterexample.service.SomeService' in your configuration.

This seems to not load any configuration at all.

EDIT 3 put the log level to TRACE and put all classes under the same package, the package of ExampleAutoConfiguration class that now looks like this:

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
//@EnableJpaRepositories(basePackages = {"com.example.springbootstarterexample.repository"})
@AutoConfigurationPackage
//@Import({SomeServiceImpl.class, SomeEntityController.class /*, SomeEntity.class NOT WORKING*/})
//@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}

I found log of the @AutoConfiguration class being scanned but I can't find any bean defined in the package in the logs:

2022-09-08 20:03:24.495 TRACE 17132 --- [           main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'com.example.springbootstarterexample.autoconfigure.ExampleAutoConfiguration'

if I use normal configuration i see all beans been created

2022-09-08 22:31:34.580 TRACE 2308 --- [           main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'com.example.springbootstarterexample.service.SomeServiceImpl'
2022-09-08 22:31:34.581 TRACE 2308 --- [           main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'com.example.springbootstarterexample.controller.SomeEntityController'
2022-09-08 22:31:34.585 TRACE 2308 --- [           main] a.ConfigurationClassBeanDefinitionReader : Registered bean definition for imported class 'com.example.springbootstarterexample.autoconfigure.ExampleAutoConfiguration'
2022-09-08 22:31:34.685 TRACE 2308 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Spring Data JPA - Registering repository: someEntityRepository - Interface: com.example.springbootstarterexample.repository.SomeEntityRepository - Factory: org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean
2022-09-08 22:31:39.094 DEBUG 2308 --- [           main] org.hibernate.cfg.Ejb3Column             : Binding column: Ejb3DiscriminatorColumn{logicalColumnName'DTYPE', discriminatorTypeName='string'}
2022-09-08 22:31:39.112 DEBUG 2308 --- [           main] o.h.cfg.annotations.EntityBinder         : Import with entity name SomeEntity
2022-09-08 22:31:39.113 TRACE 2308 --- [           main] o.h.b.i.InFlightMetadataCollectorImpl    : Import: SomeEntity -> com.example.springbootstarterexample.domain.SomeEntity
2022-09-08 22:31:39.114 TRACE 2308 --- [           main] o.h.b.i.InFlightMetadataCollectorImpl    : Import: com.example.springbootstarterexample.domain.SomeEntity -> com.example.springbootstarterexample.domain.SomeEntity

Solution

  • Finally found a solution thanks to this post. The configuration class becomes:

    @AutoConfigureBefore(JpaRepositoriesAutoConfiguration.class)
    @EnableJpaRepositories(basePackages = {"com.example.springbootstarterexample.repository"})
    @Import({SomeServiceImpl.class, SomeEntityController.class, StarterEntityRegistrar.class /*, SomeEntity.class NOT WORKING*/})
    public class ExampleAutoConfiguration {
    
    }
    

    and the registar for entities:

    public class StarterEntityRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, SomeEntity.class.getPackageName());
        }
    
    }
    

    a working example

    you can use the register method to add all packages that you need:

    Programmatically registers the auto-configuration package names. Subsequentinvocations will add the given package names to those that have already beenregistered. You can use this method to manually define the base packages that willbe used for a given BeanDefinitionRegistry. Generally it's recommended thatyou don't call this method directly, but instead rely on the default conventionwhere the package name is set from your @EnableAutoConfigurationconfiguration class or classes.

    Edit:

    upgraded example to spring boot 3.2.1 still working