平常在使用Spring过程中,经常会用到类型转换,但一直没时间对这一块进行系统的整理。因此接下来会用几篇文章对这一块进行系统的整理。
大概的想法是下面俩篇:
Spring类型转换整理
BeanWrapper使用以及原理
DataBinder使用以及原理
这篇文章用于记录自己在学习Spring类型转换相关的内容,主要有以下三块。
PropertyEditor:用于String到Object的类型转换
Conver :用于Object到Object之间的转换
Format :主要用于格式化,将对象转换成指定格式的字符串,比如Date和string之间的转换
PropertyEditor
这个是Spring最早支持的不同类型对象之间的转换,兼容JavaBean规范。这里的不同类型指的是String到不同Object之间的转换。
PropertyEditor SPI 通过java.beans.PropertyEditor包名可以看出,其本身并非Spring中定义的接口,它其实是Java Bean规范中所定义的一个接口,其设计初衷在于是用于完成GUI中的输入与对象属性之间的转换操作;Spring只是借用了PropertyEditor这个接口的定义与功能,来完成字符串与对象属性之间的转换。在Spring 3.0之前,在Spring整个体系中,所有完成字符串与其他数据类型转换操作都是由ProperyEditor接口来完成的。
Spring通过PropertyEditor作为其数据类型转换的基础架构,同时自己还定义了许多默认的ProrpertyEditor的实现,这些实现类都位于spring-beans这个模块中的propertyeditors包中。
这些内置的PropertyEditor会有部分在默认情况下就已经被加了到IOC容器中,而有些PropertyEditor在默认情况下并没有自动加入,需要用户手动进行配置,后面我们通过源码可以看到Spring默认所注册了哪些PropertyEditor。
PropertyEditorSupport 由于PropertyEditor是一个类型转换的接口,其里面定义了很多与我们实际使用上无关的方法。如果我们想要使用PropertyEditor的话,我们通常只需要继承java.beans.PropertyEditorSupport这个类,并且重写其setAsText(String source)方法即可,通过它将输入的字符串转换成我们期望的数据类型。
Spring所提供的内置的PropertyEditor也都是继承PropertyEditorSupport来完成类型转换的。
下面展示下基本使用,这个例子展示了如何将String转换成Point类型
定义的辅助类如下
1 2 3 4 5 6 7 8 9 10 11 12 13 import lombok.Data;@Data @Component public class Circle { @Value("1;2") private Point point; } @Data public class Point { int x, y; }
上面给出两个非常简单的类,我们希望完成的是将输入字符串”1;2“,自动进行分割,然后转换成point的x和y属性。
下面我们自己定义PropertyEditor来完成数据的转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class PointEditor extends PropertyEditorSupport { @Override public void setAsText (String text) throws IllegalArgumentException { String[] splits = text.split(";" ); Point point = new Point (); point.setX(Integer.parseInt(splits[0 ])); point.setY(Integer.parseInt(splits[1 ])); setValue(point); } }
在完成ProperyEditor编写完成后,我们只需将其注册到IOC容器中就可以自动完成String到Point之间的转换,这里使用编程的方式来注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Configuration @ComponentScan(basePackages = {"club.fengxiu.vailidation.editor"}) public class AppConfig { @Bean public CustomEditorConfigurer customEditorConfigurer () { CustomEditorConfigurer customEditorConfigurer = new CustomEditorConfigurer (); PropertyEditorRegistrar propertyEditorRegistrar = new PropertyEditorRegistrar () { @Override public void registerCustomEditors (PropertyEditorRegistry registry) { registry.registerCustomEditor(String.class, new PointEditor ()); } }; customEditorConfigurer.setPropertyEditorRegistrars(new PropertyEditorRegistrar []{propertyEditorRegistrar}); return customEditorConfigurer; } }
当上面的注册完成后,就可以使用这个PropertyEditor,使用demo
1 2 3 4 5 6 @Test public void testAppConfig () { ApplicationContext context = new AnnotationConfigApplicationContext (AppConfig.class); Circle circle = context.getBean(Circle.class); System.out.println(circle); }
上面的是用来自定义String到Object的类型转换,但是这么多的PropertyEditor是由谁来管理呢,下面将介绍Spring是如何来管理这么多自定义的PropertyEditor
PropertyEditorRegistry org.springframework.beans.PropertyEditorRegistry接口提供了对PropertyEditor注册以及查找功能,因此其主要提供是提供了对PropertyEditor的管理功能,首先来看看这个接口的描述:
1 2 3 4 5 6 7 8 9 10 11 12 13 package org.springframework.beans;import java.beans.PropertyEditor;public interface PropertyEditorRegistry { void registerCustomEditor (Class<?> requiredType, PropertyEditor propertyEditor) ; void registerCustomEditor (Class<?> requiredType, String propertyPath, PropertyEditor propertyEditor) ; PropertyEditor findCustomEditor (Class<?> requiredType, String propertyPath) ; }
从上面可以接口的定义,PropertyEditorRegistry这个接口的作用,下面我们来看看其具体的几个实现类。
PropertyEditorRegistrySupport 由于PropertyEditorRegistry只是定义对PropertyEditor注册和查找的方法,其具体的核心实现类是org.springframework.beans.PropertyEditorRegistrySupport,真正对PropertyEditor管理的操作全部在该类中实现,下面来看看PropertyEditorRegistrySupport的源码,由于PropertyEditorRegistrySupport源码篇幅比较多,这里就采用截图来说明其实现:
通过上面的标注我们看到PropertyEditorSupport底层对于不同种类的PropertyEditor使用不同的Map来进行存储,下面我们看下它是如何进行注册的。
其注册的实现机制也并没有出人意料的地方,就是判断存储Classs与PropertyEditor之间映射关系的Map是否已经存在,如果不存在则先创建一个LinkedHashMap,如果有就直接进行存储映射关系。 前面我们提到过在IOC容器中默认就会内置一些PropertyEditor,通过createDefaultEditors()我们可以清楚地看到其默认所添加的PropertyEditor。
PropertyEditorRegistrar PropertyEditorRegistrar接口的定义如下:
1 2 3 4 public interface PropertyEditorRegistrar { void registerCustomEditors (PropertyEditorRegistry registry) ; }
从接口的描述上我们可以看到,PropertyEditorRegistrar的作用是将用户自定义的PropertyEditor注册到PropertyEditorRegistry中。通过其registerCustomEditors方法中的参数我们可以看到,其所接受的正是一个PropertyEditorRegistry,通过方法的参数将用户自定义的ProepertyEditor加入到PropertyEditorRegistry被其进行管理。 PropertyEditorRegistrar对于如果我们希望将一组相同的PropertyEditor应用在多个地方时是非常有用的 ( 比如希望将相同的一组PropertyEditor既应用在IOC容器中,同时又应用在Spring MVC的DataBinder中),此时就可以先定义一个PropertyEditorRegistrar的实现类,来完成通用的ProepertyEditor注册操作,然后将PropertyEditorRegistrar作为一个ProeprtyEditor的集合设置到不同的地方,此时就可以做到代码复用。
相同的PropertyEditor需要在多处进行注册的原因是因为我们在IOC容器中通过CustomEditorConfigurer添加了自定义的PropertyEditor后,其并不会对SpringMVC中所使用的DataBinder而生效,因此需要再次进行注册,我们通过分析CustomEditorConfigurer可以在其注释说明中清楚地看到这点说明。
上面那个注册自定义的PointEditor,就是用自定义的PropertyEditorRegistrar来注册的。这里就不在做具体的补充。
PropertyEditor的缺点分析 1.只能完成字符串到Java类型的转换,并不能完成任意类型之间的转换。 2.由于PropertyEditor是非线程安全,因此对于每一次的类型转换,都需要创建一个新的PropertyEdtitor,如果希望达到共享,那么底层会使用synchronized来对其进行并发地控制。
convert core.convert是在Spring3之后,引进的一个新的通用类型转换系统,主要用来替代前面说到的PropertyEditor。PropertyEditor主要用在String到Object类型的准换,但是在实际开发中还有需要Object到Obejct的类型转换。下面主要介绍Convert的基本使用。
Converter SPI 这个接口的定义比较简单,在Spring中如果需要自定义类型转换,推介使用这个。
接口定义如下
1 2 3 4 5 6 package org.springframework.core.convert.converter;public interface Converter <S, T> { T convert (S source) ; }
下面是一个简单的String转换成Integer的例子
1 2 3 4 5 6 7 8 package org.springframework.core.convert.support;final class StringToInteger implements Converter <String, Integer> { public Integer convert (String source) { return Integer.valueOf(source); } }
从上面可以看出,如果使用这个自定一类型转换实现起来比较简单。在Spring中已经提供了一些基本类型之间的转换,主要在org.springframework.core.convert.support包下。
ConverterFactory 这个相对于上面来说比较复杂,主要用来转换一个类型到另一个继承体系类型。举个例子,如果需要String转成Enum以及他的子类,这时可以使用ConverFactory这个接口来定义类型转换。
接口定义如下
1 2 3 4 5 6 package org.springframework.core.convert.converter;public interface ConverterFactory <S, R> { <T extends R > Converter<S, T> getConverter (Class<T> targetType) ; }
String转换成Enum的例子如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public 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()); } } }
GenericConverter 这个比之前的类型转换接口更加强大,可以在多种类型到多种类型之间转换。通常情况下会做一个标记,比如注解,或者在属性值上设置一个标记,用来判断转换成哪种类型。
接口定义如下
1 2 3 4 5 6 7 package org.springframework.core.convert.converter;public interface GenericConverter { public Set<ConvertiblePair> getConvertibleTypes () ; Object convert (Object source, TypeDescriptor sourceType, TypeDescriptor targetType) ; }
其中getConvertibleTypes用来表示当前类支持的source到target的转换类型组,convert(Object, TypeDescriptor, TypeDescriptor),用来执行转换逻辑,其中TypeDescriptor用来帮助访问target和source对象的属性,并设置属性值。
下面是一个实现GenericConverter的例子,数组转成列表,ArrayToCollectionConverter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class ArrayToCollectionConverter implements GenericConverter { @Override public Set<ConvertiblePair> getConvertibleTypes () { return Collections.singleton(new ConvertiblePair (Object[].class, Collection.class)); } @Override public Object convert (Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null ) { return null ; } int length = Array.getLength(source); TypeDescriptor elementDesc = targetType.getElementTypeDescriptor(); Collection<Object> target = CollectionFactory.createCollection(targetType.getType(), (elementDesc != null ? elementDesc.getType() : null ), length); if (elementDesc == null ) { for (int i = 0 ; i < length; i++) { Object sourceElement = Array.get(source, i); target.add(sourceElement); } } return target; } }
ConditionalGenericConverter 这个接口用来提供当满足某些条件时,才能够使用Convert。比如,当指定的注解出现,才能执行converter逻辑。此时就需要增加转换前的验证逻辑,即ConditionalConverter接口表示的意义。为了方便使用ConditionalGenericConverter,实现了GenericConverter 和ConditionalConverter俩个接口。
1 2 3 4 5 6 7 public interface ConditionalConverter { boolean matches (TypeDescriptor sourceType, TypeDescriptor targetType) ; } public interface ConditionalGenericConverter extends GenericConverter , ConditionalConverter {}
这里看一个Spring中的实现例子,用来在一个实体的唯一标识符和实体引用之间转换,代码如下,下面的整体思路是,首先判断是否有对应的finder方法以及是否有对应的Conver,如果都有则执行转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 final class IdToEntityConverter implements ConditionalGenericConverter { private final ConversionService conversionService; public IdToEntityConverter (ConversionService conversionService) { this .conversionService = conversionService; } @Override public Set<ConvertiblePair> getConvertibleTypes () { return Collections.singleton(new ConvertiblePair (Object.class, Object.class)); } @Override public boolean matches (TypeDescriptor sourceType, TypeDescriptor targetType) { Method finder = getFinder(targetType.getType()); return (finder != null && this .conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0 ]))); } @Override public Object convert (Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null ) { return null ; } Method finder = getFinder(targetType.getType()); Object id = this .conversionService.convert( source, sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0 ])); return ReflectionUtils.invokeMethod(finder, source, id); } private Method getFinder (Class<?> entityClass) { String finderMethod = "find" + getEntityName(entityClass); Method[] methods; boolean localOnlyFiltered; try { methods = entityClass.getDeclaredMethods(); localOnlyFiltered = true ; } catch (SecurityException ex) { methods = entityClass.getMethods(); localOnlyFiltered = false ; } for (Method method : methods) { if (Modifier.isStatic(method.getModifiers()) && method.getName().equals(finderMethod) && method.getParameterTypes().length == 1 && method.getReturnType().equals(entityClass) && (localOnlyFiltered || method.getDeclaringClass().equals(entityClass))) { return method; } } return null ; } private String getEntityName (Class<?> entityClass) { String shortName = ClassUtils.getShortName(entityClass); int lastDot = shortName.lastIndexOf('.' ); if (lastDot != -1 ) { return shortName.substring(lastDot + 1 ); } else { return shortName; } } }
ConversionService ConversionService定义了一个统一的API,用于在运行时执行类型转换逻辑。转换器通常运行在以下外观接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 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,用来提供注册自定义converters,然后ConversionServic在执行转换的时候,会委托对应的converters来执行转换。
配置ConversionService ConversionService是无状态的,可以在多线程中使用,通常在应用启动的时候,会配置一个ConversionService。当应用中没有ConversionService,则会使用默认的PropertyEditor。
通过编程的方式可以按照下面这种方式来配置ConversionService,代码如下
1 2 3 4 5 6 7 @Configuration public class AppConfig { @Bean public ConversionServiceFactoryBean ConversionService () { return new ConversionServiceFactoryBean (); } }
如果需要注入一些自定义的Convert,则可以使用setConverters方法来注入。
在代码中使用ConversionService,可以通过自动注入的方式。代码如下
1 2 3 4 5 6 7 8 9 10 11 @Service public class MyService { @Autowired ConversionService conversionService public void doIt () { this .conversionService.convert(...) } }
在系统中,默认创建DefaultConversionService,这里面包含了需要spring自定义的Convert,方便使用。如果需要添加一些自定义的Convert来代替默认的,则可以通过addDefaultConverters方法来添加。
下面展示一个复杂的Collection之间的转换,List< Integer> 转成List< String>
1 2 3 4 5 6 DefaultConversionService cs = new DefaultConversionService ();List<Integer> input = ... cs.convert(input, TypeDescriptor.forObject(input), TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
前面介绍的Convert,是用于通用类型的转换,在一些应用中,可能需要对返回给客户端的值进行格式化。这时就需要Formatter,这个是一种特殊的类型转换,主要是用来进行格式化使用。
比如java.util.Date 格式化成 Long,在SpringMVC中就是通过Formatter来做的。
接口定义如下
1 2 3 4 package org.springframework.format;public interface Formatter <T> extends Printer <T>, Parser<T> {}
Formatter继承了Printer和Parser接口,它们分别用来将String转换成Object和Object转换成String。
Printer用于Object格式化为String
1 2 3 4 public interface Printer <T> { String print (T fieldValue, Locale locale) ; }
Parser 用于将String转换为Object
1 2 3 4 public interface Parser <T> { T parse (String clientValue, Locale locale) throws ParseException; }
org.springframework.format下面提供了很多默认的Fromat,具体的类如下
下面是一个时间格式化的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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; } }
属性的格式化,可以通过配置属性类型或者注解,如果需要使用注解来标记Formatter,需要实现AnnotationFormatterFactory接口,定义如下
1 2 3 4 5 6 7 8 public interface AnnotationFormatterFactory <A extends Annotation > { Set<Class<?>> getFieldTypes(); Printer<?> getPrinter(A annotation, Class<?> fieldType); Parser<?> getParser(A annotation, Class<?> fieldType); }
其中泛型A是指这个Fromatter处理逻辑对应的注解,方法getFieldTypes用于返回这个注解可以在哪些类型上使用。getPrinter和getParser用来返回格式化和范格式化的对象。
下面是NumberFormatAnnotationFormatterFactory的例子
NumberFormat注解定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface NumberFormat { Style style () default Style.DEFAULT; String pattern () default "" ; enum Style { DEFAULT, NUMBER, PERCENT, CURRENCY } }
注解对应的处理器如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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 (); } } } }
使用方式
1 2 3 4 5 public class MyModel { @NumberFormat(style=Style.CURRENCY) private BigDecimal decimal; }
FormatterRegistry是用来注册formatters和converters。这个用来中心化管控所有使用的formatting。FormattingConversionService是一个实现了FormatterRegistry在大多数场景下使用。可以通过FormattingConversionServiceFactoryBean来注册FormattingConversionService,从而注册了FormatterRegistry。
FormatterRegistry 接口定义如下
1 2 3 4 5 6 7 8 9 10 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的接口,我们可以将属于某一个类别里面的多个converters 和formatters注册到FormatterRegistry,然后在将这个FormatterRegistry注册到FormatterRegistrar中。比如date类别的格式化,就可以组装成一个来使用。
接口逻辑如下
1 2 3 public interface FormatterRegistrar { void registerFormatters (FormatterRegistry registry) ; }
配置一个全局Date和Time 格式 下面是使用编程的方式配置默认的时间格式,具体配置就是在ApplicationContex中修改默认的时间格式,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Configuration public class AppConfig { @Bean public FormattingConversionService conversionService () { DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService (); conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory ()); DateTimeFormatterRegistrar dateTimeFormatterRegistrar = new DateTimeFormatterRegistrar (); dateTimeFormatterRegistrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd" )); dateTimeFormatterRegistrar.registerFormatters(conversionService); DateFormatterRegistrar registrar = new DateFormatterRegistrar (); registrar.setFormatter(new DateFormatter ("yyyyMMdd" )); registrar.registerFormatters(conversionService); return conversionService; } }
具体使用方式
1 2 3 4 5 6 7 8 9 10 11 @Component public class DateFormat { @Autowired FormattingConversionService formattingConversionService; public void getDate (Date date) { String date1 = formattingConversionService.convert(date, String.class); System.out.println(date1); } }
参考
深入分析Spring中的类型转换与校验(1)