(四)使用Feign实现声明式Rest调用

使用Feign实现声明式Rest调用

1.什么是Feign

Feign是一个http请求调用的轻量级框架。使用Feign,可以直接以Java接口注解的方式发送Http请求,而不需要在Java中通过封装HTTP工具类来发送请求。
Feign源码地址:Feign

2.Feign解决了什么问题

Feign封装了Http 请求调用流程,实现了申明式Http接口调用。
使用Feign的方式调用远程服务,服务消费者与生产者不需要实现同一个接口,可以做到消费者与生产者代码上的完全解耦。就代码耦合而言,Feign与jdk提供的rmi和阿里的dubbo有所差异,后者进行远程服务调用需要实现共同的api。另外服务消费者一方只需要关注FeignClient配置即可,不需要关注具体Http Request的实现,所以说Feign最终目的是将Java Http客户端调用过程变得简单。

从角色职能划分,Feign提供http调用服务流程如下:

在这里插入图片描述

3.Feign工作原理

Feign是一个伪java客户端,Feign不做任何的请求处理。Feign通过处理注解生成Request模板,从而简化了Http API的开发,开发人员可以使用注解的方式定制Request API模板。在发送Http Request请求之前,Feign通过处理注解的方式替换掉Request模板中的参数,生成真正的Request,并交给Java Http客户端处理。

综合来讲,发送一个Http请求,Feign做了两件事情:

​1、Http请求处理流程封装,包含:请求行、请求头、请求体、响应;

2、选择Http Client发送请求。

我们在通过源码去理解Feign原理的时候,不妨带着这两个问题,从源码中理解,Feign是如何处理这两个问题的,这样对于我们理解Feign会有所帮助。

3.1.流程梳理

​ 我们可以把Feign处理Http请求的基本流程分为两个部分,第一部分是初始化阶段,进行Proxy和MethodHandler的创建,第二部分则是具体请求处理流程。

3.1.1.初始化流程

初始化流程基本如下:

在这里插入图片描述

Feign初始化过程基本分为两个部分:

1.ReflectiveFeign根据指定的Contract为每一个方法创建了一个SynchronousMethodHandler;

2.基于动态代理,为Target接口创建了一个proxy对象,同时定义一个统一的InvocationHandler用于请求处理,将请求分发到指定的SynchronousMethodHandler处理。

3.1.2.Request处理过程

Request处理过程基本如下:

在这里插入图片描述

Feign封装了整个Request的处理过程,按照请求顺序,如下:

1.具体方法处理类SynchronousMethodHandler创建请求模板;

2.对Request请求进行预处理,编码;

3.将Request交给client去执行处理,若有拦截器先执行拦截器;

4.返回结果处理,解码;

5.返回结果最终转化为javaBean交付给具体方法处理类SynchronousMethodHandler。

3.2.FeignClient注册

在@EnableFeignClients标签中,import了FeignClientsRegistrar,通过FeignClientsRegistrar的registerBeanDefinitions方法完成了FeignClient的Bean的注入。程序启动时,会检查是否有@EnableFeignClients注解,如果有,则会执行FeignClientsRegistrar的registerBeanDefinitions方法。其中registerBeanDefinitions代码如下:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {
  	//扫描EnableFeignClients标签里配置的信息,注册到beanDefinitionNames中。
	registerDefaultConfiguration(metadata, registry);
	registerFeignClients(metadata, registry);
}

其中registerFeignClients完成了对FeignClient的注册,代码如下:

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());
  // 定义Filter规则,用于scanner过滤
  AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
    FeignClient.class);
  // 获取EnableFeignClients注解中的clients属性的值
  final Class<?>[] clients = attrs == null ? null
    : (Class<?>[]) attrs.get("clients");
  if (clients == null || clients.length == 0) {
    scanner.addIncludeFilter(annotationTypeFilter);
    basePackages = getBasePackages(metadata);
  }
  else {
    // @EnableFeignClients提供了clients属性用于指定扫描的clients
    // 存在指定的clients,依次将client所在的package添加到basePackages
    ...// 省略代码
  }
  for (String basePackage : basePackages) {
    // 从basePackage中扫描到的FeignClient
    Set<BeanDefinition> candidateComponents = scanner
      .findCandidateComponents(basePackage);
    for (BeanDefinition candidateComponent : candidateComponents) {
      if (candidateComponent instanceof AnnotatedBeanDefinition) {
        AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
        AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
        // 注意,@FeignClient只能标注在接口上
        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);
        /**
         * 关键地方:Feign子容器概念:
         * 在注入FeignAutoConfiguration类的时候,注入了一个FeignContext对象,这个就是Feign的子容器。
         * 这里面装了List<FeignClientSpecification>对象,FeignClientSpecification对象的实质就是在@feignClient上配置的configuration指定对象的值
         * 这个地方比较关键,主要是因为后期对feign客户端的编码解码会用到自定义的类
         */
        registerClientConfiguration(registry, name,
                                    attributes.get("configuration"));
        // 注册feignClient
        registerFeignClient(registry, annotationMetadata, attributes);
      }
    }
  }
}

​ 大致逻辑如下:

​ 1.获取EnableFeignClients注解的相关属性;

​ 2.定义按照FeignClient注解过滤的过滤器annotationTypeFilter;

​ 3.根据注解和定义的过滤规则确定扫描范围basePackages,basePackages默认是启动类的同级目录,若EnableFeignClients指定了clients,则basePackages是clients指定的每一个类的同级目录的集合;

​ 4.扫描basePackages中FeignClient,依次注入到Spring容器中。

​ 从上文中,我们可以了解到在registerBeanDefinitions是方法中完成了FeignClient的Bean注入,那么registerBeanDefinitions这个方法又是在上面时候执行的呢?我们不妨进一步探索一下,跟着Spring的源码走下去,看过源码的人都会直接看到AbstractApplicationContext#refresh()方法,整体整理一下代码:

@Override
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
      	// Prepare this context for refreshing.
		prepareRefresh();
        // 扫描本项目里面的java文件,把bean对象封装成BeanDefinitiaon对象,
      	//然后调用DefaultListableBeanFactory#registerBeanDefinition()方法把beanName放到DefaultListableBeanFactory 的 List<String> beanDefinitionNames 中去
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  		// Prepare the bean factory for use in this context.
        prepareBeanFactory(beanFactory);
        try {
            postProcessBeanFactory(beanFactory);
            // 在这里调用到FeignClientsRegistrar对象的registerBeanDefinitions()方法
            invokeBeanFactoryPostProcessors(beanFactory);
            //从DefaultListableBeanFactory里面的beanDefinitionNames中找到所有实现了BeanPostProcessor接口的方法,如果有排序进行排序后放到list中
            registerBeanPostProcessors(beanFactory);
            //Spring的国际化
            initMessageSource();
            initApplicationEventMulticaster();
            // Initialize other special beans in specific context subclasses.
            onRefresh();
            registerListeners();
            // Spring的IOC、ID处理。Spring的AOP。事务都是在IOC完成之后调用了BeanPostProcessor#postProcessBeforeInitialization()和postProcessBeforeInitialization()方法,AOP(事务)就是在这里处理的
            finishBeanFactoryInitialization(beanFactory);
            // 执行完之后调用实现了所有LifecycleProcessor接口的类的onRefresh()方法,同时调用所有观察了ApplicationEvent接口的事件(观察者模式)
            finishRefresh();
        }
        catch (BeansException ex) {
            // 找到所有实现了DisposableBean接口的方法,调用了destroy()方法,这就是bean的销毁
            destroyBeans();
            // Reset 'active' flag.
            cancelRefresh(ex);
            throw ex;
        }
        finally {
            resetCommonCaches();
        }
    }
}

​ 根据上面整理的代码发现,FeignClientsRegistrar#registerBeanDefinitions()方法是在扫描完bean之后,只放了一个beanname的情况下, 并没有进行IOC注册的时候调用的,这就是Spring动态扩展Bean。另外,实现BeanDefinitionRegistryPostProcessor接口的所有方法也会在这里调用下postProcessBeanDefinitionRegistry()方法。

​ 总结一下:

​ 我们平时工作和学习中,留心的话,不难发现:spring作为整合专家,在整合其它框架时存在一个基本套路:1.自定义三方注解、2.定义注册器Registrar,扫描注解标准类注入到spring的IoC容器中。Feign正是其中之一,对这方面比较感兴趣的话,不妨去深入研究一下spring自定义注解和spring-bean。

3.3.创建代理

​ 注入BeanDefinition之后, ReflectiveFeign内部使用了jdk的动态代理为目标接口生成了一个代理类,这里会生成一个InvocationHandler统一的方法处理器,同时为接口的每个方法生成一个SynchronousMethodHandler拦截。

​ 下面围绕两个方面讲述:

​ 1、如何创建代理,创建的是谁的代理;

​ 2、请求是怎么分发到具体的SynchronousMethodHandler方法处理器的。

​ ReflectiveFeign#newInstance代码如下:

public <T> T newInstance(Target<T> target) {
  	// 为每一个method创建一个MethodHandler
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
  	// method 容器,key为targetd的Method 
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if(Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
    // 创建InvocationHandler,总创建InvocationHandler,最终会分到具体的SynchronousMethodHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
  	// 创建代理对象,经过源码追踪可以了解到target即为@FeignClient注解标注的接口
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

​ 上面代码中targetToHandlersByName.apply(target),根据contract协议为每一个method创建了一个MethodHandler,具体实现类是SynchronousMethodHandler,代码如下:

public Map<String, MethodHandler> apply(Target key) {
  List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
  Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
  for (MethodMetadata md : metadata) {
    BuildTemplateByResolvingArgs buildTemplate;
    ···// 省略代码
    result.put(md.configKey(),
               factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
  }
  return result;
}

​ 进入factory.create可以发现,实际上创建的MethodHandler即为SynchronousMethodHandler,代码如下:

public MethodHandler create(Target<?> target, MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options, Decoder decoder, ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
                                          logLevel, md, buildTemplateFromArgs, options, decoder,
                                          errorDecoder, decode404);
}        

​ 另外,在创建InvocationHandler的时候,我们发现传入的参数是methodToHandler,从上文中可知,其中key为method,value为SynchronousMethodHandler对象。继续代码跟进,可以发现创建的InvocationHandler实际上就是FeignInvocationHandler,并且将methodToHandler赋值给了dispatch,代码如下:

public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
  return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
}
FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
  this.target = checkNotNull(target, "target");
  this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
}

​ 总结一下:

​ 1.ReflectiveFeign内部使用了jdk的动态代理为目标接口(@FeignClient注解标注的接口)生成了一个代理类,并且生成统一的InvocationHandler;

​ 2.为每一个Method创建SynchronousMethodHandler,并将方法及放入dispatch方法容器中,其中key为Method,保证key的唯一性,value为具体的SynchronousMethodHandler。那么在后面的接口调用中,则可以通过具体的Method获取到具体的SynchronousMethodHandler了。

3.4.接口调用

​ 根据上文创建代理部分可知,当调用Feign Client接口里面的方法时,该方法会被FeignInvocationHandler拦截,并且调用invoke方法,在invoke方法中,完成了分发到指定的SynchronousMethodHandler处理的动作,代码如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  ···// 省略代码
  return dispatch.get(method).invoke(args);
}

​ SynchronousMethodHandler处理请求时,根据传入参数生成RequestTemplate对象,该对象即为请求模板,代码如下:

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    // 根据Target接口中的方法注解,创建请求模板
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    // 获取重试策略,默认不重试
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        // 请求异常重试
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

在 executeAndDecode()方法中,通过RequestTemplate创建Request请求对象,然后用Http Client执行request,即通过Http Client进行Http请求获取结果,代码如下:

Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);
     ···// 省略代码,参数编码
      // client发送request请求
      response = client.execute(request, options);
     ···// 省略代码,response解码
 }

此处,进入 targetRequest()方法,发现执行了一些列的拦截器,代码如下:

Request targetRequest(RequestTemplate template) {
  	// 执行拦截器
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
  	// 生成Request并返回
    return target.apply(new RequestTemplate(template));
}

​ 总结一下调用步骤:

​ 1.以method为key,获取到具体的SynchronousMethodHandler;

​ 2.创建请求模板;

​ 3.获取重试策略;

​ 4.执行拦截器;

​ 5.创建Request;

​ 6.参数编码;

​ 7.发送请求;

​ 8.response解码;

​ 9.请求异常,执行重试策略。

3.5.重试策略

从SynchronousMethodHandler的invoke方法中可以看到,声明了一个重试器Retryer,在请求执行失败后会根据重试策略进行请求重试,调用Retryer的continueOrPropagate方法。从FeignClientsConfiguration代码中可以看到默认定义的Retryer是不进行重试的,因为continueOrPropagate方法直接抛出了异常,代码如下:

@Configuration
public class FeignClientsConfiguration {
    ···// 省略代码
  	@Bean
    @ConditionalOnMissingBean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }
  	···// 省略代码
}
public interface Retryer extends Cloneable {
  	···// 省略代码
	Retryer NEVER_RETRY = new Retryer() {
      	// 重试方法,直接返回异常,不重试
        @Override
        public void continueOrPropagate(RetryableException e) {
          throw e;
        }
        @Override
        public Retryer clone() {
          return this;
        }
    };
  	···// 省略代码
}

​ 所以说,如果需要具有重试功能,可以重新定义一个Retryer覆盖默认的即可,Feign也默认提供Retryer.Default的重试策略,可以定义好重试参数后直接使用,不需要拓展重试策略了。

注意:

​ spring-cloud-feign之所以默认Retryer.NEVER_RETRY,即不重试,是因为spring-cloud-feign整合了ribbon,ribbon也有重试策略,如果fegin也开启重试策略,容易造成混乱。如果feign单独使用的情况下,建议定义一下重试策略。

3.6.Client动态注入

​ 看到这里,其实还有一个疑问,执行Http请求使用的Client是什么时候初始化的,整体一下,提供两个点思考方向:

​ 1.发送http请求的client工具类是怎么集成进去的;

​ 2.Feign是怎么实现负载均衡的。

​ 先看第1个问题:

​ Feign默认集成了3种Http调用工具,分布为:ApacheHttpClient、OkHttpClient、HttpURLConnection。默认情况下使用的是HttpURLConnection,当引入ApacheHttpClient依赖时,client即为ApacheHttpClient,想切换为OkHttpClient,只需要将依赖替换为OkHttpClient即可。相关加载原理可以查看FeignAutoConfiguration类中对于ApacheHttpClient和OkHttpClient的加载条件。

​ 对于性能有要求的项目中,建议不要使用HttpURLConnection,可以使用OkHttpClient或者ApacheHttpClient,对这3个工具类性能有兴趣的同学,可以深入了解一下,做一下对比。

​ 接下来说一下第2个问题:

​ 用过Feign的同学可能知道,spring-cloud-feign是支持负载均衡的,而第1个问题中提到的3种http工具类本身是不支持负载均衡的。那么,Feign是怎么保证初始化在内存中的client能够进行负载均衡的呢?这里的client有两个实现类,分别是Client.Defaut和LoadBalancerFeignClient,而默认值是Client.Defaut。继续阅读源码,发现FeignClientFactoryBean中的loadBalance方法会重置client,程序启动时,从这里使用LoadBalancerFeignClient的实例覆盖了默认的Client.Defaut,代码如下:

class FeignClientFactoryBean implements FactoryBean<Object>, 	    InitializingBean,ApplicationContextAware {
    ···// 省略代码
    @Override
    public Object getObject() throws Exception {
        ···// 省略代码
        if (!StringUtils.hasText(this.url)) {
        ···// 省略代码
            return loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
        ···// 省略代码
    }
    ···// 省略代码
    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
        Client client = getOptional(context, Client.class);
        if (client != null) {
            // 重置client,设置为loadBalanceClient
            builder.client(client);
            Targeter targeter = get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        }
        ···// 省略代码
    }
    ···// 省略代码
}

​ 总结一下:

​ Client的注册使用了动态注入的方式,其实现逻辑是根据FeignClient是否配置了指定的url,如果没有配置url则使用负载均衡策略,配置了url,则直接使用url绑定的服务。我们平时编码直接使用@Service注解,而这种方式是静态注入。

4.Feign使用示例

4.1.原生Feign使用

4.1.1.需求

  1. 用户下单;
  2. 通过资金账号查询资产;
  3. 下单时对用户等级做身份认证,给高等级的机构用户提供快速下单渠道。

4.1.2.创建一个服务端

​ 新建一个Spring Boot的Moudle工程,命名traderServer-1,满足需求中的相关接口,controller中代码如下:

@RestController
@RequestMapping("/trade")
public class TradeController {
    @RequestMapping(value="/queryFund")
    public String queryFund(String account) {
        return "tradeServer-1,账户余额:1,000,000";
    }
    @RequestMapping(value="/order")
    @ResponseBody
    public String orderJSON(String stock, Double price, Double count) {
        return "tradeServer-1,下单成功";
    }
    @RequestMapping(value="/orderJSON")
    @ResponseBody
    public String orderJSON(@RequestBody JSONObject body) {
        return "tradeServer-1,下单成功";
    }
}

4.1.3.新建原生Feign客户端

public interface ITradeService {
    @RequestLine("GET /trade/queryFund?account={account}")
    String queryFund(@Param("account") String account);

    @RequestLine("POST /trade/order")
    @Headers("Content-Type: application/json")
    @Body("%7B\"stock\": \"{stock}\", \"price\": {price}, \"count\":{count}%7D")
    String order(@Param("stock") String stock, @Param("price") Double price, @Param("count") Double count);
}

public class TestFeign {
    public static void main(String[] args) {
        ITradeService tradeService = Feign.builder()
                .options(new Options(2000, 6000))
                .target(ITradeService.class, "http://localhost:2002");
        String result = tradeService.queryFund("xumiao");
        System.out.println(result);
        result = tradeService.order("300033", 20.0, 1000.0);
        System.out.println(result);
    }
}

​ 从上文中可以看到,Feign支持get和post请求,并且新定义了一套注解,这种方式一定程度上提高了学习成本。Spring对feign进行整合后,对Spring MVC注解做了一定程度上的支持,基本满足项目中的使用,推荐使用Spring MVC注解。Feign默认的协议规范,如下:

在这里插入图片描述

4.2.Spring-cloud-feign用法

4.2.1.新建一个Feign客户端

​ 新建一个Spring Boot的Moudle工程,命名spring-cloud-feign,在pom文件中加入相关依赖,application.yml文件中增加eureka相关配置启动类增加@EnableFeignClients注解,开启Feign Client功能,该程序就具备了Feign功能了。
根据需求,只需要创建一个包含资金查询和交易下单的接口即可,在接口上加@FeignClient注解来声明一个Feign Client,其中name为远程调用其他服务的服务名,本工程中使用eureka作为注册中心,则name的值即为服务端注册在eureka中的服务名称,代码如下:

@FeignClient(name = "trade-server")
public interface ITradeService {
    @RequestMapping(value = "/trade/queryFund")
    String queryFund(@RequestParam("account") String account);
  
    @RequestMapping(value = "/trade/order")
    String order(@RequestParam("stock") String stock, 				 			@RequestParam("price") Double price,
            @RequestParam("count") Double count);
  
    @RequestMapping(value = "/trade/orderJSON")
    String orderJson(@RequestBody JSONObject json);
}

​ 新增相关controller,提供外部调用接口,使用ITradeService进行相关远程服务的调用,部分代码如下:

@RestController
@RequestMapping("/trade")
public class TradeController {
    @Autowired
    private ITradeService tradeService;
    @RequestMapping(value="/queryFund")
    public String queryFund(String account) {
        return tradeService.queryFund(account);
    }
    ...
}

至此,已经完成需求中的下单和资金查询的功能,至于普通用户和机构客户的身份认证可以通过拦截器来实现,下文将在讲述拦截器的时候进一步实现该功能。浏览器中访问http://localhost:2000/trade/queryFund?account=3302…即可访问提供的服务了。结果如下:

在这里插入图片描述

复制traderServer-1命名为traderServer-2,同时启动traderServer-1和traderServer-2,发现Feign具备负载均衡功能。因为Feign本身并不支持负载均衡,属于Ribbon中的内容,有兴趣的同学建议去了解一下Ribbon。至此,工程架构图如下:

5.FeignClient配置

5.1.常规配置

​ FeignClient默认的配置类为FeignClientsConfiguration,打开这个类,可以发现这个类注入了很多Feign相关的配置Bean,包括Retryer、FeignLoggerFactory、FormattingConversionService等。另外,Decoder、Encoder和Contract这3个类使用@ConditionalOnMissingBean标记,即在没有Bean注入的情况下,会自动注入默认配置的Bean,部分代码如下:

@Configuration
public class FeignClientsConfiguration {
    ···//省略代码
    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }
    
    @Bean
    @ConditionalOnMissingBean
    public Encoder feignEncoder() {
        return new SpringEncoder(this.messageConverters);
    }
    
    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }
    ···//省略代码
}

我们在实际使用中,可根据具体需求覆盖掉 FeignClientsConfiguration类中默认的配置Bean,从而达到自定义配置的目的。例如在Feign默认的配置在请求失败后,重试次数为0。现在希望请求失败后能够重试,这时写一个配置FeignConfig类,在该类中注入Retryer的Bean,覆盖掉默认的Retryer的Bean,并将FeignConfig指定为ITradeService的配置类。

​ FeignConfig类代码如下:

public class FeignConfig {
    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(100, SECONDS.toMillis(1), 5);
    }
}

注意:@FeignClient标注的目标接口类中使用的方法注解一定要与Contract契约相匹配。

我们可以写个例子看一下,不匹配的时候会有什么现象,看下面的例子:

此时,@FeignClient标注接口方法中使用的还是MVC契约的注解,当用Feign原生契约覆盖默认的MVC契约时,在原工厂中新建一个FeignConfiguration 配置类,代码如下:

@Configuration
public class FeignConfiguration 
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }
}

在@Configuration不被注释并且配置类与启动类在统计目录下时,启动服务,会报一个比较常见的错误,如下:

在这里插入图片描述

​ 报错原因:

​ 当把Contract替换为feign.Contract.Default()后,ITradeService中方法上使用的注解还是基于MVC的spring-web包中的注解,两种契约出现冲突,所以抛出此异常。同理,当使用MVC契约,接口中使用Feign原生注解时,也会抛出此异常。

​ 除上述情况之外,在不加@Configuration时,虽然不会启动报错,但是一旦 FeignConfiguration被@FeignClient使用,并且接口被类似@Autowired注解标记,启动会报同样的错,代码如下:

@FeignClient(name = "trade-server", configuration = FeignConfiguration.class)
public interface TradeFeignClient {
	···// 省略代码
}
@RestController
@RequestMapping("/trade")
public class TradeController {
    @Autowired
    private TradeFeignClient tradeFeignClient;
    ···// 省略代码
}

​ 若将改配置类置于@ComponentScan扫描范围(默认启动类同级目录)之外,此时,可启动正常。
​ 根据这个现象,可以得出一个结论:

​ 加了@Configuration注解,那么该类不能存放在主应用程序上下文@ComponentScan所扫描的包中。否则, 该类中的配置的feign.Decoder、feign.Encoder、feign.Contract 等配置就会被所有的@FeignClient共享,一旦Contract契约与注解不匹配时,会出错,所以最好不要混用。

5.2.拦截器配置

​ Client在执行Http Request之前,会执行相关RequestInterceptor拦截器,而Feign中默认也实现了BasicAuthRequestInterceptor,用于访问服务时,进行用户名和密码的基础认证,一般与Spring-cloud-security共同使用。同样,可通过配置类进行拦截器的定义,代码如下:

public class FooConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestlnterceptor() {
        return new BasicAuthRequestInterceptor("organ", "123456");
    }
}
增加上述代码后,引入上述FooConfiguration的FeignClient就具有HttpBasic认证的功能了。

​ 我们再回顾上文中第3个需求,对机构和普通用户做一个身份认证,以便给高等级机构用户提供一个快速下单通道。对于此处的用户身份认证,可以采用spring-cloud-security做基础认证。

​ 具体方案如下:

​ 首先复制一下trade-server工程,命名为trade-server-auth,引入spring-cloud-security相关依赖,增加security相关配置类,代码如下

@SuppressWarnings("deprecation")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级别保护
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
    /**
     * @param authenticationManagerBuilder
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 所有的请求,都需要经过HTTP basici人证
        http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
    }
     @Bean
    public UserDetailsService userDetailsService() {
     InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(
     // 机构角色
     manager.createUser(User.withUsername("organ").password("123456").roles("ORGAN").build());  
     // 个人角色		 
     manager.createUser(User.withUsername("person").password("123456").roles("PERSON").build());
     return manager;
    }
}

​ 上述代码中,约定了organ和person这两种用户可以访问下单服务,所有请求都需要做HttpBasic认证。

​ 服务提供方已经做好相关security的基础认证,服务调用方在调用服务的时候将身份信息传递过来即可,服务提供者根据调用者传入身份信息进行身份认证,机构用户则可以走机构用户下单的快速渠道。

​ 接下来编写客户端相关代码,复制spring-cloud-feign命名为spring-cloud-feign-auth,新建TradeFooClient类引入上文中的FooConfiguration配置,代码如下:

@FeignClient(name = "trade-server-auth", configuration = FooConfiguration.class)
public interface TradeFooClient {
    @RequestMapping(value = "/trade/queryFund")
    String queryFund(@RequestParam("account") String account);

    @RequestMapping(value = "/trade/orderJson")
    String orderJson(@RequestParam(value = "stock") String stock, @RequestParam(value = "price") Double price,
            @RequestParam(value = "count") Double count);
}

​ 此时,我们已经基于拦截器的方式,实现了对用户身份识别,至于上文中第3个需求,机构用户使用快色渠道下单,只需要在识别身份后做对应的分发即可。

6.Feign之负载均衡

​ Fegin本身不支持负载均衡,其整合了Ribbon,通过Ribbon实现负载均衡。

​ FeginRibbonClientAutoConfiguration类通过@Import引入了HttpClientFeignLoadBalancedConfiguration、Ok-HttpFeignLoadBalancedConfiguration、DefaultFeignLoadBalancedConfiguration,不同版本可能有差异,但是目的都是为了配置Client的类型,分别为ApacheHttpClient、OkHttp和HttpURLConnection。3个配置类最终向容器注入的都是Client的实现类LoadBalancerFeignClient,即负载均衡客户端。查看LoadBalancerFeignClient的execute方法,代码如下:

@Override
public Response execute(Request request, Request.Options options) throws IOException {
	···// 省略代码
	return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
			requestConfig).toResponse();
	···// 省略代码
}

​ 其中 executeWithLoadBalancer()方法,即通过负载均衡的方式来执行网络请求。代码继续跟进到LoadBalancerCommand,其中selectServer()方法则为选择服务进行负载均衡的方法,代码如下:

private Observable<Server> selectServer() {
    return Observable.create(new OnSubscribe<Server>() {
        @Override
        public void call(Subscriber<? super Server> next) {
            try {
                Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                next.onNext(server);
                next.onCompleted();
            } catch (Exception e) {
                next.onError(e);
            }
        }
    });
}

由上述代码可知,负载均衡的服务选择策略是 loadBalancerContext实现的,是ribbonloadbalancer包中的类。实际上feign本身是没有负载均衡能力的,spring-cloud-feign整合了ribbon使其具有负载均衡功能。如果需要有效的使用feign的负载均衡功能,建议先熟悉一下ribbon负载均衡的用法。

同时启动两个server时,工程架构图,如下:

7.总结

​ 总的来说,Feign的源码实现过程如下:
​ 1.首先通过@EnableFeignClients注解开启FeignClient功能,只有这个注解存在,才会在程序启动时开启对@FeignClient注解包的扫描。
​ 2.根据Feign的规则实现接口、并在接口上面加上@FeignClient注解。
​ 3.程序启动后,会进行包扫描,扫描所有的@FeignClient的注解类,并将这些信息注入IoC容器。
​ 4.当接口的方法被调用时,通过JDK的代理类生产具体的RequestTemplate模板对象。
​ 5.根据RequestTemplate再生成Http请求的Request对象。
​ 6.Request对象交给Client处理,其中Client的网络请求框架可以是HttpURLConnection、HttpClient和OkHttp。
​ 7.最后Client被封装到LoadBalanceClient类,这个类结合Ribbon做到了负载均衡。

8.参考文献

1.《深入理解Srping Cloud 与微服务构建》

2.https://blog.csdn.net/luanlouis/article/details/82821294

3.https://blog.csdn.net/lgq2626/article/details/80392914

4.https://segmentfault.com/a/1190000014981170

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值