From 8729131488d4518a31cc162ceae416acd3933a5e Mon Sep 17 00:00:00 2001 From: Michel Davit Date: Mon, 13 Jan 2025 14:56:36 +0100 Subject: [PATCH] Use ChildFirstClassLoader When building with multiple avro versions, the default URLClassLoader may load a wrong resource file. Use a custom ChildFirstClassLoader to avoid this issue. Also set nop slf4j logger for avro-compiler classpath. --- build.sbt | 2 +- .../sbt/avro/ChildFirstClassLoader.java | 85 +++++++++++++++++++ .../scala/com/github/sbt/avro/SbtAvro.scala | 11 ++- .../sbt-test/sbt-avro/avscparser/build.sbt | 1 + .../sbt-test/sbt-avro/publishing/build.sbt | 2 +- 5 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 plugin/src/main/java/com/github/sbt/avro/ChildFirstClassLoader.java diff --git a/build.sbt b/build.sbt index 7bd4c33..065ee4c 100644 --- a/build.sbt +++ b/build.sbt @@ -84,7 +84,7 @@ lazy val `sbt-avro-compiler-api`: Project = project lazy val `sbt-avro-compiler-bridge`: Project = project .in(file("bridge")) - .dependsOn(`sbt-avro-compiler-api`) + .dependsOn(`sbt-avro-compiler-api` % "provided") .settings( crossPaths := false, autoScalaLibrary := false, diff --git a/plugin/src/main/java/com/github/sbt/avro/ChildFirstClassLoader.java b/plugin/src/main/java/com/github/sbt/avro/ChildFirstClassLoader.java new file mode 100644 index 0000000..8ae3797 --- /dev/null +++ b/plugin/src/main/java/com/github/sbt/avro/ChildFirstClassLoader.java @@ -0,0 +1,85 @@ +package com.github.sbt.avro; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * An almost trivial no-fuss implementation of a class loader + * following the child-first delegation model. + * + * @author Ceki Gulcu + */ +public class ChildFirstClassLoader extends URLClassLoader { + + public ChildFirstClassLoader(URL[] urls) { + super(urls); + } + + public ChildFirstClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + public void addURL(URL url) { + super.addURL(url); + } + + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + /** + * We override the parent-first behavior established by + * java.lang.Classloader. + *

+ * The implementation is surprisingly straightforward. + */ + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + + //System.out.println("ChildFirstClassLoader("+name+", "+resolve+")"); + + // First, check if the class has already been loaded + Class c = findLoadedClass(name); + + // if not loaded, search the local (child) resources + if (c == null) { + try { + c = findClass(name); + } catch(ClassNotFoundException cnfe) { + // ignore + } catch(SecurityException se) { + // ignore + } + } + + // if we could not find it, delegate to parent + // Note that we don't attempt to catch any ClassNotFoundException + if (c == null) { + if (getParent() != null) { + c = getParent().loadClass(name); + } else { + c = getSystemClassLoader().loadClass(name); + } + } + + if (resolve) { + resolveClass(c); + } + + return c; + } + + /** + * Override the parent-first resource loading model established by + * java.lang.Classloader with child-first behavior. + */ + public URL getResource(String name) { + URL url = findResource(name); + + // if local search failed, delegate to parent + if(url == null) { + url = getParent().getResource(name); + } + return url; + } +} diff --git a/plugin/src/main/scala/com/github/sbt/avro/SbtAvro.scala b/plugin/src/main/scala/com/github/sbt/avro/SbtAvro.scala index 7c0dfb3..bc43af3 100644 --- a/plugin/src/main/scala/com/github/sbt/avro/SbtAvro.scala +++ b/plugin/src/main/scala/com/github/sbt/avro/SbtAvro.scala @@ -7,14 +7,10 @@ import PluginCompat.* import sbt.librarymanagement.DependencyFilter import java.io.File -import java.net.URLClassLoader /** Plugin for generating the Java sources for Avro schemas and protocols. */ object SbtAvro extends AutoPlugin { - // Force Log4J to not use JMX to avoid duplicate mbeans registration due to multiple classloader - sys.props("log4j2.disableJmx") = "true" - val AvroCompiler: Configuration = config("avro-compiler") val Avro: Configuration = config("avro") val AvroTest: Configuration = config("avro-test") @@ -73,9 +69,12 @@ object SbtAvro extends AutoPlugin { ivyConfigurations ++= Seq(AvroCompiler, Avro, AvroTest), avroVersion := "1.12.0", avroAdditionalDependencies := Seq( + // disable slf4j logging in avro-compiler child 1st classloader + "org.slf4j" % "slf4j-api" % "2.0.16" % AvroCompiler, + "org.slf4j" % "slf4j-nop" % "2.0.16" % AvroCompiler, "com.github.sbt" % "sbt-avro-compiler-bridge" % BuildInfo.version % AvroCompiler, "org.apache.avro" % "avro-compiler" % avroVersion.value % AvroCompiler, - "org.apache.avro" % "avro" % avroVersion.value + "org.apache.avro" % "avro" % avroVersion.value, ), libraryDependencies ++= avroAdditionalDependencies.value ) @@ -245,7 +244,7 @@ object SbtAvro extends AutoPlugin { // - output files are missing // TODO Cache class loader - val avroClassLoader = new URLClassLoader( + val avroClassLoader = new ChildFirstClassLoader( (AvroCompiler / dependencyClasspath).value .map(toNioPath) .map(_.toUri.toURL) diff --git a/plugin/src/sbt-test/sbt-avro/avscparser/build.sbt b/plugin/src/sbt-test/sbt-avro/avscparser/build.sbt index bb43bf9..c8ea29b 100644 --- a/plugin/src/sbt-test/sbt-avro/avscparser/build.sbt +++ b/plugin/src/sbt-test/sbt-avro/avscparser/build.sbt @@ -4,6 +4,7 @@ lazy val parser = project crossPaths := false, autoScalaLibrary := false, libraryDependencies ++= Seq( + "com.github.sbt" % "sbt-avro-compiler-api" % sys.props("plugin.version") % "provided", "com.github.sbt" % "sbt-avro-compiler-bridge" % sys.props("plugin.version"), "org.apache.avro" % "avro-compiler" % "1.12.0" ) diff --git a/plugin/src/sbt-test/sbt-avro/publishing/build.sbt b/plugin/src/sbt-test/sbt-avro/publishing/build.sbt index 33dd75f..8d78863 100644 --- a/plugin/src/sbt-test/sbt-avro/publishing/build.sbt +++ b/plugin/src/sbt-test/sbt-avro/publishing/build.sbt @@ -41,7 +41,7 @@ lazy val `transitive`: Project = project libraryDependencies ++= Seq( // when using avro scope, it won't be part of the pom dependencies -> intransitive // to declare transitive dependency use the compile scope - "com.github.sbt" % "external" % "0.0.1-SNAPSHOT" classifier "avro" + ("com.github.sbt" % "external" % "0.0.1-SNAPSHOT").classifier("avro") ), Compile / avroDependencyIncludeFilter := artifactFilter(classifier = "avro"), // create a test jar with a schema as resource