歡迎光臨
每天分享高質量文章

深入 JVM 分析 spring-boot 應用 hibernate-validatorNoClassDefFoundError

(點選上方公眾號,可快速關註)


來源:hengyunabc ,

hengyunabc.github.io/depth-analysis-hibernate-validar-noclassdefounderror/

問題

可重現的Demo程式碼:demo.zip

http://hengyunabc.github.io/img/demo.zip

最近排查一個spring boot應用丟擲hibernate.validator NoClassDefFoundError的問題,異常資訊如下:

Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.hibernate.validator.internal.engine.ConfigurationImpl

at org.hibernate.validator.HibernateValidator.createGenericConfiguration(HibernateValidator.java:33) ~[hibernate-validator-5.3.5.Final.jar:5.3.5.Final]

at javax.validation.Validation$GenericBootstrapImpl.configure(Validation.java:276) ~[validation-api-1.1.0.Final.jar:na]

at org.springframework.boot.validation.MessageInterpolatorFactory.getObject(MessageInterpolatorFactory.java:53) ~[spring-boot-1.5.3.RELEASE.jar:1.5.3.RELEASE]

at org.springframework.boot.autoconfigure.validation.DefaultValidatorConfiguration.defaultValidator(DefaultValidatorConfiguration.java:43) ~[spring-boot-autoconfigure-1.5.3.RELEASE.jar:1.5.3.RELEASE]

at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_112]

at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_112]

at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_112]

at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_112]

at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:162) ~[spring-beans-4.3.8.RELEASE.jar:4.3.8.RELEASE]

… 32 common frames omitted

這個錯誤資訊錶面上是NoClassDefFoundError,但是實際上ConfigurationImpl這個類是在hibernate-validator-5.3.5.Final.jar裡的,不應該出現找不到類的情況。

那為什麼應用裡丟擲這個NoClassDefFoundError ?

有經驗的開發人員從Could not initialize class 這個資訊就可以知道,實際上是一個類在初始化時丟擲的異常,比如static的靜態程式碼塊,或者static欄位初始化的異常。

誰初始化了 org.hibernate.validator.internal.engine.ConfigurationImpl

但是當我們在HibernateValidator 這個類,建立ConfigurationImpl的程式碼塊裡打斷點時,發現有兩個執行緒觸發了斷點:

public class HibernateValidator implements ValidationProvider {

    @Override

    public Configuration > createGenericConfiguration(BootstrapState state) {

        return new ConfigurationImpl( state );

    }

其中一個執行緒的呼叫棧是:

Thread [background-preinit] (Class load: ConfigurationImpl)

    HibernateValidator.createGenericConfiguration(BootstrapState) line: 33

    Validation$GenericBootstrapImpl.configure() line: 276

    BackgroundPreinitializer$ValidationInitializer.run() line: 107

    BackgroundPreinitializer$1.runSafely(Runnable) line: 59

    BackgroundPreinitializer$1.run() line: 52

    Thread.run() line: 745

另外一個執行緒呼叫棧是:

Thread [main] (Suspended (breakpoint at line 33 in HibernateValidator))

    owns: ConcurrentHashMap  (id=52)

    owns: Object  (id=53)

    HibernateValidator.createGenericConfiguration(BootstrapState) line: 33

    Validation$GenericBootstrapImpl.configure() line: 276

    MessageInterpolatorFactory.getObject() line: 53

    DefaultValidatorConfiguration.defaultValidator() line: 43

    NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]

    NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62

    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43

    Method.invoke(Object, Object…) line: 498

    CglibSubclassingInstantiationStrategy(SimpleInstantiationStrategy).instantiate(RootBeanDefinition, String, BeanFactory, Object, Method, Object…) line: 162

    ConstructorResolver.instantiateUsingFactoryMethod(String, RootBeanDefinition, Object[]) line: 588

    DefaultListableBeanFactory(AbstractAutowireCapableBeanFactory).instantiateUsingFactoryMethod(String, RootBeanDefinition, Object[]) line: 1173

顯然,這個執行緒的呼叫棧是常見的spring的初始化過程。

BackgroundPreinitializer 做了什麼

那麼重點來看下 BackgroundPreinitializer 執行緒做了哪些事情:

@Order(LoggingApplicationListener.DEFAULT_ORDER + 1)

public class BackgroundPreinitializer

        implements ApplicationListener {

    @Override

    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {

        try {

            Thread thread = new Thread(new Runnable() {

                @Override

                public void run() {

                    runSafely(new MessageConverterInitializer());

                    runSafely(new MBeanFactoryInitializer());

                    runSafely(new ValidationInitializer());

                    runSafely(new JacksonInitializer());

                    runSafely(new ConversionServiceInitializer());

                }

                public void runSafely(Runnable runnable) {

                    try {

                        runnable.run();

                    }

                    catch (Throwable ex) {

                        // Ignore

                    }

                }

            }, “background-preinit”);

            thread.start();

        }

可以看到BackgroundPreinitializer類是spring boot為了加速應用的初始化,以一個獨立的執行緒來載入hibernate validator這些元件。

這個 background-preinit 執行緒會吞掉所有的異常。

顯然ConfigurationImpl 初始化的異常也被吞掉了,那麼如何才能獲取到最原始的資訊?

獲取到最原始的異常資訊

在BackgroundPreinitializer的 run() 函式裡打一個斷點(註意是Suspend thread型別, 不是Suspend VM),讓它先不要觸發ConfigurationImpl的載入,讓spring boot的正常流程去觸發ConfigurationImpl的載入,就可以知道具體的資訊了。

那麼打出來的異常資訊是:

Caused by: java.lang.NoSuchMethodError: org.jboss.logging.Logger.getMessageLogger(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Object;

    at org.hibernate.validator.internal.util.logging.LoggerFactory.make(LoggerFactory.java:19) ~[hibernate-validator-5.3.5.Final.jar:5.3.5.Final]

    at org.hibernate.validator.internal.util.Version.(Version.java:22) ~[hibernate-validator-5.3.5.Final.jar:5.3.5.Final]

    at org.hibernate.validator.internal.engine.ConfigurationImpl.(ConfigurationImpl.java:71) ~[hibernate-validator-5.3.5.Final.jar:5.3.5.Final]

    at org.hibernate.validator.HibernateValidator.createGenericConfiguration(HibernateValidator.java:33) ~[hibernate-validator-5.3.5.Final.jar:5.3.5.Final]

    at javax.validation.Validation$GenericBootstrapImpl.configure(Validation.java:276) ~[validation-api-1.1.0.Final.jar:na]

    at org.springframework.boot.validation.MessageInterpolatorFactory.getObject(MessageInterpolatorFactory.java:53) ~[spring-boot-1.5.3.RELEASE.jar:1.5.3.RELEASE]

那麼可以看出是 org.jboss.logging.Logger 這個類不相容,少了getMessageLogger(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Object 這個函式。

那麼檢查下應用的依賴,可以發現org.jboss.logging.Logger 在jboss-common-1.2.1.GA.jar和jboss-logging-3.3.1.Final.jar裡都有。

顯然是jboss-common-1.2.1.GA.jar 這個依賴過時了,需要排除掉。

總結異常的發生流程

  1. 應用依賴了jboss-common-1.2.1.GA.jar,它裡面的org.jboss.logging.Logger太老

  2. spring boot啟動時,BackgroundPreinitializer裡的執行緒去嘗試載入ConfigurationImpl,然後觸發了org.jboss.logging.Logger的函式執行問題

  3. BackgroundPreinitializer 吃掉了異常資訊,jvm把ConfigurationImpl標記為不可用的

  4. spring boot正常的流程去載入ConfigurationImpl,jvm發現ConfigurationImpl類是不可用,直接丟擲NoClassDefFoundError

Caused by: java.lang.NoClassDefFoundError: Could not initialize class org.hibernate.validator.internal.engine.ConfigurationImpl

深入JVM

為什麼第二次嘗試載入ConfigurationImpl時,會直接丟擲java.lang.NoClassDefFoundError: Could not initialize class ?

下麵用一段簡單的程式碼來重現這個問題:

try {

  org.hibernate.validator.internal.util.Version.touch();

} catch (Throwable e) {

  e.printStackTrace();

}

System.in.read();

try {

  org.hibernate.validator.internal.util.Version.touch();

} catch (Throwable e) {

  e.printStackTrace();

}

使用HSDB來確定類的狀態

當丟擲第一個異常時,嘗試用HSDB來看下這個類的狀態。

sudo java -classpath “$JAVA_HOME/lib/sa-jdi.jar” sun.jvm.hotspot.HSDB

然後在HSDB console裡查詢到Version的地址資訊

hsdb> class org.hibernate.validator.internal.util.Version

org/hibernate/validator/internal/util/Version @0x00000007c0060218

然後在Inspector查詢到這個地址,發現_init_state是5。

再看下hotspot程式碼,可以發現5對應的定義是initialization_error:

// /hotspot/src/share/vm/oops/instanceKlass.hpp

// See “The Java Virtual Machine Specification” section 2.16.2-5 for a detailed description

// of the class loading & initialization procedure, and the use of the states.

enum ClassState {

  allocated,                          // allocated (but not yet linked)

  loaded,                             // loaded and inserted in class hierarchy (but not linked yet)

  linked,                             // successfully linked/verified (but not initialized yet)

  being_initialized,                  // currently running class initializer

  fully_initialized,                  // initialized (successfull final state)

  initialization_error                // error happened during initialization

};

JVM規範裡關於Initialization的內容

http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.5

從規範裡可以看到初始一個類/介面有12步,比較重要的兩步都用黑體標記出來了:

  • 5: If the Class object for C is in an erroneous state, then initialization is not possible. Release LC and throw a NoClassDefFoundError.

  • 11: Otherwise, the class or interface initialization method must have completed abruptly by throwing some exception E. If the class of E is not Error or one of its subclasses, then create a new instance of the class ExceptionInInitializerError with E as the argument, and use this object in place of E in the following step.

第一次嘗試載入Version類時

當第一次嘗試載入時,hotspot InterpreterRuntime在解析invokestatic指令時,嘗試載入org.hibernate.validator.internal.util.Version類,InstanceKlass的_init_state先是標記為being_initialized,然後當載入失敗時,被標記為initialization_error。

對應Initialization的11步。

// hotspot/src/share/vm/oops/instanceKlass.cpp

// Step 10 and 11

Handle e(THREAD, PENDING_EXCEPTION);

CLEAR_PENDING_EXCEPTION;

// JVMTI has already reported the pending exception

// JVMTI internal flag reset is needed in order to report ExceptionInInitializerError

JvmtiExport::clear_detected_exception((JavaThread*)THREAD);

{

  EXCEPTION_MARK;

  this_oop->set_initialization_state_and_notify(initialization_error, THREAD);

  CLEAR_PENDING_EXCEPTION;   // ignore any exception thrown, class initialization error is thrown below

  // JVMTI has already reported the pending exception

  // JVMTI internal flag reset is needed in order to report ExceptionInInitializerError

  JvmtiExport::clear_detected_exception((JavaThread*)THREAD);

}

DTRACE_CLASSINIT_PROBE_WAIT(error, InstanceKlass::cast(this_oop()), -1,wait);

if (e->is_a(SystemDictionary::Error_klass())) {

  THROW_OOP(e());

} else {

  JavaCallArguments args(e);

  THROW_ARG(vmSymbols::java_lang_ExceptionInInitializerError(),

            vmSymbols::throwable_void_signature(),

            &args;);

}

第二次嘗試載入Version類時

當第二次嘗試載入時,檢查InstanceKlass的_init_state是initialization_error,則直接丟擲NoClassDefFoundError: Could not initialize class.

對應Initialization的5步。

// hotspot/src/share/vm/oops/instanceKlass.cpp

void InstanceKlass::initialize_impl(instanceKlassHandle this_oop, TRAPS) {

// …

    // Step 5

    if (this_oop->is_in_error_state()) {

      DTRACE_CLASSINIT_PROBE_WAIT(erroneous, InstanceKlass::cast(this_oop()), -1,wait);

      ResourceMark rm(THREAD);

      const char* desc = “Could not initialize class “;

      const char* className = this_oop->external_name();

      size_t msglen = strlen(desc) + strlen(className) + 1;

      char* message = NEW_RESOURCE_ARRAY(char, msglen);

      if (NULL == message) {

        // Out of memory: can’t create detailed error message

        THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), className);

      } else {

        jio_snprintf(message, msglen, “%s%s”, desc, className);

        THROW_MSG(vmSymbols::java_lang_NoClassDefFoundError(), message);

      }

    }

總結

  • spring boot在BackgroundPreinitializer類裡用一個獨立的執行緒來載入validator,並吃掉了原始異常

  • 第一次載入失敗的類,在jvm裡會被標記為initialization_error,再次載入時會直接丟擲NoClassDefFoundError: Could not initialize class

  • 當在程式碼裡吞掉異常時要謹慎,否則排查問題帶來很大的困難

  • http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.5

看完本文有收穫?請轉發分享給更多人

關註「ImportNew」,提升Java技能

贊(0)

分享創造快樂