这里写目录标题
系列文章目录
【Spring Cloud一】微服务基本知识
【Spring Cloud 三】Eureka服务注册与服务发现
【Spring Cloud 四】Ribbon负载均衡
背景
目前开发的项目其微服务之间的调用方式使用的就是OpenFeign的方式,为了更加的体会到它代码的便捷和高效,所以博主对OpenFeign进行了再次学习和实践,加强对OpenFeign的整体理解。
一、OpenFeign是什么
在了解OpenFeign之前我们先来了解Feign,因为OpenFeign和Feign之间是一种继承关系。
Feign是什么
Feign是一个声明式的模块化的HTTP客户端工具,他是由Netflix开发。它简化了在Java应用中编写HTTP请求的方式,能够让开发者可以通过简单的注解来定义对其他服务的RESTFul调用,从而避免编写繁琐的HTTP请求。
在传统的HTTP客户端中,我们通常需要手动构建HTTP请求,包括设置请求的URL,方法、请求头、请求体等。而使用Feign,你只需要定义一个接口,并在接口的方法上添加注解,就可以实现对其他服务的调用。Feign在运行时会根据接口定义自动创建代理实现,帮助你处理底层的HTTP请求细节。
Netflix的Feign默认集成了Eureka和Ribbon。
Feign的局限性
- NetFlix Feign不支持Spring MVC注解,如果在Spring Cloud使用Spring MVC注解的话,可以考虑使用Spring Cloud OpenFeign。
- NetFlix Feign在2018年宣布不再对其进行积极开发,虽然设计任然在维护和改进Feign但是毕竟不那么积极了。
OpenFeign是什么
OpenFeign也是一个HTTP客户端工具,它是基于Feign的封装和增强,使得在Spring Cloud环境中使用Feign更加强大和灵活。
-
OpenFeign继承了Feign的声明式HTTP客户端的特性,允许开发者使用接口和注解来定义对其他服务的RESTful调用,从而简化了服务间通信的代码。
-
OpenFeign还增加了一些功能,如自动负载均衡、而在Feign中,你需要手动使用Ribbon的相关注解来实现负载均衡。
-
支持熔断器:OpenFeign整合了Hystrix,提供了熔断器的功能,可以在服务不可用时快速失败,防止级联故障。这使得系统在面对服务故障时更加稳健。
在Spring Cloud项目中推荐使用OpenFeign来实现服务间通信,以获得更好的功能和集成性能。
二、为什么要有OpenFeign
为什么要有OpenFeign,它解决了什么问题。
- 相比较于传动发送HTTP请求,使用OpenFeign发送跨服务请求更加方便、灵活和高效。
- 更好的与Spring Cloud集成与其他组件(Eureka、Ribbon、Hystrix等)无缝集成,更加方便和高效。
- 自动负载均衡:OpenFeign整合了Ribbon,可以自动实现负载均衡,将请求分发给多个服务实例,提高了系统的可用性和性能。
三、如何使用OpenFeign
整个系统中有三个服务,Eurka服务,一个服务提供者,一个服务消费者。
如何搭建Eurka服务可以访问这篇博客【Spring Cloud 三】Eureka服务注册与服务发现
服务提供者order-service
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wangwei</groupId>
<artifactId>order-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>order-service</name>
<description>order-service</description>
<properties>
<java.version>8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<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>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
yml配置文件
server:
port: 8080
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
register-with-eureka: true #设置为fasle 不往eureka-server注册
fetch-registry: true #应用是否拉取服务列表到本地
registry-fetch-interval-seconds: 10 #为了缓解服务列表的脏读问题,时间越短脏读越少 性能相应的消耗回答
instance: #实例的配置
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
hostname: localhost #主机名称或者服务ip
prefer-ip-address: true #以ip的形式显示具体的服务信息
lease-renewal-interval-in-seconds: 10 #服务实例的续约时间间隔
启动类
@SpringBootApplication
@EnableEurekaClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
实体
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Order {
private Integer id;
private String name;
private Double price;
private Date time;
}
ParamController
@RestController
public class ParamController {
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name")String name ,@PathVariable("age")Integer age){
System.out.println(name+":"+age);
return "ok";
}
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false)String name){
System.out.println(name);
return "ok";
}
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false)String name,@RequestParam(required = false)Integer age){
System.out.println(name+":"+age);
return "ok";
}
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order){
System.out.println(order);
return "ok";
}
@PostMapping("oneObjOneParam")
public String twoParam(@RequestBody Order order,@RequestParam("name")String name){
System.out.println(order+":"+name);
return "ok";
}
}
服务消费者user-service
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wangwei</groupId>
<artifactId>user-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>user-service</name>
<description>user-service</description>
<properties>
<java.version>8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<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>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
yml配置文件
server:
port: 8081
spring:
application:
name: user-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
register-with-eureka: true #设置为fasle 不往eureka-server注册
fetch-registry: true #应用是否拉取服务列表到本地
registry-fetch-interval-seconds: 10 #为了缓解服务列表的脏读问题,时间越短脏读越少 性能相应的消耗回答
instance: #实例的配置
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
hostname: localhost #主机名称或者服务ip
prefer-ip-address: true #以ip的形式显示具体的服务信息
lease-renewal-interval-in-seconds: 10 #服务实例的续约时间间隔
启动类
package com.wangwei.userservice;
import feign.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients //开启feign的客户端,才可以帮助我们发送调用
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
接口类
package com.wangwei.userservice.feign;
import com.wangwei.userservice.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* @FeignClient(value = "order-service")
* value就是提供者的应用名称
*/
@FeignClient(value = "order-service")
public interface UserOrderFeign {
/**
* 你需要调用哪个controller 就写它的方法签名(除了方法体的一个方法的所有属性
* @return
*/
@GetMapping("doOrder")
String doOrder();
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name")String name , @PathVariable("age")Integer age);
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false)String name);
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false)String name,@RequestParam(required = false)Integer age);
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order);
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order,@RequestParam("name")String name);
}
UserController
package com.wangwei.userservice.controller;
import com.wangwei.userservice.domain.Order;
import com.wangwei.userservice.feign.UserOrderFeign;
import feign.Feign;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;
@RestController
public class UserController {
@Autowired
private UserOrderFeign userOrderFeign;
/**
* 总结:
* feign的默认等待时间为1s
* 超过1s就直接报错了
*
* @return
*/
@GetMapping("userDoOrder")
public String userDoOrder(){
//这里发起远程调用
String result = userOrderFeign.doOrder();
return result;
}
@GetMapping("testParam")
public String testParam(){
String url = userOrderFeign.testUrl("David", 18);
System.out.println(url);
String david = userOrderFeign.oneParam("David");
System.out.println(david);
String wangwei = userOrderFeign.twoParam("wangwei", 18);
System.out.println(wangwei);
Order order = Order.builder()
.name("西蓝花")
.price(5D)
.time(new Date())
.id(1)
.build();
String s = userOrderFeign.oneObj(order);
System.out.println(s);
String wangwei1 = userOrderFeign.oneObjOneParam(order, "wangwei");
System.out.println(wangwei1);
return "ok";
}
}
运行效果
先启动Eureka服务再启动服务提供者,最后启动服务消费者。
如下图所示已经调用成功。
调用超时配置
因为ribbon默认调用超时时长为1s,可以进行修改,可以查看DefaultClientConfigImpl。
我们在服务提供者order-service中新建一个controller,进行超时测试。
@RestController
public class OrderController {
@GetMapping("doOrder")
public String doOrder(){
try{
//模拟操作数据库等 耗时2s
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e){
e.printStackTrace();
}
return "牛奶-鸡蛋-麦片";
}
}
然后我们通过服务消费者user-service进行调用测试:
可以发现出现了超时。
我们可以在消费者的配置文件中声明Ribbon的超时时间。
# feign只是帮你封装了远程调用的功能 底层还是ribbon 所以我们需要去修改ribbon的时间
ribbon:
ReadTimeout: 3000 # 3s超时时间(从服务器读取到可用资源所用的时间)
ConnectTimeout: 3000 #连接服务超时时间
添加这个配置就行了。
五、OpenFeign调用参数的处理
首先传参确保消费者和提供者的参数列表一致,包括返回值,方法签名。
- 通过URL传参,GET请求,参数列表使用@PathVariable
- 如果是GET请求,每个基本参数必须加上@RequestParam
- 如果是POST请求,而且是对象集合等参数,必须加上@RequestBody或者@RequestParam
六、OpenFeign日志打印功能
OpenFeign还提供了日志打印功能,通过日志打印功能,能够清晰的看到发送的HTTP请求中的细节。
总结
在Spring Cloud中是OpenCluod的步骤主要是,引入openFeign依赖;启动类添加开启openFeign客户端;声明与服务端相同的方法并添加对应注册;需要注意根据项目情况配置调用服务的超时时间。