您的位置:首页 > 编程语言 > Java开发

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

2015-09-21 09:46 1041 查看
我目前在做的一个项目以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中对应的同名文件就大功告成啦。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  web service axis2