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.
由一个奇怪的SPRINGBOOT错误衍生开_第1张图片

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.
由一个奇怪的SPRINGBOOT错误衍生开_第2张图片

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.
由一个奇怪的SPRINGBOOT错误衍生开_第3张图片