相见恨晚的apt

作者: 41cf853949de | 来源:发表于2016-09-20 22:42 被阅读2698次

    android apt 自动代码生成


    前言
    随着开发的项目越来越复杂,module模块也是越来越多,多人合作的项目如果规范不到位的话,会出现各式各样的代码形态,对于后期的项目维护难以维持,更不用说要埋点,Log打印等功能了
    所以针对于已存在的项目想要更好的完成一些,买点,Log打印等功能,必定要是用AOP的开发模式,那么对于android来说,apt自然是首选的方案。 很多开源的框架也使用apt的能力来进行框架的功能开发如: Dagger , butterKnife , auto-value, RoboBinding ....
    所以为了能够更快的实现一些基础功能,我也准备采用apt的技术来处理我们的一些问题。
    此文章主要是记录在实现的过程中,一些库的使用和理解。 如果不正确的地方还请指出。

    总览项目结构



    工程说明:

    • app ( app module) 主工程运行项目的android工程
    • annotation (java module) 注解包
    • aptapi (java module) 对外提供的api
    • aptcompiler ( java module) 代码生成包
      工程初探:
      在讲解项目之前,我们先来看一下效果, 先说明一下,此工程是模拟了数据采集,可以通过注解方便的实现数据采集,当然数据采集本身来说是非常简单的,当然在实际的开发过程中,我们可能需要写很多代码来完成数据采集的打印比如:
    public void onClick(View view){
         MobAgent.stickNomal("tag", new MsHashMap()
                        .put("id", uid)
                        .put("actiontag", EventInfoConstants.Action_Tag)
                        .put(DCO_EventInfoChannel.event_stamp, MobAgent.currentMills())
                        .put(DCO_EventInfoChannel.product_tag, EventProductTag.channel)
                        .put(DCO_EventInfoChannel.event_id, EventInfoConstants.ID_channel_close)
                        .put(DCO_EventInfoChannel.event_entrance, EventInfoConstants.Entrance_channel)
                        .put(DCO_EventInfoChannel.event_param1, moduleID)
                        .put(DCO_EventInfoChannel.event_param2, null)
                        .put(DCO_EventInfoChannel.event_param3, null)
                        .put(DCO_EventInfoChannel.event_param_other, null));
    }
    

    先不讨论,上面的这样的写法是否是可以再优化,是否可以写的更简单。
    我们可以看一下上面的一些参数。
    其实除了 event_param1 .... 2....3 这几个以外, 几乎都是常量
    而event_stamp也可以在采集的时候自动生成。
    按照上面的写法,我们如果只有 1,2,个采集,这么写也算是可以忍受的。
    但是如果10个,20个的才几点,先不说会有多少的copy的机械劳动,而且代码行数也会无形的增加。
    当然面对这种情况,我们怎么可能选择接受呢? 所以我们要采用一种方式,对于每一个采集点,我是否可以一行代码完成呢? 答案是必须的。

     @AnDataCollect(tagname = "tag" ,action = "actiontag")
      public void onClick(View view){
         AptDC.log(this,"onClick");
      }
    

    我们并没有把上面的所有参数都来举例说明,只取了 tag 和 actiontag 两个参数来作为例子,其他参数大同小异
    当然有同学会问了: 如果我把所有的参数都写上

     @AnDataCollect(
              tagname = "tag" ,
              action = "actiontag",
              product_tag="producttag",
              event_id="eventid",
              event_entrance="entrance"
    )
      public void onClick(View view){
         AptDC.log(this,"onClick");
      }
    

    大家会认为这个代码行数也没少多少啊。
    其实仔细想想,还是有区别和好处的的。 首先我们不用每次都去new一个HashMap来存储这些值,写法也更加的简单,不需要去记住还有一个 MobAgent 这么个类是做这个的,而且我们看到这个方法的时候,就可以一目了然的看到这个方法是针对于那种类型的采集。采集的内容是什么,而且利用注解的方式固定到一个位置,也是一个规范团队风格的好习惯。
    好了说了这么多,貌似没到正题啊,我们马上进入正题, 我们如何实现的上述改变呢?


    进入主题

    在进入主题之前,我们把上诉功能的逻辑图先画出来,然后在进行每一步的讲解

    关系图解

    图虽然比较糙,不过我们主要注意内在就好了。

    简单来说,就是生成了一个可以帮助我们做一些辅助功能的类,就不用手动人工写了,但是在项目中如何使用呢? 就是通过 对外的api一调用。 就齐活了。对应的对外api 就是 AptDC.log 就是干这个活的

    apt代码生成

    我们先从 annotation 和 aptcompiler 工程开始说起

    annotation 工程

    我们先来看一下 annotation 的内容

    annotation工程

    此工程中,放着的就是我们需要用到的注解,是一个java工程,不需要任何的外部依赖。
    我们看到里面的 AnDataCollect 注解,就是我们要进行数据采集的主要部分,里面定义了两个值 tagname , action 对应的就是我们上面提到的的注解中的参数
    这个工程没有什么过多的知识点,如果对注解不太了解的同学可以看一下
    java注解学习

    重头戏 aptcompiler

    我们编译期生成代码的关键就在这个工程中
    首先我们来看一下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"
    

    关键依赖是

      google auto 提供运行时处理注解的库
      compile  'com.google.auto.service:auto-service:1.0-rc2'
      square 出品专门用于代码生成的库,生成类时不需要手动拼写了,非常赞
      compile  'com.squareup:javapoet:1.7.0'
    

    其次我们来看一下aptcompiler 工程中的结构

    aptcompiler工程

    按照图中标识的顺序来逐一进行讲解

    1. AnLogProcessor 处理类继承 AbstractProcessor
      AbstractProcessor 是 javax.annotation.processing 报下的类
      在javac编译的时候,会调用AbstractProcessor的子类,来让我们处理类。

      需要注意的地方
      单单继承了AbstractProcessor 是不够的,还需要在META-INF中的 javax.annotation.processing.Processor 文件中配置我们的入口类,如 6 所示。
      内容如下:

        com.aptdc.AnLogProcessor
    

    为了更方便的增加这个配置,我们只需要在,AnLogProcessor类中,加一个注解 @AutoService(Processor.class) 编译器会自动的为我们生成这个配置文件了。

    1. init 方法,初始化方法,这个没什么特殊说明,就是初始化一些数据
      • Filer 生成java类的辅助类。
      • Elements 可以帮助我们获取到注解的相关信息,非常方便的一个类
    2. getSupportedSourceVersion
      默认processor 是必须要有支持的版本的
      一般使用 SourceVersion.latestSupported()
    3. getSupportedAnnotationTypes 方法
      过滤我们要处理的注解类型
    4. process 方法,最关键的方法没有之一
      该方法会返回我们所有注解所关系到的元素并且生成的工作也在这里进行
      下面主要讲解 process 方法所做的事情

    process方法处理元素,生成java代码

     //处理注解生成代码的地方
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
            //从注解中找到所有的类,以及对应的注解的方法
            LinkedHashMap<TypeElement,List<AnLogClass>> targetClassMap =  findAnnoationClass(roundEnvironment);
            //生成每一个类和方法
            for (Map.Entry<TypeElement, List<AnLogClass>> item : targetClassMap.entrySet()){
    
                TypeElement closeTypeElement = item.getKey();
    
                //获取包名
                String packageName = mElementsUtils.getPackageOf(closeTypeElement).getQualifiedName().toString();
                //获取类名称
                String className = item.getKey().getSimpleName().toString();
                //生成代理类
                GenerationClass generationClass = new GenerationClass(packageName,className);
                TypeSpec.Builder classGenBuilder =  generationClass.createClass();
    
                //生成抽象类的实现方法
                MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeVariableName.get("T"),"target")
                    .addParameter(String.class,"method");
    
                //添加处理方法
                List<AnLogClass> methods =  item.getValue();
                for (AnLogClass method: methods) {
                    MethodSpec.Builder methodBuilder = generationClass.createMethod(method);
                    MethodSpec methodSpec = methodBuilder.build();
                    classGenBuilder.addMethod(methodSpec);
    
                    injectMethod.addCode("if(method != null && method.length()>0)\n"
                        +"{\n"
                        +   " if(method.equals($S)){\n"
                        +   "    $N();\n"
                        +   " }\n"
                        +"}\n"
                        , method.getMethodName(),methodSpec
                    );
                }
                classGenBuilder.addMethod(injectMethod.build());
                //生成类文件
                JavaFile javaFile = JavaFile.builder(packageName, classGenBuilder.build())
                         .addFileComment("Generated code from dc    . Do not modify!")
                         .build();
                try {
                     javaFile.writeTo(mFiler);
                } catch (IOException ex) {
                     error(closeTypeElement, "Unable to write binding for type %s: %s", closeTypeElement, ex.getMessage());
                }
            }
            return true;
        }
    

    核心代码在上面,具体的注释也都写的比较明确
    不过有两处地方需要着重说明一下

    1. 在方法的第一行 findAnnoationClass方法主要是用来处理Element 的返回节点,包装处理成我们自己封装的对象。
     private LinkedHashMap<TypeElement, List<AnLogClass>> findAnnoationClass(RoundEnvironment roundEnvironment)
        {
            LinkedHashMap<TypeElement,List<AnLogClass>> targetClassMap = new LinkedHashMap<>();
           //找到所有跟AnDataCollect注解相关元素
            Collection<? extends Element> anLogSet =  roundEnvironment.getElementsAnnotatedWith(AnDataCollect.class);
            //遍历所有元素
             for (Element e: anLogSet) { 
                if (e.getKind() != ElementKind.METHOD) {
                    messager.printMessage(Diagnostic.Kind.ERROR, "only support class");
                    continue;
                }
                //此处找到的是类的描述类型,因为我们的AnDataCollect的注解描述是method的所以closingElement元素是类
                TypeElement enclosingElement = (TypeElement) e.getEnclosingElement();
                
                //对类做一个缓存
                List<AnLogClass> findLogList = targetClassMap.get(enclosingElement);
                if(findLogList == null){
                    findLogList = new ArrayList<>();
                    targetClassMap.put(enclosingElement,findLogList);
                }
                //生成注解描述的方法
                AnDataCollect log =  e.getAnnotation(AnDataCollect.class);
                //从注解中获取参数
                String action  =  log.action();
                String tagName = log.tagname();
                AnLogClass anLogClass = new AnLogClass();
                anLogClass.setAction(action);
                anLogClass.setTagName(tagName);
                anLogClass.setMethodElement(e);
    
                findLogList.add(anLogClass);
    
            }
            return targetClassMap;
        }
    

    其实在处理Element 的过程中,我们可以通过Element 获取到,类,方法,参数等一切信息,跟反射一样,其实我们在处理元素信息的时候,完全可以按照反射的思路来做就可以。

    1. 就是代码生成的部分
     GenerationClass generationClass = new GenerationClass(packageName,className);
                TypeSpec.Builder classGenBuilder =  generationClass.createClass();
    
                //生成抽象类的实现方法
                MethodSpec.Builder injectMethod = MethodSpec.methodBuilder("inject")
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(TypeVariableName.get("T"),"target")
                    .addParameter(String.class,"method");
    
                //添加处理方法
                List<AnLogClass> methods =  item.getValue();
                for (AnLogClass method: methods) {
                    MethodSpec.Builder methodBuilder = generationClass.createMethod(method);
                    MethodSpec methodSpec = methodBuilder.build();
                    classGenBuilder.addMethod(methodSpec);
    
                    injectMethod.addCode("if(method != null && method.length()>0)\n"
                        +"{\n"
                        +   " if(method.equals($S)){\n"
                        +   "    $N();\n"
                        +   " }\n"
                        +"}\n"
                        , method.getMethodName(),methodSpec
                    );
                }
                classGenBuilder.addMethod(injectMethod.build());
                //生成类文件
                JavaFile javaFile = JavaFile.builder(packageName, classGenBuilder.build())
                         .addFileComment("Generated code from dc    . Do not modify!")
                         .build();
                try {
                     javaFile.writeTo(mFiler);
                } catch (IOException ex) {
                     error(closeTypeElement, "Unable to write binding for type %s: %s", closeTypeElement, ex.getMessage());
                }
    

    这里在生成代理类 和 方法的时候可以纯手动的拼写,但是这样太不人性化了,还好square 为我们提供了 javapoet

    这样,我们就生成了我们的代理类,代理类在项目中的位置是

    代码位置

    这样我们生成的类就出现在了 build/generated/source/apt/ 目录下。
    编译后,会与原来 MainActivity.class 是同一目录下。

    使用

    我们的代理辅助类已经生成了,很多的代码不用人为写了,只要加好注解apt就会帮我们自动生成了, 但是 我们如何将 MainActivity 与 MainActivity$$YLL 这两个类关联起来,达到我们的效果呢??

    这就是我们下面要说的 aptapi 模块的作用了。
    老规矩还是先看一下 aptapi 模块的目录结构和build文件中的依赖

    build.gradle

    apply plugin: 'java'
    dependencies {
      compile fileTree(dir: 'libs', include: ['*.jar'])
    }
    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"
    
    

    aptapi 里面就是提供给外部 app 使用的api , 他不依赖任何的东西。

    目录结构:

    aptapi项目结构

    里面最关键的类,则是 AptDC 他的功能就是将我们自动生成类与实际工作的类关联起来

      package com.aptdc;
    
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class AptDC {
    
        private static final Map<Class<?>, AbstractLogInjector<Object>> INJECTORS = new LinkedHashMap<Class<?>, AbstractLogInjector<Object>>();
    
        /**
         * 外部调用api
         * @param object 需要注册的类
         * @param methodName 注册的方法
         */
        public static void log(Object object,String methodName){
             AbstractLogInjector<Object> logInInjector = findInjector(object);
             //调用代理类的实现方法
             //当原始类的方法执行的时候,就会调用此处代理类的实现
             //而代理类就是实现 AbstractLogInjector  这个接口的类了
             //原始类与自动生成的代理类他们关系就这样建立了
             logInInjector.inject(object,methodName);
        }
    
        private static AbstractLogInjector<Object> findInjector(Object activity) {
            Class<?> clazz = activity.getClass();
            AbstractLogInjector<Object> injector = INJECTORS.get(clazz);
            if (injector == null) {
                try {
                     //通过原始类找到我们生成的代理类
                    Class injectorClazz = Class.forName(clazz.getName() + "$$YLL");
                    //创建代理类
                    injector = (AbstractLogInjector<Object>) injectorClazz.newInstance();
                    INJECTORS.put(clazz, injector);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return injector;
        }
    }
    

    好现在看一下我们上面这个例子在android 项目中如何使用的

    首先看一下 android 项目中的 build 文件
    此处只贴出关键点

    apply plugin: 'com.android.application'
    apply plugin: 'com.neenbedankt.android-apt'
    android {
      ...
    }
    dependencies {
      compile project(':aptapi')
      compile project(':annotation')
      apt project(':aptcompiler')
    }
    

    aptcompiler工程不能使用compile 的方式来依赖,否则aptcompiler就找不到javax 包下的处理类了。 只可以使用apt的方式来依赖
    并且要应用 apply plugin: 'com.neenbedankt.android-apt' 插件

    要想使用 apply plugin: 'com.neenbedankt.android-apt' 插件则需要在root project 中的build.gradle 文件进行如下配置

    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    
    buildscript {
      repositories {
        jcenter()
      }
      dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0-rc1'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
      }
    }
    allprojects {
      repositories {
        jcenter()
      }
    }
    task clean(type: Delete) {
      delete rootProject.buildDir
    }
    

    总结

    现在我们学会了如何使用apt来帮助我们更好的构建我们的项目
    现在来说一下使用apt的优缺点

    优点

    对代码进行标记,在编译时收集信息,并做处理。
    生成一套独立代码,辅助代码运行
    生成代码位置的可控性(可以在任意包位置生成代码),与原有代码的关联性更为紧密方便
    更为可靠的自动代码生成
    自动生成的代码可以最大程度的简单粗暴,在不必考虑编写效率的情况下提高运行效率

    缺点

    APT往往容易被误解可以实现代码插入,然而事实是并不可以
    APT可以自动生成代码,但在运行时却需要主动调用
    与GreenDao不同,GreenDao代码生成于app目录下,可以在编写时调用并修改。APT代码生成于Build目录,只能在运行时通过接口等方式进行操作。这意味着生成的代码必须要有一套固定的模板

    APT容易被你忽视的点
    一个非常容易被你误解的点:只有被注解标记了的类或方法等,才可以被处理或收集信息。或者这样说,想要收集一些信息,只能先用注解修饰它。
    产生这样误解容易引起一个问题:你可能会觉得一个需要大量注解的框架体验不好而决定放弃。
    事实是怎么样呢?想一下同源的运行时注解+反射。反射可以通过一个类名便获取一个类的所有信息(方法、属性、方法参数等等等)。编译时注解也是可以的。当你修饰一个类时,可以通过类的Element获得类的属性和方法的Element,通过属性的Element可以获得属性所属类的信息,通过方法的Element可以获得所属类和其参数的信息。
    说白了,编译时注解你也完全可以当反射来理解。

    APT的优缺点都非常明显,优点足够了,缺点也不致命,只是让你在设计你的框架,选择技术方案时注意就好了。那么基于上面列出的几点,几个通用的应用场景就可以被设想了~ 一定要放大你的脑洞!!!

    PS: 总结部分来自于 暴打小女孩-BLOG 因为本人觉得此博客中做的总结还是比较全面的,所以引用了进来

    不过对于目前的写法,作者本人还有一些不太满意的地方。
    因为现在在编写代码的时候,还是需要手动的通过api提供的调用方式做原始类与代理类的绑定。 这部分工作其实可以使用 aspectJ 的静态织入来解决,这样就可以做到代码尽可能的简洁,当然必要的api还是要提供的。

    下一篇会分享如何使用aspectJ 将生成代码的绑定关系,在编译时写入到class中

    本篇内容涉及的技术点
    javapoet
    google-auto
    apt api doc

    使用该技术的开源项目
    butterKnife
    google-Dagger
    还有一些开源项目大家都比较熟悉就不一一列举了

    特别鸣谢BLOG:
    暴打小女孩-BLOG
    Septenary

    本篇文章的demo已经上传到github
    android-apt

    相关文章

      网友评论

      本文标题:相见恨晚的apt

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