为什么需要网关
为什么选择Spring cloud Gateway
常见的网关有gateway和zuul,和zuul相比,Spring Cloud Gateway具有如下优势:
- 基于Reactor模型的WebFlux构建,运行在Netty上,具有更好的性能。
- 可拓展性高,内置了非常丰富的断言及过滤器等,除此之外,我们也可以定义自己的断言和过滤器。
搭建gateway
pom依赖配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>gateway</description>
<properties>
<spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.1.RELEASE</spring-cloud-alibaba.version>
</properties>
<!-- 只声明依赖,不引入依赖 -->
<dependencyManagement>
<dependencies>
<!-- 声明springCloud版本 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 声明 springCloud Alibaba 版本 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 8080
servlet:
context-path: /${spring.application.name}
spring:
application:
name: gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8001
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: route1
# 请求最终被转发到的目标地址
#uri: http://localhost:8081
uri: lb://provider1
# 设置断言
predicates:
# Path Route Predicate Factory 断言,满足 /gateway/provider1/** 路径的请求都会被路由到 http://localhost:8081 这个uri中
- Path=/gateway/provider1/**
# 配置过滤器(局部)
filters:
# StripPrefix:去除原始请求路径中的前2级路径,即/gateway/provider1
- StripPrefix=2
- AddRequestHeader=X-Request-red, blue
- My
- id: route2
#uri: http://localhost:8082
uri: lb://provider2
# 设置断言
predicates:
- Path=/gateway/provider2/**
# 配置过滤器(局部)
filters:
# StripPrefix:去除原始请求路径中的前2级路径,即/gateway/provider1
- StripPrefix=2
以上的路由配置也可以基于bean来配置:
@Bean
public RouteLocator addLocator(RouteLocatorBuilder routeLocatorBuilder){
return routeLocatorBuilder.routes()
.route("gateway-client", r -> r.path("/gclient/**")
.filters(f->f.addRequestParameter("X-Request-Token","2020ABC"))
.uri("lb://gateway-client")).build();
}
核心名词解释
- 路由(Route):gateway的基本构建模块。它由ID、目标URI、断言和过滤器等组成。
- 断言(Predicate ):也翻译成谓词,用于定义转发规则。
- 过滤器(Filter):可以在返回请求之前或之后修改请求和响应的内容。
Predicate (断言) (断言-官网参考文档)
gateway内置了很多断言,篇幅限制,列出两种,源码位于:org.springframework.cloud.gateway.handler.predicate下:
AfterRoutePredicateFactory
spring: cloud: gateway: routes: - id: after_route uri: http://mantou.plus #如果请求时间在配置的时间点之后,全部转发到http://mantou.plus predicates: - After=2023-03-07T22:42:47.789-07:00[America/Denver]
BeforeRoutePredicateFactory
spring: cloud: gateway: routes: - id: after_route uri: http://www.mantou.plus #如果请求时间在配置的时间点之前,全部转发到http://mantou.plus predicates: - Before=2023-03-07T22:42:47.789-07:00[America/Denver]
过滤器 (过滤器-官网参考文档)
gateway也内置了很多过滤器,,篇幅限制,列出两种,源码位于:org.springframework.cloud.gateway.filter.factor下:
- AddRequestHeader
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://mantou.plus #在请求发到目标微服务之前添加请求头X-Request-red,其值为:blue
filters:
- AddRequestHeader=X-Request-red, blue
- StripPrefix
spring:
cloud:
gateway:
routes:
- id: strip_prefix_route
uri: https://mantou.plus #在请求发到目标微服务之前去掉1级前缀,例如 http://localhost:8080/name/weixiaojie,就会转发到:https://mantou.plus/weixiaojie
predicates:
- Path=/name/**
filters:
- StripPrefix=1
自定义过滤器:
@Slf4j
@Component
public class MyGatewayFilterFactory extends AbstractGatewayFilterFactory {
@Override
public GatewayFilter apply(Object config) {
return ((exchange, chain) -> {
log.info("自定义过滤器添加请求头:blog,添加响应头: Title");
ServerHttpRequest request = exchange.getRequest().mutate().header("blog", "https://mantou.plus").build();
ServerWebExchange ex = exchange.mutate().request(request).build();
ex.getResponse().getHeaders().add("Title","BlackHorse");
return chain.filter(ex);
});
}
}
GlobalFilter,无需配置,全局生效:
@Slf4j
@Component
@Order(1)
public class TokenGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("进入token过滤器");
String token = exchange.getRequest().getHeaders().getFirst("token");
if (token != null && !"".equals(token)) {
log.info("找到了token:{}", token);
return chain.filter(exchange);
} else {
log.error("没有找到token,未授权,不允许访问");
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
}
}
@Slf4j
@Component
@Order(2)
public class LogGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("进入日志过滤器");
//filter的前置处理
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().pathWithinApplication().value();
InetSocketAddress remoteAddress = request.getRemoteAddress();
return chain
//继续调用filter
.filter(exchange)
//filter的后置处理
.then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
HttpStatus statusCode = response.getStatusCode();
log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
}));
}
}
集成注册中心
微服务情况下,每个服务有多个实例,为了防止IP变化等引起的调用问题,就需要引入服务注册和服务发现
pom文件添加依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
启动类添加:@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
每个服务的application.yml加入以下内容,表示把该服务注册到nacos注册中心中去:
spring:
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 127.0.0.1:8001
修改routes配置,可以使用服务名称来调用了:其中lb表示负载均衡,
#uri: http://localhost:8081
uri: lb://provider_1
自定义全局异常处理
当遇到异常的时候,可以做一些自定义处理,比如使用返回json格式结果,更易于前段解析
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// JOSN格式返回
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
CommonResponse resultMsg = new CommonResponse("500", ex.getMessage(), null);
return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
} catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class CommonResponse {
String code;
String message;
String data;
}
}
实现动态路由
从前面我们可以看到路由的配置主要通过application.yml和代码@Bean配置,无论是哪一种,在启动网关后将无法修改路由配置,如有新服务要上线,则需要先把网关下线,修改 yml 配置后,再重启网关。核心类为:RouteDefinitionWriter,位于org.springframework.cloud.gateway.route包下 。此处需要阅读源码,方式较多,我们放到下一节介绍。