美文网首页
自家开发的比ARouter更强大的路由模块

自家开发的比ARouter更强大的路由模块

作者: Ryan哥哥 | 来源:发表于2019-08-01 17:17 被阅读0次

    概况

    曾经的多模块项目已有一套路由系统,已有各种上几百条不同风格的路由。各种不满足需求。同时ARouter也不能完全满足,并且迁移成本很大。 所以决定开发一套类似ARouter的路由系统并且可以兼容旧系统。新路由系统走不通走旧系统。

    需求 & 各方案比较

    • 旧方案

      • 模块依赖不好,所有路由解析都实在公用的模块中,不是真正跨模块调用
      • 需要逐一配置路由
      • 要逐一解析参数
      • 不支持startActivityForResult
      • 不支持拦截
    • 阿里的ARouter

      • 支持编译期注解
      • 无需注意解析参数
      • 支持拦截
      • 支持跨模块调用
      • 不支持Restful风格
      • 不支持一个Activity多路由
      • 不支持一级路由(如: meijiabang://live_list ),只支持二级以上( meijiabang://live/list
    • 需求

      • 具有ARouter的绝大部分功能
      • 支持restful风格路由
      • 支持一个Activity多路由
      • 支持一级路由
      • 兼容旧的方案,跑不通新路由跑旧路由
      • 有拦截器,有其他没有UI的纯命令性路由
      • Activity内有很多goActivity方法代码可以不需要修改,直接兼容

    怎样做到兼容旧的路由模块?

    • 典型场景要到代理设计模式,兼容两套录用路由,抽象出同一个接口
    public interface IMJBRoute {
    
        void routeTo(Activity activity, String routeUrl);
    
    }
    
    @Deprecated
    public class AppRoute implements IMJBRoute 
    
    @Override
        public void routeTo(final Activity activity, String routeUrl) {
            try {
                if (XTextUtil.isEmpty(routeUrl)) {
                    return;
                }
                try {
                    EventStatisticsUtil.onPageHit(new UserTrackerModel.Builder("")
                        .pageParam("").
                            action("点击路由").actionParam("app_route", URLDecoder.decode(routeUrl, "utf-8")).build());
                } catch (Exception e) {
                    XLogUtil.log().e("UnsupportedEncodingException");
                }
    
                ActivityRoute activityRoute = (ActivityRoute) Router.getRoute(routeUrl);
                activityRoute.withOpenMethodStart(activity);
                boolean newModuleOpenSuccess = activityRoute.open();
    
                if (!newModuleOpenSuccess) {
                    EventStatisticsUtil.onEvent("openRoute", "old", routeUrl);
                    XLogUtil.log().i("new module callbackIntercept failure , use old module , route : " + routeUrl);
                    oldModule.routeTo(activity, routeUrl);
                } else {
                    XLogUtil.log().i("new module callbackIntercept success , use new module , route :" + routeUrl);
                }
            } catch (Exception e) {
                EventStatisticsUtil.onEvent("openRoute", "exception", routeUrl);
                XLogUtil.log().e("new module callbackIntercept exception , use old module , route :" + routeUrl);
                oldModule.routeTo(activity, routeUrl);
            }
        }
    

    路由拦截器

    • 解决某些路由页面进入前需要先登录 (解决方法:登录拦截器)
    • 解决某些没有UI型,纯逻辑型的路由(解决方法:逻辑路由拦截器)
    • 解决黑名单路由等等 (解决办法: 黑名单拦截器)

    说的拦截器,这里用了典型的责任链设计模式

           // 登录拦截器
            Router.setInterceptor(new Interceptor() {
                @Override
                public boolean intercept(Intent intent, Context context, String url, String matchedRoute, Class<? extends Activity> matchedActivity) {
                    if (ActivityRouter.getInstance().getExtraList().contains(matchedRoute) && !UserDataUtil.getInstance().getLoginStatus()) {
                        if (context instanceof Activity) {
                            LoginActivity.goActivity((Activity) context);
                        }
                        return true;
                    } else {
                        return false;
                    }
                }
            });
            //没有UI路由拦截器
            Router.setInterceptor(noUiInterceptor);
            //招聘模块路由拦截器
            Router.setInterceptor(jobModuleInterceptor);
            //教育模块路由拦截器
            Router.setInterceptor(educationModuleInterceptor);
            //商城模块路由拦截器
            Router.setInterceptor(mallModuleInterceptor);
            //社区模块路由拦截器
            Router.setInterceptor(communityModuleInterceptor);
            //meijalove模块路由拦截器
            Router.setInterceptor(mainModuleInterceptor);
    

    反射静态方法拦截器

    需求1:需要适配以前的业务
    需求2: 跳转前各种逻辑
    需求3: 除了传递基本数据类型,还要传递对象
    需求4: 一些旧逻辑写在旧Acitivty,不想迁移代码怎么破?

    解决办法:使用反射静态方法拦截器。@MJBRouteIntercept 注解于相应的Activity方法上,会在跳转前拦截。返回true代表拦截掉Intent不再往下走。返回false会继续往下运行

    原理:找到Actiivty被@MJBRouteIntercept注解的方法,解析方法的路由,通过反射调用回Activity里的静态方法。反向控制了Activity. (其实就是EventBus的那一套!)
            @MJBRouteIntercept
            @JvmStatic
            fun interceptRoute(activity: Activity, intent: Intent): Boolean {
                val extras = intent.extras
                if (!extras.containsKey("group_id") || !extras.containsKey("title")) {
                    return true
                }
                TopicGroupActivity.goActivity(activity, extras.getString("title"), GroupModel(extras.get("group_id") as String, MJLOVE.TopicList.IMAGE_TEXT_BIG))
                return true
            }
    
            //反射静态方法拦截器
            Router.setInterceptor(new Interceptor() {
                @Override
                public boolean intercept(Intent intent, Context context, String url, String matchedRoute, Class<? extends Activity> matchedActivity) {
                    XLogUtil.log().i(String.format("[route intercept] , matchedRoute : %s ,matchedActivity : %s", matchedRoute, matchedActivity.getSimpleName()));
                    boolean result = false;
                    Method[] methods = matchedActivity.getDeclaredMethods();
                    for (Method method : methods) {
                        if (method.getAnnotation(MJBRouteIntercept.class) != null) {
                            String value = method.getAnnotation(MJBRouteIntercept.class).value();
                            if (XTextUtil.isEmpty(value) || matchedRoute.equals(value)) {
                                try {
                                    result = (boolean) method.invoke(null, context, intent);
                                } catch (IllegalAccessException e) {
                                    e.printStackTrace();
                                } catch (InvocationTargetException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                    return result;
                }
            }); 
    

    如何调用注解生成的代码?

             //添加不同模块的自生产代码
            String[] moduleNames = new String[]{"meijiaLove", "Community", "BusinessCenter", "Education", "Job", "Mall", "Support", "dwtapplet"};
    
            for (String moduleName : moduleNames) {
                try {
                    //利用反射调用注解生成的代码
                    Constructor<?> constructor = Class.forName(String.format("com.meijialove.router.router.AnnotatedRouterTableInitializer$$%s", moduleName)).getConstructor();
                    IActivityRouteTableInitializer initializer = (IActivityRouteTableInitializer) constructor.newInstance();
                    mActivityRouter.initActivityRouterTable(initializer);
                } catch (Exception e) {
                    Log.e(TAG, String.format("init %s AnnotatedRouterTableInitializer!", moduleName));
                }
            }
    
    gradle配置,这段是学ARouter的

    如何支持RestFul路由

    private Intent setKeyValueInThePath(String routeUrl, String givenUrl, Intent intent) {
            List<String> routePathSegs = getPathSegments(routeUrl);
            List<String> givenPathSegs = getPathSegments(givenUrl);
            for (int i = 0; i < routePathSegs.size(); i++) {
                String seg = routePathSegs.get(i);
                if (seg.startsWith(":")) {
                    int indexOfLeft = seg.indexOf("{");
                    int indexOfRight = seg.indexOf("}");
                    String key = seg.substring(indexOfLeft + 1, indexOfRight);
                    char typeChar = seg.charAt(1);
                    switch (typeChar) {
                        //integer type
                        case 'i':
                            try {
                                int value = Integer.parseInt(givenPathSegs.get(i));
                                intent.putExtra(key, value);
                            } catch (Exception e) {
                                Log.e(TAG, "解析整形类型失败 " + givenPathSegs.get(i), e);
                                if (BuildConfig.DEBUG) {
                                    throw new InvalidValueTypeException(givenUrl, givenPathSegs.get(i));
                                } else {
                                    //如果是在release情况下则给一个默认值
                                    intent.putExtra(key, 0);
                                }
                            }
                            break;
                        case 'f':
                            //float type
                            try {
                                float value = Float.parseFloat(givenPathSegs.get(i));
                                intent.putExtra(key, value);
                            } catch (Exception e) {
                                Log.e(TAG, "解析浮点类型失败 " + givenPathSegs.get(i), e);
                                if (BuildConfig.DEBUG) {
                                    throw new InvalidValueTypeException(givenUrl, givenPathSegs.get(i));
                                } else {
                                    intent.putExtra(key, 0f);
                                }
                            }
                            break;
                        case 'l':
                            //long type
                            try {
                                long value = Long.parseLong(givenPathSegs.get(i));
                                intent.putExtra(key, value);
                            } catch (Exception e) {
                                Log.e(TAG, "解析长整形失败 " + givenPathSegs.get(i), e);
                                if (BuildConfig.DEBUG) {
                                    throw new InvalidValueTypeException(givenUrl, givenPathSegs.get(i));
                                } else {
                                    intent.putExtra(key, 0l);
                                }
                            }
                            break;
                        case 'd':
                            try {
                                double value = Double.parseDouble(givenPathSegs.get(i));
                                intent.putExtra(key, value);
                            } catch (Exception e) {
                                Log.e(TAG, "解析double类型失败 " + givenPathSegs.get(i), e);
                                if (BuildConfig.DEBUG) {
                                    throw new InvalidValueTypeException(givenUrl, givenPathSegs.get(i));
                                } else {
                                    intent.putExtra(key, 0d);
                                }
                            }
                            break;
                        case 'c':
                            try {
                                char value = givenPathSegs.get(i).charAt(0);
                            } catch (Exception e) {
                                Log.e(TAG, "解析Character类型失败" + givenPathSegs.get(i), e);
                                if (BuildConfig.DEBUG) {
                                    throw new InvalidValueTypeException(givenUrl, givenPathSegs.get(i));
                                } else {
                                    intent.putExtra(key, ' ');
                                }
                            }
                            break;
                        case 's':
                        default:
                            intent.putExtra(key, givenPathSegs.get(i));
                    }
                }
    
            }
            return intent;
        }
    

    注解生成代码

    AbstractProcessor

    AbstractProcess (在编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类)

    ProcessingEnviroment参数提供很多有用的工具类Elements, Types和Filer。 processingEnv.getMessager(),processingEnv.getFiler(),processingEnv.getElementUtils()

    Types是用来处理TypeMirror的工具类

    Message用于打印

    Filer用来创建生成辅助文件。

    process() 这个方法的作用主要是扫描、评估和处理我们程序中的注解,然后
    生成Java文件,处理所有事情的入口

    getSupportedAnnotationTypes() 定义 需要处理的注解,需要逐一添加

    /**
     * 编译时编译器会检查AbstractProcessor的子类,并且调用该类型的process函数,然后将添加了注解的所有元素都传递到process函数中,使得开发人员可以在编译器进行相应的处理,例如,根据注解生成新的Java类
     */
    @AutoService(Processor.class)
    public class RouterProcessor extends AbstractProcessor {
    
        private Messager mMessager;
        private Filer mFiler;
        private Elements elementUtils;
        private String moduleName = null;
        private List<ClassName> needLoginClassNames = new ArrayList<>();
        private Map<ClassName, String> needLoginRouteMap = new HashMap<>();
        private boolean loggerEnable = true;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            // 打印用的
            mMessager = processingEnv.getMessager();
            // Filer用来创建生成辅助文件
            mFiler = processingEnv.getFiler(); 
            elementUtils = processingEnv.getElementUtils();
    
            // 区别不同的模块
            Map<String, String> options = processingEnv.getOptions();
            if (options != null && !options.isEmpty()) {
                moduleName = options.get("moduleName");
                log("RouteProcessor:[init] moduleName  : " + moduleName);
            }
        }
    
        /**
         * 定义需要处理的注解,需要逐个添加
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> set = new LinkedHashSet<>();
            set.add(MJBRoute.class.getCanonicalName());
            set.add(NeedLogin.class.getCanonicalName());
            return set;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        private void initNeedLoginTypeElements(RoundEnvironment roundEnv) {
            needLoginClassNames.clear();
            Set<? extends Element> needLoginElements = roundEnv.getElementsAnnotatedWith(NeedLogin.class);
            for (Element element : needLoginElements) {
                needLoginClassNames.add(ClassName.get((TypeElement) element));
            }
        }
    
    各种Elements
    AutoService

    Google 为我们提供了更便利的工具,叫 AutoService,此时只需要为注解处理器增加 @AutoService 注解就可以了,如下

    JavaPoet

    JavaPoet github地址 & 详细用法 : https://github.com/square/javapoet
    MethodSpec 代表一个构造函数或方法声明
    TypeSpec 代表一个类,接口,或者枚举声明
    FieldSpec 代表一个成员变量,一个字段声明
    JavaFile包含一个顶级类的Java文件

    private TypeSpec getRouterTableInitializer(Set<? extends Element> elements) throws ClassNotFoundException, TargetErrorException {
           if (elements == null || elements.size() == 0) {
               return null;
           }
           TypeElement activityType = elementUtils.getTypeElement("android.app.Activity");
    
           //构造 void initRouterTable(Map<String, Class<? extends Activity>> router) 方法
           ParameterizedTypeName mapTypeName = ParameterizedTypeName
               .get(ClassName.get(Map.class), ClassName.get(String.class),
                   ParameterizedTypeName.get(ClassName.get(Class.class), WildcardTypeName.subtypeOf(ClassName.get(activityType))));
           ParameterSpec mapParameterSpec = ParameterSpec.builder(mapTypeName, "router")
               .build();
           MethodSpec.Builder routerInitBuilder = MethodSpec.methodBuilder("initRouterTable")
               .addAnnotation(Override.class)
               .addModifiers(Modifier.PUBLIC)
               .addParameter(mapParameterSpec);
           for (Element element : elements) {
               if (element.getKind() != ElementKind.CLASS) {
                   throw new TargetErrorException();
               }
               MJBRoute router = element.getAnnotation(MJBRoute.class);
               String[] routerUrls = router.value();
               if (routerUrls != null) {
                   for (String routerUrl : routerUrls) {
                       log("RouteProcessor:[getRouterTableInitializer]" + routerUrl);
                       ClassName clsName = ClassName.get((TypeElement) element);
                       routerInitBuilder.addStatement("router.put($S, $T.class)", routerUrl, clsName);
    
                       if (needLoginClassNames.contains(clsName)) {
                           needLoginRouteMap.put(clsName, routerUrl);
                       }
                   }
               }
           }
           MethodSpec routerInitMethod = routerInitBuilder.build();
    
           //构造 void initExtraRouteList(List<String> extraRouteList)方法
           ParameterizedTypeName listTypeName = ParameterizedTypeName
               .get(ClassName.get(List.class), ClassName.get(String.class));
           ParameterSpec listParameterSpec = ParameterSpec.builder(listTypeName, "extraRouteList")
               .build();
           MethodSpec.Builder initExtraRouteListBuilder = MethodSpec.methodBuilder("initExtraRouteList")
               .addAnnotation(Override.class)
               .addModifiers(Modifier.PUBLIC)
               .addParameter(listParameterSpec);
           for (String loginRoute : needLoginRouteMap.values()) {
               log("RouteProcessor:[initExtraRouteList] " + loginRoute);
               initExtraRouteListBuilder.addStatement("extraRouteList.add($S)", loginRoute);
           }
    
           TypeElement routerInitializerType = elementUtils.getTypeElement("com.meijialove.router.router.IActivityRouteTableInitializer");
    
           //构建好TypeSpec
           return TypeSpec.classBuilder("AnnotatedRouterTableInitializer" + "$$" + moduleName)
               .addSuperinterface(ClassName.get(routerInitializerType))
               .addModifiers(Modifier.PUBLIC)
               .addMethod(routerInitMethod)
               .addMethod(initExtraRouteListBuilder.build())
               .build();
       }
    
    

    分析一些ButterKnife的源码

    parseBindView() , 针对@BindView这个注解生成代码
    logParsingError(), 打印错误的log .
    isInaccessibleViaGeneratedCode() , 对注解作检查,是否有private, static修饰 ,是否存在在class内。
    isBindingInWrongPackage(),是否绑错包
    isSubtypeOfType() 判断注解的类是否View
    BindingSet这个类是用JAVAPOET生成代码的核心类

    相关文章

      网友评论

          本文标题:自家开发的比ARouter更强大的路由模块

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