PS:人は自己暗示を受け入れることに非常に積極的な生物です。あなたが自分に消極的な暗示を与えれば、簡単に落ち込んでしまいますが、逆に自分に積極的な暗示を与えれば、あなたも積極的になれるでしょう。
今日はコンパイル時のアノテーションに関する知識を見てみましょう。手動で実践すれば、Dagger、ARouter、ButterKnife などのコンパイル時アノテーションを使用したフレームワークを理解しやすくなり、その内部のソースコードの実装も理解しやすくなるでしょう。内容は以下の通りです:
- コンパイル時と実行時のアノテーション
- アノテーション処理器 APT
- AbstractProcessor
- Element と Elements
- カスタムアノテーション処理器
- カスタムアノテーション処理器の使用
コンパイル時と実行時のアノテーション#
まず、コンパイル時と実行時の違いを理解しましょう:
- コンパイル時:コンパイラがソースコードを機械が認識できるコードに翻訳するプロセスを指します。Java では、Java ソースコードを JVM が認識できるバイトコードファイルにコンパイルするプロセスです。
- 実行時:JVM がメモリを割り当て、バイトコードファイルを解釈して実行するプロセスを指します。
メタアノテーション@Retention
は、アノテーションがコンパイル時か実行時かを決定します。その設定可能な戦略は以下の通りです:
public enum RetentionPolicy {
SOURCE, // コンパイル時に破棄され、ソースコード内にのみ存在
CLASS, // デフォルトの戦略で、実行時に破棄され、classファイル内にのみ存在
RUNTIME // コンパイル時にアノテーション情報がclassファイルに記録され、実行時にも保持され、リフレクションを通じてアノテーション情報を取得可能
}
コンパイル時アノテーションと実行時アノテーションは、上記の違いに加えて、実装方法も異なります。コンパイル時アノテーションは一般的にアノテーション処理器(APT)を通じて実装され、実行時アノテーションは一般的にリフレクションを通じて実装されます。
アノテーションとリフレクションに関する詳細は、以下の 2 つの記事を参照してください:
APT とは#
APT(Annotation Processing Tool)は、javac が提供するアノテーションを処理するためのツールで、コンパイル時にアノテーションをスキャンして処理します。簡単に言えば、APT を使用してアノテーションおよびそのアノテーションの位置情報を取得し、これらの情報を使用してコンパイラがコードを生成します。コンパイル時アノテーションは、APT を通じてアノテーション情報を使用してコードを生成し、特定の機能を実現します。典型的な例として ButterKnife、Dagger、ARouter などがあります。
AbstractProcessor#
AbstractProcessor
はProcessor
を実装したアノテーション処理器の抽象クラスで、アノテーション処理器を実装するにはAbstractProcessor
を継承して拡張する必要があります。主なメソッドの説明は以下の通りです:
- init:Processor を初期化します。
ProcessingEnvironment
のパラメータからツールクラスElements
、Types
、Filer
、およびMessager
などを取得できます。 - getSupportedSourceVersion:使用する Java バージョンを返します。
- getSupportedAnnotationTypes:処理するすべてのアノテーション名を返します。
- process:指定されたすべてのアノテーションを取得して処理します。
process メソッドは、他のクラスが生成されなくなるまで、実行中に複数回実行される可能性があります。
Element と Elements#
Element
は XML のタグに似ており、Java ではElement
はプログラム要素(クラス、メンバー、メソッドなど)を表します。各Element
は特定の構造を表し、Element
オブジェクトの操作には visitor または getKind () メソッドを使用して判断し、具体的に処理します。Element
のサブクラスは以下の通りです:
- ExecutableElement
- PackageElement
- Parameterizable
- QualifiedNameable
- TypeElement
- TypeParameterElement
- VariableElement
上記の要素構造は以下のコード構造に対応します:
// PackageElement
package manu.com.compiler;
// TypeElement
public class ElementSample {
// VariableElement
private int a;
// VariableElement
private Object other;
// ExecutableElement
public ElementSample() {
}
// メソッドパラメータVariableElement
public void setA(int newA) {
}
// TypeParameterElementはパラメータ化された型を表し、ジェネリックパラメータで使用されます
}
Elements
はElement
を処理するためのツールクラスで、インターフェースのみを提供し、具体的には Java プラットフォームが実装しています。
カスタムコンパイルアノテーション処理器#
以下に APT を使用してアノテーション@Bind
を実装し、ButterKnife のアノテーション@BindView
を模倣します。サンプルプロジェクトの構造は以下の通りです:
api モジュールでアノテーション@Bind
を以下のように定義します:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
compiler モジュールは Java モジュールで、Google の auto-service を導入して META-INFO 下の関連ファイルを生成し、javapoet を使用して Java ファイルをより簡単に作成します。カスタムアノテーション処理器BindProcessor
は以下の通りです:
// META-INF/services/javax.annotation.processing.Processorファイルを生成するために使用
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// Javaファイルを作成するために使用
implementation 'com.squareup:javapoet:1.12.1'
/**
* BindProcessor
*/
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor {
private Elements mElements;
private Filer mFiler;
private Messager mMessager;
// 特定のクラスに対応するBindModelを保存
private Map<TypeElement, List<BindModel>> mTypeElementMap = new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mMessager = processingEnvironment.getMessager();
print("init");
// Processorを初期化
mElements = processingEnvironment.getElementUtils();
mFiler = processingEnvironment.getFiler();
}
@Override
public SourceVersion getSupportedSourceVersion() {
print("getSupportedSourceVersion");
// 使用するJavaバージョンを返す
return SourceVersion.RELEASE_8;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
print("getSupportedAnnotationTypes");
// 処理するすべてのアノテーション名を返す
Set<String> set = new HashSet<>();
set.add(Bind.class.getCanonicalName());
set.add(OnClick.class.getCanonicalName());
return set;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
print("process");
mTypeElementMap.clear();
// 指定されたClass型のElementを取得
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Bind.class);
// 条件に合うElementを保存
for (Element element : elements) {
// Elementに対応するクラスの完全修飾名を取得
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
print("process typeElement name:" + typeElement.getSimpleName());
List<BindModel> modelList = mTypeElementMap.get(typeElement);
if (modelList == null) {
modelList = new ArrayList<>();
}
modelList.add(new BindModel(element));
mTypeElementMap.put(typeElement, modelList);
}
print("process mTypeElementMap size:" + mTypeElementMap.size());
// Javaファイル生成
mTypeElementMap.forEach((typeElement, bindModels) -> {
print("process bindModels size:" + bindModels.size());
// パッケージ名を取得
String packageName = mElements.getPackageOf(typeElement).getQualifiedName().toString();
// Javaファイルのファイル名を生成
String className = typeElement.getSimpleName().toString();
String newClassName = className + "_ViewBind";
// MethodSpec
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.bestGuess(className), "target");
bindModels.forEach(model -> {
constructorBuilder.addStatement("target.$L=($L)target.findViewById($L)",
model.getViewFieldName(), model.getViewFieldType(), model.getResId());
});
// typeSpec
TypeSpec typeSpec = TypeSpec.classBuilder(newClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(constructorBuilder.build())
.build();
// JavaFile
JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
.addFileComment("AUTO Create")
.build();
try {
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
});
return true;
}
private void print(String message) {
if (mMessager == null) return;
mMessager.printMessage(Diagnostic.Kind.NOTE, message);
}
}
ここでBindModel
はアノテーション@Bind
の情報を簡単にラップしたものです:
/**
* BindModel
*/
public class BindModel {
// メンバー変数Element
private VariableElement mViewFieldElement;
// メンバー変数の型
private TypeMirror mViewFieldType;
// ViewのリソースID
private int mResId;
public BindModel(Element element) {
// Elementがメンバー変数であることを検証
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException("element is not FIELD");
}
// メンバー変数Element
mViewFieldElement = (VariableElement) element;
// メンバー変数の型
mViewFieldType = element.asType();
// アノテーションの値を取得
Bind bind = mViewFieldElement.getAnnotation(Bind.class);
mResId = bind.value();
}
public int getResId(){
return mResId;
}
public String getViewFieldName(){
return mViewFieldElement.getSimpleName().toString();
}
public TypeMirror getViewFieldType(){
return mViewFieldType;
}
}
bind モジュールで生成するファイルを作成します:
/**
* 初期化
*/
public class BindKnife {
public static void bind(Activity activity) {
// activityの完全修飾クラス名を取得
String name = activity.getClass().getName();
try {
// リフレクションを使用してActivityを生成し、注入
Class<?> clazz = Class.forName(name + "_ViewBind");
clazz.getConstructor(activity.getClass()).newInstance(activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
もちろん、ButterKnife は生成したオブジェクトをキャッシュしており、毎回新しいオブジェクトを生成することはありません。リフレクションを使用していますが、実行時アノテーションに比べてリフレクションのテストが大幅に減少するため、性能も実行時アノテーションよりも良好です。これがコンパイル時アノテーションと実行時アノテーションの違いです。
カスタムアノテーション処理器の使用#
使用方法は ButterKnife に似ています。以下のようになります:
public class MainActivity extends AppCompatActivity{
@Bind(R.id.tvData)
TextView tvData;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
BindKnife.bind(this);
tvData.setText("data");
}
}
コンパイル時アノテーションを理解することは、すぐに新しいものを作ることにはならないかもしれませんが、他のフレームワークを見る際に非常に役立ち、問題を解決する際の選択肢が増えます。