持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情
前言
Zuul是Spring Cloud Netflix开源的应用层网关。Zuul网关的核心是一系列的过滤器,这些过滤器可以对请求或者响应结果做一系列过滤,Zuul提供了一个框架可以支持动态加载,编译,运行这些过滤器,这些过滤器是使用责任链方式顺序对请求或者响应结果进行处理的,这些过滤器不会直接进行通信,但是通过责任链传递的RequestContext参数可以共享一些东西。
在zuul中过滤器分为四种:
-
PRE Filters(前置过滤器) :当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤器,日志过滤器,还有路由选择过滤器 -
ROUTING Filters(路由过滤器):该过滤器作用是把请求具体转发到后端服务器上,一般是通过Apache HttpClient 或者 Netflix Ribbon把请求发送到具体的后端服务器上 -
POST Filters(后置过滤器):当把请求路由到具体后端服务器后执行的过滤器;场景有添加标准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等。 -
ERROR Filters(错误过滤器):当上面任何一个类型过滤器执行出错时候执行该过滤器
zuul的作用服务的灰度发布、服务跨域、服务限流、负载均衡和降级、动态路由。
请求流程如下图:
下面将介绍如何在项目中通过数据库配置实现动态路由。
zuul动态路由
Java是一门面向对象的编程语言,一切皆对象,zuul网关存储的注册中心中的服务实例也是一种对象,这个对象叫rote。rote可以通过数据库+自定义RouteLocator,也可以通过zuul的配置文件+config(apollo\zk)bus配置中心+bus动态刷新配置文件完成。
原理就是通过将zuul的rote对象映射到实际的数据库表当中,在zuul服务中添加定时器,定时读取数据库路由表信息,生成路由对象放在应用内存的路由表中。
动态路由原理如下图:
创建数据库
创建数据库的路由配置表。
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 );
引入依赖
<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
创建数据库实体类
import java.io.Serializable;
/**
* @ClassName GatewayApiRouteDO
* @Description
* 网关API动态路由配置表
*
* 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;
}
```
字段一定不要修改,参照ZuulRoute进行编写,动态路由的时候通过BeanUtils.copyProperties()方法进行复制即可。
修改配置文件
删除配置文件application.yml中的配置,也可以保留,保留后也可以在数据库配置,但是数据库配置就不生效了,但是不会因为配置文件和数据库都有而重复。
#zuul:
# routes:
# vipProducer:
# path: /vipProducer/**
# serviceId: vip-producer
# vipConsumer:
# path: /vipConsumer/**
# serviceId: vip-consumer
自定义RouteLocator类
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配置类
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;
/**
* @ClassName DynamicRouteConfiguration
* @Description 自定义路由 Spring 配置类
* @Author lantianbaiyun
* @Date 2021/4/6
* @Version 1.0
*/
@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;
}
}
创建定时器
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.info("定时刷新路由表==>{}", routeLocator.getRoutes());
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routeLocator);
}
}