• SpringCloud Feign的分析


    Feign是一个声明式的Web Service客户端,它使得编写Web Serivce客户端变得更加简单。我们只需要使用Feign来创建一个接口并用注解来配置它既可完成。

    @FeignClient(value = "qrcodepay-dike-service")
    public interface TestRoute {
        @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
        HdResult get();
    }  

     我们只需要在相应的接口上添加@FeignClient注解即可将他声明为一个web客户端。这其中的原理我们后续分析。我们首先先关注下feign暴露的几个配置。

    • value: 目标服务名,一般都是 application.name
    • fallback : 服务降级策略
    
    
    @FeignClient(value = "qrcodepay-dike-service",fallback = TestRoute.TestRouteFaback.class)
    public interface TestRoute {
    @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
    HdResult get();
    }

    @Component
    class TestRouteFaback implements TestRoute{ @Override public HdResult get() { return HdResult.makeFail("服务降级"); } }
    •  fallbackFactory :fallback的升级版,可以获取更加详细的异常信息
    @FeignClient(value = "qrcodepay-dike-service",fallbackFactory = TestRoute.TestRouteFallbackFactory.class)
    public interface TestRoute {
        @RequestMapping(value = "/dike/get", method = RequestMethod.GET)
        HdResult get();
    
        @Component
        class TestRouteFallbackFactory implements FallbackFactory<TestRoute>{
            private static final Logger logger = LoggerFactory.getLogger(TestRouteFallbackFactory.class);
            @Override
            public TestRoute create(Throwable throwable) {
                String msg = throwable == null ? "" : throwable.getMessage();
                if (!StringUtils.isEmpty(msg)) {
                    logger.error("异常信息打印:{}",msg);
                }
                return new TestRoute() {
                    @Override
                    public HdResult get() {
                        return HdResult.makeFail(msg);
                    }
                };
            }
    
        }
    }
    • configuration:重写feign的配置

     具体哪些内容可以配置我们可以看  FeignClientsConfiguration和feign.Feign.Builder。

    下面用两种方式重写feign的配置

    覆盖原有的配置bean达到重写目的

    @Configuration
    public class FeignBreakerConfiguration {
        @Bean
        public ErrorDecoder errorDecoder() {
            return new UserErrorDecoder();
        }
        /**
         * 自定义错误解码器 只有返回http status 非200才会进入
         */
        public class UserErrorDecoder implements ErrorDecoder {
            private Logger logger = LoggerFactory.getLogger(getClass());
            @Override
            public Exception decode(String methodKey, Response response) {
                Exception exception = null;
                try {
                    String json = Util.toString(response.body().asReader());
                    System.out.println("自定义解码:"+json);
                    exception = new RuntimeException(json);
                    HdResult result=HdResult.makeFail(json);
                    // 业务异常包装成 HystrixBadRequestException,不进入熔断逻辑
    //                if (!result.isSuccess()) {
    //                    exception = new HystrixBadRequestException(result.getMessage());
    //                }
                } catch (IOException ex) {
                    logger.error(ex.getMessage(), ex);
                }
                return exception;
            }
        }
    }

    自定义客户端达到重写的目的

    @Import(FeignClientsConfiguration.class)
    @RestController
    public class DefaultController {
        private FeignClientService feignClientService;
        public DefaultController(Decoder decoder, Encoder encoder, Client client){
            this.feignClientService = Feign.builder().client(client)
                    .encoder(encoder)
                    .decoder(decoder)
                    .requestInterceptor(new BasicAuthRequestInterceptor("user","password"))
                    .target(FeignClientService.class,"http://eureka-client");
        }
    
        @RequestMapping(name = "/default",method = RequestMethod.GET)
        public String  getInfo(){
            return feignClientService.getValue("hello world!");
        }
    }

     feignclient最常用的配置大致如上,接下来介绍下feign实现的原理。

    先说结论,feign是通过动态代理的技术将一个interface变为Web Service客户端。那我们应该从哪里入手呢。在使用feign的时候,我们应该关注两个注解,一个就是我们上文所说的feignClient,但是仅仅只用这个注解feign是不会生效的,必须要在启动类上加上EnableFeignClients,feign才会自动扫描feignClient。所以我们的入口应该是 EnableFeignClients

    EnableFeignClients 导入了FeignClientsRegistrar,这个注解真正的逻辑就在FeignClientsRegistrar中

    这个类实现了三个接口,我们先关注 ImportBeanDefinitionRegistrar,这是spring动态注册bean的接口。所以spring在启动的时候会调用以下方法

    public void registerBeanDefinitions(AnnotationMetadata metadata,
                BeanDefinitionRegistry registry) {
            registerDefaultConfiguration(metadata, registry);
            registerFeignClients(metadata, registry);
        }

    将配置类纳入beandefinationMap管理 ,这一块更为详细的内容可以看  SpringIoc分析

    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"));
            }
        }

    扫描FeignClient注解,将interface纳入beanDefination

    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);
                    }
                }
            }
        }

     接下来,我们需要找到jdk代理的地方

    我们在构建feign的地方发现如下方法

    public Feign build() {
          SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
              new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                                   logLevel, decode404);
          ParseHandlersByName handlersByName =
              new ParseHandlersByName(contract, options, encoder, decoder,
                                      errorDecoder, synchronousMethodHandlerFactory);
          return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
        }

     最终我们在SynchronousMethodHandler类中发现了真正拦截的代码

    public Object invoke(Object[] argv) throws Throwable {
        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;
          }
        }
      }

    真正执行的逻辑如下,这里也是feign最为关键的地方。这里我们主要关注下真正请求的那一行。如果想对feign做debug或者重写一些配置,参考这里会是一个很好的入口。

    Object executeAndDecode(RequestTemplate template) throws Throwable {
        Request request = targetRequest(template);
    
        if (logLevel != Logger.Level.NONE) {
          logger.logRequest(metadata.configKey(), logLevel, request);
        }
    
        Response response;
        long start = System.nanoTime();
        try {
          response = client.execute(request, options);
          // ensure the request is set. TODO: remove in Feign 10
          response.toBuilder().request(request).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);
    
        boolean shouldClose = true;
        try {
          if (logLevel != Logger.Level.NONE) {
            response =
                logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
            // ensure the request is set. TODO: remove in Feign 10
            response.toBuilder().request(request).build();
          }
          if (Response.class == metadata.returnType()) {
            if (response.body() == null) {
              return response;
            }
            if (response.body().length() == null ||
                    response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
              shouldClose = false;
              return response;
            }
            // Ensure the response body is disconnected
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            return response.toBuilder().body(bodyData).build();
          }
          if (response.status() >= 200 && response.status() < 300) {
            if (void.class == metadata.returnType()) {
              return null;
            } else {
              return decode(response);
            }
          } else if (decode404 && response.status() == 404) {
            return decoder.decode(response, metadata.returnType());
          } else {
            throw errorDecoder.decode(metadata.configKey(), response);
          }
        } catch (IOException e) {
          if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
          }
          throw errorReading(request, response, e);
        } finally {
          if (shouldClose) {
            ensureClosed(response.body());
          }
        }
      }

     这里的client是请求客户端,feign统一封装为LoadBalancerFeignClient

    @ConditionalOnClass({ ILoadBalancer.class, Feign.class })
    @Configuration
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    public class FeignRibbonClientAutoConfiguration {
    
    @Bean
        @ConditionalOnMissingBean
        public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                SpringClientFactory clientFactory) {
            return new LoadBalancerFeignClient(new Client.Default(null, null),
                    cachingFactory, clientFactory);
        }
    
    }

    默认的Client 是HttpURLConnection,同时 feign也支持httpclient和okhhtp

    @Configuration
        @ConditionalOnClass(ApacheHttpClient.class)
        @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
        @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
        protected static class HttpClientFeignConfiguration {
    
            @Autowired(required = false)
            private HttpClient httpClient;
    
            @Bean
            @ConditionalOnMissingBean(Client.class)
            public Client feignClient() {
                if (this.httpClient != null) {
                    return new ApacheHttpClient(this.httpClient);
                }
                return new ApacheHttpClient();
            }
        }
    
        @Configuration
        @ConditionalOnClass(OkHttpClient.class)
        @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
        @ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
        protected static class OkHttpFeignConfiguration {
    
            @Autowired(required = false)
            private okhttp3.OkHttpClient okHttpClient;
    
            @Bean
            @ConditionalOnMissingBean(Client.class)
            public Client feignClient() {
                if (this.okHttpClient != null) {
                    return new OkHttpClient(this.okHttpClient);
                }
                return new OkHttpClient();
            }
        }

     只要满足 配置条件,就可以将httpclient或okhhtp引入,这里举例说明怎么使用httpclient

    在pom文件加上:

    <dependency>
        <groupId>com.netflix.feign</groupId>
        <artifactId>feign-httpclient</artifactId>
        <version>RELEASE</version>
    </dependency>

     在配置文件上加上feign.httpclient.enabled为true(默认为true,可不写)

    最后,我们再看看feign是怎么使用ribbon的,上文我们说过feign统一将client封装为LoadBalancerFeignClient,fein的请求最终都会到以下代码

    public Response execute(Request request, Request.Options options) throws IOException {
            try {
                URI asUri = URI.create(request.url());
                String clientName = asUri.getHost();
                URI uriWithoutHost = cleanUrl(request.url(), clientName);
                FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                        this.delegate, request, uriWithoutHost);
    
                IClientConfig requestConfig = getClientConfig(options, clientName);
                return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,
                        requestConfig).toResponse();
            }
            catch (ClientException e) {
                IOException io = findIOException(e);
                if (io != null) {
                    throw io;
                }
                throw new RuntimeException(e);
            }
        }

    具体我们可以看下 executeWithLoadBalancer 

    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
            RequestSpecificRetryHandler handler = getRequestSpecificRetryHandler(request, requestConfig);
            LoadBalancerCommand<T> command = LoadBalancerCommand.<T>builder()
                    .withLoadBalancerContext(this)
                    .withRetryHandler(handler)
                    .withLoadBalancerURI(request.getUri())
                    .build();
    
            try {
                return command.submit(
                    new ServerOperation<T>() {
                        @Override
                        public Observable<T> call(Server server) {
                            URI finalUri = reconstructURIWithServer(server, request.getUri());
                            S requestForServer = (S) request.replaceUri(finalUri);
                            try {
                                return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                            } 
                            catch (Exception e) {
                                return Observable.error(e);
                            }
                        }
                    })
                    .toBlocking()
                    .single();
            } catch (Exception e) {
                Throwable t = e.getCause();
                if (t instanceof ClientException) {
                    throw (ClientException) t;
                } else {
                    throw new ClientException(e);
                }
            }
            
        }

    在submit方法里,发现了如下代码

    // Use the load balancer
            Observable<T> o = 
                    (server == null ? selectServer() : Observable.just(server))
                    .concatMap(new Func1<Server, Observable<T>>() {
    }

    这里的selectServer 最终会调用 ILoadBalancer 选择一个server

    ILoadBalancer lb = getLoadBalancer();
            if (host == null) {
                // Partial URI or no URI Case
                // well we have to just get the right instances from lb - or we fall back
                if (lb != null){
                    Server svc = lb.chooseServer(loadBalancerKey);

     关于这方面的具体内容,请参考 SpringCloud Ribbon的分析

    以上,就是对feign的具体分析

  • 相关阅读:
    netty(八) netty中自带channelhandler
    netty(七) Handler的执行顺序
    netty(六) websocket开发应用
    netty(五) http协议开发应用
    netty(四) 编解码技术
    netty(三) TIP黏包/拆包问题解决之道
    netty(二) 创建一个netty服务端和客户端
    netty(一) netty有哪几部分构成
    使用jsp制作index,可以通过<c:if test==“管理员”>或<c:if test=="客户">来区别展示用户界面
    使用jstl和el表达式来展示request域中存放的user对象的信息
  • 原文地址:https://www.cnblogs.com/xmzJava/p/9612988.html
Copyright © 2020-2023  润新知