7.3、數據格式化
在如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等。
該格式化轉換系統是Spring通用的,其定義在org.springframework.format包中,不僅僅在Spring Web MVC場景下。
7.3.1、架構
1、格式化轉換器:提供格式化轉換的實現支持。
一共有如下兩組四個接口:
(1、Printer接口:格式化顯示接口,將T類型的對象根據Locale信息以某種格式進行打印顯示(即返回字符串形式);
- package org.springframework.format;
- public interface Printer<T> {
- String print(T object, Locale locale);
- }
(2、Parser接口:解析接口,根據Locale信息解析字符串到T類型的對象;
- package org.springframework.format;
- public interface Parser<T> {
- T parse(String text, Locale locale) throws ParseException;
- }
解析失敗可以拋出java.text.ParseException或IllegalArgumentException異常即可。
(3、Formatter接口:格式化SPI接口,繼承Printer和Parser接口,完成T類型對象的格式化和解析功能;
- package org.springframework.format;
- public interface Formatter<T> extends Printer<T>, Parser<T> {
- }
(4、AnnotationFormatterFactory接口:註解驅動的字段格式化工廠,用於創建帶註解的對象字段的Printer和Parser,即用於格式化和解析帶註解的對象字段。
- package org.springframework.format;
- public interface AnnotationFormatterFactory<A extends Annotation> {//①可以識別的註解類型
- Set<Class<?>> getFieldTypes();//②可以被A註解類型註解的字段類型集合
- Printer<?> getPrinter(A annotation, Class<?> fieldType);//③根據A註解類型和fieldType類型獲取Printer
- Parser<?> getParser(A annotation, Class<?> fieldType);//④根據A註解類型和fieldType類型獲取Parser
- }
返回用於格式化和解析被A註解類型註解的字段值的Printer和Parser。如JodaDateTimeFormatAnnotationFormatterFactory可以爲帶有@DateTimeFormat註解的java.util.Date字段類型創建相應的Printer和Parser進行格式化和解析。
2、格式化轉換器註冊器、格式化服務:提供類型轉換器註冊支持,運行時類型轉換API支持。
一個有如下兩種接口:
(1、FormatterRegistry:格式化轉換器註冊器,用於註冊格式化轉換器(Formatter、Printer和Parser、AnnotationFormatterFactory);
- package org.springframework.format;
- public interface FormatterRegistry extends ConverterRegistry {
- //①添加格式化轉換器(Spring3.1 新增API)
- void addFormatter(Formatter<?> formatter);
- //②爲指定的字段類型添加格式化轉換器
- void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
- //③爲指定的字段類型添加Printer和Parser
- void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
- //④添加註解驅動的字段格式化工廠AnnotationFormatterFactory
- void addFormatterForFieldAnnotation(
- AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);
- }
(2、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類似)。
7.3.2、Spring內建的格式化轉換器如下所示:
類名 |
說明 |
DateFormatter |
java.util.Date<---->String 實現日期的格式化/解析 |
NumberFormatter |
java.lang.Number<---->String 實現通用樣式的格式化/解析 |
CurrencyFormatter |
java.lang.BigDecimal<---->String 實現貨幣樣式的格式化/解析 |
PercentFormatter |
java.lang.Number<---->String 實現百分數樣式的格式化/解析 |
NumberFormatAnnotationFormatterFactory |
@NumberFormat註解類型的數字字段類型<---->String ①通過@NumberFormat指定格式化/解析格式 ②可以格式化/解析的數字類型:Short、Integer、Long、Float、Double、BigDecimal、BigInteger |
JodaDateTimeFormatAnnotationFormatterFactory |
@DateTimeFormat註解類型的日期字段類型<---->String ①通過@DateTimeFormat指定格式化/解析格式 ②可以格式化/解析的日期類型: joda中的日期類型(org.joda.time包中的):LocalDate、LocalDateTime、LocalTime、ReadableInstant java內置的日期類型:Date、Calendar、Long
classpath中必須有Joda-Time類庫,否則無法格式化日期類型 |
NumberFormatAnnotationFormatterFactory和JodaDateTimeFormatAnnotationFormatterFactory(如果classpath提供了Joda-Time類庫)在使用格式化服務實現DefaultFormattingConversionService時會自動註冊。
7.3.3、示例
在示例之前,我們需要到http://joda-time.sourceforge.net/下載Joda-Time類庫,本書使用的是joda-time-2.1版本,將如下jar包添加到classpath:
7.3.3.1、類型級別的解析/格式化
一、直接使用Formatter SPI進行解析/格式化
- //二、CurrencyFormatter:實現貨幣樣式的格式化/解析
- CurrencyFormatter currencyFormatter = new CurrencyFormatter();
- currencyFormatter.setFractionDigits(2);//保留小數點後幾位
- currencyFormatter.setRoundingMode(RoundingMode.CEILING);//舍入模式(ceilling表示四捨五入)
- //1、將帶貨幣符號的字符串“$123.125”轉換爲BigDecimal("123.00")
- Assert.assertEquals(new BigDecimal("123.13"), currencyFormatter.parse("$123.125", Locale.US));
- //2、將BigDecimal("123")格式化爲字符串“$123.00”展示
- 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)信息進行解析/格式化。
其他測試用例請參考cn.javass.chapter7.web.controller.support.formatter.InnerFormatterTest的testNumber測試方法和testDate測試方法。
- @Test
- public void testWithDefaultFormattingConversionService() {
- DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
- //默認不自動註冊任何Formatter
- CurrencyFormatter currencyFormatter = new CurrencyFormatter();
- currencyFormatter.setFractionDigits(2);//保留小數點後幾位
- currencyFormatter.setRoundingMode(RoundingMode.CEILING);//舍入模式(ceilling表示四捨五入)
- //註冊Formatter SPI實現
- conversionService.addFormatter(currencyFormatter);
- //綁定Locale信息到ThreadLocal
- //FormattingConversionService內部自動獲取作爲Locale信息,如果不設值默認是 Locale.getDefault()
- LocaleContextHolder.setLocale(Locale.US);
- Assert.assertEquals("$1,234.13", conversionService.convert(new BigDecimal("1234.128"), String.class));
- LocaleContextHolder.setLocale(null);
- LocaleContextHolder.setLocale(Locale.CHINA);
- Assert.assertEquals("¥1,234.13", conversionService.convert(new BigDecimal("1234.128"), String.class));
- Assert.assertEquals(new BigDecimal("1234.13"), conversionService.convert("¥1,234.13", BigDecimal.class));
- LocaleContextHolder.setLocale(null);}
DefaultFormattingConversionService:帶數據格式化功能的類型轉換服務實現;
conversionService.addFormatter():註冊Formatter SPI實現;
conversionService.convert(new BigDecimal("1234.128"), String.class):用於將BigDecimal類型數據格式化爲字符串類型,此處根據“LocaleContextHolder.setLocale(locale)”設置的本地化信息進行格式化;
conversionService.convert("¥1,234.13", BigDecimal.class):用於將字符串類型數據解析爲BigDecimal類型數據,此處也是根據“LocaleContextHolder.setLocale(locale)”設置的本地化信息進行解;
LocaleContextHolder.setLocale(locale):設置本地化信息到ThreadLocal,以便Formatter SPI根據本地化信息進行解析/格式化;
具體測試代碼請參考cn.javass.chapter7.web.controller.support.formatter.InnerFormatterTest的testWithDefaultFormattingConversionService測試方法。
三、自定義Formatter進行解析/格式化
此處以解析/格式化PhoneNumberModel爲例。
(1、定義Formatter SPI實現
- package cn.javass.chapter7.web.controller.support.formatter;
- //省略import
- 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)) {
- //①如果source爲空 返回null
- 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));
- }
- }
- }
類似於Convert SPI實現,只是此處的相應方法會傳入Locale本地化信息,這樣可以爲不同地區進行解析/格式化數據。
(2、測試用例:
- package cn.javass.chapter7.web.controller.support.formatter;
- //省略import
- public class CustomerFormatterTest {
- @Test
- public void test() {
- DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
- conversionService.addFormatter(new PhoneNumberFormatter());
- PhoneNumberModel phoneNumber = new PhoneNumberModel("010", "12345678");
- Assert.assertEquals("010-12345678", conversionService.convert(phoneNumber, String.class));
- Assert.assertEquals("010", conversionService.convert("010-12345678", PhoneNumberModel.class).getAreaCode());
- }
- }
通過PhoneNumberFormatter可以解析String--->PhoneNumberModel和格式化PhoneNumberModel--->String。
到此,類型級別的解析/格式化我們就介紹完了,從測試用例可以看出類型級別的是對項目中的整個類型實施相同的解析/格式化邏輯。
有的同學可能需要在不同的類的字段實施不同的解析/格式化邏輯,如用戶模型類的註冊日期字段只需要如“2012-05-02”格式進行解析/格式化即可,而訂單模型類的下訂單日期字段可能需要如“2012-05-02 20:13:13”格式進行展示。
接下來我們學習一下如何進行字段級別的解析/格式化吧。
7.3.3.2、字段級別的解析/格式化
一、使用內置的註解進行字段級別的解析/格式化:
(1、測試模型類準備:
- package cn.javass.chapter7.model;
- 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;
- //省略getter/setter
- }
此處我們使用了Spring字段級別解析/格式化的兩個內置註解:
@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。
(2、測試用例:
- @Test
- public void test() throws SecurityException, NoSuchFieldException {
- //默認自動註冊對@NumberFormat和@DateTimeFormat的支持
- DefaultFormattingConversionService conversionService =
- new DefaultFormattingConversionService();
- //準備測試模型對象
- FormatterModel model = new FormatterModel();
- model.setTotalCount(10000);
- model.setDiscount(0.51);
- model.setSumMoney(10000.13);
- model.setRegisterDate(new Date(2012-1900, 4, 1));
- model.setOrderDate(new Date(2012-1900, 4, 1, 20, 18, 18));
- //獲取類型信息
- TypeDescriptor descriptor =
- new TypeDescriptor(FormatterModel.class.getDeclaredField("totalCount"));
- TypeDescriptor stringDescriptor = TypeDescriptor.valueOf(String.class);
- Assert.assertEquals("10,000", conversionService.convert(model.getTotalCount(), descriptor, stringDescriptor));
- Assert.assertEquals(model.getTotalCount(), conversionService.convert("10,000", stringDescriptor, descriptor));
- }
TypeDescriptor:擁有類型信息的上下文,用於Spring3類型轉換系統獲取類型信息的(可以包含類、字段、方法參數、屬性信息);通過TypeDescriptor,我們就可以獲取(類、字段、方法參數、屬性)的各種信息,如註解類型信息;
conversionService.convert(model.getTotalCount(), descriptor, stringDescriptor):將totalCount格式化爲字符串類型,此處會根據totalCount字段的註解信息(通過descriptor對象獲取)來進行格式化;
conversionService.convert("10,000", stringDescriptor, descriptor):將字符串“10,000”解析爲totalCount字段類型,此處會根據totalCount字段的註解信息(通過descriptor對象獲取)來進行解析。
(3、通過爲不同的字段指定不同的註解信息進行字段級別的細粒度數據解析/格式化
- descriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("registerDate"));
- Assert.assertEquals("2012-05-01", conversionService.convert(model.getRegisterDate(), descriptor, stringDescriptor));
- Assert.assertEquals(model.getRegisterDate(), conversionService.convert("2012-05-01", stringDescriptor, descriptor));
- descriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("orderDate"));
- Assert.assertEquals("2012-05-01 20:18:18", conversionService.convert(model.getOrderDate(), descriptor, stringDescriptor));
- Assert.assertEquals(model.getOrderDate(), conversionService.convert("2012-05-01 20:18:18", stringDescriptor, descriptor));
通過如上測試可以看出,我們可以通過字段註解方式實現細粒度的數據解析/格式化控制,但是必須使用TypeDescriptor來指定類型的上下文信息,即編程實現字段的數據解析/格式化比較麻煩。
其他測試用例請參考cn.javass.chapter7.web.controller.support.formatter.InnerFieldFormatterTest的test測試方法。
二、自定義註解進行字段級別的解析/格式化:
此處以解析/格式化PhoneNumberModel字段爲例。
(1、定義解析/格式化字段的註解類型:
- package cn.javass.chapter7.web.controller.support.formatter;
- //省略import
- @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
- @Retention(RetentionPolicy.RUNTIME)
- public @interface PhoneNumber {
- }
(2、實現AnnotationFormatterFactory註解格式化工廠:
- package cn.javass.chapter7.web.controller.support.formatter;
- //省略import
- 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();//此處使用之前定義的Formatter實現
- }
- //②指定可以被解析/格式化的字段類型集合
- @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;
- }
- }
AnnotationFormatterFactory實現會根據註解信息和字段類型獲取相應的解析器/格式化器。
(3、修改FormatterModel添加如下代碼:
(4、測試用例
- @Test
- ublic void test() throws SecurityException, NoSuchFieldException {
- DefaultFormattingConversionService conversionService =
- new DefaultFormattingConversionService();//創建格式化服務
- conversionService.addFormatterForFieldAnnotation(
- new PhoneNumberFormatAnnotationFormatterFactory());//添加自定義的註解格式化工廠
- FormatterModel model = new FormatterModel();
- TypeDescriptor descriptor =
- new TypeDescriptor(FormatterModel.class.getDeclaredField("phoneNumber"));
- TypeDescriptor stringDescriptor = TypeDescriptor.valueOf(String.class);
- PhoneNumberModel value = (PhoneNumberModel) conversionService.convert("010-12345678", stringDescriptor, descriptor); //解析字符串"010-12345678"--> PhoneNumberModel
- model.setPhoneNumber(value);
- Assert.assertEquals("010-12345678", conversionService.convert(model.getPhoneNumber(), descriptor, stringDescriptor));//格式化PhoneNumberModel-->"010-12345678"
此處使用DefaultFormattingConversionService的addFormatterForFieldAnnotation註冊自定義的註解格式化工廠PhoneNumberFormatAnnotationFormatterFactory。
到此,編程進行數據的格式化/解析我們就完成了,使用起來還是比較麻煩,接下來我們將其集成到Spring Web MVC環境中。
7.3.4、集成到Spring Web MVC環境
一、註冊FormattingConversionService實現和自定義格式化轉換器:
- <bean id="conversionService"
- class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
- <!—此處省略之前註冊的自定義類型轉換器-->
- <property name="formatters">
- <list>
- <bean class="cn.javass.chapter7.web.controller.support.formatter.
- PhoneNumberFormatAnnotationFormatterFactory"/>
- </list>
- </property>
- </bean>
其他配置和之前學習7.2.2.4一節一樣。
二、示例:
(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>
在瀏覽器輸入測試URL:
http://localhost:9080/springmvc-chapter7/format1?totalCount=100000&discount=0.51&sumMoney=100000.128&phoneNumber=010-12345678
數據會正確綁定到我們的formatModel,即請求參數能被正確的解析並綁定到我們的命令對象上,而且在JSP頁面也能正確的顯示格式化後的數據(即正確的被格式化顯示)。
(2、功能處理方法參數級別的數據解析:
- @RequestMapping(value = "/format2")
- public String test2(
- @PhoneNumber @RequestParam("phoneNumber") PhoneNumberModel phoneNumber,
- @DateTimeFormat(pattern="yyyy-MM-dd") @RequestParam("date") Date date) {
- System.out.println(phoneNumber);
- System.out.println(date);
- return "format/success2";
- }
此處我們可以直接在功能處理方法的參數上使用格式化註解類型進行註解,Spring Web MVC能根據此註解信息對請求參數進行解析並正確的綁定。
在瀏覽器輸入測試URL:
http://localhost:9080/springmvc-chapter7/format2?phoneNumber=010-12345678&date=2012-05-01
數據會正確的綁定到我們的phoneNumber和date上,即請求的參數能被正確的解析並綁定到我們的參數上。
控制器代碼位於cn.javass.chapter7.web.controller.DataFormatTestController中。
如果我們請求參數數據不能被正確解析並綁定或輸入的數據不合法等該怎麼處理呢?接下來的一節我們來學習下綁定失敗處理和數據驗證相關知識。