本章知识点:OpenFeign介绍、负载均衡Ribbon的算法、Spring cloud 如何通过RestTemplate调用微服务,以及RestTemplate负载均衡原理。
Springboot 2.3.12.RELEASE,spring cloud Hoxton.SR12,spring cloud alibaba 2.2.9.RELEASE
Spring Cloud OpenFeign是一个声明式、模板化的HTTP客户端,主要用于Spring Cloud微服务之间的调用。以下是其特点:
使用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还支持自定义的负载均衡策略,可以根据具体需求实现特定的算法逻辑。这些算法和策略可以灵活配置,以满足不同场景下的负载均衡需求。
<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>
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);
}
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);
}
}
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);
}
}
? 图5-1 输出结果
@FeignClient(“service-provider”),其中service-provider是系统的服务名(实际是虚拟主机名),也可以自定义成需要的名字,但是此时需要用一个属性url来指定它对应的实际地址。
@FeignClient(name = "service-provider",url = "http://localhost:8080")
系统中并没有service-provider服务,但是通过url指定了它具体访问的路径,就可以对它进行访问了。
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