Spring Cloud Feign

RestTemplate

依赖注入

1
2
3
4
5
6
@Bean
// 开启负载均衡
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

接下来便可以使用资源地址调用服务

1
2
String url ="http://provider/getHi";
String respStr = restTemplate.getForObject(url, String.class);

get 请求处理

getForEntity

getForEntity方法的返回值是一个ResponseEntity,ResponseEntity是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。

1
<200,Hi,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"8", Date:"Fri, 10 Apr 2020 09:58:44 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>

返回一个Map

调用方

1
2
3
String url ="http://provider/getMap";
ResponseEntity<Map> entity = restTemplate.getForEntity(url, Map.class);
System.out.println("respStr: " + entity.getBody() );

生产方

1
2
3
4
5
6
@GetMapping("/getMap")
public Map<String, String> getMap() {
HashMap<String, String> map = new HashMap<>();
map.put("name", "500");
return map;
}

返回对象

调用方

1
2
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class);
System.out.println("respStr: " + ToStringBuilder.reflectionToString(entity.getBody() ));

生产方

1
2
3
4
5
6
7
@GetMapping("/getObj")
public Person getObj() {
Person person = new Person();
person.setId(100);
person.setName("xiaoming");
return person;
}
1
2
3
4
public class Person {
private int id;
private String name;
}

传参调用

使用占位符

1
2
String url ="http://provider/getObjParam?name={1}";
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class,"hehehe...");

使用map

1
2
3
String url ="http://provider/getObjParam?name={name}";
Map<String, String> map = Collections.singletonMap("name", " memeda");
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class,map);

返回对象

1
Person person = restTemplate.getForObject(url, Person.class,map);

post 请求处理

调用方

1
2
3
//form表单的post请求可以用参数拼接
String url ="http://provider/getObjParam?name=admin";
ResponseEntity<AdapterInfo> result = restTemplate.postForEntity(url, null, AdapterInfo.class);

调用方

1
2
3
4
//Application-JSON的请求、@ResquestBody需要使用Map当参数,不能使用参数拼接
String url ="http://provider/postParam";
Map<String, String> map = Collections.singletonMap("name", " memeda");
ResponseEntity<Person> entity = restTemplate.postForEntity(url, map, Person.class);

生产方

1
2
3
4
5
6
7
8
@PostMapping("/postParam")
public Person postParam(@RequestBody String name) {
System.out.println("name:" + name);
Person person = new Person();
person.setId(100);
person.setName("xiaoming" + name);
return person;
}

postForLocation

postForLocation也是提交新资源,提交成功之后,返回新资源的URI,postForLocation的参数和前面两种的参数基本一致,只不过该方法的返回值为Uri,这个只需要服务提供者返回一个Uri即可,该Uri表示新资源的位置。

调用方

1
2
3
4
String url ="http://provider/postParam";
Map<String, String> map = Collections.singletonMap("name", " memeda");
URI location = restTemplate.postForLocation(url, map, Person.class);
System.out.println(location);

生产方

需要设置头信息,不然返回的是null

1
2
3
4
public URI postParam(@RequestBody Person person,HttpServletResponse response) throws Exception {
URI uri = new URI("https://www.baidu.com/s?wd="+person.getName());
response.addHeader("Location", uri.toString());
}

exchange

可以自定义http请求的头信息,同时保护get和post方法

拦截器

需要实现ClientHttpRequestInterceptor接口

拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {

System.out.println("拦截啦!!!");
System.out.println(request.getURI());

ClientHttpResponse response = execution.execute(request, body);

System.out.println(response.getHeaders());
return response;
}
}

添加到resttemplate中

1
2
3
4
5
6
7
@Bean
@LoadBalanced
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}

Ribbon

两种负载均衡

​ 当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。

1
2
3
4
5
6
7
软件负载均衡:nginx,lvs

硬件负载均衡:F5

我们只关注软件负载均衡,
第一层可以用DNS,配置多个A记录,让DNS做第一层分发。
第二层用比较流行的是反向代理,核心原理:代理根据一定规则,将http请求转发到服务器集群的单一服务器上。

软件负载均衡分为:服务端(集中式),客户端。

服务端负载均衡:在客户端和服务端中间使用代理,nginx。

客户端负载均衡:根据自己的情况做负载。Ribbon就是。

客户端负载均衡和服务端负载均衡最大的区别在于 服务端地址列表的存储位置,以及负载算法在哪里

客户端负载均衡

在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些列表统统都是从服务注册中心获取的;

服务端负载均衡

在服务端负载均衡中,客户端节点只知道单一服务代理的地址,服务代理则知道所有服务端的地址。

1
2
3
4
5
手写客户端负载均衡
1、知道自己的请求目的地(虚拟主机名,默认是spring.application.name)
2、获取所有服务端地址列表(也就是注册表)。
3、选出一个地址,找到虚拟主机名对应的ip、port(将虚拟主机名 对应到 ip和port上)。
4、发起实际请求(最朴素的请求)。
1
2
3
4
5
Ribbon作为Spring Cloud的负载均衡机制的实现,

1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。
2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。
3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了ribbon。

负载均衡算法

默认实现:

ZoneAvoidanceRule(区域权衡策略):复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。

其他规则:

BestAvailableRule(最低并发策略):会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。

RoundRobinRule(轮询策略):以简单轮询选择一个服务器。按顺序循环选择一个server。

RandomRule(随机策略):随机选择一个服务器。

AvailabilityFilteringRule(可用过滤策略):会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。

WeightedResponseTimeRule(响应时间加权策略):据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。

RetryRule(重试策略):先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。

切换负载均衡策略

注解方式

1
2
3
4
5
@Bean
public IRule myRule(){
//return new RoundRobinRule();
//return new RandomRule();
return new RetryRule();

配置文件

针对服务定ribbon策略:

1
provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

给所有服务定ribbon策略:

1
ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

属性配置方式优先级高于Java代码。

Ribbon脱离Eureka

1
2
ribbon.eureka.enabled=false
ribbon.listOfServers=localhost:80,localhost:81

Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址。

Feign

OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用http api。

OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。

feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。

英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程,回想第一节课的SOP。

Feign和OpenFeign的关系

Feign本身不支持Spring MVC的注解,它有一套自己的注解

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,
并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

公共API依赖

编写公共API,并且打成Jar包(User-API),然后给生产者和消费者引用。

接口有参数时必须加@RequestParam注解标识参数名称

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/User")
public interface RegisterApi {

@GetMapping("/isAlive")
public String isAlive();

@GetMapping("/getMap")
public void getMap(@RequestParam("id") Integer id);

@GetMapping("/getMap3")
public Map<Integer, String> getMap3(@RequestParam Map<String, Object> map);
}

生产者

创建生产者项目User-Provider,引入公共API依赖,注册eureka,生产者编写Controller时继承RegisterApi,实现方法,作为实现类

1
2
3
4
5
<dependency>
<groupId>com.kuro.User-API</groupId>
<artifactId>User-API</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class UserController implements RegisterApi {

@Override
public String isAlive() {
return "ok";
}

@GetMapping("/getMap")
public void map(Integer id) {
}

@GetMapping("/getMap3")
public Map<Integer, String> map3(@RequestParam Map<String, Object> map) {
return map;
}
}

消费者

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>

创建消费者项目User-Consumer引入公共API依赖,继承RegisterApi,并添加@FeignClient,标识给feign,消费者代理和调用接口,具体实现在生产者项目中。

1
2
3
@FeignClient(name = "user-provider")
public interface UserConsumerService extends RegisterApi {
}
1
2
3
4
5
6
7
8
9
10
@RestController
public class ConsumerController {
@Autowired
UserConsumerService consumerSrv;

@GetMapping("/alive")
public String alive() {
return consumerSrv.isAlive();
}
}
1
2
3
4
5
6
7
8
9
@SpringBootApplication
//开启feign注解
@EnableFeignClients
public class UserConsumerApplication {

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

Feign默认所有带参数的请求都是Post,想要使用指定的提交方式需引入依赖

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

日志

1
2
3
4
5
6
7
8
9
//重写日志等级
@Configuration
public class FeiginConfig {

@Bean
Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
1
2
#配置日志等级
logging.level.com.kuro.UserConsumer=debug

权限

feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。

允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。

1
2
3
4
5
<!-- 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable();
// 表示所有的访问都必须认证,认证处理后才可以正常进行
http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
// 所有的rest服务一定要设置为无状态,以提升操作效率和性能
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
1
2
3
4
5
spring: 
security:
user:
name: root
password: root
1
2
3
4
5
6
public class FeignAuthConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("root", "root");
}
}
1
2
//在feign上加配置
@FeignClient(name = "user-provider",configuration = FeignAuthConfiguration.class)
1
2
3
4
小结:如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。
最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动:

@FeignClient(name = "user-provider",configuration = FeignAuthConfiguration.class)

超时

Feign默认支持Ribbon;Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制

1
2
3
4
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000

重试

1
2
3
4
5
6
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false

使用ribbon重试机制,请求失败后,在6秒内不会再请求该失败的机器,每隔6秒会再次重新尝试该失败的机器。

拦截器

1
2
3
4
5
6
7
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
//拦截处理
}
}
1
2
3
4
5
6
feign:
client:
config:
user-provider:
request-interceptors:
- com.kuro.interceptor.MyBasicAuthRequestInterceptor

压缩

1
2
3
4
5
6
7
8
9
10
#服务端开启压缩
server.compression.enabled=true

#调用方压缩配置
#配置请求GZIP压缩
feign.compression.request.enabled=true
#配置响应GZIP压缩
feign.compression.response.enabled=true
#单位是B
feign.compression.request.min-request-size=100

原理

1
2
3
4
5
6
7
8
1. 主程序入口添加@EnableFeignClients注解开启对Feign Client扫描加载处理。根据Feign Client的开发规范,定义接口并加@FeignClient注解。

2. 当程序启动时,会进行包扫描,扫描所有@FeignClient注解的类,并将这些信息注入Spring IoC容器中。
当定义的Feign接口中的方法被调用时,通过JDK的代理方式,来生成具体的RequestTemplate。
当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装了HTTP请求需要的全部信息,如请求参数名、请求方法等信息都在这个过程中确定。

3. 然后由RequestTemplate生成Request,然后把这个Request交给client处理,这里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp。
最后Client被封装到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。

最后更新: 2020年12月09日 17:48

原始链接: https://midkuro.gitee.io/2020/06/27/springcloud-feign/

× 请我吃糖~
打赏二维码