SpringBoot(十七):SpringBoot2.1.1数据类型转化器Converter
什么场景下需要使用类型化器Converter?
springboot2.1.1在做Restful Api开发过程中往往希望接口直接接收date类型参数,但是默认不加设置是不支持的,会抛出异常:系统是希望接收date类型,string无法转化为date错误。
{ "timestamp": "2019-10-29 11:52:05", "status": 400, "error": "Bad Request", "message": "Failed to convert value of type 'java.lang.String' to required type 'java.util.Date';
nested exception is org.springframework.core.convert.ConversionFailedException:
Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.util.Date] for value '2019-10-09';
nested exception is java.lang.IllegalArgumentException", "path": "/api/v1/articles" }
此时就需要配置自定义类型转化器。
实际上在SpringMvc框架中已经内置了很多类型转化器,当发送一个post,get等请求后,调用请求方法之前会对方法参数进行类型转化,默认在SpringMvc系统中由‘org.springframework.core.convert.support.DefaultConversionService’装配了一套默认Converters。
public class DefaultConversionService extends GenericConversionService { @Nullable private static volatile DefaultConversionService sharedInstance; /** * Create a new {@code DefaultConversionService} with the set of * {@linkplain DefaultConversionService#addDefaultConverters(ConverterRegistry) default converters}. */ public DefaultConversionService() { addDefaultConverters(this); } /** * Return a shared default {@code ConversionService} instance, * lazily building it once needed. * <p><b>NOTE:</b> We highly recommend constructing individual * {@code ConversionService} instances for customization purposes. * This accessor is only meant as a fallback for code paths which * need simple type coercion but cannot access a longer-lived * {@code ConversionService} instance any other way. * @return the shared {@code ConversionService} instance (never {@code null}) * @since 4.3.5 */ public static ConversionService getSharedInstance() { DefaultConversionService cs = sharedInstance; if (cs == null) { synchronized (DefaultConversionService.class) { cs = sharedInstance; if (cs == null) { cs = new DefaultConversionService(); sharedInstance = cs; } } } return cs; } /** * Add converters appropriate for most environments. * @param converterRegistry the registry of converters to add to * (must also be castable to ConversionService, e.g. being a {@link ConfigurableConversionService}) * @throws ClassCastException if the given ConverterRegistry could not be cast to a ConversionService */ public static void addDefaultConverters(ConverterRegistry converterRegistry) { addScalarConverters(converterRegistry); addCollectionConverters(converterRegistry); converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); converterRegistry.addConverter(new ObjectToObjectConverter()); converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); } /** * Add common collection converters. * @param converterRegistry the registry of converters to add to * (must also be castable to ConversionService, e.g. being a {@link ConfigurableConversionService}) * @throws ClassCastException if the given ConverterRegistry could not be cast to a ConversionService * @since 4.2.3 */ public static void addCollectionConverters(ConverterRegistry converterRegistry) { ConversionService conversionService = (ConversionService) converterRegistry; converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService)); converterRegistry.addConverter(new CollectionToArrayConverter(conversionService)); converterRegistry.addConverter(new ArrayToArrayConverter(conversionService)); converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService)); converterRegistry.addConverter(new MapToMapConverter(conversionService)); converterRegistry.addConverter(new ArrayToStringConverter(conversionService)); converterRegistry.addConverter(new StringToArrayConverter(conversionService)); converterRegistry.addConverter(new ArrayToObjectConverter(conversionService)); converterRegistry.addConverter(new ObjectToArrayConverter(conversionService)); converterRegistry.addConverter(new CollectionToStringConverter(conversionService)); converterRegistry.addConverter(new StringToCollectionConverter(conversionService)); converterRegistry.addConverter(new CollectionToObjectConverter(conversionService)); converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService)); converterRegistry.addConverter(new StreamConverter(conversionService)); } private static void addScalarConverters(ConverterRegistry converterRegistry) { converterRegistry.addConverterFactory(new NumberToNumberConverterFactory()); converterRegistry.addConverterFactory(new StringToNumberConverterFactory()); converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverter(new StringToCharacterConverter()); converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverter(new NumberToCharacterConverter()); converterRegistry.addConverterFactory(new CharacterToNumberFactory()); converterRegistry.addConverter(new StringToBooleanConverter()); converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverterFactory(new StringToEnumConverterFactory()); converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry)); converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory()); converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToLocaleConverter()); converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverter(new StringToCharsetConverter()); converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverter(new StringToCurrencyConverter()); converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter()); converterRegistry.addConverter(new StringToPropertiesConverter()); converterRegistry.addConverter(new PropertiesToStringConverter()); converterRegistry.addConverter(new StringToUUIDConverter()); converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter()); } }
DefaultConversionService中在给converterRegistry添加转化器分为了三类去添加:addScalarConverters-参数到其他类型参数;addCollectionConverters-集合转化器;addDefaultConverters-默认转化器。
查找相应类型转化器的方式,通过sourceType,targetType去配置。在注册转化器时,会记录该converter是将什么类型的数据处理为什么类型的数据,其实就是记录了sourceType,targetType。
SpringMvc中Converter的用法
Converter是SpringMvc框架中的一个功能点,通过转化器可以实现对UI端传递的数据进行类型转化,实现类型转化可以实现接口Converter<S,T>接口、ConverterFactory接口、GenericConverter接口。ConverterRegistry接口就是对这三种类型提供了对应的注册方法。
Converter接口用法:
Converter接口的定义:
public interface Converter<S, T> { T convert(S source); }
接口是使用了泛型的,第一个类型表示原类型,第二个类型表示目标类型,然后里面定义了一个convert方法,将原类型对象作为参数传入进行转换之后返回目标类型对象。
用法:
自定义实现字符串日期转化为日期类型供接口接收:
import org.springframework.core.convert.converter.Converter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; public class StringToDateConverter implements Converter<String, Date> { private static ThreadLocal<SimpleDateFormat[]> formats = new ThreadLocal<SimpleDateFormat[]>() { protected SimpleDateFormat[] initialValue() { return new SimpleDateFormat[]{ new SimpleDateFormat("yyyy-MM"), new SimpleDateFormat("yyyy-MM-dd"), new SimpleDateFormat("yyyy-MM-dd HH"), new SimpleDateFormat("yyyy-MM-dd HH:mm"), new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") }; } }; @Override public Date convert(String source) { if (source == null || source.trim().equals("")) { return null; } Date result = null; String originalValue = source.trim(); if (source.matches("^\\d{4}-\\d{1,2}$")) { return parseDate(source, formats.get()[0]); } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2}$")) { return parseDate(source, formats.get()[1]); } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}$")) { return parseDate(source, formats.get()[2]); } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}$")) { return parseDate(source, formats.get()[3]); } else if (source.matches("^\\d{4}-\\d{1,2}-\\d{1,2} {1}\\d{1,2}:\\d{1,2}:\\d{1,2}$")) { return parseDate(source, formats.get()[4]); } else if (originalValue.matches("^\\d{1,13}$")) { try { long timeStamp = Long.parseLong(originalValue); if (originalValue.length() > 10) { result = new Date(timeStamp); } else { result = new Date(1000L * timeStamp); } } catch (Exception e) { result = null; e.printStackTrace(); } } else { result = null; } return result; } /** * 格式化日期 * * @param dateStr String 字符型日期 * @param dateFormat 日期格式化器 * @return Date 日期 */ public Date parseDate(String dateStr, DateFormat dateFormat) { Date date = null; try { date = dateFormat.parse(dateStr); } catch (Exception e) { } return date; } }
在WebMvcConfiguration中注入该Converter.
/** * WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现 WebMvcConfigurer 这个接口 */ @Configuration @Import({WebMvcAutoConfiguration.class}) @ComponentScan( value = "com.dx.test.web", includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class) }) public class WebMvcConfig implements WebMvcConfigurer { @Bean public StringToDateConverter stringToDateConverter() { return new StringToDateConverter(); } ... }
这样前端访问Restful api时,当api接口,接口需要接收date类型的参数时,前端传入日期字符串后,后端会使用该类型转化器将参数转化为date后传递给api接口函数。
考虑这样一种情况,我们有一个表示用户状态的枚举类型UserStatus,如果要定义一个从String转为UserStatus的Converter,根据之前Converter接口的说明,我们的StringToUserStatus大概是这个样子:
public class StringToUserStatus implements Converter<String, UserStatus> { @Override public UserStatus convert(String source) { if (source == null) { return null; } return UserStatus.valueOf(source); } }
如果这个时候有另外一个枚举类型UserType,那么我们就需要定义另外一个从String转为UserType的Converter——StringToUserType,那么我们的StringToUserType大概是这个样子:
public class StringToUserType implements Converter<String, UserType> { @Override public UserType convert(String source) { if (source == null) { return null; } return UserType.valueOf(source); } }
如果还有其他枚举类型需要定义原类型为String的Converter的时候,我们还得像上面那样定义对应的Converter。有了ConverterFactory之后,这一切都变得非常简单,因为UserStatus、UserType等其他枚举类型同属于枚举,所以这个时候我们就可以统一定义一个从String到Enum的ConverterFactory,然后从中获取对应的Converter进行convert操作。
ConverterFactory接口的用法:
ConverterFactory接口的定义:
public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); }
用法:
Spring官方已经为我们实现了这么一个StringToEnumConverterFactory:
Spring官方已经为我们实现了这么一个StringToEnumConverterFactory: package org.springframework.core.convert.support import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @SuppressWarnings({"unchecked", "rawtypes"}) final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnum(targetType); } private class StringToEnum<T extends Enum> implements Converter<String, T> { private final Class<T> enumType; public StringToEnum(Class<T> enumType) { this.enumType = enumType; } public T convert(String source) { if (source.length() == 0) { // It's an empty enum identifier: reset the enum value to null. return null; } return (T) Enum.valueOf(this.enumType, source.trim()); } } }
GenericConverter接口的用法:
GenericConverter接口是所有的Converter接口中最灵活也是最复杂的一个类型转换接口。
Converter接口只支持从一个原类型转换为一个目标类型;ConverterFactory接口只支持从一个原类型转换为一个目标类型对应的子类型;而GenericConverter接口支持在多个不同的原类型和目标类型之间进行转换,这也就是GenericConverter接口灵活和复杂的地方。
GenericConverter接口的定义:
public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); public static final class ConvertiblePair { private final Class<?> sourceType; private final Class<?> targetType; public ConvertiblePair(Class<?> sourceType, Class<?> targetType) { Assert.notNull(sourceType, "Source type must not be null"); Assert.notNull(targetType, "Target type must not be null"); this.sourceType = sourceType; this.targetType = targetType; } public Class<?> getSourceType() { return this.sourceType; } public Class<?> getTargetType() { return this.targetType; } } }
关于GenericConverter的使用,这里也举一个例子。假设我们有一项需求是希望能通过user的id或者username直接转换为对应的user对象,那么我们就可以针对于id和username来建立一个GenericConverter。这里假设id是int型,而username是String型的,所以我们的GenericConverter可以这样来写:
public class UserGenericConverter implements GenericConverter { @Autowired private UserService userService; @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) { return null; } User user = null; if (sourceType.getType() == Integer.class) { user = userService.findById((Integer) source);//根据id来查找user } else if (sourceType.getType() == String.class) { user = userService.find((String)source);//根据用户名来查找user } return user; } @Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>(); pairs.add(new ConvertiblePair(Integer.class, User.class)); pairs.add(new ConvertiblePair(String.class, User.class)); return pairs; } }
使用GenericConverter实现对@RequestHeader中文参数值进行解码
默认从UI端传入到服务器端的header中文参数都会被encoder,为了实现对header中文解码,可以通过GenericConverter实现解码。
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.RequestHeader; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashSet; import java.util.Set; public class RequestHeaderDecodeConverter implements GenericConverter { private static final String ENCODE = "utf-8"; private String encoder = null; public RequestHeaderDecodeConverter(@Nullable String encoder) { if (encoder == null) { this.encoder = ENCODE; } else { this.encoder = encoder; } } @Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>(); pairs.add(new ConvertiblePair(String.class, String.class)); return pairs; } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null || sourceType == null || targetType == null) { return null; } Object userName = source; if (targetType.hasAnnotation(RequestHeader.class) && targetType.getType().equals(String.class)) { try { System.out.println(source.toString()); userName = (source != null ? URLDecoder.decode(source.toString(), ENCODE) : null); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } return userName; } }
在SpringBoot中配置中引入
/** * WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现 WebMvcConfigurer 这个接口 */ @Configuration @Import({WebMvcAutoConfiguration.class}) @ComponentScan( value = "com.dx.test.web", includeFilters = { @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class) }) public class WebMvcConfig implements WebMvcConfigurer { @Bean public RequestHeaderDecodeConverter requestHeaderDecodeConverter() { return new RequestHeaderDecodeConverter(null); } ... }
测试Controller接口
@ApiOperation(value = "查询文章列表", code = 200, httpMethod = "GET", produces = "application/json", notes = "queryById方法定义说明:根据title检索文章,返回文章列表。") @ApiImplicitParams(value = { @ApiImplicitParam(name = "userId", paramType = "header", value = "操作用户id", required = false, dataType = "String"), @ApiImplicitParam(name = "userName", paramType = "header", value = "操作用户", required = false, dataType = "String"), @ApiImplicitParam(name = "title", paramType = "query", value = "文章标题检索值", required = false, dataType = "String"), @ApiImplicitParam(name = "articleType", paramType = "query", value = "文章类型", required = false, dataType = "ArticleType"), @ApiImplicitParam(name = "createTime", paramType = "query", value = "文章发布时间", required = false, dataType = "Date") }) @RequestMapping(value = {"/articles"}, method = {RequestMethod.GET}, produces = {MediaType.APPLICATION_JSON_VALUE}) @ResponseBody public List<Article> queryList( @RequestHeader(value = "userId", required = false) String userId, @RequestHeader(value = "userName", required = false) String userName, @RequestParam(value = "title", required = false) String title, @RequestParam(value = "articleType",required = false) ArticleType articleType, @RequestParam(value = "createTime", required = false) Date createTime) { System.out.println(createTime); List<Article> articles = new ArrayList<>(); articles.add(new Article(1L, "文章1", "", "", new Date())); articles.add(new Article(2L, "文章2", "", "", new Date())); articles.add(new Article(3L, "文章3", "", "", new Date())); articles.add(new Article(4L, "文章4", "", "", new Date())); return articles.stream().filter(s -> s.getTitle().contains(title)).collect(Collectors.toList()); }
断点在Resetful api内部,可以发现当WebMvcConfiguration中注入 RequestHeaderDecodeConverter 对userName是否encoder变化情况。
《SpringMVC数据类型转换——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC》
基础才是编程人员应该深入研究的问题,比如:
1)List/Set/Map内部组成原理|区别
2)mysql索引存储结构&如何调优/b-tree特点、计算复杂度及影响复杂度的因素。。。
3)JVM运行组成与原理及调优
4)Java类加载器运行原理
5)Java中GC过程原理|使用的回收算法原理
6)Redis中hash一致性实现及与hash其他区别
7)Java多线程、线程池开发、管理Lock与Synchroined区别
8)Spring IOC/AOP 原理;加载过程的。。。
【+加关注】。