likes
comments
collection
share

OpenFeign实战

作者站长头像
站长
· 阅读数 53

  OpenFeignNetflix开发的声明式、模板化的http请求客户端,作用和RestTemplate差不多,只不过OpenFeign可以更加便捷、优雅地调用http api。   OpenFeign可以将提供者提供的http接口伪装为Java接口进行消费,消费者只需使用 接口 + 注解 的方式便可直接调用提供者提供的http接口,而无需再使用RestTemplate

  1. OpenFeignFeign

Spring Cloud Dalston 版及之前的版本使用的是 Feign,而该项目现已更新为了 OpenFeign,新版本中的依赖也发生了变化。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Feign本身不支持Spring MVC的注解,它有一套自己的注解;OpenFeignSpring CloudFeign的基础上支持了Spring MVC的注解,如@RequesMapping等等。 OpenFeign@FeignClient可以解析SpringMVC@RequestMapping注解下的接口, 并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

  1. OpenFeignRibbon

RibbonNetflix的一个开源的负载均衡项目,是一个客户端负载均衡器,运行在消费者端。OpenFeign也是运行在消费者端的,并且使用Ribbon进行负载均衡,所以OpenFeign直接内置了Ribbon,即在导入 OpenFeign依赖后,无需再导入Ribbon依赖了。

一、OpenFeign项目搭建

1. 提供者应用

OpenFeign实战

  • 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 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.10.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>user-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>8</java.version>
        <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
    </properties>

    <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>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <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>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • application.properties
server.port=8081
#
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=user-provider-8081
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 当前服务名称
spring.application.name=user-provider
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到server
eureka.instance.prefer-ip-address=true
#
# eureka 服务名,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.appname=user-provider
#
# 实例的虚拟主机名称,默认值 unknown;如果没有配置,则取 spring.application.name
#eureka.instance.virtual-host-name=user-provider
#
# 对外开放所有监控端点
management.endpoints.web.exposure.include=*
#
# 是否将自己注册到其他Eureka Server,默认为true
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息, 需要
eureka.client.fetch-registry=true
  • 实体bean
public class User implements Serializable {
    private String idCard;
    private String username;

    public User() {
    }

    public User(String idCard, String username) {
        this.idCard = idCard;
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }

    @Override
    public String toString() {
        return "User{" +
                "idCard='" + idCard + ''' +
                ", username='" + username + ''' +
                '}';
    }
}

  • controller接口
@RestController
@RequestMapping("/user")
public class UserController {

    private final static Logger log = LoggerFactory.getLogger(UserController.class);

    @PostMapping("/save")
    public Boolean saveUser(@RequestBody User user) {
        log.info("save user success : {}", JSON.toJSONString(user));
        return Boolean.TRUE;
    }

    @DeleteMapping("/del/{id}")
    public Boolean deleteUser(@PathVariable("id") Long id) {
        log.info("delete user success, user id : {}", id);
        return Boolean.TRUE;
    }

    @GetMapping("/list")
    public List<User> getUserList() {
        User user = new User("110", "大宝");
        log.info("getUserList result : " + user);
        return Lists.newArrayList(user);
    }

    @GetMapping("/get")
    public User getUserById(@RequestParam(value = "id", required = true) Long id) {
        User user = new User("111", "大宝");
        log.info("getUserById result : {} , id : {}", user, id);
        return user;
    }
}
  • 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserServiceProviderApplication.class, args);
    }

}

2. 消费者应用

OpenFeign实战

  • 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 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.10.RELEASE</version>
      <relativePath/>
   </parent>

   <groupId>com.example</groupId>
   <artifactId>open-feign-consumer</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>open-feign-consumer</name>
   <description>open-feign-consumer</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>

   </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>
         </plugin>
      </plugins>
   </build>

</project>
  • application.properties
server.port=7072
# 注册中心地址
eureka.client.service-url.defaultZone=http://admin:123@ek1.com:7901/eureka/,http://admin:123@ek2.com:7902/eureka/
#
# 客户端在注册中心中的名称
eureka.instance.instance-id=open-feign-consumer-7072
#
# 当前服务对外暴露的名称
spring.application.name=open-feign-consumer
#
# 指定 feign 从请求到获取提供者响应的超时时间
feign.client.config.default.read-timeout=5000
#
# 指定 feign 连接提供者的超时时间
feign.client.config.default.connect-timeout=5000
#
# 设置当前 client 每5秒向 server 发送一次心跳,默认 30s
eureka.instance.lease-renewal-interval-in-seconds=5
#
# 表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance,默认为90
eureka.instance.lease-expiration-duration-in-seconds=90
#
# 表示将自己的ip注册到Eureka Server上。不配置,表示将操作系统的 hostname 注册到 server
eureka.instance.prefer-ip-address=true
#
# 是否将自己注册到其他Eureka Server
eureka.client.register-with-eureka=true
#
# 是否从eureka server获取注册信息
eureka.client.fetch-registry=true
#
# 表示eureka client间隔多久去拉取服务注册信息,默认为30秒,如果要迅速获取服务注册状态,可以缩小该值,比如5
eureka.client.registry-fetch-interval-seconds=5
  • 创建实体
public class User implements Serializable {

    private String idCard;
    private String username;

    public User() {
    }

    public User(String idCard, String username) {
        this.idCard = idCard;
        this.username = username;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getIdCard() {
        return idCard;
    }

    public void setIdCard(String idCard) {
        this.idCard = idCard;
    }

    @Override
    public String toString() {
        return "User{" +
                "idCard='" + idCard + ''' +
                ", username='" + username + ''' +
                '}';
    }
}

public class ResultBody<T> implements Serializable {

    private boolean status;
    // 响应码
    private String code;
    // 响应描述信息
    private String message;
    // 响应数据
    private T data;

    public ResultBody() {
    }

    private ResultBody(T data) {
        this.data = data;
    }

    private ResultBody(String code, String msg) {
        this.code = code;
        this.message = msg;
    }

    public static <T> ResultBody<T> success() {
        ResultBody<T> result = new ResultBody<>();
        result.setCode("1");
        result.setStatus(Boolean.TRUE);
        result.setMessage("成功");
        return result;
    }

    public static <T> ResultBody<T> success(T data) {
        ResultBody<T> result = new ResultBody<>();
        result.setCode("1");
        result.setStatus(Boolean.TRUE);
        result.setMessage("成功");
        result.setData(data);
        return result;
    }

    public static <T> ResultBody<T> error(String code, String message) {
        ResultBody<T> result = new ResultBody<>(code, message);
        result.setStatus(Boolean.FALSE);
        return result;
    }

    public boolean isStatus() {
        return status;
    }

    public void setStatus(boolean status) {
        this.status = status;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
  • @FeignClient定义接口
/**
 * 定义http请求接口
 */
@FeignClient(value = "user-provider/user")
public interface OpenFeignUserService {

    @PostMapping("/save")
    Boolean saveUser(@RequestBody User user);

    @DeleteMapping("/del/{id}")
    public Boolean deleteUser(@PathVariable("id") Long id);

    @GetMapping("/list")
    List<User> getUserList();

    @GetMapping("/get")
    User getUserById(@RequestParam(value = "id", required = true) Long id);
}

注意: 如果这里你要使用@RequestMapping注解的时候,你必须说明请求方式,例如:@RequestMapping(value = "/save", method = RequestMethod.POST)

  • controller定义
@RestController
@RequestMapping("/open-feign")
public class OpenFeignController {
    @Resource
    private OpenFeignUserService userService;

    @PostMapping("/save")
    public ResultBody saveUser(@RequestBody User user) {
        Boolean result = userService.saveUser(user);
        return ResultBody.success(result);
    }

    @DeleteMapping("/del/{id}")
    public ResultBody deleteUser(@PathVariable("id") Long id) {
        Boolean result = userService.deleteUser(id);
        return ResultBody.success(result);
    }

    @GetMapping("/list")
    public ResultBody getUserList() {
        List<User> userList = userService.getUserList();
        return ResultBody.success(userList);
    }

    @GetMapping("/get")
    public ResultBody getUserById(@RequestParam(value = "id", required = true) Long id) {
        User user = userService.getUserById(id);
        return ResultBody.success(user);
    }
}
  • 启动类定义, @EnableFeignClients启用 OpenFeign
@SpringBootApplication
@EnableDiscoveryClient
// 开启 Feign 客户端,指定service接口所在的包
@EnableFeignClients(basePackages = "com.example.openfeign.consumer.service")
public class OpenFeignConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(OpenFeignConsumerApplication.class, args);
    }
}
  • IDEAHttpClient 测试文件TestUser.http
###
POST http://127.0.0.1:7072/open-feign/save
Content-Type: application/json

{
   "idCard":"123",
   "username":"Kate"
}

###
DELETE http://127.0.0.1:7072/open-feign/del/110

###
GET http://127.0.0.1:7072/open-feign/list

###
GET http://127.0.0.1:7072/open-feign/get?id=111

二、 超时和重试

OpenFeign默认支持RibbonRibbon的重试机制和OpenFeign的重试机制有冲突,所以源码中默认关闭了OpenFeign的重试机制,使用Ribbon重试机制。

1. 超时

  • 服务提供者接口
private AtomicLong atomicLong = new AtomicLong();

@GetMapping("/retry")
public User retryUser() {

    try {
        log.info("超时模拟 ...");
        Thread.sleep(6000);
    } catch (Exception e) {
        log.info("执行异常");
    }

    long i = atomicLong.getAndIncrement();
    log.info("retryUser 接口第 {} 次调用", i);
    User user = new User("111", "大宝");
    return user;
}
  • 客户端设置业务超时时间
# 业务超时时间
ribbon.ReadTimeout=2000
@FeignClient(value = "user-provider/user")
public interface OpenFeignUserService {
    @GetMapping("/retry")
    User retryUser();
}
@RestController
@RequestMapping("/open-feign")
public class OpenFeignController { 
    @GetMapping("/retry")
    public ResultBody retryUser() {
        User user = userService.retryUser();
        return ResultBody.success(user);
    }
}

OpenFeign实战

1. 重试

客户端设置重试次数

# 同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=3
#
# 重试负载均衡其他实例最大此时,不包括首次调用
ribbon.MaxAutoRetriesNextServer=3
#
# 是否所有操作都重试
ribbon.okToRetryOnAllOperations=false

三、日志配置

properties文件中配置日志级别,方便本地调试,参考官方文档:docs.spring.io/spring-clou…

# none:不记录任何日志,默认值;
# basic:仅记录请求方法,url,响应状态码,执行时间;
# headers:在basic基础上,记录header信息;
# full:记录请求和响应的header,body,元数据
#
feign.client.config.default.logger-level=full
#
# logger-level只对 debug 级别日志做出响应
logging.level.com.example.openfeign.consumer=debug

如果不想通过feign.client.config.default.logger-level的方式配置,也可通过Java代码的方式来配置

@Configuration
public class FeiginConfig {
	@Bean
	Logger.Level logLevel(){	
		return Logger.Level.FULL;
	}
}
转载自:https://juejin.cn/post/7253466047889965115
评论
请登录