Retrofit现在在网络请求中是使用的最多的库,它是对OkHttp的一层封装,使用起来非常方便。但是在工作中经常会遇到一些需求,比如对某些接口加上一些token验证,某一些不需要加上token验证,我们当然可以在每一个接口后面直接加上获取不加这个参数,但是这样做不是好的解决方案。那么这个时候我们就需要考虑使用自定义注解的方式来解决这个问题。

首先实现一个简单的网络请求

我们使用wanandroid中的两个接口用来做演示,wanandroid地址是https://www.wanandroid.com/

用到的两个接口如下:

https://www.wanandroid.com/banner/json
https://www.wanandroid.com/friend/json

首先定义了一个Bean类和接口类WanAndroidApi, 如下:

data class Bean(val errorCode: Int,
                val errorMsg: String,
                val data: List<*>)

public interface WanAndroidApi {

    @GET("banner/json")
    Observable<Bean> getBanner();

    @GET("friend/json")
    Observable<Bean> getFriend();
}

然后我们直接在MainActivity中使用这个接口(记得加上网络请求权限),看看效果如何:

@SuppressLint("CheckResult")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .addInterceptor(new TokenInterceptor())
                .addInterceptor(new LogInterceptor())
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl("https://www.wanandroid.com/")
                .build();

        retrofit.create(WanAndroidApi.class).getBanner()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Bean>() {
                    @Override
                    public void accept(Bean bannerBean) {
                        Log.i("oman", "getBanner data: " + bannerBean);
                    }
                });
    }

运行后打印的结果比较多,就粘贴一点:

android Retrofit post 注解 body_android

OK, 到这里简单的请求就实现了,是不是很简单,但是关于本篇的需求还是一点没开始呢?



自定义Token

首先自定义一个Token注解, 需要传递一个参数,缺省是true代表需要传递token参数,

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Token {
    boolean value() default true;
}



那么假如现在有这么一个需求,就是第一个接口banner.json不需要加token参数,而friend.json需要加token参数的话,我们就需要修改我们的接口类,对接口方法加上注解, 如下:

public interface WanAndroidApi {

    @Token(value = false)
    @GET("banner/json")
    Observable<Bean> getBanner();

    @Token
    @GET("friend/json")
    Observable<Bean> getFriend();
}

加上了注解后怎么使用呢?这里就需要思考一下了,我们想要获取方法上面的注解,其实方法有好几种,比如自定义CallAdapter.Factory,从参数中获取注解信息。或者使用双重动态代理的思想。这里我们就使用双重动态代理的思想来实现这个功能。

首先我们声明一个枚举单例(枚举单例不了解的可以点击枚举单例查看),用来保存需要添加token的接口:

public enum TokenSets {
    INSTANCE;
    public Set<String> tokenUrls = new CopyOnWriteArraySet<>();

    public void add(String url) {
        tokenUrls.add(url);
    }

    public boolean contains(String url) {
        return tokenUrls.contains(url);
    }
}

然后修改我们的代理代码块如下,这里为了方便看代码,并没有做封装,真实项目中最好封装一下:

WanAndroidApi api = retrofit.create(WanAndroidApi.class);
   wanAndroidApi = (WanAndroidApi) Proxy.newProxyInstance(this.getClassLoader(), new Class[]{WanAndroidApi.class}, new InvocationHandler() {
       @RequiresApi(api = Build.VERSION_CODES.N)
       @Override
       public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
           Token tokenAnnotation = method.getDeclaredAnnotation(Token.class);
           boolean needToken = tokenAnnotation != null && tokenAnnotation.value();
           String url = null;
           for (Annotation annotation : method.getDeclaredAnnotations()) {
               if (annotation instanceof GET) {
                   url = ((GET) annotation).value();
               } else if (annotation instanceof POST) {
                   url = ((POST) annotation).value();
               }
               //TODO 处理其它的请求类型,这里只是演示,只处理了GET POST请求
           }
           if (needToken)
               TokenSets.INSTANCE.add(BASE_URL + url);
           return method.invoke(api, objects);
       }
   });

上面的代码就实现了双重代理,在方法调用的时候,我们可以保存需要添加token的接口,保存起来。

然后就用到了OkHttp的伟大的拦截器了,拦截器的作用主要就是为了拦截我们的请求,做出一些需求上的参数改变,代码如下, 这里仅仅是模拟添加一个假的token:

public class TokenInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        String url = chain.request().url().toString();
        if (TokenSets.INSTANCE.contains(url)) {
            Log.i("aaa", "intercept need add token: " + url);
            return chain.proceed(chain.request().newBuilder().url(
                    chain.request().url().newBuilder().addQueryParameter("token", "asdf").build()).build());
        } else {
            return chain.proceed(chain.request());
        }
    }
}

好了,准备工作做好了,现在运行一下吧,看看结果:

android Retrofit post 注解 body_json_02


很明显,上面已经实现了我们的需求,仅仅对需要添加token的接口添加了token。这样以后需要添加token的接口直接注解上Token就可以了,是不是非常方便?而且如果想向header中添加以下参数的话,只需要在拦截器中添加到header中即可,原理明白了,怎么使用就随意了。

下面粘上整个MainActivity类的代码, xml文件中其实仅仅只有两个按钮而已,点击分别获取两个接口的数据。

public class MainActivity extends AppCompatActivity {

    WanAndroidApi wanAndroidApi;

    private static final String BASE_URL = "https://www.wanandroid.com/";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .addInterceptor(new TokenInterceptor())
                .addInterceptor(new LogInterceptor())
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .client(client)
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl("https://www.wanandroid.com/")
                .build();

        WanAndroidApi api = retrofit.create(WanAndroidApi.class);

        wanAndroidApi = (WanAndroidApi) Proxy.newProxyInstance(this.getClassLoader(), new Class[]{WanAndroidApi.class}, new InvocationHandler() {
            @RequiresApi(api = Build.VERSION_CODES.N)
            @Override
            public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                Token tokenAnnotation = method.getDeclaredAnnotation(Token.class);
                boolean needToken = tokenAnnotation != null && tokenAnnotation.value();
                String url = null;
                for (Annotation annotation : method.getDeclaredAnnotations()) {
                    if (annotation instanceof GET) {
                        url = ((GET) annotation).value();
                    } else if (annotation instanceof POST) {
                        url = ((POST) annotation).value();
                    }
                    //TODO 处理其它的请求类型,这里只是演示,只处理了GET POST请求
                }
                if (needToken)
                    TokenSets.INSTANCE.add(BASE_URL + url);
                return method.invoke(api, objects);
            }
        });
    }

    @SuppressLint("CheckResult")
    public void apiNotContainToken(View view) {
        wanAndroidApi.getBanner()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Bean>() {
                    @Override
                    public void accept(Bean bannerBean) {
                        Log.i("oman", "getBanner data: " + bannerBean.toString());
                    }
                });
    }

    @SuppressLint("CheckResult")
    public void apiContainToken(View view) {
        wanAndroidApi.getFriend()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<Bean>() {
                    @Override
                    public void accept(Bean friendBean) {
                        Log.i("oman", "getFriend data: " + friendBean);
                    }
                });
    }
}

源码的地址在github中,点击Android进阶指南查看,里面还有一些常见的面试题和对应的答案,后面会不停的丰富这个项目,喜欢的话就star支持一下。