当前位置:网站首页>如何自定义feign方法级别的超时时间
如何自定义feign方法级别的超时时间
2022-08-02 14:00:00 【Ethan_199402】
阅读之前,希望你已经了解过feign的调用流程
问题:feign暴露出来的config默认只支持接口contextId级别的配置,也就是如果我们项目中一些二方依赖接口比较慢,但是他们被包在一个方法较多的client中,那么该如何对这一个单独的接口进行超时配置呢?
如果你已经了解过feign源码,应该SynchronousMethodHandler不陌生了,回顾一下他的invoke方法
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
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;
}
}
}
调用的代码明显是executeAndDecode(template, options)
,而超时时间相关的参数就在options中,知道了这一点,我们可以
通过反射修改options
options的获取就在findOptions中
Options options = findOptions(argv);
Options findOptions(Object[] argv) {
if (argv == null || argv.length == 0) {
return this.options;
}
return (Options) Stream.of(argv)
.filter(o -> o instanceof Options)
.findFirst()
.orElse(this.options);
}
首先自定义XXXTargetAwareFeignClient继承TargetAwareFeignClient方法
public class XXXTargetAwareFeignClient extends TargetAwareFeignClient {
public XXXTargetAwareFeignClient(Client delegate) {
super(delegate);
}
@Override
public Response execute(Request request, Request.Options options) throws IOException {
Integer timeout = FeignTimeoutUtil.get();
if (Objects.nonNull(timeout)) {
Request.Options currentOptions = new Options(timeout,timeout);
return super.execute(request, currentOptions);
}
return super.execute(request, options);
}
}
然后获取容器中名字为feignClient的bean,将它的clientDelegate替换为我么自定义的XXXTargetAwareFeignClient,这样每次调用client.execute(request, options)方法时,就会使用重写过的execute方法来改写options的值
@Component
public class FeignClientBeanPostProcessor implements BeanPostProcessor {
@Autowired
private okhttp3.OkHttpClient okHttpClient;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("feignClient")) {
//通过反射来设置ClusterAwareLoadBalancerFeignClient的client属性
try {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
XXXTargetAwareFeignClient engineTargetAwareFeignClient = new XXXTargetAwareFeignClient(delegate);
Class clazz = AopUtils.getTargetClass(bean);
Class superClass = clazz.getSuperclass();
Field clientDelegateField = clazz.getDeclaredField("clientDelegate");
clientDelegateField.setAccessible(true);
Field delegateField = superClass.getDeclaredField("delegate");
delegateField.setAccessible(true);
try {
clientDelegateField.set(bean, engineTargetAwareFeignClient);
delegateField.set(bean, engineTargetAwareFeignClient);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
return bean;
}
@Override
public int getOrder() {
return 0;
}
}
确点:feignClient这个bean会随着内部中间件的升级而改变继承结构,这种写法不够稳定
修改字节码
思路就是直接修改findOptions方法的字节码,比如使用Instrumenter来需改
public class FeignClientInstrumenter implements Instrumenter {
// feign do http invoke class
private static final String ENHANCE_CLASS = "feign.SynchronousMethodHandler";
private static final String ENHANCE_METHOD = "findOptions";
// both connect and read are use same timeout config
private static final int MAX_TIMEOUT = 30_000;
@Override
public AgentBuilder instrument(AgentBuilder agentBuilder) {
return agentBuilder.type(named(ENHANCE_CLASS)).transform(new FeignClientInstrumenter.Transformer());
}
private static class Transformer implements AgentBuilder.Transformer {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module) {
return builder.visit(Advice.to(Interceptor.class).on(named(ENHANCE_METHOD)));
}
}
public static class Interceptor {
@Advice.OnMethodEnter
public static Options onEnter(@Advice.Origin Method method,
@Argument(0) Object[] args,
@Advice.This Object obj) {
//超时逻辑
Integer customizedTimeout = getCustomizedTimeout(method,args)
if (customizedTimeout != null) {
// safety check, make sure the final Option is not
if (customizedTimeout < 0 || customizedTimeout > MAX_TIMEOUT) {
customizedTimeout = MAX_TIMEOUT;
}
return new Options(customizedTimeout, customizedTimeout);
}
return null;
}
@Advice.OnMethodExit
public static void onExit(
@Advice.Enter Options enter,
@Advice.Return(readOnly = false, typing = Typing.DYNAMIC) Options returned) {
}
}
}
优点是直接修改字节码性能更高
缺点
- 依赖来feign的版本,不是每个版本feign.SynchronousMethodHandler中都有findOptions给你拿来做修改的
- 不能区分具体接口,适合统一路由接口,根据租户的标识来分发具体的超时时间(可以判断args[0]的类型区分是否是路由接口)
aop切面
切面的切入时间应该在findOptions之后,那么明显LoadBalancerFeignClient的public Response execute(Request request, Request.Options options) throws IOException
更合适
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
虽然代码中IClientConfig requestConfig = getClientConfig(options, clientName);
方法也可能修改到options的值
IClientConfig getClientConfig(Request.Options options, String clientName) {
IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
}
else {
requestConfig = new FeignOptionsClientConfig(options);
}
return requestConfig;
}
但是目前的实现只是判断如果options为DEFAULT_OPTIONS,则获取对应ribbon的config,否则直接以传入的options构建FeignOptionsClientConfig,因为在这之前我们就已经修改过options,所以options不可能为DEFAULT_OPTIONS。
AOP代码如下:
@Aspect
@Component
@Slf4j
@Order(0)
@ConditionalOnProperty(prefix = "feign.dynamic.timeout", value = "enabled", matchIfMissing = true)
public class FeignDynamicTimeoutAop {
@Resource
private FeignTimeoutProperties feignTimeoutProperties;
@Value("${ddmc.feign.dynamic.timeout.enabled:true}")
private Boolean enabled;
public FeignDynamicTimeoutAop() {
log.info("[ddmc-feign-ext]feign加载超时时间动态拦截启用加载成功...");
}
@Pointcut("execution(execution(public * org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient.execute(..))")
public void pointcut() {
//
}
private Request.Options getOptions(Map<String, TimeOutVO> configs, URI asUri) {
TimeOutVO timeOutVO = configs.get(asUri.getHost() + asUri.getPath());
if (timeOutVO == null) {
timeOutVO = configs.get(asUri.getHost());
}
if (timeOutVO == null) {
return null;
}
if (timeOutVO.getConnectTimeout() == null) {
timeOutVO.setConnectTimeout(timeOutVO.getReadTimeout());
}
if (timeOutVO.getReadTimeout() == null) {
timeOutVO.setReadTimeout(timeOutVO.getConnectTimeout());
}
Request.Options options = new Request.Options(timeOutVO.getConnectTimeout(), timeOutVO.getReadTimeout());
return options;
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
if (enabled && MapUtils.isNotEmpty(feignTimeoutProperties.getConfig())) {
try {
Map<String, TimeOutVO> configs = feignTimeoutProperties.getConfig();
Request.Options options = null;
if (args.length == FeignKeyConstants.ARGS_LEN) {
Request request = (Request) args[FeignKeyConstants.FEIGN_REQUEST_ARGS];
URI asUri = URI.create(request.url());
options = getOptions(configs, asUri);
Cat.logEvent("FeignDepend", new StringBuilder().append(asUri.getHost()).append(":").append(asUri.getPath()).append("@").append(options == null ? -1 : options.connectTimeoutMillis()).toString());
}
if (options != null) {
args[FeignKeyConstants.FEIGN_REQUEST_OPTION_ARGS] = options;
}
} catch (Exception e) {
log.error("feign超时设置异常.", e);
}
return joinPoint.proceed(args);
}
return joinPoint.proceed();
}
}
推荐使用这种方式,比较灵活
边栏推荐
- 方舟生存进化淘宝面板服务器是怎么一回事?
- wait() ,notify(),notifyAll()以及wait()与sleep()比较
- 机器学习——交叉验证法
- RowBounds[通俗易懂]
- 网络安全第三次作业
- C language improvement (3)
- 鲲鹏devkit & boostkit
- Sentinel源码(六)ParamFlowSlot热点参数限流
- You can't accept 60% slump, there is no eligible for gain of 6000% in 2021-05-27
- 关于Google词向量模型(googlenews-vectors-negative300.bin)的导入问题
猜你喜欢
Configure zabbix auto-discovery and auto-registration.
WeChat Mini Program-Recent Dynamic Scrolling Implementation
【Tensorflow】AttributeError: ‘_TfDeviceCaptureOp‘ object has no attribute ‘_set_device_from_string‘
The world's largest Apache open source foundation is how it works?
A number of embassies and consulates abroad have issued reminders about travel to China, personal and property safety
eclipse连接数据库后插入数据报错null
RKMPP 在FFmpeg上实现硬编解码
配置zabbix自动发现和自动注册。
乐心湖‘s Blog——MySQL入门到精通 —— 囊括 MySQL 入门 以及 SQL 语句优化 —— 索引原理 —— 性能分析 —— 存储引擎特点以及选择 —— 面试题
科研试剂DSPE-PEG-VIP,二硬脂酰基磷脂酰乙醇胺-聚乙二醇-血管活性肠肽VIP
随机推荐
Image retrieval method based on deep learning!
A number of embassies and consulates abroad have issued reminders about travel to China, personal and property safety
[C language] Analysis of function recursion (3)
关于市场后市的发展预测? 2021-05-23
mysql的case when如何用
uview 2.x版本 tabbar在uniapp小程序里头点击两次才能选中图标
ping命令的使用及代码_通过命令查看ping路径
删除链表的节点
CSDN(成长一夏竞赛)- 最大数
WeChat Mini Program-Recent Dynamic Scrolling Implementation
网络安全第一次作业(2)
SQL函数 UCASE
玉溪卷烟厂通过正确选择时序数据库 轻松应对超万亿行数据
腾讯安全游戏行业研讨会:生态共建,护航游戏产业健康发展
The world's largest Apache open source foundation is how it works?
拯救流浪猫 | 「喵先锋」系列数字版权盲盒明日开抢
RowBounds[通俗易懂]
定了!就在7月30日!
SQL函数 TRUNCATE
科研试剂DSPE-PEG-VIP,二硬脂酰基磷脂酰乙醇胺-聚乙二醇-血管活性肠肽VIP