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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM