springcloud动力节点-03OpenFeign
Spring Cloud OpenFeign
1.说在前面
上 一 节 我 们 讲 到 Ribbon 做 了 负 载 均 衡 , 用 Eureka-Client 来 做 服 务 发 现 , 通 过RestTemplate 来完成服务调用,但是这都不是我们的终极方案,终极方案是使用 OpenFeign
2.OpenFeign 简介
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 调用测试