Annotation processing

Post on 12-Jan-2017

94 views 3 download

Transcript of Annotation processing

Annotations Processing

Annotations Processing

Florent Champigny @florent_champ

@florent37

Annotation Processing ?

Quésaco ?

Inspection d’annotations

Génération de code

Et pourquoi ?

Éviter de répéter la même tâche encore et encore

Réduire le boilerplate

Assurer une bonne qualité du code

Suivre facilement des standards / patterns

Remonter des erreurs à la compilation

Qui utilise ça ?

Qui utilise ça ?public class ExampleActivity extends Activity {

@Bind(R.id.title) TextView title; @Bind(R.id.avatar) ImageView image;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); }}

http://jakewharton.github.io/butterknife/

Qui utilise ça ?public class ExampleActivity extends Activity {

@Inject UserManager userManager;

@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); MyApplication.get().component().inject(this); }}

http://google.github.io/dagger/

Qui utilise ça ?@Data @Builderpublic class DataExample { private final String name; @Setter(AccessLevel.PACKAGE) private int age; private double score; private String[] tags; }

https://projectlombok.org/

@Annotation

Back to basics : @Annotation

Java 5

Précède la partie de code gérée

Formats@Override, @NonNull, @Inject@Bind(R.id.text), @Author(name = “Florent”)

Package, Types, Variables, Constructors

Back to basics : @Annotation

@OverrideAssure à la compilation que la méthode est bien une

surcharge

@NonNull Object myObject;Vérifie qu’aucune valeur nulle ne sera insérée dans myObject

@Bind(R.id.text) TextView myLabel;Exécutera un findViewById(R.id.text) pour le placer dans

myLabelVérifie que la vue trouvée est bien une TextView

Back to basics : @Annotation

JDK / SDK :java.lang.Overrideandroid.support.annotation.NonNull

Interfaces JavaX : (JSR 330 Dependency Injection for Java)

javax.Injectjavax.Singleton

Tierce : butterknife.Bind

Back to basics : @Annotation

@Retention(SOURCE)@Target({TYPE, FIELD, METHOD})public @interface Author {

String name(); int revision() default 1;

}

Back to basics : @Annotation

@Retention(SOURCE)@Target({TYPE, FIELD, METHOD})public @interface Author {

String name(); int revision() default 1;

}

Back to basics : @Annotation

Retention : Source / Class / Runtime

Target : Type / Annotation_Type / PackageMethod / ConstructorField / Local_Variable / Parameter

APT ?

APT ?

Annotation Processing Tool

Java 6

JSR 269

APT ?

*.java Analyse APT

@

Génère des fichiers .java

*.class

Annotation Processing API

Annotation Processorimplémenter javax.annotation.processing.Processorétendre

javax.annotation.processing.AbstractProcessor

Définir les annotations acceptées : getSupportedAnnotationsType()

Traiter les annotations : process()

Annotation Processing API : getSupportedAnnotationsType@Retention(CLASS) @Target(FIELD)public @interface MyAnnotation {}

public class MyProcessor implements Processor{

@Override Set<String> getSupportedAnnotationType(){return Sets.newHashSet(

MyAnnotation.class.getCanonicalName());}

}

Annotation Processing API : getSupportedAnnotationsType@Retention(CLASS) @Target(FIELD)public @interface MyAnnotation {}

@SupportedAnnotationTypes(“com.github.florent37.processor.MyAnnotation”)public class MyProcessor extends AbstractProcessor {

}

Annotation Processing API : process

@SupportedAnnotationTypes(“com.github.florent37.processor.MyAnnotation”)public class MyProcessor extends AbstractProcessor {

@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {

return true;}} Indique au compilateur que le

processeur a géré l’annotation

Rounds

L’annotation processing s'exécute sur plusieurs rounds

Sur chaque round, une partie des sources sont compilées, et les processeurs acceptant les annotations trouvées sont appelés

Ce procédé s’arrête lorsque toutes les sources/annotations ont été examinées

La méthode process() reçoit deux paramètres :Set<? extends TypeElement> - Les types d’annotations qui ont débuté le

traitementRoundEnvironment - Les informations sur le round actuel

RoundsEnvironment

getRootElements()Liste des classes parcourues par le processor

getElementsAnnotatedWith(Class<? extends Annotation>)Cherche dans ces éléments les occurrences d’une

annotation

RoundsEnvironment

class MyModel {@MyAnnotation int number;

}

env.getRootElements() = [ R, BuildConfig, MainActivity, appcompat.R, MyModel ]

env.getElementsAnnotatedWith(MyAnnotation.class) =Element{ name=”number”, type=”int” }

ProcessingEnvironment

Disponible depuis le AbstractProcessor :

this.processingEnv

Utilitaires : .getMessager().getFiler().getElementUtils().getTypeUtils()

Element ? Type ?

Element

Représente un “élément” du programme

.getKind() -> ElementKind

.asType() -> ElementType

javax.lang.model.element.Element

Element

Représente un “élément” du programmePackageClassMethodVariable

.getKind() -> ElementKind

.asType() -> Type

javax.lang.model.element.Element

Element

Représente un “élément” du programme

.getKind() -> ElementKind package : PACKAGE, declared types : ENUM, CLASS, ANNOTATION_TYPEvariables : FIELD, PARAMETER, LOCAL_VARIABLEexecuytables : METHOD, CONSTRUCTOR, STATIC_INIT

.asType() -> Type

javax.lang.model.element.ElementKind

Element

Représente un “élément” du programme

.getKind() -> ElementKind

.asType() -> TypeJCPrimitiveType int, float, booleanJCNoType voidArrayType int[], float[]ClassType java.lang.StringMethodType int count(char[] seq) throws NullPointerException...

JC : JavacCodejavax.lang.model.type.TypeMiror

Elementclass MyModel {

@MyAnnotation private int number;}

Element element = env.getElementsAnnotatedWith(MyAnnotation.class).get(0);

{ element.getKind() = ElementKind.FIELD ; element.asType() = JCPrimitiveType }

Elementclass MyModel {

@MyAnnotation private int number;}

number : String variableName = element.getSimpleName().toString();

int : TypeName variableType = TypeName.get(element.asType());

MyModel : Element parent = element.getEnclosingElement();

private : Set<Modifier> modifiers = element.getModifiers();

Comment le faire connaître au compilateur ?

Comment le faire connaître au compilateur ?

Inclure un fichier

META-INF/services/javax.annotation.processing.Processor

Contenant la liste des processeurs (nom complet)com.github.florent37.processor.MyProcessor

Comment le faire connaître au compilateur ?

Ou utiliser Auto-Service (génère le fichier META-INF/services/...)

compile 'com.google.auto.service:auto-service:1.0-rc2'

Ajouter l’annotation @AutoService(Processor.class) au processor

@AutoService(Processor.class) public class MyProcessor extends Processor{ }

Génération de code

Génération de code

JavaPoet

A Java API for generating .java source files

Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate the need to write boilerplate while also keeping a single source of truth for the metadata.

https://github.com/square/javapoet

JavaPoetpackage com.example.helloworld;

public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); }}

JavaPoetMethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld) .build();

JavaPoet

$L for Literals .addStatement("getIntent().get$LExtra(“a”,0) ”, “Int”) getIntent().getIntExtra(“a”,0)

$S for String .addStatement("getIntent().getStringExtra($S) ”, “name”) getIntent().getStringExtra(“name”)

$T for TypesGénère automatiquement les imports

import java.util.Date;

.addStatement("return new $T()", Date.class) return new Date();

Et comment on débug ?

Et comment on débug ?

gradle.properties :

org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5007

Et comment on débug ?

1. Lancer le gradle daemon ./gradlew --daemon

2. Ajouter des points d’arrêt

3. Lancer le remote debug “APT”

4. Titiller gradle./gradlew clean build

Exemple

HolyFragment https://github.com/florent37/HolyFragment

public class MyFragment extends Fragment{@Holy int number;@Holy String name;

@Override public void onCreate(){super.onCreate();HolyMyFragment.bless(this);

}}

MyFragment fragment = HolyMyFragment.newInstance(2,"florent");

HolyFragment https://github.com/florent37/HolyFragment

public final class HolyMyFragment { public static MyFragment newInstance(int number, String name) { Bundle bundle = new Bundle(); bundle.putInt("number",number); bundle.putString("name",name); MyFragment fragment = new MyFragment(); fragment.setArguments(bundle); return fragment; }

public static void bless(MyFragment fragment) { Bundle args = fragment.getArguments(); fragment.number = args.getInt("number"); fragment.name = args.getString("name"); }}

HolyFragment https://github.com/florent37/HolyFragment

Modules :

annotations compiler

java library java library

app

android module

HolyFragment https://github.com/florent37/HolyFragment

/annotations/.../Holy.java :

@Retention(CLASS) @Target(FIELD)public @interface Holy {}

HolyFragment https://github.com/florent37/HolyFragment

/compiler/build.gradle

apply plugin: 'java'

targetCompatibility = JavaVersion.VERSION_1_7sourceCompatibility = JavaVersion.VERSION_1_7

dependencies { compile 'com.squareup:javapoet:1.3.0' compile 'com.google.auto.service:auto-service:1.0-rc2'

compile project(":annotations")}

HolyFragment https://github.com/florent37/HolyFragment

/compiler/.../HolyProcessor.java

@SupportedAnnotationTypes("com.github.florent37.holy.annotations.Holy")@AutoService(Processor.class)public class HolyProcessor extends AbstractProcessor {

@Override public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment env) {

processHolys(env);

writeHoldersOnJavaFile();

return true;}

}

HolyFragment https://github.com/florent37/HolyFragment

/compiler/.../HolyProcessor.java

@SupportedAnnotationTypes("com.github.florent37.holy.annotations.Holy")@AutoService(Processor.class)public class HolyProcessor extends AbstractProcessor {

@Override public boolean process(Set<? extends TypeElement> annotations,

RoundEnvironment env) {

processHolys(env);

writeHoldersOnJavaFile();

return true;}

}

HolyFragment https://github.com/florent37/HolyFragment

protected void processHolys(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(Holy.class)) {

String variableName = element.getSimpleName().toString(); //number TypeName variableType = TypeName.get(element.asType()); //int

TypeElement enclosing = (TypeElement) element.getEnclosingElement();

String elementName = enclosing.getSimpleName().toString();

ClassName enclosingClassName = ClassName.get(enclosing);

HolyFragmentHolder holder = findOrCreateHolyFragmentHolder(enclosing, elementName);

holder.addArgument(new Variable(variableType, variableName)); }}

HolyFragment https://github.com/florent37/HolyFragment

protected void processHolys(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(Holy.class)) { //ex: @Holy Integer number; String variableName = element.getSimpleName().toString(); //number TypeName variableType = TypeName.get(element.asType()); //int

TypeElement enclosing = (TypeElement) element.getEnclosingElement();

String elementName = enclosing.getSimpleName().toString();

ClassName enclosingClassName = ClassName.get(enclosing);

HolyFragmentHolder holder = findOrCreateHolyFragmentHolder(enclosing, elementName);

holder.addArgument(new Variable(variableType, variableName)); }}

HolyFragment https://github.com/florent37/HolyFragment

protected void processHolys(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(Holy.class)) { String variableName = element.getSimpleName().toString(); //number TypeName variableType = TypeName.get(element.asType()); //int

//ex : com.github.florent37.MyFragment TypeElement enclosing = (TypeElement) element.getEnclosingElement();

//ex: MyFragment String elementName = enclosing.getSimpleName().toString();

//ex : com.github.florent37.MyFragment ClassName enclosingClassName = ClassName.get(enclosing);

HolyFragmentHolder holder = findOrCreateHolyFragmentHolder(enclosing, elementName);

holder.addArgument(new Variable(variableType, variableName));

}}

HolyFragment https://github.com/florent37/HolyFragment

protected void processHolys(RoundEnvironment env) { for (Element element : env.getElementsAnnotatedWith(Holy.class)) { String variableName = element.getSimpleName().toString(); //number TypeName variableType = TypeName.get(element.asType()); //int

TypeElement enclosing = (TypeElement) element.getEnclosingElement();

String elementName = enclosing.getSimpleName().toString();

//ex : com.github.florent37.MyFragment ClassName enclosingClassName = ClassName.get(enclosing);

HolyFragmentHolder holder = findOrCreateHolyFragmentHolder(enclosing, elementName);

holder.addArgument(new Variable(variableType, variableName));

}}

HolyFragment https://github.com/florent37/HolyFragment

public class HolyFragmentHolder { ClassName classNameComplete; List<Variable> args; String className;

...}

public class Variable { TypeName typeName; String elementName;

...}

HolyFragment https://github.com/florent37/HolyFragment

public static MyFragment newInstance(int number, String name){

}

public void construct(HolyFragmentHolder holder) { MethodSpec.Builder newInstanceB = MethodSpec.methodBuilder("newInstance") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(holder.classNameComplete);

for (Variable arg : holder.args) { newInstanceB.addParameter(arg.typeName, arg.elementName); }

...

HolyFragment https://github.com/florent37/HolyFragment

Bundle bundle = new Bundle();bundle.putInt(“number”,number);bundle.putString(“name”,name);

ClassName bundleClass = ClassName.get("android.os", "Bundle");

newInstanceB.addStatement("$T bundle = new $T()", bundleClass, bundleClass);

for (Variable arg : holder.args) { newInstanceB.addStatement("bundle.put$L($S,$L)",

getType(arg.typeName), arg.elementName, arg.elementName);}

HolyFragment https://github.com/florent37/HolyFragment

protected String getType(TypeName typeName) { if (typeName == TypeName.INT || typeName == ClassName.get(Integer.class)) return "Int"; if (typeName.equals(ClassName.get(String.class))) return "String"; return "";}

HolyFragment https://github.com/florent37/HolyFragment

MyFragment fragment = new MyFragment();fragment.setArguments(bundle);return fragment;

newInstanceB.addStatement("$T fragment = new $T()",

holder.classNameComplete, holder.classNameComplete)

.addStatement("fragment.setArguments(bundle)") .addStatement("return

fragment");

MethodSpec newInstance = newInstanceB.build();

HolyFragment https://github.com/florent37/HolyFragment

TypeSpec holyFragment = TypeSpec.classBuilder("Holy" + holder.className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(newInstance) .addMethod(bless) .build();

JavaFile javaFile = JavaFile.builder(holder.classNameComplete.packageName(), holyFragment

).build();

javaFile.writeTo(System.out);

javaFile.writeTo(this.processingEnv.getFiler());

Questions ?