美文网首页flutter
编译时注解到自定义Router框架

编译时注解到自定义Router框架

作者: 土肥圆的诺诺 | 来源:发表于2018-10-17 16:07 被阅读193次

    找了很久网上也没讲编译期注解的视频,只能对着网上代码一句句研究总结,主要学完这块,路由框架啊,奶油刀啊,基本自己就可以写点low版本的了

    @Route(path = "/main/main")
    public class MainActivity extends AppCompatActivity {}
    

    上面是路由框架的注解,这个注解有什么用呢,路由框架会在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中,只要拿到了映射关系便能拿到Activity.class。相当于有一个类专门去保存了这些类和路径的关系。
    好吧,我们先去学习下怎么搞个编译期注解
    注解篇可以看我之前的基本注解讲解 :https://www.jianshu.com/p/e59059a509f1
    我这里就直接上手了,其实核心原理就是我们扫描自己自定义的注解,然后根据注解拿到被我们注解的类的相关信息,保存生成一个我们的类,类里写上我们需要的东西,然后我们就可以根据这个类去搞一些事情。
    大家也可以参考这篇博客,我是直接搞一下当自己日记了
    https://blog.csdn.net/yang_yang1994/article/details/79729621

    虚处理器AbstractProcessor

    我们首先看一下处理器的API。每一个处理器都是继承于AbstractProcessor,如下所示:

    
    public class MyProcessor extends AbstractProcessor {
    
        @Override
        public synchronized void init(ProcessingEnvironment env){ }
    
        @Override
        public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() { }
    
        @Override
        public SourceVersion getSupportedSourceVersion() { }
    
    }
    

    先进行参数讲解,也会贴一些api上来,不然后面没办法用。

    • process(Set<? extends TypeElement> annotations, RoundEnvironment env): 这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。

    *getSupportedAnnotationTypes(): 这里返回一个set集合,告诉你需要支持的哪些注解类,要把你的注解类全路径写成字符串返回给他。
    *getSupportedSourceVersion(): 用来指定你使用的Java版本。通常这里返SourceVersion.latestSupported()。
    java7以后可以使用,,但是看网上大家都说不建议这么使用,为了版本兼容

    @SupportedSourceVersion(SourceVersion.latestSupported())
    @SupportedAnnotationTypes({
       // 合法注解全名的集合
     })
    

    这里我们需要加入两个工具框架
    第一个是AutoService,因为生成注解需要一个特定的格式



    AutoService可以帮我们自动生成这些包和路径,就省得自己创建了,尤其android是木有META-INF的
    基友网地址
    https://github.com/google/auto/tree/master/service

    第二个工具是javapoet,JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。这个框架功能非常有用,我们可以很方便的使用它根据注解、数据库模式、协议格式等来对应生成代码。通过这种自动化生成代码的方式,可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作。
    参考链接 :https://blog.csdn.net/xuguobiao/article/details/72775730
    基友网地址: https://github.com/square/javapoet

    我们现在开始写编译期注解
    第一步,在AnroidStudio 项目上新建两个modle,分别取名为processor,processor_lib,processor依赖processor_lib,app依赖processor和processor_lib。processor_lib存放我们自定义注解,processor用来编译。



    创建
    app依赖processor和processor_libl
    processor依赖processor_lib

    在processor引入我们的AutoService和javapoet


    引入AutoService和javapoet

    最后出来效果就是下图这样子


    下面开始编写代码
    我们在processor_lib写下我们的注解@Leo,这块不清楚的可以看我写的自定义注解篇

    @Target(ElementType.FIELD)//声明在字段
    @Retention(RetentionPolicy.CLASS)//声明为编译期注解
    public @interface Leo {
        String path();//俩参数
        String name();
    }
    
    

    在我们的processor开启我们的注解生成部分



    覆盖process,init,getSupportedSourceVersion,getSupportedAnnotationTypes 四个方法
    getSupportedSourceVersion方法比较简单,直接返回支持最新的

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

    getSupportedAnnotationTypes,返回支持我们的Leo注解,getCanonicalName和和getName其实一样,getName()返回的是虚拟机里面的class的表示,而getCanonicalName()返回的是更容易理解的表示。其实对于大部分class来说这两个方法没有什么不同的。但是对于array或内部类来说是有区别的。
    另外,类加载(虚拟机加载)的时候需要类的名字是getName。
    详情可以看这篇博客
    https://blog.csdn.net/hustzw07/article/details/71108945

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

    在我们的类上加上AutoService标示


    AutoService

    init方法

    这里有几个知识点ProcessingEnvironment,Messager,Elements

        private Filer filer;
        private Messager messager;
        private Elements elementUtils;
     @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            filer = processingEnvironment.getFiler();
            messager = processingEnvironment.getMessager();
            elementUtils = processingEnvironment.getElementUtils();
        }
    

    ProcessingEnvironment
    这玩意还得是看API,虽然英文的但是我们可以翻译啊哈哈
    大体意思就是可以提供方法进行编写新文件、报告错误消息和查找其他实用工具。具体可以自己看看
    https://docs.oracle.com/javase/8/docs/api/javax/annotation/processing/ProcessingEnvironment.html

    api

    process方法

    在这里处理我们注解以及生成类对象
    有几个知识点

    开始编写process,我们先整理下思路,我们在通过编译期可以拿到所有符合我们要求注解字段,生成我们想要的包,类和根据符合要求做的操作
    那么可以分为一下几步

    • 以类做key,拿到当前类所有的符合要求注解
    • 按照类<注解>形式生成类
    • 将类写到我们特定的包里
    • 如果多包我们还可以按照总包-包名-类名这种形式进行分层
      按照如下思路我们写下代码
       //类名
            String element4className;
            //存放类名和元素注解的集合,HashMap保证唯一
            HashMap<String, ArrayList<VariableElement>> map = new HashMap<>();
            //存放元素的集合
            ArrayList<VariableElement> varList;
    
    
            //拿到被leo标注的所有注解元素
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Leo.class);
            //遍历elements
            for (Element element : elements) {
                //返回此元素的类型。
                ElementKind elementKind = element.getKind();
                //如果类型是 作用在字段上
                if (elementKind == ElementKind.FIELD) {
                    //那么element就是一个VariableElement
                    VariableElement var = (VariableElement) element;
                    //我们要得到它上层类 返回封装此元素(非严格意义上)的最里层元素。
                    TypeElement element4class = (TypeElement) var.getEnclosingElement();
                    //拿到返回此类型元素的完全限定名称
                    element4className = element4class.getQualifiedName().toString();
                    //利用elementUtils 然后  此包的完全限定名称
                    String packageName = elementUtils.getPackageOf(element4class).toString();
    
                    //判断是不是null,如果是null就生成新的存放进去
                    varList = map.get(element4className);
                    if (varList == null) {
                        varList = new ArrayList<VariableElement>();
                        map.put(element4className, varList);
                    }
                    //队列里木有就加进去
                    if (!varList.contains(var)) {
                        varList.add(var);
                    }
    
    
                }
            }
    
    

    到这里还没结束,我们要根据我们发现的注解进行操作,这里书写类和包,我建议最好看下javapoet怎么使用

       for (String key : map.keySet()) {
                //根据key取出所有类
                List<VariableElement> elementFileds = map.get(key);
                //去掉类名最后的。class
                String className = key.substring(key.lastIndexOf(".") + 1);
                //类名后缀添加4Leo
                className += "4Leo";
                //创建一个public的4leo结尾的类
                TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
                        .addModifiers(Modifier.PUBLIC);
                //生成一个返回值为String的方法 public static
                MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("getElemens")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(String.class);
              //遍历 注解上的name和path
                for (VariableElement e : elementFileds) {
                    Leo annotation = e.getAnnotation(Leo.class);
                    String name = annotation.name();
                    String path = annotation.path();
                  //返回name和path
                    methodBuild.returns(String.class)
                    .addStatement("return $S", "name=" + name + "----path---" + path);
                }
    //创建方法
                MethodSpec printNameMethodSpec = methodBuild.build();
    //创建类
                TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
    
                try {
                 //写出包名和类
                    JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
                          //添加注解
                            .addFileComment(" Leo Compile time annotations!")
                            .build();
                    javaFile.writeTo(filer);
                } catch (IOException exception) {
                    exception.printStackTrace();
                }
    
            }
    
    
    
            return true;
    

    运行一下,我们发现这个路径下出现一个和我们包一样的4Leo的类



    在我们页面测试一下

    public class MainActivity extends AppCompatActivity {
        @Leo(name = "leo", path = "MainActivity")
        private String Text;
        private static final String TAG = "MainActivity";
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            String elemens = MainActivity4Leo.getElemens();
            Log.e(TAG, "onCreate: "+elemens );
    
    
        }
    }
    
    

    看下log


    Log

    可见我们的自定义注解成功了
    再写这篇文章,我学习翻阅了很多API,用了一天,终于啃下了这块硬骨头,学会了编译期注解我们能做很多事,比如写个low版本的路由框架,接下来我们试一下。
    先看一下Arouter写的,我们虽然暂时只想实现一个页面跳转功能,但是也得把XX装足了
    基本上就是app类初始化,页面添加@Route加path,然后

    ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化,跳转,我们也对着写,先把最重要的注解先写好
    // 在支持路由的页面上添加注解(必选)
    // 这里的路径需要注意的是至少需要有两级,/xx/xx
    @Route(path = "/test/activity")
    public class YourActivity extend Activity {
        ...
    }
    ARouter.getInstance().build("/test/activity").navigation();
    

    在processor_lib下写好我们的注解


    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface LRoute {
        String path();
        String name()default  "";
    }
    
    

    processor下开启我们的注解处理


    package xzzb.com.processor;
    
    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.ParameterizedTypeName;
    import com.squareup.javapoet.TypeName;
    import com.squareup.javapoet.TypeSpec;
    
    
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Filer;
    import javax.annotation.processing.Messager;
    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.ElementKind;
    import javax.lang.model.element.Modifier;
    import javax.lang.model.element.TypeElement;
    import javax.lang.model.util.Elements;
    
    import xzzb.com.processor_lib.LRouter;
    //添加AutoService注解
    @AutoService(Processor.class)
    public class RouterPorcessor extends AbstractProcessor {
        private Filer filer;
        private Messager messager;
        private Elements elementUtils;
        private String packageName;
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    
        //初始化装填我们注解的Set
            HashSet<TypeElement> map = new HashSet<>();
             //获取我们的注解
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(LRoute.class);
             //遍历
            for (Element element : elements) {
                 //拿到注解类型
                ElementKind elementKind = element.getKind();
    //如果作用于类上
                if (elementKind == ElementKind.CLASS) {
    //转成TypeElement
                    TypeElement element4Class = (TypeElement) element;
    //添加进去
                    map.add(element4Class);
    //获取包名
                    packageName = elementUtils.getPackageOf(element4Class).toString();
    
    
                }
            }
    //创建类
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder("LRouterMap")
                    .addModifiers(Modifier.PUBLIC);
          //javapoet Classname方法可以参考基友网,我们要生成一个HashMap<String,Sting>
            ClassName hashMap = ClassName.get("java.util", "HashMap");
            ClassName key = ClassName.get("java.lang", "String");
            ClassName value = ClassName.get("java.lang", "String");
          //生成一个map,将类名和注解值添加进去
            MethodSpec.Builder build = MethodSpec.methodBuilder("getMaps").addModifiers(Modifier.PUBLIC);
            TypeName listOfHoverboards = ParameterizedTypeName.get(hashMap, key, value);
            build.addStatement("$T result = new $T<>()", listOfHoverboards, hashMap);
            build.returns(listOfHoverboards);
            for (TypeElement e : map) {
                //遍历添加
                String classname = e.getQualifiedName().toString();
                LRoute annotation = e.getAnnotation(LRoute.class);
                String path = annotation.path();
                build.addStatement("result.put($S,$S)", path, classname);
    
            }
              //返回map,生成类和方法
            build.addStatement("return result");
            MethodSpec printNameMethodSpec = build.build();
            TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
    
            try {
               //写出去类
                JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
                        .addFileComment(" Leo Compile time annotations !")
                        .build();
                javaFile.writeTo(filer);
            } catch (IOException exception) {
                exception.printStackTrace();
            }
    
    
            return true;
        }
    //获取需要的工具
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            filer = processingEnvironment.getFiler();
            messager = processingEnvironment.getMessager();
            elementUtils = processingEnvironment.getElementUtils();
        }
     //支持版本
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
    //要支持的注解
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            HashSet<String> set = new HashSet<>();
            set.add(LRoute.class.getName());
            return set;
        }
    }
    
    

    注解这块写完了,我们先在俩页面,写一下试试好用能生成么,




    运行结果

    发现我们的类名和path都被保存了下来,那么我们可以使用包名加类名的方式进行跳转了
    跳转也需要一个Context,我们就一路模仿Arouter去写

    public class LRouter {
    
    
        private static LRouter lRouter;
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:56
         * @describe 获取传入的Context
         */
        public static Context getContext() {
            return context;
        }
    
       
    
        private static Context context;
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:56
         * @describe 私有化构造函数
         */
        private LRouter() {
    
        }
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:57
         * @describe 初始化
         */
        public static void init(Application application) {
            context = application;
        }
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:56
         * @describe 单例模式
         */
        public static LRouter getInstance() {
            if (lRouter == null) {
                lRouter = new LRouter();
            }
    
            return lRouter;
        }
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:56
         * @describe 拿到生成的路由表 返回一个Postcard对象
         */
        public Postcard build(String path) {
            LRouterMap lRouterMap = new LRouterMap();
            HashMap<String, String> maps = lRouterMap.getMaps();
            Postcard postcard = new Postcard();
            String classNmae = maps.get(path);
            postcard.setPath(classNmae);
            return postcard;
    
        }
    
    
    }
    
    public class Postcard {
        //path
        private String path;
        //要跳转的包名
        private String packageName;
    
        public String getPath() {
            return path;
        }
    
        public void setPath(String path) {
            this.path = path;
        }
    
        /**
         * @author Administrator
         * @time 2018/10/18  10:58
         * @describe 跳转
         */
        public void navigation() {
    //判断路径是不是null
            if (!TextUtils.isEmpty(path)) {
                //截取包名
                int pos = path.lastIndexOf(".");
                packageName = path.substring(0, pos);
                //根据包名和类名跳转
                Intent intent = new Intent();
                ComponentName componentName = new ComponentName(packageName, path);
                intent.setComponent(componentName);
                LRouter.getContext().startActivity(intent);
    
            } else {
                //路径为null直接返回
                return;
            }
    
        }
    }
    
    

    我们去测试一下 跳转,成功跳转到了第二个页面

    @LRoute(path = "Main")
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    //按钮跳转
            findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    //跳转Main2页面
                    LRouter.getInstance().build("Main2").navigation();
                }
            });
    
    
        }
    }
    
    第二个页面
    到此为止我们完成了最low版本的页面跳转,其实我们可以仿照写更多功能。
    这篇到此为止,再见
    项目已经传送到基友网,地址 :https://github.com/594dudulang/LRouter,欢迎搞基

    相关文章

      网友评论

        本文标题:编译时注解到自定义Router框架

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