Created
May 7, 2024 07:22
-
-
Save erikvanoosten/4b498eac3d00d33cf17ceafe68af0ea5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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