Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Deep Dive into Spring Bean Property Injection and Initialization

Tech May 14 1

Following the instantiation phase discussed in the previous section, the Spring container moves on to the critical steps of populating the bean instance with dependencies and performing initialization routines. Although the raw object exists, it is not yet functional because its properties are unset and initialization logic hasn't run. This article explores the mechanisms of property injection and initialization, transforming a plain object into a managed Spring Bean.

The populateBean Phase

Once a bean instance is created, the populateBean method is invoked. This method is responsible for assigning property values defined in the BeanDefinition or resolving autowiring dependencies. It handles explicit property values, legacy autowiring modes, and annotation-driven injection via post-processors.

Below is a refactored representation of the core logic within this method:

protected void assignProperties(String beanId, RootBeanDefinition beanDef, @Nullable BeanWrapper wrapper) {
    PropertyValues propVals = beanDef.hasPropertyValues() ? beanDef.getPropertyValues() : null;

    int resolvedMode = beanDef.getResolvedAutowireMode();
    // Handle legacy XML autowiring (byName or byType)
    if (resolvedMode == AUTOWIRE_BY_NAME || resolvedMode == AUTOWIRE_BY_TYPE) {
        MutablePropertyValues mutableProps = new MutablePropertyValues(propVals);
        
        if (resolvedMode == AUTOWIRE_BY_NAME) {
            handleAutowireByName(beanId, beanDef, wrapper, mutableProps);
        }
        if (resolvedMode == AUTOWIRE_BY_TYPE) {
            handleAutowireByType(beanId, beanDef, wrapper, mutableProps);
        }
        propVals = mutableProps;
    }

    boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
    PropertyDescriptor[] filteredPds = null;

    if (hasInstAwareBpps) {
        if (propVals == null) {
            propVals = beanDef.getPropertyValues();
        }
        
        // Iterate over post-processors to handle annotations like @Autowired
        for (InstantiationAwareBeanPostProcessor processor : getBeanPostProcessorCache().instantiationAware) {
            PropertyValues processedProps = processor.postProcessProperties(propVals, wrapper.getWrappedInstance(), beanId);
            if (processedProps == null) {
                if (filteredPds == null) {
                    filteredPds = filterPropertyDescriptorsForDependencyCheck(wrapper, beanDef.allowCaching);
                }
                processedProps = processor.postProcessPropertyValues(propVals, filteredPds, wrapper.getWrappedInstance(), beanId);
                if (processedProps == null) {
                    return;
                }
            }
            propVals = processedProps;
        }
    }

    // Apply the final property values to the bean instance
    if (propVals != null) {
        applyPropertyValues(beanId, beanDef, wrapper, propVals);
    }
}

Handling PropertyValues

The PropertyValues object typically comes from the BeanDefinition. While standard beans might not use this directly, developers can manually inject properties by implementing MergedBeanDefinitionPostProcessor. For instance, one could intercept the definition of a specific bean and add a property value programmatically:

@Component
public class CustomDefinitionPostProcessor implements InstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor {

    @Override
    public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class> beanType, String beanName) {
        if ("userService".equals(beanName)) {
            beanDefinition.setPropertyValues(
                new MutablePropertyValues().add("orderService", new OrderServiceImpl())
            );
        }
    }
}

For this injection to work, the target class requires a corresponding setter method (e.g., setOrderService).

Legacy Autowiring by Name and Type

It is crucial to distinguish between the @Autowired annotation and the legacy autowiring modes defined in XML (e.g., autowire="byName"). These legacy modes scan all setter methods within the class indiscriminately.

Consider the autowireByName logic:

protected void handleAutowireByName(String id, AbstractBeanDefinition bd, BeanWrapper bw, MutablePropertyValues pvs) {
    // Identify properties requiring injection
    String[] propertyNames = findNonSimpleProperties(bd, bw);
    
    for (String propName : propertyNames) {
        if (containsBean(propName)) {
            Object dependency = getBean(propName);
            pvs.add(propName, dependency);
            // Register the dependency relationship
            registerDependentBean(propName, id);
        }
    }
}

Unlike @Autowired, which offers fine-grained control, legacy autowiring relies on naming conventions or type matching and applies to all setters. While less common in modern annotation-based configurations, it remains a core part of the container's flexibility.

Annotation-Based Injection via PostProcessors

The modern standard for dependency injection relies on the AutowiredAnnotationBeanPostProcessor. During the populateBean phase, this processor scans fields and methods for @Autowired, @Value, and @Inject annotations.

The following logic demonstrates how the injection metadata is constructed:

private InjectionMetadata resolveInjectionMetadata(final Class> targetClass) {
    if (!AnnotationUtils.isCandidateClass(targetClass, this.autowiredAnnotationTypes)) {
        return InjectionMetadata.EMPTY;
    }

    List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
    Class<?> currentClass = targetClass;

    do {
        final List<InjectionMetadata.InjectedElement> currentElements = new ArrayList<>();

        ReflectionUtils.doWithLocalFields(currentClass, field -> {
            MergedAnnotation<?> ann = findAutowiredAnnotation(field);
            if (ann != null) {
                if (Modifier.isStatic(field.getModifiers())) {
                    return; // Static fields are ignored
                }
                boolean required = determineRequiredStatus(ann);
                currentElements.add(new AutowiredFieldElement(field, required));
            }
        });

        ReflectionUtils.doWithLocalMethods(currentClass, method -> {
            Method bridged = BridgeMethodResolver.findBridgedMethod(method);
            if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridged)) {
                return;
            }
            MergedAnnotation<?> ann = findAutowiredAnnotation(bridged);
            if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, targetClass))) {
                if (Modifier.isStatic(method.getModifiers())) {
                    return; // Static methods are ignored
                }
                PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridged, targetClass);
                boolean required = determineRequiredStatus(ann);
                currentElements.add(new AutowiredMethodElement(method, required, pd));
            }
        });

        elements.addAll(0, currentElements);
        currentClass = currentClass.getSuperclass();
    } while (currentClass != null && currentClass != Object.class);

    return InjectionMetadata.forElements(elements, targetClass);
}

It is worth noting that if a property is already populated via PropertyValues, the injection logic for @Autowired on the corresponding setter method will be skipped to avoid conflicts. How ever, the applyPropertyValues step ultimately applies the PropertyValues to the bean, potentially overrriding values previously set by annotation processors if explicit configurations exist.

The initializeBean Phase

After properties are set, the container proceeds to initialization. This phase prepares the bean for use, invoking callback methods and applying AOP proxies if necessary. The entry point is the initializeBean method:

protected Object setupBean(String beanId, Object beanInstance, @Nullable RootBeanDefinition beanDef) {
    invokeAwareInterfaces(beanId, beanInstance);

    Object wrappedBean = beanInstance;

    // Phase 1: Post-processors before initialization
    if (beanDef == null || !beanDef.isSynthetic()) {
        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanId);
    }

    // Phase 2: Invoke custom init methods
    try {
        invokeInitializationMethods(beanId, wrappedBean, beanDef);
    } catch (Throwable ex) {
        throw new BeanCreationException(
            (beanDef != null ? beanDef.getResourceDescription() : null),
            beanId, "Invocation of init method failed", ex);
    }

    // Phase 3: Post-processors after initialization (AOP typically happens here)
    if (beanDef == null || !beanDef.isSynthetic()) {
        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanId);
    }

    return wrappedBean;
}

Aware Interface Callbacks

The first step invokes standard Aware interfaces. This allows the bean to obtain information about its own name, class loader, or the managing factory:

private void invokeAwareInterfaces(String id, Object instance) {
    if (instance instanceof Aware) {
        if (instance instanceof BeanNameAware) {
            ((BeanNameAware) instance).setBeanName(id);
        }
        if (instance instanceof BeanClassLoaderAware) {
            ClassLoader loader = getBeanClassLoader();
            if (loader != null) {
                ((BeanClassLoaderAware) instance).setBeanClassLoader(loader);
            }
        }
        if (instance instanceof BeanFactoryAware) {
            ((BeanFactoryAware) instance).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
        }
    }
}

Pre-Initialization Processing

The method applyBeanPostProcessorsBeforeInitialization is a key extension point. Two important processors act here:

  • ApplicationContextAwareProcessor: Triggers callbacks for other Aware interfaces like EnvironmentAware, ApplicationContextAware, etc.
  • InitDestroyAnnotationBeanPostProcessor: Searches for methods annotated with @PostConstruct and invokes them. This includes building lifecycle metadata and executing the relevant methods on the bean instance.

Invoking Initialization Methods

Spring supports two initialization mechanisms: 1. The InitializingBean interface, specifically the afterPropertiesSet() method. 2. A custom init-method defined in the BeanDefinition.

The container logic prioritizes these as follows:

protected void invokeInitializationMethods(String id, Object instance, @Nullable RootBeanDefinition beanDef) throws Throwable {
    boolean isInitBean = (instance instanceof InitializingBean);
    
    // 1. Check InitializingBean interface
    if (isInitBean && (beanDef == null || !beanDef.isExternallyManagedInitMethod("afterPropertiesSet"))) {
        ((InitializingBean) instance).afterPropertiesSet();
    }

    // 2. Check custom init-method
    if (beanDef != null && instance.getClass() != NullBean.class) {
        String customInitMethod = beanDef.getInitMethodName();
        if (StringUtils.hasLength(customInitMethod) &&
                !(isInitBean && "afterPropertiesSet".equals(customInitMethod)) &&
                !beanDef.isExternallyManagedInitMethod(customInitMethod)) {
            invokeCustomInitMethod(id, instance, beanDef);
        }
    }
}

Post-Initialization and AOP

Finally, applyBeanPostProcessorsAfterInitialization is executed. This is the standard hook for Aspect-Oriented Programming (AOP). If a bean requires a proxy (for transactions or security), it is usually created here. The proxy replaces the original instance, becoming the object registered in the application context.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.