在如Web /客户端项目中,通常需要将数据转换为具有某种格式的字符串进行展示,因此上节我们学习的数据类型转换系统核心作用不是完成这个需求,因此Spring3引入了格式化转换器(Formatter SPI) 和格式化服务API(FormattingConversionService)从而支持这种需求。在Spring中它和PropertyEditor功能类似,可以替代PropertyEditor来进行对象的解析和格式化,而且支持细粒度的字段级别的格式化/解析。
Formatter SPI核心是完成解析和格式化转换逻辑,在如Web应用/客户端项目中,需要解析、打印/展示本地化的对象值时使用,如根据Locale信息将java.util.Date—->java.lang.String打印/展示、java.lang.String—->java.util.Date等。
架构
一共有如下两组四个接口:
Printer接口
Printer接口:格式化显示接口,将T类型的对象根据Locale信息以某种格式进行打印显示(即返回字符串形式);
package org.springframework.format;
public interface Printer<T> {
String print(T object, Locale locale);
}
Parser接口
Parser接口:解析接口,根据Locale信息解析字符串到T类型的对象;
package org.springframework.format;
public interface Parser<T> {
T parse(String text, Locale locale)
throws ParseException;
}
解析失败可以抛出java.text.ParseException或IllegalArgumentException异常即可。
格式化SPI接口,继承Printer和Parser接口,完成T类型对象的格式化和解析功能;
package org.springframework.format;
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
注解驱动的字段格式化工厂,用于创建带注解的对象字段的Printer和Parser,即用于格式化和解析带注解的对象字段.
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
返回用于格式化和解析被A注解类型注解的字段值的Printer和Parser。如JodaDateTimeFormatAnnotationFormatterFactory可以为带有@DateTimeFormat注解的java.util.Date字段类型创建相应的Printer和Parser进行格式化和解析
格式化转换器注册器、格式化服务
提供类型转换器注册支持,运行时类型转换API支持。 一个有如下两种接口:
FormatterRegistry:格式化转换器注册器,用于注册格式化转换器(Formatter、Printer和Parser、AnnotationFormatterFactory);
package org.springframework.format;
public interface FormatterRegistry extends ConverterRegistry {
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(
AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
}
FormattingConversionService:继承自ConversionService,运行时类型转换和格式化服务接口,提供运行期类型转换和格式化的支持。 FormattingConversionService内部实现如下图所示:
我们可以看到FormattingConversionService内部实现如上所示,当你调用convert方法时: - 若是S类型—–>String:调用私有的静态内部类PrinterConverter,其又调用相应的Printer的实现进行格式化; - 若是String—–>T类型:调用私有的静态内部类ParserConverter,其又调用相应的Parser的实现进行解析; - 若是A注解类型注解的S类型—–>String:调用私有的静态内部类AnnotationPrinterConverter,其又调用相应的AnnotationFormatterFactory的getPrinter获取Printer的实现进行格式化; - 若是String—–>A注解类型注解的T类型:调用私有的静态内部类AnnotationParserConverter,其又调用相应的AnnotationFormatterFactory的getParser获取Parser的实现进行解析。
注:S类型表示源类型,T类型表示目标类型,A表示注解类型。
此处可以可以看出之前的Converter SPI完成任意Object与Object之间的类型转换,而Formatter SPI完成任意Object与String之间的类型转换(即格式化和解析,与PropertyEditor类似)。
Spring内建的格式化转换器
类名说明
DateFormatterjava.util.Date<—->String 实现日期的格式化/解析NumberFormatterjava.lang.Number<—->String 实现通用样式的格式化/解析CurrencyFormatterjava.lang.BigDecimal<—->String 实现货币样式的格式化/解析PercentFormatterjava.lang.Number<—->String 实现百分数样式的格式化/解析NumberFormatAnnotationFormatterFactory@NumberFormat注解类型的数字字段类型<—->String ①通过@NumberFormat指定格式化/解析格式 ②可以格式化/解析的数字类型:Short、Integer、Long、Float、Double、BigDecimal、BigIntegerJodaDateTimeFormatAnnotationFormatterFactory@DateTimeFormat注解类型的日期字段类型<—->String ①通过@DateTimeFormat指定格式化/解析格式 ②可以格式化/解析的日期类型: joda中的日期类型(org.joda.time包中的):LocalDate、LocalDateTime、LocalTime、ReadableInstant java内置的日期类型:Date、Calendar、Long classpath中必须有Joda-Time类库,否则无法格式化日期类型
NumberFormatAnnotationFormatterFactory和JodaDateTimeFormatAnnotationFormatterFactory(如果classpath提供了Joda-Time类库)在使用格式化服务实现DefaultFormattingConversionService时会自动注册。
类型级别的解析/格式化
CurrencyFormatter currencyFormatter =
new CurrencyFormatter();
currencyFormatter.setFractionDigits(
2);
currencyFormatter.setRoundingMode(RoundingMode.CEILING);
Assert.assertEquals(
new BigDecimal(
"123.13"), currencyFormatter.parse(
"$123.125", Locale.US));
Assert.assertEquals(
"$123.00", currencyFormatter.print(
new BigDecimal(
"123"), Locale.US));
Assert.assertEquals(
"¥123.00", currencyFormatter.print(
new BigDecimal(
"123"), Locale.CHINA));
Assert.assertEquals(
"¥123.00", currencyFormatter.print(
new BigDecimal(
"123"), Locale.JAPAN));
parse方法:将带格式的字符串根据Locale信息解析为相应的BigDecimal类型数据; print方法:将BigDecimal类型数据根据Locale信息格式化为字符串数据进行展示。
不同于Convert SPI,Formatter SPI可以根据本地化(Locale)信息进行解析/格式化。
public class PhoneNumberFormatter implements Formatter<PhoneNumberModel> {
Pattern pattern = Pattern.compile(
"^(\\d{3,4})-(\\d{7,8})$");
@Override
public String
print(PhoneNumberModel phoneNumber, Locale locale) {
if(phoneNumber ==
null) {
return "";
}
return new StringBuilder().append(phoneNumber.getAreaCode()).append(
"-")
.append(phoneNumber.getPhoneNumber()).toString();
}
@Override
public PhoneNumberModel
parse(String text, Locale locale)
throws ParseException {
if(!StringUtils.hasLength(text)) {
return null;
}
Matcher matcher = pattern.matcher(text);
if(matcher.matches()) {
PhoneNumberModel phoneNumber =
new PhoneNumberModel();
phoneNumber.setAreaCode(matcher.group(
1));
phoneNumber.setPhoneNumber(matcher.group(
2));
return phoneNumber;
}
else {
throw new IllegalArgumentException(String.format(
"类型转换失败,需要格式[010-12345678],但格式是[%s]", text));
}
}
}
测试用例:
@Test
public void test() {
DefaultFormattingConversionService conversionService =
new DefaultFormattingConversionService();
conversionService.addFormatter(
new PhoneNumberFormatter());
Assert.assertEquals(
"010-12345678", conversionService.convert(phoneNumber, String.class));
Assert.assertEquals(
"010", conversionService.convert(
"010-12345678", PhoneNumberModel.class).getAreaCode());
}
字段级别的解析/格式化
使用内置的注解进行字段级别的解析/格式化
public class FormatterModel {
@NumberFormat(style=Style.NUMBER, pattern=
"#,###")
private int totalCount;
@NumberFormat(style=Style.PERCENT)
private double discount;
@NumberFormat(style=Style.CURRENCY)
private double sumMoney;
@DateTimeFormat(iso=ISO.DATE)
private Date registerDate;
@DateTimeFormat(pattern=
"yyyy-MM-dd HH:mm:ss")
private Date orderDate;
}
@Number:定义数字相关的解析/格式化元数据(通用样式、货币样式、百分数样式),参数如下: style:用于指定样式类型,包括三种:Style.NUMBER(通用样式) Style.CURRENCY(货币样式) Style.PERCENT(百分数样式),默认Style.NUMBER; pattern:自定义样式,如patter=”#,###”;
@DateTimeFormat:定义日期相关的解析/格式化元数据,参数如下: pattern:指定解析/格式化字段数据的模式,如”yyyy-MM-dd HH:mm:ss” iso:指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) ISO.DATE(yyyy-MM-dd) ISO.TIME(hh:mm:ss.SSSZ) ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ),默认ISO.NONE; style:指定用于格式化的样式模式,默认“SS”,具体使用请参考Joda-Time类库的org.joda.time.format.DateTimeFormat的forStyle的javadoc; 优先级: pattern 大于 iso 大于 style。
自定义注解进行字段级别的解析/格式化:
定义解析/格式化字段的注解类型:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @
interface PhoneNumber {
}
public class PhoneNumberFormatAnnotationFormatterFactory
implements AnnotationFormatterFactory<PhoneNumber> {
private final Set<Class<?>> fieldTypes;
private final PhoneNumberFormatter formatter;
public PhoneNumberFormatAnnotationFormatterFactory() {
Set<Class<?>> set =
new HashSet<Class<?>>();
set.add(PhoneNumberModel.class);
this.fieldTypes = set;
this.formatter =
new PhoneNumberFormatter();
}
@Override
public Set<Class<?>>
getFieldTypes() {
return fieldTypes;
}
@Override
public Parser<?>
getParser(PhoneNumber annotation, Class<?> fieldType) {
return formatter;
}
@Override
public Printer<?>
getPrinter(PhoneNumber annotation, Class<?> fieldType) {
return formatter;
}
}
集成到Spring Web MVC环境
package com.lf.web;
import com.lf.web.config.MyFormatterRegistry;
import com.lf.web.config.MyHttpMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.util.List;
/**
* Created by LF on 2017/5/4.
*/
@Configuration
@EnableWebMvc
@ComponentScan
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addFormatters(FormatterRegistry registry) {
super.addFormatters(registry);
registry.addFormatter(
new PhoneNumberFormatter());
}
}
示例:
(1、模型对象字段的数据解析/格式化:
@RequestMapping(value =
"/format1")
public String
test1(@
ModelAttribute("model") FormatterModel formatModel) {
return "format/success";
}
totalCount:<spring:bind path=
"model.totalCount">${status.value}</spring:bind><br/>
discount:<spring:bind path=
"model.discount">${status.value}</spring:bind><br/>
sumMoney:<spring:bind path=
"model.sumMoney">${status.value}</spring:bind><br/>
phoneNumber:<spring:bind path=
"model.phoneNumber">${status.value}</spring:bind><br/>
<!-- 如果没有配置org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor将会报错 -->
phoneNumber:<spring:eval expression=
"model.phoneNumber"></spring:eval><br/>
<br/><br/>
<form:form commandName=
"model">
<form:input path=
"phoneNumber"/><br/>
<form:input path=
"sumMoney"/>
</form:form>