在旧服务网关——Zuul原理中,我们介绍了Zuul中的路由配置规则,这种通过配置文件对路由规则进行配置的方式被称之为"静态路由"。我们在微服务系统构建过程中,通过"静态路由"的方式将路由规则和服务实例之间的映射关系进行了配置,在微服务系统迭代的过程中,难免会在服务网关中引入新的服务实例和路由规则。Zuul在启动时将配置文件中的路由规则写入内存,要引入新的映射规则,只能通过修改Zuul配置文件后重新启动Zuul应用的方式,这也是静态路由的弊端。
要解决上述问题,需要通过动态路由的方式实现不停止Zuul网关应用,动态读取路由映射规则并加载,进行路由转发。实现动态路由,有以下两种解决方案:
结合Spring Cloud Config+Bus,动态刷新配置文件。这种方式的好处是不用Zuul维护映射规则,可以随时修改、随时生效;缺点是需要集成不频繁使用的组件,Config没有可视化界面,维护映射规则麻烦。
重写Zuul的配置读取方式,采用事件刷新机制,从数据库读取路由映射规则并修改。基于数据库持久化,可轻松实现管理界面,灵活度高。
本例主要采用第二种方式。
动态路由核心类
在采用事件刷新机制,从数据库中读取路由映射规则的实现方式中,涉及以下核心类。
DiscoveryClientRouteLocator
该类主要用于对路由配置信息读取与新服务实例节点注册变更相关操作。重要方法如下:
package org.springframework.cloud.netflix.zuul.filters.discovery;public class DiscoveryClientRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {public static final String DEFAULT_ROUTE = "/**";//默认路由访问规则字符串 private DiscoveryClient discovery; //discovery负责服务发现,通过@EnabledDiscoveryClient注解启用 private ZuulProperties properties;//封装Zuul配置信息/***@param String:路由访问规则字符串(Path)*@param ZuulRoute:路由实例信息(对路由配置信息进行封装)*/protected LinkedHashMap locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap(); routesMap.putAll(super.locateRoutes());//继承自SimpleRouteLocator类并重写方法 LinkedHashMap values; Iterator var3; String path; if (this.discovery != null) { values = new LinkedHashMap(); var3 = routesMap.values().iterator(); //对路由配置信息进行迭代 while(var3.hasNext()) { ZuulRoute route = (ZuulRoute)var3.next(); path = route.getServiceId();//获取service-id配置信息并赋值给path //未配置service-id使用服务实例ID if (path == null) { path = route.getId(); } //配制了service-id,将service-id作为Key,路由实例作为value if (path != null) { values.put(path, route); } } //获取服务发现(DiscoveryClient)中的所有服务实例列表(包含服务实例ID,服务实例IP,服务实例端口) List services = this.discovery.getServices(); String[] ignored = (String[])this.properties.getIgnoredServices().toArray(new String[0]); Iterator var13 = services.iterator(); while(var13.hasNext()) { String serviceId = (String)var13.next(); String key = "/" + this.mapRouteToService(serviceId) + "/**"; if (values.containsKey(serviceId) && ((ZuulRoute)values.get(serviceId)).getUrl() == null) { ZuulRoute staticRoute = (ZuulRoute)values.get(serviceId); if (!StringUtils.hasText(staticRoute.getLocation())) { staticRoute.setLocation(serviceId); } } if (!PatternMatchUtils.simpleMatch(ignored, serviceId) && !routesMap.containsKey(key)) { routesMap.put(key, new ZuulRoute(key, serviceId)); } } } //Zuul配置文件配置了访问路径为/**的路由规则,重新添加默认路由规则及路由映射实例 if (routesMap.get("/**") != null) { ZuulRoute defaultRoute = (ZuulRoute)routesMap.get("/**"); routesMap.remove("/**"); routesMap.put("/**", defaultRoute); } values = new LinkedHashMap(); //迭代所有路由配置信息并添加至LinkedHashMap Entry entry; for(var3 = routesMap.entrySet().iterator(); var3.hasNext(); values.put(path, entry.getValue())) { entry = (Entry)var3.next(); path = (String)entry.getKey(); //路由访问规则不以“/”开头,添加"/",添加后为"/"+path if (!path.startsWith("/")) { path = "/" + path; } //路由访问规则是否配制了访问前缀,如果配置了访问前缀需要在访问规则前拼接访问前缀 if (StringUtils.hasText(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } } //返回LinkedHashMap(路由实例集合,key为访问path配置信息,值为封装路由信息实例) return values; } public void refresh() { this.doRefresh(); }}
locateRoute方法继承自SimpleRouteLocator类,并重写了locateRoute方法,该方法主要功能就是读取Zuul应用配置相关信息,封装为LinkedHashMap,键为配置信息访问path,值为路由配置封装类(ZuulRoute)。refresh方法实现了 RefreshableRouteLocator接口中的refresh方法,添加路由信息动态刷新路由配置必须实现此方法。doRefresh方法来自SimpleRouteLocator类。
SimpleRouteLocator
该类作为DiscoveryClientRouteLocator父类,实现了RouteLocator接口,对读取的配置信息做一些基本处理,提供方法doRefresh和locateRoutes供DiscoveryClientRouteLocator实现服务路由刷新和服务路由动态加载相关功能。
package org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;public class SimpleRouteLocator{ private AtomicReference> routes = new AtomicReference(); protected void doRefresh() { this.routes.set(this.locateRoutes()); } protected Map locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap(); Iterator var2 = this.properties.getRoutes().values().iterator(); while(var2.hasNext()) { ZuulRoute route = (ZuulRoute)var2.next(); routesMap.put(route.getPath(), route); } return routesMap; }}
为了保证往服务路由配置集合(Map)添加路由配置信息的操作原子化(多线程操作路由配置集合保证路由配置集合最终元素一致性),需要使用AutomicReference对路由配置集合进行封装。
locateRoutes方法用于迭代路由配置信息并添加至LinkedHashMap(key为配置信息的访问路径,值为具体的路由配置封装实例)。
ZuulServerAutoConfiguration
该类的主要目的是自动装配各种过滤器、监听器及其他功能。
package org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration;@Configuration( proxyBeanMethods = false)@EnableConfigurationProperties({ZuulProperties.class})@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})@ConditionalOnBean({Marker.class})public class ZuulServerAutoConfiguration { private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> { @Autowired private ZuulHandlerMapping zuulHandlerMapping;//用于将本地配置的路由规则映射到远程过程控制器 private HeartbeatMonitor heartbeatMonitor;//用于服务注册中心监听服务心跳 private ZuulRefreshListener() { this.heartbeatMonitor = new HeartbeatMonitor(); } public void onApplicationEvent(ApplicationEvent event) { if (!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent) && !(event instanceof InstanceRegisteredEvent)) { if (event instanceof ParentHeartbeatEvent) { ParentHeartbeatEvent e = (ParentHeartbeatEvent)event; this.resetIfNeeded(e.getValue()); } else if (event instanceof HeartbeatEvent) { HeartbeatEvent e = (HeartbeatEvent)event; this.resetIfNeeded(e.getValue()); } } else { this.reset(); } } private void resetIfNeeded(Object value) { if (this.heartbeatMonitor.update(value)) { this.reset(); } } private void reset() { this.zuulHandlerMapping.setDirty(true); } } }
在方法onApplicationEvent中,Zuul会接收3种事件通知去刷新路由映射配置信息,心跳续约监视器HeartbeatMonitor也会触发这个事件。
事件名称 | 事件作用 |
ContextRefreshedEvent | Spring容器初始化完成触发该事件 |
RefreshScopeRefreshedEvent | 局部刷新配置触发该事件 |
RoutesRefreshedEvent | 路由配置刷新触发该事件 |
ZuulHandlerMapping
用于将本地配置的映射关系映射到远程的过程控制器,与服务路由事件刷新相关方法如下:
package org.springframework.cloud.netflix.zuul.web;public class ZuulHandlerMapping extends AbstractUrlHandlerMapping { private volatile boolean dirty = true; public void setDirty(boolean dirty) { this.dirty = dirty; if (this.routeLocator instanceof RefreshableRouteLocator) { ((RefreshableRouteLocator)this.routeLocator).refresh(); } }}
distry属性用于标识当前是否需要重新加载路由映射配置信息。Zuul每次进行网关路由转发操作时会检查这个值,如果为true,触发重新加载路由配置信息成功后,将该属性设置为false。
实现刷新加载路由映射配置信息,需要实现RefreshableRouteLocator接口中的refresh方法,该方法主要用于刷新路由规则配置相关信息。
package org.springframework.cloud.netflix.zuul.filters;public interface RefreshableRouteLocator extends RouteLocator { void refresh();}
RouteLocator接口主要完成如下工作:获取不需要(禁用)进行服务路由的映射规则(path)集合;获取所有的路由实例集合;根据路由映射规则(path)获取路由实例信息。
package org.springframework.cloud.netflix.zuul.filters;public interface RouteLocator { //忽略(禁用),不进行服务路由的映射规则(path)集合 CollectiongetIgnoredPaths(); //获取所有的路由实例集合 ListgetRoutes(); //根据路由映射规则获取路由实例 Route getMatchingRoute(String path);}
动态路由实例
概述
通过对动态路由实现原理的描述,对动态路由映射规则的动态加载原理及路由规则动态刷新方式有所了解。我们在构建动态路由时,只需要重写SimpleRouteLocator类的locateRoute方法,并且实现RefreshableRouteLocator接口中的refresh方法,再在内部调用SimpleRouteLocator类的doRefresh方法,就可以实现一个Zuul内部的自定义动态路由加载器。
如果不想使用自定义动态路由加载器完成动态路由加载和刷新,改为手动触发,可以重写onApplicationEvent方法。
动态路由实现
建库脚本
DROP TABLE IF EXISTS `db_config_route`;CREATE TABLE `db_config_route` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '服务路由编号', `path` varchar(60) NOT NULL COMMENT '服务路由请求路径', `service_id` varchar(60) DEFAULT NULL COMMENT '服务路由编号', `url` varchar(200) DEFAULT NULL COMMENT '服务路由映射路径', `enable` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否启用路由', `retryable` bit(1) DEFAULT NULL COMMENT '启用支持重试', PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;-- ------------------------------ Records of db_config_route-- ----------------------------insert into db_config_route(path,service_id,url) VALUES ('/baidu/**', null, 'http://www.baidu.com');insert into db_config_route(path,service_id,url) VALUES ('/aliyun/**', null, 'https://www.aliyun.com/');insert into db_config_route(path,service_id,url) VALUES ('/jetbrains/**', null, 'https://www.jetbrains.com/');
添加项目依赖
<dependency> <groupId>mysqlgroupId> <artifactId>mysql-connector-javaartifactId> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-jdbcartifactId> dependency>
配置数据库连接信息
#动态路由数据库连接配置spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/zuul_route?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaispring.datasource.username=rootspring.datasource.password=xxxxxx
查询数据库路由信息(忽略接口代码)
@Repositorypublic class CustomizeRouteDaoImpl implements CustomizeRouteDao{ @Autowired private static JdbcTemplate jdbcTemplate; private static String query_route_sql="select id,path,service_id,url,enable,retryable " + "from db_config_route where enable=true"; @Override public Map<String, ZuulProperties.ZuulRoute> findEnabledRouteByDb() { Map<String,ZuulProperties.ZuulRoute> routeMap=new LinkedHashMap<>(); jdbcTemplate.query(query_route_sql,(ResultSet rs,int index)->{ ZuulProperties.ZuulRoute zuulRoute=new ZuulProperties.ZuulRoute(); try { zuulRoute.setId(rs.getLong("id")+"");//设置路由服务编号 zuulRoute.setPath(rs.getString("path"));//设置路由请求路径 zuulRoute.setServiceId(rs.getString("service_id"));//设置路由服务标识 zuulRoute.setUrl(rs.getString("url"));//设置路由访问映射 zuulRoute.setRetryable(rs.getBoolean("retryable"));//设置路由是否可重试 zuulRoute.setStripPrefix(true);//设置路由访问前缀 routeMap.put(zuulRoute.getPath(),zuulRoute);//将动态路由信息添加至集合(key为访问路径,值为动态路由实例) }catch (Exception ex) { ex.printStackTrace(); } return zuulRoute; }); return routeMap; }}
自定义路由定位器
public class CustomizeRouteLocator extends DiscoveryClientRouteLocator { @Autowired private CustomizeRouteDao customizeRouteDao; public CustomizeRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties, ServiceRouteMapper serviceRouteMapper, ServiceInstance localServiceInstance) { //父类构造方法 super(servletPath, discovery, properties, serviceRouteMapper, localServiceInstance); } @Override protected LinkedHashMap<String, ZuulProperties.ZuulRoute> locateRoutes() { //调用父类构造方法,加载配置文件中的静态配置路由规则 LinkedHashMap<String,ZuulProperties.ZuulRoute> routeMap=super.locateRoutes(); //获取数据库的动态路由配置数据 Map<String,ZuulProperties.ZuulRoute> dbrouteResult=customizeRouteDao.findEnabledRouteByDb(); //将动态路由配置数据添加至父类构造方法的路由集合中 routeMap.putAll(dbrouteResult); return routeMap; }}
Zuul程序入口文件
@EnableZuulProxy@SpringBootApplication(scanBasePackages ="com.spring.cloud.zuul")public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } @Autowired private ZuulProperties zuulProperties;//Zuul配置信息 @Autowired private ServerProperties serverProperties;//服务器配置信息 @Autowired(required = false) private Registration registration;//服务实例注册表 @Autowired private ServiceRouteMapper serviceRouteMapper;//服务路由映射器 @Autowired private DiscoveryClient discoveryClient;//服务发现客户端 @Bean(name = "routeLocator") public CustomizeRouteLocator initRouteLocator() { return new CustomizeRouteLocator(this.serverProperties.getServlet().getContextPath(), this.discoveryClient, this.zuulProperties, this.serviceRouteMapper,this.registration); } }