美文网首页
组件化路由思想和JavaPoet生成代码

组件化路由思想和JavaPoet生成代码

作者: 石器时代小古董 | 来源:发表于2019-07-21 17:18 被阅读0次

    javaPoet文章

    网易云课堂笔记

    一、设计组和路径

    组件化的路由组件中有组的概念,组中又维护着属于这一组内所有的类的路径。在设计时通常使用 HashMap 来实现组和路径。可以看到path中可能维护着多个类的“路径”,是一对n的关系,组内只对应一个Path对象是一对一的关系

    
    public class ARouter$$Path$$app implements ARouterLoadPath {
      @Override
      public Map<String, RouterBean> loadPath() {
        Map<String, RouterBean> pathMap = new HashMap<>();
        pathMap.put("/app/MainActivity", RouterBean.create(RouterBean.Type.ACTIVITY, MainActivity.class, "/app/MainActivity", "app"));
        pathMap.put("/app/Main2Activity", RouterBean.create(RouterBean.Type.ACTIVITY, Main2Activity.class, "/app/Main2Activity", "app"));
        return pathMap;
      }
    }
    
    
    public class ARouter$$Group$$app implements ARouterLoadGroup {
      @Override
      public Map<String, Class<? extends ARouterLoadPath>> loadGroup() {
        Map<String, Class<? extends ARouterLoadPath>> groupMap = new HashMap<>();
        groupMap.put("app", ARouter$$Path$$app.class);
        return groupMap;
      }
    }
    

    在设计时我们将所有类的路径都由一个 ARouterLoadPath 对象维护,而 ARouterLoadPath 又都由一个 ARouterLoadGroup 对象维护

    public interface ARouterLoadGroup {
        Map<String, Class<? extends ARouterLoadPath>> loadGroup();
    }
    public interface ARouterLoadPath {
        Map<String, RouterBean> loadPath();
    }
    

    这样做的好处是避免内存的浪费,试想一下如果没有 Group 的概念,所有类都由一个 Path 对象维护,那么即使这些类在跳转时没有被使用到,仍然被加载到了 HashMap 中,如果各个模块合起来有100多个类,那么将造成大量的内存浪费

    二、使用 JavaPoet 生成上面的代码

    JavaPoet 是 square 公司开源的技术,可以更加方便的再编译时生成代码。

    1.在module中加入arguments,在编译时传入模块的名称(作为组名),生成代码的包名(packageName)

    def packageNameForAPT = "com.baidu.crazyorange.test"
    javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [moduleName: project.getName(), packageNameForAPT: packageNameForAPT]
                }
            }
    

    2. 在 AbstractProcess 组件初始化时接收传递的参数<BR>

       Map<String, String> options = processingEnvironment.getOptions();
            if (!EmptyUtils.isEmpty(options)) {
                moduleName = options.get(Constants.MODULE_NAME);
                packageNameForAPT = options.get(Constants.APT_PACKAGE);
                // 有坑:Diagnostic.Kind.ERROR,异常会自动结束,不像安卓中Log.e
                messager.printMessage(Diagnostic.Kind.NOTE, "moduleName >>> " + moduleName);
                messager.printMessage(Diagnostic.Kind.NOTE, "packageNameForAPT >>> " + packageNameForAPT);
            }
    
    

    3. 在 process 方法中处理注解<BR>

      @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            // 必须有地方使用了我们设置的注解
            if (!EmptyUtils.isEmpty(set)) {
                // 获取所有被 @ARouter 注解的元素集合
                Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
            
                if (!EmptyUtils.isEmpty(elements)) {
                    // 解析元素
                    try {
                        parseElements(elements);
                        return true;
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                // 坑:必须写返回值,表示处理@ARouter注解完成
                return true;
            }
            return false;
        }
    
    

    确保注解使用在 Activity 的类上<BR>
    Java 提供了 TypeElement 类可以通过类的路径创建出这个类的 TypeMirror (镜像信息)。通过类的镜像信息以及 typeUtils.isSubtype 方法可以判断注解是否使用在了 Activity的子类上

      public static final String ACTIVITY = "android.app.Activity";
      /***
        * elements 存储的是所有使用了 ARouter 注解的元素的类型信息 
        * 所以只要确保这些元素是 Activity的子类就可以
       **/
      private void parseElements(Set<? extends Element> elements) throws IOException {
            // 通过Element工具类,获取Activity、Callback类型
            TypeElement activityType = elementUtils.getTypeElement(Constants.ACTIVITY);
    
            // 显示类信息(获取被注解节点,类节点)这里也叫自描述 Mirror
            TypeMirror activityMirror = activityType.asType();
    
            // 遍历节点
            for (Element element : elements) {
                // 获取每个元素类信息,用于比较
                TypeMirror elementMirror = element.asType();
                messager.printMessage(Diagnostic.Kind.NOTE, "遍历元素信息:" + elementMirror.toString());
    
                // 获取每个类上的@ARouter注解中的注解值
                ......
                ......
                // isSubType 方法要求传入的必须是 TypeMirror,
                if (typeUtils.isSubtype(elementMirror, activityMirror)) {
                    bean.setType(RouterBean.Type.ACTIVITY);
                } else {
                    // 不匹配抛出异常,这里谨慎使用!考虑维护问题
                    throw new RuntimeException("@ARouter注解目前仅限用于Activity类之上");
                }
               .....
            }
       ......
        }
    

    提取ARouter注解中存放的信息并存储

    如果注解符合我们设定的要求,那么就可以将这些信息提取出来为生成代码做准备。注解符合要求主要看:

    1.是否作用于 Activity 子类上
    2.注解的 path 和 group 的名称是否符合规范.这里参考阿里ARouter的要求

    ARouter的要求<BR>1.group名必须和module完全一致
    2.必须是/开头

    /**
         * 校验@ARouter注解的值,如果group未填写就从必填项path中截取数据
         *
         * @param bean 路由详细信息,最终实体封装类
         */
        private boolean checkRouterPath(RouterBean bean) {
            String group = bean.getGroup();
            String path = bean.getPath();
    
            // @ARouter注解中的path值,必须要以 / 开头(模仿阿里Arouter规范)
            if (EmptyUtils.isEmpty(path) || !path.startsWith("/")) {
                messager.printMessage(Diagnostic.Kind.NOTE, "@ARouter注解中的path值,必须要以 / 开头");
                return false;
            }
    
            // 比如开发者代码为:path = "/MainActivity",最后一个 / 符号必然在字符串第1位
            if (path.lastIndexOf("/") == 0) {
                // 架构师定义规范,让开发者遵循
                messager.printMessage(Diagnostic.Kind.NOTE, "@ARouter注解未按规范配置,如:/app/MainActivity");
                return false;
            }
    
            // 从第一个 / 到第二个 / 中间截取,如:/app/MainActivity 截取出 app 作为group
            String finalGroup = path.substring(1, path.indexOf("/", 1));
    
            // 如果 group 和 moduleName 不一致(module在gradle中设置,编译时传入)
            if (!EmptyUtils.isEmpty(group) && !group.equals(moduleName)) {
                // 架构师定义规范,让开发者遵循
                messager.printMessage(Diagnostic.Kind.NOTE, "@ARouter注解中的group值必须和子模块名一致!");
                return false;
            } else {
                bean.setGroup(finalGroup);
            }
    
            return true;
        }
    

    如果符合要求,将这些节点存储到一个临时的 Map 中, 这里的 tempPathMap 以组为 key。为的是更好的区分出 path 的类别,在真正生成 ARouterPath 代码时,将对应组的 path 交给 ARouterPath(注意 ARouterPath内部的 Map 的 key 是 path

      // 获取每个类上的@ARouter注解中的注解值
      ARouter aRouter = element.getAnnotation(ARouter.class);
    
      // 路由详细信息,最终实体封装类
      RouterBean bean = new RouterBean.Builder()
              .setGroup(aRouter.group())
              .setPath(aRouter.path())
              .setElement(element)
              .build();
              
       private void valueOfPathMap(RouterBean bean) {
            if (checkRouterPath(bean)) {
                messager.printMessage(Diagnostic.Kind.NOTE, "RouterBean >>> " + bean.toString());
    
                // 开始赋值Map
                List<RouterBean> routerBeans = tempPathMap.get(bean.getGroup());
                // 如果从Map中找不到key为:bean.getGroup()的数据,就新建List集合再添加进Map
                if (EmptyUtils.isEmpty(routerBeans)) {
                    routerBeans = new ArrayList<>();
                    routerBeans.add(bean);
                    tempPathMap.put(bean.getGroup(), routerBeans);
                } else { // 找到了key,直接加入List集合
                    routerBeans.add(bean);
                }
    
            } else {
                messager.printMessage(Diagnostic.Kind.ERROR, "@ARouter注解未按规范配置,如:/app/MainActivity");
            }
        }
    

    3.使用存储好的信息生成类

    首先生成的两个类需要继承ARouterLoadPath和ARouterGroup接口,使用JavaPoet技术生成时ClassName对象,ClassName对象又可以通过javaPoet提供的ClassName.get(TypeELement)对象生成

    
            // 获取ARouterLoadGroup、ARouterLoadPath两个接口类型的路径(生成类文件需要实现的接口)
            TypeElement groupLoadType = elementUtils.getTypeElement(Constants.AROUTE_GROUP); // 组接口
            TypeElement pathLoadType = elementUtils.getTypeElement(Constants.AROUTE_PATH); // 路径接口
              // 第一步:生成路由组Group对应详细Path类文件,如:ARouter$$Path$$app
            createPathFile(pathLoadType);
              // 第二步:生成路由组Group类文件(没有第一步,取不到类文件),如:ARouter$$Group$$app
            createGroupFile(groupLoadType, pathLoadType);
    

    必须先创建ARouterPath类文件,因为ARouterGroup文件内部引用了ARouterPath类

    创建ARouterPath类文件

    1.创建返回值类型

    我们需要返回一个维护了这组所有Path的HashMap,JavaPoet的ParameterizedTypeName可以创建参数化的类型

        TypeName methodReturns = ParameterizedTypeName.get(
                    ClassName.get(Map.class), // Map
                    ClassName.get(String.class), // Map<String,
                    // 第二个参数:Class<? extends ARouterLoadPath>
                    // 某某Class是否属于ARouterLoadPath接口的实现类
                    ParameterizedTypeName.get(ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(pathLoadType)))
            );
    
    

    2.创建方法名称和方法体信息<BR>

    // 创建成这样:
    //@Override
    //public Map<String, Class<? extends ARouterLoadPath>> loadGroup()
    MethodSpec.Builder methodBuidler = MethodSpec.methodBuilder(Constants.GROUP_METHOD_NAME) // 方法名
              .addAnnotation(Override.class) // 重写注解
              .addModifiers(Modifier.PUBLIC) // public修饰符
              .returns(methodReturns); // 方法返回值
    // 创建一个hashMap对象 Map<String, RouterBean> pathMap = new HashMap<>();       
         methodBuidler.addStatement("$T<$T, $T> $N = new $T<>()",
                        ClassName.get(Map.class),
                        ClassName.get(String.class),
                        ClassName.get(RouterBean.class),
                        Constants.PATH_PARAMETER_NAME,
                        HashMap.class);
    
    
    
    
     // 一个分组,如:ARouter$$Path$$app。有很多详细路径信息,如:/app/MainActivity、/app/OtherActivity
                List<RouterBean> pathList = entry.getValue();
                // 方法内容配置(遍历每个分组中每个路由详细路径)
                for (RouterBean bean : pathList) {
                    // 类似String.format("hello %s net163 %d", "net", 163)通配符
                    // pathMap.put("/app/MainActivity", RouterBean.create(
                    //        RouterBean.Type.ACTIVITY, MainActivity.class, "/app/MainActivity", "app"));
                    methodBuidler.addStatement(
                            "$N.put($S, $T.create($T.$L, $T.class, $S, $S))",
                            Constants.PATH_PARAMETER_NAME, // pathMap.put
                            bean.getPath(), // "/app/MainActivity"
                            ClassName.get(RouterBean.class), // RouterBean
                            ClassName.get(RouterBean.Type.class), // RouterBean.Type
                            bean.getType(), // 枚举类型:ACTIVITY
                            ClassName.get((TypeElement) bean.getElement()), // MainActivity.class
                            bean.getPath(), // 路径名
                            bean.getGroup() // 组名
                    );
                }
    
                // 遍历之后:return pathMap;
                methodBuidler.addStatement("return $N", Constants.PATH_PARAMETER_NAME);
    
                // 最终生成的类文件名
                String finalClassName = Constants.PATH_FILE_NAME + entry.getKey();
                messager.printMessage(Diagnostic.Kind.NOTE, "APT生成路由Path类文件:" +
                        packageNameForAPT + "." + finalClassName);
    
                // 生成类文件:ARouter$$Path$$app
                JavaFile.builder(packageNameForAPT, // 在这个包下面创建一个类文件
                        TypeSpec.classBuilder(finalClassName) // 被构建的类的名称
                                .addSuperinterface(ClassName.get(pathLoadType)) // 实现ARouterLoadPath接口
                                .addModifiers(Modifier.PUBLIC) // public修饰符
                                .addMethod(methodBuidler.build()) // 方法的构建(方法参数 + 方法体)
                                .build()) // 类构建完成
                        .build() // JavaFile构建完成
                        .writeTo(filer); // 文件生成器开始生成类文件
    
                // 非常重要一步!!!!!路径文件生成出来了,才能赋值路由组tempGroupMap
                tempGroupMap.put(entry.getKey(), finalClassName);
    

    同理生成 Group 也是

      /**
         * 生成路由组Group文件,如:ARouter$$Group$$app
         *
         * @param groupLoadType ARouterLoadGroup接口信息
         * @param pathLoadType ARouterLoadPath接口信息
         */
        private void createGroupFile(TypeElement groupLoadType, TypeElement pathLoadType) throws IOException {
            // 判断是否有需要生成的类文件
            if (EmptyUtils.isEmpty(tempGroupMap) || EmptyUtils.isEmpty(tempPathMap)) return;
    
            TypeName methodReturns = ParameterizedTypeName.get(
                    ClassName.get(Map.class), // Map
                    ClassName.get(String.class), // Map<String,
                    // 第二个参数:Class<? extends ARouterLoadPath>
                    // 某某Class是否属于ARouterLoadPath接口的实现类
                    ParameterizedTypeName.get(ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(pathLoadType)))
            );
    
            ....
            // 遍历之前:Map<String, Class<? extends ARouterLoadPath>> groupMap = new HashMap<>();
            methodBuidler.addStatement("$T<$T, $T> $N = new $T<>()",
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ParameterizedTypeName.get(ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(pathLoadType))),
                    Constants.GROUP_PARAMETER_NAME,
                    HashMap.class);
    
            // 方法内容配置  tempGroupMap的内容是在path生成时被加入的
            for (Map.Entry<String, String> entry : tempGroupMap.entrySet()) {
                // 类似String.format("hello %s net163 %d", "net", 163)通配符
                // groupMap.put("main", ARouter$$Path$$app.class);
                methodBuidler.addStatement("$N.put($S, $T.class)",
                        Constants.GROUP_PARAMETER_NAME, // groupMap.put
                        entry.getKey(),
                        // 类文件在指定包名下
                        ClassName.get(packageNameForAPT, entry.getValue()));
            }
         ......
        }
    

    如果要生成的是 Map<String, Class<? extends ARouterLoadPath>>,可以使用

     TypeName methodReturns = ParameterizedTypeName.get(
                    ClassName.get(Map.class), // Map
                    ClassName.get(String.class), // Map<String,
                    // 第二个参数:Class<? extends ARouterLoadPath>
                    // 某某Class是否属于ARouterLoadPath接口的实现类
                    ParameterizedTypeName.get(ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(pathLoadType)))
            );
    

    相关文章

      网友评论

          本文标题:组件化路由思想和JavaPoet生成代码

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