1. 程式人生 > >struts2中Date日期轉換失敗

struts2中Date日期轉換失敗

在學習過程中出現了一個印象特別深刻的一個問題,當時控制檯丟擲異常是:java.lang.NoSuchMethodException:com.ca.agent.model.mybatis.ApprovalInforCangra.setSubDate([Ljava.lang.String;),隨後在debug的除錯下檢視報錯的具體情況,發現根本沒進入到對應的action類中就跑出了該異常,通過進一步的探討,發現在實體類處,所傳遞的日期資料無法自動的進行轉換(當時用的是火狐瀏覽器)。當時想著Struts2是以webwork為核心,採用攔截器的機制來處理使用者的請求,Struts2也會自動轉換8大基本資料型別和對應的包裝類,以及Date型別,但是這Date型別對應的資料提交格式是有要求的:yyyy-MM-dd。當時看著客戶端傳遞的日期引數形式是yyyy-MM-dd形式,但是就是封裝不進去。隨後就在實體類引數獲取之前自己將傳遞的資料進行轉換,再一次走了一次流程,發現自己封裝過的日期資料完全的傳遞到後臺,並且無異常丟擲。但是就很納悶,為什麼Struts2突然間就不能封裝日期資料了呢?!   最終搞了將近兩個小時,通過網路搜尋,發現有一個部落格說曾經出現這個問題,他是通過修改他的瀏覽器預設語言設定解決的,他的預設語言是美國,不是中文,然後我們就換了個瀏覽器——谷歌瀏覽器,發現引數轉換正常,所以頓時明白,肯定是我的火狐瀏覽器問題,仔細檢視之後,我的獲取瀏覽器預設語言也是美國的……,修改語言設定之後,一切恢復正常。

eb開發會涉及到很多型別轉換的情況。我們知道,頁面中的一切值都是字串型別,而到後臺,我們需要的可能是其他各種型別;同時,頁面顯示也是字串型別。這就涉及到Web中基本的型別轉換問題:從String轉換為各種型別與從各種型別轉換為String型別。

在Java Web開發中,進行上述轉換一般有以下幾種:
1、在Servlet中,這一切的轉換我們得自己寫程式碼完成;
2、在Struts1.x中,我們通過apache commons-beanutils中的converters來幫助完成這些事情;
3、在Struts2中,使用的則是基於ongl的型別轉換;
……

由於型別轉換的通用性,因而Web框架都會實現大多數型別的轉換功能,而不需要程式設計師編碼實現。然而,對於java.util.Date這種型別的轉換,各大框架似乎做得都不盡如人意。如:在Struts1.x中,該型別的轉換就會有問題,很多人建議使用java.sql.Date這種型別來解決日期轉換的問題(實際上可以自定義一個型別轉換器來解決該問題)。在Struts2中,這個問題似乎依然存在,也許你從來沒有遇到過。的確,一般人確實不會遇到,會覺得沒有這個問題。下面就是我遇到的問題與解決方法。

日期型別的轉換,Web開發中幾乎都會遇到,我現在做得專案也不例外。在開發的過程中,也許就像你一樣,我沒有對日期型別的轉換做任何特殊的處理,而且Struts2也很好的幫我完成了轉換。然而同事測試的時候卻出現了一個“莫名其妙”的問題:輸入一個常用格式的日期型別yyyy-MM-dd,到後臺卻報錯:找不到對應的set方法——setEffDate(Ljava.lang.String)。的確,程式中只有setEffDate(java.util.Date)這個方法,沒有setEffDate(Ljava.lang.String)這個方法。從Ljava.lang.String可以看出,傳到後臺的String型別並沒有轉換成Date型別,因而報錯。

一開始,我以為是我UT沒做好,於是在自己的電腦上模擬同事的測試,結果一點問題也沒有。這就奇怪了。經過自己分析,覺得可能是IE瀏覽器的原因,因為同事測試用的是IE,而我用的是FireFox。於是在自己的機子上用IE測試,同時在同事機子上用FireFox測試,結果這兩次測試都沒有出現上面的問題。雖然沒有找到問題所在,但可以初步肯定:IE的問題,但似乎又不完全是IE的問題,因為在我的電腦上的IE(版本與同事一樣,都是IE6)沒有上述問題。這就奇怪了,是什麼問題呢,真是百思不得其解。

這個時候,我想起了之前遇到的一個不解得情況:從後臺獲得的日期型別在頁面上顯示時,跟上面情況一樣,在同事的IE中,日期顯示的格式竟然是:yyyy-MM-ddTHH:mm:ss。多了一個T,真是莫名其妙,而且只在同事的IE瀏覽器中出現(當時解決方法是在JS中將'T'替換為空格,沒有去深究,但現在必須的深究了)。yyyy-MM-ddTHH:mm:ss這種日期格式有嗎?於是查詢JDK:在SimpleDateFormat類中找到了該日期格式,這種格式是“美國語言環境中日期和時間的模式之一”。原來還真有這種格式。竟然這是美國語言中使用的日期格式,而Struts2是美國人開發的,也許跟這個有點關係。於是檢視Struts2中關於Date型別轉換的原始碼。

在XWorkBasicConverter類中

private Object doConvertToDate(Map<String, Object> context, Object value, Class toType) {
                Date result = null;

                if (value instanceof String && value != null && ((String) value).length() > 0) {
                        String sa = (String) value;
                        Locale locale = getLocale(context);

                        DateFormat df = null
;
                        if (java.sql.Time.class == toType) {
                                df = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale);
                        } else if (java.sql.Timestamp.class == toType) {
                                Date check = null;
                                SimpleDateFormat dtfmt = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT,
                                                DateFormat.MEDIUM,
                                                locale);
                                SimpleDateFormat fullfmt = new SimpleDateFormat(dtfmt.toPattern() + MILLISECOND_FORMAT,
                                                locale);

                                SimpleDateFormat dfmt = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
                                                locale);

                                SimpleDateFormat[] fmts = {fullfmt, dtfmt, dfmt};
                                for (SimpleDateFormat fmt : fmts) {
                                        try {
                                                check = fmt.parse(sa);
                                                df = fmt;
                                                if (check != null) {
                                                        break;
                                                }
                                        } catch (ParseException ignore) {
                                        }
                                }
                        } else if (java.util.Date.class == toType) {
                                Date check = null;
                                DateFormat[] dfs = getDateFormats(locale);
                                for (DateFormat df1 : dfs) {
                                        try {
                                                check = df1.parse(sa);
                                                df = df1;
                                                if (check != null) {
                                                        break;
                                                }
                                        }
                                        catch (ParseException ignore) {
                                        }
                                }
                        }
                        //final fallback for dates without time
                        if (df == null) {
                                df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
                        }
                        try {
                                df.setLenient(false); // let's use strict parsing (XW-341)
                                result = df.parse(sa);
                                if (!(Date.class == toType)) {
                                        try {
                                                Constructor constructor = toType.getConstructor(new Class[]{long.class});
                                                return constructor.newInstance(new Object[]{Long.valueOf(result.getTime())});
                                        } catch (Exception e) {
                                                throw new XWorkException("Couldn't create class " + toType + " using default (long) constructor", e);
                                        }
                                }
                        } catch (ParseException e) {
                                throw new XWorkException("Could not parse date", e);
                        }
                } else if (Date.class.isAssignableFrom(value.getClass())) {
                        result = (Date) value;
                }
                return result;
        }

        private DateFormat[] getDateFormats(Locale locale) {
                DateFormat dt1 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale);
                DateFormat dt2 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);
                DateFormat dt3 = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);

                DateFormat d1 = DateFormat.getDateInstance(DateFormat.SHORT, locale);
                DateFormat d2 = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
                DateFormat d3 = DateFormat.getDateInstance(DateFormat.LONG, locale);

                DateFormat rfc3399 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

                DateFormat[] dfs = {dt1, dt2, dt3, rfc3399, d1, d2, d3}; //added RFC 3339 date format (XW-473)
                return dfs;
        }


其中SHORT、MEDIUM、LONG在JDK中的DateFormat類中有說明。
從這句DateFormat rfc3399 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");可以看出,Struts2硬編碼使用了這樣一種格式。然而,Struts2中這種格式是放在最後的,為啥只有同事的IE瀏覽器測試時使用的是這種格式呢?(在除錯時,用同時IE,日期輸入中按這種格式輸入,還真的沒有問題了)這說明,同事的電腦中,前面六種DateFormat都沒有匹配,檢視DateFormat中關於SHORT、MEDIUM、LONG的說明,可以知道,對於yyyy-MM-dd這種日期型別,在英語語言中是沒法匹配的,由於Struts2匹配日期時,使用了Locale,可見,同事的IE瀏覽器預設的語言環境是英語。一經檢視,果然如此,把中文設定為預設語言環境,再測試,沒問題了。終於知道了原因。

個人覺得,Struts2中,最後一種日期模式寫死成美國標準,不是很好。

針對這個問題,我們沒法要求客戶一定設定中文為預設瀏覽器的語言環境。因而對於Date型別的轉換,可以自己定義一個轉換器。以下來自http://www.javaeye.com/wiki/struts2/1365-passing-parameters-in-struts2 中的一個型別轉換器定義(不適合國際化的環境),如需要,你可以定義自己的轉換器:


public class DateConverter extends DefaultTypeConverter {

        private static final Logger logger = Logger.getLogger(DateConverter.class);

        private static final String DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

        private static final String DATE_PATTERN = "yyyy-MM-dd";
        
        private static final String MONTH_PATTERN = "yyyy-MM";

        /**
         * Convert value between types
         */

        @SuppressWarnings("unchecked")
        public Object convertValue(Map ognlContext, Object value, Class toType) {
                Object result = null;
                if (toType == Date.class) {
                        result = doConvertToDate(value);
                } else if (toType == String.class) {
                        result = doConvertToString(value);
                }
                return result;
        }

        /**
         * Convert String to Date
         *
         * @param value
         * @return
         */

        private Date doConvertToDate(Object value) {
                Date result = null;

                if (value instanceof String) {
                        result = DateUtils.parseDate((String) value, new String[] { DATE_PATTERN, DATETIME_PATTERN, MONTH_PATTERN });

                        // all patterns failed, try a milliseconds constructor
                        if (result == null && StringUtils.isNotEmpty((String)value)) {

                                try {
                                        result = new Date(new Long((String) value).longValue());
                                } catch (Exception e) {
                                        logger.error("Converting from milliseconds to Date fails!");
                                        e.printStackTrace();
                                }

                        }

                } else if (value instanceof Object[]) {
                        // let's try to convert the first element only
                        Object[] array = (Object[]) value;

                        if ((array != null) && (array.length >= 1)) {
                                value = array[0];
                                result = doConvertToDate(value);
                        }

                } else if (Date.class.isAssignableFrom(value.getClass())) {
                        result = (Date) value;
                }
                return result;
        }

        /**
         * Convert Date to String
         *
         * @param value
         * @return
         */

        private String doConvertToString(Object value) {
                SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DATETIME_PATTERN);
                String result = null;
                if (value instanceof Date) {
                        result = simpleDateFormat.format(value);
                }
                return result;
        }
}