目录
一、Feign 简介
1、RestTemplate 远程调用中存在的问题
2、定义和使用 Feign 客户端
3、Feign 自定义配置
4、Feign 性能优化
5、Feign 最佳实践
6、Feign 使用问题汇总
二、Gateway 网关简介
1、搭建网关服务
2、路由断言工厂
3、路由的过滤器配置
4、全局过滤器
5、过滤器链执行顺序
6、网关的 cors 跨域配置
7、Gateway 网关使用问题汇总
一、Feign 简介
Feign 是 Netflix 开发的声明式、模板化的 Http 客户端,Feign 可以帮助我们更快捷、优雅地调用Http API。
- Feign 是在 Ribbon 的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。
- SpringCloud 对 Feign 进行了增强,使 Feign 支持了 Spring MVC 注解,并整合了 Ribbon 和 Eureka,集成了服务发现和负载均衡,从而让 Feign 的使用更加方便。
1、RestTemplate 远程调用中存在的问题
该远程调用的代码有以下问题:
- 代码可读性差,编程体验不统一;
- 参数复杂,URL 难以维护;
2、定义和使用 Feign 客户端
在 SpringCloud 中,使用 Feign 非常简单,创建一个 Interface,并在接口上添加一些注解,代码就完成了。
(1)引入依赖
- 给需要远程调用的模块,引入 openfeign 起步依赖;
(2)添加 @EnableFeignClients 注解
- 给需要远程调用的启动类,添加 @EnableFeignClients 注解;
(3)编写 Feign 客户端
- 添加 @FeignClient 和 @GetMapping 注解;
上述注解声明了如下信息:
- 服务名称:user-service;
- 请求方式:get;
- 请求路径:/user/{userId};
- 请求参数:String userId;
- 返回值类型:User;
(4)OrderService 远程调用过程
该远程调用过程,就好像平常处理请求的逆过程:
- OrderService 将 userId 传递给接口中的方法;
- queryUserById 方法再将参数赋值给请求路径中的占位符 {userId};
- 然后再通过 get 请求向 UserController 发送请求;
- 最终获得 User 对象信息;
3、Feign 自定义配置
Spring 虽然帮我们做好了配置,但是也允许我们修改默认配置。
配置 Feign 日志有 2 种方式:
(1)application/bootstrap 配置文件方式
- 注意:logging.level 属性值需要改为 trace;
(2)Java 代码方式
4、Feign 性能优化
Feign 底层客户端实现:
- URLConnection:默认使用,java 原生 jdk,性能较差,不支持连接池;
- Apache HttpClient:支持连接池;(推荐)
- OKHttp:支持连接池;(推荐)
对 Feign 的性能优化,最重要的一点就是对底层实现的改变。
因此优化 Feign 的性能主要包括:
- 使用连接池代替默认的 URLConnection;
- 日志级别,最好用 basic 或 none;(因为开启日志也需要占用较多资源)
下面以 HttpClient 为例子,说明性能优化的步骤:
(1)引入依赖
- httpClient 已经被 Spring 管理好版本了,不需要指定版本;
(2)修改 application 配置文件
- 将 feign.httpclient.enabled 的属性值改为 true,表明底层使用 httpclient;
5、Feign 最佳实践
最佳实践是指,企业开发过程中,总结设计缺点,得出的一种相对比较好的使用方式。
通常会用到 2 种方式,各有利弊,按自己的需求使用:
- 继承:给消费者的 FeignClient 和提供者的 controller 定义统一的父接口,然后实现接口;
- 比如 order 模块要调用 user 模块的服务,就定义 UserClient 接口,继承 UserAPI;
- 缺点:耦合度高;
- 优点:遵从了契约;
- 抽取:将 FeignClient 抽取为独立模块,并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用;
- 缺点:消费者可能会引入很多用不到的 api;
- 优点:耦合度低,层次分明;
我个人更喜欢第二种抽取的方式,下面使用这种方式做一个例子。
(1)创建 feign-api 模块
- 在 feign-api 模块中引入 feign 的起步依赖;
- 将消费者模块中对提供者模块所需的 Client、Pojo,以及 Config 类都移动到 feign-api 中;
(2)引入 feign-api 模块
- 在消费者 order-service 模块中,引入 feign-api 模块;
(3)消除报错
- 将 order-service 模块内原来依赖 pojo、config、client 的类,修改为 feign-api 模块内的类;
- 由于 order-service 模块的 application 启动类无法扫描到 feign-api 模块下的 Client 的 Bean 对象,因此会出现如下报错:
- 需要在 @EnableFeignClients 中加入 clients 属性;
6、Feign 使用问题汇总
(1)Did you forget to include spring-cloud-starter-loadbalancer?
出错原因:
- SpringCloud Feign 在 Hoxton.M2 RELEASED 版本之后不再使用 Ribbon,而是使用 spring-cloud-loadbalancer,所以在不引入 spring-cloud-loadbalancer 的情况下会报错。
参考了很多解决方法,主要是要区别版本问题:
在包含 RELEASE 关键字的版本中,只有下面这样操作才能使用:
- 首先排除 ribbon 的起步依赖,排除位置在消费者依赖的服务提供者的 dependency;
- 添加 loadBalancer 依赖;
而在 2021 及更新版本中(也就是没有 RELEASE 关键字的版本),只需要如下操作:
- 引入 loadBalancer 依赖;
- 不需要排除 ribbon 的起步依赖;
二、Gateway 网关简介
为什么需要网关?
- 如果没有网关,那么所有人都可以访问我们所有的服务,但是有很多服务其实是不能对外公开的。
- 需要使用网关进行身份验证,通过后才能访问敏感服务。
- 一切请求需要先通过网关,才能到微服务。
因此,网关有如下功能:
- 身份认证和权限校验;
- 服务路由、负载均衡;(也就是能知道将请求送到哪个微服务)
- 限制请求流量;
SpringCloud 提供了 2 个组件实现网关功能:
- gateway;(新版本)
- zuul;(早期版本)
Zuul 是基于 Servlet 的实现,属于阻塞式编程。
而 SpringCloudGateway 则是基于 Spring5 中提供的 WebFlux,属于响应式编程的实现,具备更好的性能。
1、搭建网关服务
(1)创建 gateway 模块
- 创建一个新的模块专门用于网关:gateway-module,需要 SpringBoot 启动类;
- 引入 SpringCloudGateway 起步依赖;
- 引入 Nacos 服务发现依赖;
(2)编写路由配置以及 Nacos 地址
- gateway 中的 routes 属性之前的配置,目的是为了让 gateway 将服务注册到 nacos;
- gateway 中的 routes 属性之后的配置,指明了可以使用的 service 路由;
- id:表示这个路由的名字,与其他属性无关;
- uri:表示路由目标地址,其中 lb 表示负载均衡,后面跟着服务的 name;
- predicates:指定路由规则,符合规则就可以放行;
(3)测试网关功能
- 启动 gateway 的 application;
- 发起请求:localhost:10010/order/queryOrderById/1;
- 这个请求中,order-service 模块会调用 user-service 的服务;
- 访问失败,说明我们的网关起作用了,因为我们没有为网关配置 order-service 的路由;
- 添加对 order-service 的服务的路由后,就可以从 gateway 访问 order 的服务了;
2、路由断言工厂
路由断言工厂 Route Predicate Factory,作用是:
- 我们在配置文件中写的 predicates 断言规则只是字符串,这些字符串会被 Predicate Factory 读取并解析为路由判断的条件。
- 每种规则都有各自的断言工厂去解析,比如 Path 有 PathRoutePredicateFactory 断言工厂;
下面以 After 举一个例子:
(1)添加断言规则 After
- After 断言规则,规定在这个时间之后,才能进行访问;
(2)访问 order 的服务
- 发送请求 /order/queryOrderById/1;
- 理向情况应该是:无法访问;
- 修改断言规则的时间为:2020-12-31,重启 gateway 模块;
- 再次发起请求,就可以访问了;
3、路由的过滤器配置
路由过滤器是:GatewayFilter。
- GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
在这个过程中,过滤器就可对请求和响应做出各种各样的处理,这样往下游传递的数据中,就包含了修改后的信息。
具体有什么样的操作,就要看使用哪个过滤器工厂:
下面我们以添加一个请求头为例子:
(1)修改 application 配置文件
- 给 routes 中的一个路由添加 filters 属性;
- filters 中添加:- AddRequestHeader=[key],[value];
(2)编写 Controller,获取请求头信息
- 既然在 filter 中设置了 header:orderStatus=001;
- 那么请求传递到 Controller 后,一定可以获取到这个头信息;
- 访问 /order/queryOrderById/1,查看控制台输出;
(3)编写默认过滤器 default-filter
如果我们想为所有的请求都添加 header 信息,也不需要给每一个路由都写上 filters。
- 只需要在 routes 同级下,添加 default-filter 属性;
- 给 default-filter 属性添加:- AddRequestHeader=[key],[value] 等属性值;
- 访问 /order/queryOrderById/1,查看控制台输出;
4、全局过滤器
全局过滤器是:GlobalFilter。
- 全局过滤器的作用:处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样,只是作用范围不同。
- 其中,GatewayFilter 通过配置定义,运行逻辑是固定的。(官方定义)
- 而 GlobalFilter 的运行逻辑需要自己写代码实现。(自定义)
需求
定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
- 参数中是否有 authorization;
- authorization 参数值是否为 admin;
- 如果同时满足则放行,否则拦截;
(1)定义实现类
- 创建 AuthorFilter 实现类,实现 GlobalFilter 接口;
- exchange:请求上下文,里面可以获取Request. Response等信息;
- chain:用来把请求委托给下一个过滤器;
- Nono<Void>:返回表示当前过滤器业务结束
(2)定义 filter 方法的运行逻辑
- 添加 @Component;
- 添加 @Order,方便多过滤器时指定顺序;
- 网关中采用的都是基于 WebFlux(响应式编程)的 API,因此使用上与 ServletAPI 不同;
@Order(-1)
@Component
public class AuthorFilter implements GlobalFilter {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取请求参数ServerHttpRequest request = exchange.getRequest();MultiValueMap<String, String> params = request.getQueryParams();// 2.获取请求参数中的 author 参数值String author = params.getFirst("author");// 3.判断是否等于 adminif (author.equalsIgnoreCase("admin")) {// 放行return chain.filter(exchange);}// 4.不相等,获取响应ServerHttpResponse response = exchange.getResponse();// 4.1.设置状态码,401表示未登录response.setStatusCode(HttpStatus.UNAUTHORIZED);// 4.2.拦截请求return response.setComplete();}
}
(3)启动 gateway 的 application
- 访问 /order/queryOrderById/1/author=admin111,先使用错误的 authon 参数值;
- 发现返回了 401;
- 访问 /order/queryOrderById/1/author=admin,使用正确的 authon 参数值;
- 可以正常访问
5、过滤器链执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter。
请求路由后,会将当前路由过滤器和 DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。
过滤器执行顺序:
- 每一个过滤器都必须指定一个 int 类型的 order 值:order 值越小,优先级越高,执行顺序越靠前;
- GlobalFilter 通过实现 Ordered 接口,或者添加 @Order 注解来指定 order 值,由我们自己指定;
- 路由过滤器和 defaultFilter 的 order 由 Spring 官方指定,默认是按照声明顺序从 1 递增;
- 当过滤器的 order 值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter 的顺序执行;
6、网关的 cors 跨域配置
由于一些页面可能需要从微服务中获取某些数据,此时需要发起 AJAX 请求,那么这就可能属于跨域请求的范畴。(一般我们的 web 页面端口都不会与微服务的相同,所以请求基本上都是跨域请求)
跨域请求不需要在微服务中处理,只需要在网关中处理即可。
跨域:域名不一致就是跨域,主要包括:
- 域名不同:www.taobao.com 和 www.taobao.org 和 www.jd.com 和 miaosha.jd.com;
- 域名相同,端口不同:localhost:8080 和 localhost:8081
跨域问题的 2 个要素:
- 浏览器、服务端:浏览器禁止请求向服务端发起跨域 ajax 请求,请求会被拦截;
- 解决方案:CORS;
需求:
- 使用 localhost:8090 发起请求,8090 发送的是 AJAX 的 GET 请求。
- 8090 发起的 AJAX 请求需要通过网关,在 AJAX 的目标地址中要用 10010 端口;
(1)编写 gateway 的配置文件
- 各个属性的作用都写在注释中了;
(2)端口 8090 的 AJAX 请求
- 这里使用 JQuery 发起 AJAX 请求,传递参数为 author=admin;
- 这里参数是必须传递的,因为发起的请求目标地址是要通过网关的,需要鉴权;
<head><meta charset="UTF-8"><title>Title</title><script type="text/javascript" src="JQuery-3.7.0.js"></script><script type="text/javascript">$(function() {$("#ajaxButton").click(function() {$.ajax({url:"http://localhost:10010/order/queryOrderById/1",data: {author : "admin"},type: "get",dataType: "json",success: function(data) {console.log(data);}});});});</script>
</head>
<body><a href="http://localhost:10010/order/queryOrderById/1?author=admin"> 发起普通GET请求 </a><input id="ajaxButton" type="button" value="发起 ajax 请求"/>
</body>
- 发起请求,可以在控制台看到返回的数据;
- 而如果我们用 <a> 来请求,是会跳转到 10010 得页面的,这就不算跨域请求了;
7、Gateway 网关使用问题汇总
(1)搭建网关服务时,报错 503
这是因为在 Spring Cloud 2020 版本以后,默认移除了对 Netflix 的依赖,其中就包括 Ribbon,官方默认推荐使用 Spring Cloud Loadbalancer 正式替换 Ribbon,并成为了 Spring Cloud 负载均衡器的唯一实现。
Loadbalancer 依赖: