annotation processing and code gen
TRANSCRIPT
Annotation Processing & Code Gen
@kojilin 2015/05/30@TWJUG
Outline•My requirements
•Annotation Processing
•Code gen
My Requirements
Kaif Android Client•Using REST client to fetch article and
debates
•Need stable connection
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
Rest Http Client•Retrofit from Square
•A type-safe REST client for Android and Java
•http://square.github.io/retrofit/
public interface GitHubService {
@GET("/users/{user}/repos")
List<Repo> listRepos(@Path("user") String user);
}
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
GitHubService service = restAdapter.create(GitHubService.class);
List<Repo> repos = service.listRepos("octocat");
Header
@Headers("Cache-Control: max-stale=86400")
@GET("/users/{user}/repos")
List<Repo> listRepos$$RetryStale(@Path("user") String user);
How to retry ?
try {
// normal access
} catch (Exception e) {
// force local cache if network error
}
How to retry ?try {
service.listRepos("koji");
} catch (Exception e) {
if (e instanceof RetrofitError
&& isNetworkError(e)) {
service.listRepos$$RetryStale("koji");
}
}
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");
}}
If we do it manually•Every service access need previous try
& catch code.
•Using Proxy pattern
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
If we do it manually•Every service access need previous try
& catch code.
•Using Proxy pattern
•Almost every GET method need $$RetryStale version.
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);
}
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.
How ?•Scan all interface has @GET method.
•Generate new interface has both non-cache and cache method.
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 ?
Generate code•bytecode
•.java
Generate bytecode•Can do more than what java source can.
•Bytecode is difficult to read and write.
•ASM
•JarJar
•Dexmaker for android
Generate .java•Readable and maintainable.
•JavaPoet from Square
•Successor of JavaWriter
•https://github.com/square/javapoet
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);}
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);
}
AutoValue
@AutoValuepublic abstract class Foo {
public abstract String name();
public abstract int age();
}
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; }}
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()); }
@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;}
JPA metamodel@Entitypublic class Pet {
@Id Long id;
String name;
}
@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);
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
@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) { }}
@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) { }}
@SupportedSourceVersion(...)@SupportedAnnotationTypes(...)public class MyProcessor extends AbstractProcessor {
@Override public synchronized void init(ProcessingEnvironment env){ }
@Override public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment env) { }}
ProcessingEnvironment•Types
•ElementUtils
•Filer
•Messager
Elements
package foo.bar;public class Foo { // Type Element
String bar; // Variable Element
public void hoge() { // Executable Element System.out.println("Hello!"); }}
Types
package foo.bar;public class Foo {
String bar;
public void hoge() { System.out.println("Hello!"); }}
@SupportedSourceVersion(...)@SupportedAnnotationTypes(...)public class MyProcessor extends AbstractProcessor {
@Override public synchronized void init(ProcessingEnvironment env){ }
@Override public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment env) { }}
@Override public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment env) {
Set<? extends Element> elements = env.getElementsAnnotatedWith(GET.cl ass);
// generating code for elements
}
JavaPoet
package foo.bar;public final class Foo { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); }}
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);
Element to JavaPoet•AnnotationSpec#get
•AnnotationMirror to AnnotationSpec
•MethodSpect#overriding
•Override MethodElement
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);}
TypeSpec.Builder typeSpecBuilder = TypeSpec.interfaceBuilder( classElement.getSimpleName() + "$RetryStale") .addModifiers(Modifier.PUBLIC);
Interfacepublic interface Foo$$RetryStale
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)
Method
methodElement.getAnnotationMirrors() .stream() .map(AnnotationSpecUtil::generate) .forEach(builder::addAnnotation);
@GET("/foo/{user}")
Convenient Libraries•Google Auto-Service
•Truth
•Making your tests and their error messages more readable and discoverable
•Google Compile testing
AutoService
@AutoService(Processor.class)public class MyProcessor extends AbstracProcessor {
...
}
•Generating META-INF/services/javax.annotation.processing.Processor
Truth and Compile testing
ASSERT.about(javaSources()) .that(ImmutableList.of(inputFile)) .processedWith(new MyProcessor()) .compilesWithoutError() .and() .generatesSources(outputFile);
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