diff --git a/common/src/main/java/org/apache/iceberg/common/DynClasses.java b/common/src/main/java/org/apache/iceberg/common/DynClasses.java index 3d42171847d3..2162655090cc 100644 --- a/common/src/main/java/org/apache/iceberg/common/DynClasses.java +++ b/common/src/main/java/org/apache/iceberg/common/DynClasses.java @@ -66,8 +66,8 @@ public Builder impl(String className) { try { this.foundClass = Class.forName(className, true, loader); - } catch (ClassNotFoundException e) { - // not the right implementation + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // cannot load this implementation } return this; diff --git a/common/src/main/java/org/apache/iceberg/common/DynConstructors.java b/common/src/main/java/org/apache/iceberg/common/DynConstructors.java index bf857a15ab89..9a419a86d31b 100644 --- a/common/src/main/java/org/apache/iceberg/common/DynConstructors.java +++ b/common/src/main/java/org/apache/iceberg/common/DynConstructors.java @@ -222,7 +222,7 @@ public Ctor build() { private Class classForName(String className) throws ClassNotFoundException { try { return Class.forName(className, true, loader); - } catch (ClassNotFoundException e) { + } catch (ClassNotFoundException | NoClassDefFoundError e) { if (loader != Thread.currentThread().getContextClassLoader()) { return Class.forName(className, true, Thread.currentThread().getContextClassLoader()); } else { diff --git a/common/src/main/java/org/apache/iceberg/common/DynFields.java b/common/src/main/java/org/apache/iceberg/common/DynFields.java index d3c806bd40db..a44dc0991a23 100644 --- a/common/src/main/java/org/apache/iceberg/common/DynFields.java +++ b/common/src/main/java/org/apache/iceberg/common/DynFields.java @@ -235,8 +235,8 @@ public Builder impl(String className, String fieldName) { try { Class targetClass = Class.forName(className, true, loader); impl(targetClass, fieldName); - } catch (ClassNotFoundException e) { - // not the right implementation + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // cannot load this implementation candidates.add(className + "." + fieldName); } return this; @@ -284,8 +284,8 @@ public Builder hiddenImpl(String className, String fieldName) { try { Class targetClass = Class.forName(className, true, loader); hiddenImpl(targetClass, fieldName); - } catch (ClassNotFoundException e) { - // not the right implementation + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // cannot load this implementation candidates.add(className + "." + fieldName); } return this; diff --git a/common/src/main/java/org/apache/iceberg/common/DynMethods.java b/common/src/main/java/org/apache/iceberg/common/DynMethods.java index 5760c8694abe..5314928fe7fa 100644 --- a/common/src/main/java/org/apache/iceberg/common/DynMethods.java +++ b/common/src/main/java/org/apache/iceberg/common/DynMethods.java @@ -252,8 +252,8 @@ public Builder impl(String className, String methodName, Class... argClasses) try { Class targetClass = Class.forName(className, true, loader); impl(targetClass, methodName, argClasses); - } catch (ClassNotFoundException e) { - // not the right implementation + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // cannot load this implementation } return this; } @@ -333,8 +333,8 @@ public Builder hiddenImpl(String className, String methodName, Class... argCl try { Class targetClass = Class.forName(className, true, loader); hiddenImpl(targetClass, methodName, argClasses); - } catch (ClassNotFoundException e) { - // not the right implementation + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // cannot load this implementation } return this; } diff --git a/common/src/test/java/org/apache/iceberg/common/TestDynClasses.java b/common/src/test/java/org/apache/iceberg/common/TestDynClasses.java new file mode 100644 index 000000000000..b96d186c732f --- /dev/null +++ b/common/src/test/java/org/apache/iceberg/common/TestDynClasses.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class TestDynClasses { + static class Available {} + + @Test + void implWithNoClassDefFoundError() throws ClassNotFoundException { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.apache.iceberg.MissingDependencyClass".equals(name)) { + throw new NoClassDefFoundError("some/TransitiveDependency"); + } + + return super.loadClass(name, resolve); + } + }; + + assertThatThrownBy( + () -> + DynClasses.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .buildChecked()) + .isInstanceOf(ClassNotFoundException.class) + .hasMessage("Cannot find class; alternatives: org.apache.iceberg.MissingDependencyClass"); + + assertThat( + DynClasses.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .orNull() + .buildChecked()) + .isNull(); + + assertThat( + DynClasses.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .impl("org.apache.iceberg.common.TestDynClasses$Available") + .buildChecked()) + .isEqualTo(Available.class); + } + + @Test + void implWithExceptionInInitializerError() { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + throw new ExceptionInInitializerError("static initializer failed"); + } + }; + + assertThatThrownBy( + () -> + DynClasses.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.FailingInitClass") + .buildChecked()) + .isInstanceOf(ExceptionInInitializerError.class) + .hasMessage("static initializer failed"); + } +} diff --git a/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java b/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java index 1edf70a7fbba..59004396f9c7 100644 --- a/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java +++ b/common/src/test/java/org/apache/iceberg/common/TestDynConstructors.java @@ -77,6 +77,92 @@ public void testLoaderFallback() throws Exception { assertThat(ctor.newInstance()).isInstanceOf(MyClass.class); } + @Test + public void implWithNoClassDefFoundError() throws Exception { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.apache.iceberg.MissingDependencyClass".equals(name)) { + throw new NoClassDefFoundError("some/TransitiveDependency"); + } + + return super.loadClass(name, resolve); + } + }; + + assertThatThrownBy( + () -> + DynConstructors.builder(MyInterface.class) + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .buildChecked()) + .isInstanceOf(NoSuchMethodException.class) + .hasMessageStartingWith("Cannot find constructor for interface") + .hasMessageContaining("Missing org.apache.iceberg.MissingDependencyClass"); + + assertThat( + DynConstructors.builder(MyInterface.class) + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .impl(MyClass.class) + .buildChecked() + .newInstance()) + .isInstanceOf(MyClass.class); + + assertThat( + DynConstructors.builder(MyInterface.class) + .loader(errorLoader) + .hiddenImpl("org.apache.iceberg.MissingDependencyClass") + .impl(MyClass.class) + .buildChecked() + .newInstance()) + .isInstanceOf(MyClass.class); + } + + @Test + public void implWithNoClassDefFoundErrorFallsBackToContextClassLoader() throws Exception { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (MyClass.class.getName().equals(name)) { + throw new NoClassDefFoundError("some/TransitiveDependency"); + } + + return super.loadClass(name, resolve); + } + }; + + assertThat( + DynConstructors.builder(MyInterface.class) + .loader(errorLoader) + .impl(MyClass.class.getName()) + .buildChecked() + .newInstance()) + .isInstanceOf(MyClass.class); + } + + @Test + public void implWithExceptionInInitializerError() { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + throw new ExceptionInInitializerError("static initializer failed"); + } + }; + + assertThatThrownBy( + () -> + DynConstructors.builder(MyInterface.class) + .loader(errorLoader) + .impl("org.apache.iceberg.FailingInitClass") + .buildChecked()) + .isInstanceOf(ExceptionInInitializerError.class) + .hasMessage("static initializer failed"); + } + public interface MyInterface {} public static class MyClass implements MyInterface {} diff --git a/common/src/test/java/org/apache/iceberg/common/TestDynFields.java b/common/src/test/java/org/apache/iceberg/common/TestDynFields.java new file mode 100644 index 000000000000..3211a66acfa0 --- /dev/null +++ b/common/src/test/java/org/apache/iceberg/common/TestDynFields.java @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class TestDynFields { + static class FieldHolder { + public String value = "hello"; + + @SuppressWarnings("unused") + private String hidden = "secret"; + } + + @Test + void implWithNoClassDefFoundError() throws NoSuchFieldException { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.apache.iceberg.MissingDependencyClass".equals(name)) { + throw new NoClassDefFoundError("some/TransitiveDependency"); + } + + return super.loadClass(name, resolve); + } + }; + + assertThatThrownBy( + () -> + DynFields.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass", "value") + .buildChecked()) + .isInstanceOf(NoSuchFieldException.class) + .hasMessage( + "Cannot find field from candidates: org.apache.iceberg.MissingDependencyClass.value"); + + assertThat( + DynFields.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass", "value") + .impl(FieldHolder.class, "value") + .buildChecked() + .get(new FieldHolder())) + .isEqualTo("hello"); + + assertThat( + DynFields.builder() + .loader(errorLoader) + .hiddenImpl("org.apache.iceberg.MissingDependencyClass", "hidden") + .hiddenImpl(FieldHolder.class, "hidden") + .buildChecked() + .get(new FieldHolder())) + .isEqualTo("secret"); + } + + @Test + void implWithExceptionInInitializerError() { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + throw new ExceptionInInitializerError("static initializer failed"); + } + }; + + assertThatThrownBy( + () -> + DynFields.builder() + .loader(errorLoader) + .impl("org.apache.iceberg.FailingInitClass", "value") + .buildChecked()) + .isInstanceOf(ExceptionInInitializerError.class) + .hasMessage("static initializer failed"); + } +} diff --git a/common/src/test/java/org/apache/iceberg/common/TestDynMethods.java b/common/src/test/java/org/apache/iceberg/common/TestDynMethods.java new file mode 100644 index 000000000000..197d439d02fb --- /dev/null +++ b/common/src/test/java/org/apache/iceberg/common/TestDynMethods.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.iceberg.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class TestDynMethods { + static class Available { + public static String register() { + return "available"; + } + + @SuppressWarnings("unused") + private static String hiddenRegister() { + return "hidden-available"; + } + } + + @Test + void implWithNoClassDefFoundError() throws NoSuchMethodException { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if ("org.apache.iceberg.MissingDependencyClass".equals(name)) { + throw new NoClassDefFoundError("some/TransitiveDependency"); + } + + return super.loadClass(name, resolve); + } + }; + + assertThatThrownBy( + () -> + DynMethods.builder("register") + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .buildStaticChecked()) + .isInstanceOf(NoSuchMethodException.class) + .hasMessage("Cannot find method: register"); + + assertThat( + DynMethods.builder("register") + .loader(errorLoader) + .impl("org.apache.iceberg.MissingDependencyClass") + .impl(Available.class) + .buildStaticChecked() + .invoke()) + .isEqualTo("available"); + + assertThat( + DynMethods.builder("register") + .loader(errorLoader) + .hiddenImpl("org.apache.iceberg.MissingDependencyClass") + .hiddenImpl(Available.class, "hiddenRegister") + .buildStaticChecked() + .invoke()) + .isEqualTo("hidden-available"); + } + + @Test + void implWithExceptionInInitializerError() { + ClassLoader errorLoader = + new ClassLoader(Thread.currentThread().getContextClassLoader()) { + @Override + public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + throw new ExceptionInInitializerError("static initializer failed"); + } + }; + + assertThatThrownBy( + () -> + DynMethods.builder("register") + .loader(errorLoader) + .impl("org.apache.iceberg.FailingInitClass") + .buildStaticChecked()) + .isInstanceOf(ExceptionInInitializerError.class) + .hasMessage("static initializer failed"); + } +}