Spring中欄位格式化的使用詳解

語言: CN / TW / HK

環境:Spring5.3.12.RELEASE。

Spring提供的一個core.convert包 是一個通用型別轉換系統。它提供了統一的 ConversionService   API和強型別的Converter SPI,用於實現從一種型別到另一種型別的轉換邏輯。Spring容器使用這個系統繫結bean屬性值。此外,Spring Expression Language (SpEL)和DataBinder都使用這個系統來繫結欄位值。例如,當SpEL需要將Short強制轉換為Long來完成表示式時。setValue(物件bean,物件值)嘗試,核心。轉換系統執行強制轉換。

現在考慮典型客戶端環境(例如web或桌面應用程式)的型別轉換需求。在這種環境中,通常從String轉換為支援客戶端回發過程,並返回String以支援檢視呈現過程。此外,你經常需要本地化String值。更一般的core.convert Converter SPI不能直接解決這些格式要求。為了直接解決這些問題,Spring 3引入了一個方便的Formatter SPI,它為客戶端環境提供了PropertyEditor實現的一個簡單而健壯的替代方案。

通常,當你需要實現通用型別轉換邏輯時,你可以使用Converter SPI,例如,用於java.util.Date和Long之間的轉換。當你在客戶端環境(例如web應用程式)中工作並需要解析和列印本地化的欄位值時,你可以使用Formatter SPI。ConversionService為這兩個spi提供了統一的型別轉換API。

1、Formatter SPI

實現欄位格式化邏輯的Formatter SPI是簡單且強型別的。下面的清單顯示了Formatter介面定義:

package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

Formatter繼承自Printer和Parser介面。

@FunctionalInterface
public interface Printer<T> {
  String print(T object, Locale locale);
}
@FunctionalInterface
public interface Parser<T> {
  T parse(String text, Locale locale) throws ParseException;
}

預設情況下Spring容器提供了幾個Formatter實現。數字包提供了NumberStyleFormatter、CurrencyStyleFormatter和PercentStyleFormatter來格式化使用java.text.NumberFormat的number物件。datetime包提供了一個DateFormatter來用java.text.DateFormat格式化 java.util.Date物件。

自定義Formatter。

public class StringToNumberFormatter implements Formatter<Number> {
  @Override
  public String print(Number object, Locale locale) {
    return "結果是:" + object.toString();
  }
  @Override
  public Number parse(String text, Locale locale) throws ParseException {
    return NumberFormat.getInstance().parse(text);
  }
}

如何使用?我們可以通過FormattingConversionService 轉換服務進行新增自定義的格式化類。

FormattingConversionService fcs = new FormattingConversionService();
// 預設如果不新增自定義的格式化類,那麼程式執行將會報錯
fcs.addFormatterForFieldType(Number.class, new StringToNumberFormatter());
Number number = fcs.convert("100.5", Number.class);
System.out.println(number);

檢視原始碼:

public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {
  public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
    addConverter(new PrinterConverter(fieldType, formatter, this));
    addConverter(new ParserConverter(fieldType, formatter, this));
  }
  private static class PrinterConverter implements GenericConverter {}
  private static class ParserConverter implements GenericConverter {}
}
public class GenericConversionService implements ConfigurableConversionService {
  private final Converters converters = new Converters();
  public void addConverter(GenericConverter converter) {
    this.converters.add(converter);
  }
}

Formatter最後還是被適配成GenericConverter(型別轉換介面)。

2、基於註解的格式化

欄位格式化可以通過欄位型別或註釋進行配置。要將註釋繫結到Formatter,請實現AnnotationFormatterFactory。

public interface AnnotationFormatterFactory<A extends Annotation> {
  Set<Class<?>> getFieldTypes();
  Printer<?> getPrinter(A annotation, Class<?> fieldType);
  Parser<?> getParser(A annotation, Class<?> fieldType);
}

類及其方法說明:

引數化A為欄位註解型別,你希望將該欄位與格式化邏輯關聯,例如org.springframework.format.annotation.DateTimeFormat。

  1. getFieldTypes()返回可以使用註釋的欄位型別。
  2. getPrinter()返回一個Printer來列印帶註釋的欄位的值。
  3. getParser()返回一個Parser來解析帶註釋欄位的clientValue。

自定義註解解析器。

public class StringToDateFormatter implements AnnotationFormatterFactory<DateFormatter> {
  @Override
  public Set<Class<?>> getFieldTypes() {
    return new HashSet<Class<?>>(Arrays.asList(Date.class));
  }
  @Override
  public Printer<?> getPrinter(DateFormatter annotation, Class<?> fieldType) {
    return getFormatter(annotation, fieldType);
  }
  @Override
  public Parser<?> getParser(DateFormatter annotation, Class<?> fieldType) {
    return getFormatter(annotation, fieldType);
  }
  private StringFormatter getFormatter(DateFormatter annotation, Class<?> fieldType) {
    String pattern = annotation.value();
    return new StringFormatter(pattern);
  }
  class StringFormatter implements Formatter<Date> {
    private String pattern;
    public StringFormatter(String pattern) {
      this.pattern = pattern;
    }
    @Override
    public String print(Date date, Locale locale) {
      return DateTimeFormatter.ofPattern(pattern, locale).format(date.toInstant());
    }
    @Override
    public Date parse(String text, Locale locale) throws ParseException {
      DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern, locale);
      return Date.from(LocalDate.parse(text, formatter).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
    }
  }
}

註冊及使用:

public class Main {
  @DateFormatter("yyyy年MM月dd日")
  private Date date;
  public static void main(String[] args) throws Exception {
    FormattingConversionService fcs = new FormattingConversionService();
    fcs.addFormatterForFieldAnnotation(new StringToDateFormatter());
    Main main = new Main();
    Field field = main.getClass().getDeclaredField("date");
    TypeDescriptor targetType = new TypeDescriptor(field);
    Object result = fcs.convert("2022年01月21日", targetType);
    System.out.println(result);
    field.setAccessible(true);
    field.set(main, result);
    System.out.println(main.date);
  }
}

3、FormatterRegistry

FormatterRegistry是用於註冊格式化程式和轉換器的SPI。FormattingConversionService是FormatterRegistry的一個實現,適用於大多數環境。可以通過程式設計或宣告的方式將該變體配置為Spring bean,例如使用FormattingConversionServiceFactoryBean。因為這個實現還實現了ConversionService,所以您可以直接配置它,以便與Spring的DataBinder和Spring表示式語言(SpEL)一起使用。

public interface FormatterRegistry extends ConverterRegistry {
  void addPrinter(Printer<?> printer);
  void addParser(Parser<?> parser);
  void addFormatter(Formatter<?> formatter);
  void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
  void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
  void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}

4、SpringMVC中配置型別轉換

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
  @Override
  public void addFormatters(FormatterRegistry registry) {
    // ...
  }
}

SpringBoot中有如下的預設型別轉換器。

public class WebMvcAutoConfiguration {
  public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
    @Bean
    @Override
    public FormattingConversionService mvcConversionService() {
      Format format = this.mvcProperties.getFormat();
      WebConversionService conversionService = new WebConversionService(new DateTimeFormatters().dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
      addFormatters(conversionService);
      return conversionService;
    }    
  }
}