在使用OpenFeign时候总是要写很多Fallback,此文章可以通过扩展实现一个统一的降级,规范返回格式;
OpenFeign+Sentinel统一降级
OpenFeign使用方式及问题
见上一篇文章@FeignClient中的fallbackFactory,fallback不起作用的正确使用示例
实现统一降级
原理:通过自定义实现Feign.Builder,启动时候扫描所有带有@FeignClient注解的类在生成其代理类的同时配置fallbackFactory或fallback,为其注入返回参数;
- 重写com.alibaba.cloud.sentinel.feign.SentinelFeign,使其支持降级注入
package com.yangxc.feign.util;
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import feign.Contract;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.hystrix.FallbackFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author yangxc
* @version 1.0.0
* @ClassName MySentinelFeign.java
* @Description TODO
* @createTime 2022年11月08日 16:00:00
*/
public final class MySentinelFeign {
private MySentinelFeign() {
}
public static MySentinelFeign.Builder builder() {
return new MySentinelFeign.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 MySentinelFeign.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) {
// 查找所有标记FeignClient
FeignClient feignClient = AnnotationUtils.findAnnotation(target.type(), FeignClient.class);
Class fallback = feignClient.fallback();
Class fallbackFactory = feignClient.fallbackFactory();
String beanName = feignClient.contextId();
if (!StringUtils.hasText(beanName)) {
beanName = feignClient.name();
}
Object fallbackInstance;
FallbackFactory fallbackFactoryInstance;
// check fallback and fallbackFactory properties
if (void.class != fallback) {
fallbackInstance = getFromContext(beanName, "fallback", fallback, target.type());
return new MySentinelInvocationHandler(target, dispatch,
new FallbackFactory.Default(fallbackInstance));
}
if (void.class != fallbackFactory) {
fallbackFactoryInstance = (FallbackFactory) getFromContext(beanName, "fallbackFactory",
fallbackFactory, FallbackFactory.class);
return new MySentinelInvocationHandler(target, dispatch, fallbackFactoryInstance);
}
return new MySentinelInvocationHandler(target, dispatch);
}
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();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
feignContext = this.applicationContext.getBean(FeignContext.class);
}
}
}
- 重写为标有@FeignClient注解的接口生成代理类的类(com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler)
package com.yangxc.feign.util;
import com.alibaba.cloud.sentinel.feign.SentinelContractHolder;
import com.alibaba.cloud.sentinel.feign.SentinelInvocationHandler;
import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.Tracer;
import com.alibaba.csp.sentinel.context.ContextUtil;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.MethodMetadata;
import feign.Target;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.LinkedHashMap;
import java.util.Map;
import static feign.Util.checkNotNull;
/**
* @author yangxc
* @version 1.0.0
* @ClassName MySentinelInvocationHandler.java
* @Description TODO
* @createTime 2022年11月08日 16:07:00
*/
@Slf4j
public class MySentinelInvocationHandler implements InvocationHandler {
public static final String EQUALS = "equals";
public static final String HASH_CODE = "hashCode";
public static final String TO_STRING = "toString";
private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
private FallbackFactory fallbackFactory;
private Map<Method, Method> fallbackMethodMap;
MySentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
FallbackFactory fallbackFactory) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackFactory = fallbackFactory;
this.fallbackMethodMap = toFallbackMethod(dispatch);
}
MySentinelInvocationHandler(Target<?> target, Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
}
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
if (EQUALS.equals(method.getName())) {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return equals(otherHandler);
}
catch (IllegalArgumentException e) {
return false;
}
}
else if (HASH_CODE.equals(method.getName())) {
return hashCode();
}
else if (TO_STRING.equals(method.getName())) {
return toString();
}
Object result;
InvocationHandlerFactory.MethodHandler methodHandler = this.dispatch.get(method);
// only handle by HardCodedTarget
if (target instanceof Target.HardCodedTarget) {
Target.HardCodedTarget hardCodedTarget = (Target.HardCodedTarget) target;
MethodMetadata methodMetadata = SentinelContractHolder.METADATA_MAP
.get(hardCodedTarget.type().getName() + Feign.configKey(hardCodedTarget.type(), method));
// resource default is HttpMethod:protocol://url
if (methodMetadata == null) {
result = methodHandler.invoke(args);
}
else {
String resourceName = methodMetadata.template().method().toUpperCase() + ":" + hardCodedTarget.url()
+ methodMetadata.template().path();
Entry entry = null;
try {
ContextUtil.enter(resourceName);
entry = SphU.entry(resourceName, EntryType.OUT, 1, args);
result = methodHandler.invoke(args);
}
catch (Throwable ex) {
// fallback handle
if (!BlockException.isBlockException(ex)) {
Tracer.trace(ex);
}
if (fallbackFactory != null) {
try {
Object fallbackResult = fallbackMethodMap.get(method).invoke(fallbackFactory.create(ex),
args);
return fallbackResult;
}
catch (IllegalAccessException e) {
// shouldn't happen as method is public due to being an
// interface
throw new AssertionError(e);
}
catch (InvocationTargetException e) {
throw new AssertionError(e.getCause());
}
}
else {
// 注入重点逻辑
//如果未自定义过sentinel限流后的返回值类型,此处需要修改为sentinel默认的返回类型,详情见com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler
if (R.class == method.getReturnType()) {
log.error("feign 服务间调用异常", ex);
return R.failed(ex.getLocalizedMessage());
}
else {
throw ex;
}
}
}
finally {
if (entry != null) {
entry.exit(1, args);
}
ContextUtil.exit();
}
}
}
else {
// other target type using default strategy
result = methodHandler.invoke(args);
}
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SentinelInvocationHandler) {
MySentinelInvocationHandler other = (MySentinelInvocationHandler) obj;
return target.equals(other.target);
}
return false;
}
@Override
public int hashCode() {
return target.hashCode();
}
@Override
public String toString() {
return target.toString();
}
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
Map<Method, Method> result = new LinkedHashMap<>();
for (Method method : dispatch.keySet()) {
method.setAccessible(true);
result.put(method, method);
}
return result;
}
}
- 修改sentinel限流后的返回值(com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler)
package com.sunacwy.frame.common.sentinel.handle;
import cn.hutool.http.ContentType;
import cn.hutool.json.JSONUtil;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.sunacwy.frame.common.core.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
public class MyUrlBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
log.error("sentinel 降级 资源名称{}", e.getRule().getResource(), e);
response.setContentType(ContentType.JSON.toString());
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().print(JSONUtil.toJsonStr(R.failed(e.getMessage())));
}
}
- 自定义的返回值
package com.yangxc.feign.util;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @author yangxc
* @version 1.0.0
* @ClassName R.java
* @Description TODO
* @createTime 2022年11月08日 16:09:00
*/
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class R<T> implements Serializable {
private static final long serialVersionUID = 1L;
@Getter
@Setter
private int code;
@Getter
@Setter
private String msg;
@Getter
@Setter
private T data;
public static <T> R<T> ok() {
return restResult(null, 0, null);
}
public static <T> R<T> ok(T data) {
return restResult(data, 0, null);
}
public static <T> R<T> ok(T data, String msg) {
return restResult(data, 0, msg);
}
public static <T> R<T> failed() {
return restResult(null, -1, null);
}
public static <T> R<T> failed(String msg) {
return restResult(null, -1, msg);
}
public static <T> R<T> failed(T data) {
return restResult(data, -1, null);
}
public static <T> R<T> failed(T data, String msg) {
return restResult(data, -1, msg);
}
}
- 编写自动注入配置类(替换容器中默认注入的 Feign.Builder)
package com.yangxc.feign.config;
import com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration;
import com.yangxc.feign.util.MySentinelFeign;
import feign.Feign;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
/**
* @author yangxc
* @version 1.0.0
* @ClassName FeignConfig.java
* @Description TODO
* @createTime 2022年11月08日 16:34:00
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(SentinelFeignAutoConfiguration.class)
public class FeignConfig {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.sentinel.enabled")
public Feign.Builder feignSentinelBuilder() {
return MySentinelFeign.builder();
}
}
总结
OpenFeign+Sentinel目前已成为远程调用和限流的标配,但OpenFeign默认提供的fallback需要写大量的方法,不太符合程序员追求极简的代码风格。此方式将大大减少代码量不需要关注限流后的返回值,同时可以标准化返回值。
评论区