注解和注解处理器

0.导语

Java 作为一门低语法糖的语言,核心在其虚拟机的实现,语言层面提供的“黑科技”并不多,而注解就是其中比较重要的一点。注解在 Java5 中开始加入,在 Java6 中对外暴露出注解处理器的接口供程序员来按需处理注解。现如今,不论是安卓客户端还是Java后端技术栈都使用到了大量注解相关的库和框架,甚至可以说是到了“泛滥成灾”的地步。在其中相对比较重要的便是自定义注解处理器了,本文将从两个方面来进行介绍:

  • 注解的通俗解释及使用
  • 注解的处理方式,以安卓中的 ButterKnife 库为例,通过运行时期处理注解和编译时期通过注解处理器来处理注解这两种方式,实现和 ButterKnife 库相似的功能

1.什么是注解

注解,通俗的来说,就是像注释一样,是由程序员在代码中加入的一种“标注”,不影响所编写的原有代码的执行。而这种标注(注解)可以被编码用的IDE、编译器、类加载器的代理程序、其他第三方工具以及原有代码运行期间读取和处理,生成一些新的辅助代码或是提示,从而节省时间,提升效率。这些工具读取注解的时机是根据注解的生命周期来定的,注解的生命周期就是其“存在寿命”,分为三种:

  • 源代码时期注解,即注解只出现在 .java 文件中,编译后便不再出现在生成的.class 文件中,这一阶段,对注解的处理有两种方式:

    • 被 IDE 中代码检查工具读取,如图 1-1中的 1 处,实时地提示程序员缩写代码中的错误,例如 @Override 注解就是用来检查程序员所复写的父类方法的签名一致性的;
    • 在原代码编译期间,被程序员在编译器中所注册的注解处理器所读取,如图 1-1中的 2 处,用于生成新的源代码文件参与编译,省去重复地书写样板代码的成本,提升效率,编译之后便被去除,不再出现在 .class 文件中;
  • 字节码时期注解,即出现在 .class 文件中的注解,对这类注解的处理主要涉及字节码的修改,需要使用ASM等类库,根据处理时机的不同,也可分为两种方式:

    • 在源代码编译后处理:如果当前工程采用了Gradle、Maven等构建工具来构建,则在源代码被编译为 .class 文件后,可以通过相应地脚本调用使用了ASM库编写的第三方工具来读取文件中的注解并修改 .class 文件中的虚拟机指令,没有采用构建工具,也可以通过命令行来手动更新 .class 文件,虚拟机加载.class 文件时不会加载字节码级注解,如图 1-1中的 3 处所示;
    • 在类加载时处理:通过代理程序,在类加载器加载.class 文件前,读取文件中的注解修改字节码,但并不保存,即原有的.class文件内容不变,内存中处理过的字节码的类被类加载器直接加载到虚拟机中,同样的并不加载.class 文件中的字节码级注解,如图 1-1中的 4 处所示;
  • 运行时期注解,即注解被类加载器加载到内存当中,和类的其他构成元素一样被放置于元数据区,供堆区中相应的 class 对象访问,换句话说,此时的注解可以通过反射的方式来读取和处理,这时的处理过程往往是由程序员在编码期间就确定的,是运行效率最低但却是最容易实现的一种注解处理方式,如图 1-1中的 5 处所示;

图1-1.java注解处理流程

2.处理注解

由前所述,对注解的处理由处理时机的不同可以有不同的实现方式。这里介绍开发中最常用的两种方式,运行时期处理注解和编译时期通过自定义注解处理器来处理,口说无凭,先设立一个目标,ButterKnife 是安卓中很有名的利用注解来进行控件绑定的库,我将通过上述两种注解处理方式来实现类似 ButterKnife 库的功能。

2.1 ButterKnife 的简单介绍

ButterKnife 这个库主要用于安卓端控件绑定的问题。在很多流程类似的GUI程序中,都是先将 xml 等标签语言书写的界面文件读入并解析,在内存中生成视图界面中所有控件所对应的控件树对象,之后将控件树中的控件实体对象和逻辑控制类(安卓中的Activity)中的对象引用进行绑定,需要由程序员手工进行,如下所示:

public class SourceTestActivity extends AppCompatActivity {

    private static final String TAG = SourceTestActivity.class.getSimpleName();

    // 控件引用
    private TextView mTextView;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 读入文件,解析生成对象树
        setContentView(R.layout.activity_source_test);
        
        // 根据控件id在控件树中找到对应的实体控件对象并绑定
        mTextView = findViewById(R.id.tv);
        mButton = findViewById(R.id.btn);
        
        // 绑定按键控件的点击事件处理回调
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
            }
        });
    }
}

界面中有一个文本控件和一个按钮控件,都需要程序员通过 findViewById 和 setOnClickListner 方法来绑定实体 View 和其对应的回调方法。通过 ButterKnife 库可以将代码简化成如下所示:

public class SourceTestActivity extends AppCompatActivity {

    private static final String TAG = SourceTestActivity.class.getSimpleName();
    
    // 用注解标记引用需要绑定的控件的id
    @BindView(R.id.tv)
    TextView mTextView;
    @BindView(R.id.btn)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_source_test);
        // 执行绑定过程
        ButterKnife.bind(this);
    }

    // 用注解标记事件回调需要绑定的控件id
    @OnClick(R.id.btn)
    public void test(View v) {
        Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
    }
}

在需要绑定实体 View 的引用上加上 @BindView 注解,在需要绑定的回调方法上加上 @OnClick 注解,最后在代码中通过 ButterKnife.bind() 方法来实现绑定。上述例子由于需要绑定的控件和方法较少,看不出优势,但当需要绑定的对象很多是,ButterKnife 库可以帮我们节省很多重复的 findViewById 和 setOnClickListner 方法的书写。

2.2 反射处理运行时注解实现 “ButterKnife”

2.2.1 自定义注解

首先我们需要自定义注解 @BindView 和 @OnClick
BindView

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    @IdRes int value();
}

@Target 元注解中的ElementType.FIELD参数设定了@BindView 注解所适用的场景为 Field ,即属性字段;@Retention 元注解中的RetentionPolicy.RUNTIME 参数设定了@BIndView 注解的生命周期为运行时,即 .java .class 加载到内存中三个时期,@BindView 注解一直都存在。

OnClick

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    @IdRes int[] value() default {View.NO_ID};
}

OnClick 注解生命周期和 BindView 一致,不同在于适用的目标为方法(ElementType.METHOD)

2.2.2 运行时反射绑定

public class ButterKnife {

    /**
     * 负责 将容器类中的监听方法绑定到容器类中声明的指定 view 上
     *
     * @param container 容器类,待绑定方法和被绑定的 view 都位于其中, 一般是 Activity or Fragment
     */
    public static void bind(final Activity container) {

        // key: view's resId; value: view
        SparseArray<View> viewsMap = new SparseArray<>(8);

        Class<?> cls = container.getClass();

        // 获取已注解的 view 域, 并且帮其 findViewById
        for (Field viewField: cls.getDeclaredFields()){
            BindView fieldAnnotation = viewField.getAnnotation(BindView.class);
            if (fieldAnnotation != null) {
                viewField.setAccessible(true);
                try {
                    // 进行 View 绑定
                    View viewRef = container.findViewById(fieldAnnotation.value());
                    viewField.set(container, viewRef);
                    viewsMap.put(fieldAnnotation.value(), viewRef);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }

        // 获取已注解的方法域, 并且帮其通过动态代理 setOnClickListener
        for(final Method onClickMethod: cls.getDeclaredMethods()){
            OnClick methodAnnotation = onClickMethod.getAnnotation(OnClick.class);
            if (methodAnnotation != null) {
                int[] viewResIds = methodAnnotation.value();
                for (int resId : viewResIds) {
                    View viewToAttach;
                    if ((viewToAttach = viewsMap.get(resId)) != null) {
                        onClickMethod.setAccessible(true);
                        viewToAttach.setOnClickListener(
                                (View.OnClickListener) Proxy.newProxyInstance(null, new Class[]{View.OnClickListener.class},
                                        new InvocationHandler() {
                                            @Override
                                            public Object invoke(Object proxy, Method method, Object[] args) throws
                                                    InvocationTargetException, IllegalAccessException {
                                                return onClickMethod.invoke(container, args);
                                            }
                                        })
                        );
                    }
                }
            }
        }
    }
}

绑定方法主要是假的ButterKnife中的静态bind方法,其中做了两件事:

  • 首先通过反射遍历了所有的属性字段的,找出其中附加了@BindView注解的域,即View的引用,通过@BindView注解中设置的View的resId找到View实体,并绑定到View引用上,最后将其缓存到SparseArray中。
  • 之后,通过反射遍历所有方法域,找出其中附加了@OnClick注解的方法,由于Java中方法不是对象,不能独立存在,必须存在于类中,所以需要通过动态代理机制来代理View.OnClickListener接口,“包裹”所注解的方法,最后通过SparseArray的键值resId找到View实体进行回调方法的绑定。
    最后是Activity中的使用,界面中一共是两个按钮控件,绑定同一个回调方法,使用的形式上和ButterKnife库是类似的:
    RuntimeTestActivity.java
public class RuntimeTestActivity extends AppCompatActivity {

    @BindView(R.id.btn_one)
    private Button btnTest1;

    @BindView(R.id.btn_two)
    private Button btnTest2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_runtime_test);
        ButterKnife.bind(this);
    }

    @OnClick({R.id.btn_one, R.id.btn_two})
    public void onClick(View v) {
        Toast.makeText(v.getContext(), ((TextView)v).getText(), Toast.LENGTH_SHORT).show();
    }

}

综上,虽然使用的形式上和 ButterKnife 相类似,但原理上却完全不同,我们所写的伪ButterKnife库中的注解都是运行时注解,即源文件中的注解也会出现在最后的运行时内存中,可以通过反射的方式拿到注解,从而获得被注解的域或方法,最后在运行时再进行绑定,由于待绑定的元素都需要通过反射来拿到,故效率较低。

2.3 编译时注解处理器处理注解实现 “ButterKnife”

反射处理运行时注解的效率较低,那我们有没有办法减少反射的调用呢?回顾真ButterKnife库的作用,主要是减少书写findViewById之类的样板代码的书写,如果我们可以提前生成这些绑定方法的模板代码,只是通过反射调用唯一的bind()方法,那就可以适当提升效率,实际上,真ButterKnife库中也是这么做的。首先看伪ButterKnife库工程的结构:


图2-1.项目结构
  • app为安卓的测试module,用于测试自定义的伪ButterKnife库;
  • butterknife-annotation 为包含自定义注解的module;
  • butterknife-compiler 为自定义注解处理器的module,这里将注解处理器和所能处理的注解分在两个不同的module,主要是因为注解处理器实际上只是被编译器在编译时调用,并不属于工程,所以在打包时可以将其不打入apk中以节省空间,如果自定义的注解和注解处理器处在同一module,那么使用时,app module中会找不到自定义注解,所以需要将注解独立成butterknife-annotation module ,在 app 和注解处理器 butterknife-compiler module 中分别引用,自定义注解处理器在编译时读取源文件中的注解,生成新的源代码文件参与编译;
  • butterknife-library module 负责通过反射调用 butterknife-compiler 中自动生成的代码

2.3.1 自定义注解

BindView

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

OnClick

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface OnClick {
    int value();
}

可以看到和反射式伪ButterKnife库中的自定义注解所不同的是注解生命周期的定义,@Retention(RetentionPolicy.SOURCE)声明了这些注解都是源码级注解,即编译后在 .class 文件中便不存在了,注意建立butterknife-annotation 库时,选择该库为 java library,如下图所示:


图2-2.java library

最后的module工程结构如下:


图2-3.annotation目录结构

2.3.2 自定义注解处理器

自定义注解处理器的基本原理是将程序员自定义的注解处理器注册到编译器中,编译器在编译源文件时调用该注解处理器处理源文件中的注解,处理这些注解时是无法修改源文件的,只能生成新的源文件,生成的新文件会继续调用注解处理器处理,这是一个递归的过程,一轮一轮地处理,直到没有新的源文件生成,我们正是利用这点来自动生成绑定控件的模板代码。大致原理如下图所示:


图2-4.编译器原理

编译器编译源文件后生成语法树(AST),即结构化后的源文件元素,并将其传入自定义的注解处理器,自定义的注解处理器可以通过RoundEnvironment获得这些语法元素来处理,最后生成字节码文件。想要自己实现注解处理器并注册到编译器中的具体步骤如下:

  1. 创建java lib butterknife-cmplier和创建 butterknife-annotation的方式一致;
  2. 创建Processor,代码如下:
    BindViewProcessor.java
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({Constants.FULL_NAME_ANNO_BINDVIEW, Constants.FULL_NAME_ANNO_ONCLICK})
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Messager mMessager;
    private Elements mElementUtils;
    private Map<String, Creator> mProxyMap = new HashMap<>();

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

    // @Override
    // public Set<String> getSupportedAnnotationTypes() {
    //     HashSet<String> supportTypes = new LinkedHashSet<>();
    //     supportTypes.add(BindView.class.getCanonicalName());
    //     return supportTypes;
    // }
    //
    // @Override
    // public SourceVersion getSupportedSourceVersion() {
    //     return SourceVersion.latestSupported();
    // }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "processing...");
        mProxyMap.clear();
        
        // 获取源文件中带有 BindView 注解的域
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : elements) {
            VariableElement variableElement = (VariableElement) element;
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            Creator proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new Creator(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
            int id = bindAnnotation.value();
            proxy.putFieldElement(id, variableElement);
        }
        
        // 获取源文件中带有 OnClick 注解的方法
        elements = roundEnvironment.getElementsAnnotatedWith(OnClick.class);
        for(Element element: elements){
            ExecutableElement methodElement = (ExecutableElement) element;
            TypeElement classElement = (TypeElement) methodElement.getEnclosingElement();
            String fullClassName = classElement.getQualifiedName().toString();
            Creator proxy = mProxyMap.get(fullClassName);
            if (proxy == null) {
                proxy = new Creator(mElementUtils, classElement);
                mProxyMap.put(fullClassName, proxy);
            }
            OnClick bindAnnotation = methodElement.getAnnotation(OnClick.class);
            int id = bindAnnotation.value();
            proxy.putMethodElement(id, methodElement);
        }
      
        // 生成java代码
        for (String key : mProxyMap.keySet()) {
            Creator proxyInfo = mProxyMap.get(key);
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCode())
                    .indent("    ")
                    .addFileComment("generate file, do not modify!")
                    .build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        mMessager.printMessage(Diagnostic.Kind.NOTE, "process finish ...");
        return true;
    }
}

Constants.java

public class Constants {

    public static final String FULL_NAME_ANNO_BINDVIEW = "com.danielchen.demo.butterknife_annotation.BindView";
    public static final String FULL_NAME_ANNO_ONCLICK = "com.danielchen.demo.butterknife_annotation.OnClick";
}

从代码中可以看出,注解处理器主要是继承AbstractProcessor并实现 process() 方法, 可选择实现 init()、getSupportedAnnotationTypes() 、
getSupportedSourceVersion()三个方法,其中,getSupportedAnnotationTypes() 、
getSupportedSourceVersion()是返回本注解处理器所支持的注解和源代码版本,可以在注解处理器上加上@SupportedSourceVersion()
@SupportedAnnotationTypes()注解实现同样的效果,init()方法主要是为了初始化一些全局的工具。

  1. 向编译器注册本注解处理器,可以手动注册:在main文件夹下,创建路径 META-INF/services/,在其中创建文件javax.annotation.processing.Processor,在文件中加入注解处理器的全限定名;这样做很麻烦,更简便的方法是引入google的auto-service库,gradle 文件中远程依赖 'com.google.auto.service:auto-service:1.0-rc4',在注解处理器类上加上@AutoService(Processor.class)即可,这样会自动创建上述路径和文件,如下图所示:
图2-5.auto-services

4.实现注解处理器的process()方法,形参Set是本处理器可处理的注解类型,就是@SupportedAnnotationTypes()中所设置的类型,RoundEnvironment 则提供了本轮次的语法树元素,如果方法返回true,则后续其他处理器不能处理这些注解,否则可以处理,类似点击事件的拦截。上述代码中将读取到的属性字段和方法对应的语法元素存入 Creator,Creator 代码如下:
Creator.java

public class Creator {

    private String mBindingClassName;
    private String mFullPackageName;
    private ClassName mClassName;

    private Map<Integer, VariableElement> mVariableElementMap;
    private Map<Integer, ExecutableElement> mExecutableElementMap;

    public Creator(PackageElement pkgElement, TypeElement classElement) {
        mVariableElementMap = new HashMap<>(8);
        mExecutableElementMap = new HashMap<>(8);

        this.mClassName = ClassName.get(classElement);
        this.mFullPackageName = pkgElement.getQualifiedName().toString();
        this.mBindingClassName = classElement.getSimpleName().toString() + "_ViewBinding";
    }

    public String getFullPackageName() {
        return mFullPackageName;
    }

    public void putFieldElement(int id, VariableElement element) {
        mVariableElementMap.put(id, element);
    }

    public void putMethodElement(int id, ExecutableElement element) {
        mExecutableElementMap.put(id, element);
    }

    public TypeSpec generateJavaCode() {
        return TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateBindMethod())
                .build();
    }

    private MethodSpec generateBindMethod() {

        String paramName = "activity";
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(mClassName, paramName, Modifier.FINAL);

        for (int resId : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(resId);
            String viewName = element.getSimpleName().toString();
            String viewType = element.asType().toString();
            methodBuilder.addStatement("$L.$L = ($L)$L.findViewById($L)",
                    paramName, viewName, viewType, paramName, resId);

            if (mExecutableElementMap.containsKey(resId)) {
                ExecutableElement methodElement = mExecutableElementMap.get(resId);
                String methodName = methodElement.getSimpleName().toString();

                ClassName viewClass = ClassName.get("android.view", "View");
                ClassName clickClass = ClassName.get("android.view.View", "OnClickListener");

                TypeSpec onClickListener = TypeSpec.anonymousClassBuilder("")
                        .addSuperinterface(clickClass)
                        .addMethod(MethodSpec.methodBuilder("onClick")
                                .addAnnotation(Override.class)
                                .addModifiers(Modifier.PUBLIC)
                                .addParameter(viewClass, "v")
                                .returns(TypeName.VOID)
                                .addStatement("$L.$L(v)", paramName, methodName)
                                .build())
                        .build();
                methodBuilder.addStatement("$L.$L.setOnClickListener($L)", paramName, viewName, onClickListener);
            }
        }
        return methodBuilder.build();
    }
}

Creator 中提供了根据缓存的语法元素生成源代码的方法,这里可以采用用字符串自己拼装的形式,但这样太过复杂且不易维护,所以推荐使用JavaPoet库来生成源代码,可读性更强。
最终, butterknife-compiler 的工程结构如下:

图2-6.butterknife-compiler目录结构

依赖关系如下:

apply plugin: 'java-library'

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

    // 远程依赖 auto-service 库, 用于注册自定义注解处理器到编译器中
    implementation 'com.google.auto.service:auto-service:1.0-rc4'

    // 远程以来 javapoet 库,用于动态生成代码
    implementation 'com.squareup:javapoet:1.11.1'

    // 本地依赖 声明了自定义注解的 module
    implementation project(':butterknife-annotation')
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

最终自动生成的源代码如下:

// generate file, do not modify!
package com.danielchen.demo.demo_reflect;

import android.view.View;
import android.view.View.OnClickListener;
import java.lang.Override;

public class SourceTestActivity_ViewBinding {
    public void bind(final SourceTestActivity activity) {
        activity.mTextView = (android.widget.TextView)activity.findViewById(2131230914);
        activity.mButton = (android.widget.Button)activity.findViewById(2131230755);
        activity.mButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                activity.test(v);
            }
        });
    }
}

2.3.3 反射调用生成的代码

最终通过butterknife-libraray中的 ButterKnife 类来调用自动生成的 SourceTestActivity_ViewBinding 的 bind() 方法,ButterKnfie 代码如下:

public class ButterKnife {

    public static void bind(Activity activity) {
        try {
            Class<?> bindViewClass = Class.forName(activity.getClass().getName() + "_ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

由于自动生成的类名和方法名bind都是约定好的,所以可以这么用,最后在测试代码中调用ButterKnife.bind()即可完成绑定,如下:

public class SourceTestActivity extends AppCompatActivity {

    private static final String TAG = SourceTestActivity.class.getSimpleName();

    @BindView(R.id.tv)
    TextView mTextView;
    @BindView(R.id.btn)
    Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_source_test);
        // butterknife-library bind方法中通过反射调用 SourceTestActivity_ViewBinding 中的bind方法完成绑定
        ButterKnife.bind(this);
    }

    @OnClick(R.id.btn)
    public void test(View v) {
        Toast.makeText(v.getContext(), ((TextView) v).getText(), Toast.LENGTH_SHORT).show();
    }
}

综上,实际上,仿照真ButterKnife库的原理,我们是将控件绑定的模板代码先自动生成,运行时在用过一次反射调用即完成绑定,比运行时注解处理的方式的多次反射绑定要好那么一点,当然真ButterKnife库中的实际代码要更复杂些,不过大致原理是这样。

3.总结

本文顺着注解的生命周期大致描述了处理注解的不同时机和处理方法。参照ButterKnife库的原理,对运行时动态处理注解和编译时根据注解生成代码这两种处理注解的方式进行了代码演示,限于本人的水平,写的比较冗长,很多细节也没能描述清楚,之后会将代码传至github,感觉还是看代码理解更快。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,652评论 2 59
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,057评论 25 707
  • 什么是注解注解分类注解作用分类 元注解 Java内置注解 自定义注解自定义注解实现及使用编译时注解注解处理器注解处...
    Mr槑阅读 1,062评论 0 3
  • Java中的注解是个很神奇的东西,还不了解的可以看下一小时搞明白自定义注解(Annotation)。现在很多And...
    顾明伟阅读 3,094评论 1 3
  • 下午工作有点倦怠, 问同事鹏哥,你闺女也是一口东北腔吗? 鹏哥:不是的,来,我让你听听她唱的歌 鹏哥满脸幸福的从录...
    璞玉丁阅读 195评论 0 0