Assign value to final variable in Kotlin and bitwise operator

주의!

이 코드를 프로덕션 용으로 사용하면 차후 인수인계 맡는 사람에게 맞을지도 모릅니다.

도입

일반적 상식으로는 final 변경자가 달린 변수는 값을 설정할 수 없다.

하지만 Reflection 을 잘 활용하면 final 변경자가 달린 변수에 멀쩡하게 값을 설정할 수 있다.

이 과정을 위해 필요한 것은 다음과 같다.

  • 해당 기능을 실행할 클래스의 인스턴스 (this)
  • 해당 필드 이름
  • 설정할 값

필드 검색

val field = findField(receiver.javaClass, fieldName) ?: return

private fun findField(cls: Class<*>, fieldName: String): Field? {
        // Locate the field going back to parent.
        // for extension uses.
        // 확장성을 위해 부모 클래스까지 거슬러 올라가 필드를 찾음
        var targetClass = cls
        while (true) {
            try {
                val field = targetClass.getDeclaredField(fieldName)
                // getDeclaredField checks for non-public scopes as well and it returns accurate results
                // public 범위가 아닌 declaredField를 체크하고, 정확한 결과를 반환한다.
                if (!Modifier.isPublic(field.modifiers)) {
                    field.isAccessible = true
                }
                return field
            } catch (ex: NoSuchFieldException) {
                // ignored
            }

            targetClass = cls.superclass
        }
}

이미 필드 이름이 주어져 있으므로, 해당 클래스의 Class 객체에서 getDeclaredField 를 사용하면 된다.

만일 필드 이름이 public 가 아닌 경우에는 accessible 를 설정한다.

Final 제거

private fun removeFinalModifierAndSet(receiver: Any, field: Field, value: Any) {
        try {
            if (Modifier.isFinal(field.modifiers)) {
                val modifiersField = Field::class.java.getDeclaredField("modifiers")
                if (!modifiersField.isAccessible) {
                    modifiersField.isAccessible = true
                }

                try {
                    modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
                } finally {
                    if (!modifiersField.isAccessible) {
                        modifiersField.isAccessible = false
                    }
                }
            }
        } catch (ex: NoSuchFieldException) {
            // ignored
        } catch (ex: IllegalAccessException) {
            // ignored
        }
    }

Field 클래스에 선언된  modifiers 라는 필드가 public 가 아닐 경우 다시 한번 accessible를 설정하고, field.modifiers & ~Modifier.FINAL 를 적용한다.

여기서 사용된 비트 연산자는 & 연산자, ~ 연산자가 있는데, &는 그렇다 쳐도 ~는 잘 모르고 사용했던 기억이 있기 때문에 한번 정리해보려고 한다.

~ 연산자란?

~ 연산자, 비트 연산자, Bitwise operator라고 불리는 이 연산자는 bit 값이 1일 경우 0, 0일 경우 1로 하는 연산자이다.  Kotlin에서는 inv() 메서드로 사용된다.

예를 들자면, 큿 의 사이즈를 바이너리 값으로 표현해보자.

72 = 01001000

그렇다면, 이 72에 ~를 적용해보자.

~72 = 10110111 = 183

이런 식으로 비트 단위로 직접 반대로 설정하게 하여 Final 값만 제거하고 설정하는 것이다.

값 설정

if (Modifier.isPrivate(field.modifiers)) {
    field.isAccessible = true
}

try {
    field.set(receiver, value)
} finally {
    if (Modifier.isPrivate(field.modifiers)) {
        field.isAccessible = false
    }
}

다시 한번, field 가 private 면 accessible를 true로 설정하고, 해당 필드에 value를 적용한다.

마무리

전체 코드는 다음과 같다.

package pyxis.uzuki.live.attribute.parser

import java.lang.reflect.Field
import java.lang.reflect.Modifier


/**
 * FieldModifier will execute extremely evil things...
 *
 * 1. Find fields in Class<*> (in demo, it will be StyleViewAttributes)
 * 2. remove final modifier of field
 * 3. write value into field
 *
 *
 * reference: https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/reflect/FieldUtils.java
 */
object FieldModifier {

    @JvmStatic
    fun process(receiver: Any, fieldName: String, value: Any) {
        // It can be nullable cause field doesn't exists.
        // for extension uses.
        val field = findField(receiver.javaClass, fieldName) ?: return
        removeFinalModifierAndSet(receiver, field, value)
    }

    private fun findField(cls: Class<*>, fieldName: String): Field? {
        // Locate the field going back to parent.
        // for extension uses.
        var targetClass = cls
        while (true) {
            try {
                val field = targetClass.getDeclaredField(fieldName)
                // getDeclaredField checks for non-public scopes as well and it returns accurate results
                if (!Modifier.isPublic(field.modifiers)) {
                    field.isAccessible = true
                }
                return field
            } catch (ex: NoSuchFieldException) {
                // ignored
            }

            targetClass = cls.superclass
        }
    }

    private fun removeFinalModifierAndSet(receiver: Any, field: Field, value: Any) {
        try {
            if (Modifier.isFinal(field.modifiers)) {
                val modifiersField = Field::class.java.getDeclaredField("modifiers")
                if (!modifiersField.isAccessible) {
                    modifiersField.isAccessible = true
                }

                try {
                    modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
                } finally {
                    if (!modifiersField.isAccessible) {
                        modifiersField.isAccessible = false
                    }
                }
            }

            if (Modifier.isPrivate(field.modifiers)) {
                field.isAccessible = true
            }

            try {
                field.set(receiver, value)
            } finally {
                if (Modifier.isPrivate(field.modifiers)) {
                    field.isAccessible = false
                }
            }
        } catch (ex: NoSuchFieldException) {
            // ignored
        } catch (ex: IllegalAccessException) {
            // ignored
        }
    }
}

사용할 때에는 FieldModifier.process(this, "test", "abc"); 이렇게 사용하면 된다.

물론, 실제로 사용할 일이 없어야 하는 것은 맞지만서도(..)

참고 링크

https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/reflect/FieldUtils.java

Injection by Annotation in Kotlin :: parse AttributeSet

뭔가 엄청 오랜만의 포스팅인 것 같다.

1월을 마무리 하기에 좋은 포스팅은, 역시 코드 포스팅이다.

그래서, 오늘은 리플렉션 기술을 이용한 Annotation 파싱을 해보려 한다.


도입

최근 회사에서 작업할때나, 개인적으로 작업할때나 커스텀 뷰를 만들어서 사용할 때가 매우 많아졌다.

공통 요소를 한 파일에 구현하고, 사용할 화면에 추가만 해주면 되기 때문이다.

이 5개가 전부 커스텀 뷰를 구현해놓은 라이브러리고, 여러 프로젝트에서 아주 잘 쓰이고 있다.

보통 커스텀 뷰를 만들 때 XML에 app:**** 라는 속성을 붙이는데 사용자가 작성한 속성을 코드에 가져오려면 Attributes 란 인터페이스를 이용해 TypedArray를 가져오고, 다음과 같은 코드를 구현해야 한다.

val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CombinedCheckBox) ?: return

textPrimary = typedArray.getString(R.styleable.CombinedCheckBox_textPrimary)
textSecondary = typedArray.getString(R.styleable.CombinedCheckBox_textSecondary)
textPrimaryColor = typedArray.getColor(R.styleable.CombinedCheckBox_textPrimaryColor, Color.BLACK)
textSecondaryColor = typedArray.getColor(R.styleable.CombinedCheckBox_textSecondaryColor, Color.BLACK)
textPrimarySize = typedArray.getDimension(R.styleable.CombinedCheckBox_textPrimarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textSecondarySize = typedArray.getDimension(R.styleable.CombinedCheckBox_textSecondarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textExtraSpace = typedArray.getInt(R.styleable.CombinedCheckBox_textExtraSpace, 1)
fontPrimaryText = typedArray.getString(R.styleable.CombinedCheckBox_fontPrimaryText)
fontSecondaryText = typedArray.getString(R.styleable.CombinedCheckBox_fontSecondaryText)
textPrimaryStyle = typedArray.getInt(R.styleable.CombinedCheckBox_textPrimaryStyle, 0)
textSecondaryStyle = typedArray.getInt(R.styleable.CombinedCheckBox_textSecondaryStyle, 0)

typedArray.recycle()

딱 봐도 엄청 지루한 작업이다.

이걸 어노테이션을 이용해 자동으로 파싱하게 한다면 어떨까? 란 생각을 떠올렸고, 그것이 오늘 다뤄볼 Injection by Annotation 이다.

설계

위 코드를 분석해보면, TypedArray로부터 값을 추출하려면 R.styleable ~ 로 되는 index값(int), 해당 속성의 포맷, 그리고 기본값이 있다.

그리고 당연히 Annotation은 필드만 적용 가능하게 해야된다는 걸 생각해보면, 아래와 같이 정리할 수 있다.

  • 적용 대상(AnnotationTarget) -> 필드(FIELD)
  • 주 파라미터(value) -> index값
  • 부 파라미터 1 -> enum 값, String, Color, Dimension 등…
  • 부 파라미터 2 -> int 값

그리고 이를 코드로 작성해보자.

enum class AttrType {
    Boolean, Color, Dimension, DimensionPixelSize, Drawable, Float, Int, Integer, ResourceId, String
}

@Target(AnnotationTarget.FIELD)
annotation class BindAttr(val value: Int, val type: AttrType = AttrType.Int, val defValue: Int = 0)

AttrType 란 enum 클래스에 각 속성을 넣고, annotation 클래스를 구현한다.

타겟은 FIELD로, 주 파라미터는 index값으로 value란 이름을, type와 defValue는 각각 기본값으로 Int와 0을 집어넣었다.

실제 사용 예제는 다음과 같아진다.

textExtraSpace = typedArray.getInt(R.styleable.CombinedCheckBox_textExtraSpace, 1)
fontPrimaryText = typedArray.getString(R.styleable.CombinedCheckBox_fontPrimaryText)

->>

@BindAttr(R.styleable.CombinedCheckBox_textExtraSpace)
private int mTextExtraSpace;

@BindAttr(value = R.styleable.CombinedCheckBox_fontPrimaryText, type = AttrType.String)
private String mFontPrimaryText;

어노테이션이 적용된 필드 추출

그다음 해야될 것은, 액티비티나 뷰 로부터 클래스 객체와 TypedArray 객체를 받는 것이다.

fun inject(array: TypedArray, receiver: Any) {

}

그 다음으론 receiver 의 javaClass 를 받는다.

var cls = receiver.javaClass

receiver.javaClass 에서 receiver 는 Any, 즉 어떤 타입도 되지만 javaClass 를 뒤에 붙여주면 해당 코드를 실행한 클래스의 객체가 반환된다.

만일 TitleView 에서 불렀다면 Class<TitleView> 가 반환된다.

그 다음, do-while 문을 걸어서 해당 클래스의 선언된 필드를 가지고 오자.

do {
    cls.declaredFields.filter { it.declaredAnnotations.isNotEmpty() }.forEach { field ->
                    
    }

    try {
        cls = cls.getSuperclass()
    } catch (e: Exception) {
        cls = null
    }

} while (cls != null)

선언된 필드들을 가져올 때 어노테이션이 붙지 않은 필드는 필요 없으므로 미리 리스트에서 제외시키고, forEach 메서드를 호출하게 한다.

여기서 forEach로 오는 파라미터는 1개이니 it(implicit parameter) 를 사용할 수는 있지만, 필드에서 어노테이션 리스트를 가져와 forEach를 사용해야 하니 여기서는 파라미터의 이름을 명시적으로 작성하면 된다.

그 다음 try-catch 문의 경우에는 필드를 가져와서 작업을 시행하고, Class의 슈퍼 클래스, 즉 부모 클래스로 다시 이동시키는 코드이다. 이렇게 하면 TitleView 가 BaseView를 상속하는데, BaseView에도 우리들의 어노테이션이 있으면 같이 파싱할 수 있기 때문이다.

그 다음으로는 해당 field 에서 어노테이션을 찾아서 그 어노테이션이 우리가 만든 BindAttr이면 메서드를 실행하게 해보자.

field.declaredAnnotations.forEach {
    if (it is BindAttr) {
        attachBindAttr(array, field, it, receiver)
    }
}

여기의 forEach 에서는 it를 사용한다.

그래서 attachBindAttr 란 메서드에 TypedArray, Field, BindAttr, Any 를 각각 넘겨준다.

값 설정

private fun attachBindAttr(array: TypedArray, field: Field, bindAttr: BindAttr, receiver: Any) {
        val index = bindAttr.value
        val defValue = bindAttr.defValue
}

attachBindAttr 메서드에서 각각 파싱에 필요한 index와 defValue를 꺼낸다.

이 값들로부터 TypeArray 에서 값을 가져올텐데, 문제는 위에서도 있었지만 메서드가 타입마다 다 다르다는 문제가 있다.

그래서 when 문을 사용해 value 변수를 initialize 시키자.

val value: Any = when (bindAttr.type) {
    AttrType.Boolean -> array.getBoolean(index, (defValue != 0))
    AttrType.Color -> array.getColor(index, defValue)
    AttrType.Dimension -> array.getDimension(index, defValue.toFloat())
    AttrType.DimensionPixelSize -> array.getDimensionPixelSize(index, defValue)
    AttrType.Drawable -> array.getDrawable(index)
    AttrType.Float -> array.getFloat(index, defValue.toFloat())
    AttrType.Int -> array.getInt(index, defValue)
    AttrType.ResourceId -> array.getResourceId(index, defValue)
    AttrType.String -> array.getString(index)
    AttrType.Integer -> array.getInteger(index, defValue)
}

마지막으로 해당 필드에 접근 가능 여부를 true로 하고, 값을 설정한다.

try {
    field.isAccessible = true
    field.set(receiver, value)
} catch (e: Exception) {

}

전체 코드

package com.github.windsekirun.baseapp.module.attrparser

import android.content.res.TypedArray
import pyxis.uzuki.live.richutilskt.utils.tryCatch
import java.lang.reflect.Field

/**
 * PyxisBaseApp
 * Class: AttrParser
 * Created by Pyxis on 2018-01-29.
 *
 * Description:
 */

object AttrParser {

    @JvmStatic
    fun inject(array: TypedArray, receiver: Any) {
        tryCatch {
            var cls = receiver.javaClass

            do {
                cls.declaredFields.filter { it.declaredAnnotations.isNotEmpty() }.forEach { field ->
                    field.declaredAnnotations.forEach {
                        if (it is BindAttr) {
                            attachBindAttr(array, field, it, receiver)
                        }
                    }
                }

                try {
                    cls = cls.getSuperclass()
                } catch (e: Exception) {
                    break
                }

            } while (cls != null)
        }
    }

    private fun attachBindAttr(array: TypedArray, field: Field, bindAttr: BindAttr, receiver: Any) {
        val index = bindAttr.value
        val defValue = bindAttr.defValue

        val value: Any = when (bindAttr.type) {
            AttrType.Boolean -> array.getBoolean(index, (defValue != 0))
            AttrType.Color -> array.getColor(index, defValue)
            AttrType.Dimension -> array.getDimension(index, defValue.toFloat())
            AttrType.DimensionPixelSize -> array.getDimensionPixelSize(index, defValue)
            AttrType.Drawable -> array.getDrawable(index)
            AttrType.Float -> array.getFloat(index, defValue.toFloat())
            AttrType.Int -> array.getInt(index, defValue)
            AttrType.ResourceId -> array.getResourceId(index, defValue)
            AttrType.String -> array.getString(index)
            AttrType.Integer -> array.getInteger(index, defValue)
        }

        try {
            field.isAccessible = true
            field.set(receiver, value)
        } catch (e: Exception) {

        }
    }
}

코드에서 부를때는 AttrParser.inject(array, this) 로 사용하면 된다.

마무리

100줄 미만의 코드로 어노테이션 파싱 기능을 만들 수 있다는 건 정말로 큰 장점이기도 하다.

물론 디버깅 할 때 어려워진다는 단점은 있지만, 코드가 직관적으로 된다는 것도 무시할 수도 없게 된다.

Kotlin – Reflection (4) – Create Custom Annotation


Reflection 시리즈

  1. With Java API
  2. With Kotlin API
  3. Type Erasure on the JVM
  4. Create Custom Annotation

 

이제 마지막으로 Reflection을 이용한 간단한 어노테이션을 만들어보려고 한다.

예제

먼저 데이터 클래스를 하나 만든다.

data class Contact(val id: Int, val name: String, val email: String)

그리고 만들 어노테이션에 대해 생각해보자.

여기서는 음… @Table 라고 적으면 저 데이터 클래스가 자동으로 데이터베이스의 한 테이블로서 들어가는 기능을 만들 것이다.

Kotlin 에서 어노테이션을 만드는 방법은 class 앞에 annotation을 붙여주면 된다.

annotation class Table(val name: String)

내친 김에 하나 더 만들면, 데이터 클래스에 있는 각각의 필드를 테이블의 요소로서 집어넣을 수 있을 것이다.

annotation class Field(val name: String)

그런데 이렇게 하면 문제가 있다.

@Table("ContactTable")
@Field("Contact")
data class Contact(val id: Int, @Field("name") val name: String, @Field("email") val email: String)

이런 식으로, 분명히 Contact는 클래스인데 Field 를 선언할 수 있게 하면 분명히 버그가 날 것이다.

그래서 annotation class 에 추가적으로 달 수 있는 어노테이션이 있다.

타겟 제한

이름제한하는 범위
AnnotationTarget.CLASS클래스, 인터페이스, Object, 어노테이션 클래스
AnnotationTarget.ANNOTATION_CLASS어노테이션 클래스
AnnotationTarget.TYPE_PARAMETER타입 파라미터 (제너릭)
AnnotationTarget.PROPERTY요소
AnnotationTarget.FIELD필드, 요소 포함
AnnotationTarget.LOCAL_VARIABLE지역 변수
AnnotationTarget.VALUE_PARAMETER함수 또는 생성자의 값 매개변수
AnnotationTarget.CONSTRUCTOR주 생성자, 부 생성자
AnnotationTarget.FUNCTION함수
AnnotationTarget.PROPERTY_GETTER요소 중 GETTER
AnnotationTarget.PEROPERTY_SETTER요소 중 SETTER
AnnotationTarget.TYPE타입
AnnotationTarget.EXPRESSION표현
AnnotationTarget.FILE파일
AnnotationTarget.TYPEALIAS타입 별칭

쓰는 방법은 아래와 같다.

@Target(AnnotationTarget.CLASS)
annotation class Table(val name: String)

여러개의 제한을 두고 싶다면 ,(쉼표)로 이으면 된다.

Reflection가 수행 가능한 단계

Kotlin 에서는 기본적으로 런타임 상과 코드 상에서 둘 다 가능하지만 @Retention으로 설정할 수 있다.

어노테이션 반복

@Repeatable 로 어노테이션을 반복해서 쓸 수 있다.

문서  필수화

@MustBeDocumented로 문서에 반드시 정의되게 할 수 있다.