Skip to content

Latest commit

 

History

History
446 lines (350 loc) · 17.1 KB

跳转拦截器.md

File metadata and controls

446 lines (350 loc) · 17.1 KB

注意

请 checkout interceptor 分支进行查看。

拦截器的作用

拦截器主要是在真正执行跳转操作之前做一些逻辑判断。比如, 我们平时 startActivity 的时候,使用隐式跳转会有一个问题,就是无法确定目标 activity 是否存在,这个时候就需要下面的代码判断一下:

PackageManager packageManager = getPackageManager();
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null){
    startActivity(intent);
}else {
    Toast.makeText(MainActivity.this,"找不到你想要的activity",Toast.LENGTH_SHORT).show();
}

但是这里有个问题,就是每次跳转前都要调用这一段代码。而使用拦截器就不一样了,虽然拦截器也是这样的一段逻辑,但是拦截器就相当于在跳转前设置了一个关卡,每个跳转请求都需要经过它,这样就与我们的跳转代码分离了,可以重用,有不少好处。那么该如何设计拦截器呢?由于拦截器可能不止一个,还要考虑优先级问题,这里我们采用责任链的设计方式。

拦截器与责任链模式

这里其实就是参考的 OkHttp 的代码写的。责任链模式就不具体介绍了,贴个图吧,不了解的可以 Google。

处理者依次排列,组成一条链

根据这个模式,我们定义出如下接口:

com.aprz.brouter.api.interceptor.IRouteInterceptor

/**
 * 拦截器接口
 */
public interface IRouteInterceptor {

    /**
     * 拦截器的优先级,数值越小优先级越高,为了规范,数字不应该小于 0
     * 或者可以做一个拦截器的依赖图,后续有精力再搞
     */
    int priority();

    /**
     * 拦截器的拦截方法,Chain 作为参数,可以使用它来决定是否拦截
     */
    void intercept(@NonNull Chain chain, @NonNull Callback callback);


    interface Chain {
        /**
         * 如果拦截器决定不拦截,调用这个方法
         */
        void proceed(@NonNull Navigation navigation);

        /**
         * 如果拦截器决定拦截,调用这个方法
         */
        void interrupt(@NonNull Throwable exception);

        /**
         * 返回路由对象
         */
        Navigation navigate();
    }

    interface Callback {
        /**
         * 跳转成功
         */
        void onSuccess(@NonNull Navigation navigation);

        /**
         * 跳转失败,被拦截器拦下了
         */
        void onFail(@NonNull Throwable exception);
    }

}

这里 IRouteInterceptorChain 都比较好理解,照着责任链的例子都可以写出来,那么 Callback 的作用是搞啥的呢?它的作用就是通知使用者,该次跳转是否成功。对于一个跳转请求,在整个责任链流程里面,callback 参数都是同一个对象

Chain 这个接口是我们需要实现的,它将 IRouteInterceptor 的实现类都收集在一起,然后依次调用:

com.aprz.brouter.api.interceptor.RouteInterceptorChain

    @Override
    public void proceed(@NonNull Navigation navigation) {
        handleInterceptors();
    }

    private void handleInterceptors() {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                if (index >= interceptors.size()) {
                    // todo 抛出异常
                    return;
                }

                IRouteInterceptor interceptor = interceptors.get(index);
                RouteInterceptorChain next = new RouteInterceptorChain(interceptors, index + 1, navigation, callback);
                interceptor.intercept(next, callback);
            }
        });
    }

这里,我将拦截器的流程分发到了主线程去执行,这里主要是考虑到拦截器里面需要弹窗等更新UI之类的操作才这样做的,当然,如果你的拦截器里面有比较繁重的逻辑,可以自己开线程处理,只要最后调用 com.aprz.brouter.api.interceptor.IRouteInterceptor.Chain#proceed 方法就行了。

拦截器的拦截逻辑

拦截器的拦截逻辑需要在跳转之前,所以,我们直接在跳转前面,加上我们的逻辑就好了:

com.aprz.brouter.api.core.Navigation#navigate(android.content.Context, com.aprz.brouter.api.interceptor.IRouteInterceptor.Callback)

    public void navigate(final Context context, final IRouteInterceptor.Callback callback) {
        Navigation target = RouteStore.getNavigation(path);
        List<IRouteInterceptor> interceptorList = InterceptorStore.getInterceptorList(target.path);
        // 最后的拦截器,不然没有 callback.onSuccess 回调
        interceptorList.add(new LastInterceptor());
        Collections.sort(interceptorList, (o1, o2) -> o1.priority() - o2.priority());
        IRouteInterceptor.Chain chain = new RouteInterceptorChain(
                interceptorList,
                0,
                target,
                new IRouteInterceptor.Callback() {
                    @Override
                    public void onSuccess(@NonNull Navigation navigation) {
                        // 执行真正的跳转逻辑
                        internalNavigate(context, navigation);
                        if (callback != null) {
                            callback.onSuccess(navigation);
                        }
                    }

                    @Override
                    public void onFail(@NonNull Throwable exception) {
                        if (callback != null) {
                            callback.onFail(exception);
                        }
                    }
                });
        chain.proceed(target);
    }

    public void internalNavigate(Context context, Navigation navigation) {
        if (navigation.valid() && context != null) {
            Intent intent = new Intent(context, targetActivityClass);
            if (params != null) {
                intent.putExtras(params);
            }
            if (!(context instanceof Activity)) {
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            context.startActivity(intent);
        }
    }

这里,对比一下没有拦截器功能的代码,可以看到,实际上就是多了几行代码而已,这几行代码做了这些事情:

  • 获取所有的拦截器
  • 对拦截器进行排序
  • 触发拦截器的责任链,依次执行拦截器里面的逻辑

拦截器实例介绍

我们看看一个拦截器例子,进一步了解拦截器,了解它可以做什么事情。

com.aprz.brouter.api.interceptor.LastInterceptor

/**
 * 该拦截器应该在所有拦截器的最后添加
 * 作用:
 * 通知拦截回调,拦截链走完了,回调成功
 */
public class LastInterceptor implements IRouteInterceptor {
    @Override
    public int priority() {
        return Integer.MAX_VALUE;
    }

    @Override
    public void intercept(@NonNull Chain chain, @NonNull Callback callback) {
        callback.onSuccess(chain.navigate());
    }
}

这个拦截器是自带的拦截器,它的作用只是用来回调 callback 的方法的。所以它应该最后一个执行才对!!!可以看跳转逻辑,我们是最后一个添加的。

com.aprz.wallet.WalletParamsInterceptor

/**
 * 该拦截器用来监测,跳转到 wallet 时,传递的参数是否合规
 */
@Interceptor(path = Constants.RoutePath.WALLET_ACTIVITY)
public class WalletParamsInterceptor implements IRouteInterceptor {

    @Override
    public int priority() {
        return 100;
    }

    @Override
    public void intercept(@NonNull Chain chain, @NonNull Callback callback) {
        Navigation navigate = chain.navigate();
        // 因为注解里面指定了path,不符合的不会走这个拦截器
        Bundle params = navigate.getParams();
        if (params != null && params.getLong("userId") > 0) {
            chain.proceed(navigate);
        } else {
            chain.interrupt(new IllegalArgumentException("参数不正确,userId 不合法!!! "));
            if (params == null) {
                Toast.makeText(BRouter.context(), "没有传递参数不正确,不合法!!!", Toast.LENGTH_SHORT).show();
            } else {
                Toast.makeText(BRouter.context(), "参数不正确,userId 不合法!!!" + params.getLong("userId"), Toast.LENGTH_SHORT).show();
            }
        }
    }
}

这个,是开发者自定义的拦截器,注意这个类需要使用 Interceptor 来修饰,因为,只有用这个注解修饰,我们才能收集到这个类,然后将他添加到拦截器的集合中。另一个需要注意的地方,就是这个拦截器指定了 path,这就说明它不是一个全局的拦截器,它只拦截指定路径的跳转请求!!!

这个类的作用注释也写的很清楚了,不管是否拦截,一定要调用 proceed 或者 interrupt 方法的其中一个,否则跳转流程就走不下去了。

拦截器的收集

自定义的拦截器需要使用 Interceptor 来修饰,这是因为我们写了一个注解处理器,他会生成这样的代码:

com.aprz.brouter.interceptors.BRouter$$Interceptor$$app

public class BRouter$$Interceptor$$app implements IModuleInterceptor {
    public Map<String, IRouteInterceptor> interceptors() {
        Map<String, IRouteInterceptor> result = new HashMap<String, IRouteInterceptor>();
        return result;
    }

    public List<IRouteInterceptor> globalInterceptors() {
        List<IRouteInterceptor> result = new ArrayList<IRouteInterceptor>();
        result.add(new NotFoundInterceptor());
        return result;
    }
}

这个类有两个方法,一个是收集全局拦截器,也就是注解里面没有指定 path 属性的。一个是收集非全局拦截器,也就是指定了 path 属性的。

有了这个类,我们使用反射的方式(也可以使用字节码方式,但是懒得实现了)实例化这个对象,就可以拿到该 module 下的所有拦截器了。

但是,这里还有问题,我们如何知道,工程有哪些 module 呢?而且,拦截器这样处理,还有类似的组件服务也是要这样处理,能不能将逻辑写的通用一点呢?

为了解决这两个问题,我又为每个 Module 生成了一个 BRouter$$Module$$xxx 类,这个类里面代码如下:

com.aprz.brouter.module.BRouter$$Module$$app

public final class BRouter$$Module$$app implements IModule {
    private IModule module;

    public BRouter$$Module$$app() {
        this.module = new com.aprz.home.HomeModule();
    }

    @Override
    public void onCreate(Application application) {
        this.module.onCreate(application);
        InterceptorHelper.addModuleInterceptor("app");
        DegradeHelper.addModuleDegrade("app");
    }

    @Override
    public void onDestroy() {
        this.module.onDestroy();
    }
}

这个类实际上是一个增强类,它在 com.aprz.home.HomeModule 的基础上(com.aprz.home.HomeModule 就是相当于组件的 Application 了),额外的做了一些事情。主要是收集 module 里面的拦截器等等。我们看看逻辑:

com.aprz.brouter.api.interceptor.InterceptorHelper

    /**
     * 这里添加每个 module 里面的自定义拦截器
     */
    public static void addModuleInterceptor(String module) {
        IModuleInterceptor moduleInterceptor = findModuleInterceptor(module);
        List<IRouteInterceptor> globalInterceptors = moduleInterceptor.globalInterceptors();
        for (IRouteInterceptor interceptor : globalInterceptors) {
            InterceptorStore.putGlobalInterceptor(interceptor);
        }
        Map<String, IRouteInterceptor> interceptors = moduleInterceptor.interceptors();
        for (Map.Entry<String, IRouteInterceptor> entry : interceptors.entrySet()) {
            InterceptorStore.putInterceptor(entry.getKey(), entry.getValue());
        }
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private static IModuleInterceptor findModuleInterceptor(String module) {
        try {
            Class<IModuleInterceptor> interceptorClass =
                    (Class<IModuleInterceptor>) Class.forName("com.aprz.brouter.interceptors.BRouter$$Interceptor$$" + module);
            return interceptorClass.newInstance();
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
            Log.e(TAG, "没有在 module 内部找到拦截器: " + module);
            return new EmptyModuleInterceptor();
        }
    }


    static class EmptyModuleInterceptor implements IModuleInterceptor {

        @Override
        public List<IRouteInterceptor> globalInterceptors() {
            return new ArrayList<>(0);
        }

        @Override
        public Map<String, IRouteInterceptor> interceptors() {
            return new HashMap<>(0);
        }
    }

就是使用反射,拿到生成的类,然后生成实例,获取 module 里面的拦截器。有了这样的一份模板,那么以后,想要收集什么,都可以这样做。

最后,我们只需要为每个module注册一下即可:

com.aprz.brouter.App#onCreate

        ModuleHelper.register("app");
        ModuleHelper.register("wallet");

这样,每个module想收集啥,都可以使用反射来做了。register 里面,就调用了 module 的生命周期:

com.aprz.brouter.api.module.ModuleHelper#register

    public static void register(String moduleName) {
        if (modules.containsKey(moduleName)) {
            Log.e(TAG, "发现了重复注册的module (默认替换为新的): " + moduleName);
        }
        IModule module = findModuleByName(moduleName);
        // 执行 module 的 onCreate 方法
        module.onCreate(BRouter.application());
        modules.put(moduleName, module);
    }

降级策略

看了一些其他项目的代码,降级策略,都是在跳转失败的时候,做降级处理。我想了半天没想明白,什么情况下才会跳转失败呢?我们的app里面,没有做过降级处理,所以暂时无法理解是什么需求导致的。但是我还是开动我的脑筋,写了一个降级的例子。

com.aprz.brouter.interceptos.NotFoundInterceptor

/**
 * 本来想将这个拦截器写在库里面的,但是发现怎么写都不爽,后来才想明白,这个拦截器应该给使用者实现才是对的
 * 当没有匹配的 url 的时候,跳转到一个 404 页面
 */
@Interceptor
public class NotFoundInterceptor implements IRouteInterceptor {

    private static final String TAG = "NotFoundInterceptor";

    @Override
    public int priority() {
        return 10;
    }

    @Override
    public void intercept(@NonNull Chain chain, @NonNull Callback callback) {
        Log.e(TAG, "NotFoundInterceptor run on " + Thread.currentThread().getName());
        Navigation navigate = chain.navigate();
        if (navigate.invalid()) {
            chain.interrupt(new RouteNotFoundException("没有找到匹配的路由地址:" + navigate.getPath()));
            // 有降级策略,走降级
            IRouteDegrade routeDegrade = DegradeHelper.getRouteDegrade(navigate);
            if (routeDegrade != null) {
                routeDegrade.handleDegrade(navigate);
            }
            // 没有降级策略,走默认页面
            else {
                // 不要搞成死循环了
                BRouter.getInstance().path("app/not_found").navigate();
            }
        } else {
            // 有降级策略,走降级
            IRouteDegrade routeDegrade = DegradeHelper.getRouteDegrade(navigate);
            if (routeDegrade != null) {
                routeDegrade.handleDegrade(navigate);
            }
            // 没有降级策略,继续执行原来的跳转
            else {
                chain.proceed(navigate);
            }
        }
    }
}

这个拦截器的逻辑比较简单,就是在 path 找不到对应页面的时候,跳转一个默认页面,或者跳到降级页面。这里可以看出来,降级算是拦截器的一个分支。

为了让这个例子更有意义,我在 path 有对应页面的时候,也会进行降级处理。例子大概是这样的,开发了一个新功能来替换老的功能,但是新的功能不一定稳定,所以有一个降级策略,就是监控新功能crash次数,当超过一定次数后,就开始降级处理:

com.aprz.wallet.WalletDegrade

@Degrade
public class WalletDegrade implements IRouteDegrade {

    @Override
    public boolean isMatch(@NonNull Navigation navigation) {
        // 当新的 activity crash 次数超过限制了之后,需要降级
        String targetPage = Constants.RoutePath.WALLET_ACTIVITY;

        return CrashMonitor.needDegrade(targetPage)
                && targetPage.equals(navigation.getPath());
    }

    @Override
    public void handleDegrade(@NonNull Navigation navigation) {
        // 降级,跳转到老的页面
        BRouter.getInstance()
                .path(Constants.RoutePath.WALLET_OLD_ACTIVITY)
                .navigate();
    }

}