【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,类型为FeignClientSpecificationBeanDefinition

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会报错的原因了。

接着还会设置aliasfactoryBeanObjectType,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);

最后就到了上篇文档分析的最后一步了。