НейроАгент

Почему Spring использует PropertyEditors вместо Converters для @Value

Узнайте, почему Spring Framework использует PropertyEditors вместо Converters для аннотации @Value. Исторический контекст, внутренняя реализация и решения для кастомных типов.

Вопрос

Почему для @Value используются PropertyEditors, а не Converter?

Добрый день!

В документации сказано, что для преобразование значение из @Value в необходимый тип поля используется ConversionService. Для преобразования значения из @Value в кастомный тип необходимо добавить свой Converter в ConversionService (например, в DefaultConvetsionService).

На деле столкнулся, что при преобразование String значения из @Value в long значение поля, выбрасывается исключение, трассировка для которого выглядит так (неполная)

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

Отсюда видно, что AutowiredAnnotationBeanPostProcessor использует DefaultListableBeanFactory, который вызывает TypeConverterDelegate, который в свою очередь использует CustomNumberEditor. Никакого ConversionService нет, как об этом пишут в документации.

Использую Spring Boot 3.5.6

НейроАгент

Spring использует PropertyEditors для обработки @Value аннотаций по историческим причинам и из-за внутренней реализации механизма внедрения зависимостей. Это связано с тем, что Spring Framework постепенно эволюционировал от PropertyEditors к Converters, но для обратной совместимости и в определенных сценариях PropertyEditors все еще задействуются.

Содержание

Исторический контекст эволюции системы преобразования типов

PropertyEditors являются частью JavaBeans спецификации, которая существовала задолго до появления Spring. Spring Framework изначально строился поверх этой инфраструктуры, и PropertyEditors стали стандартным способом преобразования строковых значений в другие типы данных.

PropertyEditors были определены в JavaBeans спецификации и изначально использовались Spring для преобразования текстовых значений в типы объектов.

С появлением Spring 3.0 был введен более современный подход - Converters. Converters решали несколько ключевых проблем PropertyEditors:

  1. Безопасность для многопоточности: PropertyEditors являются stateful (состоятельными) и не потокобезопасными, что создавало проблемы в многопоточных средах
  2. Гибкость: Converters могут преобразовывать между любыми типами, а не только между String и Object
  3. Четкая типизация: Converters явно указывают исходный и целевой типы в сигнатуре метода

Несмотря на эти преимущества, Spring сохранил поддержку PropertyEditors для обратной совместимости.

Внутренняя реализация @Value и роль ConversionService

Как видно из вашей трассировки стека, AutowiredAnnotationBeanPostProcessor использует DefaultListableBeanFactory, который вызывает TypeConverterDelegate. Это ключевой момент понимания:

java
// Из вашего стека вызовов:
AutowiredAnnotationBeanPostProcessor
    -> DefaultListableBeanFactory
        -> TypeConverterDelegate
            -> CustomNumberEditor

Важно отметить, что хотя документация упомина ConversionService, внутренняя реализация Spring использует более сложную логику:

  1. AutowiredAnnotationBeanPostProcessor обрабатывает @Value аннотации
  2. DefaultListableBeanFactory разрешает зависимости через TypeConverterDelegate
  3. TypeConverterDelegate использует комбинацию PropertyEditors и Converters

Как указано в официальной документации Spring: “A Spring BeanPostProcessor uses a ConversionService behind the scenes to handle the process for converting the String value in @Value to the target type”. Однако на практике это происходит через промежуточный слой TypeConverterDelegate.

Почему PropertyEditors все еще используются

Основные причины, почему PropertyEditors задействуются вместо Converters в вашем случае:

1. Встроенные преобразования для стандартных типов

Для стандартных типов данных (как long в вашем случае) Spring использует встроенные PropertyEditors, такие как CustomNumberEditor. Это происходит потому, что:

  • Встроенные преобразования для примитивных типов и их оберток уже реализованы через PropertyEditors
  • Для простых типов не требуется регистрация дополнительных конвертеров
  • Система PropertyEditors глубоко интегрирована в инфраструктуру Spring

2. Порядок разрешения преобразований

Spring использует следующий порядок для разрешения преобразований:

  1. Специфичные PropertyEditors - для конкретных типов
  2. Конфигурируемые PropertyEditors - через CustomEditorConfigurer
  3. ConversionService - как основной механизм
  4. Стандартные преобразования - через PropertyEditors

В вашем случае CustomNumberEditor срабатывает на шаге 1, так как это встроенный преобразователь для числовых типов.

3. Обратная совместимость

Spring Framework сохраняет поддержку старого подхода для обеспечения совместимости с существующими проектами и библиотеками.

Конфигурация ConversionService для @Value

Чтобы заставить Spring использовать Converters вместо PropertyEditors для @Value аннотаций, необходимо правильно настроить ConversionService:

1. Создание кастомного 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. Регистрация ConversionService

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

3. Включение в Spring MVC конфигурацию

Если вы используете Spring MVC, убедитесь, что ConversionService правильно настроен:

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

Решение проблемы с Spring Boot

В Spring Boot ситуация упрощается благодаря автоматической конфигурации:

1. Автоматическая регистрация Converters

Spring Boot автоматически регистрирует Converters, помеченные специальными аннотациями:

java
@ConfigurationPropertiesBinding
public class StringToLongConverter implements Converter<String, Long> {
    // реализация convert()
}

2. Настройка через application.properties

Для простых случаев можно использовать встроенные механизмы Spring Boot:

properties
# Включение автоматической конфигурации ConversionService
spring.mvc.converters.preferred-json-mapper=jackson

3. Использование @ConfigurationProperties

Альтернативный подход - использовать @ConfigurationProperties вместо @Value:

java
@Configuration
@ConfigurationProperties(prefix = "app")
public class AppProperties {
    private long userFileMinSize;
    
    // геттеры и сеттеры
}

Сравнение подходов

Характеристика PropertyEditors Converters
Потокобезопасность ❌ Не потокобезопасны ✅ Потокобезопасны
Типы преобразования String ↔ Object Любые типы ↔ Любые типы
Область применения JavaBeans, Spring Core Spring Core, Spring MVC
Конфигурация Через CustomEditorConfigurer Через ConversionService
Поддержка в @Value ✅ Да ✅ Да (требует настройки)
Производительность Ниже Выше

Рекомендации по использованию

1. Для стандартных типов

Используйте встроенные механизмы Spring. Для числовых типов, как в вашем случае, лучше использовать:

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

2. Для кастомных типов

При работе с кастомными типами используйте Converters:

java
// 1. Создайте Converter
public class StringToMyTypeConverter implements Converter<String, MyType> {
    @Override
    public MyType convert(String source) {
        // логика преобразования
    }
}

// 2. Зарегистрируйте с @ConfigurationPropertiesBinding
@ConfigurationPropertiesBinding
@Bean
public Converter<String, MyType> myTypeConverter() {
    return new StringToMyTypeConverter();
}

3. Для сложных сценариев

Если вам нужна сложная логика преобразования, рассмотрите использование Formatter:

java
public class MyTypeFormatter implements Formatter<MyType> {
    @Override
    public MyType parse(String text, Locale locale) {
        // логика разбора
    }
    
    @Override
    public String print(MyType object, Locale locale) {
        // логика форматирования
    }
}

4. Для Spring Boot приложений

Используйте автоматическую конфигурацию Spring Boot максимально эффективно. Spring Boot автоматически обнаруживает и регистрирует Converters, помеченные аннотацией @ConfigurationPropertiesBinding.

Источники

  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

Заключение

  1. PropertyEditors используются для @Value из-за исторической совместимости и глубокой интеграции в инфраструктуру Spring, особенно для стандартных типов данных.

  2. ConversionService действительно задействуется Spring за кулисами, но через промежуточный слой TypeConverterDelegate, который может использовать PropertyEditors для встроенных преобразований.

  3. Для кастомных типов рекомендуется использовать Converters, которые потокобезопасны и более гибки, но требуют правильной конфигурации через ConversionService.

  4. Spring Boot упрощает процесс автоматической регистрацией Converters через аннотацию @ConfigurationPropertiesBinding.

  5. Выбор подхода зависит от сложности преобразования: для простых типов можно использовать встроенные механизмы, для сложных - реализовывать собственные Converters.