DynamicEntity - 字节码生成实践

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

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


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

1.一种是使用注解,Room、Moshi 都是走这种方式,通过编译时生成另外一份中间代码或者运行时拿注解信息做判断。

2.另外一种就是字节码生成技术,可以让你看到的是一份简洁的代码,而实际上是一份复杂的代码,代表就是 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"}

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

1.lazy 读取,如果是在主线程遇到读取大数据,会造成卡顿,也没有提示,需要使用者注意

2.Android Studio 断点是通过反射字段的方式读取值的,因为也会遇到 gson 序列化类似的问题,你想通过断点读取 lazy 读取的属性值时,会得不到值,而要真正运行后才能才能拿到,有时容易给开发者造成误解。


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

目录
打赏
0
0
0
0
60
分享
相关文章
移动应用开发之旅:探索Android和iOS平台
在这篇文章中,我们将深入探讨移动应用开发的两个主要平台——Android和iOS。我们将了解它们的操作系统、开发环境和工具,并通过代码示例展示如何在这两个平台上创建一个简单的“Hello World”应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧,帮助你更好地理解和掌握移动应用开发。
187 17
|
8月前
|
探索操作系统的心脏:内核设计与实现
在数字世界的庞大机器中,操作系统扮演着至关重要的角色。本文将深入浅出地探讨操作系统的核心——内核的设计原理与实现细节。我们将从内核的概念出发,逐步深入到内核的各个组成部分,包括进程管理、内存管理、文件系统以及输入输出系统的工作机制。通过本文,读者不仅能够了解操作系统内核的基本框架,还将掌握如何通过编程实践加深对操作系统核心概念的理解。让我们一起揭开操作系统内核的神秘面纱,探索它的精妙设计,并体会编程实践中的乐趣和挑战。
109 2
使用 C# 比较两个对象是否相等的7个方法总结
比较对象是编程中的一项基本技能,在实际业务中经常碰到,比如在ERP系统中,企业的信息非常重要,每一次更新,都需要比较记录更新前后企业的信息,直接比较通常只能告诉我们它们是否指向同一个内存地址,那我们应该怎么办呢?分享 7 个方法给你!
216 2
「AIGC算法」将word文档转换为纯文本
使用Node.js模块`mammoth`和`html-to-text`,该代码示例演示了如何将Word文档(.docx格式)转换为纯文本以适应AIGC的文本识别。流程包括将Word文档转化为HTML,然后进一步转换为纯文本,进行格式调整,并输出到控制台。转换过程中考虑了错误处理。提供的代码片段展示了具体的实现细节,包括关键库的导入和转换函数的调用。
216 0
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
187 1
|
11月前
|
优化数据加载策略:深入探讨Entity Framework Core中的懒加载与显式加载技术及其适用场景
【8月更文挑战第31天】在 Entity Framework Core(EF Core)中,数据加载策略直接影响应用性能。本文将介绍懒加载(Lazy Loading)和显式加载(Eager Loading)的概念及适用场景。懒加载在访问导航属性时才加载关联实体,可优化性能,但可能引发多次数据库查询;显式加载则一次性加载所有关联实体,减少查询次数但增加单次查询的数据量。了解这些策略有助于开发高性能应用。
161 0
|
11月前
|
Go
golang对遍历目录操作的优化
【8月更文挑战第7天】在Golang中优化目录遍历能提升性能。可通过缓冲读取减少系统调用、使用协程并发处理大量文件、按需跳过不必要目录及仅获取所需文件信息等方式实现。示例代码展示了如何运用协程并行遍历子目录以加快处理速度。实际应用时需依据场景选择合适策略。
127 1
微信小程序结合PWA技术,提供离线访问、后台运行、桌面图标及原生体验,增强应用性能与用户交互。
微信小程序结合PWA技术,提供离线访问、后台运行、桌面图标及原生体验,增强应用性能与用户交互。开发者运用Service Worker等实现资源缓存与实时推送,利用Web App Manifest添加快捷方式至桌面,通过CSS3和JavaScript打造流畅动画与手势操作,需注意兼容性与性能优化,为用户创造更佳体验。
389 0
高校学生在家实践ECS弹性云服务器
简单谈谈我这几周使用ECS弹性云服务器的体验感
AI助理
登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问

你好,我是AI助理

可以解答问题、推荐解决方案等