Log4X

链路纵横
一个细小,难以察觉的错误

 

这是一个控制Java动态代理执行的类(附C#实现),target域是他的代理目标,动态代理会生成一个可以替代target的类,在调用target类的某方法时,实际上是调用被改造过的方法(会多执行代码块中标记custom code的那些代码)。

不过这些不是重点。

重点是,ignorePrefixList中包含了一些字符串,而要求是以这些字符串开头的方法,不执行//custom code中省略的逻辑,而是绕到else块直接执行原方法。

下面这段代码,看似在对方法名和ignorePrefixList中的字符串进行逐个比较的时候,没什么问题,但还是希望各位别看后面,推测下isMethodIgnored方法会发生什么问题。

代码:

 1 public class MgtInvocation implements InvocationHandler{
 2 
 3     private static List ignorePrefixList;
 4     
 5     static {
 6         ignorePrefixList = new ArrayList();
 7         ignorePrefixList.add("get");
 8         ignorePrefixList.add("list");
 9     }
10     
11     private Object target;
12     
13     public Object invoke(Object proxy, Method method, Object[] args)
14             throws Throwable {
15         if(!isMethodIgnore(method)){
16             //custom code 
17             //
18             return method.invoke(target, args);
19             //custom code
20             //
21         }else {
22             return method.invoke(target, args);
23         }
24     }
25     
26     private boolean isMethodIgnore(Method mtd){
27         boolean res = false;
28         String mtdName = mtd.getName();
29         for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
30             res = res||mtdName.startsWith((String)it.next());
31         }
32         return res;
33     }
34 }

 

翻译成C#大致是这样的(C#没有动态代理,所以这个类是做做样子的,又因为Iterator和IEnumrator稍有不同,此处使用并不会导致错误,所以isMethodIgnore方法内部也稍有不同):

 1 public class FakeInvocation 
 2     {
 3         private static IList ignorePrefixList;
 4         private object target;
 5 
 6         static FakeInvocation(){
 7             ignorePrefixList = new ArrayList();
 8             ignorePrefixList.Add("get");
 9             ignorePrefixList.Add("list");
10         }
11 
12         public object invoke(object proxy, MethodInfo method, object[] args)
13         {
14             if (!isMethodIgnore(method))
15             {
16                 //custom code 
17                 //
18                 return method.Invoke(target, args);
19                 //custom code
20                 //
21             }
22             else
23             {
24                 return method.Invoke(target, args);
25             }
26         }
27         public bool isMethodIgnore(MethodInfo mtd) {
28             bool res = false;
29             String mtdName = mtd.Name;
30             for (int i = 0; i < ignorePrefixList.Count; )
31             {
32                 res = res || mtdName.StartsWith((String)ignorePrefixList[i++]);
33             }
34             return res;
35         }
36     
37     }
38 

 

上面代码的问题是,isMethodIgnored方法极易陷入死循环。

当ignorePrefixList中,除去最后一个字符串元素外,任何一个若为mtdName的起始字符串,整个方法就会陷入死循环。

印象当中,死循环是菜鸟中的菜鸟才犯,因此当时根本想不到自己的代码会死循环,还以为是线程间死锁了。看来看去,也没发现哪里同步块不对,最后靠单步debug才找出错误。

原因很简单,就是||运算符,在发现左边为true时,就直接返回true而不执行右边了。

 

所以当这个代理去代理以get开头的目标方法时,就出现了这种景象:第一轮比较了"get"后,res被设成了true,迭代子看到后面还有个"list"的字符串元素,于是执行

1 res = res||mtdName.startsWith((String)it.next());

 

而||发现res本来就为true,结果直接返回,没有执行关键的it.next();接着就去看循环条件了。但因为next()方法没执行,因此迭代子位置都没动,下一元素还是"list"字符串。结果又回到开头一幕。于是就死掉了...

修正得方法很多,交换||两端位置可以:

 

1 private boolean isMethodIgnore(Method mtd){
2         boolean res = false;
3         String mtdName = mtd.getName();
4         for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
5             res = mtdName.startsWith((String)it.next())||res;
6         }
7         return res;
8     }

 

把||改成|也可以:

 

1 private boolean isMethodIgnore(Method mtd){
2         boolean res = false;
3         String mtdName = mtd.getName();
4         for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
5             res = res|mtdName.startsWith((String)it.next());
6         }
7         return res;
8     }

 

 遇到res为true直接跳出方法也可以:

 

1     private boolean isMethodIgnore(Method mtd){
2         String mtdName = mtd.getName();
3         for(Iterator it = ignorePrefixList.iterator();it.hasNext();){
4             if(mtdName.startsWith((String)it.next()))return true;
5         }
6         return false;
7     }

 

另外,我在把java换成C#过程中,发现要在C#中重现这错误还不容易。

因为如果'直译'isMethodIgnore方法,就是

 

1         public bool isMethodIgnore(MethodInfo mtd) {
2             bool res = false;
3             String mtdName = mtd.Name;
4             for (IEnumerator eu = ignorePrefixList.GetEnumerator(); eu.MoveNext(); )
5             {
6                 res = res || mtdName.StartsWith((String)eu.Current);
7             }
8             return res;
9         }

 

C#中的迭代子是不管后面有没有元素,先移动并返回移动成功与否(MoveNext),移动完了取当前元素(Current)。而java的迭代子是先向后瞟一眼(hasNext),发现有元素,取出下一个,并移动到那里(next)。

这样的话,移动操作并不是在 循环块内,因此可以保证每次都执行。

另外其实在C#和1.5以后的jdk里面,以上的for循环写法其实就是foreach的'手工版'。而如果用foreach的话,就不会有这个问题。

从上面的小错误,可以总结出两个经验:

 - 用&&和||的时候得想一想,右边如果不执行会不会对整体造成影响。

 - 尽量不把结束条件,前进操作之类写到for循环内部。

posted on 2008-10-07 00:03  YYX  阅读(2297)  评论(5编辑  收藏  举报