最近,一个曾经出现的struts2报错 在项目部署在某服务器后再次出现,于是很有兴致的去挖了一下struts2的源代码(struts2-core-2.1.6、xwork-2.1.2、ognl-2.6.11)。报错如下:
ognl.MethodFailedException: Method "setEndtime" failed for object com.piptrade.action.tradetools.eCalerddarAction@17db177 [java.lang.NoSuchMethodException:
setEndtime([Ljava.lang.String;)]
首先需要的是开头,于是来到web.xml找到过滤器:
org.apache.struts2.dispatcher.FilterDispatcher
直接找到doFilter,关键源码(395行):
dispatcher.serviceAction(request, response, servletContext, mapping);
进入方法serviceAction,关键源码(468行):
proxy.execute();
进入类StrutsActionProxy方法execute,关键源码(52行):
return invocation.invoke();
之后在类DefaultActionInvocation方法invoke中进行一系列拦截器的调用(231-243行):
if (interceptors.hasNext()) {final InterceptorMapping interceptor = (InterceptorMapping) interceptors.next();String interceptorMsg = "interceptor: " + interceptor.getName();UtilTimerStack.push(interceptorMsg);try {resultCode = interceptor.getInterceptor().intercept(DefaultActionInvocation.this);}finally {UtilTimerStack.pop(interceptorMsg);}}
而调用action中setter的拦截器名称是:
com.opensymphony.xwork2.interceptor.ParametersInterceptor
其中方法doIntercept中带有关键源码(186-187行):
ValueStack stack = ac.getValueStack();setParameters(action, stack, parameters);
继续进入方法setParameters,关键源码(248行、273行):
ValueStack newStack = valueStackFactory.createValueStack(stack);
newStack.setValue(name, value);
继续进入类OgnlValueStack方法setValue链,关键源码(155行):
ognlUtil.setValue(expr, context, root, value);
继续进入类OgnlUtil方法setValue,关键源码(192行):
Ognl.setValue(compile(name), context, root, value);
这时,方法调用到达ongl包的核心部分,关键源码(476行):
n.setValue( ognlContext, root, value );
调用链如下:
类ognl.SimpleNode(246行、177行):
evaluateSetValueBody(context, target, value);
setValueBody(context, target, value);
到达类ASTProperty方法setValueBody,关键源码(101行):
OgnlRuntime.setProperty( context, target, getProperty( context, target), value );
继续进入类OgnlRuntime方法setProperty,关键源码(1656行):
accessor.setProperty( context, target, name, value );
这时accessor类型为CompoundRootAccessor,
继续进入类CompoundRootAccessor方法setProperty,关键源码(49-52行):
if (OgnlRuntime.hasSetProperty(ognlContext, o, name)) {
OgnlRuntime.setProperty(ognlContext, o, name, value);
return;}
再次进入类OgnlRuntime方法setProperty,关键源码(1656行):
accessor.setProperty( context, target, name, value );
这时accessor类型为ObjectAccessor,
并调用到父类ObjectPropertyAccessor方法setProperty,关键源码(131行):
if (setPossibleProperty(context, target, name, value) == OgnlRuntime.NotFound)
继续,目标方法调用出现(75-76行):
if (!OgnlRuntime.setMethodValue(ognlContext, target, name, value, true))
result = OgnlRuntime.setFieldValue(ognlContext, target, name, value) ? null : OgnlRuntime.NotFound;
这里涉及2个方法,顺便简单分析一下这2个方法的关键源码:
1、 OgnlRuntime.setMethodValue(964行):
callAppropriateMethod(context, target, target, m.getName(), propertyName, Collections.nCopies(1, m), args);
从中可见,ognl.MethodFailedException就是从这个callAppropriateMethod方法中抛出的
2、 OgnlRuntime.setFieldValue(1136行、1140行、1146行)
state = context.getMemberAccess().setup(context, target, f, propertyName);
f.set(target, value);
context.getMemberAccess().restore(context, target, f, propertyName, state);
从中可见,为field赋值时,如果访问范围是不可外部访问的,先改为可访问,赋值后再改为原访问范围
这2个方法都有一个共同的逻辑:
都判断了(1)传入参数的类型是否为setter方法的参数类型(isAssignableFrom)
否则(2)把传入参数转换为setter方法的参数类型(getConvertedType)
其中逻辑(2)使用了接口ognl.TypeConverter,xwork2中的对应实现类为
com.opensymphony.xwork2.ognl.OgnlTypeConverterWrapper
并且再提供了接口com.opensymphony.xwork2.conversion.TypeConverter,默认的实现类为
com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter
至此,到达本文开始提到的报错问题相关的源代码位置:
类XWorkBasicConverter方法convertValue,关键源码(102-103行):
else if (Date.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);}
方法doConvertToDate,关键源码(305行、310-319行、332-336行):
Locale locale = getLocale(context);
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);
else if (java.util.Date.class == toType) {Date check = null;SimpleDateFormat d1 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.LONG, locale);SimpleDateFormat d2 = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM, locale);SimpleDateFormat d3 = (SimpleDateFormat)DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, locale);
方法getLocale,源码(145-157行):
private Locale getLocale(Map<String, Object> context) {if (context == null) {return Locale.getDefault();}Locale locale = (Locale)context.get(ActionContext.LOCALE);if (locale == null) {locale = Locale.getDefault();}return locale;}
分析:由于java.sql.Timestamp extends java.util.Date,
Date.class.isAssignableFrom判断成功,进入doConvertToDate,
其中使用的Locale变量取自getLocale方法,当context中未被设置Locale时,使用的是本地的默认值。
因此,不管是Date还是Timestamp,当服务器的默认Locale不是通常使用的zh_CN时,转换异常,返回后在callAppropriateMethod中抛出java.lang.NoSuchMethodException。
解决方法有两种:
1、 服务器配置:将服务器的环境变量Lang改为zh_CN.UTF-8
2、 工程配置:对配置文件struts.properties,
写入struts.locale=zh_CN,顺便加上struts.i18n.encoding=UTF-8
注:方法2在struts2.1.6下发现无效,经查是由于xwork-core-2.1.2中如下代码段缺少造成的:
ParametersInterceptor.setParameters
缺少代码
//keep locale from original context
context.put(ActionContext.LOCALE,stack.getContext().get(ActionContext.LOCALE));
建议替换为已解决此bug的struts2.1.8
或者将新版本的ParametersInterceptor类编译覆盖源文件,地址:
http://svn.apache.org/viewvc?view=revision&revision=956389
问题解决,顺便附上源码包。