Bootstrap

Feign 异常传递

通过Feign交互,如果服务端发送异常,feign默认会将异常包装为自定义FeignException,这样我们就不能直接获取服务端抛出的异常类型和异常描述。下面我们就来使用对象序列化的形式,将服务端的异常(Exception对象)传递到客户端。

思路

实现服务端异常传递,需要满足以下特点

  1. 服务端既能支持http直连,也能支持公共Feign请求异常对象传递

    http直连:需要对异常进行包装,比如返回{"status":false,"message":"余额不足"} REST格式风格响应信息

    Feign客户端:服务端将异常进行序列化,客户端将异常反序列化

  2. 服务端的异常类型在客户端不存在,需要在服务端将异常转换为RunTimeException

    如服务端和客户端依赖不同的jar,会导致服务端的异常无法在客户端进行反序列化,导致客户端解析错误,最好的方式是将可能抛出的异常,在Feign 远程服务api接口声明中显示抛出,这样服务端和客户端的异常类型一致,在序列化时不会报错。

实现

  • 运行环境
    • feignorg.springframework.cloud:spring-cloud-starter-openfeign:2.2.9.RELEASE
    • springorg.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) {
    				//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();
    		}
    }
    
;