一个细小,难以察觉的错误
这是一个控制Java动态代理执行的类(附C#实现),target域是他的代理目标,动态代理会生成一个可以替代target的类,在调用target类的某方法时,实际上是调用被改造过的方法(会多执行代码块中标记custom code的那些代码)。
不过这些不是重点。
重点是,ignorePrefixList中包含了一些字符串,而要求是以这些字符串开头的方法,不执行//custom code中省略的逻辑,而是绕到else块直接执行原方法。
下面这段代码,看似在对方法名和ignorePrefixList中的字符串进行逐个比较的时候,没什么问题,但还是希望各位别看后面,推测下isMethodIgnored方法会发生什么问题。
代码:
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方法内部也稍有不同):
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"的字符串元素,于是执行
而||发现res本来就为true,结果直接返回,没有执行关键的it.next();接着就去看循环条件了。但因为next()方法没执行,因此迭代子位置都没动,下一元素还是"list"字符串。结果又回到开头一幕。于是就死掉了...
修正得方法很多,交换||两端位置可以:
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 }
把||改成|也可以:
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直接跳出方法也可以:
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方法,就是
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循环内部。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端