注:本人所有的spring-cloud系列的文章均为黑马的《Spring Cloud微服务架构开发》的个人笔记。
- 掌握Feign的应用
- 掌握使用Feign实现声明式的REST调用
- 理解Feign的参数绑定以及继承特性
- 掌握FeignClient 的配置方法
在上一篇文章中Ribbon与RestTemplate实现了负载均衡,本章讲解Feign。当Feign、Eureka、Ribbon组合使用时,Feign就具有了负载均衡功能,只需要自定义一个接口并使用注解配置,即可完成服务接口的绑定,从而简化。
与RestTemplate相比:
- Feign不需要RibbonConfig配置类
- 请求方式不一样 RestTemplate需要每个请求都拼接url+参数+类文件,灵活性高但是消息封装臃肿。Feign使用注解
- 底层实现方式不一样
RestTemplate在拼接url的时候,可以直接指定ip地址+端口号,不需要经过服务注册中心就可以直接请求接口;也可以指定服务名,请求先到服务注册中心(如nacos)获取对应服务的ip地址+端口号,然后经过HTTP转发请求到对应的服务接口(注意:这时候的restTemplate需要添加@LoadBalanced注解,进行负载均衡)。
Feign的底层实现是动态代理,如果对某个接口进行了@FeignClient注解的声明,Feign就会针对这个接口创建一个动态代理的对象,在调用这个接口的时候,其实就是调用这个接口的代理对象,代理对象根据@FeignClient注解中name的值在服务注册中心找到对应的服务,然后再根据@RequestMapping等其他注解的映射路径构造出请求的地址,针对这个地址,再从本地实现HTTP的远程调用。
- 第一个Feign程序
- 参数绑定
- 继承特性
- Ribbon的相关配置
- 日志配置
1.第一个Feign程序
1.1 使用以往的eureka-server作为注册中心
1.2 使用以往的eureka-provider作为服务提供者
创建一个controller包,创建一个HelloController类
package com.li.eurekaprovide.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello(){
return "hello,feign";
}
}
1.3 创建一个名为eureka-feign-client的模块
1.4 pom.xml添加Eureka Feign Web依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--1.父工程启动类信息-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.li</groupId>
<artifactId>spring-provide</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-provide</name>
<description>Demo project for spring boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<dependencies>
<!--2.spring-cloud-starter-netflix-eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--3.spring-boot-starter-test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--4.spring-boot-starter-web 客户端需要web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
<!--5.父依赖相关信息-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
1.5 修改配置文件为application.yml
spring:
application:
name: eureka-feign-client
server:
port: 8764
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
1.6 在启动类添加注解
@EnableEurekaClient
@EnableFeignClients
1.7 创建service包,创建FeignService接口
package com.li.eurekaprovide.eurekafeignclient.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
@Service
//此处为提供者配置文件application的name,相当于ip+端口
@FeignClient(name = "eureka-provider")
public interface FeignService {
//此处的值是提供者实例的api
@GetMapping("/hello")
public String sayHello();
}
1.8 创建controller包,创建FeignController类
该类定义的hello()方法用于调用FeignService的sayHello()方法
package com.li.eurekaprovide.eurekafeignclient.controller;
import com.li.eurekaprovide.eurekafeignclient.service.FeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
@Autowired
FeignService feignService;
@GetMapping("/hello") //供浏览器访问
public String sayHello(){
return feignService.sayHello();
}
1.9 测试
依次启动eureka-server,eureka-provide,eureka-feign-client
浏览器访问localhost:8764/hello
1.10 注意包类的层级结构,否则有报错的可能性
============================================
2. 参数绑定
2.1 在eureka-provider和eureka-feign-client创建entity包,User实体类
package com.li.eurekaprovide.eneity;
public class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
2.2 修改eureka-provider的HelloController
添加hello1,hello2,hello3,其中hello3是**@PostMapping**;
package com.li.eurekaprovide.controller;
import com.li.eurekaprovide.eneity.User;
import org.springframework.web.bind.annotation.*;
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello(){
return "hello,feign";
}
@GetMapping("/hello1")
public String hello(@RequestParam String name){
return "hello,"+name;
}
@GetMapping("/hello2")
public User hello(@RequestParam String name, @RequestParam Integer age){
return new User(name, age);
}
@PostMapping("/hello3") //注意PostMapping
public String hello(@RequestBody User user){
return "hello,"+user.getName()+","+user.getAge();
}
}
2.3 修改eureka-feign-client的service包的Feignservice
package com.li.eurekaprovide.eurekafeignclient.service;
import com.li.eurekaprovide.eurekafeignclient.eneity.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
@Service
@FeignClient(name = "eureka-provider")
public interface FeignService {
//此处的值是提供者实例的api
@GetMapping("/hello")
public String sayHello();
@GetMapping("/hello1")
public String hello(@RequestParam String name);
@GetMapping("/hello2")
public User hello(@RequestParam String name, @RequestParam Integer age);
@PostMapping("/hello3") //注意Post
public String hello(@RequestBody User user);
}
2.4 修改eureka-feign-client的controller的FeignController
package com.li.eurekaprovide.eurekafeignclient.controller;
import com.li.eurekaprovide.eurekafeignclient.eneity.User;
import com.li.eurekaprovide.eurekafeignclient.service.FeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
@Autowired
FeignService feignService;
@GetMapping("/hello") //供浏览器访问
public String sayHello(){
return feignService.sayHello();
}
@GetMapping("/helloUser")
public String helloUser(){
StringBuilder sb = new StringBuilder();
//append 追加内容
sb.append(feignService.hello("abcd")).append("<br/>")
.append(feignService.hello(new User("abcd",30))).append("<br/>")
.append(feignService.hello("abcd",30)).append("<br/>");
return sb.toString();
}
}
2.5 访问 localhost:8764/helloUser
============================================
3.继承特性
解决代码重复问题,进一步减少编码量,将重复的实体类和接口抽取成一个独立的模块,打成一个jar包,在其他模块使用的时候导入对应的jar包
3.1 新建一个spring-boot项目common-client
pom.xml,只需要web和openfeign依赖和 jar
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging> <!--打包成jar包-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.li</groupId>
<artifactId>common-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common-client</name>
<description>common-client</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--用到一些springMVC的注解-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--引入feign客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
</dependencies>
</project>
复制eureka-feign-client的User类和FeignService接口,修改接口名称为FeignClientService
3.2 新建服务提供者eureka-provider
- 使用前面的提供者的pom.xml,引入common-client的jar包
<dependency>
<groupId>com.li</groupId>
<artifactId>common-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 在controller包下面新建一个RefactorController类,实现FeignClientService接口
package com.li.eurekaprovide.eurekaprovideanother.controller;
import com.li.entity.User;
import com.li.service.FeignClientService;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RefactorController implements FeignClientService {
@Override
public String sayHello() {return "hello,feign";}
@Override
public String hello( String name) {return "hello,"+name;}
@Override
public User hello( String name, Integer age) {
return new User(name, age);
}
@Override
public String hello( User user) {
return "hello,"+user.getName()+","+user.getAge();
}
}
application.yml
server:
port: 7007
spring:
application:
name: eureka-provider
eureka:
client:
service-url:
defaultZone:
http://localhost:7000/eureka/
instance:
hostname: localhost
3.3 新建服务消费者eureka-feign-client
- 复制前面的服务消费者的pom.xml,引入common-client的jar包
<dependency>
<groupId>com.li</groupId>
<artifactId>common-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 在service包下新建一个RefactorService接口
package com.li.eurekaprovide.eurekafeignclient.service;
import com.li.service.FeignClientService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
@FeignClient(name = "eureka-provider")
public interface RefactorService extends FeignClientService {
}
3.application.yml
spring:
application:
name: eureka-feign-client
server:
port: 8764
eureka:
client:
service-url:
defaultZone: http://localhost:7000/eureka
- 修改controller下面的FeignController 注入共用模块的接口, 添加一个方法
package com.li.eurekaprovide.eurekafeignclient.controller;
import com.li.eurekaprovide.eurekafeignclient.eneity.User;
import com.li.eurekaprovide.eurekafeignclient.service.FeignService;
import com.li.service.FeignClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class FeignController {
/*注入接口*/
@Autowired
FeignClientService feignClientService;
/*浏览器访问此*/
@GetMapping("/helloRefactor")
public String helloRefactor(){
StringBuilder sb = new StringBuilder();
sb.append(feignClientService.hello("abcd")).append("<br/>")
.append(feignClientService.hello(new com.li.entity.User("abcd",30))).append("<br/>")
.append(feignClientService.hello("abcd",30)).append("<br/>");
return sb.toString();
}
}
也就是说,浏览器访问eureka-feign-client的FeignController类的helloRefactor()方法,然后该方法调用共用模块common-client的接口,然后调用服务提供者的controller
测试结果
Ribbon的相关配置
Feign 默认整合了Ribbon能够实现负载均衡,我们可以通过配置Ribbon,自定义各个服务的调用方式。接下来,我们从全局配置和指定服务配置两个方面介绍如何在Spring Cloud Feign 中进行Ribbon的相关配置。
1.全局配置
Ribbon的全局配置其实非常简单,我们可以直接在application.yml.配置文件中,使用ribbon.=的方式设定ribbon的各项默认参数,示例代码如下:
ribbon :
conInectTimeout: 5000 #设置连接超时时间,默认为1s 此为5s
ReadTimeout:5000 #设置读取超时时间
OkToRetryOnAlloperations: true #对所有操作请求都进行重试
MaxAutoRetries: 1 #对当前实例的重试次数
MaxAutoRetriesNextServer:2 #切换实例的重试次数
上述代码中,MaxAutoRetries,表示超时之后,重新对当前实例进行访问的次数,我们设置了超时之后,首先会继续尝试访问当前实例1次,如果还是失败,则会报
SocketTimeoutException.异常。
接下来,为了测试设置的 ConnectTimeout参数是否生效,我们在服务提供者eureka-provider的hello()方法中添加下列代码:
try{
Thread.sleep(6000) ;
}catch(Exception e){
}
由于 ConnectTimeout,参数设置的超时时间是5秒,上述代码设置的程序等待时间是6秒,这样必然会导致出现超时。此时使用浏览器访问http://localhost:8764/hello,发现消费者eureka-feign-client的控制台会报 java. net. SocketTimeoutException: Readtimed out。
2.指定服务配置
大多数的情况下,我们对于服务调用的超时时间可能会根据实际服务的特性做一些调整,所以仅仅依靠默认的全局配置是不行的。使用Spring Cloud Feign 时,如果要对各个服务消费者进行不同配置,可以采用.ribbon.key=value 的格式进行设置,示例代码如下:
eurera-provider :
ribbon :
ConnectTimeout: 5000
ReadTimeout:2000
OkToRetryOnAl1operations: true
MaxAutoRetries: 2
MaxAutoRetriesNextserver: 2
上述代码中,我们在ribbon 属性的前面添加了服务名称eureka-provider,表示上述配置是针对eureka-provider 服务的。
日志配置
在Spring Cloud Feign中,构建@FeignClient注解修饰客户端时,会为每一个客户端都创建一个表示日志对象的feign. Logger实例,我们可以利用该日志对象的DEBUG模式分析Feign的请求细节。
在application. yml 文件中,可以使用logging.level.参数配置格式开启指定Feign消费者的 DEBUG日志,其中指的是Feign消费者中需要开启DEBUG日志的接口的完整路径,比如针对本章中我们的Feign 客户端接口 FeignService(eureka-feign-client项目中的类)可以按如下配置开启:
logging:
level:
全路径:DEBUG
例:com.itheima.eurekafeignclient.service.FeignService: DEBUG
需要注意的是,如果只添加上述配置,还是无法输出 DEBUG日志的,因为Feign客户端的Logger.Level对象默认定义为NONE级别,该级别不会记录任何Feign调用过程中的信息,所以我们需要调整它的级别。
Feign中的日志级别主要分为四类,具体介绍如下:
1.NONE:不记录任何信息。
2. BASIC:仅记录请求方法,URL以及响应状态码和执行时间。
3.HEADERS:除了记录 BASIC级别的信息之外,还会记录请求和响应的头信息。
4.FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等。
然后,在eureka-feign-client创建config包,创建LogConfigration类
package com.li.eurekaprovide.eurekafeignclient.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class LogConfiguration {
@Bean
Logger.Level feignLogConfiguration(){
return Logger.Level.FULL; //设置FULL级别
}
}
在service包下面的FeignService修改注解
@FeignClient(name = "eureka-provider" , configuration = LogConfiguration.class)
启动访问任意接口,即可在控制台看到日志
其他配置
我们从请求压缩方面讲解,Spring Cloud Feign支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗。我们只需要通过以下参数设置,就可以开启请求与响应的压缩功能,代码如下:
feign :
compression
request:
enabled: true
Response:
Enabled: true
同时,我们还能对请求压缩做一些细致的设置,例如,下面的配置内容指定了压缩请求支持的MIME TYPE类型,并设置了请求压缩的大小下限,*只有超过这个大小的请求才会对其进行压缩。*代码如下:
feign :
compression:
request :
mime-types: text/ xml , application/xml, application/json
min-request-size: 2048 //2048M
上述配置的参数中,feign.compression.request.mime-types和feign.compression.request.min-request-size 设置的均为默认值。
Feign服务调用的工作原理可以总结为以下几个步骤:
(1)首先通过@EnableFeignClients注解开启EeignClient功能。程序启动时,会通过该注解开启对@EeignClient 注解的包扫描。
(2)根据Feign规则实现接口,并在接口上面添加@FeignClient注解。
(3)程序启动后,会进行包扫描,扫描所有的@FeignClient,注解类,并将这些信息注入IoC,容器!
(4)当接口方法被调用时,通过JDK的代理生成具体的RequestTemplate.模板对象。根据RequestTemplate再生成HTTP 请求的Request对象,Request对象交给Client处理。