美文网首页
THE 10-STEP GUIDE TO ANNOTATION

THE 10-STEP GUIDE TO ANNOTATION

作者: 普莱 | 来源:发表于2017-04-19 11:39 被阅读0次

    原文链接:http://blog.stablekernel.com/the-10-step-guide-to-annotation-processing-in-android-studio

    When working on a testing library in my spare time, I thought that annotations would be useful to create a graph structure the same way that Dagger does for object dependency, but I was only experienced with writing annotations that were referenced at run-time.

    So I went to the web for tutorials, blog posts, and videos on annotation processing. I was able to find enough information to set up my own annotation processor, but there wasn’t a comprehensive walkthrough on how to set it up for Android. Since annotation processing is purely Java, all the tutorials showed the processor in its own project, but I wanted my processor in the same project as my Android app so that a call to build the project would also trigger a build for the annotation processor; after all I needed this to be responsive to the tests I would create. So when I was asked what my first blog post would be, I was ready.

    Annotations are a class of metadata that can be associated with classes, methods, fields, and even other annotations. This metadata can be accessed at runtime via reflection, but you can also access this metadata at build time via annotation processors. Annotation processors are a useful interface added in Java 6 that perform exhaustive searches over all annotations at build time and allow you to access reflective information regarding which element was annotated as well as any additional metadata stored in the corresponding annotation. Annotations and processors have not been deeply explored by mainstream developers until recently.

    Jake Wharton gives a great in-depth presentation on annotation processing, its history, and its use in Dagger/Dagger2.

    Hannes Dorfman gives a greatpresentationcovering just the concepts of how annotation processing works.

    Before we begin, let’s cover how we will be using annotation processing at a high level:

    When you build and run this code, the first thing that happens is the annotation processor code is compiled into a jar via a pre-build gradle task so it can be automatically included and used in our Android project (annotation processors are pure Java modules as of RELEASE_8 and will not have access to the Android API). Next, the build process will begin processing all annotations in our Android project, including the custom one that we will write. Our annotation processor will create a generated java class object that can be used inside of our Android code at runtime. In doing so we will have proven the concept of generating code from annotations at build time and using the generated code during runtime.

    The package structure is important and trying to rename or move packages to fix a mistake doesn’t always work as intended, so getting them right the first time makes life easier. The naming convention boils down to these two packages:

    =>com.stablekernel.annotationprocessor.processor

    =>com.stablekernel.annotationprocessor.processor

    For this tutorial we will start with an Empty Activity default “Hello, world!” app created through the Android Studio wizard and I will have my Project pane set to Project.

    1) Creating the processor module:

    Since we are starting from an existing app, we will first need to create the annotation processor in its own module by either right clicking the project folder and selectingNew>New Moduleor by going toFile>New>New Module.

    Because this is not an Android native feature you will need to create a Java library module, not Android.

    For this tutorial, the module will be named processor.

    Make sure that your package name is correct:.processor

    The class name is simply the first file it generates in the library, I chose to name it for the annotation processor we will be making.

    2) Setting the source compatibility:

    In thebuild.gradlefile for the in theapp/directory, set the android compile options.

    For this tutorial the compile options are:

    compileOptions {

    sourceCompatibility JavaVersion.VERSION_1_7

    targetCompatibility JavaVersion.VERSION_1_7

    }

    And in the processor module’sbuild.gradlefile:

    Add the compatibility options:

    sourceCompatibility = 1.7

    targetCompatibility = 1.7

    Note that while the gradle file indicates that these arguments aren’t used, they are needed during the build process.

    3) Creating the annotation:

    Before we get to the processor, let’s create ourCustomAnnotation.classas an annotation in this new module.

    For now we will leave the auto-generated annotation empty as we only care about which elements are annotated in this tutorial.

    4) Creating the processor:

    The processor class should extend from theAbstractProcessorclass and be annotated with fully qualified paths of the annotation types that are expected to be handled (for this tutorial there is only the one) as well as the source version of Java. For this tutorial the source version is Java 7 but if your project is using Java 6 you would useRELEASE_6.

    @SupportedAnnotationTypes(“”)

    @SupportedSourceVersion(SourceVersion.RELEASE_7)

    The easiest way to get the fully qualified path of the supported annotation type is to copy it using Android Studio’s project pane.

    It is important that you use a correct qualified path name and if you refactor the class later that you update this annotation otherwise the build will fail and the error tracing is not the easiest to follow.

    Android Studio should now be notifying you that you need to implement theprocessmethod, so let’s do that before moving on.

    Then replace theprocessmethod with the following:

    @Override

    public boolean process(Set annotations, RoundEnvironment roundEnv) {

    StringBuilder builder = new StringBuilder()

    .append("package com.stablekernel.annotationprocessor.generated;\n\n")

    .append("public class GeneratedClass {\n\n") // open class

    .append("\tpublic String getMessage() {\n") // open method

    .append("\t\treturn \"");

    // for each javax.lang.model.element.Element annotated with the CustomAnnotation

    for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {

    String objectType = element.getSimpleName().toString();

    // this is appending to the return statement

    builder.append(objectType).append(" says hello!\\n");

    }

    builder.append("\";\n") // end return

    .append("\t}\n") // close method

    .append("}\n"); // close class

    try { // write the file

    JavaFileObject source = processingEnv.getFiler().createSourceFile("com.stablekernel.annotationprocessor.generated.GeneratedClass");

    Writer writer = source.openWriter();

    writer.write(builder.toString());

    writer.flush();

    writer.close();

    } catch (IOException e) {

    // Note: calling e.printStackTrace() will print IO errors

    // that occur from the file already existing after its first run, this is normal

    }

    return true;

    }

    To give a conceptual idea of what is happening here, theStringBuilderis creating a Java file with the package name in the generated namespace. This file is given a single method,getMessagewhich will return a string. That return value is being generated by finding each of the supported annotations and finding the name of the element associated with the annotation. In the case of this tutorial it will beMainActivityandonCreateas the two items annotated, so the generated file should look like this:

    Note that this is a generated file, it is created during the build process so you will not be able to view it until after the project has been built successfully. For reference you will find the file after a successful build in this directory:app/build/generated/source/apt/debug//GeneratedClass.java

    Also, we are writing a source file here viaWriterwhich serves our purpose for now but more complex file writing as your project develops may be made easier by third party libraries likeJavaPoet.

    5) Create the resource:

    Now that we have created our processor we will need to tell Java to use it by creating the javax Processor file, this is what the compiler looks for to know how to handle the annotations. The steps are a bit tedious, but this is because the processor expects a specific structure to be in place.

    From the processor module’s main directory, create a new directory calledresources

    Within this directory create a new directory calledMETA-INF

    Within this directory create a new directory calledservices

    Within this directory create a new file calledjavax.annotation.processing.Processor

    Inside this file you will put the fully qualified name of each of your processors, separated by a newline.They should autocomplete and in this instance we only have one so our file looks like this:

    6) Add android-apt:

    Next, apply theandroid-aptplugin by first updating thebuild.gradlefile for your project:

    And adding the buildscript dependency:

    classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

    Then in thebuild.gradlefile in theapp/directory:

    Apply the plugin:

    apply plugin: 'com.neenbedankt.android-apt'

    7) Set build dependencies:

    The main concept of this section is that we need to compile the processor and the annotation into a jar and then put that jar into our app module and make it available for reference, but this needs to be done before the app is built. First, we will update the dependencies to look for the jar file:

    dependencies {

    compile files('libs/processor.jar')

    testCompile 'junit:junit:4.12'

    compile 'com.android.support:appcompat-v7:23.1.1'

    }

    Then we will create a gradle task that will move the jar file into thelibs/folder

    task processorTask(type: Exec) {

    commandLine 'cp', '../processor/build/libs/processor.jar', 'libs/'

    }

    Finally, we establish the order of dependencies, so the:app:preBuilddepends on our newprocessorTask, which depends on:processor:build:

    processorTask.dependsOn(':processor:build')

    preBuild.dependsOn(processorTask)

    So the build sequence is:

    :processor:buildwill generate the jar file with our annotation and its processor

    processorTaskwill copy this jar file to the androidapp/libs/folder

    The:appmodule will now reference the new jar.

    To verify that all of this happens as expected perform a clean build by either going toBuild>Rebuildor in terminal executing

    ./gradlew :app:clean :app:build

    and you should see the build process run in the correct order and finish with aprocessor.jarfile in yourapp/libs/folder.

    8) Apply annotations:

    Now that the jar file containing the annotation and the annotation processor is in the android app we can reference theCustomAnnotationand can apply it to theMainActivityclass declaration and theonCreatemethod.

    But just applying these annotations will not create the generated class that the annotation processor is supposed to create, to do so we will need to rebuild again by going toBuild>Rebuildor in terminal executing

    ./gradlew :app:clean :app:build

    and now the generated file should be seen at this location:

    app/build/generated/source/apt/debug//GeneratedClass.java

    9) Verify annotations are working:

    In order to verify that the annotations are being processed, we will launch an alert dialog by adding the following code toMainActivity.javaand call it fromonCreate

    private void showAnnotationMessage() {GeneratedClass generatedClass = new GeneratedClass();String message = generatedClass.getMessage();            // android.support.v7.app.AlertDialognew AlertDialog.Builder(this).setPositiveButton("Ok", null).setTitle("Annotation Processor Messages").setMessage(message)

    .show();

    }

    Note that thisGeneratedClasswill not exist until a build has succeeded.

    10) Running the build:

    Building and running on a device now produces the following.

    This is the basis of annotation processing: by applying the@CustomAnnotationwe are able to intercept it at build time, create a generated file, then at runtime this generated file is able to be used.

    Are you an Android developer wanting to learn more? Check out "Level-up with Android Studio Shortcuts and Live Templates."

    ABOUT THE AUTHOR

    Chris Logan is an Android developer at stable|kernel. He has worked in a polyglot capacity for several Atlanta-based companies including Tech, ranging from Web Development to OpenGL/CUDA to Chrome extensions. You can find out what he’s currently up to on his website.

    COMMENTS

    Hesam

    2016/4/7 上午1:54:06

    Awesome bro, thanks.

    REPLY TOHESAM

    TellH Tom

    2016/7/30 上午12:18:45

    In short, this is a quite awesome article!

    But I think the Gradle task written by this would be better :

    task processorTask(type: Copy) {

    from '../processor/build/libs/processor.jar'

    into 'libs/'

    }

    REPLY TOTELLH TOM

    someone

    2016/8/28 下午6:52:27

    Full ACK I was going to suggest the same :)

    REPLY TOSOMEONE

    Luis Xu

    2016/8/22 下午5:16:44

    A wonderful post that I am searching for a long time. I want to translate it into Chinese and share to more local developers.

    REPLY TOLUIS XU

    Chris Logan

    2016/8/26 上午2:36:29

    Hi Luis,

    Thanks for the compliment! I ask that if you translate to Chinese you credit me with writing the original article. Thank you

    REPLY TOCHRIS LOGAN

    rekire

    2016/8/28 下午7:00:35

    Hi Chris,

    do you know how this works with jack and jill (the new build system)?

    See also this blog post: http://tools.android.com/tech-docs/jackandjill#TOC-Compilation-support there is a section about Annotation processing.

    I am just searching for references and found your great article (I wish I have found this earlier), as a tip for code generation you should use https://github.com/square/javapoet this generated nice formatted code.

    Feel free to mail me you should have my mail address via this form.

    REPLY TOREKIRE

    Chris Logan

    2016/9/13 上午4:55:52

    I mentioned JavaPoet at the end of section 4, it is great but just more than was needed for this project plus I wanted to drive home that you are writing code that writes code at build time which executes during runtime. As for Jack and Jill working with AnnotationProcessing for Android, there are already some open repos up that indicate it should work fine, just change `apt` to `annotationProcessor` in the build.gradle file.

    REPLY TOCHRIS LOGAN

    Alex

    2016/10/1 下午7:19:02

    Really useful. Great ThankYou!!!

    REPLY TOALEX

    Fil

    2016/12/26 上午1:57:26

    This is exactly what i need, but sadly it doesn't work. Build exception when using those lines of code:

    for (Element element : roundEnv.getElementsAnnotatedWith(CustomAnnotation.class)) {

    String objectType = element.getSimpleName().toString();

    // this is appending to the return statement

    builder.append(objectType).append(" says hello!\n");

    }

    and

    try { // write the file

    JavaFileObject source = processingEnv.getFiler().createSourceFile("here_goes_my_package_name.generated.GeneratedClass");

    Writer writer = source.openWriter();

    writer.write(builder.toString());

    writer.flush();

    writer.close();

    } catch (IOException e) {

    // Note: calling e.printStackTrace() will print IO errors

    // that occur from the file already existing after its first run, this is normal

    }

    please help.

    REPLY TOFIL

    Fil

    2016/12/26 上午2:28:49

    Sorry for the previous post. My bad. Works like a charm. You are a magician!

    Having problems with jvm on my machine. It doesn't quit after the compilation and has to be killed manually. Hence the false issue detection.

    REPLY TOFIL

    geethadevi

    2017/1/8 下午3:02:38

    Perfect blog about annotation process in android..really well researched..

    REPLY TOGEETHADEVI

    George

    2017/2/7 上午10:29:49

    Hi Chris,

    I'm a little bit confused about this:

    commandLine 'cp', '../processor/build/libs/processor.jar', 'libs/'

    What does cp mean?When I try to rebuild the project,Gradle reminds me this:

    Error:Execution failed for task ':app:processorTask'.

    > A problem occurred starting process 'command 'cp''

    And also I wonder how to set AnnotationProcessor with annotationProcessor.I read some article noticed that apt has been replaced by annotationProcessor.Sorry for my poor english...

    REPLY TOGEORGE

    George

    2017/2/7 下午5:38:35

    The Processor(extends AbstractProcessor) doen't work at all.There is no gerented class at "app/build/generated/source/apt/debug//GeneratedClass.java".It seems the process method in Processor doesn't run at all,I try to print some message at process method ,but there is no message in Gradle Console.I would appreciate it if you answer me with email.

    REPLY TOGEORGE

    Tom

    2017/2/15 上午12:38:08

    Nice article.

    I've created 2 submodules "annotation-processor" and "annotation-processor-api"

    annotation-processor module contains just the processor plus service declaration as you've described.

    annotation-processor-api contains just the annotation(s)

    Then in the app gradle file dependencies look like:

    dependencies {

    compile project(':annotation-processor-api')

    ...

    apt project(':annotation-processor')

    }

    This should work just fine without need to copy files and the processor classes aren't bundled in the app.

    REPLY TOTOM

    Afshin

    2017/2/23 下午4:00:08

    Thank you... It was awesome and complete.

    REPLY TOAFSHIN

    Prashant Kashetti

    2017/3/1 上午1:53:43

    Finally got this awesome tutorial about Annotation....Thank You

    REPLY TOPRASHANT KASHETTI

    First Name*

    Last Name

    Email*

    Website

    Comment*

    Subscribe to follow-up comments for this post

    相关文章

      网友评论

          本文标题:THE 10-STEP GUIDE TO ANNOTATION

          本文链接:https://www.haomeiwen.com/subject/psiczttx.html