annotation processing and code gen

51
Annotation Processing & Code Gen @kojilin 2015/05/30@TWJUG

Upload: koji-lin

Post on 25-Jul-2015

62 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: Annotation processing and code gen

Annotation Processing & Code Gen

@kojilin 2015/05/30@TWJUG

Page 2: Annotation processing and code gen

Outline•My requirements

•Annotation Processing

•Code gen

Page 3: Annotation processing and code gen

My Requirements

Page 4: Annotation processing and code gen

Kaif Android Client•Using REST client to fetch article and

debates

•Need stable connection

Page 5: Annotation processing and code gen

Kaif Android Client•Using REST client to fetch article and

debates

•Need stable connection

•If not, stale data is better than no data

•Local cache Database

•Http cache response

Page 6: Annotation processing and code gen

Rest Http Client•Retrofit from Square

•A type-safe REST client for Android and Java

•http://square.github.io/retrofit/

Page 7: Annotation processing and code gen

public interface GitHubService {

@GET("/users/{user}/repos")

List<Repo> listRepos(@Path("user") String user);

}

Page 8: Annotation processing and code gen

RestAdapter restAdapter = new RestAdapter.Builder()

.setEndpoint("https://api.github.com")

.build();

GitHubService service = restAdapter.create(GitHubService.class);

Page 9: Annotation processing and code gen

List<Repo> repos = service.listRepos("octocat");

Page 10: Annotation processing and code gen

Header

@Headers("Cache-Control: max-stale=86400")

@GET("/users/{user}/repos")

List<Repo> listRepos$$RetryStale(@Path("user") String user);

Page 11: Annotation processing and code gen

How to retry ?

try {

// normal access

} catch (Exception e) {

// force local cache if network error

}

Page 12: Annotation processing and code gen

How to retry ?try {

service.listRepos("koji");

} catch (Exception e) {

if (e instanceof RetrofitError

&& isNetworkError(e)) {

service.listRepos$$RetryStale("koji");

}

}

Page 13: Annotation processing and code gen

If we do it manually•Every service access need previous try

& catch code.try {

service.listRepos("koji");

} catch (Exception e) {

if (e instanceof RetrofitError

&& isNetworkError(e)) {

service.listRepos$$RetryStale("koji");

}}

Page 14: Annotation processing and code gen

If we do it manually•Every service access need previous try

& catch code.

•Using Proxy pattern

Page 15: Annotation processing and code gen

service.listRepos("koji");

service = Proxy.newProxyInstance(serviceClass.getClassLoader(), new Class[] { serviceClass }, new RetryStaleHandler(restAdapter.create( Class.forName(serviceClass.getName() + "$$RetryStale"))));

@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ... if(method isGet and canRetry) { try & catch block }...}

RetryStaleHandler.java

Page 16: Annotation processing and code gen

If we do it manually•Every service access need previous try

& catch code.

•Using Proxy pattern

•Almost every GET method need $$RetryStale version.

Page 17: Annotation processing and code gen

public interface GitHubService {

@GET("/users/{user}/repos") List<Repo> listRepos(@Path("user") String user); @GET("/users/{user}/repos") @Headers("Cache-Control: max-stale=86400") List<Repo> listRepos$$RetryStale(@Path("user") String user); @GET("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Path("owner") String owner, @Path("repo") String repo); @GET("/repos/{owner}/{repo}/contributors") @Headers("Cache-Control: max-stale=86400") List<Contributor> contributors$$RetryStale(@Path("owner") String owner, @Path("repo") String repo);

}

Page 18: Annotation processing and code gen

If we do it manually•Every service access need previous try

& catch code.

•Using Proxy pattern.

•Almost every GET method need $$RetryStale.

•Generating boilerplate code during build.

Page 19: Annotation processing and code gen

How ?•Scan all interface has @GET method.

•Generate new interface has both non-cache and cache method.

Page 20: Annotation processing and code gen

When scan?•Compile time.

•Annotation Processing.

•Run time.

•Reflection.

•Slow and feedback at runtime

•Do we know the concrete class we want to generate when we are writing ?

Page 21: Annotation processing and code gen

Generate code•bytecode

•.java

Page 22: Annotation processing and code gen

Generate bytecode•Can do more than what java source can.

•Bytecode is difficult to read and write.

•ASM

•JarJar

•Dexmaker for android

Page 23: Annotation processing and code gen

Generate .java•Readable and maintainable.

•JavaPoet from Square

•Successor of JavaWriter

•https://github.com/square/javapoet

Page 24: Annotation processing and code gen

public interface GitHubService {

@GET("/users/{user}/repos") List<Repo> listRepos(@Path("user") String user); @GET("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Path("owner") String owner, @Path("repo") String repo);}

Page 25: Annotation processing and code gen

public interface GitHubService$$RetryStale {

@GET("/users/{user}/repos") List<Repo> listRepos(@Path("user") String user); @GET("/users/{user}/repos") @Headers("Cache-Control: max-stale=86400") List<Repo> listRepos$$RetryStale(@Path("user") String user); @GET("/repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Path("owner") String owner, @Path("repo") String repo); @GET("/repos/{owner}/{repo}/contributors") @Headers("Cache-Control: max-stale=86400") List<Contributor> contributors$$RetryStale(@Path("owner") String owner, @Path("repo") String repo);

}

Page 26: Annotation processing and code gen

AutoValue

@AutoValuepublic abstract class Foo {

public abstract String name();

public abstract int age();

}

Page 27: Annotation processing and code gen

final class AutoValue_Foo extends Foo { private final String name; private final int age; AutoValue_Foo( String name, int age) { if (name == null) { throw new NullPointerException("Null name"); } this.name = name; this.age = age; } @Override public String name() { return name; } @Override public int age() { return age; }

@Override public String toString() { return "Foo{" + "name=" + name + ", age=" + age + "}"; } @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof Foo) { Foo that = (Foo) o; return (this.name.equals(that.name())) && (this.age == that.age()); } return false; } @Override public int hashCode() { int h = 1; h *= 1000003; h ^= name.hashCode(); h *= 1000003; h ^= age; return h; }}

Page 28: Annotation processing and code gen

Butter Knife@InjectView(R.id.content) public TextView content; @InjectView(R.id.last_update_time) public TextView lastUpdateTime; @InjectView(R.id.vote_score) public TextView voteScore; @InjectView(R.id.debater_name) public TextView debaterName; @InjectView(R.id.debate_actions) public DebateActions debateActions; public DebateViewHolder(View itemView) { super(itemView); ButterKnife.inject(this, itemView); content.setOnTouchListener(new ClickableSpanTouchListener()); }

Page 29: Annotation processing and code gen

@Override public void inject(final Finder finder, final T target, Object source) { View view; view = finder.findRequiredView(source, 2131296343, "field 'content'"); target.content = finder.castView(view, 2131296343, "field 'content'"); view = finder.findRequiredView(source, 2131296375, "field 'lastUpdateTime'"); target.lastUpdateTime = finder.castView(view, 2131296375, "field 'lastUpdateTime'"); view = finder.findRequiredView(source, 2131296374, "field 'voteScore'"); target.voteScore = finder.castView(view, 2131296374, "field 'voteScore'"); view = finder.findRequiredView(source, 2131296373, "field 'debaterName'"); target.debaterName = finder.castView(view, 2131296373, "field 'debaterName'"); view = finder.findRequiredView(source, 2131296370, "field 'debateActions'"); target.debateActions = finder.castView(view, 2131296370, "field 'debateActions'"); } @Override public void reset(T target) { target.content = null; target.lastUpdateTime = null; target.voteScore = null; target.debaterName = null; target.debateActions = null;}

Page 30: Annotation processing and code gen

JPA metamodel@Entitypublic class Pet {

@Id Long id;

String name;

}

Page 31: Annotation processing and code gen

@StaticMetaModel(Pet.class)public class Pet_ {

public static volatile SingularAttribute<Person, Long> id;

public static volatile SingularAttribute<Person, String> name;

}

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Order> cq = cb.createQuery(Pet.class);

cq.where(cb.equal(itemNode.get(Pet_.id), 5)) .distinct(true);

Page 32: Annotation processing and code gen

Annotation Processing•JSR 269

•Pluggable Annotation Processing API

•Run automatically when javac runs

•Using ServiceLoader to find

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

•Running inside JVM

Page 33: Annotation processing and code gen

@SupportedSourceVersion(...)@SupportedAnnotationTypes( "retrofit.http.GET")public class MyProcessor extends AbstractProcessor {

@Override public synchronized void init(ProcessingEnvironment env){ }

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

Page 34: Annotation processing and code gen

@SupportedSourceVersion(...)@SupportedAnnotationTypes( "retrofit.http.GET")public class MyProcessor extends AbstractProcessor {

@Override public synchronized void init(ProcessingEnvironment env){ }

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

Page 35: Annotation processing and code gen

@SupportedSourceVersion(...)@SupportedAnnotationTypes(...)public class MyProcessor extends AbstractProcessor {

@Override public synchronized void init(ProcessingEnvironment env){ }

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

Page 36: Annotation processing and code gen

ProcessingEnvironment•Types

•ElementUtils

•Filer

•Messager

Page 37: Annotation processing and code gen

Elements

package foo.bar;public class Foo { // Type Element

String bar; // Variable Element

public void hoge() { // Executable Element System.out.println("Hello!"); }}

Page 38: Annotation processing and code gen

Types

package foo.bar;public class Foo {

String bar;

public void hoge() { System.out.println("Hello!"); }}

Page 39: Annotation processing and code gen

@SupportedSourceVersion(...)@SupportedAnnotationTypes(...)public class MyProcessor extends AbstractProcessor {

@Override public synchronized void init(ProcessingEnvironment env){ }

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

Page 40: Annotation processing and code gen

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

Set<? extends Element> elements = env.getElementsAnnotatedWith(GET.cl ass);

// generating code for elements

}

Page 41: Annotation processing and code gen

JavaPoet

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

Page 42: Annotation processing and code gen

MethodSpec 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("foo.bar", helloWorld).build();javaFile.writeTo(System.out);

Page 43: Annotation processing and code gen

Element to JavaPoet•AnnotationSpec#get

•AnnotationMirror to AnnotationSpec

•MethodSpect#overriding

•Override MethodElement

Page 44: Annotation processing and code gen

public interface Foo$$RetryStale {

@GET("/foo/{user}") List<Foo> foo(@Path("user") String user); @GET("/foo/{user}") @Headers("Cache-Control: max-stale=86400") List<Foo> foo$$RetryStale(@Path("user") String user);}

Page 45: Annotation processing and code gen

TypeSpec.Builder typeSpecBuilder = TypeSpec.interfaceBuilder( classElement.getSimpleName() + "$RetryStale") .addModifiers(Modifier.PUBLIC);

Interfacepublic interface Foo$$RetryStale

Page 46: Annotation processing and code gen

MethodMethodSpec.Builder builder = MethodSpec.methodBuilder("foo$RetryStale"); builder.addModifiers(Modifier.ABSTRACT) .addModifiers(Modifier.PUBLIC); methodElement.getParameters().stream().map(variableElement -> { ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(TypeName.get(variableElement.asType()), variableElement.getSimpleName().toString()); variableElement.getAnnotationMirrors().stream() .map(AnnotationSpecUtil::generate) .forEach(parameterBuilder::addAnnotation); return parameterBuilder.build(); }).forEach(builder::addParameter);

List<Foo> foo(@Path("user") String user)

Page 47: Annotation processing and code gen

Method

methodElement.getAnnotationMirrors() .stream() .map(AnnotationSpecUtil::generate) .forEach(builder::addAnnotation);

@GET("/foo/{user}")

Page 48: Annotation processing and code gen

Convenient Libraries•Google Auto-Service

•Truth

•Making your tests and their error messages more readable and discoverable

•Google Compile testing

Page 49: Annotation processing and code gen

AutoService

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

...

}

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

Page 50: Annotation processing and code gen

Truth and Compile testing

ASSERT.about(javaSources()) .that(ImmutableList.of(inputFile)) .processedWith(new MyProcessor()) .compilesWithoutError() .and() .generatesSources(outputFile);

Page 51: Annotation processing and code gen

References•ANNOTATION PROCESSING 101

http://hannesdorfmann.com/annotation-processing/annotationprocessing101/ •Annotation Processing Boilerplate Destruction https://speakerdeck.com/jakewharton/annotation-processing-boilerplate-destruction-square-waterloo-2014