springcloud动力节点-03OpenFeign

发布时间 2023-12-28 20:13:23作者: 爵岚

Spring Cloud OpenFeign 

1.说在前面

上 一 节 我 们 讲 到 Ribbon 做 了 负 载 均 衡 , 用 Eureka-Client 来 做 服 务 发 现 , 通 过RestTemplate 来完成服务调用,但是这都不是我们的终极方案,终极方案是使用 OpenFeign

2.OpenFeign 简介

https://docs.spring.io/spring-cloud-openfeign/docs/2.2.4.RELEASE/reference/html/#spring-cloud-feign
Feign 是声明性(注解)Web 服务客户端。它使编写 Web 服务客户端更加容易。要使用 Feign,请创建一个接口并对其进行注解。它具有可插入注解支持,包括 Feign 注解和 JAX-RS 注解。
Feign 还支持可插拔编码器和解码器。Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用 HttpMessageConverters,Spring Web 中默认使用的注解。Spring Cloud 集成了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载平衡的 http 客户端。
Feign 是一个远程调用的组件 (接口,注解) http 调用的
Feign 集成了 ribbon
ribbon 里面集成了 eureka

3.OpenFeign 快速入门

3.1 本次调用的设计图

 

 

3.2 启动一个 eureka-server 服务,这里不重复演示,参考 eureka文档

3.3 先创建 provider-order-service,选择依赖 

 

3.4 provider-order-service 修改配置文件 

# 应用服务 WEB 访问端口
server:
  port: 8080
spring:
  application: # 注册名称
    name: order-service
eureka:
  client:
    service-url: # 注册地址
      defaultZone: http://localhost:8761/eureka
  instance:
  instance-id: ${spring.application.name}:${server.port}
  prefer-ip-address: true

3.5 provider-order-service 修改启动类增加一个访问接口

package com.tongda.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
public class OrderController {

    @GetMapping("doOrder")
    public String doOrder() {
        System.out.println("有用户来下单了");
        return "我是订单服务";
    }
}

3.6 provider-order-service 启动测试访问

 

 

3.7 再创建 consumer-user-service,选择依赖

 

3.8 consumer-user-service 修改配置文件

# 应用服务 WEB 访问端口
server:
  port: 8081
spring:
  application: # 注册名称
    name: user-service
eureka:
  client:
    service-url: # 注册地址
      defaultZone: http://localhost:8761/eureka
  instance:
  instance-id: ${spring.application.name}:${server.port}
  prefer-ip-address: true

3.9 consumer-user-service 创建一个接口(重点)

package com.tongda.feign;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 *
 * @FeignClient(value = "order-service")
 * value 就是提供者的应用名称,value 后面的值必须和提供者的服务名一致
 *
 */
@FeignClient(value = "order-service") // 注解(value=“应用者注册名称”)
public interface UserOrderFeign {

    // 直接调用编写controller中doorder方法
    /*
     * 你需要调用哪个controller,就写它的方法签名。
     * 方法签名(就是包含一个方法的所有的属性)
     * 描述: 下单的方法 这里的路径必须和提供者的路径一致
     * @param
     * @return {@link java.lang.String}
     *
     **/
    @GetMapping("doOrder")
    public String doOrder(); // 方法的所有的属性 就是 方法签名

} 

3.10 consumer-user-service 创建 controller 

package com.tongda.controller;

import com.tongda.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    /*
     * 接口是不能做事情的
     * 如果想做事,必须要有对象
     * 那么这个接口肯定是被创建出代理对象的
     * 动态代理jdk(java interface 接口 $Proxy) cglib(subClass 子类)
     * jdk动态代理,只要是代理对象调用的方法必须走invoke方法
     * @param null
     * @return {@link null}
     *
     **/
    @Autowired
    private UserOrderFeign userOrderFeign;

    /*
     * 总结:
     * 浏览器(前端)---->user-service(/userDoOrder)---->RPC远程过程调用(feign)---->order-service(/doOrder接口)
     * feign的默认等待时间是 1s,超多1s就会报错:Time out超时
     * 可以用yml来配置超时设定
     * @param
     * @return {@link java.lang.String}
     *
     **/
    @GetMapping("userDoOrder")
    public String UserDoOrder() {
        System.out.println("有用户进来了");
        // 需要调用订单orderService,发起远程调用
        // 1.需要开启feign的客户端功能,修改启动类才可以帮助我们发起调用
        // 2.需要创建一个接口,包名feign,并加注解@FeignClient(value = "order-service") // 注解(value=“应用者注册名称”)
        String s = userOrderFeign.doOrder();
        return s;
    }
}

3.11 consumer-user-service 修改启动类 

package com.tongda;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient // eureka 开启
@EnableFeignClients // 开启feign的客户端功能,才可以帮助我们发起调用
public class UserServiceApplication {

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

3.12 启动调用测试 

 

访问:http://localhost:8081/userDoOrder 

 

3.13 本次调用总结

consumer-user-service---》 /userDoOrder ---》通过 feign 调用 /doOrder ---》provider-order-service 下单成功 

3.14 测试 feign 调用的负载均衡 

启动多台 provider-order-service: 

 

测试访问:

 

3.15 调用超时设置

因 为 ribbon 默 认 调 用 超 时 时 长 为 1s , 可 以 修 改 , 超 时 调 整 可 以 查 看DefaultClientConfigImpl
# feign只是帮你防撞了远程调用的功能,底层还是ribbon所以我们需要去修改ribbon的时间配置
ribbon:
  ReadTimeout: 3000 # 给3s查时间
  connectTimeout: 3000 # 链接服务的超时时间

4.OpenFeign 调用参数处理(开发重点)

4.1 说在前面

Feign 传参确保消费者和提供者的参数列表一致 包括返回值 方法签名要一致
1. 通过 URL 传参数,GET 请求,参数列表使用@PathVariable(“”)
2. 如果是 GET 请求,每个基本参数必须加@RequestParam(“”)
3. 如果是 POST 请求,而且是对象集合等参数,必须加@Requestbody 或者@RequestParam

4.2 修改 provider-order-service

4.2.1 创建 BaseResult 类 :获取code\data、msg属性

public class BaseResult implements Serializable {
    private Integer code;
    private String msg;
    private Object data;
    public static BaseResult success(Integer code, String msg, Object data) {
        BaseResult baseResult = new BaseResult();
        baseResult.setCode(code);
        baseResult.setData(data);
        baseResult.setMsg(msg);
        return baseResult;
    }
}        

4.2.2 创建 Order 类

package com.tongda.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Order {
    private Integer id;
    private String name;
    private double price;
    private Date time;
}

4.2.3 创建 TestParamController 类

package com.tongda.controller;

import com.tongda.domain.Order;
import org.springframework.web.bind.annotation.*;

/**
 *
 * url   /doOrder/热干面/add/油条/aaa ;无需key=value
 * get传递一个参数
 * get传递多个参数
 * post传递一个对象
 * post传递一个对象+一个基本参数
 * post传递2个对象?
 * @Date 2023/7/17 7:44
 * @Version 1.0
 */
@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 BaseResult.success(200, "ok", order);
    }

    @GetMapping("oneParam")
    // get传递一个参数,省略传参:required = false
    public String oneParam(@RequestParam(required = false) String name){
        System.out.println(name);
        return BaseResult.success(200, "ok", order);
    }

    @GetMapping("twoParam")
    // get传递多个参数
    public String twoParam(@RequestParam(required = false)String name,@RequestParam(required = false)Integer age) {
        System.out.println(name);
        System.out.println(age);
        return BaseResult.success(200, "ok", order);
    }

    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order){
        System.out.println(order);
        return BaseResult.success(200, "ok", order);
    }

    @PostMapping("oneObjOneParam")
    // post传递一个对象+一个基本参数,body只能放一个对象,请求param参数?name
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
        System.out.println(name);
        System.out.println(order);
        return "ok";
    }
}

4.3 修改 consumer-user-service

4.3.1 将 Order 类和 BaseResult 类拷贝过来,后面会抽到公共模块里

4.3.2 修改 UserOrderFeign 接口

package com.tongda.feign;

import com.tongda.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 *
 * @FeignClient(value = "order-service")
 * value 就是提供者的应用名称,value 后面的值必须和提供者的服务名一致
 *
 */
@FeignClient(value = "order-service") // 注解(value=“应用者注册名称”)
public interface UserOrderFeign {

    // 直接调用编写controller中doorder方法
    /*
     * 你需要调用哪个controller,就写它的方法签名。
     * 方法签名(就是包含一个方法的所有的属性)
     * 描述: 下单的方法 这里的路径必须和提供者的路径一致
     * @param
     * @return {@link java.lang.String}
     *
     **/
    @GetMapping("doOrder")
    public String doOrder(); // 方法的所有的属性 就是 方法签名

    // 内部传参:技巧拷贝order下ParamController,去掉请求体
    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name")String name, @PathVariable("age")Integer age);

    @GetMapping("oneParam")
    // get传递一个参数,省略传参:required = false
    public String oneParam(@RequestParam(required = false) String name);

    @GetMapping("twoParam")
    // get传递多个参数
    public String twoParam(@RequestParam(required = false)String name,@RequestParam(required = false)Integer age);

    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order);

    @PostMapping("oneObjOneParam")
    // post传递一个对象+一个基本参数,body只能放一个对象,请求param参数?name
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name);

}

4.3.3 创建 TestController 类

package com.tongda.controller;

import com.tongda.domain.Order;
import com.tongda.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

@RestController
public class UserController {

    /*
     * 接口是不能做事情的
     * 如果想做事,必须要有对象
     * 那么这个接口肯定是被创建出代理对象的
     * 动态代理jdk(java interface 接口 $Proxy) cglib(subClass 子类)
     * jdk动态代理,只要是代理对象调用的方法必须走invoke方法
     * @param null
     * @return {@link null}
     *
     **/
    @Autowired
    private UserOrderFeign userOrderFeign;

    /*
     * 总结:
     * 浏览器(前端)---->user-service(/userDoOrder)---->RPC远程过程调用(feign)---->order-service(/doOrder接口)
     * feign的默认等待时间是 1s,超多1s就会报错:Time out超时
     * 可以用yml来配置超时设定
     * @param
     * @return {@link java.lang.String}
     *
     **/
    @GetMapping("userDoOrder")
    public String UserDoOrder() {
        System.out.println("有用户进来了");
        // 需要调用订单orderService,发起远程调用
        // 1.需要开启feign的客户端功能,修改启动类才可以帮助我们发起调用
        // 2.需要创建一个接口,包名feign,并加注解@FeignClient(value = "order-service") // 注解(value=“应用者注册名称”)
        String s = userOrderFeign.doOrder();
        return s;
    }


    @GetMapping("testParam")
    public String testParam() {
        // url传参
        String juelan = userOrderFeign.testUrl("juelan", 18);
        System.out.println(juelan);

        // 一个参数
        String t = userOrderFeign.oneParam("老唐");
        System.out.println(t);

        // 两个参数
        String l = userOrderFeign.twoParam("老李", 32);
        System.out.println(l);

        // 对象:使用链式创建
        Order order = Order.builder()
                .name("牛排")
                .price(188D)
                .time(new Date())
                .id(1)
                .build();

        String s = userOrderFeign.oneObj(order);
        System.out.println(s);

        // 一个对象一个参数
        String h = userOrderFeign.oneObjOneParam(order, "汉");
        System.out.println(h);

        return "OK";
    }
}

4.3.4 测试调用

访问: http://localhost:8081/testFeignParam 

4.3.5 时间日期参数问题

使用 feign 远程调用时,传递 Date 类型,接收方的时间会相差 14 个小时,是因为时区造成
处理方案:
1. 使用字符串传递参数,接收方转换成时间类型(推荐使用)不要单独传递时间
2. 使用 JDK8 的 LocalDate(日期) 或 LocalDateTime(日期和时间,接收方只有秒,没有毫
秒)
3. 自定义转换方法
第一步:order-Service中ParamController
// 时间
    @GetMapping("testTime")
    public String testTime(@RequestParam Date date) {
        System.out.println(date);
        return "OK";
    }

第二步:user-Service中UserOrderFergn接口interface

// 时间
    @GetMapping("testTime")
    public String testTime(@RequestParam Date date);

第三步:user-Service中UserController

// 时间测试: 发现调用时间之间相差+—10小时
    /*
     * Sun Mar 20 10:24:23 CST 2022
     * Mon Mar 21 00:24:13 CST 2022 +- 14小时
     * 1. 不建议单独传递时间参数,出现上面问题
     * 2. 转成字符串解决问题: 2022-03-20 10:25:55:213 因为字符串不会改变
     * 3. jdk LocalDate 年月日 LocalDateTime 会丢失s秒
     * 4. 改feign的源码
     * 
     * @param
     * @return {@link java.lang.String}
     *
     **/
    @GetMapping("time")
    public String time() {
        Date date = new Date();
        System.out.println(date);
        String s = userOrderFeign.testTime(date);
        // 字符串转
        LocalDate now = LocalDate.now();// 年月日
        LocalDateTime now1 = LocalDateTime.now(); // 年月日时分秒
        return s;
    }
传参总结:
get 请求只用来传递基本参数 而且加注解@RequestParam
post 请求用来传递对象参数 并且加注解@RequestBody

5.OpenFeign 源码分析

 

(学习别人的思想,可以找 bug,优化你的代码,提高代码的
健壮性)
看源码之前要先大致猜想一下 他是怎么实现的?(先使用在分析)

5.1 OpenFeign 的原理是什么?

根据上面的案例,我们知道 feign 是接口调用,接口如果想做事,必须要有实现类,可是我们并没有写实现类,只是加了一个@FeignClient(value=”xxx-service”)的注解所以我们猜测 feign 帮我们创建了代理对象,然后完成真实的调用。
动态代理 1  jdk (invoke) 2  
1. 给接口创建代理对象(启动扫描)
2. 代理对象执行进入 invoke 方法
3. 在 invoke 方法里面做远程调用
具体我们这次的流程:
A. 扫描注解得到要调用的服务名称和 url 

 

B. 拿到 provider-order-service/doOrder,通过 ribbon 的负载均衡拿到一个服务,
provider-order-service/doOrder---》http://ip:port/doOrder
C. 发起请求,远程调用

5.2 看看 OpenFeign 的内部是如何实现这些的

 

  

5.2.1 如何扫描注解@FeignClient

查看启动类的@EnableFeignClients

 

进入 FeignClientsRegistrar 这个类 去查看里面的东西

 

 

真正的扫描拿到注解和服务名称

 

5.2.2 如何创建代理对象去执行调用?

当我们启动时,在 ReflectiveFeign 类的 newInstance 方法,给接口创建了代理对象

 

当我们执行调用的时候,打个断点去查看

 

ReflectiveFeign 类中的 invoke 方法帮我们完成调用

 

SynchronousMethodHandler 的 invoke 中给每一个请求创建了一个 requestTemplate 对象,去执行请求

 

executeAndDecode

 

我们去看 LoadBalancerFeignClient 的 execute 方法

 

executeWithLoadBalancer 继续往下看

 

 

只要是 feign 调用出了问题
看 feign 包下面的 Client 接口下面的 108 行
200 成功
400 请求参数错误
429 被限流
401 没有权限;无token
403 权限不够;token
404 路径不匹配
405 方法不允许
500 提供者报错了
302 资源重定向

 6.OpenFeign 总结

OpenFeign 主要基于接口和注解实现了远程调用
源码总结:面试
1. OpenFeign 用过吗?它是如何运作的?
主启动类上加上@EnableFeignClients 注解后,启动会进行包扫描,把所有加了@FeignClient(value=”xxx-service”)注解的接口进行创建代理对象通过代理对象,使用ribbon 做了负载均衡和远程调用
2. 如何创建的代理对象?
当 项 目 在 启 动 时 , 先 扫 描 , 然 后 拿 到 标 记 了 @FeignClient 注 解 的 接 口 信 息 , 由ReflectiveFeign 类的 newInstance 方法创建了代理对象 JDK 代理
3. OpenFeign 到底是用什么做的远程调用?
使用的是 HttpURLConnection (java.net)
4. OpenFeign 怎么和 ribbon 整合的?
在代理对象执行调用的时候

 7.OpenFeign 其他

7.1 OpenFeign 的日志功能

从前面的测试中我们可以看出,没有任何关于远程调用的日志输出,如请头,参数Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而揭开 Feign 中 Http 请求的所有细节

7.1.1 OpenFeign 的日志级别

 

NONE 默认的,不显示日志
BASE 仅记录请求方法,URL ,响应状态码及执行时间
HEADERS 在 BASE 之上增加了请求和响应头的信息
FULL 在 HEADERS 之上增加了请求和响应的正文及无数据

7.1.2 创建配置类

package com.tongda;

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 // eureka 开启
@EnableFeignClients // 开启feign的客户端功能,才可以帮助我们发起调用
public class UserServiceApplication {

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

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    // 配置日志:FULL 打印全部信息级别feign,还需要yml中配置开启
    @Bean
    public Logger.Level level() {
        return Logger.Level.FULL;
    }

}

7.1.3 修改配置文件

logging:
    level:
        com.bjpowernode.feign.UserOrderFeign: debug    

7.1.4 调用测试