【SpringCloud】OpenFeign
🔥个人主页: 中草药
🔥专栏:【中间件】企业级中间件剖析
一、OpenFeign
OpenFeign 是 Spring Cloud 生态中的声明式 HTTP 客户端工具,基于 Netflix Feign 开发。它通过接口和注解定义 HTTP 请求,简化了服务间的 RESTful 调用,并与 Ribbon(负载均衡)、Hystrix(熔断)等组件无缝集成。
快速上手
观察之前RestTemplate所写的代码
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
String url = "http://product-service/product/"+ orderInfo.getProductId();
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
虽然RestTemplate对HTTP进行封装之后,已经build直接使用HTTPClient简单方便许多,但是仍然存在一些问题。
1、需要拼接URL,灵活性较高,但是封装臃肿,URL复杂时,容易出错
2、代码的可读性差,风格不统一
此处我们可以用到OpenFeign
添加依赖
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用 OpenFeign
在 Spring Boot 启动类添加 @EnableFeignClients
:
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
定义 Feign 客户端接口
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductInfo(@PathVariable("productId") Integer productId);
}
value: 指定FeignClient的名称,用于服务发现,feign的底层会使用Spring Cloud LoadBalance进行负载均衡,也可以使用url
path:定义当前FeignClient的统一前缀
调用 Feign 客户端
@Slf4j
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductApi productApi;
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
ProductInfo productInfo = productApi.getProductInfo(orderInfo.getProductId());
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
}
参数传递
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductInfo(@PathVariable("productId") Integer productId);
//传递单个参数
@RequestMapping("/p1")
public String p1(@RequestParam("id") Integer id);
//传递多个参数
@RequestMapping("/p2")
public String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);
//传递对象
@RequestMapping("/p3")
public String p3(@SpringQueryMap ProductInfo productInfo);
//传递json
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo);
}
注意:@RequestParam 不能省略,作为参数绑定
抽取方式
官方推荐 Feign 的使用方式为继承的方式,但企业开发中,更多是把 Feign 接口抽取为一个独立的模块(做法和继承相似,但理念不同)。
继承
创建一个新的module,并引入依赖,编写相关 interface(内容基本和原服务调用方所用的代码一样)
public interface ProductInterface {
@RequestMapping("/{productId}")
ProductInfo getProductInfo(@PathVariable("productId") Integer productId);
//传递单个参数
@RequestMapping("/p1")
public String p1(@RequestParam("id") Integer id);
//传递多个参数
@RequestMapping("/p2")
public String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);
//传递对象
@RequestMapping("/p3")
public String p3(@SpringQueryMap ProductInfo productInfo);
//传递json
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo);
}
再通过maven打包
引入相关依赖
<dependency>
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>4
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
修改服务消费方中ProductApi,ProductInfo的路径为 product-api 中的路径
并修改ProductApi的代码,继承引入jar包的ProductInterface
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi extends ProductInterface {
}
修改服务提供方Controller的代码,实现ProductInterface接口
@RestController
@RequestMapping("/product")
public class ProductController implements ProductInterface {
@Autowired
private ProductService productService;
@RequestMapping("/{productId}")
public ProductInfo getProductInfo(@PathVariable("productId") Integer productId) {
return productService.selectProductById(productId);
}
//测试传递单个参数
@RequestMapping("/p1")
public String p1(Integer id){
return "product-service 接受到参数 id:"+id;
}
//测试传递多个参数
@RequestMapping("/p2")
public String p2(Integer id,String name){
return "product-service 接受到参数 id:"+id+",name:"+name;
}
//测试传递对象参数
@RequestMapping("/p3")
public String p3(ProductInfo productInfo){
return "product-service 接受到参数 productInfo:"+productInfo;
}
//测试传递Json
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo){
return "product-service 接受到参数 productInfo:"+productInfo;
}
}
完成抽取
抽取为独立模块
将 Feign 的 Client 抽取为一个独立的模块,并把涉及到的实体类等都放在这个模块中,打成一个 Jar。服务消费方只需要依赖该 Jar 包即可。这种方式在企业中比较常见,Jar 包通常由服务提供方来实现。
创建一个新的module,并引入依赖,编写相关api
再通过maven打包
引入相关依赖
<dependency>
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>4
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
修改服务消费方中ProductApi,ProductInfo的路径为 product-api 中的路径
启动类添加扫描路径
@EnableFeignClients(clients = {ProductApi.class})
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
完成抽取
服务部署
修改数据库,Nacos 等相关配置
对两个服务进行打包
Maven 打包默认是从远程仓库下载的,product-api 这个包在本地,有以下解决方案:
上传到 Maven 中央仓库 (参考:如何发布 Jar 包到 Maven 中央仓库,比较麻烦)[不推荐]
搭建 Maven 私服,上传 Jar 包到私服 [企业推荐]
从本地读取 Jar 包 [个人学习阶段推荐]
前两种方法比较复杂,咱们使用第三种方式,要先修改服务消费方的pom文件
plugin
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
dependency
<dependency>
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>system</scope>
<systemPath>C:/Users/18254/.m2/repository/org/example/product-api/1.0-SNAPSHOT/product-api-1.0-SNAPSHOT.jar</systemPath>
</dependency>
然后完成正常的打包部署运行操作即可
二、Gateway
API网关
API网关是分布式系统中的一个核心组件,充当客户端与后端服务之间的“中间层”。它统一处理所有外部请求,有点类似于设计模式中的Facade模式(门面模式),负责路由、协议转换、安全控制、监控等功能,类似于系统的“守门人”和“交通指挥中心”。
网关核心功能:
权限控制:作为微服务的入口,对用户进行权限校验,校验失败则拦截。
动态路由:请求先经网关,网关不处理业务,按规则转发到微服务。
负载均衡:当目标服务有多个时,进行负载均衡操作。
限流:流量过高时,按配置放行流量,避免服务压力过大 。
快速上手
创建网关项目
引入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
添加Gateway的路由配置
server:
port: 10030
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: url:8848
gateway:
routes:
- id: order-service #路由规则id,随便起,不重复即可
uri: lb://order-service/
predicates:
- Path=/order/**,/feign/**
- id: product-service
uri: lb://product-service/
predicates:
- Path=/product/**
即可通过网关进行访问
Route Predicate Factories
Predicate
Predicate
是 Java 8 引入的函数式接口,位于 java.util.function
包中。它表示一个条件判断函数,接收一个输入参数,返回布尔值(true
/false
)。常用于集合过滤、数据校验和条件组合等场景。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
//...
}
核心方法
方法 | 功能说明 |
---|---|
boolean test(T t) | 对输入参数 t 进行条件判断,返回布尔值。 |
Predicate<T> and(Predicate) | 逻辑 与 操作,组合当前谓词和另一个谓词。 |
Predicate<T> or(Predicate) | 逻辑 或 操作,组合当前谓词和另一个谓词。 |
Predicate<T> negate() | 逻辑 非 操作,返回当前谓词的反义条件。 |
static <T> Predicate<T> isEqual(Object) | 静态方法,生成一个判断对象是否相等的谓词。 |
代码演示
public class PredicateTest {
@Test
public void test() {
//Predicate<String> predicate = new StringPredicate();
//匿名内部类
// Predicate<String> predicate = new Predicate<String>() {
//
// @Override
// public boolean test(String s) {
// return s==null || s.isEmpty();
// }
// };
Predicate<String> predicate = s -> s==null || s.isEmpty();
System.out.println(predicate.test(""));
System.out.println(predicate.test("a"));
}
/**
* negate 非
*/
@Test
public void test2() {
Predicate<String> predicate = s -> s==null || s.isEmpty();
System.out.println(predicate.negate().test(""));
System.out.println(predicate.negate().test("a"));
}
/**
* or
*/
@Test
public void test3() {
Predicate<String> predicate = s -> "a".equals(s);
Predicate<String> predicate2 = s -> "b".equals(s);
System.out.println(predicate.or(predicate2).test("a"));
System.out.println(predicate.or(predicate2).test("b"));
System.out.println(predicate.or(predicate2).test("c"));
}
/**
* and
*/
@Test
public void test4() {
Predicate<String> predicate = s -> s!=null && !s.isEmpty();
Predicate<String> predicate2 = s -> s!=null && s.chars().allMatch(Character::isDigit);//判断每一个字符是否都是数字类型
System.out.println(predicate.and(predicate2).test("aa"));
System.out.println(predicate.and(predicate2).test("123"));
}
}
Route Predicate Factories
Route Predicate Factories :: Spring Cloud Gateway
在 Spring Cloud Gateway 中,Route Predicate Factories 是用于定义路由规则的核心组件。它们通过条件匹配(即“断言”)决定哪些请求应该被路由到特定的下游服务。每个断言工厂生成一个 Predicate<ServerWebExchange>
,用于判断当前请求是否符合路由规则。
名称 | 说明 | 示例 |
---|---|---|
After | 这个工厂需要一个日期时间 (Java 的 ZonedDateTime) 对象,匹配指定日期之后的请求 |
|
Before | 匹配指定日期之前的请求 |
|
Between | 匹配两个指定时间之间的请求,datetime2 的参数必须在 datetime1 之后 |
|
Cookie | 请求中包含指定 Cookie,且该 Cookie 值符合指定的正则表达式 |
|
Header | 请求中包含指定 Header,且该 Header 值符合指定的正则表达式 |
|
Host | 请求必须是访问某个 host (根据请求中的 Host 字段进行匹配) |
|
Method | 匹配指定的请求方式 |
|
Path | 匹配指定规则的路径 |
|
Remote Addr | 请求者的 IP 必须为指定范围 |
|
Gateway Filter Factories
GatewayFilter Factories :: Spring Cloud Gateway
在 Spring Cloud Gateway 中,Gateway Filter Factories 是用于处理请求和响应的核心组件。它们可以对请求进行修改(如添加请求头、重写路径)或对响应进行增强(如添加响应头、记录日志),从而实现流量控制、安全防护、数据转换等功能。过滤器分为两类:
-
Gateway Filter:作用于特定路由的过滤器。
-
Global Filter:全局过滤器,对所有路由生效。
Gateway Filter
快速上手
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: url:8848
gateway:
routes:
- id: order-service #路由规则id,随便起,不重复即可
uri: lb://order-service/
predicates:
- Path=/order/**,/feign/**
- After=2025-03-20T17:45:02.565907200+08:00[Asia/Shanghai]
filters:
- AddRequestParameter=userName,test
此时发送请求会自动加上参数
@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o1")
public String o1(Integer id,String userName){
log.info("接收到过滤器添加的参数,userName:{}",userName);
return productApi.p1(id);
}
}
以下是部分常见过滤器的说明
名称 | 说明 | 示例 |
---|---|---|
AddRequestHeader | 为当前请求添加 Header | - AddRequestHeader=X-Request-red, blue 参数: Header 的名称及值 |
AddRequestParameter | 为当前请求添加请求参数 | - AddRequestParameter=red, blue 参数:参数的名称及值 |
AddResponseHeader | 为响应结果添加 Header | - AddResponseHeader=X-Response-Red, Blue 参数: Header 的名称及值 |
RemoveRequestHeader | 从当前请求删除某个 Header | - RemoveRequestHeader=X-Request-Foo 参数: Header 的名称 |
RemoveResponseHeader | 从响应结果删除某个 Header | - RemoveResponseHeader=X-Response-Foo 参数: Header 的名称 |
RequestRateLimiter | 为当前网关的所有请求执行限流过滤,如果被限流,默认响应 HTTP 429-Too ManyRequests。默认提供了 RedisRateLimiter 的限流实现,采用令牌桶算法实现限流功能。此处不做具体介绍 |
|
Retry | 针对不同的响应进行重试。当后端服务不可用时,网关会根据配置参数来发起重试请求 |
|
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large |
|
默认过滤器 | 添加一个 filter 并将其应用于所有路由,这个属性需要一个 filter 的列表,详细参考后续内容 | - |
Default Filters
举例
server:
port: 10030
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: url:8848
gateway:
routes:
- id: order-service #路由规则id,随便起,不重复即可
uri: lb://order-service/
predicates:
- Path=/order/**,/feign/**
- After=2025-03-20T17:45:02.565907200+08:00[Asia/Shanghai]
filters:
- AddRequestParameter=userName,test
- id: product-service
uri: lb://product-service/
predicates:
- Path=/product/**
default-filters: #对全部生效
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
此处配置后,如若接受到BAD_GATEWAY(502)将会重试3次
@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o1")
public String o1(Integer id, String userName, HttpServletResponse response) {
log.info("接收到过滤器添加的参数,userName:{}",userName);
response.setStatus(502);
return productApi.p1(id);
}
}
Global Filter
在 Spring Cloud Gateway 中,Global Filter 是一种作用于所有路由的全局过滤器,用于统一处理请求和响应。与路由级别的过滤器(GatewayFilter
)不同,全局过滤器无需绑定到特定路由,全局过滤器通常用于实现与安全性,性能监控和日志记录等相关的全局功能。
快速上手
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加配置
spring:
cloud:
gateway:
metrics:
enabled: true
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
shutdown:
enabled: true
测试结果
localhost:10030/actuator 显示所有监控的信息链接
过滤器的执行顺序
一个项目中,既有GatewayFilter,又有GlobalFilter时,在请求路由后,网关会把当前项目中的GatewayFilter和GlobalFilter合并到一个过滤器链(集合)中,并进行排序,依次执行过滤器。
每一个过滤器都必须指定一个 int 类型的 order 值,默认值为 0,表示该过滤的优先级. order 值越小,优先级越高,执行顺序越靠前。
- Filter 通过实现 Order 接口或者添加 @Order 注解来指定 order 值。
- Spring Cloud Gateway 提供的 Filter 由 Spring 指定。用户也可以自定义 Filter,由用户指定。
- 当过滤器的 order 值一样时,会按照 defaultFilter > GatewayFilter > GlobalFilter 的顺序执行。
自定义过滤器
自定义GatewayFilter
自定义GatewayFilter,需要去实现对应的接口GatewayFilterFactory,Spring Boot默认帮我们实现的抽象类是AbstractGatewayFilterFactory,可以直接使用
代码示例
@Data
public class CustomConfig {
private String name;
}
@Slf4j
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {
public CustomGatewayFilterFactory() {
super(CustomConfig.class);
}
/*
过滤器的核心方法
*/
@Override
public GatewayFilter apply(CustomConfig config) {
return new GatewayFilter() {
/**
* exchange ServerWebExchange: HTTP请求-响应交互契约,提供了对HTTP请求和响应的访问
* chain GatewayFilterChain:过滤器链
* Mono 是Reactor中的核心类,数据流的发布者,Mono最多只触发一个事件,可以把Mono用在异步完成任务时,发出通知
* chain.filter(exchange); 执行请求
* Mono.fromRunnable() 创建一个包括Runnable元素的数据流
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//Pre类型 执行请求 Post类型
//pre类型过滤器的代码逻辑
log.info("Pre Filter , config:{}", config);
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("Post Filter ... ");//post类型过滤器的代码逻辑
}));
}
};
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
进行配置
这几个类之间的匹配关系是
测试结果
自定义GlobalFilter
GlobalFilter的实现相对比较简单,不需要额外的配置,只需要实现GlobalFilter接口,自动会过滤所有的filter
代码示例
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("pre Global Filter...");
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("post Global Filter...");
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
无需额外配置,即可测试代码
今天应做的事没有做,明天再早也是耽误了。——裴斯泰洛齐
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸