六. Feign远程调用
6.1 替代RestTemplate
RestTemplate调用问题:代码可读性差,参数复杂且URL难维护。
Feign是一个声明式的HTTP客户端,官方地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier
它可以解决上述提到的问题。
STEP1:首先,在orderservice中引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
STEP2:在启动类添加@EnableFeignClients注解
STEP3:创建一个调用接口
// clients/UserClient
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
STEP4:编写业务代码
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId){
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.利用RestTemplate发起HTTP请求,查询用户
User user = userClient.findById(order.getUserId());
// 3.封装user到order
order.setUser(user);
// 4.返回
return order;
}
}
对比一下之前的代码,使用Feign方式更简洁,编程体验也更统一。
6.2 自定义配置
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析JSON字符串为Java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
方式一:配置文件方式
-
全局
feign: client: config: default: loggerLevel: FULL
-
针对服务
feign: client: config: userservice: # 针对某个微服务的配置 loggerLevel: FULL # 日志级别
方式二:代码方式
先定义一个Bean对象:
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
全局生效,需要在启动类注解中添加:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
局部生效,需要找到对应的Client,例如:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
6.3 性能优化
Feign底层发起HTTP请求,依赖于其它的框架,底层客户端实现包括:
URLConnection:默认实现,不支持连接池
Apache HttpClient :支持连接池
OKHttp:支持连接池
因此,要提高Feign的性能,最重要的一点就是使用连接池。同时,建议使用BASIC日志级别(太多日志影响性能)。
使用 Apache HttpClient
1)引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2)配置连接池
feign:
httpclient:
enabled: true
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 每个路径最大连接数
除了上面的参数,还有很多参数可以配置,例如存活时间等。
6.4 最佳实践
先来看看原来的代码。
Feign客户端:
@FeignClient("userservice")
public interface UserClient{
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
被调用的服务Controller:
@GetMapping("/user/{id}")
public User queryById(@PathVariable("id") Long id){
return userservice.queryById(id);
}
有没有什么方法简化代码呢?答案就是:继承或抽取(根据需求选择,没有十全十美的方案)。
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
优点:可以规范接口,实现面向契约编程和代码共享。
缺点:会造成服务提供者和消费者的代码紧耦合,且参数列表的注解不会被继承,Controller需要再次声明方法、参数列表和注解。
方式二(抽取):将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。
优点:不用重复编写UserClient,降低代码量。
缺点:假设只需要用到userservice的部分接口,但是使用这种方式会将接口全部引入,造成冗余。
七. 网关
7.1 概述
不是所有人都可以调用微服务,我们需要网关(Gateway)作为微服务的统一入口,它的功能包括身份认证、权限校验、服务路由、负载均衡和请求限流等。
在SpringCloud中,网关的实现有两种:SpringCloudGateway和Zuul。
Zuul是基于Servlet的实现,属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
7.2 搭建
STEP1:创建一个新的module,引入依赖:
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
STEP2:编写启动类:
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
STEP3:配置网关举例:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: 172.20.0.2:80 # nacos 服务地址
discovery:
namespace: 5b812fef-b156-4783-be29-c6ff749e38cd # dev
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
访问 localhost:10010/user/1
即可查询id为1的用户信息,访问 localhost:10010/order/101
即可查询id为101的订单信息,实现了路由,整个流程如下图所示。
7.3 断言工厂
在配置文件中写的断言规则是字符串,会被Predicate Factory读取和处理。除了Path匹配路径,还有很多个参数可以配置:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack 或者 - Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
7.4 路由过滤器和默认过滤器
GatewayFilter
是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
种类:Spring Cloud Gateway
以 AddRequestHeader 为例
目的:给所有进入userservice的请求加一个请求头。
在application.yml中配置如下:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, 515code is freaking awesome! # 添加请求头
然后去userservice的controller中打印一下:
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id, @RequestHeader(value = "Truth", required = false) String truth) {
System.out.printf("truth: " + truth); // 打印Header
return userService.queryById(id);
}
默认过滤器(全局,配置实现)
配置default-filters
字段即可,注意与routes
同级。
7.5 全局过滤器(代码实现)
与上面提到的GatewayFilter作用一样,区别在于GatewayFilter使用配置文件来定义,处理逻辑固定。
而GlobalFilter
的逻辑需要代码实现,方式是实现GlobalFilter
接口。
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
自定义:以实现Authorization过滤器为例,检查该字段是否为admin,是则通过,不是则过滤
// @Order(-1) // 优先级,默认2147483647,越小优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
ServerHttpRequest request = exchange.getRequest();
MultiValueMap<String, String> params = request.getQueryParams();
// 2.获取参数中的Authorization字段
String auth = params.getFirst("Authorization");
// 3.是否等于admin
if ("admin".equals(auth)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 拦截
}
@Override
public int getOrder() {
return -1; // 和注解一样效果
}
}
这时候访问http://localhost:10010/order/101
,页面会报401错误。访问http://localhost:10010/order/101?Authorization=admin
则正常。
实际开发中,业务逻辑肯定会比上面的例子更复杂,例如读取Cookie/Session等。
7.6 过滤器执行顺序
之前已经学过,进入网关后会有三类过滤器:路由过滤器、默认过滤器(DefaultFilter)和全局过滤器(GlobalFilter)。
请求路由后,会将当前路由过滤器和默认过滤器、全局过滤器合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
路由 <—> 默认过滤器
<—> 路由过滤器
<—> 全局过滤器
<—> 微服务
排序的规则:
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
- 当过滤器的order值一样时,会按照 默认过滤器 > 路由过滤器 > 全局过滤器 的顺序执行。
7.7 跨域问题
编写一个跨域请求来测试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
这是跨域请求测试,请查看控制台。
</body>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script>
axios.get("http://localhost:10010/user/1?Authorization=admin")
.then(resp => console.log(resp.data))
.catch(err => console.log(err))
</script>
</html>
进入控制台,可以查看到错误信息:
index.html:1 Access to XMLHttpRequest at 'http://localhost:10010/user/1?Authorization=admin' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
采用CORS方案解决,通过配置即可实现:
YML
spring:
cloud:
gateway:
# ......
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://127.0.0.1:5500"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期(秒)