Record a spring boot project startup failure troubleshooting and DubboReference source code analysis

Posted by Meltdown on Wed, 26 Jan 2022 16:34:14 +0100

Problem phenomenon

In our project, there is an internal two-party package with a class:
MvcInterceptorAutoConfiguration defines a Bean accessContextResolver. To generate this Bean, you need to automatically inject another Bean: accessContextService. The code is as follows:

public class MvcInterceptorAutoConfiguration implements WebMvcConfigurer, ApplicationContextAware {

	   @Bean
 	   public AccessContextResolver accessContextResolver(@Autowired AccessContextService accessContextService, @Autowired WebAuthConfig webAuthConfig) {
       	 return new DefaultAccessContextResolver(webAuthConfig, accessContextService);
   		}
}

In our project, there is another class: ProxyCenter, which uses
@DubboReference defines accessContextService. The code is as follows

@Component
public class ProxyCenter {

    @DubboReference(timeout = 10000, check = false, version = "1.0.0")
    private AccessContextService accessContextService;
    
    ...
}

However, the following errors are reported during the project startup

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method accessContextResolver in cn.xxx.xxx.xxx.xxx.config.MvcInterceptorAutoConfiguration required a bean of type 'cn.xxx.xxx.xxx.xxx.service.AccessContextService' that could not be found.


Action:

Consider defining a bean of type 'cn.xxx.xxx.xxx.xxx.service.AccessContextService' in your configuration.

This error may be familiar to everyone. It means that Spring needs to automatically inject the accessContextService Bean when creating the accessContextResolver Bean, but the Spring container cannot find the Bean, so the startup fails.

problem analysis

Dubbo version: 2.7.0

Analysis ideas

  • The essence of this problem is that @ Autowired cannot inject the Bean declared by @ DubboReference. The most important thing is to find out what @ DubboReference and @ Autowired do and when they do it respectively.
  • If you only use @ Autowired, the above situation will not occur, so we locate the direction of the problem and give priority to the implementation logic of @ DubboReference.

@DubboReference implementation logic analysis

background knowledge

Let's talk about a background knowledge first: we know that Spring needs to create a Bean through the steps of instantiating an object, property filling and initializing an object. The property filling is implemented in the populateBean method (the code is as follows). Here is a logic to obtain all beanpostprocessors in the Bean factory. If yes
If the instantiaawarebeanpostprocessor type, then the postProcessPropertyValues method is called.

be careful:
Instantiawarebeanpostprocessor is an abstract class. It does not provide the implementation of postProcessPropertyValues. All the implementations are in subclasses.

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
 
    ...
       
    Iterator var5 = this.getBeanPostProcessors().iterator();
 
     BeanPostProcessor bp = (BeanPostProcessor)var9.next();
     if (bp instanceof InstantiationAwareBeanPostProcessor) {
        InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor)bp;
        pvs = ibp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
        if (pvs == null) {
         return;
         }
    }
    
    ...
}

Here is
InstantiationAwareBeanPostProcessorAdapter implementation class. Here we only list the subclasses related to our problem this time

InstantiationAwareBeanPostProcessorAdapter

Autowiredannotationbeanpostprocessor (spring provides property / method injection Implementation)

|

AbstractAnnotationBeanPostProcessor [com.alibaba.spring...]

|

ReferenceAnnotationBeanPostProcessor [org.apache.dubbo...] (the @ dubboreference provided by Dubbo, the @ reference implementation)

From the above source code and class inheritance relationship, we can conclude that spring will call
The postProcessPropertyValues method of the ReferenceAnnotationBeanPostProcessor class. The ReferenceAnnotationBeanPostProcessor class is the post processor of the Bean provided by Dubbo, @ DubboReference, @Reference is implemented in this method.

Source code analysis

After understanding the above background knowledge, we begin to enter the source code analysis of @ DubboReference. Listed below are
ReferenceAnnotationBeanPostProcessor's implementation of postProcessPropertyValues.

We should note that the Bean being created at this time is proxyCenter. As for why the Bean is proxyCenter, this is very simple. In this case, accessContextService is the property of proxyCenter class, so the filling action of accessContextService property occurs when creating the Bean of proxyCenter.

 public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException {
       
     //Find object
     InjectionMetadata metadata = this.findInjectionMetadata(beanName, bean.getClass(), pvs);

        try {
            //Execute injection
            metadata.inject(bean, beanName, pvs);
            return pvs;
        } catch (BeanCreationException var7) {
            throw var7;
        } catch (Throwable var8) {
            throw new BeanCreationException(beanName, "Injection of @" + this.getAnnotationType().getSimpleName() + " dependencies is failed", var8);
        }
    }

The postProcessPropertyValues method mainly does two things:

1. Find @ DubboReference and @ Reference modifier attributes, and encapsulate metadata information in InjectionMetadata.

2. Perform injection. Here, the inject method calls
The inject method in AbstractAnnotationBeanPostProcessor is the inject method that executes the parent class.

Analysis idea: since this object was not found during automatic injection, that is, it is not found in the Spring container, it is possible that Dubbo generated a proxy object but did not put it in the Spring container, so it was not found during automatic injection. Therefore, we can look at the inject method first. (it is also confirmed later that there is no problem with findInjectionMetadata, so it will not be analyzed here.)

 protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
           
           Class<?> injectedType = this.resolveInjectedType(bean, this.field);
          
            //Generate proxy object
            Object injectedObject = AbstractAnnotationBeanPostProcessor.this.getInjectedObject(this.attributes, bean, beanName, injectedType, this);
            
     		     //Reflection, setting values for attributes
             // bean: proxyCenter
             // this.field: accessContextService
            ReflectionUtils.makeAccessible(this.field);
            this.field.set(bean, injectedObject);
        }

The inject method mainly generates a proxy object, and then sets values for the properties of the current object. That is, the generated proxy object accessContextResolver will be set to the property of the current Bean, that is, the Bean proxyCenter.

Analysis idea: however, our problem is not that the attribute of this Bean is null, but that we do not get the value of the object when Spring automatically injects, but the inject method does not involve the code of putting the proxy object accessContextResolver into the Spring container, so we can continue to look down. (the core logic of getInjectedObject method is in doGetInjectedBean, which only adds cache operation, so it is not listed here.)

 protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, String beanName, Class<?> injectedType,
                                       InjectionMetadata.InjectedElement injectedElement) throws Exception {
        
       ...
         
        ReferenceBean referenceBean = buildReferenceBeanIfAbsent(referenceBeanName, attributes, injectedType);
        
        //Judge whether the current Service is defined locally using @ DubboService or @ Service
        boolean localServiceBean = isLocalServiceBean(referencedBeanName, referenceBean, attributes);

        //If it is a local service and has not been registered, the early registration service will be triggered
        prepareReferenceBean(referencedBeanName, referenceBean, localServiceBean);

        //Putting the service information into the bean factory does not involve obtaining the real service
           //1. Local exposed services
           //2. Services that need to be read from the registry
        registerReferenceBean(referencedBeanName, referenceBean, attributes, localServiceBean, injectedType);

        //Get remote service
        return referenceBean.get();
    }

The doGetInjectedBean method is the core of the @ DubboReference implementation. Comments are written at each step here.

Analysis idea: here is a method, registerReferenceBean. As the name suggests, it should register ReferenceBean. Here, the registration should register the current Bean in the Bean factory, so the answer we need should be in this method. (ReferenceBean is an object that encapsulates the applicationContext and proxy object of the interface: ref, etc., where ref is the generated proxy object, such as @ DubboReference AService aService; then ref is the proxy object of aService. This object will be packaged as a ReferenceBean, so ReferenceBean can be roughly regarded as a service object Body.)

private void registerReferenceBean(String referencedBeanName, ReferenceBean referenceBean,
                                       AnnotationAttributes attributes,
                                       boolean localServiceBean, Class<?> interfaceClass) {

        ConfigurableListableBeanFactory beanFactory = getBeanFactory();

        String beanName = getReferenceBeanName(attributes, interfaceClass);

        //Case 1: @ Service is local
        if (localServiceBean) {  
            
            //If it is local, all the information of the service is local,
            AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) beanFactory.getBeanDefinition(referencedBeanName);
            RuntimeBeanReference runtimeBeanReference = (RuntimeBeanReference) beanDefinition.getPropertyValues().get("ref");
            //  @bean name corresponding to Service
            String serviceBeanName = runtimeBeanReference.getBeanName();
            
            // Instead of creating a new bean, the bean generated by @ Service is used to avoid bean duplication
            beanFactory.registerAlias(serviceBeanName, beanName);
            
        } else { 
            
            //@Service is remote
            if (!beanFactory.containsBean(beanName)) {
                
                //spring dynamically registers bean s
                beanFactory.registerSingleton(beanName, referenceBean);
            }
        }
    }

registerReferenceBean this method mainly registers the ReferenceBean in Spring. So far, we have also found our answer, that is: @ DubboReference will put the generated proxy object into the Spring container, and the trigger time is in the process of creating the Bean corresponding to the @ DubboReference modifier attribute. In other words, as long as the Bean is not created, the attribute modified by @ DubboReference will not be placed in the Spring container.

The above steps are represented by the flow chart as follows:

summary

The above is the time when @ DubboReference is triggered during Spring startup, that is, when Spring creates a Bean, if the attribute modified by @ DubboReference is found in the attribute filling phase,
ReferenceAnnotationBeanPostProcessor, the Bean post processor, will create the proxy object referenced by the service and put it into the Spring container.

Analysis idea: so the problem at the beginning of the article can be understood as: when Spring creates the Bean proxyCenter, it will instantiate the accessContextService object and put it into the Spring container, but when @ Autowired is used to inject accessContextService, it does not find the Bean. The most likely reason for this is that Spring first uses @ Autowired to inject accessContextService, and then creates the Bean proxyCenter.

We know that when @ Autowired automatically injects, if the Bean does not exist, the process of creating the Bean will be triggered. Let's analyze the implementation logic of @ Autowired and why the object here is null.

@Logic analysis of Autowired implementation

Note: due to the complex implementation logic of @ Autowired, the codes listed below are related to this case, and other codes will be omitted accordingly.

Analysis ideas

  • @Autowired annotation can modify attributes, methods, input parameters, etc. @ Autowired functions on different objects, and the processing time is also different. For example, when @ Autowired modifies attributes or methods, it is processed when the attributes are filled. In this case, @ Autowired is processed when instantiating beans.
@Bean
 public AccessContextResolver accessContextResolver(@Autowired AccessContextService accessContextService, @Autowired WebAuthConfig webAuthConfig) {
   return new DefaultAccessContextResolver(webAuthConfig, accessContextService);
}

Source code analysis

In the process of Bean instantiation, there is a step: createArgumentArray. Here is a case of creating an automatic injection parameter: constructorresolver#resolveautowiredaregument. This is the entry point of this case analysis. Since many of the following logic are irrelevant to this case, this part of the code will not be listed. You can check it yourself. Let's start with the method of doResolveDependency.

Note: beanName refers to the name of the Bean currently to be created, not the name of the Bean automatically injected. In this case, it refers to accessContextResolver rather than accessContextService. You can see the code above.

@Nullable
	public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
                 
            //31 processing ordinary bean key: the name of the bean automatically injected; value: class object or concrete bean
			Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
			if (matchingBeans.isEmpty()) {
                //If @ Autowire (required=true) is not obtained according to the bean name, an exception will be reported
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
                //If it is @ Autowire (required=false), null will be returned directly
				return null;
			}

			String autowiredBeanName; //Name of the automatically injected bean
			Object instanceCandidate; //Automatically injected objects

            //If more than one object is found based on the bean name
			if (matchingBeans.size() > 1) {
                // @Primary - > @ priority - > method name or field name match
				autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
				if (autowiredBeanName == null) {
					if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
						return descriptor.resolveNotUnique(type, matchingBeans);
					}
					else {
						return null;
					}
				}
				instanceCandidate = matchingBeans.get(autowiredBeanName);
			}
			else {
				// According to the type, only one bean information is found, so this is the object we want
				Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
				autowiredBeanName = entry.getKey();
				instanceCandidate = entry.getValue();
			}

			if (autowiredBeanNames != null) {
				autowiredBeanNames.add(autowiredBeanName);
			}
            
            //It is used to determine whether the returned bean is a created bean or just a class. If it is a class, the logic of creating a bean needs to be executed to obtain the real bean object
            //Because what is needed during injection is an object, class is useless
			if (instanceCandidate instanceof Class) {
                //This is actually the execution of getBean() logic
				instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
			}
			Object result = instanceCandidate;
			if (result instanceof NullBean) {
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
				result = null;
			}
			if (!ClassUtils.isAssignableValue(type, result)) {
				throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
			}
			return result;
		}
	}

doResolveDependency is the core method of dependency injection. It does the following things:

1. Handle parameters decorated with @ Value

2. Handle multiplebeans, that is, objects decorated with List, Map, Array and Set. For example: @ Autowire private List aServiceList; This situation.

3. Processing common injection

31. First: find all Bean names according to the type and put them in the map. findAutowireCandidates returns a map. The key of the map is the Bean name, and the value is the Bean that has been created, or the Bean that has not been created. The returned object is a class object.

32. matchingBeans size > 1: that is, multiple beans can be found for the same type. One is to generate multiple objects from the same class, such as most data sources, and there are multiple implementations of one interface. Only the interface is injected during injection. At this time, the first one will be selected according to the priority (@ primary - > @ priority)

33. matchingBeans size =1: This is the object we need.

34. Judge whether this object is an instance of class. If so, then create a Bean

4. Finally, this object is returned.

Analysis idea: in step [31], there will be a problem. If the Bean information cannot be found according to the type, if this is still @ Autowire(require=true), raiseNoMatchingBeanFound will be executed, and some exceptions will be reported. Personally, I wonder if the error report when we start here is because the AccessContextService does not find the corresponding Bean information according to the type, so the error is reported? Let's move on to the implementation logic of findAutowireCandidates. (although there are some scenarios below that will report errors, they are not consistent with the situation of this case)

protected Map<String, Object> findAutowireCandidates(
			@Nullable String beanName, Class<?> requiredType, DependencyDescriptor descriptor) {

        //Find all bean names of this type according to the requiredType
		String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
				this, requiredType, true, descriptor.isEager());
    
		Map<String, Object> result = new LinkedHashMap<>(candidateNames.length);
    
		//Find the corresponding bean or class object according to the bean name and put it in the map. Note: if this bean has not been instantiated, it will not be instantiated in advance
        ...
            
		return result;
	}

The findAutowireCandidates method is a process of finding all Bean names and objects according to the type.

Analysis idea: there may be problems in both parts:

1. When searching all Bean names, the Bean name is not found;

2. Find the corresponding Bean or class object by name;

Give priority to whether the first step is to find the Bean name according to the type. When we just analyzed @ DubboReference, there was a piece of code: it is a dynamic registration Bean, and the Bean name will be put into the manualSingletonNames object during the registration process. But the time to put it in is when the proxyCenter is created.

beanFactory.registerSingleton(beanName, referenceBean);


//registerSingleton implementation logic
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {

	...
  
  if (!this.beanDefinitionMap.containsKey(beanName)) {
				this.manualSingletonNames.add(beanName);
		
	}

Let's focus on how Spring finds all Bean names. The main logic is in the doGetBeanNamesForType method.

// Include nonsinglets: does it include non simple interest
//Alloweegerinit: handle the of factoryBean
private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
		List<String> result = new ArrayList<>();

		// The definition of all beans in the loop bean factory
		for (String beanName : this.beanDefinitionNames) {
		
			if (!isAlias(beanName)) {
				try {
                    
					RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                    
                    //Beans are non abstract
					if (!mbd.isAbstract() && 
                        //
                        (allowEagerInit ||(mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading()) &&
                         //factoryBean related processing
						!requiresEagerInitForType(mbd.getFactoryBeanName()))) {
						
                        //Determine whether it is a factoryBean
                        boolean isFactoryBean = isFactoryBean(beanName, mbd);
						BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
                      
                        //Condition 1: non factoryBean, dbd and non lazy loading, or the bean already exists in the simple interest pool
                        //Condition 2: it contains non simple interest, or the bean is simple interest
                        //Condition 3: the type is consistent with the type of the current bean
						boolean matchFound =
								(allowEagerInit || !isFactoryBean ||(dbd != null && !mbd.isLazyInit()) || containsSingleton(beanName)) &&
								(includeNonSingletons ||(dbd != null ? mbd.isSingleton() : isSingleton(beanName))) &&
								isTypeMatch(beanName, type);
                        
                        //Processing factoryBean
						if (!matchFound && isFactoryBean) {
							beanName = FACTORY_BEAN_PREFIX + beanName;
							matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
						}
                        
                        //After matching, it is put into the collection and returned later
						if (matchFound) {
							result.add(beanName);
						}
					}
				}
				catch (CannotLoadBeanClassException ex) {
					//exception handling
					onSuppressedException(ex);
				}
				catch (BeanDefinitionStoreException ex) {
					//exception handling
					onSuppressedException(ex);
				}
			}
		}


      // Handle manually registered bean registerSingleton(beanName,Object)
      //In addition to scanning some annotations, such as @ Service and @ composition, spring can also be manually registered in the code
      for (String beanName : this.manualSingletonNames) {
			....
		}

		return StringUtils.toStringArray(result);
	}

doGetBeanNamesForType finds all Bean names that match the Bean type. Note: ordinary beans and factorybeans are included here. Meanwhile, the matching beans here include automatic registration and manual registration. We can see that two objects are looped here: beanDefinitionNames and manualSingletonNames

The Bean information in the beanDefinitionNames object is generated by Spring scanning annotations like @ component and @ Service in the project during initialization. Our AccessContextService will certainly not be in this object, because AccessContextService is not a Bean defined according to the Bean specification defined by Spring, manualSingletonNames is put in when registerSingleton is called.

summary

So far, the reason for the problem is clear: spring starts with @ Autowired first. At this time, it mainly injects beans of the type AccessContextService, but it is not a Bean defined by using the methods of defining beans provided by spring such as @ Service and @ composition, Therefore, there will be no definition information of any Bean of AccessContextService in the spring container. At this time, the proxyCenter object has not been instantiated and property filling has not occurred. The proxy object of AccessContextService class has not been injected into the spring environment, so it is impossible to obtain the AccessContextService type object. Spring starts with an error.

Solution ideas

1. The core problem of this case is: the use of beans takes precedence over the creation of beans, but this Bean is not a Bean defined according to the Spring specification, so there is no way to create it when the automatic injection cannot be found. So we just need to ensure that we create beans first and then inject beans.

Based on this, the solution can be: let the Bean of proxyCenter instantiate before the accessContextResolver, because the attribute will be filled during creation. At this time, the instantiation of the remote service AccessContextService will be triggered. However, the creation of spring beans is out of order. How can these two beans be created in a certain order?

In Spring, you can use the @ DependsOn annotation to create a Bean prior to another Bean, but in our case, accessContextResolver is in a two-party package, and adding @ DependsOn is not realistic. Therefore, we can define a beanfactoryprocessor and manually modify the BeanDefinition corresponding to accessContextResolver to solve the problem. The code is as follows:

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("accessContextResolver");
        beanDefinition.setDependsOn("proxyCenter");
    }
}

Personal thinking

The above solution is not very elegant. Dubbo uses Bean post processor to implement @ DubboReference, which has defects. @ DubboReference is not a Bean defined by Spring, so it will not generate BeanDefinition, that is, it will not actively create a Bean, and can only be triggered during attribute injection, which will lead to this problem in this article. I think a better implementation is to create all the objects corresponding to @ DubboReference in advance before Spring instantiates any Bean, and then use them when Spring creates a Bean, so the above problems will not occur.

The above problem Dubbo has been solved in the subsequent version (3.0.0), so our previous problems can also be solved by upgrading the Dubbo version. As for how Dubbo solves this problem later, we don't talk about the modified implementation logic here. If you have a rise, you can look at the source code by yourself.

Author: Zhengcai cloud technology team
Link:
https://juejin.cn/post/7041345416792637471

Topics: Java Spring Spring Boot