2016-08-09 22:52:35 a1018875550 阅读数 7625
  • Spring注解驱动开发

    《Spring注解驱动开发》是一套帮助我们深入了解Spring原理机制的教程; 现今SpringBoot、SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解、原理,比如@Conditional、@Import、@EnableXXX等。如果掌握这些底层原理、注解,那么我们对这些高层框架就能做到高度定制,使用的游刃有余;

    17478 人正在学习 去看看 雷丰阳

Android注解越来越引领潮流,比如 Dagger2, ButterKnife, EventBus3 等,他们都是注解类型,而且他们都有个共同点就是编译时生成代码,而不是运行时利用反射,这样大大优化了性能;而这些框架都用到了同一个工具就是:APT(Annotation Processing Tool ),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。

今天我们要自己实现的就是类似ButterKnife的简单的view初始化和点击事件;

先看下整个项目的目录结构:
图片名称

  • inject :API module用来把生成的文件与控件相关联
  • viewinject-annotation :注解module
  • viewinject-compiler : 用来生成java文件module

先从最简单入手,注解moudle:
1.创建名字为viewinject-annotation的java类型module
2.该module只有两个类:

1.BindView用来对成员变量进行注解,并且接收一个 int 类型的参数

 * Created by JokAr on 16/8/6.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2.OnClick对方法进行注解,接收一个或一组 int 类型参数,相当于给一组 View 指定点击响应事件。

/**
 * Created by JokAr on 16/8/6.
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

注解module就完成了,下面看看API module

1.首先创建一个Android moudle 的inject,然后创建interface

/**
 * Created by JokAr on 16/8/6.
 */
public interface Inject<T> {

    void inject(T host, Object object, Provider provider);
}
/**
 * Created by JokAr on 16/8/6.
 */
public interface Provider {
    Context getContext(Object object);

    View findView(Object object, int id);
}

因为我们需要生成的文件是这么写的:

public class MainActivity$$ViewInject implements Inject<MainActivity> {
  @Override
  public void inject(final MainActivity host, Object source, Provider provider) {
    host.textView = (TextView)(provider.findView(source, 2131427412));
    host.button1 = (Button)(provider.findView(source, 2131427413));
    View.OnClickListener listener = new View.OnClickListener() {
      @Override
      public void onClick(View view) {
        host.click();
      }
    } ;
    provider.findView(source, 2131427412).setOnClickListener(listener);
  }
}

当然这个生成文件是根据自己需求生成,然后需要一个类来关联自己的activity类与生成的类:

/**
 * Created by JokAr on 16/8/6.
 */
public class ViewInject {
    private static final ActivityProvider activityProvider = new ActivityProvider();

    private static final ViewProvider viewProvider = new ViewProvider();
    private static final ArrayMap<String, Inject> injectMap = new ArrayMap<>();

    public static void inject(Activity activity) {
        inject(activity, activity, activityProvider);
    }

    public static void inject(View view) {
        inject(view, view);
    }

    private static void inject(Object host, View view) {
        inject(host, view, viewProvider);
    }

    private static void inject(Object host, Object object, Provider provider) {
        String className = host.getClass().getName();
        try {
            Inject inject = injectMap.get(className);

            if (inject == null) {
                Class<?> aClass = Class.forName(className + "$$ViewInject");
                inject = (Inject) aClass.newInstance();
                injectMap.put(className, inject);
            }
            inject.inject(host, object, provider);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

使用方法就是:

ViewInject.inject(this);
  • host 表示注解 View 变量所在的类,也就是注解类
  • object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
  • provider 是一个接口,定义了不同对象(比如 Activity、View 等)如何去查找目标 View,项目中分别为 Activity、View 实现了 Provider 接口(具体实现参考项目代码)
  • 为了提高效率,避免每次注入的时候都去找 Inject 对象,用一个 Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。

API module类就完成了

再看viewinject-compilermodule:
首先创建名为iewinject-compiler的Java module ,然后在该module的buile.gradle加上一些依赖:

compile project(':viewinject-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
  • Javapoet是square一个工具,提供了各种 API 让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
  • auto-service 主要用于注解 Processor,对其生成 META-INF 配置信息。

首先创建ViewInjectProcesser类:

/**
 * Created by JokAr on 16/8/8.
 */
@AutoService(Processor.class)
public class ViewInjectProcesser extends AbstractProcessor {
    private Filer mFiler; //文件相关的辅助类
    private Elements mElementUtils; //元素相关的辅助类
    private Messager mMessager; //日志相关的辅助类

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }

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

        return false;
    }


     /**
     * 指定使用的 Java 版本。通常返回SourceVersion.latestSupported()。
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 指定哪些注解应该被注解处理器注册
     * @return 
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }


}

  • @AutoService 来注解这个处理器,可以自动生成配置信息。
  • 在 init() 可以初始化拿到一些实用的工具类。
    这里涉及到了Element 元素,借用一下别人的分析:

这个类的的基本内容就完成了,
现在创建BindViewField类,来解析BindView注解类来获取用该注解的相关信息

/**
 * Created by JokAr on 16/8/8.
 */
public class BindViewField {
    private VariableElement mVariableElement;
    private int mresId;

    public BindViewField(Element element) throws IllegalArgumentException{
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mVariableElement = (VariableElement) element;

        BindView bindView = mVariableElement.getAnnotation(BindView.class);
        mresId = bindView.value();
        if (mresId < 0) {
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
                            mVariableElement.getSimpleName()));
        }
    }

   /**
     * 获取变量名称
     * @return
     */
    public Name getFieldName() {
        return mVariableElement.getSimpleName();
    }

    /**
     * 获取变量id
     * @return
     */
    public int getResId() {
        return mresId;
    }

    /**
     * 获取变量类型
     * @return
     */
    public TypeMirror getFieldType() {
        return mVariableElement.asType();
    }
}

创建OnClickMethod类来解析使用OnClick注解的方法,获取相关信息

public class OnClickMethod {
    private ExecutableElement mExecutableElement;
    private int[] resIds;
    private Name mMethodName;

    public OnClickMethod(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.METHOD) {
            throw new IllegalArgumentException(
                    String.format("Only methods can be annotated with @%s",
                            OnClick.class.getSimpleName()));
        }

        mExecutableElement = (ExecutableElement) element;

        resIds = mExecutableElement.getAnnotation(OnClick.class).value();

        if (resIds == null) {
            throw new IllegalArgumentException(String.format("Must set valid ids for @%s",
                    OnClick.class.getSimpleName()));
        } else {
            for (int id : resIds) {
                if (id < 0) {
                    throw new IllegalArgumentException(String.format("Must set valid id for @%s",
                            OnClick.class.getSimpleName()));
                }
            }
        }
        mMethodName = mExecutableElement.getSimpleName();
        List<? extends VariableElement> parameters = mExecutableElement.getParameters();

        if (parameters.size() > 0) {
            throw new IllegalArgumentException(
                    String.format("The method annotated with @%s must have no parameters",
                            OnClick.class.getSimpleName()));
        }
    }

    /**
     * 获取方法名称
     * @return
     */
    public Name getMethodName() {
        return mMethodName;
    }

    /**
     * 获取id数组
     * @return
     */
    public int[] getResIds() {
        return resIds;
    }
}

然后重点就是生成Java代码文件的类:

/**
 * Created by JokAr on 16/8/8.
 */
public class AnnotatedClass {

    private TypeElement mTypeElement;
    private ArrayList<BindViewField> mFields;
    private ArrayList<OnClickMethod> mMethods;
    private Elements mElements;

    public AnnotatedClass(TypeElement typeElement, Elements elements) {
        mTypeElement = typeElement;
        mElements = elements;
        mFields = new ArrayList<>();
        mMethods = new ArrayList<>();
    }

    public String getFullClassName() {
        return mTypeElement.getQualifiedName().toString();
    }

    public void addField(BindViewField field) {
        mFields.add(field);
    }

    public void addMethod(OnClickMethod method) {
        mMethods.add(method);
    }

    public JavaFile generateFile() {
        //generateMethod
        MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.PROVIDER,"provider");

        for(BindViewField field : mFields){
            // find views
            injectMethod.addStatement("host.$N = ($T)(provider.findView(source, $L))",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()), field.getResId());
        }

        for(OnClickMethod method :mMethods){
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(TypeUtil.ANDROID_VIEW, "view")
                            .addStatement("host.$N()", method.getMethodName())
                            .build())
                    .build();
            injectMethod.addStatement("View.OnClickListener listener = $L ", listener);
            for (int id : method.getResIds()) {
                // set listeners
                injectMethod.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
            }
        }

        //generaClass
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewInject")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.INJET, TypeName.get(mTypeElement.asType())))
                .addMethod(injectMethod.build())
                .build();

        String packgeName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile.builder(packgeName, injectClass).build();
    }
}

具体的可以看javapoet的API,然后我们需要完善ViewInjectProcesser类,增加:

private Map<String, AnnotatedClass> mAnnotatedClassMap;

 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();

        try {
            processBindView(roundEnv);
            processOnClick(roundEnv);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            error(e.getMessage());
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateFile().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {

        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotatedClass.addField(bindViewField);
        }
    }

    private void processOnClick(RoundEnvironment roundEnv) throws IllegalArgumentException {
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            OnClickMethod onClickMethod = new OnClickMethod(element);
            annotatedClass.addMethod(onClickMethod);
        }
    }
private void error(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }

实际使用

Android Stduio 2.2以下使用方法

在项目的根目录的build.gradle添加:

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

在项目的主module的build.gradle添加:

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

compile project(':viewinject-annotation')
compile project(':inject')
apt project(':viewinject-compiler')

Android Stduio 2.2以上使用方法


compile project(':viewinject-annotation')
compile project(':inject')
annotationProcessor project(':viewinject-compiler')

在自己的activity类使用:

/**
 * Created by JokAr on 16/8/8.
 */
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.textView)
    TextView textView;
    @BindView(R.id.button1)
    Button button1;


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

    }

    @OnClick(R.id.textView)
    public void click() {
        Toast.makeText(getApplicationContext(), "hello", Toast.LENGTH_SHORT).show();
    }


}

点击makeProject 就编译完成后就可以在主项目module的/build/generated/source/apt/debug 目录下看到生成的java类文件了

一个学习级的apt项目就完成了。

项目源码

实战项目:Android6.0权限管理 工具,我用java重写别人的kotlin项目;地址:
https://github.com/a1018875550/PermissionDispatcher

2017-05-11 10:52:29 loveBuZhiDao 阅读数 382
  • Spring注解驱动开发

    《Spring注解驱动开发》是一套帮助我们深入了解Spring原理机制的教程; 现今SpringBoot、SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解、原理,比如@Conditional、@Import、@EnableXXX等。如果掌握这些底层原理、注解,那么我们对这些高层框架就能做到高度定制,使用的游刃有余;

    17478 人正在学习 去看看 雷丰阳

编写android 注解解释器(apt)

步骤:
1. 编写注解文件
2. 写注解解释器,编译时生成注解文件
3. 运行时通过反射调用注解生成的类和方法

编写注解文件

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Components {
    String[] value();
}

写注解解释器,编译时生成注解文件

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

    private Filer mFiler;
    private Messager mMessager;

    private Map<String, String> mStaticRouterMap = new HashMap<>();
    private List<String> mAutoRouterList = new ArrayList<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
    }

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new HashSet<>();
        set.add(AutoRouter.class.getCanonicalName());
        set.add(StaticRouter.class.getCanonicalName());
        set.add(Component.class.getCanonicalName());
        set.add(Components.class.getCanonicalName());
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        mStaticRouterMap.clear();
        mAutoRouterList.clear();

        try {
            Set<? extends Element> mainAppElement = roundEnvironment.getElementsAnnotatedWith(Components.class);
            if (!mainAppElement.isEmpty()) {
                processInstaller(mainAppElement);
                return true;
            }
            processComponent(roundEnvironment);
        } catch (Exception e) {
            mMessager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
        }

        return true;
    }

    private void processInstaller(Set<? extends Element> mainAppElement) throws IOException {
        TypeElement typeElement = (TypeElement) mainAppElement.iterator().next();
        JavaFileObject javaFileObject = mFiler.createSourceFile(Config.ROUTER_MANAGER, typeElement);
        PrintWriter writer = new PrintWriter(javaFileObject.openWriter());

        writer.println("package " + Config.PACKAGE_NAME + ";");
        writer.println("public class " + Config.ROUTER_MANAGER + " {");
        writer.println("public static void " + Config.ROUTER_MANAGER_METHOD + "() {");

        Components componentsAnnotation = typeElement.getAnnotation(Components.class);
        String[] components = componentsAnnotation.value();
        for (String item : components) {
            writer.println(Config.FILE_PREFIX + item + ".router();");
        }

        writer.println("}");
        writer.println("}");

        writer.flush();
        writer.close();
    }

    private void processComponent(RoundEnvironment roundEnvironment) throws Exception {
        Set<? extends Element> compElements = roundEnvironment.getElementsAnnotatedWith(Component.class);
        if (compElements.isEmpty()) { return;}

        Element item = compElements.iterator().next();
        String componentName = item.getAnnotation(Component.class).value();

        Set<? extends Element> routerElements = roundEnvironment.getElementsAnnotatedWith(StaticRouter.class);
        for (Element e : routerElements) {
            if (! (e instanceof TypeElement)) { continue;}
            TypeElement typeElement = (TypeElement) e;
            String pattern = typeElement.getAnnotation(StaticRouter.class).value();
            mStaticRouterMap.put(pattern, typeElement.getQualifiedName().toString());
        }

        Set<? extends Element> autoRouterElements = roundEnvironment.getElementsAnnotatedWith(AutoRouter.class);
        for (Element e : autoRouterElements) {
            if (!(e instanceof TypeElement)) { continue;}
            TypeElement typeElement = (TypeElement) e;
            mAutoRouterList.add(typeElement.getQualifiedName().toString());
        }

        writeComponentFile(componentName);
    }

    private void writeComponentFile(String componentName) throws Exception {
        String className = Config.FILE_PREFIX + componentName;
        JavaFileObject javaFileObject = mFiler.createSourceFile(className);
//        javaFileObject.delete();

        PrintWriter printWriter = new PrintWriter(javaFileObject.openWriter());

        printWriter.println("package " + Config.PACKAGE_NAME + ";");

        printWriter.println("import android.app.Activity;");
        printWriter.println("import android.app.Service;");
        printWriter.println("import android.content.BroadcastReceiver;");

        printWriter.println("public class " + className + " {");
        printWriter.println("public static void router() {");

        // // Router.router(ActivityRule.ACTIVITY_SCHEME + "shop.main", ShopActivity.class);
        for(Map.Entry<String, String> entry : mStaticRouterMap.entrySet()) {
            printWriter.println("org.loader.router.Router.router(\"" + entry.getKey()
                    +"\", "+entry.getValue()+".class);");
        }

        for (String klass : mAutoRouterList) {
            printWriter.println("if (Activity.class.isAssignableFrom(" + klass + ".class)) {");
            printWriter.println("org.loader.router.Router.router(org.loader.router.rule.ActivityRule.ACTIVITY_SCHEME + \""
                    +klass+"\", " + klass + ".class);");
            printWriter.println("}");

            printWriter.println("else if (Service.class.isAssignableFrom(" + klass + ".class)) {");
            printWriter.println("org.loader.router.Router.router(org.loader.router.rule.ServiceRule.SERVICE_SCHEME + \""
                    +klass+"\", " + klass + ".class);");
            printWriter.println("}");

            printWriter.println("else if (BroadcastReceiver.class.isAssignableFrom(" + klass + ".class)) {");
            printWriter.println("org.loader.router.Router.router(org.loader.router.rule.ReceiverRule.RECEIVER_SCHEME + \""
                    +klass+"\", "+klass+".class);");
            printWriter.println("}");
        }

        printWriter.println("}");
        printWriter.println("}");
        printWriter.flush();
        printWriter.close();
    }
}

这里写图片描述

使用注解


@Component("bbs")
public class BBS {
}

编译后生成注解类

这里写图片描述

public class Router_bbs {
public static void router() {
if (Activity.class.isAssignableFrom(org.loader.bbslib.BBSActivity.class)) {
org.loader.router.Router.router(org.loader.router.rule.ActivityRule.ACTIVITY_SCHEME + "org.loader.bbslib.BBSActivity", org.loader.bbslib.BBSActivity.class);
}
else if (Service.class.isAssignableFrom(org.loader.bbslib.BBSActivity.class)) {
org.loader.router.Router.router(org.loader.router.rule.ServiceRule.SERVICE_SCHEME + "org.loader.bbslib.BBSActivity", org.loader.bbslib.BBSActivity.class);
}
else if (BroadcastReceiver.class.isAssignableFrom(org.loader.bbslib.BBSActivity.class)) {
org.loader.router.Router.router(org.loader.router.rule.ReceiverRule.RECEIVER_SCHEME + "org.loader.bbslib.BBSActivity", org.loader.bbslib.BBSActivity.class);
}
}
}

通过反射调用

public class RouterHelper {

    public static void install() {
        try {
            Class<?> klass = Class.forName(Config.PACKAGE_NAME + "." + Config.ROUTER_MANAGER);
            Method method = klass.getDeclaredMethod(Config.ROUTER_MANAGER_METHOD);
            method.invoke(null);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
}
2019-06-02 08:58:37 ShuSheng0007 阅读数 1593
  • Spring注解驱动开发

    《Spring注解驱动开发》是一套帮助我们深入了解Spring原理机制的教程; 现今SpringBoot、SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解、原理,比如@Conditional、@Import、@EnableXXX等。如果掌握这些底层原理、注解,那么我们对这些高层框架就能做到高度定制,使用的游刃有余;

    17478 人正在学习 去看看 雷丰阳

【版权申明】非商业目的可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/90734159
出自:shusheng007

前言

现在Android开发中许多流行的第三方库都使用了注解生成代码的方式,例如 ButterKnife, Dagger2Glide等等,初次接触时感觉好神奇,后来感觉很实用,再后来想知道怎么实现的,再后来就是试试自己也写一个。

我们先人很早就说过:纸上得来终觉浅,绝知此事要躬行,所以让我们干起来吧。

概述

Java 是从 Java 5 开始支持注解的,是其很重要的语言特性,然而其潜力的发掘也仅仅是近几年的事情。Java 5 真的是Java 一个里程碑式的版本,各种重要特性均源自此版本。

注解一般有两种方式处理方式:
第一:运行时获取并使用注解信息,此方式属于反射范畴,有性能损失,例如Retrofit2等。
第二:编译时获取注解信息,并据此产生java代码文件,无性能损失,例如ButterKnife, Dagger2等。

此次我们重点关注第二种方式

注解处理器

预备知识

  1. 理解java注解的基本知识, 如果不熟请参考 秒懂 Java注解类型(@Annotation)
  2. 理解java代码生成库 javapoet的基本使用方法 JavpPoet

工作原理

将我们的源码使用特殊的注解标记,然后在编译成字节码的时候根据这些注解生成新的java源码,省去了我们自己编写此部分代码的工作量。

项目结构

我们准备实现类似于ButterKnife中最常使用到的两个注解,一个@BindView,一个是@OnClick
在这里插入图片描述
如上图所示,整个项目主要有如下四个部分

  1. app : Android 项目主module,是一个 android application
  2. annotation: 定义了所有的注解, 是一个 java library
  3. compiler: 处理相关注解,关键的代码都在这个module里,是一个 java library
  4. binder: 处理生成代码的调用,是一个android library

接下来我们就从零构建一个Android 注解处理器,并在此过程中掌握相关的知识。首先我们先使用AS创建一个Android 工程,这个就不用再啰嗦了,如果这个都不会还搞个毛线。

定义注解

在工程中添加一个java library类型的module,取名annotation

Android Studio -> file -> new module -> java library

定义三个注解

  1. BindView:其实不用做过多的解释,用过ButterKnife都知道这是干啥的。其作用就是将XML layout 文件中的view映射到代码中
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
    @IdRes int value();
}

note: 如果对注解的定义有疑问, 秒懂 Java注解类型(@Annotation) 走起来。

  1. OnClick : 映射view的一个click事件,当一个view被点击后,此注解标记的对应方法就会触发。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface OnClick {
    @IdRes int value();
}
  1. Keep :这个就比较有意思了,我们准备使用此注解告诉编译器,不要混淆被此注解标记的类。由于混淆的存在程序中使用反射的类都不应该被混淆,不然运行时就找不到了。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface Keep {
}

至此,注解就定义完了,接下来我们定义本文最重要的部分,注解处理器

定义注解处理器

原理

注解处理器和编译是并行运行的,注解处理器是一轮一轮的运行的。例如我们有一个包含注解的类MainActivity

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_hello)
    TextView tvHello;
  }
  

在第一轮处理注解时,我们生成了新的类,而这个类使用**@Keep**注解标记了,那么注解处理器就会开始下一轮的处理,直到生成的类里面再也没有任何注解了。

@Keep
public class MainActivity$Binding {
...
}

上述为java注解的运行机制,我们要使用这个机制还需要将我们写的注解处理器注册到编译器认识的地方去。

创建项目

新建一个module,取名为compiler,类型必须为java library,如下所示

Android Studio -> file -> new module -> java library

  1. 新建一个类Processor,这类必须继承至 AbstractProcessor

  2. 必须严格按如下图所示的命名方式创建一个文件javax.annotation.processing.Processor,文件的内容为你定义的注解处理器的全类名,例如此处就是Processor的全类名:top.ss007.compiler.Processor

    note: 这是java原生的方法,如果你觉得麻烦,可以使用google的 AutoService,使用@AutoService标记你的注解处理器,这个库就会为你产生这个注册目录了。

在这里插入图片描述
Processor 一般会重写父类的4个方法:

  1. init
    初始化工作,我们可以得到一些有用的工具,例如 Filer,我们需要它将生成的代码写入文件中
  2. process
    最重要的方法,所有的注解处理都是在此完成
  3. getSupportedAnnotationTypes
    返回我们所要处理的注解的一个集合
  4. getSupportedSourceVersion
    要支持的java版本

下面是processor的代码片段

public class Processor extends AbstractProcessor {
    private Filer filer;
    private Messager messager;
    private Elements elementUtils;

    //每个存在注解的类整理出来,key:package_classname value:被注解的类型元素
    private Map<String, List<Element>> annotationClassMap = new HashMap<>();

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {      
        if (!roundEnv.processingOver()) {
           ...
            for (Map.Entry<String, List<Element>> entry : annotationClassMap.entrySet()) {
                /*
                创建要生成的类,如下所示
                @Keep
                public class MainActivity$Binding {}*/
                TypeSpec.Builder classBuilder = TypeSpec.classBuilder(generatedClassName)
                        .addModifiers(Modifier.PUBLIC)
                        .addAnnotation(Keep.class);
               ...

                //将类写入文件中
                try {
                    JavaFile.builder(packageName,
                            classBuilder.build())
                            .build()
                            .writeTo(filer);
                } catch (IOException e) {
                    messager.printMessage(Diagnostic.Kind.ERROR, e.toString());
                }
            }
        }
        return true;
    }


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return new TreeSet<>(Arrays.asList(
                BindView.class.getCanonicalName(),
                OnClick.class.getCanonicalName(),
                Keep.class.getCanonicalName()));
    }

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

}

上面的代码也没有什么神奇的,就是在process方法里面处理我们定义的注解,然后使用javapoet 生成相应的代码,而这些代码本应该是我们程序员自己手写的,现在自动生成了,具体的看源码吧,文章最后贴出了源代码的地址。

定义初始化模块

代码是生成了,那我们怎么使用呢,注解生成器的代码需要我们主动去触发,还记得ButterKnife中那句ButterKnife.bind(this); 吗,就是干这个事情的

新建一个module,取名为binder,类型必须为android library,如下所示

Android Studio -> file -> new module -> android library

定义一个Binding类

public class Binding {
    private Binding(){}

    private static <T extends Activity> void instantiateBinder(T target,String suffix){
        Class<?> targetClass=target.getClass();
        String className=targetClass.getName();
        try {
            Class<?>bindingClass =targetClass
                    .getClassLoader()
                    .loadClass(className+suffix);
            Constructor<?> classConstructor=bindingClass.getConstructor(targetClass);
            try {
                classConstructor.newInstance(target);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Unable to invoke " + classConstructor, e);
            } 
            ...
    }

    public static <T extends Activity> void bind(T activity) {
        instantiateBinder(activity, BindingSuffix.GENERATED_CLASS_SUFFIX);
    }
}

我们通过反射调用生成类的构造方法,而所有的绑定逻辑都在那个类的构造方法里面,如下所示

  public MainActivity$Binding(MainActivity activity) {
    bindViews(activity);
    bindOnClicks(activity);
  }

是否还记得前面我们使用@Keep标记了生成的类,它的作用就是防止此类被混淆,确保反射调用可以顺利进行。那keep是怎么起作用的呢?

在此module的 proguard-rules.pro 文件中添加如下代码。

-keep class top.ss007.annotation.Keep
-keep @top.ss007.annotation.Keep public class *
-keepclassmembers @top.ss007.annotation.Keep class ** { *;}

然后在其build.gradle 文件中添加如下代码

apply plugin: 'com.android.library'
android {
    defaultConfig {
        ...
        consumerProguardFiles 'proguard-rules.pro'
    }
...

}

上面的代码表示使用此module的 android application 都要使用 proguard-rules.pro 文件中定义的规则,我们就是在这个文件中申明不要混淆带有@Keep标记的类的。

如何使用

这个就比较简单了,毕竟我们使用ButterKnife那么多年了。在app的build.gradle 文件中添加

dependencies {
    ...
    implementation project(':binder')
    annotationProcessor project(':compiler')
    ...

在Activity中绑定即可

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_hello)
    TextView tvHello;

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

    @OnClick(R.id.btn_click)
    void onHelloBtnClick(View v){
        tvHello.setText("hello android annotation processor");
    }
}

总结

本文只是描述如何开发一个 Android Annotation Processor, 要想深入掌握这部分内容,必须自己动手实践一下,一些著名开源项目可能太大,那就从这个玩具开始吧,等了解了原理后就可以去看著名的开源库了,然后进而写一个著名的开源库。

好了,我媳妇叫我吃饭了,就到这吧… 翠花,今天啥饭啊?

源码地址:AnnotationProcessorToys

参考文章:Android Annotation Processing Tutorial

2016-04-05 15:27:57 zhuwentao2150 阅读数 9103
  • Spring注解驱动开发

    《Spring注解驱动开发》是一套帮助我们深入了解Spring原理机制的教程; 现今SpringBoot、SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解、原理,比如@Conditional、@Import、@EnableXXX等。如果掌握这些底层原理、注解,那么我们对这些高层框架就能做到高度定制,使用的游刃有余;

    17478 人正在学习 去看看 雷丰阳

使用Android Studio时,每当我们新建一个文件,系统就会给我们自动生成一个文件注释信息,如下



在实际开发程序时,我们都会给文件添加上一些关于文件的注释信息,如开发者的姓名,开发的时间,修改的版本号,开发者的联系方式等等。那么在android studio中该如何设置呢?


一、打开菜单栏File -> Settings,或者面板上的小扳手图标



二、在弹出的框框中找到File and Code Templates,并选中右边Templates中的Class



三、然后选中Includes中的File Header,之后就可以在右边的编辑框里面自定义我们的注释信息了

比如我们输入

/**
 * 作者:zhuwentao2150 on ${DATE}.
 * 邮箱:xxxxxx@163.com
 * 版本:v1.0
 */



设置完后,我们新建的java文件注释就会变成我们自定义的


2019-09-20 13:29:50 qq_14876133 阅读数 95
  • Spring注解驱动开发

    《Spring注解驱动开发》是一套帮助我们深入了解Spring原理机制的教程; 现今SpringBoot、SpringCloud技术非常火热,作为Spring之上的框架,他们大量使用到了Spring的一些底层注解、原理,比如@Conditional、@Import、@EnableXXX等。如果掌握这些底层原理、注解,那么我们对这些高层框架就能做到高度定制,使用的游刃有余;

    17478 人正在学习 去看看 雷丰阳

Android注解基本使用

注解(Annotation),是源码中特殊的语法元数据,类、方法、变量、参数都可以被注解。利用注解可以标记源码以便编译器为源码生成文档和检查代码,也可以让编译器和注解处理器在编译时根据注解自动生成代码,甚至可以保留到运行时以便改变运行时的行为。

内置注解
  • @Override 注解用来表明该方法是重写父类方法,编译器会负责检查该方法与父类方法的声明是否一致。
  • @Deprecated 注解用来表明该元素已经被废弃不建议使用了。
  • @SuppressWarnings 注解用来表示编译器可以忽略特定警告。
@Retention

注解的保留位置:

  1. RetentionPolicy.SOURCE:源码阅读时使用,注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
  2. RetentionPolicy.CLASS:编译时使用,注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
  3. RetentionPolicy.RUNTIME:运行时使用,注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

这3个生命周期分别对应于:Java源文件(.java文件) —> .class文件 —> 内存中的字节码

@Target

注解作用目标:

  1. ElementType.TYPE:类、接口、枚举
  2. ElementType.FIELD:属性
  3. ElementType.METHOD:类中的方法
  4. ElementType.PARAMETER:方法参数
  5. ElementType.CONSTRUCTOR:构造器
  6. ElementType.LOCAL_VARIABLE:局部变量
  7. ElementType.ANNOTATION_TYPE:注解
  8. ElementType.PACKAGE:包

Java自定义注解

自定义注解:

/**
 * 定义类注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassInfo {
    String value();
}

/**
 * 定义字段注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldInfo {
    int[] value();
}

/**
 * 定义方法注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodInfo {
    String name() default "默認值";
    String data();
    int age() default 100;
}

/**
 * 使用注解
 */
@ClassInfo("测试类")
public class TestAnnotation {
    @FieldInfo(value = {1, 2})
    public String fieldA = "字段属性";

    @FieldInfo(value = {100})
    public int fieldB = 100;

    @MethodInfo(name = "这是一个方法名字", data = "hello")
    public static String getMethodA() {
        return TestAnnotation.class.getSimpleName();
    }

    @MethodInfo(data = "hello2")
    public static String getMethodB() {
        return TestAnnotation.class.getSimpleName();
    }
}

运行注解:

public class Test {
    public static void main(String[] args) {
        Class<TestAnnotation> cls = TestAnnotation.class;

        ClassInfo classInfo = cls.getAnnotation(ClassInfo.class);
        if (classInfo != null) {
            System.out.println(cls);
            System.out.println(classInfo);
            System.out.println(classInfo.value());
        }

        System.out.println("---------------------");

        Field[] fields = cls.getDeclaredFields();
        for (Field field : fields) {
            FieldInfo fieldInfo = field.getAnnotation(FieldInfo.class);
            if (fieldInfo != null) {
                System.out.println(field);
                System.out.println(fieldInfo);
                System.out.println(Arrays.toString(fieldInfo.value()));
            }
        }

        System.out.println("---------------------");
        Method[] methods = cls.getDeclaredMethods();
        for (Method method : methods) {
            MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);
            if (methodInfo != null) {
                System.out.println(method);
                System.out.println(methodInfo);
                System.out.println(methodInfo.name());
                System.out.println(methodInfo.data());
                System.out.println(methodInfo.age());
            }
        }
    }
}

输出信息:

class com.immersionstatusbar.test.TestAnnotation
@com.immersionstatusbar.test.ClassInfo(value=测试类)
测试类
---------------------
public java.lang.String com.immersionstatusbar.test.TestAnnotation.fieldA
@com.immersionstatusbar.test.FieldInfo(value=[1, 2])
[1, 2]
public int com.immersionstatusbar.test.TestAnnotation.fieldB
@com.immersionstatusbar.test.FieldInfo(value=[100])
[100]
---------------------
public static java.lang.String com.immersionstatusbar.test.TestAnnotation.getMethodA()
@com.immersionstatusbar.test.MethodInfo(name=这是一个方法名字, age=100, data=hello)
这是一个方法名字
hello
100
public static java.lang.String com.immersionstatusbar.test.TestAnnotation.getMethodB()
@com.immersionstatusbar.test.MethodInfo(name=默認值, age=100, data=hello2)
默認值
hello2
100

Android中注解替代枚举/常量

通常定义固定值可以直接使用枚举或static final定义常量。

  • 使用枚举缺点比较消耗资源,优点是可以限制取值范围;
  • 使用常量优点是相对轻量级,确实不能限制取值范围。
// 枚举
public enum TYPE_ENUM {
    TYPE_A,
    TYPE_B,
    TYPE_C
}

// 常量
public class TYPE_CONSTANT {
    public static final int TYPE_A = 1;
    public static final int TYPE_B = 2;
    public static final int TYPE_C = 3;
}

// 定义变量
// 取值只能为{TYPE_A,TYPE_B,TYPE_C}
TYPE_ENUM enumVar;
// 取值不受限制
int type;

为了解决上面的这些问题,Android提供一个有用的注解,简化枚举和常量

implementation 'com.android.support:support-annotations:25.2.0'

如:@IntDef@StringDef

public class Person {
    public static final int TYPE_A = 1;//男
    public static final int TYPE_B = 2;//女
    public static final int TYPE_C = 3;//保密

    // 定义一个注解
    @IntDef({TYPE_A, TYPE_B, TYPE_C})
    @interface Type {
    }

    //年齡限制:0-100
    private int age;
    //性別限制:男,女,保密
    private int sex;

    @Type
    public int getSex() {
        return sex;
    }

    public void setSex(@Type int sex) {
        this.sex = sex;
    }

    public void setAge(@IntRange(from = 0, to = 100) int age) {
        this.age = age;
    }

    @IntRange(from = 0, to = 100)
    public int getAge() {
        return age;
    }
}

使用:

Person person = new Person();
person.setAge(18);
person.setAge(Person.TYPE_A);
没有更多推荐了,返回首页