Как говорилось в предыдущем разделе, core.convert – это система преобразования типов общего назначения. Она предоставляет унифицированный API-интерфейс ConversionService, а также строго типизированный SPI-интерфейс Converter для реализации логики преобразования из одного типа в другой. Контейнер Spring использует эту систему для привязки значений свойств бина. Кроме того, как язык выражений Spring Expression Language (SpEL), так и DataBinder используют эту систему для привязки значений полей. Например, если SpEL необходимо преобразовать Short в Long для завершения попытки expression.setValue(Object bean, Object value), система core.convert выполняет это преобразование.

Теперь рассмотрим требования к преобразованию типов в типичной клиентской среде, такой как веб-приложение или приложение для настольных систем. В таких средах преобразование из String обычно осуществляется для обеспечения процесса обратной передачи клиентской части, а также обратно в String для обеспечения процесса визуализации представления. Кроме того, зачастую требуется локализация значений String. Более общий SPI-интерфейс core.convert Converter не удовлетворяет такие требования к форматированию напрямую. Для их непосредственного удовлетворения в Spring 3 появился удобный SPI-интерфейс Formatter, который обеспечивает простую и надежную альтернативу реализации PropertyEditor для клиентских сред.

В целом, можно использовать SPI-интерфейс Converter, если вам нужно реализовать логику преобразования типов общего назначения – например, для осуществления преобразования между java.util.Date и Long. Можно использовать SPI-интерфейс Formatter, если вы работаете в клиентской среде (например, в веб-приложении) и необходимо провести синтаксический анализ и вывести локализованные значения полей. ConversionService предоставляет единый API-интерфейс преобразования типов для обоих SPI-интерфейсов.

SPI-интерфейс Formatter

SPI-интерфейс Formatter для реализации логики форматирования полей является простым и строго типизированным. В следующем листинге показано определение интерфейса Formatter:

package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter расширяется из интерфейсов конструктивных блоков Printer и Parser. В следующем листинге показаны определения этих двух интерфейсов:

public interface Printer<T> {
    String print(T fieldValue, Locale locale);
}
import java.text.ParseException;
public interface Parser<T> {
    T parse(String clientValue, Locale locale) throws ParseException;
}

Чтобы создать свой собственный Formatter, реализуйте интерфейс Formatter, показанный ранее. Параметризируйте T как тип объекта, который вы хотите отформатировать – например, java.util.Date. Реализуйте операцию print(), чтобы вывести экземпляр T для отображения в локали клиента. Реализуйте операцию parse(), чтобы осуществить синтаксический разбор экземпляра T из форматированного представления, возвращаемого из клиентской локали. Ваш Formatter должен генерировать исключения ParseException или IllegalArgumentException при неудачной попытке синтаксического анализа. Озаботьтесь, чтобы ваша реализация Formatter была потокобезопасной.

Подпакеты format для удобства содержат несколько реализаций Formatter. Пакет Number содержит NumberStyleFormatter, CurrencyStyleFormatter и PercentStyleFormatter для форматирования объектов Number, которые используют java.text.NumberFormat. Пакет datetime содержит DateFormatter для форматирования объектов java.util.Date с помощью java.text.DateFormat.

Следующий DateFormatter является примером реализации Formatter:

Java
package org.springframework.format.datetime;
public final class DateFormatter implements Formatter<Date> {
    private String pattern;
    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }
    public String print(Date date, Locale locale) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }
    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }
    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }
}
Kotlin
class DateFormatter(private val pattern: String) : Formatter<Date> {
    override fun print(date: Date, locale: Locale)
            = getDateFormat(locale).format(date)
    @Throws(ParseException::class)
    override fun parse(formatted: String, locale: Locale)
            = getDateFormat(locale).parse(formatted)
    protected fun getDateFormat(locale: Locale): DateFormat {
        val dateFormat = SimpleDateFormat(this.pattern, locale)
        dateFormat.isLenient = false
        return dateFormat
    }
}

Команда Spring приветствует участие сообщества в разработке Formatter. Для внесения своих предложений см. "GitHub Issues".

Форматирование, управляемое аннотациями

Форматирование полей можно сконфигурировать по типу поля или аннотации. Чтобы привязать аннотацию к Formatter, реализуйте AnnotationFormatterFactory. В следующем листинге показано определение интерфейса AnnotationFormatterFactory:

package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
    Set<Class<?>> getFieldTypes();
    Printer<?> getPrinter(A annotation, Class<?> fieldType);
    Parser<?> getParser(A annotation, Class<?> fieldType);
}

Чтобы создать реализацию:

  1. Параметризируйте A как annotationType поля, с которым вы хотите связать логику форматирования – например, org.springframework.format.annotation.DateTimeFormat.

  2. Пусть getFieldTypes() возвращает типы полей, для которых может быть использована аннотация.

  3. Пусть getPrinter() возвращает Printer для вывода значения аннотированного поля.

  4. Пусть getParser() возвращает Parser для синтаксического анализа clientValue для аннотированного поля.

Следующий пример реализации AnnotationFormatterFactory привязывает аннотацию @NumberFormat к форматировщику, чтобы можно было указать стиль или шаблон нумерации:

Java
public final class NumberFormatAnnotationFormatterFactory
        implements AnnotationFormatterFactory<NumberFormat> {
    public Set<Class<?>> getFieldTypes() {
        return new HashSet<Class<?>>(asList(new Class<?>[] {
            Short.class, Integer.class, Long.class, Float.class,
            Double.class, BigDecimal.class, BigInteger.class }));
    }
    public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }
    public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
        return configureFormatterFrom(annotation, fieldType);
    }
    private Formatter<Number> configureFormatterFrom(NumberFormat annotation, Class<?> fieldType) {
        if (!annotation.pattern().isEmpty()) {
            return new NumberStyleFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentStyleFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyStyleFormatter();
            } else {
                return new NumberStyleFormatter();
            }
        }
    }
}
Kotlin
class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory<NumberFormat> {
    override fun getFieldTypes(): Set<Class<*>> {
        return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java)
    }
    override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer<Number> {
        return configureFormatterFrom(annotation, fieldType)
    }
    override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser<Number> {
        return configureFormatterFrom(annotation, fieldType)
    }
    private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter<Number> {
        return if (annotation.pattern.isNotEmpty()) {
            NumberStyleFormatter(annotation.pattern)
        } else {
            val style = annotation.style
            when {
                style === NumberFormat.Style.PERCENT -> PercentStyleFormatter()
                style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter()
                else -> NumberStyleFormatter()
            }
        }
    }
}

Чтобы вызвать форматирование, можно аннотировать поля с помощью @NumberFormat, как показано в следующем примере:

Java
public class MyModel {
    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;
}
Kotlin
class MyModel(
    @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal
)

API-интерфейс аннотации формата

В пакете org.springframework.format.annotation существует API аннотаций переносимых форматов. Вы можете использовать @NumberFormat для форматирования Number полей, таких как Double и Long, и @DateTimeFormat для форматирования java.util.Date, java.util.Calendar, Long (для миллисекундных меток времени), а также java.time из JSR-310.

В следующем примере для форматирования java.util.Date как даты по стандарту ISO (гггг-ММ-дд) используется @DateTimeFormat:

Java
public class MyModel {
    @DateTimeFormat(iso=ISO.DATE)
    private Date date;
}
Kotlin
class MyModel(
    @DateTimeFormat(iso=ISO.DATE) private val date: Date
)

SPI-интерфейс FormatterRegistry

FormatterRegistry – это SPI-интерфейс для регистрации форматировщиков и преобразователей. FormattingConversionService – это реализация FormatterRegistry, подходящая для большинства окружений. Вы можете программно или декларативно сконфигурировать этот вариант как бин Spring, например, с помощью FormattingConversionServiceFactoryBean. Поскольку эта реализация также реализует ConversionService, можно напрямую сконфигурировать ее для использования с DataBinder из Spring и языком выражений Spring Expression Language (SpEL).

В следующем листинге показан SPI-интерфейс FormatterRegistry:

package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
    void addPrinter(Printer<?> printer);
    void addParser(Parser<?> parser);
    void addFormatter(Formatter<?> formatter);
    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

Как следует из предыдущего листинга, можно регистрировать форматировщики по типу поля или по аннотации.

SPI-интерфейс FormatterRegistry позволяет настраивать правила форматирования централизованно, вместо того, чтобы дублировать такую конфигурацию для всех контроллеров. Например, можно принудительно задать, чтобы все поля даты были отформатированы определенным образом или чтобы поля с определенной аннотацией были отформатированы определенным образом. При помощи общего FormatterRegistry вы единожды определяете эти правила, а они применяются всякий раз, когда требуется форматирование.

SPI-интерфейс FormatterRegistrar

FormatterRegistrar – это SPI-интерфейс для регистрации форматировщиков и преобразователей через FormatterRegistry. В следующем листинге показано определение его интерфейса:

package org.springframework.format;
public interface FormatterRegistrar {
    void registerFormatters(FormatterRegistry registry);
}

FormatterRegistrar полезен при регистрации нескольких связанных преобразователей и форматировщиков для определенной категории форматирования, например, форматирования даты. Он также может быть полезен, если декларативной регистрации недостаточно – например, если форматировщик нужно проиндексировать под определенный тип поля, отличный от его собственного <T>, или если регистрируется пара Printer/Parser. В следующем разделе представлена дополнительная информация о регистрации преобразователей и форматировщиков.

Конфигурирование форматирования в Spring MVC

Конфигурирование глобального формата даты и времени

По умолчанию поля даты и времени, не аннотированные @DateTimeFormat, преобразуются из строк с помощью стиля DateFormat.SHORT. При желании вы можете изменить это, определив свой собственный глобальный формат.

Для этого удостоверьтесь, чтоб Spring не регистрировал форматировщики по умолчанию. Вместо этого регистрируйте форматировщики вручную с помощью:

  • org.springframework.format.datetime.standard.DateTimeFormatterRegistrar

  • org.springframework.format.datetime.DateFormatterRegistrar

Например, следующая конфигурация Java регистрирует глобальный формат вида ггггММдд:

Java
@Configuration
public class AppConfig {
    @Bean
    public FormattingConversionService conversionService() {
        // Используем DefaultFormattingConversionService, но не регистрируем значения по умолчанию
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
        // Удостоверимся, что @NumberFormat все еще поддерживается
        conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
        // Регистрируем преобразование даты по стандарту JSR-310 в определенном глобальном формате
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
        registrar.registerFormatters(conversionService);
        // Регистрируем преобразование даты в определенном глобальном формате
        DateFormatterRegistrar registrar = new DateFormatterRegistrar();
        registrar.setFormatter(new DateFormatter("yyyyMMdd"));
        registrar.registerFormatters(conversionService);
        return conversionService;
    }
}
Kotlin
@Configuration
class AppConfig {
    @Bean
    fun conversionService(): FormattingConversionService {
        // Используем DefaultFormattingConversionService, но не регистрируем значения по умолчанию
        return DefaultFormattingConversionService(false).apply {
            // Удостоверимся, что @NumberFormat все еще поддерживается
            addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory())
            // Регистрируем преобразование даты по стандарту JSR-310 в определенном глобальном формате
            val registrar = DateTimeFormatterRegistrar()
            registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"))
            registrar.registerFormatters(this)
            // Регистрируем преобразование даты в определенном глобальном формате
            val registrar = DateFormatterRegistrar()
            registrar.setFormatter(DateFormatter("yyyyMMdd"))
            registrar.registerFormatters(this)
        }
    }
}

Если вы отдаете предпочтение конфигурации на основе XML, то можете использовать FormattingConversionServiceFactoryBean. В следующем примере показано, как это сделать:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd>
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="registerDefaultFormatters" value="false" />
        <property name="formatters">
            <set>
                <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
            </set>
        </property>
        <property name="formatterRegistrars">
            <set>
                <bean class="org.springframework.format.datetime.standard.DateTimeFormatterRegistrar">
                    <property name="dateFormatter">
                        <bean class="org.springframework.format.datetime.standard.DateTimeFormatterFactoryBean">
                            <property name="pattern" value="yyyyMMdd"/>
                        </bean>
                    </property>
                </bean>
            </set>
        </property>
    </bean>
</beans>

Обратите внимание, что при настройке форматов даты и времени в веб-приложениях необходимо учитывать дополнительные факторы.