背景
Feign可以通过配置文件来定义调用服务器端REST API的超时设置,可以指定全局超时,也可以指定声明@FeignClient
的单个Client,但是不可以设置Client中的每个接口方法的超时。由于项目中部分接口需要较大的超时,如果通过单独定义一个额外的Client,则需要调整大量代码,那能否通过底层框架解决呢。
Feign原理
Spring Cloud Open Feign 针对每个@FeignClient
声明的每个接口,通过动态代理生成一个代理类,InvocationHandler
的实现类为feign.ReflectiveFeign.FeignInvocationHandler
。
static class FeignInvocationHandler implements InvocationHandler {
private final Target target;
private final Map<Method, MethodHandler> dispatch;
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}
@Override
public Object invoke(Object proxy, Method method, 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 ("hashCode".equals(method.getName())) {
return hashCode();
} else if ("toString".equals(method.getName())) {
return toString();
}
return dispatch.get(method).invoke(args);
}
dispatch
的MethodHandler
具体实现为SynchronousMethodHandler
final class SynchronousMethodHandler implements MethodHandler {
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
//每次调用,如果Feign的接口方法,有Options 参数,则单独设置超时。
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
while (true) {
try {
return executeAndDecode(template, options);
} catch (RetryableException e) {
try {
retryer.continueOrPropagate(e);
} catch (RetryableException th) {
Throwable cause = th.getCause();
if (propagationPolicy == UNWRAP && cause != null) {
throw cause;
} else {
throw th;
}
}
if (logLevel != Logger.Level.NONE) {
logger.logRetry(metadata.configKey(), logLevel);
}
continue;
}
}
}
}
Sentinel的扩展
Sentinel对Feign进行了扩展,以便进行埋点。
public class SentinelInvocationHandler implements InvocationHandler {
private final Target<?> target;
private final Map<Method, MethodHandler> dispatch;
private FallbackFactory fallbackFactory;
private Map<Method, Method> fallbackMethodMap;
@Override
public Object invoke(final Object proxy, final Method method, final Object[] args)
throws Throwable {
// ......
Object result;
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);
//调用的是 SynchronousMethodHandler 方法。
result = methodHandler.invoke(args);
}
catch (Throwable ex) {
// fallback handle
if (!BlockException.isBlockException(ex)) {
Tracer.traceEntry(ex, entry);
}
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 {
// throw exception if fallbackFactory is null
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;
}
}
启用Sentinel Feign
feign:
sentinel:
enabled: true
解决方案
既然底层是通过额外的参数进行了超时设置,那么是否可以通过对bean实现代理解决呢?
可以通过BeanPostProcessor
的实现进行操作。
源码
DynamicTimeoutFeginInvocationHandler
代理接口实现:
package com.jurassic.cloud.sample.config;
import feign.Request;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
/**
* @author lihz
* @date 2024/11/10
*/
@RequiredArgsConstructor
public class DynamicTimeoutFeginInvocationHandler implements InvocationHandler {
final private Object targetObject;
final private DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption;
private Map<Method, Holder> holders = new HashMap<>(16);
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(targetObject, args);
}
if (timeoutOption.getConfig().isEmpty()) {
return method.invoke(targetObject, args);
}
if (holders.containsKey(method)) {
Holder holder = holders.get(method);
Object[] newArgs = additionalArgs(args, holder.getOptions());
return holder.getHandler().invoke(targetObject, method, newArgs);
}
RequestMapping annotation = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
if (annotation != null) {
String[] value = annotation.value();
for (String s : value) {
if (timeoutOption.getConfig().containsKey(s)) {
DynamicTimeoutFeignConfiguration.FeignTimeoutOptionInfo feignTimeoutOptionInfo = timeoutOption.getConfig().get(s);
Request.Options options = new Request.Options(
feignTimeoutOptionInfo.getConnectTimeout()
, feignTimeoutOptionInfo.getReadTimeout()
);
InvocationHandler handler = Proxy.getInvocationHandler(targetObject);
Object[] newArgs = additionalArgs(args, options);
holders.put(method, new Holder(handler, options));
return handler.invoke(targetObject, method, newArgs);
}
}
}
return method.invoke(targetObject, args);
}
private Object[] additionalArgs(Object[] args, Request.Options options) {
int length = args == null ? 0 : args.length;
Object[] newArgs = new Object[length + 1];
for (int i = 0; i < length; i++) {
newArgs[i] = args[i];
}
newArgs[newArgs.length - 1] = options;
return newArgs;
}
@AllArgsConstructor
@Getter
public static class Holder {
private InvocationHandler handler;
private Request.Options options;
}
}
BeanPostProcessor实现
package com.jurassic.cloud.sample.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cloud.openfeign.FeignClient;
import java.lang.reflect.Proxy;
/**
* @author lihz
* @date 2024/11/10
*/
@Slf4j
public class DynamicTimeoutFeignBeanProcessor implements BeanPostProcessor {
private DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption;
public DynamicTimeoutFeignBeanProcessor(DynamicTimeoutFeignConfiguration.FeignTimeoutOption timeoutOption) {
this.timeoutOption = timeoutOption;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (hasFeign(bean)) {
DynamicTimeoutFeginInvocationHandler handler = new DynamicTimeoutFeginInvocationHandler(
bean, timeoutOption
);
Object o = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(), handler);
log.info(beanName + " ... postProcessAfterInitialization ");
return o;
}
return bean;
}
private boolean hasFeign(Object bean) {
for (Class<?> anInterface : bean.getClass().getInterfaces()) {
if (anInterface.getAnnotation(FeignClient.class) != null) {
return true;
}
}
return false;
}
}
自动配置
package com.jurassic.cloud.sample.config;
import feign.Feign;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@ConditionalOnClass(Feign.class)
@Configuration
@EnableConfigurationProperties(DynamicTimeoutFeignConfiguration.FeignTimeoutOption.class)
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class DynamicTimeoutFeignConfiguration {
@Bean
public DynamicTimeoutFeignBeanProcessor beanProcessor(FeignTimeoutOption timeoutOption) {
return new DynamicTimeoutFeignBeanProcessor(timeoutOption);
}
@ConfigurationProperties(prefix = "feign.timeout")
@Getter
@Setter
public static class FeignTimeoutOption {
private Map<String, FeignTimeoutOptionInfo> config = new HashMap<>();
}
@Getter
@Setter
public static class FeignTimeoutOptionInfo {
private int connectTimeout;
private int readTimeout;
}
}
yaml配置
feign:
timeout:
config:
"[/T1/timeout]": # API PATH
connectTimeout: 1000
readTimeout: 8000