项目概况

第07篇:Converter SPI类型转换

2026-01-297 min read后端技术
Spring Framework

公众号: 西魏陶渊明
CSDN: https://springlearn.blog.csdn.net

天下代码一大抄, 抄来抄去有提高, 看你会抄不会抄!

[[toc]]

一、前言

本篇文章中的内容,非常的小众,虽然在实际开发中,基本上不会有使用的场景,但是在Spring中却无处不在的知识点。因为我们是学习Spring,所以我们最好了解一下。

本篇文章,主要学习两个东西。第一个是类型转换, 第二个是格式化输出。

1.1 类型转换

类型转换,比如说Long类型转换Date、String类型转换Long类型。 在实际的开发中我们可能直接使用 BeanUtils.copy() 或者其他三方工具来实现,但其实Spring已经提供了这种的接口能力了。我们只需要下面这样就可以了。

如下演示,将Long类型转Date。

java
1@SpringBootApplication 2public class Application { 3 4 // 注册一个转换器,目标由Long转Date 5 public static class LongToDateConvert implements Converter<Long, Date> { 6 @Override 7 public Date convert(Long source) { 8 return new Date(source); 9 } 10 } 11 12 @Bean("customerConvert") 13 public ConversionServiceFactoryBean customerConvert() { 14 ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean(); 15 conversionServiceFactoryBean.setConverters(Collections.singleton(new LongToDateConvert())); 16 return conversionServiceFactoryBean; 17 } 18} 19@SpringBootTest 20@TestConfiguration 21public class SpringConvertTest { 22 23 @Autowired 24 @Qualifier("customerConvert") 25 private ConversionService conversionService; 26 27 @Test 28 public void test() { 29 // 直接使用即可。 30 Date convert = conversionService.convert(System.currentTimeMillis(), Date.class); 31 // Mon Oct 17 21:38:07 CST 2022 32 System.out.println(convert); 33 } 34}

举一反三,通过上面的接口能力,我们还能实现更多的使用场景。如上我们只实现了1:1的转换,其还可以1:N、N:N,更多的内容下面会讲。

1.2 格式化输出

什么是格式化输出,往往只针对的是文本类型。

  1. 对象类型转文本类型
  2. 文本类型转对象类型

所以格式化是围绕String进行的,在格式化这方面最典型的一个案例就是国际化。

同样的文本,针对不同国家地域展示为当地的语言类型。 下面我们看他的接口定义。

java
1public interface Formatter<T> extends Printer<T>, Parser<T> { 2} 3 4@FunctionalInterface 5public interface Printer<T> { 6 // 对象类型,转换String类型,支持国际化 7 String print(T object, Locale locale); 8} 9@FunctionalInterface 10public interface Parser<T> { 11 // String类型转换泛型,支持国际化 12 T parse(String text, Locale locale) throws ParseException; 13}

下面我们看详细的内容。

二、Converter 类型转换

Spring 3 引入了一个core.convert提供通用类型转换系统的包。系统定义了一个 SPI 来实现类型转换逻辑和一个 API 来在运行时执行类型转换。在 Spring 容器中,您可以使用此系统作为实现的替代PropertyEditor方案,将外部化的 bean 属性值字符串转换为所需的属性类型。您还可以在应用程序中需要类型转换的任何地方使用公共 API。

接口介绍
Converter单一的类型转换,从泛型 S -> T
ConverterFactory按照官方的描述是,具有层次的转换,从泛型 S -> 转换成 R 的子类,实现一对多个类型的转换
GenericConverter前面是一对多,一对一,这个是多对多
ConditionalGenericConverter在前者的基础上,添加上条件判断,符合条件才进行转换

下面我们来以此看下,每个接口的

2.1 Converter

2.1.1 接口定义

text
1package org.springframework.core.convert.converter; 2 3public interface Converter<S, T> { 4 5 T convert(S source); 6}

这个接口非常的简单,没什么好解释的。我们要创建自己的转换器,只用实现Converter接口就可以了。

2.1.2 接口功能

实现从 S,向 T 的泛型转换,Spring提供了很多内置的转换,如下示例。

Spring默认提供了很多的默认实现,下面我们看一个简单的实现。看下面的源码,感觉Spring是真的用心呀。

  • on、true、1、yes 都会转换成 true
  • off、false、0、no 都会转换成 false
java
1final class StringToBooleanConverter implements Converter<String, Boolean> { 2 private static final Set<String> trueValues = new HashSet(8); 3 private static final Set<String> falseValues = new HashSet(8); 4 5 StringToBooleanConverter() { 6 } 7 8 @Nullable 9 public Boolean convert(String source) { 10 String value = source.trim(); 11 if (value.isEmpty()) { 12 return null; 13 } else { 14 value = value.toLowerCase(); 15 if (trueValues.contains(value)) { 16 return Boolean.TRUE; 17 } else if (falseValues.contains(value)) { 18 return Boolean.FALSE; 19 } else { 20 throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); 21 } 22 } 23 } 24 25 static { 26 trueValues.add("true"); 27 trueValues.add("on"); 28 trueValues.add("yes"); 29 trueValues.add("1"); 30 falseValues.add("false"); 31 falseValues.add("off"); 32 falseValues.add("no"); 33 falseValues.add("0"); 34 } 35}

2.2 ConverterFactory

ConverterFactory 跟 Converter的区别在于, ConverterFactory 提供一个泛化的接口。根据泛型获取自己的转换类。但是前提是Converter要具备能处理返回接口的能力。以此来处理 1 对 N的转换。

2.2.1 接口定义

java
1package org.springframework.core.convert.converter; 2 3public interface ConverterFactory<S, R> { 4 5 <T extends R> Converter<S, T> getConverter(Class<T> targetType); 6}

可以看到泛型是从 S -> R, getConverter 泛型方法允许 <T extends R> 返回 T

2.2.2 接口功能

java
1package org.springframework.core.convert.support; 2 3final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { 4 5 public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { 6 return new StringToEnumConverter(targetType); 7 } 8 9 private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> { 10 11 private Class<T> enumType; 12 13 public StringToEnumConverter(Class<T> enumType) { 14 this.enumType = enumType; 15 } 16 // 将泛化类型 17 public T convert(String source) { 18 return (T) Enum.valueOf(this.enumType, source.trim()); 19 } 20 } 21}

通过 <T exends R> 的限定, 最终实现 1 : N 的转换。

2.3 GenericConverter

  • Converter 处理 1:1的转换
  • ConverterFactory 处理 1:N的转换
  • GenericConverter 处理里 N: N的转换

下面我们看接口

2.3.1 接口定义

  • getConvertibleTypes 返回了一个集合,而集合中每个key都是一个键值对。就支持 N:N 了。
text
1package org.springframework.core.convert.converter; 2 3public interface GenericConverter { 4 5 public Set<ConvertiblePair> getConvertibleTypes(); 6 7 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); 8} 9

getConvertibleTypes() 是一个Set集合。可以看到ConvertiblePair是成对的,只要转换双方是包含在这set结合中,都会调用这个进行转换。

java
1final class ConvertiblePair { 2 private final Class<?> sourceType; 3 private final Class<?> targetType; 4}

意味着一个转换器可以处理多种类型的转换。

2.3.2 接口功能

下面举一个例子

java
1 @Data 2 public static class SourceOne { 3 private String name; 4 } 5 6 @Data 7 public static class TargetOne { 8 private String name; 9 } 10 11 @Data 12 public static class TargetTwo { 13 private String name; 14 } 15 16 17 @Test 18 public void test() { 19 ApplicationConversionService applicationConversionService = new ApplicationConversionService(); 20 GenericConverter genericConverter = new GenericConverter() { 21 @Override 22 public Set<ConvertiblePair> getConvertibleTypes() { 23 Set<ConvertiblePair> paris = new HashSet<>(); 24 paris.add(new ConvertiblePair(SourceOne.class, TargetOne.class)); 25 paris.add(new ConvertiblePair(SourceOne.class, TargetTwo.class)); 26 27 return paris; 28 } 29 30 @Override 31 public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { 32 if (sourceType.getObjectType().equals(SourceOne.class) && targetType.getObjectType().equals(TargetOne.class)) { 33 TargetOne targetOne = new TargetOne(); 34 targetOne.setName(((SourceOne) source).getName() + "-> TargetOne"); 35 return targetOne; 36 } 37 if (sourceType.getObjectType().equals(SourceOne.class) && targetType.getObjectType().equals(TargetTwo.class)) { 38 TargetTwo TargetTwo = new TargetTwo(); 39 TargetTwo.setName(((SourceOne) source).getName() + "-> TargetTwo"); 40 return TargetTwo; 41 } 42 return null; 43 } 44 }; 45 applicationConversionService.addConverter(genericConverter); 46 SourceOne sourceOne = new SourceOne(); 47 sourceOne.setName("Jay"); 48 System.out.println(applicationConversionService.convert(sourceOne, TargetOne.class)); 49 System.out.println(applicationConversionService.convert(sourceOne, TargetTwo.class)); 50 51 }

2.4 ConditionalGenericConverter

有时,你希望 Converter只有在特定条件成立时才运行,此时可以实现这个接口。这个接口是实现了 GenericConverterConditionalConverter

2.4.1 接口定义

java
1public interface ConditionalConverter { 2 3 boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); 4} 5 6public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { 7}

2.5 Spring 实践

ConversionService定义了一个统一的 API,用于在运行时执行类型转换逻辑。

2.5.1 ConversionService

java
1package org.springframework.core.convert; 2 3public interface ConversionService { 4 // 如果sourceType的对象可以转换为targetType ,则返回true 5 boolean canConvert(Class<?> sourceType, Class<?> targetType); 6 // 将给定的source转换为指定的targetType 。 7 <T> T convert(Object source, Class<T> targetType); 8 // 如果sourceType的对象可以转换为targetType ,则返回true 9 boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); 10 // 将给定的source转换为指定的targetType 。 TypeDescriptors 提供有关将发生转换的源和目标位置的附加上下文,通常是对象字段或属性位置。 11 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); 12}

2.5.2 硬编码使用

如果转换失败会抛出 org.springframework.core.convert.ConversionFailedException

java
1public class ConvertTest { 2 3 @Test 4 public void test(){ 5 ConversionService sharedInstance = DefaultConversionService.getSharedInstance(); 6 System.out.println(sharedInstance.convert("1", Boolean.class)); 7 System.out.println(sharedInstance.convert("123", Long.class)); 8 System.out.println(sharedInstance.convert("1234", Integer.class)); 9 System.out.println(sharedInstance.convert("1235", int.class)); 10 } 11 12}

2.5.3 整合Spring

java
1@SpringBootApplication 2public class Application { 3 4 5 public static class LongToDateConvert implements Converter<Long, Date> { 6 @Override 7 public Date convert(Long source) { 8 return new Date(source); 9 } 10 } 11 12 @Bean("customerConvert") 13 public ConversionServiceFactoryBean customerConvert() { 14 ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean(); 15 conversionServiceFactoryBean.setConverters(Collections.singleton(new LongToDateConvert())); 16 return conversionServiceFactoryBean; 17 } 18} 19 20@SpringBootTest 21@TestConfiguration 22public class SpringConvertTest { 23 24 @Autowired 25 @Qualifier("customerConvert") 26 private ConversionService conversionService; 27 28 @Test 29 public void test() { 30 Date convert = conversionService.convert(System.currentTimeMillis(), Date.class); 31 System.out.println(convert); 32 } 33}

三、Formatter 格式化输出

core.convert 是一个通用的类型转换系统。它提供了一个统一的ConversionServiceAPI 以及一个强类型的ConverterSPI,用于实现从一种类型到另一种类型的转换逻辑。

现在考虑典型客户端环境的类型转换要求,例如 Web 或桌面应用程序。在这样的环境中,您通常转换 fromString 以支持客户端回发过程,以及转换回String以支持视图呈现过程。

此外,您经常需要本地化String值(国际化)。core.convert Converter 不直接解决此类格式要求。为了直接解决这些问题,Spring 3 引入了一个方便的SPI,它为客户端环境的实现Formatter提供了一个简单而健壮的替代方案。PropertyEditor。

3.1 自定义Formatter

要想自定义Formatter我们只用实现 Formatter 接口即可。下面我们看他们的接口定义,就能看到。 Formatter 跟Convert的区别是什么。

  • Formatter 只支持String和对象的双向转换,适合文本格式化、国际化的处理。
  • Converter 支持任意类型的转换
java
1public interface Formatter<T> extends Printer<T>, Parser<T> {} 2@FunctionalInterface 3public interface Printer<T> { 4 // 对象转String 5 String print(T object, Locale locale); 6} 7@FunctionalInterface 8public interface Parser<T> { 9 // String转对象 10 T parse(String text, Locale locale) throws ParseException; 11}

如下我们自定义一个时间的转换器

java
1public class DateFormatterTest { 2 3 @Test 4 public void test() { 5 DefaultFormattingConversionService defaultFormattingConversionService = new DefaultFormattingConversionService(); 6 defaultFormattingConversionService.addFormatter(new DateFormatter("yyyy-MM-dd")); 7 Date convert = defaultFormattingConversionService.convert("2022-10-10", Date.class); 8 System.out.println(convert); 9 } 10 11 public final class DateFormatter implements Formatter<Date> { 12 private String pattern; 13 public DateFormatter(String pattern) { 14 this.pattern = pattern; 15 } 16 public String print(Date date, Locale locale) { 17 if (date == null) { 18 return ""; 19 } 20 return getDateFormat(locale).format(date); 21 } 22 public Date parse(String formatted, Locale locale) throws ParseException { 23 if (formatted.length() == 0) { 24 return null; 25 } 26 return getDateFormat(locale).parse(formatted); 27 } 28 protected DateFormat getDateFormat(Locale locale) { 29 DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); 30 dateFormat.setLenient(false); 31 return dateFormat; 32 } 33 } 34}

3.1 注解驱动Formatter

在Spring中很多很多功能都是可以基于注解进行驱动的,开发者不用关心底层实现,直接使用注解。就能使用很强大的工具了。下面我们实现一个注解驱动的类型转换。

自定一个注解 @DatePattern , 将String类型,根据注解的配置最终给方法参数赋值。

java
1 public String print(@DatePattern(pattern = "yyyy-MM-dd") Date date) { 2 System.out.println(date.toString()); 3 return date.toString(); 4 }
  1. 首先我们要实现这个接口。
java
1public interface AnnotationFormatterFactory<A extends Annotation> { 2 3 Set<Class<?>> getFieldTypes(); 4 5 Printer<?> getPrinter(A annotation, Class<?> fieldType); 6 7 Parser<?> getParser(A annotation, Class<?> fieldType); 8}

注意这里一定要用 TypeDescriptor 构造的方式来处理,因为只有这样才会处理注解。

java
1 @Test 2 public void test() throws Exception { 3 DefaultFormattingConversionService defaultFormattingConversionService = new DefaultFormattingConversionService(); 4 defaultFormattingConversionService.addFormatterForFieldAnnotation(new DatePatternFormatAnnotationFormatterFactory()); 5 Method print = getClass().getDeclaredMethod("print", Date.class); 6 // 注意一定要使用 TypeDescriptor 构造的方式声明才会有注解信息 7 for (Parameter parameter : print.getParameters()) { 8 // true 9 System.out.println(new TypeDescriptor(MethodParameter.forParameter(parameter)).hasAnnotation(DatePattern.class)); 10 } 11 // 通过注解的方式实现解析 12 Object convert = defaultFormattingConversionService.convert("2021-12-12", new TypeDescriptor(MethodParameter.forExecutable(print, 0))); 13 System.out.println(convert); 14 } 15 16 public String print(@DatePattern(pattern = "yyyy-MM-dd") Date date) { 17 System.out.println(date.toString()); 18 return date.toString(); 19 } 20 21 @Documented 22 @Retention(RetentionPolicy.RUNTIME) 23 @Target({ElementType.FIELD, ElementType.PARAMETER}) 24 public @interface DatePattern { 25 String pattern(); 26 } 27 28 public final class DatePatternFormatAnnotationFormatterFactory 29 implements AnnotationFormatterFactory<DatePattern> { 30 public Set<Class<?>> getFieldTypes() { 31 return new HashSet<Class<?>>(Arrays.asList(new Class<?>[]{Date.class})); 32 } 33 34 public Printer<Date> getPrinter(DatePattern annotation, Class<?> fieldType) { 35 return configureFormatterFrom(annotation, fieldType); 36 } 37 38 public Parser<Date> getParser(DatePattern annotation, Class<?> fieldType) { 39 return configureFormatterFrom(annotation, fieldType); 40 } 41 42 private Formatter<Date> configureFormatterFrom(DatePattern annotation, Class<?> fieldType) { 43 return new DateFormatter(annotation.pattern()); 44 } 45 } 46 public final class DateFormatter implements Formatter<Date> { 47 private String pattern; 48 public DateFormatter(String pattern) { 49 this.pattern = pattern; 50 } 51 public String print(Date date, Locale locale) { 52 if (date == null) { 53 return ""; 54 } 55 return getDateFormat(locale).format(date); 56 } 57 public Date parse(String formatted, Locale locale) throws ParseException { 58 if (formatted.length() == 0) { 59 return null; 60 } 61 return getDateFormat(locale).parse(formatted); 62 } 63 protected DateFormat getDateFormat(Locale locale) { 64 DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); 65 dateFormat.setLenient(false); 66 return dateFormat; 67 } 68 }

最后,都看到这里了,最后如果这篇文章,对你有所帮助,请点个关注,交个朋友。