从 Could not initialize class ReflectionToStringBuilder
思考NoClassDefFound
和ClassNotFound
项目环境
- JDK 11
- Spring Boot 2.7
- commons-lang 2.4
现象
系统的一个服务中,异步任务使用 OpenFeign
调用另一个服务的接口。调用前打印了请求参数,其中请求参数中的一个类的 toString
方法被重写为如下代码:
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
ReflectionToStringBuilder 使用的是 commons-lang 2.4 包。
在切面打印参数时,出现了多次类似以下的错误日志:
2024-01-08 23:26:36.813 ERROR 13896 --- [nio-8089-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.apache.commons.lang.builder.ReflectionToStringBuilder] with root cause
java.lang.NoClassDefFoundError: Could not initialize class org.apache.commons.lang.builder.ReflectionToStringBuilder
at com.notwaste.nacosprovider.controller.ProviderController.hello(ProviderController.java:56) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.19.jar:5.3.19]
...
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
还存在一个藏得比较深的异常日志:
java.lang.StringIndexOutOfBoundsException: begin 0, end 3, length 2
at java.base/java.lang.String.checkBoundsBeginEnd(String.java:3319)
at java.base/java.lang.String.substring(String.java:1874)
at org.apache.commons.lang.SystemUtils.getJavaVersionAsFloat(SystemUtils.java:1133)
at org.apache.commons.lang.SystemUtils.<clinit>(SystemUtils.java:818)
at org.apache.commons.lang.builder.ToStringStyle$MultiLineToStringStyle.<init>(ToStringStyle.java:2269)
at org.apache.commons.lang.builder.ToStringStyle.<clinit>(ToStringStyle.java:95)
at org.apache.commons.lang.builder.ToStringBuilder.<clinit>(ToStringBuilder.java:98)
问题排查
一开始只注意到了NoClassDefFoundError的异常,认为与ClassNotFound类似,所以对服务进行了重新打包构建,在这之后发现问题仍在,并且发现了上面提到的第二部分的日志StringIndexOutOfBoundsException,在多次尝试以后发现问题能够稳定复现
1.服务重启后第一次调用到这个toString代码段时能够稳定出现StringIndexOutOfBoundsException。
2.在这之后再多次调用的话则会变成NoClassDefFoundError。
基于上面的现象,在本地断点尝试,但本地未能复现问题。
随后查阅资料NoClassDefFoundError问题原因发现实际与ClassNotFound并不相同
- ClassNotFound出现于发射加载类的时候找不到相应的类
- NoClassDefFound是一个Error不是Exception,发生在编译时该类存在但运行时无法加载该类
随后发现本地环境的JDK与综测不一致,随机换成综测环境的OpenJDK11,重新尝试后发现本地能够复现问题,通过对代码进行断点发现,报错来源于下面的代码,在SystemUtils中存在这么一段代码
/**
* <p>Gets the Java version as a <code>float</code>.</p>
*
* <p>Example return values:</p>
* <ul>
* <li><code>1.2f</code> for JDK 1.2
* <li><code>1.31f</code> for JDK 1.3.1
* </ul>
*
* <p>The field will return zero if {@link #JAVA_VERSION} is <code>null</code>.</p>
*
* @since 2.0
*/
public static final float JAVA_VERSION_FLOAT = getJavaVersionAsFloat();
/**
* <p>Gets the Java version number as a <code>float</code>.</p>
*
* <p>Example return values:</p>
* <ul>
* <li><code>1.2f</code> for JDK 1.2
* <li><code>1.31f</code> for JDK 1.3.1
* </ul>
*
* <p>Patch releases are not reported.
* Zero is returned if {@link #JAVA_VERSION_TRIMMED} is <code>null</code>.</p>
*
* @return the version, for example 1.31f for JDK 1.3.1
*/
private static float getJavaVersionAsFloat() {
if (JAVA_VERSION_TRIMMED == null) {
return 0f;
}
String str = JAVA_VERSION_TRIMMED.substring(0, 3);
if (JAVA_VERSION_TRIMMED.length() >= 5) {
str = str + JAVA_VERSION_TRIMMED.substring(4, 5);
}
try {
return Float.parseFloat(str);
} catch (Exception ex) {
return 0;
}
}
而这段代码在JDK11中JAVA_VERSION_TRIMMED=11,在执行
String str = JAVA_VERSION_TRIMMED.substring(0, 3);
会出现下标越界的问题
后续实际是在ReflectionToStringBuilder实例化的时候会调用到父类ToStringBuilder的构造方法
public static String toString(Object object, ToStringStyle style, boolean outputTransients, boolean outputStatics,
Class reflectUpToClass) {
return new ReflectionToStringBuilder(object, style, null, reflectUpToClass, outputTransients, outputStatics)
.toString();
}
public ReflectionToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer, Class reflectUpToClass,
boolean outputTransients, boolean outputStatics) {
super(object, style, buffer);
this.setUpToClass(reflectUpToClass);
this.setAppendTransients(outputTransients);
this.setAppendStatics(outputStatics);
}
public ToStringBuilder(Object object, ToStringStyle style, StringBuffer buffer) {
if (style == null) {
style = getDefaultStyle();
}
if (buffer == null) {
buffer = new StringBuffer(512);
}
this.buffer = buffer;
this.style = style;
this.object = object;
style.appendStart(buffer, object);
}
在getDefaultStyle方法中使用到了SystemUtils需要将其实例化,而public static final float JAVA_VERSION_FLOAT = getJavaVersionAsFloat(); 作为静态常量在实例化的时候需要执行到该方法导致。
解决
最后升级commons-lang 2.4为2.6可解决问题
或者使用带小版本的JDK如11.0.18等