自家開發(fā)的比ARouter更強(qiáng)大的路由模塊

概況

曾經(jīng)的多模塊項(xiàng)目已有一套路由系統(tǒng)椰棘,已有各種上幾百條不同風(fēng)格的路由别洪。各種不滿足需求。同時(shí)ARouter也不能完全滿足携栋,并且遷移成本很大搭盾。 所以決定開發(fā)一套類似ARouter的路由系統(tǒng)并且可以兼容舊系統(tǒng)。新路由系統(tǒng)走不通走舊系統(tǒng)婉支。

需求 & 各方案比較

  • 舊方案

    • 模塊依賴不好鸯隅,所有路由解析都實(shí)在公用的模塊中,不是真正跨模塊調(diào)用
    • 需要逐一配置路由
    • 要逐一解析參數(shù)
    • 不支持startActivityForResult
    • 不支持?jǐn)r截
  • 阿里的ARouter

    • 支持編譯期注解
    • 無需注意解析參數(shù)
    • 支持?jǐn)r截
    • 支持跨模塊調(diào)用
    • 不支持Restful風(fēng)格
    • 不支持一個(gè)Activity多路由
    • 不支持一級(jí)路由(如: meijiabang://live_list ),只支持二級(jí)以上( meijiabang://live/list
  • 需求

    • 具有ARouter的絕大部分功能
    • 支持restful風(fēng)格路由
    • 支持一個(gè)Activity多路由
    • 支持一級(jí)路由
    • 兼容舊的方案蝌以,跑不通新路由跑舊路由
    • 有攔截器炕舵,有其他沒有UI的純命令性路由
    • Activity內(nèi)有很多goActivity方法代碼可以不需要修改,直接兼容

怎樣做到兼容舊的路由模塊?

  • 典型場(chǎng)景要到代理設(shè)計(jì)模式跟畅,兼容兩套錄用路由咽筋,抽象出同一個(gè)接口
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("點(diǎn)擊路由").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);
        }
    }

路由攔截器

  • 解決某些路由頁面進(jìn)入前需要先登錄 (解決方法:登錄攔截器)
  • 解決某些沒有UI型,純邏輯型的路由(解決方法:邏輯路由攔截器)
  • 解決黑名單路由等等 (解決辦法: 黑名單攔截器)

說的攔截器徊件,這里用了典型的責(zé)任鏈設(shè)計(jì)模式

       // 登錄攔截器
        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);
        //社區(qū)模塊路由攔截器
        Router.setInterceptor(communityModuleInterceptor);
        //meijalove模塊路由攔截器
        Router.setInterceptor(mainModuleInterceptor);

反射靜態(tài)方法攔截器

需求1:需要適配以前的業(yè)務(wù)
需求2: 跳轉(zhuǎn)前各種邏輯
需求3: 除了傳遞基本數(shù)據(jù)類型奸攻,還要傳遞對(duì)象
需求4: 一些舊邏輯寫在舊Acitivty,不想遷移代碼怎么破虱痕?

解決辦法:使用反射靜態(tài)方法攔截器睹耐。@MJBRouteIntercept 注解于相應(yīng)的Activity方法上,會(huì)在跳轉(zhuǎn)前攔截部翘。返回true代表攔截掉Intent不再往下走硝训。返回false會(huì)繼續(xù)往下運(yùn)行

原理:找到Actiivty被@MJBRouteIntercept注解的方法,解析方法的路由略就,通過反射調(diào)用回Activity里的靜態(tài)方法捎迫。反向控制了Activity. (其實(shí)就是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
        }
        //反射靜態(tài)方法攔截器
        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;
            }
        }); 

如何調(diào)用注解生成的代碼表牢?

         //添加不同模塊的自生產(chǎn)代碼
        String[] moduleNames = new String[]{"meijiaLove", "Community", "BusinessCenter", "Education", "Job", "Mall", "Support", "dwtapplet"};

        for (String moduleName : moduleNames) {
            try {
                //利用反射調(diào)用注解生成的代碼
                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配置窄绒,這段是學(xué)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情況下則給一個(gè)默認(rèn)值
                                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, "解析浮點(diǎn)類型失敗 " + 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 (在編譯時(shí)編譯器會(huì)檢查AbstractProcessor的子類,并且調(diào)用該類型的process函數(shù)崔兴,然后將添加了注解的所有元素都傳遞到process函數(shù)中彰导,使得開發(fā)人員可以在編譯器進(jìn)行相應(yīng)的處理,例如敲茄,根據(jù)注解生成新的Java類)

ProcessingEnviroment參數(shù)提供很多有用的工具類Elements, Types和Filer位谋。 processingEnv.getMessager(),processingEnv.getFiler(),processingEnv.getElementUtils()

Types是用來處理TypeMirror的工具類

Message用于打印

Filer用來創(chuàng)建生成輔助文件。

process() 這個(gè)方法的作用主要是掃描堰燎、評(píng)估和處理我們程序中的注解掏父,然后
生成Java文件,處理所有事情的入口

getSupportedAnnotationTypes() 定義 需要處理的注解秆剪,需要逐一添加

/**
 * 編譯時(shí)編譯器會(huì)檢查AbstractProcessor的子類赊淑,并且調(diào)用該類型的process函數(shù),然后將添加了注解的所有元素都傳遞到process函數(shù)中仅讽,使得開發(fā)人員可以在編譯器進(jìn)行相應(yīng)的處理陶缺,例如,根據(jù)注解生成新的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用來創(chuàng)建生成輔助文件
        mFiler = processingEnv.getFiler(); 
        elementUtils = processingEnv.getElementUtils();

        // 區(qū)別不同的模塊
        Map<String, String> options = processingEnv.getOptions();
        if (options != null && !options.isEmpty()) {
            moduleName = options.get("moduleName");
            log("RouteProcessor:[init] moduleName  : " + moduleName);
        }
    }

    /**
     * 定義需要處理的注解洁灵,需要逐個(gè)添加
     */
    @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,此時(shí)只需要為注解處理器增加 @AutoService 注解就可以了,如下

JavaPoet

JavaPoet github地址 & 詳細(xì)用法 : https://github.com/square/javapoet
MethodSpec 代表一個(gè)構(gòu)造函數(shù)或方法聲明
TypeSpec 代表一個(gè)類苫费,接口汤锨,或者枚舉聲明
FieldSpec 代表一個(gè)成員變量,一個(gè)字段聲明
JavaFile包含一個(gè)頂級(jí)類的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");

       //構(gòu)造 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();

       //構(gòu)造 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");

       //構(gòu)建好TypeSpec
       return TypeSpec.classBuilder("AnnotatedRouterTableInitializer" + "$$" + moduleName)
           .addSuperinterface(ClassName.get(routerInitializerType))
           .addModifiers(Modifier.PUBLIC)
           .addMethod(routerInitMethod)
           .addMethod(initExtraRouteListBuilder.build())
           .build();
   }

分析一些ButterKnife的源碼

parseBindView() , 針對(duì)@BindView這個(gè)注解生成代碼
logParsingError(), 打印錯(cuò)誤的log .
isInaccessibleViaGeneratedCode() , 對(duì)注解作檢查百框,是否有private, static修飾 泥畅,是否存在在class內(nèi)。
isBindingInWrongPackage()琅翻,是否綁錯(cuò)包
isSubtypeOfType() 判斷注解的類是否View
BindingSet這個(gè)類是用JAVAPOET生成代碼的核心類

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市柑贞,隨后出現(xiàn)的幾起案子方椎,更是在濱河造成了極大的恐慌,老刑警劉巖钧嘶,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棠众,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡有决,警方通過查閱死者的電腦和手機(jī)闸拿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來书幕,“玉大人新荤,你說我怎么就攤上這事√ɑ悖” “怎么了苛骨?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長苟呐。 經(jīng)常有香客問我痒芝,道長,這世上最難降的妖魔是什么牵素? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任严衬,我火速辦了婚禮,結(jié)果婚禮上笆呆,老公的妹妹穿的比我還像新娘请琳。我一直安慰自己,他們只是感情好腰奋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布单起。 她就那樣靜靜地躺著,像睡著了一般劣坊。 火紅的嫁衣襯著肌膚如雪嘀倒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音测蘑,去河邊找鬼灌危。 笑死,一個(gè)胖子當(dāng)著我的面吹牛碳胳,可吹牛的內(nèi)容都是我干的勇蝙。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼挨约,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼味混!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起诫惭,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤翁锡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后夕土,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體馆衔,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年怨绣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了角溃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡篮撑,死狀恐怖减细,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赢笨,我是刑警寧澤邪财,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站质欲,受9級(jí)特大地震影響树埠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘶伟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一怎憋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧九昧,春花似錦绊袋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蹋笼,卻和暖如春展姐,著一層夾襖步出監(jiān)牢的瞬間躁垛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工圾笨, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留教馆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓擂达,卻偏偏與公主長得像土铺,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子板鬓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354