そんなオプションが、、、無い
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 だけでもわりと良いところまではコードを書いてくれる。。。怖すぎ。。。