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

概况

曾经的多模块项目已有一套路由系统,已有各种上几百条不同风格的路由。各种不满足需求。同时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生成代码的核心类

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