一、Fegin介绍
1.什么是Fegin?
Feign旨在是编写Java Http客户端变得更加容易。Feign是一种声明式、模板化的HTTP客户端(仅仅在consumer中使用)
之前使用Ribbon+RestTemplate时,利用RESTTemplate请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,有偶遇对于服务依赖的调用可能不止一处,往往一个借口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务借口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon是,自动封装服务调用客户端的开发量。
2.什么是声明式?
什么是声明式,有什么作用,解决什么问题?
声明式调用就像调用本地方法一样调用远程方法;无感知远程 http 请求。
- Spring Cloud 的声明式调用, 可以做到使用 HTTP 请求远程服务时能就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个 HTTP 请求。
- 它像 Dubbo 一样,consumer 直接调用接口方法调用 provider,而不需要通过常规的Http Client构造请求再解析返回数据。
- 它解决了让开发者调用远程接口就跟调用本地方法一样,无需关注与远程的交互细节,更无需关注分布式环境开发。
3.Feign入门案例
3.1 设计需求
模拟一个分布式系统中消费者通过注册中心(Eureka)获取提供者提供的获取商品信息的服务的场景。
3.2 服务结构
组件说明
服务 | 说明 |
---|---|
Eureka集群 | 注册中心,提供服务的注册和发现 |
service服务 | 声明Service接口,定义方法,provider和consumer都需要依赖此服务而且接口中的方法需要使用到SpringMVC的组件 |
provider服务 | 提供商品信息查询服务,需要依赖service服务 |
consumer服务 | 服务消费者,需要依赖service服务,同时通过feign调用provider的服务获取商品信息 |
3.3 编写Service服务
3.3.1 创建项目
3.3.2 添加依赖
因为在Service服务中心我们需要使用到@RequestMapping注解,所以需要添加Web依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
3.3.3 声明接口
我们需要在Service服务中心声明Provider提供的功能有哪些,同时声明pojo对象
pojo类对象
package com.biao.pojo;
/**
* 商品的实体类
*/
public class Product {
private Integer id;
private String name;
public Product() {
}
public Product(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
服务接口
package com.biao.service;
import com.biao.pojo.Product;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
/**
* 公共服务接口
*/
@RequestMapping("/product")
public interface ProductService {
/**
* 查询所有商品的方法
* @return
*/
@GetMapping("/findAll")
public List<Product> findAll();
}
3.3.4 install
避免出现SpringBoot项目install后的jar中的class文件出现在BOOT-INF目录下,造成依赖的工程引用不到对应的源码
例如:
所以在install前要去在service的服务中心我们要添加如下的设置
然后进行install
3.4 编写Provider服务
3.4.1 创建项目
创建一个SpringBoot项目即可
3.4.2 添加依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--表示这是一个SpringCloud Eureka的Client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 添加service服务的依赖 -->
<dependency>
<groupId>com.biao</groupId>
<artifactId>springcloud-feign-products-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
3.4.3 配置文件设置
spring.application.name=shop-product-provider
server.port=9099
# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bb:1234@192.168.134.129:8761/eureka/,http://bb:1234@192.168.134.131:8761/eureka/
3.4.4 实现接口服务
创建Controller,实现接口服务
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductService;
import java.util.ArrayList;
import java.util.List;
@RestController
public class ProductController implements ProductService {
/**
* 具体实现service中声明的接口
* 同时要注意我们不需要在该方法的头部添加@RequestMapping等注解
* @return
*/
@Override
public List<Product> findAll() {
List<Product> list = new ArrayList<>();
list.add(new Product(1,"电脑"));
list.add(new Product(2,"冰箱"));
list.add(new Product(3,"空调"));
return list;
}
}
3.4.5 启动服务
直接启动服务,去注册中心中查看是否注册成功
3.5 编写Consumer服务
3.5.1 创建项目
创建一个SpringBoot项目
3.5.2 添加相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 添加Feign的依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<!-- service 的依赖 -->
<dependency>
<groupId>com.biao</groupId>
<artifactId>springcloud-feign-products-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR11</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.5.3 配置信息修改
spring.application.name=shop-product-consumer
server.port=9098
# 设置服务注册中心的地址
eureka.client.service-url.defaultZone=http://bb:1234@192.168.134.129:8761/eureka/,http://bb:1234@192.168.134.131:8761/eureka/
3.5.4 创建接口
package com.biao;
import com.biao.service.ProductService;
import org.springframework.cloud.openfeign.FeignClient;
/**
* Fegin的消费者的Service服务
* name中我们指定 在Eureka中注册的服务提供者的名称
*/
@FeignClient(name = "shop-product-provider")
public interface ProductConsumerService extends ProductService {
}
3.5.5 创建控制器
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 服务消费者的控制器
*
*/
@RestController
public class ProductController {
@Autowired
private ProductConsumerService service;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public List<Product> getAll(){
System.out.println("list.....");
return service.findAll();
}
}
3.5.6 创建启动器
package com.biao;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 我们需要放开Feign的客户端注解
*/
@EnableFeignClients
@SpringBootApplication
public class SpringcloudFeignproductConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudFeignproductConsumerApplication.class, args);
}
}
3.5.7 启动测试
启动服务后,首先在Eureka注册中心中我们可以发现服务,然后访问测试即可
3.6 Ribbon和Feign的比较
二、Feign参数处理
我们在实际的处理过程中会面对各种各样的请求情况,那么就会出现传递各种类型参数的情况,所以我们来分析下。
1.单个参数
1.1 Service服务
我们首先在Service服务中心新增加一个接受单个参数的方法
package com.biao.service;
import com.biao.pojo.Product;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
/**
* 公共服务接口
*/
@RequestMapping("/product")
public interface ProductService {
/**
* 查询所有商品的方法
* @return
*/
@GetMapping("/findAll")
public List<Product> findAll();
/**
* 根据ID查询商品信息
* Fegin本身也是基于HTTP请求的客户端
* 那么在接受参数的时候我们需要通过@RequestParam注解来执行要接受的参数
* @param id
* @return
*/
@GetMapping("/getProductById")
public Product getProductById(@RequestParam("id") Integer id);
}
记得install打包
1.2 Provider服务
既然新增加了方法的声明,那么我们就需要在Provider中实现该功能
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductService;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class ProductController implements ProductService {
/**
* 具体实现service中声明的接口
* 同时要注意我们不需要在该方法的头部添加@RequestMapping等注解
* @return
*/
@Override
public List<Product> findAll() {
List<Product> list = new ArrayList<>();
list.add(new Product(1,"电脑"));
list.add(new Product(2,"冰箱"));
list.add(new Product(3,"空调"));
return list;
}
/**
* 单个参数的处理
* @param id
* @return
*/
@Override
public Product getProductById(Integer id) {
return new Product(4,"扫地机器人");
}
}
1.3 Consumer服务
在消费者我们通过Fegin直接调用
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 服务消费者的控制器
*
*/
@RestController
public class ProductController {
@Autowired
private ProductConsumerService service;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public List<Product> getAll(){
System.out.println("list.....");
return service.findAll();
}
@GetMapping("/get")
public Product getProductByID(@RequestParam("id") Integer id){
return service.getProductById(id);
}
}
1.4 测试
2.多个参数
多个参数传递的情况下,我们接受的时候可以通过多个参数来接受,也可以通过自定义对象接受,我们要区分GET方式和POST方式的请求
2.1 Service服务
package com.biao.service;
import com.biao.pojo.Product;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 公共服务接口
*/
@RequestMapping("/product")
public interface ProductService {
/**
* 查询所有商品的方法
* @return
*/
@GetMapping("/findAll")
public List<Product> findAll();
/**
* 根据ID查询商品信息
* Fegin本身也是基于HTTP请求的客户端
* 那么在接受参数的时候我们需要通过@RequestParam注解来执行要接受的参数
* @param id
* @return
*/
@GetMapping("/getProductById")
public Product getProductById(@RequestParam("id") Integer id);
@PostMapping("/addProductGet")
public Product addProductGet(@RequestParam("id") Integer id,
@RequestParam("name") String name);
/**
* Post方式提交
* 获取多个参数:@RequestBody
* @param product
* @return
*/
@PostMapping("/addProductPost")
public Product addProductPost(@RequestBody Product product);
}
2.2 Provider服务
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductService;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class ProductController implements ProductService {
/**
* 具体实现service中声明的接口
* 同时要注意我们不需要在该方法的头部添加@RequestMapping等注解
* @return
*/
@Override
public List<Product> findAll() {
List<Product> list = new ArrayList<>();
list.add(new Product(1,"电脑"));
list.add(new Product(2,"冰箱"));
list.add(new Product(3,"空调"));
return list;
}
/**
* 单个参数的处理
* @param id
* @return
*/
@Override
public Product getProductById(Integer id) {
System.out.println("服务端:"+id);
return new Product(id,"扫地机器人");
}
@Override
public Product addProductGet(Integer id, String name) {
System.out.println(id+":"+name);
return new Product(id,name);
}
@Override
public Product addProductPost(Product product) {
System.out.println(product);
return product;
}
}
2.3 Consumer服务
package com.biao.controller;
import com.biao.pojo.Product;
import com.biao.service.ProductConsumerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 服务消费者的控制器
*
*/
@RestController
public class ProductController {
@Autowired
private ProductConsumerService service;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public List<Product> getAll(){
System.out.println("list.....");
return service.findAll();
}
@GetMapping("/get")
public Product getProductByID(@RequestParam("id") Integer id){
return service.getProductById(id);
}
@RequestMapping("/get1")
public Product addProductGet(Product product){
return service.addProductGet(product.getId(),product.getName());
}
@RequestMapping("/get2")
public Product addProductPost(Product product){
return service.addProductPost(product);
}
}
2.4 测试
三、压缩处理
在数据传输过程中压缩数据肯定是必须的,而gzip是我们比较常用的方式,而且我们在刚刚接触http协议的时候就介绍过gzip。那在微服务环境下我们该如何通过gzip来压缩数据。
1. gzip介绍
gzip是一种数据格式,采用用deflate算法来压缩data;gzip是一种流行的文件压缩算法,应用十分广泛,尤其是在Linux平台。
能力
当使用Gzip压缩到一个纯文本文件时,效果是非常明显的,大约是减少70%以上的文件大小
作用
网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。网页加载速度加快的好处不言而喻,除了节省流量,改善用户的浏览体验外,另一个潜在的好处是Gzip 与搜索引擎的抓取工具有着更好的关系。例如 Google就可以通过直接读取 gzip 文件来比普通手工抓取更快地检索网页。
2.HTTP压缩传输的规定
- 客户端向服务器请求中带有:Accept-Encoding:gzip, deflate 字段,向服务器表示,客户端支持的压缩格式(gzip 或者 deflate),如果不发送该消息头,服务器是不会压缩的。
- 服务端在收到请求之后,如果发现请求头中含有 Accept-Encoding 字段,并且支持该类型的压缩,就对响应报文压缩之后返回给客户端,并且携带 Content-Encoding:gzip 消息头,表示响应报文是根据该格式压缩过的。
- 客户端接收到请求之后,先判断是否有 Content-Encoding 消息头,如果有,按该格式解压报文。否则按正常报文处理。
3.案例
结构
3.1 配置consumer到provider的压缩
# -----feign gzip
# 配置请求 GZIP 压缩
feign.compression.request.enabled=true
# 配置响应 GZIP 压缩
feign.compression.response.enabled=true
# 配置压缩支持的 MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的最小阈值,默认 2048
feign.compression.request.min-request-size=512
3.2 配置浏览器到consumer的压缩
#--- spring boot gzip #是否启用压缩
server.compression.enabled=true
server.compression.mime-types=application/json,application/ xml,text/html,text/xml,text/plain
使用前
使用后
四、HttpClient连接池
http连接池能提高性能。
1.http的背景原理
- 两台服务器建立http连接的过程是很复杂的一个过程,设计到多个数据包的交换,并且也很耗时间
- HTTP连接需要的3次握手4次分手开销很大,这一开销对于大量的比较小的http消息来说更大
2.优化解决方案
- 如果我们直接采用 http 连接池,节约了大量的 3 次握手 4 次分手;这样能大大提升吞吐率。
- feign 的 http 客户端支持 3 种框架;HttpURLConnection、httpclient、okhttp;默认是HttpURLConnection。
- 传统的 HttpURLConnection 是 JDK 自带的,并不支持连接池,如果要实现连接池的机制,还需要自己来管理连接对象。对于网络请求这种底层相对复杂的操作,如果有可用的其他方案,也没有必要自己去管理连接对象。
- HttpClient 相比传统 JDK 自带的 HttpURLConnection,它封装了访问 http 的请求头,参数,内容体,响应等等;它不仅使客户端发送 HTTP 请求变得容易,而且也方便了开发人员测试接口(基于 Http协议的),即提高了开发的效率,也方便提高代码的健壮性;另外高并发大量的请求网络的时候,还是用“连接池”提升吞吐量。
3.案例
3.1 添加依赖
<!-- 添加HTTPClient的依赖 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- 添加feign对HTTPClient的支持 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
3.2 修改配置
## 启用 httpclient
feign.httpclient.enabled=true
3.3 测试访问
五、日志处理
在前面的案例中我们可以发现,浏览器到consumer中的请求情况我们可以通过浏览器来帮助我们查看,但是consumer对provider的调用情况我们是不清楚的,这时我们可以通过日志来分析
1.添加日志文件
我们通过logback.xml来记录日志信息
注意日志级别设置为DEBUG
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="${catalina.base}/logs/" />
<!-- 控制台输出 -->
<appender name="Stdout" class="ch.qos.logback.core.ConsoleAppender">
<!-- 日志输出编码 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</layout>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="RollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/server.%d{yyyy-MM-dd}.log</FileNamePattern>
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</layout>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<root level="DEBUG">
<appender-ref ref="Stdout" />
<appender-ref ref="RollingFile" />
</root>
<!--日志异步到数据库 -->
<!-- <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
日志异步到数据库
<connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
连接池
<dataSource class="com.mchange.v2.c3p0.ComboPooledDataSource">
<driverClass>com.mysql.jdbc.Driver</driverClass>
<url>jdbc:mysql://127.0.0.1:3306/databaseName</url>
<user>root</user>
<password>root</password>
</dataSource>
</connectionSource>
</appender> -->
</configuration>
2.设置Feign日志级别
然后我们需要在SpringBoot的配置类中向IOC容器中注入一个Logger.Level对象
package com.biao;
import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
/**
* 我们需要放开Feign的客户端注解
*/
@EnableFeignClients
@SpringBootApplication
public class SpringcloudFeignproductConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudFeignproductConsumerApplication.class, args);
}
/**
* 定义日志的输出级别
* NONE,不记录任何信息 默认值
* BASIC,记录基本信息(请求方法,请求URL,状态码和用时)
* HEADERS,在BASIC的基础上再记录一些常用信息
* FULL:记录信息请求的和响应所有的数据
* @return
*/
@Bean
public Logger.Level getLogger(){
return Logger.Level.FULL;
}
}
3.测试
访问请求后我们就可以在控制台输出查看到对应的请求信息
六、超出时间设置
Feign调用服务的时候默认的时长是1秒钟,也就是如果超过1秒钟没有连接上,或者超过1秒钟没有响应,那么就会响应的报错,但是实际的情况因为业务不同处理的时间会不一样,所以这时我们需要手动的来调整超时时间。
1.全局配置
Feign的负载均衡底层使用的是Ribbon,我们可以在属性文件中配置Ribbon的连接和读写信息。
## 全局配置 超时时间
## 请求连接时间 默认是1秒钟
ribbon.ConnectionTimeout=8000
## 请求处理的时间
ribbon.ReadTimeout=8000
2.局部配置
#局部配置
# 对所有操作请求都进行重试
shop-product-provider.ribbon.OkToRetryOnAllOperations=true
# 对当前实例的重试次数
shop-product-provider.ribbon.MaxAutoRetries=2
# 切换实例的重试次数
shop-product-provider.ribbon.MaxAutoRetriesNextServer=0
# 请求连接的超时时间
shop-product-provider.ribbon.ConnectTimeout=3000
# 请求处理的超时时间
shop-product-provider.ribbon.ReadTimeout=3000