Skip to content

Commit

Permalink
Use ChildFirstClassLoader
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RustedBones committed Jan 13, 2025
1 parent 46f5124 commit bba598d
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 7 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="http://www.qos.ch/log4j/">Ceki Gulcu</a>
*/
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.
* <p>
* 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;
}
}
9 changes: 4 additions & 5 deletions plugin/src/main/scala/com/github/sbt/avro/SbtAvro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -73,6 +69,9 @@ 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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions plugin/src/sbt-test/sbt-avro/avscparser/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 1 addition & 1 deletion plugin/src/sbt-test/sbt-avro/publishing/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit bba598d

Please sign in to comment.