通过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
- feign:
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) { //ignore } return new ModelAndView(); } try { String errorMsg = String.format("%s : %s", ex.getClass().getName(), ex.getMessage()); IOUtils.write(errorMsg, response.getOutputStream()); } catch (IOException e) { //ignore } return new ModelAndView(); } @Override public int getOrder() { return order; } public void setOrder(int order) { this.order = order; } /** * 是否是可序列化异常 * * @param exception * @param methodHandle * @return */ private boolean normalException(Exception exception, HandlerMethod methodHandle) { // Checked Exception if (!(exception instanceof RuntimeException)) { return true; } // 方法声明中的异常 Method method = methodHandle.getMethod(); for (Class<?> exceptionClass : method.getExceptionTypes()) { if (exception.getClass().equals(exceptionClass)) { return true; } } // 如果异常类和接口类在同一jar文件中,则直接抛出 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; } } // jdk exception String className = exception.getClass().getName(); if (className.startsWith("java.") || className.startsWith("javax.")) { return true; } // customer exception 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(); } }