注意:本文章有些例子需要對Java或Android有一定編程基礎。
與Python相比,Java是一門比較嚴肅的語言。作為一個先學Python的程序員,做起Android難免會覺得不舒服,有些死板,非常懷念decorator等方便的方法。為了實現一個簡單的邏輯,你可能需要寫很多額外的代碼。
舉個例子,做數據庫查詢時,怎麼從一個Cursor
裡取出類型為User
的實例到List
?
假設User
是這樣的
class User {
int id;
String name;
}
1. 找出User
對應所有的列和每列在Cursor
對應的索引。
2. 如果索引存在,根據類型取出正確的值。
3. 對於每個屬性,不斷重復上述步驟取出對應的值。
{
int columnIndex = cursor.getColumnIndex("id");
if (columnIndex >= 0) {
instance.id = cursor.getInt(columnIndex);
}
}
{
int columnIndex = cursor.getColumnIndex("name");
if (columnIndex >= 0) {
instance.name = cursor.getString(columnIndex);
}
}
這麼做的問題在哪?
* 重復代碼
* 重復代碼
* 無聊
* 容易出錯,不好維護
我就是不想寫那麼多無聊的代碼,怎麼辦?要不試試范型/反射。
1. 取出所有的屬性。
2. 循環屬性隊列。
3. 把屬性設置成accessible。
4. 找到索引。
5. 取出屬性的類型,根據類型從Cursor
裡取出正確的值。
public static <T> T fromCursor(Cursor cursor, Class<T> cls) {
T instance = cls.newInstance();
List<Field> fields = Arrays.asList(cls.getDeclaredFields())
for (Field field : fields) {
field.setAccessible(true);
String fieldName = field.getName();
int columnIndex = cursor.getColumnIndex(fieldName);
if (columnIndex < 0) {
continue;
}
Class fieldType = field.getType();
if (fieldType.equals(int.class)) {
field.setInt(instance, cursor.getInt(columnIndex));
} else {
// 處理更多類型 String/float...
}
return instance;
}
這樣我們就不用很無聊的把同樣的邏輯對於每種類型寫一遍又一遍。
用了反射後,也會有一些其他問題,這樣的代碼可讀性不是太好,不是很容易調試。
既然我們可以通過反射實現這些邏輯,為什麼不干脆通過反射把這部分代碼直接生成出來呢?
1. 定義你要處理的annotation。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
String value();
}
2. 定義你的Processor
類,繼承AbstractProcessor
。
// Helper to define the Processor
@AutoService(Processor.class)
// Define the supported Java source code version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Define which annotation you want to process
@SupportedAnnotationTypes("com.glow.android.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
3. 重載process
方法,實現生成代碼的邏輯。
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MyAnnotation.class);
Set<? extends TypeElement> typeElements = ElementFilter.typesIn(elements);
for (TypeElement element : typeElements) {
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");
... // 像在反射那節中,取出所有的fields,並循環取出每個元素,即每列
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);
}
blockBuilder.addStatement("instance.$L = cursor.get$L(columnIndex)",
fieldName,
cursorType);
blockBuilder.endControlFlow();
blockBuilder.endControlFlow();
builder.addCode(blockBuilder.build());
// 結束循環
// 生成代碼到文件裡
String className = ... // 設置你要生成的代碼class名字
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(className, element);
Writer writer = sourceFile.openWriter();
javaFile.writeTo(writer);
writer.close();
}
return false;
}
4. 修改User
class加上annotation
@MyAnnotation
class User {
int id;
String name;
}
5. 用apt工具把我們上面寫的庫加到編譯過程去。
Tips:
* 用AutoService可以方便的生成Processor
方法
* 強推Javapoet,用來生成漂亮的代碼
AOP的做法和Processor類似,這裡就不詳述。你可能用AspectJ。
最後我還是沒有完全采用上面的方法,因為:
* 在編譯時生成的代碼在打開編譯器時找不到
* 有時候有些特殊需求,比如很多屬性要在多個地方共享使用,能配置化會更好些
於是我們就用了Gradle Plugin來通過可配置文件生成代碼
以下是簡單的例子:
1. 定義配置文件,這裡選用比較簡單的toml文件
srcDir = "src/main/java"
pkg = "com.glow.android.storage.db"
[[tables]]
name = "user"
[[tables.columns]]
name = "id"
type = "int"
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. 這樣就可以得到漂亮的已經生成好的代碼
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11