每次调试打印日志都很头痛

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 每次调试打印日志都很头痛

引子


当代码的运行效果不符合预期时就得进行调试,排查下整个数据链路上到底是哪个环节出了问题。


断点调试当然是首选,因为它可以单步执行程序,并查看当前执行步骤中所有的数据值。但有些场景下,断点调试就显得笨拙。比如大量异步并发的场景,当程序不是线性执行而是跳来跳去时,就会发生你期望下一步是执行到这里,断点调试却跳到了另一个线程,这样的复杂度,让正在执行的代码变得难以理解。除此之外,有些型号的手机,一断点调试就卡的不行,甚至 crash。


在这种情况下,打日志就是唯一的选择了。


日志输出一个简单的变量值是一件轻而易举的事情:


Log.v("test", "duration=${duration}")


但在复杂的业务场景中,会存在各种嵌套的复杂结构,比如 List,Map。断点调试中,可以轻松地点开这些数据结构,查看任何感兴趣的字段,甚至还可以当场计算。在输出日志时有什么好办法能够轻松地输出这些复杂的数据结构吗?


打印列表 & Map


刚开始开发 Android 时,我是这样打印列表的:


for (int i = 0; i < list.size(); i++) {
    Log.d("test", "list item="+list.get(i)); 
}


用一个 for 循环来打印列表所有元素。


后来我学会了用更高级的语法来简化日志输出:


for (String str:list) {
    Log.v("test", "list item="+str);
}


这样的写法是无法被复用的,因为不同的业务场景,数据类型都不一样。为了调试,这样的 for 循环就会散落在各处。


这样写还有一个坏处,输出的列表信息可能被其他日志穿插。因为每一个列表内容都是一条新得日志,中间极有可能被别的 log 打断。


有没有一个函数可以打印包含任意数据类型的列表,并将列表内容组织成更具可读性的字符串?


用 Kotlin  的扩展函数+泛型+高阶函数就能优雅地做到:


fun <T> Collection<T>.print(mapper: (T) -> String) =
    StringBuilder("\n[").also { sb ->
        //遍历集合元素将元素转换成感兴趣的字串,并独占一行
        this.forEach { e -> sb.append("\n\t${mapper(e)},") }
        sb.append("\n]")
    }.toString()


为集合的基类Collection新增一个扩展函数,它是一个高阶函数,因为它的参数是另一个函数,该函数用 lambda 表示。再把集合元素抽象成泛型。通过StringBuilder将所有集合内容拼接成一个自动换行的字符串。


写段测试代码看下效果:


data class Person(var name: String, var age: Int)
val persons = listOf(
    Person("Peter", 16),
    Person("Anna", 28),
    Person("Anna", 23),
    Person("Sonya", 39)
)
persons.print { "${it.name}_${it.age}" }.let { Log.v("test",it) }


打印结果如下:


V/test: [
      Peter_16,
      Anna_28,
      Anna_23,
      Sonya_39,
    ]


这样整个列表内容会作为一条log输出。


同样地,可以如法炮制一个打印 Map 的扩展函数:


fun <K, V> Map<K, V?>.print(mapper: (V?) -> String): String =
    StringBuilder("\n{").also { sb ->
        this.iterator().forEach { entry ->
            sb.append("\n\t[${entry.key}] = ${mapper(entry.value)}")
        }
        sb.append("\n}")
    }.toString()


打印复杂数据结构


有些数据类字段比较多,调试时,想把它们通通打印出来,在 Java 中,借助于 AndroidStudio 的 toString功能倒是可以方便地生成可读性很高的字串:


public class Person {
    private String name;
    private int age;
    @Override
    public String toString() {
        return ”Person{“ +
                ”name=‘“ + name + ’\” +
                ”, age=“ + age +
                ‘}’;
    }
}


但是每新建一个数据类都要手动生成一个toString()方法也挺麻烦。


利用 Kotlin 的 data class可以省去这一步,但打印效果是所有字段都在同一行中:


data class Person(var name: String, var age: Int)
Log.v(“test”, “person=${Person("Peter", 16)}”)
//输出如下:
V/test: person=Person(name=Peter, age=16)


如果字段很多,把它们都打印在一行中可读性很差。


有没有一种方法,可以读取一个类中所有的字段信息?


这样我们就可以将他们组织成想要的形状。


请看下面这个方法:


fun Any.ofMap() =
    // 过滤掉除data class以外的其他类
    this::class.takeIf { it.isData }
        // 遍历类的所有成员 过滤掉成员方法 只考虑成员属性
        ?.members?.filterIsInstance<KProperty<Any>>()
        // 将成员属性名和值存储在Pair中
        ?.map { it.name to it.call(this) }
        // 将Pair转换成map
        ?.toMap()


为任意 Kotlin 中的类添加一个扩展函数,它的功能是将data class中所有的字段名及其对应值存在一个 map 中。其中用到的 Kotlin 语法糖如下:


  • isDataKClass中的一个属性,用于判断该类是不是一个data classKClass是 Kotlin 中用来描述 类的类型KClass可以通过对象::class语法获得。


  • members也是KClass中的一个属性,它以列表的形式返回了类中所有的方法和属性。


  • filterIsInstance()Iterable接口的扩展函数,用于过滤出集合中指定的类型。


  • to是一个infix扩展函数,它的定义如下:


public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)


  • 带有infix标识的函数只允许带有一个参数,并且在调用时可以省略包裹参数的括号。这种语法叫中缀表达式,它用于简化方法调用。


写段测试代码,结合上一节的打印 map 函数看下效果:


data class Person(var name: String, var age: Int)
Person("Peter", 16).ofMap()?.print { it.toString() }.let { Log.v("test","$it") }


测试代码先将Person实例转换成 map,然后打印 map。输出结果如下:


V/test:
    {
      [age] = 16
      [name] = Peter
    }


data class嵌套会发生什么?


//位置,嵌套在Person类中
data class Location(var x: Int, var y: Int)
data class Person(
    var name: String,
    var age: Int, 
    var locaton: Location? = null
)
Person("Peter", 16, Location(20, 30))
    .ofMap()
    ?.print { it.toString() }
    .let { Log.v("test", "$it") }
// 打印结果如下 
    {
      [age] = 16
      [locaton] = Location(x=20, y=30)
      [name] = Peter
    }


期望得到类似 Json 的打印效果,但输出结果还差一点。是因为将Person转化成Map时并没有将嵌套的Location也转化成键值对。


需要将ofMap()方法重构成递归调用:


fun Any.ofMap(): Map<String, Any?>? {
    return this::class.takeIf { it.isData }
        ?.members?.filterIsInstance<KProperty<Any>>()
        ?.map { member ->
            val value = member.call(this)?.let { v->
                //'若成员变量是data class,则递归调用ofMap(),将其转化成键值对,否则直接返回值'
                if (v::class.isData) v.ofMap()
                else v
            }
            member.name to value
        }
        ?.toMap()
}


为了让打印结果也有嵌套缩进效果,打印 Map 的函数也需要相应地重构:


/**
 * 打印 Map,生成结构化键值对子串
 * @param space 行缩进量
 */
fun <K, V> Map<K, V?>.print(space: Int = 0): String {
    //'生成当前层次的行缩进,用space个空格表示,当前层次每一行内容都需要带上缩进'
    val indent = StringBuilder().apply {
        repeat(space) { append(" ") }
    }.toString()
    return StringBuilder("\n${indent}{").also { sb ->
        this.iterator().forEach { entry ->
            //'如果值是 Map 类型,则递归调用print()生成其结构化键值对子串,否则返回值本身'
            val value = entry.value.let { v ->
                (v as? Map<*, *>)?.print("${indent}${entry.key} = ".length) ?: v.toString()
            }
            sb.append("\n\t${indent}[${entry.key}] = $value,")
        }
        sb.append("\n${indent}}")
    }.toString()
}


写段测试代码,看看效果:


//'坐标类,嵌套在Location类中'
data class Coordinate(var x: Int, var y: Int)
//'位置类,嵌套在Person类中'
data class Location(
    var country: String, 
    var city: String, 
    var coordinate: Coordinate
)
data class Person(
    var name: String, 
    var age: Int, 
    var locaton: Location? = null
)
Person("Peter", 16, Location("china", "shanghai", Coordinate(10, 20)))
    .ofMap()
    ?.print()
    .let { Log.v("test", "$it") }
//'打印效果如下'
    {
      [age] = 16,
      [locaton] = 
              {
                [city] = shanghai,
                [coordinate] = 
                           {
                             [x] = 10,
                             [y] = 20,
                           },
                [country] = china,
              },
      [name] = Peter,
    }


推荐阅读


业务代码参数透传满天飞?(一)


业务代码参数透传满天飞?(二)


全网最优雅安卓控件可见性检测


全网最优雅安卓列表项可见性检测


页面曝光难点分析及应对方案


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
6月前
|
Kubernetes Shell Linux
linux|shell脚本|有趣的知识---格式化输出日志和脚本调试方法以及kubernetes集群核心服务重启和集群证书备份脚本
linux|shell脚本|有趣的知识---格式化输出日志和脚本调试方法以及kubernetes集群核心服务重启和集群证书备份脚本
214 0
|
3月前
|
消息中间件 存储 Java
手动实现 Spring Boot 日志链路追踪:提升调试效率的利器
【8月更文挑战第8天】在复杂的分布式系统中,日志是诊断问题、追踪系统行为的重要工具。然而,随着微服务架构的普及,服务间的调用链路错综复杂,传统的日志记录方式往往难以快速定位问题源头。今天,我们将探讨如何在不依赖外部组件(如Zipkin、Sleuth等)的情况下,手动实现Spring Boot应用的日志链路追踪,让日志定位更加便捷高效。
178 1
|
1月前
|
Java 程序员 应用服务中间件
「测试线排查的一些经验-中篇」&& 调试日志实战
「测试线排查的一些经验-中篇」&& 调试日志实战
22 1
「测试线排查的一些经验-中篇」&& 调试日志实战
|
3月前
|
XML Java 数据库
"揭秘!Spring Boot日志链路追踪大法,让你的调试之路畅通无阻,效率飙升,问题无所遁形!"
【8月更文挑战第11天】在微服务架构中,请求可能跨越多个服务与组件,传统日志记录难以全局追踪问题。本文以电商系统为例,介绍如何手动实现Spring Boot应用的日志链路追踪。通过为每个请求生成唯一追踪ID并贯穿全链路,在服务间传递该ID,并在日志中记录,即使日志分散也能通过ID串联。提供了实现这一机制所需的关键代码片段,包括使用过滤器设置追踪ID、业务代码中的日志记录及Logback配置。此方案显著提升了问题定位的效率,适用于基于Spring Boot构建的微服务环境。
91 4
|
3月前
|
SQL 数据库 Java
Hibernate 日志记录竟藏着这些秘密?快来一探究竟,解锁调试与监控最佳实践
【8月更文挑战第31天】在软件开发中,日志记录对调试和监控至关重要。使用持久化框架 Hibernate 时,合理配置日志可帮助理解其内部机制并优化性能。首先,需选择合适的日志框架,如 Log4j 或 Logback,并配置日志级别;理解 Hibernate 的多级日志,如 DEBUG 和 ERROR,以适应不同开发阶段需求;利用 Hibernate 统计功能监测数据库交互情况;记录自定义日志以跟踪业务逻辑;定期审查和清理日志避免占用过多磁盘空间。综上,有效日志记录能显著提升 Hibernate 应用的性能和稳定性。
50 0
|
3月前
|
存储 JSON 监控
FastAPI日志之谜:如何揭开Web应用监控与调试的面纱?
【8月更文挑战第31天】在现代Web开发中,日志记录对于监控应用状态、诊断问题和了解用户行为至关重要。FastAPI框架提供了强大的日志功能,使开发者能轻松集成日志记录。本文将详细介绍如何在FastAPI中设置和利用日志,包括基础配置、请求响应日志、错误处理和结构化日志等内容,帮助提升应用的可维护性和性能。
134 0
|
5月前
|
C++
spdlog 日志库部分源码说明——日志格式设定,DIY你自己喜欢的调试信息,你能调试的远比你想象的还要丰富
spdlog 日志库部分源码说明——日志格式设定,DIY你自己喜欢的调试信息,你能调试的远比你想象的还要丰富
298 6
|
5月前
|
SQL 运维 关系型数据库
|
4月前
|
JavaScript
【vue】 将Vue2中的console.log()调试信息移除
【vue】 将Vue2中的console.log()调试信息移除
189 0
|
4月前
|
SQL
关于ThinkPHP5.1+的Log无法记录SQL调试记录的小经历
项目开发阶段,除了基本编码外,性能也需要实时关注与优化。之前我的大部分项目都是使用ThinkPHP5.0以及ThinkPHP3.2,对于框架提供的日志记录和日志配置都差不多,然后使用ThinkPHP5.1的时候就吃瘪,花了十几分钟才好,所以写一下防止后面忘记了再踩坑。
137 0