Spring Cloud uses Sentinel instead of Hystrix as the unified processing of Fallback for Feign's fuse

The idea of ​​this article is based on the initial use of Feign. If you are still a zero foundation, it is recommended to add some knowledge of Spring Cloud Feign + Hystrix. Spring Cloud Feign uses Hystrix as a fuse by default. As the Spring Cloud Alibaba family grows stronger, Sentinel-0.9.0 version also supports Gateway. Today we take a look at how to simplify the code, use Feign, how to use Sentinel instead of Hystrix and simplify the code, unified management of Fallback.

  • Source of the problem:

The usage scenarios of Feign are as follows:

@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;
	}
}

When we use Feign, the @FeignClient annotation used must be set to fallbackFactory parameters every time. As a result, there will be a lot of redundant code in the project. So can we have a customized default Fallback to handle these same things?

  • identify the problem

First, we use Sentinel and introduce Alibaba-Sentinel related packages

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

Looking at the source code directly, there is not much code related to Feign in the spring-cloud-starter-alibaba-sentinel package. The main thing is SentinelFeign implemented by Hystrix.

Then after Feign breakpoints and source code debugging, I locate the core code part, and I take screenshots of related classes and line numbers.

It can be seen from the SentinelFeign source code that when fallbackFactory is not set, FallbackFactory is not passed into the SentinelInvocationHandler construction method. Then our solution comes out:

  1. Write a public FallbackFactory
  2. When rewriting SentinelFeign to make fallbackFactory void.class, we pass in our own public FallbackFactory instance
  • problem solved

  1. First write the real Fallback processor
    @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. Then write 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. Re-implement SentinelFeign. Since we don't have access to SentinelInvocationHandler, we have to create the
    org.springframework.cloud.alibaba.sentinel.feign package in our project and put the new SentinelFeign under this package.
    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. Finally inject our 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();
    	}
    }

     

Remember when using sentinel, you need to configure feign.sentinel.enabled=true in the configuration file and comment out feign.hystrix.enabled=true.

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

You're done here. To write Feign later, just write the following code. As for some special ones, you can also customize the fallbackFactory and add it in the annotations.

Guess you like

Origin blog.csdn.net/ttzommed/article/details/90669320