Skip to content

Instantly share code, notes, and snippets.

@erikvanoosten
Created May 7, 2024 07:22
Show Gist options
  • Save erikvanoosten/4b498eac3d00d33cf17ceafe68af0ea5 to your computer and use it in GitHub Desktop.
Save erikvanoosten/4b498eac3d00d33cf17ceafe68af0ea5 to your computer and use it in GitHub Desktop.
package com.adevinta.blog.pluginengine.pluginapi
trait ConverterPlugin {
def name: String
def init(): Unit
def makeConverter(specification: String): Converter
}
trait Converter {
def convert(in: String): String
}
package com.adevinta.blog.pluginengine.core
import java.net.{URL, URLClassLoader}
import java.util
import java.util.ServiceLoader
import scala.jdk.CollectionConverters.IteratorHasAsScala
object IsolatingClassLoader {
def apply(jarUrls: Array[URL], parent: ClassLoader): IsolatingClassLoader = new IsolatingClassLoader(jarUrls, parent)
// White listed package prefixes, only classes from these packages may be loaded from the parent class loader.
private val PackagePrefixes = Array("java.", "scala.", "jdk.", "com.adevinta.blog.pluginengine.pluginapi.")
private val ResourcePrefixes = PackagePrefixes.map(_.replace('.', '/'))
}
final class IsolatingClassLoader private (jarUrls: Array[URL], parent: ClassLoader) extends URLClassLoader(jarUrls, parent) {
import IsolatingClassLoader._
def loadPlugin(pluginName: String): ConverterPlugin = {
val allConverterPlugins = WithClassLoader.withClassLoader(this) {
ServiceLoader
.load[ConverterPlugin](classOf[ConverterPlugin], this)
.iterator()
.asScala
.toSeq
}
val plugin = allConverterPlugins
.find(plugin => WithClassLoader.withClassLoader(this)(plugin.name) == pluginName)
.getOrElse(throw new IllegalArgumentException(s"No converter plugin found for name $pluginName"))
WithContextClassLoaderConverterPlugin(plugin, this)
}
override def loadClass(name: String, resolve: Boolean): Class[_] = {
getClassLoadingLock(name).synchronized {
var c = findLoadedClass(name)
// Check if the class has already been loaded
if (c == null) {
if (PackagePrefixes.exists(name.startsWith)) {
c = parent.loadClass(name)
} else {
// Load here in this class loader.
c = findClass(name)
}
}
if (resolve) resolveClass(c)
c
}
}
override def getResource(name: String): URL = {
if (ResourcePrefixes.exists(name.startsWith)) parent.getResource(name)
else findResource(name)
}
override def getResources(name: String): util.Enumeration[URL] = {
if (ResourcePrefixes.exists(name.startsWith)) parent.getResources(name)
else findResources(name)
}
override def getPackages: Array[Package] = {
getDefinedPackages ++ parent.getDefinedPackages
.filter(p => PackagePrefixes.exists(p.getName.startsWith))
}
}
/** A ConverterPlugin that wraps another ConverterPlugin, using the given class loader as the thread's
* context class loader for all its invocations.
*/
final private case class WithContextClassLoaderConverterPlugin(plugin: ConverterPlugin, classLoader: IsolatingClassLoader) extends ConverterPlugin {
@inline private def withClassLoader[A](task: => A): A = WithClassLoader.withClassLoader(classLoader)(task)
override def name: String = withClassLoader(plugin.name)
override def init(): Unit = withClassLoader(plugin.init())
override def makeConverter(specification: String): Converter = {
val wrapped = withClassLoader(plugin.makeConverter(specification))
new Converter {
override def convert(in: String): String = withClassLoader(wrapped.convert(in))
}
}
}
object WithClassLoader {
/** Runs `task` with the given `classLoader` as the thread's context class loader, and restores the current
* context class loader afterwards.
*/
def withClassLoader[A](classLoader: ClassLoader)(task: => A): A = {
val currentThread = Thread.currentThread()
val previous = currentThread.getContextClassLoader
currentThread.setContextClassLoader(classLoader)
try {
task
} finally {
currentThread.setContextClassLoader(previous)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment