• 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之类型转换Converter》 

    SpringMVC数据类型转换——第七章 注解式控制器的数据验证、类型转换及格式化——跟着开涛学SpringMVC

  • 相关阅读:
    thinkphp 5 隐藏index.php
    jquery ajax参数
    图标字的使用方法
    jquery监听浏览宽度
    手机屏幕的宽度自动适应
    前站常用代码
    服务消费者(Feign-上)
    服务消费者(Ribbon)
    注册中心(Eureka/Consul)
    JDK8 日期格式化
  • 原文地址:https://www.cnblogs.com/yy3b2007com/p/11757900.html
Copyright © 2020-2023  润新知