《Spring 5 官方文档》5. 验证、数据绑定和类型转换(二)

简介:

5.5 Spring类型转换

Spring 3引入了core.convert包来提供一个一般类型的转换系统。这个系统定义了实现类型转换逻辑的服务提供接口(SPI)以及在运行时执行类型转换的API。在Spring容器内,这个系统可以当作是PropertyEditor的替代选择,用于将外部bean的属性值字符串转换成所需的属性类型。这个公共的API也可以在你的应用程序中任何需要类型转换的地方使用。

5.5.1 Converter SPI

实现类型转换逻辑的SPI是简单并且强类型的:

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);

}

要创建属于你自己的转换器,只需要简单的实现以上接口即可。泛型参数S表示你想要进行转换的源类型,而泛型参数T表示你想要转换的目标类型。如果一个包含S类型元素的集合或数组需要转换为一个包含T类型的数组或集合,那么这个转换器也可以被透明地应用,前提是已经注册了一个委托数组或集合的转换器(默认情况下会是DefaultConversionService处理)。

对每次方法convert(S)的调用,source参数值必须确保不为空。如果转换失败,你的转换器可以抛出任何非受检异常(unchecked exception);具体来说,为了报告一个非法的source参数值,应该抛出一个IllegalArgumentException。还有要注意确保你的Converter实现必须是线程安全的。

为方便起见,core.convert.support包已经提供了一些转换器实现,这些实现包括了从字符串到数字以及其他常见类型的转换。考虑将StringToInteger作为一个典型的Converter实现示例:

package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

    public Integer convert(String source) {
        return Integer.valueOf(source);
    }

}

5.5.2 ConverterFactory

当你需要集中整个类层次结构的转换逻辑时,例如,碰到将String转换到java.lang.Enum对象的时候,请实现ConverterFactory

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);

}

泛型参数S表示你想要转换的源类型,泛型参数R表示你可以转换的那些范围内的类型的基类。然后实现getConverter(Class),其中T就是R的一个子类。

考虑将StringToEnum作为ConverterFactory的一个示例:

package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnumConverter(targetType);
    }

    private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

        private Class<T> enumType;

        public StringToEnumConverter(Class<T> enumType) {
            this.enumType = enumType;
        }

        public T convert(String source) {
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }
}

5.5.3 GenericConverter

当你需要一个复杂的转换器实现时,请考虑GenericConverter接口。GenericConverter具备更加灵活但是不太强的类型签名,以支持在多种源类型和目标类型之间的转换。此外,当实现你的转换逻辑时,GenericConverter还可以使源字段和目标字段的上下文对你可用,这样的上下文允许类型转换由字段上的注解或者字段声明中的泛型信息来驱动。

package org.springframework.core.convert.converter;

public interface GenericConverter {

    public Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

要实现一个GenericConverter,getConvertibleTypes()方法要返回支持的源-目标类型对,然后实现convert(Object,TypeDescriptor,TypeDescriptor)方法来实现你的转换逻辑。源TypeDescriptor提供了对持有被转换值的源字段的访问,目标TypeDescriptor提供了对设置转换值的目标字段的访问。

一个很好的GenericConverter的示例是一个在Java数组和集合之间进行转换的转换器。这样一个ArrayToCollectionConverter可以通过内省声明了目标集合类型的字段以解析集合元素的类型,这将允许原数组中每个元素可以在集合被设置到目标字段之前转换成集合元素的类型。

由于GenericConverter是一个更复杂的SPI接口,所以对基本类型的转换需求优先使用Converter或者ConverterFactory。

ConditionalGenericConverter

有时候你只想要在特定条件成立的情况下Converter才执行,例如,你可能只想要在目标字段存在特定注解的情况下才执行Converter,或者你可能只想要在目标类中定义了特定方法,比如static valueOf方法,才执行ConverterConditionalGenericConverterGenericConverterConditionalConveter接口的联合,允许你定义这样的自定义匹配条件:

public interface ConditionalGenericConverter
        extends GenericConverter, ConditionalConverter {

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

ConditionalGenericConverter的一个很好的例子是一个在持久化实体标识和实体引用之间进行转换的实体转换器。这个实体转换器可能只匹配这样的条件–目标实体类声明了一个静态的查找方法,例如findAccount(Long),你将在matches(TypeDescriptor,TypeDescriptor)方法实现里执行这样的查找方法的检测。

5.5.4 ConversionService API

ConversionService接口定义了运行时执行类型转换的统一API,转换器往往是在这个门面(facade)接口背后执行:

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    <T> T convert(Object source, Class<T> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

}

大多数ConversionService实现也会实现ConverterRegistry接口,这个接口提供一个用于注册转换器的服务提供接口(SPI)。在内部,一个ConversionService实现会以委托给注册其中的转换器的方式来执行类型转换逻辑。

core.convert.support包已经提供了一个强大的ConversionService实现,GenericConversionService是适用于大多数环境的通用实现,ConversionServiceFactory以工厂的方式为创建常见的ConversionService配置提供了便利。

5.5.5 配置ConversionService

ConversionService是一个被设计成在应用程序启动时会进行实例化的无状态对象,随后可以在多个线程之间共享。在一个Spring应用程序中,你通常会为每一个Spring容器(或者应用程序上下文ApplicationContext)配置一个ConversionService实例,它会被Spring接收并在框架需要执行一个类型转换时使用。你也可以将这个ConversionService直接注入到你任何的Bean中并直接调用。

如果Spring没有注册ConversionService,则会使用原始的基于PropertyEditor的系统。

要向Spring注册默认的ConversionService,可以用conversionService作为id来添加如下的bean定义:

<bean id="conversionService"
    class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的ConversionService可以在字符串、数字、枚举、映射和其他常见类型之间进行转换。为了使用你自己的自定义转换器来补充或者覆盖默认的转换器,可以设置converters属性,该属性值可以是Converter、ConverterFactory或者GenericConverter之中任何一个的接口实现。

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="example.MyCustomConverter"/>
        </set>
    </property>
</bean>

在一个Spring MVC应用程序中使用ConversionService也是比较常见的,可以去看Spring MVC章节的Section 18.16.3 “Conversion and Formatting”

在某些情况下,你可能希望在转换期间应用格式化,可以看5.6.3 “FormatterRegistry SPI”获取使用FormattingConversionServiceFactoryBean的细节。

5.5.6 编程方式使用ConversionService

要以编程方式使用ConversionService,你只需要像处理其他bean一样注入一个引用即可:

@Service
public class MyService {

    @Autowired
    public MyService(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public void doIt() {
        this.conversionService.convert(...)
    }
}

对大多数用例来说,convert方法指定了可以使用的目标类型,但是它不适用于更复杂的类型比如参数化元素的集合。例如,如果你想要以编程方式将一个IntegerList转换成一个StringList,就需要为原类型和目标类型提供一个正式的定义。

幸运的是,TypeDescriptor提供了多种选项使事情变得简单:

DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = ....
cs.convert(input,
    TypeDescriptor.forObject(input), // List<Integer> type descriptor
    TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));

注意DefaultConversionService会自动注册对大部分环境都适用的转换器,这其中包括了集合转换器、标量转换器还有基本的ObjectString的转换器。可以通过调用DefaultConversionService类上的静态方法addDefaultConverters来向任意的ConverterRegistry注册相同的转换器。

因为值类型的转换器可以被数组和集合重用,所以假设标准集合处理是恰当的,就没有必要创建将一个SCollection转换成一个TCollection的特定转换器。

5.6 Spring字段格式化

如上一节所述,core.convert包是一个通用类型转换系统,它提供了统一的ConversionService API以及强类型的Converter SPI用于实现将一种类型转换成另外一种的转换逻辑。Spring容器使用这个系统来绑定bean属性值,此外,Spring表达式语言(SpEL)和DataBinder也都使用这个系统来绑定字段值。举个例子,当SpEL需要将Short强制转换成Long来完成一次expression.setValue(Object bean, Object value)尝试时,core.convert系统就会执行这个强制转换。

现在让我们考虑一个典型的客户端环境如web或桌面应用程序的类型转换要求,在这样的环境里,你通常会经历将字符串进行转换以支持客户端回传的过程以及转换回字符串以支持视图渲染的过程。此外,你经常需要对字符串值进行本地化。更通用的core.convert包中的Converter SPI不直接解决这种格式化要求。Spring 3为此引入了一个方便的Formatter SPI来直接解决这些问题,这个接口为客户端环境提供一种简单强大并且替代PropertyEditor的方案。

一般来说,当你需要实现通用的类型转换逻辑时请使用Converter SPI,例如,在java.util.Date和java.lang.Long之间进行转换。当你在一个客户端环境(比如web应用程序)工作并且需要解析和打印本地化的字段值时,请使用Formatter SPI。ConversionService接口为这两者提供了一套统一的类型转换API。

5.6.1 Formatter SPI

Formatter SPI实现字段格式化逻辑是简单并且强类型的:

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接口。泛型参数T代表你想要格式化的对象的类型,例如,java.util.Date。实现print()操作可以将类型T的实例按客户端区域设置的显示方式打印出来。实现parse()操作可以从依据客户端区域设置返回的格式化表示中解析出类型T的实例。如果解析尝试失败,你的格式化器应该抛出一个ParseException或者IllegalArgumentException。请注意确保你的格式化器实现是线程安全的。

为方便起见,format子包中已经提供了一些格式化器实现。number包提供了NumberFormatterCurrencyFormatterPercentFormatter,它们通过使用java.text.NumberFormat来格式化java.lang.Number对象 。datetime包提供了DateFormatter,其通过使用java.text.DateFormat来格式化java.util.Datedatetime.joda包基于Joda Time library提供了全面的日期时间格式化支持。

考虑将DateFormatter作为Formatter实现的一个例子:

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;
    }

}

Spring团队欢迎社区驱动的Formatter贡献,可以登陆网站jira.spring.io了解如何参与贡献。

5.6.2 注解驱动的格式化

如你所见,字段格式化可以通过字段类型或者注解进行配置,要将一个注解绑定到一个格式化器,可以实现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);

}

泛型参数A代表你想要关联格式化逻辑的字段注解类型,例如org.springframework.format.annotation.DateTimeFormat。让getFieldTypes()方法返回可能使用注解的字段类型,让getPrinter()方法返回一个可以打印被注解字段的值的打印机(Printer),让getParser()方法返回一个可以解析被注解字段的客户端值的解析器(Parser)。

下面这个AnnotationFormatterFactory实现的示例把@NumberFormat注解绑定到一个格式化器,此注解允许指定数字样式或模式:

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 NumberFormatter(annotation.pattern());
        } else {
            Style style = annotation.style();
            if (style == Style.PERCENT) {
                return new PercentFormatter();
            } else if (style == Style.CURRENCY) {
                return new CurrencyFormatter();
            } else {
                return new NumberFormatter();
            }
        }
    }
}

要触发格式化,只需要使用@NumberFormat对字段进行注解:

public class MyModel {

    @NumberFormat(style=Style.CURRENCY)
    private BigDecimal decimal;

}

Format Annotation API

org.springframework.format.annotation包中存在一套可移植(portable)的格式化注解API。请使用@NumberFormat格式化java.lang.Number字段,使用@DateTimeFormat格式化java.util.Date、java.util.Calendar、java.util.Long(注:此处可能是原文错误,应为java.lang.Long)或者Joda Time字段。

下面这个例子使用@DateTimeFormat将java.util.Date格式化为ISO时间(yyyy-MM-dd)

public class MyModel {

    @DateTimeFormat(iso=ISO.DATE)
    private Date date;

}

 

5.6.3 FormatterRegistry SPI

FormatterRegistry是一个用于注册格式化器和转换器的服务提供接口(SPI)。FormattingConversionService是一个适用于大多数环境的FormatterRegistry实现,可以以编程方式或利用FormattingConversionServiceFactoryBean声明成Spring bean的方式来进行配置。由于它也实现了ConversionService,所以可以直接配置它与Spring的DataBinder以及Spring表达式语言(SpEL)一起使用。

请查看下面的FormatterRegistry SPI:

package org.springframework.format;

public interface FormatterRegistry extends ConverterRegistry {

    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);

    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);

    void addFormatterForFieldType(Formatter<?> formatter);

    void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);

}

如上所示,格式化器可以通过字段类型或者注解进行注册。

FormatterRegistry SPI允许你集中地配置格式化规则,而不是在你的控制器之间重复这样的配置。例如,你可能要强制所有的时间字段以某种方式被格式化,或者是带有特定注解的字段以某种方式被格式化。通过一个共享的FormatterRegistry,你可以只定义这些规则一次,而在需要格式化的时候应用它们。

5.6.4 FormatterRegistrar SPI

FormatterRegistrar是一个通过FormatterRegistry注册格式化器和转换器的服务提供接口(SPI):

package org.springframework.format;

public interface FormatterRegistrar {

    void registerFormatters(FormatterRegistry registry);

}

当要为一个给定的格式化类别(比如时间格式化)注册多个关联的转换器和格式化器时,FormatterRegistrar会非常有用。

下一部分提供了更多关于转换器和格式化器注册的信息。

5.6.5 在Spring MVC中配置格式化

请查看Spring MVC章节的Section 18.16.3 “Conversion and Formatting”

转载自 并发编程网 - ifeve.com    


相关文章
|
1月前
|
存储 搜索推荐 Java
|
3月前
|
SQL Java 数据库连接
Spring Boot - 构建数据访问层
Spring Boot - 构建数据访问层
41 0
|
4月前
|
Java 数据安全/隐私保护 Spring
Spring案例:百度网盘密码数据兼容处理
Spring案例:百度网盘密码数据兼容处理
42 0
|
4月前
Spring-AOP通知获取数据
Spring-AOP通知获取数据
37 0
|
4月前
|
JSON 前端开发 Java
利用Spring Boot处理JSON数据实战(包括jQuery,html,ajax)附源码 超详细
利用Spring Boot处理JSON数据实战(包括jQuery,html,ajax)附源码 超详细
60 0
|
4月前
|
XML Java 数据库连接
Spring Boot的数据访问之Spring Data JPA以及Hibernate的实战(超详细 附源码)
Spring Boot的数据访问之Spring Data JPA以及Hibernate的实战(超详细 附源码)
47 0
|
13天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
23 3
|
13天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
29 3
|
1月前
|
Java 数据库连接 数据库
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
Spring Boot整合MyBatis Plus集成多数据源轻松实现数据读写分离
26 2
|
1月前
|
Java 大数据 数据库
【Spring底层原理高级进阶】Spring Batch清洗和转换数据,一键处理繁杂数据!Spring Batch是如何实现IO流优化的?本文详解!
【Spring底层原理高级进阶】Spring Batch清洗和转换数据,一键处理繁杂数据!Spring Batch是如何实现IO流优化的?本文详解!