前言
微服务项目里 OpenFeign 是最常用的声明式 HTTP 客户端,但 @FeignClient 的属性含义很多人只停留在 value 上。这篇整理注解的全属性解读,以及一个常见的坑:多个 FeignClient 指向同一个服务时的 bean 重名冲突。
一、@FeignClient 全属性解读
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
|
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
/**
* 1、value 与 name 互为别名,两者二选一即可
* 2、当 contextId 没有值的时候,会默认获取(value/name)的值
* 3、当未指定 url 请求地址的时候,最终会通过 ribbon-loadbalancer 工具,
* 从注册中心选取 service-id 等于 value 的服务作为请求地址
*/
@AliasFor("name")
String value() default "";
/**
* serviceId 已作废,其目的与 value 一致。如果设置了 serviceId,
* 则 value/name 皆以 serviceId 为准
*/
@Deprecated
String serviceId() default "";
/**
* 1、当 contextId 没有值的时候,会默认获取(value/name)的值
* 2、当 qualifier 没有值的时候,会将 '${contextId}FeignClient' 作为
* feign 的 bean 组件别名
*/
String contextId() default "";
/** 同 value,二选一 */
@AliasFor("value")
String name() default "";
/**
* feign 的 bean 组件别名,拥有最高优先级;
* 当 qualifier 为空时,取 '${contextId}FeignClient' 作为 bean 的名称
*/
String qualifier() default "";
/**
* 1、设置 url 以后,后续发起 http 调用时,直接读取该地址作为请求目标;
* 2、未设置 url 时,借助 ribbon-loadbalancer 组件,根据 (value/name)
* 从注册中心的服务列表中选中 service-id 匹配的目标服务
*/
String url() default "";
boolean decode404() default false;
Class<?>[] configuration() default {};
Class<?>[] fallback() default void.class;
Class<?>[] fallbackFactory() default void.class;
/**
* 目标服务器对应的资源 uri(FeignClient 下所有方法相同的 path 前缀)
*/
String path() default "";
boolean primary() default true;
}
|
属性速查:
| 属性 |
作用 |
| value / name |
目标服务名(注册中心里的 service-id),互为别名 |
| contextId |
bean 的上下文标识,默认取 value/name;解决同名 bean 冲突的关键 |
| qualifier |
bean 别名,优先级最高,为空时取 ${contextId}FeignClient |
| url |
直连地址,设置后不走注册中心负载均衡 |
| decode404 |
404 是否抛 FeignException(false 时解码为 null/实体) |
| configuration |
自定义配置类(编解码器、重试等) |
| fallback / fallbackFactory |
熔断降级回退实现 |
| path |
统一路径前缀 |
| primary |
是否标记为 primary bean |
二、实战:bean 重名冲突
场景:项目里需要调用本项目中定义的另一个 Feign 接口,两个 @FeignClient 指向了同一个服务名。因为 springboot 版本问题,使用配置文件配置 allow-bean-definition-overriding=true 不起作用,启动直接报 bean 重名。
解决方案:在 Feign 接口上加 contextId 区分。注意 contextId 不能和业务层的 bean 同名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package com.example.demo.feign;
import com.example.demo.common.core.web.ServerResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(name = "demo-admin-server", contextId = "SysDeptFeign")
@RequestMapping("/sysDept")
public interface SysDeptFeign {
/**
* 根据ID查询所有子部门
* @param deptId 部门ID
* @return 部门列表
*/
@GetMapping("/findChildrenDeptById/{deptId}")
ServerResponse findChildrenDeptById(@PathVariable Long deptId);
}
|
加了 contextId 后,两个指向同一服务的 Feign 接口各自生成独立的 bean,冲突解除。
三、完整使用示例
3.1 开启 FeignClient 扫描
1
2
3
4
|
@Configuration
@EnableFeignClients(basePackages = "com.example")
public class FeignConfiguration {
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Slf4j
@Component(value = "core_feign_interceptor")
@AllArgsConstructor
public class FeignInterceptor implements RequestInterceptor {
private final ClientTokenTemplate clientTokenTemplate;
/**
* feign请求添加header
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 如果token中的类型加入了验证, 则设置rdc token
Collection<String> authHeaders = requestTemplate.headers().get(TokenConstant.AUTH_TYPE);
if (CollectionUtils.isNotEmpty(authHeaders) && authHeaders.contains(TokenConstant.CLIENT_AUTH)) {
requestTemplate.header("Authorization", clientTokenTemplate.getRedisToken().getAccess_token());
} else {
AuthInfo authInfo = BizContext.getValue(Constants.BizContextKey.AUTH);
if (authInfo != null && !StringUtils.isEmpty(authInfo.getAccess_token())) {
requestTemplate.header("Authorization", authInfo.getAccess_token());
}
}
}
}
|
3.3 文件服务的客户端实现
url + path 组合的典型用法——不走注册中心,直接指向网关地址,并声明统一前缀:
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
|
/**
* 文件服务客户端
*/
@ConditionalOnProperty(name = {"gateway.host", "gateway.apis.file-service"})
@FeignClient(value = "demo-file-service", url = "${gateway.host}/${gateway.apis.file-service}", path = "/api/v2/files")
public interface RdcFileClient {
/**
* 获取文件信息
* @param fileId 文件ID
* @return 文件内容
*/
@GetMapping(value = "/{fileId}", headers = TokenConstant.GATEWAY_AUTH_HEADER)
PluginFileInfoResponse getFileInfo(@PathVariable String fileId);
/**
* 上传文件
*/
@PostMapping(value = "/", headers = TokenConstant.GATEWAY_AUTH_HEADER, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
PluginFileInfoResponse uploadFile(MultipartFile file);
/**
* 获取下载地址
*/
@GetMapping(value = "/{fileId}/download-url", headers = TokenConstant.GATEWAY_AUTH_HEADER)
String getDownloadUrl(@PathVariable String fileId);
/**
* 批量获取文件详情
*/
@PostMapping(value = "/batchInfos", headers = TokenConstant.GATEWAY_AUTH_HEADER,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
List<PluginFileInfoResponse> batchGetFileDetail(JSONObject object);
/**
* 删除缓存的文件
*/
@DeleteMapping(value = "/{fileId}", headers = TokenConstant.GATEWAY_AUTH_HEADER)
Object delFile(@PathVariable String fileId);
}
|
总结
记住三条:value/name 指定目标服务;同一服务多个 Feign 接口时必须用 contextId 区分(比开 bean 覆盖开关干净得多);直连场景用 url + path 组合。