问题重现
下面一个SpringMVC的异常通知类
@ControllerAdvice
public class CommonExceptionHandler {
@ExceptionHandler(HealthException.class)
public ResponseEntity<ExceptionResult> handlerLyException(HealthException e) {
return ResponseEntity.status(e.getExceptionEnum().getCode())
.body(new ExceptionResult(e.getExceptionEnum()));
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ExceptionResult> handlerRuntimeException(RuntimeException e) {
e.printStackTrace();
return ResponseEntity.status(500)
.body(new ExceptionResult(500, getHeadMessage(e.getMessage())));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ExceptionResult> handlerException(Exception e){
return ResponseEntity.status(500).body(new ExceptionResult(500, getHeadMessage(e.getMessage())));
}
}
如果在消费方通过RPC调用提供方时,此时提供方抛出了一个自定义异常HealthException
throw new HealthException(ExceptionEnum.INVALID_REQUEST);
由于提供方和消费方是基于RPC通信,所以消费方是不可能直接捕获到这个自定义异常的,这个过程是由RPC框架实现的。
此时消费方输出的日志是
java.lang.RuntimeException: com.l1yp.common.exception.HealthException
可以看出异常类型是java.lang.RuntimeException而不是com.liangyp.common.exception.HealthException,网上一顿搜索最后发现是一个ExceptionFilter用RuntimeException把HealthException包裹起来的。
下面是ExceptionFilter的代码
// 异常过滤器的逻辑
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
// 调用service
Result result = invoker.invoke(invocation);
// 内部抛出异常并且不是dubbo的service
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
// 获取异常对象 此时是 HealthException 的实例
Throwable exception = result.getException();
// 如果是checked exception(编译期异常)就直接抛出,比如IOException
// directly throw if it's checked exception
if (!(exception instanceof RuntimeException)
&& (exception instanceof Exception)) {
return result;
}
// 如果接口签名上有声明可能抛出这个异常,直接抛出
// directly throw if the exception appears in the signature
try {
Method method = invoker.getInterface().getMethod(
invocation.getMethodName(),
invocation.getParameterTypes());
Class<?>[] exceptionClassses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClassses) {
if (exception.getClass().equals(exceptionClass)) {
return result;
}
}
} catch (NoSuchMethodException e) {
return result;
}
// 如果签名没有 就输出个日志
// for the exception not found in method's signature, print ERROR message in server's log.
logger.error("Got unchecked and undeclared exception which called by "
+ RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName()
+ ", method: " + invocation.getMethodName()
+ ", exception: " + exception.getClass().getName()
+ ": " + exception.getMessage(), exception);
// 如果自定义异常类和service接口在同一个jar包,直接抛出
// directly throw if exception class and interface class are in the same jar file.
String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
if (serviceFile == null
|| exceptionFile == null
|| serviceFile.equals(exceptionFile)) {
return result;
}
// 如果是JDK的异常,直接抛出
// directly throw if it's JDK exception
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return result;
}
// 如果是dubbo的异常,直接抛出
// directly throw if it's dubbo exception
if (exception instanceof RpcException) {
return result;
}
// 否则用RuntimeException包裹并抛出。:) 直接调用toString拉
// otherwise, wrap with RuntimeException and throw back to the client
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
} catch (Throwable e) {
logger.warn("Fail to ExceptionFilter when called by "
+ RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName()
+ ", method: " + invocation.getMethodName()
+ ", exception: " + e.getClass().getName()
+ ": " + e.getMessage(), e);
return result;
}
}
return result;
} catch (RuntimeException e) {
logger.error("Got unchecked and undeclared exception which called by "
+ RpcContext.getContext().getRemoteHost()
+ ". service: " + invoker.getInterface().getName()
+ ", method: " + invocation.getMethodName()
+ ", exception: " + e.getClass().getName()
+ ": " + e.getMessage(), e);
throw e;
}
}
下面分析为什么会有上面的条件:
提前剧透,dubbo为了避免服务消费者不能顺滑的反序列化服务提供者的自定义异常类,很多库的严重bug都是因为序列化问题引起的,比如Jackson前些天...
编译期异常可以直接抛出
方法签名上有声明可能抛出异常
自定义异常类和service接口在同一jar包
JDK的异常(前缀是java./javax.)
dubbo的异常
针对以上几点给出以下几种解决方案:
自定义异常一般都是继承RuntimeException
service接口上每一个函数都添加throws HealthException(不科学的方案)
将HealthException放到和service接口的jar包
将HealthException声明在javax的包下,因为ClassLoader只是不允许java.开头
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")
&& this != getBuiltinPlatformClassLoader()) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) {
checkCerts(name, pd.getCodeSource());
}
return pd;
}
添加自己的一个CustomExceptionFilter并把dubbo自带的ExceptionFilter干掉
ExceptionFilter的代码复制到自己的CustomExceptionFilter,稍微修改// 如果是checked exception(编译期异常)就直接抛出,比如IOException
// directly throw if it's checked exception
if (!(exception instanceof RuntimeException)
&& (exception instanceof Exception)) {
return result;
}
// 自定义异常直接返回
if (exception instanceof HealthException){
return result;
}
在resource下新增文件META-INF\dubbo\com.alibaba.dubbo.rpc.Filter
customExceptionFilter=com.liangyp.common.filter.CustomExceptionFilter
dubbo的ProviderConfig
@Bean // #1
public ProviderConfig providerConfig() {
ProviderConfig providerConfig = new ProviderConfig();
providerConfig.setTimeout(1000);
providerConfig.setFilter("customExceptionFilter,-exception");
return providerConfig;
}
至于dubbo的ExceptionFilter的filterName从何得知?
其实很简单,到dubbo的jar包一翻就看到了,
META-INF\dubbo\internal\com.alibaba.dubbo.rpc.Filtermonitor=com.alibaba.dubbo.monitor.support.MonitorFilter
validation=com.alibaba.dubbo.validation.filter.ValidationFilter
cache=com.alibaba.dubbo.cache.filter.CacheFilter
trace=com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter
echo=com.alibaba.dubbo.rpc.filter.EchoFilter
generic=com.alibaba.dubbo.rpc.filter.GenericFilter
genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter
token=com.alibaba.dubbo.rpc.filter.TokenFilter
accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter
activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter
context=com.alibaba.dubbo.rpc.filter.ContextFilter
consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter
exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter
executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter
compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter
timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter