【Spring Cloud Alibaba 温故而知新】(八)微服务通信 Ribbon + OpenFeign | Eddie'Blog
【Spring Cloud Alibaba 温故而知新】(八)微服务通信 Ribbon + OpenFeign

【Spring Cloud Alibaba 温故而知新】(八)微服务通信 Ribbon + OpenFeign

eddie 787 2021-11-18

目录

11.1.1 微服务通信方案解读

11.1.1.1 RPC 方案

  • RPC 实现微服务通信的核心思想
    • 全局注册表:将 RPC 支持的所有方法都注册进去
    • 通过将 Java 对象进行编码(IDL、JSON、XML 等等) + 方法名传递(TCP / IP 协议)到目标服务器实现微服务通信

11.1.1.2 HTTP 方案 (Rest)

  • 认识 HTTP
    • 标准化的 HTTP 协议(GET、POST、PUT、DELETE等),目前主流的微服务框架通信实现都是 HTTP
    • 简单、标准,需要做的工作和维护工作少;几乎不需要做额外的工作即可与其他的微服务集成

11.1.1.3 Message 方案

  • 认识 Message
    • 通过 Kafka、RocketMQ 等消息队列实现消息的发布与订阅(消费)
    • 可以实现(削峰填谷),缓冲机制实现数据、任务处理
    • 最大的缺点就是只能够做到最终一致性,而不能做到实时一致性;当然,这也是看业务需求

11.1.1.4 微服务通信该如何选择

  • 结合微服务框架与业务的需要做出选择
    • SpringCloud 建议的通信方法是 OpenFeign(Rest)
    • 需要最终一致性且不要求快速响应的业务场景可以选择使用 Message
    • 问题来了:SpringCloud 可不可以使用 RPC 呢? (但是,要有足够强的理由说明你为什么要使用 RPC)

11.2.1 使用RestTemplate实现微服务通信 (基本很少用,除非第三方对接)

11.2.1.1 RestTemplate 思想

  • 使用 RestTemplate 的两种方式(思想)
    • 在代码(或配置文件中)写死 IP 和 端口号 (需要知道,这并不是不可行!)
    • 通过注册中心获取服务地址,可以实现负载均衡的效果

在这里插入图片描述

11.2.1.2 RestTemplate 方式模拟获取 Token

在实践项目进行:sca-commerce-alibaba-nacos-client

使用 RestTemplate 实现微服务通信

package com.edcode.commerce.service.communication;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 使用 RestTemplate 实现微服务通信
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UseRestTemplateService {

	/**
	 * 随机挑选客户端(负载均衡)
	 */
	private final LoadBalancerClient loadBalancerClient;

	/**
	 * 从授权服务中获取 JwtToken
	 */
	public JwtToken getTokenFromAuthorityService(UsernameAndPassword usernameAndPassword) {

		// 第一种方式: 写死 url
		String requestUrl = "http://127.0.0.1:7000/scacommerce-authority-center" + "/authority/token";
		log.info("RestTemplate请求url和正文: [{}], [{}]",
				requestUrl,
				JSON.toJSONString(usernameAndPassword)
		);

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);

		return new RestTemplate().postForObject(
				requestUrl,
				new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
				JwtToken.class
		);
	}

	/**
	 * 从授权服务中获取 JwtToken, 且带有负载均衡
	 */
	public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(UsernameAndPassword usernameAndPassword) {

		// 第二种方式: 通过注册中心拿到服务的信息(是所有的实例), 再去发起调用
		ServiceInstance serviceInstance = loadBalancerClient.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
		log.info("Nacos客户端信息: [{}], [{}], [{}]",
				serviceInstance.getServiceId(),
				serviceInstance.getInstanceId(),
				JSON.toJSONString(serviceInstance.getMetadata())
		);

		// 与第一种方式区别,就是不用在写死 url, 从 loadBalancerClient 里面获取
		String requestUrl = String.format(
				"http://%s:%s/scacommerce-authority-center/authority/token",
				serviceInstance.getHost(),
				serviceInstance.getPort()
		);
		log.info("登录请求url和正文: [{}], [{}]", requestUrl, JSON.toJSONString(usernameAndPassword));

		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.APPLICATION_JSON);

		return new RestTemplate().postForObject(
				requestUrl,
				new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
				JwtToken.class
		);
	}
}

微服务通信 Controller

package com.edcode.commerce.controller;

import com.edcode.commerce.service.communication.UseRestTemplateService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 微服务通信 Controller
 */
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {

	private final UseRestTemplateService restTemplateService;

	@PostMapping("/rest-template")
	public JwtToken getTokenFromAuthorityService(@RequestBody UsernameAndPassword usernameAndPassword) {
		return restTemplateService.getTokenFromAuthorityService(usernameAndPassword);
	}

	@PostMapping("/rest-template-load-balancer")
	public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(@RequestBody UsernameAndPassword usernameAndPassword) {
		return restTemplateService.getTokenFromAuthorityServiceWithLoadBalancer(usernameAndPassword);
	}

}

HTTP请求测试

communication.http

### 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/rest-template
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}


### 获取 Token, 带有负载均衡
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/rest-template-load-balancer
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

###

日志输出

2021-11-17 22:05:43.168  INFO [sca-commerce-nacos-client,ee3376fb85e00ef1,ee3376fb85e00ef1,true] 19192 --- [nio-8000-exec-1] c.e.c.s.c.UseRestTemplateService         : RestTemplate请求url和正文: [http://127.0.0.1:7000/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]

...

2021-11-17 22:06:33.216  INFO [sca-commerce-nacos-client,cffcaf69c382565e,cffcaf69c382565e,true] 19192 --- [nio-8000-exec-9] c.e.c.s.c.UseRestTemplateService         : Nacos客户端信息: [sca-commerce-authority-center], [192.168.3.192:7000], [{"preserved.register.source":"SPRING_CLOUD","management.context-path":"/scacommerce-authority-center/actuator"}]
2021-11-17 22:06:33.216  INFO [sca-commerce-nacos-client,cffcaf69c382565e,cffcaf69c382565e,true] 19192 --- [nio-8000-exec-9] c.e.c.s.c.UseRestTemplateService         : 登录请求url和正文: [http://192.168.3.192:7000/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]
2021-11-17 22:07:03.197  INFO [sca-commerce-nacos-client,,,] 19192 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty  : Flipping property: sca-commerce-authority-center.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647

11.3.1 Ribbon实现微服务通信及其原理

11.3.1.1 SpringCloud Netflix Ribbon 实现微服务通信及其原理

  • 如何使用 SpringCloud Netflix Ribbon
    • pom.xml 文件中引入 Ribbon 依赖
    • 增强 RestTemplate,添加 @LoadBalanced 注解,使之具备负载均衡的能力
  • SpringCloud Netflix Ribbon 实现原理
    • 根据服务名从注册中心获取服务地址 + 负载均衡策略

在这里插入图片描述

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

@LoadBalanced 注解

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
	return new RestTemplate();
}

11.3.1.1 SpringCloud Netflix Ribbon 实践

使用 Ribbon 之前的配置, 增强 RestTemplate

package com.edcode.commerce.service.communication.ribbon;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 使用 Ribbon 之前的配置, 增强 RestTemplate
 */
@Component
public class RibbonConfig {

    /**
     * 注入 RestTemplate
     * @return
     */
	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

}

使用 Ribbon 实现微服务通信

package com.edcode.commerce.service.communication.ribbon;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import com.netflix.loadbalancer.*;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import rx.Observable;

import java.util.ArrayList;
import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 使用 Ribbon 实现微服务通信
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UseRibbonService {

    private final RestTemplate restTemplate;

    private final DiscoveryClient discoveryClient;

    /**
     * 通过 Ribbon 调用 Authority 服务获取 Token
     * */
    public JwtToken getTokenFromAuthorityServiceByRibbon(
            UsernameAndPassword usernameAndPassword) {

        // 注意到 url 中的 ip 和端口换成了服务名称
        String requestUrl = String.format(
                "http://%s/scacommerce-authority-center/authority/token",
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );
        log.info("登录请求url和正文: [{}], [{}]", requestUrl,
                JSON.toJSONString(usernameAndPassword));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // 这里一定要使用自己注入的 RestTemplate
        return restTemplate.postForObject(
                requestUrl,
                new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                JwtToken.class
        );
    }

    /**
     * 使用原生的 Ribbon Api, 看看 Ribbon 是如何完成: 服务调用 + 负载均衡
     * */
    public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {

        String urlFormat = "http://%s/scacommerce-authority-center/authority/token";

        // 1. 找到服务提供方的地址和端口号
        List<ServiceInstance> targetInstances = discoveryClient.getInstances(
                CommonConstant.AUTHORITY_CENTER_SERVICE_ID
        );

        // 构造 Ribbon 服务列表
        List<Server> servers = new ArrayList<>(targetInstances.size());
        targetInstances.forEach(i -> {
            servers.add(new Server(i.getHost(), i.getPort()));
            log.info("找到目标实例: [{}] -> [{}]", i.getHost(), i.getPort());
        });

        // 2. 使用负载均衡策略实现远端服务调用
        // 构建 Ribbon 负载实例
        BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
                .buildFixedServerListLoadBalancer(servers);
        // 设置负载均衡策略
        loadBalancer.setRule(new RetryRule(new RandomRule(), 300));

        String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer)
                .build().submit(server -> {

                    String targetUrl = String.format(
                            urlFormat,
                            String.format("%s:%s", 
                                    server.getHost(), 
                                    server.getPort()
                            )
                    );
                    log.info("目标请求url: [{}]", targetUrl);

                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);

                    String tokenStr = new RestTemplate().postForObject(
                            targetUrl,
                            new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
                            String.class
                    );

                    return Observable.just(tokenStr);

                }).toBlocking().first().toString();

        return JSON.parseObject(result, JwtToken.class);
    }

}

微服务通信 Controller

package com.edcode.commerce.controller;

import com.edcode.commerce.service.communication.restTemplate.UseRestTemplateService;
import com.edcode.commerce.service.communication.ribbon.UseRibbonService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 微服务通信 Controller
 */
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {

	private final UseRibbonService ribbonService;

	@PostMapping("/ribbon")
	public JwtToken getTokenFromAuthorityServiceByRibbon(@RequestBody UsernameAndPassword usernameAndPassword) {
		return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword);
	}

	@PostMapping("/thinking-in-ribbon")
	public JwtToken thinkingInRibbon(@RequestBody UsernameAndPassword usernameAndPassword) {
		return ribbonService.thinkingInRibbon(usernameAndPassword);
	}

}

communication.http

### 通过 Ribbon 去获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/ribbon
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}


### 通过原生 Ribbon Api 去获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/thinking-in-ribbon
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

###

日志输出

2021-11-17 22:59:47.748  INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,8d83d0dae46c4dd5,true] 19612 --- [nio-8000-exec-1] c.e.c.s.c.ribbon.UseRibbonService        : 登录请求url和正文: [http://sca-commerce-authority-center/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]
2021-11-17 22:59:48.200  INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: sca-commerce-authority-center.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-11-17 22:59:48.220  INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client: sca-commerce-authority-center instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=sca-commerce-authority-center,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2021-11-17 22:59:48.224  INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2021-11-17 22:59:50.276  INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client sca-commerce-authority-center initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=sca-commerce-authority-center,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@2d424cd6

...

2021-11-17 23:35:32.712  INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.e.c.s.c.ribbon.UseRibbonService        : 找到目标实例: [192.168.3.192] -> [7000]
2021-11-17 23:35:32.714  INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.netflix.config.ChainedDynamicProperty  : Flipping property: default.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-11-17 23:35:32.715  INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.netflix.loadbalancer.BaseLoadBalancer  : Client: default instantiated a LoadBalancer: {NFLoadBalancer:name=default,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}
2021-11-17 23:35:32.749  INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.e.c.s.c.ribbon.UseRibbonService        : 目标请求url: [http://192.168.3.192:7000/scacommerce-authority-center/authority/token]

11.4.1 SpringCloud OpenFeign 的简单应用

OpenFeign 基于 Ribbon 实现,而 Ribbon 基于 RestTemplate 实现

11.4.1.1 如何使用 OpenFeign

  • pom.xml 引入 open-feign 依赖
  • 添加 @EnableFeignClients 注解,启用 open-feign

11.4.1.2 OpenFeign 简单实践

pom.xml 引入 open-feign 依赖

<!-- open feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

添加 @EnableFeignClients 注解

@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class NacosClientApplication {

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

}

与 Authority 服务通信的 Feign Client 接口定义

package com.edcode.commerce.service.communication.feign;

import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 与 Authority 服务通信的 Feign Client 接口定义
 */
@FeignClient(contextId = "AuthorityFeignClient", value = "sca-commerce-authority-center")
public interface AuthorityFeignClient {

    /**
     * 通过 OpenFeign 访问 Authority 获取 Token
     * @param usernameAndPassword
     * @return
     */
	@RequestMapping(value = "/scacommerce-authority-center/authority/token", method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
	JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);

}

创建 POST API

@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {

	private final AuthorityFeignClient feignClient;

	@PostMapping("/token-by-feign")
	public JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
		return feignClient.getTokenByFeign(usernameAndPassword);
	}

}

communication.http

### 通过 OpenFeign 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/token-by-feign
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

11.5.1 配置 SpringCloud OpenFeign

11.5.1.1 最常用的配置

  • OpenFeign 开启 gzip 压缩 (通常不会开启,除非特殊场景)
  • 统一 OpenFeign 使用配置:日志、重试、请求连接和响应时间限制
  • 使用 okhttp 替换 httpclient (需要引入 okhttp 依赖)

11.5.1.2 OpenFeign 开启 gzip 压缩

# Feign 的相关配置
feign:
  # feign 开启 gzip 压缩
  compression:
    # 压缩请求数据
    request:
      enabled: true
      min-request-size: 1024
      mime-types: text/xml,application.xml,application/json
    # 压缩响应数据
    response:
      enabled: true

11.5.1.3 统一 OpenFeign 使用配置:日志、重试、请求连接和响应时间限制

package com.edcode.commerce.service.communication.feign;

import feign.Logger;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description OpenFeign 配置类
 */
@Configuration
public class FeignConfig {

	/**
	 * 开启 OpenFeign 日志
	 */
	@Bean
	public Logger.Level feignLogger() {
        // 需要注意, 日志级别需要修改成 debug
		return Logger.Level.FULL;
	}

	/**
	 * OpenFeign 开启重试
     * period = 100 发起当前请求的时间间隔, 单位是 ms
     * maxPeriod = 1000 发起当前请求的最大时间间隔, 单位是 ms
     * maxAttempts = 5 最多请求次数
	 */
	@Bean
	public Retryer feignRetryer() {
		return new Retryer.Default(100,
                SECONDS.toMillis(1),
                5);
	}

    /**
     * 连接超时,单位是 ms
     */
	public static final int CONNECT_TIMEOUT_MILLS = 5000;
    /**
     * 读取超时,单位是 ms
     */
	public static final int READ_TIMEOUT_MILLS = 5000;

	/**
	 * 对请求的连接和响应时间进行限制
	 */
	@Bean
	public Request.Options options() {
		return new Request.Options(
		        CONNECT_TIMEOUT_MILLS,
                TimeUnit.MICROSECONDS,
                READ_TIMEOUT_MILLS,
				TimeUnit.MILLISECONDS,
                true
        );
	}
}

11.5.1.4 使用 okhttp 替换 httpclient (需要引入 okhttp 依赖)

Maven 依赖

<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

bootstrap.yml 配置

# Feign 的相关配置
feign:
  # 禁用默认的 http, 开启 okhttp
  httpclient:
    enabled: false
  okhttp:
    enabled: true

OpenFeign 使用 OkHttp 配置类

package com.edcode.commerce.service.communication.feign;

import feign.Feign;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description OpenFeign 使用 OkHttp 配置类
 *
 *   尽管不写这个配置类,okhttp也是能用,这个配置类并非必要,但通常都会写,使得 okhttp 更友好
 */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class) // 在 feign 初始化之前就注入 okhttp
public class FeignOkHttpConfig {

    /**
     * 注入 OkHttp, 并自定义配置
     * @return
     */
    @Bean
    public okhttp3.OkHttpClient okHttpClient() {

        return new OkHttpClient.Builder()
                .connectTimeout(5, TimeUnit.SECONDS)    // 设置连接超时
                .readTimeout(5, TimeUnit.SECONDS)   // 设置读超时
                .writeTimeout(5, TimeUnit.SECONDS)  // 设置写超时
                .retryOnConnectionFailure(true)     // 是否自动重连
                // 配置连接池中的最大空闲线程个数为 10, 并保持 5 分钟
                .connectionPool(
                        new ConnectionPool(10, 5L, TimeUnit.MINUTES)
                )
                .build();
    }

}

尽管不写这个配置类,okhttp也是能用,这个配置类并非必要,但通常都会写,使得 okhttp 更友好

11.6.1 通过Feign的原生API解析其实现原理(理解即可)

11.6.1.1 Feign 工作流程图

在这里插入图片描述

图片出自于:Feign 工作流程图解

11.6.1.2 Feign 客户端初始化, 必须要配置 encoder、decoder、contract

<!-- 使用原生的 Feign Api 做的自定义配置, encoder 和 decoder -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>11.0</version>
</dependency>

11.6.1.3 使用 Feign 的原生 Api, 而不是 OpenFeign = Feign + Ribbon

原生Api实践

package com.edcode.commerce.service.communication.feign;

import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.stereotype.Service;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Random;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 使用 Feign 的原生 Api, 而不是 OpenFeign = Feign + Ribbon
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UseFeignApi {

    private final DiscoveryClient discoveryClient;

	/**
	 * 使用 Feign 原生 api 调用远端服务
     *
     * Feign 默认配置初始化、设置自定义配置、生成代理对象
	 */
    public JwtToken thinkingInFeign(UsernameAndPassword usernameAndPassword) {

        // 通过反射去拿 serviceId
        String serviceId = null;
        Annotation[] annotations = AuthorityFeignClient.class.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType().equals(FeignClient.class)) {
                serviceId = ((FeignClient) annotation).value();
                log.info("get service id from AuthorityFeignClient: [{}]", serviceId);
                break;
            }
        }

        // 如果服务 id 不存在, 直接抛异常
        if (null == serviceId) {
            throw new RuntimeException("can not get serviceId");
        }

        // 通过 serviceId 去拿可用服务实例
        List<ServiceInstance> targetInstances = discoveryClient.getInstances(serviceId);
        if (CollectionUtils.isEmpty(targetInstances)) {
            throw new RuntimeException("can not get target instance from serviceId: " +
                    serviceId);
        }

        // 随机选择一个服务实例: 负载均衡
        ServiceInstance randomInstance = targetInstances.get(
                new Random().nextInt(targetInstances.size())
        );
        log.info("choose service instance: [{}], [{}], [{}]", serviceId,
                randomInstance.getHost(), randomInstance.getPort());

        // Feign 客户端初始化, 必须要配置 encoder、decoder、contract
        AuthorityFeignClient feignClient = Feign.builder()  // 1. Feign 默认配置初始化
                .encoder(new GsonEncoder())                 // 2.1 设置定义配置
                .decoder(new GsonDecoder())                 // 2.2 设置定义配置
                .logLevel(Logger.Level.FULL)                // 2.3 设置定义配置
                .contract(new SpringMvcContract())
                .target(                                    // 3 生成代理对象
                        AuthorityFeignClient.class,
                        String.format("http://%s:%s",
                                randomInstance.getHost(), randomInstance.getPort())
                );

        return feignClient.getTokenByFeign(usernameAndPassword);
    }
    
}

控制层

package com.edcode.commerce.controller;

import com.edcode.commerce.service.communication.feign.AuthorityFeignClient;
import com.edcode.commerce.service.communication.feign.UseFeignApi;
import com.edcode.commerce.service.communication.restTemplate.UseRestTemplateService;
import com.edcode.commerce.service.communication.ribbon.UseRibbonService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 微服务通信 Controller
 */
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {

	private final UseFeignApi useFeignApi;

	@PostMapping("/thinking-in-feign")
	public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
		return useFeignApi.thinkingInFeign(usernameAndPassword);
	}
}

communication.http

### 通过原生 Feign Api 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/thinking-in-feign
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

11.6.1.4 Feign 客户端初始化的过程

  • Feign 客户端初始化包含三个部分
AuthorityFeignClient feignClient = Feign.builder()  // 1. Feign 默认配置初始化
        .encoder(new GsonEncoder())                 // 2.1 设置定义配置
        .decoder(new GsonDecoder())                 // 2.2 设置定义配置
        .logLevel(Logger.Level.FULL)                // 2.3 设置定义配置
        .contract(new SpringMvcContract())
        .target(                                    // 3 生成代理对象
                AuthorityFeignClient.class,
                String.format("http://%s:%s",
                        randomInstance.getHost(), randomInstance.getPort())
        );

11.7.1 微服务通信总结

  • 三类常用的微服务通信方案
    • RPC 效率高,可选实现方式多
    • REST 标准化程度高,学习、使用成本低
    • Message 对于削峰填谷有重大意义
  • Rest -> Ribbon -> OpenFeign 演进过程
    • Rest 需要写死服务的 IP 与 Port (可以通过注册中心手动获取),灵活性低
    • Ribbon 提供基于 RestTemplate 的 HTTP 客户端并且支持服务负载均衡功能
    • OpenFeign 基于 Ribbon,只需要使用注解和接口的配置即可完成对服务提供方的接口绑定