DynamicEntity - 字节码生成实践

1,433 阅读10分钟

在 MVC、MVVM 等分层架构中,一般是让 Model/ Entity 类尽量纯净,只有属性和一堆的 setter/getter 方法,但是在复杂的需求场景下,我们又经常会使得它变得不那么纯净。

好在 Java 的手段多,我们可以用一些手段降低 Entity 的视觉和使用的污染程度:

  • 一种是使用注解,Room、Moshi 都是走这种方式,通过编译时生成另外一份中间代码或者运行时拿注解信息做判断。
  • 另外一种就是字节码生成技术,可以让你看到的是一份简洁的代码,而实际上是一份复杂的代码,代表就是 Lombok,可以让开发者不用写一堆的 setter/getter,当然现在 kotlin 当道,这个也没啥用了。

新框架的产生是因为需求的存在以及现有实现的种种缺陷,而我开发 DynamicEntity,也有我们独特的需求场景:

  1. 我们前后端有 syncKey 逻辑,就是每次请求只返回增量的数据,因而服务器返回的数据可能只是一个 Entity 的部分字段,并且我们只能把服务器返回的字段写入db,而不能整个 Entitiy 刷入 DB(Entity 的未赋值字段会覆盖掉 DB 中原有的值)。
  2. 当我们将 Entity 序列化时,我们希望只序列化更改过的值。这在更新设置项并同步到服务器上时很有帮助,我们只将新更改的设置同步到服务器。
  3. KV 存储,我们希望 Entity 的 getter 与 setter 就是我们就是 KV 的读写,这样可以让 KV 读写时感知不到 Key 的存在。
  4. 某些场景,我们希望在访问 Entity 的字段时才去初始化它的值,从而实现懒加载。

DyanmicEntity

针对上述需求,我们要做的关键是监控 Entity 字段的 setter 与 getter,我将之称为 DyanmicEnitity。其实现原理其实很简单:

假设我们纯净的 Entity

class A {
 var a: String = ""
 var b: String = ""
}

为了监控它的 getter 与 setter,我们需要对每个字段的 setter 和 getter 添加 mask:

class A {
    var __setter__ = BitSet()
    var __getter__ = BitSet()
    var a: String = ""
        get() {
            if(!__getter__[0]){
                __getter__[0] = true
                // lazy read
            }
            return field
        }
        set(value) {
            __setter__[0] = true
            field = value
        }

    var b: String = ""
        get() {
            if(!__getter__[0]){
                __getter__[0] = true
                // lazy read
            }
            return field
        }
        set(value) {
            __setter__[1] = true
            field = value
        }
    
    fun opAssignedField(){
        if(__setter__[0]){
            // a is set
        }

        if(__setter__[b]){
            // a is set
        }
    }
}

瞬间,这个 Entity 就变得不忍直视。 而且这个类肯定不行每次都手写。 在以前我们会用工具生成, 虽然不忍直视,但是能用。但是作为有追求的开发者,肯定要来美化它,让它看上去很好,就像 Hilt 一样,接口设计得很好,虽然编译器生成了一堆的代码,但开发时感知不到。

因而我重新设计了整个体系,借助 kotlin 和 字节码注入,让它最终看起来是这样子的:

class A: DynamicEntity {
    var a: String = ""
    var b: String = ""
}

如你所见,就是增加了个 DynamicEntity 接口。但我们编译后去看它编译的字节码,反编译成 Java 代码后,是这样的:

public final class A implements DynamicEntity {
   @NotNull
   private String a = "";
   @NotNull
   private String b = "";
   private BitSet __setterBitSet__ = new BitSet();

   @NotNull
   public final String getA() {
      return this.a;
   }

   public final void setA(@NotNull String var1) {
      this.__setterBitSet__.set(0);
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.a = var1;
   }

   @NotNull
   public final String getB() {
      return this.b;
   }

   public final void setB(@NotNull String var1) {
      this.__setterBitSet__.set(1);
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      this.b = var1;
   }

   public void eachField(@NotNull DynamicEntityReceiver var1) {
      long var2 = System.currentTimeMillis();
      var1.onStart();
      var1.onReceive(this, "a", "a", this.a, String.class);
      var1.onReceive(this, "b", "b", this.b, String.class);
      var1.onFinished(2);
      Performance.report("com.tencent.wehear.reactnative.util.A", "eachField", System.currentTimeMillis() - var2);
   }

   @NotNull
   public String getPrimaryKey() {
      return DefaultImpls.getPrimaryKey(this);
   }

   public long getPrimaryKeyValue() {
      return DefaultImpls.getPrimaryKeyValue(this);
   }

   public boolean isAssigned(@NotNull String var1) {
      if ("a".equals(var1)) {
         return this.__setterBitSet__.get(0);
      } else {
         return "b".equals(var1) ? this.__setterBitSet__.get(1) : false;
      }
   }

   @NotNull
   public String tableName() {
      return DefaultImpls.tableName(this);
   }

   public void writeAssignedFieldTo(@NotNull DynamicEntityReceiver var1) {
      var1.onStart();
      int var4 = 0;
      if (this.__setterBitSet__.get(0)) {
         ++var4;
         var1.onReceive(this, "a", "a", this.a, String.class);
      }

      if (this.__setterBitSet__.get(1)) {
         ++var4;
         var1.onReceive(this, "b", "b", this.b, String.class);
      }

      var1.onFinished(var4);
   }
}

经过字节码注入,我们就可以看到了字段都加了 mask 和 并且补充了 writeAssignedFieldTo 等方法的实现。 这些方法都定义在 DynamicEntity 里:

/**
 *
 * 实体类,可以继承这个接口来监控字段的值是否通过 setter 修改了。
 * 然后通过调用 writeAssignedFieldTo() 将修改字段传递给接受者:
 * 可以是 DB 存储或者 KV 存储, 或者只是赋值给另外一个对象。
 *
 * Notice: 不支持继承, 仅用于数据类
 */
interface DynamicEntity {

    /**
     * 将被赋予了值的属性写入 DynamicEntityReceiver,其可以是 Sqlite, 也可以是通过 batch 批量写入 KV
     */
    fun writeAssignedFieldTo(receiver: DynamicEntityReceiver){}

    /**
     * 不区分是否被赋值,全部字段输出
     */
    fun eachField(receiver: DynamicEntityReceiver){}

    /**
     * 对于 sqlite 而言,就是表名
     * 对于 KV 存储而言,一般是文件名或者作为 key 的 前缀
     */
    fun tableName(): String = this.javaClass.simpleName.toLowerCase()

    /**
     * 对于 sqlite 而言,就是主键,用于 insert 或 update
     * 对于 KV 存储而言,可以作为 K(${primaryKey-primaryKeyValue-fieldName})  的部分prefix
     */
    fun getPrimaryKey(): String = "id"

    /**
     * 对于 sqlite 而言,就是主键值,用于 insert 或 update
     * 对于 KV 存储而言,可以作为 K(${primaryKey-primaryKeyValue-fieldName})  的部分prefix
     */
    fun getPrimaryKeyValue(): Long = -1

    /**
     * 字段是否被赋值
     */
    fun isAssigned(fieldName: String): Boolean = false
}

而如果为了监听 getter 实现 lazy read 功能, Entity 可以实现 DynamicEntityWithAutoRead 接口:

/**
 * 在调用 getter 方法时,如果字段属性没有被赋值,则先从 DynamicEntityReader 里 read 数据,然后返回。
 *
 * Notice: 仅用于数据类直接继承该结构,不支持间接继承
 */
interface DynamicEntityWithAutoRead: DynamicEntity {
    fun setDynamicReader(reader: DynamicEntityReader){}
}

同时接口与实现分离,我抽象出 DynamicEntityReceiverDynamicEntityReader

interface DynamicEntityReceiver {
    fun onStart()
    fun <T> onReceive(entity: DynamicEntity, fieldName: String, key: String, value: T?, type: Class<T>)
    fun onFinished(count: Int)
}

interface DynamicEntityReader {
    // javassist + R8 没办法自动 unbox,因此基础类型必须分别提供一个方法。
    // readBool、readByte、readChar 等系列方法
    fun readBool(entity: DynamicEntityWithAutoRead, fieldName: String, key: String, defaultValue: Boolean): Boolean
    //...
    fun readString(entity: DynamicEntityWithAutoRead, fieldName: String, key: String): String?

    // readIntArray、readByteArray 等系列方法
    fun readIntArray(entity: DynamicEntityWithAutoRead, fieldName: String, key: String): IntArray?
    //...

    fun <T> readArray(entity: DynamicEntityWithAutoRead, fieldName: String, key: String, componentType: Class<T>): Array<T>?
    fun <T> readList(entity: DynamicEntityWithAutoRead, fieldName: String, key: String, genericType: Class<T>): List<T>?
    fun <T> readObject(entity: DynamicEntityWithAutoRead, fieldName: String, key: String, type: Class<T>): T?
}

这样我们可以通过实现 DynamicEntityReceiverDynamicEntityReader 来从 sqlite、KV 等存储读写数据。

写入 sqlite

对于 sqlite orm 框架,现在毫无疑问 Room 是最好的选择,官方 Jetpack 成员,配合 LiveData、Flow、Paging 这些库,极大的简化了开发工作。

我们可以通过声明一个个的 POJO 类,来实现读部分字段、读关系表等需求,最为重要的是,通过 POJO 类,我们可以按需读取字段,界面上需要什么字段,我们就读什么字段,而不是每次都去读全部字段。

Room 只提供了声明 POJO 类写入 DB 更新部分字段的方式,但是我们定义给 Retrofit 来接收后台数据的类是固定的,即使后台返回的只是部分数据,我们也需要定义一个大而全的结构,因而我们需要使用 DanamicEntity 来写入 DB(当然,也可以前后端约定协议,后台保证协议字段全部返回。但对于设置项等数据结构,每次请求后台都返回全量数据,也会造成浪费,只需要返回新增的数据就好。)

讲清楚了需求背景,实现其实很简单的:

object RoomHelper {

    fun insertOrReplaceAssignedField(
        database: RoomDatabase,
        entity: DynamicEntity) {
        val writableDatabase = database.openHelper.writableDatabase
        writableDatabase.beginTransaction()
        try {
            entity.writeAssignedFieldTo(object : DynamicEntityReceiver {
                val contentValue = ContentValues()
                override fun onStart() {
                    contentValue.clear()
                }


                override fun <T> onReceive(
                    entity: DynamicEntity,
                    fieldName: String,
                    key: String,
                    value: T?,
                    type: Class<T>
                ) {
                    when (type) {
                        Boolean::class.java -> {
                            contentValue.put(key, if((value as? Boolean) == true) "1" else "0")
                        }
                        Int::class.java -> {
                            contentValue.put(key, value as? Int)
                        }
                        Long::class.java -> {
                            contentValue.put(key, value as? Long)
                        }
                        Short::class.java -> {
                            contentValue.put(key, value as? Short)
                        }
                        Byte::class.java -> {
                            contentValue.put(key, value as? Byte)
                        }
                        Double::class.java -> {
                            contentValue.put(key, value as? Double)
                        }
                        String::class.java -> {
                            contentValue.put(key, value as? String)
                        }
                        ByteArray::class.java -> {
                            contentValue.put(key, value as? ByteArray)
                        }
                        else -> {
                            throw RuntimeException("not support: $type")
                        }
                    }
                }

                override fun onFinished(count: Int) {
                    if (!contentValue.containsKey(entity.getPrimaryKey())) {
                        contentValue.put(entity.getPrimaryKey(), entity.getPrimaryKeyValue())
                    }

                    val rawId = writableDatabase.insert(
                        entity.tableName(),
                        SQLiteDatabase.CONFLICT_IGNORE,
                        contentValue
                    )
                    if (rawId == -1L) {
                        writableDatabase.update(
                            entity.tableName(),
                            SQLiteDatabase.CONFLICT_IGNORE,
                            contentValue,
                            "${entity.getPrimaryKey()} = ?",
                            arrayOf(entity.getPrimaryKeyValue())
                        )
                    }
                }

            })
            writableDatabase.setTransactionSuccessful()
        } finally {
            writableDatabase.endTransaction()
        }
    }
}

假设我们用于接收网络数据的类是 ANet

class ANet: DynamicEntity {
    var a: String = ""
    var b: String = ""
    override fun tableName(): String {
        return "the_table_name"
    }
}

那么我们在使用时:

val aNet = ANet()
aNet.a = "xxx"
RoomHelper.insertOrReplaceAssignedField(database, a)

这样就可以只是将 a 字段的值写入 DB 了。

写入 KV

对于 KV 存储,很关键的就是 Key 的管理了,因为读写都会用到同一个 key,如果传错了,就会得到错误的结果。所以一般我们会将 Key 定义为静态变量,然后提供 get 与 set 方法, 一个常规的使用案例是:

const val mmkvId = "id"
const val KEY_1 = "key1"
const val KEY_2 = "key2"

fun setKey1(value: String){
   MMKV.mmkvWithID(mmkvId).putString(KEY_1, value)
}

fun getKey1(): String?{
   MMKV.mmkvWithID(mmkvId).getString(KEY_1, null)
}

....

随着业务的增加,这些工具方法就会越来越多,越来越多。对于 key 是和业务相关的,我们还会提供一些 key 的 拼装方法,更为复杂了。 而对于 LevelDB 这些有 batch 写入能力的 DB,写入方法就更复杂了。

那么我们来看看 DynamicEntity 是怎么封装的(LevelDb 实现):

// 提供通用前缀封装,tableName 默认为 类名
fun DynamicEntity.kvPrefix(): String = this.tableName() + "_" + this.getPrimaryKey() + "_" + this.getPrimaryKeyValue() + "_"

// 实现 DynamicEntityReader
class LevelDbDynamicFieldReader(val needLogin: Boolean) : DynamicEntityReader{
    override fun readBool(
        entity: DynamicEntityWithAutoRead,
        fieldName: String,
        key: String,
        defaultValue: Boolean
    ): Boolean {
        val k = (entity.kvPrefix() + key).toByteArray()
        val str = levelDb.get(k)?.let { String(it) }
        if (str == null || str.isBlank()) {
            return defaultValue
        }
        return try {
            str.toBoolean()
        } catch (e: Throwable) {
            defaultValue
        }
    }

    override fun <T> readObject(
        entity: DynamicEntityWithAutoRead,
        fieldName: String,
        key: String,
        type: Class<T>
    ): T? {
        val k = (entity.kvPrefix() + key).toByteArray()
        val str = levelDb.get(k)?.let { String(it) }
        if (str == null || str.isBlank()) {
            return null
        }
        val adapter = moshi.adapter(type)
        return adapter.nullSafe().fromJson(str)
    }

    // 其它的 read 方法实现类似
}


object LevelDbHelper : LogTag {

    /**
    * 将更新了的字段 batch 写入 LevelDB
    */
    fun insertOrUpdateAssignedField(levelDb: LevelDB, ,entity: DynamicEntity): Boolean {
        val batch = SimpleWriteBatch(levelDb)
        val prefix = entity.kvPrefix()
        var success: Boolean = true
        entity.writeAssignedFieldTo(object : DynamicEntityReceiver {

            override fun onStart() {

            }

            override fun <T> onReceive(
                entity: DynamicEntity,
                fieldName: String,
                key: String,
                value: T?,
                type: Class<T>
            ) {
                val k = (prefix + key).toByteArray()
                when {
                    value == null -> {
                        batch.del(k)
                    }
                    type.isPrimitive -> {
                        batch.put(k, value.toString().toByteArray())
                    }
                    type == String::class.java -> {
                        batch.put(k, (value as String).toByteArray())
                    }
                    else -> {
                        val adapter = moshi.adapter(type)
                        batch.put(k, adapter.toJson(value).toByteArray())
                    }
                }

            }

            override fun onFinished(count: Int) {
                try {
                    batch.write()
                } catch (e: Exception) {
                    success = false
                }
            }
        })
        return success
    }

    /**
     * 删除所有字段
     */
    fun deleteAll(levelDb: LevelDB, entity: DynamicEntity): Boolean{
        val batch = SimpleWriteBatch(levelDb)
        val prefix = entity.kvPrefix()
        var success: Boolean = true
        entity.eachField(object: DynamicEntityReceiver, KoinComponent{

            val logger: Logger by inject(appCommonLoggerName)

            override fun onStart() {
            }



            override fun <T> onReceive(
                entity: DynamicEntity,
                fieldName: String,
                key: String,
                value: T?,
                type: Class<T>
            ) {
                val k = (prefix + key).toByteArray()
                batch.del(k)
            }


            override fun onFinished(count: Int) {
                try {
                    batch.write()
                } catch (e: Exception) {
                    success = false
                }
            }
        })
        return success
    }
}

假设我现在定义一个设置相关的实体类:

class KVSetting(val userVid: Long): DynamicEntityWithAutoRead{
    var field1: String = ""
    var field2: List<String> = ArrayList()

    override fun getPrimaryKeyValue(): Long {
        return userVid
    }
}

如果我们想存入 LevelDb, 则:

val kv = KVSetting(1)
kv.field1 = "xxx"
LevelDbHelper.insertOrUpdateAssignedField(levelDb,kv)

上面会将 field1 的值存入 LevelDB, 其 key 为 KVSetting_id_1_field1, 完全自动化了。

如果我想实现 lazy 读取 field1/field2, 则:

val kv = KVSetting(1)
kv.setDynamicReader(LevelDbDynamicFieldReader())
// field1 只有当首次调用时才会去读取。
val a = kv.field1

如果我们要将数据存储到不同的实现上,我们只需要提供不同存储的实现就好。

kv.setDynamicReader(LevelDbDynamicFieldReader())
kv.setDynamicReader(MMKVDynamicFieldReader())
...

我们可以实现一个 DynamicFieldReader 的 Factory 类,这样可以让调用方感知不到具体的实现类,从而达到面向接口而不是实现的目的。(设计模式用起来)

序列化框架的选择问题

我们在使用 Retrofit 时,都会选用一个序列化框架,官方当然推荐的是 gson,但是 DynamicEntity 的却不能选取它,原因是因为 gson 反序列化是采用反射 Filed 的方式而非反射 setter 的方式,这就导致我们在 setter 里注入的代码不会被执行,一切都会失效。

fastjson 是一个选择, 不过我们可以选择一个更现代化的序列化库: Moshi,它对 Kotlin 更友好,而且可以使用注解生成的方式完全避免反射。

对于只序列化被修改过的字段的问题,我们虽然能够通过 DynamicEntityReceiver 拿到修改信息,但是如何将这些信息传递给 Moshi 这些序列化库是个难题。我也不得不妥协,clone 了 Moshi 的注解生成的源码,魔改了一番,让它能够感知 DynamicEntity 的特性,这也为后续的维护挖了一个大坑。

魔改后的使用是这样的:

@JsonClass(generateAdapter = true)
@OnlyToJsonAssignedField
class A: DynamicEntity {
   var a: String = ""
   var b: String = ""
}

第一个 @JsonClass 是 Moshi 库提供, 第二个 @OnlyToJsonAssignedField 是由我提供,使用例子:

val a = A()
a.a = "123"
val str = moshi.adapter(A::class.java).toJson(a)
// str 的结果为 {"a":"123"}

DynamicEntity 的缺陷

事务总是难以达到 100% 完美,DynamicEntity 也有缺陷:

  1. lazy 读取,如果是在主线程遇到读取大数据,会造成卡顿,也没有提示,需要使用者注意
  2. Android Studio 断点是通过反射字段的方式读取值的,因为也会遇到 gson 序列化类似的问题,你想通过断点读取 lazy 读取的属性值时,会得不到值,而要真正运行后才能才能拿到,有时容易给开发者造成误解。

好了,说是字节码生成实践,其实也只是在讲 DynamicEntity 的设计而已,毕竟字节码生成只是工具,工具也只是为了实现我们的想法而已。更重要的是,我们非常缺人啊,有兴趣的快点进来看看