用代理模式解决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日志并进行多维度分析。
相关文章
|
1天前
|
Ubuntu Java Linux
查看Linux系统中日志文件
查看Linux系统中日志文件
|
1天前
|
存储 监控 应用服务中间件
查看nginx日志文件
器性能和提高网站可用性。掌握日志文件的路径、查看方法和基本分析技能对于任何服务器管理员来说都是必备技能。
7 1
|
2天前
|
SQL Oracle NoSQL
实时计算 Flink版操作报错合集之报错“找不到对应的归档日志文件”,怎么处理
在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
|
12天前
|
存储 Kubernetes 网络安全
[k8s]使用nfs挂载pod的应用日志文件
[k8s]使用nfs挂载pod的应用日志文件
|
23天前
|
Shell 测试技术 Linux
Shell 脚本循环遍历日志文件中的值进行求和并计算平均值,最大值和最小值
Shell 脚本循环遍历日志文件中的值进行求和并计算平均值,最大值和最小值
29 3
|
6天前
|
应用服务中间件 Linux nginx
Nginx log 日志文件较大,按日期生成 实现日志的切割
Nginx log 日志文件较大,按日期生成 实现日志的切割
28 0
|
6天前
|
C#
C# 写日志文件
C# 写日志文件
12 0
|
7天前
|
关系型数据库 MySQL Linux
Linux——日志文件按天切割
Linux——日志文件按天切割
24 0
|
19天前
|
存储 运维 Java
SpringBoot使用log4j2将日志记录到文件及自定义数据库
通过上述步骤,你可以在Spring Boot应用中利用Log4j2将日志输出到文件和数据库中。这不仅促进了良好的日志管理实践,也为应用的监控和故障排查提供了强大的工具。强调一点,配置文件和代码的具体实现可能需要根据应用的实际需求和运行环境进行调优和修改,始终记住测试配置以确保一切运行正常。
102 0
|
22天前
|
存储 弹性计算 监控
函数计算产品使用问题之程序正常运行,但无法在 /home/lang_serve_severless_log 下找到日志文件,该如何排查
函数计算产品作为一种事件驱动的全托管计算服务,让用户能够专注于业务逻辑的编写,而无需关心底层服务器的管理与运维。你可以有效地利用函数计算产品来支撑各类应用场景,从简单的数据处理到复杂的业务逻辑,实现快速、高效、低成本的云上部署与运维。以下是一些关于使用函数计算产品的合集和要点,帮助你更好地理解和应用这一服务。

热门文章

最新文章