在上篇文章中,我們已經學習過了Spring中的類型轉換機制。現在我們考慮這樣一個需求:在我們web應用中,我們經常需要將前端傳入的字符串類型的數據轉換成指定格式或者指定數據類型來滿足我們調用需求,同樣的,後端開發也需要將返回數據調整成指定格式或者指定類型返回到前端頁面。這種情況下,Converter已經沒法直接支撐我們的需求了。這個時候,格式化的作用就很明顯了,這篇文章我們就來介紹Spring中格式化的一套體系。本文主要涉及官網中的
3.5
及3.6
小結
Formatter
接口定義
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
可以看到,本身這個接口沒有定義任何方法,只是聚合了另外兩個接口的功能
- Printer
// 將T類型的數據根據Locale信息打印成指定格式,即返回字符串的格式
public interface Printer<T> {
String print(T fieldValue, Locale locale);
}
- Parser
public interface Parser<T> {
// 將指定的字符串根據Locale信息轉換成指定的T類型數據
T parse(String clientValue, Locale locale) throws ParseException;
}
從上面可以看出,這個兩個接口維護了兩個功能相反的方法,分別完成對String類型數據的解析以及格式化。
繼承樹
可以發現整個繼承關係並不複雜,甚至可以說非常簡單。只有一個抽象子類,AbstractNumberFormatter
,這個類抽象了對數字進行格式化時的一些方法,它有三個子類,分別處理不同的數字類型,包括貨幣
,百分數
,正常數字
。其餘的子類都是直接實現了Formatter
接口。其中我們比較熟悉的可能就是DateFormatter
了
使用如下:
public class Main {
public static void main(String[] args) throws Exception {
DateFormatter dateFormatter = new DateFormatter();
dateFormatter.setIso(DateTimeFormat.ISO.DATE);
System.out.println(dateFormatter.print(new Date(), Locale.CHINA));
System.out.println(dateFormatter.parse("2020-03-26", Locale.CHINA));
// 程序打印:
// 2020-03-26
// Thu Mar 26 08:00:00 CST 2020
}
}
註解驅動的格式化
我們在配置格式化時,除了根據類型進行格式外(比如常見的根據Date類型進行格式化),還可以根據註解來進行格式化,最常見的註解就是org.springframework.format.annotation.DateTimeFormat
。除此之外還有NumberFormat
,它們都在format包下。
爲了將一個註解綁定到指定的格式化器上,我們需要藉助到一個接口AnnotationFormatterFactory
AnnotationFormatterFactory
public interface AnnotationFormatterFactory<A extends Annotation> {
// 可能被添加註解的字段的類型
Set<Class<?>> getFieldTypes();
// 根據註解及字段類型獲取一個格式化器
Printer<?> getPrinter(A annotation, Class<?> fieldType);
// 根據註解及字段類型獲取一個解析器
Parser<?> getParser(A annotation, Class<?> fieldType);
}
以Spring內置的一個DateTimeFormatAnnotationFormatterFactory
來說,這個類實現的功能就是將DateTimeFormat
註解綁定到指定的格式化器,源碼如下:
public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport
implements AnnotationFormatterFactory<DateTimeFormat> {
private static final Set<Class<?>> FIELD_TYPES;
// 只有在這些類型下加這個註解纔會進行格式化
static {
Set<Class<?>> fieldTypes = new HashSet<>(4);
fieldTypes.add(Date.class);
fieldTypes.add(Calendar.class);
fieldTypes.add(Long.class);
FIELD_TYPES = Collections.unmodifiableSet(fieldTypes);
}
@Override
public Set<Class<?>> getFieldTypes() {
return FIELD_TYPES;
}
@Override
public Printer<?> getPrinter(DateTimeFormat annotation, Class<?> fieldType) {
return getFormatter(annotation, fieldType);
}
@Override
public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) {
return getFormatter(annotation, fieldType);
}
protected Formatter<Date> getFormatter(DateTimeFormat annotation, Class<?> fieldType) { // 通過這個DateFormatter來完成格式化
DateFormatter formatter = new DateFormatter();
String style = resolveEmbeddedValue(annotation.style());
if (StringUtils.hasLength(style)) {
formatter.setStylePattern(style);
}
formatter.setIso(annotation.iso());
String pattern = resolveEmbeddedValue(annotation.pattern());
if (StringUtils.hasLength(pattern)) {
formatter.setPattern(pattern);
}
return formatter;
}
}
使用@DateTimeFormat
,我們只需要在字段上添加即可
public class MyModel {
@DateTimeFormat(iso=ISO.DATE)
private Date date;
}
關於日期的格式化,Spring還提供了一個類似的AnnotationFormatterFactory
,專門用於處理java8中的日期格式,如下
public class Jsr310DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport
implements AnnotationFormatterFactory<DateTimeFormat> {
private static final Set<Class<?>> FIELD_TYPES;
static {
// 這裏添加了對Java8日期的支持
Set<Class<?>> fieldTypes = new HashSet<>(8);
fieldTypes.add(LocalDate.class);
fieldTypes.add(LocalTime.class);
fieldTypes.add(LocalDateTime.class);
fieldTypes.add(ZonedDateTime.class);
fieldTypes.add(OffsetDateTime.class);
fieldTypes.add(OffsetTime.class);
FIELD_TYPES = Collections.unmodifiableSet(fieldTypes);
}
........
學習到現在,對Spring的脾氣大家應該都有所瞭解,上面這些都是定義了具體的功能實現,它們必定會有一個管理者,一個Registry
,用來註冊這些格式化器
FormatterRegistry
接口定義
// 繼承了ConverterRegistry,所以它同時還是一個Converter註冊器
public interface FormatterRegistry extends ConverterRegistry {
// 一系列添加格式化器的方法
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Formatter<?> formatter);
void addFormatterForAnnotation(AnnotationFormatterFactory<?, ?> factory);
}
UML類圖
我們可以發現FormatterRegistry
默認只有兩個實現類
FormattingConversionService
// 繼承了GenericConversionService ,所以它能對Converter進行一系列的操作
// 實現了接口FormatterRegistry,所以它也可以註冊格式化器了
// 實現了EmbeddedValueResolverAware,所以它還能有非常強大的功能:處理佔位符
public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware {
// ....
// 最終也是交給addFormatterForFieldType去做的
// getFieldType:它會拿到泛型類型。並且支持DecoratingProxy
@Override
public void addFormatter(Formatter<?> formatter) {
addFormatterForFieldType(getFieldType(formatter), formatter);
}
// 存儲都是分開存儲的 讀寫分離
// PrinterConverter和ParserConverter都是一個GenericConverter 採用內部類實現的
// 注意:他們的ConvertiblePair必有一個類型是String.class
// Locale一般都可以這麼獲取:LocaleContextHolder.getLocale()
// 在進行printer之前,會先判斷是否能進行類型轉換,如果能進行類型轉換會先進行類型轉換,之後再格式化
// 在parse之後,會判斷是否還需要進行類型轉換,如果需要類型轉換會先進行類型轉換
@Override
public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
addConverter(new PrinterConverter(fieldType, formatter, this));
addConverter(new ParserConverter(fieldType, formatter, this));
}
// 哪怕你是一個AnnotationFormatterFactory,最終也是被適配成了GenericConverter(ConditionalGenericConverter)
@Override
public void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory) {
Class<? extends Annotation> annotationType = getAnnotationType(annotationFormatterFactory);
// 若你自定義的實現了EmbeddedValueResolverAware接口,還可以使用佔位符喲
// AnnotationFormatterFactory是下面的重點內容
if (this.embeddedValueResolver != null && annotationFormatterFactory instanceof EmbeddedValueResolverAware) {
((EmbeddedValueResolverAware) annotationFormatterFactory).setEmbeddedValueResolver(this.embeddedValueResolver);
}
// 對每一種字段的type 都註冊一個AnnotationPrinterConverter去處理
// AnnotationPrinterConverter是一個ConditionalGenericConverter
// matches方法爲:sourceType.hasAnnotation(this.annotationType);
// 這個判斷是呼應的:因爲annotationFormatterFactory只會作用在指定的字段類型上的,不符合類型條件的不用添加
Set<Class<?>> fieldTypes = annotationFormatterFactory.getFieldTypes();
for (Class<?> fieldType : fieldTypes) {
addConverter(new AnnotationPrinterConverter(annotationType, annotationFormatterFactory, fieldType));
addConverter(new AnnotationParserConverter(annotationType, annotationFormatterFactory, fieldType));
}
}
// .......
// 持有的一個內部類
private static class PrinterConverter implements GenericConverter {
private final Class<?> fieldType;
private final TypeDescriptor printerObjectType;
@SuppressWarnings("rawtypes")
private final Printer printer;
// 最終也是通過conversionService完成類型轉換
private final ConversionService conversionService;
public PrinterConverter(Class<?> fieldType, Printer<?> printer, ConversionService conversionService) {
this.fieldType = fieldType;
this.printerObjectType =
// 會通過解析Printer中的泛型獲取具體類型,主要是爲了判斷是否需要進行類型轉換
TypeDescriptor.valueOf(resolvePrinterObjectType(printer));
this.printer = printer;
this.conversionService = conversionService;
}
// ......
}
DefaultFormattingConversionService
類比我們上篇文中介紹的GenericConversionService
跟DefaultConversionService
,它相比於FormattingConversionService
而言,提供了大量的默認的格式化器,源碼如下:
public class DefaultFormattingConversionService extends FormattingConversionService {
private static final boolean jsr354Present;
private static final boolean jodaTimePresent;
static {
ClassLoader classLoader = DefaultFormattingConversionService.class.getClassLoader();
// 判斷是否導入了jsr354相關的包
jsr354Present = ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader);
// 判斷是否導入了joda
jodaTimePresent = ClassUtils.isPresent("org.joda.time.LocalDate", classLoader);
}
// 會註冊很多默認的格式化器
public DefaultFormattingConversionService() {
this(null, true);
}
public DefaultFormattingConversionService(boolean registerDefaultFormatters) {
this(null, registerDefaultFormatters);
}
public DefaultFormattingConversionService(
@Nullable StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) {
if (embeddedValueResolver != null) {
setEmbeddedValueResolver(embeddedValueResolver);
}
DefaultConversionService.addDefaultConverters(this);
if (registerDefaultFormatters) {
addDefaultFormatters(this);
}
}
public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
// 添加針對@NumberFormat的格式化器
formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
// 針對貨幣的格式化器
if (jsr354Present) {
formatterRegistry.addFormatter(new CurrencyUnitFormatter());
formatterRegistry.addFormatter(new MonetaryAmountFormatter());
formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
}
new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
// 如沒有導入joda的包,那就默認使用Date
if (jodaTimePresent) {
// 針對Joda
new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry);
}
else {
// 沒有joda的包,是否Date
new DateFormatterRegistrar().registerFormatters(formatterRegistry);
}
}
}
FormatterRegistrar
在上面DefaultFormattingConversionService
的源碼中,有這麼幾行:
new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry);
new DateFormatterRegistrar().registerFormatters(formatterRegistry);
其中的JodaTimeFormatterRegistrar
,DateFormatterRegistrar
就是FormatterRegistrar
。那麼這個接口有什麼用呢?我們先來看看它的接口定義:
public interface FormatterRegistrar {
// 最終也是調用FormatterRegistry來完成註冊
void registerFormatters(FormatterRegistry registry);
}
我們思考一個問題,爲什麼已經有了FormatterRegistry
,Spring還要開發一個FormatterRegistrar
呢?直接使用FormatterRegistry
完成註冊不好嗎?
以這句代碼爲例:new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry)
,這段代碼是將joda
包下所有的默認的轉換器已經註冊器都註冊到formatterRegistry
中。
我們可以發現FormatterRegistrar
相當於對格式化器及轉換器進行了分組,我們調用它的registerFormatters
方法,相當於將這一組格式化器直接添加到指定的formatterRegistry
中。這樣做的好處在於,如果我們對同一個類型的數據有兩組不同的格式化策略,例如就以上面的日期爲例,我們既有可能採用joda
的策略進行格式化,也有可能採用Date
的策略進行格式化,通過分組的方式,我們可以更見方便的在確認好策略後將需要的格式化器添加到容器中。
配置SpringMVC中的格式化器
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
// 調用registry.addFormatter添加格式化器即可
}
}
配置實現的原理
@EnableWebMvc
註解上導入了一個DelegatingWebMvcConfiguration
類
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
DelegatingWebMvcConfiguration
// 繼承了WebMvcConfigurationSupport
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 這個方法會注入所有的WebMvcConfigurer,包括我們的WebConfig
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
//.....,省略無關代碼
// 複寫了父類WebMvcConfigurationSupport的方法
// 調用我們配置的configurer的addFormatters方法
@Override
protected void addFormatters(FormatterRegistry registry) {
this.configurers.addFormatters(registry);
}
//.....,省略無關代碼
}
3.WebMvcConfigurationSupport
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
// 這就是真相,這裏會創建一個FormattingConversionService,並且是一個DefaultFormattingConversionService,然後調用addFormatters方法
@Bean
public FormattingConversionService mvcConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
addFormatters(conversionService);
return conversionService;
}
protected void addFormatters(FormatterRegistry registry) {
}
}
總結
Spring中的格式化到此就結束了,總結畫圖如下: