I am creating a library that scans various annotations and takes actions based on them which is a standard use case of KSP (Kotlin Symbol Processing).
The non-standard thing is the fact that the user of the library can affect the result of execution by providing the path to an object taking the corresponding action (implementing the defined interface) within used annotation.
In such case, I would like to retrieve an instance of such an object and call the corresponding method from it. Unfortunately, I fail to resolve the instance of this class - the ClassNotFoundException
is thrown (despite the fact that Resolver.getClassDeclarationByName() works correctly i.e.: it finds the class given its qualified name)
Here is a simplified example, presenting the problem:
package com.example.greeter.processor
import com.google.devtools.ksp.*
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
annotation class Greet(val greeterName: String)
interface Greeter {
fun greet(logger: KSPLogger)
}
class GreeterProcessor(
val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver) = GreeterProcessorContext(logger, resolver).process()
private class GreeterProcessorContext(val logger: KSPLogger,
val resolver: Resolver) {
fun process(): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(Greet::class.qualifiedName!!)
val ret = symbols.filter { !it.validate() }.toList()
symbols
.filter { it is KSClassDeclaration && it.validate() }
.forEach { it.accept(GreeterVisitor(), Unit) }
return ret
}
inner class GreeterVisitor : KSVisitorVoid() {
@OptIn(KspExperimental::class)
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
val greeterName = classDeclaration.getAnnotationsByType(Greet::class).first().greeterName
val correspondingGreeterClassDeclaration = resolver.getClassDeclarationByName(resolver.getKSNameFromString(greeterName))
if (correspondingGreeterClassDeclaration == null) {
logger.error("No greeter of name $greeterName for class ${classDeclaration.qualifiedName}", classDeclaration)
return
}
val classDeclarationName = classDeclaration.simpleName.asString()
logger.info("Corresponding greeter class declaration for $classDeclarationName: ${correspondingGreeterClassDeclaration.simpleName.asString()} - using getClassDeclarationByName") //<=== This works fine
val greeterClass = Class.forName(greeterName) //<=== Throws java.lang.ClassNotFoundException
logger.info("Corresponding greeter class declaration for $classDeclarationName: ${greeterClass.canonicalName} - using Class.forName()")
val greeterInstance = greeterClass.kotlin.objectInstance!! as Greeter
greeterInstance.greet(logger)
}
}
}
}
package com.example.greeter.implementation
import com.example.greeter.processor.*
import com.google.devtools.ksp.processing.KSPLogger
@Greet("com.example.greeter.implementation.GreeterImpl")
object GreeterTest
object GreeterImpl : Greeter {
override fun greet(logger: KSPLogger) {
logger.info("Hello from ${javaClass.simpleName}!")
}
}
As you can see, it succeeded in retrieving the class declaration from KSP, but failed to obtain an instance of the class. I am aware that KSP processes classes before they are compiled, but for already compiled classes it should be possible to retrieve such an instance. The following lines of library class code describe the problem well
logger.info("Corresponding greeter class declaration for $classDeclarationName: ${correspondingGreeterClassDeclaration.simpleName.asString()} - using getClassDeclarationByName") //<=== This works fine
val greeterClass = Class.forName(greeterName) //<=== Throws java.lang.ClassNotFoundException
logger.info("Corresponding greeter class declaration for $classDeclarationName: ${greeterClass.canonicalName} - using Class.forName()")
val greeterInstance = greeterClass.kotlin.objectInstance!! as Greeter
greeterInstance.greet(logger)
The expected result is to call the “greet” method from GreeterImpl. What can I do to solve my problem? The expected result is that the line
val greeterClass = Class.forName(greeterName) //<=== Throws java.lang.ClassNotFoundException
instead of throwing an exception, returns an instance of the class object, on which we then execute the greet
method.
An example ready to reproduce can be found here on my Github.
Thank you very much in advance for your help!
Your KSP processor program fails to access the type GreeterImpl
because that class is not on the classpath of the KSP processor program. Getting that class on the classpath requires its code to have been first compiled to a .class
file.
However, as you have written it, the KSP processor program is part of that same compilation process for GreeterImpl
. So the overall build is circular and it's not going to work without some rethinking.
If it works for your design, what you can do is put GreeterImpl
into another module so that it is compiled separately. Then that module/JAR can be passed to the KSP processor program as a dependency (perhaps as an implementation
dependency in that program's Gradle build, if it is known at that time, or perhaps loaded at runtime via a URLClassloader
from an argument passed to the processor).
I imagine there are other possibilities depending on the wider objectives and circumstances of what you are trying to do.