03 Spring Bean Dependency Injection Common Errors Part 2

03 Spring Bean Dependency Injection Common Errors Part 2 #

Hello, I’m Fu Jian. In this lesson, let’s continue discussing Spring’s autowiring.

In the previous lecture, we introduced three common errors in dependency injection in Spring programming. If you analyze them closely, you’ll find that they mostly revolve around the use of @Autowired and @Qualifier, and the types being autowired are ordinary object types.

In practical applications, we also use less common annotations, such as @Value, for autowiring. There are also scenarios where we need to inject into collections, arrays, and other complex types. In these cases, we may encounter some problems. So in this lecture, let’s organize them. One scenario where @Value may not inject the expected value occurs when we use it to inject an object member property. Typically, we use @Autowired for injection, but sometimes we use @Value. However, these two annotations have different usage styles. In general, @Autowired does not set property values, while @Value must specify a string value because its definition requires it. The definition code is as follows:

public @interface Value {

   /**
    * The actual value expression &mdash; for example, <code>#{systemProperties.myProp}</code>.
    */
   String value();

}

In comparing these two annotations, we often mistakenly believe that @Value cannot be used for non-primitive object injection because @Value is commonly used for string type injection. In reality, this is a common misconception. For example, we can use the following method to autowire a property member:

@Value("#{student}")
private Student student;

Where the student Bean is defined as follows:

@Bean
public Student student(){
    Student student = createStudent(1, "xie");
    return student;
}

Of course, as mentioned earlier, we use @Value primarily for string injection, and it supports various powerful injection methods. Typical examples include:

// Register a normal string
@Value("I am a string")
private String text;

// Inject system parameters, environment variables, or values in the configuration file
@Value("${ip}")
private String ip;

// Inject other Bean properties, where student is the ID of the bean and name is its property
@Value("#{student.name}")
private String name;

Above, I briefly introduced the powerful features of @Value and its differences with @Autowired. So what are the potential errors when using @Value? Here, I’ll share a typical error, which occurs when @Value injects an unexpected value.

We can simulate a scenario where we configure a property in the application.properties file as follows:

username=admin
password=pass

Then, in a Bean, we define two properties to reference them:

@RestController
@Slf4j
public class ValueTestController {
    @Value("${username}")
    private String username;
    @Value("${password}")
    private String password;
 
    @RequestMapping(path = "user", method = RequestMethod.GET)
    public String getUser(){
       return username + ":" + password;
    };
}

When we print the username and password in the above code, we’ll find that the password is returned correctly, but the username is not admin as specified in the configuration file. Instead, it returns the username of the computer running this program. Obviously, the value injected using @Value does not fully meet our expectations.

Analysis #

By analyzing the runtime result, we can see that the usage of @Value should be correct, because the password field is successfully injected. However, why didn’t the username field take effect as the correct value? Let’s analyze it in detail.

First, let’s understand how Spring queries the “value” based on @Value. We can start by studying the core workflow of @Value in the DefaultListableBeanFactory#doResolveDependency method. The code is as follows:

@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
      @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    // Omitted non-critical code
    Class<?> type = descriptor.getDependencyType();
      // Find @Value
      Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
      if (value != null) {
         if (value instanceof String) {
            // Resolve the value
            String strVal = resolveEmbeddedValue((String) value);
            BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                  getMergedBeanDefinition(beanName) : null);
            value = evaluateBeanDefinitionString(strVal, bd);
         }
         
         // Convert the result of value resolution to the injection type
         TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
         try {
            return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
         }
         catch (UnsupportedOperationException ex) {
            // Exception handling
         }
}

} //Omit other non-essential code }

As we can see, the work of @Value can be roughly divided into the following three core steps.

  1. Find @Value

In this step, the main task is to determine whether the property field is marked with @Value. The method used for this validation is QualifierAnnotationAutowireCandidateResolver#findValue:

@Nullable
protected Object findValue(Annotation[] annotationsToSearch) {
   if (annotationsToSearch.length > 0) {  
      AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes(
            AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType);
      //valueAnnotationType is @Value
      if (attr != null) {
         return extractValue(attr);
      }
   }
   return null;
}
  1. Parse the @Value string value

If a field is marked with @Value, the corresponding string value can be obtained, and then this string value can be parsed. The final parsed result may be a string or an object, depending on how the string is written.

  1. Convert the parsed result to the type of the object to be assembled

After obtaining the result generated in step 2, we may find that it does not match the type of the object we want to assemble. In the case where we define a UUID but obtain a string as the result, a converter is used to convert the string to a UUID. The conversion from string to UUID actually occurs in UUIDEditor:

public class UUIDEditor extends PropertyEditorSupport {

   @Override
   public void setAsText(String text) throws IllegalArgumentException {
      if (StringUtils.hasText(text)) {
         //Conversion operation
         setValue(UUID.fromString(text.trim()));
      }
      else {
         setValue(null);
      }
   }
   //Omit other non-essential code
      
}

By analyzing these key steps, we can gain a general understanding of the workflow of @Value. In combination with our case, it is obvious that the problem should occur in step 2, which is the process of parsing the specified string value of @Value. The relevant code execution process is as follows:

String strVal = resolveEmbeddedValue((String) value);

In this case, it is actually parsing the embedded value, which is essentially “placeholder replacement” work. Specifically, it uses PropertySourcesPlaceholderConfigurer to replace based on PropertySources. However, when using ${username} to obtain the replacement value, the actual lookup and replacement process does not only occur in the application.properties file. By debugging, we can see that the following “sources” are the basis for replacement:

Replacement basis sources

To be more specific, let’s take a look at how it is executed (in PropertySourcesPropertyResolver#getProperty):

@Nullable
protected <T> T getProperty(String key, Class<T> targetValueType, boolean resolveNestedPlaceholders) {
   if (this.propertySources != null) {
      for (PropertySource<?> propertySource : this.propertySources) {
         Object value = propertySource.getProperty(key);
         if (value != null) {
         //Exit once the value is found
         return convertValueIfNecessary(value, targetValueType);
         }
      }
   }
 
   return null;
}

From this, we can see that when parsing the Value string, there is actually an order (the sources to be searched are stored in a CopyOnWriteArrayList and are ordered and fixed at startup). The search is performed by executing one source after another, and once the value is found in one of the sources, it can be directly returned.

If we take a look at the systemEnvironment source, we will find that it happens to have a username that overlaps with ours and its value is not pass.

Overlapping username

Therefore, now that we have reached this point, you should know where the problem lies, right? This is an accidental case, where the system environment variable (systemEnvironment) happens to contain a configuration with the same name. Actually, the same issue occurs with system parameters (systemProperties). These parameters or variables have many possible values, and if we use a string with the same name as the value for @Value without being aware of its existence, it is easy to encounter this kind of problem.

Problem Fixes #

With the source code analysis, we can quickly find a solution for this case. For example, we can avoid using the same name. The specific modification is as follows:

user.name=admin
user.password=pass

However, if we make this change, it still won’t work. In fact, through the previous debugging method, we can find similar reasons. The systemProperties PropertiesPropertySource happens to have a user.name similar to ours. It seems that there is no avoiding it. Therefore, when naming, we must pay attention to not only avoiding conflicts with environment variables, but also avoiding conflicts with system variables and other variables. Only then can we fundamentally solve this problem.

Through this case, we can see that while Spring provides us with many useful features, when these features are intertwined, we may accidentally fall into some pitfalls. Only by understanding how it operates can we quickly identify and solve problems.

Case 2: Confused Collection Injection #

In the previous cases, we introduced many examples of injection errors, but these cases were limited to injections of a single type and did not mention injections of collection types. In fact, automatic injection of collection types is another powerful feature provided by Spring.

Let’s assume we have a requirement to find and store multiple student beans in a list. The definition of multiple student beans is as follows:

@Bean
public Student student1(){
    return createStudent(1, "xie");
}

@Bean
public Student student2(){
    return createStudent(2, "fang");
}

private Student createStudent(int id, String name) {
    Student student = new Student();
    student.setId(id);
    student.setName(name);
    return student;
}

With the injection of collection types, we can collect scattered student beans. The code example is as follows:

@RestController
@Slf4j
public class StudentController {

    private List<Student> students;

    public StudentController(List<Student> students){
        this.students = students;
    }

    @RequestMapping(path = "students", method = RequestMethod.GET)
    public String listStudents(){
       return students.toString();
    };

}

With the above code, we can complete the injection of collection types and the output is as follows:

[Student(id=1, name=xie), Student(id=2, name=fang)]

However, business is always complex and requirements are constantly changing. When we continue to add some students, we may not like to inject collection types in this way, but prefer to use the following method to complete the injection:

@Bean
public List<Student> students(){
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student3, student4);
} 

For ease of remembering, we can call the above method “Direct Assembly” and the previous method “Collection Assembly”.

In fact, if these two methods coexist, there is naturally no problem and they can both work. But what happens if we accidentally let these two methods exist at the same time?

At this point, many people would think that Spring is powerful and should be able to merge the results above, or believe that the direct assembly result should prevail. However, when we run the program, we find that the injection method in the latter has not taken effect at all. It still returns the 2 students defined earlier. Why did this error occur?

Case Analysis #

To understand the root cause of this error, you need to first understand how these two injection styles are implemented in Spring. For the collection assembly style, Spring uses DefaultListableBeanFactory#resolveMultipleBeans to complete the assembly. The key code for this case is as follows:

private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName,
      @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {
   final Class<?> type = descriptor.getDependencyType();
   if (descriptor instanceof StreamDependencyDescriptor) {
      // Assemble stream
      return stream;
   }
   else if (type.isArray()) {
      // Assemble array
      return result;
   }
   else if (Collection.class.isAssignableFrom(type) && type.isInterface()) {
      // Assemble collection
      // Get the element type of the collection
      Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();
      if (elementType == null) {
         return null;
      }
      // Find all beans based on the element type
      Map<String, Object> matchingBeans = findAutowireCandidates(beanName, elementType,
            new MultiElementDescriptor(descriptor));
      if (matchingBeans.isEmpty()) {
         return null;
if (autowiredBeanNames != null) {
  autowiredBeanNames.addAll(matchingBeans.keySet());
}
// Add all matching beans to the autowiredBeanNames set
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
Object result = converter.convertIfNecessary(matchingBeans.values(), type);
// Convert all matching beans to the target type and store them in result
// Omitting non-essential code
return result;

In this code snippet, we can summarize the overall process of the collection autowiring mechanism.

1. Get the element type of the collection type

In our case, the target type is defined as List students, so the element type is Student. We can obtain the element type with the following code:

Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric();

2. Find all beans based on the element type

With the element type determined in the previous step, we can find all matching beans based on the element type. The key code is as follows:

Map matchingBeans = findAutowireCandidates(beanName, elementType, new MultiElementDescriptor(descriptor));

3. Convert all matching beans to the target type

After obtaining the matching beans, they are stored as java.util.LinkedHashMap.LinkedValues, which is likely different from our target type. Thus, in the last step, we need to convert them according to our needs. In this case, we need to convert them to a List. The key conversion code is as follows:

Object result = converter.convertIfNecessary(matchingBeans.values(), type);

The actual conversion is performed by the CollectionToCollectionConverter.

Having understood the process of the collection autowiring mechanism, let’s take a look at the process of the direct autowiring mechanism, which we mentioned in previous lessons (i.e., the DefaultListableBeanFactory#findAutowireCandidates method). We won’t go into detail about the execution process.

Knowing the execution process, the next step is to find matching beans based on the target type. In this case, we want to autowire the List named students to the StudentController#students property.

Now, let’s consider how Spring handles the simultaneous satisfaction of both autowiring mechanisms. We can refer to the key code snippet in the DefaultListableBeanFactory#doResolveDependency method:

Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
   return multipleBeans;
}
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);

Obviously, these two autowiring mechanisms cannot coexist. In this case, if any matching bean can be found using the collection autowiring mechanism, it will be returned. Only if no matching bean is found, the direct autowiring mechanism will be used. This is why the Student beans directly added as a List in later stages are not effective.

Problem Correction #

Correcting this issue becomes much simpler now - you must consciously avoid using both of these mechanisms to autowire a collection. Just choose one of them to resolve the issue. For example, in this case, you can use the direct autowiring mechanism to correct the problem with the following code:

@Bean
public List<Student> students(){
    Student student1 = createStudent(1, "xie");
    Student student2 = createStudent(2, "fang");
    Student student3 = createStudent(3, "liu");
    Student student4 = createStudent(4, "fu");
    return Arrays.asList(student1, student2, student3, student4);
}

Or, you can use the collection autowiring mechanism to correct the problem with the following code:

@Bean
public Student student1(){
    return createStudent(1, "xie");
}
@Bean
public Student student2(){
    return createStudent(2, "fang");
}
@Bean
public Student student3(){
    return createStudent(3, "liu");
}
@Bean
public Student student4(){
    return createStudent(4, "fu");
}

In any case, both methods will work. It is important to note that mixing multiple autowiring mechanisms for the same collection object is not recommended, as it will only lead to confusion with no benefit gained.

Key Takeaways #

Today we learned about two typical cases of Spring automatic injection.

Through Case 1, we learned that @Value can not only be used to inject String types, but also custom object types. At the same time, when injecting String, you must be aware that it can not only refer to values configured in the configuration file, but also refer to environment variables, system parameters, etc.

Through Case 2, we learned that there are two common ways to inject collection types, namely the named collection assembly method and the direct assembly method mentioned earlier. When these two methods are used to assemble the same property together, the latter will be invalid.

Considering the content from the previous lesson, we analyzed a total of five questions and the principles behind them. Through the analysis of these cases, we can see that Spring’s automatic injection is very powerful. With the built-in annotations @Autowired, @Qualifier, @Value, we can achieve different injection goals and requirements. However, as mentioned in my opening words, this power is built upon many implicit rules. Only when you master these rules can you avoid problems effectively.

Reflection Question #

In Case 2, the initial result we obtained when running the program is as follows:

[Student(id=1, name=xie), Student(id=2, name=fang)]

So how can we ensure that Student 2 is printed first?

Please leave your thoughts in the comments section!