Retrofit2實現(xiàn)Mock數(shù)據(jù)

github

https://github.com/javalong/RetrofitMocker

如何使用

*******
//在開發(fā)階段,會需要一個enable開關(guān),可以關(guān)閉mock數(shù)據(jù)
  @MOCK(value = "", enable = false)
    @GET("http://**mock2")
    Observable<String> mock1();

   //直接使用文件名,會從assets文件夾中讀取文件,直接mock數(shù)據(jù)返回
    @MOCK("mockdata.json")
    @GET("http://**mock2")
    Observable<MockBean> mock2();

  //支持暫時訪問某個http地址
    @MOCK("http://***mock1")
    @GET("http://***mock2")
    Observable<String> mock3();

**********

public class MockBean {
    private String test;

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }

    @Override
    public String toString() {
        return "MockBean{" +
                "test='" + test + '\'' +
                '}';
    }
}

總結(jié):原本自己本地的Retrofit2如何使用,現(xiàn)在還是如何使用,只是在需要mock的接口上面添加注釋@MOCK。

@Target(METHOD)
@Retention(RUNTIME)
public @interface MOCK {
    //可以直接是assets文件夾中的一個json文件,也可以是一個暫時的mock地址
    String value() default "";
    //提供一個開關(guān),自己debug的時候使用
    boolean enable() default true;
}
解決的問題
  1. 在開發(fā)階段,后臺經(jīng)常會發(fā)布,或者接口還未寫好,但是格式已經(jīng)定好,其實這時候完全可以自己先寫一個json文件放在assets文件夾中,然后自己使用,不需要一直等待。后臺經(jīng)常時不時的發(fā)布,重啟。前端只能一直等待。

  2. 我自己本身會使用rap來mock數(shù)據(jù),但是在發(fā)布的時候必須要改回來,也就是需要改動代碼。記得某一次,上線,發(fā)布。代碼中有一處mock數(shù)據(jù)還是用的rap上的url。這就很操蛋了。這里可以統(tǒng)一配置。
    public void init(Context context, String baseUrl, boolean needMock);
    最后一個參數(shù)可以配置成BuildConfig.DEBUG 就再也不會出錯了。

  3. 記得還有一種mock方式是直接在代碼里面一個個創(chuàng)建對象,然后塞入不同數(shù)據(jù),相對來說會比較麻煩,而且也是存在2的問題的。

實現(xiàn)的原理

大家都知道Retrofit的主要的一段代碼就是

 public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, Object... args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod serviceMethod = loadServiceMethod(method);
            OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

使用了動態(tài)代理模式,當(dāng)我們調(diào)用**Api方法時,其實就是進入了這里,然后它會進行解析。
為了不影響Retrofit之前的使用,我這里也決定采用動態(tài)代理的方式。也就是在這一層動態(tài)代理的外面再包一層動態(tài)代理。

既然使用到了動態(tài)代理,那么也會使用到反射。

當(dāng)調(diào)用**Api里面的方法時,首先會進入我的動態(tài)代理,反射獲取這個方法是否有@MOCK注釋,有就自己進行處理,沒有就傳遞給Retrofit的動態(tài)代理。

總結(jié):使用到了反射,動態(tài)代理。

源碼解析

由于這個mock的使用是基于我之前寫的Retrofit-RxJava的框架,如果還不太了解的人,可以先簡單了解下。
http://www.itdecent.cn/p/17e3e3102c1f

  1. needMock的使用
 public void init(Context context, String baseUrl, boolean needMock)
...
 public <T> void registerApi(Class<T> cls) {
        if (mContext == null) {
            throw new RuntimeException("need to run init method!");
        }
        if (cls != null && !apiMap.containsKey(cls)) {
            T api = mRetrofit.create(cls);
            if (needMock) {
                apiMap.put(cls, Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{cls}, new MockHandler(mRetrofit, mContext, api)));
            } else {
                apiMap.put(cls, api);
            }
        }
    }
..

如果 needMock=true,那么apiMap中存的就是Retrofit.create得到的動態(tài)代理的動態(tài)代理。也就是雙重代理。如果為false,則不做特殊處理。所以大家不需要懷疑正式發(fā)布版上性能上的問題。

  1. MockHandler
    當(dāng)needMock=true,我們設(shè)置了自己的動態(tài)代理,這里的關(guān)鍵代碼是
 Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{cls}, new MockHandler(mRetrofit, mContext, api))

可以看到MockHandler就是用來處理主要邏輯的類,是InvocationHandler的子類

public class MockHandler<T> implements InvocationHandler{
...
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
...

}

MockHandler其實就是這個mock數(shù)據(jù)的框架中最重要的類了,其中最重要的方法也就是invoke方法。
如果我們使用了Retrofit去請求接口會發(fā)現(xiàn)所有的接口都會先走這里,所以我們在這里進行統(tǒng)一的處理。

  1. 是否存在MOCK注釋
      boolean isExist = method.isAnnotationPresent(MOCK.class);
      if (isExist) {
          MOCK mock = method.getAnnotation(MOCK.class);
         ...
          }
      } else {
          //如果method有mock注解,就處理下,如果沒有,就直接調(diào)用后返回
          return method.invoke(api, args);
      }
    
    如果存在就繼續(xù)下一步進行處理,如果不存在,就直接調(diào)用 Retrofit原本的動態(tài)代理的方法。
  2. MOCK注釋是否是以http開頭
       if (mock.value().startsWith("http")) {
             //如果是http的 就嘗試自己去請求,就自己修改下url 然后請求
            preLoadServiceMethod(method, mock.value());
            return method.invoke(api, args);
       } 
          ...
         private void preLoadServiceMethod(Method method, String relativeUrl) {
          try {
              Method m = Retrofit.class.getDeclaredMethod("loadServiceMethod", Method.class);
              m.setAccessible(true);
              Object serviceMethod = m.invoke(retrofit, method);
              Field field = serviceMethod.getClass().getDeclaredField("relativeUrl");
              field.setAccessible(true);
              field.set(serviceMethod, relativeUrl);
          } catch (Exception e) {
              e.printStackTrace();
          }
      } 
    

如果是http開頭的,就是要網(wǎng)絡(luò)請求??催^Retrofit源碼的都知道每個請求方法都會被解析成一個ServiceMethod方法,然后緩存起來。然后請求的地址呢,其實就是ServiceMethod中的一個參數(shù),就是relativeUrl。這里直接反射進行修改。修改后,還是繼續(xù)調(diào)用Retrofit的動態(tài)代理。這時候會發(fā)現(xiàn)方法對應(yīng)的ServiceMethod對象已經(jīng)存在了,就不會再解析,而是拿過來直接使用。

    if (mock.value().startsWith("http")) {
                  ...
              } else {
                  //認為是在assets中
                  String response = readAssets(mock.value());
                  
                  Object responseObj = retrofit.nextResponseBodyConverter(null, getReturnTye(method), method.getAnnotations()).convert(ResponseBody.create(MediaType.parse("application/json"), response));
                  Object obj = retrofit.nextCallAdapter(null, method.getGenericReturnType(), method.getAnnotations()).adapt(new MockCall(responseObj));
                  return obj;
              }

這里一共3行代碼 ,第一行就不說了。后面2行代碼,我想了很久。因為我不能去修改Retrofit本身的代碼,也盡量要滿足他現(xiàn)有的一些功能,比如返回值是Observale<MockBean>類型的。
這里就涉及到了2個方面的問題

  1. String類型的轉(zhuǎn)化
  2. Observale對象的轉(zhuǎn)化

其實在Retrofit本身呢是做好了這2個方面的支持的,而且是可以配置的,

 mRetrofit = new Retrofit.Builder().baseUrl(BASE_URL).
                addConverterFactory(TWGsonConverterFactory.create()).
                addCallAdapterFactory(RxJava2CallAdapterFactory.create()).
                client(mOkHttpClient).
                build();

前面對應(yīng)的2個問題需要用如下2個對象去解決

  1. ConvertFatory
  2. CallAdapterFactory

所以

//獲取到retrofit中設(shè)置的 ConvertFactory,然后得到轉(zhuǎn)化后的類型。
 retrofit.nextResponseBodyConverter(null, getReturnTye(method), method.getAnnotations()).convert(ResponseBody.create(MediaType.parse("application/json"), response));
//獲取retrofit中對應(yīng)的CallAdapterFactory,然后適配RxJava
retrofit.nextCallAdapter(null, method.getGenericReturnType(), method.getAnnotations()).adapt(new MockCall(responseObj));

  1. MockCall
    由于CallAdapteradapt需要傳入Call對象,這里只能自己構(gòu)造一個

class MockCall<R> implements Call<R> {

    Object data;

    public MockCall(Object data) {
        this.data = data;
    }

    private Response getResponse() {
        return Response.success(data);
    }

    @Override
    public Response<R> execute() throws IOException {
        return getResponse();
    }

    @Override
    public void enqueue(Callback<R> callback) {
        callback.onResponse(null, getResponse());
    }

    @Override
    public boolean isExecuted() {
        return false;
    }

    @Override
    public void cancel() {

    }

    @Override
    public boolean isCanceled() {
        return false;
    }

    @Override
    public Call<R> clone() {
        return this;
    }

    @Override
    public Request request() {
        return null;
    }
}

構(gòu)造的很簡單,從assets中讀取文件返回數(shù)據(jù),就直接返回 Response.success(data) 。

注意

  1. assets文件中的mock的數(shù)據(jù)是json格式,不需要
     {
       success:true,
       data:{返回的具體的數(shù)據(jù)}
     }
    

這里就直接用 返回的具體的數(shù)據(jù)就好了。

  1. 暫時還沒有想到

不足

因為是剛完成的代碼,沒有經(jīng)過時間的檢驗,肯定是由很多不足的地方的。希望有時間能有所改進,下面我列出我自己能想到的幾點。

  1. 當(dāng)@MOCK value是“http”開頭時,直接修改ServiceMethodrelativeUrl,可能存在隱患。因為他本身的代碼是這樣的
private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody) {
      if (this.httpMethod != null) {
        throw methodError("Only one HTTP method is allowed. Found: %s and %s.",
            this.httpMethod, httpMethod);
      }
      this.httpMethod = httpMethod;
      this.hasBody = hasBody;

      if (value.isEmpty()) {
        return;
      }

      // Get the relative URL path and existing query string, if present.
      int question = value.indexOf('?');
      if (question != -1 && question < value.length() - 1) {
        // Ensure the query string does not have any named parameters.
        String queryParams = value.substring(question + 1);
        Matcher queryParamMatcher = PARAM_URL_REGEX.matcher(queryParams);
        if (queryParamMatcher.find()) {
          throw methodError("URL query string \"%s\" must not have replace block. "
              + "For dynamic query parameters use @Query.", queryParams);
        }
      }

      this.relativeUrl = value;
      this.relativeUrlParamNames = parsePathParameters(value);
    }

不僅僅只是給relativeUrl賦值,還做了路徑的解析。這里可能還存在一點隱患。

  1. 功能上還不是很全
    目前只能做返回的body數(shù)據(jù)的mock。希望以后能完善,加上header數(shù)據(jù)的mock,這樣應(yīng)該更好。

  2. 代碼上
    代碼上始終覺得有點粗糙,希望能再優(yōu)化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容