本文的思想建立在已经能初步使用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 {
@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相关包
org.springframework.cloud
spring-cloud-starter-alibaba-sentinel
0.9.0.RELEASE
直接打开源码来看,spring-cloud-starter-alibaba-sentinel这个包中与Feign相关的代码并不多,主要还是使用的Hystrix的思路去实现的SentinelFeign
接着经过Feign断点和源码的调试,定位核心代码部分,相关类和行数我都截图出来
从SentinelFeign源码中可以看出,当未设置fallbackFactory时,并未向SentinelInvocationHandler构造方法中传入FallbackFactory。那我们解决的思路就出来了:
@Slf4j
@AllArgsConstructor
public class FunFeignFallback implements MethodInterceptor {
private final Class 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);
}
}
@AllArgsConstructor
public class FunFallbackFactory implements FallbackFactory {
private final Target target;
@Override
@SuppressWarnings("unchecked")
public T create(Throwable cause) {
final Class 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();
}
}
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 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);
}
}
}
@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加在注解里。