一、网关作用
1. 统一服务的对外入口,鉴权,跨域,日志;
2. 统一过滤与拦截;
3. 动态路由;
4. 限流熔断。
二、网关原理
- 网关的核心概念
- Route:网关的基础元素,由 ID、目标 URI、断言、过滤器组成。
- Predicate:匹配条件。
- Filter:过滤器
网关请求链路
当网关接受到请求后,会在Gateway Handler Mapping中找到与请求相匹配的路径,将请求转发到Gateway Web Handler中,Handler在通过路由指定的Filter过滤器,判断过滤器的类型,如果是pre过滤器,则执行完过滤器在执行代理请求,如果是post过滤器,则先执行代理请求在执行过滤器。
三、网关接入
pom.xml加入依赖
<!-- spring cloud gateway 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- spring cloud alibaba sentinel 网关整合-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
这里有个要注意的点是gateway是依赖spring-boot-starter-webflux实现的,与spring-boot-starter-web会有冲突。所以在网关项目中不要引用spring-boot-starter-web,或者排除它。
全部pom配置:
<dependencies>
<dependency>
<groupId>cn.wisefly</groupId>
<artifactId>business-wcenter-sdk</artifactId>
</dependency>
<!--引入配置中心阿里巴巴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--引入注册中心阿里巴巴-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel 熔断-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- sentinel nacos 数据源-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!-- spring cloud gateway 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- spring cloud alibaba sentinel 网关整合-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- spring boot actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot 监控客户端-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
</dependency>
<!--验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
</dependency>
<!-- Swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.fox.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.fox.version}</version>
</dependency>
<!-- SpringCloud Zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<!--kafka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
<!-- kafka client-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<!-- 指定项目编译时的java版本和编码方式 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<!--新增的docker maven插件-->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>${docker-maven-plugin.version}</version>
<!--docker镜像相关的配置信息-->
<configuration>
<!--镜像名,要带上私有服务器IP和端口-->
<imageName>${docker-registryUrl}/${project.artifactId}:${project.version}</imageName>
<!--TAG,这里用工程版本号-->
<imageTags>
<imageTag>${project.version}</imageTag>
<imageTag>latest</imageTag>
</imageTags>
<!--Dockerfile文件地址-->
<dockerDirectory>${project.basedir}</dockerDirectory>
<!--构建镜像的配置信息-->
<resources>
<resource>
<targetPath>/</targetPath>
<!--指定复制jar包的根目录-->
<directory>${project.build.directory}</directory>
<!--指定复制的文件-->
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<!--指定推送的仓库-->
<registryUrl>${docker-registryUrl}</registryUrl>
<!-- 开启远程API -->
<dockerHost>${docker-dockerHost}</dockerHost>
<!-- 是否有push功能 -->
<pushImage>true</pushImage>
<!--push后是否覆盖存在的标签镜像-->
<forceTags>true</forceTags>
</configuration>
</plugin>
</plugins>
</build>
配置文件加入
Spring:
cloud:
# 路由网关配置
gateway:
# 设置与服务注册发现组件结合,这样可以采用服务名的路由策略
discovery:
locator:
enabled: true
# 配置之后访问时无需大写
lower-case-service-id: true
#路由配置
routes:
- id: file-route
uri: lb://wisefly-file
predicates:
- Path=/file/v1.0/**
default-filters:
- StripPrefix=0
nacos上的全部配置:
展开查看
spring:
cloud:
# 使用 Naoos 作为服务注册发现
nacos:
discovery:
server-addr: 10.0.1.104:8848
namespace: 09dd3d79-92c0-47fe-bd1d-0f1b7f8dc928
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
sentinel:
transport:
port: 8730
dashboard: 10.0.1.104:8080
# 服务启动直接建立心跳连接
eager: true
# Sentinel Nacos 数据源
datasource:
ds:
nacos:
server-addr: 10.0.1.104:8848
groupId: DEFAULT_GROUP
dataId: gateway-sentinel
namespace: 09dd3d79-92c0-47fe-bd1d-0f1b7f8dc928
rule-type: flow
# 路由网关配置
gateway:
# 设置与服务注册发现组件结合,这样可以采用服务名的路由策略
discovery:
locator:
enabled: true
# 配置之后访问时无需大写
lower-case-service-id: true
routes:
- id: file-route
uri: lb://wisefly-file
predicates:
- Path=/file/v1.0/**
- id: workflow-route
uri: lb://wisefly-workflow
predicates:
- Path=/workflow/v1.0/**
- id: wcenter-route
uri: lb://wisefly-wcenter
predicates:
- Path=/wcenter/v1.0/**
- id: resource-route
uri: lb://wisefly-resource
predicates:
- Path=/resource/v1.0/**
- id: inspection-route
uri: lb://wisefly-inspection
predicates:
- Path=/inspection/v1.0/**
- id: criterion-route
uri: lb://wisefly-criterion
predicates:
- Path=/criterion/v1.0/**
- id: hbase-route
uri: lb://wisefly-hbase
predicates:
- Path=/hbase/v1.0/**
- id: turnhospital-route
uri: lb://wisefly-turnhospital
predicates:
- Path=/turnhospital/v1.0/**
- id: cms-route
uri: lb://business-cms
predicates:
- Path=/cms/v1.0/**
default-filters:
- StripPrefix=0
#自定义过滤的地址
filter:
url:
whites:
- /wcenter/v1.0/authentication/login
- /v2/api-docs
# redis 配置
redis:
# 地址
host: 10.0.1.104
# 端口,默认为6379
port: 6379
# 密码
password: 123456
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
四、动态网关实现
framework2.0动态路由是基础nacos配置中心实现动态路由配置以及存储。具体实现在cn.wisefly.gateway.route包下面,主要有三个要点,大概说下:
动态路由nacos配置类
package cn.wisefly.gateway.route.config;
import cn.wisefly.gateway.handler.SentinelFallbackHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
@Configuration
public class GatewayConfig {
public static final long DEFAULT_TIMEOUT = 30000;
@Value("${spring.cloud.nacos.config.server-addr}")
private String address;
@Value("${spring.cloud.nacos.config.namespace}")
private String namespace;
@Value("${nacos.gateway.route.config.data-id}")
private String dataId;
@Value("${nacos.gateway.route.config.group}")
private String groupId;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
return new SentinelFallbackHandler();
}
@Bean
@Order(-1)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getDataId() {
return dataId;
}
public void setDataId(String dataId) {
this.dataId = dataId;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
}
初始化路由,监听nacos配置变化
监听是利用了nacos-api提供的ConfigService.addListener来实现的。
package cn.wisefly.gateway.route.nacos;
import cn.wisefly.gateway.route.config.GatewayConfig;
import cn.wisefly.gateway.route.service.DynamicRouteServiceImpl;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import javax.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class DynamicRouteServiceImplByNacos {
//定义全局静态MAP
private static Map<String, List<RouteDefinition>> map = new ConcurrentHashMap<>();
@Autowired
private DynamicRouteServiceImpl dynamicRouteService;
@Autowired
private GatewayConfig gatewayConfig;
private ConfigService configService;
@PostConstruct
public void init() {
log.info("gateway route init...");
try {
configService = initConfigService();
if (configService == null) {
log.warn("initConfigService fail");
return;
}
String configInfo = configService.getConfig(gatewayConfig.getDataId(), gatewayConfig.getGroupId(), GatewayConfig.DEFAULT_TIMEOUT);
log.info("获取网关当前配置:\r\n{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition definition : definitionList) {
log.info("update route : {}", definition.toString());
dynamicRouteService.add(definition);
}
//放入全局MAP中
map.put(gatewayConfig.getDataId(), definitionList);
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
}
dynamicRouteByNacosListener(gatewayConfig.getDataId(), gatewayConfig.getGroupId());
}
/**
* 监听Nacos下发的动态路由配置
*
* @param dataId
* @param group
*/
public void dynamicRouteByNacosListener(String dataId, String group) {
try {
configService.addListener(dataId, group, new Listener() {
@Override
public void receiveConfigInfo(String configInfo) {
log.info("进行网关更新:\n\r{}", configInfo);
List<RouteDefinition> definitionList = JSON.parseArray(configInfo, RouteDefinition.class);
for (RouteDefinition definition : definitionList) {
log.info("update route : {}", definition.toString());
dynamicRouteService.update(definition);
}
//更具dataId获取map里面存在的route
List<RouteDefinition> staticRouteMap = map.get(dataId);
if (staticRouteMap != null) {
//比较与监听到的路由配置是否一致(只比较ID)
for (RouteDefinition mapRoute : staticRouteMap) {
Boolean bool = false;
for (RouteDefinition definition : definitionList) {
//如果MAP的数据在动态获取的数据中不存在,则表示此次动态删除了
if (mapRoute.getId().equals(definition.getId())) {
bool = true;
break;
}
//如果为false则主动触发删除操作
if (!bool) {
dynamicRouteService.delete(mapRoute.getId());
}
}
}
}
//将最新的动态数据放入map
map.put(dataId, definitionList);
}
@Override
public Executor getExecutor() {
log.info("getExecutor\n\r");
return null;
}
});
} catch (NacosException e) {
log.error("从nacos接收动态路由配置出错!!!", e);
}
}
/**
* 初始化网关路由 nacos config
*
* @return
*/
private ConfigService initConfigService() {
try {
Properties properties = new Properties();
properties.setProperty("serverAddr", gatewayConfig.getAddress());
properties.setProperty("namespace", gatewayConfig.getNamespace());
return configService = NacosFactory.createConfigService(properties);
} catch (Exception e) {
log.error("初始化网关路由时发生错误", e);
return null;
}
}
}
动态更新路由网关
这里的核心是实现Spring提供的事件推送接口ApplicationEventPublisherAware。
package cn.wisefly.gateway.route.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
@Slf4j
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
@Autowired
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
/**
* 增加路由
*
* @param definition
* @return
*/
public String add(RouteDefinition definition) {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
}
/**
* 更新路由
*
* @param definition
* @return
*/
public String update(RouteDefinition definition) {
try {
this.routeDefinitionWriter.delete(Mono.just(definition.getId()));
} catch (Exception e) {
return "update fail,not find route routeId: " + definition.getId();
}
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";
} catch (Exception e) {
return "update route fail";
}
}
/**
* 删除路由
*
* @param id
* @return
*/
public String delete(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
return "delete success";
} catch (Exception e) {
e.printStackTrace();
return "delete fail";
}
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
}
五、网关路由配置
网关模块搭建完成后,改动最频繁的就是路由配置了,目前我们的路由配置在配置中心的wisefly-gateway-dev.yaml文件中。配置规则如下:
#路由配置
routes:
- id: file-route #路由ID,不能重复
uri: lb://wisefly-file #请求转发的路径,一般配置成服务名称即可。
predicates: #断言
- Path=/file/v1.0/** #拦截的请求方法的路径,一般配置与context-path相同,如果不相同的话swagger无法接入网关中。
以上路由规则配置完成后,访问file模块的请求路径只需要为http://网关ip:网关port/file/v1.0/**
就能通过网关转发到业务请求(当然不做以上配置的话也是能通过http://网关ip:网关port/服务名称/context-path/**
通过网关转发业务请求)。