第五章 : Spring cloud 微服务调用-OpenFeign

2024-01-09 15:47:17

第五章 : Spring cloud 微服务调用-OpenFeign

前言

本章知识点:OpenFeign介绍、负载均衡Ribbon的算法、Spring cloud 如何通过RestTemplate调用微服务,以及RestTemplate负载均衡原理。

Springboot 2.3.12.RELEASE,spring cloud Hoxton.SR12,spring cloud alibaba 2.2.9.RELEASE

OpenFeign介绍

Spring Cloud OpenFeign是一个声明式、模板化的HTTP客户端,主要用于Spring Cloud微服务之间的调用。以下是其特点:

  1. 声明式调用:就像调用本地方法一样调用远程方法,无需感知操作远程HTTP请求。
  2. 支持Spring MVC注解:例如@RequesMapping,@FeignClient等。
  3. 可以和Eureka和ribbon配合使用。
  4. 无需编写接口实现,通过@Autowired注入即可使用。
  5. 在Spring Cloud应用启动时,Feign会扫描标有@FeignClient注解的接口,生成代理,并注册到Spring容器中。

使用OpenFeign,开发者可以更方便地调用远程服务,无需关注与远程的交互细节,更无需关注分布式环境开发。

OpenFeign负载均衡

在Spring Cloud中,OpenFeign是一个用于构建基于HTTP的RESTful服务客户端的框架。它可以与Ribbon负载均衡器结合使用,以实现负载均衡的功能。

OpenFeign通过使用@FeignClient注解来定义客户端接口,该注解中可以指定服务的名称。在调用该接口的方法时,OpenFeign会根据服务名称和方法参数来选择一个可用的服务实例进行调用。当OpenFeign与Ribbon负载均衡器结合使用时,它会根据Ribbon的负载均衡策略选择一个服务实例进行调用。Ribbon负载均衡器会通过维护一张服务实例列表,并根据一定的策略将请求分发到不同的实例上,以实现负载均衡的效果。

在OpenFeign中,可以通过在@FeignClient注解中设置value属性来指定服务的名称,如@FeignClient(value = “service-name”)。这样,OpenFeign会根据服务名称来查找注册中心中的服务实例,并根据Ribbon的负载均衡策略选择一个实例进行调用。另外,OpenFeign还支持使用@LoadBalanced注解来启用Ribbon负载均衡器,以实现更细粒度的负载均衡控制。通过在OpenFeign客户端接口的方法上添加@LoadBalanced注解,可以使得该方法的调用会经过Ribbon负载均衡器进行服务实例的选择。

Ribbon负载均衡器

Ribbon负载均衡器可以实现多种负载均衡算法,包括但不限于以下几种:

  1. 轮询算法(Round Robin):这是一种简单的负载均衡算法,它按顺序将请求依次分发到每个服务实例,确保每个实例都能均匀地处理请求。
  2. 随机算法(Random):该算法随机选择一个服务实例来处理请求,这有助于分散请求负载,避免某个实例过载。
  3. 加权轮询算法(Weighted Round Robin):在这种算法中,每个服务实例都被分配一个权重值。权重较高的实例将接收到更多的请求,这有助于根据实例的处理能力来分配请求。
  4. 最少连接数算法(Least Connections):这种算法将请求发送给当前连接数最少的实例,有助于避免实例过载并提高资源利用率。
  5. 加权响应时间算法(Weighted Response Time):该算法根据每个服务实例的平均响应时间及其权重来分配请求。响应时间较快的实例将优先处理请求。
  6. 重试算法(Retry):在某些情况下,如果请求失败,Ribbon可以尝试重新发送请求到其他可用的服务实例。
  7. 区域感知路由(Zone Aware Routing):这是Ribbon的一个特性,它可以根据服务实例所在的区域和可用性来选择最佳的实例进行请求。这有助于实现跨地域的负载均衡。
  8. 最佳可用规则(Best Available Rule):该规则会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。

Ribbon还支持自定义的负载均衡策略,可以根据具体需求实现特定的算法逻辑。这些算法和策略可以灵活配置,以满足不同场景下的负载均衡需求。

OpenFeign实战

  1. 在调用方的pom中添加依赖。
 		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
  1. 定义一个接口EchoService。每个@FeignClient对应着一个服务,它是调用某个服务的一个抽象。
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(name = "service-provider")
public interface EchoService {

    @GetMapping(value = "/echo/{message}")
    String echo(@PathVariable("message") String message);

}
  1. 写一个测试接口。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class ConsumerController {

    @Autowired
    private EchoService echoService;

    @GetMapping(value = "/api/feign/echo/{message}")
    public String feign(@PathVariable("message") String message) {
        log.info("message:{}",message);
        return echoService.echo(message);
    }

}
  1. 在启动类上加注解@EnableFeignClients。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class SpringbootDay03Application {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootDay03Application.class, args);
    }

}
  1. 访问http://localhost:8083/api/feign/echo/张三,输出结果如图5-1所示。

在这里插入图片描述

? 图5-1 输出结果

@FeignClient自定义URL

@FeignClient(“service-provider”),其中service-provider是系统的服务名(实际是虚拟主机名),也可以自定义成需要的名字,但是此时需要用一个属性url来指定它对应的实际地址。

@FeignClient(name = "service-provider",url = "http://localhost:8080")

系统中并没有service-provider服务,但是通过url指定了它具体访问的路径,就可以对它进行访问了。

OpenFeign源码解析

OpenFeign是一个声明式RESTful网络请求客户端,它的原理基于Java的注解和反射机制。通过自动将Java接口转换为HTTP请求,以及将HTTP响应转换为Java对象,OpenFeign简化了远程调用代码,使得远程调用就像本地调用一样简单。

在Spring Cloud应用启动时,OpenFeign会扫描标有@FeignClient注解的接口,生成代理,并注册到Spring容器中。这些接口会经过MVC构造解析,将方法上的注解解析出来放到MethodMetadata中。每一个FeignClient接口会生成一个动态代理对象,指向包含方法的MethodHandler的HashMap。

当服务A发起远程调用时,从动态代理中找到一个MethodHandler的实例,生成request,包含请求的URL。经过负载均衡算法找到一个服务的IP地址,拼接请求的URL。服务B处理服务A发起的远程调用,执行逻辑后,返回响应给A。

使用Feign的时候,在启动类上加了一个注解@EnableFeignClients,同时在每个调用的接口上写了注解@FeignClient。这样程序在启动后,就开启了对Feign Client的扫描。

打开org.springframework.cloud.openfeign.FeignClientsRegistrar类,可以看到registerBeanDefinitions方法。

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

再进入registerDefaultConfiguration方法,发现它的主要作用是获取@EnableFeignClients注解的属性键-值对。

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

下面是registerFeignClients方法,其主要作用是对被注解@FeignClient标记的接口进行处理。

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) {
				candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
			}
		}
		else {
			for (Class<?> clazz : clients) {
				candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
			}
		}

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

上述代码调用了registerFeignClient方法,将Bean实例注册到Spring容器中,便于以后使用。

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);
		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);
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

上述代码中,前面两个方法进行了BeanDefinition注册。下面来看在具体调用的过程中是如何将接口进行实例化的。因为接口无法进行实际的业务处理,所以需要对应的类实例来完成。打开org.springframework.cloud.openfeign.FeignClientFactoryBean类,通过类中的getObject()方法,类可获取对应Bean的实例信息,此时的实例是指被@FeignClient修饰的接口类的实例。

@Override
	public Object getObject() {
		return getTarget();
	}

实际上,getObject()方法是通过getTarget()方法来获取实例的。

<T> T getTarget() {
		FeignContext context = beanFactory != null
				? beanFactory.getBean(FeignContext.class)
				: applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(url)) {

			if (LOG.isInfoEnabled()) {
				LOG.info("For '" + name
						+ "' URL not provided. Will try picking an instance via load-balancing.");
			}
			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();
			}
			if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				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));
	}

getTarget()方法中有一个Feign.Builder,它的作用是负责生成被@FeignClient修饰的接口类实例,通过Java的反射机制生成实例,当feignclient的方法被调用时,InvocationHandler的回调函数会被调用。接下来看一下调用时的逻辑处理。打开feign.SynchronousMethodHandler类,有一个invoke方法。

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } 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;
      }
    }
  }

利用buildTemplateFromArgs.create(argv)构建了一个RequestTemplate,打开executeAndDecode方法。

Object executeAndDecode(RequestTemplate template, Options options) 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 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;
    }
  }

在上述方法里执行了response = client.execute(request, options)。下面以FeignBlockingLoadBalancerClient为例。

public Response execute(Request request, Request.Options options) throws IOException {
		final URI originalUri = URI.create(request.url());
		String serviceId = originalUri.getHost();
		Assert.state(serviceId != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		ServiceInstance instance = loadBalancerClient.choose(serviceId);
		if (instance == null) {
			String message = "Load balancer does not contain an instance for the service "
					+ serviceId;
			if (LOG.isWarnEnabled()) {
				LOG.warn(message);
			}
			return Response.builder().request(request)
					.status(HttpStatus.SERVICE_UNAVAILABLE.value())
					.body(message, StandardCharsets.UTF_8).build();
		}
		String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
				.toString();
		Request newRequest = buildRequest(request, reconstructedUrl);
		return delegate.execute(newRequest, options);
	}

上述代码中有一行String reconstructedUrl =loadBalancerClient.reconstructURI(instance,originalUri). toString();可实现跟踪到最深处,下面看一下实际的代码实现。

private static URI doReconstructURI(ServiceInstance serviceInstance, URI original) {
		String host = serviceInstance.getHost();
		String scheme = Optional.ofNullable(serviceInstance.getScheme())
				.orElse(computeScheme(original, serviceInstance));
		int port = computePort(serviceInstance.getPort(), scheme);

		if (Objects.equals(host, original.getHost()) && port == original.getPort()
				&& Objects.equals(scheme, original.getScheme())) {
			return original;
		}

		boolean encoded = containsEncodedParts(original);
		return UriComponentsBuilder.fromUri(original).scheme(scheme).host(host).port(port)
				.build(encoded).toUri();
	}

从上述代码中,可以看到scheme是协议(HTTP),host是调用的目标主机地址,port是端口号,这样就可以发起对目标服务的调用了。

小结

openfeign使用步骤

1、引入负载均衡依赖:spring-cloud-starter-openfeign

2、@FeignClient定义feign接口服务

3、启动类上加注解@EnableFeignClients

4、启动注解@EnableDiscoveryClient

在这里插入图片描述

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