PROBLEM:
A strange issue occurs when I use spring boot with spring data. When the spring boot application starts, context creation fails with the following exception stacktrace. The stacktrace is pretty long, so only the crucial excerption is provided.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'metricsEvaluationRepository' defined in MetricsConfig: Unsatisfied dependency expressed through method 'metricsEvaluationRepository' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException:
.....
Caused by: org.springframework.beans.factory.BeanCreationException:
Error creating bean with name 'metricsRepository': Cannot create inner bean '(inner bean)#460510aa' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager';
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#460510aa': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument;
nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Instantiation of bean failed;
nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration$$EnhancerBySpringCGLIB$$67b5940d]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration$$EnhancerBySpringCGLIB$$67b5940d.()
.....
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Instantiation of bean failed;
nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration$$EnhancerBySpringCGLIB$$67b5940d]: No default constructor found;
nested exception is java.lang.NoSuchMethodException: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration$$EnhancerBySpringCGLIB$$67b5940d.()
The source code is also attached.
class MetricsEvaluatorFactory(private val metricsRepository: MetricsRepository) : BeanFactoryPostProcessor {
private val logger = LoggerFactory.getLogger(MetricsEvaluatorFactory::class.java)
private val metricsEvaluators = mutableMapOf()
private val metricsDefinitions = mutableMapOf()
override fun postProcessBeanFactory(beanFactory: ConfigurableListableBeanFactory?) {
loadAllMetricsDefinitions()
logger.debug("Metrics definitions loaded: {}", metricsDefinitions)
beanFactory!!.getBeansWithAnnotation(MetricsProvider::class.java).forEach{
beanName, metricsProvider ->
if (metricsProvider is MetricsEvaluator) {
val metricsProviderAnnotations = metricsProvider::class.java.getAnnotationsByType(MetricsProvider::class.java)
val providerType = metricsProviderAnnotations[0].type
val providerValue = metricsProviderAnnotations[0].value
logger.info("Adding bean name={}, type={}, class={} to metricsEvaluators", beanName, providerType, metricsProvider::class.java.name)
val key = providerType.name + providerValue
if (metricsEvaluators[key] == null) {
metricsEvaluators[key] = metricsProvider
}
else {
logger.error("Duplicated metricsEvaluator definition for key: {}", key)
}
}
else {
logger.error("Ignore bean $beanName with metricsProvider annotation, it is not MetricsEvaluator")
}
}
}
private fun loadAllMetricsDefinitions() {
// todo only load active ones
metricsRepository.findByStatus(MetricsStatus.ACTIVE).forEach {
metricsDefinitions[it.id] = it
}
}
open fun getMetricsEvaluator(metricsDefinition: MetricsDefinition): MetricsEvaluator? {
val metricsEvaluator = metricsEvaluators[metricsDefinition.type.name]
return Option.of(metricsEvaluator).orElse(
Option.of(metricsEvaluators["${metricsDefinition.type}${metricsDefinition.name}"])
).getOrElseThrow {
IllegalArgumentException("No metrics evaluator found for metrics id: ${metricsDefinition.id}, " +
"metrics type: ${metricsDefinition.type}")
}
}
}
@MetricsProvider(type = MetricsType.SPRINGEL)
class SpringElMetricsEvaluator(private val expressionEvaluator: ExpressionEvaluator): MetricsEvaluator {
private val logger = LoggerFactory.getLogger(SpringElMetricsEvaluator::class.java)
.....
}
Here is a brief explanation of the code:
We have a list of metricsEvaluator implementations, each one is marked with an annotation "MetricsProvider", the MetricsEvaluatorFactory scans applicationContext and looks for metricsEvaluator implementations. Metrics definition is the actual metrics configuration entity stored in database. Metrics definition is associated
ANALYSIS:
First, we need to have some background knowledge of how spring application context loads. Taking a look at AbstractApplicationContext.refresh() method, it is clearly divided into multiple stages listed in the table below.
Method name | Description |
---|---|
prepareRefresh | initializePropertySources: Does nothing by default, delegate to subclass implementation. Eg. For webApplicationContext, it loads properties from servletContext getEnvironment().validateRequiredProperties validate properties set by AbstractEnvironment.setRequiredProperties Delegate to PropertySourcePropertyResolver.getProperty, which will get property value from property sources injected with placeholder resolver |
prepareBeanFactory | Prepare beanFactory, add ELExpressionResolver. Add beanPostProcessor: ApplicationContextAwareProcessor. Add various beans to beanFactory like environment and environment system properties |
postProcessBeanFactory | BeanFactory post process hook, empty implementation by default. All bean definitions have been loaded but no beans have been instantiated. Can be used to register special BeanPostProcessors. Eg. GenericWebApplicationContext |
invokeBeanFactoryPostProcessor | Handle all BeanDefinitionRegistryPostProcessor: invoke postProcessBeanDefinitionRegistry method to create additional beanDefinition according to @priorityOrdered, @ordered and normal Handle all BeanFactoryPostProcessor: invoke postProcessBeanFactory method according to @priorityOrdered @ordered and normal Handle @ComponentScan annotation Specifically, call ConfigurationClassPostProcessor.processConfigBeanDefinitions which parses classes with @Configuration annotation and add beanDefinitions |
registerBeanPostProcessors | Handle all BeanPostProcessors from application context. Add beanPostProcessors to beanFactory according to @priorityOrdered, @ordered and finally register all MergedBeanDefinitionPostProcessor. A typical MergedBeanDefinitionPostProcessor will be AutowiredAnnotationBeanPostProcessor, which handles @Autowired and @Value annotation |
initMessageSource | Initialize messageSource, which will be used for internationalization purpose |
initApplicationEventMulticaster | Find or register a new ApplicationEventMulticaster. Default set to SimpleApplicationEventMulticaster |
onRefresh | Empty implementation by default |
registerListeners | Register application listeners with applicationEventMulticaster and multicast earlyApplicationEvent |
finishBeanFactoryInitialization | Call beanFactory.freezeConfiguration Call beanFactory.preInstantiateSingletons to initialize all none lazy beans. Delegate to AbstractBeanFactory.getBean. 1.Call AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization:Call all BeanPostProcessor.postProcessBeforeInitialization. Specifically: InitDestroyAnnotationBeanPostProcessor handles @PostConstruct annotation 2.Call AbstractAutowireCapableBeanFactory.invokeInitMethods. Specifically: call afterPropertiesSet method of InitializingBean 3.Call AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization. Call all BeanPostProcessor.postProcessAfterInitialization |
finishRefresh | Clear application context cache |
Given the analysis above, we can further present a sequence diagram illustrating how metricsEvaluationFactory is created in spring application context loading.
As illustrated in previous section, in order to create MetricsEvaluatorFactory, spring has to create JPA repository MetricsRepository first. The Spring JPA initialize process is shown in the diagram below.
If you are not interested in details, here is a brief explanation of how it works: MetricsRepository is created by JpaRepositoryFactory. JpaRepositoryFactory expects an JPA. EntityManager as parameter. The EntityManager is created by SharedEntityManagerCreator.createSharedEntityManager, which requires an EntityManagerFactory as parameter. The EntityManagerFactory is constructed in LocalContainerEntityManagerFactoryBean.afterPropertiesSet method. The EntityManagerFactory expects a JpaVendorAdapter which is created by HibernateJpaConfiguration which requires a basic source. Usually, this is performed at spring application context's finishBeanFactoryInitialization stage. However, since MetricsEvaluatorFactory is a BeanFactoryPostProcessor, it is initialized at invokeBeanFactoryPostProcessor stage. It forces MetricsRepository to be created early at this stage also, as well as its dependencies lik EntityManager, EntityManagerFactory, HibernateJpaConfiguration etc. However, DataSource is not yet created. This is why construction of HibernateJpaConfiguration fails. The table below further describes spring bean's dependency
Normal:
Application Context Refresh Stage | Source Config | Depends On | Created Object |
---|---|---|---|
finishBeanFactoryInitialization | DataSourceAutoConfiguration | DataSource | |
finishBeanFactoryInitialization | HibernateJpaConfiguration | DataSource | AbstractJpaVendorAdapter |
finishBeanFactoryInitialization | LocalContainerEntityManagerFactoryBean | JpaVendorAdapter | EntityManagerFactory |
finishBeanFactoryInitialization | SharedEntityManagerCreator | EntityManagerFactory | EntityManager |
finishBeanFactoryInitialization | JpaRepositorFactory | EntityManager | Repository Instance |
Abormal: |
Application Context Refresh Stage | Source Config | Depends On | Created Object |
---|---|---|---|---|
invokeBeanFactoryPostProcessor | JpaRepositorFactory | EntityManager | Repository Instance | |
invokeBeanFactoryPostProcessor | SharedEntityManagerCreator | EntityManagerFactory | EntityManager | |
invokeBeanFactoryPostProcessor | LocalContainerEntityManagerFactoryBean | JpaVendorAdapter | EntityManagerFactory | |
invokeBeanFactoryPostProcessor | HibernateJpaConfiguration | Error: Unable to find datasource | AbstractJpaVendorAdapter |
SOLUTION:
I later realized that the annotation processing part is unnecessary thanks to spring's
autowiring capability. So two important changes are made here: First, the MetricsEvaluatorFactory no longer implements BeanFactoryPostProcessor, second, the list of MetricsEvaluators are injected directly to MetricsEvaluatorFactory in constructor. This avoid annotation definition and processing. The new code looks like the following:
open class MetricsEvaluatorFactory(private val metricsRepository: MetricsRepository) {
private val logger = LoggerFactory.getLogger(MetricsEvaluatorFactory::class.java)
private val metricsEvaluators = mutableMapOf()
private val metricsDefinitions = mutableMapOf()
constructor(metricsRepository: MetricsRepository, metricsEvaluatorList: List) : this(metricsRepository) {
doInit(metricsEvaluatorList)
}
private fun doInit(metricsEvaluatorList: List) {
loadAllMetricsDefinitions()
logger.debug("Metrics definitions loaded: {}", metricsDefinitions)
metricsEvaluatorList.forEach { metricsEvaluator ->
val providerType = metricsEvaluator.type
val providerName = metricsEvaluator.name
logger.info("Adding type={}, class={} to metricsEvaluators", providerType, metricsEvaluator::class.java.name)
val key = providerType.name + providerName
if (metricsEvaluators[key] == null) {
metricsEvaluators[key] = metricsEvaluator
} else {
logger.error("Duplicated metricsEvaluator definition for key: {}", key)
}
}
}
private fun loadAllMetricsDefinitions() {
// todo only load active ones
metricsRepository.findByStatus(MetricsStatus.ACTIVE).forEach {
metricsDefinitions[it.id] = it
}
}
open fun getMetricsEvaluator(metricsDefinition: MetricsDefinition): MetricsEvaluator? {
val metricsEvaluator = metricsEvaluators[metricsDefinition.type.name]
return Option.of(metricsEvaluator).orElse(
Option.of(metricsEvaluators["${metricsDefinition.type}${metricsDefinition.name}"])
).getOrElseThrow {
IllegalArgumentException("No metrics evaluator found for metrics id: ${metricsDefinition.id}, " +
"metrics type: ${metricsDefinition.type}")
}
}
}
With this change, the MetricsEvaluatorFactory is instantiated at finishBeanFactoryInitialization stage instead of invokeBeanFactoryPostProcessor stage. So all JPA dependencies can be resolved before the bean is created.
CONCLUSION:
@Bean annotation defined in config class may be instantiated at different stages depending on bean type. BeanFactoryPostProcessors are instantiated at “invokeBeanFactoryPostProcessor” stage, while other beans can be instantiated at "finishBeanFactoryInitialization" stage. We should try our best to avoid injecting constructor parameters to a spring BeanFactoryPostProcessor, which might implicitly impact bean loading order. We should also try to avoid invoking "getBean" method in BeanFactoryPostProcessor's postProcessBeanFactory method for the same reason. BeanFactoryPostProcessor should only manipulate BeanDefinition rather than actual bean instance.