javaclassloaderdynamic-class-loadersclassloading

How to create a parent-last classloader hierarchy?


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:


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.


Solution

  • 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".