自定义注解框架实现

 日常项目开发中,注解使用越来越广泛,我们会经常用到各类注解框架为我们减轻工作中的一些重复劳动,比如AndroidAnnotation、Dagger2、ButterKnife等这些大名鼎鼎的框架。这些框架有一个共同点就是使用编译时生成代码代替反射,大大优化了性能。
 那为什么一个小小的注解@BindView就可以实现view的查找功能呢?本文就来一探究竟,打造一个类似ButterKnife简单实现view绑定的注解框架。
 在开始之前,我们先认识一项关键技术APT(Annotation Processing Tool)。官方解释:APT是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。简单的说就是APT在编译时把注解生成代码,关于APT更多知识此处不展开,有兴趣可以百度查看相关资料。
 伴随着去年Android Gradle 插件 2.2版本发布,android-apt的作者在官网发表声明后续将不会继续维护android-apt,并推荐大家使用 Android 官方插件annotationProcessor。不过由于很多框架还是用的APT,本文还是基于APT实现。

一、创建工程

 首先在Android studio里新建一个Android工程APTDemo。为了使用android-apt插件,需要在工程的build.gradle中加入依赖。

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

二、创建 annotation Module

 新建一个Java Library Module,命名annotation,然后创建一个BindView注解类。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS) 
public @interface BindView {
    int value() default 0;
}

 BindView的target是FIELD,只对成员变量进行注解,有一个int类型的参数。默认值为0,用来传入view的Id。
 build.gradle,采用默认就好。

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

三、创建 compiler Module

 新建一个Java Library Module,命名compiler。
 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'
    compile project(':annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

 依赖了annotation模块,因为要使用annotation中定义的Bindview注解,另外引入了auto-service和javapoet库。auto-service ,主要用于注解 Processor,对其生成 META-INF 配置信息。javapoet可以通过预先设置好的规则,自动生成Java 代码文件,这个真是个好东西。
 新建BindViewProcesor类,这个类就是整个注解框架的核心,包括自动生成代码等。

@AutoService(Processor.class)
public class BindViewProcesor extends AbstractProcessor {

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

 下面我们来编辑BindViewProcesor类,主要逻辑都在process方法中。
 为了一些工具类的方便使用,重写父类的init方法。

 private Elements elements;
 private Filer filer;

 @Override
 public synchronized void init(ProcessingEnvironment processingEnvironment) {
     super.init(processingEnvironment);
     elements = processingEnvironment.getElementUtils();
     filer = processingEnvironment.getFiler();
 }

 Elements是元素操作相关的辅助类,主要用于获取各种元素,结构类似DOM树。Filer是文件操作的辅助类。parentAndChildMap是一个map,用来存放类与方法的对应关系。
 修改getSupportedAnnotationTypes,指定可以被注解处理器处理的类型,这里是BindView.class。

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

 另外还有一个指定java版本的方法getSupportedSourceVersion,这里我们使用注解@SupportedSourceVersion(SourceVersion.RELEASE_7)。
 下面就是核心方法process的修改,大致的步骤如下:
  1、获取所有标注了@BindView注解的的Element。
  2、遍历标注了注解的Element集合,获取每一个Element的父元素,由于@BindView的Target是FIELD,那么父元素就是该FIELD的类即TypeElement。当然由于一个类中可能有多个标记了@BindView的字段,此处用HashMap来存放之间的对应关系。
  3、遍历HashMap,通过javapoet生成目标类。先指定MethodSpec的生成规则,接着指定TypeSpec和JavaFile的规则,最后调用javaFile.writeTo(filer)生成Java文件。

 for (Map.Entry<TypeElement, List<Element>> entry : parentAndChildMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView").addModifiers(Modifier.PUBLIC,
                    Modifier.STATIC).returns(void.class).addParameter(ClassName.get(typeElement.asType()), "activity");
            List<Element> childElementList = entry.getValue();
            for (Element element : childElementList) {
                int id = element.getAnnotation(BindView.class).value();
                String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element.getSimpleName
                        (), ClassName.get(element.asType()).toString(), id);
                methodSpecBuilder.addStatement(statement);
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers
                    (Modifier.PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod
                    (methodSpecBuilder.build()).build();
            JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(), 
                    typeSpec).build();
            try {
                javaFile.writeTo(filer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

 具体javapoet的使用方法,可以参见Javapoet源码。后面有时间再单独写一篇关于javapoet的。
 完整的代码如下:

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcesor extends AbstractProcessor {
    private Elements elements;
    private Filer filer;
    private HashMap<TypeElement,List<Element>> parentAndChildMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elements = processingEnvironment.getElementUtils();
        filer = processingEnvironment.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        Set<? extends Element> fieldElements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (fieldElements == null) {
            return false;
        }
        parentAndChildMap = new LinkedHashMap<>();
        for (Element fieldEle : fieldElements) {
            TypeElement typeElement = (TypeElement) fieldEle.getEnclosingElement();
            if (parentAndChildMap.containsKey(typeElement)) {
                parentAndChildMap.get(typeElement).add(fieldEle);
            } else {
                List<Element> childEleList = new ArrayList<>();
                childEleList.add(fieldEle);
                parentAndChildMap.put(typeElement, childEleList);
            }
        }
        for(Map.Entry<TypeElement,List<Element>> entry:parentAndChildMap.entrySet()){
            TypeElement typeElement = entry.getKey();
            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(ClassName.get(typeElement.asType()), "activity");
            List<Element> childElementList = entry.getValue();
            for(Element element:childElementList){
                int id = element.getAnnotation(BindView.class).value();
                String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element
                        .getSimpleName(), ClassName.get(element.asType()).toString(), id);
                methodSpecBuilder.addStatement(statement);
            }
            TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers(Modifier
                    .PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod(methodSpecBuilder.build())
                    .build();
            JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(),
                    typeSpec).build();
            try {
                javaFile.writeTo(filer);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return true;
    }

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

四、使用注解

 为了在app模块中使用注解,需要在app的build.gradle配置注解的依赖。

compile project(':compiler')

 新建一个MainActivity,在textview上标记注解@BindView(R.id.text)

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

 然后编译一下整个工程,编译完成之后,会在app/build/generated/source/apt/debug/目录下自动生成BindView$$MainActivity类。

public final class BindView$$MainActivity extends MainActivity {
  public static void bindView(MainActivity activity) {
    activity.textView = (android.widget.TextView)activity.findViewById(2131427415);
  }
}

 这个就是compiler中根据注解按照设定的规则使用Javapoet自动生成的。如果不知道在compiler中怎样重写process方法,我们可以先手动写出BindView$$MainActivity类,然后参考这个类再去想我们该怎样写自动生成代码的规则,这样会简单很多。
 此时MainActivity中textview还没有初始化,需要在onCreate方法中调用BindView$$MainActivity.bindView(this)进行注册。

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        BindView$$MainActivity.bindView(this);
        textView.setText("Hello World!");
 }

 此时就完成了通过一个@BindView注解实现view的初始化的目的,整个流程都是套路,具体实现就是compiler中的process方法了。当然本文中的自定义框架只是初步的实现了@BindView的功能,如果想要完整的实现类似ButterKnife的功能,可以参考ButterKnife的源码。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java自定义注解是指在Java语言中可以通过编写代码来定义自己的注解自定义注解可以提供一些额外的元数据信息,用于标记和描述Java代码中的某个元素。自定义注解可以用于类、方法、属性等各个层面。 实现自定义注解步骤如下: 1. 使用@Retention注解指定注解的保留策略,默认为RetentionPolicy.CLASS。可选的保留策略有三种:RetentionPolicy.SOURCE、RetentionPolicy.CLASS和RetentionPolicy.RUNTIME。 2. 使用@Target注解指定注解的作用目标,默认可以用于所有的Java元素。可选的作用目标包括ElementType.TYPE(类、接口、枚举等)、ElementType.FIELD(字段、枚举常量等)、ElementType.METHOD(方法)、ElementType.PARAMETER(方法参数)、ElementType.CONSTRUCTOR(构造方法)、ElementType.LOCAL_VARIABLE(局部变量)等。 3. 使用@interface关键字定义注解,并定义注解的属性。注解的属性以无参无异常抛出的方法的形式定义,可以指定默认值。 4. 在需要使用注解的地方使用自定义注解自定义注解可以携带信息,这些信息可以在运行时通过反射获取,对注解进行解析和处理自定义注解可以用于编写各种工具、框架和库,来增强程序的扩展性和灵活性。 实现自定义注解的一个典型应用场景是在Spring框架中的依赖注入(DI)和面向切面编程(AOP)中。通过自定义注解,可以标记需要注入的Bean,或者标记需要进行切面拦截的方法,从而实现依赖注入和切面编程的功能。 总的来说,Java自定义注解是Java语言提供的一种灵活的元编程机制,可以通过注解增加程序的可读性和可维护性,同时也可以用于实现一些特定的功能,如依赖注入和切面编程等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值