鸿蒙系统网络请求框架—蒹葭

作者: 裴云飞 | 来源:发表于2021-04-30 00:02 被阅读0次

    一、前言

    蒹葭(JianJia)是一款鸿蒙系统上的网络请求框架,其实就是将安卓的Retrofit移植到鸿蒙系统上,我将鸿蒙版的Retrofit命名为蒹葭(JianJia)。蒹葭不仅能实现Retrofit的功能,还会提供一些Retrofit没有的功能。Retrofit不支持动态替换域名,国内的应用一般都是有多个域名的,蒹葭支持动态替换域名。

    二、源码

    源码
    要想读懂源码,需要具备以下技能。

    • 熟悉okhttp的常见用法 ;
    • 熟悉面向接口编程、反射、泛型、注解;
    • 熟悉构造者模式、适配器模式、工厂模式、策略模式、静态代理、动态代理、责任链模式等设计模式。

    三、混淆

    如果项目开启了混淆,请在proguard-rules.pro添加如下的代码。关于混淆,可以查看鸿蒙代码配置混淆

    -renamesourcefileattribute SourceFile
    -keepattributes SourceFile,LineNumberTable
    -dontwarn javax.annotation.**
    -keepattributes Signature, InnerClasses, EnclosingMethod, Exceptions
    # 蒹葭
    -dontwarn poetry.jianjia.**
    -keep class poetry.jianjia.** { *; }
    -keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
    -keepclassmembers,allowshrinking,allowobfuscation interface * {
        @poetry.jianjia.http.* <methods>;
    }
    
    # OkHttp3
    -dontwarn okhttp3.logging.**
    -keep class okhttp3.internal.**{*;}
    -dontwarn okio.**
    
    # gson
    -keep class sun.misc.Unsafe { *; }
    -keep class com.google.gson.stream.** { *; }
    -keepattributes *Annotation*
    -keepclassmembers class * implements java.io.Serializable {
        static final long serialVersionUID;
        private static final java.io.ObjectStreamField[] serialPersistentFields;
        private void writeObject(java.io.ObjectOutputStream);
        private void readObject(java.io.ObjectInputStream);
        java.lang.Object writeReplace();
        java.lang.Object readResolve();
    }
    # 在我的示例代码中,com.poetry.jianjia.bean这个包下面的类实现了Serialized接口,
    # 实现了Serialized接口的类不能被混淆,请把com.poetry.jianjia.bean这个包名替换成你自己的包名
    -keep class com.poetry.jianjia.bean.**{*;}
    

    四、添加依赖

    4、1 在项目根目录下的build.gradle文件中添加mavenCentral()仓库,打开项目根目录下的build.gradle文件,在build.gradle文件的repositories闭包下面添加mavenCentral()

    buildscript {
        repositories {
            // 添加maven中央仓库
            mavenCentral()
            maven {
                url 'https://mirrors.huaweicloud.com/repository/maven/'
            }
            maven {
                url 'https://developer.huawei.com/repo/'
            }
        }
        dependencies {
            classpath 'com.huawei.ohos:hap:2.4.2.5'
            classpath 'com.huawei.ohos:decctest:1.0.0.6'
        }
    }
    
    allprojects {
        repositories {
            // 添加maven中央仓库
            mavenCentral()
            maven {
                url 'https://mirrors.huaweicloud.com/repository/maven/'
            }
            maven {
                url 'https://developer.huawei.com/repo/'
            }
        }
    }
    

    4、2 打开entry目录下的build.gradle文件中,在build.gradle文件中的dependencies闭包下添加下面的依赖

    // 蒹葭的核心代码
    implementation 'io.gitee.zhongte:jianjia:1.0.3'
    // 数据转换器,数据转换器使用gson来帮我们解析json,不需要我们手动解析json
    implementation 'io.gitee.zhongte:converter-gson:1.0.2'
    implementation "com.google.code.gson:gson:2.8.2"
    // 日志拦截器,通过日志拦截器可以看到请求头、请求体、响应头、响应体
    implementation 'com.squareup.okhttp3:logging-interceptor:3.7.0'
    // 如果服务端返回的json有特殊字符,比如中文的双引号,gson在解析的时候会对特殊字符进行转义
    // 这时就需要将转义后的字符串进行反转义,commons-lang可以对特殊字符进行转义和反转义
    implementation 'commons-lang:commons-lang:2.6'
    

    4、3 在配置文件中添加如下的权限

    ohos.permission.INTERNET
    

    五、具体用法,用法跟retrofit一样

    蒹葭提供了一系列的注解,在进行网络请求的时候,就需要用到这些注解。

    5、1 GET注解

    创建接口,在方法里面使用GET注解,GET注解用于标识这是一个GET请求,方法的返回值是Call对象,泛型是ResponseBody,其实泛型也可以是具体的实体对象,这个后面再说。蒹葭如何完成网络请求?使用构造者模式创建jianjia对象,baseUrl就是域名,在创建jianjia对象的时候就必须指定域名。调用create方法来生成接口的实例,调用wan.getBanner().enqueue来执行网络请求,请求成功就会回调onResponse方法,请求失败就会回调onFailure方法。

    public interface Wan {
     
        @GET("banner/json")
        Call<ResponseBody> getBanner();
    }
     
    JianJia jianJia = new JianJia.Builder()
            .baseUrl("https://www.wanandroid.com")
            .build();
     
    Wan wan = jianJia.create(Wan.class);
    wan.getBanner().enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            try {
                String json = response.body().string();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     
        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            LogUtils.info("yunfei", t.getMessage());
        }
    });
    
    5、2 BaseUrl注解

    国内的应用一般都是有多个域名的,BaseUrl注解可以对某个接口设置单独的域名。

    public interface Wan {
     
        @BaseUrl("https://api.apiopen.top")
        @GET("getJoke")
        Call<ResponseBody> getJoke(@QueryMap Map<String, String> param);
         
    }
    
    5、3 Path注解

    Path注解用于在路径中替换指定的参数值,定义下面的方法。可以看到我们定义了一个getArticle方法,方法接收一个page参数,并且我们的@GET注解中使用{page}声明了访问路径,这里你可以把{page}当做占位符,而实际运行中会通过@Path("page")所标注的参数进行替换。比如,调用者在调用getArticle方法时,传递的参数是0,那么GET注解的参数就会变成article/list/0/json

    public interface Wan {
     
        @GET("article/list/{page}/json")
        Call<ResponseBody> getArticle(@Path("page") int page);
     
    }
    
    5、4 Query注解

    Query注解用于给get请求添加请求参数,被Query注解修饰的参数类型可以是数组、集合、字符串等。

    public interface Wan {
     
        @GET("wxarticle/list/405/1/json")
        Call<ResponseBody> search(@Query("k") String k);
     
        @GET("wxarticle/list/405/1/json")
        Call<ResponseBody> search(@Query("k") String... k);
     
        @GET("wxarticle/list/405/1/json")
        Call<ResponseBody> search(@Query("k") List<String> k);
     
    }
    
    5、5 QueryMap注解

    QueryMap注解以map的形式添加查询参数,被QueryMap注解修饰的参数类型必须是Map对象。

    public interface Wan {
     
        @GET("wxarticle/list/405/1/json")
        Call<ResponseBody> search(@QueryMap Map<String, String> param);
         
    }
    
    5、6 SkipCallbackExecutor注解

    在鸿蒙系统上,蒹葭默认会将服务端的响应回调到主线程,如果在方法上使用SkipCallbackExecutor注解,那就不会将服务端的结果回调到主线程。

    public interface Wan {
        @SkipCallbackExecutor
        @GET("wxarticle/list/405/1/json")
        Call<ResponseBody> search(@QueryMap Map<String, String> param);
         
    }
    
    5、7 FormUrlEncoded注解和Field注解

    FormUrlEncoded注解用于发送一个表单请求,使用该注解必须在方法的参数添加Field注解,被Field注解修饰的参数类型可以是数组、集合、字符串等。

    public interface Wan {
        @POST("user/login")
        @FormUrlEncoded
        Call<ResponseBody> login(@Field("username") String username, @Field("password") String password);
         
    }
    
    5、8 FormUrlEncoded注解和FieldMap注解

    有时候表单的参数会比较多,如果使用Field注解,方法的参数就会比较多,此时就可以使用FieldMap注解,FieldMap注解以键值对的形式发送一个表单请求。如果被FieldMap注解修饰的参数不是Map类型,就会抛异常。如果Map的键值对为空,也会抛异常。

    public interface Wan {
        @POST("user/login")
        @FormUrlEncoded
        Call<ResponseBody> login(@FieldMap Map<String, String> map);
         
    }
    
    5、9 Body注解

    服务端会要求端上把json字符串作为请求体发给服务端。此时就可以使用Body注解定义的参数可以直接传入一个实体类,内部会把该实体序列化并将序列化后的结果直接作为请求体发送出去。
    如果被Body注解修饰的参数的类型是RequestBody对象,那调用者可以不添加数据转换器,内部会使用默认的数据转换器。
    如果被Body注解修饰的参数的类型不是RequestBody对象,是一个具体的实体类,那调用者需要自定义一个类,并且继承Converter.Factory

    public interface Wan {
     
        /**
         * 被Body注解修饰的参数的类型是RequestBody对象,那调用者可以不添加数据转换器,内部会使用默认的数据转换器
         *
         * @param body
         * @return
         */
        @POST("user/register")
        Call<ResponseBody> register(@Body RequestBody body);
     
        /**
         * 被Body注解修饰的参数的类型不是RequestBody对象,是一个具体的实体类,那调用者需要自定义一个类,并且继承Converter.Factory
         * 
         * @param user
         * @return
         */
        @POST("user/register")
        Call<ResponseBody> register(@Body User user);
    }
    
    5、10 Url注解

    Url注解用于添加接口的完整地址。在Retrofit里面,如果接口的域名与创建retrofit对象指定的域名不相同,那就会使用Url注解来解决问题。在蒹葭里面同样可以使用Url注解来解决问题,但蒹葭还提供了BaseUrl来解决该问题。

    public interface Wan {
     
        @GET()
        Call<ResponseBody> getArticle(@Url String url);
         
    }
    
    5、11 Headers注解

    Headers注解是作用于方法上的注解,用于添加一个或多个请求头。

    public interface Wan {
     
        @Headers("Cache-Control: max-age=640000")
        @GET("/")
        Call<ResponseBody> getArticle(@Url String url);
         
        @Headers({
         "X-Foo: Bar",
         "X-Ping: Pong"
       })
        @GET("/")
        Call<ResponseBody> getArticle(@Url String url);
         
    }
    
    5、12 Header注解

    Header注解是作用于参数上的注解,用于添加请求头。

    public interface Wan {
     
        @GET()
       Call<ResponseBody> foo(@Header("Accept-Language") String lang);
         
    }
    
    5、13 HeaderMap注解

    HeaderMap注解是作用于参数上的注解,以map的形式添加请求头,map中每一项的键和值都不能为空,否则会抛异常。

    public interface Wan {
     
        @GET("/search")
       Call<ResponseBody> list(@HeaderMap Map<String, String> headers);
         
    }
    
    5、14 Multipart注解和Part注解

    Multipart注解和Part注解用于上传单个文件

    ohos.permission.READ_MEDIA
    ohos.permission.WRITE_MEDIA
    

    在配置文件添加上面两个权限,这个两个权限需要动态申请

    public interface Wan {
    
        /**
         * 上传文件,需要使用Multipart注解和Part注解
         *
         * @param photo 本地文件的路径
         * @return
         */
        @Multipart
        @POST()
        Call<ResponseBody> upload(@Part MultipartBody.Part photo);
    }
    // 文件路径
    File file = new File(getExternalCacheDir(), "icon.png");
    // 创建请求体对象
    RequestBody photoBody = RequestBody.create(MediaType.parse("image/png"), file);
    // 
    MultipartBody.Part photo = MultipartBody.Part.createFormData("photos", "icon.png", photoBody);
    
    JianJia jianJia = new JianJia.Builder()
            .baseUrl("https://www.wanandroid.com")
            .build();
     
    Wan wan = jianJia.create(Wan.class);
    // 上传文件
    wan.upload(photo).enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            // 上传成功
        }
     
        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            LogUtils.info("yunfei", t.getMessage());
        }
    });
    
    5、15 PartMap注解和Multipart注解

    PartMap注解和Multipart注解用于上传多个文件
    在配置文件添加下面两个权限,这个两个权限需要动态申请

    ohos.permission.READ_MEDIA
    ohos.permission.WRITE_MEDIA
    

    PartMap注解修饰的mapmap的第一个泛型必须是String

    public interface Wan {
    
        /**
         * 使用PartMap注解上传多个文件
         *
         * @param params 第一个泛型必须是String
         * @return
         */
        @Multipart
        @POST()
        Call<ResponseBody> upload(@PartMap Map<String, RequestBody> params);
    }
    File photoFile = new File(getExternalCacheDir(), "photo.png");
    RequestBody photoBody = RequestBody.create(MediaType.parse("image/png"), photoFile);
    
    File avatarFile = new File(getExternalCacheDir(), "avatar.png");
    RequestBody avatarBody = RequestBody.create(MediaType.parse("image/png"), avatarFile);
    
    Map<String, RequestBody> photos = new HashMap<>();
    photos.put("photo", photoBody);
    photos.put("avatar", avatarBody);
    
    JianJia jianJia = new JianJia.Builder()
            .baseUrl("https://www.wanandroid.com")
            .build();
     
    Wan wan = jianJia.create(Wan.class);
    // 上传文件
    wan.upload(photos).enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            // 上传成功
        }
     
        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            LogUtils.info("yunfei", t.getMessage());
        }
    });
    
    5、16 下载文件

    下载文件的话,可以直接使用OKhttp,至于如何使用OKhttp下载文件,大家自行在网上搜索吧。当然了,也可以使用蒹葭下载文件。

    public interface Wan {
    
        /**
         * 蒹葭的onResponse方法默认在主线程中执行,SkipCallbackExecutor注解会让onResponse方法在子线程执行。下载文件和保存文件都是耗时操作,耗时操作在子线程中执行
         *
         * @return
         */
        @SkipCallbackExecutor
        @GET("download")
        Call<ResponseBody> download();
    }
    JianJia jianJia = new JianJia.Builder()
            .baseUrl("https://www.wanandroid.com")
            .build();
     
    Wan wan = jianJia.create(Wan.class);
    wan.download().enqueue(new Callback<ResponseBody>() {
                @Override
                public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                    // onResponse方法在子线程中执行
                    // 得到文件输入流后,就可以写文件了
                    InputStream inputStream = response.body().byteStream();
    
                }
    
                @Override
                public void onFailure(Call<ResponseBody> call, Throwable throwable) {
    
                }
            });
    
    5、17 添加数据转换器

    之前我们在接口里面定义方法的时候,方法的返回值时Call对象,泛型是ResponseBody。在这种情况下,服务端返回给端上的数据就会在ResponseBody里面,端上需要手动解析json,将json解析成一个实体类。
    其实,我们没必要手动解析json,可以让gson帮我们解析json。蒹葭支持添加数据转换器,在创建对象的时候添加数据转换器,也就是把gson添加进来。在onResponse方法里面就可以直接得到实体类对象了,gson帮我们把json解析成了一个实体对象。
    首先在build.gradle文件添加数据转换器的依赖。

    // 数据转换器,数据转换器使用gson来帮我们解析json,不需要我们手动解析json
    implementation 'io.gitee.zhongte:converter-gson:1.0.0'
    implementation "com.google.code.gson:gson:2.8.2"
    

    在代码中使用数据转换器

    public interface Wan {
     
        @GET("banner/json")
        Call<Banner> getBanner();
    }
     
    JianJia jianJia = new JianJia.Builder()
            .baseUrl("https://www.wanandroid.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
     
    Wan wan = jianJia.create(Wan.class);
    wan.getBanner().enqueue(new Callback<Banner>() {
        @Override
        public void onResponse(Call<Banner> call, Response<Banner> response) {
            try {
                if (response.isSuccessful()) {
                    // json已经被解析成banner对象了
                    Banner banner = response.body();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
     
        @Override
        public void onFailure(Call<Banner> call, Throwable t) {
            LogUtils.info("yunfei", t.getMessage());
        }
    });
    
    5、18 解决转义字符问题

    如果服务端返回的json有特殊字符,比如中文的双引号。gson在解析的时候会对特殊字符进行转义,这时就需要将转义后的字符串进行反转义。如下图所示


    转义字符.png

    如何将转义后的字符串进行反转义?commons-lang这个库可以将转义后的字符串进行反转义
    build.gradle文件添加下面的依赖

    // commons-lang可以对特殊字符进行转义和反转义
    implementation 'commons-lang:commons-lang:2.6'
    

    调用StringEscapeUtilsunescapeHtml方法,如果字符串中没有转义字符,unescapeHtml方法会直接返回原字符串,否则会对字符串进行反转义。

    // json里面有一些特殊符号,特殊符号会被gson转义,
    // StringEscapeUtils可以对转义的字符串进行反转义
    String title = StringEscapeUtils.unescapeHtml("");
    

    反转义之后,特殊字符正常显示


    特殊字符正常显示.png

    六、总结

    本文介绍了蒹葭的用法,蒹葭的原理跟retrofit是一样的,有兴趣的同学可以去看下源码。

    相关文章

      网友评论

        本文标题:鸿蒙系统网络请求框架—蒹葭

        本文链接:https://www.haomeiwen.com/subject/nhygrltx.html