通过Feign交互,如果服务端发送异常,feign默认会将异常包装为自定义FeignException ,这样我们就不能直接获取服务端抛出的异常类型和异常描述。下面我们就来使用对象序列化的形式,将服务端的异常(Exception对象)传递到客户端。
思路
实现服务端异常传递,需要满足以下特点
-
服务端既能支持http直连,也能支持公共Feign请求异常对象传递 http直连:需要对异常进行包装,比如返回{"status":false,"message":"余额不足"} REST格式风格响应信息 Feign客户端:服务端将异常进行序列化,客户端将异常反序列化 -
服务端的异常类型在客户端不存在,需要在服务端将异常转换为RunTimeException 如服务端和客户端依赖不同的jar ,会导致服务端的异常无法在客户端进行反序列化,导致客户端解析错误,最好的方式是将可能抛出的异常,在Feign 远程服务api接口声明中显示抛出,这样服务端和客户端的异常类型一致,在序列化时不会报错。
实现
- 运行环境
- feign:
org.springframework.cloud:spring-cloud-starter-openfeign:2.2.9.RELEASE - spring:
org.springframework.boot:spring-boot-starter-parent:2.3.12.RELEASE
1. 服务端
-
服务端自定义异常拦截器
为了兼容Http直连,使用Feign请求时,会在请求Heard中加标签入RemoteConstant.Heard.ERROR_ENCODE=RemoteConstant.Heard.ERROR_ENCODE_SERIAL 来标记是Feign请求,并且将异常序列化,如果没有配置这个Heard,或者配置的``RemoteConstant.Heard.ERROR_ENCODE是其他值,代表异常是其他的返回形式(如: {“status”:false,“message”:“余额不足”}),本例中是将异常异常信息直接输出为: 异常类型:异常描述`
public class ExceptionHandle implements HandlerExceptionResolver, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionHandle.class);
private int order = Ordered.LOWEST_PRECEDENCE;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
LOGGER.error("Request error", ex);
String errorEncode = request.getHeader(RemoteConstant.Heard.ERROR_ENCODE);
if (RemoteConstant.Heard.ERROR_ENCODE_SERIAL.equals(errorEncode)) {
response.addHeader(RemoteConstant.Heard.ERROR_ENCODE, RemoteConstant.Heard.ERROR_ENCODE_SERIAL);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
HandlerMethod method = (HandlerMethod) handler;
boolean normalException = normalException(ex, method);
Exception exception = normalException ? ex : new RuntimeException(ExceptionUtils.getStackTrace(ex));
try {
IOUtils.write(SerializableUtil.serialize(exception), response.getOutputStream());
} catch (IOException e) {
}
return new ModelAndView();
}
try {
String errorMsg = String.format("%s : %s", ex.getClass().getName(), ex.getMessage());
IOUtils.write(errorMsg, response.getOutputStream());
} catch (IOException e) {
}
return new ModelAndView();
}
@Override
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
private boolean normalException(Exception exception, HandlerMethod methodHandle) {
if (!(exception instanceof RuntimeException)) {
return true;
}
Method method = methodHandle.getMethod();
for (Class<?> exceptionClass : method.getExceptionTypes()) {
if (exception.getClass().equals(exceptionClass)) {
return true;
}
}
Class<?>[] interfaces = method.getDeclaringClass().getInterfaces();
for (Class<?> interfaceClazz : interfaces) {
RemoteClient remoteClient = interfaceClazz.getDeclaredAnnotation(RemoteClient.class);
if (null == remoteClient) {
continue;
}
String serviceFile = getCodeBase(interfaceClazz);
String exceptionFile = getCodeBase(exception.getClass());
if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
return true;
}
}
String className = exception.getClass().getName();
if (className.startsWith("java.") || className.startsWith("javax.")) {
return true;
}
if (className.startsWith("com.zto.zbase.common") || className.startsWith("com.zto.zbase.manager")) {
return true;
}
return false;
}
public static String getCodeBase(Class<?> cls) {
if (cls == null) {
return null;
}
ProtectionDomain domain = cls.getProtectionDomain();
if (domain == null) {
return null;
}
CodeSource source = domain.getCodeSource();
if (source == null) {
return null;
}
URL location = source.getLocation();
if (location == null) {
return null;
}
return location.getFile();
}
}
-
设置Spring异常拦截器 @Configuration
public class ExceptionInterceptor implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
ExceptionHandle exceptionHandle = new ExceptionHandle();
exceptionHandle.setOrder(1);
resolvers.add(exceptionHandle);
}
}
-
java Exception序列化,反序列化工具 SerializableUtil.java public class SerializableUtil {
public static byte[] serialize(Exception exception) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(byteArrayOutputStream);) {
oo.writeObject(exception);
oo.flush();
return byteArrayOutputStream.toByteArray();
}
}
public static Exception deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(byteArrayInputStream);) {
return (Exception) ois.readObject();
}
}
}
2. 客户端
-
自定义Feign请求过滤器
将Feign请求Heard中,设置当前异常序列化
public class ExceptionRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header(RemoteConstant.Heard.ERROR_ENCODE, RemoteConstant.Heard.ERROR_ENCODE_SERIAL);
}
}
-
Feign异常解析
只会对RemoteConstant.Heard.ERROR_ENCODE=RemoteConstant.Heard.ERROR_ENCODE_SERIAL 标记的异常响应反序列化
public class FeignExceptionErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.body() != null) {
Collection<String> errorDecodes = response.headers().get(RemoteConstant.Heard.ERROR_ENCODE);
if (CollectionUtils.isEmpty(errorDecodes)) {
return errorStatus(methodKey, response);
}
String decodeType = errorDecodes.toArray()[0].toString();
if (ERROR_ENCODE_SERIAL.equals(decodeType) && HttpStatus.INTERNAL_SERVER_ERROR.value() == response.status()) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
InputStream inputStream = response.body().asInputStream();) {
IOUtils.copy(inputStream, byteArrayOutputStream);
try {
return SerializableUtil.deserialize(byteArrayOutputStream.toByteArray());
} catch (ClassNotFoundException e) {
return new RuntimeException(byteArrayOutputStream.toString());
}
} catch (IOException e) {
return e;
}
}
}
return errorStatus(methodKey, response);
}
}
-
设置Feign配置 @Configuration
public class FeignConfiguration {
@Bean
public RequestInterceptor exceptionRequestInterceptor() {
return new ExceptionRequestInterceptor();
}
@Bean
public ErrorDecoder feignErrorDecoder() {
return new FeignExceptionErrorDecoder();
}
}
|