浅谈SpringCloud-OpenFeign的配置使用和分析

SpringCloud-OpenFeign的配置使用和分析

1.说明

Feign是声明性的web服务客户端。 它使编写web服务客户端更加容易。要使用Feign,请创建一个接口并对其进行注释。它具有可插入的注释支持,包括Feign注释和JAX-RS注释。Feign还支持可插拔编码器和解码器。Spring Cloud添加了对Spring MVC注释的支持,并支持使用Spring Web中默认使用的同一HttpMessageConverters。Spring Cloud集成了Ribbon和Eureka以在使用Feign时提供负载平衡的http客户端。

简单的说,使用Feign进行rest服务请求调用更偏向类似于java编程中接口调用,可以认为是对Ribbon + RestTemplate的进一步封装,但是两者还是有区别的!!!!

2.配置和使用

  1. API的模块的pom中添加依赖,并创建service虚拟客户端(记住是接口),且在该接口上添加@FeignClient注解(value值为服务实例名称)

    api-pom.xml

    		<!-- 导入OpenFeign依赖       -->
            <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    

    DeptClientService.java

    package com.laoliu.springcloud.service;
    
    import com.laoliu.springcloud.pojo.Dept;
    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.PostMapping;
    
    import java.util.List;
    
    @FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT") //value为服务实例名称
    // 以这种格式,通过源码可以知道,届时将会被拼接为http://SPRINGCLOUD-PROVIDER-DEPT/rest服务名
    public interface DeptClientService {
        @GetMapping("/dept/get/{id}")
        public Dept queryById(@PathVariable("id") Long id);
    
        @PostMapping("/dept/add")
        public boolean addDept(Dept dept);
    
        @GetMapping("/dept/list")
        public List<Dept> queryAll();
    }
    

    项目模块结构图:

    在这里插入图片描述

  2. 在consumer模块添加依赖,启动类上添加 @EnableFeignClients,在controller层中@Autowired 获取ApiService接口并使用

    consumer-pom.xml

    <dependencies>
            <dependency>
                <groupId>com.laoliu</groupId>
                <artifactId>springcloud-api</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>3.1.2</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <!--
                springCloud2020 版本 把Bootstrap被默认禁用,
                spring.config.import加入了对解密的支持。对于Config Client、Consul、Vault和Zookeeper的配置导入,
                如果需要使用原来的配置引导功能,
                那么需要将org.springframework.cloud:spring-cloud-starter-bootstrap依赖引入到工程中
                这样才能正常使用springCloud
            -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
    
            <!-- 导入OpenFeign依赖       -->
            <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
        </dependencies>
    

    application.yml

    server:
      port: 80
    
    eureka:
      client:
        register-with-eureka: false # false 不是服务提供者,不需要注册到Eureka中
        fetch-registry: true # true 消费者需要检索注册中心服务才能调用实例,否则找不到实例,调用失败,报错!!!!!
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
    
    

    启动类

    package com.laoliu.springcloud;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    @EnableEurekaClient //开启Eureka
    @EnableFeignClients //开启Feign
    public class DeptConsumerOpenFeign80 {
        public static void main(String[] args) {
            SpringApplication.run(DeptConsumerOpenFeign80.class);
        }
    }
    
    

    Controller层的类

    package com.laoliu.springcloud.controller;
    
    import com.laoliu.springcloud.pojo.Dept;
    import com.laoliu.springcloud.service.DeptClientService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @RestController
    public class DeptConsumerController {
    
        @Autowired
        private DeptClientService clientService; //从Spring容器中拿到服务接口
    
        @PostMapping("/consumer/dept/add")
        public boolean add(Dept dept){
            return this.clientService.addDept(dept);    // 方法调用
        }
    
        @GetMapping("/consumer/dept/get/{id}")
        public Dept get(@PathVariable("id") Long id){
            return this.clientService.queryById(id);    // 方法调用
        }
    
        @GetMapping("/consumer/dept/list")
        public List<Dept> getAll(){
            return this.clientService.queryAll();   // 方法调用
        }
    
    }
    
    

    通过上述的调用方式,可以看出很符合我们熟悉的java接口调用开发,相对Ribbon + RestTemplate来说,可读性较强,但是性能也相比较较低。

    项目模块结构图:

在这里插入图片描述

  1. 启动Eeruka注册中心服务,provider服务和consumer服务,进入浏览器发送请求,查询对应结果。

    通过结果可以看出,Feign的确实现了负载均衡,且默认为轮询策略其实Feign不是做负载均衡的,负载均衡是Ribbon的功能,Feign只是集成了Ribbon,负载均衡的功能还是feign内置的Ribbon在做,而不是feign。

    注:还不知道Eeruka如何使用的,请看我之前写的一篇”SpringCloud-Eureka配置“博客!

    OpenFeign的配置与使用就此告一段落,其他详细的配置可以去官网查阅!!

3.通过源码浅谈service接口可以@Autowired的问题

现在让我们看看@FeignClient的源码

/**
*导入包省略
*/

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    String contextId() default "";

    @AliasFor("value")
    String name() default "";

    /** @deprecated */
    @Deprecated
    String qualifier() default "";

    String[] qualifiers() default {};

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {}; //配置类,可自行实现

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";

    boolean primary() default true;
}

由上可以看出该注解没有复合@Configuration或@Component,那么通过它标注的服务接口又是如何被Spring托管的呢?

对此我从@EnableFeignClients注解入手,源码如下:

@EnableFeignClients

/**
*导入包省略
*/

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

我们可以看到该注解中import了FeignClientsRegistrar.class,再加上在启动类上,配合@SpringBootApplication,该类最后将被实现,对此我们进一步分析该类:

FeignClientsRegistrar.class(部分相关源码)

/**
*导入包省略
*/

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    private ResourceLoader resourceLoader;
    private Environment environment;

        /**
        *	略
        */
    FeignClientsRegistrar() {
    }

    static String getName(String name) {
        if (!StringUtils.hasText(name)) {
            return "";
        } else {
            String host = null;

            try {
                String url;
                if (!name.startsWith("http://") && !name.startsWith("https://")) {
                    // 由此可以看出届时请求将会被拼接为http://服务实例名称
                    url = "http://" + name;
                } else {
                    url = name;
                }

                host = (new URI(url)).getHost();
            } catch (URISyntaxException var3) {
            }

            Assert.state(host != null, "Service id not legal hostname (" + name + ")");
            return name;
        }
    }

   		/**
        *	略
        */

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 集合,不重复
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
        if (clients != null && clients.length != 0) {
            Class[] var12 = clients;
            int var14 = clients.length;

            for(int var16 = 0; var16 < var14; ++var16) {
                Class<?> clazz = var12[var16];
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        } else {
            ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            
            // 查询FeignClient,即扫描过滤出所有的带@FeignClient
            scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            Set<String> basePackages = this.getBasePackages(metadata);
            Iterator var8 = basePackages.iterator();

            while(var8.hasNext()) {
                String basePackage = (String)var8.next();
                
                //将basePackage下的所有带@FeignClient标识的接口添加到candidateComponents中
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }

        Iterator var13 = candidateComponents.iterator();

        // 迭代器,遍历进行FeignClient注册,注册到Spring容器中
        while(var13.hasNext()) {
            BeanDefinition candidateComponent = (BeanDefinition)var13.next();
            if (candidateComponent instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
                //annotationMetadata 对应的接口
                AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
                Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
                String name = this.getClientName(attributes);
                // 拿到@FeignClient中configuration并进行配置替换
                this.registerClientConfiguration(registry, name, attributes.get("configuration"));
                // 将接口和@FeignClient中参数配置并注册到Spring容器中
                this.registerFeignClient(registry, annotationMetadata, attributes);
            }
        }

    }
	// 注册详细源码如下
    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);
        ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;
        String contextId = this.getContextId(beanFactory, attributes);
        String name = this.getName(attributes);
        FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
        factoryBean.setBeanFactory(beanFactory);
        factoryBean.setName(name);
        factoryBean.setContextId(contextId);
        // clazz接口类型设置,即@FeignClient标注下的接口
        factoryBean.setType(clazz);
        factoryBean.setRefreshableClient(this.isClientRefreshEnabled());
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
            factoryBean.setUrl(this.getUrl(beanFactory, attributes));
            factoryBean.setPath(this.getPath(beanFactory, attributes));
            factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
            Object fallback = attributes.get("fallback");
            if (fallback != null) {
                factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));
            }

            Object fallbackFactory = attributes.get("fallbackFactory");
            if (fallbackFactory != null) {
                factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));
            }

            return factoryBean.getObject();
        });
        definition.setAutowireMode(2);
        definition.setLazyInit(true);
        this.validate(attributes);
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute("factoryBeanObjectType", className);
        beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
        boolean primary = (Boolean)attributes.get("primary");
        beanDefinition.setPrimary(primary);
        String[] qualifiers = this.getQualifiers(attributes);
        if (ObjectUtils.isEmpty(qualifiers)) {
            qualifiers = new String[]{contextId + "FeignClient"};
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
        // 注册为bean到容器中
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        this.registerOptionsBeanDefinition(registry, contextId);
    }
    
 		/**
        *	略
        */

结论: 通过短暂的源码分析可以看出,虽然@FeignClient没有复合@Configuration或@Component,但在注册FeignClientsRegistrar的过程中,已经通过 BeanDefinitionReaderUtils.registerBeanDefinition将所标识的接口注册到容器中,所以可以使用@Autowired拿到容器中对应的service接口。

4.结语

源码部分没有很详细深究,如果有大佬深入了解过源码,上文若有错的地方请大佬不吝指出,我将进行改正和学习,最后感谢各位的观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值