阅读内容 

一个细小,难以察觉的错误

[日期:2008-10-08] 来源:  作者: [字体: ]
     这是一个控制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循环内部。  
阅读:
录入:blue1000

推荐 】 【 打印
相关新闻      
本文评论       全部评论
发表评论
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款


点评: 字数
姓名:
Advertisement
内容查询


Advertisement