parent
6b69d06381
commit
3e350dc766
Binary file not shown.
After Width: | Height: | Size: 147 KiB |
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>OpenFeign 配置类</h1>
|
||||
* */
|
||||
@Configuration
|
||||
public class FeignConfig {
|
||||
|
||||
/**
|
||||
* <h2>开启 OpenFeign 日志</h2>
|
||||
* */
|
||||
@Bean
|
||||
public Logger.Level feignLogger() {
|
||||
return Logger.Level.FULL; // 需要注意, 日志级别需要修改成 debug, 因为 feign 基本都是 debug 级别的日志, info 的日志很少
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>OpenFeign 开启重试</h2>
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* <h2>对请求的连接和响应时间进行限制</h2>
|
||||
* */
|
||||
@Bean
|
||||
public Request.Options options() {
|
||||
|
||||
return new Request.Options(
|
||||
CONNECT_TIMEOUT_MILLS, TimeUnit.MICROSECONDS,
|
||||
READ_TIMEOUT_MILLS, TimeUnit.MILLISECONDS,
|
||||
// 转发请求是否要进行限制
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>OpenFeign 使用 OkHttp 配置类</h1>
|
||||
* 前置条件是 Feign
|
||||
* */
|
||||
@Configuration
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
public class FeignOkHttpConfig {
|
||||
|
||||
/**
|
||||
* <h2>注入 OkHttp, 并自定义配置</h2>
|
||||
* */
|
||||
@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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>使用 Feign 的原生 Api, 而不是 OpenFeign = Feign + Ribbon</h1>
|
||||
* */
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UseFeignApi {
|
||||
|
||||
private final DiscoveryClient discoveryClient;
|
||||
|
||||
public UseFeignApi(DiscoveryClient discoveryClient) {
|
||||
this.discoveryClient = discoveryClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>使用 Feign 原生 api 调用远端服务</h2>
|
||||
* 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<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
|
||||
// 默认的 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>使用 Ribbon 之前的配置, 增强 RestTemplate</h1>
|
||||
* */
|
||||
@Component
|
||||
public class RibbonConfig {
|
||||
|
||||
/**
|
||||
* <h2>注入 RestTemplate</h2>
|
||||
* */
|
||||
@Bean
|
||||
@LoadBalanced
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>与 Authority 服务通信的 Feign Client 接口定义</h1>
|
||||
* */
|
||||
@FeignClient(
|
||||
// contextId 是对 FeignClient 的声明, 每一个进行的通信都要进行定义, value 表示需要进行通信的服务id是什么
|
||||
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center"
|
||||
// fallback = AuthorityFeignClientFallback.class
|
||||
// fallbackFactory = AuthorityFeignClientFallbackFactory.class
|
||||
)
|
||||
public interface AuthorityFeignClient {
|
||||
|
||||
/**
|
||||
* <h2>通过 OpenFeign 访问 Authority 获取 Token</h2>
|
||||
*
|
||||
* 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);
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* <h1>使用 Ribbon 实现微服务通信</h1>
|
||||
* */
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <h2>通过 Ribbon 调用 Authority 服务获取 Token, [{实战使用}]</h2>
|
||||
* */
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* <h2>使用原生的 Ribbon Api, 看看 Ribbon 是如何完成: 服务调用 + 负载均衡 [{学习使用}]</h2>
|
||||
* */
|
||||
public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {
|
||||
|
||||
String urlFormat = "http://%s/ecommerce-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("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);
|
||||
}
|
||||
}
|
@ -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
|
Loading…
Reference in New Issue