Reflection and properties, checking for custom getters/setters

Hi all,

So I’m using reflection to scrape classes for annotated properties, which I then use to inject automatically loaded resources. This seems to be working well, but I have a question about getters and setters in this context: Is there a way to determine if custom getter/setters exist on property? I have a few use cases:

The first is for the sake of robust warnings. E.g. if the annotation allows the property to be mutated, but only if the user “promises” to make a callback, then I’d like to throw an exception if no custom setter is defined at all. It’s not perfect, but I believe this would capture the majority of cases someone declares a property as var but forgets to make the callback.

The second case is related to optimization. I’d actually like to use the javaField property directly when possible, because it exposes get/set methods that don’t allocate vararg arrays; however, whether it’s null is predicated only on the existence of a backing field, which can be true regardless of whether a custom getter/setter is defined. Hence the desire for a direct check.

Thoughts? I’m trying to minimize allocations as much as possible here - both to minimize GC overhead and maintain cache coherency, if at all possible. I can tolerate the reflective aspect the few times it is invoked, but I’d like to avoid having wrapper classes everywhere just to make an optional callback on mutations. Annotations, reflection and LOTS of documentation seemed like a decent way of going about this, but there are some pitfalls like I’ve described here.

Let’s use this for experimenting:

class Test {
    val foo: String = "foo"
    val bar: String get() = "bar"
    val baz: String = "baz"
        get() = field
}

foo has a default implementation that gets a backing field. bar has a custom implementation without a backing field. baz has a custom implementation that uses a backing field.

We can very easily verify whether some property is backed by a field:

println("foo.javaField: " + Test::foo.javaField) // not null
println("bar.javaField: " + Test::bar.javaField) // null
println("baz.javaField: " + Test::baz.javaField) // not null

What about foo vs baz? Well, if we look into the resulting bytecode we’ll see they’re actually identical. I believe there is nothing in the bytecode that could help distinguish them. Nothing, except one thing.

Fortunately, Kotlin compiler puts a lot of extra info about the compiled code into auto-generated @Metadata annotations. These annotations contain information like e.g. whether a class is a data class or… whether a property getter has default or custom implementation.

As data contained in @Metadata is encoded in a binary form, you need to use kotlinx-metadata library to read it. There are dragons ahead though, I think it works with JVM only and it is pretty much experimental. I was able to acquire the data we need with the following code:

fun main() {
    val meta = Test::class.java.getKotlinMetadata()
    val kmClass = (meta as KotlinClassMetadata.Class).toKmClass()

    kmClass.properties.forEach { prop ->
        println("name: " + prop.name)
        println("getter.IS_NOT_DEFAULT: " + Flag.PropertyAccessor.IS_NOT_DEFAULT(prop.getterFlags))
    }
}

fun Class<*>.getKotlinMetadata(): KotlinClassMetadata? {
    val meta = getAnnotation(Metadata::class.java) ?: return null
    val header = KotlinClassHeader(
        kind = meta.kind,
        metadataVersion = meta.metadataVersion,
        data1 = meta.data1,
        data2 = meta.data2,
        extraString = meta.extraString,
        packageName = meta.packageName,
        extraInt = meta.extraInt,
    )
    return KotlinClassMetadata.read(header)
}

Output:

name: bar
getter.IS_NOT_DEFAULT: true
name: baz
getter.IS_NOT_DEFAULT: true
name: foo
getter.IS_NOT_DEFAULT: false

There is quite a lot of boilerplate code above, especially converting Metadata object to KotlinClassHeader. This is pretty weird to me, but I didn’t find a straightforward way to just load @Metadata of a class. Maybe I miss something.

2 Likes

Wow, this is probably exactly what I needed, thanks! I’ll have to do some research, particularly on the JVM-only front, but if the library isn’t big I think this will do nicely.

1 Like