持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
Zuul是Netflix提供的一个开源组件,提供动态路由、监控、弹性、安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分。Zuul包含了对请求的路由和过滤两个最主要的功能,外加代理功能。路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。
动态路由需要达到可持久化配置,动态刷新的效果。不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。下面将进行动态路由的实战。
zuul实现动态路由
环境说明
spring boot 2.2.11\spring cloud Hoxton.SR3
数据库配置
创建数据库的路由配置表,该表用来保存微服务的路由信息,表结构是根据zuul的ZuulRoute类进行定义的,zuul启动的时候除了从配置文件读取路由信息外,也从DB中读取路由路由信息,生成router对象。
CREATE TABLE `gateway_api_route` (
`id` varchar(50) NOT NULL,
`path` varchar(255) NOT NULL,
`service_id` varchar(50) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
`retryable` tinyint(1) DEFAULT NULL,
`enabled` tinyint(1) NOT NULL,
`strip_prefix` int(11) DEFAULT NULL,
`api_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
INSERT INTO gateway_api_route (id, path, service_id, retryable, strip_prefix, url, enabled) VALUES ('order-service', '/order/**', 'order-service',0,1, NULL, 1);
INSERT INTO gateway_api_route (id, path, service_id, retryable, strip_prefix, url, enabled) VALUES ('vipProducer', '/vipProducer/**', 'vip-producer',0,1, NULL, 1);
INSERT INTO gateway_api_route (id, path, service_id, retryable, strip_prefix, url, enabled) VALUES ('vipConsumer', '/vipConsumer/**', 'vip-consumer',0,1, NULL, 1);
# MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
# where enabled = true 或者写成 enabled = 1都能查询到
select * from gateway_api_route where enabled = true;
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
<version>16.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置数据库连接
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.56.101:3306/vip?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
创建数据库实体类
package com.xinxin.vip.sso.model;
import java.io.Serializable;
/**
* @ClassName GatewayApiRouteDO
* @Description
* 网关API动态路由配置表
* 字段一定不要修改,参照ZuulRoute进行编写,动态路由的时候通过BeanUtils.copyProperties()方法进行复制即可
* org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute
* @Author lantianbaiyun
* @Date 2021/4/6
* @Version 1.0
*/
public class GatewayApiRouteDO implements Serializable {
private String id;
private String path;
private String serviceId;
private String url;
private boolean stripPrefix = true;
private Boolean retryable;
// MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
// where enabled = true 或者写成 enabled = 1都能查询到
private Boolean enabled;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isStripPrefix() {
return stripPrefix;
}
public void setStripPrefix(boolean stripPrefix) {
this.stripPrefix = stripPrefix;
}
public Boolean getRetryable() {
return retryable;
}
public void setRetryable(Boolean retryable) {
this.retryable = retryable;
}
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
修改配置文件
删除配置文件application.yml中的配置,也可以保留,保留后也可以在数据库配置,但是数据库配置就不生效了,但是不会因为配置文件和数据库都有而重复。
#zuul:
# routes:
# vipProducer:
# path: /vipProducer/**
# serviceId: vip-producer
# vipConsumer:
# path: /vipConsumer/**
# serviceId: vip-consumer
自定义RouteLocator类
加载配置文件和数据,生成ZuulRoute对象。
package com.xinxin.vip.sso.locator;
import com.xinxin.vip.sso.model.GatewayApiRouteDO;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @ClassName DynamicRouteLocator
* @Description 自定义路由管理器
* @Author lantianbaiyun
* @Date 2021/4/6
* @Version 1.0
*/
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
private JdbcTemplate jdbcTemplate;
private ZuulProperties properties;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public DynamicRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
this.properties = properties;
}
@Override
public void refresh() {
doRefresh();
}
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
// 加载配置文件application.yml中的路由表
routesMap.putAll(super.locateRoutes());
// 加载db中的路由表,也可以配置在redis\zk等任意可以读取的地方
routesMap.putAll(locateRoutesFromDB());
// 统一处理一下路由path格式
LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.hasText(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
/**
* 从数据库中读取配置的路由表
*
* @return
*/
private Map<String, ? extends ZuulProperties.ZuulRoute> locateRoutesFromDB() {
Map<String, ZuulProperties.ZuulRoute> routes = new HashMap<>();
List<GatewayApiRouteDO> results = jdbcTemplate.query(
// MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
// where enabled = true 或者写成 enabled = 1都能查询到
"select * from gateway_api_route where enabled = true",
new BeanPropertyRowMapper<>(GatewayApiRouteDO.class));
for (GatewayApiRouteDO result : results) {
if (StringUtils.isEmpty(result.getPath())) {
continue;
}
if (StringUtils.isEmpty(result.getServiceId())
&& StringUtils.isEmpty(result.getUrl())) {
continue;
}
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
BeanUtils.copyProperties(result, zuulRoute);
routes.put(zuulRoute.getPath(), zuulRoute);
}
return routes;
}
}
RouteLocator配置类
将自定义的RouteLocator类DynamicRouteLocator加入到Spring容器中。
package com.xinxin.vip.sso.config;
import com.xinxin.vip.sso.locator.DynamicRouteLocator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class DynamicRouteConfiguration {
@Autowired
private ZuulProperties zuulProperties;
@Autowired
private ServerProperties server;
@Autowired
private JdbcTemplate jdbcTemplate;
@Bean
public DynamicRouteLocator routeLocator() {
// spring boot 1.5.x spring cloud Edgware.SR3写法
// DynamicRouteLocator routeLocator = new DynamicRouteLocator(
// this.server.getServletPrefix(), this.zuulProperties);
// spring boot 2.2.x spring cloud Hoxton.SR3写法
DynamicRouteLocator routeLocator = new DynamicRouteLocator(
this.server.getServlet().getContextPath(), this.zuulProperties);
routeLocator.setJdbcTemplate(jdbcTemplate);
return routeLocator;
}
}
创建定时器
使用Spring的定时任务,定时刷新数据库的路由配置。
package com.xinxin.vip.sso.task;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @ClassName RefreshRouteTask
* @Description 定时从数据库刷新最新的路由信息
* @Author lantianbaiyun
* @Date 2021/4/6
* @Version 1.0
*/
@Component
@Configuration
@EnableScheduling
public class RefreshRouteTask {
private static final Logger LOGGER = LoggerFactory.getLogger(RefreshRouteTask.class);
@Autowired
private ApplicationEventPublisher publisher;
@Autowired
private RouteLocator routeLocator;
@Scheduled(fixedRate = 5000)
private void refreshRoute() {
LOGGER.debug("定时刷新路由表==>{}", routeLocator.getRoutes());
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routeLocator);
}
}