解决axis2处理java.util.Date类型对象时丢弃时间部分的问题

来源:互联网 发布:乐知英语待遇 编辑:程序博客网 时间:2024/04/20 06:15

我目前在做的一个项目以axis2为webservice框架,客户端和服务器端要传输很多复杂对象,在这方面,axis2做得不错,基本满足了我的需要,但当我把客户端提供给要使用的同事时,同事发现了一个问题:就是所有java.util.Date类型的对象,不论从服务器发到客户端的还是从客户端发送到服务器的,都只剩下日期部分(年/月/日),时间部分(时/分/秒)则被抹掉了。。。百思不得其姐啊。。。
这是几个月前的事儿了,那时,这个问题并不影响整个系统的开发,所以一直搁置在那里,最近整个系统接近完成了。做为一个重要但不紧急的问题,我又重新开始研究axis2的代码,着手解决这个问题。
很多人遇到这个问题,把这归结为axis2的bug,但我认为,这是axis2遵循WSDL规范设计的,这个设计的确有些反人类,异于通常技术人员对Date的理解和使用习惯,其实也可以说我们是对webservice的理解不足导致,对于这个问题的理解我也是一点一点加深的。
webservice设计的目标是跨平台的数据交换,所以描述webservice的WSDL( Web Services Description Language)定义了很多基本数据类型(byte,int,long,short,…..),而对于日期时间则分别定义了date,time和dateTime三种不同的类型。

wsdl使用xmlshcema约束文档,这些基本数据类型都是在xmlschema中定义的,
关于wsdl与xmlschema的关系,参见这个博客:(http://blog.csdn.net/ljfbest/article/details/12135035)

顾名思义,date中只包含日期,time中只包含时间(时分秒),dateTime中包含完整的日期和时间。
我们再来看看org.apache.axis2.description.java2wsdl.TypeTable类(axis2-kernel-1.6.2.jar)中java类对date,time,dateTime的映射关系

        simpleTypetoxsd.put("java.util.Date",                new QName(Java2WSDLConstants.URI_2001_SCHEMA_XSD, "date", "xs"));        simpleTypetoxsd.put("java.util.Calendar",                new QName(Java2WSDLConstants.URI_2001_SCHEMA_XSD, "dateTime", "xs"));        // SQL date time         simpleTypetoxsd.put("java.sql.Date",                new QName(Java2WSDLConstants.URI_2001_SCHEMA_XSD, "date", "xs"));         simpleTypetoxsd.put("java.sql.Time",                new QName(Java2WSDLConstants.URI_2001_SCHEMA_XSD, "time", "xs"));        simpleTypetoxsd.put("java.sql.Timestamp",                new QName(Java2WSDLConstants.URI_2001_SCHEMA_XSD, "dateTime", "xs"));

从上面代码片段中可以看出,axis2将java.util.Date是做为date处理的,java.util.Calendar和java.sql.Timestamp对应的才是完整的日期。
也就是说,这个问题最简单的
解决方案一
就是你需要在webservice中使用java.util.Calendar代替java.util.Date来传递完整日期时间数据
那么你可能会问了:java.util.Calendar和java.sql.Timestamp对应的都是dateTime,为什么不能用java.sql.Timestamp来代替java.util.Date?java.sql.Timestamp是java.util.Date子类,用它不是更方便?
这个。。。就是因为java.sql.Timestamp是java.util.Date子类,用它才比较麻烦。。。
看看方案二的第一段代码中的中文注释你就明白了。
如果因为各种原因你不想修改自己的项目代码,坚持要用Date来传递完整的日期时间,怎么办?
下面来说解决方案二:
从修改axis2源代码着手,让Date在转换成字符串时包含时间
这个方案要修改axis2的几处源代码,建议做之前先用svn,git等工具做一个原始备份,以方便对照检查。
请先从axis2官网下载 axis2-1.6.2源代码
在网上找到一些关于这个问题的一些提示(https://issues.apache.org/jira/browse/AXIS2-4075):
从这里我们可以得知所有基本数据类型(String,byte[],int,boolean,long,Date…)的序列化和反序列化都是在org.apache.axis2.databinding.typemapping.SimpleTypeMapper这个类(axis2-adb-1.6.2.jar)中完成的。在这里我们找到了Date转String的代码

    public static String getStringValue(Object obj) {        if (obj instanceof Float ||                obj instanceof Double) {            double data;            if (obj instanceof Float) {                data = ((Float)obj).doubleValue();            } else {                data = (Double)obj;            }            if (Double.isNaN(data)) {                return "NaN";            } else if (data == Double.POSITIVE_INFINITY) {                return "INF";            } else if (data == Double.NEGATIVE_INFINITY) {                return "-INF";            } else {                return obj.toString();            }        } else if (obj instanceof Calendar) {            Calendar calendar = (Calendar) obj;            return ConverterUtil.convertToString(calendar);        } else if (obj instanceof Date) { //因为java.sql.Timestamp是java.util.Date子类所以在这里java.sql.Timestamp被当做Date处理了,所以如果不修改axis2的代码,而只简单地把你项目中代码中的java.util.Date替换成java.sql.Timestamp,还是不能解决问题,我觉得这算是axis2的一个bug            SimpleDateFormat zulu = new SimpleDateFormat("yyyy-MM-dd");//请注意,上面这行中生成的SimpleDateFormat对象的格式中没有时间部分哦                    MessageContext messageContext = MessageContext.getCurrentMessageContext();            if (messageContext != null) {                AxisService axisServce = messageContext.getAxisService();                // if the user has given a pirticualr timezone we use it.                if (axisServce.getParameter("TimeZone") != null) {                    zulu.setTimeZone(TimeZone.getTimeZone((String) axisServce.getParameter("TimeZone").getValue()));                }            }            return zulu.format(obj);        } else if (obj instanceof URI){            return obj.toString();        }        return obj.toString();    }

请注意看这一行

SimpleDateFormat zulu = new SimpleDateFormat(“yyyy-MM-dd”);

看到这里就明白了吧?就是这里,将一个Date对象转为String时,有意丢弃了时间部分。
所以把这一行改为

SimpleDateFormat zulu = new SimpleDateFormat(“yyyy-MM-dd’T’HH:mm:ss.SSSZ”);

Date转换的String就有了时间和时区信息。
那么到这里,这事儿就完了吗?no,no,no事情还远未结束呢!
上面的修改使Date转String已经包含了时间和时区信息,但通过xml传送到客户端/服务器端时,String还要被解析成Date。
显然,经过上面的修改后,转换的String格式与原来已经不同了(多了时间部分),能不能也同样被正确转换成Date呢?
答案是不能。
原因还在org.apache.axis2.databinding.typemapping.SimpleTypeMapper这个类(axis2-adb-1.6.2.jar)中,见下面的代码

    public static Object getSimpleTypeObject(Class parameter, String text) {        String name = parameter.getName();        if(name.equals(STRING)) {            return text;        } else  if (text == null || text.length() == 0) {            return null;        } else if (name.equals(INT)) {            return new Integer(text);        } else if (name.equals(BOOLEAN)) {            return ConverterUtil.convertToBoolean(text);        } else if (name.equals(BYTE)) {            return new Byte(text);        } else if (name.equals(DOUBLE)) {            return new Double(text);        } else if (name.equals(SHORT)) {            return new Short(text);        } else if (name.equals(LONG)) {            return new Long(text);        } else if (name.equals(FLOAT)) {            return new Float(text);        } else if (name.equals(CHAR)) {            return text.toCharArray()[0];        } else if (name.equals(W_INT)) {            return new Integer(text);        } else if (name.equals(W_BOOLEAN)) {            return Boolean.valueOf(text);        } else if (name.equals(W_BYTE)) {            return new Byte(text);        } else if (name.equals(W_DOUBLE)) {            return new Double(text);        } else if (name.equals(W_SHORT)) {            return new Short(text);        } else if (name.equals(W_LONG)) {            return new Long(text);        } else if (name.equals(W_FLOAT)) {            return new Float(text);        } else if (name.equals(W_CHAR)) {            return text.toCharArray()[0];        } else if (name.equals(W_CALENDAR)) {            return makeCalendar(text);        } else if (name.equals(W_DATE)) {            return makeDate(text);        }/*         * return the correpsonding object for adding data type         */        else if(name.equals(BIG_DECIMAL)) {            return new java.math.BigDecimal(text);        }        else if(name.equals(BIG_INTEGER)) {            return new java.math.BigInteger(text);        }        else if(name.equals(DAY)) {            return new org.apache.axis2.databinding.types.Day(text);        }        else if(name.equals(DURATION)) {            return new org.apache.axis2.databinding.types.Duration(text);        }        else if(name.equals(MONTH)) {            return new org.apache.axis2.databinding.types.Month(text);        }        else if(name.equals(MONTH_DAY)) {            return new org.apache.axis2.databinding.types.MonthDay(text);        }        else if(name.equals(TIME)) {            return new org.apache.axis2.databinding.types.Time(text);        }        else if(name.equals(YEAR)) {            return new org.apache.axis2.databinding.types.Year(text);        }        else if(name.equals(YEAR_MONTH)) {            return new org.apache.axis2.databinding.types.YearMonth(text);        } else if(name.equals(W_URI)) {            try {                return new URI(text);            } catch (URISyntaxException e) {                throw new RuntimeException(" Invalid URI " + text, e);            }        } else {            return null;        }    }
    public static Object makeDate(String source) {        return ConverterUtil.convertToDate(source);    }

上面的代码中getSimpleTypeObject这个方法在将String转为Date时,调用了makeDate方法,而makeDate则是调用org.apache.axis2.databinding.utils.ConverterUtil中的convertToDate方法实现的,所以我们的目光要转到ConverterUtil这个类了,下面是ConverterUtil中convertToDate方法的原始代码(注意代码中我添加的中文注释):

//convertToDate方法的原始代码    public static Date convertToDate(String source) {        // the lexical form of the date is '-'? yyyy '-' mm '-' dd zzzzzz?        if ((source == null) || source.trim().equals("")) {            return null;        }        source = source.trim();        boolean bc = false;        if (source.startsWith("-")) {            source = source.substring(1);            bc = true;        }        int year = 0;        int month = 0;        int day = 0;        int timeZoneOffSet = TimeZone.getDefault().getRawOffset();        if (source.length() >= 10) {            //first 10 numbers must give the year            if ((source.charAt(4) != '-') || (source.charAt(7) != '-')){                throw new RuntimeException("invalid date format (" + source + ") with out - s at correct place ");            }            //解析标准的日期格式yyyy-mm-dd            year = Integer.parseInt(source.substring(0,4));            month = Integer.parseInt(source.substring(5,7));            day = Integer.parseInt(source.substring(8,10));            if (source.length() > 10) {//>10代表有时区信息                String restpart = source.substring(10);                //没有解析 HH:mm:ss.SSS,直接解析时区(timeZone),所以如果参数source中包含了时间部分,会抛出RuntimeException异常                if (restpart.startsWith("Z")) {                    // this is a gmt time zone value                    timeZoneOffSet = 0;                } else if (restpart.startsWith("+") || restpart.startsWith("-") || restpart.startsWith("T")) {                    // this is a specific time format string                    //注意这里,所识别时区数字格式为08:00,而不是我们常见的0800                    if (restpart.charAt(3) != ':'){                        throw new RuntimeException("invalid time zone format (" + source                                + ") without : at correct place");                    }                    int hours = Integer.parseInt(restpart.substring(1,3));                    int minits = Integer.parseInt(restpart.substring(4,6));                    timeZoneOffSet = ((hours * 60) + minits) * 60000;                    if (restpart.startsWith("-")){                        timeZoneOffSet = timeZoneOffSet * -1;                    }                } else {                    throw new RuntimeException("In valid string sufix");                }            }        } else {            throw new RuntimeException("In valid string to parse");        }        Calendar calendar = Calendar.getInstance();        calendar.clear();        calendar.setLenient(false);        calendar.set(Calendar.YEAR, year);        //xml month stars from the 1 and calendar month is starts with 0        calendar.set(Calendar.MONTH, month - 1);        calendar.set(Calendar.DAY_OF_MONTH, day);        calendar.set(Calendar.ZONE_OFFSET, timeZoneOffSet);        // set the day light off set only if time zone        if (source.length() >= 10) {            calendar.set(Calendar.DST_OFFSET, 0);        }        calendar.getTimeInMillis();        if (bc){            calendar.set(Calendar.ERA, GregorianCalendar.BC);        }        return calendar.getTime();    }

从上面的代码及注释,可以看出,axis2在对Date转成的String重新解析成Date时,只识别 yyyy-MM-dd(Z)?这种格式,Z在这里代表时区信息,如果遇到yyyy-MM-dd’T’HH:mm:ss.SSSZ格式的时间,是不能正常解析的。所以必须修改convertToDate方法,下面是修改后的convertToDate方法:

//修改后的convertToDate方法代码    /**     * 针对可变长格式的日期的正则表达式     */    private static final Pattern DATE_PATTERN=Pattern.compile("^(\\d{4})(\\-|\\/|\\.)(\\d{1,2})\\2(\\d{1,2})((T((\\d{2}):(\\d{2}):(\\d{2})))?(\\.(\\d{3}))?)((([\\+-])((\\d{2}):?(\\d{2})))|Z)?$");    /**     * Converts a given string into a date. Code from Axis1 DateDeserializer.     *     * @param source     * @return Returns Date.     */    public static Date convertToDate(String source) {        // the lexical form of the date is '-'? yyyy '-' mm '-' dd zzzzzz?        if ((source == null) || source.trim().equals("")) {            return null;        }        source = source.trim();        Calendar calendar = Calendar.getInstance();        calendar.clear();        calendar.setLenient(false);        boolean bc = false;        if (source.startsWith("-")) {            source = source.substring(1);            bc = true;        }        int year = 0;        int month = 0;        int day = 0;        int timeZoneOffSet = TimeZone.getDefault().getRawOffset();        int hour = 0;        int minute = 0;        int second = 0;        int miliSecond = 0;        Matcher matcher=DATE_PATTERN.matcher(source);        if(matcher.find()){//正则表达式匹配,不匹配则抛出RuntimeException异常            //date part yyyy-MM-dd 日期部分必须有            year = Integer.parseInt(matcher.group(1));            month = Integer.parseInt(matcher.group(3));            day = Integer.parseInt(matcher.group(4));            if(matcher.group(5)!=null&&!matcher.group(5).isEmpty()){//如果有时间部分,就解析,没有就跳过                // HH:mm:ss                 hour = Integer.parseInt(matcher.group(8));                minute = Integer.parseInt(matcher.group(9));                second = Integer.parseInt(matcher.group(10));                if(matcher.group(11)!=null&&!matcher.group(11).isEmpty())//如果有毫秒部分就解析,没有就跳过                    // .SSS                    miliSecond=Integer.parseInt(matcher.group(12));            }            // timeZone            if(matcher.group(13)!=null&&!matcher.group(13).isEmpty()){//如果有时区部分,就解析,没有就跳过                if(matcher.group(13).equals("Z")){// Z结尾,代表GMT 0时区                    timeZoneOffSet = 0;                    calendar.setTimeZone(TimeZone.getTimeZone("GMT"));                }else{ //数字表示的时区                    // +0800或+08:00                    int zhour,zminute;                    zhour = Integer.parseInt(matcher.group(17));                    zminute = Integer.parseInt(matcher.group(18));                    timeZoneOffSet = ((zhour * 60) + zminute) * 60000;                    if (matcher.group(15).startsWith("-")) {                        timeZoneOffSet = timeZoneOffSet * -1;                    }                }            }        }else             throw new RuntimeException(String.format("Invalid date string[%s] to parse",source));        calendar.set(Calendar.YEAR, year);        //xml month stars from the 1 and calendar month is starts with 0        calendar.set(Calendar.MONTH, month - 1);        calendar.set(Calendar.DAY_OF_MONTH, day);        calendar.set(Calendar.HOUR_OF_DAY, hour);        calendar.set(Calendar.MINUTE, minute);        calendar.set(Calendar.SECOND, second);        calendar.set(Calendar.MILLISECOND, miliSecond);        calendar.set(Calendar.ZONE_OFFSET, timeZoneOffSet);        // set the day light off set only if time zone        if (source.length() >= 10) {            calendar.set(Calendar.DST_OFFSET, 0);        }        calendar.getTimeInMillis();        if (bc){            calendar.set(Calendar.ERA, GregorianCalendar.BC);        }        return calendar.getTime();    }

这代码利用正则表达式重写了convertToDate方法(话说用正则表达式解析灵活多了,原来那种一个个字符判断的方式,好辛苦,数字符数的好累啊),既可以支持原来所支持的yyyy-MM-ddZ格式的时间,也支持yyyy-MM-dd’T’HH:mm:ss.SSSZ格式的时间String解析,而且这其中时间(‘T’HH:mm:ss)毫秒(.SSS)和时区(.Z)都是可选的(时区格式同时支持0800和08:00),
也就是说
“2015-09-18”,
“2015-09-18T12:32:22”,
“2015-09-18T12:32:22.798”,
“2015-09-18T12:32:22.798+0800”,
“2015-09-18T12:32:22.798+08:00”
这些代表时间的String都能被正确解析了。
好了,到这一步,问题算是基本解决了,但这就完了么?未必哦。
如果你的客户端是使用RPC方式调用(org.apache.axis2.rpc.client.RPCServiceClient),那么到这一步,你已经可以收工了。如果你是用Stub方式,那么这个坑还没填完呢。

如果你不清楚什么是RPC方式什么时Stub方式,参见这个blog Axis2开发WebService客户端 的3种方式

下面是一段axis2的wsdl2java工具生成的Stub代码的片段:

if (localCreateTime==null){  // write the nil attribute    writeAttribute("xsi","http://www.w3.org/2001/XMLSchema-instance","nil","1",xmlWriter);        }else{    xmlWriter.writeCharacters(org.apache.axis2.databinding.utils.ConverterUtil.convertToString(localCreateTime));}

,在这段代码中可以看到,Stub方式调用webservice时,Date转String是用org.apache.axis2.databinding.utils.ConverterUtil.convertToString(Date value)方法。

//convertToString(Date value)方法代码原始代码,中文注释是本文作者添加    public static String convertToString(Date value) {        if (isCustomClassPresent) {            // this means user has define a seperate converter util class            return invokeToStringMethod(value,Date.class);        } else {            // lexical form of the date is '-'? yyyy '-' mm '-' dd zzzzzz?            Calendar calendar = Calendar.getInstance();            calendar.clear();            calendar.setTime(value);            if (!calendar.isSet(Calendar.ZONE_OFFSET)){                calendar.setTimeZone(TimeZone.getDefault());            }            StringBuffer dateString = new StringBuffer(16);            appendDate(dateString, calendar);//添加日期 yyyy-MM-dd                        appendTimeZone(calendar, dateString);//添加时区 +08:00            return dateString.toString();        }    }

再对比处理java.util.Calendar的convertToString(Calendar value)方法

    public static String convertToString(Calendar value) {        if (isCustomClassPresent) {            return invokeToStringMethod(value,Calendar.class);        } else {            // lexical form of the calendar is '-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?            if (value.get(Calendar.ZONE_OFFSET) == -1){                value.setTimeZone(TimeZone.getDefault());            }            StringBuffer dateString = new StringBuffer(28);            appendDate(dateString, value);            dateString.append("T");            //adding hours            appendTime(value, dateString);            appendTimeZone(value, dateString);//添加时间部分 'T'HH:mm:ss.SSS            return dateString.toString();        }    }

对比可以发现convertToString(Date value)方法比convertToString(Calendar value)方法果然没有把时间部分加入String.所以结论很简单,还要修改convertToString(Date value)方法:
只需要照猫画虎,参照convertToString(Calendar value)在日期后添加时间就好了:

//修改后的convertToString(Date value)方法    public static String convertToString(Date value) {        if (isCustomClassPresent) {            // this means user has define a seperate converter util class            return invokeToStringMethod(value,Date.class);        } else {            // lexical form of the date is '-'? yyyy '-' mm '-' dd zzzzzz?            Calendar calendar = Calendar.getInstance();            calendar.clear();            calendar.setTime(value);            if (!calendar.isSet(Calendar.ZONE_OFFSET)){                calendar.setTimeZone(TimeZone.getDefault());            }            StringBuffer dateString = new StringBuffer(16);            appendDate(dateString, calendar);            //adding HH:mm:ss.SSS            //modified by guyadong 2015/09/20 增加时间部分            dateString.append("T");            appendTime(calendar, dateString);            appendTimeZone(calendar, dateString);            return dateString.toString();        }    }

到这里,所有的代码修改就结束了,剩下的,就是用修改后代码生成的两个class文件(org.apache.axis2.databinding.typemapping.SimpleTypeMapper和org.apache.axis2.databinding.utils.ConverterUtil)替代掉axis2-adb-1.6.2.jar中对应的同名文件就大功告成啦。

0 0