Instant.toString() を JST で表示する

~~そんなオプションが、、、無い~~

ByteBuddy を利用した方法
===

* ByteBuddy を使うと Class の Method 内を書き換える事ができる。
* Class が既に Load されている場合を考えると / Boostrap class loader で読み込まれる class を読み替えるには Java Agent が必要。
  * 2 秒ぐらいかかる。。。もっさりしてる。


```kotlin

object InstantsToStringFormatUpdate {
    /**
     * This byte code is equal to following code:
     *
     * ```java
     * class Instant {
     *   public String toString() {
     *       return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(
     *           OffsetDateTime.ofInstant(this, ZoneOffset.ofHours(+9))
     *       )
     *   }
     * }
     * ```
     *
     * This method returns like `2023-01-01T09:00:00+09:00`.
     */
    internal class ToStringJstMethodByteCodeAppender(
        /** Injectable for testing */
        private val instantAccessor: List<StackManipulation>
    ) : ByteCodeAppender {
        override fun apply(
            methodVisitor: MethodVisitor,
            implementationContext: Implementation.Context,
            instrumentedMethod: MethodDescription
        ): ByteCodeAppender.Size {
            val stackManipulation = StackManipulation.Compound(
                FieldAccess.forField(ForLoadedField(DateTimeFormatter::ISO_OFFSET_DATE_TIME.javaField!!))
                    .read(),
                *instantAccessor.toTypedArray<StackManipulation>(),
                IntegerConstant.forValue(+9),
                MethodInvocation.invoke(ForLoadedMethod(ZoneOffset::ofHours.javaMethod!!)),
                MethodInvocation.invoke(ForLoadedMethod(OffsetDateTime::ofInstant.javaMethod!!)),
                MethodInvocation.invoke(ForLoadedMethod(DateTimeFormatter::format.javaMethod!!)),
                MethodReturn.REFERENCE,
            )

            val maturationSize: StackManipulation.Size =
                stackManipulation.apply(methodVisitor, implementationContext)

            return ByteCodeAppender.Size(maturationSize.maximalSize, instrumentedMethod.stackSize)
        }

    }

    internal class ToStringJstMethodImplementation(
        private val instantAccessor: List<StackManipulation>,
    ) : Implementation {
        override fun prepare(instrumentedType: InstrumentedType) = instrumentedType
        override fun appender(implementationTarget: Implementation.Target) = ToStringJstMethodByteCodeAppender(
            instantAccessor = instantAccessor,
        )
    }

    init {
        ByteBuddyAgent.install()

        ByteBuddy()
            .redefine(Instant::class.java)
            .method(ElementMatchers.isToString())
            .intercept(ToStringJstMethodImplementation(
                instantAccessor = listOf(MethodVariableAccess.REFERENCE.loadFrom(0)), // = this
            ))
            .make()
            .load(Instant::class.java.classLoader, ClassReloadingStrategy.fromInstalledAgent())
    }

    fun install() {
        // NO-OP, init body is important.
    }
}
```

Testing
====
```kotlin
class RedefineTarget(
    val instant: Instant,
) {
    override fun toString(): String {
        return "TO_BE_REDEFINED"
    }
}

class InstantsToStringFormatUpdateTest {
    companion object {
        private val log = KotlinLogging.logger {}

        /** Instant を実際に書き換えるのが UnitTest っぽくないので psvm で */
        @JvmStatic
        fun main(args: Array<String>) {
            InstantsToStringFormatUpdate.install()

            log.info {
                """Instant.parse("2023-01-02T03:04:05Z") -> """ + Instant.parse("2023-01-02T03:04:05Z")
            }
        }
    }

    /** Unit test としては `RedefineTarget` を redefine してやる */
    @Test
    fun test() {
        ByteBuddyAgent.install()
        ByteBuddy()
            .redefine(RedefineTarget::class.java)
            .method(ElementMatchers.isToString())
            .intercept(ToStringJstMethodImplementation(
                instantAccessor = listOf(
                    MethodVariableAccess.REFERENCE.loadFrom(0),
                    FieldAccess.forField(ForLoadedField(RedefineTarget::instant.javaField!!))
                        .read(),
                ), // = this.instant.
            ))
            .make()
            .load(javaClass.classLoader, ClassReloadingStrategy.fromInstalledAgent())

        // Prepare
        val newInstance = RedefineTarget(Instant.parse("2022-01-02T03:04:05Z"))

        // Do
        val result = newInstance.toString()

        // Verify
        assertThat(result)
            .isEqualTo("2022-01-02T12:04:05+09:00")
            .isNotEqualTo("TO_BE_REDEFINED")
    }
}
```

おまけ
====

ChatGPT だけでもわりと良いところまではコードを書いてくれる。。。怖すぎ。。。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52956/baca5aaa-0bea-0004-0b10-7297d34ca401.png)