Retrofit 2.0的封装与异常处理

Retrofit 2.0的基本使用,网上教程很多,Retrofit 官方教程

  • 定义接口
    interface ApiService {
        @POST("market/getMarket")
        fun getMarket(): Call<HttpResponse<Market>>
    }
  • 初始化Retrofit
    const val URL_BASE = "http://192.168.1.1:9200/myproject/api/"
    
    class RetrofitClient private constructor() {
        private var apiService: ApiService
        init {
            val mRetrofit = Retrofit.Builder()
                    .baseUrl(URL_BASE)
                    .client(getOkHttpClient())
                     /** 添加自定义的Gson解析 */
                    .addConverterFactory(CustomGsonConverterFactory.create())
                    .build()
            apiService = mRetrofit.create(ApiService::class.java)
        }
    
        companion object {
            fun getInstance(): RetrofitClient {
                return Inner.instance
            }
        }
    
        private object Inner {
            val instance = RetrofitClient()
        }
    
        private fun getOkHttpClient(): OkHttpClient {
            val builder = OkHttpClient.Builder()
            builder.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
            /** 为请求添加公共参数 */
            builder.addInterceptor(BasicParamsInterceptor.Builder()
                    .addParam("channel", "googlePlay").addParam("apiVer", "15").build())
            builder.addInterceptor(BasicParamsInterceptor.Builder().build())
            builder.connectTimeout(20, TimeUnit.SECONDS)
            builder.writeTimeout(20, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS)
            builder.retryOnConnectionFailure(true)
            return builder.build()
        }
    
        fun getAPI(): ApiService {
            return Companion.getInstance().apiService
        }
    }
  • 调用接口
    RetrofitClient.getInstance().getAPI().getMarket().enqueue(object : Callback<HttpResponse<Market>> {
        override fun onResponse(call:Call<HttpResponse<Market>>,response:Response<HttpResponse<Market>>{
            //TODO on success response
        }
        override fun onFailure(call: Call<HttpResponse<Market>>, throwable: Throwable) {
            //TODO on failures response         
        }
    })
  1. 为每个http请求添加公共参数,如channel、apiVersion、imei..

    利用OkHttp提供的拦截器对将要发出的请求进行拦截;
    BasicParamsInterceptor实现了okhttp3.Interceptor接口, 我们只需在初始化OkHttpClient时addInterceptor添加拦截器,并将公共参数添加进BasicParamsInterceptor;
    Github地址 https://github.com/jkyeo/okhttp-basicparamsinterceptor

  2. 数据解析和统一异常处理

    一般后台给的响应数据都有一个统一格式,包含响应码、提示和具体数据,如

  {
    "code":0,
    "msg":"success",
    "data":{
        "id":1212,
        "name":"MyName"
    }
  } 

如果服务端数据响应成功,这里的code为0,msg为success;
如果服务器端出现错误或者用户操作失败,code为对应错误码,msg为对应提示信息,如"服务器忙,请稍后再试"、"用户权限不足"等;
注意,这种code不为0的情况属于服务端主动提示的异常,但整个http请求与响应过程是没有问题的,所以这种情况下是不会走Callback的onFailure回调的。

  • 定义响应model
    open class HttpStatus {
        var msg: String? = null
        var code: Int = -1
        fun isSuccess(): Boolean {
            return code == 0
        }
    }
    class HttpResponse<T> : HttpStatus() {
        var data: T? = null
    }

retrofit已经为我们封装好了converter-gson,只需在gradle中添加依赖implementation 'com.squareup.retrofit2:converter-gson:2.4.0',然后Retrofit初始化时addConverterFactory添加GsonConverterFactory 响应即可由Gson进行处理。

  • 定义一个Exception代表服务端异常
class ApiException(var code: Int, override var message: String?) : RuntimeException(message) {
    fun code(): Int {
        return code
    }
    fun message(): String? {
        return message
    }
}
  • 参照GsonConverterFactory,自定义一个CustomGsonConverterFactory,添加统一异常处理功能。
    class CustomGsonConverterFactory(val gson: Gson) : Converter.Factory() {
       companion object {
            fun create(): CustomGsonConverterFactory {
                return create(Gson())
            }
            private fun create(gson: Gson?): CustomGsonConverterFactory {
                if (gson == null) throw NullPointerException("gson == null")
                return CustomGsonConverterFactory(gson)
            }
        }

        override fun responseBodyConverter(type: Type?, annotations: Array<out Annotation>?, retrofit:Retrofit?):Converter<ResponseBody, *>? {
            return CustomGsonResponseBodyConverter(gson, gson.getAdapter(TypeToken.get(type)))
        }

        override fun requestBodyConverter(type: Type?, parameterAnnotations: Array<out Annotation>?,methodAnnotations: Array<out Annotation>?, retrofit: Retrofit?): Converter<*, RequestBody>? {
            return CustomGsonRequestBodyConverter(gson, gson.getAdapter(TypeToken.get(type)))
        }
  }
    class CustomGsonRequestBodyConverter<T>(private val gson: Gson, private val adapter: TypeAdapter<T>) : Converter<T, RequestBody> {
        private val MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8")
        private val UTF_8 = Charset.forName("UTF-8")
    
        override fun convert(value: T): RequestBody {
            val buffer = Buffer()
            val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
            val jsonWriter = gson.newJsonWriter(writer)
            adapter.write(jsonWriter, value)
            jsonWriter.close()
            return RequestBody.create(MEDIA_TYPE, buffer.readByteString())
        }
    }

上面这两个类基本没做什么改动,直接照搬GsonConverterFactoryGsonRequestBodyConverter,我们主要改动的是GsonResponseBodyConverter , 具体说明见注释

class CustomGsonResponseBodyConverter<T>(private val gson: Gson, private val adapter: TypeAdapter<T>) : Converter<ResponseBody, T> {
    override fun convert(value: ResponseBody): T {
        val response = value.string()
        val httpStatus = gson.fromJson(response, HttpStatus::class.java)
        /** 先将code与msg解析出来,code非0的情况下直接抛ApiException异常,这样我们就将这种异常交给onFailure()处理了**/
        if (!httpStatus.isSuccess()) {
            value.close()
            throw ApiException(httpStatus.code, httpStatus.msg)
        }
        val contentType = value.contentType()
        val charset = contentType?.charset(UTF_8) ?: UTF_8
        val inputStream = ByteArrayInputStream(response.toByteArray())
        val reader = InputStreamReader(inputStream, charset!!)
        val jsonReader = gson.newJsonReader(reader)

        value.use { _ ->
            val result = adapter.read(jsonReader)
            if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
                throw JsonIOException("JSON document was not fully consumed.")
            }
            return result
        }
    }
}

上面工作完成后,再写个解析异常的帮助类,也可以选择进一步向上封装。

object HttpResponseProcess {
    private const val NO_CONNECTION = 3001
    private const val CONNECTION_TIMEOUT = 3002
    private const val CONNECTION_REFUSED = 3003
    private const val STATUS_EXCEPTION = 3004
    private const val JSON_EXCEPTION = 3005
    private const val PERMISSION_DENIED = 3006
    private const val HTTPS_HANDSHAKE_FAILED = 3007
    private const val API_ERROR = 3008
    private const val UNKNOWN = 3009

    fun <R> responseProcess(response: Response<HttpResponse<R>>): HttpResponse<R> {
        var httpResponse: HttpResponse<R> = HttpResponse()
        if (response.isSuccessful) {
            if (response.body() == null || response.code() == 204) {
                httpResponse.code = STATUS_EXCEPTION
                httpResponse.msg = String.format(MyApplication.getAppContext().getString(R.string.netError_httpResponseError), response.code())
            } else {
                httpResponse = response.body() as HttpResponse<R>
            }
        } else {
            httpResponse.code = STATUS_EXCEPTION
            httpResponse.msg = String.format(MyApplication.getAppContext().getString(R.string.netError_httpResponseError), response.code())
        }
        return httpResponse
    }

    fun <R> responseProcess(throwable: Throwable): HttpResponse<R> {
        val context = MyApplication.getAppContext()
        val httpResponse: HttpResponse<R> = HttpResponse()
        when (throwable) {
            is HttpException -> {
                httpResponse.code = STATUS_EXCEPTION
                httpResponse.msg = String.format(context.getString(R.string.netError_httpResponseError), throwable.code())
            }
            is SocketTimeoutException -> {
                httpResponse.code = CONNECTION_TIMEOUT
                httpResponse.msg = context.getString(R.string.netError_connectionTimeout)
            }
            is SSLHandshakeException -> {
                httpResponse.code = HTTPS_HANDSHAKE_FAILED
                httpResponse.msg = String.format(context.getString(R.string.netError_httpsHandshakeFailed))
            }
            is JSONException, is JsonParseException, is ParseException -> {
                httpResponse.code = JSON_EXCEPTION
                httpResponse.msg = String.format(context.getString(R.string.netError_parseJsonException), throwable.message)
            }
            is UnknownHostException, is ConnectException, is SocketException -> {
                val causeMessage = throwable.message
                if (causeMessage != null && causeMessage.contains("Permission denied")) {
                    httpResponse.code = PERMISSION_DENIED
                    httpResponse.msg = String.format(context.getString(R.string.netError_connectionDenied), context.getString(R.string.app_name))
                } else if (causeMessage != null && causeMessage.contains("Connection refused")) {
                    httpResponse.code = CONNECTION_REFUSED
                    httpResponse.msg = context.getString(R.string.netError_connectionRefused)
                } else {
                    httpResponse.code = NO_CONNECTION
                    httpResponse.msg = context.getString(R.string.netError_noConnection)
                }
            }
            is ApiException -> {
                httpResponse.code = API_ERROR
                httpResponse.msg = String.format(context.getString(R.string.netError_apiException), throwable.message())
            }
            else -> {
                httpResponse.code = UNKNOWN
                httpResponse.msg = String.format(context.getString(R.string.netError_unknown), throwable.message)
            }
        }
        return httpResponse
    }
}

HttpResponseProcess中定义了多种异常类型和对应的代码,比如无连接3001,连接超时3002;两个responseProcess方法分别用于处理retrofit2.Callback的onResponse与onFailure的异常。

 RetrofitClient.getInstance().getAPI().getMarket().enqueue(object : Callback<HttpResponse<Market>> {
            override fun onResponse(call: Call<HttpResponse<Market>>, response: Response<HttpResponse<Market>>) {
                val httpResponse = HttpResponseProcess.responseProcess(response)
            }
            override fun onFailure(call: Call<HttpResponse<Market>>, throwable: Throwable) {
                val errorResponse = HttpResponseProcess.responseProcess<Market>(throwable)
            }
        })
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342