举个例子,怎么从一个 Cursor 里取出类型为 ClassA 的实例到 List ?
1. 找出 ClassA 对应所有的列和每列在 Cusor 对应的索引。
int columnIndex = cursor.getColumnIndex("columnA");
2. 如果索引存在,根据类型取出正确的值。
if (columnIndex >= 0) {
instance.columnA = cursor.getString(columnIndex);
}
3. 对于每个属性,不断重复上述步骤取出对应的值。
这么做的问题在哪?
* 重复代码
* 重复代码
* 无聊
* 容易出错,不好维护
反射
我就是不想写那么多无聊的代码,怎么办?要不试试范型/反射。
1. 取出所有的属性。
Arrays.asList(cls.getDeclaredFields())
2. 循环属性队列。
3. 把属性设置成 accessible 。
field.setAccessible(true);
4. 找到索引。
int columnIndex = cursor.getColumnIndex(fieldName);
if (columnIndex < 0) {
continue;
}
5. 取出属性的类型,根据类型从 Cursor 里取出正确的值。
Class fieldType = field.getType();
if (fieldType.equals(int.class)) {
field.setInt(instance, cursor.getInt(columnIndex));
} else {
// more type check
6. 结束循环。
这样我们就不用很无聊的把同样的逻辑对于每种类型写一遍又一遍。
Processor
用了反射后,也会有一些其他问题,这样的代码可读性不是太好,不是很容易调试。
既然我们可以通过反射实现这些逻辑,为什么不干脆通过反射把这部分代码直接生成出来呢?
1. 定义你要处理的 annotation 。
2. 定义你的 Processor 类,继承 AbstractProcessor 。
@
AutoService(Processor.class)
@
SupportedSourceVersion(SourceVersion.RELEASE_7)
@
SupportedAnnotationTypes("com.glow.android.Annotation")
public class MyProcessor extends AbstractProcessor {
3. 创建要生成的方法。
ClassName currentType = ClassName.get(element);
MethodSpec.Builder builder = MethodSpec.methodBuilder("fromCursor")
.returns(currentType)
.addModifiers(Modifier.STATIC)
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get("android.database", "Cursor"), "cursor");
4. 循环取出每一列,并像下面这样生成代码。
CodeBlock.Builder blockBuilder = CodeBlock.builder();
blockBuilder.beginControlFlow("");
blockBuilder.addStatement("int columnIndex = cursor.getColumnIndex($S)", column);
blockBuilder.beginControlFlow("if (columnIndex >= 0)");
ColumnType columnType = columnTypeMap.get(column);
String cursorType = null;
if (columnType ==
ColumnType.INT) {
cursorType = "Int";
} else if (columnType == ColumnType.LONG) {
cursorType = "Long";
} else if (columnType == ColumnType.FLOAT) {
cursorType = "Float";
} else if (columnType == ColumnType.STRING) {
cursorType = "String";
} else {
abort("Unsupported type", element);
}
5. 把代码输出到编译时的文件里。
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(
concatClassName(packageName, className), element);
Writer writer = sourceFile.openWriter();
javaFile.writeTo(writer);
writer.close();
6. 用 apt 工具把我们上面写的库加到编译过程去。
Tips:
* 用 AutoService 可以方便的生成 Processor 方法
* 强推 Javapoet ,用来生成漂亮的代码
AOP
AOP 的做法和 Processor 类似,这里就不详述。你可能用 AspectJ 。
Gradle plugin
最后我还是没有完全采用上面的方法,因为:
* 在编译时生成的代码在打开编译器时找不到
* 有时候有些特殊需求,比如很多属性要在多个地方共享使用,能配置化会更好些
于是我们就用了 Gradle Plugin 来通过可配置文件生成代码
以下是简单的例子:
1. 定义配置文件,这里选用比较简单的 toml 文件
srcDir = "src/main/java"
pkg = "com.glow.android.baby.storage.db"
[[tables]]
name = "user"
[[tables.columns]]
name = "user_id"
type = "long"
isKey = true
2. 在 buildSrc 项目里创建 Plugin
public class DbPlugin implements Plugin<Project> {
@
Override void apply(Project project) {
project.task('initDb') << {
def dir = project.getProjectDir()
def file = new File(dir, "table.toml")
generateCode(dir, new Toml().parse(file).to(DB.class))
}
}
static void generateCode(File dir, DB db) {
def outputDir = new File(dir, db.srcDir)
outputDir.mkdirs()
for (Table table : db.tables) {
// Process it
}
}
}
3. 像在上节讲的那样生成代码,把数据源 从 annotation 换成 toml 里的定义
4. 在项目里把 Plugin 引用进去,并执行
5. 这样就可以得到漂亮的已经生成好的代码