Springcloud 微服务实战笔记 Feign

优点

基于Netflix Feign实现,整合了Spring cloud Ribbon 和 Spring cloud Hystrix

提供了声明式的WEB服务客户端定义方式

扩展了Spring MVC的注解支持

使用

1、pom导入包:

XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    <version>1.4.4.RELEASE</version>
</dependency>

2、接口增加注解: @FeignClient

java 复制代码
@FeignClient(name = "spring-cloud-study-demo")
public interface DemoRemoteClient {
	
	@GetMapping("/demo/getData/{uid}")
	public ApiReturnObject getData(@PathVariable(value="uid") String uid,@RequestParam(value="data") String data);

}

注解@FeignClient的name参数配置为服务名(服务提供方可以自己随便写一个)

3、Controller直接调用

java 复制代码
@RestController
public class FeignController {
	@Resource
	DemoRemoteClient demoRemoteClient;
	
	@GetMapping("/remote/demo/getData/{uid}")
	public ApiReturnObject  basePath(@PathVariable String uid ,String data){
		return demoRemoteClient.getData(uid, data);
	}
}

4、启动类增加注解 @EnableFeignClients

java 复制代码
@EnableEurekaClient
@EnableFeignClients
@SpringBootApplication
public class SpringCloudStudyFeignApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringCloudStudyFeignApplication.class,args);
		System.out.println("http://127.0.0.1:6666/feign/remote/demo/222");
	}
}

Ribbon配置

全局配置

直接使用ribbon.key=value的方式设置即可,比如:

bash 复制代码
ribbon.ConnectTime=500
ribbon.ReadTimeout=5000

指定服务配置

使用@FeignClient注解中的name或则Value属性值来设置对应的Ribbon参数,比如:

SPRING-CLOUD-STUDY-DEMO.ribbon.ConnectTime=500
SPRING-CLOUD-STUDY-DEMO.ribbon.ReadTimeout=5000
SPRING-CLOUD-STUDY-DEMO.ribbon.OkToRetryOnAllOperations=true

重试机制

Spring Cloud Feign中默认实现了请求的重试机制(重试次数参数可配置)

需注意:Ribbon的超时与Hystrix的超时是两个概念。为了能让Ribbon超时重试生效,需要让Hystrix的超时时间设置大于Ribbon的超时时间,否则Hystrix超时后直接熔断了,也就没有重试了。

Hystrix配置

全局配置

Hystrix全局配置与Ribbon全局配置一样,直接使用默认前缀hystrix.xxx.xxx.xx即可配置,比如设置超时时间:

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
hystrix.command.default.execution.timeout.enabled=false ##来关闭熔断功能。

在 对 Hystrix 进 行 配 置 之 前 , 我 们 需 要 确 认feign.hystrix.enabled参数没有被设置为false,否则该参数设置会关闭Feign客户端的Hystrix支持。

禁用Hystrix

  1. 通过feign.hystrix.enabled参数配置,全局配置,默认为false。

  2. 针对某个服务关闭hystrix,需要通过使用@Scope("prototype")注解为指定的客户端配置Feign.Builder实例,详细实现步骤如下所示。

    java 复制代码
    // 构建一个关闭Hystrix的配置类
    @Configuration
    public class DisableHystrixConfiguration {
      @Bean
      @Scope("prototype")
      public Feign.Builder feignBuilder(){
      return Feign.builder();
    }
    
    }
    // 在HelloService的@FeignClient注解中,通过configuration参数引入上面实现的配置。
    @FeignClient(name="SPRING-CLOUD-STUDY-DEMO",configuration=DisableHystrixConfiguration.class)
    public interface HelloService {
      ...
    }

指定命令配置

采用hystrix.command.(commadKey默认为方法名)为前缀来进行配置

服务降级配置

注解@FeignClient包含参数:org.springframework.cloud.openfeign.FeignClient#fallback

fallback参数为实现FeignClient接口实现类class

java 复制代码
// FeignClient注解增加fallback参数,配置为当前接口实现类
@FeignClient(name = "SPRING-CLOUD-STUDY-DEMO", fallback = DemoFallback.class)
public interface DemoFeignClient {

    @GetMapping("/demo/hello")
    String hello();
}

// 实现FeignClient接口,重写降级方法
@Component
public class DemoFallback implements DemoFeignClient{

    @Override
    public String hello() {
        return "fallbcak";
    }
}

请求压缩

Spring Cloud Feign支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需通过下面两个参数设置,就能开启请求与响应的压缩功能:

feign.compression.request.enabled=true
feign.compression.response.enabled=true

指定压缩类型

feign.compression.request.mime.types=text/xml,application/xml,application/json(默认值)

设置请求压缩的大小下限,只有超过这个大小的请求才会对其进行压缩

feign.compression.request.min-request-size=2048(默认值)

工作原理

1、获取feign相关配置信息

我们使用 Feign 的话,会在 Application 的主启动类上,标记 EnableFeignClient 注解,在要调用的接口上标记 FeignClient 注解。 ​在 EnableFeignClients 注解内部,有一个 @Import(FeignClientsRegistrar.class) ,这个类实现了 ImportBeanDefinitionRegistrar 接口,这个的话是 Spring Context 项目下的,所以会在 Spring Boot 项目启动的时候,会调用 FeignClientsRegistrar.registerBeanDefinitions , 扫描 FeiginClient 注解,并设置相关信息,主要实现在org.springframework.cloud.openfeign.FeignClientsRegistrar#registerDefaultConfiguration:

java 复制代码
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"));
        }
    }

2、扫描FeignClient注解接口

主要实现org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients:

java 复制代码
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);
                }
            }
        }
    }

最终会调用org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient方法,主要实现:

java 复制代码
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);
        String contextId = getContextId(attributes);
        definition.addPropertyValue("contextId", contextId);
        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 = contextId + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

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

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

registerFeignClient中通过BeanDefinitionBuilder构建FeignClientFactoryBean(构建Feign的核心),将通过@FeginClient 注解获取到的信息,都设置到这个 definition 中,如图:

3、FeignClientFactoryBean.getObject()方法 动态代理的实现入口

java 复制代码
<T> T getTarget() {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);

        if (!StringUtils.hasText(url)) {
            if (!name.startsWith("http")) {
                url = "http://" + name;
            }
            else {
                url = name;
            }
            url += cleanPath();
            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) {
                // not load balancing because we have a url,
                // but ribbon is on the classpath, so unwrap
                client = ((LoadBalancerFeignClient) client).getDelegate();
            }
            if (client instanceof FeignBlockingLoadBalancerClient) {
                // not load balancing because we have a url,
                // but Spring Cloud LoadBalancer is on the classpath, so unwrap
                client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
            }
            builder.client(client);
        }
        Targeter targeter = get(context, Targeter.class);
        return (T) targeter.target(this, builder, context,
                new HardCodedTarget<>(type, name, url));
    }

方法这一系列执行后,生成动态代理对象,动态代理对象为每个方法都初始化了SynchronousMethodHandler负责处理方法的请求,然后将动态代理对象放入到Spring容器中,当执行Controller执行时候,其实执行的是InvacationHandler的invoke()方法。

在创建SynchronousMethodHandler.Factory时候,发现是与LoadBalancerFeignClient结合

调用动态代理invoke方法时候,根据构造的Map<Method, MethodHandler>找到对应的handler对象,最终调用com.netflix.client.AbstractLoadBalancerAwareClient#executeWithLoadBalancer(S, com.netflix.client.config.IClientConfig)方法(Ribbon的实现):

java 复制代码
 public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

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

到这里其实就是Ribbon的实现了。

参考资料:

《Spring Cloud微服务实战》

相关推荐
王彬泽1 小时前
【微服务】组件、基础工程构建(day2)
微服务
Cikiss1 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
Cikiss1 小时前
微服务实战——平台属性
java·数据库·后端·微服务
杨荧4 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
攸攸太上5 小时前
JMeter学习
java·后端·学习·jmeter·微服务
妍妍的宝贝6 小时前
k8s 中微服务之 MetailLB 搭配 ingress-nginx 实现七层负载
nginx·微服务·kubernetes
架构师吕师傅8 小时前
性能优化实战(三):缓存为王-面向缓存的设计
后端·微服务·架构
sdg_advance10 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
王彬泽10 小时前
【微服务】服务注册与发现、分布式配置管理 - Nacos
微服务·服务注册与发现·分布式配置管理
杨荧11 小时前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游