2

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
Pp88
  • 830
  • 6
  • 19
  • There is a, bit hidden, annotation for this. Use [`@AutoConfigurationPackage`] (https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/AutoConfigurationPackage.html) and ditch the `@EnableJpaRepositories` and `@EntityScan`. You might even need to ditch the `@AutoConfigureAfter`. – M. Deinum Aug 30 '22 at 14:20
  • @M.Deinum tryed various configurtion with `@AutoConfigurationPackage` nothing worked, if you can make an example I'will be happy to make a test – Pp88 Sep 02 '22 at 14:54
  • What have you tried so far? What combinations? It should be a matter of putting that annotation on your auto config. – M. Deinum Sep 02 '22 at 18:59
  • @M.Deinum updated the question with more infos – Pp88 Sep 05 '22 at 18:01
  • You need to remove the `@ComponentScan` as well and make sure you properly registered your auto configuration class in the `spring.factories` or `org.springframework.boot.autoconfigure.AutoConfiguration.imports` file, depending on your spring boot version. See https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration – M. Deinum Sep 06 '22 at 04:57
  • Sorry but I’m not using any @ComponentScan and my configuration class is correctly loaded – Pp88 Sep 06 '22 at 05:54
  • Where I said `@ComponentScan` I meant `@Import`. Too early didn't have enough coffee yet :). Is it loaded or correctly loaded as those are different things :). It should be loaded through the auto configuration mechanism and not as a regular `@Configuration` class. – M. Deinum Sep 06 '22 at 06:42
  • @M.Deinum removed `@Import` now nothing is been istantiated by the `@AutoConfiguration` the starter does nothing at all. updated question with more info – Pp88 Sep 06 '22 at 14:29
  • Enable debug (or even trace) logging and see what happens. Instead of defining classes with basepackages specify a single base package that covers all of those (if that is possible). – M. Deinum Sep 06 '22 at 16:21
  • @M.Deinum tryed to put all classes in the same package but still not working. updated the question for more infos – Pp88 Sep 08 '22 at 20:37
  • 1 thing is that it shouldn't run after the `JpaRepositoriesAutoConfiguration.class` as that will make it run after JPA etc. has done its thing registering entities etc. – M. Deinum Sep 09 '22 at 08:22

3 Answers3

1

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.

Pp88
  • 830
  • 6
  • 19
0

You need to add @ComponentScan to configuration bean.

@AutoConfiguration(after = JpaRepositoriesAutoConfiguration.class)
//@AutoConfigureAfter(JpaRepositoriesAutoConfiguration.class)
@EnableJpaRepositories(basePackages = "com.example.springbootstarterexample.repository")
@ComponentScan("com.example.springbootstarterexample")
@EntityScan(basePackages = "com.example.springbootstarterexample.domain")
public class ExampleAutoConfiguration {

}
viking
  • 260
  • 2
  • 10
  • 1
    it's a bad practice to use `@ComponentScan` in the starter [docs](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration.locating-auto-configuration-candidates). And this not solves the problems related to `@EntityScan` – Pp88 Aug 30 '22 at 08:14
  • No, you are post link to `@Import` configuration beans, I mean you should use `@ComponentScan` to load your services annotated as `@Service` or `@Component` `@ComponentScan("com.example.springbootstarterexample.controller")` – viking Aug 30 '22 at 08:23
  • Nope the doc stays to use `@Import` not `@ComponentScan` also for beans annotated not only for configuration classes – Pp88 Aug 30 '22 at 08:28
  • You do realize that the first thing `@SpringBootApplication` does is `@ComponentScan` all classes in that package and below, right? Ideally you would have a configuration file in the right package with `@ComponentScan`, and you would @Import that configuration class in your application. – Calabacin Aug 30 '22 at 12:00
  • Also, if your entities are in a different jar, you can expose them explicitly to Spring Boot in file `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`, one class per line – Calabacin Aug 30 '22 at 12:03
  • this will not solve the `@EntityScan` problem `@ComponentScan` doesn’t find entities. I know `@SpringBootAoplication` Wraps `@ComponentScan` but it’s on the package of the consumer of the starter it’s totally different – Pp88 Aug 30 '22 at 12:29
  • also putting the entities in `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` doesn't work @Calabacin – Pp88 Sep 02 '22 at 14:58
0

An interesting problem, but if you had an agreed way with consumers of your library on how to share information where their entities are, like for example they would provide a META-INF/entityscanforyouapp file with a list of packages where they have their entities

Consumer 1 would create META-INF/entityscanforyouapp

com.consumerapp1.entities

Consumer 1 would create META-INF/entityscanforyouapp

com.consumerapp2.entities

You could discover all these files and then just create an EntityManagerFactory bean that would include all the discovered packages

@Bean
@Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder factoryBuilder, DataSource dataSource) {
    Map<String, Object> vendorProperties = <... read the additional hibernate properties if you need...>;

    String[] packagesWithEntities = discoverAllThePackages();

    return factoryBuilder.dataSource(datasource).packages(packagesWithEntities).properties(vendorProperties).build();
}

This is kind of "rough and ready" but should provide a good starting point if you want to go that way.

Strelok
  • 50,229
  • 9
  • 102
  • 115
  • Still you have to do work in the consumer and goes against the starter "philosophy", I have to take a look to `spring-boot-starter-batch` because the spring batch needs some tables to work, but I don't know what they are using – Pp88 Sep 02 '22 at 13:59
  • ‍♂️ `spring-boot-starter-batch` uses JdbcTemplate, not entities. So it doesn't use JPA. It doesn't really go against the starter philosophy at all. For example if you're using the jpa starter you can use @EnableJpaRepositories to tell where your entities are. But anyway you can do a full class path scan for entities to discover @Entity annotated classes and use the above pattern to register them all. Will just take a long time to start up :) – Strelok Sep 02 '22 at 15:55