在 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){} }
同时接口与实现分离,我抽象出 DynamicEntityReceiver
、DynamicEntityReader
:
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? }
这样我们可以通过实现 DynamicEntityReceiver
和 DynamicEntityReader
来从 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
的设计而已,毕竟字节码生成只是工具,工具也只是为了实现我们的想法而已。更重要的是,我们非常缺人啊,有兴趣的快点进来看看。