Spring Cloud 使用Sentinel代替Hystrix做为Feign的熔断器之Fallback统一处理

本文的思想建立在已经能初步使用Feign的基础之上,如果你还是零基础,建议先去补充一些Spring Cloud Feign +Hystrix的知识。Spring Cloud Feign默认使用Hystrix作为熔断器,随着Spring Cloud Alibaba家族日益壮大,Sentinel-0.9.0版本也支持Gateway了。今天我们来看看如何简化代码,使用Feign,如何用Sentinel代替Hystrix并简化代码,统一管理Fallback。

  • 问题来源:

Feign的使用场景如下:

@FeignClient(value = "user-service", fallbackFactory = UserFeignFallbackFactory.class)
public interface RemoteUserFeign {
	@GetMapping("/user/{id}")
	User findById(@PathVariable("id") int id);
}
@Component
public class UserFeignFallbackFactory implements FallbackFactory<RemoteUserFeign> {
	@Override
	public RemoteUserFeign create(Throwable cause) {
		RemoteUserFeignImpl feign = new RemoteUserFeignImpl();
		feign.setCause(cause);
		return feign;
	}
}
@Slf4j
public class RemoteUserFeignImpl implements RemoteUserFeign {
	@Setter
	private Throwable cause;
	@Override
	public User findById(int id) {
		log.error("feign 查询用户信息失败 id:{},throw:{}", id, cause);
		return null;
	}
}

我们使用Feign时,使用的@FeignClient注解每次都要为其设置fallbackFactory参数。导致项目中会多出很多冗余代码。那我们能不能有一个自己定制化的默认Fallback去处理这些相同的事情呢。

  • 问题定位

首先我们使用Sentinel,引入Alibaba-Sentinel相关包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>0.9.0.RELEASE</version>
</dependency>

直接打开源码来看,spring-cloud-starter-alibaba-sentinel这个包中与Feign相关的代码并不多,主要还是使用的Hystrix的思路去实现的SentinelFeign

接着经过Feign断点和源码的调试,定位核心代码部分,相关类和行数我都截图出来

从SentinelFeign源码中可以看出,当未设置fallbackFactory时,并未向SentinelInvocationHandler构造方法中传入FallbackFactory。那我们解决的思路就出来了:

  1. 编写公共FallbackFactory
  2. 改写SentinelFeign使得fallbackFactory为void.class时,我们传入自己的公共FallbackFactory实例
  • 问题解决

  1. 首先编写真正的Fallback处理器
    @Slf4j
    @AllArgsConstructor
    public class FunFeignFallback<T> implements MethodInterceptor {
    	private final Class<T> targetType;
    	private final String targetName;
    	private final Throwable cause;
    
    	@Nullable
    	@Override
    	public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    		String errorMessage = cause.getMessage();
    		log.error("FunFeignFallback:[{}.{}] serviceId:[{}] message:[{}]", targetType.getName(), method.getName(), targetName, errorMessage);
    		// 非 FeignException,直接返回
    		if (!(cause instanceof FeignException)) {
                //此处只是示例,具体可以返回带有业务错误数据的对象
    			return null;
    		}
    		FeignException exception = (FeignException) cause;
            //此处只是示例,具体可以返回带有业务错误数据的对象
    		return JsonUtils.toJson(exception.content());
    	}
    
    	@Override
    	public boolean equals(Object o) {
    		if (this == o) {
    			return true;
    		}
    		if (o == null || getClass() != o.getClass()) {
    			return false;
    		}
    		FunFeignFallback<?> that = (FunFeignFallback<?>) o;
    		return targetType.equals(that.targetType);
    	}
    
    	@Override
    	public int hashCode() {
    		return Objects.hash(targetType);
    	}
    }
  2. 然后编写FallbackFactory
    @AllArgsConstructor
    public class FunFallbackFactory<T> implements FallbackFactory<T> {
    	private final Target<T> target;
    
    	@Override
    	@SuppressWarnings("unchecked")
    	public T create(Throwable cause) {
    		final Class<T> targetType = target.type();
    		final String targetName = target.name();
    		Enhancer enhancer = new Enhancer();
    		enhancer.setSuperclass(targetType);
    		enhancer.setUseCache(true);
    		enhancer.setCallback(new FunFeignFallback<>(targetType, targetName, cause));
    		return (T) enhancer.create();
    	}
    }
  3. 重新实现SentinelFeign,由于没有对SentinelInvocationHandler的访问权限,我们得在自己项目里建立
    org.springframework.cloud.alibaba.sentinel.feign包并将新的SentinelFeign放在此包下。
    public class FunSentinelFeign {
    	public static FunSentinelFeign.Builder builder() {
    		return new FunSentinelFeign.Builder();
    	}
    	public static final class Builder extends Feign.Builder
    			implements ApplicationContextAware {
    		private Contract contract = new Contract.Default();
    		private ApplicationContext applicationContext;
    		private FeignContext feignContext;
    		@Override
    		public Feign.Builder invocationHandlerFactory(
    				InvocationHandlerFactory invocationHandlerFactory) {
    			throw new UnsupportedOperationException();
    		}
    		@Override
    		public FunSentinelFeign.Builder contract(Contract contract) {
    			this.contract = contract;
    			return this;
    		}
    		@Override
    		public Feign build() {
    			super.invocationHandlerFactory(new InvocationHandlerFactory() {
    				@Override
    				public InvocationHandler create(Target target,
    												Map<Method, MethodHandler> dispatch) {
    					Object feignClientFactoryBean = FunSentinelFeign.Builder.this.applicationContext
    							.getBean("&" + target.type().getName());
    
    					Class fallback = (Class) getFieldValue(feignClientFactoryBean,
    							"fallback");
    					Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean,
    							"fallbackFactory");
    					String name = (String) getFieldValue(feignClientFactoryBean, "name");
    
    					Object fallbackInstance;
    					FallbackFactory fallbackFactoryInstance;
    					// check fallback and fallbackFactory properties
    					if (void.class != fallback) {
    						fallbackInstance = getFromContext(name, "fallback", fallback,
    								target.type());
    						return new SentinelInvocationHandler(target, dispatch,
    								new FallbackFactory.Default(fallbackInstance));
    					}
    					if (void.class != fallbackFactory) {
    						fallbackFactoryInstance = (FallbackFactory) getFromContext(name,
    								"fallbackFactory", fallbackFactory,
    								FallbackFactory.class);
    						return new SentinelInvocationHandler(target, dispatch,
    								fallbackFactoryInstance);
    					}
    					// 默认的 fallbackFactory
    					FunFallbackFactory funFallbackFactory = new FunFallbackFactory(target);
    					return new SentinelInvocationHandler(target, dispatch, funFallbackFactory);
    				}
    
    				private Object getFromContext(String name, String type,
    											  Class fallbackType, Class targetType) {
    					Object fallbackInstance = feignContext.getInstance(name,
    							fallbackType);
    					if (fallbackInstance == null) {
    						throw new IllegalStateException(String.format(
    								"No %s instance of type %s found for feign client %s",
    								type, fallbackType, name));
    					}
    
    					if (!targetType.isAssignableFrom(fallbackType)) {
    						throw new IllegalStateException(String.format(
    								"Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
    								type, fallbackType, targetType, name));
    					}
    					return fallbackInstance;
    				}
    			});
    			super.contract(new SentinelContractHolder(contract));
    			return super.build();
    		}
    		private Object getFieldValue(Object instance, String fieldName) {
    			Field field = ReflectionUtils.findField(instance.getClass(), fieldName);
    			field.setAccessible(true);
    			try {
    				return field.get(instance);
    			} catch (IllegalAccessException e) {
    				// ignore
    			}
    			return null;
    		}
    		@Override
    		public void setApplicationContext(ApplicationContext applicationContext)
    				throws BeansException {
    			this.applicationContext = applicationContext;
    			feignContext = this.applicationContext.getBean(FeignContext.class);
    		}
    	}
    }
    
  4. 最后注入我们的SentinelFeign Bean
    @Configuration
    public class FunFeignFallbackConfiguration {
    	@Bean
    	@Scope("prototype")
    	@ConditionalOnClass({SphU.class, Feign.class})
    	@ConditionalOnProperty(name = "feign.sentinel.enabled")
    	@Primary
    	public Feign.Builder feignSentinelBuilder() {
    		return FunSentinelFeign.builder();
    	}
    }

记得使用sentinel的时候,需要在配置文件中配置feign.sentinel.enabled=true,注释掉feign.hystrix.enabled=true。

@FeignClient(value = "user-service")
public interface RemoteUserFeign {
	@GetMapping("/user/{id}")
	User findById(@PathVariable("id") int id);
}

到这里就大功告成了。后面写Feign只用写下面代码就可以了,至于一些特殊的,也可以自己定制fallbackFactory加在注解里。

猜你喜欢

转载自blog.csdn.net/ttzommed/article/details/90669320