LoginSignup
0
0

More than 1 year has passed since last update.

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

Posted at

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

ByteBuddy を利用した方法

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

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0