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:
- Write a public FallbackFactory
- When rewriting SentinelFeign to make fallbackFactory void.class, we pass in our own public FallbackFactory instance
-
problem solved
- 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); } }
- 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(); } }
- 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); } } }
- 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.