實(shí)現(xiàn)一個(gè)簡(jiǎn)單Retrofit(一)

在Android上寫(xiě)過(guò)網(wǎng)絡(luò)請(qǐng)求的小伙伴對(duì)Retrofit應(yīng)該不陌生,這里我們先來(lái)簡(jiǎn)單回顧一下Retrofit。

Retrofit

Retrofit 是由Square公司開(kāi)源的適用于Android和java的類(lèi)型安全的Http客戶(hù)端。

使用方法相當(dāng)簡(jiǎn)單,首先將需要的http請(qǐng)求以接口的形式定義出來(lái):

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

接著使用Retrofit生成GitHubService接口的實(shí)例。

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

GitHubService service = retrofit.create(GitHubService.class);

接著只需要調(diào)用對(duì)應(yīng)接口即可:

Call<List<Repo>> repos = service.listRepos("octocat");

以上調(diào)用會(huì)使用get方式請(qǐng)求https://api.github.com/users/octocat/repos

分析實(shí)現(xiàn)原理

如果只是簡(jiǎn)單看上面的demo,會(huì)發(fā)現(xiàn) Retrofit 似乎給我們變了一個(gè)小魔術(shù),讓我們的接口直接就能做網(wǎng)絡(luò)請(qǐng)求了。但魔術(shù)終歸只是障眼法,我們來(lái)分析一下魔術(shù)背后的原理。

首先,為什么定義了接口就能直接用了,其實(shí)這在 java 中也不是什么新鮮事了,有很多手段可以實(shí)現(xiàn)這個(gè)。其中最簡(jiǎn)單的就是 Proxy 的 newProxyInstance() 方法,它可以動(dòng)態(tài)生成一個(gè)接口的實(shí)例。

接著就是get網(wǎng)絡(luò)請(qǐng)求 url:https://api.github.com/users/octocat/repos 是如何得到的?這個(gè)其實(shí)也很簡(jiǎn)單,Retrofit 使用 Get 這個(gè)注解來(lái)標(biāo)明這是一個(gè) get 請(qǐng)求,然后使用 Path 來(lái)表示請(qǐng)求中需要被替換的 path,最后使用將 baseUrl 中拿到的 url 和 Get 中的路徑拼接,就能得到最后需要的 url 了。

魔術(shù)背后的原理都知道了,接下來(lái)就看看怎么進(jìn)行表演網(wǎng)絡(luò)請(qǐng)求這個(gè)魔術(shù)了。

實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的 Get 請(qǐng)求

接下來(lái)我們實(shí)現(xiàn)這么一個(gè)簡(jiǎn)單的請(qǐng)求百度首頁(yè)的功能:

    interface SimpleRequest{
        @GET
        String goBaidu(@Url String url);
    }
SimpleRequest simpleRequest = new Retro.
                Builder().
                build().
                create(SimpleRequest.class);
println(simpleRequest.goBaidu("http://www.baidu.com"));

上面我們定義了一個(gè) SimpleRequest 的接口,定義了 goBaidu 這個(gè)方法,使用 Get 來(lái)表示它是一個(gè) get 請(qǐng)求,用 Url 來(lái)表示參數(shù) url 就是這個(gè)請(qǐng)求的完整的地址。

接著使用 Retro 這個(gè)類(lèi)來(lái)生成 SimpleRequest 的實(shí)例,最后使用 println 來(lái)打印出 goBaidu("http://www.baidu.com") 這個(gè)調(diào)用的結(jié)果。

定義 GET 和 Url 注解

首先定義 GTE 和 Url 這兩個(gè)注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GET {
    String value() default "";
}


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Url {
}

這兩個(gè)注解比較簡(jiǎn)單,不多說(shuō)什么。

定義 Retro

接著定義Retro這個(gè)類(lèi):

public class Retro {
    
    @SuppressWarnings("unchecked")
    public <T> T create(final Class<T> server){
        return (T)Proxy.newProxyInstance(server.getClassLoader(), new Class<?>[]{server}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                ServiceMethod.Builder builder = new ServiceMethod.Builder(Retro.this,method,args);
                ServiceMethod serviceMethod = builder.build();
                return serviceMethod.call();
            }
        });
    }

}

目前Retro這個(gè)類(lèi)只有一個(gè)方法 create 方法,其接受一個(gè) Class 對(duì)象,并返回這個(gè) Class 所對(duì)應(yīng)的實(shí)例。由于使用的是 Proxy 的 newProxyInstance 方法,所有的方法都會(huì)被 InvocationHandler 對(duì)象的 invoke 方法攔截,也可以說(shuō)是被代理了。

接著我們?cè)?invoke 方法中看到了 ServiceMethod 這個(gè)類(lèi),它是這里的關(guān)鍵,所有請(qǐng)求相關(guān)的處理都是在其中被完成。接著我們就來(lái)看一下 ServiceMethod 應(yīng)該怎么編寫(xiě)。

定義 ServiceMethod

由于需要在 ServiceMethod 處理相關(guān)數(shù)據(jù),所以需要將 Retro,method,args 這幾個(gè)對(duì)象傳遞給它。其中 Retro 用于獲得一些全局的變量,這里暫時(shí)用不到。 method 用于獲得 http 的請(qǐng)求方法和 Url 注解所標(biāo)注的 url。 args 則是 Url 注解所標(biāo)注的實(shí)際參數(shù)。

知道了這些以后就可以編寫(xiě)代碼了:

public static final class Builder{

        private String baseUrl;

        private Object args[];

        private String httpMethod ;

        private Class<?>[] methodParamsTypes;

        private final Annotation[] methodAnnotations;
        private final Annotation[][] methodParamsAnnotations;
        
        public Builder(Retro retro, Method method, Object[] args){
            this.args = args;
            this.methodAnnotations = method.getAnnotations();
            this.methodParamsAnnotations = method.getParameterAnnotations();
            this.methodParamsTypes = method.getParameterTypes();
        }

        public ServiceMethod build(){
            for(int i = 0;i<methodAnnotations.length;i++){
                parseMethodAnnotation(methodAnnotations[i]);
            }
            for(int i = 0;i<methodParamsAnnotations.length;i++){
                parseMethodParamsAnnotation(methodParamsAnnotations[i],args[i],methodParamsTypes[i]);
            }

            return new ServiceMethod(this);
        }

        private void parseMethodAnnotation(Annotation annotation){
            if(annotation instanceof GET){
                parseHttpMethodAndPath("GET",((GET)annotation).value(),false);
            }
        }

        private void parseMethodParamsAnnotation(Annotation[] annotations,Object value,Class<?> type){
            if(annotations.length == 1){
                Annotation annotation = annotations[0];
                if(annotation instanceof Url){
                    baseUrl = String.valueOf(value);
                }
            }
        }
        private void parseHttpMethodAndPath(String httpMethod,String value,boolean hasBody){
            this.httpMethod = httpMethod;
        }
    }

ServiceMethod 還是使用 Builder 來(lái)構(gòu)建,看 build 方法,首先對(duì)方法的注解進(jìn)行處理,即需要得到這里的 GET 注解,看 parseMethodAnnotation 方法,

 private void parseMethodAnnotation(Annotation annotation){
     if(annotation instanceof GET){
         parseHttpMethodAndPath("GET",((GET)annotation).value(),false);
     }
 }

這里過(guò)濾我們的 GET 注解,讓后調(diào)用 parseHttpMethodAndPath 方法對(duì)其進(jìn)行處理:

   private void parseHttpMethodAndPath(String httpMethod,String value,boolean hasBody){
       this.httpMethod = httpMethod;
   }

這里我們就得到了 http 的請(qǐng)求方法,即 GET 。

接著看 parseMethodParamsAnnotation 這個(gè)方法,用于解析方法參數(shù)的標(biāo)注,即這里的 Url 標(biāo)注。

private void parseMethodParamsAnnotation(Annotation[] annotations,Object value,Class<?> type){
    if(annotations.length == 1){
        Annotation annotation = annotations[0];
        if(annotation instanceof Url){
            baseUrl = String.valueOf(value);
        }
    }
}

這個(gè)方法會(huì)遍歷所有參數(shù)的標(biāo)注,如果有 Url 標(biāo)注,就拿到對(duì)應(yīng)參數(shù)的值,將其賦值給 baseUrl。

最后返回 ServiceMethod 實(shí)例。

下面看一下 ServiceMethod 的主要代碼:

public class ServiceMethod{

    private final String mBaseUrl;

    private final String mHttpMethod;

    ServiceMethod(Builder builder){
        mBaseUrl = builder.baseUrl;
        mHttpMethod = builder.httpMethod;
    }

    public String call(){

        try {
            URL url = new URL(mBaseUrl);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            //set heep method
            connection.setRequestMethod(mHttpMethod);

            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            InputStream inputStream = connection.getInputStream();
            int len ;
            byte[] buffer = new byte[1024];
            while ((len = inputStream.read(buffer))>0){
                bos.write(buffer,0,len);
            }
            String body = new String(bos.toByteArray());
            connection.disconnect();
            return body;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    ...

我們可以看到在 ServiceMethod 的構(gòu)造方法中,拿到了 Builder 中的 baseUrl 和 httpMethod,有了這兩個(gè)標(biāo)量其實(shí)就能做實(shí)際的 http 請(qǐng)求了。

接著看 call 這個(gè)方法,魔術(shù)的最終一環(huán)就是在這里完成的,單看 call 這個(gè)方法中的內(nèi)容很簡(jiǎn)單,就是使用 URL 進(jìn)行一次 http 請(qǐng)求。請(qǐng)求方法為 mHttpMethod,url 為 mBaseUrl,即一開(kāi)始的 "http://www.baidu.com" 。
最后將請(qǐng)求到的數(shù)據(jù)轉(zhuǎn)換成 String 對(duì)象返回,即對(duì)應(yīng)下面的 serviceMethod.call() 。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    ServiceMethod.Builder builder = new ServiceMethod.Builder(Retro.this,method,args);
    ServiceMethod serviceMethod = builder.build();
    return serviceMethod.call();
}

運(yùn)行結(jié)果

自后我們跑一下下面的方法:

    @Test
    public void testAnnotation_Url(){
        SimpleRequest simpleRequest = new Retro().
                create(SimpleRequest.class);
        println(simpleRequest.goBaidu("http://www.baidu.com"));

    }

總結(jié)

到這里一個(gè)簡(jiǎn)單的類(lèi)似 Retrofit 的 http 客戶(hù)端就完成了,它可以完成最簡(jiǎn)單 http 的 get 請(qǐng)求。
當(dāng)然這篇主要講的是 Retro 的一個(gè)大體結(jié)構(gòu),用于了解類(lèi)似 Retrofit 這類(lèi)第三方庫(kù)的實(shí)現(xiàn)思路。后面還會(huì)推出一系列文章,用于講解如何實(shí)現(xiàn) GET 注解自定義 Path,POST 注解及表單處理,返回類(lèi)型轉(zhuǎn)換,錯(cuò)誤處理等功能。

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

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

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