【OpenFeign】 注解
【OpenFeign】 注解
Metadata
title: 【OpenFeign】 注解
date: 2023-01-02 18:54
tags:
- 行动阶段/完成
- 主题场景/组件
- 笔记空间/KnowladgeSpace/ProgramSpace/ModuleSpace
- 细化主题/Module/OpenFeign/基础
categories:
- OpenFeign
keywords:
- OpenFeign
description: 【OpenFeign】 注解
原生注解
Feign 注解定义了接口和底层客户端应之间该如何工作的关系。Feign 的默认定义了以下注解:
注解 | 作用位置 | 用法 |
---|---|---|
@RequestLine | 方法 | 为请求定义 HttpMethod 和 UriTemplate。 花括号 {expression} 中的值使用其相应的带 @Param 注解的参数解析。 |
@Param | 参数 | 定义一个模板变量,其值将用于解析相应的表达式模板,通过作为注解值提供的名称。如果缺少值,它将尝试从字节码方法参数名称中获取名称(如果代码是用 - parameters 标志编译的)。 |
@Headers | 方法、类型 | 定义一个 HeaderTemplate。使用带 @Param 注释的值来解析相应的 Expressions. 在 Type 上使用时,模板将应用于每个请求。 |
@QueryMap | 参数 | 范围 定义一个 Map 名称 - 值对或 POJO,以扩展为查询字符串。 |
@HeaderMap | 参数 | 范围 定义一个 Map 名称 - 值对,扩展为 Http Headers |
@Body | 方法 | 定义 Template,类似于 UriTemplateand、 HeaderTemplate,它使用带 @Param 注释的值来解析相应的 Expressions. |
@RequestLine
概述
RequestLine
是请求行的意思,我们知道在 HTTP 中,请求行一般由以下几部分构成。
@RequestLine
注解只能标注在方法上,为请求定义 HttpMethod(请求方式 GET、POST 等)和 UriTemplate(访问路径),然后可以将@Param
注解中表示的参数,解析到表达式 { } 对应的位置上。
@RequestLine
注解有以下几个配置项,一般我们只需要配置 value 值就可以了。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLine {
// 请求行的值
String value();
// 斜杠转义,默认true
boolean decodeSlash() default true;
// URL参数中编码方式,默认使用 &aa=aabb&bb=bb,不需要改
CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
}
案例
接下来我们看下如何使用@RequestLine
注解进行传参访问。
首先将接口改为需要一个 name 参数:
@GetMapping("/test")
@ApiOperation(value = "测试接口")
@ApiOperationSupport(author = "xiaoymin@foxmail.com")
public String test(String name) {
return name;
}
Feign 接口中,定义好请求行,使用@Param
注解表示将请求参数赋值给表达式 {name}。
public interface TestFeignClient {
@RequestLine("GET /app1/test?name={name}")
String test(@Param("name") String name);
}
然后创建客户端并远程访问:
public static void main(String[] args) {
// 1. 生成Feign 客户端
TestFeignClient feignClient = Feign.builder()
.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL) // 打印所有日志
.retryer(Retryer.NEVER_RETRY) // 关闭重启
.target(TestFeignClient.class, "http://127.0.0.1:9001"); // 接口及请求地址
// 2. 打印结果
String test = feignClient.test("张三");
System.out.println(test);
}
启动该工程,并执行 Feign 请求,可以通过日志,看到当前请求的整个过程。
@Param
概述
@Param
只能做用于方法参数上,它的主要作用是,通过变量名,绑定一个参数值,然后可以将参数填充到模板表达式中。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
public @interface Param {
// Key,对应模板表达式中的变量
String value() default "";
// 填充方式,默认直接使用ToString方法,
Class<? extends Param.Expander> expander() default Param.ToStringExpander.class;
// 是否转义,默认false
/** @deprecated */
boolean encoded() default false;
// 内部类填充器 使用ToString方法填充
public static final class ToStringExpander implements Param.Expander {
public ToStringExpander() {
}
public String expand(Object value) {
return value.toString();
}
}
public interface Expander {
String expand(Object var1);
}
}
在上面的案例中,会将 name 参数,填充到 {name} 表达式中。
@RequestLine("GET /app1/test?name={name}")
String test(@Param("name") String name);
@Headers
概述
@Headers
可作用于方法或者类上,用于添加请求头。
它的属性很简单,是一个字符串数组。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Headers {
// 请求头
String[] value();
}
案例
注意添加的时候,消息头键值对使用冒号:分隔。
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
可以看到,注解上标识的请求头被添加到了请求中。
@QueryMap
概述
@QueryMap
注解只可作用于方法参数上,表示将多个参数拼接在 URL 后面访问。
其只有一个属性,表示是否转义,默认 false。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface QueryMap {
boolean encoded() default false;
}
案例
如果某个请求接口需要多个参数,这个时候就可以使用@QueryMap
注解传递,注意这个会后参数必须是 Map 类型的。
比如我们可以通过以下方式传递:
@RequestLine("GET /app1/test")
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
String testQueryMap(@QueryMap Map<String, String> map);
可以看到在请求时,将 Map 中的键值对拼接到了 URL 后面。
@HeaderMap
概述
@HeaderMap
注解只能做用于方法参数上,和@QueryMap
差不多,是将 Map 中的键值对,添加到请求头中。
该注解没有配置项:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface HeaderMap {
}
案例
@RequestLine("GET /app1/test")
String testQueryMap(@HeaderMap Map<String, String> map);
@Body
概述
@Body
做用于方法上,会使用请求体来传递参数。可以传递字符串、Json 数据,在传递 Bean 对象时,会调用其toString
方法再进行传递。
该注解只有一个属性值,可以写表达式,通过@Param
传值。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Body {
String value();
}
案例
通过请求体传递一个实体类对象。
@RequestLine("POST /app1/post")
@Body("{user}")
String post(@Param("user") User user);
可以看到在发送请求时,实际传递的是实体类的 toString 字符串,如果在接口提供方用对象是接受不到的。。。具体怎么回事,后续会介绍
@EnableFeignClients
在 Cloud 中使用@EnableFeignClients
启用 Feign 客户端,接下来分析下这个注解的基本原理。
@EnableFeignClients
注解会扫描包路径下的@FeignClient
注解定义的接口,并注册到 IOC 容器中。
配置属性主要是配置扫描路径:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
// 配置扫描@FeignClient的路径,默认是当前标识了该注解类下的路径,和@ComponentScans一样
String[] value() default {};
// 扫描路径,多个
String[] basePackages() default {};
// 指定扫描某个类所在包下的所有类
Class<?>[] basePackageClasses() default {};
// 指定客户端配置配
Class<?>[] defaultConfiguration() default {};
// 直接指定扫描客户端的类,配置了就不会扫描
Class<?>[] clients() default {};
}
在@EnableFeignClients
注解中,使用了@Import
注解导入了FeignClientsRegistrar
,学过 Spring 的应该知道@Configuration
类上使用@Import
,可以注入 Bean 对象。
FeignClientsRegistrar
实现了以下几个接口:
- ImportBeanDefinitionRegistrar:支持使用
@Import
注解注册 BeanDefinition - ResourceLoaderAware:读取文件
- EnvironmentAware:获取环境及配置属性
启动项目后,进行BeanDefinition
注册时,就会进入到registerBeanDefinitions
方法,该方法会注册默认配置和注册 Feign 客户端。
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
this.registerDefaultConfiguration(metadata, registry);
this.registerFeignClients(metadata, registry);
}
registerDefaultConfiguration
会进行注册客户端配置类的BeanDefinition
:
// 参数为EnableFeignClients注解配置的类的信息、BeanDefinition注册器
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 1. 获取EnableFeignClients注解上的配置属性
Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
// 取个名字=》default.account.AccountApp
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
} else {
name = "default." + metadata.getClassName();
}
// 2. 注册客户端配置
this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
}
}
接着进入到registerClientConfiguration
,注册客户端配置,会在注册器中添加一个名字为default.account.AccountApp.FeignClientSpecification
,类型为FeignClientSpecification
的BeanDefinition
。
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
// 创建一个 BeanDefinition构建者,使用FeignClientSpecification(这里存放了客户端配置名称和配置类)
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
// 添加FeignClientSpecification 构造函数的参数
// name => default.account.AccountApp
// configuration=> 注解上的配置类,这里没有
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
// 注册,名称为=》default.account.AccountApp.FeignClientSpecification
// 类型为=》FeignClientSpecification
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}
接着注册 Feign 客户端,会调用扫描器,扫描类路径下的@FeignClient
标记的类,然后读取配置信息,将该类的配置项、类信息都注册到注册器中。
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// BeanDefinition 不重复集合
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
// EnableFeignClients 注解配置信息
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
new AnnotationTypeFilter(FeignClient.class);
Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
// 查看是否配置了clients 属性,看来这个属性配置了以后,就不会进行包扫描
if (clients != null && clients.length != 0) {
Class[] var13 = clients;
int var15 = clients.length;
for(int var17 = 0; var17 < var15; ++var17) {
Class<?> clazz = var13[var17];
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
} else {
// 创建一个ClassPath扫描器
ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
// 设置ResourceLoader
scanner.setResourceLoader(this.resourceLoader);
// 设置需要扫描的类,也就是配置了FeignClient 注解的类
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
// 获取配置上的 这里没配置,就是扫描当前项目包下面的了
Set<String> basePackages = this.getBasePackages(metadata);
Iterator var9 = basePackages.iterator();
// 扫描到Feign 接口类,放入集合中
while(var9.hasNext()) {
String basePackage = (String)var9.next();
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
Iterator var14 = candidateComponents.iterator();
while(var14.hasNext()) {
// 循环扫描到的BeanDefinition
BeanDefinition candidateComponent = (BeanDefinition)var14.next();
if (candidateComponent instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
// FeignClient注解上的元信息
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
// FeignClient注解配置的属性
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// BeanDefinition 名称=》order-service
String name = this.getClientName(attributes);
// 注册客户端配置=》configuration属性
this.registerClientConfiguration(registry, name, attributes.get("configuration"));
// 注册客户端
this.registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
注册了客户端的配置后,就会注册客户端,调用的是registerFeignClient
方法:
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
// account.OrderFeign
String className = annotationMetadata.getClassName();
// 创建FeignClientFactoryBean 类型的BeanDefinition
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
this.validate(attributes);
// 将这些FeignClient 注解上的配置信息都添加到definition中,
definition.addPropertyValue("url", this.getUrl(attributes));
// 省略....
beanDefinition.setPrimary(primary);
String qualifier = this.getQualifier(attributes);
if (StringUtils.hasText(qualifier)) {
alias = qualifier;
}
// 添加到注册器中
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
可以看到,最终每个 Feign 接口注册到了 注册器中,名称为包名+接口名
,Feign 接口实际生成的 Bean 对象为FeignClientFactoryBean
,这是一个FactoryBean
,由它最终生成的代理对象后续会讲解)。
@FeignClient
在上篇文档中@EnableFeignClients
注解,扫描@FeignClient
注解表示的接口,并将加载到容器中,接下来分析下@FeignClient
注解配置项及加载流程。
标记接口为 Feign 客户端,Feign 启动会扫描当前接口及注解属性,加载到容器中,执行时,通过 IOC 中当前注解对应的FactoryBean
对象,创建动态代理对象,然后去执行请求流程
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
// 指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
@AliasFor("name")
String value() default "";
//
/** @deprecated */
@Deprecated
String serviceId() default "";
// 每个客户端,对应不用的上下文,这里就是这个上下文的ID,
// 当name属性一样时,可以自定义这个ID,来解决冲突问题
String contextId() default "";
@AliasFor("value")
String name() default "";
String qualifier() default "";
// 指定@FeignClient调用的IP地址,一般都是服务发现,所以不需要配置
String url() default "";
// 是否解码404,404时,会返回NULL对象,不建议配置
boolean decode404() default false;
// Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
Class<?>[] configuration() default {};
// 定义容错的处理类,当调用远程接口失败或超时时,
// 会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
Class<?> fallback() default void.class;
// 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
Class<?> fallbackFactory() default void.class;
// 定义当前FeignClient的统一前缀
String path() default "";
// 是否是primary
boolean primary() default true;
}
还是一样分析FeignClientsRegistrar
类。
1. 获取客户端名称
之前分析过,扫描之后,就会在registerFeignClients
方法注册客户端,首先会获取客户端名称:
getClientName
方法传入的参数,就是@FeignClient
注解配置属性:
getClientName
处理逻辑如下,也就是名称会从配置项中选择,优先级为 contextId=》value=》name=》serviceId,没有则会报错。
// 参数为
private String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
} else {
// 先设置名称为 contextId配置项,这里没有为“”
String value = (String)client.get("contextId");
// 如果为空查看value配置项
if (!StringUtils.hasText(value)) {
value = (String)client.get("value");
}
// 如果为空查看name配置项
if (!StringUtils.hasText(value)) {
value = (String)client.get("name");
}
// 如果为空查看serviceId配置项
if (!StringUtils.hasText(value)) {
value = (String)client.get("serviceId");
}
// 如果有,则直接返回,没有报错
if (StringUtils.hasText(value)) {
return value;
} else {
throw new IllegalStateException("Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
}
}
}
2. 获取 configuration
接着就是将configuration
中的配置类加载到BeanDefinitionRegistry
中了。
3. 注册客户端
接着就是registerFeignClient
注册客户端了,首先会将注解属性添加到definition
中,这里 url、path 没有配置,所以都为空,然后注意contextId
,没有配置时,默认使用 name 属性,这也就是为啥多个同名的@FeignClient
,没有指定contextId
会报错的原因了。
接着还会设置alias
,factoryBeanObjectType
,primary
属性:
// 别名 :order-serviceFeignClient
String alias = contextId + "FeignClient";
// factoryBeanObjectType=》account.OrderFeign
// 告诉factoryBean,要生成对象的类
beanDefinition.setAttribute("factoryBeanObjectType", className);
// 是否primary ,也就是@Primary 注解,多个相同Bean时,优先使用这一个
boolean primary = (Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);
最后就到了上篇文档分析的最后一步了。