一種基于Retrofit 1.x的簡(jiǎn)單Mock機(jī)制

背景

客戶端開發(fā)過程中難免會(huì)遇到服務(wù)器接口尚未開發(fā)完成、服務(wù)器正在部署扫皱、測(cè)試服務(wù)器掛了等等情況導(dǎo)致接口無(wú)法訪問,影響客戶端的調(diào)試。如果你們的APP的開發(fā)流程是某一個(gè)版本的客戶端和服務(wù)端同步開發(fā)的話族购,這種情況會(huì)更頻繁。這時(shí)候我們就需要一個(gè) Mock service 來為服務(wù)器接口請(qǐng)求做一個(gè)“假冒”的響應(yīng)了陵珍。

本文介紹我們項(xiàng)目基于 Retrofit 1.9.0 的一直簡(jiǎn)單的 Mock 實(shí)現(xiàn)寝杖。Retrofit 2.x 已經(jīng)出來很長(zhǎng)時(shí)間了,而且 2.x 有 內(nèi)建的 Mock 機(jī)制互纯,所以對(duì)于 2.x瑟幕,本文就沒有直接意義了。俺們?yōu)槭裁礇]有升級(jí) 2.x留潦,因?yàn)檎邕x擇一個(gè)第三方庫(kù)是一個(gè)需要十分慎重的決定一樣只盹,升級(jí)一個(gè)第三方庫(kù)也需要慎重,尤其是大版本升級(jí)兔院。Retrofit 2.x 相對(duì)于1.x 有很大的變化殖卑,在俺們的項(xiàng)目目前用 1.9.0 沒有出過什么問題的情況下,不想冒險(xiǎn)去升級(jí)2.x 坊萝。所以基于 1.9.0 實(shí)現(xiàn)了這很簡(jiǎn)單的 Mock孵稽,只在 Retrofit 1.9.0 測(cè)試過许起,但是相信 Retrofit 1.x 都能使用。

發(fā)現(xiàn)突破口

關(guān)于 Retrofit 的使用菩鲜,前面的 這篇文章 已經(jīng)有介紹街氢。
Retrofit 1.x 需要先 new RestAdapter.Builder(),并且需要調(diào)用這個(gè) Builder 實(shí)例的 setClient(final Client client) 方法來設(shè)置 Client (這里這個(gè) Client 的類型是 retrofit.client.Client)睦袖。一般來說珊肃,也可以不調(diào)用 setClient()RestAdapterensureSaneDefaults() 方法保證了有默認(rèn)值可以使用馅笙,具體可查看源碼伦乔。查看源碼可以發(fā)現(xiàn) retrofit.client.ClientResponse execute(Request request) 方法就是執(zhí)行 Request 并得到 Response 的地方。那么我們就可以在這里入手做些文章董习。

實(shí)現(xiàn)

用裝飾者模式烈和,實(shí)現(xiàn) Client 接口,把本來的 Client 包裝一下皿淋,來給 execute() 方法增加 mock 的職能招刹。

class MyClient implements Client {

    private Client mClient;
    private MockServer mMockServer = null;

    /**
     * @param endPoint 服務(wù)器HOST
     * @param realClient 不用mock時(shí),真正的client
     * @param interfaceClass 聲明API方法的interface
     */
    public MyClient(String endPoint, Client realClient, Class interfaceClass) {
        if (realClient == null) {
            throw new IllegalArgumentException("real client must not be null!");
        }

        this.mClient = realClient;
        if (Constants.MOCK_RESP_ENABLED) {
            this.mMockServer = new MockServer(endPoint, interfaceClass);
        }
    }

    @Override
    public Response execute(Request request) throws IOException {
        String url = request.getUrl();
        // 由于執(zhí)行到execute()方法時(shí)窝趣,如果是GET疯暑,request的url已經(jīng)帶了參數(shù),
        // 所以我們需要處理url哑舒,取出一個(gè)API本身的path
        String path = null;
        try {
            int indexOfQuery = url.indexOf('?');
            if (indexOfQuery > 0) {
                path = url.substring(0, indexOfQuery);
            } else {
                path = url;
            }
        } catch (Exception ignored) {
        }

        Response mockResponse = null;
        // 配置一個(gè)是否啟用mock的開關(guān)妇拯,最好配置在build.gradle里,在上線前關(guān)閉
        if (Constants.MOCK_RESP_ENABLED) {
            // 根據(jù)path來區(qū)分是哪個(gè)API
            mockResponse = mMockServer.makeMockResponse(url, path);
        }

        // 如果這個(gè)API配置了mock洗鸵,則使用mock越锈,否則請(qǐng)求真實(shí)服務(wù)器接口
        if (mockResponse != null) {
            return mockResponse;
        } else {
            return mClient.execute(request);
        }
    }
}

接下來,MockServer 類的實(shí)現(xiàn)思路就很簡(jiǎn)單了膘滨, makeMockResponse() 方法針對(duì)不同的API(通過 path 參數(shù)判斷)返回相應(yīng)的假數(shù)據(jù)即可甘凭。用一個(gè) HashMap<String, String> 來存儲(chǔ)path 對(duì)應(yīng) mock response 的鍵值對(duì)表,對(duì)相應(yīng)的API根據(jù)key查詢火邓。

但是為了與 Retrofit 本身使用注解的風(fēng)格相統(tǒng)一丹弱,我們也可以使用注解來標(biāo)注一個(gè)接口方法的Mock。如:

interface MyApiService {

    // ...

    @MyMock(MockResponses.USER_INFO)
    @GET("/api/userInfo")
    void getUserInfo(Callback<String> cb);

    // ...
}
interface MockResponses {

    // ...
    
    String USER_INFO = "{\"status\":0,\"message\":\"\",\"data\":{\"nickname\":\"Donald Trump\",\"age\":70}}";
    
    // ...
}

自定義注解 MyMock

package me.tangni.xxx;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * @author tangni
 */

@Documented
@Target(METHOD)
@Retention(RUNTIME)
public @interface MyMock {
    String value();
}

MockServer 中贡翘,構(gòu)造方法需要傳入聲明接口方法的 interface 的類型蹈矮,本例中就是 MyApiService.class,然后通過反射鸣驱,遍歷其中的所有聲明的方法泛鸟,并取出每個(gè)方法的 @GET@POST 注解 (暫時(shí)只處理這兩個(gè))中的API的path,用作存儲(chǔ) Mock response 的 key踊东。

public class MockServer {

    private HashMap<String, String> mockResps;

    public MockServer(String endPoint, Class interfaceClass) {
        if (Constants.MOCK_RESP_ENABLED) {
            mockResps = generateMockRespMap(endPoint, interfaceClass);
        }
    }

    /**
     *
     * @param reqUrl 請(qǐng)求的url
     * @param path 接口的path
     * @return 如果該接口指定了mock response北滥,則返回mock response刚操,否則返回null。
     */
    public Response makeMockResponse(String reqUrl, String path) {
        if (mockResps != null && mockResps.containsKey(path)) {
            String mockResp = mockResps.get(path);
            return generateMockResponse(reqUrl, mockResp, System.currentTimeMillis());
        } else {
            return null;
        }
    }

    private static HashMap<String, String> generateMockRespMap(String endPoint, Class interfaceClass) {
        HashMap<String, String> map = new HashMap<>();
        Method[] methods = interfaceClass.getDeclaredMethods();
        if (methods != null && methods.length > 0) {
            for (Method method : methods) {
                MyMock mockRespAnno = method.getAnnotation(MyMock.class);
                if (mockRespAnno != null) {
                    String path = null;
                    // TODO: 這里先只處理 GET 和 POST
                    Annotation methodAnno = method.getAnnotation(GET.class);
                    if (methodAnno == null) {
                        methodAnno = method.getAnnotation(POST.class);
                        if (methodAnno != null) {
                            path = ((POST)methodAnno).value();
                        }
                    } else {
                        path = ((GET)methodAnno).value();
                    }

                    if (!TextUtils.isEmpty(path)) {
                        String mockResp = mockRespAnno.value();
                        map.put(endPoint + path, mockResp);
                    }
                }
            }
        }
        return map;
    }

    private static Response generateMockResponse(String url, final String mockResp, long sentMillis) {
        final int contentLen = mockResp != null ? mockResp.length() : 0;

        List<Header> headers = new ArrayList<>();
        headers.add(new Header("Server", "MOCK-SERVER"));
        headers.add(new Header("Content-Type", "application/json;charset=UTF-8"));
        headers.add(new Header("OkHttp-Selected-Protocol", "http/1.1"));
        headers.add(new Header("OkHttp-Sent-Millis", String.valueOf(sentMillis)));
        headers.add(new Header("OkHttp-Received-Millis", String.valueOf(System.currentTimeMillis())));

        TypedInput body = new TypedInput() {
            @Override
            public String mimeType() {
                return "application/json;charset=UTF-8";
            }

            @Override
            public long length() {
                return contentLen;
            }

            @Override
            public InputStream in() throws IOException {
                String content = mockResp;
                if (content == null) {
                    content = "";
                }
                return new ByteArrayInputStream(content.getBytes("UTF-8"));
            }
        };

        return new Response(url, 200, "MOCK-OK", headers, body);
    }
}

最后再芋,使用:

RestAdapter restAdapter = new RestAdapter.Builder()
        .setLogLevel(Constants.DEBUG ? RestAdapter.LogLevel.FULL : RestAdapter.LogLevel.NONE)
        .setRequestInterceptor(/*...*/)
        .setConverter(new MyCustomJsonConverter())
        .setClient(new MyClient(Constants.SERVER_HOST, new MyRealClient(), MyApiService.class))
        .setEndpoint(Constants.SERVER_HOST)
        .build();

mApiService = restAdapter.create(MyApiService.class);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菊霜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子济赎,更是在濱河造成了極大的恐慌鉴逞,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件司训,死亡現(xiàn)場(chǎng)離奇詭異构捡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)壳猜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門勾徽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人统扳,你說我怎么就攤上這事喘帚。” “怎么了咒钟?”我有些...
    開封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵吹由,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我盯腌,道長(zhǎng)溉知,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任腕够,我火速辦了婚禮,結(jié)果婚禮上舌劳,老公的妹妹穿的比我還像新娘帚湘。我一直安慰自己,他們只是感情好甚淡,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開白布大诸。 她就那樣靜靜地躺著,像睡著了一般贯卦。 火紅的嫁衣襯著肌膚如雪资柔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天撵割,我揣著相機(jī)與錄音贿堰,去河邊找鬼。 笑死啡彬,一個(gè)胖子當(dāng)著我的面吹牛羹与,可吹牛的內(nèi)容都是我干的故硅。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼纵搁,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼吃衅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腾誉,我...
    開封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤徘层,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后利职,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惑灵,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年眼耀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了英支。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡哮伟,死狀恐怖干花,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情楞黄,我是刑警寧澤池凄,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站鬼廓,受9級(jí)特大地震影響肿仑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碎税,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一尤慰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧雷蹂,春花似錦伟端、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至萎庭,卻和暖如春霜医,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驳规。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工肴敛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人达舒。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓值朋,卻偏偏與公主長(zhǎng)得像叹侄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子昨登,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,321評(píng)論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理趾代,服務(wù)發(fā)現(xiàn),斷路器丰辣,智...
    卡卡羅2017閱讀 134,715評(píng)論 18 139
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說閱讀 11,010評(píng)論 6 13
  • —1— 小時(shí)候性格咋咋呼呼 在班里有一種“我就是老大”的感覺 朋友挺多 但無(wú)法交心 有一段時(shí)間我和燕子撒强、曼曼關(guān)系很...
    一號(hào)公路上閱讀 279評(píng)論 0 0
  • 很多時(shí)候我們都拼命的想要?jiǎng)e人別的更好,想要給他更好笙什,卻忘記了本質(zhì)這種東西飘哨。本質(zhì)使然,豈是你我之力可以扭轉(zhuǎn)的
    Qiao雪蓮閱讀 162評(píng)論 0 0