How to create a parent-last classloader hierarchy?

145 Views Asked by At

I've 3 class loaders:

  1. MainLoader
  2. PreloadingLoader
  3. GameSceneLoader

There's only one instance of MainLoader throughout execution of a program, but PreloadingLoader and GameSceneLoader can be recreated on demand.

When I load any class in my program, I want to:

  • if the class name begins with any of staticClasses load it with MainLoader
  • if the class name begins with any of preloadingClasses load it with PreloadingLoader
  • if the class name beings with pl.gieted.flappy_bird but isn't listed on neither of above lists, load it with GameSceneLoader
  • otherwise load it with a default class loader

This below code works, but only for the first class loaded, e.g:

  1. pl.gieted.flappy_bird.engine.Renderer is requested by GameSceneLoader
  2. MainLoader tries to load it, because it's the oldest parent of GameSceneLoader
  3. The Renderer has a class dependency of LoadingScene
  4. Since Renderer was loaded using MainLoader, the Loading Scene is also being loaded using MainLoader, however it can't find it.
  5. java.lang.NoClassDefFoundError is thrown.

What I want to happen instead is:

  1. pl.gieted.flappy_bird.engine.Renderer is requested by GameSceneLoader
  2. MainLoader tries to load it, because it's the oldest parent of GameSceneLoader
  3. The Renderer has a class dependency of LoadingScene
  4. Loading of LoadingScene is passed back to GameSceneLoader
  5. MainLoader cannot find it.
  6. PreloadingLoader finds it and loads it
  7. Loading continues...
val mainClassLoader = object : URLClassLoader(arrayOf(File(classesUrl).toURI().toURL()), null) {

    val staticClasses = listOf(
        "pl.gieted.flappy_bird.engine.Renderer",
        "pl.gieted.flappy_bird.engine.Processing",
        "pl.gieted.flappy_bird.engine.Scene",
        "pl.gieted.flappy_bird.engine.LifecycleElement",
    )
    
    override fun findClass(name: String): Class<*>? {
        return when {
            staticClasses.any { name.startsWith(it) } -> super.findClass(name)
            name.startsWith("pl.gieted.flappy_bird") -> null
            else -> this::class.java.classLoader.loadClass(name)
        }
    }
}

var preloadingLoader = object : URLClassLoader(arrayOf(File(classesUrl).toURI().toURL()), mainClassLoader) {

    val preloadingClasses = listOf(
        "pl.gieted.flappy_bird.game.LoadingScene",
        "pl.gieted.flappy_bird.game.FlappyBirdResourceLoader",
        "pl.gieted.flappy_bird.game.Resources",
    )
    
    override fun findClass(name: String): Class<*>? {
        return when {
            preloadingClasses.any { name.startsWith(it) } -> super.findClass(name)
            else -> null
        }
    }
}

var gameSceneLoader = URLClassLoader(arrayOf(File(classesUrl).toURI().toURL()), preloadingLoader)

val rendererClass = gameSceneLoader.loadClass("pl.gieted.flappy_bird.engine.Renderer")

How to achieve such a thing?

The examples are written in Kotlin, however you can answer me in Java without any problems.

1

There are 1 best solutions below

0
On BEST ANSWER

I've ended up with creating such class loader like this:

object MainClassLoader : ClassLoader() {
    private class MyClassLoader : URLClassLoader(
        listOf(classesUrl, resourcesUrl).map { File(it).toURI().toURL() }.toTypedArray(), null
    ) {

        override fun loadClass(name: String?, resolve: Boolean): Class<*> = MainClassLoader.loadClass(name)

        fun actuallyLoad(name: String): Class<*> = super.loadClass(name, false)
    }

    private val staticClassLoader = MyClassLoader()
    private var preloadingLoader = MyClassLoader()
    private var gameSceneLoader = MyClassLoader()

    private val staticClasses = listOf(
        "pl.gieted.flappy_bird.engine.Renderer",
        "pl.gieted.flappy_bird.engine.Processing",
        "pl.gieted.flappy_bird.engine.Scene",
        "pl.gieted.flappy_bird.engine.LifecycleElement",
        
        "pl.gieted.flappy_bird.engine.Object",
        "pl.gieted.flappy_bird.engine.Vector2",
        "pl.gieted.flappy_bird.engine.Sound",
        "pl.gieted.flappy_bird.engine.Camera",
        "pl.gieted.flappy_bird.engine.Bounds",
    )

    private val preloadingClasses = listOf(
        "pl.gieted.flappy_bird.game.LoadingScene",
        "pl.gieted.flappy_bird.game.FlappyBirdResourceLoader",
        "pl.gieted.flappy_bird.game.Resources",
        "pl.gieted.flappy_bird.game.objects.Bird\$Color"
    )

    override fun loadClass(name: String, resolve: Boolean): Class<*> = when {
        staticClasses.any { name.startsWith(it) } -> staticClassLoader.actuallyLoad(name)
        preloadingClasses.any { name.startsWith(it) } -> preloadingLoader.actuallyLoad(name)
        name.startsWith("pl.gieted.flappy_bird") -> gameSceneLoader.actuallyLoad(name)
        else -> MainClassLoader::class.java.classLoader.loadClass(name)
    }

    fun newPreloading() {
        preloadingLoader = MyClassLoader()
    }

    fun newGameScene() {
        gameSceneLoader = MyClassLoader()
    }
}

The whole trick is creating an extra actuallyLoad() function, that actually loads the class and delegating all loadClass() calls back to your "router".