Springcloud 微服务实战笔记 Feign

发布时间:2024年01月04日

优点

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

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

扩展了Spring MVC的注解支持

使用

1、pom导入包:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-feign</artifactId>
    <version>1.4.4.RELEASE</version>
</dependency>

2、接口增加注解:?@FeignClient

@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直接调用

@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

@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的方式设置即可,比如:

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实例,详细实现步骤如下所示。
    // 构建一个关闭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

// 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:

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:

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方法,主要实现:

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()方法 动态代理的实现入口

<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的实现):

 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微服务实战》

文章来源:https://blog.csdn.net/u010960161/article/details/135381365
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。