原因:项目已经上线了,但是突然有一天,客户说他们的仓库有个4.3版本的pda需要装这个app,然后发现装上去闪退的。经过我的排查,原来是retrofit不支持5.0的,当时做项目的时候,也没想这么多,谁能想到还有5.0以下的版本的pda呢,然后让客户换pda不现实,所以只能用HttpURLConnection来实现咯,然后为了少改动代码,决定用HttpURLConnection来近似实现一下retrofit,能少改动一点是一点。废话不多说,开始上代码吧。
HttpCreator.kt这个文件的功能:
1.自定义注解,入HTTPGET,HTTPPOST等,之所以和retrofit定义的GET不一样是因为自己的项目是5.0以上用retrofit,然后5.0以下用自己的代码,所以注解不一样。虽然可以直接抛弃retrofit,但是由于项目是老早上线了,还是尽量少改,所以采用的是5.0以下的版本才走这个逻辑。
2.HttpCreator的单例类,实现外部的便捷调用,内部依靠动态代理实现,对定义的接口上的参数注解和方法上的注解进行校验,最后返回一个自己定义的MyCall对象(继承Call接口)。
3.sendHttpByHttpURLConnection 方法实现正真的发送网络请求,并且对数据进行处理。
(功能都在这个地方实现,所以如果要加功能的话,其实就在这里改的,我只是写了一些常见的,我没用到的就没写上)
package com.rengda.testdemon
import android.util.Log
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.annotation.Documented
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.net.HttpURLConnection
import java.net.SocketTimeoutException
import java.net.URL
import java.net.UnknownServiceException
import kotlin.concurrent.thread
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPMultipart
//自定义注解get注解
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPGET(
val value: String = "")
//自定义注解post注解
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPPOST(
val value: String = "")
//自定义注解put注解
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPPUT(
val value: String = "")
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPDELETE(
val value: String = "")
//自定义注解header注解
@Documented
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPHeaders(val value: kotlin.Array<kotlin.String> )
@Documented
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPQuery(
/** The query parameter name. */
val value: String,
/**
* Specifies whether the parameter [name][.value] and value are already URL encoded.
*/
val encoded: Boolean = false)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class HTTPPath(val value: String,
/**
* Specifies whether the argument value to the annotated method parameter is already URL encoded.
*/
val encoded: Boolean = false)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(AnnotationTarget.VALUE_PARAMETER)
annotation class HTTPHeader(val value: String)
@Documented
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPBody
@Documented
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
annotation class HTTPMultiBody
//动态代理接口 得到接口的注释 并返回一个call类型
object HttpCreator {
//创建接口的的代理
fun <T> createProxy(service: Class<T>): T {
return Proxy.newProxyInstance(
service.classLoader, arrayOf<Class<*>>(service),
object : InvocationHandler {
@Throws(Throwable::class)
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
return createHttpByHttpURLConnection(method,args)
}
}) as T
}
fun <T> create(serviceClass:Class<T>):T= createProxy(serviceClass)
//外部用的
inline fun <reified T>creat():T= create(T::class.java)
//进行解析 最后返回自定义的call
fun createHttpByHttpURLConnection(method: Method, args: Array<Any>?):Call<Any?>{
val queryMap= linkedMapOf<String,Any>()
val pathMap= linkedMapOf<String,Any>()
val headersMap= linkedMapOf<String,String>()
var body:RequestBody?=null
var url=""
var partUrl=""
var httpMethod=""
try {
//获取接口方法上的参数
val methodAnnotations= method.annotations
for (item in methodAnnotations){//遍历所有的注解
if (item.annotationClass.simpleName=="HTTPGET"){
partUrl= method.getAnnotation(HTTPGET::class.java).value
httpMethod="GET"
}else if( item.annotationClass.simpleName=="HTTPPOST"){
partUrl= method.getAnnotation(HTTPPOST::class.java).value
httpMethod="POST"
}else if (item.annotationClass.simpleName=="HTTPPUT"){
partUrl= method.getAnnotation(HTTPPUT::class.java).value
httpMethod="PUT"
}else if (item.annotationClass.simpleName=="HTTPDELETE"){
partUrl= method.getAnnotation(HTTPDELETE::class.java).value
httpMethod="DELETE"
}else if(item.annotationClass.simpleName=="HTTPHeaders"){
val headersList=method.getAnnotation(HTTPHeaders::class.java).value
Log.d("HttpCreator", headersList.toString())
for (item in headersList){
val font=item.indexOf(":")
headersMap[item.substring(0,font)]=item.substring(font+1,item.length)
}
}
}
//获取参数上的注解
val parameterAnnotations=method.parameterAnnotations
var i=0
while(i<args?.size?:0){
//根据参数进行遍历
for (item in parameterAnnotations[i]){
if (item.annotationClass.simpleName.toString()=="HTTPQuery"){//是查询的参数
val item=item as HTTPQuery
if (args!![i]!=null&&args!![i]!=""){//不为空我才往上拼
queryMap[item.value]=args!![i]
}
}else if (item.annotationClass.simpleName.toString()=="HTTPPath") {//是url上的参数
val item=item as HTTPPath
pathMap[item.value]=args!![i]
}else if (item.annotationClass.simpleName.toString()=="HTTPBody"){//是请求体
body=args!![i] as RequestBody
}
else if (item.annotationClass.simpleName.toString()=="HTTPHeader"){
val item=item as HTTPHeader
headersMap[item.value]=args!![i].toString()
}else if (item.annotationClass.simpleName.toString()=="HTTPMultiBody"){//是mutil的
body=args!![i] as MutilBody
}
}
i++
}
if (Url.BASE_URL.substring(Url.BASE_URL.length-1,Url.BASE_URL.length)=="/"){//基础的以/结尾了
if (partUrl.indexOf("/")==0){//分路径又以/开头 重复了
partUrl=partUrl.substring(1,partUrl.length)
}
}
url=Url.BASE_URL+partUrl
//替换掉路径里的{}
for (item in pathMap){
while (url.indexOf("{")!=-1){//说明有{}
val front= url.indexOf("{")
val back=url.indexOf("}")
if(back==url.length-1){
url=url.substring(0,front)+item.value
}else{
url=url.substring(0,front)+item.value+url.substring(back+1)
}
}
}
//显示的拼接到路径上
if (queryMap.size>0){
url= "$url?"
}
for (item in queryMap){
url= "${url}${item.key}=${item.value}&"
}
if (queryMap.size>0){
url=url.substring(0,url.length-1)
}
}catch (e:java.lang.Exception){
e.printStackTrace()
}
return MyCall(url,headersMap,httpMethod,body)
}
class MyCall<T>(val url:String,val headerMap:LinkedHashMap<String,String>,val httpMethod: String,val body:RequestBody?) :Call<T>{
override fun enqueue(callback: Callback<T>) {
sendHttpByHttpURLConnection(url,httpMethod,headerMap,body,callback,this)
}
}
//原生的
fun <T> sendHttpByHttpURLConnection(url: String,requestMethod: String,headerMap:LinkedHashMap<String,String>, body:RequestBody?,callback:Callback<T>,call:Call<T>) {
thread {
var connection: HttpURLConnection? = null
try {
val url = URL(url)
connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 10000
connection.readTimeout = 10000
connection.requestMethod = requestMethod
//参数里面的放头信息
for (item in headerMap){
Log.d("HttpCreator", "自己的头信息: ${item.key}:${item.value}")
connection.setRequestProperty(item.key,item.value)
}
//如果是有body方法
if (body!=null){
connection.setRequestProperty("Content-Type",body.contentType)
connection.setDoOutput(true);
val outputStream = connection.outputStream
outputStream.write(body.bytes);
outputStream.flush();
outputStream.close();
}
Log.d("HttpCreator", "url: "+url)
Log.d("HttpCreator", "头信息: "+connection.headerFields)
if (connection.responseCode == -1) {//连接发生错误
callback.onFailure(call,Throwable("无状态码", null))
} else {
if (connection.responseCode >= 200 && connection.responseCode < 300) {//通话成功 [200..300)
Log.d("HttpCreator", "responseCode "+connection.responseCode )
// json 解析成功的就去成功,解析失败的就去失败
if (connection.responseCode == 200) {//真的成功
val rawData = StringBuilder()
val input = connection.inputStream
val reader = BufferedReader(InputStreamReader(input))
reader.use {
reader.forEachLine {
rawData.append(it)
}
}
try {
//解析
val data = Gson().fromJson(rawData.toString(), callback.myResultClassType(call)) as T
callback.onResponse(call,Response(connection.responseCode, data, rawData.toString(), null))
} catch (e: JsonSyntaxException) {//不是有效的json
callback.onFailure(call,Throwable(rawData.toString(), null))
}
} else {//连接成功了 但是某些原因出错的
val errorData = StringBuilder()
val error = connection.errorStream
val reader1 = BufferedReader(InputStreamReader(error))
reader1.use {
reader1.forEachLine {
errorData.append(it)
}
}
callback.onResponse(call,Response(connection.responseCode, null, null, ErrorBody(errorData.toString())))
}
} else {
val errorData = StringBuilder()
val error = connection.errorStream
val reader1 = BufferedReader(InputStreamReader(error))
reader1.use {
reader1.forEachLine {
errorData.append(it)
}
}
callback.onFailure(call,Throwable(errorData.toString(), null))
}
}
}
catch (e: SocketTimeoutException){//超时
callback.onFailure(call,Throwable("timeout", null))
}
catch (e: UnknownServiceException){
Log.d("HttpCreator", "UnknownServiceException: "+e.message)
callback.onFailure(call,Throwable(e.message, null))
e.printStackTrace()
}
catch (e: Exception) {
Log.d("HttpCreator", "Exception: "+e.message)
callback.onFailure(call,Throwable(e.message, null))
e.printStackTrace()
} finally {
connection?.disconnect()
}
}
}
}
Callback.kt文件实现了,很多用到的接口的定义和类的定义,都是在发送请求和回调的时候需要用到的,定义的类和接口我都是和retrofit重名的,因为这样才能少改动原来的接口。
package com.rengda.testdemon
import android.util.Log
import java.io.*
import java.lang.Exception
import java.lang.StringBuilder
import java.lang.reflect.Type
import java.nio.charset.Charset
interface Call<T> : Cloneable {
fun enqueue(callback: Callback<T>)
}
interface Callback<T> {
//失败的回调
fun onFailure( call: Call<T>,t: Throwable)
//成功的回调
fun onResponse(call: Call<T>,response: Response<T>)
fun myResultClassType(call: Call<T>):Type
}
class Response<T> (val code:Int,val body:T?,val raw:String?,val errorData:ErrorBody?){
fun body():T?{
return body
}
fun errorBody():ErrorBody?{
return errorData
}
}
class ErrorBody(val msg:String?){
fun string():String{
return msg.toString()
}
}
//自己用的 缩减过代码
open class RequestBody(val bytes:ByteArray?=null,val contentType:String="application/json"){
//创建
companion object{
//创建普通的
fun create(content: String): RequestBody {
var charset = Charset.forName("UTF-8")
val bytes = content.toByteArray(charset!!)
return RequestBody(bytes)
}
//创建文件
fun create(content: File,contentType: String): RequestBody {
return RequestBody(content.readBytes(),contentType)
}
}
}
//可以根据request body来创建表单内容 contentType是文件的类型 例如照片的话image/jpeg
class MutilBodyPart(val name:String,val fileName:String,val contentType:String,val requetBody: RequestBody )
//发送复杂的文件的原理 可以参考
//http://t.zoukankan.com/zyzl-p-4526914.html
//https://blog.csdn.net/qq_16957817/article/details/109205773
class MutilBody( bytes:ByteArray?=null, contentType:String="multipart/form-data"):RequestBody(bytes,contentType){
companion object{
fun createImageFormData(mutilBodyPartList: kotlin.Array<MutilBodyPart>): MutilBody {
val boundary="aaaaaaaaaaaaaaaaaaaaaaa" //用来分割信息的边界 正文的数据就以该字段来区分
var bodyByte= byteArrayOf()
for (item in mutilBodyPartList){
val content=StringBuilder()//用来拼接数据
//每个参数开始之前 要--bundery和一个回车
content.append("--").append(boundary)
content.append("\r\n")
//来到对参数的描述
content.append("Content-Disposition: form-data;")
content.append("name=${item.name};")
content.append("filename=${item.fileName};")
content.append("filename=${item.fileName};")
content.append("\r\n")
content.append("Content-Type: ${item.contentType}")//这里是文件的类型
//开始真正的数据前 要有两个回车换行
content.append("\r\n")
content.append("\r\n")
//要拼接真实数据了
//由于我的item的内容直接是ByteArray的 所以 要先把前面一段变成ByteArray,然后才拼接真的数据
bodyByte += content.toString().toByteArray()//真实数据之前的内容的ByteArray
bodyByte += item.requetBody.bytes?: byteArrayOf()//数据的ByteArray
//真实数据结束之后 要一个回车
bodyByte+= "\r\n".toByteArray()
}
//参数都结束时 boundary前后各需添加两上横线,最添加添回车换行
val content=StringBuilder()
content.append("--").append(boundary).append("--").append("\r\n")
bodyByte += content.toString().toByteArray()//真实数据之前的内容的ByteArray
return MutilBody(bodyByte,"multipart/form-data ;boundary="+boundary)
}
fun createImageFormData(item: MutilBodyPart): MutilBody {
val boundary="aaaaaaaaaaaaaaaaaaaaaaa" //用来分割信息的边界 正文的数据就以该字段来区分
var bodyByte= byteArrayOf()
var content=StringBuilder()//用来拼接数据
//每个参数开始之前 要--bundery和一个回车
content.append("--").append(boundary)
content.append("\r\n")
//来到对参数的描述
content.append("Content-Disposition: form-data;")
content.append("name=${item.name};")
content.append("filename=${item.fileName};")
content.append("filename=${item.fileName};")
content.append("\r\n")
content.append("Content-Type: ${item.contentType}")//这里是文件的类型
//开始真正的数据前 要有两个回车换行
content.append("\r\n")
content.append("\r\n")
//要拼接真实数据了
//由于我的item的内容直接是ByteArray的 所以 要先把前面一段变成ByteArray,然后才拼接真的数据
bodyByte += content.toString().toByteArray()//真实数据之前的内容的ByteArray
bodyByte += item.requetBody.bytes?: byteArrayOf()//数据的ByteArray
//真实数据结束之后 要一个回车
bodyByte+= "\r\n".toByteArray()
//参数都结束时 boundary前后各需添加两上横线,最添加添回车换行
content=StringBuilder()
content.append("--").append(boundary).append("--").append("\r\n")
bodyByte += content.toString().toByteArray()//真实数据之前的内容的ByteArray
return MutilBody(bodyByte,"multipart/form-data ;boundary="+boundary)
}
}
}
用来定义访问的接口的,典型的几个接口,get,post,put,delete,有动态Header的通过参数传递的。还有文件的上传
package com.rengda.testdemon
interface InServiceHttp {
@HTTPGET("code")
fun getRandomStr(): Call<MyResult<RandomStr>>
@HTTPDELETE ("/wms/car-info-fee/{id}")
fun deleteCarInfoFee(@HTTPHeader("Authorization") authorization: String,@HTTPPath("id") id:String):Call<MyResult<String>>
@HTTPPOST ("/wms/car-info-fee")
fun saveCarInfoFee (@HTTPHeader("Authorization") authorization: String,@HTTPHeader("Content-Type") contentType: String,@HTTPBody body: RequestBody):Call<MyResult<CarInfo>>
@HTTPPUT ("/wms/car-info")
fun updateCarInfoRemark(@HTTPHeader("Authorization") authorization: String,@HTTPHeader("Content-Type") contentType: String,@HTTPBody body: RequestBody):Call<MyResult<CarInfo>>
//图片上传
@HTTPMultipart
@HTTPPOST("/admin/file/upload")
fun upload(@HTTPMultiBody requestBody: MutilBody):Call<MyResult<String>>
}
object Url {
const val BASE_URL="http://xxx.xxx.xxx/"
}
package com.rengda.testdemon
import com.google.gson.annotations.SerializedName
data class RandomStr (val randomStr:String,val base64:String)
data class MyResult<T>(val code:Int,val msg:String?, val data:T?)
data class CarInfo(val appId: String?, val applyFlag: String?, val arrivalTime: String?, val carTeamId: String?)
data class User(@SerializedName("access_token")val accessToken:String?, @SerializedName("token_type")val tokenType:String?, @SerializedName("refresh_token") val refreshToken:String?, @SerializedName("expires_in") val expiresIn:String?, val scope:String?, val license:String?, @SerializedName("user_id")val userId:String?, @SerializedName("user_name") val username:String?, val mobile:String?, val active:String?, @SerializedName("dept_id")val deptId:String?, @SerializedName("display_name") val displayName: String?) {
}
package com.rengda.testdemon
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.google.gson.reflect.TypeToken
import org.json.JSONObject
import java.io.File
import java.lang.reflect.Type
class HttpConnectionActivity : AppCompatActivity() {
var context:Context?=null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_http_connection)
context=this
doQuery()
doPost()
doUploadFile()
}
fun doQuery(){
val service= HttpCreator.creat<InServiceHttp>()
service.getRandomStr().enqueue(object :Callback<MyResult<RandomStr>>{
override fun onFailure(call: Call<MyResult<RandomStr>>, t: Throwable) {
runOnUiThread { //里面执行失败的操作
Toast.makeText(context,"错误信息:${t?.message}",Toast.LENGTH_SHORT).show()
}
}
override fun onResponse(
call: Call<MyResult<RandomStr>>,
response: Response<MyResult<RandomStr>>
) {
runOnUiThread {//里面实现成功的操作
val myResult= response.body()
if (myResult !=null){//说明解析成功了,而且成功被转为 了MyResult类型
if (myResult.code==0){//成功
myResult.data as RandomStr
Toast.makeText(context,"获取到的随机字符:${myResult?.data.randomStr}",Toast.LENGTH_SHORT).show()
}else{//失败
Toast.makeText(context,"错误信息:${myResult?.msg}",Toast.LENGTH_SHORT).show()
}
}else{
Toast.makeText(context,"错误信息:${response.errorBody()?.string()}",Toast.LENGTH_SHORT).show()
}
}
}
override fun myResultClassType(call: Call<MyResult<RandomStr>>): Type {
//用来gson解析的时候需要的类型,由于我不会和retrofit一样的的动态获取类型,所以只能手动传递了
return object : TypeToken<MyResult<RandomStr>>() {}.type
}
})
}
fun doPost(){
val service= HttpCreator.creat<InServiceHttp>()
val body= JSONObject()
body.put("carId","111")
body.put("tallyId","222")
body.put("remark","444")
body.put("content","333")
val requestBody=RequestBody.create(body.toString())//默认contentType是application/json
service.saveCarInfoFee("11","111",requestBody).enqueue(object :Callback<MyResult<CarInfo>>{
override fun onFailure(call: Call<MyResult<CarInfo>>, t: Throwable) {
runOnUiThread { //里面执行失败的操作
}
}
override fun onResponse(call: Call<MyResult<CarInfo>>, response: Response<MyResult<CarInfo>>) {
runOnUiThread { //里面是成功的操作
}
}
override fun myResultClassType(call: Call<MyResult<CarInfo>>): Type {
return object : TypeToken<MyResult<CarInfo>>() {}.type
}
})
}
fun doUploadFile(){
val service= HttpCreator.creat<InServiceHttp>()
val filePath="一个图片的路径"
val file = File(filePath)
val fileBody = RequestBody.create(file,"multipart/form-data")
val mutilBodyPart=MutilBodyPart("file",file.name,"image/jpeg",fileBody)
val mutilBody= MutilBody.createImageFormData(mutilBodyPart)
service.upload(mutilBody).enqueue(object :Callback<MyResult<String>>{
override fun onFailure(call: Call<MyResult<String>>, t: Throwable) {
runOnUiThread { //里面执行失败的操作
}
}
override fun onResponse(call: Call<MyResult<String>>, response: Response<MyResult<String>>) {
runOnUiThread { //里面是成功的操作
}
}
override fun myResultClassType(call: Call<MyResult<String>>): Type {
return object : TypeToken<MyResult<CarInfo>>() {}.type
}
})
}
}