概述
由来
在实际开发中,被调用方一般是将需要提供给外部的代码再次进行封装,而调用方则是根据提供的URL来进行调用开发。
在前面的组件介绍中,我们远程调用使用的是RestTemplate类中的方法。但是,使用此类来实现远程调用,我们需要在调用方进行拼接URL、封装请求等内容,并且每个程序猿的代码风格不同,这样不仅导致容易出错,还导致后期维护成本难,容易造成屎山代码。
综上所述,一种新型的组件横空出世。在调用方,它简单到像controller层调用service层一样;在被调用方,它只需一个注解就可以将被调用的服务和封装的接口绑定。这样,无论是服务提供方还是服务调用方,都大大减少了代码的开发。这个组件就是OpenFeign。
微服务之间的通信方式,通常有两种:RPC和HTTP。
在SpringCloud中,默认是使用HTTP来进行微服务的通信,最常用的实现形式有两种:
- RestTemplate。
- OpenFeign
功能
- 支持SpringCloudLoadBalancer的负载均衡
- 支持Sentinel和它的Fallback
代码案例
搭建商品服务
建模块
写pom文件
采用的是Nacos + OpenFeign的组件。
<?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><parent><groupId>com.wbz</groupId><artifactId>spring-cloud-test</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-provider-product-open-feign-8401</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--SpringBoot通用模块--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--Lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--Druid--><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId></dependency><!--MySQL驱动--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!--MP--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency><!--注册中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--配置中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--BootStrap.yml--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency></dependencies></project>
写yml文件
使用的是bootstrap.yml文件
server:port: 8401spring:application:name: cloud-provider-product-open-feign-8401cloud:nacos:server-addr: 127.0.0.1:8848discovery:service: ${spring.application.name}config:prefix: ${spring.application.name}file-extension: ymlprofiles:active: devmvc:pathmatch:matching-strategy: ant_path_matcherdatasource:url: jdbc:mysql://127.0.0.1:3306/cloud_product?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath:mapper/**Mapper.xmltype-aliases-package: com.wbz.domain
写主启动类
@MapperScan("com.wbz.mapper")
@SpringBootApplication
public class OpenFeignProductServerApplication8401 {public static void main(String[] args) {SpringApplication.run(OpenFeignProductServerApplication8401.class, args);}}
写业务类
// JavaBean
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("product_detail")
public class Product {@TableIdprivate Long id;@TableFieldprivate String productName;@TableFieldprivate Long productPrice;@TableFieldprivate Integer state;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime createTime;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime updateTime;}// mapper接口
public interface ProduceMapper extends BaseMapper<Product> {
}// service接口
public interface ProductService extends IService<Product> {Product getProductById(Long productId);}// service实现类
@Service
public class ProductServiceImpl extends ServiceImpl<ProduceMapper, Product> implements ProductService {@Overridepublic Product getProductById(Long productId) {return this.getById(productId);}}// controller类
@RestController
@RequestMapping("/product")
public class ProductController {@Resourceprivate ProductService productService;@GetMapping("/query/{productId}")public Product getProductById(@PathVariable Long productId) {return this.productService.getProductById(productId);}}
项目启动之后,在Nacos的管理界面出现改服务就表示启动成功。
搭建commons服务
在微服务开发中,一般某一个类可能会在多个服务中被调用,例如实体类。因此,我们就要自定义一个或者多个模块,把一些常用的,基础的类拿出来,封装成一个模块。这样,当我们其他服务进行使用时,引入一个pom依赖即可。
在个人开发中,我们直接install就会将构建好的模块放到本地maven仓库中,直接调用即可。在企业开发中,则是将写好的jar包放入到私服中,这样其他开发人员也可以进行调用。
由于我们的学习过程中的代码量非常小,因此我们就不建多个模块了,直接建一个模块,把远程调用的接口和实体类放到一起。
建模块
写pom文件
<?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><parent><groupId>com.wbz</groupId><artifactId>spring-cloud-test</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-commons</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--Lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--OpenFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--MP--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId></dependency></dependencies></project>
写业务类
// 商品类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("product_detail")
public class Product {@TableIdprivate Long id;@TableFieldprivate String productName;@TableFieldprivate Long productPrice;@TableFieldprivate Integer state;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime createTime;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime updateTime;}
// 订单表
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("order_detail")
public class Order {@TableIdprivate Long id;@TableFieldprivate Long userId;@TableFieldprivate Long productId;@TableFieldprivate Integer num;@TableFieldprivate Long price;@TableFieldprivate Integer deleteFlag;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime createTime;@TableField@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")private LocalDateTime updateTime;@TableField(exist = false)private Product product;}
// 远程调用的接口,服务调用方直接引入该模块的依赖,然后调用即可
@FeignClient(value = "cloud-provider-product-open-feign-8401", path = "/product")
public interface ProductFeignApi {@GetMapping("/query/{productId}")Product getProductById(@PathVariable("productId") Long productId);}
在cloud-commons模块搭建完成之后,在商品服务中删除实体类,然后引入该模块的依赖,观察是否够启动并调用成功。
搭建订单服务
建模块
写pom文件
<?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><parent><groupId>com.wbz</groupId><artifactId>spring-cloud-test</artifactId><version>1.0-SNAPSHOT</version></parent><artifactId>cloud-consumer-order-open-feign-84</artifactId><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!--注册中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--配置中心--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--bootstrap--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><!--OpenFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--负载均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--MySQL驱动--><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!--MP--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId></dependency><!--cloud-commons--><dependency><groupId>com.wbz</groupId><artifactId>cloud-commons</artifactId><version>1.0-SNAPSHOT</version></dependency></dependencies></project>
新增的pom文件有OpenFeign的依赖和cloud-commons的依赖,并且因为OpenFeign也实现了负载均衡,所以要把父子均衡的依赖也加上去。
<!--OpenFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--cloud-commons--><dependency><groupId>com.wbz</groupId><artifactId>cloud-commons</artifactId><version>1.0-SNAPSHOT</version></dependency>
写yml文件
server:port: 84spring:application:name: cloud-consumer-order-open-feign-84cloud:nacos:server-addr: 127.0.0.1:8848discovery:service: ${spring.application.name}config:prefix: ${spring.application.name}file-extension: ymlprofiles:active: devmvc:pathmatch:matching-strategy: ant_path_matcherdatasource:url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Drivermybatis-plus:configuration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations: classpath:mapper/**Mapper.xmltype-aliases-package: com.wbz.domain
改主启动类
@MapperScan("com.wbz.mapper")
@SpringBootApplication
@EnableFeignClients // 服务调用
public class OpenFeignOrderConsumerApplication84 {public static void main(String[] args) {SpringApplication.run(OpenFeignOrderConsumerApplication84.class, args);}}
新增一个@EnableFeignClients注解用来开启服务调用。
写业务类
// mapper接口
public interface OrderMapper extends BaseMapper<Order> {
}// service接口
public interface OrderService extends IService<Order> {Order getOrderById(Integer id);}// service实现类
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {@Resourceprivate ProductFeignApi productFeignApi;@Overridepublic Order getOrderById(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用Product product = this.productFeignApi.getProductById(order.getProductId());order.setProduct(product);// 返回结果return order;}}// controller实现类
@RestController
@RequestMapping("/order")
public class OrderController {@Resourceprivate OrderService orderService;@GetMapping("/query/{id}")public Order getOrderById(@PathVariable Integer id) {return this.orderService.getOrderById(id);}}
在上述的service实现类中可以看到,远程调用真的就像层与层之间的调用一样简单。
启动服务之后,输入127.0.0.1:84/product/query/1,出现下述结果就算成功:
总结
到这里,使用OpenFeign进行远程调用的服务就搭建完成了。很容易能够看出,OpenFeign使用一行代码就实现了远程调用,所以相较于RestTemplate来说,更见简单方便。
在使用OpenFeign组件时,只需要引入对应依赖,然后在调用方使用@EnableFeignClients的注解,以及在客户端使用@FeignClient就能轻松解决问题。
OpenFeign参数传递
在OpenFeign的客户端接口中,进行参数绑定时不能省略,省略之后就会报错。
商品服务
@RestController
@RequestMapping("/product")
public class ProductController {@Resourceprivate ProductService productService;@GetMapping("/query/{productId}")public Product getProductById(@PathVariable Long productId) {return this.productService.getProductById(productId);}@GetMapping("/test1")public String test1(Long productId) {return "商品服务 - 测试传递单个参数成功";}@GetMapping("/test2")public String test2(Long productId, Long id) {return "商品服务 - 测试传递多个参数成功";}@GetMapping("/test3")public String test3(Order order) {return "商品服务 - 测试传递对象成功";}@PostMapping("/test4")public String test4(@RequestBody Order order) {return "商品服务 - 测试传递json成功";}}
cloud-commons
// 在OpenFeign中进行参数绑定时,不能省略。
@FeignClient(value = "cloud-provider-product-open-feign-8401", path = "/product")
public interface ProductFeignApi {// 传递URL上的参数@GetMapping("/query/{productId}")Product getProductById(@PathVariable("productId") Long productId);// 传递单个参数@GetMapping("/test1")String test1(@RequestParam("productId") Long productId);// 传递多个参数@GetMapping("/test2")String test2(@RequestParam("productId") Long productId,@RequestParam("id") Long id);// 传递对象@GetMapping("/test3")String test3(@SpringQueryMap Order order);// 传递json@PostMapping("/test4")String test4(@RequestBody Order order);}
订单服务
// controller类
@RestController
@RequestMapping("/order")
public class OrderController {@Resourceprivate OrderService orderService;@GetMapping("/query/{id}")public Order getOrderById(@PathVariable Integer id) {return this.orderService.getOrderById(id);}@GetMapping("/test1/{id}")public String test1(@PathVariable Integer id) {return this.orderService.test1(id);}@GetMapping("/test2/{id}")public String test2(@PathVariable Integer id) {return this.orderService.test2(id);}@GetMapping("/test3/{id}")public String test3(@PathVariable Integer id) {return this.orderService.test3(id);}@GetMapping("/test4/{id}")public String test4(@PathVariable Integer id) {return this.orderService.test4(id);}}// serive接口
public interface OrderService extends IService<Order> {Order getOrderById(Integer id);String test1(@PathVariable Integer id);String test2(@PathVariable Integer id);String test3(@PathVariable Integer id);String test4(@PathVariable Integer id);}// service实现类
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {@Resourceprivate ProductFeignApi productFeignApi;@Overridepublic Order getOrderById(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用Product product = this.productFeignApi.getProductById(order.getProductId());order.setProduct(product);// 返回结果return order;}@Overridepublic String test1(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用return this.productFeignApi.test1(order.getProductId());}@Overridepublic String test2(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用return this.productFeignApi.test2(order.getProductId(), order.getId());}@Overridepublic String test3(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用return this.productFeignApi.test3(order);}@Overridepublic String test4(Integer id) {// 获取订单Order order = this.getById(id);// 远程调用return this.productFeignApi.test4(order);}}
高级特性
大致原理
在OpenFeign组件中,主要使用的两个注解就是在启动类上的@EnableFeignClients注解以及在客户端接口上的@FeignClient注解。
首先,主启动类上的@EnableFeignClients注解开启对Feign接口代理对象的构建以及装配。在这个注解中,导入了一个FeignClientsRegistrar的类,这个类会扫描添加了FeignClient注解的接口,并且创建远程调用的对象,然后将该对象注入到Spring容器中。这样,在我们进行远程调用的代码就可以直接进行注入,并且对于注入的内容来说,会生产一个RequestTemplate的请求模板实例,在其中存储了请求路径、请求参数等内容。在请求调用时,会生成一个Request请求实例,然后根据Feign.Client的负载均衡实例,选择合适的服务进行调用。
这只是大概的过程,并且我也没有翻阅源码,在网上找的一段教程来看从而给出的方法。
@EnableFeignClients
basePackages表示扫描指定的包;
basePackageClasses表示扫描指定的类或接口对应的包;
defaultConfiguration表示自定义的FeignClient配置;
clients表示扫描指定的接口,配置之后,不会根据类路径进行扫描。
这几个定义扫描的都是用来查找FeignClient接口所在的位置,原因是因为如果cloud-commons的包结构和订单服务的包结构不相同的话,那么就无法进行扫描,所以就得自己配置扫描路径。
@FeignClient
value是用来定义服务名称,当多实例部署时,需要名称去拉取服务列表,然后再进行负载均衡,从而找到合适的服务。
url是用来给出服务地址,当只有一个服务时,直接给出URL,然后就可以进行调用。
fallback和fallbackFactory都是用来进行服务降级等功能的,后续在Sentinel中会进行介绍。
path是用来表示统一前缀的,例如都是商品服务中一个类下都是以product为前缀的,就可以直接放在path中,减少代码。
超时处理
在SpringCloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会出现问题的,但是如果业务比较复杂,服务间要进行比较繁杂的业务计算,那后台很有可能会出现ReadException这个依次,因此就要定制化配置超时时间。
测试:首先在商品服务让其睡眠10秒,然后在订单服务中配置超时时间为3秒:
spring:application:name: cloud-consumer-order-open-feign-84cloud:nacos:server-addr: 127.0.0.1:8848discovery:service: ${spring.application.name}config:prefix: ${spring.application.name}file-extension: ymlopenfeign:client:config:default:#连接超时时间connectTimeout: 3000#读取超时时间readTimeout: 3000
项目启动之后,输入127.0.0.1:84/query/product/1,等待一段时间发现如下报错:
官方默认的等待时间为60秒钟,服务端超过规定时间就会导致Feign客户端返回报错。为了避免这样的情况,我们就需要设置Feign客户端的超时控制。
我们不仅可以控制所有的超时时间,还可以针对不同的服务来设置不同的超时时间:
spring:cloud:nacos:server-addr: 127.0.0.1:8848discovery:service: ${spring.application.name}config:prefix: ${spring.application.name}file-extension: ymlopenfeign:client:config:default:#连接超时时间connectTimeout: 3000#读取超时时间readTimeout: 3000# 配置商品服务的超时时间,会覆盖全局的默认时间cloud-provider-product-open-feign-8401:#连接超时时间connectTimeout: 3000#读取超时时间readTimeout: 3000
重试机制
OpenFeign默认没有开重试机制,可以通过配置类开启:
@Configuration
public class FeignConfig {@Beanpublic Retryer retryer() {// 初识间隔时间为100毫秒// 重试间最大间隔时间为1秒// 最大请求次数为3次return new Retryer.Default(100, 1, 3);}}
为了测试,将订单服务中的代码进行如下修改:
项目启动之后,产生的日志为:
通过日志可以判断得出,我们添加的重试机制生效了。
请求/响应压缩
OpenFeign支持对请求/响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过如下配置就可以实现相对应的压缩功能,并且还对请求压缩做了一些更细致的设置,比如下面的指定压缩数据类型以及指定触发压缩的大小。
spring:openfeign:client:config:default:#连接超时时间connectTimeout: 3000#读取超时时间readTimeout: 3000# 配置商品服务的超时时间,会覆盖全局的默认时间cloud-provider-product-open-feign-8401:#连接超时时间connectTimeout: 3000#读取超时时间readTimeout: 3000compression: # 压缩request:enabled: truemin-request-size: 2048 #最小触发压缩的大小mime-types: text/xml,application/xml,application/json #触发压缩数据类型response:enabled: true
日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。说白了就是对Feign接口的调用情况进行监控和输出。
Feign的日志级别有四种:
- NONE:默认的,不显示任何日志;
- BASIC:仅记录请求方法、URL、响应状态码及执行时间;
- HEADERS:包含上述信息以及请求和响应的头信息;
- FULL:包含上述信息以及请求和响应的正文及元数据。
想要开启日志打印功能,首先要进行配置类的配置,然后再写配置文件:
@Configuration
public class FeignConfig {@BeanLogger.Level feignLoggerLevel() {return Logger.Level.FULL;}}
logging:level:com:wbz:api:ProductFeignApi: debug