openfeign方法级别自定义超时时间

一、介绍

最近,因为工作原因,一直在看openfeign相关的内容,其中就包括调研了如何支持到方法级别自定义超时时间

全局设置的很简单

1
2
3
4
5
6
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000

而如果不设置,将会走默认的设置

image-20240410223029203

二、代码

单条方法的话,代码其实很简单,我以前就会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.banmoon.feign;

import com.banmoon.constant.ServerNameConstant;
import com.banmoon.entity.UserEntity;
import com.banmoon.feign.fallback.FeignTestClientFallbackFactory;
import feign.Request;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = ServerNameConstant.WEB_MQ, contextId = "FeignTestClient", fallbackFactory = FeignTestClientFallbackFactory.class)
public interface FeignTestClient {

@GetMapping("/feign/customTimeoutRetryTest")
UserEntity customTimeoutRetryTest(@RequestParam Integer id, Request.Options options);

}

在方法后面加个Request.Options options就行了。

在调用的时候,实例化创建出来传递进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.banmoon.controller;

import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.entity.UserEntity;
import com.banmoon.feign.FeignTestClient;
import feign.Request;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@Api(tags = "feign测试模块")
@RestController
@RequestMapping("/feign")
public class FeignTestController {

@Resource
private FeignTestClient feignTestClient;

@ApiOperation("自定义超时重试测试")
@GetMapping("/customTimeoutRetryTest")
public ResultData<UserEntity> customTimeoutRetryTest(@RequestParam Integer id) {
Request.Options options = new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
UserEntity userEntity = feignTestClient.customTimeoutRetryTest(id, options);
return ResultData.success(userEntity);
}

}

三、使用AOP进行简化

是这样的,如果是一个两个还行,那万一是有一堆方法需要自定义超时时间呢?这样对代码的侵入就真的太大了。

而且,领导说了要支持可动态配置的(代码呢不写,B事一大堆

好吧,目标如下

  • 每个调用方法都要支持动态配置,这个最终选用了nacos

  • 对代码的侵入较大,我打算使用AOP技术来进行实现

1)代码

首先来一个注解吧,AOP会对标注上注解的方法进行增强

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.banmoon.feign.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* @author banmoon
* @date 2024/04/10 18:26:28
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeignOption {
}

再来一个切面,FeignOptionAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.banmoon.feign.aspect;

import cn.hutool.core.util.ArrayUtil;
import com.banmoon.utils.FeignOptionsUtil;
import feign.Request;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
* @author banmoon
* @date 2024/04/10 18:24:44
*/
@Slf4j
@Aspect
@Component
public class FeignOptionAspect {

@Resource
public FeignOptionsUtil feignOptionsUtil;

@Pointcut("@annotation(com.banmoon.feign.annotations.FeignOption)")
public void pointcut() {
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Class<?> clazz = method.getDeclaringClass();
// 获取类名
String clazzName = clazz.getSimpleName();
// 获取方法名
String methodName = method.getName();
Class<?>[] types = method.getParameterTypes();
// 判断方法的最后一个入参是否是Options,并且实际传参是不是null
if (ArrayUtil.isNotEmpty(types)) {
int lastIndex = types.length - 1;
Class<?> type = types[lastIndex];
if (Objects.equals(type, Request.Options.class)) {
Object[] args = joinPoint.getArgs();
// 如果实际传参为null,则读取配置文件中的信息并设置进去
if (Objects.isNull(args[lastIndex])) {
Integer connectTimeout = feignOptionsUtil.getConnectTimeout(clazzName, methodName);
Integer readTimeout = feignOptionsUtil.getReadTimeout(clazzName, methodName);
args[lastIndex] = new Request.Options(connectTimeout, TimeUnit.MILLISECONDS, readTimeout, TimeUnit.MILLISECONDS, true);
return joinPoint.proceed(args);
}
}
}
return joinPoint.proceed();
}

}

还有一个工具类,FeignOptionsUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.banmoon.utils;

import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
* @author banmoon
* @date 2024/04/10 17:01:49
*/
@Component
public class FeignOptionsUtil {

@Resource
private Environment environment;

/**
* 点
*/
private static final String POINT = ".";

/**
* 连接超时前缀
*/
private static final String FEIGN_CONNECT_PREFIX = "feign.connect-timeout.";

/**
* 读取超时前缀
*/
private static final String FEIGN_READ_PREFIX = "feign.read-timeout.";

/**
* 公共连接超时
*/
private static final String COMMON_CONNECT_TIMEOUT_SUFFIX = FEIGN_CONNECT_PREFIX + "common";

/**
* 公共读取超时
*/
private static final String COMMON_READ_TIMEOUT_SUFFIX = FEIGN_READ_PREFIX + "common";

/**
* 默认连接超时时间
*/
private static final Integer DEFAULT_CONNECT_TIMEOUT = 5000;

/**
* 默认读取超时时间
*/
private static final Integer DEFAULT_READ_TIMEOUT = 60000;

public Integer getCommonConnectTimeout() {
return environment.getProperty(COMMON_CONNECT_TIMEOUT_SUFFIX, Integer.class, DEFAULT_CONNECT_TIMEOUT);
}

public Integer getCommonReadTimeout() {
return environment.getProperty(COMMON_READ_TIMEOUT_SUFFIX, Integer.class, DEFAULT_READ_TIMEOUT);
}

public Integer getConnectTimeout(String clazzName, String methods) {
return getConnectTimeout(clazzName + POINT + methods);
}

public Integer getReadTimeout(String clazzName, String methods) {
return getReadTimeout(clazzName + POINT + methods);
}

private Integer getConnectTimeout(String feignSuffix) {
return environment.getProperty(FEIGN_CONNECT_PREFIX + feignSuffix, Integer.class, getCommonConnectTimeout());
}

private Integer getReadTimeout(String feignSuffix) {
return environment.getProperty(FEIGN_READ_PREFIX + feignSuffix, Integer.class, getCommonReadTimeout());
}

}

最后,最重要的,不要忘记添加配置啊

1
2
3
4
5
6
7
8
9
feign:
connect-timeout:
common: 10000
read-timeout:
common: 60000
# 这里填写类名
FeignTestClient:
# 这里填写方法名
customTimeoutRetryTest: 5000

2)测试

代码已经可以了,我们来改造一下,其实也就是添加上注解而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.banmoon.feign;

import com.banmoon.constant.ServerNameConstant;
import com.banmoon.entity.UserEntity;
import com.banmoon.feign.annotations.FeignOption;
import com.banmoon.feign.fallback.FeignTestClientFallbackFactory;
import feign.Request;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = ServerNameConstant.WEB_MQ, contextId = "FeignTestClient", fallbackFactory = FeignTestClientFallbackFactory.class)
public interface FeignTestClient {

@FeignOption
@GetMapping("/feign/customTimeoutRetryTest")
UserEntity customTimeoutRetryTest(@RequestParam Integer id, Request.Options options);

}

在调用的地方,就不需要自己手动创建一个Options实例了,如下直接传递个null进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.banmoon.controller;

import com.banmoon.business.obj.dto.ResultData;
import com.banmoon.entity.UserEntity;
import com.banmoon.feign.FeignTestClient;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@Api(tags = "feign测试模块")
@RestController
@RequestMapping("/feign")
public class FeignTestController {

@Resource
private FeignTestClient feignTestClient;

@ApiOperation("自定义超时重试测试")
@GetMapping("/customTimeoutRetryTest")
public ResultData<UserEntity> customTimeoutRetryTest(@RequestParam Integer id) {
UserEntity userEntity = feignTestClient.customTimeoutRetryTest(id, null);
return ResultData.success(userEntity);
}

}

来请求一下,当前配置的是5000,可以看到超时后,立刻降级返回

image-20240410225328938

那么我们现在修改一下配置,配置改为2000,来看一下

image-20240410225541309

四、最后

真的,AOP能做的很多,得看如何进行使用。

SpringBoot使用AOP详解 | 半月无霜 (banmoon.top)

我是半月,你我一同共勉!!!