在 MVC、MVVM 等分层架构中,一般是让 Model/ Entity 类尽量纯净,只有属性和一堆的 setter/getter 方法,但是在复杂的需求场景下,我们又经常会使得它变得不那么纯净。
好在 Java 的手段多,我们可以用一些手段降低 Entity 的视觉和使用的污染程度:
- 一种是使用注解,Room、Moshi 都是走这种方式,通过编译时生成另外一份中间代码或者运行时拿注解信息做判断。
- 另外一种就是字节码生成技术,可以让你看到的是一份简洁的代码,而实际上是一份复杂的代码,代表就是 Lombok,可以让开发者不用写一堆的 setter/getter,当然现在 kotlin 当道,这个也没啥用了。
新框架的产生是因为需求的存在以及现有实现的种种缺陷,而我开发 DynamicEntity
,也有我们独特的需求场景:
- 我们前后端有 syncKey 逻辑,就是每次请求只返回增量的数据,因而服务器返回的数据可能只是一个
Entity
的部分字段,并且我们只能把服务器返回的字段写入db,而不能整个Entitiy
刷入 DB(Entity 的未赋值字段会覆盖掉 DB 中原有的值)。 - 当我们将
Entity
序列化时,我们希望只序列化更改过的值。这在更新设置项并同步到服务器上时很有帮助,我们只将新更改的设置同步到服务器。 - KV 存储,我们希望
Entity
的 getter 与 setter 就是我们就是 KV 的读写,这样可以让 KV 读写时感知不到 Key 的存在。 - 某些场景,我们希望在访问
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"}
DynamicEntity 的缺陷
事务总是难以达到 100% 完美,DynamicEntity 也有缺陷:
- lazy 读取,如果是在主线程遇到读取大数据,会造成卡顿,也没有提示,需要使用者注意
- Android Studio 断点是通过反射字段的方式读取值的,因为也会遇到 gson 序列化类似的问题,你想通过断点读取 lazy 读取的属性值时,会得不到值,而要真正运行后才能才能拿到,有时容易给开发者造成误解。
好了,说是字节码生成实践,其实也只是在讲 DynamicEntity
的设计而已,毕竟字节码生成只是工具,工具也只是为了实现我们的想法而已。更重要的是,我们非常缺人啊,有兴趣的快点进来看看