Apt(Annotation Processing Tool) = コンパイル時にアノテーションからソースを自動生成したりするアレ。DaggerやButterKnifeが使ってます。
Aptを使ったライブラリを作ろうと思ったのですが分かりにくかったので、ゼロベースで作っていく方法をまとめました。
作るもの
簡単な例として、POJOにアノテーションを付けると、フィールドをログ出力してくれるようなライブラリを作ってみます。
こんな感じにモデルにアノテーションを付けると、
MyModel.java
@Loggable
public class MyModel {
@LogField
public String foo;
@LogField
public int bar;
public MyModel(String foo, int bar) {
this.foo = foo;
this.bar = bar;
}
}
こんな感じでログが出せるライブラリ。
MyModel myModel = new MyModel("Hello World", 123);
// MyModelLoggerはライブラリで自動生成されるクラス
// "foo=Hello World, bar=123" と出力する
MyModelLogger.log(myModel);
今回作ったものはここにあります
作り方
Android Studioでプロジェクトを作成
- Android applicationで作成。適当にBlank Activityを作っときます。
- パッケージ名はサンプルアプリのパッケージ名になるので、
com.kobakei.modelloggerexample
としました。 - これでappというモジュールが生成されます。これはライブラリのサンプルとして使います。
プロジェクト直下のbuild.gradleを修正
buildscriptに、android-aptを追加する
build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
// ここを追加
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
libraryモジュールを追加
- Android Libraryを選んで作成します
- パッケージ名はライブラリのパッケージ名になるので、
com.kobakei.modellogger
とします - build.gradleを修正
library/build.gradle
apply plugin: 'com.android.library'
// ここを追加 'com.android.library'より下に書く
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
// compilerから参照するので、コメントアウト
//compile 'com.android.support:appcompat-v7:23.1.1'
}
- アノテーションクラスを作成します。
- クラスにつける
@Loggable
と、フィールドにつける@LogField
-
@Target
は、そのアノテーションがどのタイプに付けれるか(TYPE = クラス、FIELD = フィールド) - アノテーションは引数をとったりデフォルト値を定義したりできるが、ここでは使わないので説明を省略。詳しくは http://www.ne.jp/asahi/hishidama/home/tech/java/annotation.html を参考に。
- クラスにつける
Loggable.java
@Target(ElementType.TYPE)
public @interface Loggable {
}
LogField.java
@Target(ElementType.FIELD)
public @interface LogField {
}
compilerモジュールを追加
- Java Libraryを選んで作成(Android Libraryではないので注意!)
- パッケージ名はlibraryと同じく、
com.kobakei.modellogger
- compiler/build.gradleを以下のように修正
compiler/build.gradle
apply plugin: 'java'
// ここを追加
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
// 以下を追加
compile project(":library")
compile 'com.squareup:javapoet:1.2.0' // Javaのソースを出力するためのライブラリ
compile 'com.google.auto.service:auto-service:1.0-rc2' // META-INFを自動生成するためのライブラリ
compile 'com.google.android:android:4.1.1.4' // compilerはpure java moduleなので、Android frameworkのクラスを操作する場合はこれが必要
}
// ここを追加
// libaryモジュールにいるアノテーションクラスをcompilerで使うため
sourceSets {
main {
java {
srcDirs = ['src/main/java', '../library/src/main/java']
}
}
}
- 以下のように、AbstractProcessorを継承したクラスを作成
- アノテーション
- META-INF出力用に、
@AutoService
を付けておく -
@SupportedAnnotationTypes
には、サポートするアノテーションのクラス名を渡す -
@SupportedSourceVersion
には、Javaのバージョンを指定(7にする)
- META-INF出力用に、
- processメソッドの中で、Javaのソースを出力する
- ここでは、
@Loggable
なクラスを見つけて、その中の@LogField
なフィールドをLogCatに出力するクラスをsquare/javapoetを使って出力しています
- ここでは、
- アノテーション
MyProcessor.java
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.kobakei.modellogger.Loggable", "com.kobakei.modellogger.LogField"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elements;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.messager = processingEnv.getMessager();
this.filer = processingEnv.getFiler();
this.elements = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
Class<Loggable> loggableClass = Loggable.class;
for (Element element : roundEnv.getElementsAnnotatedWith(loggableClass)) {
ElementKind kind = element.getKind();
if (kind == ElementKind.CLASS) {
try {
generateLogger(element);
} catch (IOException e) {
this.messager.printMessage(Diagnostic.Kind.ERROR, "IO error");
}
} else {
this.messager.printMessage(Diagnostic.Kind.ERROR, "Type error");
}
}
return true;
}
/**
* エレメントからLoggerのソースを出力する
*/
private void generateLogger(Element element) throws IOException {
String packageName = elements.getPackageOf(element).getQualifiedName().toString();
ClassName modelClass = ClassName.get(packageName, element.getSimpleName().toString());
// logメソッドの定義
// LogFieldアノテーション付きのフィールドを見つけて、ログの作成式に追加
Class<LogField> logFieldClass = LogField.class;
String message = null;
for (Element el : element.getEnclosedElements()) {
if (el.getAnnotation(logFieldClass) != null) {
String fieldName = el.getSimpleName().toString();
if (message == null) {
message = String.format("\"%s = \" + model.%s", fieldName, fieldName);
} else {
message += String.format(" + \" %s = \" + model.%s ", fieldName, fieldName);
}
}
}
String tag = element.getSimpleName().toString();
ClassName logClass = ClassName.get("android.util", "Log");
MethodSpec logMethod = MethodSpec.methodBuilder("log")
.addParameter(modelClass, "model")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addStatement("$T.v(\"" + tag + "\", " + message + ")", logClass) // Log.v("modelclass", "hoge=" + hoge)
.build();
// クラスの定義
String className = element.getSimpleName() + "Logger";
TypeSpec logger = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(logMethod)
.build();
// ファイルへ出力
JavaFile.builder(packageName, logger)
.build()
.writeTo(filer);
}
}
appモジュール(サンプルアプリ)を修正
- build.gradleを、compiler, libraryモジュールを使うように変更
app/build.gradle
apply plugin: 'com.android.application'
// ここを追加
apply plugin: 'android-apt'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
applicationId "com.kobakei.modelloggerexample"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.1.1'
// ここを追加
compile project(':library')
apt project(':compiler')
}
- サンプルアプリに以下のようなモデルクラスを追加
MyModel.java
@Loggable
public class MyModel {
@LogField
public String foo;
@LogField
public int bar;
public MyModel(String foo, int bar) {
this.foo = foo;
this.bar = bar;
}
}
ビルドしてみる
- Android Studioでビルドしてみる
- 成功すれば、
app/build/generated/source/apt/
以下にMyModelLogger.javaが生成されているはず
出力されたクラスを使ってみる
- MainActivityのonCreateに以下を追加して再ビルド
MainActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
//...
MyModel myModel = new MyModel("Hello World", 123);
MyModelLogger.log(myModel);
}
これでLogCatに出力されればOK!
(おまけ)aptを使ったライブラリをJitpack.ioで配布する
Jitpackのドキュメントの手順を、libraryとcompilerの両方に対してやればよいだけ
ライブラリのユーザーは、以下のようにして使います。コロンの位置がプロジェクト名の後ろなので注意。
app/build.gradle
dependencies {
// com.github.USERNAME.PROJECTNAME:MODULENAME:VERSION
compile 'com.github.kobakei.ModelLogger:library:0.0.2'
apt 'com.github.kobakei.ModelLogger:compiler:0.0.2'
}
参考にしたリポジトリ
-
rejasupotaro/kvs-schema
- Gradleでのプロジェクト構成がわかりやすい
-
airbnb/DeepLinkDispatch
- Javaのソース出力部分が読みやすい