用代理模式解决Okhttp日志拦截器在下载文件时的窘境

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 用代理模式解决Okhttp日志拦截器在下载文件时的窘境

okhttp官方拦截器之殇


  众所周知,当我们使用okhttp并且希望打印网络请求的日志时,我们会在okhttp的拦截器链中添加一个日志拦截器,并且在拦截器中输出网络请求和网络相应的各种信息(包括请求头,请求体,url等)。很暖心的是,官方给我们提供了一个日志拦截器HttpLoggingInterceptor,于是乎你喜闻乐见地把日志拦截器添加到了okhttp的拦截器序列中,一切都没问题,日志输出的很清晰,直到你遇上了下载请求,程序出现阻塞,卡死。。。


问题本源


  要说清楚这个问题,我们要先搞清楚所谓的下载请求和普通请求有什么区别,答案是没有区别,他们都是将管道里面的字节流输出到手机中,只不过下载请求的目的地是存储卡,普通请求的目的地是内存(当然普通的请求也是广义上的“下载”,只不过数据一般不是文件而是接口返回的结构化数据)。因此,我们回到okhttp的官方拦截器源码中去看(这里用的是新版的okhttp,已经用kotlin重写)。


class HttpLoggingInterceptor @JvmOverloads constructor(
 private val logger: Logger = Logger.DEFAULT
) : Interceptor {
   //...省略部分代码
   @Throws(IOException::class)
   override fun intercept(chain: Interceptor.Chain): Response {
       //...省略部分代码
       val contentType = responseBody.contentType()
       val charset: Charset = contentType?.charset(UTF_8) ?: UTF_8
       if (!buffer.isProbablyUtf8()) {
         logger.log("")
         logger.log("<-- END HTTP (binary ${buffer.size}-byte body omitted)")
         return response
       }
       if (contentLength != 0L) {
         logger.log("")
         //关键,将管道里面的数据写入到字符串中,并用log输出
         logger.log(buffer.clone().readString(charset))
       }
       //...省略部分代码
   }
}

  我们可以看到,这个拦截器的本质就是将管道深拷贝一份,然后输出到字符串中(即内存中),这个对于一般的请求而言没有问题,无非最多就是几kb的字符串数据(后台传回来的结构化信息),但是对于下载请求而言是致命的,因为这个拦截器尝试把一个几mb(甚至几百mb)的内容缓存到内存里面!一个普通的安卓app进程也就几百mb,问题出现在这里。


解决方案


  解决思想非常简单,由于这个拦截器提供了分级的选项,我们可以想办法让它只有遇到非下载请求的时候,才去使用BODY级,而遇到下载请求的时候,切换到HEADER或者BASIC级让它规避把响应体输出到内存即可,但是真的有那么简单吗?我们继续回到源码。


class HttpLoggingInterceptor @JvmOverloads constructor(
 private val logger: Logger = Logger.DEFAULT
) : Interceptor {
   //...
}

  很遗憾,这是一个不可重写的类,官方也没有提供类似的api让我们持有这种能力,笔者看了网上很多的解决方案,大多数是选择第三方框架或者直接复制粘贴源码然后修改,这两种解决方案笔者都不太满意,因此决定另寻一种出路,这里笔者选择了一种设计模式:代理模式

顺便推荐一个适合学习设计模式的网站,图文并茂非常适合初学者理解设计模式:免费在线学习代码重构和设计模式 (refactoringguru.cn)

废话不多说直接上代码:


abstract class HttpLoggingProxy(
    private val client: HttpLoggingInterceptor
) : Interceptor {
    /**
     * 通过请求判断是否需要输出日志
     */
    abstract fun needToLog(request: Request):Boolean
    override fun intercept(chain: Interceptor.Chain): Response {
        val request=chain.request()
        //需要输出日志,用日志拦截器输出
        return if(needToLog(request)){
            client.intercept(chain)
        }
        //不需要输出日志
        else{
            chain.proceed(request)
        }
    }
}

  笔者设计了一个抽象类,去持有被代理的日志拦截器,然后通过needToLog(request: Request)方法去判断是否要输出日志,如果需要输出日志,则使用被代理的拦截器去拦截chain,否则继续把request往下一个拦截器传递。

笔者注:你可以再传入一个level为BASIC的拦截器,如果不需要输出body的情况则用那个拦截器,这样在下载请求中也可以监听一下请求头,url等信息

然后继承抽象类,把你实现的拦截器代理器传入到okhttp的拦截器链中即可


class MyHttpLoggingProxy:HttpLoggingProxy(
    HttpLoggingInterceptor()
) {
    override fun needToLog(request: Request): Boolean {
        //通过请求来判断是否需要下载
        return request.url.toString()=="你喜欢的业务"
    }
}


更进一步:在Retrofit中的使用


  我们再实现一个在Retrofit中使用日志拦截器代理器的方法,由于Retrofit在构建okhttp请求的过程中,会把请求方法(即那个Retrofit的Service接口里面的方法)放入到okhttp的Tag中,因此我们可以通过这个Tag来判断方法是否包含了Stream这个注解,如果包含则说明我们在使用Retrofit完成下载的功能。


/**
 * 返回某个Retrofit定义在方法上的注解,例如[POST],[GET]
 */
fun <T : Annotation> Request.getMethodAnnotation(annotationClass: Class<T>): T? {
    return tag(Invocation::class.java)?.method()?.getAnnotation(annotationClass)
}
fun <T : Annotation> Request.containMethodAnnotation(annotationClass: Class<T>): Boolean {
    return getMethodAnnotation(annotationClass) != null
}
class RetrofitHttpLoggingProxy:HttpLoggingProxy(
    HttpLoggingInterceptor()
) {
    override fun needToLog(request: Request): Boolean {
        //Retrofit方法中是否包含了Streaming这个注解,如果包含则说明是下载,不输出日志
        return !request.containMethodAnnotation(Streaming::class.java)
    }
}

那么这篇文章就到此结束了,很感谢你观看,如果喜欢请点赞关注,如果你有疑问或者有更好的建议请在评论区给我留言,我会尽快回复


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
20天前
|
存储 Oracle 关系型数据库
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
本文介绍了MySQL InnoDB存储引擎中的数据文件和重做日志文件。数据文件包括`.ibd`和`ibdata`文件,用于存放InnoDB数据和索引。重做日志文件(redo log)确保数据的可靠性和事务的持久性,其大小和路径可由相关参数配置。文章还提供了视频讲解和示例代码。
129 11
【赵渝强老师】MySQL InnoDB的数据文件与重做日志文件
|
20天前
|
SQL Oracle 关系型数据库
【赵渝强老师】Oracle的控制文件与归档日志文件
本文介绍了Oracle数据库中的控制文件和归档日志文件。控制文件记录了数据库的物理结构信息,如数据库名、数据文件和联机日志文件的位置等。为了保护数据库,通常会进行控制文件的多路复用。归档日志文件是联机重做日志文件的副本,用于记录数据库的变更历史。文章还提供了相关SQL语句,帮助查看和设置数据库的日志模式。
【赵渝强老师】Oracle的控制文件与归档日志文件
|
7天前
|
监控 数据挖掘 数据安全/隐私保护
Python脚本:自动化下载视频的日志记录
Python脚本:自动化下载视频的日志记录
|
20天前
|
Oracle 关系型数据库 数据库
【赵渝强老师】Oracle的参数文件与告警日志文件
本文介绍了Oracle数据库的参数文件和告警日志文件。参数文件分为初始化参数文件(PFile)和服务器端参数文件(SPFile),在数据库启动时读取并分配资源。告警日志文件记录了数据库的重要活动、错误和警告信息,帮助诊断问题。文中还提供了相关视频讲解和示例代码。
|
2月前
|
监控 Linux 应用服务中间件
系统监控:使用日志文件 journalctl的使用
本文介绍了如何使用`journalctl`命令来监控和查看Linux系统的日志文件,包括查看特定行数、过滤日志级别、实时跟踪日志、按时间段查询日志以及日志轮换和压缩的配置。
57 2
系统监控:使用日志文件 journalctl的使用
|
2月前
|
SQL 数据库
为什么 SQL 日志文件很大,我应该如何处理?
为什么 SQL 日志文件很大,我应该如何处理?
|
2月前
|
开发工具 git
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
git显示开发日志+WinSW——将.exe文件注册为服务的一个工具+图床PicGo+kubeconfig 多个集群配置 如何切换
39 1
|
2月前
|
存储 监控 固态存储
如何监控和优化 WAL 日志文件的存储空间使用?
如何监控和优化 WAL 日志文件的存储空间使用?
|
2月前
|
SQL 数据库
为什么SQL日志文件很大,该如何处理?
为什么SQL日志文件很大,该如何处理?
|
3月前
|
缓存 监控 算法
分析慢日志文件来优化 PHP 脚本的性能
分析慢日志文件来优化 PHP 脚本的性能