kurukuru-papaのブログ

主に、ソフトウェア開発に関連したメモを書き溜めたいと思います。

AndroidのO/Rマッパーを題材にAPTを使ってみましたよ

Java6から、APT(Annotation Processing Tool)と呼ばれる便利な機能があることを知りました。とりあえず使ってみようと思い、AndroidSQLiteでDBアクセスするための、O/Rマッパーのような処理を題材に、APTで実現してみました。

APTで自動生成するクラス

APTを使用する側が、テーブルの1レコードを表すデータクラスを作成した際に、次のようなDB操作用クラスを自動生成するように考えました。メソッドには、テーブルを作成・削除するメソッド、データを検索するメソッド、レコードを更新/削除するメソッドがあります。

public class RecordDao {
	private SQLiteDatabase db;

	public RecordDao(SQLiteDatabase db) {
		this.db = db;
	}

	public List<Record> findAll() {
		List<Record> result = new ArrayList<Record>();
		Cursor cursor = db.query("record",
				new String[] { "id", "text", "date" }, null, null, null, null,
				"id");
		while (cursor.moveToNext()) {
			Record record = new Record();
			record.id = cursor.getInt(0);
			record.text = cursor.getString(1);
			record.setDate(cursor.getString(2));
			result.add(record);
		}
		cursor.close();
		LogUtil.d("count=" + result.size());
		return result;
	}

	public long insert(Record record) {
		record.date = new Date();
		ContentValues values = record.getNoIdContentValues();
		long count = db.insert("record", null, values);
		LogUtil.d("id=" + values.getAsInteger("id") + ", count=" + count);
		return count;
	}

	public int update(Record record) {
		int count = db.update("record", record.getFullContentValues(), "id="
				+ record.id, null);
		LogUtil.d("id=" + record.id + ", count=" + count);
		return count;
	}

	public int delete(Record record) {
		int count = db.delete("record", "id=" + record.id, null);
		LogUtil.d("id=" + record.id + ", count=" + count);
		return count;
	}
}

APT処理の概要

下図のようなクラス構成としました。青い部分が自作した部分になります。ソースファイルを自動生成する処理には、ApacheのVelocityを使いました。ここには出て来ませんが、文字列処理などには、SeasarのS2Utilを使用しています。

Eclipseのパッケージエクスプローラで表示したプロジェクト構成は次のようになります。

APT処理の実装

まずは、APT処理の目印となるアノテーションを定義します。

@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.TYPE })
public @interface Entity {
}

APTI処理を開始するクラスです。Android用のソースは「SourceVersion.RELEASE_6」で良いらしいです。

@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes({ "com.kurukurupapa.tryandroid.apt.Entity" })
public class TryAndroidProcesser extends AbstractProcessor {

	@Override
	public boolean process(Set<? extends TypeElement> annotations,
			RoundEnvironment roundEnv) {
		// 引数チェック
		if (annotations.isEmpty()) {
			return true;
		}

		// ソース生成
		TypeElement annotation = annotations.iterator().next();
		DaoGenerater daoGenerator = new DaoGenerater();
		for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
			daoGenerator.generateSource(element, processingEnv);
		}
		return true;
	}

}

上記クラスから呼び出され、Velocityを利用してソースを生成するクラスです。TableData、FieldDataクラスに必要なデータを詰め込んで、Velocityに渡し、Dao.vmファイルをテンプレートとして、ソース生成しています。TableData、FieldDataクラスやvmファイルなど詳細は割愛します。

public class DaoGenerater {
	public void generateSource(Element element,
			ProcessingEnvironment processingEnv) {
		PrintWriter writer = null;

		try {
			TableData data = getData(element, processingEnv);
			JavaFileObject file = AptUtils.createSourceFile(
					data.getDestClassName(), processingEnv);
			writer = new PrintWriter(file.openWriter(), true);
			writeClass(writer, data);
			writer.flush();

		} catch (Exception e) {
			Messager messager = processingEnv.getMessager();
			messager.printMessage(Kind.ERROR, e.toString());
			throw new RuntimeException(e);

		} finally {
			if (writer != null) {
				writer.close();
			}
		}
	}

	private TableData getData(Element classElement,
			ProcessingEnvironment processingEnv) {
		TableData data = new TableData();

		String packageName = AptUtils.getPackageName(classElement,
				processingEnv);
		data.setSrcPackageName(packageName);
		data.setSrcSimpleName(classElement.getSimpleName().toString());
		data.setDestPackageName(packageName);
		data.setDestSimpleName(data.getSrcSimpleName() + "Dao");

		for (Element element : classElement.getEnclosedElements()) {
			// フィールドのみ対象
			if (element.getKind() != ElementKind.FIELD) {
				continue;
			}
			// Excludeアノテーションの付いているフィールドは無視
			if (element.getAnnotation(Exclude.class) != null) {
				continue;
			}
			String type = element.asType().toString();
			String name = element.getSimpleName().toString();
			data.addField(type, name);
		}

		return data;
	}

	private void writeClass(PrintWriter w, TableData data) throws Exception {
		// Velocity初期化
		// 当APTをJARに入れても動作するようにStreamとして設定ファイルを読み込む。
		InputStream is = getClass().getClassLoader().getResourceAsStream(
				"velocity.properties");
		if (is == null) {
			throw new RuntimeException("Velocity設定ファイルが見つかりませんでした。URL="
					+ getClass().getClassLoader().getResource(
							"velocity.properties"));
		}
		Properties props = new Properties();
		props.load(is);
		Velocity.init(props);

		// データ詰め込み
		VelocityContext context = new VelocityContext();
		context.put("data", data);

		// データとテンプレートをマージ
		Template template = Velocity.getTemplate("Dao.vm", "UTF-8");
		template.merge(context, w);
	}
}

当APT処理を使う側で必要となるjavax.annotation.processing.Processorファイルを用意します。

com.kurukurupapa.tryandroid.apt.TryAndroidProcesser

APTを使用する側

プロジェクトのプロパティを開き、Javaコンパイラ→注釈処理で、「プロジェクト固有の設定を可能にする」にチェックを入れます。

Javaコンパイラ→注釈処理→ファクトリーパスを開き、「プロジェクト固有の設定を可能にする」にチェックを入れます。さらに、「Jar追加」ボタンを押して、次のJarを追加します。
・tryandroidapt.jar ←今回作成したAPT処理をするJARです。
・commons-collections-xxx.jar ←tryandroidapt.jarで使用
log4j-xxx.jar ←tryandroidapt.jarで使用
log4j-core-xxx.jar ←tryandroidapt.jarで使用
・velocity-xxx.jar ←tryandroidapt.jarで使用
・s2util-xxx.jar ←tryandroidapt.jarで使用

プロジェクトのプロパティの「Javaのビルドパス」にも、tryandroidapt.jarを追加しておきます。

APTを使うクラスは、次のように、上記で定義したEntityアノテーションをつけるだけです。

@Entity
public class Record {
	public int id;
	public String text;
	public long timestamp;
	
	省略

ダウンロード

今回作成したAPT側の全ファイルは、次のページから確認できます。

動作確認環境