diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/pic/Feign实现流程图.png b/dev-protocol-springcloud/dev-protocol-springcloud-communication/pic/Feign实现流程图.png new file mode 100644 index 0000000..1daa67f Binary files /dev/null and b/dev-protocol-springcloud/dev-protocol-springcloud-communication/pic/Feign实现流程图.png differ diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/pom.xml b/dev-protocol-springcloud/dev-protocol-springcloud-communication/pom.xml index 240f4af..538cc28 100644 --- a/dev-protocol-springcloud/dev-protocol-springcloud-communication/pom.xml +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/pom.xml @@ -20,6 +20,12 @@ + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + io.github.openfeign feign-micrometer @@ -28,6 +34,19 @@ org.springframework.cloud spring-cloud-starter-openfeign + + + io.github.openfeign + feign-okhttp + + + + io.github.openfeign + feign-gson + 12.1 + + + org.springframework.boot spring-boot-starter-web @@ -36,6 +55,8 @@ org.springframework.boot spring-boot-starter-test + + org.projectlombok lombok diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/controller/CommunicationController.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/controller/CommunicationController.java index 91bf95b..c778c0c 100644 --- a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/controller/CommunicationController.java +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/controller/CommunicationController.java @@ -2,7 +2,10 @@ package org.example.controller; import org.example.common.vo.JwtToken; import org.example.common.vo.UsernameAndPassword; +import org.example.feign.UseFeignApi; +import org.example.service.AuthorityFeignClient; import org.example.service.UseRestTemplateService; +import org.example.service.UseRibbonService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -16,19 +19,19 @@ import org.springframework.web.bind.annotation.RestController; public class CommunicationController { private final UseRestTemplateService restTemplateService; -// private final UseRibbonService ribbonService; -// private final AuthorityFeignClient feignClient; -// private final UseFeignApi useFeignApi; + private final UseRibbonService ribbonService; + private final AuthorityFeignClient feignClient; + private final UseFeignApi useFeignApi; - public CommunicationController(UseRestTemplateService restTemplateService -// UseRibbonService ribbonService, -// AuthorityFeignClient feignClient, -// UseFeignApi useFeignApi + public CommunicationController(UseRestTemplateService restTemplateService, + UseRibbonService ribbonService, + AuthorityFeignClient feignClient, + UseFeignApi useFeignApi ) { this.restTemplateService = restTemplateService; -// this.ribbonService = ribbonService; -// this.feignClient = feignClient; -// this.useFeignApi = useFeignApi; + this.ribbonService = ribbonService; + this.feignClient = feignClient; + this.useFeignApi = useFeignApi; } @PostMapping("/rest-template") @@ -44,7 +47,7 @@ public class CommunicationController { usernameAndPassword); } - /* @PostMapping("/ribbon") + @PostMapping("/ribbon") public JwtToken getTokenFromAuthorityServiceByRibbon( @RequestBody UsernameAndPassword usernameAndPassword) { return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword); @@ -63,5 +66,5 @@ public class CommunicationController { @PostMapping("/thinking-in-feign") public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) { return useFeignApi.thinkingInFeign(usernameAndPassword); - }*/ + } } diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignConfig.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignConfig.java new file mode 100644 index 0000000..b5e97e8 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignConfig.java @@ -0,0 +1,57 @@ +package org.example.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; + +/** + *

OpenFeign 配置类

+ * */ +@Configuration +public class FeignConfig { + + /** + *

开启 OpenFeign 日志

+ * */ + @Bean + public Logger.Level feignLogger() { + return Logger.Level.FULL; // 需要注意, 日志级别需要修改成 debug, 因为 feign 基本都是 debug 级别的日志, info 的日志很少 + } + + /** + *

OpenFeign 开启重试

+ * period = 100 发起当前请求的时间间隔, 单位是 ms + * maxPeriod = 1000 发起当前请求的最大时间间隔, 单位是 ms + * maxAttempts = 5 最多请求次数 + * */ + @Bean + public Retryer feignRetryer() { + return new Retryer.Default( + 100, + SECONDS.toMillis(1), + 5 + ); + } + + public static final int CONNECT_TIMEOUT_MILLS = 5000; + 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 + ); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignOkHttpConfig.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignOkHttpConfig.java new file mode 100644 index 0000000..379f535 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/FeignOkHttpConfig.java @@ -0,0 +1,39 @@ +package org.example.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; + +/** + *

OpenFeign 使用 OkHttp 配置类

+ * 前置条件是 Feign + * */ +@Configuration +@ConditionalOnClass(Feign.class) +@AutoConfigureBefore(FeignAutoConfiguration.class) +public class FeignOkHttpConfig { + + /** + *

注入 OkHttp, 并自定义配置

+ * */ + @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(); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/UseFeignApi.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/UseFeignApi.java new file mode 100644 index 0000000..95230b3 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/feign/UseFeignApi.java @@ -0,0 +1,87 @@ +package org.example.feign; + +import feign.Feign; +import feign.Logger; +import feign.gson.GsonDecoder; +import feign.gson.GsonEncoder; +import lombok.extern.slf4j.Slf4j; +import org.example.common.vo.JwtToken; +import org.example.common.vo.UsernameAndPassword; +import org.example.service.AuthorityFeignClient; +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 org.springframework.util.CollectionUtils; + +import java.util.List; +import java.lang.annotation.Annotation; +import java.util.Random; + +/** + *

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

+ * */ +@Slf4j +@Service +public class UseFeignApi { + + private final DiscoveryClient discoveryClient; + + public UseFeignApi(DiscoveryClient discoveryClient) { + this.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 去拿可用服务实例 OpenFeign 是使用 Ribbon 做这个事 + List 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 + // 默认的 encoder、decoder 不支持对象,不能实现编解码, 所以要自己进行定义 + // 默认的 contract 默认的协议不支持 SpringCloud 对应的 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()) + ); + + return feignClient.getTokenByFeign(usernameAndPassword); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/ribbon/RibbonConfig.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/ribbon/RibbonConfig.java new file mode 100644 index 0000000..a5ffccb --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/ribbon/RibbonConfig.java @@ -0,0 +1,22 @@ +package org.example.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; + +/** + *

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

+ * */ +@Component +public class RibbonConfig { + + /** + *

注入 RestTemplate

+ * */ + @Bean + @LoadBalanced + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/AuthorityFeignClient.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/AuthorityFeignClient.java new file mode 100644 index 0000000..d4c7bab --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/AuthorityFeignClient.java @@ -0,0 +1,31 @@ +package org.example.service; + +import org.example.common.vo.JwtToken; +import org.example.common.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; + +/** + *

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

+ * */ +@FeignClient( + // contextId 是对 FeignClient 的声明, 每一个进行的通信都要进行定义, value 表示需要进行通信的服务id是什么 + contextId = "AuthorityFeignClient", value = "e-commerce-authority-center" +// fallback = AuthorityFeignClientFallback.class +// fallbackFactory = AuthorityFeignClientFallbackFactory.class +) +public interface AuthorityFeignClient { + + /** + *

通过 OpenFeign 访问 Authority 获取 Token

+ * + * value 只需要定义路径即可, 不需要定义 ip + port + * consumes, produces: 标识请求和返回的数据格式, 可以不指定, 但是在使用原生 Api 接口的时候必须要进行指定, 要不会不能进行识别对应的接口 + * */ + @RequestMapping(value = "/ecommerce-authority-center/authority/token", + method = RequestMethod.POST, + consumes = "application/json", produces = "application/json") + JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword); +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/UseRibbonService.java b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/UseRibbonService.java new file mode 100644 index 0000000..355971b --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/java/org/example/service/UseRibbonService.java @@ -0,0 +1,116 @@ +package org.example.service; + +import com.alibaba.fastjson.JSON; +import com.netflix.loadbalancer.*; +import com.netflix.loadbalancer.reactive.LoadBalancerCommand; +import lombok.extern.slf4j.Slf4j; +import org.example.common.constant.CommonConstant; +import org.example.common.vo.JwtToken; +import org.example.common.vo.UsernameAndPassword; +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; + +/** + *

使用 Ribbon 实现微服务通信

+ * */ +@Slf4j +@Service +public class UseRibbonService { + + private final RestTemplate restTemplate; + private final DiscoveryClient discoveryClient; + + public UseRibbonService(RestTemplate restTemplate, + DiscoveryClient discoveryClient) { + this.restTemplate = restTemplate; + this.discoveryClient = discoveryClient; + } + + + /** + *

通过 Ribbon 调用 Authority 服务获取 Token, [{实战使用}]

+ * */ + public JwtToken getTokenFromAuthorityServiceByRibbon( + UsernameAndPassword usernameAndPassword) { + + // 注意到 url 中的 ip 和端口换成了服务名称 + String requestUrl = String.format( + "http://%s/ecommerce-authority-center/authority/token", + CommonConstant.AUTHORITY_CENTER_SERVICE_ID + ); + log.info("login request url and body: [{}], [{}]", 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/ecommerce-authority-center/authority/token"; + + // 1. 找到服务提供方的地址和端口号 + List targetInstances = discoveryClient.getInstances( + CommonConstant.AUTHORITY_CENTER_SERVICE_ID + ); + + // 构造 Ribbon 服务列表 + List servers = new ArrayList<>(targetInstances.size()); + targetInstances.forEach(i -> { + servers.add(new Server(i.getHost(), i.getPort())); + log.info("found target instance: [{}] -> [{}]", 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("target request 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); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/bootstrap.yml b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..2795d6f --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/bootstrap.yml @@ -0,0 +1,18 @@ +# Feign 的相关配置 +feign: + # feign 开启 gzip 压缩 + compression: + request: + enabled: true # 对请求的数据开启压缩 + mime-types: text/xml,application/xml,application/json # 压缩的数据类型 + min-request-size: 1024 # 最小多大的请求数据开启压缩 + response: + enabled: true # 对响应的数据开启压缩 + # 禁用默认的 http, 启用 okhttp + httpclient: + enabled: false + okhttp: + enabled: true +# # OpenFeign 集成 Hystrix +# hystrix: +# enabled: true \ No newline at end of file diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/http/communication.http b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/http/communication.http index 3501e51..98da9c7 100644 --- a/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/http/communication.http +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/src/main/resources/http/communication.http @@ -17,6 +17,25 @@ Content-Type: application/json "password": "25d55ad283aa400af464c76d713c07ad" } +### 通过 Ribbon 去获取 Token +POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/ribbon +Content-Type: application/json + +{ + "username": "Qinyi@imooc.com", + "password": "25d55ad283aa400af464c76d713c07ad" +} + + +### 通过原生 Ribbon Api 去获取 Token +POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/thinking-in-ribbon +Content-Type: application/json + +{ + "username": "Qinyi@imooc.com", + "password": "25d55ad283aa400af464c76d713c07ad" +} + ### 通过 OpenFeign 获取 Token POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/token-by-feign Content-Type: application/json diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-communication/微服务通信方案.md b/dev-protocol-springcloud/dev-protocol-springcloud-communication/微服务通信方案.md index 6b89acb..2f76f0a 100644 --- a/dev-protocol-springcloud/dev-protocol-springcloud-communication/微服务通信方案.md +++ b/dev-protocol-springcloud/dev-protocol-springcloud-communication/微服务通信方案.md @@ -36,14 +36,49 @@ - 通过注册中心(推荐Nacos)获取服务地址,可以实现负载均衡的效果 --- +## 3. Ribbon 实现微服务通信及其原理解读 +- 如何使用 + - 引入依赖: spring-cloud-starter-netflix-ribbon + - 增强 RestTemplate @LoadBalanced 注解.使其具备负载均衡的能力 +- ps: 学习源码最好的方式就是对部分看懂的逻辑进行仿写 +--- +## 4. OpenFeign 的简单应用及配置 +- OpenFeign 是实际企业中使用更为常见的一种使用 +- 如何使用 + - 引入依赖 spring-cloud-starter-openfeign + - 添加 @EnableFeignClients 注解, 启用 open-feign +- SpringCloud OpenFeign 最常用的配置 + - OpenFeign 开启 gzip 压缩 + - 统一 OpenFeign 使用配置: 日志、重试、请求连接和响应时间限制 + - 使用 okhttp 替换 httpclient(别忘了引入 okhttp 依赖) + - feign-okhttp + - 补充: okhttp 和 httpclient 的区别和优缺点 +- ps: 写框架或者工程加载时候的日志一般都用 debug 级别, 别总是打 info 级别的日志 +--- +- OkHttp和HttpClient都是Java语言中常用的HTTP客户端库,用于发送HTTP请求并处理响应。它们之间的区别如下: + - OkHttp是由Square公司开发的,而HttpClient是Apache软件基金会开发的。 + - OkHttp的性能更快,更高效。因为它使用了连接池、请求压缩、缓存等技术,而HttpClient则需要手动进行配置和优化才能达到类似的效果。 + - OkHttp支持SPDY(升级版的HTTP/1.1协议)和HTTP/2,而HttpClient只支持HTTP/1.1。 + - OkHttp是基于异步请求的,而HttpClient只能使用同步请求。 + - OkHttp的代码量更少,更加简洁易懂,而HttpClient的API更加复杂,使用起来需要更多的代码。 + - OkHttp的优点: + - 高效,支持连接池、请求压缩、缓存等技术,性能更快。 + - 支持SPDY和HTTP/2协议,可以更好地适应现代网络环境。 + - 简单易用,代码量少,API清晰明了。 + - HttpClient的优点: + - 稳定可靠,经过多年的发展和优化,被广泛使用和验证。 + - 功能强大,支持重试、重定向、认证、代理、Cookie等功能,使用起来非常灵活。 + - 综上所述,**OkHttp适用于对性能和效率要求比较高的场景,而HttpClient适用于功能比较复杂的场景。** - +--- +## 5. 通过 Feign 的原生 API 解析其实现原理 +- ![Feign实现流程图.png](pic/Feign实现流程图.png) ## OpenFeign 核心源码解析