概述
由来
在实际开发中,被调用方一般是将需要提供给外部的代码再次进行封装,而调用方则是根据提供的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: 8401
spring:
application:
name: cloud-provider-product-open-feign-8401
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
service: ${spring.application.name}
config:
prefix: ${spring.application.name}
file-extension: yml
profiles:
active: dev
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
url: jdbc:mysql://127.0.0.1:3306/cloud_product?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/**Mapper.xml
type-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 {
@TableId
private Long id;
@TableField
private String productName;
@TableField
private Long productPrice;
@TableField
private 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 {
@Override
public Product getProductById(Long productId) {
return this.getById(productId);
}
}
// controller类
@RestController
@RequestMapping("/product")
public class ProductController {
@Resource
private 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 {
@TableId
private Long id;
@TableField
private String productName;
@TableField
private Long productPrice;
@TableField
private 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 {
@TableId
private Long id;
@TableField
private Long userId;
@TableField
private Long productId;
@TableField
private Integer num;
@TableField
private Long price;
@TableField
private 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: 84
spring:
application:
name: cloud-consumer-order-open-feign-84
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
service: ${spring.application.name}
config:
prefix: ${spring.application.name}
file-extension: yml
profiles:
active: dev
mvc:
pathmatch:
matching-strategy: ant_path_matcher
datasource:
url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/**Mapper.xml
type-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 {
@Resource
private ProductFeignApi productFeignApi;
@Override
public 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 {
@Resource
private 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 {
@Resource
private 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 {
@Resource
private 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 {
@Resource
private ProductFeignApi productFeignApi;
@Override
public Order getOrderById(Integer id) {
// 获取订单
Order order = this.getById(id);
// 远程调用
Product product = this.productFeignApi.getProductById(order.getProductId());
order.setProduct(product);
// 返回结果
return order;
}
@Override
public String test1(Integer id) {
// 获取订单
Order order = this.getById(id);
// 远程调用
return this.productFeignApi.test1(order.getProductId());
}
@Override
public String test2(Integer id) {
// 获取订单
Order order = this.getById(id);
// 远程调用
return this.productFeignApi.test2(order.getProductId(), order.getId());
}
@Override
public String test3(Integer id) {
// 获取订单
Order order = this.getById(id);
// 远程调用
return this.productFeignApi.test3(order);
}
@Override
public 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-84
cloud:
nacos:
server-addr: 127.0.0.1:8848
discovery:
service: ${spring.application.name}
config:
prefix: ${spring.application.name}
file-extension: yml
openfeign:
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:8848
discovery:
service: ${spring.application.name}
config:
prefix: ${spring.application.name}
file-extension: yml
openfeign:
client:
config:
default:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
# 配置商品服务的超时时间,会覆盖全局的默认时间
cloud-provider-product-open-feign-8401:
#连接超时时间
connectTimeout: 3000
#读取超时时间
readTimeout: 3000
重试机制
OpenFeign默认没有开重试机制,可以通过配置类开启:
@Configuration
public class FeignConfig {
@Bean
public 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: 3000
compression: # 压缩
request:
enabled: true
min-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 {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
logging:
level:
com:
wbz:
api:
ProductFeignApi: debug
本站资源均来自互联网,仅供研究学习,禁止违法使用和商用,产生法律纠纷本站概不负责!如果侵犯了您的权益请与我们联系!
转载请注明出处: 免费源码网-免费的源码资源网站 » 【微服务】服务调用 - OpenFeign(day6)
发表评论 取消回复