動手寫個Retrofit簡易版

黃燕斌發表於2017-11-21

Retrofit簡易版本


為啥要寫這個?

之前一直使用OKHttp,之前修改過鴻洋的OKhttpUtils增加了快取功能。但對Retrofit並沒有使用過,前幾天按網上例子用了,感覺確實簡約多了。總覺得Retrofit就是個註解版OKHttp,應該寫個簡易版本很容易,就是個編譯時註解唄。於是沒看原始碼寫個簡單版本。現在已經可以集合Rxjava,Gson。我試圖去想Retrofit作者是咋寫的。肯定有人說又造重複的輪子,放心,寫完我也不用,因為真的只是demo,只是為了增加自己程式設計的能力。。github地址:github.com/huangyanbin…

咋開始呢?

我想著邊寫邊改,於是我首先建了個module寫了Get註解類,用於等下解析用。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Get {
    String value();
}複製程式碼

真是很簡單吧,然後就是建了個類HttpProcessor繼承AbstractProcessor類,結果發現死活導不了AbstractProcessor類,坑爹啊,只好百度了,原來module必須用java library。只有刪了重新建。

接著就是寫HttpProcessor了,肯定有人問AbstractProcessor類幹嘛的,建議百度。因為我也是百度的,哈哈。查完就知道主要就是 public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv)這個方法來幹活的。就是編譯的時候呼叫該方法,我們可以通過這個方法來自動生成程式碼。

問題又來了,咋生成程式碼。squareup 這個javapoet框架可以優雅生成程式碼。百度查下就應該會用了,比較簡單。

 compile 'com.squareup:javapoet:1.9.0'複製程式碼

Build project還是不會呼叫HttpProcessor類呢,原來還需要我們告訴它在哪,這個時候googleauto-service上場了,不需要寫啥Xml什麼的,只需要

compile 'com.google.auto.service:auto-service:1.0-rc3'複製程式碼

HttpProcessor類上增加註解

@AutoService(Processor.class)複製程式碼

還有咋debug編譯Build,方便我們看我們到底生成什麼鬼東西。在gradle.properties新增兩行程式碼

org.gradle.daemon=true
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005複製程式碼

新增一個Remote除錯,然後在終端輸入gradlew clean assembleDebug.然後可以快樂的debug了,如果你還是不會,去網上看下資料就會了。

第一步

使用Retrofit我們一般都是新建介面,然後寫個抽象方法,類似下面的。

 @Get("{query}/pm10.json")
    Call<List<PM25>> getWeather(@Path("query") String query, @Query("city")String city,@Query("token")String token);複製程式碼

或者這樣

 @Get("{query}/pm10.json")
    Observable<List<PM25>> getWeather(@Path("query") String query, @Query("city") String city, @Query("token") String token);複製程式碼

我第一反應,應該用HttpProcessor攔截到GetPost註解,然後再生成一個類,實現新建的Http請求介面,萬事開頭難,我們先在獲取GetPost註解:

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        //獲取到所有的Get註解
        Set<? extends Element> getSet = roundEnv.getElementsAnnotatedWith(Get.class);
        //獲取到所有Post註解
        Set<? extends Element> postSet = roundEnv.getElementsAnnotatedWith(Post.class);
        //放入新的Set集合裡面
        HashSet<Element> allSet = new HashSet<>();
        allSet.addAll(getSet);
        allSet.addAll(postSet);
        ...複製程式碼

獲取到了GetPost註解,然後就是獲取註解類的包名了:

    //迭代
    for (Element e : allSet) {
            //判斷註解在方法上面
            if (e.getKind() != ElementKind.METHOD) {
                onError("Builder annotation can only be applied to method", e);
                return false;
            }
            //獲取包名
            String packageName = elementUtils.getPackageOf(e).getQualifiedName().toString();
            ...複製程式碼

然後我們要依次解析我們的方法。我們先建一個類AnnotatedClass用於放註解介面相關資訊以及生成類程式碼,然後在建AnnotatedMethod類放方法相關資訊以及生成方法程式碼。感覺很複雜?一步步來:

首先我們獲取包名,每個方法對應一個AnnotatedMethod類:

//將element轉成方法Element
   ExecutableElement element = (ExecutableElement) e;
   //建立一個方法生成類
   AnnotatedMethod annotatedMethod = new AnnotatedMethod(element);
   //獲取類名(包含包名的),以便生成AnnotatedClass類
   String qualifiedClassName = annotatedMethod.getQualifiedClassName();複製程式碼

將類名和AnnotatedClass做為key-value放在map,中,保證不會重複生成類程式碼:

    AnnotatedClass annotatedClass;
    //判斷是否已經有這個AnnotatedClass類了
    if(classMap.containsKey(qualifiedClassName)){
        annotatedClass = classMap.get(qualifiedClassName);
    }else{
         //生成AnnotatedClass類
         annotatedClass = new AnnotatedClass(packageName,annotatedMethod.getSimpleClassName()
        ,annotatedMethod.getClassElement());
         classMap.put(qualifiedClassName,annotatedClass);
    }
    //將方法加入annotatedClass類
     annotatedClass.addMethod(annotatedMethod);
     onNote("retrofit build ---"+element.getSimpleName()+"--- method", e);複製程式碼

迭代出來呼叫生成AnnotatedClass程式碼:

//迭代呼叫annotatedClass方法生成類程式碼
 for (Map.Entry<String, AnnotatedClass> annotatedClassEntry : classMap.entrySet()) {
            AnnotatedClass annotatedClass = annotatedClassEntry.getValue();
            annotatedClass.generateCode(elementUtils,filer);
        }複製程式碼

如何生成程式碼(核心)

TypeSpec就是用於生成類資訊的,採用Build方式來完成。

 public void generateCode(Elements elementUtils, Filer filer) {
        //獲取介面名
        TypeName classType = TypeName.get(classElement.asType());

        TypeSpec.Builder typeBuilder =
        //類名 介面名+imp imp隨便寫的。
        TypeSpec.classBuilder(className+"Imp")
        //類訪問許可權
                    .addModifiers(Modifier.PUBLIC)
        //介面 實現我們包含Get註解的介面            
                    .addSuperinterface(classType)
                    //繼承APIService類 ,這個類主要是輔助完成很多工作,等下會介紹
                    .superclass(APIService.class);
        //迭代生成方法程式碼            
        for (int i = 0;i < methods.size();i++) {
            AnnotatedMethod m = methods.get(i);
            MethodSpec methodSpec = m.generateMethodSpec();
            if(methodSpec !=null) {
                typeBuilder.addMethod(methodSpec);
            }
        }
        //建立一個java File
        JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();
        try {
            //寫java檔案
            javaFile.writeTo(filer);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }複製程式碼

方法生成程式碼複雜很多,每行都註釋。MethodSpec就是方法生成的類,也是通過build方式在構造的。思路就是拼接一個方法,在裡面獲取出用於請求Call,通過IConverterFactory轉換成我們需要返回的型別,通過ICallAdapterFactory將請求回掉型別轉換成我們需要的型別,我沒有將所有程式碼都通過javapoet生成,而是通過繼承APIService類,因為javapoet寫起來確實比寫程式碼累多了。哈哈!

public MethodSpec generateMethodSpec() {
        ExecutableElement methodElement = getMethodElement();
        //獲取一個BaseAnnotatedParse 用於Get和Post不同解析
        BaseAnnotatedParse parse = getParser();
        if (parse == null) {
            return null;
        }
        //獲取註解的url
        String url = parse.getUrl(methodElement);
        //獲取方法返回型別
        TypeName returnType = TypeName.get(methodElement.getReturnType());
        //獲取所有方法形參
        List<? extends VariableElement> params = methodElement.getParameters();
        //獲取方法名
        String methodName = methodElement.getSimpleName().toString();
        //構造一個方法
        MethodSpec.Builder methodBuilder = MethodSpec
                .methodBuilder(methodName)
                .addModifiers(Modifier.PUBLIC)
                .returns(returnType);
        //通過拼接可以得到對應method類,用於請求後由於泛型擦除導致無法得到Type      
        StringBuffer methodFieldStr = new StringBuffer(" $T method = this.getClass().getMethod(\"" + methodName + "\"");
        //迭代引數
        for (int i = 0; i < params.size(); i++) {
            //獲取引數Element
            VariableElement paramElement = params.get(i);
            //引數名稱
            String paramName = paramElement.getSimpleName().toString();
            //引數型別 包含泛型
            TypeName paramsTypeName = TypeName.get(paramElement.asType());
            //新增引數
            methodBuilder.addParameter(paramsTypeName, paramName);
            //去除泛型
            String paramsTypeStr =  paramsTypeName.toString();
            if(paramsTypeStr.contains("<")){
                paramsTypeStr = paramsTypeStr.substring(0,paramsTypeStr.indexOf("<"));
            }
            methodFieldStr.append("," + paramsTypeStr + ".class");
            //判斷形參是否包含Path註解,放入pathMap中
            Path path = paramElement.getAnnotation(Path.class);
            if (path != null) {
                String value = path.value();
                pathMap.put(value, paramName);
            }
             //判斷形參是否包含Query註解,放入queryMap中
            Query query = paramElement.getAnnotation(Query.class);
            if (query != null) {
                String value = query.value();
                queryMap.put(value, paramName);
            }
        }
        methodFieldStr.append(")");
        methodBuilder.addStatement("String url = $S", url);
        //替換所有的Path
        for (Map.Entry<String, String> entry : pathMap.entrySet()) {
            methodBuilder.addStatement("url =  url.replaceAll(\"\\\\{$N\\\\}\",$N)"
                    , entry.getKey(), entry.getValue());
        }

        String returnTypeName = returnType.toString();
          //獲取返回型別的泛型
        String generic = returnTypeName.substring(returnTypeName.indexOf("<"));
        //解析head 和 query
        parse.parse(methodElement, methodBuilder, queryMap);
        //建立Call
        methodBuilder.addStatement("$T$N call = new $T$N(createCall(request))", Call.class, generic, Call.class, generic);
        //設定CallAdapterFactory
        methodBuilder.addStatement("call.setCallAdapterFactory(getCallAdapterFactory())");
        methodBuilder.beginControlFlow("try");
        methodBuilder.addStatement(methodFieldStr.toString(), Method.class);
        //設定返回型別的泛型
        methodBuilder.addStatement("setCallGenericReturnType(method,call)");
        methodBuilder.endControlFlow();
        methodBuilder.beginControlFlow("catch (Exception e)");
        methodBuilder.addStatement("e.printStackTrace()");
        methodBuilder.endControlFlow();
        //最後通過ConverterFactory()轉換成返回型別
        methodBuilder.addStatement("$T convertCall = ($T)(getConverterFactory().converter(call))", returnType, returnType);
        methodBuilder.addStatement("return convertCall");
        return methodBuilder.build();

    }複製程式碼

我們在APIService裡面獲取返回型別的泛型,最後傳給Call,Callenqueue(Callback calback)傳給callback,這樣callback就知道它該解析成什麼型別了。。。

 protected void setCallGenericReturnType(Method method,Call<?> call){
        Type type = method.getGenericReturnType();
        if (type instanceof ParameterizedType) {
            Type genericType = ((ParameterizedType) type).getActualTypeArguments()[0];
            call.setGenericType(genericType);
        }
    }複製程式碼

設定head

 public void setHead(ExecutableElement methodElement, MethodSpec.Builder methodBuilder) {
        if(methodElement.getAnnotation(Head.class) != null){
            Head header =  methodElement.getAnnotation(Head.class);
            String[] headerStr = header.value();
            methodBuilder.addStatement("$T.Builder headBuilder = new $T.Builder()", Headers.class,Headers.class);
            for (String headStr : headerStr) {
                methodBuilder.addStatement("headBuilder.add(\"$N\")", headStr);
            }
            methodBuilder.addStatement("requestBuilder.headers(headBuilder.build())");
        }
    }複製程式碼

還有就是如何ConverterCallAdapter,兩個其實邏輯是一樣的。只不過CallAdapterFatory需要方法返回型別的泛型,上面已經得到了。啦啦啦

//轉換介面
public interface IConverterFactory<T> {

    <R> T converter(Call<R> call);

}

//CallAdapter 介面
public interface ICallAdapterFactory {

    <T> T converter(Response response, Type returnType);

}複製程式碼

RxjavaOkttp結合在一起:

 @Override
    public <R> Observable<R> converter(final Call<R> call) {
        return Observable.create(new ObservableOnSubscribe<R>() {
            @Override
            public void subscribe(final ObservableEmitter<R> e) throws Exception {
                call.enqueue(new Callback<R>() {
                    @Override
                    public void onResponse(okhttp3.Call call, R response) {
                        e.onNext(response);
                        e.onComplete();
                    }
                    @Override
                    public void onFailure(okhttp3.Call call, IOException e1) {
                        e.onError(e1);
                        e.onComplete();
                    }
                });
            }
        });

    }複製程式碼

最後通過Retrofit create來獲取實現類的物件,雖然Class是一個介面,但是實際上獲取的是clazz.getName()+Imp類,APIService這個類主要是用於設定Retrofit的配置,比如baseUrl等.

 public  <T> T create(Class<T> clazz) {
        String impClazz = clazz.getName()+"Imp"
        try {
            Class childClazz = Class.forName(impClazz);
            T t = (T) childClazz.newInstance();
            APIService apiService = (APIService)t;
            apiService.setOkHttpClient(builder.client);
            apiService.setConverterFactory(builder.converterFactory);
            apiService.setBaseUrl(builder.baseUrl);
            apiService.setCallAdapterFactory(builder.callAdapterFactory);
            return t;
        }catch (ClassNotFoundException e){
            throw new RetrofitException("ClassNotFoundException "+impClazz);
        } catch (IllegalAccessException e) {
            throw new RetrofitException("IllegalAccessException "+impClazz);
        } catch (InstantiationException e) {
            throw new RetrofitException("InstantiationException "+impClazz);
        }
    }複製程式碼

未完待續

總結

用了兩天時間寫這個思路實現,感覺這個最難的就是泛型,因為泛型會編譯之後會被擦除,最後投機取巧了,用方法獲取泛型,然後將泛型Type傳給Callback。完成了Get, Post,Path Query,QuertMap,Head註解,其他PutDelete等請求就不寫了。取一反三而已,還可以自定義IConverterFactoryICallAdapterFactory.當然真正的Retrofit比我寫的複雜多了。後續有時間把多種快取http cache功能加上。
github地址:github.com/huangyanbin…

相關文章