给Retrofit嵌套动态代理,高效处理运营打点
本篇文章已授权微信公众号 玉刚说 (任玉刚)独家发布
需求背景:
相信大部分朋友都经历过,运营突然来要求,要给某部分接口带上某个参数(这个参数可能是from,表示当前在哪个页面;或者 duration,表示当前界面停留了多久)。这个时候,最直接的做法就是,直接加呗~ 有些接口还被多个界面调用,要改代码的界面可能是十多个,也可能是大几十个。
//举例子,帖子点赞,原本的请求调用:
ApiService.getInstance().likePost(likeType, postId);
//直接在调用方法时加 from 参数:
ApiService.getInstance().likePost(likeType, postId, from);
而我收到的需求则是要带上当前页面和上一级页面。。。这个需求,按常规做法,在各个Activity间的intent都要传入上一级Activity的信息。这个代码量就更大了,而且代码会很累赘。
这时候,我的上级给了我提示,可以试下多重动态代理。之前我也考虑过这个需求适合用动态代理做,但是我知道Retrofit本身已经用了,我没想到还可以多重动态代理。接着就试了一下,还真的OK,果然还是大佬牛啊~!
给Retrofit嵌套一层动态代理后,我们项目中调用请求接口的地方不需要修改代码了,不用每处请求都手动添加上 from 参数,因为在这个自定义的动态代理工作时,已经帮我们统一加上了这个 from 参数。
相关知识
动态代理:方便的对被代理类的方法进行统一处理。
反射:一种能够在程序运行时动态访问、修改某个类中任意属性(状态)和方法(行为)的机制(包括private实例和方法)。
阅读本文需要你对动态代理和反射有一定的理解,不然建议先熟悉一下相关知识点。
Retrofit嵌套动态代理步骤:
1.给 Retrofit.create (final Class<T> service)方法的返回值,再加上自定义的动态代理
/**
* 获取对应的 Service
*/
<T> T create(Class<T> service) {
// Retrofit 的代理
T retrofitProxy = retrofit.create(service);
//再添加一层自定义的代理。
T customProxy = addCustomProxy(retrofitProxy);
//返回这个嵌套的代理
return customProxy;
}
/**
* 嵌套添加动态代理
* @param target 被代理的对象
* @return 返回嵌套动态代理之后的对象
*/
public <T> T addCustomProxy(T target) {
CustomProxy customProxy = new CustomProxy();
return (T) customProxy.newProxy(target);
}
2.在原来的请求接口的基础上,加上带运营打点所需要的参数(在本例就是 from 参数)
如果请求参数是每个值分开传的才需要这一步( 例如这里的 likePost 接口);
对于请求参数是一个bean类或者Map,不需要这一步( 例如这里的 savePost 接口)。
/**
* 广场发帖
*/
@FormUrlEncoded
@POST("square/post/save")
Call<RootBean<Object>> savePost(@Body EditPostRequest editPostRequest);
/**
* 帖子点赞
*/
@FormUrlEncoded
@POST("square/post/like")
Call<RootBean<Object>> likePost(@Field("likeType") int likeType, @Field("postId") long postId);
/**
* 帖子点赞
* 带"from"参数的版本
* 不要删除,动态代理会调用{@link CustomProxy}
*/
@FormUrlEncoded
@POST("square/post/like")
Call<RootBean<Object>> likePost(@Field("likeType") int likeType, @Field("postId") long postId, @Field("from") String from);
PS:注释说明“不要删除,动态代理会调用”建议一定不能省~~因为动态代理的方法在IDE中是索引不到的,同事甚至自己很容易删掉,编译是不会报错的。
3.在自定义的代理类里,真正执行统一加参数的操作
(这里还是以加 from 做例子)
/**
* 嵌套添加动态代理
* 简例:https://blog.csdn.net/zhenghuangyu/article/details/102808338
*/
public static class CustomProxy implements InvocationHandler {
//被代理对象,在这里就是 Retrofit.create(service) 的返回值
private Object mTarget;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String from = "testFrom";
final String methodName = method.getName();
switch (methodName) {
case "savePost": {
//形参是一个bean类,用这种方式
//获取第一个请求参数args[0],这是我们定义该接口形参时的bean类
EditPostRequest editPostRequest = (EditPostRequest) args[0];
//以变量形式设置
editPostRequest.setFrom(from);
break;
}
case "likePost": {
//形参是一个个值的形式,用这种方式
//将参数长度+1,作为新的参数数组
args = Arrays.copyOf(args, (args.length + 1));
//在新的参数数组末端加上 from
args[args.length - 1] = from;
//为了调用带 from 版本的方法,构造新的形参
Class[] newParams = Arrays.copyOf(method.getParameterTypes(), (method.getParameterTypes().length + 1));
//新的形参里,最后一个参数 from 是String类型的,这个必须声明,才能准确调用反射
newParams[newParams.length - 1] = String.class;
//找出新method对象,就是带 from 版本的那个方法
method = mTarget.getClass().getDeclaredMethod(method.getName(), newParams);
break;
}
}
//正式执行方法
return method.invoke(mTarget, args);
}
//在这里嵌套外层的动态代理
public Object newProxy(Object target) {
this.mTarget = target;
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
}
嗯,这样就完成了为多个接口添加参数的需求。本来少说也要修改几十个地方,现在简单优雅的解决了。更重要的是,不需要机械地添加累赘的代码,用工程化的方案解决问题。
文章重点是多重动态代理。至于我的需求里,怎么优雅地处理当前和上一级Activity的路径,我想到的方法有两种:1.用AMS获取Activity栈 2.用ActivityLifecycle 。
我用的是第二种,并通过一个Stack对象,自行记录Activity的入栈出栈。不过这个不是文章重点,不详细展开了。放上简单代码:
/**
* 要记录最新的两个页面,用栈操作
*/
private Stack<String> tagsRecords = new Stack<>();
/**
* 标签入栈
*/
public void pushTagRecord(String tag) {
tagsRecords.push(tag);
}
/**
* 标签出栈
*/
public void popTagRecord() {
tagsRecords.pop();
}
//注册LifeCycle监听,在这里完成界面对应tag的出栈入栈
Application.ActivityLifecycleCallbacks lifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//新建界面,入栈
pushTagRecord(activity.getLocalClassName());
}
@Override
public void onActivityDestroyed(Activity activity) {
//界面销毁,出栈
popTagRecord();
}
}
//然后在项目Application类的初始化方法中注册lifecycle
registerActivityLifecycleCallbacks(lifecycleCallbacks);
嗯,通过这种用lifecycle配合栈结构的方式,记录页面访问路径,就避免了在每处 startActivity()的intent里传递参数。而且这种方法比AMS获取Activity栈的方式更灵活。例如我的实际需求就是,特定的几个Activity才算有效路径。在Activity入栈出栈时,我可以做一层判断过滤,而AMS我是控制不了的。