AndroidのO/Rマッパーを題材にAPTを使ってみましたよ
Java6から、APT(Annotation Processing Tool)と呼ばれる便利な機能があることを知りました。とりあえず使ってみようと思い、AndroidのSQLiteで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側の全ファイルは、次のページから確認できます。