在上篇文章中,我们已经学习过了Spring中的类型转换机制。现在我们考虑这样一个需求:在我们web应用中,我们经常需要将前端传入的字符串类型的数据转换成指定格式或者指定数据类型来满足我们调用需求,同样的,后端开发也需要将返回数据调整成指定格式或者指定类型返回到前端页面。这种情况下,Converter已经没法直接支撑我们的需求了。这个时候,格式化的作用就很明显了,这篇文章我们就来介绍Spring中格式化的一套体系。本文主要涉及官网中的3.53.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

类比我们上篇文中介绍的GenericConversionServiceDefaultConversionService,它相比于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);

其中的JodaTimeFormatterRegistrarDateFormatterRegistrar就是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添加格式化器即可
}
}

配置实现的原理

  1. @EnableWebMvc注解上导入了一个DelegatingWebMvcConfiguration
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
  1. 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中的格式化到此就结束了,总结画图如下:

Spring官网阅读(十五)Spring中的格式化(Formatter)的更多相关文章

  1. Spring官网阅读(五)BeanDefinition(下)

    上篇文章已经对BeanDefinition做了一系列的介绍,这篇文章我们开始学习BeanDefinition合并的一些知识,完善我们整个BeanDefinition的体系,Spring在创建一个bea ...

  2. Spring官网阅读(十六)Spring中的数据绑定

    文章目录 DataBinder UML类图 使用示例 源码分析 bind方法 doBind方法 applyPropertyValues方法 获取一个属性访问器 通过属性访问器直接set属性值 1.se ...

  3. Spring官网阅读 | 总结篇

    接近用了4个多月的时间,完成了整个<Spring官网阅读>系列的文章,本文主要对本系列所有的文章做一个总结,同时也将所有的目录汇总成一篇文章方便各位读者来阅读. 下面这张图是我整个的写作大 ...

  4. Spring官网阅读(十八)Spring中的AOP

    文章目录 什么是AOP AOP中的核心概念 切面 连接点 通知 切点 引入 目标对象 代理对象 织入 Spring中如何使用AOP 1.开启AOP 2.申明切面 3.申明切点 切点表达式 excecu ...

  5. Spring官网阅读(十七)Spring中的数据校验

    文章目录 Java中的数据校验 Bean Validation(JSR 380) 使用示例 Spring对Bean Validation的支持 Spring中的Validator 接口定义 UML类图 ...

  6. Spring官网阅读(十一)ApplicationContext详细介绍(上)

    文章目录 ApplicationContext 1.ApplicationContext的继承关系 2.ApplicationContext的功能 Spring中的国际化(MessageSource) ...

  7. Spring官网阅读(三)自动注入

    上篇文章我们已经学习了1.4小结中关于依赖注入跟方法注入的内容.这篇文章我们继续学习这结中的其他内容,顺便解决下我们上篇文章留下来的一个问题-----注入模型. 文章目录 前言: 自动注入: 自动注入 ...

  8. Spring官网阅读(一)容器及实例化

    从今天开始,我们一起过一遍Spring的官网,一边读,一边结合在路神课堂上学习的知识,讲一讲自己的理解.不管是之前关于动态代理的文章,还是读Spring的官网,都是为了之后对Spring的源码做更全面 ...

  9. Spring官网阅读(二)(依赖注入及方法注入)

    上篇文章我们学习了官网中的1.2,1.3两小节,主要是涉及了容器,以及Spring实例化对象的一些知识.这篇文章我们继续学习Spring官网,主要是针对1.4小节,主要涉及到Spring的依赖注入.虽 ...

随机推荐

  1. java的套接字实现远程连接

    package jnet;//客户端程序,使用套接字连接服务器import java.net.*;import java.io.*;import javax.swing.*; public class ...

  2. AJ学IOS(50)多线程网络之GCD简单介绍(任务,队列)

    AJ分享,必须精品 GCD简单介绍 1.什么是GCD? 全称是Grand Central Dispatch,可译为“牛逼的中枢调度器” 纯C语言,提供了非常多强大的函数 2.GCD的优势 GCD是苹果 ...

  3. 【Java】从Null开始,在Windows上下载和安装JDK

    下载部分: 方式一: 从官方网站上下载:https://www.oracle.com/java/technologies/javase-downloads.html Oracle已经更新了软件政策,要 ...

  4. spark——详解rdd常用的转化和行动操作

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是spark第三篇文章,我们继续来看RDD的一些操作. 我们前文说道在spark当中RDD的操作可以分为两种,一种是转化操作(trans ...

  5. nmon 的下一代工具 njmon

    njmon njmon = nmon + JSON format + real-time push to a stats database + instant graphing of "al ...

  6. Android应用架构分析

    一.res目录: 1.属性:Android必需: 2.作用:存放Android项目的各种资源文件.这些资源会自动生成R.java. 2.1.layout:存放界面布局文件. 2.2.strings.x ...

  7. 10个步骤教你如何安装Anaconda安装,Python数据分析入门必看

    前言 文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:小白 PS:如有需要Python学习资料的小伙伴可以加点击下方链接自行 ...

  8. 极验反爬虫防护分析之slide验证方式下图片的处理及滑动轨迹的生成思路

    本文要分享的内容是去年为了抢鞋而分析 极验(GeeTest)反爬虫防护的笔记,由于篇幅较长(为了多混点CB)我会按照我的分析顺序,分成如下四个主题与大家分享: 极验反爬虫防护分析之交互流程分析 极验反 ...

  9. 在.NET Core中用最原生的方式读取Nacos的配置

    背景 之前老黄写过一篇<ASP.NET Core结合Nacos来完成配置管理和服务发现>简单介绍了如何让.NET Core程序接入Nacos,之前的SDK里面更多的是对Nacos的Open ...

  10. 百度paddlepaddle学习体会

    一个偶然从微信公众号中刷到了<python小白逆袭A1大神>的文章,让我不经意的邂逅了飞桨(paddlepaddle),通过加入飞桨训练营一周的学习.实践,对飞桨有了很多的了解(飞桨官网: ...