子类未实现父类方法时,如何在日志中打印调用的子类?
问题场景
先看示例代码:
public class A { public void myMethod() throws Exception { } } public class B extends A { }
当调用子类B的实例的myMethod()方法并抛出异常时,栈追踪中不会包含B.myMethod()——因为B没有重写该方法,栈帧只会显示A.myMethod()。
现在有1000个继承自A的子类,其中某个子类调用myMethod()时抛出异常,无法定位具体是哪个子类;而给每个子类添加仅调用super.myMethod()的实现成本过高,需要更高效的解决办法。
可行解决方案
1. 父类方法内直接记录当前实例的类信息
在父类A的myMethod()开头,通过this.getClass().getName()获取当前调用方法的实际子类名称,将其输出到日志或控制台。这样即使抛出异常,也能提前知道是哪个子类的实例发起的调用:
public class A { public void myMethod() throws Exception { // 记录当前调用的子类名称 System.err.println("调用myMethod的子类:" + this.getClass().getName()); // 原方法逻辑 // ... throw new Exception("业务异常"); } }
这个方案零侵入子类,只需要修改父类代码,快速见效。
2. 异常消息中嵌入子类标识
如果不想额外输出日志,可以在父类方法里捕获(或构造)异常时,把当前子类的名称拼入异常消息中,这样查看异常信息时就能直接定位子类:
public class A { public void myMethod() throws Exception { try { // 原方法逻辑 // ... throw new Exception("原异常内容"); } catch (Exception e) { // 包装异常,加入子类信息 throw new Exception("子类[" + this.getClass().getName() + "]调用myMethod出错: " + e.getMessage(), e); } } }
这种方式不会改变栈帧结构,但异常消息里直接包含关键信息,排查时一目了然。
3. 编译期自动生成子类重写方法(AspectJ)
使用AspectJ在编译阶段为所有继承A的子类自动生成myMethod()的重写实现(方法体仅调用super.myMethod()),无需手动修改任何子类代码。
示例AspectJ切面代码:
public aspect SubclassMethodInjector { // 匹配所有继承A的子类,且不是A本身 declare parents : * extends A implements MyMethodOverride; // 为MyMethodOverride接口实现myMethod方法,自动继承给所有子类 public void MyMethodOverride.myMethod() throws Exception { super.myMethod(); } }
配置AspectJ编译插件后,编译时会自动给所有A的子类注入重写方法,此时异常栈中就会显示对应子类的myMethod()栈帧,完美解决定位问题。
4. 动态代理统一处理(CGLIB)
如果项目中所有子类实例都是通过工厂创建的,可以使用CGLIB为每个子类生成动态代理类,代理类重写myMethod()方法并调用父类实现。这样调用时栈帧会包含代理类的方法,而代理类名称会保留原子类的标识,同时也可以在代理方法中记录子类信息。
示例代理工厂代码:
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class SubclassProxyFactory { @SuppressWarnings("unchecked") public static <T extends A> T createProxy(Class<T> subclassClass) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(subclassClass); enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> { // 记录子类信息 if ("myMethod".equals(method.getName())) { System.err.println("代理调用子类:" + obj.getClass().getSuperclass().getName()); } // 调用父类方法 return proxy.invokeSuper(obj, args); }); return (T) enhancer.create(); } }
使用时通过SubclassProxyFactory.createProxy(B.class)获取子类实例,调用myMethod()时不仅能记录子类信息,栈帧也会包含代理类的方法,辅助定位。
总结
手动给每个子类添加重写方法是最直接但成本极高的方案,上述几种方法都能避免手动修改子类,其中父类内记录子类信息的方案实现最简单,AspectJ编译期注入的方案最彻底(完全模拟手动重写的效果),可以根据项目实际情况选择。
内容的提问来源于stack exchange,提问作者Jonas

