• 微服务RPC框架-Feign


        一个成熟的微服务集群,内部调用必然依赖一个好的RPC框架,比如:基于http协议的feign,基于私有tcp协议的dubbo。本文内容介绍feign。

    一、What?

    如果不使用rpc框架,那么调用服务需要走http的话,配置请求head、body,然后才能发起请求。获得响应体后,还需解析等操作,十分繁琐。

    Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,封装了http调用流程。

    二、How?

    feign底层基于http协议,适应绝大部分内外部API调用的应用场景,并且SpringCloud对feign已经有了比较好的封装。使用上可以依赖于SpringCloud封装过的feign:

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

    Feign在默认情况下使用的是JDK原生的URLConnection发送HTTP请求,没有连接池,但是对每个地址会保持一个长连接,即利用HTTP的
    persistence connection。建议替换为Apache HttpClient,作为底层的http client包,从而获取连接池、超时时间等与性能息息相关的控制能力:

    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
    </dependency>
    

    在配置文件中启用ApacheHttpClient:

    feign.httpclient.enabled=true
    

    FeignClient参数:

    public @interface FeignClient {
        @AliasFor("name")
        String value() default "";
    
        /** @deprecated */
        @Deprecated
        String serviceId() default "";
    
        String contextId() default "";
    
        // 指定FeignClient的名称
        @AliasFor("value")
        String name() default "";
    
        String qualifier() default "";
    
        // 全路径地址或hostname,http或https可选
        String url() default "";
        // 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
        boolean decode404() default false;
        // Feign配置类,可以自定义Feign的LogLevel
        Class<?>[] configuration() default {};
        // 容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑
        Class<?> fallback() default void.class;
        // 工厂类,用于生成fallback类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
        Class<?> fallbackFactory() default void.class;
        // 定义当前FeignClient的统一前缀,类似于controller类上的requestMapping
        String path() default "";
    
        boolean primary() default true;
    }
    

    三、Why?

    • 启动时,程序会进行包扫描,扫描所有包下所有@FeignClient注解的类,并将这些类注入到spring的IOC容器中。
    • 当定义的Feign中的接口被调用时,通过JDK的动态代理来生成RequestTemplate。RequestTemplate中包含请求的所有信息,如请求参数,请求URL等。
    • RequestTemplate声场Request,然后将Request交给client处理,client默认是JDK的HTTPUrlConnection,也可以是OKhttp、Apache的HTTPClient等。
    • 最后client封装成LoadBaLanceClient,结合ribbon负载均衡地发起调用。

    使用Feign涉及两个注解:@EnableFeignClients,用来开启Feign;@FeignClient,标记要用Feign来拦截的请求接口。

    1、启用

    启动配置上检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解的接口。扫描出该注解后,
    通过beanDefinition注入到IOC容器中,方便后续被调用使用。

    @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 {};
    }
    

    @EnableFeignClients 是关于注解扫描的配置,使用了@Import(FeignClientsRegistrar.class)。在spring context处理过程中,这个Import会在解析Configuration的时候当做提供了其他的bean definition的扩展,Spring通过调用其registerBeanDefinitions方法来获取其提供的bean definition。

    class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
        
        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            registerDefaultConfiguration(metadata, registry);
            registerFeignClients(metadata, registry);
        }
    
    }
    

    FeignClientsRegistrar里重写了spring里ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法。也就是在启动时,处理了EnableFeignClients注解后,registry里面会多出一些关于Feign的BeanDefinition。

    2、发起请求

    ReflectiveFeign内部使用了jdk的动态代理为目标接口生成了一个动态代理类,这里会生成一个InvocationHandler统一的方法拦截器,同时为接口的每个方法生成一个SynchronousMethodHandler拦截器,并解析方法上的元数据,生成一个http请求模板RequestTemplate。

    public class ReflectiveFeign extends Feign {
    
        @Override
        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;
        }
    
    }
    

    Feign内置了一个重试器,当HTTP请求出现IO异常时,Feign会有一个最大尝试次数发送请求:

    final class SynchronousMethodHandler implements MethodHandler {
    
        @Override
        public Object invoke(Object[] argv) throws Throwable {
            // 根据输入参数,构造Http请求
            RequestTemplate template = buildTemplateFromArgs.create(argv);
            // 克隆出一份重试器
            Retryer retryer = this.retryer.clone();
            // 尝试最大次数,如果中间有结果,直接返回
            while (true) {
              try {
                return executeAndDecode(template);
              } catch (RetryableException e) {
                try {
                  retryer.continueOrPropagate(e);
                } catch (RetryableException th) {
                  Throwable cause = th.getCause();
                  if (propagationPolicy == UNWRAP && cause != null) {
                    throw cause;
                  } else {
                    throw th;
                  }
                }
                if (logLevel != Logger.Level.NONE) {
                  logger.logRetry(metadata.configKey(), logLevel);
                }
                continue;
              }
            }
        }
    }
    

    Feign真正发送HTTP请求是委托给feign.Client来做的:

    public interface Client {
        Response execute(Request request, Options options) throws IOException;
        class Default implements Client {
            @Override
            public Response execute(Request request, Options options) throws IOException {
                HttpURLConnection connection = convertAndSend(request, options);
                return convertResponse(connection, request);
            }
        }
    }
    

    默认底层通过JDK的java.net.HttpURLConnection实现了feign.Client接口类。在每次发送请求的时候,都会创建新的HttpURLConnection链接,这样的话默认情况下Feign的性能很差,一般扩展该接口,比如使用Apache的HttpClient或者OkHttp3等基于连接池的高性能Http客户端。

    注意:SynchronousMethodHandler并不是直接完成远程URL的请求,而是通过负载均衡机制,定位到合适的远程server服务器,然后再完成真正的远程URL请求。即:SynchronousMethodHandler实例的client成员,其实际不是feign.Client.Default类型,而是LoadBalancerFeignClient客户端负载均衡类型。

    3、性能分析

    Feign框架比较小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理Http请求的环节。可以从这个方面着手分析系统的性能提升点。

    长不大的童心
  • 相关阅读:
    LeetCode 面试题 08.02. 迷路的机器人
    LeetCode 96. 不同的二叉搜索树
    LeetCode 198 打家劫舍
    LeetCode 931 下降路径最小和
    Anaconda的安装及使用教程
    2020-8
    gdb与core dump
    实用linux命令
    offer_JZ25
    javaweb学习笔记整理补课
  • 原文地址:https://www.cnblogs.com/yaofengdoit/p/12995125.html
Copyright © 2020-2023  润新知