堆栈跟踪 堆栈跟踪
术语“泄漏抽象”已经存在了一段时间。 对其进行硬币化通常归因于乔尔·斯波斯基(Joel Spolsky),他撰写了这篇经常被引用的文章 。 我现在偶然发现了对泄漏抽象的另一种解释,该解释由堆栈跟踪的深度来衡量:
因此,根据Geek&Poke的说法,长堆栈跟踪很不好。 我在Igor Polevoy的博客 (他是ActiveJDBC的创建者, 它是流行的Ruby ActiveRecord查询接口的Java实现) 的博客上就已经看到过这种说法。 就像Joel Spolsky的论点经常被用来批评ORM一样,Igor的论点也被用来比较ActiveJDBC和Hibernate 。 我在引用:
有人会说:那又怎样,为什么我要关心依赖关系的大小,堆栈跟踪的深度等。我认为一个好的开发人员应该关心这些事情。 框架越厚,它越复杂,分配的内存越多,出错的可能性就越大。
我完全同意,具有一定复杂性的框架倾向于具有更长的堆栈跟踪。 因此,如果我们通过心理Prolog处理器运行这些公理:
- 如果Hibernate是一个泄漏抽象,并且
- 如果Hibernate很复杂,并且
- 如果复杂性导致较长的堆栈跟踪,则
- 泄漏抽象和长堆栈跟踪相关
我不会声称存在正式的因果关系。 但相关性似乎合乎逻辑。
但是这些事情并不一定是坏事。 实际上,就软件质量而言,长堆栈跟踪可能是一个好兆头。 这可能意味着某个软件的内部组件显示出很高的凝聚力和高度的DRY-ness ,这再次意味着,在您的框架中隐藏细微错误的风险很小。 请记住,高凝聚力和高DRY强度会导致大部分代码在整个框架内极为相关,这再次意味着,任何低级错误都将炸毁整个框架,因为它会导致一切正常错误。 如果您进行测试驱动的开发 ,您将立即注意到您的愚蠢错误未能通过90%的测试用例,从而获得回报。
一个真实的例子
让我们以jOOQ为例说明这一点,因为我们已经在比较Hibernate和ActiveJDBC。 数据库访问抽象中某些最长的堆栈跟踪可以通过在断点与JDBC的接口处放置一个断点来实现。 例如,从JDBC ResultSet获取数据时。
Utils.getFromResultSet(ExecuteContext, Class<T>, int) line: 1945
Utils.getFromResultSet(ExecuteContext, Field<U>, int) line: 1938
CursorImpl$CursorIterator$CursorRecordInitialiser.setValue(AbstractRecord, Field<T>, int) line: 1464
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(AbstractRecord) line: 1447
CursorImpl$CursorIterator$CursorRecordInitialiser.operate(Record) line: 1
RecordDelegate<R>.operate(RecordOperation<R,E>) line: 119
CursorImpl$CursorIterator.fetchOne() line: 1413
CursorImpl$CursorIterator.next() line: 1389
CursorImpl$CursorIterator.next() line: 1
CursorImpl<R>.fetch(int) line: 202
CursorImpl<R>.fetch() line: 176
SelectQueryImpl<R>(AbstractResultQuery<R>).execute(ExecuteContext, ExecuteListener) line: 274
SelectQueryImpl<R>(AbstractQuery).execute() line: 322
T_2698Record(UpdatableRecordImpl<R>).refresh(Field<?>...) line: 438
T_2698Record(UpdatableRecordImpl<R>).refresh() line: 428
H2Test.testH2T2698InsertRecordWithDefault() line: 931
与ActiveJDBC的堆栈跟踪相比,它要多得多,但与Hibernate(使用大量反射和检测)相比,它要少得多。 而且它涉及相当隐秘的内部类,并且带有很多方法重载。 怎么解释呢? 让我们从下至上(或堆栈跟踪中的自上而下)进行检查
CursorRecordInitialiser
CursorRecordInitialiser是一个内部类,它封装了Cursor对Record的初始化,并且它确保ExecuteListener SPI的相关部分被覆盖在一个地方。 它是JDBC的各种ResultSet
方法的网关。 这是一个通用的内部RecordOperation
实现,由…调用。
RecordDelegate
…一个RecordDelegate
。 尽管类名几乎没有任何意义,但其目的是屏蔽和包装所有直接记录操作,以便可以实现RecordListener SPI的集中实现。 客户端代码可以实现此SPI,以监听活动记录生命周期事件。 保持此SPI DRY实现的代价是堆栈跟踪上的几个要素,因为此类回调是用Java语言实现闭包的标准方法 。 但是保持此逻辑DRY可以保证,无论如何初始化Record,都将始终调用SPI。 没有(几乎)没有被遗忘的极端情况。
但是我们正在初始化记录…
CursorImpl
…CursorImpl, Cursor的实现。 这可能看起来很奇怪,因为jOOQ游标用于“延迟获取”,即用于从JDBC一对一地获取记录。
另一方面,来自此堆栈跟踪的SELECT
查询仅刷新单个UpdatableRecord ,即jOOQ等效于活动记录。 但是,仍然执行所有惰性获取逻辑,就像我们正在获取大型复杂数据集一样。 再次这样做是为了在获取数据时使事物保持干燥 。 当然,仅读取一条记录就可以节省大约6层堆栈跟踪,因为我们知道只有一条记录 。 但是同样,游标中的任何细微错误都可能会出现在某些测试用例中,即使是在诸如刷新记录的测试用例之类的远程测试用例中也是如此。
有些人可能会声称所有这些都是在浪费内存和CPU周期。 相反的可能性更大。 现代的JVM实现非常适合管理和垃圾回收短期对象和方法调用,而稍微增加的复杂性几乎不会给运行时环境带来任何额外的工作。
TL; DR:较长的堆叠痕迹
关于长堆栈跟踪是一件坏事的说法不一定正确。 如果良好地实现了复杂的框架,则会发生长堆栈跟踪。 复杂性将不可避免地导致“泄漏抽象” 。 但是,只有精心设计的复杂性才会导致较长的堆栈跟踪。
相反,短堆栈跟踪可能意味着两件事:
- 缺乏复杂性:框架很简单,功能很少。 这与Igor关于ActiveJDBC的主张相符,因为他将ActiveJDBC宣传为“简单框架”。
- 缺乏凝聚力和干性:该框架的编写不正确,可能覆盖了较差的测试范围和许多错误。
树数据结构
作为最后的说明,值得一提的是,不可避免地要进行长堆栈跟踪的另一种情况是, 使用visits遍历树结构/复合模式结构。 曾经调试过XPath或XSLT的任何人都将知道这些跟踪的深度。
翻译自: https://www.javacodegeeks.com/2013/11/deep-stack-traces-can-be-a-sign-for-good-code-quality.html
堆栈跟踪 堆栈跟踪