前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >解决axis2处理java.util.Date类型对象时丢弃时间部分的问题

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

作者头像
10km
发布2022-05-07 09:52:35
6580
发布2022-05-07 09:52:35
举报
文章被收录于专栏:10km的专栏

我目前在做的一个项目以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的映射关系

代码语言:javascript
复制
        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的代码

代码语言:javascript
复制
    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)中,见下面的代码

代码语言:javascript
复制
    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;
        }
    }
代码语言:javascript
复制
    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方法的原始代码(注意代码中我添加的中文注释):

代码语言:javascript
复制
//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方法:

代码语言:javascript
复制
//修改后的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代码的片段:

代码语言:javascript
复制
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)方法。

代码语言:javascript
复制
//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)方法

代码语言:javascript
复制
    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)在日期后添加时间就好了:

代码语言:javascript
复制
//修改后的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中对应的同名文件就大功告成啦。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2015-09-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档