天天看點

ByteBuddy的一些使用場景

ByteBuddy 是一個非常強大的JAVA二進制碼生成工具,以前我使用過JAVASIST,那時的主要應用場景是根據為沒有源代碼的JAVA類(如一些第三方JAVA庫)添加一些日志友善排查問題,但是JAVASIST有一些缺陷: 首先添加日志在方法的開始和結束比較容易, 如果想更改方法本身的二進制碼會相當麻煩. 其次是不支援動态代碼 而ByteBuddy則提供了許多友善好用的HELPER方法幫助我們生成或者增強JAVA類二進制碼. 但是ByteBuddy本身相關的資料略少, API也比較複雜. 這裡我主要結合工作中的一些實際使用場景給大家介紹一下.

使用場景1:動态生成JAVA CLASS

這是ByteBuddy最基本但是最有用的USECASE, 比如說,我定義一個SCHEMA,希望根據SCHEMA生成對應的JAVA類并加載到JVM中. 當然你可以用一些模闆工具比如VELOCITY之類生成JAVA源代碼再進行編譯, 但是這樣一般需要對項目的BUILD腳本(maven/gradle)進行一些定制. 用ByteBuddy生成類的強大之處在于, 這個類可以通過JAVA代碼生成并且可以自動加載到目前的JVM中,這樣我可以在application初始化的時候從SCHEMA REGISTRY中讀取到SCHEMA定義,通過ByteBuddy生成對應的JAVA類加載到JVM中, 并且一旦SCHEMA發生變化如增加新屬性, 不需要做任何改動, 在JVM重新開機後類定義會自動同步更新. 當然一個限制是你隻能用反射的方式通路這個新定義的類. 代碼樣例如下 (Kotlin語言):

private fun buildSchemaClass(schema: Schema, classLoader: ClassLoader): Class<*> {
        logger.info("Start building schema class for schema={}, version={}", schema.name, schema.version)
        val typeBuilder = ByteBuddy().subclass(Any::class.java).name(className(schema))
        var tempBuilder = typeBuilder
        schema.columns.forEach { column ->
            tempBuilder = tempBuilder
                .defineField(column.name, columnTypeToJavaClassMapping[column.type], Visibility.PRIVATE)
        }
        schema.columns.forEach { column ->
            tempBuilder = tempBuilder.defineMethod(toGet(column.name), columnTypeToJavaClassMapping[column.type], Modifier.PUBLIC)
                .intercept(FieldAccessor.ofBeanProperty())
                .defineMethod(toSet(column.name), Void.TYPE, Modifier.PUBLIC).withParameters(columnTypeToJavaClassMapping[column.type])
                .intercept(FieldAccessor.ofBeanProperty())
        }
        return tempBuilder.make().load(classLoader, ClassLoadingStrategy.Default.WRAPPER).loaded.also {
            logger.info("Success building schema class: {}", it.name)
        }
    }           

大家可以看到,定義GET/SET方法完全不需要實作方法體,通過intercept(FieldAccessor.ofBeanProperty) 可以自動将GET/SET方法和對應的屬性綁定自動生成方法體.

使用場景2:JAVA AGENT代理.

這是另一個異常強大的功能,可以動态修改類的定義,用于強行修改一些第三方LIB中一些不容易擴充的類,而不需要修改類的源代碼和JAR包. 不知道大家有沒有用過JAVA 本身的AGENT, 在一些性能監控的工具常常會用到, 需要在JVM啟動的時候加agentlib參數,在AGENT中可以對原始JAVA的二進制碼做增強埋點. ByteBuddy強大之處是它連agentlib參數都不需要了,侵入性更小. 一個很好的例子是FLYWAY,我們需要在FLYWAY執行資料庫腳本的時候将腳本執行到一個額外的資料庫(SPANNER). 需要擴充org.flywaydb.core.internal.resolver.sql.SqlMigrationExecutor的execute方法執行額外的寫操作, 不得不吐槽一下FLYWAY,一大堆的繼承接口但是代碼基本沒法擴充. 這時可以通過ByteBuddy Agent 攔截SqlMigrationExecutor的execute方法,在原始方法執行之後實作額外的資料庫寫操作. 唯一需要提的一點是ByteBuddy Agent隻支援JVM不支援JRE. 代碼示例如下:

object SqlMigrationResolverEnhancer {
   fun enhance(inst: Instrumentation) {
        if (!SpannerConfigUtils.enableSpanner) {
            logger.info("Spanner is not enabled!!!!!!!!!!!!!!!!, no intercept will occure")
            return
        }
        val temp = Files.createTempDirectory("tmp").toFile()
        ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, inst).inject(
                Collections.singletonMap(
                        TypeDescription.ForLoadedType(MigrationResolverInterceptor::class.java),
                        ClassFileLocator.ForClassLoader.read(MigrationResolverInterceptor::class.java!!)
                )
        )
        AgentBuilder.Default()
                .ignore(ElementMatchers.nameStartsWith("net."))
                .ignore(ElementMatchers.nameStartsWith("com."))
                .enableBootstrapInjection(inst, temp)
                .type(ElementMatchers.nameEndsWith("SqlMigrationResolver"))
                .transform { builder, _, _, _ ->
                    builder.method(ElementMatchers.hasMethodName("resolveMigrations"))
                            .intercept(MethodDelegation.to(MigrationResolverInterceptor::class.java))
                }.with(object : AgentBuilder.Listener {
                    override fun onComplete(
                        typeName: String?,
                        classLoader: ClassLoader?,
                        module: JavaModule?,
                        loaded: Boolean
                    ) {
                        // just ignore onComplete
                    }

                    override fun onDiscovery(
                        typeName: String?,
                        classLoader: ClassLoader?,
                        module: JavaModule?,
                        loaded: Boolean
                    ) {
                        // just ignore onDiscovery
                    }

                    override fun onIgnored(
                        typeDescription: TypeDescription?,
                        classLoader: ClassLoader?,
                        module: JavaModule?,
                        loaded: Boolean
                    ) {
                        // just ignore onIgnored
                    }

                    override fun onTransformation(
                        typeDescription: TypeDescription?,
                        classLoader: ClassLoader?,
                        module: JavaModule?,
                        loaded: Boolean,
                        dynamicType: DynamicType?
                    ) {
                        logger.debug("Tranforming class: $typeDescription")
                    }

                    override fun onError(
                        typeName: String?,
                        classLoader: ClassLoader?,
                        module: JavaModule?,
                        loaded: Boolean,
                        throwable: Throwable?
                    ) {
                        logger.error("Error intercepting type: $typeName", throwable)
                    }
                })
                .installOn(inst)
    }
}

class MigrationResolverInterceptor {
    companion object {
        private val logger: Logger = LoggerFactory.getLogger(MigrationResolverInterceptor::class.java)

        @JvmStatic
        @RuntimeType
        fun intercept(@SuperCall delegate: Callable<Collection<ResolvedMigration>>): Collection<ResolvedMigration> {
            val spannerProperties = SpannerProperties(SpannerConfigUtils.projectId, SpannerConfigUtils.instanceId, SpannerConfigUtils.databaseName)
            val originalCol = delegate.call() as MutableList<ResolvedMigration>
            logger.info("Intercepting migration resolver method ---------------------------------------- $originalCol")
            return ResolvedMigrationExecutorReplacer.replaceSqlMigrationExecutor(originalCol, spannerProperties)
        }
    }
}
           

代碼并不複雜,通過ElementMatchers先縮小CLASSPATH中掃描包的範圍,在通過ElementMatcher類名和方法名指定需要攔截的方法,再指定攔截器的類名. 這裡注意的是Agent的enhance方法必須在被攔截的類被JVM加載之前執行,因為一個類在一個CLASSLOADER中隻能被加載一次,加載完無法修改了. 注冊AgentBuilder.Listener并非必須,但是對排查期望的類方法沒有被正确攔截的問題非常有用. 另外注意我們隻指定了Interceptor的類名而沒有指定方法, 而且Interceptor類中的方法必須是一個Static方法,通過@RuntimeType指定是攔截器需要執行的方法. @SuperCall用于注入原始方法調用的代理. 可以在SpringBoot 主方法的開始調用

SqlMigrationResolverEnhancer.enhance(ByteBuddyAgent.install())           

至于ResolveMigrationExecutorReplacer的實作和ByteBuddy無關, 代碼僅供參考不再贅述.

object ResolvedMigrationExecutorReplacer {
    private val databaseField = SqlMigrationExecutor::class.java.getDeclaredField("database")
    private val sqlScriptField = SqlMigrationExecutor::class.java.getDeclaredField("sqlScript")

    init {
        databaseField.isAccessible = true
        sqlScriptField.isAccessible = true
    }

    fun replaceSqlMigrationExecutor(migrationList: MutableList<ResolvedMigration>, spannerProperties: SpannerProperties): List<ResolvedMigration> {
        migrationList.forEach { resolvedMigration ->
            val migrationExecutor = resolvedMigration.executor
            if (migrationExecutor is SqlMigrationExecutor &&
                    resolvedMigration is ResolvedMigrationImpl) {
                val database: Database<*> = databaseField.get(migrationExecutor) as Database<*>
                val sqlScript: SqlScript = sqlScriptField.get(migrationExecutor) as SqlScript
                resolvedMigration.executor = SpannerSqlMigrationExecutor(database, sqlScript, spannerProperties)
            }
        }
        return migrationList
    }
}           

使用場景3:AOP切面

和場景2類似,但有時我們不需要改變原始類的實作,而是希望産生一個新的類對原始類的某些行為做增強. AOP本質是生成一個新的PROXY代理類替換原有的實作. JAVA本身提供了基于InvocationHandler的DynamicProxy, 但是有幾個比較大的限制. 1. 被攔截的類必須實作一個接口. 2. InvocationHandler 隻提供了一個方法: public Object invoke(Object proxy, Method method, Object[] args) throws Throwable. 假設被攔截的接口有很多個方法, 如java.sql.PreparedStatement, 需要對某些方法進行特殊處理,那需要基于方法名寫一大堆的If/else邏輯,代碼不夠優雅. Spring提供了基于AspectJ的AOP, 但是這個強依賴于Spring體系必須是在Spring容器中受管理的Bean. 而ByteBuddy則可通過靈活的比對模式指定需要代理的方法,其他方法則可預設為原始類的實作不改變行為. 并且類似于ASPECTJ, 切面的實作可以獨立出來. 一個使用場景是代理java.sql.DataSource/Connection/PreparedStatement/ResultSet. 名額統計,分庫分表等實作都需要. 這裡實作了一個簡單通用的代理織入器,可以對某個類的某一組方法應用一個Advisor攔截器,傳回一個被增強的原始類的子類.

object DelegateAgent {
    fun <T> buildDelegateClass(sourceClass: Class<T>, methodNames: List<String>,
                           advisorClass: Class<*>): Class<out T> {
        val builder = ByteBuddy().subclass(sourceClass, ConstructorStrategy.Default.IMITATE_SUPER_CLASS)
        val methodMatchers = getMethodMachers(methodNames)
        return builder.method(methodMatchers)
            .intercept(MethodDelegation.to(advisorClass))
            .make().load(
                DelegateAgent::class.java.classLoader
            ).loaded
    }

    private fun getMethodMachers(methodNames: List<String>): ElementMatcher<MethodDescription> {
        var methodMatcher =
            ElementMatchers.none<MethodDescription>()
        if (methodNames.isEmpty()) {
            return ElementMatchers.any()
        }
        methodNames.forEach {methodName ->
           methodMatcher = methodMatcher.or(ElementMatchers.named<MethodDescription>(methodName))
        }
        return methodMatcher
    }
}           

注意ByteBuddy().subclass(sourceClass, ConstructorStrategy.Default.IMITATE_SUPER_CLASS), 這樣生成的子類自動擁有父類所有的Constructor. 無需重新定義. 使用的例子如下:

object DataSourceAdvisor {
    @JvmStatic
    @RuntimeType
    fun onMethodExecution(
        @This sourceObj: Any,
        @Origin method: Method
        @AllArguments arguments: Array<Any?>): Any {
        //just for demo purpose
                println("Current method is: " + method.name)
        return method.invoke(sourceObj, * arguments)
    }
}

fun testAgent() {
        val config = HikariConfig().apply {
            this.jdbcUrl = "jdbc:mysql://xxxx"
            this.driverClassName = "org.postgresql.Driver"
            this.username = "postgres"
            this.password = "postgres"
            this.maximumPoolSize = 1
        }
        val resultDsClass = DelegateAgent.buildDelegateClass(HikariDataSource::class.java, listOf("getConnection"),
            DataSourceAdvisor::class.java)
        val newDs = resultDsClass.getConstructor(HikariConfig::class.java).newInstance(config)
        println(newDs.connection)

    }
           
  • 構造方法無法被直接Instrument. 參考以下連結的一些讨論:

    http://www.it1352.com/995885.html

    https://stackoverflow.com/questions/34907382/intercepting-default-constructor-with-byte-buddy

  • 場景2 Agent實作中如果方法的參數簽名和攔截器的參數不完全比對,則需要使用@RuntimeType annotation. 否則可能遇到以下錯誤:
    java.lang.IllegalArgumentException: None of [interceptor methods]  allows for delegation from [target method]
    at net.bytebuddy.implementation.bind.MethodDelegationBinder$Processor.bind(MethodDelegationBinder.java:1096)