Tomcat会话复制序列化:如何展示对象引用图及日志记录
针对你遇到的Tomcat会话复制时的NotSerializableException,要展示完整的对象引用链(比如会话中的对象A -> 对象B -> ... -> ClassFoo),以及追踪每个writeObject()处理的对象类型,有以下几种可行方案:
一、获取序列化对象引用链
1. 自定义ObjectOutputStream扩展
继承JDK的ObjectOutputStream,重写writeObject0方法,在序列化每个对象时记录其类型和调用栈,当抛出NotSerializableException时,就能回溯出完整的引用路径。示例代码:
import java.io.IOException; import java.io.ObjectOutputStream; import java.io.OutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LoggingObjectOutputStream extends ObjectOutputStream { private static final Logger log = LoggerFactory.getLogger(LoggingObjectOutputStream.class); public LoggingObjectOutputStream(OutputStream out) throws IOException { super(out); } @Override protected void writeObject0(Object obj, boolean unshared) throws IOException { log.debug("正在序列化对象: {}", obj.getClass().getName()); try { super.writeObject0(obj, unshared); } catch (NotSerializableException e) { log.error("序列化失败,当前对象: {}", obj.getClass().getName(), e); throw e; } } }
在Tomcat会话复制的逻辑中替换默认的ObjectOutputStream为这个自定义实现,就能拿到每个序列化对象的日志,进而梳理出引用链。
2. 开启JVM序列化调试日志
添加JVM启动参数:
-Dsun.io.serialization.extendedDebugInfo=true
开启后,NotSerializableException的栈追踪会附带额外信息,比如当前正在序列化的对象字段名称,结合栈帧里的writeOrdinaryObject、defaultWriteFields等方法,就能倒推哪个对象的哪个字段引用了未序列化的ClassFoo。
3. IDE调试追踪
在IDEA或Eclipse中,在java.io.ObjectOutputStream.writeObject0方法处设置断点,断点条件设为obj instanceof com.example.ClassFoo或者当抛出NotSerializableException时暂停。此时查看栈帧的局部变量,比如每个栈帧里的obj参数,就能一步步往上找到引用ClassFoo的上层对象,直到会话中的根对象。
二、Java序列化的日志记录方案
- 自定义日志工具:用上面的
LoggingObjectOutputStream,结合日志框架(如SLF4J),可以记录所有被序列化的对象类型、字段信息,甚至引用关系。 - 字节码增强动态日志:用ASM、ByteBuddy等字节码工具,动态修改
ObjectOutputStream的方法,插入日志逻辑,无需修改业务代码就能实现全局序列化日志。 - Tomcat专属日志:将Tomcat的
org.apache.catalina.ha.session.DeltaManager日志级别设为DEBUG,会输出会话中哪些属性被序列化、哪些发生变更,能缩小排查范围,辅助定位问题对象。
你提供的报错栈追踪:
java.io.NotSerializableException: com.example.ClassFoo at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1175) at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1543) at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1500) at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1423) at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1169) at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1543) at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1500) at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1423) at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1169) at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1543) at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1500) at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1423) at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1169) at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:345) at org.apache.catalina.ha.session.DeltaRequest$AttributeInfo.writeExternal(DeltaRequest.java:438) at org.apache.catalina.ha.session.DeltaRequest.writeExternal(DeltaRequest.java:326) at org.apache.catalina.ha.session.DeltaRequest.serialize(DeltaRequest.java:340) at org.apache.catalina.ha.session.DeltaSession.getDiff(DeltaSession.java:168) at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:1046) at org.apache.catalina.ha.session.DeltaManager.requestCompleted(DeltaManager.java:1014) at org.apache.catalina.ha.tcp.ReplicationValve.send(ReplicationValve.java:527) at org.apache.catalina.ha.tcp.ReplicationValve.sendMessage(ReplicationValve.java:515) at org.apache.catalina.ha.tcp.ReplicationValve.sendSessionReplicationMessage(ReplicationValve.java:497) at org.apache.catalina.ha.tcp.ReplicationValve.sendReplicationMessage(ReplicationValve.java:409) at org.apache.catalina.ha.tcp.ReplicationValve.invoke(ReplicationValve.java:340)
内容的提问来源于stack exchange,提问作者eastwater

