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


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

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