SpringCloud系列之服务消费者Feign(四)

上一篇文章介绍了springcloud的负载均衡组件ribbon,这篇文章继续介绍负载均衡的第二种组件fegin,OK 开始fegin的学习路程吧!

 

Fegin是什么?

Feign : Declarative REST clients。 

Feign 是一个声明web服务客户端,这便得编写web服务客户端更容易,使用Feign 创建一个接口并对它进行注解,它具有可插拔的注解支持包括Feign注解与JAX-RS注解,Feign还支持可插拔的编码器与解码器,Spring Cloud 增加了对 Spring MVC的注解,Spring Web 默认使用了HttpMessageConverters, Spring Cloud 集成 Ribbon 和 Eureka 提供的负载均衡的HTTP客户端 Feign

Fegin功能使用

& 基础fegin功能演示

继续在上一个项目当中新建立一个模块叫看一下整体项目的结构:

首先看一下pom.xml的文件内容,

 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>server_fegion</artifactId>
        <groupId>com.suning.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user_server_consumer_fegion</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>

       <!--引入这个包才能使用fegin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-netflix-core</artifactId>
            <version>1.3.6.RELEASE</version>
        </dependency>

    </dependencies>
</project>

红色标注的就是springcloud集成feign的依赖组件...,接下来需要定义一个fegin接口,代码如下

 

package com.server.fegin.fegin;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @Author 18011618
 * @Description 定义一个fegin
 * @Date 20:53 2018/7/10
 * @Modify By
 */
@FeignClient("service-provider-user")
public interface FeginClient {
    @RequestMapping(value = "/getInfo",method = RequestMethod.GET)
    String showInfo(@RequestParam(value ="name") String name);
}
 

我这里就是为了演示一个简单的负载均衡,所以代码功能很简单,但是生产环境中有些坑是要避免的,这点和ribbon是不一样的

> 这里不能使用GetMapping,PostMapping这种复合mapping,而要使用RequestMapping

> @PathVariable,@RequestParam必须要给value进行赋值

>如果方法里面的参数是复杂对象,尽快指定是post,但是fegin默认还是以post方法进行请求

>这里定义的只能是接口,且不需要实现,但是有一个要求就是这么的@RequestMapping中的value要和服务提供者里面的方法是一致的,这样才能调得通,方法名可以不一样.

:这个注解代表开启负载均衡机制,后面的value是服务提供者的名称。接下来再看一下对应的controller代码

 

package com.server.fegin.controller;

import com.server.fegin.fegin.FeginClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 18011618
 * @Description
 * @Date 21:15 2018/7/10
 * @Modify By
 */
@RestController
public class FeginController {


    @Autowired
    private FeginClient feginClient;

    //Required String parameter 'message' is not present
    @RequestMapping(value = "/getInfo",method = RequestMethod.GET)
    public String showInfo(@RequestParam String name){
        return feginClient.showInfo(name);
    }
}
 

很简单把刚刚定义的FeginClient接口注入进来,调用对应的方法接口,好了就剩一个main方法的类

 

package com.server.fegin;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

/**
 * @Author 18011618
 * @Description
 * @Date 21:24 2018/7/10
 * @Modify By
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServerFeginApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerFeginApplication.class,args);
    }
}
 

相比ribbon多了一个新注解,代表开启Fegin负载均衡,好了最好看一下配置文件,几乎没啥变化

 

spring:
  application:
    name: server-fegin
server:
  port: 8013
eureka:
  client:
    healthcheck:
      enabled: true
    serviceUrl:
      defaultZone: http://admin:admin@localhost:8080/eureka
  instance:
    prefer-ip-address: true

上面就把消费者的代码写完了,然后我们修改上一章节服务提供者的controller里面的方法:

对应的controller代码如下所示:

 

package com.suning.provider.controller;

import com.suning.provider.bean.User;
import com.suning.provider.respoitory.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 18011618
 * @Description 用户提供服务的controller
 * @Date 9:51 2018/7/9
 * @Modify By
 */
@RestController
public class UserController {

    @Value("${server.port}")
    String port;

    @Autowired
    private UserRepository userRepository; //数据访问层接口

    @GetMapping("/findUser/{id}")
    public User findUserById(@PathVariable long id){
        return this.userRepository.findOne(id);
    }


    @GetMapping("/getInfo")
    public String getServerPort(@RequestParam String name){
        System.out.println(name+",this from server port is:"+port);
        return name+",this from server port is:"+port;
    }
}

 

然后修改对应的配置文件端口号为9001和9002,来启动两个服务提供者的实例,然后在浏览器端输入 

http://localhost:8013/getInfo?name=i am jhp ,i am do test fegin,连续刷新浏览器会出现如下的截图

这里就可以很明显的看出来 一个请求访问的是9001端口,一个请求访问的是9002端口..

& 自定义配置覆盖fegin的默认配置

有时候可能业务需求,需要能够自定义配置功能,这个fegin和ribbon都是支持的,看一下官网的介绍

 

在Spring Cloud中,Feign的默认配置类是FeignClientsConfiguration,该类定义了Feign默认使用的编码器、解码器、所使用的契约等。

  Spring Cloud允许通过注解@FeignClient的configuration属性自定义Feign的配置,自定义配置的优先级比FeignClientsConfiguration要高。

  在Spring Cloud的文档中可以看到以下段落,描述了Spring Cloud提供的默认配置。

 

 

也就是说我们自定义fegin可以包含上面的6个部分,下面就演示其中一到两个的配置:

为了便于演示,还是再新建立一个模块叫,看一下项目结构

首先看一下自定义的配置

 

package com.server.fegin.custom.config;

import feign.Feign;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import feign.Contract;
import feign.Logger;

/**
* @Author 18011618
* @Date 0:45 2018/7/11
* @Function 覆盖默认fegin的配置
*/
@Configuration
public class FeginConfiguration {

  /**
  * @Author 18011618
  * @Date 0:45 2018/7/11
  * @Function 覆盖springmvc的默认注解 使用fegin自身的注解
  */
  @Bean
  public Contract feignContract() {
    return new Contract.Default();
  }

  /**
  * @Author 18011618
  * @Date 0:45 2018/7/11
  * @Function 改变日志等级
  */
  @Bean
  Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
  }

  /**
  * @Author 18011618
  * @Date 0:49 2018/7/11
  * @Function 覆盖默认的HystrixFegin.Builder
  */
//  @Bean
//  Feign.Builder feignBuilder(){
//    return null;
//  }

}
 

自定义FeginClient,代码如下

 

package com.server.fegin.custom.fegin;

import com.server.fegin.custom.config.FeginConfiguration;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * @Author 18011618
 * @Description 定义一个fegin
 * @Date 20:53 2018/7/10
 * @Modify By
 */
@FeignClient(name = "service-provider-user",configuration = FeginConfiguration.class)
public interface UserFeginClient {
    //使用fegin自带的注解 不能使用springmvc 因为启动不了
    @RequestLine("GET /getInfo")
    String showInfo(@Param(value = "name") String name);
}
 

这里要在引入新自定义的配置类,然后红色标注对的注解也改了,只能使用fegin原生的注解(这个可以去netfilx的github去看fegin的源码),然后main方法的主类

 

package com.server.fegin.custom;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

/**
 * @Author 18011618
 * @Description 实现覆盖fegin的默认配置功能
 * @Date 0:28 2018/7/11
 * @Modify By
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeginCustomConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(FeginCustomConfigApplication.class,args);
    }
}
 

没有任何变化,对应的controller也没有任何变化,只是换了个FeginClient而已

 

package com.server.fegin.custom.controller;
import com.server.fegin.custom.fegin.UserFeginClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
* @Author 18011618
* @Date 16:12 2018/7/9
* @Function
*/
@RestController
public class UserInfoController {

  @Autowired
  private UserFeginClient feginClient;

  //Required String parameter 'message' is not present
  @RequestMapping(value = "/getInfo",method = RequestMethod.GET)
  public String showInfo(@RequestParam String name){
    return feginClient.showInfo(name);
  }
}
 

配置文件也没有什么变化,只是为了区分,所以改一个端口号

 

spring:
  application:
    name: server-fegin-custom
server:
  port: 8014
eureka:
  client:
    healthcheck:
      enabled: true
    serviceUrl:
      defaultZone: http://admin:admin@localhost:8080/eureka
  instance:
    prefer-ip-address: true

这个时候启动main方法的类,然后浏览器端访问http://localhost:8014?name = i am jhp ,i doing test fegin,会发现效果和上面是一致的,这个就代表了我们自定义的config生效了,和fegin相关的负载均衡功能就讲完了,接下来简单介绍一下相关源码.

& fegin的源码分析

入口处

在@EnableFeignClients标签中,import了一个FeignClientsRegistrar类,那就看一下这个类,仔细阅读源码会发现一个很重要的方法

方法里面有两个子方法,分别是加载默认配置和fegin的,

这个方法是用来加载默认配置的,看一下这个方法的具体实现:

 

private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   Map<String, Object> defaultAttrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

   if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
      String name;
      if (metadata.hasEnclosingClass()) {
         name = "default." + metadata.getEnclosingClassName();
      }
      else {
         name = "default." + metadata.getClassName();
      }
      registerClientConfiguration(registry, name,
            defaultAttrs.get("defaultConfiguration"));
   }
}

如果用户没有配置configuration就使用默认的,然后它会注册配置:registerClientConfiguration:

 

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
         name + "." + FeignClientSpecification.class.getSimpleName(),
         builder.getBeanDefinition());
}

配置加载好了,就会注册feginclient客户端:继续看

 

public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);

   Set<String> basePackages;

   Map<String, Object> attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   final Class<?>[] clients = attrs == null ? null
         : (Class<?>[]) attrs.get("clients");
   if (clients == null || clients.length == 0) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
   }
   else {
      final Set<String> clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      for (Class<?> clazz : clients) {
         basePackages.add(ClassUtils.getPackageName(clazz));
         clientClasses.add(clazz.getCanonicalName());
      }
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
         @Override
         protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\\$", ".");
            return clientClasses.contains(cleaned);
         }
      };
      scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
   }

   for (String basePackage : basePackages) {
      Set<BeanDefinition> candidateComponents = scanner
            .findCandidateComponents(basePackage);
      for (BeanDefinition candidateComponent : candidateComponents) {
         if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            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 = getClientName(attributes);
            registerClientConfiguration(registry, name,
                  attributes.get("configuration"));

            registerFeignClient(registry, annotationMetadata, attributes);//真正执行注册的方法
         }
      }
   }
}
 

再看一下...这个方法...

 

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
   String className = annotationMetadata.getClassName();
   BeanDefinitionBuilder definition = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientFactoryBean.class);
   validate(attributes);
   definition.addPropertyValue("url", getUrl(attributes));
   definition.addPropertyValue("path", getPath(attributes));
   String name = getName(attributes);
   definition.addPropertyValue("name", name);
   definition.addPropertyValue("type", className);
   definition.addPropertyValue("decode404", attributes.get("decode404"));
   definition.addPropertyValue("fallback", attributes.get("fallback"));
   definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
   definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

   String alias = name + "FeignClient";
   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

   boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

   beanDefinition.setPrimary(primary);

   String qualifier = getQualifier(attributes);
   if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
   }

   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
         new String[] { alias });
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
 

这个方法就是把所有能够配置的参数都进行了注册,到这里fegin自身源码就看完了,那么还剩下一个关键问题,就是spring是如何初始化它(在哪里调用它?)跟着Spring的源码走下去,看过源码的人都会直接看到AbstractApplicationContext#refresh()方法,整体整理一下代码:其实这个方法非常重要,因为spring的其他很多的组件也是在这里进行处理的,比如spring io,di,aop等

 

@Override
public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         postProcessBeanFactory(beanFactory);

         // 这里调用了FeignClientsRegistrar
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         registerBeanPostProcessors(beanFactory);

         // Initialize message source for this context.
         initMessageSource();

         // Initialize event multicaster for this context.
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         onRefresh();

         // Check for listener beans and register them.
         registerListeners();

         // Instantiate all remaining (non-lazy-init) singletons.
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

其它的方法就不细说了

版权声明:本文为博主原创文章,未经博主允许不得转载:https://blog.csdn.net/qq_18603599/article/details/80941683

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值