Could not initialize class ReflectionToStringBuilder思考NoClassDefFoundClassNotFound

项目环境

  • 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等