Skip to content

I tried to set enum field by reflection

enum class Command(private vararg val commands: String) {

  @FieldEnrich("changeLanguages", "commands")
  CHANGE_LANGUAGE;
}

enum class CreatingCarStep(private vararg val values: String) : AbstractStep<CreatingCarStep> {
  @FieldEnrich("brand-name", "values")
  BRAND_NAME {
    override fun next() = MODEL
    override fun previous() = BRAND_NAME
  };
}

I have two enums with vararg property and @FieldEnrich annotation.

Annotation looks like

@Target(FIELD, CLASS)
@Retention(RUNTIME)
annotation class FieldEnrich(
  val property: String,
  val field: String
)

Annotation is processing by object

object FieldEnricher {

  lateinit var configurationProvider: ConfigurationProvider

  fun enrichClass(clazz: Class<*>) {
    clazz.enumConstants.forEach {
      enrichField(it, clazz)
    }
  }

  private fun enrichField(enum: Any, clazz: Class<*>) {
    val enumClass = enum::class.java
    if (enumClass.isAnnotationPresent(FieldEnrich::class.java)) {
      val fieldEnrich = enumClass.getAnnotation(FieldEnrich::class.java)
      val values = configurationProvider.getProperty(fieldEnrich.property, Array<String>::class.java)
      val field = clazz.declaredFields.firstOrNull { it.name == fieldEnrich.field }
      field?.isAccessible = true
      field?.set(enum, values)
    }
  }
}

The logic is as follows. We annotate an enum member with the annotation @FieldEnrich and pass the property we would like to read value from and the name of the field to which we set the value of the property.

I was debugging and found out when it was trying to process CreatingCarStep enum is okay, because the enumConstants method returns actual objects of the enum values. So i can just take the class of this value and get the actual class of this enum and process it by my method enrichField. But when it was trying to process Command enum, I got just enum values. So if we take the class of enum value, the same class of Command will be returned. Enter the image description here.

Command image -> enter image description here

CreatingCarStep image -> enter image description here

Answer

It works for CreatingCarStep because its enum constants have a non-empty body. This forces the kotlin compiler to create subclasses of the enum class for each of the enum constants. Additionally, the annotations on the enum constant will be put on the generated subclass. Therefore, enum::class.java evaluates to a Class<*> instance that represents a subclass of CreatingCarStep that has a FieldEnrich annotation.

When the enum constants have an empty body, or do not have a body at all, no subclasses are generated. The enum constants are instances of the enum class itself. Hence, enum::class.java evaluates to Command::class.java, which has no FieldEnrich annotation.

Rather than getting clazz.enumConstants and getting the annotations on their ::class.java, you should get the annotations on the enum constant fields. This way, you do not depend on whether or not the enum constants have empty bodies.

fun enrichClass(clazz: Class<*>) {
    clazz.declaredFields.forEach {
        if (it.isEnumConstant) {
            enrichField(it, clazz)
        }
    }
}

private fun enrichField(enumField: Field, clazz: Class<*>) {
    if (enumField.isAnnotationPresent(FieldEnrich::class.java)) {
        val fieldEnrich = enumField.getAnnotation(FieldEnrich::class.java)
        val values = configurationProvider.getProperty(fieldEnrich.property, Array<String>::class.java)
        val field = clazz.declaredFields.firstOrNull { it.name == fieldEnrich.field }
        field?.isAccessible = true
        field?.set(enumField.get(null), values)
    }
}