zoukankan      html  css  js  c++  java
  • Spring Cloud专题之二:OpenFeign

    欢迎查看上一篇博客:SpringCloud专题之一:Eureka

    OpenFeign是一种声明式的webservice客户端调用框架。你只需要声明接口和一些简单的注解,就能像使用普通的Bean一样调用远程服务,Ribbon 和 OpenFeign 都可以实现服务调用和实现负载均衡.OpenFeign 也内置了Ribbon.

    OpenFeign是在feign的基础上做了一些修改封装,增加了对Spring Mvc注解的支持.

    OpenFiegn注解讲解

    一般我们会使用@GetMapping和@PostMapping两种方式来调用Rest服务。

    使用@RequestParam和@RequestBody来获取参数

    @RequestBody只能用在Post请求中,并且一个Post请求只能有一个@RequestBody,@RequestBody的参数可以包含复杂类型。

    @RequestParam可以用在Post和Get请求中,但是要注意,@RequestParam的参数只能是基本类型或者是Enum,或者是List和Map(List和Map里只能是基本类型),所以@RequestParam可以和@RequestBody一起使用。

    如果是Get请求,但是有时复合类型怎么办呢?比如我们想传递一个User对象,User对象里面只有普通的两个String属性,就可以使用@SpringQueryMap。@SpringQueryMap的参数是鞥你是普通的POJO,不能是复合类型,否则解析不了,如果必须使用复合类型,那么使用@RequestBody吧。

    多个FeignClient使用同一个name的问题

    在eureka的客户端添加一个类:

    /**
     * @className: Hello1Controller
     * @description: 测试多个feign使用相同的name的问题
     * @author: charon
     * @create: 2021-06-06 09:35
     */
    @RestController
    public class Hello1Controller {
        /**
         * 日志记录类
         */
        private final Logger logger = LoggerFactory.getLogger(getClass());
    
        @Value("${server.port}")
        private String host;
    
        @Value("${spring.application.name}")
        private String instanceName;
    
        @RequestMapping("/sayHello1")
        public String sayHello1(@RequestParam("name") String name){
            logger.info("你好,服务名:{},端口为:{},接收到的参数为:{}",instanceName,host,name);
            return "你好,服务名:"+instanceName+",端口为:"+host+",接收到的参数为:"+name;
        }
    }
    

    eureka消费者端的controller里添加sayHello1(String name)方法:

    /**
     * @className: CustomerController
     * @description:
     * @author: charon
     * @create: 2021-05-19 22:56
     */
    @RestController
    public class CustomerController {
    
        @Autowired
        private CustomerSerivce serivce;
    
        @RequestMapping("/sayHello")
        public String invokeSayHello(){
           return serivce.invokeSayHello();
        }
    
        @RequestMapping("/sayHello1")
        public String invokeSayHello1(String name){
            return serivce.invokeSayHello1(name);
        }
    }
    
    

    接口及实现类:

    /**
     * @className: CustomerSerivce
     * @description:
     * @author: charon
     * @create: 2021-05-19 22:56
     */
    public interface CustomerSerivce {
    
        String invokeSayHello();
    
        String invokeSayHello1(String name);
    }
    
    @Service
    public class CustomerServiceImpl implements CustomerSerivce {
    
        @Autowired
        private CustomerFeign feign;
    
        @Autowired
        private Customer1Feign feign1;
    
        @Override
        public String invokeSayHello() {
            return feign.sayHello();
        }
    
        @Override
        public String invokeSayHello1(String name) {
            return feign1.sayHello1(name);
        }
    }
    

    feignClient:

    /**
     * @className: CustomerFeign
     * @description: @FeignClient使用的value参数,表示从HELLO-SERVER这个服务中调用服务
     * @author: charon
     * @create: 2021-05-19 23:01
     */
    @FeignClient("HELLO-SERVER")
    public interface CustomerFeign {
    
        /**
         * 要求:
         *    返回值要对应,方法名随意,参数值要对应
         *    方法上添加SpringMVC的注解
         * @return
         */
        @RequestMapping("/sayHello")
        String sayHello();
    }
    
    /**
     * @className: Customer1Feign
     * @description: 测试多个feign使用相同的name的问题
     * @author: charon
     * @create: 2021-06-06 09:42
     */
    @FeignClient("HELLO-SERVER")
    public interface Customer1Feign {
        /**
         * 要求:
         *    必须要指定RequestParam属性的value值,同时RequestMethod的method也需要指定
         *    方法上添加SpringMVC的注解
         * @return
         */
        @RequestMapping(value = "/sayHello1",method = RequestMethod.GET)
        String sayHello1(@RequestParam("name") String name);
    }
    

    如上图所示,运行时候就会报错。 原因是两个FeignClient使用了同一个value,对于同一个service-id只能使用一个配置类,如果有多个@FeignClient注解使用了相同的name属性,则注解的configuration参数会被覆盖。至于谁覆盖谁要看Spring容器初始化Bean的顺序。

    改动:

    # 设置为true,表示后发现的bean会覆盖之前相同名称的bean
    spring.main.allow-bean-definition-overriding=true
    

    源码解读

    openfeign的自动配置

    @EnableFeignClients开启openfeign

    首先,我们从@EnableFeignClients这个注解开始了解。

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
    }
    

    这个注解导入了一个类FeignClientsRegistrar,这个类实现了ImportBeanDefinitionRegistrar接口,该接口用于向Bean容器中注册添加BeanDefinition。

    跟进FeignClientsRegistrar的registerBeanDefinitions方法,看看它注册了哪些东西。

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { 
        // 注册默认配置,会读取@EnableFeignClients接口的属性,如果存在自定义配置类那么就会被注册到容器中
        registerDefaultConfiguration(metadata, registry); 
        // 注册FeignClient接口的Bean,会扫描所有注解了@FeignClient的接口,然后像spring本地Bean一样地注册到容器中。
        registerFeignClients(metadata, registry);
    }
    

    下面重点看看,registerFeignClients方法,这个方法的核心逻辑就是扫描类路径,获取BeanDefinition,然后遍历进行注册。

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    
        LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
        Map<String, Object> attrs = metadata
            .getAnnotationAttributes(EnableFeignClients.class.getName());
        final Class<?>[] clients = attrs == null ? null
            : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            ClassPathScanningCandidateComponentProvider scanner = getScanner();
            scanner.setResourceLoader(this.resourceLoader);
            scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            // 扫描所有路径,默认情况下扫描启动类下的路径
            Set<String> basePackages = getBasePackages(metadata);
            for (String basePackage : basePackages) {
                // 将所有 @FeignClient 的接口的BeanDefinition拿到
                candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
            }
        }
        else {
            for (Class<?> clazz : clients) {
                candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
            }
        }
        // 遍历扫描到的FeignClient的Bean
        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);
                // 注册FeignClient的配置
                registerClientConfiguration(registry, name,attributes.get("configuration"));
    		   // 注册FeignClient
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
    

    下面来看看注册FeignClient的方法:

    private void registerFeignClient(BeanDefinitionRegistry registry,
                                     AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, null);
        ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
            ? (ConfigurableBeanFactory) registry : null;
        String contextId = getContextId(beanFactory, attributes);
        String name = getName(attributes);
        FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
        factoryBean.setBeanFactory(beanFactory);
        factoryBean.setName(name);
        factoryBean.setContextId(contextId);
        factoryBean.setType(clazz);
        // 使用FactoryBean,将Bean的具体生成过程收拢到FeignClientFactoryBean之中
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
            .genericBeanDefinition(clazz, () -> {
                factoryBean.setUrl(getUrl(beanFactory, attributes));
                factoryBean.setPath(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(), null));
                }
                Object fallbackFactory = attributes.get("fallbackFactory");
                if (fallbackFactory != null) {
                    factoryBean.setFallbackFactory(fallbackFactory instanceof Class
                                                   ? (Class<?>) fallbackFactory
                                                   : ClassUtils.resolveClassName(fallbackFactory.toString(),
                                                                                 null));
                }
                return factoryBean.getObject();
            });
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        definition.setLazyInit(true);
        validate(attributes);
    
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
        beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
    
        // has a default, won't be null
        boolean primary = (Boolean) attributes.get("primary");
    
        beanDefinition.setPrimary(primary);
    
        String[] qualifiers = getQualifiers(attributes);
        if (ObjectUtils.isEmpty(qualifiers)) {
            qualifiers = new String[] { contextId + "FeignClient" };
        }
    
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
        // 将这个使用了 @FeignClient 的接口的工厂Bean的 BeanDefinition 注册到Spring容器中
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }
    

    这里值得注意的是genericBeanDefinition方法最终生成的其实是FeignClientFactoryBean,而registerBeanDefinition方法注册进容器的也是FeignClientFactoryBean。而FeignClientFactoryBean是FactoryBean的实现类。FactoryBean接口是spring开放出来的,用于自定义Bean的生成过程。也就是说,spring将会通过调用FeignClientFactoryBean的getObject来获取@FeignClient注解的接口对应的Bean对象。

    openfeign生成并调用客户端动态代理对象

    从FeignClientFactoryBean的getObject()方法开始,看看代理对象的生成。getObject()方法调用了一个getTarget()方法,该方法做了一些预处理。获取了一个上下文以及Feign的构造器,没有URL的情况下拼接了一个。

    @Override
    public Object getObject() {
        return getTarget();
    }
    
    <T> T getTarget() {
        // 获取上下文,FeignContext是在FeignAutoConfiguration被解析的时候成为Bean.
        FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
            : applicationContext.getBean(FeignContext.class);
        // feign用于构建代理对象,builder将会构建feign
        Feign.Builder builder = feign(context);
    
        if (!StringUtils.hasText(url)) {
            // 然后在没有url 的情况下是按照服务名进行处理,拼接url 属性为http://服务名称。 
            // 如果有URL会按照URL的方式进行处理,并且如果URL没有加http:// 会在这里加上,也就是URL可以只写域名加端口
            if (!name.startsWith("http")) {
                url = "http://" + name;
            }
            else {
                url = name;
            }
            url += cleanPath();
            // HardCodedTarget 对象,实际上就是一个记录的功能,记录了接口类型,服务名称,地址信息
            return (T) loadBalance(builder, context,
                                   new HardCodedTarget<>(type, name, url));
        }
        if (StringUtils.hasText(url) && !url.startsWith("http")) {
            url = "http://" + url;
        }
        String url = this.url + cleanPath();
        Client client = getOptional(context, Client.class);
        if (client != null) {
            if (client instanceof LoadBalancerFeignClient) {
                client = ((LoadBalancerFeignClient) client).getDelegate();
            }
            if (client instanceof FeignBlockingLoadBalancerClient) {
                client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
            }
            if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
                client = ((RetryableFeignBlockingLoadBalancerClient) client)
                    .getDelegate();
            }
            builder.client(client);
        }
        Targeter targeter = get(context, Targeter.class);
        return (T) targeter.target(this, builder, context,
                                   new HardCodedTarget<>(type, name, url));
    }
    

    用org.springframework.cloud.openfeign.FeignClientFactoryBean#loadBalance 方法:

    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
          HardCodedTarget<T> target) {
        // 获取执行HTTP请求的client对象
       Client client = getOptional(context, Client.class);
       if (client != null) {
          builder.client(client);
          // 获取Target对象,默认为HystrixTargeter
          Targeter targeter = get(context, Targeter.class);
          // 创建代理对象
          return targeter.target(this, builder, context, target);
       }
    }
    

    跟进HystrixTargeter的target方法:

    @Override
    public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
          FeignContext context, Target.HardCodedTarget<T> target) {
       if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
          return feign.target(target);
       }
       feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
       String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
             : factory.getContextId();
       SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
       if (setterFactory != null) {
          builder.setterFactory(setterFactory);
       }
       Class<?> fallback = factory.getFallback();
       if (fallback != void.class) {
          return targetWithFallback(name, context, target, builder, fallback);
       }
       Class<?> fallbackFactory = factory.getFallbackFactory();
       if (fallbackFactory != void.class) {
          return targetWithFallbackFactory(name, context, target, builder,
                fallbackFactory);
       }
    
       return feign.target(target);
    }
    

    HystrixTargeter的target方法里,最后调用了feign.target(target);方法,feign实现了构造代理对象的过程,所以这里将会回调feign的构造过程方法,在feign的target方法中,将会构造出一个Feign对象,并返回对象。

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }
    
    public Feign build() {
      // ...
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
    

    跟进ReflectiveFeign#newInstance方法,主要是通过JDK的动态代理构建代理对象:

    public <T> T newInstance(Target<T> target) {
      Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
      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 handler = factory.create(target, methodToHandler);
      T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
          new Class<?>[] {target.type()}, handler);
    
      for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
        defaultMethodHandler.bindTo(proxy);
      }
      return proxy;
    }
    

    代理对象的构建主要由3个内容组成:

    1. 构建Method到MethodHandler的映射关系,后面调用代理对象的时候将会根据Method找到MethodHandler,然后调用MethodHandler的invoke方法,而MethodHandler包含发起HTTP请求的实现。
    2. jdk动态代理需要提供InvocationHandler。而InvocationHandler将由InvocationHandlerFactory的create方法实现。
    3. 通过Proxy.newProxyInstance方法,生成proxy对象。

    调用proxy对象发起HTTP请求

    我们都知道,JDK的动态代理将会调用FeignInvocationHandler(ReflectiveFeign的静态内部类)的invoke方法.

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       // equals、toString、hashcode方法特殊处理
       return dispatch.get(method).invoke(args);
    }
    

    在前面构建代理对象的时候,构建了Method到MethodHandler的映射关系.所以在这里就是根据method来获取到MethodHandler,在调用invoke方法的.

    进入到invoke方法里,MethodHandler接口的默认实现类为SynchronousMethodHandler:

    @Override
    public Object invoke(Object[] argv) throws Throwable {
      // 根据上面创建对象过程中解析出来的RequestTemplate克隆一个RequestTemplate 
      RequestTemplate template = buildTemplateFromArgs.create(argv);
      Options options = findOptions(argv);
      Retryer retryer = this.retryer.clone();
      while (true) {
        try {
          // executeAndDecode将会负责发起http请求
          return executeAndDecode(template, options);
        } catch (RetryableException e) {
          try {
            retryer.continueOrPropagate(e);
          } catch (RetryableException th) {
           // ...
          }
          continue;
        }
      }
    }
    
    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
      // 对FeignInteceptor 拦截器做处理,并将信息封装到feign.Request 类中
      Request request = targetRequest(template);
    
      if (logLevel != Logger.Level.NONE) {
        logger.logRequest(metadata.configKey(), logLevel, request);
      }
    
      Response response;
      long start = System.nanoTime();
      try {
          // 执行HTTP请求
        response = client.execute(request, options);
        // ensure the request is set. TODO: remove in Feign 12
        response = response.toBuilder()
            .request(request)
            .requestTemplate(template)
            .build();
      } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
          logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
        }
        throw errorExecuting(request, e);
      }
      long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    
      // 解码结果
      if (decoder != null)
        return decoder.decode(response, metadata.returnType());
    
      CompletableFuture<Object> resultFuture = new CompletableFuture<>();
      asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response,
          metadata.returnType(),
          elapsedTime);
    
      try {
        if (!resultFuture.isDone())
          throw new IllegalStateException("Response handling not done");
    
        return resultFuture.join();
      } catch (CompletionException e) {
        Throwable cause = e.getCause();
        if (cause != null)
          throw cause;
        throw e;
      }
    }
    

    总结

    openFeign生成@FeignClient注解的接口的代理对象是从FeignClientFactoryBean的getObject方法开始的,生成proxy对象主要由ReflectiveFeign对象来实现。动态代理方法由jdk原生的动态代理支持。

    调用proxy对象,其实就是发起http请求,请求结果将被解码并返回。

    所以,正如Feign本身的意义一样,http远程调用被伪装成了本地调用一样简单的代理对象,对于使用者来说就是调用本地接口一样简单

    参考文章:

    https://zhuanlan.zhihu.com/p/133378040

    https://blog.csdn.net/manzhizhen/article/details/110013311

    https://www.cnblogs.com/qlqwjy/p/14568086.html

    本文版权归Charon和博客园共有,原创文章,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    springboot mybatis 后台框架平台 集成代码生成器 shiro 权限
    java二维码工具类,中间带LOGO的,很强大
    Itween 动画插件中 的画线
    对象池的简单使用
    DoTween 动画插件简单示例
    快速排序 and 拉格朗日插值查找
    简单工厂模式
    Java集合框架学习
    幂等性学习
    实战重构工厂模式
  • 原文地址:https://www.cnblogs.com/pluto-charon/p/14881482.html
Copyright © 2011-2022 走看看