1. 前言
在日常使用中,会经常碰到时间日期、数值、金额等类型的数据,它们有一个共同的特点,即同一个值往往有多种表现方式。就拿时间日期来说,不同国家和地区的表达习惯是不一样的,中国是年月日,大单位在前,小单位在后。西方是日月年,小单位在前而大单位在后。即便是同一种语言习惯,具体的表达方式也不尽相同,比如 2022 年 7 月 8 日,2022-07-08,2022/07/08。
时间日期在 Java 中使用 long
或 Date
等类型表示,当转换为字符串之后,却有多种多样的表现形式。这不仅是一个类型转换成另一个类型的问题,而是与文本内容的显示风格(style)有关。转换器的主要功能是不同类型之间的转换,我们需要一种新的组件聚焦表达方式的多样性,而这正是格式化器(formatter)的用武之地。
2. 格式化器
2.1 概述
格式化是指将指定类型转换成字符串,以文本的方式进行展示。这意味着对同一个原始值来说,以不同的文本形式进行展示,这是一种一对多的关系。java.text
包定义了常见类型的格式化类,比如 DateFormat
、NumberFormat
、MessageFormat
等。Spring 的格式化器实际上是一个包装类,底层逻辑主要由 Java 的格式化工具类完成。对于转换器(Converter)来说,两个类型之间的单向转换,因此类名必须写明 A 转换到 B。格式化器不用这么麻烦,转换的一端固定是字符串,因此只需言明另一端的类型即可,比如 DateFormatter
表示 Date
与字符串的转换。
Formatter
接口定义了格式化器的两个主要工作,其中 parse
表示将字符串解析为指定的格式(泛型 T),print
方法表示将指定的数据转换成字符串形式。从这里可以看到,格式化器和转换器有两个明显的区别,如下所示:
-
从数据流动方面来看,Converter 是单向的,Formatter 是双向的。
-
从转换类型方面来看,Converter 是两个类型之间的转换,Formatter 则是一个类型与字符串(固定类型)之间的转换。
public interface Formatter <T>{
T parse(String text, Locale locale) throws ParseException;
String print(T object, Locale locale);
}
2.2 DateFormatter
DateFormatter
类的 parse
方法和 print
方法都是调用 DateFormat
的相关方法,因此我们只需要关心 DateFormat
对象是如何创建的。getDateFormat
方法实现了三种创建方式,如下所示:
-
使用指定的
pattern
,比如yyyy-MM-dd HH:mm:ss
-
通过
ISO
枚举间接指定pattern
,仅支持三种pattern
。其中yyyy-MM-dd
表示日期,HH:mm:ss.SSSZ
表示时间,yyyy-MM-dd'T'HH:mm:ss.SSSZ
表示日期时间。 -
通过
style
字段指定DateFormat
的相关样式,详情请查阅相关 API。
public class DateFormatter implements Formatter<Date> {
private String pattern;
private int style = DateFormat.DEFAULT;
private ISO iso;
@Override
public Date parse(String text, Locale locale) throws ParseException {
return getDateFormat().parse(text);
}
@Override
public String print(Date date, Locale locale) {
return getDateFormat().format(date);
}
protected DateFormat getDateFormat(Locale locale) {
//使用指定的pattern
if (StringUtils.hasLength(this.pattern)) {
return new SimpleDateFormat(this.pattern, locale);
}
//通过IOS枚举指定pattern
if (this.iso != null && this.iso != ISO.NONE) {
String pattern = ISO_PATTERNS.get(this.iso);
if (pattern == null) {
throw new IllegalStateException("Unsupported ISO format " + this.iso);
}
SimpleDateFormat format = new SimpleDateFormat(pattern, locale);
format.setTimeZone(UTC);
return format;
}
//指定DateFormat的样式,默认为MEDIUM
return DateFormat.getDateInstance(this.style, locale);
}
}
2.3 NumberStyleFormatter
与时间日期格式化的逻辑类似,数值格式化也是把相关操作委托给 Java 原生的 NumberFormat
处理,getNumberFormat
方法实现了两种格式化器。其中 NumberFormat
用于处理普通的数值,DecimalFormat
可以处理特殊的数值,比如使用科学计数法表示的数值。
public class NumberStyleFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
return getNumberFormat().parse(text);
}
@Override
public String print(Number number, Locale locale) {
return getNumberFormat().format(number);
}
private NumberFormat getNumberFormat(Locale locale) {
//处理普通的数值
NumberFormat format = NumberFormat.getInstance(locale);
if (!(format instanceof DecimalFormat)) {
if (this.pattern != null) {
throw new IllegalStateException("Cannot support pattern for non-DecimalFormat: " + format);
}
return format;
}
//处理Decimal数值
DecimalFormat decimalFormat = (DecimalFormat) format;
decimalFormat.setParseBigDecimal(true);
if (this.pattern != null) {
decimalFormat.applyPattern(this.pattern);
}
return decimalFormat;
}
}
2.4 CurrencyStyleFormatter
除了被用来计算的数字之外,还有一种经常会用到的数值类型,就是货币的金额。货币金额与普通数字的区别在于,除了表示面额的数字外,还有表示币种的特殊字符。比如 ¥ 表示人民币,$ 表示美元,€ 表示欧元,£ 表示英镑,因此货币金额特别强调本地化(Locale)。CurrencyStyleFormatter
用于对货币金额进行转换,包括四个属性:
fractionDigits
表示小数位数,默认保留两位小数roundingMode
表示如何取近似值,比如四舍五入、截断等currency
表示货币的种类pattern
表示对应字符串的格式
public class CurrencyStyleFormatter implements Formatter<Number> {
private int fractionDigits = 2; //小数位数
private RoundingMode roundingMode; //近似值模式
private Currency currency; //货币
private String pattern; //指定格式
}
parse
和 print
方法都需要调用 getNumberFormat
方法来获取 NumberFormat
实例。需要注意的是,getNumberFormat
方法创建的是 NumberFormat
的子类DecimalFormat
,用来精确地表示浮点数),且必须指定 Locale
参数。然后还要对实例进行设置,比如指定保留几位小数(默认是 2 位);近似值的取值模式,直接截断还是四舍五入等;还可以通过 Currency
指定币种,或者直接指定 pattern
。
public class CurrencyStyleFormatter implements Formatter<Number> {
@Override
public Number parse(String text, Locale locale) throws ParseException {
BigDecimal decimal = (BigDecimal) getNumberFormat(locale).parse(text);
if (decimal != null) {
if (this.roundingMode != null) {
decimal = decimal.setScale(this.fractionDigits, this.roundingMode);
}
else {
decimal = decimal.setScale(this.fractionDigits);
}
}
return decimal;
}
@Override
public String print(Number number, Locale locale) {
return getNumberFormat(locale).format(number);
}
//创建一个可以处理Decimal的格式化类的实例
protected NumberFormat getNumberFormat(Locale locale) {
DecimalFormat format = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
format.setParseBigDecimal(true);
format.setMaximumFractionDigits(this.fractionDigits);
format.setMinimumFractionDigits(this.fractionDigits);
if (this.roundingMode != null) {
format.setRoundingMode(this.roundingMode);
}
if (this.currency != null) {
format.setCurrency(this.currency);
}
if (this.pattern != null) {
format.applyPattern(this.pattern);
}
return format;
}
}
3. 转换服务
3.1 概述
格式化器单独使用的意义并不大,Java 提供的各种格式化工具类就可以胜任这一工作。格式化器真正的用武之地是作为转换器的补充,依托 Spring 的转换服务,提供强大的类型转换功能。整合过程分为三步:
- 将 JDK 格式化类包装成 Spring 格式化器。
- Spring 格式化器被进一步包装成
Converter
对象。由于格式化器是双向转换,而Converter
是单向转换,因此一个格式化器对应两个Converter
。 - 将
Converter
注册到FormattingConversionService
的转换器列表中,以类型转换的方式来提供格式化的服务。
从类图中可以看到,FormattingConversionService
作为核心类,继承了已有的转换服务,并拥有注册格式化器的能力。
3.2 FormatterRegistry
FormatterRegistry
接口继承了 ConverterRegistry
接口,定义了添加格式化器的方法。
public interface FormatterRegistry extends ConverterRegistry {
void addFormatter(Formatter<?> formatter);
}
3.3 FormattingConversionService
FormattingConversionService
除了用于转换器的功能之外,还新增了格式化的功能。格式化器是包装成转换器来使用的,由于格式化器是双向的,而转换器是单向的,因此 addFormatter
方法的实质是添加了两个对应的转换器来进行适配。这两个转换器都是 FormattingConversionService
的内部类,各自负责一个方向的转换。
PrinterConverter
表示将指定类型转成字符串,底层调用了格式化器的print
方法ParserConverter
表示将字符串转成指定类型,底层调用了格式化器的parse
方法
public class FormattingConversionService extends GenericConversionService implements FormatterRegistry {
@Override
public void addFormatter(Formatter<?> formatter) {
Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class);
addConverter(new PrinterConverter(fieldType, formatter));
addConverter(new ParserConverter(fieldType, formatter));
}
}
PrinterConverter
实现了 GenericConverter
接口,getConvertibleTypes
方法的作用是判断转换前后的两个类型,converter
方法是真正的转换逻辑,实际上调用了格式化器的 print
方法。ParserConverter
的逻辑是类似的,这里不展开介绍,详情参考代码。
//内部类
private static class PrinterConverter implements GenericConverter {
private final Class<?> fieldType;
private final Formatter formatter;
public PrinterConverter(Class<?> fieldType, Formatter<?> formatter) {
this.fieldType = fieldType;
this.formatter = formatter;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(this.fieldType, String.class));
}
@Override
@SuppressWarnings("unchecked")
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return "";
}
return this.formatter.print(source, Locale.getDefault());
}
}
//ParserConverter,实现略
4. 测试
4.1 时间日期格式化
//测试方法
@Test
public void testDateFormatter() throws ParseException {
//指定pattern
Locale locale = Locale.getDefault();
DateFormatter patternFt = new DateFormatter("yyyy/MM/dd");
System.out.println("指定pattern -> 编码:" + patternFt.print(new Date(), locale));
System.out.println("指定pattern -> 解码:" + patternFt.parse("2022/08/08", locale));
//指定ISO枚举
DateFormatter isoFt = new DateFormatter();
isoFt.setIso(DateFormatter.ISO.TIME);
System.out.println("指定ISO类型 -> 编码:" + isoFt.print(new Date(), locale));
System.out.println("指定ISO类型 -> 解码:" + isoFt.parse("12:05:38.148+0800", locale));
//使用默认的DateFormat格式,即yyyy-mm-dd
DateFormatter defaultFt = new DateFormatter();
System.out.println("默认格式 -> 编码:" + defaultFt.print(new Date(), locale));
System.out.println("默认格式 -> 解码:" + defaultFt.parse("2022-8-16", locale));
}
测试结果分为三组,
指定pattern -> 编码:2023/05/30
指定pattern -> 解码:Mon Aug 08 00:00:00 CST 2022
指定ISO类型 -> 编码:01:45:47.720+0000
指定ISO类型 -> 解码:Thu Jan 01 12:05:38 CST 1970
默认格式 -> 编码:2023-5-30
默认格式 -> 解码:Tue Aug 16 00:00:00 CST 2022
4.2 数值格式化
将字符串转为数值,实际类型是 BigDecimal
。将数值类型转换为字符串,原始的数字可以表示为科学计数法。
//测试方法
@Test
public void testNumberStyleFormatter() throws ParseException {
Locale locale = Locale.getDefault();
NumberStyleFormatter formatter = new NumberStyleFormatter();
System.out.println("字符串转Decimal:" + formatter.parse("1,138.04", locale));
System.out.println("Decimal转字符串:" + formatter.print(new BigDecimal("1.23E+3"), locale));
}
从测试结果可以看到,字符串和 BigDecimal
可以互相转换。
字符串转数值:1138.04
Decimal转字符串:1,230
4.3 货币金额格式化
首先创建一个 CurrencyStyleFormatter
实例,在进行解析的时候要指定正确的 Locale
参数,否则会抛出 ParseException
异常。
//测试方法
@Test
public void testCurrencyStyleFormatter() throws ParseException {
CurrencyStyleFormatter formatter = new CurrencyStyleFormatter();
//人民币
Number cnPrice = formatter.parse("¥999.26", Locale.SIMPLIFIED_CHINESE);
System.out.println("人民币: " + cnPrice);
//美元
Number usPrice = formatter.parse("$11,050.26", Locale.US);
System.out.println("美元: " + usPrice);
//欧元(德国)
Number dePrice = formatter.parse("128 €", Locale.GERMANY);
System.out.println("欧元(德国):" + dePrice);
//英镑
Number ukPrice = formatter.parse("£588.09", Locale.UK);
System.out.println("英镑:" + ukPrice);
}
从测试结果可以看到,不同国家的货币得到了正确的显示。
人民币: 999.26
美元: 11050.26
欧元(德国):128.00
英镑:588.09
4.4 格式化转换服务
在测试方法中,创建了一个 FormattingConversionService
实例,添加了两个格式化器 DateFormatter
和 NumberStyleFormatter
。实际调用的不再是格式化器的 parse
或 print
方法,而是转换器的 convert
方法。
//测试方法
@Test
public void testFormattingConversionService(){
FormattingConversionService conversionService = new FormattingConversionService();
conversionService.addFormatter(new DateFormatter("yyyy/MM/dd"));
conversionService.addFormatter(new NumberStyleFormatter());
//调用convert方法,以转换器的方式使用格式化器
String dateStr = conversionService.convert(new Date(), String.class);
String numberStr = conversionService.convert(new BigDecimal("1.23E+3"), String.class);
System.out.println("日期格式化转换器: " + dateStr);
System.out.println("数值格式化转换器: " + numberStr);
}
从测试结果可以看到,日期和数值的格式化执行成功,说明格式化器都被包装成了转换器。
日期格式化转换器: 2023/05/30
数值格式化转换器: 1,230
5. 总结
格式化器的主要作用是将某一类型的数据转换成字符串,并以不同的方式予以呈现。本节介绍了三种常见的格式化器,其中 DateFormatter
用于时间日期的格式化,NumberStyleFormatter
用于数值类型的格式化,CurrencyStyleFormatter
用于货币金额的格式化,是数值类型的一个细分类别。它们的共同特点是以 Java 提供的格式化类为基础,因此格式化器是一个包装类。
一般来说,格式化器不是用来单独使用的,而是作为 Spring 已有的类型转换功能的补充,先适配成转换器类,然后集成到转换服务中。FormattingConversionService
就是一个拥有了格式化功能的转换服务类,该类又作为 TypeConverter
的一部分,对外提供通用的类型转换服务。
6. 项目信息
新增修改一览,新增(7),修改(0)。
context
└─ src
├─ main
│ └─ java
│ └─ cn.stimd.spring
│ └─ format
│ ├─ datetime
│ │ └─ DateFormatter.java (+)
│ ├─ number
│ │ ├─ CurrencyStyleFormatter.java (+)
│ │ └─ NumberStyleFormatter.java (+)
│ ├─ support
│ │ └─ FormattingConversionService.java (+)
│ ├─ Formatter.java (+)
│ └─ FormatterRegistry.java (+)
└─ test
└─ java
└─ context
└─ data
└─ FormatterTest.java (+)
注:+号表示新增、*表示修改
注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。
欢迎关注公众号【Java编程探微】,加群一起讨论。
原创不易,觉得内容不错请关注、点赞、收藏。