From 8fa37117213421146a55655bc79b5f4f6f0dfdf9 Mon Sep 17 00:00:00 2001 From: shangeyao Date: Wed, 1 Jul 2026 11:06:31 +0800 Subject: [PATCH] [Common] Fix ClassLoaderUtils dynamic classpath on JDK 9+ Walk the class hierarchy to locate the ucp field on JDK 11+ app classloaders, add JUnit coverage, and enable surefire add-opens on JDK 9+. Closes #4409 (part 1/3). Ref #4410. Generated-by: Cursor Co-authored-by: Cursor --- streampark-common/pom.xml | 20 +++++++ .../common/util/ClassLoaderUtils.scala | 17 ++++-- .../common/util/ClassLoaderUtilsTest.scala | 53 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 streampark-common/src/test/scala/org/apache/streampark/common/util/ClassLoaderUtilsTest.scala diff --git a/streampark-common/pom.xml b/streampark-common/pom.xml index 115477f4aa..03f4211b32 100644 --- a/streampark-common/pom.xml +++ b/streampark-common/pom.xml @@ -197,4 +197,24 @@ + + + + jdk9-plus-test-opens + + [9,) + + + + + org.apache.maven.plugins + maven-surefire-plugin + + --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens jdk.zipfs/jdk.nio.zipfs=ALL-UNNAMED + + + + + + diff --git a/streampark-common/src/main/scala/org/apache/streampark/common/util/ClassLoaderUtils.scala b/streampark-common/src/main/scala/org/apache/streampark/common/util/ClassLoaderUtils.scala index e052de96c3..e1664dc859 100644 --- a/streampark-common/src/main/scala/org/apache/streampark/common/util/ClassLoaderUtils.scala +++ b/streampark-common/src/main/scala/org/apache/streampark/common/util/ClassLoaderUtils.scala @@ -146,9 +146,20 @@ object ClassLoaderUtils extends Logger { addURL.setAccessible(true) addURL.invoke(c, file.toURI.toURL) case _ => - val field = classLoader.getClass.getDeclaredField("ucp") - field.setAccessible(true) - val ucp = field.get(classLoader) + var clazz: Class[_] = classLoader.getClass + var ucpField: java.lang.reflect.Field = null + while (clazz != null && ucpField == null) { + try { + ucpField = clazz.getDeclaredField("ucp") + } catch { + case _: NoSuchFieldException => clazz = clazz.getSuperclass + } + } + require( + ucpField != null, + "[StreamPark] ClassLoaderUtils.addURL: cannot locate ucp field on classloader chain") + ucpField.setAccessible(true) + val ucp = ucpField.get(classLoader) val addURL = ucp.getClass.getDeclaredMethod("addURL", Array(classOf[URL]): _*) addURL.setAccessible(true) diff --git a/streampark-common/src/test/scala/org/apache/streampark/common/util/ClassLoaderUtilsTest.scala b/streampark-common/src/test/scala/org/apache/streampark/common/util/ClassLoaderUtilsTest.scala new file mode 100644 index 0000000000..f4193e154a --- /dev/null +++ b/streampark-common/src/test/scala/org/apache/streampark/common/util/ClassLoaderUtilsTest.scala @@ -0,0 +1,53 @@ +/* + * 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.streampark.common.util + +import org.junit.jupiter.api.{Assertions, Test} + +import java.io.{File, FileOutputStream} +import java.util.jar.{JarEntry, JarOutputStream} + +class ClassLoaderUtilsTest { + + @Test def loadJarShouldAppendJarToSystemClassloader(): Unit = { + val jarFile = File.createTempFile("streampark-classloader-test", ".jar") + jarFile.deleteOnExit() + try { + val jarOut = new JarOutputStream(new FileOutputStream(jarFile)) + try { + jarOut.putNextEntry(new JarEntry("META-INF/MANIFEST.MF")) + jarOut.write("Manifest-Version: 1.0\n".getBytes("UTF-8")) + jarOut.closeEntry() + } finally { + jarOut.close() + } + ClassLoaderUtils.loadJar(jarFile.getAbsolutePath) + } finally { + jarFile.delete() + } + } + + @Test def loadResourceShouldAppendDirectoryToSystemClassloader(): Unit = { + val dir = FileUtils.createTempDir() + try { + ClassLoaderUtils.loadResource(dir.getAbsolutePath) + } finally { + Assertions.assertTrue(dir.delete()) + } + } +}