注解入门:反射+APT


一、什么是注解

在我们写代码时,有时候会有一些配置信息,比如Spring里面的每个Service,最开始我们是在xml里面定义这些服务的名称和路径的,后来呢,觉得这些配置文件需要与Java源代码时刻同步,很容易写了一个服务后忘记在xml里面定义了。

于是,就有了注解,通过直接给一个类加上表示它是Service的注解,就无需再去在xml里面定义它了,这样就只需要在一个地方维护这些信息就好了。其它部分所需的信息则通过自动的方式来生成。

注解有点类似注释,不同的是注解不是提供代码功能的说明,而是实现程序功能的重要组成部分。

比如,我们可以自定义一个注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface IName {
    String value() default "";
}

好了,注解已经定义好了,那么,如何应用自定义的注解呢?一般来说,有两种方案:运行时通过反射获取注解的值,编译时通过apt技术获取注解的值

二、注解的几种应用

1、反射

定义一个类,使用@IName注解

@IName("苹果")
public class Fruit {
}

然后在调用的地方,这样获取注解值

public class AnnoClient {

    public static void main(String[] args) {
        Fruit fruit = new Fruit();

        Class<? extends Fruit> clazz = fruit.getClass();

        if (clazz.isAnnotationPresent(IName.class)) {
            IName annotation = clazz.getAnnotation(IName.class);
            String name = annotation.value();
            System.out.println("水果的名称是:" + name);
        }
    }
}

输出结果如下,说明我们成功获取到了注解的值。这就是注解+反射的一个经典应用场景。

水果的名称是:苹果

下面,我就用反射做一个简单的Android上的IOC框架
定义一个注解:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface IView {
    int value() default 0;
}

根据反射处理注解的工具类

public class InjectUtil {

    public static void bind(Activity activity) {
        Class<? extends Activity> clazz = activity.getClass();
        //遍历所有的字段
        for (Field field : clazz.getDeclaredFields()) {
            //处理字段
            if (field.isAnnotationPresent(IView.class)) {
                IView anno = field.getAnnotation(IView.class);
                int value = anno.value();
                try {
                    field.setAccessible(true);
                    field.set(activity, activity.findViewById(value));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然后在Activity中调用

public class ReflectActivity extends AppCompatActivity {

    @IView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        InjectUtil.bind(this);
        tv.setText("ReflectActivity");
    }
}

这样,我们就不用再去重复写findViewById了。不过呢,反射比较耗性能,为了提高逼格,下面我们用APT技术实现同样的功能。

2、APT

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。

现在有很多主流库都用上了 APT,比如 Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐!

不过,普通的APT技术在根据注解自动生成Java文件时,那个书写的语法简直太蛋疼了(相当于你用StringBuffer通过不断的append,写了一个Java类),所以呢,这里隆重推荐一个APT的编辑库:JavaPoet

首先,在AndroidStudio上配置一下apt和JavaPoet,因为Android环境中找不到AbstractProcessor,所以呢,我们需要添加一个Java Library(我命名为aptlib)来写自动生成代码的代码。
1、配置Project的build.gradle文件:

dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }

2、配置aptlib这个mudule的build.gradle文件:

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.7.0'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

3、配置app的build.gradle文件:

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

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.soubu.aptstudy"
        minSdkVersion 15
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.1.0'
    testCompile 'junit:junit:4.12'
    compile project(':aptlib')
    apt project(':aptlib')
    compile project(':reflectlib')
}

4、开始写具体的代码
先来个简单的栗子:
假如我想要用注解+apt自动帮我生成这么一个类:

package com.soubu.helloworld;

import java.lang.String;
import java.lang.System;

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

首先,我定义一个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Hello {
    String value() default "var";
}

然后,写apt的注解处理器

package com.soubu.hello;

import com.google.auto.service.AutoService;
import com.soubu.di.DIActivity;
import com.soubu.di.DIView;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

/**
 * 作者:余天然 on 2017/2/6 上午11:01
 */
@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Hello.class.getCanonicalName());
    }

    /**
     * package com.example.helloworld;
     * public final class HelloWorld {
     *  public static void main(String[] args) {
     *      System.out.println("Hello, APT");
     * }
     * }
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Hello.class);
        for (Element element : elements) {
            Hello hello = element.getAnnotation(Hello.class);
            String var = hello.value();

            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, " + var)
                    .build();
            TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(main)
                    .build();
            JavaFile javaFile = JavaFile.builder("com.soubu.helloworld", helloWorld)
                    .build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
}

最后,在app的场景类使用这个注解

@Hello("哈哈哈")
public class HelloClient {
}

其实,这个场景类啥都没干,就定义了这个注解的值为“哈哈哈”而已。然后,编译一下项目



就可以在这里看到自动生成的类了。


好了,apt自动生成代码的栗子我们已经了解了,下面,我们就也用APT做一个简单的Android上的IOC框架

首先,定义了两个注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DIActivity {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DIView {
    int value() default 0;
}

然后,使用JavaPoet来编写注解处理器

package com.soubu.di;


import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;

/**
 * 作者:余天然 on 2017/2/6 上午11:21
 */
@AutoService(Processor.class)
public class DIProcessor extends AbstractProcessor {

    private Elements elementUtils;

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 规定需要处理的注解
        return Collections.singleton(DIActivity.class.getCanonicalName());
    }

    /**
     * public final class DIMainActivity extends MainActivity {
     *  public static void bindView(MainActivity activity) {
     *      activity.tv = (android.widget.TextView) activity.findViewById(R.id.tv);
     * }
     * }
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(DIActivity.class);
        for (Element element : elements) {
            // 判断是否Class
            TypeElement typeElement = (TypeElement) element;
            List<? extends Element> members = elementUtils.getAllMembers(typeElement);
            MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(TypeName.VOID)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");
            for (Element item : members) {
                DIView diView = item.getAnnotation(DIView.class);
                if (diView == null) {
                    continue;
                }
                bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)", item.getSimpleName(), ClassName.get(item.asType()).toString(), diView.value()));
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
                    .superclass(TypeName.get(typeElement.asType()))
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(bindViewMethodSpecBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    private String getPackageName(TypeElement typeElement) {
        return elementUtils.getPackageOf(typeElement).getQualifiedName().toString();
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_7;
    }
}

最后,在我们的app的Activity中使用

@DIActivity
public class AptActivity extends AppCompatActivity {

    @DIView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        DIAptActivity.bindView(this);
        tv.setText("ReflectActivity");
    }
}

注意,这里的DIAptActivity是我们自动生成的,要使用的话,必须先编译一下项目。


现在,关于注解的两种使用方式:运行时注解和编译时注解,想必大家都有了一定认识了,那么,还不赶紧玩一把?

最后,奉上源码:Github-AnnoDemo



作者:天然鱼
链接:https://www.jianshu.com/p/dce26aa75060
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

猜你喜欢

转载自blog.csdn.net/eydwyz/article/details/79642279