如何优雅处理存在依赖关系的多层嵌套try-with-resources语句
嵌套try-with-resources(MDC场景)优化方案
大量存在依赖关系的嵌套try-with-resources会导致代码层级过深、可读性差,典型的MDC埋点嵌套写法如下:
try (MDC.MDCCloseable test= MDC.putCloseable("test", test)) { try (MDC.MDCCloseable paw= MDC.putCloseable("paw", test.getPaw().toString())) { try (MDC.MDCCloseable tru = MDC.putCloseable("tru", paw.getTRU().toString())) { try (MDC.MDCCloseable mdcKey = MDC.putCloseable("rtu", paw.getKey())) { try (MDC.MDCCloseable rtu= MDC.putCloseable("rtu", "test")) { try (MDC.MDCCloseable ito= MDC.putCloseable("ito", rtu.getIto())) { ... } } try (MDC.MDCCloseable eht = MDC.putCloseable("eht", "eht")) { try (MDC.MDCCloseable epr= MDC.putCloseable("epr", eht.getEPR())) { } } try (MDC.MDCCloseable lkt = MDC.putCloseable("lkt", "lkt")) { try (MDC.MDCCloseable plo= MDC.putCloseable("plo", lkt.getPlo())) { } } } } } }
方案1:利用try-with-resources原生多资源声明(零额外依赖,推荐优先使用)
Java 7及以上版本的try-with-resources支持在同一个try块内声明多个资源,规则完全匹配嵌套写法的语义:
- 资源按代码书写顺序初始化
- 资源按书写的逆序关闭,和一层一层嵌套的执行逻辑完全一致
- 后面声明的资源初始化时,可以直接引用前面已经完成初始化的资源/对应业务值
对于原代码中外层4个线性依赖的MDC资源,完全不需要嵌套,直接合并到同一个try块即可;内层两两依赖的MDC对(rtu&ito、eht&epr、lkt&plo)也可以用同样方式合并,优化后代码如下:
// 注意:建议将外部业务变量重命名避免和MDCCloseable变量重名遮蔽,减少NPE风险 try ( MDC.MDCCloseable testMdc = MDC.putCloseable("test", testVal); MDC.MDCCloseable pawMdc = MDC.putCloseable("paw", testVal.getPaw().toString()); MDC.MDCCloseable truMdc = MDC.putCloseable("tru", testVal.getPaw().getTRU().toString()); MDC.MDCCloseable keyMdc = MDC.putCloseable("rtu", testVal.getPaw().getKey()) ) { // 平级MDC块1 try ( MDC.MDCCloseable rtuMdc = MDC.putCloseable("rtu", "test"); MDC.MDCCloseable itoMdc = MDC.putCloseable("ito", rtuMdc.getIto()) ) { // 业务逻辑... } // 平级MDC块2 try ( MDC.MDCCloseable ehtMdc = MDC.putCloseable("eht", "eht"); MDC.MDCCloseable eprMdc = MDC.putCloseable("epr", ehtMdc.getEPR()) ) { // 业务逻辑... } // 平级MDC块3 try ( MDC.MDCCloseable lktMdc = MDC.putCloseable("lkt", "lkt"); MDC.MDCCloseable ploMdc = MDC.putCloseable("plo", lktMdc.getPlo()) ) { // 业务逻辑... } }
优化后原来的6层嵌套直接缩减为2层,没有任何额外封装,语义和原代码100%一致。
注意:不要为了减少层级把生命周期不一致的资源强行合并到同一个try块。比如原代码中rtu、eht、lkt三个MDC值是独立生命周期,退出对应块就需要立刻移除,不能和外层的test、paw等长生命周期MDC值放在一起,否则会污染后续无关代码的日志上下文。
方案2:封装链式MDC作用域工具(适合大量MDC埋点、动态拼接MDC键值对的场景)
如果项目中这类MDC埋点非常多,甚至存在动态决定要放哪些MDC键的场景,可以封装一个实现AutoCloseable的作用域类,内部维护资源栈保证关闭顺序正确,支持链式调用:
public class MDCScope implements AutoCloseable { private final Deque<MDC.MDCCloseable> closeableStack = new ArrayDeque<>(); public static MDCScope create() { return new MDCScope(); } public MDCScope put(String key, String value) { closeableStack.push(MDC.putCloseable(key, value)); return this; } @Override public void close() { // 栈结构天然保证逆序关闭,和原生try-with-resources逻辑一致 while (!closeableStack.isEmpty()) { closeableStack.pop().close(); } } }
使用时和原生写法体验一致,对于动态拼接的场景比原生try-with-resources更灵活:
try (MDCScope scope = MDCScope.create() .put("test", testVal) .put("paw", testVal.getPaw().toString()) .put("tru", testVal.getPaw().getTRU().toString()) .put("rtu", testVal.getPaw().getKey())) { // 动态添加MDC值示例 if (needPrintIto) { try (MDCScope inner = MDCScope.create() .put("rtu", "test") .put("ito", itoVal)) { // 业务逻辑 } } // 其他平级逻辑... }
额外避坑点
原代码存在变量名遮蔽问题:MDCCloseable类型的变量名和外部业务对象变量名完全相同(比如都叫test、paw),虽然语法允许,但很容易在初始化资源时误引用还未完成初始化的MDCCloseable变量,触发空指针,建议命名时做明确区分。
内容的提问来源于stack exchange,提问作者Mariya Takova
相关产品推荐
相关产品推荐

