通过Feign交互,如果服务端发送异常,feign默认会将异常包装为自定义
FeignException
,这样我们就不能直接获取服务端抛出的异常类型和异常描述。下面我们就来使用对象序列化的形式,将服务端的异常(Exception对象)传递到客户端。
实现服务端异常传递,需要满足以下特点
服务端既能支持http直连,也能支持公共Feign请求异常对象传递
http直连:需要对异常进行包装,比如返回{"status":false,"message":"余额不足"}
REST格式风格响应信息
Feign客户端:服务端将异常进行序列化,客户端将异常反序列化
服务端的异常类型在客户端不存在,需要在服务端将异常转换为RunTimeException
如服务端和客户端依赖不同的jar
,会导致服务端的异常无法在客户端进行反序列化,导致客户端解析错误,最好的方式是将可能抛出的异常,在Feign 远程服务api接口声明中显示抛出,这样服务端和客户端的异常类型一致,在序列化时不会报错。
org.springframework.cloud:spring-cloud-starter-openfeign:2.2.9.RELEASE
org.springframework.boot:spring-boot-starter-parent:2.3.12.RELEASE
服务端自定义异常拦截器
为了兼容Http直连,使用Feign请求时,会在请求Heard中加标签入
RemoteConstant.Heard.ERROR_ENCODE=RemoteConstant.Heard.ERROR_ENCODE_SERIAL
来标记是Feign请求,并且将异常序列化,如果没有配置这个Heard,或者配置的``RemoteConstant.Heard.ERROR_ENCODE是其他值,代表异常是其他的返回形式(如:
{“status”:false,“message”:“余额不足”}),本例中是将异常异常信息直接输出为:
异常类型:异常描述`
publicclassExceptionHandleimplementsHandlerExceptionResolver,Ordered{privatestaticfinalLogger LOGGER=LoggerFactory.getLogger(ExceptionHandle.class);privateint order=Ordered.LOWEST_PRECEDENCE;@OverridepublicModelAndViewresolveException(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:newRuntimeException(ExceptionUtils.getStackTrace(ex));try{IOUtils.write(SerializableUtil.serialize(exception), response.getOutputStream());}catch(IOException e){//ignore}returnnewModelAndView();}try{String errorMsg=String.format("%s : %s", ex.getClass().getName(), ex.getMessage());IOUtils.write(errorMsg, response.getOutputStream());}catch(IOException e){//ignore}returnnewModelAndView();}@OverridepublicintgetOrder(){return order;}publicvoidsetOrder(int order){this.order= order;}/** * 是否是可序列化异常 * * @param exception * @param methodHandle * @return */privatebooleannormalException(Exception exception,HandlerMethod methodHandle){// Checked Exceptionif(!(exceptioninstanceofRuntimeException)){returntrue;}// 方法声明中的异常Method method= methodHandle.getMethod();for(Class<?> exceptionClass: method.getExceptionTypes()){if(exception.getClass().equals(exceptionClass)){returntrue;}}// 如果异常类和接口类在同一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)){returntrue;}}// jdk exceptionString className= exception.getClass().getName();if(className.startsWith("java.")|| className.startsWith("javax.")){returntrue;}// customer exceptionif(className.startsWith("com.zto.zbase.common")|| className.startsWith("com.zto.zbase.manager")){returntrue;}returnfalse;}publicstaticStringgetCodeBase(Class<?> cls){if(cls==null){returnnull;}ProtectionDomain domain= cls.getProtectionDomain();if(domain==null){returnnull;}CodeSource source= domain.getCodeSource();if(source==null){returnnull;}URL location= source.getLocation();if(location==null){returnnull;}return location.getFile();}}
设置Spring异常拦截器
@ConfigurationpublicclassExceptionInterceptorimplementsWebMvcConfigurer{@OverridepublicvoidextendHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers){ExceptionHandle exceptionHandle=newExceptionHandle(); exceptionHandle.setOrder(1); resolvers.add(exceptionHandle);}}
java Exception序列化,反序列化工具SerializableUtil.java
publicclassSerializableUtil{publicstaticbyte[]serialize(Exception exception)throwsIOException{try(ByteArrayOutputStream byteArrayOutputStream=newByteArrayOutputStream();ObjectOutputStream oo=newObjectOutputStream(byteArrayOutputStream);){ oo.writeObject(exception); oo.flush();return byteArrayOutputStream.toByteArray();}}publicstaticExceptiondeserialize(byte[] bytes)throwsIOException,ClassNotFoundException{try(ByteArrayInputStream byteArrayInputStream=newByteArrayInputStream(bytes);ObjectInputStream ois=newObjectInputStream(byteArrayInputStream);){return(Exception) ois.readObject();}}}
自定义Feign请求过滤器
将Feign请求Heard中,设置当前异常序列化
publicclassExceptionRequestInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplate template){//异常序列化 template.header(RemoteConstant.Heard.ERROR_ENCODE,RemoteConstant.Heard.ERROR_ENCODE_SERIAL);}}
Feign异常解析
只会对
RemoteConstant.Heard.ERROR_ENCODE=RemoteConstant.Heard.ERROR_ENCODE_SERIAL
标记的异常响应反序列化
publicclassFeignExceptionErrorDecoderimplementsErrorDecoder{@OverridepublicExceptiondecode(String methodKey,Response response){if(response.body()!=null){Collection<String> errorDecodes= response.headers().get(RemoteConstant.Heard.ERROR_ENCODE);if(CollectionUtils.isEmpty(errorDecodes)){returnerrorStatus(methodKey, response);}String decodeType= errorDecodes.toArray()[0].toString();if(ERROR_ENCODE_SERIAL.equals(decodeType)&&HttpStatus.INTERNAL_SERVER_ERROR.value()== response.status()){try(ByteArrayOutputStream byteArrayOutputStream=newByteArrayOutputStream();InputStream inputStream= response.body().asInputStream();){IOUtils.copy(inputStream, byteArrayOutputStream);try{returnSerializableUtil.deserialize(byteArrayOutputStream.toByteArray());}catch(ClassNotFoundException e){returnnewRuntimeException(byteArrayOutputStream.toString());}}catch(IOException e){return e;}}}returnerrorStatus(methodKey, response);}}
设置Feign配置
@ConfigurationpublicclassFeignConfiguration{@BeanpublicRequestInterceptorexceptionRequestInterceptor(){returnnewExceptionRequestInterceptor();}@BeanpublicErrorDecoderfeignErrorDecoder(){returnnewFeignExceptionErrorDecoder();}}