文章目录
前言
这里记录了feign的入门、整合Ribbon、整合Hystrix熔断器 和 日志打印几个方面的内容。
一、为什么学习Feign
Ribbon的负载均衡(前面的学习记录)可以很大程度的简化代码的调用,可以通过服务的id直接选择实例进行调用:
// 通过服务名user-service获取实例
String url = "http://user-service/user/" + userId;
return restTemplate.getForObject(url, User.class);
但是,这样写也有一个弊端,那就是还要手动拼接url和参数,如果被调用的服务id发生变化,那么我们要逐个方法去修改拼接的url;重复性的工作也很多,而且不利于后期的维护更新。由此,我们开始学习Feign,进一步的减少重复的工作量,提高代码的质量。
提示:以下是本篇文章正文内容,下面案例可供参考
二、使用步骤
2.1 简介
Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样,这样一来,我们就不用自己进行 url拼接、参数拼接 等操作,一切都可以交给feign进行操作。
2.2 引入依赖
使用Feign要导入相关的依赖(在服务调用者的pom.xml文件引入),版本与父工程引入的springcloud版本一致;
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.3 编写Feign的客户端
package com.kaikeba.client;
import com.kaikeba.configure.FeignConfigure;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign客户端
*/
@FeignClient(value = "user-service")
public interface ConsumerUserFeignClient {
@GetMapping("/user/{userId}")
String searchByUserId(@PathVariable Integer userId);
}
- 首先,客户端是一个接口,Feign会通过动态代理,帮我们生成实现类(有点类似与Mybatis 中的Mapper接口);
- @FeignClient 注解生命这是一个Feign的客户端。通过value属性指定被调用服务的名称(服务id);
- 接口中定义的方法,完全采用SpringMVC的注解,Feign会帮我们生成调用的URL,并得到访问结果;
- Feign本质上调用服务的方式,也是通过拼接url和参数实现的,只是这是通过动态代理实现的,简化了我们手写的代码;
- 公共的路径可以提取到类上,如上面代码中,如果"/user"是这个客户端中,所有的方法都有的访问路径,则可以不在方法上写,提取到类上。如
@FeignClient(value = "user-service/user")
public interface ConsumerUserFeignClient {
@GetMapping("/{userId}")
String searchByUserId(@PathVariable Integer userId);
}
2.4 开启Feign
在启动类上通过**@EnableFeignClient**开启Feign功能
@EnableFeignClients
@SpringBootApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class);
}
}
2.5 调用
在代码中通过刚才的编写的客户端进行调用
package com.kaikeba.controller;
import com.kaikeba.client.ConsumerUserFeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Resource
private ConsumerUserFeignClient consumerUserFeignClient;
@RequestMapping("/{userId}")
public String searchByUserId(@PathVariable Integer userId) {
String s = consumerUserFeignClient.searchByUserId(userId);
return s;
}
}
三、入门案例
3.1 父工程pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kaikeba</groupId>
<artifactId>springcloud-feign-01</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>user-service</module>
<module>eureka-server</module>
<module>consumer-service</module>
</modules>
<!-- springboot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.18.RELEASE</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<springcloud.version>Greenwich.SR1</springcloud.version>
<tk.mybatis.version>2.1.5</tk.mybatis.version>
<mysql.connector.version>8.0.25</mysql.connector.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- spring cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${springcloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- tk-mybatis -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>${tk.mybatis.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.connector.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 服务调用者
我们还是用consumer-service充当服务调用者
3.2.1 pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-feign-01</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<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>
</project>
3.2.2 application.yml
server:
port: ${port:20000}
spring:
application:
name: consumer-service
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka
3.2.3 ConsumerController.java
package com.kaikeba.controller;
import com.kaikeba.client.ConsumerUserFeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/consumer")
public class ConsumerController {
@Resource
private ConsumerUserFeignClient consumerUserFeignClient;
@RequestMapping("/{userId}")
public String searchByUserId(@PathVariable Integer userId) {
String s = consumerUserFeignClient.searchByUserId(userId);
return s;
}
}
3.2.4 User.java
package com.kaikeba.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@Accessors(chain = true)
public class User {
private Integer id;
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private Date created;
private Date updated;
private String note;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
", birthday=" + birthday +
", created=" + created +
", updated=" + updated +
", note='" + note + '\'' +
'}';
}
}
3.2.5 ConsumerServiceApplication.java
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class);
}
}
3.2.6 客户端ConsumerUserFeignClient.java
package com.kaikeba.client;
import com.kaikeba.configure.FeignConfigure;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign客户端
*/
@FeignClient(value = "user-service")
public interface ConsumerUserFeignClient {
@GetMapping("/user/{userId}")
String searchByUserId(@PathVariable Integer userId);
}
3.3 服务提供者
我们使用user-service充当服务的提供者
3.3.1 pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-feign-01</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
3.3.2 application.yml
server:
port: ${port:10000}
spring:
application:
name: user-service # 服务名
datasource: # 数据源
username: root
password: root
url: jdbc:mysql://localhost:3306/springcloud
driver-class-name: com.mysql.cj.jdbc.Driver
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka # 注册中心地址
instance:
ip-address: 127.0.0.1 # 显示的ip
prefer-ip-address: true # 以ip的形式被调用
lease-expiration-duration-in-seconds: 90 # 服务失效时间:默认90s
lease-renewal-interval-in-seconds: 30 # 服务续约的时间间隔
3.3.3 UserServiceApplication.java
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class);
}
}
3.3.4 User.java
package com.kaikeba.entity;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.persistence.*;
import java.util.Date;
@Data
@Accessors(chain = true)
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "user_name")
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private Date created;
private Date updated;
private String note;
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", name='" + name + '\'' +
", age=" + age +
", sex=" + sex +
", birthday=" + birthday +
", created=" + created +
", updated=" + updated +
", note='" + note + '\'' +
'}';
}
}
3.3.5 UserMapper.java
package com.kaikeba.mapper;
import com.kaikeba.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User> {
}
3.3.6 UserService.java
package com.kaikeba.service;
import com.kaikeba.entity.User;
public interface UserService {
/**
* 根据主键查询
*/
User findById(Integer userId);
}
3.3.7 UserServiceImpl.java
package com.kaikeba.service.impl;
import com.kaikeba.entity.User;
import com.kaikeba.mapper.UserMapper;
import com.kaikeba.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public User findById(Integer userId) {
return userMapper.selectByPrimaryKey(userId);
}
}
3.3.8 UserController.java
package com.kaikeba.controller;
import com.kaikeba.entity.User;
import com.kaikeba.service.UserService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/{userId}")
public String searchByUserId(@PathVariable Integer userId) {
User user = userService.findById(userId);
return user.toString();
}
}
3.4 注册中心
我们的注册中心还是Eureka-server
3.4.1 pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-feign-01</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
3.4.2 application.yml
server:
port: 8080 #访问端口
spring:
application:
name: eureka-server #服务名
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka # 其他微服务注册使用的uri
register-with-eureka: false # 不把自己当作服务注册到注册中心
server:
enable-self-preservation: false # 关闭自我保护
eviction-interval-timer-in-ms: 6000 # 扫描失效服务的时间间隔默认是(60 * 1000)s
3.4.3 EurekaServerApplication .java
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class);
}
}
3.5 测试结果
3.5.1 注册中心
3.5.2 调用
四、 负载均衡
4.1 依赖
Feign本身已经集成了Ribbon的依赖和自动配置,
因此,我们不用在额外的引入依赖,也不用显示注册RestTemplate对象。
4.2 配置
4.2.1 配置项
ribbon:
ConnectTimeout: 1000 # 链接超时时长
ReadTimeout: 2000 # 数据通信超时时长
MaxAutoRetries: 1 # 当前服务器的重试测试
MaxAutoRetriesNextServer: 1 # 集群中其他实例的重试次数(包括自己这个实例)
OkToRetryOnAllOperations: false # 是否对所有的请求方式都重试
-
ConnectTimeout:表示连接时的超时时间
-
ReadTimeout:表示数据通信的超时时间
-
MaxAutoRetries:表示当前服务器实例的重试次数
-
MaxAutoRetriesNextServer:表示对起群中其他实例的请求次数(包括当前实例)
会对当前实例进行第二次请求,比如集群中有3个实例,每个请求2次,那么在请求当前实例失败之后,重新请求 时还是会请求当前实例,也就是说,总共会请求:2 + 3*2 = 8次。 -
OkToRetryOnAllOperations:是否对所有的请求方式都重试
4.2.2 特定配置
如果是对所有的服务的Ribbon配置都一致,就能按照4.2.1中的进行配置,若是要对某个服务进行特定的配置,就需要在ribbon前增加服务id,如下:
user-service
ribbon:
ReadTimeout: 2000 # 读取超时时长
ConnectTimeout: 1000 # 建立链接的超时时长
user-service是服务的id;
4.3 服务重试
ribbon有自己的重试机制,一旦超时,就会重新发起请求
如上图,我们配置的超时时长是2000ms,重试次数是1次,所以当请求失败时,页面显示的时间就是2000(ms) * 2 = 4000(ms) , 如果不想重试,将重试次数设置成0即可;
五、 熔断器Hystrix
5.1 使用步骤
- Feign默认的集成Hystrix,我们不用额外引入依赖;只不过默认的是关闭(不开启)状态;
- 使用时,我们要自己编写熔断降级的实现类和实现方法;
- 熔断类需要实现Feign接口,在对应的方法中编写降级逻辑;
- 并且在Feign客户端接口中进行配置。
5.2 开启配置
在配置文件中,增加如下配置,开始熔断器
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
5.3 熔断处理类
package com.kaikeba.client;
import org.springframework.stereotype.Component;
@Component
public class FeignClientFallBack implements ConsumerUserFeignClient {
@Override
public String searchByUserId(Integer userId) {
return "查询出错!!!";
}
}
5.4 客户端中添加
在Feign客户端中**@FeignClient**注解中的 fallback 属性指定熔断降级的处理类
package com.kaikeba.client;
import com.kaikeba.configure.FeignConfigure;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign客户端
*/
@FeignClient(value = "user-service/user", fallback = FeignClientFallBack.class)
public interface ConsumerUserFeignClient {
@GetMapping("/{userId}")
String searchByUserId(@PathVariable Integer userId);
}
5.5 测试结果
服务请求出错时,不是报错,是返回对应信息。
六、 打印日志
我们原来设置日志级别是通过在配置文件中设置 logging.level.报名=日志级别来配置的:
logging:
level:
com.kaikeba: debug
然而这对Feign是不生效的。因为@FeignClient注解修饰的客户端,在被代理是,都会创建一个新的Feign.Logger实例,我们需要对这个实例。进行额外的日志界别配置才可以。
6.1 日志级别
Feign有四种日志级别
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
6.2 日志配置类
package com.kaikeba.configure;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 的日志配置
*/
@Configuration
public class FeignConfigure {
@Bean
public Logger.Level feignLoggerLevel() {
// 记录所有请求和响应的明细,包括头信息、请求体、元数据
return Logger.Level.FULL;
}
}
6.3 指定日志配置
package com.kaikeba.client;
import com.kaikeba.configure.FeignConfigure;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* Feign客户端
*/
@FeignClient(value = "user-service", fallback = FeignClientFallBack.class, configuration = FeignConfigure.class)
public interface ConsumerUserFeignClient {
@GetMapping("/user/{userId}")
String searchByUserId(@PathVariable Integer userId);
}
6.4 测试结果
七、 压缩
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
response:
enabled: true # 开启响应压缩
其他的压缩配置
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下
注:上面的数据类型、压缩大小下限均为默认值。