一、问题描述
听同事反馈,通过发布平台发布了一个新版本,总共发布到了多台机器,但是只有其中一台报了NoSuchMethodError
的错误,堆栈信息如下:
java.lang.NoSuchMethodError: org.apache.log4j.MDC.put(Ljava/lang/String;Ljava/lang/String;)V
at com.tencent.fit.oms.framework.aspect.ControllerAccLogAspect.doBefore(ControllerAccLogAspect.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:627)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:609)
at org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:43)
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:55)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:55)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
许多"有经验"的开发人员一眼就知道是什么问题---基本上可以断定是版本冲突。
而且本来就不建议使用具体的日志实现类来加日志,推荐使用slf4j,所以我们应该为自己的错误买单。
二、问题解决思路
仔细对比了一下机器的lib,发现该机器和其它机器都是相同的。而且也排除开发人员二次开发log4j,然后deploy到了公共私服的可能(md5值相同)。既然对比包解决不了,就只能看具体的代码实现了。
来到IDEA,搜索下MDC,确实发现有两个实现,一个是log4j下的,一个是log4j-over-slf4j下的。并且log4j下的MDC并没有put(String,String)
方法,而是put(String,Object)
。基本上可以断定是加载了错误的log4j,但是即使没有put(String,String)也不应该报错才对啊?毕竟String也属于Object.先不纠结这个问题,直接线上验证下MDC再说。
直接上Arthas,通过jad org.apache.log4j.MDC
查看字节码,发现内存中确实是log4j1.2.17的实现,没有put(String,String)
方法,而且通过sc -d org.apache.log4j.MDC
查看类加载信息,发现该类来自于log4j1.2.17,而正常的机器该类却来自于log4j-over-slf4j.解决问题要紧,赶紧删了log4j1.2.17,然后重新启动,问题解决。
三、问题延伸
今天也比较纠结,和一些"高端玩家"讨论了jvm加载相同package、类名class的顺序问题,也没有个所以然,大概有这么几类说法:
- 不知道,没了解过springboot类加载机制
因为我们这是springboot应用 - 顺序加载,谁在前面就加载谁
问题:怎么判断前后?jar名字?系统时间戳? - 随机加载
感觉不太可信,如果是随机加载,感觉java语言本身就存在漏洞。 - 使用层面的问题,就不应该用log4j.MDC,应该使用slf4j.MDC
的确是这样。但是任何项目都是有历史原因的,而且感觉是为自己不了解jvm的加载顺序机制找个理由"开脱",开个玩笑,别当真~
回过头来再看为什么会抛出NoSuchMethodError
这个错误,明明是put(String,Object)
却不能接收put(String,String)
,看似很奇怪,其实是编译器给我们施了一个障眼法.当我们编译的时候MDC.put(xx,xx)
被编译成了MDC.put(String,String),当我们启动的时候却错误的加载了put(String,Object),导致方法签名不兼容,从而抛出了这个错误。下面我们来演示一下:
新建一个MyMDC.java:
public class MyMDC {
public static void main(String[] args) {
put("jerrik","handsome");
}
private static void put(String jerrik, String world) {
System.out.println("string");
MDC.put("ok",world);
}
}
编译后,将MyMDC.class复制出来保存,然后去掉log4j.over.slf4j的依赖,加上log4j1.2.17的依赖。防止编译器给我们重新编译,用之前保存的MyMDC.class覆盖掉当前MyMDC.class,然后在命令行使用java MyMDC运行,就会出错:
Exception in thread "main" java.lang.NoSuchMethodError: org.apache.log4j.MDC.put(Ljava/lang/String;Ljava/lang/String;)V
at MyMDC.put(MyMDC.java:18)
at MyMDC.main(MyMDC.java:13)
终于真相大白了,也是今天群里一哥们@liufor给了我一个思路,表示感谢。这里也是我们的一个推断,不代表完全正确,希望读者也有自己的思考。
四、最后
不了解arthas的童鞋,可以去arthas官网学习一下Arthas,线上问题定位的利器。btrace,housemd感觉复杂度还是太高了,这个完全是傻瓜式的。不过如果要定位高大上的问题,估计还是得派上用场。今天这个问题如果没有arthas协助的话,可以增加一个jvm参数--XX:+TraceClassLoading
,将类加载信息打出来,也能知道MDC具体来自哪个jar包,是不是美滋滋? 还想多说几句,今天知道了几个工具:po转vo的mapStruct
和微服务脚手架JHipster
,有兴趣的童鞋可以研究下。