01 Spring Bean Common Definition Errors

01 Spring Bean Common Definition Errors #

Hello, I am Fu Jian.

As we know from the introduction, the core of Spring revolves around beans. Whether it is Spring Boot or Spring Cloud, any technology with the keyword “Spring” cannot escape from beans. And in order to use a bean, it is essential to first define it. Therefore, defining a bean becomes particularly important.

Of course, for such an important task, Spring naturally provides us with many simple and easy-to-use methods. However, this simplicity is due to Spring’s principle of " convention over configuration “. But we may not always be clear about all the conventions, so we still make some classic mistakes in bean definition.

Next, let’s take a look at those classic errors and the principles behind them. You can compare them to see if you have made any of these mistakes before, and how you resolved them.

Case 1: Implicitly Scanning for Undefined Beans #

When building web services, we often use Spring Boot for rapid development. For example, we can use the following package structure and code to create a simple web version of HelloWorld:

The Application class responsible for launching the program is defined as follows:

package com.spring.puzzle.class1.example1.application
// omit imports
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

The HelloWorldController code for providing the interface is as follows:

package com.spring.puzzle.class1.example1.application
// omit imports
@RestController
public class HelloWorldController {
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld";
    };
}

The above code can achieve a simple functionality: accessing http://localhost:8080/hi will return “helloworld”. The two key classes are located in the same package (i.e., application). Because HelloWorldController is annotated with @RestController, it is recognized as a Controller Bean.

However, let’s say one day we need to add multiple similar controllers and want to organize them in a clearer package structure. We may create a separate Controller package outside the application package and adjust the class locations. After the adjustment, the structure would look like this:

In fact, we didn’t change any code, just the package structure. However, we find that this web application no longer works, meaning HelloWorldController cannot be recognized as a Bean anymore. Why is that?

Case Analysis #

To understand why HelloWorldController fails to work, we need to understand how it worked before. For Spring Boot, the key point is the use of the @SpringBootApplication annotation in Application.java. This annotation extends several other annotations, as defined below:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
      @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
// omit non-essential code
}

From the definition, we can see that SpringBootApplication enables many features, one of which is ComponentScan. Referencing its configuration:

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)

When Spring Boot starts, the activation of ComponentScan means it will scan for all defined Beans. Where does it scan? This is specified by the basePackages attribute in the ComponentScan annotation. Refer to the definition below:

public @interface ComponentScan {

/**
* Base packages to scan for annotated components.
* <p>{@link #value} is an alias for (and mutually exclusive with) this
* attribute.
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor("value")
String[] basePackages() default {};
// omit other non-essential code
}

In our case, we directly use the ComponentScan defined by @SpringBootApplication, and its basePackages attribute is not specified, so it is empty (i.e., {}). What package does it scan? Let’s debug it with this question in mind (debugging position refers to the ComponentScanAnnotationParser#parse method), and the debugging view is as follows:

From the above image, we can see that when basePackages is empty, the scanned package is the package where declaringClass is located. In the present case, the declaringClass is Application.class, so the scanned package is actually its own package, i.e., com.spring.puzzle.class1.example1.application.

Comparing the package structure before and after the reorganization, we naturally find the root cause of this issue: before the adjustment, HelloWorldController was within the scanning range, but after the adjustment, it was far from the scanning range (no longer in the same package as Application.java). Although the code remains unchanged, this feature no longer works.

Therefore, looking at the whole picture, this issue is caused by our insufficient understanding of Spring Boot’s default scanning rules. We have only enjoyed its convenience without understanding the story behind it, so any slight change may make it difficult to function properly.

Problem Resolution #

Having analyzed the source code with this case, we can quickly find a solution. Of course, the solution is not to move HelloWorldController back to its original location, but to truly meet the requirements. In this case, the real solution is to explicitly configure @ComponentScan. The specific modification is as follows:

@SpringBootApplication
@ComponentScan("com.spring.puzzle.class1.example1.controller")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

With this modification, we explicitly specify the scanning range as com.spring.puzzle.class1.example1.controller. However, it should be noted that after explicit specification, the default scanning range (i.e., com.spring.puzzle.class1.example1.application) will not be added. Alternatively, we can also fix the problem using @ComponentScans. The usage is as follows:

@ComponentScans(value = { @ComponentScan(value = “com.spring.puzzle.class1.example1.controller”) })

As the name suggests, you can see that ComponentScans supports multiple package scanning range specifications, compared to ComponentScan, by adding an “s”.

At this point, you may notice: if you lack an understanding of the source code, it is easy to overlook certain things. Taking ComponentScan as an example, the original code scanned the default package while ignoring other packages; once we explicitly specify other packages, the original default scanning package will be ignored.

Case 2: Missing Implicit Dependency in Bean Definition #

When we are first learning Spring, it is often difficult to switch our thinking quickly. For example, during the process of program development, sometimes we define a class as a Bean, and at the same time, we feel that this Bean definition is not much different from adding some Spring annotations. So when we later use it, sometimes we define it without thinking, for example, we may write code like this:

@Service
public class ServiceImpl {

    private String serviceName;

    public ServiceImpl(String serviceName){
        this.serviceName = serviceName;
    }

}

ServiceImpl becomes a Bean because it is marked with @Service. In addition, we have explicitly defined a constructor for ServiceImpl. However, the above code does not always run correctly and sometimes reports the following error:

Parameter 0 of constructor in com.spring.puzzle.class1.example2.ServiceImpl required a bean of type ‘java.lang.String’ that could not be found.

So how does this error occur? Let’s analyze it.

Case Analysis #

When creating a Bean, the method called is AbstractAutowireCapableBeanFactory#createBeanInstance. It mainly consists of two basic steps: finding the constructor and using reflection to create an instance. For this case, the most important code execution can be referenced in the following code snippet:

// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
      mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
   return autowireConstructor(beanName, mbd, ctors, args);
}

First, Spring executes the determineConstructorsFromBeanPostProcessors method to obtain the constructors, and then uses the autowireConstructor method to create an instance with the constructors. Obviously, in this case, there is only one constructor, so it is very easy to track this issue.

The autowireConstructor method needs to create an instance, not only need to know which constructor to use, but also need to know the corresponding parameters for the constructor. This can be seen from the method name to create an instance, as shown below (i.e., ConstructorResolver#instantiate):

private Object instantiate(
      String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse) 

So how do we get the argsToUse, which is the constructor parameter when we already know the constructor ServiceImpl(String serviceName) and want to create an instance of ServiceImpl?

Obviously, here we are using Spring, and we cannot directly use the new keyword to create an instance. Spring can only look for dependencies to be used as constructor invocation parameters.

So how do we get this parameter? You can refer to the code snippet below (i.e., ConstructorResolver#autowireConstructor):

argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
      getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);

We can call the createArgumentArray method to build an array of parameters to call the constructor, and the final implementation of this method is to get the Bean from the BeanFactory. You can refer to the following code:

return this.beanFactory.resolveDependency(
  new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);

If we use the debug view, we can see more information:

As shown in the figure, the above call is to find the corresponding Bean based on the parameter. In this case, if the corresponding Bean cannot be found, an exception will be thrown, indicating assembly failure.

Problem Fix #

After understanding the cause of the error from the source code level, now let’s reflect on why this error occurs. Going back to the root cause, as mentioned at the beginning, because we are not aware of many implicit rules: when we define a class as a Bean, if we explicitly define a constructor, then when building this Bean, Spring will automatically search for the corresponding Bean based on the constructor parameters and then create this Bean by reflection.

Once we understand these implicit rules, fixing this problem becomes much simpler. We can directly define a Bean that can be autowired into the constructor parameter of ServiceImpl. For example, define the following:

// This bean will be autowired into the constructor parameter "serviceName" of ServiceImpl
@Bean
public String serviceName(){
    return "MyServiceName";
}

Run the program again and everything works fine.

Therefore, when using Spring, do not always think that the defined Bean can also be directly used with the new keyword outside the Spring context. This way of thinking is not feasible.

In addition, similarly, if we do not understand the implicit rules of Spring and try to fix the problem, we may write more seemingly executable code, such as:

@Service
public class ServiceImpl {
    private String serviceName;
    public ServiceImpl(String serviceName){
        this.serviceName = serviceName;
    }
    public ServiceImpl(String serviceName, String otherStringParameter){
        this.serviceName = serviceName;
    }
}

If we still review this code with a non-Spring mindset, we may not think there is any problem, because the String type can be autowired, and it just adds an extra String type parameter.

However, if you understand that Spring uses reflection to construct Beans internally, you will quickly realize the problem: there are two constructors that can be called, so which one should be used? In the end, Spring has no way to choose and can only try to call the default constructor, but this default constructor does not exist. Therefore, when testing this program, it will throw an error.

Case 3: Prototype Bean being Fixed #

Next, let’s take a look at another case where the bean definition doesn’t take effect. Sometimes, when defining a bean, we use a prototype bean, like this:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}

And then we use it in the following way:

@RestController
public class HelloWorldController {

    @Autowired
    private ServiceImpl serviceImpl;

    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld, service is : " + serviceImpl;
    };
}

The result is that no matter how many times we access http://localhost:8080/hi, the result remains the same, as follows:

helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af

Obviously, this is contrary to the intended purpose of defining ServiceImpl as a prototype bean. How should we understand this phenomenon?

Case Analysis #

When a property member serviceImpl is declared as @Autowired, creating the HelloWorldController bean will first create an instance using reflection, and then inject the dependencies into all properties marked as @Autowired (see the populateBean method in AbstractAutowireCapableBeanFactory for the injection process).

During the execution, many BeanPostProcessor are used to complete the process. One of them is AutowiredAnnotationBeanPostProcessor, which uses DefaultListableBeanFactory#findAutowireCandidates to find a bean of type ServiceImpl and then injects it into the corresponding property (serviceImpl) (see the inject method in AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement):

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
   Field field = (Field) this.member;
   Object value;
   // Find 'bean'
   if (this.cached) {
      value = resolvedCachedArgument(beanName, this.cachedFieldValue);
   }
   else {
     // Ignoring other non-critical code
     value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
   }
   if (value != null) {
      // Set the bean to the member field
      ReflectionUtils.makeAccessible(field);
      field.set(bean, value);
   }
}

Once we find the bean to be autowired, we can use reflection to set it to the corresponding field. However, this field is only executed once, so it becomes fixed and will not change even if ServiceImpl is marked as SCOPE_PROTOTYPE.

Therefore, when a singleton bean uses the @Autowired annotation to inject a property, you must be aware that the value of this property will be fixed.

Fixing the Issue #

From the above analysis of the source code, we can see that to fix this issue, we definitely need to avoid fixing the ServiceImpl bean to the property, but instead get a new instance every time it is used. So here are two ways to fix this issue:

1. Autowire the Context

In other words, autowire the ApplicationContext and then define a getServiceImpl() method that retrieves a new instance of ServiceImpl in the method. The fixed code is as follows:

@RestController
public class HelloWorldController {

    @Autowired
    private ApplicationContext applicationContext;

    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld, service is : " + getServiceImpl();
    };
 
    public ServiceImpl getServiceImpl(){
        return applicationContext.getBean(ServiceImpl.class);
    }

}

2. Use the Lookup Annotation

Similar to the first fix, we also add a getServiceImpl() method, but this method is marked with the @Lookup annotation. The fixed code is as follows:

@RestController
public class HelloWorldController {
 
    @RequestMapping(path = "hi", method = RequestMethod.GET)
    public String hi(){
         return "helloworld, service is : " + getServiceImpl();
    };

    @Lookup
    public ServiceImpl getServiceImpl(){
        return null;
    }  

}

By using these two fixes, when we test the program again, we will find that the result meets our expectations (a new bean is created every time we access this interface).

Let’s further discuss how the Lookup annotation works. After all, in the fixed code, we see that the implementation of the getServiceImpl() method returns null, which may be difficult to convince oneself.

First, we can debug the execution of the method, as shown in the following diagram:

From the above diagram, we can see that our method execution finally goes into CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor because it is marked with LookupOverride. The key implementation of this method can be found in LookupOverrideMethodInterceptor#intercept:

private final BeanFactory owner;

public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
   LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
   Assert.state(lo != null, "LookupOverride not found");
   Object[] argsToUse = (args.length > 0 ? args : null);  // if no-arg, don't insist on args at all
   if (StringUtils.hasText(lo.getBeanName())) {
      return (argsToUse != null ? this.owner.getBean(lo.getBeanName(), argsToUse) :
            this.owner.getBean(lo.getBeanName()));
   }
   else {
      return (argsToUse != null ? this.owner.getBean(method.getReturnType(), argsToUse) :
            this.owner.getBean(method.getReturnType()));
   }
}

The method execution does not enter the return null statement in the example implementation, but instead gets the bean from the BeanFactory. So from this point, we can also see that the specific implementation of the getServiceImpl() method is not important.

For example, we can use the following implementation to test this conclusion:

@Lookup
public ServiceImpl getServiceImpl(){
    // Will this log be output?
    log.info("executing this method");
    return null;
}

In the above code, we added a line of code to output a log message. After testing, we found that there is no log output. This also confirms that when a method is annotated with Lookup, the specific implementation of the method is not important.

Looking back at the previous analysis, why do we go into the class created by CGLIB? This is because we have a method marked with Lookup. This can be verified from the following code (see instantiate method in SimpleInstantiationStrategy):

@Override
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
   // Don't override the class with CGLIB if no overrides.
   if (!bd.hasMethodOverrides()) {
      //
      return BeanUtils.instantiateClass(constructorToUse);
   }
   else {
      // Must generate CGLIB subclass.
      return instantiateWithMethodInjection(bd, beanName, owner);
   }
}

In the above code, if hasMethodOverrides is true, CGLIB is used. In this case, the condition is met because when parsing the HelloWorldController bean, we find that a method is marked with Lookup, and this method is added to the methodOverrides property (this process is completed by AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors).

The effect after adding is shown in the following diagram:

The above is the key implementation of Lookup. There are many details, such as how the CGLIB subclass is generated, which cannot be explained one by one. If you are interested, you can further study it. Leave a comment and stay tuned.

Key Summary #

In this lesson, we discussed three classic errors regarding Bean definitions and analyzed the underlying principles.

It is not difficult to see that in order to use Spring well, it is necessary to understand some of its implicit rules, such as the default scope of Bean scanning and automatic wiring of constructors, etc. If we do not understand these rules, although it may still work in most cases, a slight change can render it completely ineffective. For example, in case one, when we simply move the Controller from one package to another, the interface becomes invalid.

Furthermore, through the analysis of these three cases, we can also perceive that many of Spring’s implementations are done through reflection, and understanding this point will be helpful in understanding its source code implementation. For example, in case two, why defining multiple constructors may cause an error is because creating instances using reflection requires a clear indication of which constructor to use.

Finally, I would like to say that in the Spring framework, there are often multiple ways to solve a problem, so do not be limited to a fixed routine. Just like in case three, both using ApplicationContext and Lookup annotation can solve the problem of fixed prototype Beans.

Thought Question #

In Case 2, we discussed the behavior of looking for the corresponding bean based on constructor parameters when a constructor is explicitly defined. Now, let’s think about a question: if the corresponding bean cannot be found, will it always raise an error directly as in Case 2?

Let’s try to solve this question together in the comments section!