NeuroAgent

Why does Spring use PropertyEditors instead of Converters for @Value

Learn why Spring Framework uses PropertyEditors instead of Converters for the @Value annotation. Historical context, internal implementation, and solutions for custom types.

Question

Why are PropertyEditors used for @Value instead of Converters?

Good day!

According to the documentation, for converting values from @Value to the required field type, ConversionService is used. To convert a value from @Value to a custom type, you need to add your own Converter to the ConversionService (for example, to DefaultConversionService).

In practice, I encountered an exception when trying to convert a String value from @Value to a long field value. The exception trace looks like this (incomplete):

java.lang.NumberFormatException: For input string: "userfileminsize"atjava.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) [na:na]atjava.base/java.lang.Long.parseLong(Long.java:697) [na:na]atjava.base/java.lang.Long.valueOf(Long.java:1163) [na:na]atorg.springframework.util.NumberUtils.parseNumber(NumberUtils.java:204) [springcore6.2.11.jar:6.2.11]atorg.springframework.beans.propertyeditors.CustomNumberEditor.setAsText(CustomNumberEditor.java:115) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:439) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:412) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:161) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:80) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:71) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1672) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643) [springbeans6.2.11.jar:6.2.11]atorg.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor{user-file-min-size}" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) ~[na:na] at java.base/java.lang.Long.parseLong(Long.java:697) ~[na:na] at java.base/java.lang.Long.valueOf(Long.java:1163) ~[na:na] at org.springframework.util.NumberUtils.parseNumber(NumberUtils.java:204) ~[spring-core-6.2.11.jar:6.2.11] at org.springframework.beans.propertyeditors.CustomNumberEditor.setAsText(CustomNumberEditor.java:115) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.TypeConverterDelegate.doConvertTextValue(TypeConverterDelegate.java:439) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.TypeConverterDelegate.doConvertValue(TypeConverterDelegate.java:412) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:161) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:80) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:71) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1672) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1643) ~[spring-beans-6.2.11.jar:6.2.11] at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessorAutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:785) ~[spring-beans-6.2.11.jar:6.2.11]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject

From this, it’s clear that AutowiredAnnotationBeanPostProcessor uses DefaultListableBeanFactory, which calls TypeConverterDelegate, which in turn uses CustomNumberEditor. There’s no ConversionService being used, contrary to what the documentation states.

I’m using Spring Boot 3.5.6.

NeuroAgent

Spring Uses PropertyEditors for @Value Annotations

Spring uses PropertyEditors for processing @Value annotations for historical reasons and due to the internal implementation of the dependency injection mechanism. This is because the Spring Framework gradually evolved from PropertyEditors to Converters, but for backward compatibility and in certain scenarios, PropertyEditors are still utilized.

Contents

Historical Context of Type Conversion System Evolution

PropertyEditors are part of the JavaBeans specification, which existed long before Spring. The Spring Framework was initially built on top of this infrastructure, and PropertyEditors became the standard way to convert string values to other data types.

PropertyEditors were defined in the JavaBeans specification and were initially used by Spring to convert text values to object types.

With the introduction of Spring 3.0, a more modern approach was introduced - Converters. Converters solved several key problems of PropertyEditors:

  1. Thread Safety: PropertyEditors are stateful and not thread-safe, which caused issues in multi-threaded environments
  2. Flexibility: Converters can convert between any types, not just between String and Object
  3. Clear Typing: Converters explicitly specify source and target types in the method signature

Despite these advantages, Spring maintained support for PropertyEditors for backward compatibility.

Internal Implementation of @Value and ConversionService Role

As seen in your stack trace, AutowiredAnnotationBeanPostProcessor uses DefaultListableBeanFactory, which calls TypeConverterDelegate. This is a key point of understanding:

java
// From your stack trace:
AutowiredAnnotationBeanPostProcessor
    -> DefaultListableBeanFactory
        -> TypeConverterDelegate
            -> CustomNumberEditor

It’s important to note that while the documentation mentions ConversionService, Spring’s internal implementation uses more complex logic:

  1. AutowiredAnnotationBeanPostProcessor processes @Value annotations
  2. DefaultListableBeanFactory resolves dependencies through TypeConverterDelegate
  3. TypeConverterDelegate uses a combination of PropertyEditors and Converters

As stated in the official Spring documentation: “A Spring BeanPostProcessor uses a ConversionService behind the scenes to handle the process for converting the String value in @Value to the target type”. However, in practice, this occurs through the intermediate TypeConverterDelegate layer.

Why PropertyEditors Are Still Used

The main reasons why PropertyEditors are used instead of Converters in your case are:

1. Built-in Conversions for Standard Types

For standard data types (like long in your case), Spring uses built-in PropertyEditors such as CustomNumberEditor. This happens because:

  • Built-in conversions for primitive types and their wrappers are already implemented through PropertyEditors
  • No additional converter registration is required for simple types
  • The PropertyEditors system is deeply integrated into Spring’s infrastructure

2. Conversion Resolution Order

Spring uses the following order for resolving conversions:

  1. Specific PropertyEditors - for specific types
  2. Configurable PropertyEditors - through CustomEditorConfigurer
  3. ConversionService - as the primary mechanism
  4. Standard Conversions - through PropertyEditors

In your case, CustomNumberEditor is triggered at step 1, as it’s a built-in converter for numeric types.

3. Backward Compatibility

The Spring Framework maintains support for the old approach to ensure compatibility with existing projects and libraries.

ConversionService Configuration for @Value

To force Spring to use Converters instead of PropertyEditors for @Value annotations, you need to properly configure ConversionService:

1. Creating a Custom Converter

java
public class StringToLongConverter implements Converter<String, Long> {
    @Override
    public Long convert(String source) {
        if (source == null || source.trim().isEmpty()) {
            return null;
        }
        try {
            return Long.parseLong(source);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("Invalid long value: " + source, e);
        }
    }
}

2. Registering ConversionService

java
@Configuration
public class ConversionConfig {
    
    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = 
            new DefaultFormattingConversionService();
        conversionService.addConverter(new StringToLongConverter());
        return conversionService;
    }
}

3. Including in Spring MVC Configuration

If you’re using Spring MVC, ensure that ConversionService is properly configured:

xml
<mvc:annotation-driven conversion-service="conversionService"/>

Spring Boot Solution

In Spring Boot, the situation is simplified due to automatic configuration:

1. Automatic Converter Registration

Spring Boot automatically registers Converters marked with special annotations:

java
@ConfigurationPropertiesBinding
public class StringToLongConverter implements Converter<String, Long> {
    // convert() implementation
}

2. Configuration through application.properties

For simple cases, you can use Spring Boot’s built-in mechanisms:

properties
# Enabling automatic ConversionService configuration
spring.mvc.converters.preferred-json-mapper=jackson

3. Using @ConfigurationProperties

An alternative approach is to use @ConfigurationProperties instead of @Value:

java
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private long userFileMinSize;
    
    // getters and setters
}

Comparison of Approaches

Characteristic PropertyEditors Converters
Thread Safety ❌ Not thread-safe ✅ Thread-safe
Conversion Types String ↔ Object Any types ↔ Any types
Scope of Application JavaBeans, Spring Core Spring Core, Spring MVC
Configuration Through CustomEditorConfigurer Through ConversionService
Support in @Value ✅ Yes ✅ Yes (requires configuration)
Performance Lower Higher

Usage Recommendations

1. For Standard Types

Use Spring’s built-in mechanisms. For numeric types like in your case, it’s better to use:

java
@Value("${user-file-min-size:0}")
private long userFileMinSize;

2. For Custom Types

When working with custom types, use Converters:

java
// 1. Create a Converter
public class StringToMyTypeConverter implements Converter<String, MyType> {
    @Override
    public MyType convert(String source) {
        // conversion logic
    }
}

// 2. Register with @ConfigurationPropertiesBinding
@ConfigurationPropertiesBinding
@Bean
public Converter<String, MyType> myTypeConverter() {
    return new StringToMyTypeConverter();
}

3. For Complex Scenarios

If you need complex conversion logic, consider using a Formatter:

java
public class MyTypeFormatter implements Formatter<MyType> {
    @Override
    public MyType parse(String text, Locale locale) {
        // parsing logic
    }
    
    @Override
    public String print(MyType object, Locale locale) {
        // formatting logic
    }
}

4. For Spring Boot Applications

Use Spring Boot’s automatic configuration as effectively as possible. Spring Boot automatically detects and registers Converters marked with the @ConfigurationPropertiesBinding annotation.

Sources

  1. Spring Framework Reference - Type Conversion
  2. Spring Framework Reference - Using @Value
  3. Stack Overflow - Spring MVC type conversion: PropertyEditor or Converter?
  4. TheServerSide - Spring Converters and Formatters
  5. Prasanth Gullapalli - Type Conversion in Spring
  6. Stack Overflow - What is the difference between PropertyEditor, Formatter and Converter in Spring?
  7. LogicBig - Spring - Property Editors
  8. Spring Boot Features - Properties Conversion

Conclusion

  1. PropertyEditors are used for @Value due to historical compatibility and deep integration into Spring’s infrastructure, especially for standard data types.

  2. ConversionService is indeed utilized by Spring behind the scenes, but through the intermediate TypeConverterDelegate layer, which may use PropertyEditors for built-in conversions.

  3. For custom types, it’s recommended to use Converters, which are thread-safe and more flexible, but require proper configuration through ConversionService.

  4. Spring Boot simplifies the process by automatically registering Converters through the @ConfigurationPropertiesBinding annotation.

  5. The choice of approach depends on the complexity of the conversion: for simple types, you can use built-in mechanisms, for complex ones - implement custom Converters.